@getmarrow/install 0.1.18 → 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 CHANGED
@@ -11,15 +11,15 @@ npx @getmarrow/install --repair
11
11
  npx @getmarrow/install doctor
12
12
  ```
13
13
 
14
- ## What's New in v0.1.18
14
+ ## What's New in v0.1.19
15
15
 
16
- v0.1.18 makes Marrow key loading more reliable for agents after install.
16
+ v0.1.19 makes Marrow doctor and release smoke stricter, so agents know exactly why logging is degraded.
17
17
 
18
- - Installer, SDK passive runtime, and MCP hooks now all recognize the same key locations: process env, MCP/agent secret env, `.marrow/env`, `.marrow/env.local`, `.env`, `.env.local`, `~/.marrow/env`, and `~/.marrow/env.local`.
19
- - `MARROW_API_KEY` remains the canonical variable. `MARROW_KEY` is accepted as an alias for fleet runners and secret managers.
20
- - `npx @getmarrow/install doctor` now reports whether a key is loaded, where a likely key file exists, and the exact repair command without printing the key.
21
- - Generated `.marrow/passive-runtime.mjs` loads `.marrow/env` before deciding Marrow is missing, so agents keep logging even when the shell did not export the key.
22
- - Full keys are never printed in diagnostics. Keep `.marrow/env` out of git and set file permissions to owner-only when possible.
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
23
 
24
24
  ## What's New in v0.1.17
25
25
 
@@ -233,6 +233,14 @@ Doctor check:
233
233
  MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install doctor
234
234
  ```
235
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
+
236
244
  Repair missing hooks/config:
237
245
 
238
246
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.18",
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
 
@@ -77,14 +80,14 @@ function usage() {
77
80
 
78
81
  Options:
79
82
  --dry-run Print planned changes without writing
80
- --doctor Check install health without writing
83
+ --doctor Check install health; self-test writes and closes a harmless test event
81
84
  --repair Write missing hooks/config, then run self-test and status check
82
85
  --yes, -y Write detected config files
83
86
  --mode <mode> auto, mcp, sdk, both, or md
84
87
  --key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
85
88
  --base-url <url> Marrow API base URL
86
89
  --agent-id <id> Agent/fleet id for self-test headers
87
- --no-self-test Skip API smoke/self-test
90
+ --no-self-test Skip API smoke/self-test for a read-only doctor check
88
91
  `;
89
92
  }
90
93
 
@@ -533,6 +536,82 @@ function inspectSdkDependency(detection) {
533
536
  };
534
537
  }
535
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
+
536
615
  function buildPlan(detection, options) {
537
616
  const mode = options.mode === 'auto'
538
617
  ? detection.node && (detection.claudeCode || detection.cursor || detection.codex || detection.openclaw)
@@ -637,11 +716,108 @@ async function requestJson(url, options) {
637
716
  }
638
717
  if (!res.ok) {
639
718
  const message = json.error || json.message || `HTTP ${res.status}`;
640
- 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;
641
724
  }
642
725
  return json.data || json;
643
726
  }
644
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
+
645
821
  async function runSelfTest(options) {
646
822
  if (!options.selfTest) return { skipped: true, reason: 'disabled' };
647
823
  if (!options.apiKey) {
@@ -927,6 +1103,24 @@ function printReport(report) {
927
1103
  process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
928
1104
  if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
929
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
+ }
930
1124
  if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
931
1125
  }
932
1126
 
@@ -949,6 +1143,7 @@ async function install(options) {
949
1143
  const changes = applyPlan(plan, options);
950
1144
  const configInspection = inspectNpmTokenConfig();
951
1145
  const sdkDependency = inspectSdkDependency(detection);
1146
+ const packageVersions = inspectPackageVersions(detection);
952
1147
  const configDiagnostics = configInspection.safe;
953
1148
  const configRepairs = options.repair && !options.dryRun && !options.doctor
954
1149
  ? repairConfigDiagnostics(configDiagnostics)
@@ -958,7 +1153,11 @@ async function install(options) {
958
1153
  skipped: false,
959
1154
  active: false,
960
1155
  error: error instanceof Error ? error.message : String(error),
1156
+ status: error?.status || null,
1157
+ code: error?.code || null,
1158
+ details: error?.details || null,
961
1159
  }));
1160
+ const doctorValidation = await runDoctorValidation(options, selfTest);
962
1161
  const changedConfig = changes.some((change) => change.changed) || configRepairs.some((repair) => repair.changed);
963
1162
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
964
1163
  const remediation = options.repair
@@ -995,6 +1194,8 @@ async function install(options) {
995
1194
  missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
996
1195
  envHints,
997
1196
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
1197
+ validation: doctorValidation,
1198
+ packageVersions,
998
1199
  recommendedFix: configDiagnostics.npm_token.recommended_fix || selfTest.recommended_fix || (!options.apiKey
999
1200
  ? envHints.length
1000
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.`
@@ -1005,6 +1206,7 @@ async function install(options) {
1005
1206
  configDiagnostics,
1006
1207
  configRepairs,
1007
1208
  sdkDependency,
1209
+ packageVersions,
1008
1210
  selfTest,
1009
1211
  warnings: options.keyFromArg
1010
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.']
@@ -1038,5 +1240,7 @@ module.exports = {
1038
1240
  resolveMarrowKeyMaterial,
1039
1241
  inspectNpmTokenConfig,
1040
1242
  inspectSdkDependency,
1243
+ inspectPackageVersions,
1244
+ runDoctorValidation,
1041
1245
  buildInstallValueMoment,
1042
1246
  };