doctor.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
2 days ago
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * knowtation doctor — self-hosted vault + optional Hub API checks. |
| 4 | * Token story: see docs/TOKEN-SAVINGS.md (vault retrieval vs terminal tooling). |
| 5 | */ |
| 6 | |
| 7 | import fs from 'fs'; |
| 8 | import path from 'path'; |
| 9 | import { loadConfig } from '../lib/config.mjs'; |
| 10 | |
| 11 | /** |
| 12 | * @param {{ useJson: boolean, hubUrlOpt: string | null }} opts |
| 13 | * @returns {Promise<number>} exit code (0 ok, 1 config/vault failure, 2 hub unreachable or auth failure) |
| 14 | */ |
| 15 | export async function runDoctor(opts) { |
| 16 | const { useJson, hubUrlOpt } = opts; |
| 17 | /** @type {{ id: string, status: 'ok' | 'warn' | 'error', message: string, detail?: string }[]} */ |
| 18 | const checks = []; |
| 19 | |
| 20 | const tokenLayers = { |
| 21 | vault_retrieval: |
| 22 | 'Vault retrieval (MCP/CLI search, snippets, limits) is the primary in-product token saver — see docs/TOKEN-SAVINGS.md.', |
| 23 | terminal_tooling: |
| 24 | 'Shrinking shell or terminal logs is optional on your coding host; Knowtation hosted canisters do not run shell hooks for log compaction.', |
| 25 | }; |
| 26 | |
| 27 | let selfHosted = { |
| 28 | config_loaded: false, |
| 29 | vault_path: null, |
| 30 | vault_exists: false, |
| 31 | vault_readable: false, |
| 32 | memory_enabled: null, |
| 33 | }; |
| 34 | |
| 35 | try { |
| 36 | const config = loadConfig(); |
| 37 | selfHosted = { |
| 38 | config_loaded: true, |
| 39 | vault_path: config.vault_path, |
| 40 | vault_exists: fs.existsSync(config.vault_path), |
| 41 | vault_readable: false, |
| 42 | memory_enabled: Boolean(config.memory?.enabled), |
| 43 | }; |
| 44 | if (selfHosted.vault_exists) { |
| 45 | try { |
| 46 | fs.accessSync(config.vault_path, fs.constants.R_OK); |
| 47 | selfHosted.vault_readable = true; |
| 48 | checks.push({ |
| 49 | id: 'vault_path', |
| 50 | status: 'ok', |
| 51 | message: 'Vault path exists and is readable.', |
| 52 | detail: config.vault_path, |
| 53 | }); |
| 54 | } catch (e) { |
| 55 | checks.push({ |
| 56 | id: 'vault_path', |
| 57 | status: 'error', |
| 58 | message: 'Vault path exists but is not readable.', |
| 59 | detail: (e && e.message) || String(e), |
| 60 | }); |
| 61 | } |
| 62 | } else { |
| 63 | checks.push({ |
| 64 | id: 'vault_path', |
| 65 | status: 'error', |
| 66 | message: 'Configured vault path does not exist on disk.', |
| 67 | detail: config.vault_path, |
| 68 | }); |
| 69 | } |
| 70 | } catch (e) { |
| 71 | checks.push({ |
| 72 | id: 'config', |
| 73 | status: 'error', |
| 74 | message: 'Failed to load config (config/local.yaml + env).', |
| 75 | detail: e.message || String(e), |
| 76 | }); |
| 77 | } |
| 78 | |
| 79 | const hubUrlRaw = hubUrlOpt || process.env.KNOWTATION_HUB_URL; |
| 80 | const hubToken = process.env.KNOWTATION_HUB_TOKEN; |
| 81 | const hubVaultId = process.env.KNOWTATION_HUB_VAULT_ID; |
| 82 | |
| 83 | const hubApi = { |
| 84 | KNOWTATION_HUB_URL: hubUrlRaw || null, |
| 85 | KNOWTATION_HUB_TOKEN_set: Boolean(hubToken), |
| 86 | KNOWTATION_HUB_VAULT_ID: hubVaultId || null, |
| 87 | health_ok: null, |
| 88 | notes_probe_status: null, |
| 89 | }; |
| 90 | |
| 91 | if (hubUrlRaw) { |
| 92 | const base = hubUrlRaw.replace(/\/$/, ''); |
| 93 | try { |
| 94 | const res = await fetch(`${base}/health`, { method: 'GET' }); |
| 95 | hubApi.health_ok = res.ok; |
| 96 | if (res.ok) { |
| 97 | checks.push({ id: 'hub_health', status: 'ok', message: `Hub health OK at ${base}.` }); |
| 98 | } else { |
| 99 | checks.push({ |
| 100 | id: 'hub_health', |
| 101 | status: 'error', |
| 102 | message: `Hub health returned HTTP ${res.status}.`, |
| 103 | detail: base, |
| 104 | }); |
| 105 | } |
| 106 | } catch (e) { |
| 107 | hubApi.health_ok = false; |
| 108 | checks.push({ |
| 109 | id: 'hub_health', |
| 110 | status: 'error', |
| 111 | message: 'Hub health request failed (network or DNS).', |
| 112 | detail: e.message || String(e), |
| 113 | }); |
| 114 | } |
| 115 | |
| 116 | if (hubToken && hubVaultId) { |
| 117 | try { |
| 118 | const res = await fetch(`${base}/api/v1/notes?limit=1`, { |
| 119 | method: 'GET', |
| 120 | headers: { |
| 121 | Authorization: `Bearer ${hubToken}`, |
| 122 | 'X-Vault-Id': hubVaultId, |
| 123 | Accept: 'application/json', |
| 124 | }, |
| 125 | }); |
| 126 | hubApi.notes_probe_status = res.status; |
| 127 | if (res.ok) { |
| 128 | checks.push({ |
| 129 | id: 'hub_auth_notes', |
| 130 | status: 'ok', |
| 131 | message: 'Hub API notes probe succeeded (token + vault id accepted).', |
| 132 | }); |
| 133 | } else if (res.status === 401 || res.status === 403) { |
| 134 | checks.push({ |
| 135 | id: 'hub_auth_notes', |
| 136 | status: 'error', |
| 137 | message: `Hub API returned ${res.status} — token may be expired or vault id invalid.`, |
| 138 | }); |
| 139 | } else { |
| 140 | checks.push({ |
| 141 | id: 'hub_auth_notes', |
| 142 | status: 'warn', |
| 143 | message: `Hub API notes probe returned HTTP ${res.status}.`, |
| 144 | }); |
| 145 | } |
| 146 | } catch (e) { |
| 147 | hubApi.notes_probe_status = 'error'; |
| 148 | checks.push({ |
| 149 | id: 'hub_auth_notes', |
| 150 | status: 'error', |
| 151 | message: 'Hub API notes probe failed.', |
| 152 | detail: e.message || String(e), |
| 153 | }); |
| 154 | } |
| 155 | } else if (hubToken || hubVaultId) { |
| 156 | checks.push({ |
| 157 | id: 'hub_auth_notes', |
| 158 | status: 'warn', |
| 159 | message: 'Set both KNOWTATION_HUB_TOKEN and KNOWTATION_HUB_VAULT_ID to probe authenticated Hub API.', |
| 160 | }); |
| 161 | } |
| 162 | } else { |
| 163 | checks.push({ |
| 164 | id: 'hub_health', |
| 165 | status: 'ok', |
| 166 | message: 'KNOWTATION_HUB_URL not set — skipping hosted Hub checks (normal for pure self-hosted).', |
| 167 | }); |
| 168 | } |
| 169 | |
| 170 | const hasWarn = checks.some((c) => c.status === 'warn'); |
| 171 | const ok = !checks.some((c) => c.status === 'error'); |
| 172 | |
| 173 | if (useJson) { |
| 174 | console.log( |
| 175 | JSON.stringify( |
| 176 | { |
| 177 | ok, |
| 178 | token_layers: tokenLayers, |
| 179 | self_hosted: selfHosted, |
| 180 | hub_api: hubApi, |
| 181 | checks, |
| 182 | }, |
| 183 | null, |
| 184 | 2 |
| 185 | ) |
| 186 | ); |
| 187 | } else { |
| 188 | console.log('Knowtation doctor'); |
| 189 | console.log(''); |
| 190 | console.log('Token layers (see docs/TOKEN-SAVINGS.md):'); |
| 191 | console.log(` Vault / retrieval: ${tokenLayers.vault_retrieval}`); |
| 192 | console.log(` Terminal tooling: ${tokenLayers.terminal_tooling}`); |
| 193 | console.log(''); |
| 194 | console.log('Self-hosted (CLI / local MCP):'); |
| 195 | if (selfHosted.config_loaded) { |
| 196 | console.log(` vault_path: ${selfHosted.vault_path}`); |
| 197 | console.log(` exists: ${selfHosted.vault_exists} readable: ${selfHosted.vault_readable}`); |
| 198 | console.log(` memory.enabled: ${selfHosted.memory_enabled}`); |
| 199 | } else { |
| 200 | console.log(' (config not loaded — fix errors above)'); |
| 201 | } |
| 202 | console.log(''); |
| 203 | console.log('Hosted Hub API (optional):'); |
| 204 | if (hubUrlRaw) { |
| 205 | console.log(` KNOWTATION_HUB_URL: ${hubUrlRaw}`); |
| 206 | console.log(` health: ${hubApi.health_ok === true ? 'ok' : hubApi.health_ok === false ? 'failed' : 'n/a'}`); |
| 207 | console.log( |
| 208 | ` token + vault set: ${hubApi.KNOWTATION_HUB_TOKEN_set && hubApi.KNOWTATION_HUB_VAULT_ID ? 'yes' : 'no'}` |
| 209 | ); |
| 210 | if (hubApi.notes_probe_status != null) { |
| 211 | console.log(` GET /api/v1/notes?limit=1 status: ${hubApi.notes_probe_status}`); |
| 212 | } |
| 213 | } else { |
| 214 | console.log(' KNOWTATION_HUB_URL not set — skipped.'); |
| 215 | } |
| 216 | console.log(''); |
| 217 | for (const c of checks) { |
| 218 | const tag = c.status.toUpperCase(); |
| 219 | console.log(`[${tag}] ${c.id}: ${c.message}`); |
| 220 | if (c.detail) console.log(` ${c.detail}`); |
| 221 | } |
| 222 | if (hasWarn && ok) console.log('\nWarnings present — review above.'); |
| 223 | } |
| 224 | |
| 225 | if (!selfHosted.config_loaded || !selfHosted.vault_exists || !selfHosted.vault_readable) { |
| 226 | return 1; |
| 227 | } |
| 228 | for (const c of checks) { |
| 229 | if (c.status !== 'error') continue; |
| 230 | if (c.id === 'hub_health' || c.id === 'hub_auth_notes') return 2; |
| 231 | } |
| 232 | return 0; |
| 233 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
2 days ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
2 days ago