@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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Universal installer for Marrow passive agent setup.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
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 without writing
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, '.openclaw', 'credentials', 'marrow-mcp.env'),
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 `const apiKey = process.env.MARROW_API_KEY;
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: process.env.MARROW_BASE_URL,
310
- agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID,
311
- sessionId: process.env.MARROW_SESSION_ID,
312
- mode: process.env.MARROW_ENFORCEMENT_MODE || 'auto',
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: process.env.MARROW_PASSIVE_VALUE_REPORT !== 'false',
317
- valueReportPeriod: process.env.MARROW_VALUE_REPORT_PERIOD || '7d',
318
- useAgentRuntime: process.env.MARROW_AGENT_RUNTIME !== 'false',
319
- useWorkflowGate: process.env.MARROW_WORKFLOW_GATE !== 'false',
320
- requireOutcomeClosure: process.env.MARROW_REQUIRE_OUTCOME_CLOSURE !== 'false',
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
- throw new Error(String(message));
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 a likely env file at ${envHints[0]}. Load that key from trusted secret storage, export only MARROW_API_KEY, then run npx @getmarrow/install --repair.`
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
  };