@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/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 || process.env.MARROW_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; self-test writes and closes a harmless test event
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 for a read-only doctor check
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, '.marrow', 'env.local'),
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
- - Before risky work, use Marrow's before-action intervention from \`GET /v1/agent/status\` or \`POST /v1/agent/runtime\`.
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 `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;
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: 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',
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: 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',
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
- 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;
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 firstValueSignal = buildFirstValueSignal(status, runtime, performance, firstValue);
902
- const installValueMoment = buildInstallValueMoment(firstValueSignal, status, runtime, performance, firstValue);
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.intervention?.agent_copy || runtime.intervention?.before_action || runtime.before_you_act || null,
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 buildInstallValueMoment(firstValueSignal = {}, status = {}, runtime = {}, performance = {}, firstValue = {}) {
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: Array.isArray(firstValue.first_value.proof) ? firstValue.first_value.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.intervention?.agent_copy
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
- runtime.intervention?.must_use_before_action ? 'Before-action intervention is active for risky work' : runtimeLesson ? 'Future risky work now gets a pre-action brief' : 'Future risky work now gets checked before action',
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 proceed/warn/block, required proof, and any matching prior lesson/playbook before the agent acts.',
964
- first_lesson: runtimeLesson || 'Marrow will stop agents before risky or repeated work and surface the prior lesson/playbook.',
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 || runtime?.intervention?.agent_copy,
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]} 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.'
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
  };