@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 +15 -7
- package/package.json +1 -1
- package/src/installer.js +207 -3
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.
|
|
14
|
+
## What's New in v0.1.19
|
|
15
15
|
|
|
16
|
-
v0.1.
|
|
16
|
+
v0.1.19 makes Marrow doctor and release smoke stricter, so agents know exactly why logging is degraded.
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
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
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
|
|
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
|
-
|
|
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
|
};
|