@getmarrow/install 0.1.19 → 0.1.21
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 +44 -89
- package/bin/marrow-install.js +1 -1
- package/package.json +1 -3
- package/src/governed-runner.js +486 -23
- package/src/installer.js +80 -333
package/src/installer.js
CHANGED
|
@@ -4,9 +4,6 @@ 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';
|
|
10
7
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
11
8
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
12
9
|
|
|
@@ -18,7 +15,7 @@ function parseArgs(argv) {
|
|
|
18
15
|
doctor: false,
|
|
19
16
|
repair: false,
|
|
20
17
|
mode: 'auto',
|
|
21
|
-
apiKey: process.env.MARROW_API_KEY ||
|
|
18
|
+
apiKey: process.env.MARROW_API_KEY || '',
|
|
22
19
|
baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
23
20
|
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
|
|
24
21
|
selfTest: true,
|
|
@@ -60,12 +57,6 @@ function parseArgs(argv) {
|
|
|
60
57
|
throw new Error('--mode must be one of auto, mcp, sdk, both, md');
|
|
61
58
|
}
|
|
62
59
|
|
|
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
|
-
|
|
69
60
|
return options;
|
|
70
61
|
}
|
|
71
62
|
|
|
@@ -80,14 +71,14 @@ function usage() {
|
|
|
80
71
|
|
|
81
72
|
Options:
|
|
82
73
|
--dry-run Print planned changes without writing
|
|
83
|
-
--doctor Check install health
|
|
74
|
+
--doctor Check install health without writing
|
|
84
75
|
--repair Write missing hooks/config, then run self-test and status check
|
|
85
76
|
--yes, -y Write detected config files
|
|
86
77
|
--mode <mode> auto, mcp, sdk, both, or md
|
|
87
78
|
--key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
|
|
88
79
|
--base-url <url> Marrow API base URL
|
|
89
80
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
90
|
-
--no-self-test Skip API smoke/self-test
|
|
81
|
+
--no-self-test Skip API smoke/self-test
|
|
91
82
|
`;
|
|
92
83
|
}
|
|
93
84
|
|
|
@@ -114,12 +105,6 @@ function findUp(startDir, names, maxDepth = 8) {
|
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
function projectRoot(startDir) {
|
|
117
|
-
const resolved = path.resolve(startDir);
|
|
118
|
-
if (path.basename(resolved) === '.marrow') return path.dirname(resolved);
|
|
119
|
-
if (path.basename(resolved) === 'env' && path.basename(path.dirname(resolved)) === '.marrow') {
|
|
120
|
-
return path.dirname(path.dirname(resolved));
|
|
121
|
-
}
|
|
122
|
-
if (exists(path.join(resolved, '.marrow'))) return resolved;
|
|
123
108
|
const marker = findUp(startDir, ['package.json', 'pyproject.toml', 'requirements.txt', '.git', 'AGENTS.md', 'CLAUDE.md']);
|
|
124
109
|
return marker ? path.dirname(marker) : path.resolve(startDir);
|
|
125
110
|
}
|
|
@@ -166,7 +151,8 @@ function findLikelyEnvFiles(detection, env = process.env) {
|
|
|
166
151
|
path.join(detection.root, '.marrow', 'env'),
|
|
167
152
|
path.join(detection.root, '.marrow', 'env.local'),
|
|
168
153
|
path.join(home, '.marrow', 'env'),
|
|
169
|
-
path.join(home, '.
|
|
154
|
+
path.join(home, '.openclaw', 'credentials', 'marrow-mcp.env'),
|
|
155
|
+
path.join(home, '.openclaw', 'gateway.systemd.env'),
|
|
170
156
|
];
|
|
171
157
|
return candidates.filter((filePath) => {
|
|
172
158
|
if (!exists(filePath)) return false;
|
|
@@ -191,36 +177,6 @@ function readEnvVar(filePath, name) {
|
|
|
191
177
|
return match ? stripQuotes(match[1]) : '';
|
|
192
178
|
}
|
|
193
179
|
|
|
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
|
-
|
|
224
180
|
function readFirstLineSecret(filePath) {
|
|
225
181
|
if (!exists(filePath)) return '';
|
|
226
182
|
return safeRead(filePath).split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
|
@@ -327,7 +283,8 @@ Marrow should run passively after install:
|
|
|
327
283
|
|
|
328
284
|
- Use MCP hooks when available: \`npx -y @getmarrow/mcp setup\`.
|
|
329
285
|
- Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
|
|
330
|
-
-
|
|
286
|
+
- Keep passive token/model usage proof enabled so Marrow can show token, cost, latency, and workflow savings after real work completes.
|
|
287
|
+
- Before risky work, use Marrow's decision brief or passive prompt hook.
|
|
331
288
|
- After meaningful work, record the outcome so future agents learn from it.
|
|
332
289
|
- Check health with \`marrow_agent_status\` or \`GET /v1/agent/status\`.
|
|
333
290
|
|
|
@@ -335,79 +292,29 @@ Required environment:
|
|
|
335
292
|
|
|
336
293
|
- \`MARROW_API_KEY\`
|
|
337
294
|
- 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.
|
|
295
|
+
- Optional: \`MARROW_PASSIVE_TOKEN_USAGE=false\` disables compact provider usage capture when needed.
|
|
344
296
|
${MARROW_BLOCK_END}`;
|
|
345
297
|
}
|
|
346
298
|
|
|
347
299
|
function passiveRuntimeSource() {
|
|
348
|
-
return `
|
|
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;
|
|
300
|
+
return `const apiKey = process.env.MARROW_API_KEY;
|
|
395
301
|
if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
396
302
|
try {
|
|
397
303
|
const { MarrowClient } = await import('@getmarrow/sdk');
|
|
398
304
|
const marrow = new MarrowClient(apiKey, {
|
|
399
|
-
baseUrl:
|
|
400
|
-
agentId:
|
|
401
|
-
sessionId:
|
|
402
|
-
mode:
|
|
305
|
+
baseUrl: process.env.MARROW_BASE_URL,
|
|
306
|
+
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID,
|
|
307
|
+
sessionId: process.env.MARROW_SESSION_ID,
|
|
308
|
+
mode: process.env.MARROW_ENFORCEMENT_MODE || 'auto',
|
|
403
309
|
});
|
|
404
310
|
|
|
405
311
|
const runtime = marrow.createPassiveRuntime({
|
|
406
|
-
includeValueReport:
|
|
407
|
-
valueReportPeriod:
|
|
408
|
-
useAgentRuntime:
|
|
409
|
-
useWorkflowGate:
|
|
410
|
-
requireOutcomeClosure:
|
|
312
|
+
includeValueReport: process.env.MARROW_PASSIVE_VALUE_REPORT !== 'false',
|
|
313
|
+
valueReportPeriod: process.env.MARROW_VALUE_REPORT_PERIOD || '7d',
|
|
314
|
+
useAgentRuntime: process.env.MARROW_AGENT_RUNTIME !== 'false',
|
|
315
|
+
useWorkflowGate: process.env.MARROW_WORKFLOW_GATE !== 'false',
|
|
316
|
+
requireOutcomeClosure: process.env.MARROW_REQUIRE_OUTCOME_CLOSURE !== 'false',
|
|
317
|
+
captureModelUsage: process.env.MARROW_PASSIVE_TOKEN_USAGE !== 'false',
|
|
411
318
|
});
|
|
412
319
|
|
|
413
320
|
runtime.install();
|
|
@@ -415,8 +322,6 @@ if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
|
415
322
|
} catch {
|
|
416
323
|
console.warn('[Marrow] passive runtime skipped: install @getmarrow/sdk or verify SDK initialization. Run npm install @getmarrow/sdk, then rerun npx @getmarrow/install --repair.');
|
|
417
324
|
}
|
|
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.');
|
|
420
325
|
}
|
|
421
326
|
`;
|
|
422
327
|
}
|
|
@@ -431,6 +336,7 @@ MARROW_PASSIVE_VALUE_REPORT=true
|
|
|
431
336
|
MARROW_AGENT_RUNTIME=true
|
|
432
337
|
MARROW_WORKFLOW_GATE=true
|
|
433
338
|
MARROW_REQUIRE_OUTCOME_CLOSURE=true
|
|
339
|
+
MARROW_PASSIVE_TOKEN_USAGE=true
|
|
434
340
|
`;
|
|
435
341
|
}
|
|
436
342
|
|
|
@@ -536,82 +442,6 @@ function inspectSdkDependency(detection) {
|
|
|
536
442
|
};
|
|
537
443
|
}
|
|
538
444
|
|
|
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
|
-
|
|
615
445
|
function buildPlan(detection, options) {
|
|
616
446
|
const mode = options.mode === 'auto'
|
|
617
447
|
? detection.node && (detection.claudeCode || detection.cursor || detection.codex || detection.openclaw)
|
|
@@ -716,108 +546,11 @@ async function requestJson(url, options) {
|
|
|
716
546
|
}
|
|
717
547
|
if (!res.ok) {
|
|
718
548
|
const message = json.error || json.message || `HTTP ${res.status}`;
|
|
719
|
-
|
|
720
|
-
error.status = res.status;
|
|
721
|
-
error.code = json.code || null;
|
|
722
|
-
error.details = json.details || null;
|
|
723
|
-
throw error;
|
|
549
|
+
throw new Error(String(message));
|
|
724
550
|
}
|
|
725
551
|
return json.data || json;
|
|
726
552
|
}
|
|
727
553
|
|
|
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
|
-
|
|
821
554
|
async function runSelfTest(options) {
|
|
822
555
|
if (!options.selfTest) return { skipped: true, reason: 'disabled' };
|
|
823
556
|
if (!options.apiKey) {
|
|
@@ -898,8 +631,14 @@ async function runSelfTest(options) {
|
|
|
898
631
|
ok: false,
|
|
899
632
|
error: error instanceof Error ? error.message : String(error),
|
|
900
633
|
}));
|
|
901
|
-
const
|
|
902
|
-
|
|
634
|
+
const valueProof = await requestJson(`${baseUrl}/v1/agent/value/proof?period_days=30`, { headers })
|
|
635
|
+
.catch((error) => ({
|
|
636
|
+
ok: false,
|
|
637
|
+
error: error instanceof Error ? error.message : String(error),
|
|
638
|
+
}));
|
|
639
|
+
const tokenValueProof = buildTokenValueProof(valueProof);
|
|
640
|
+
const firstValueSignal = buildFirstValueSignal(status, runtime, performance, firstValue, tokenValueProof);
|
|
641
|
+
const installValueMoment = buildInstallValueMoment(firstValueSignal, status, runtime, performance, firstValue, tokenValueProof);
|
|
903
642
|
return {
|
|
904
643
|
skipped: false,
|
|
905
644
|
decision_id: decisionId,
|
|
@@ -911,11 +650,11 @@ async function runSelfTest(options) {
|
|
|
911
650
|
auto_outcome_closure: status.auto_outcome_closure || null,
|
|
912
651
|
runtime_active: Boolean(runtime && runtime.ok !== false),
|
|
913
652
|
runtime_exact_next_action: runtime.exact_next_action || null,
|
|
914
|
-
runtime_before_you_act: runtime.
|
|
915
|
-
runtime_intervention: runtime.intervention || null,
|
|
653
|
+
runtime_before_you_act: runtime.before_you_act || null,
|
|
916
654
|
first_value: firstValue && firstValue.ok !== false ? firstValue : null,
|
|
917
655
|
first_value_signal: firstValueSignal,
|
|
918
656
|
install_value_moment: installValueMoment,
|
|
657
|
+
token_value_proof: tokenValueProof,
|
|
919
658
|
performance_proof: performance && performance.ok !== false ? {
|
|
920
659
|
avoided_mistakes: performance.avoided_mistakes ?? performance.avoided_repeated_mistakes ?? 0,
|
|
921
660
|
reused_winning_decisions: performance.reused_winning_decisions ?? 0,
|
|
@@ -927,11 +666,30 @@ async function runSelfTest(options) {
|
|
|
927
666
|
};
|
|
928
667
|
}
|
|
929
668
|
|
|
930
|
-
function
|
|
669
|
+
function buildTokenValueProof(valueProof = {}) {
|
|
670
|
+
const modelUsage = valueProof && valueProof.ok !== false
|
|
671
|
+
? valueProof.model_usage || valueProof.token_value_signal || valueProof
|
|
672
|
+
: null;
|
|
673
|
+
if (!modelUsage || typeof modelUsage !== 'object') {
|
|
674
|
+
return {
|
|
675
|
+
enabled: true,
|
|
676
|
+
capture_default: 'on_when_sdk_mcp_or_installer_hooks_available',
|
|
677
|
+
observed: { model_calls: 0, tokens: { total: 0 } },
|
|
678
|
+
savings: { estimated_tokens_saved: 0, estimated_minutes_saved: 0, confidence: 'none', method: 'warming_up' },
|
|
679
|
+
proof_line: 'Token usage capture is ready; no model calls have been reported yet.',
|
|
680
|
+
exact_next_action: 'Keep passive token capture enabled so Marrow can attach usage proof after real model calls complete.',
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return modelUsage;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {}, performance = {}, firstValue = {}, tokenValueProof = null) {
|
|
931
687
|
if (firstValue && firstValue.ok !== false && firstValue.first_value) {
|
|
688
|
+
const proof = Array.isArray(firstValue.first_value.proof) ? [...firstValue.first_value.proof] : [];
|
|
689
|
+
if (tokenValueProof?.proof_line && !proof.includes(tokenValueProof.proof_line)) proof.push(tokenValueProof.proof_line);
|
|
932
690
|
return {
|
|
933
691
|
headline: firstValue.headline || firstValue.first_value.headline || 'Your agent is no longer starting from zero.',
|
|
934
|
-
proof
|
|
692
|
+
proof,
|
|
935
693
|
fleet_signal: firstValue.history_signal?.summary || 'Fresh account: Marrow will build fleet memory from this first captured outcome.',
|
|
936
694
|
try_this_now: firstValue.first_value.try_this_now || 'Ask your agent: "I am about to deploy to production. What should I check first?"',
|
|
937
695
|
expected_response: firstValue.first_value.expected_response || 'Marrow should answer with a risk gate, required proof, and any matching fleet lessons before the agent acts.',
|
|
@@ -941,9 +699,7 @@ function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {
|
|
|
941
699
|
|
|
942
700
|
const proof = firstValueSignal.value_proof || [];
|
|
943
701
|
const hasFleetSignal = proof.length > 0;
|
|
944
|
-
const runtimeLesson = runtime.
|
|
945
|
-
|| runtime.intervention?.before_action
|
|
946
|
-
|| runtime.before_you_act
|
|
702
|
+
const runtimeLesson = runtime.before_you_act
|
|
947
703
|
|| runtime.before_you_act_injection?.message
|
|
948
704
|
|| runtime.exact_next_action
|
|
949
705
|
|| firstValueSignal.first_lesson;
|
|
@@ -954,18 +710,19 @@ function buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {
|
|
|
954
710
|
'Captured this setup decision',
|
|
955
711
|
'Closed the outcome successfully',
|
|
956
712
|
'Runtime gate is ' + (firstValueSignal.active ? 'active' : 'installed'),
|
|
957
|
-
|
|
713
|
+
runtimeLesson ? 'Future risky work now gets a pre-action brief' : 'Future risky work now gets checked before action',
|
|
714
|
+
tokenValueProof?.proof_line || 'Token usage proof is active and warming up after the first model call',
|
|
958
715
|
],
|
|
959
716
|
fleet_signal: hasFleetSignal
|
|
960
717
|
? 'Marrow already found signal: ' + proof.join('; ') + '.'
|
|
961
718
|
: 'Fresh account: Marrow will start building fleet memory from this first captured outcome.',
|
|
962
719
|
try_this_now: 'Ask your agent: "I am about to deploy to production. What should I check first?"',
|
|
963
|
-
expected_response: 'Marrow should answer with
|
|
964
|
-
first_lesson: runtimeLesson || 'Marrow will
|
|
720
|
+
expected_response: 'Marrow should answer with a risk gate, required proof, and any matching fleet lessons before the agent acts.',
|
|
721
|
+
first_lesson: runtimeLesson || 'Marrow will surface prior lessons before risky or repeated work.',
|
|
965
722
|
};
|
|
966
723
|
}
|
|
967
724
|
|
|
968
|
-
function buildFirstValueSignal(status, runtime, performance, firstValue = {}) {
|
|
725
|
+
function buildFirstValueSignal(status, runtime, performance, firstValue = {}, tokenValueProof = null) {
|
|
969
726
|
if (firstValue && firstValue.ok !== false && firstValue.first_value) {
|
|
970
727
|
const capture = firstValue.capture || {};
|
|
971
728
|
const proof = firstValue.value_proof || {};
|
|
@@ -974,11 +731,13 @@ function buildFirstValueSignal(status, runtime, performance, firstValue = {}) {
|
|
|
974
731
|
if (Number(proof.reused_winning_decisions || 0) > 0) proofBits.push(`${proof.reused_winning_decisions} reused winning decision(s)`);
|
|
975
732
|
if (Number(proof.prevented_bad_actions || 0) > 0) proofBits.push(`${proof.prevented_bad_actions} prevented risky action(s)`);
|
|
976
733
|
if (Number(proof.estimated_tokens_saved || 0) > 0) proofBits.push(`~${proof.estimated_tokens_saved} tokens saved`);
|
|
734
|
+
if (Number(tokenValueProof?.savings?.estimated_tokens_saved || 0) > 0) proofBits.push(`~${tokenValueProof.savings.estimated_tokens_saved} measured model tokens saved`);
|
|
735
|
+
else if (tokenValueProof?.proof_line) proofBits.push(tokenValueProof.proof_line);
|
|
977
736
|
return {
|
|
978
737
|
active: Boolean(firstValue.active),
|
|
979
738
|
headline: `Marrow active: ${(capture.surfaces || ['decisions']).join(', ')} captured.`,
|
|
980
739
|
captured: capture.surfaces || ['decisions'],
|
|
981
|
-
first_lesson: firstValue.first_value.first_lesson
|
|
740
|
+
first_lesson: firstValue.first_value.first_lesson,
|
|
982
741
|
value_proof: proofBits,
|
|
983
742
|
next_action: firstValue.next_action?.reason || 'Keep working; Marrow will capture outcomes and reuse lessons automatically.',
|
|
984
743
|
};
|
|
@@ -1001,6 +760,8 @@ function buildFirstValueSignal(status, runtime, performance, firstValue = {}) {
|
|
|
1001
760
|
if (Number(proof.prevented_bad_actions || 0) > 0) proofBits.push(`${proof.prevented_bad_actions} prevented risky action(s)`);
|
|
1002
761
|
const tokens = proof.token_time_saved_estimate?.estimated_tokens_saved || 0;
|
|
1003
762
|
if (tokens > 0) proofBits.push(`~${tokens} tokens saved`);
|
|
763
|
+
if (Number(tokenValueProof?.savings?.estimated_tokens_saved || 0) > 0) proofBits.push(`~${tokenValueProof.savings.estimated_tokens_saved} measured model tokens saved`);
|
|
764
|
+
else if (tokenValueProof?.proof_line) proofBits.push(tokenValueProof.proof_line);
|
|
1004
765
|
|
|
1005
766
|
const firstLesson = runtime.before_you_act
|
|
1006
767
|
|| runtime.before_you_act_injection?.message
|
|
@@ -1064,6 +825,20 @@ function printReport(report) {
|
|
|
1064
825
|
process.stdout.write(`- Next: ${report.selfTest.first_value_signal.next_action}\n`);
|
|
1065
826
|
}
|
|
1066
827
|
}
|
|
828
|
+
if (report.selfTest.token_value_proof) {
|
|
829
|
+
const proof = report.selfTest.token_value_proof;
|
|
830
|
+
const observed = proof.observed || {};
|
|
831
|
+
const savings = proof.savings || {};
|
|
832
|
+
const tokens = observed.tokens || {};
|
|
833
|
+
process.stdout.write('\nToken value proof:\n');
|
|
834
|
+
process.stdout.write(`- passive capture: ${proof.enabled ? 'on' : 'unknown'}\n`);
|
|
835
|
+
process.stdout.write(`- model calls observed: ${observed.model_calls || 0}\n`);
|
|
836
|
+
process.stdout.write(`- tokens observed: ${tokens.total || 0}\n`);
|
|
837
|
+
process.stdout.write(`- estimated tokens saved: ${savings.estimated_tokens_saved || 0}\n`);
|
|
838
|
+
if (savings.confidence) process.stdout.write(`- confidence: ${savings.confidence}\n`);
|
|
839
|
+
if (proof.proof_line) process.stdout.write(`- proof: ${proof.proof_line}\n`);
|
|
840
|
+
if (proof.exact_next_action) process.stdout.write(`- next: ${proof.exact_next_action}\n`);
|
|
841
|
+
}
|
|
1067
842
|
}
|
|
1068
843
|
|
|
1069
844
|
if (report.remediation) {
|
|
@@ -1103,24 +878,6 @@ function printReport(report) {
|
|
|
1103
878
|
process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
|
|
1104
879
|
if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
|
|
1105
880
|
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
|
-
}
|
|
1124
881
|
if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
|
|
1125
882
|
}
|
|
1126
883
|
|
|
@@ -1143,7 +900,6 @@ async function install(options) {
|
|
|
1143
900
|
const changes = applyPlan(plan, options);
|
|
1144
901
|
const configInspection = inspectNpmTokenConfig();
|
|
1145
902
|
const sdkDependency = inspectSdkDependency(detection);
|
|
1146
|
-
const packageVersions = inspectPackageVersions(detection);
|
|
1147
903
|
const configDiagnostics = configInspection.safe;
|
|
1148
904
|
const configRepairs = options.repair && !options.dryRun && !options.doctor
|
|
1149
905
|
? repairConfigDiagnostics(configDiagnostics)
|
|
@@ -1153,11 +909,7 @@ async function install(options) {
|
|
|
1153
909
|
skipped: false,
|
|
1154
910
|
active: false,
|
|
1155
911
|
error: error instanceof Error ? error.message : String(error),
|
|
1156
|
-
status: error?.status || null,
|
|
1157
|
-
code: error?.code || null,
|
|
1158
|
-
details: error?.details || null,
|
|
1159
912
|
}));
|
|
1160
|
-
const doctorValidation = await runDoctorValidation(options, selfTest);
|
|
1161
913
|
const changedConfig = changes.some((change) => change.changed) || configRepairs.some((repair) => repair.changed);
|
|
1162
914
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1163
915
|
const remediation = options.repair
|
|
@@ -1194,19 +946,16 @@ async function install(options) {
|
|
|
1194
946
|
missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
|
|
1195
947
|
envHints,
|
|
1196
948
|
missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
|
|
1197
|
-
validation: doctorValidation,
|
|
1198
|
-
packageVersions,
|
|
1199
949
|
recommendedFix: configDiagnostics.npm_token.recommended_fix || selfTest.recommended_fix || (!options.apiKey
|
|
1200
950
|
? envHints.length
|
|
1201
|
-
? `MARROW_API_KEY was found in ${envHints[0]}
|
|
1202
|
-
: 'Set MARROW_API_KEY
|
|
951
|
+
? `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.`
|
|
952
|
+
: 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
|
|
1203
953
|
: null),
|
|
1204
954
|
},
|
|
1205
955
|
remediation,
|
|
1206
956
|
configDiagnostics,
|
|
1207
957
|
configRepairs,
|
|
1208
958
|
sdkDependency,
|
|
1209
|
-
packageVersions,
|
|
1210
959
|
selfTest,
|
|
1211
960
|
warnings: options.keyFromArg
|
|
1212
961
|
? ['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.']
|
|
@@ -1237,10 +986,8 @@ module.exports = {
|
|
|
1237
986
|
runSelfTest,
|
|
1238
987
|
runCli,
|
|
1239
988
|
passiveRuntimeSource,
|
|
1240
|
-
resolveMarrowKeyMaterial,
|
|
1241
989
|
inspectNpmTokenConfig,
|
|
1242
990
|
inspectSdkDependency,
|
|
1243
|
-
inspectPackageVersions,
|
|
1244
|
-
runDoctorValidation,
|
|
1245
991
|
buildInstallValueMoment,
|
|
992
|
+
buildTokenValueProof,
|
|
1246
993
|
};
|