memhouse 0.18.12 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AGENTS.md +22 -151
  2. package/CHANGELOG.md +263 -29
  3. package/LICENSE +202 -22
  4. package/LICENSES/agentlytics-MIT.txt +21 -0
  5. package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
  6. package/LICENSES/models.dev-MIT.txt +22 -0
  7. package/NOTICE +28 -0
  8. package/README.md +42 -20
  9. package/SECURITY.md +21 -8
  10. package/bin/memhouse.js +2223 -311
  11. package/docs/README.md +11 -9
  12. package/docs/guides/admin-password.md +37 -10
  13. package/docs/guides/configuration.md +13 -6
  14. package/docs/guides/daemons.md +64 -3
  15. package/docs/guides/import-claude-ai.md +204 -0
  16. package/docs/guides/install.md +11 -6
  17. package/docs/guides/invite.md +21 -7
  18. package/docs/guides/operate.md +114 -7
  19. package/docs/guides/search-and-resume.md +72 -3
  20. package/docs/guides/share.md +91 -52
  21. package/docs/guides/storage.md +44 -4
  22. package/docs/guides/troubleshooting.md +121 -14
  23. package/docs/guides/uninstall.md +4 -3
  24. package/docs/guides/update.md +16 -0
  25. package/docs/guides/upgrade-to-0.20.md +188 -0
  26. package/docs/releases/README.md +2 -1
  27. package/docs/releases/v0.18.10.md +5 -5
  28. package/docs/releases/v0.18.11.md +3 -3
  29. package/docs/releases/v0.20.0.md +627 -0
  30. package/docs/releases/v0.20.1.md +114 -0
  31. package/docs/tutorials/01-first-house.md +10 -7
  32. package/docs/tutorials/02-join-a-house.md +24 -15
  33. package/docs/tutorials/03-share-your-memory.md +40 -31
  34. package/editors/antigravity.js +1 -80
  35. package/editors/base.js +463 -9
  36. package/editors/bridge.js +119 -0
  37. package/editors/claude.js +273 -105
  38. package/editors/codex.js +278 -52
  39. package/editors/copilot.js +1 -70
  40. package/editors/cursor.js +1 -75
  41. package/editors/derive-fingerprints.json +18 -0
  42. package/editors/harness-paths.js +69 -0
  43. package/editors/index.js +1 -20
  44. package/editors/normalise/agy.js +910 -0
  45. package/editors/normalise/copilot-cli.js +321 -0
  46. package/editors/normalise/cursor-agent.js +852 -0
  47. package/editors/normalise/cursor.js +474 -0
  48. package/editors/normalise/devin.js +575 -0
  49. package/editors/normalise/gemini-cli.js +602 -0
  50. package/editors/normalise/goose.js +673 -0
  51. package/editors/normalise/opencode.js +633 -0
  52. package/editors/normalise/vscode.js +549 -0
  53. package/editors/normalise/zed.js +473 -0
  54. package/editors/opencode-v4.js +119 -0
  55. package/editors/scope.js +14 -3
  56. package/editors/sessions-log.js +349 -0
  57. package/editors/sqlite.js +28 -1
  58. package/editors/vscode.js +1 -72
  59. package/editors/windsurf.js +1 -143
  60. package/examples/01-local-one-machine/README.md +24 -19
  61. package/examples/01-local-one-machine/run.sh +1 -1
  62. package/examples/02-existing-clickhouse/README.md +30 -22
  63. package/examples/02-existing-clickhouse/run.sh +5 -4
  64. package/examples/03-team-house/README.md +44 -37
  65. package/examples/03-team-house/run.sh +35 -18
  66. package/examples/04-self-hosted-team/README.md +29 -14
  67. package/examples/04-self-hosted-team/compose.yml +5 -2
  68. package/examples/04-self-hosted-team/smoke.sh +26 -12
  69. package/examples/README.md +8 -4
  70. package/examples/fixtures/README.md +4 -1
  71. package/examples/fixtures/make-sessions.js +8 -5
  72. package/memhouse/admin-secret.js +89 -24
  73. package/memhouse/argv-secrets.js +64 -0
  74. package/memhouse/channel.js +49 -9
  75. package/memhouse/codex-plugin.js +194 -0
  76. package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
  77. package/memhouse/delivery/AGENT-INSTALL.md +22 -11
  78. package/memhouse/delivery/INVITATION.md.tpl +44 -45
  79. package/memhouse/delivery/PROMPT.md +53 -5
  80. package/memhouse/delivery/UPGRADE.md.tpl +1 -1
  81. package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
  82. package/memhouse/delivery/codex/SKILL.md +50 -0
  83. package/memhouse/delivery/kernel-install.md +17 -9
  84. package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
  85. package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
  86. package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
  87. package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
  88. package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
  89. package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
  90. package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
  91. package/memhouse/deploy.js +1 -1
  92. package/memhouse/flags.js +35 -18
  93. package/memhouse/help.js +37 -0
  94. package/memhouse/host.js +82 -2
  95. package/memhouse/house/HOUSE.md +2 -2
  96. package/memhouse/house/convert-profile.js +638 -0
  97. package/memhouse/house/convert-v3.js +742 -0
  98. package/memhouse/house/doctor-v3.js +324 -0
  99. package/memhouse/house/host-alias.js +85 -0
  100. package/memhouse/house/house.js +3 -3
  101. package/memhouse/house/migrate.js +2 -2
  102. package/memhouse/house/profile-dedupe.js +183 -0
  103. package/memhouse/house/profile-edges.js +554 -0
  104. package/memhouse/house/profile-house.js +430 -0
  105. package/memhouse/house/profile-rollup.js +190 -0
  106. package/memhouse/house/q-cli.js +863 -0
  107. package/memhouse/house/queries-v3.js +1013 -0
  108. package/memhouse/house/reconcile-v3.js +192 -0
  109. package/memhouse/house/records-to-profile.js +174 -0
  110. package/memhouse/house/records-to-rows.js +169 -0
  111. package/memhouse/house/relocate.js +2 -1
  112. package/memhouse/house/schema-choice.js +121 -0
  113. package/memhouse/house/schema-profile.js +290 -0
  114. package/memhouse/house/schema-profile.sql.tpl +398 -0
  115. package/memhouse/house/schema-v3.js +267 -0
  116. package/memhouse/house/schema-v3.sql.tpl +213 -0
  117. package/memhouse/import/claude-ai.js +420 -0
  118. package/memhouse/import/import.js +372 -0
  119. package/memhouse/import/pyjson.js +206 -0
  120. package/memhouse/import/records.js +64 -0
  121. package/memhouse/import/writer-profile.js +192 -0
  122. package/memhouse/import/writer-v3.js +240 -0
  123. package/memhouse/import/zip.js +112 -0
  124. package/memhouse/invitation.js +102 -0
  125. package/memhouse/invitefile.js +5 -2
  126. package/memhouse/plugin-copies.js +176 -0
  127. package/memhouse/provision.js +7 -2
  128. package/memhouse/release-hint.js +24 -0
  129. package/memhouse/resume.js +4 -4
  130. package/memhouse/server/api-v3.js +632 -0
  131. package/memhouse/server/dashboard-auth.js +181 -0
  132. package/memhouse/server/dashboard-port.js +91 -0
  133. package/memhouse/server/queries.js +30 -0
  134. package/memhouse/server/server.js +118 -20
  135. package/memhouse/service.js +1 -1
  136. package/memhouse/share.js +687 -1
  137. package/memhouse/shipper/adapter-capture.js +756 -0
  138. package/memhouse/shipper/capture.js +2571 -0
  139. package/memhouse/shipper/dedupe-profile.js +237 -0
  140. package/memhouse/shipper/dedupe-v3.js +1027 -0
  141. package/memhouse/shipper/forks.js +233 -0
  142. package/memhouse/shipper/log-adapter.js +219 -0
  143. package/memhouse/shipper/maintenance.js +49 -0
  144. package/memhouse/shipper/normalise-run.js +148 -0
  145. package/memhouse/shipper/passlog.js +68 -5
  146. package/memhouse/shipper/profile-capture.js +903 -0
  147. package/memhouse/shipper/ship.js +91 -8
  148. package/memhouse/shipper/snapshots.js +193 -0
  149. package/memhouse/shipper/source-roots.js +111 -0
  150. package/memhouse/version-notice.js +66 -0
  151. package/package.json +14 -7
  152. package/public/THIRD_PARTY_NOTICES.txt +2 -2
  153. package/public/assets/index-CcNnhfM3.js +78 -0
  154. package/public/index.html +1 -1
  155. package/ui/package-lock.json +56 -37
  156. package/ui/package.json +1 -0
  157. package/ui/src/App.jsx +29 -14
  158. package/ui/src/components/ChatSidebar.jsx +69 -12
  159. package/ui/src/lib/api.js +76 -0
  160. package/ui/src/lib/dashboard-token.js +41 -0
  161. package/ui/src/main.jsx +22 -5
  162. package/ui/src/pages/Dashboard.jsx +22 -36
  163. package/ui/third-party-notices.js +119 -0
  164. package/ui/vite.config.js +5 -1
  165. package/docs/releases/v0.18.12.md +0 -59
  166. package/memhouse/COMPETITION.md +0 -221
  167. package/memhouse/DESIGN.md +0 -222
  168. package/memhouse/RESUME-AND-UPDATE.md +0 -182
  169. package/public/assets/index-CnWzrjF3.js +0 -78
package/editors/claude.js CHANGED
@@ -5,7 +5,7 @@ const adapterErrors = require('./adapter-errors');
5
5
 
6
6
  const HOME = os.homedir();
7
7
 
8
- // Discover all Claude Code config roots — ports memory-house agent-sync's
8
+ // Discover all Claude Code config roots — ports the predecessor tool's
9
9
  // discover_roots("claude-code"). Claude sessions don't live only under ~/.claude:
10
10
  // every CLAUDE_CONFIG_DIR (e.g. playbook installs under
11
11
  // ~/.claude-playbooks/<name>[/playbook]) has its own projects/ dir. A dir qualifies
@@ -430,108 +430,6 @@ function extractAssistantContent(content) {
430
430
  return { text: parts.join('\n') || '', toolCalls };
431
431
  }
432
432
 
433
- // ============================================================
434
- // Usage / quota data from Anthropic OAuth API
435
- // ============================================================
436
-
437
- function getClaudeCredentials() {
438
- // macOS: Keychain; Linux: secret-tool; Windows: not yet supported
439
- // Requires explicit user permission (allowSubscriptionAccess in config)
440
- const { isSubscriptionAccessAllowed } = require('./base');
441
- if (!isSubscriptionAccessAllowed()) return null;
442
- try {
443
- const { execSync } = require('child_process');
444
- let raw;
445
- if (process.platform === 'darwin') {
446
- raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', { encoding: 'utf-8', timeout: 5000 }).trim();
447
- } else if (process.platform === 'linux') {
448
- raw = execSync('secret-tool lookup service "Claude Code-credentials"', { encoding: 'utf-8', timeout: 5000 }).trim();
449
- } else {
450
- return null;
451
- }
452
- const creds = JSON.parse(raw);
453
- const oauth = creds.claudeAiOauth;
454
- if (!oauth || !oauth.accessToken) return null;
455
- return oauth;
456
- } catch { return null; }
457
- }
458
-
459
- function claudeApiFetch(token) {
460
- return new Promise((resolve) => {
461
- const https = require('https');
462
- const req = https.get('https://api.anthropic.com/api/oauth/usage', {
463
- headers: {
464
- 'Accept': 'application/json',
465
- 'Content-Type': 'application/json',
466
- 'User-Agent': 'agentlytics/1.0',
467
- 'Authorization': `Bearer ${token}`,
468
- 'anthropic-beta': 'oauth-2025-04-20',
469
- },
470
- timeout: 10000,
471
- }, (res) => {
472
- let data = '';
473
- res.on('data', (chunk) => { data += chunk; });
474
- res.on('end', () => {
475
- try { resolve(JSON.parse(data)); } catch { resolve(null); }
476
- });
477
- });
478
- req.on('error', () => resolve(null));
479
- req.on('timeout', () => { req.destroy(); resolve(null); });
480
- });
481
- }
482
-
483
- async function getUsage() {
484
- const creds = getClaudeCredentials();
485
- if (!creds) return null;
486
-
487
- const usage = await claudeApiFetch(creds.accessToken);
488
- if (!usage) return null;
489
-
490
- const result = {
491
- source: 'claude-code',
492
- plan: {
493
- name: creds.subscriptionType || null,
494
- },
495
- usage: {},
496
- extraUsage: null,
497
- };
498
-
499
- if (usage.five_hour) {
500
- result.usage.fiveHour = {
501
- utilization: usage.five_hour.utilization,
502
- resetsAt: usage.five_hour.resets_at || null,
503
- };
504
- }
505
- if (usage.seven_day) {
506
- result.usage.sevenDay = {
507
- utilization: usage.seven_day.utilization,
508
- resetsAt: usage.seven_day.resets_at || null,
509
- };
510
- }
511
- if (usage.seven_day_sonnet) {
512
- result.usage.sevenDaySonnet = {
513
- utilization: usage.seven_day_sonnet.utilization,
514
- resetsAt: usage.seven_day_sonnet.resets_at || null,
515
- };
516
- }
517
- if (usage.seven_day_opus) {
518
- result.usage.sevenDayOpus = {
519
- utilization: usage.seven_day_opus.utilization,
520
- resetsAt: usage.seven_day_opus.resets_at || null,
521
- };
522
- }
523
- if (usage.extra_usage) {
524
- result.extraUsage = {
525
- isEnabled: usage.extra_usage.is_enabled || false,
526
- monthlyLimit: usage.extra_usage.monthly_limit || null,
527
- usedCredits: usage.extra_usage.used_credits || null,
528
- utilization: usage.extra_usage.utilization || null,
529
- };
530
- }
531
-
532
- return result;
533
- }
534
-
535
433
  const labels = { 'claude-code': 'Claude Code' };
536
434
 
537
435
  function getArtifacts(folder) {
@@ -554,8 +452,278 @@ function getMCPServers() {
554
452
  return results;
555
453
  }
556
454
 
455
+ // <derive> ====================================================================================
456
+ // Capture (schema v3) — docs/design/source-identity.md draft 5, §2 and §5. Fingerprinted by
457
+ // misc/capture-test.js: change anything in this region and bump CLAUDE_DERIVE_VERSION.
458
+ //
459
+ // message_id the record `uuid` (+ `part`, the block index); '@<byte offset>' for records
460
+ // that have none (summary)
461
+ // ts the record `timestamp`; else first-seen, stored once
462
+ // tokens on ONE row per message.id: the first row of the response. A response is the
463
+ // open unit until the next one starts; a later record of it reporting more usage
464
+ // re-emits that row at a higher rev. Never per record (that doubled the totals).
465
+ // agent_id '' for the main conversation, the sub-agent's id for subagents/**/agent-<id>.jsonl
466
+ // Version 2: layout 3.2 (part_id/part_ord, harness_path, full tool output). Version 3: no derive change: comment wording in the shared capture region.
467
+ const CLAUDE_DERIVE_VERSION = 3;
468
+
469
+ function cRespId(rec) {
470
+ const m = rec.obj && rec.obj.message;
471
+ return (m && typeof m.id === 'string' && m.id) ? m.id : `@${rec.pos}`;
472
+ }
473
+ function cUsage(u) {
474
+ if (!u || typeof u !== 'object') return null;
475
+ const n = (v) => { const x = Math.trunc(Number(v)); return Number.isFinite(x) && x > 0 ? x : 0; };
476
+ return {
477
+ input_tokens: n(u.input_tokens), output_tokens: n(u.output_tokens),
478
+ cache_read_tokens: n(u.cache_read_input_tokens), cache_write_tokens: n(u.cache_creation_input_tokens),
479
+ reasoning_tokens: 0,
480
+ };
481
+ }
482
+ function cUsageTotal(u) { return u ? u.input_tokens + u.output_tokens + u.cache_read_tokens + u.cache_write_tokens : -1; }
483
+ function cBlockText(content) {
484
+ if (typeof content === 'string') return content;
485
+ if (!Array.isArray(content)) return content == null ? '' : JSON.stringify(content);
486
+ return content.map((x) => (x && typeof x.text === 'string') ? x.text : '').filter(Boolean).join('\n');
487
+ }
488
+ // A prompt that is only an envelope (a command, a caveat, a reminder) is not the session's first prompt.
489
+ function cMeaningful(text) {
490
+ const t = String(text || '')
491
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
492
+ .replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '')
493
+ .replace(/<command-(name|message|args|contents|stdout|stderr)>[\s\S]*?<\/command-\1>/g, '')
494
+ .replace(/<[^>]+>/g, '').trim();
495
+ return t.length > 0 && !/^Caveat: The messages below were generated by the user/i.test(t);
496
+ }
497
+
498
+ const claudeDeriveAdapter = {
499
+ harness: 'claude-code',
500
+ deriveVersion: CLAUDE_DERIVE_VERSION,
501
+ initState() { return { u: { resp: null }, model: '', effort: '', version: '', folder: '', cur: null, closed: [], agentCalls: {} }; },
502
+ unitStart(rec, u) { return !!rec.obj && rec.obj.type === 'assistant' && cRespId(rec) !== u.resp; },
503
+ track(rec, u) { if (rec.obj && rec.obj.type === 'assistant') u.resp = cRespId(rec); },
504
+
505
+ closeUnit(d) {
506
+ const cur = d.a.cur;
507
+ if (!cur) return;
508
+ claudeDeriveAdapter.usageRowIfNeeded(d, cur);
509
+ // Kept for a response that resumes after another one (interleaved sidechain records): its
510
+ // anchor can still be re-emitted with the larger usage. Small and bounded.
511
+ const keep = { id: cur.id, usage: cur.usage, anchor: cur.anchorRow && cur.anchorRow.text_len <= 4000 ? { ...cur.anchorRow } : null };
512
+ d.a.closed = [...(d.a.closed || []).filter((x) => x.id !== cur.id), keep].slice(-4);
513
+ d.a.cur = null;
514
+ },
515
+ openUnit(d, rec) {
516
+ const id = cRespId(rec);
517
+ const was = (d.a.closed || []).find((x) => x.id === id);
518
+ d.a.closed = (d.a.closed || []).filter((x) => x.id !== id);
519
+ d.a.cur = was
520
+ ? { id, usage: was.usage, anchorRow: was.anchor, first: null, reopened: true }
521
+ : { id, usage: null, anchorRow: null, first: { pos: rec.pos, uuid: rec.obj.uuid || `@${rec.pos}`, ts: null } };
522
+ },
523
+ endReplay(d) { if (d.a.cur) claudeDeriveAdapter.usageRowIfNeeded(d, d.a.cur); },
524
+
525
+ // A response that reported usage but produced no row still has its tokens: they go on a
526
+ // kind = 'usage' row keyed by the response's first record.
527
+ usageRowIfNeeded(d, cur) {
528
+ if (cur.anchorRow || !cur.usage || !cur.first || cUsageTotal(cur.usage) <= 0) return;
529
+ const rec = { pos: cur.first.pos, capturedAt: cur.first.capturedAt };
530
+ const row = d.turn(rec, {
531
+ message_id: cur.first.uuid, part: 0, role: 'assistant', kind: 'usage', text: '', ts: cur.first.ts,
532
+ response_id: cur.id, model: cur.first.model, effort: cur.first.effort, parent_id: cur.first.parent,
533
+ agent_id: cur.first.agent_id,
534
+ });
535
+ const next = d.revise(row, cur.usage, { pos: cur.lastUsagePos || cur.first.pos });
536
+ cur.anchorRow = next;
537
+ },
538
+
539
+ process(d, rec) {
540
+ const o = rec.obj;
541
+ const a = d.a;
542
+ if (typeof o.version === 'string' && o.version) { a.version = o.version; d.c.meta.version = o.version; }
543
+ if (typeof o.cwd === 'string' && o.cwd) {
544
+ a.folder = o.cwd;
545
+ if (!d.c.meta.folder) d.c.meta.folder = o.cwd;
546
+ }
547
+ if (typeof o.gitBranch === 'string' && o.gitBranch) d.c.meta.git_branch = o.gitBranch;
548
+ const effort = typeof o.effort === 'string' ? o.effort
549
+ : (o.message && typeof o.message.effort === 'string' ? o.message.effort : null);
550
+ if (effort !== null) a.effort = effort;
551
+ const agentId = d.file.agent_id || (o.isSidechain && typeof o.agentId === 'string' ? o.agentId : '');
552
+ const uuid = typeof o.uuid === 'string' && o.uuid ? o.uuid : `@${rec.pos}`;
553
+ const parent = typeof o.parentUuid === 'string' ? o.parentUuid : '';
554
+ const tsMs = parseTsC(o.timestamp);
555
+
556
+ // Session names: the user's own title wins over Claude Code's generated one (the latest of each).
557
+ if (o.type === 'custom-title' && typeof o.customTitle === 'string') { d.c.meta.title = o.customTitle; return; }
558
+ if (o.type === 'ai-title' && typeof o.aiTitle === 'string') { d.c.meta.ai_title = o.aiTitle; return; }
559
+ if (o.type === 'agent-name' && typeof o.agentName === 'string') { d.c.meta.agent_name = o.agentName; return; }
560
+ if (o.type === 'summary') {
561
+ d.turn(rec, { message_id: `@${rec.pos}`, part: 0, role: 'system', kind: 'summary', text: o.summary || '', ts: d.tsOf(rec, tsMs), agent_id: agentId });
562
+ return;
563
+ }
564
+ if (o.type === 'system') {
565
+ const inMsg = o.message && typeof o.message.content === 'string';
566
+ const text = inMsg ? o.message.content : (typeof o.content === 'string' ? o.content : '');
567
+ if (!text && !o.subtype) return;
568
+ d.turn(rec, {
569
+ message_id: uuid, part: 0, role: 'system', kind: inMsg ? 'text' : 'event', text, parent_id: parent,
570
+ ts: d.tsOf(rec, tsMs), agent_id: agentId, extra: o.subtype ? { subtype: String(o.subtype) } : {},
571
+ });
572
+ return;
573
+ }
574
+ if (o.type === 'user' && o.message) {
575
+ const ts = d.tsOf(rec, tsMs);
576
+ const content = o.message.content;
577
+ const blocks = typeof content === 'string' ? [{ type: 'text', text: content }] : (Array.isArray(content) ? content : []);
578
+ blocks.forEach((b, i) => {
579
+ if (!b || i > 255) return;
580
+ if (b.type === 'text') {
581
+ const extra = o.isMeta ? { meta: true } : (o.isCompactSummary ? { compact_summary: true } : {});
582
+ d.turn(rec, { message_id: uuid, part: i, role: 'user', kind: 'text', text: b.text || '', parent_id: parent, ts, agent_id: agentId, extra });
583
+ if (!o.isMeta && !o.isCompactSummary && cMeaningful(b.text)) {
584
+ if (!agentId && !d.c.meta.first_prompt) d.c.meta.first_prompt = String(b.text).slice(0, 2000);
585
+ if (agentId && !d.c.meta.agent_prompt) d.c.meta.agent_prompt = String(b.text).replace(/\s+/g, ' ').trim().slice(0, 160);
586
+ }
587
+ claudeDeriveAdapter.noteNotification(d, b.text);
588
+ } else if (b.type === 'tool_result') {
589
+ const text = cBlockText(b.content);
590
+ d.turn(rec, { message_id: uuid, part: i, role: 'tool', kind: 'tool_result', text, parent_id: parent, ts, agent_id: agentId, extra: { tool_use_id: String(b.tool_use_id || '') } });
591
+ d.result(rec, b.tool_use_id, { ts, is_error: b.is_error === true, result_len: text.length });
592
+ const info = a.agentCalls && a.agentCalls[b.tool_use_id];
593
+ if (info) {
594
+ const m = /agentId:\s*([0-9a-z][0-9a-z_-]{5,})/i.exec(text);
595
+ if (m) claudeDeriveAdapter.nameAgent(d, m[1], info);
596
+ }
597
+ }
598
+ });
599
+ return;
600
+ }
601
+ if (o.type === 'assistant' && o.message) {
602
+ const msg = o.message;
603
+ const ts = d.tsOf(rec, tsMs);
604
+ const model = typeof msg.model === 'string' ? msg.model : '';
605
+ const cur = a.cur || (a.cur = { id: cRespId(rec), usage: null, anchorRow: null, first: { pos: rec.pos, uuid, ts: null } });
606
+ if (cur.first && cur.first.pos === rec.pos) Object.assign(cur.first, { ts, model, effort: a.effort, parent, agent_id: agentId, capturedAt: rec.capturedAt });
607
+ const blocks = typeof msg.content === 'string' ? [{ type: 'text', text: msg.content }] : (Array.isArray(msg.content) ? msg.content : []);
608
+ const base = { message_id: uuid, parent_id: parent, response_id: cur.id, ts, model, effort: a.effort, agent_id: agentId };
609
+ let first = null;
610
+ blocks.forEach((b, i) => {
611
+ if (!b || i > 255) return;
612
+ let row = null;
613
+ if (b.type === 'text') row = d.turn(rec, { ...base, part: i, role: 'assistant', kind: 'text', text: b.text || '' });
614
+ else if (b.type === 'thinking') row = d.turn(rec, { ...base, part: i, role: 'assistant', kind: 'thinking', text: b.thinking || '' });
615
+ else if (b.type === 'redacted_thinking') row = d.turn(rec, { ...base, part: i, role: 'assistant', kind: 'thinking', text: '' });
616
+ else if (b.type === 'tool_use' || b.type === 'server_tool_use') {
617
+ const args = JSON.stringify(b.input === undefined ? {} : b.input);
618
+ const name = String(b.name || 'unknown');
619
+ row = d.turn(rec, { ...base, part: i, role: 'assistant', kind: 'tool_call', text: `${name}(${args})` });
620
+ const callId = b.id || `${uuid}:${i}`;
621
+ d.call(rec, { call_id: callId, message_id: uuid, ts, tool_name: name, args, model, agent_id: agentId });
622
+ if ((name === 'Agent' || name === 'Task') && b.input && typeof b.input === 'object') {
623
+ a.agentCalls = a.agentCalls || {};
624
+ a.agentCalls[callId] = { description: String(b.input.description || ''), type: String(b.input.subagent_type || '') };
625
+ const ids = Object.keys(a.agentCalls);
626
+ if (ids.length > 64) delete a.agentCalls[ids[0]];
627
+ }
628
+ } else if (typeof b.type === 'string' && b.type.endsWith('_tool_result')) {
629
+ row = d.turn(rec, { ...base, part: i, role: 'tool', kind: 'tool_result', text: cBlockText(b.content) });
630
+ }
631
+ if (row && !first) first = row;
632
+ });
633
+ // Tokens: once per message.id, on its first row; a larger later usage re-emits that row.
634
+ const usage = cUsage(msg.usage);
635
+ if (usage && cUsageTotal(usage) > cUsageTotal(cur.usage)) { cur.usage = usage; cur.lastUsagePos = rec.pos; }
636
+ if (!cur.anchorRow && first) cur.anchorRow = first;
637
+ if (cur.anchorRow && cur.usage) {
638
+ const r = cur.anchorRow;
639
+ const same = r.input_tokens === cur.usage.input_tokens && r.output_tokens === cur.usage.output_tokens
640
+ && r.cache_read_tokens === cur.usage.cache_read_tokens && r.cache_write_tokens === cur.usage.cache_write_tokens;
641
+ if (!same) cur.anchorRow = d.revise(r, cur.usage, rec);
642
+ }
643
+ }
644
+ },
645
+
646
+ // The parent names its sub-agents: an Agent/Task tool_use carries description and type, and its
647
+ // result (or a later task notification) carries the agent's id.
648
+ nameAgent(d, id, info) {
649
+ const names = d.c.meta.agent_names || (d.c.meta.agent_names = {});
650
+ if (!names[id]) names[id] = { description: info.description, type: info.type };
651
+ const ids = Object.keys(names);
652
+ if (ids.length > 512) delete names[ids[0]];
653
+ },
654
+ noteNotification(d, text) {
655
+ if (typeof text !== 'string' || !text.includes('<task-notification>')) return;
656
+ for (const block of text.split('<task-notification>').slice(1)) {
657
+ const t = /<task-id>([0-9a-z][0-9a-z_-]{5,})<\/task-id>/i.exec(block);
658
+ const u = /<tool-use-id>(toolu_[A-Za-z0-9]+)<\/tool-use-id>/.exec(block);
659
+ if (t && u && d.a.agentCalls && d.a.agentCalls[u[1]]) claudeDeriveAdapter.nameAgent(d, t[1], d.a.agentCalls[u[1]]);
660
+ }
661
+ },
662
+ };
663
+ function parseTsC(v) {
664
+ if (typeof v === 'number' && Number.isFinite(v)) return v < 1e11 ? v * 1000 : v;
665
+ return typeof v === 'string' && v ? Date.parse(v) : NaN;
666
+ }
667
+ // </derive> ===================================================================================
668
+
669
+ // Discovery under one source root: a Claude Code `projects/` directory (MEMHOUSE_SOURCE_ROOTS).
670
+ function captureSessions(root) {
671
+ const out = [];
672
+ let projects = [];
673
+ try { projects = fs.readdirSync(root.dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort(); } catch { return out; }
674
+ for (const proj of projects) {
675
+ const pdir = path.join(root.dir, proj);
676
+ let entries = [];
677
+ try { entries = fs.readdirSync(pdir, { withFileTypes: true }); } catch { continue; }
678
+ const ids = new Set();
679
+ for (const e of entries) {
680
+ if (e.isFile() && e.name.endsWith('.jsonl')) ids.add(e.name.slice(0, -6));
681
+ else if (e.isDirectory() && fs.existsSync(path.join(pdir, e.name, 'subagents'))) ids.add(e.name);
682
+ }
683
+ for (const id of [...ids].sort()) out.push({ harness: 'claude-code', session_id: id, root: root.name, dir: pdir, project_dir: proj });
684
+ }
685
+ return out;
686
+ }
687
+
688
+ // A session's source files: the main transcript, every sub-agent transcript, and each
689
+ // sub-agent's .meta.json (a side file: archived, never derived; its attribution is read lazily).
690
+ function captureSourceFiles(session) {
691
+ const files = [];
692
+ const main = path.join(session.dir, `${session.session_id}.jsonl`);
693
+ if (fs.existsSync(main)) files.push({ file_key: `${session.session_id}.jsonl`, path: main, agent_id: '', role: 'main' });
694
+ const sdir = path.join(session.dir, session.session_id);
695
+ for (const { file, workflow } of subagentFiles(path.join(sdir, 'subagents'))) {
696
+ const agentId = path.basename(file).replace(/^agent-/, '').replace(/\.jsonl$/, '');
697
+ const metaPath = file.replace(/\.jsonl$/, '.meta.json');
698
+ const hasMeta = fs.existsSync(metaPath);
699
+ files.push({ file_key: path.relative(sdir, file), path: file, agent_id: agentId, role: 'agent', workflow: workflow || '', meta_path: hasMeta ? metaPath : '' });
700
+ if (hasMeta) files.push({ file_key: path.relative(sdir, metaPath), path: metaPath, agent_id: agentId, role: 'side', workflow: workflow || '', meta_path: metaPath });
701
+ }
702
+ return files;
703
+ }
704
+
705
+ // What a sub-agent is, from its .meta.json: { agentType, description?, … }.
706
+ function captureAttribution(entry) {
707
+ if (!entry.meta_path) return { agent_type: entry.workflow ? 'workflow-subagent' : '', agent_desc: '' };
708
+ let meta = {};
709
+ try { meta = JSON.parse(fs.readFileSync(entry.meta_path, 'utf-8')) || {}; } catch { /* unreadable: no attribution */ }
710
+ const type = meta.agentType || meta.agent_type || meta.subagent_type || meta.type || (entry.workflow ? 'workflow-subagent' : '');
711
+ return { agent_type: String(type || ''), agent_desc: String(meta.description || '') };
712
+ }
713
+
714
+ const capture = {
715
+ harness: 'claude-code',
716
+ deriveVersion: CLAUDE_DERIVE_VERSION,
717
+ sessions: captureSessions,
718
+ sourceFiles: captureSourceFiles,
719
+ attribution: captureAttribution,
720
+ tsless: (o) => !o || typeof o.timestamp !== 'string', // dated by first sighting (§2.1)
721
+ derive: (input) => require('./base').capture.runDerive(claudeDeriveAdapter, input),
722
+ adapter: claudeDeriveAdapter,
723
+ };
724
+
557
725
  // discoverClaudeRoots is exported for the CLI's plugin installer, which needs the same
558
- // answer to a different question: where this pilot's Claude Code instances live. Two
726
+ // answer to a different question: where this user's Claude Code instances live. Two
559
727
  // implementations of "find every CLAUDE_CONFIG_DIR" would drift the first time a playbook
560
728
  // layout changes, and this is the copy exercised on every single ship.
561
- module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers, discoverClaudeRoots, subagentNames, subagentFiles };
729
+ module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers, discoverClaudeRoots, subagentNames, subagentFiles, capture };