doctor.mjs
233 lines 7.3 KB
Raw
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