@getmarrow/install 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/installer.js +312 -18
package/README.md
CHANGED
|
@@ -11,6 +11,16 @@ npx @getmarrow/install --repair
|
|
|
11
11
|
npx @getmarrow/install doctor
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## What's New in v0.1.19
|
|
15
|
+
|
|
16
|
+
v0.1.19 makes Marrow doctor and release smoke stricter, so agents know exactly why logging is degraded.
|
|
17
|
+
|
|
18
|
+
- `npx @getmarrow/install doctor --self-test` now validates: key found, key valid, account active, agent identity accepted, harmless write test created, and outcome closed.
|
|
19
|
+
- Doctor reports exact failure reasons such as `missing_key`, `invalid_key`, `wrong_agent_id`, `network_blocked`, and `proof_required`.
|
|
20
|
+
- Doctor warns when local `@getmarrow/install`, `@getmarrow/sdk`, or `@getmarrow/mcp` versions are behind the current release.
|
|
21
|
+
- New `scripts/fresh-install-smoke.sh` verifies a clean temp install can load `.marrow/env`, run doctor, write a test event, and close the outcome.
|
|
22
|
+
- Full keys are still never printed in diagnostics. Keep `.marrow/env` out of git and set file permissions to owner-only when possible.
|
|
23
|
+
|
|
14
24
|
## What's New in v0.1.17
|
|
15
25
|
|
|
16
26
|
v0.1.17 expands the Govern TUI harness addon coverage while preserving adaptive mode recommendations.
|
|
@@ -223,6 +233,14 @@ Doctor check:
|
|
|
223
233
|
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install doctor
|
|
224
234
|
```
|
|
225
235
|
|
|
236
|
+
Deep doctor with harmless write/outcome verification:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install doctor --self-test
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Expected healthy output includes `key valid: yes`, `write test event: passed`, and `outcome closed: passed`.
|
|
243
|
+
|
|
226
244
|
Repair missing hooks/config:
|
|
227
245
|
|
|
228
246
|
```bash
|
package/package.json
CHANGED
package/src/installer.js
CHANGED
|
@@ -4,6 +4,9 @@ const os = require('node:os');
|
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
5
|
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
7
|
+
const INSTALLER_LATEST = '0.1.19';
|
|
8
|
+
const SDK_LATEST = '3.7.35';
|
|
9
|
+
const MCP_LATEST = '3.9.35';
|
|
7
10
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
8
11
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
9
12
|
|
|
@@ -15,7 +18,7 @@ function parseArgs(argv) {
|
|
|
15
18
|
doctor: false,
|
|
16
19
|
repair: false,
|
|
17
20
|
mode: 'auto',
|
|
18
|
-
apiKey: process.env.MARROW_API_KEY || '',
|
|
21
|
+
apiKey: process.env.MARROW_API_KEY || process.env.MARROW_KEY || '',
|
|
19
22
|
baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
20
23
|
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
|
|
21
24
|
selfTest: true,
|
|
@@ -57,6 +60,12 @@ function parseArgs(argv) {
|
|
|
57
60
|
throw new Error('--mode must be one of auto, mcp, sdk, both, md');
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
const resolved = resolveMarrowKeyMaterial(options.cwd);
|
|
64
|
+
if (!options.apiKey && resolved.apiKey) options.apiKey = resolved.apiKey;
|
|
65
|
+
if (!options.agentId && resolved.agentId) options.agentId = resolved.agentId;
|
|
66
|
+
if (options.baseUrl === DEFAULT_BASE_URL && resolved.baseUrl) options.baseUrl = resolved.baseUrl;
|
|
67
|
+
if (!options.keySource && resolved.source) options.keySource = resolved.source;
|
|
68
|
+
|
|
60
69
|
return options;
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -71,14 +80,14 @@ function usage() {
|
|
|
71
80
|
|
|
72
81
|
Options:
|
|
73
82
|
--dry-run Print planned changes without writing
|
|
74
|
-
--doctor Check install health
|
|
83
|
+
--doctor Check install health; self-test writes and closes a harmless test event
|
|
75
84
|
--repair Write missing hooks/config, then run self-test and status check
|
|
76
85
|
--yes, -y Write detected config files
|
|
77
86
|
--mode <mode> auto, mcp, sdk, both, or md
|
|
78
87
|
--key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
|
|
79
88
|
--base-url <url> Marrow API base URL
|
|
80
89
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
81
|
-
--no-self-test Skip API smoke/self-test
|
|
90
|
+
--no-self-test Skip API smoke/self-test for a read-only doctor check
|
|
82
91
|
`;
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -157,8 +166,7 @@ function findLikelyEnvFiles(detection, env = process.env) {
|
|
|
157
166
|
path.join(detection.root, '.marrow', 'env'),
|
|
158
167
|
path.join(detection.root, '.marrow', 'env.local'),
|
|
159
168
|
path.join(home, '.marrow', 'env'),
|
|
160
|
-
path.join(home, '.
|
|
161
|
-
path.join(home, '.openclaw', 'gateway.systemd.env'),
|
|
169
|
+
path.join(home, '.marrow', 'env.local'),
|
|
162
170
|
];
|
|
163
171
|
return candidates.filter((filePath) => {
|
|
164
172
|
if (!exists(filePath)) return false;
|
|
@@ -183,6 +191,36 @@ function readEnvVar(filePath, name) {
|
|
|
183
191
|
return match ? stripQuotes(match[1]) : '';
|
|
184
192
|
}
|
|
185
193
|
|
|
194
|
+
function resolveMarrowKeyMaterial(cwd = process.cwd(), env = process.env) {
|
|
195
|
+
if (env.MARROW_API_KEY || env.MARROW_KEY) {
|
|
196
|
+
return {
|
|
197
|
+
apiKey: env.MARROW_API_KEY || env.MARROW_KEY,
|
|
198
|
+
baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
199
|
+
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
200
|
+
source: env.MARROW_API_KEY ? 'MARROW_API_KEY' : 'MARROW_KEY',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const detection = detectEnvironment(cwd, env);
|
|
205
|
+
for (const filePath of findLikelyEnvFiles(detection, env)) {
|
|
206
|
+
const apiKey = readEnvVar(filePath, 'MARROW_API_KEY') || readEnvVar(filePath, 'MARROW_KEY');
|
|
207
|
+
if (!apiKey) continue;
|
|
208
|
+
return {
|
|
209
|
+
apiKey,
|
|
210
|
+
baseUrl: readEnvVar(filePath, 'MARROW_BASE_URL') || env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
211
|
+
agentId: readEnvVar(filePath, 'MARROW_FLEET_AGENT_ID') || readEnvVar(filePath, 'MARROW_AGENT_ID') || env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
212
|
+
source: filePath,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
apiKey: '',
|
|
218
|
+
baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
219
|
+
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
220
|
+
source: null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
186
224
|
function readFirstLineSecret(filePath) {
|
|
187
225
|
if (!exists(filePath)) return '';
|
|
188
226
|
return safeRead(filePath).split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
|
@@ -297,27 +335,79 @@ Required environment:
|
|
|
297
335
|
|
|
298
336
|
- \`MARROW_API_KEY\`
|
|
299
337
|
- Optional: \`MARROW_BASE_URL\`, \`MARROW_FLEET_AGENT_ID\`
|
|
338
|
+
|
|
339
|
+
Key loading:
|
|
340
|
+
|
|
341
|
+
- Prefer your shell, MCP secret store, or agent secret manager.
|
|
342
|
+
- Marrow also auto-detects \`.marrow/env\`, \`.marrow/env.local\`, \`.env\`, \`.env.local\`, and \`~/.marrow/env\`.
|
|
343
|
+
- Run \`npx @getmarrow/install doctor\` any time an agent says Marrow is missing or degraded.
|
|
300
344
|
${MARROW_BLOCK_END}`;
|
|
301
345
|
}
|
|
302
346
|
|
|
303
347
|
function passiveRuntimeSource() {
|
|
304
|
-
return `
|
|
348
|
+
return `import fs from 'node:fs';
|
|
349
|
+
import path from 'node:path';
|
|
350
|
+
import os from 'node:os';
|
|
351
|
+
|
|
352
|
+
function stripQuotes(value) {
|
|
353
|
+
const trimmed = String(value || '').trim();
|
|
354
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
|
|
355
|
+
return trimmed;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function readEnvFile(filePath) {
|
|
359
|
+
if (!fs.existsSync(filePath)) return {};
|
|
360
|
+
const values = {};
|
|
361
|
+
const allowed = new Set(['MARROW_API_KEY', 'MARROW_KEY', 'MARROW_BASE_URL', 'MARROW_FLEET_AGENT_ID', 'MARROW_AGENT_ID', 'MARROW_SESSION_ID', 'MARROW_ENFORCEMENT_MODE', 'MARROW_PASSIVE_VALUE_REPORT', 'MARROW_VALUE_REPORT_PERIOD', 'MARROW_AGENT_RUNTIME', 'MARROW_WORKFLOW_GATE', 'MARROW_REQUIRE_OUTCOME_CLOSURE']);
|
|
362
|
+
for (const line of fs.readFileSync(filePath, 'utf8').split(/\\r?\\n/)) {
|
|
363
|
+
const match = line.match(/^\\s*(?:export\\s+)?([A-Z_][A-Z0-9_]*)\\s*=\\s*(.*?)\\s*$/);
|
|
364
|
+
if (!match) continue;
|
|
365
|
+
if (!allowed.has(match[1])) continue;
|
|
366
|
+
let value = match[2] || '';
|
|
367
|
+
const hashIndex = value.search(/\\s+#/);
|
|
368
|
+
if (hashIndex >= 0) value = value.slice(0, hashIndex);
|
|
369
|
+
values[match[1]] = stripQuotes(value);
|
|
370
|
+
}
|
|
371
|
+
return values;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resolveMarrowEnv() {
|
|
375
|
+
if (process.env.MARROW_API_KEY || process.env.MARROW_KEY) return process.env;
|
|
376
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
377
|
+
const files = [];
|
|
378
|
+
let dir = process.cwd();
|
|
379
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
380
|
+
files.push(path.join(dir, '.marrow', 'env'), path.join(dir, '.marrow', 'env.local'), path.join(dir, '.env'), path.join(dir, '.env.local'));
|
|
381
|
+
const parent = path.dirname(dir);
|
|
382
|
+
if (parent === dir) break;
|
|
383
|
+
dir = parent;
|
|
384
|
+
}
|
|
385
|
+
files.push(path.join(home, '.marrow', 'env'), path.join(home, '.marrow', 'env.local'));
|
|
386
|
+
for (const filePath of [...new Set(files)]) {
|
|
387
|
+
const values = readEnvFile(filePath);
|
|
388
|
+
if (values.MARROW_API_KEY || values.MARROW_KEY) return { ...process.env, ...values };
|
|
389
|
+
}
|
|
390
|
+
return process.env;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const marrowEnv = resolveMarrowEnv();
|
|
394
|
+
const apiKey = marrowEnv.MARROW_API_KEY || marrowEnv.MARROW_KEY;
|
|
305
395
|
if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
306
396
|
try {
|
|
307
397
|
const { MarrowClient } = await import('@getmarrow/sdk');
|
|
308
398
|
const marrow = new MarrowClient(apiKey, {
|
|
309
|
-
baseUrl:
|
|
310
|
-
agentId:
|
|
311
|
-
sessionId:
|
|
312
|
-
mode:
|
|
399
|
+
baseUrl: marrowEnv.MARROW_BASE_URL,
|
|
400
|
+
agentId: marrowEnv.MARROW_FLEET_AGENT_ID || marrowEnv.MARROW_AGENT_ID,
|
|
401
|
+
sessionId: marrowEnv.MARROW_SESSION_ID,
|
|
402
|
+
mode: marrowEnv.MARROW_ENFORCEMENT_MODE || 'auto',
|
|
313
403
|
});
|
|
314
404
|
|
|
315
405
|
const runtime = marrow.createPassiveRuntime({
|
|
316
|
-
includeValueReport:
|
|
317
|
-
valueReportPeriod:
|
|
318
|
-
useAgentRuntime:
|
|
319
|
-
useWorkflowGate:
|
|
320
|
-
requireOutcomeClosure:
|
|
406
|
+
includeValueReport: marrowEnv.MARROW_PASSIVE_VALUE_REPORT !== 'false',
|
|
407
|
+
valueReportPeriod: marrowEnv.MARROW_VALUE_REPORT_PERIOD || '7d',
|
|
408
|
+
useAgentRuntime: marrowEnv.MARROW_AGENT_RUNTIME !== 'false',
|
|
409
|
+
useWorkflowGate: marrowEnv.MARROW_WORKFLOW_GATE !== 'false',
|
|
410
|
+
requireOutcomeClosure: marrowEnv.MARROW_REQUIRE_OUTCOME_CLOSURE !== 'false',
|
|
321
411
|
});
|
|
322
412
|
|
|
323
413
|
runtime.install();
|
|
@@ -325,6 +415,8 @@ if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
|
325
415
|
} catch {
|
|
326
416
|
console.warn('[Marrow] passive runtime skipped: install @getmarrow/sdk or verify SDK initialization. Run npm install @getmarrow/sdk, then rerun npx @getmarrow/install --repair.');
|
|
327
417
|
}
|
|
418
|
+
} else if (!apiKey) {
|
|
419
|
+
console.warn('[Marrow] passive runtime skipped: MARROW_API_KEY missing. Put it in .marrow/env or run npx @getmarrow/install doctor for the exact fix.');
|
|
328
420
|
}
|
|
329
421
|
`;
|
|
330
422
|
}
|
|
@@ -444,6 +536,82 @@ function inspectSdkDependency(detection) {
|
|
|
444
536
|
};
|
|
445
537
|
}
|
|
446
538
|
|
|
539
|
+
function compareVersions(a, b) {
|
|
540
|
+
const left = String(a || '0.0.0').replace(/^[^\d]*/, '').split('.').map((part) => parseInt(part, 10) || 0);
|
|
541
|
+
const right = String(b || '0.0.0').replace(/^[^\d]*/, '').split('.').map((part) => parseInt(part, 10) || 0);
|
|
542
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
543
|
+
const x = left[i] || 0;
|
|
544
|
+
const y = right[i] || 0;
|
|
545
|
+
if (x > y) return 1;
|
|
546
|
+
if (x < y) return -1;
|
|
547
|
+
}
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function dependencyVersion(packageJson, packageName) {
|
|
552
|
+
for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
553
|
+
const value = packageJson?.[block]?.[packageName];
|
|
554
|
+
if (typeof value === 'string') return value;
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function installedPackageVersion(root, packageName) {
|
|
560
|
+
const packageJsonPath = path.join(root, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
561
|
+
try {
|
|
562
|
+
const raw = safeRead(packageJsonPath);
|
|
563
|
+
if (!raw) return null;
|
|
564
|
+
const parsed = JSON.parse(raw);
|
|
565
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function inspectPackageVersions(detection) {
|
|
572
|
+
const rootPackage = safeRead(detection.paths.packageJson);
|
|
573
|
+
let packageJson = {};
|
|
574
|
+
try {
|
|
575
|
+
packageJson = rootPackage ? JSON.parse(rootPackage) : {};
|
|
576
|
+
} catch {
|
|
577
|
+
packageJson = {};
|
|
578
|
+
}
|
|
579
|
+
const versions = [
|
|
580
|
+
{
|
|
581
|
+
name: '@getmarrow/install',
|
|
582
|
+
installed: require('../package.json').version,
|
|
583
|
+
latest: INSTALLER_LATEST,
|
|
584
|
+
source: 'current installer',
|
|
585
|
+
update_command: 'npm install -g @getmarrow/install@latest',
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: '@getmarrow/sdk',
|
|
589
|
+
installed: installedPackageVersion(detection.root, '@getmarrow/sdk') || dependencyVersion(packageJson, '@getmarrow/sdk'),
|
|
590
|
+
latest: SDK_LATEST,
|
|
591
|
+
source: installedPackageVersion(detection.root, '@getmarrow/sdk') ? 'node_modules' : 'package.json',
|
|
592
|
+
update_command: 'npm install @getmarrow/sdk@latest',
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: '@getmarrow/mcp',
|
|
596
|
+
installed: installedPackageVersion(detection.root, '@getmarrow/mcp') || dependencyVersion(packageJson, '@getmarrow/mcp'),
|
|
597
|
+
latest: MCP_LATEST,
|
|
598
|
+
source: installedPackageVersion(detection.root, '@getmarrow/mcp') ? 'node_modules' : 'package.json',
|
|
599
|
+
update_command: 'npm install @getmarrow/mcp@latest',
|
|
600
|
+
},
|
|
601
|
+
];
|
|
602
|
+
return versions.map((entry) => {
|
|
603
|
+
const normalized = entry.installed ? String(entry.installed).replace(/^[^\d]*/, '') : null;
|
|
604
|
+
const outdated = normalized ? compareVersions(normalized, entry.latest) < 0 : false;
|
|
605
|
+
return {
|
|
606
|
+
...entry,
|
|
607
|
+
installed: normalized,
|
|
608
|
+
present: Boolean(normalized),
|
|
609
|
+
outdated,
|
|
610
|
+
warning: outdated ? `${entry.name} ${normalized} is older than ${entry.latest}.` : null,
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
447
615
|
function buildPlan(detection, options) {
|
|
448
616
|
const mode = options.mode === 'auto'
|
|
449
617
|
? detection.node && (detection.claudeCode || detection.cursor || detection.codex || detection.openclaw)
|
|
@@ -548,11 +716,108 @@ async function requestJson(url, options) {
|
|
|
548
716
|
}
|
|
549
717
|
if (!res.ok) {
|
|
550
718
|
const message = json.error || json.message || `HTTP ${res.status}`;
|
|
551
|
-
|
|
719
|
+
const error = new Error(String(message));
|
|
720
|
+
error.status = res.status;
|
|
721
|
+
error.code = json.code || null;
|
|
722
|
+
error.details = json.details || null;
|
|
723
|
+
throw error;
|
|
552
724
|
}
|
|
553
725
|
return json.data || json;
|
|
554
726
|
}
|
|
555
727
|
|
|
728
|
+
function classifyDoctorFailure(error) {
|
|
729
|
+
const status = error?.status || 0;
|
|
730
|
+
const text = `${error?.code || ''} ${error?.message || error}`.toLowerCase();
|
|
731
|
+
if (status === 401 || /missing_key|invalid_key|unauthorized|invalid api key/.test(text)) return 'invalid_key';
|
|
732
|
+
if (status === 403 && /agent|bound|identity/.test(text)) return 'wrong_agent_id';
|
|
733
|
+
if (status === 403) return 'invalid_key';
|
|
734
|
+
if (status === 409 || /proof/.test(text)) return 'proof_required';
|
|
735
|
+
if (status === 429 || /rate limit|too many/.test(text)) return 'network_blocked';
|
|
736
|
+
if (status >= 500 || /timeout|network|fetch failed|econnreset|enotfound|eai_again/.test(text)) return 'network_blocked';
|
|
737
|
+
return 'unknown';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function runDoctorValidation(options, selfTest) {
|
|
741
|
+
if (!options.apiKey) {
|
|
742
|
+
return {
|
|
743
|
+
key_found: false,
|
|
744
|
+
key_valid: false,
|
|
745
|
+
account_active: false,
|
|
746
|
+
agent_identity_accepted: false,
|
|
747
|
+
write_test_event: 'skipped',
|
|
748
|
+
outcome_closed: 'skipped',
|
|
749
|
+
failure_reason: 'missing_key',
|
|
750
|
+
exact_fix: 'Create an API key at https://getmarrow.ai/account, then put MARROW_API_KEY in .marrow/env and run npx @getmarrow/install doctor --self-test.',
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (selfTest && !selfTest.skipped && selfTest.active && !selfTest.error) {
|
|
755
|
+
return {
|
|
756
|
+
key_found: true,
|
|
757
|
+
key_valid: true,
|
|
758
|
+
account_active: true,
|
|
759
|
+
agent_identity_accepted: true,
|
|
760
|
+
write_test_event: 'passed',
|
|
761
|
+
outcome_closed: 'passed',
|
|
762
|
+
failure_reason: null,
|
|
763
|
+
decision_id: selfTest.decision_id,
|
|
764
|
+
exact_fix: null,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (selfTest && selfTest.error) {
|
|
769
|
+
const reason = classifyDoctorFailure(selfTest);
|
|
770
|
+
return {
|
|
771
|
+
key_found: true,
|
|
772
|
+
key_valid: !['missing_key', 'invalid_key'].includes(reason),
|
|
773
|
+
account_active: !['missing_key', 'invalid_key'].includes(reason),
|
|
774
|
+
agent_identity_accepted: reason !== 'wrong_agent_id',
|
|
775
|
+
write_test_event: 'failed',
|
|
776
|
+
outcome_closed: 'failed',
|
|
777
|
+
failure_reason: reason,
|
|
778
|
+
exact_fix: reason === 'wrong_agent_id'
|
|
779
|
+
? 'Set MARROW_FLEET_AGENT_ID/MARROW_AGENT_ID to the id bound to this key, then rerun doctor.'
|
|
780
|
+
: reason === 'network_blocked'
|
|
781
|
+
? 'Retry from a network path that can reach api.getmarrow.ai, or reduce status polling if rate-limited.'
|
|
782
|
+
: 'Create/copy a live API key from the Marrow dashboard and update MARROW_API_KEY.',
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const headers = { authorization: `Bearer ${options.apiKey}` };
|
|
787
|
+
if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
|
|
788
|
+
try {
|
|
789
|
+
const status = await requestJson(`${options.baseUrl.replace(/\/+$/, '')}/v1/agent/status`, { headers });
|
|
790
|
+
return {
|
|
791
|
+
key_found: true,
|
|
792
|
+
key_valid: true,
|
|
793
|
+
account_active: true,
|
|
794
|
+
agent_identity_accepted: true,
|
|
795
|
+
write_test_event: 'skipped',
|
|
796
|
+
outcome_closed: 'skipped',
|
|
797
|
+
failure_reason: null,
|
|
798
|
+
status_health: status.health || 'unknown',
|
|
799
|
+
status_failure_reasons: status.failure_reasons || [],
|
|
800
|
+
exact_fix: status.recommended_fix || status.diagnostics?.exact_fix || null,
|
|
801
|
+
};
|
|
802
|
+
} catch (error) {
|
|
803
|
+
const reason = classifyDoctorFailure(error);
|
|
804
|
+
return {
|
|
805
|
+
key_found: true,
|
|
806
|
+
key_valid: !['missing_key', 'invalid_key'].includes(reason),
|
|
807
|
+
account_active: !['missing_key', 'invalid_key'].includes(reason),
|
|
808
|
+
agent_identity_accepted: reason !== 'wrong_agent_id',
|
|
809
|
+
write_test_event: 'skipped',
|
|
810
|
+
outcome_closed: 'skipped',
|
|
811
|
+
failure_reason: reason,
|
|
812
|
+
exact_fix: reason === 'wrong_agent_id'
|
|
813
|
+
? 'Set MARROW_FLEET_AGENT_ID/MARROW_AGENT_ID to the id bound to this key, then rerun doctor.'
|
|
814
|
+
: reason === 'network_blocked'
|
|
815
|
+
? 'Check network access to api.getmarrow.ai and rerun doctor.'
|
|
816
|
+
: 'Create/copy a live API key from the Marrow dashboard and update MARROW_API_KEY.',
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
556
821
|
async function runSelfTest(options) {
|
|
557
822
|
if (!options.selfTest) return { skipped: true, reason: 'disabled' };
|
|
558
823
|
if (!options.apiKey) {
|
|
@@ -838,6 +1103,24 @@ function printReport(report) {
|
|
|
838
1103
|
process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
|
|
839
1104
|
if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
|
|
840
1105
|
process.stdout.write(`- missing hooks/config: ${report.doctor.missingHooks.length ? report.doctor.missingHooks.join('; ') : 'none'}\n`);
|
|
1106
|
+
if (report.doctor.validation) {
|
|
1107
|
+
const validation = report.doctor.validation;
|
|
1108
|
+
process.stdout.write(`- key found: ${validation.key_found ? 'yes' : 'no'}\n`);
|
|
1109
|
+
process.stdout.write(`- key valid: ${validation.key_valid ? 'yes' : 'no'}\n`);
|
|
1110
|
+
process.stdout.write(`- account active: ${validation.account_active ? 'yes' : 'no'}\n`);
|
|
1111
|
+
process.stdout.write(`- agent identity accepted: ${validation.agent_identity_accepted ? 'yes' : 'no'}\n`);
|
|
1112
|
+
process.stdout.write(`- write test event: ${validation.write_test_event}\n`);
|
|
1113
|
+
process.stdout.write(`- outcome closed: ${validation.outcome_closed}\n`);
|
|
1114
|
+
if (validation.failure_reason) process.stdout.write(`- failure reason: ${validation.failure_reason}\n`);
|
|
1115
|
+
if (validation.exact_fix) process.stdout.write(`- exact fix: ${validation.exact_fix}\n`);
|
|
1116
|
+
}
|
|
1117
|
+
if (report.packageVersions?.length) {
|
|
1118
|
+
const outdated = report.packageVersions.filter((pkg) => pkg.outdated);
|
|
1119
|
+
process.stdout.write(`- package versions: ${outdated.length ? 'updates recommended' : 'current'}\n`);
|
|
1120
|
+
for (const pkg of outdated) {
|
|
1121
|
+
process.stdout.write(` - ${pkg.warning} Fix: ${pkg.update_command}\n`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
841
1124
|
if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
|
|
842
1125
|
}
|
|
843
1126
|
|
|
@@ -860,6 +1143,7 @@ async function install(options) {
|
|
|
860
1143
|
const changes = applyPlan(plan, options);
|
|
861
1144
|
const configInspection = inspectNpmTokenConfig();
|
|
862
1145
|
const sdkDependency = inspectSdkDependency(detection);
|
|
1146
|
+
const packageVersions = inspectPackageVersions(detection);
|
|
863
1147
|
const configDiagnostics = configInspection.safe;
|
|
864
1148
|
const configRepairs = options.repair && !options.dryRun && !options.doctor
|
|
865
1149
|
? repairConfigDiagnostics(configDiagnostics)
|
|
@@ -869,7 +1153,11 @@ async function install(options) {
|
|
|
869
1153
|
skipped: false,
|
|
870
1154
|
active: false,
|
|
871
1155
|
error: error instanceof Error ? error.message : String(error),
|
|
1156
|
+
status: error?.status || null,
|
|
1157
|
+
code: error?.code || null,
|
|
1158
|
+
details: error?.details || null,
|
|
872
1159
|
}));
|
|
1160
|
+
const doctorValidation = await runDoctorValidation(options, selfTest);
|
|
873
1161
|
const changedConfig = changes.some((change) => change.changed) || configRepairs.some((repair) => repair.changed);
|
|
874
1162
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
875
1163
|
const remediation = options.repair
|
|
@@ -906,16 +1194,19 @@ async function install(options) {
|
|
|
906
1194
|
missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
|
|
907
1195
|
envHints,
|
|
908
1196
|
missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
|
|
1197
|
+
validation: doctorValidation,
|
|
1198
|
+
packageVersions,
|
|
909
1199
|
recommendedFix: configDiagnostics.npm_token.recommended_fix || selfTest.recommended_fix || (!options.apiKey
|
|
910
1200
|
? envHints.length
|
|
911
|
-
? `MARROW_API_KEY was found in
|
|
912
|
-
: 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
|
|
1201
|
+
? `MARROW_API_KEY was found in ${envHints[0]} and can now be auto-loaded by Marrow SDK/MCP runtimes. Run npx @getmarrow/install --repair to refresh hooks and self-test.`
|
|
1202
|
+
: 'Set MARROW_API_KEY or MARROW_KEY in your shell, MCP secret store, .marrow/env, or ~/.marrow/env, then run npx @getmarrow/install --repair.'
|
|
913
1203
|
: null),
|
|
914
1204
|
},
|
|
915
1205
|
remediation,
|
|
916
1206
|
configDiagnostics,
|
|
917
1207
|
configRepairs,
|
|
918
1208
|
sdkDependency,
|
|
1209
|
+
packageVersions,
|
|
919
1210
|
selfTest,
|
|
920
1211
|
warnings: options.keyFromArg
|
|
921
1212
|
? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
|
|
@@ -946,7 +1237,10 @@ module.exports = {
|
|
|
946
1237
|
runSelfTest,
|
|
947
1238
|
runCli,
|
|
948
1239
|
passiveRuntimeSource,
|
|
1240
|
+
resolveMarrowKeyMaterial,
|
|
949
1241
|
inspectNpmTokenConfig,
|
|
950
1242
|
inspectSdkDependency,
|
|
1243
|
+
inspectPackageVersions,
|
|
1244
|
+
runDoctorValidation,
|
|
951
1245
|
buildInstallValueMoment,
|
|
952
1246
|
};
|