hippo-memory 0.36.0 → 0.38.0

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 (205) hide show
  1. package/README.md +62 -254
  2. package/dist/api.d.ts +20 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +23 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/benchmarks/e1.3/incident-recall-eval.js +74 -0
  7. package/dist/benchmarks/e1.3/incident-recall-eval.js.map +1 -0
  8. package/dist/benchmarks/e1.3/scenarios.json +2587 -0
  9. package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +102 -0
  10. package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +1 -0
  11. package/dist/cli.js +449 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/connectors/slack/backfill.d.ts +42 -0
  14. package/dist/connectors/slack/backfill.d.ts.map +1 -0
  15. package/dist/connectors/slack/backfill.js +76 -0
  16. package/dist/connectors/slack/backfill.js.map +1 -0
  17. package/dist/connectors/slack/deletion.d.ts +14 -0
  18. package/dist/connectors/slack/deletion.d.ts.map +1 -0
  19. package/dist/connectors/slack/deletion.js +46 -0
  20. package/dist/connectors/slack/deletion.js.map +1 -0
  21. package/dist/connectors/slack/dlq.d.ts +21 -0
  22. package/dist/connectors/slack/dlq.d.ts.map +1 -0
  23. package/dist/connectors/slack/dlq.js +23 -0
  24. package/dist/connectors/slack/dlq.js.map +1 -0
  25. package/dist/connectors/slack/idempotency.d.ts +5 -0
  26. package/dist/connectors/slack/idempotency.d.ts.map +1 -0
  27. package/dist/connectors/slack/idempotency.js +13 -0
  28. package/dist/connectors/slack/idempotency.js.map +1 -0
  29. package/dist/connectors/slack/ingest.d.ts +27 -0
  30. package/dist/connectors/slack/ingest.d.ts.map +1 -0
  31. package/dist/connectors/slack/ingest.js +48 -0
  32. package/dist/connectors/slack/ingest.js.map +1 -0
  33. package/dist/connectors/slack/ratelimit.d.ts +9 -0
  34. package/dist/connectors/slack/ratelimit.d.ts.map +1 -0
  35. package/dist/connectors/slack/ratelimit.js +18 -0
  36. package/dist/connectors/slack/ratelimit.js.map +1 -0
  37. package/dist/connectors/slack/scope.d.ts +16 -0
  38. package/dist/connectors/slack/scope.d.ts.map +1 -0
  39. package/dist/connectors/slack/scope.js +13 -0
  40. package/dist/connectors/slack/scope.js.map +1 -0
  41. package/dist/connectors/slack/signature.d.ts +12 -0
  42. package/dist/connectors/slack/signature.d.ts.map +1 -0
  43. package/dist/connectors/slack/signature.js +20 -0
  44. package/dist/connectors/slack/signature.js.map +1 -0
  45. package/dist/connectors/slack/tenant-routing.d.ts +13 -0
  46. package/dist/connectors/slack/tenant-routing.d.ts.map +1 -0
  47. package/dist/connectors/slack/tenant-routing.js +17 -0
  48. package/dist/connectors/slack/tenant-routing.js.map +1 -0
  49. package/dist/connectors/slack/transform.d.ts +20 -0
  50. package/dist/connectors/slack/transform.d.ts.map +1 -0
  51. package/dist/connectors/slack/transform.js +31 -0
  52. package/dist/connectors/slack/transform.js.map +1 -0
  53. package/dist/connectors/slack/types.d.ts +35 -0
  54. package/dist/connectors/slack/types.d.ts.map +1 -0
  55. package/dist/connectors/slack/types.js +23 -0
  56. package/dist/connectors/slack/types.js.map +1 -0
  57. package/dist/connectors/slack/web-client.d.ts +12 -0
  58. package/dist/connectors/slack/web-client.d.ts.map +1 -0
  59. package/dist/connectors/slack/web-client.js +43 -0
  60. package/dist/connectors/slack/web-client.js.map +1 -0
  61. package/dist/db.d.ts.map +1 -1
  62. package/dist/db.js +105 -1
  63. package/dist/db.js.map +1 -1
  64. package/dist/goals.d.ts +73 -0
  65. package/dist/goals.d.ts.map +1 -0
  66. package/dist/goals.js +227 -0
  67. package/dist/goals.js.map +1 -0
  68. package/dist/importers.js +3 -3
  69. package/dist/importers.js.map +1 -1
  70. package/dist/mcp/server.js +1 -1
  71. package/dist/server.d.ts.map +1 -1
  72. package/dist/server.js +174 -2
  73. package/dist/server.js.map +1 -1
  74. package/dist/src/ambient.js +147 -0
  75. package/dist/src/ambient.js.map +1 -0
  76. package/dist/src/api.js +343 -0
  77. package/dist/src/api.js.map +1 -0
  78. package/dist/src/audit.js +152 -0
  79. package/dist/src/audit.js.map +1 -0
  80. package/dist/src/auth.js +65 -0
  81. package/dist/src/auth.js.map +1 -0
  82. package/dist/src/autolearn.js +143 -0
  83. package/dist/src/autolearn.js.map +1 -0
  84. package/dist/src/capture.js +512 -0
  85. package/dist/src/capture.js.map +1 -0
  86. package/dist/src/cli.js +5338 -0
  87. package/dist/src/cli.js.map +1 -0
  88. package/dist/src/client.js +181 -0
  89. package/dist/src/client.js.map +1 -0
  90. package/dist/src/config.js +108 -0
  91. package/dist/src/config.js.map +1 -0
  92. package/dist/src/connectors/slack/backfill.js +76 -0
  93. package/dist/src/connectors/slack/backfill.js.map +1 -0
  94. package/dist/src/connectors/slack/deletion.js +46 -0
  95. package/dist/src/connectors/slack/deletion.js.map +1 -0
  96. package/dist/src/connectors/slack/dlq.js +23 -0
  97. package/dist/src/connectors/slack/dlq.js.map +1 -0
  98. package/dist/src/connectors/slack/idempotency.js +13 -0
  99. package/dist/src/connectors/slack/idempotency.js.map +1 -0
  100. package/dist/src/connectors/slack/ingest.js +48 -0
  101. package/dist/src/connectors/slack/ingest.js.map +1 -0
  102. package/dist/src/connectors/slack/ratelimit.js +18 -0
  103. package/dist/src/connectors/slack/ratelimit.js.map +1 -0
  104. package/dist/src/connectors/slack/scope.js +13 -0
  105. package/dist/src/connectors/slack/scope.js.map +1 -0
  106. package/dist/src/connectors/slack/signature.js +20 -0
  107. package/dist/src/connectors/slack/signature.js.map +1 -0
  108. package/dist/src/connectors/slack/tenant-routing.js +17 -0
  109. package/dist/src/connectors/slack/tenant-routing.js.map +1 -0
  110. package/dist/src/connectors/slack/transform.js +31 -0
  111. package/dist/src/connectors/slack/transform.js.map +1 -0
  112. package/dist/src/connectors/slack/types.js +23 -0
  113. package/dist/src/connectors/slack/types.js.map +1 -0
  114. package/dist/src/connectors/slack/web-client.js +43 -0
  115. package/dist/src/connectors/slack/web-client.js.map +1 -0
  116. package/dist/src/consolidate.js +517 -0
  117. package/dist/src/consolidate.js.map +1 -0
  118. package/dist/src/dag.js +104 -0
  119. package/dist/src/dag.js.map +1 -0
  120. package/dist/src/dashboard.js +409 -0
  121. package/dist/src/dashboard.js.map +1 -0
  122. package/dist/src/db.js +643 -0
  123. package/dist/src/db.js.map +1 -0
  124. package/dist/src/embeddings.js +344 -0
  125. package/dist/src/embeddings.js.map +1 -0
  126. package/dist/src/eval-suite.js +289 -0
  127. package/dist/src/eval-suite.js.map +1 -0
  128. package/dist/src/eval.js +187 -0
  129. package/dist/src/eval.js.map +1 -0
  130. package/dist/src/extract.js +87 -0
  131. package/dist/src/extract.js.map +1 -0
  132. package/dist/src/goals.js +227 -0
  133. package/dist/src/goals.js.map +1 -0
  134. package/dist/src/handoff.js +30 -0
  135. package/dist/src/handoff.js.map +1 -0
  136. package/dist/src/hooks.js +582 -0
  137. package/dist/src/hooks.js.map +1 -0
  138. package/dist/src/importers.js +399 -0
  139. package/dist/src/importers.js.map +1 -0
  140. package/dist/src/index.js +25 -0
  141. package/dist/src/index.js.map +1 -0
  142. package/dist/src/invalidation.js +94 -0
  143. package/dist/src/invalidation.js.map +1 -0
  144. package/dist/src/mcp/framing.js +45 -0
  145. package/dist/src/mcp/framing.js.map +1 -0
  146. package/dist/src/mcp/server.js +510 -0
  147. package/dist/src/mcp/server.js.map +1 -0
  148. package/dist/src/memory.js +280 -0
  149. package/dist/src/memory.js.map +1 -0
  150. package/dist/src/multihop.js +32 -0
  151. package/dist/src/multihop.js.map +1 -0
  152. package/dist/src/path-context.js +32 -0
  153. package/dist/src/path-context.js.map +1 -0
  154. package/dist/src/physics-config.js +26 -0
  155. package/dist/src/physics-config.js.map +1 -0
  156. package/dist/src/physics-state.js +163 -0
  157. package/dist/src/physics-state.js.map +1 -0
  158. package/dist/src/physics.js +361 -0
  159. package/dist/src/physics.js.map +1 -0
  160. package/dist/src/postinstall.js +68 -0
  161. package/dist/src/postinstall.js.map +1 -0
  162. package/dist/src/raw-archive.js +72 -0
  163. package/dist/src/raw-archive.js.map +1 -0
  164. package/dist/src/refine-llm.js +147 -0
  165. package/dist/src/refine-llm.js.map +1 -0
  166. package/dist/src/replay.js +117 -0
  167. package/dist/src/replay.js.map +1 -0
  168. package/dist/src/salience.js +74 -0
  169. package/dist/src/salience.js.map +1 -0
  170. package/dist/src/scheduler.js +67 -0
  171. package/dist/src/scheduler.js.map +1 -0
  172. package/dist/src/scope.js +35 -0
  173. package/dist/src/scope.js.map +1 -0
  174. package/dist/src/search.js +801 -0
  175. package/dist/src/search.js.map +1 -0
  176. package/dist/src/server-detect.js +70 -0
  177. package/dist/src/server-detect.js.map +1 -0
  178. package/dist/src/server.js +784 -0
  179. package/dist/src/server.js.map +1 -0
  180. package/dist/src/shared.js +309 -0
  181. package/dist/src/shared.js.map +1 -0
  182. package/dist/src/sso.js +22 -0
  183. package/dist/src/sso.js.map +1 -0
  184. package/dist/src/store.js +1390 -0
  185. package/dist/src/store.js.map +1 -0
  186. package/dist/src/tenant.js +17 -0
  187. package/dist/src/tenant.js.map +1 -0
  188. package/dist/src/trace.js +64 -0
  189. package/dist/src/trace.js.map +1 -0
  190. package/dist/src/working-memory.js +149 -0
  191. package/dist/src/working-memory.js.map +1 -0
  192. package/dist/src/yaml.js +98 -0
  193. package/dist/src/yaml.js.map +1 -0
  194. package/dist/store.d.ts +9 -1
  195. package/dist/store.d.ts.map +1 -1
  196. package/dist/store.js +30 -2
  197. package/dist/store.js.map +1 -1
  198. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  199. package/extensions/openclaw-plugin/package.json +1 -1
  200. package/openclaw.plugin.json +1 -1
  201. package/package.json +2 -2
  202. package/dist/import.d.ts +0 -31
  203. package/dist/import.d.ts.map +0 -1
  204. package/dist/import.js +0 -307
  205. package/dist/import.js.map +0 -1
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Auto-learn from errors and git history.
3
+ * Agents learn from failures without explicit hippo remember calls.
4
+ */
5
+ import { execSync, execFileSync, spawn } from 'child_process';
6
+ import { createMemory, Layer } from './memory.js';
7
+ import { loadAllEntries } from './store.js';
8
+ import { textOverlap } from './search.js';
9
+ /**
10
+ * Create a MemoryEntry capturing a command failure.
11
+ * Content format: "Command '<cmd>' failed: <truncated stderr>"
12
+ */
13
+ export function captureError(exitCode, stderr, command) {
14
+ // Truncate to first 500 chars to avoid storing megabytes of build logs
15
+ const wasTruncated = stderr.length > 500;
16
+ const truncated = stderr.slice(0, 500).trim();
17
+ const suffix = wasTruncated ? ' [truncated]' : '';
18
+ // Strip leading env var assignments (KEY=val or key=val) before the actual command name
19
+ const safeCmd = command.replace(/^([A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, '').trim() || '(redacted)';
20
+ const content = `Command '${safeCmd}' failed (exit ${exitCode}): ${truncated}${suffix}`;
21
+ // Derive a sanitized tag from the command name (first word, strip path)
22
+ const cmdBase = safeCmd.split(/\s+/)[0].replace(/[^a-zA-Z0-9-]/g, '');
23
+ const tags = ['error', 'autolearn'];
24
+ if (cmdBase)
25
+ tags.push(cmdBase.toLowerCase().slice(0, 30));
26
+ return createMemory(content, {
27
+ layer: Layer.Episodic,
28
+ tags,
29
+ source: 'autolearn',
30
+ confidence: 'observed',
31
+ });
32
+ }
33
+ /**
34
+ * Parse git log output for actionable lessons.
35
+ * Looks for fix:, revert:, bug:, error:, hotfix: commit messages.
36
+ */
37
+ export function extractLessons(gitLog, customPatterns) {
38
+ const lessons = [];
39
+ const lines = gitLog.split('\n');
40
+ // Patterns that indicate a lesson to learn from
41
+ // Covers: fix, revert, bug, error, hotfix, refactor, perf, chore, breaking, deprecate
42
+ const prefixes = customPatterns?.join('|') ?? 'fix|revert|bug|error|hotfix|bugfix|refactor|perf|chore|breaking|deprecate';
43
+ const patterns = [
44
+ new RegExp(`^[a-f0-9]+\\s+(${prefixes})(\\(.+?\\))?:?\\s+(.+)`, 'i'),
45
+ new RegExp(`^(${prefixes})(\\(.+?\\))?:?\\s+(.+)`, 'i'),
46
+ /^(Fix|Revert|Bug|Hotfix|Bugfix|Refactor|Perf)\s+(.+)/,
47
+ /\b(fixed|reverted|corrected|resolved|refactored|optimized|deprecated)\b.{3,100}/i,
48
+ ];
49
+ for (const line of lines) {
50
+ const trimmed = line.trim();
51
+ if (!trimmed || trimmed.startsWith('commit ') || trimmed.startsWith('Author:') || trimmed.startsWith('Date:')) {
52
+ continue;
53
+ }
54
+ // Strip leading git hash if present (real hashes are hex, but be lenient with alphanumeric prefixes)
55
+ const subject = trimmed.replace(/^[a-z0-9]{6,40}\s+/i, '');
56
+ for (const pat of patterns) {
57
+ const m = subject.match(pat);
58
+ if (m) {
59
+ // For conventional commits: use group 3 (message after prefix), group 2, or full match
60
+ const lesson = (m[3] ?? m[2] ?? m[0]).trim();
61
+ if (lesson.length > 5 && lesson.length < 500) {
62
+ lessons.push(lesson);
63
+ }
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ // Deduplicate exact matches at extraction time
69
+ return [...new Set(lessons)];
70
+ }
71
+ /**
72
+ * Check if a substantially similar memory already exists.
73
+ * Returns true if overlap > threshold (default 0.7).
74
+ */
75
+ export function deduplicateLesson(hippoRootOrEntries, lesson, threshold = 0.7) {
76
+ const entries = typeof hippoRootOrEntries === 'string'
77
+ ? loadAllEntries(hippoRootOrEntries)
78
+ : hippoRootOrEntries;
79
+ for (const entry of entries) {
80
+ const overlap = textOverlap(lesson, entry.content);
81
+ if (overlap > threshold)
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ /**
87
+ * Run a command, streaming stdout/stderr to the terminal in real time.
88
+ * Returns: { exitCode, stderr }.
89
+ */
90
+ export function runWatched(command) {
91
+ return new Promise((resolve) => {
92
+ // Use shell: true so the command string is handled by the shell as-is
93
+ const child = spawn(command, { shell: true, stdio: ['inherit', 'inherit', 'pipe'] });
94
+ const stderrChunks = [];
95
+ child.stderr.on('data', (chunk) => {
96
+ stderrChunks.push(chunk);
97
+ // Also pass through to terminal
98
+ process.stderr.write(chunk);
99
+ });
100
+ child.on('close', (code) => {
101
+ resolve({
102
+ exitCode: code ?? 1,
103
+ stderr: Buffer.concat(stderrChunks).toString('utf8'),
104
+ });
105
+ });
106
+ child.on('error', (err) => {
107
+ resolve({ exitCode: 1, stderr: err.message });
108
+ });
109
+ });
110
+ }
111
+ /**
112
+ * Check whether a directory is a git work tree.
113
+ */
114
+ export function isGitRepo(cwd) {
115
+ try {
116
+ const raw = execSync('git rev-parse --is-inside-work-tree', {
117
+ encoding: 'utf8',
118
+ cwd,
119
+ timeout: 10000,
120
+ stdio: ['ignore', 'pipe', 'ignore'],
121
+ });
122
+ return raw.trim() === 'true';
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ /**
129
+ * Fetch recent git log lines (subject lines only).
130
+ * days: how many days of history to include.
131
+ */
132
+ export function fetchGitLog(cwd, days) {
133
+ try {
134
+ const raw = execFileSync('git', [
135
+ 'log', `--since=${days} days ago`, '--pretty=format:%s',
136
+ ], { encoding: 'utf8', cwd, timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
137
+ return typeof raw === 'string' ? raw : '';
138
+ }
139
+ catch {
140
+ return '';
141
+ }
142
+ }
143
+ //# sourceMappingURL=autolearn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autolearn.js","sourceRoot":"","sources":["../../src/autolearn.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,512 @@
1
+ /**
2
+ * Capture actionable items from conversation text.
3
+ *
4
+ * Uses heuristic pattern matching (no LLM) to extract:
5
+ * - Decisions ("we decided", "let's do", "going with")
6
+ * - Specs / requirements (bullet lists after spec/feature/plan headings)
7
+ * - Rules / constraints ("never", "always", "the rule is", "must")
8
+ * - Errors / gotchas ("error:", "bug:", "gotcha:", "watch out")
9
+ * - Preferences ("prefer", "use X instead of Y", "don't use")
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { createMemory, Layer } from './memory.js';
14
+ import { isContentWorthStoring } from './audit.js';
15
+ import { isInitialized, writeEntry, loadAllEntries, updateStats, } from './store.js';
16
+ import { getGlobalRoot, initGlobal } from './shared.js';
17
+ import { isEmbeddingAvailable, embedMemory } from './embeddings.js';
18
+ // Sentence-level patterns
19
+ const DECISION_PATTERNS = [
20
+ /(?:we(?:'ve| have)?|i(?:'ve| have)?|let's)\s+decid(?:ed|e)\s+(?:to\s+)?(.{10,200})/i,
21
+ /(?:let's|we(?:'ll| will| should)?)\s+(?:go with|do|use|try|build|implement|switch to)\s+(.{5,200})/i,
22
+ /(?:going|went)\s+with\s+(.{5,200})/i,
23
+ /(?:the plan is|plan:)\s+(.{10,200})/i,
24
+ /decision:\s*(.{10,200})/i,
25
+ ];
26
+ const RULE_PATTERNS = [
27
+ /(?:never|always|must(?:\s+not)?|do(?:n't| not)\s+ever)\s+(.{5,200})/i,
28
+ /(?:the rule is|rule:)\s*(.{5,200})/i,
29
+ /(?:important|critical|remember):\s*(.{10,200})/i,
30
+ /(?:make sure|ensure)\s+(?:to\s+)?(.{10,200})/i,
31
+ ];
32
+ const ERROR_PATTERNS = [
33
+ /(?:error|bug|gotcha|watch out|careful|warning|caveat|trap):\s*(.{10,200})/i,
34
+ /(?:this broke|this breaks|this will break|broke because)\s+(.{5,200})/i,
35
+ /(?:the (?:issue|problem|fix) (?:is|was))\s+(.{10,200})/i,
36
+ /(?:don't forget|easy to miss):\s*(.{5,200})/i,
37
+ ];
38
+ const PREFERENCE_PATTERNS = [
39
+ /(?:prefer|use)\s+(.{5,100})\s+(?:instead of|over|not)\s+(.{3,100})/i,
40
+ /(?:don't use|avoid|skip)\s+(.{5,200})/i,
41
+ /(?:we(?:'re| are)\s+using|the stack is|we use)\s+(.{5,200})/i,
42
+ ];
43
+ // Heading patterns that signal a following list of specs/requirements
44
+ const SPEC_HEADING_PATTERNS = [
45
+ /^#+\s*(?:features?|requirements?|specs?|specifications?|plan|design|architecture|interface|api|todo|tasks?|implementation|notes?)(?:\s|:|$)/i,
46
+ /^(?:features?|requirements?|specs?|specifications?|plan|design|tasks?|implementation)(?:\s*:|$)/i,
47
+ ];
48
+ // ---------------------------------------------------------------------------
49
+ // Extraction engine
50
+ // ---------------------------------------------------------------------------
51
+ function splitSentences(text) {
52
+ // Split on sentence boundaries, keeping reasonable chunks
53
+ return text
54
+ .split(/(?<=[.!?])\s+|\n/)
55
+ .map((s) => s.trim())
56
+ .filter((s) => s.length > 5);
57
+ }
58
+ function cleanExtract(raw) {
59
+ return raw
60
+ .replace(/^[:\s-]+/, '')
61
+ .replace(/[.!?,;:\s]+$/, '')
62
+ .trim();
63
+ }
64
+ function extractFromPatterns(sentence, patterns, category, tag) {
65
+ for (const pat of patterns) {
66
+ const match = sentence.match(pat);
67
+ if (match) {
68
+ // Use the captured group if available, otherwise the full match
69
+ const raw = match[1] ?? match[0];
70
+ const content = cleanExtract(raw);
71
+ if (content.length >= 8 && content.length <= 500) {
72
+ return { content, category, tags: [tag, 'captured'] };
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ /** Extract spec items from bullet lists that follow spec-like headings. */
79
+ function extractSpecSections(text) {
80
+ const items = [];
81
+ const lines = text.split('\n');
82
+ let inSpecSection = false;
83
+ for (const line of lines) {
84
+ const trimmed = line.trim();
85
+ // Check if this line is a spec heading
86
+ if (SPEC_HEADING_PATTERNS.some((p) => p.test(trimmed))) {
87
+ inSpecSection = true;
88
+ continue;
89
+ }
90
+ // Another heading resets the section
91
+ if (/^#+\s/.test(trimmed) || /^[A-Z][a-z]+:$/.test(trimmed)) {
92
+ inSpecSection = false;
93
+ continue;
94
+ }
95
+ // Blank line after non-bullet content ends section
96
+ if (!trimmed && inSpecSection) {
97
+ // Keep going, blank lines within spec sections are ok
98
+ continue;
99
+ }
100
+ if (inSpecSection) {
101
+ const bulletMatch = trimmed.match(/^[-*]\s+(.+)/) || trimmed.match(/^\d+\.\s+(.+)/);
102
+ if (bulletMatch) {
103
+ const content = bulletMatch[1].trim();
104
+ if (content.length >= 8 && content.length <= 500) {
105
+ items.push({
106
+ content,
107
+ category: 'spec',
108
+ tags: ['spec', 'captured'],
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return items;
115
+ }
116
+ /**
117
+ * Main extraction function. Scans text for actionable items using heuristics.
118
+ */
119
+ export function extractFromText(text) {
120
+ const items = [];
121
+ const seen = new Set();
122
+ const addIfNew = (item) => {
123
+ const norm = item.content.toLowerCase().replace(/\s+/g, ' ').trim();
124
+ if (seen.has(norm))
125
+ return;
126
+ if (!isContentWorthStoring(item.content))
127
+ return;
128
+ seen.add(norm);
129
+ items.push(item);
130
+ };
131
+ // 1. Extract spec sections (bullet lists under spec headings)
132
+ for (const item of extractSpecSections(text)) {
133
+ addIfNew(item);
134
+ }
135
+ // 2. Pattern-match on individual sentences
136
+ const sentences = splitSentences(text);
137
+ for (const sentence of sentences) {
138
+ // Try each category in priority order
139
+ const decision = extractFromPatterns(sentence, DECISION_PATTERNS, 'decision', 'decision');
140
+ if (decision) {
141
+ addIfNew(decision);
142
+ continue;
143
+ }
144
+ const rule = extractFromPatterns(sentence, RULE_PATTERNS, 'rule', 'rule');
145
+ if (rule) {
146
+ addIfNew(rule);
147
+ continue;
148
+ }
149
+ const error = extractFromPatterns(sentence, ERROR_PATTERNS, 'error', 'error');
150
+ if (error) {
151
+ addIfNew(error);
152
+ continue;
153
+ }
154
+ const preference = extractFromPatterns(sentence, PREFERENCE_PATTERNS, 'preference', 'preference');
155
+ if (preference) {
156
+ addIfNew(preference);
157
+ continue;
158
+ }
159
+ }
160
+ return items;
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Normalisation for deduplication (mirrors import.ts)
164
+ // ---------------------------------------------------------------------------
165
+ function normalise(text) {
166
+ return text
167
+ .toLowerCase()
168
+ .replace(/[^\w\s]/g, '')
169
+ .replace(/\s+/g, ' ')
170
+ .trim();
171
+ }
172
+ function isDuplicate(content, existing) {
173
+ const norm = normalise(content);
174
+ if (!norm)
175
+ return true;
176
+ for (const e of existing) {
177
+ if (normalise(e.content) === norm)
178
+ return true;
179
+ }
180
+ return false;
181
+ }
182
+ /**
183
+ * Build a compact text summary from a Claude Code / OpenCode JSONL transcript.
184
+ * Keeps plain user messages and the final chunk of assistant text, drops
185
+ * thinking blocks, tool_use, and tool_result noise. Output is fed to the
186
+ * existing `extractFromText` pipeline.
187
+ *
188
+ * Exported for tests.
189
+ */
190
+ export function summariseTranscript(jsonl) {
191
+ const lines = jsonl.split('\n').filter((l) => l.trim());
192
+ const userMessages = [];
193
+ const assistantTexts = [];
194
+ for (const line of lines) {
195
+ let entry;
196
+ try {
197
+ entry = JSON.parse(line);
198
+ }
199
+ catch {
200
+ continue;
201
+ }
202
+ if (!entry || typeof entry !== 'object')
203
+ continue;
204
+ const e = entry;
205
+ if (e.type === 'user' || e.type === 'assistant') {
206
+ const message = e.message;
207
+ if (!message)
208
+ continue;
209
+ const content = message.content;
210
+ if (e.type === 'user') {
211
+ // Plain text user messages only (skip tool_result arrays)
212
+ if (typeof content === 'string' && content.trim()) {
213
+ userMessages.push(content.trim());
214
+ }
215
+ }
216
+ else if (Array.isArray(content)) {
217
+ // Keep assistant text blocks; drop thinking + tool_use
218
+ const chunks = [];
219
+ for (const block of content) {
220
+ if (block && typeof block === 'object') {
221
+ const b = block;
222
+ if (b.type === 'text' && typeof b.text === 'string' && b.text.trim()) {
223
+ chunks.push(b.text.trim());
224
+ }
225
+ }
226
+ }
227
+ if (chunks.length > 0) {
228
+ assistantTexts.push(chunks.join('\n'));
229
+ }
230
+ }
231
+ continue;
232
+ }
233
+ // Codex rollout transcript shape: response_item -> payload.message
234
+ if (e.type === 'response_item') {
235
+ const payload = e.payload;
236
+ if (!payload || payload.type !== 'message')
237
+ continue;
238
+ const role = payload.role;
239
+ const content = payload.content;
240
+ if (!Array.isArray(content))
241
+ continue;
242
+ const chunks = [];
243
+ for (const block of content) {
244
+ if (!block || typeof block !== 'object')
245
+ continue;
246
+ const b = block;
247
+ if (role === 'user' && b.type === 'input_text' && typeof b.text === 'string' && b.text.trim()) {
248
+ chunks.push(b.text.trim());
249
+ }
250
+ if (role === 'assistant' && b.type === 'output_text' && typeof b.text === 'string' && b.text.trim()) {
251
+ chunks.push(b.text.trim());
252
+ }
253
+ }
254
+ if (chunks.length === 0)
255
+ continue;
256
+ if (role === 'user')
257
+ userMessages.push(chunks.join('\n'));
258
+ if (role === 'assistant')
259
+ assistantTexts.push(chunks.join('\n'));
260
+ }
261
+ }
262
+ if (userMessages.length === 0 && assistantTexts.length === 0)
263
+ return '';
264
+ // Keep the tail: last ~20 user turns and last ~10 assistant replies.
265
+ // Session-end is about what was decided near the end, not at the start.
266
+ const tailUsers = userMessages.slice(-20);
267
+ const tailAssistants = assistantTexts.slice(-10);
268
+ return [
269
+ '# Session Summary',
270
+ '',
271
+ '## User Messages',
272
+ ...tailUsers.map((m) => `- ${m.replace(/\s+/g, ' ').slice(0, 500)}`),
273
+ '',
274
+ '## Assistant Responses',
275
+ ...tailAssistants.map((t) => t.slice(0, 2000)),
276
+ ].join('\n');
277
+ }
278
+ /**
279
+ * Resolve a transcript path for `--last-session`.
280
+ *
281
+ * Priority:
282
+ * 1. Explicit `transcriptPath` option (from `--transcript <path>`)
283
+ * 2. Stdin JSON payload (Claude Code / OpenCode SessionEnd hook shape)
284
+ * 3. Most recent `.jsonl` under `~/.claude/projects/<any>/`
285
+ *
286
+ * Returns null when nothing resolves. Never throws.
287
+ */
288
+ export function resolveLastSessionTranscript(explicit, stdinText) {
289
+ if (explicit && fs.existsSync(explicit))
290
+ return explicit;
291
+ // Try parsing stdin as the SessionEnd JSON payload
292
+ if (stdinText && stdinText.trim().startsWith('{')) {
293
+ try {
294
+ const payload = JSON.parse(stdinText);
295
+ const tp = payload.transcript_path;
296
+ if (typeof tp === 'string' && fs.existsSync(tp))
297
+ return tp;
298
+ }
299
+ catch {
300
+ // not JSON - fall through
301
+ }
302
+ }
303
+ // Auto-discover the most recent transcript
304
+ const home = process.env.HOME || process.env.USERPROFILE;
305
+ if (!home)
306
+ return null;
307
+ const projectsDir = path.join(home, '.claude', 'projects');
308
+ if (!fs.existsSync(projectsDir))
309
+ return null;
310
+ let newest = null;
311
+ try {
312
+ for (const entry of fs.readdirSync(projectsDir)) {
313
+ const subDir = path.join(projectsDir, entry);
314
+ const stat = fs.statSync(subDir);
315
+ if (!stat.isDirectory())
316
+ continue;
317
+ for (const file of fs.readdirSync(subDir)) {
318
+ if (!file.endsWith('.jsonl'))
319
+ continue;
320
+ const full = path.join(subDir, file);
321
+ const m = fs.statSync(full).mtimeMs;
322
+ if (!newest || m > newest.mtime)
323
+ newest = { path: full, mtime: m };
324
+ }
325
+ }
326
+ }
327
+ catch {
328
+ return null;
329
+ }
330
+ return newest?.path ?? null;
331
+ }
332
+ export function cmdCapture(hippoRoot, options) {
333
+ // Tee stdout/stderr to a log file when --log-file is set. Used by the
334
+ // SessionEnd hook so output (otherwise swallowed by TUI teardown) surfaces
335
+ // on the next session start via `hippo last-sleep`. Runs second in the
336
+ // SessionEnd sequence after `hippo sleep`, so we APPEND rather than
337
+ // truncate — sleep already wrote its own header + body to this file.
338
+ const restoreStdio = options.logFile ? beginLogTee(options.logFile) : null;
339
+ try {
340
+ cmdCaptureCore(hippoRoot, options);
341
+ if (options.logFile)
342
+ console.log('[hippo] capture complete');
343
+ }
344
+ catch (err) {
345
+ if (options.logFile)
346
+ console.log(`[hippo] capture failed: ${err.message}`);
347
+ throw err;
348
+ }
349
+ finally {
350
+ if (restoreStdio)
351
+ restoreStdio();
352
+ }
353
+ }
354
+ /**
355
+ * Append-mode tee: writes a banner line then mirrors every stdout/stderr
356
+ * chunk to `logFile` until the returned restore function is called.
357
+ * Failures to write the log are non-fatal; the real streams still get
358
+ * the data.
359
+ */
360
+ function beginLogTee(logFile) {
361
+ try {
362
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
363
+ fs.appendFileSync(logFile, `[hippo] ${new Date().toISOString()} capturing session...\n`, 'utf8');
364
+ }
365
+ catch (err) {
366
+ console.error(`[hippo] warning: could not open log file ${logFile}: ${err.message}`);
367
+ return () => { };
368
+ }
369
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
370
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
371
+ const tee = (chunk) => {
372
+ try {
373
+ const buf = typeof chunk === 'string'
374
+ ? chunk
375
+ : Buffer.isBuffer(chunk)
376
+ ? chunk.toString('utf8')
377
+ : String(chunk);
378
+ fs.appendFileSync(logFile, buf, 'utf8');
379
+ }
380
+ catch {
381
+ // log failures are non-fatal
382
+ }
383
+ };
384
+ process.stdout.write = ((chunk, enc, cb) => {
385
+ tee(chunk);
386
+ return origStdoutWrite(chunk, enc, cb);
387
+ });
388
+ process.stderr.write = ((chunk, enc, cb) => {
389
+ tee(chunk);
390
+ return origStderrWrite(chunk, enc, cb);
391
+ });
392
+ return () => {
393
+ process.stdout.write = origStdoutWrite;
394
+ process.stderr.write = origStderrWrite;
395
+ };
396
+ }
397
+ function cmdCaptureCore(hippoRoot, options) {
398
+ const useGlobal = options.global;
399
+ const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
400
+ if (useGlobal) {
401
+ initGlobal();
402
+ }
403
+ else {
404
+ if (!isInitialized(hippoRoot)) {
405
+ console.error('No .hippo directory found. Run `hippo init` first.');
406
+ process.exit(1);
407
+ }
408
+ }
409
+ // Read input text
410
+ let text;
411
+ switch (options.source) {
412
+ case 'stdin': {
413
+ try {
414
+ text = fs.readFileSync(0, 'utf8');
415
+ }
416
+ catch {
417
+ console.error('No input on stdin. Pipe text in or use --file <path>.');
418
+ process.exit(1);
419
+ }
420
+ break;
421
+ }
422
+ case 'file': {
423
+ if (!options.filePath) {
424
+ console.error('Missing file path. Usage: hippo capture --file <path>');
425
+ process.exit(1);
426
+ }
427
+ if (!fs.existsSync(options.filePath)) {
428
+ console.error(`File not found: ${options.filePath}`);
429
+ process.exit(1);
430
+ }
431
+ text = fs.readFileSync(options.filePath, 'utf8');
432
+ break;
433
+ }
434
+ case 'last-session': {
435
+ // Try to read stdin non-blockingly: SessionEnd hooks pass a JSON payload,
436
+ // but manual / test invocations have no piped stdin. fs.readFileSync(0)
437
+ // will block waiting for input when run interactively, so:
438
+ // - skip entirely when caller passed an explicit --transcript path
439
+ // - skip when stdin is a TTY (interactive shell)
440
+ let stdinText;
441
+ if (!options.transcriptPath && !process.stdin.isTTY) {
442
+ try {
443
+ stdinText = fs.readFileSync(0, 'utf8');
444
+ }
445
+ catch {
446
+ stdinText = undefined;
447
+ }
448
+ }
449
+ const resolved = resolveLastSessionTranscript(options.transcriptPath, stdinText);
450
+ if (!resolved) {
451
+ console.log('No transcript found. Pass --transcript <path> or run from a SessionEnd hook.');
452
+ return;
453
+ }
454
+ const jsonl = fs.readFileSync(resolved, 'utf8');
455
+ text = summariseTranscript(jsonl);
456
+ if (!text) {
457
+ console.log('Transcript had no user/assistant messages to summarise.');
458
+ return;
459
+ }
460
+ break;
461
+ }
462
+ }
463
+ if (!text || text.trim().length === 0) {
464
+ console.log('No text to capture from.');
465
+ return;
466
+ }
467
+ // Extract items
468
+ const extracted = extractFromText(text);
469
+ if (extracted.length === 0) {
470
+ console.log('No actionable items found in the input.');
471
+ return;
472
+ }
473
+ // Load existing for dedup
474
+ const existing = loadAllEntries(targetRoot);
475
+ let captured = 0;
476
+ let skipped = 0;
477
+ for (const item of extracted) {
478
+ if (isDuplicate(item.content, existing)) {
479
+ skipped++;
480
+ if (options.dryRun) {
481
+ console.log(` [skip] (${item.category}) ${item.content.slice(0, 80)}`);
482
+ }
483
+ continue;
484
+ }
485
+ if (options.dryRun) {
486
+ console.log(` [capture] (${item.category}) ${item.content}`);
487
+ }
488
+ else {
489
+ // A3: kind defaults to 'distilled'. capture.ts extracts curated items from
490
+ // session output (not raw transcript chunks), so distilled is correct. If a
491
+ // future variant captures full raw session text, it MUST set kind: 'raw'
492
+ // and route deletions through archiveRawMemory(). See MEMORY_ENVELOPE.md.
493
+ const entry = createMemory(item.content, {
494
+ layer: Layer.Episodic,
495
+ tags: item.tags,
496
+ source: 'capture',
497
+ confidence: 'observed',
498
+ });
499
+ writeEntry(targetRoot, entry);
500
+ updateStats(targetRoot, { remembered: 1 });
501
+ existing.push(entry); // within-batch dedup
502
+ if (isEmbeddingAvailable()) {
503
+ embedMemory(targetRoot, entry).catch(() => { });
504
+ }
505
+ }
506
+ captured++;
507
+ }
508
+ const prefix = options.dryRun ? '[dry-run] ' : '';
509
+ const globalPrefix = useGlobal ? '[global] ' : '';
510
+ console.log(`\n${prefix}${globalPrefix}Captured ${captured} items (${skipped} skipped as duplicates)`);
511
+ }
512
+ //# sourceMappingURL=capture.js.map