@scitrera/memorylayer-opencode-plugin 0.1.22

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 (58) hide show
  1. package/README.md +178 -0
  2. package/dist/src/hooks/event.d.ts +15 -0
  3. package/dist/src/hooks/event.d.ts.map +1 -0
  4. package/dist/src/hooks/event.js +53 -0
  5. package/dist/src/hooks/event.js.map +1 -0
  6. package/dist/src/hooks/message.d.ts +20 -0
  7. package/dist/src/hooks/message.d.ts.map +1 -0
  8. package/dist/src/hooks/message.js +128 -0
  9. package/dist/src/hooks/message.js.map +1 -0
  10. package/dist/src/hooks/session.d.ts +20 -0
  11. package/dist/src/hooks/session.d.ts.map +1 -0
  12. package/dist/src/hooks/session.js +121 -0
  13. package/dist/src/hooks/session.js.map +1 -0
  14. package/dist/src/hooks/tool.d.ts +23 -0
  15. package/dist/src/hooks/tool.d.ts.map +1 -0
  16. package/dist/src/hooks/tool.js +168 -0
  17. package/dist/src/hooks/tool.js.map +1 -0
  18. package/dist/src/index.d.ts +31 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/index.js +176 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/shared/client.d.ts +27 -0
  23. package/dist/src/shared/client.d.ts.map +1 -0
  24. package/dist/src/shared/client.js +69 -0
  25. package/dist/src/shared/client.js.map +1 -0
  26. package/dist/src/shared/formatters.d.ts +33 -0
  27. package/dist/src/shared/formatters.d.ts.map +1 -0
  28. package/dist/src/shared/formatters.js +187 -0
  29. package/dist/src/shared/formatters.js.map +1 -0
  30. package/dist/src/shared/observation.d.ts +59 -0
  31. package/dist/src/shared/observation.d.ts.map +1 -0
  32. package/dist/src/shared/observation.js +406 -0
  33. package/dist/src/shared/observation.js.map +1 -0
  34. package/dist/src/shared/state.d.ts +66 -0
  35. package/dist/src/shared/state.d.ts.map +1 -0
  36. package/dist/src/shared/state.js +144 -0
  37. package/dist/src/shared/state.js.map +1 -0
  38. package/dist/src/shared/types.d.ts +141 -0
  39. package/dist/src/shared/types.d.ts.map +1 -0
  40. package/dist/src/shared/types.js +9 -0
  41. package/dist/src/shared/types.js.map +1 -0
  42. package/opencode.example.json +15 -0
  43. package/package.json +52 -0
  44. package/src/commands/recall.md +55 -0
  45. package/src/commands/remember.md +37 -0
  46. package/src/commands/setup.md +47 -0
  47. package/src/commands/status.md +59 -0
  48. package/src/hooks/event.ts +63 -0
  49. package/src/hooks/message.ts +147 -0
  50. package/src/hooks/session.ts +127 -0
  51. package/src/hooks/tool.ts +207 -0
  52. package/src/index.ts +197 -0
  53. package/src/shared/client.ts +73 -0
  54. package/src/shared/formatters.ts +237 -0
  55. package/src/shared/observation.ts +447 -0
  56. package/src/shared/state.ts +159 -0
  57. package/src/shared/types.ts +144 -0
  58. package/tsconfig.json +24 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Format API responses for injection into OpenCode's context.
3
+ *
4
+ * Uses the same types returned by MemoryLayerClient (the MCP adapter),
5
+ * so hooks and MCP tools share the same data contracts.
6
+ */
7
+ /**
8
+ * Format a single memory for display
9
+ */
10
+ function formatMemory(memory, index) {
11
+ const lines = [];
12
+ const typeStr = memory.subtype
13
+ ? `${memory.type}/${memory.subtype}`
14
+ : memory.type;
15
+ const relevanceStr = memory.relevance_score
16
+ ? ` (relevance: ${(memory.relevance_score * 100).toFixed(0)}%)`
17
+ : "";
18
+ lines.push(`${index + 1}. [${typeStr}]${relevanceStr}`);
19
+ lines.push(` ${memory.content}`);
20
+ const tags = memory.tags ?? [];
21
+ if (tags.length > 0) {
22
+ lines.push(` Tags: ${tags.join(", ")}`);
23
+ }
24
+ return lines.join("\n");
25
+ }
26
+ /**
27
+ * Format recall results for context injection
28
+ */
29
+ export function formatRecallResult(result, query) {
30
+ if (result.memories.length === 0) {
31
+ return `No memories found matching "${query}".`;
32
+ }
33
+ const totalCount = result.total_count ?? result.memories.length;
34
+ const lines = [
35
+ `Found ${totalCount} memories for "${query}" (showing ${result.memories.length}):`,
36
+ "",
37
+ ];
38
+ for (let i = 0; i < result.memories.length; i++) {
39
+ lines.push(formatMemory(result.memories[i], i));
40
+ if (i < result.memories.length - 1) {
41
+ lines.push("");
42
+ }
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+ /**
47
+ * Format briefing for context injection.
48
+ * Accepts the ToolResponse from MemoryLayerClient.getBriefing().
49
+ */
50
+ export function formatBriefing(briefing) {
51
+ if (!briefing) {
52
+ return "=== Workspace Briefing ===\n\nNo workspace data available.";
53
+ }
54
+ const totalMemories = briefing.total_memories ?? 0;
55
+ const activeTopics = briefing.active_topics ?? [];
56
+ const memoryTypes = briefing.memory_types ?? {};
57
+ const recentActivity = briefing.recent_activity ?? [];
58
+ const lines = [
59
+ "=== Workspace Briefing ===",
60
+ "",
61
+ `Total memories: ${totalMemories}`,
62
+ ];
63
+ if (activeTopics.length > 0) {
64
+ lines.push(`Active topics: ${activeTopics.join(", ")}`);
65
+ }
66
+ // Memory type breakdown
67
+ const types = Object.entries(memoryTypes);
68
+ if (types.length > 0) {
69
+ lines.push("");
70
+ lines.push("Memory types:");
71
+ for (const [type, count] of types) {
72
+ lines.push(` - ${type}: ${count}`);
73
+ }
74
+ }
75
+ // Recent activity
76
+ if (recentActivity.length > 0) {
77
+ lines.push("");
78
+ lines.push("Recent activity:");
79
+ for (const activity of recentActivity.slice(0, 3)) {
80
+ const date = new Date(activity.timestamp ?? new Date().toISOString()).toLocaleDateString();
81
+ lines.push(` - ${date}: ${activity.summary ?? ""} (${activity.memories_created ?? 0} memories)`);
82
+ }
83
+ }
84
+ return lines.join("\n");
85
+ }
86
+ /**
87
+ * Format directive memories specially (high importance user instructions)
88
+ */
89
+ export function formatDirectives(memories) {
90
+ const directives = memories.filter(m => m.subtype === "directive" || (m.subtype === "preference" && (m.importance ?? 0) >= 0.9));
91
+ if (directives.length === 0) {
92
+ return "";
93
+ }
94
+ const lines = [
95
+ "=== User Directives (must follow) ===",
96
+ "",
97
+ ];
98
+ for (const directive of directives) {
99
+ lines.push(`- ${directive.content}`);
100
+ }
101
+ return lines.join("\n");
102
+ }
103
+ /**
104
+ * Format sandbox state for context injection (post-compaction recovery)
105
+ */
106
+ export function formatSandboxState(inspectResult) {
107
+ const variableCount = inspectResult.variable_count ?? 0;
108
+ const variables = inspectResult.variables ?? {};
109
+ const lines = [
110
+ "=== Existing Sandbox State (server-side) ===",
111
+ "",
112
+ `The server-side sandbox has ${variableCount} variable(s) from a prior session or before context compaction.`,
113
+ "These variables are live and available for memory_context_exec, memory_context_query, and memory_context_rlm.",
114
+ "",
115
+ ];
116
+ for (const [name, info] of Object.entries(variables)) {
117
+ lines.push(` ${name} (${info.type}): ${info.preview}`);
118
+ }
119
+ lines.push("");
120
+ lines.push("Use `memory_context_inspect` for detailed variable inspection.");
121
+ return lines.join("\n");
122
+ }
123
+ /**
124
+ * Format combined session start output
125
+ */
126
+ export function formatSessionStart(briefing, directives, topicRecall, topic, sandboxState) {
127
+ const sections = [];
128
+ // Briefing first
129
+ if (briefing) {
130
+ sections.push(formatBriefing(briefing));
131
+ }
132
+ // Directives (high priority)
133
+ const directiveSection = formatDirectives(directives);
134
+ if (directiveSection) {
135
+ sections.push(directiveSection);
136
+ }
137
+ // Topic-specific recall
138
+ if (topicRecall && topicRecall.memories.length > 0 && topic) {
139
+ sections.push(formatRecallResult(topicRecall, topic));
140
+ }
141
+ // Existing sandbox state (post-compaction or resumed session)
142
+ if (sandboxState) {
143
+ sections.push(formatSandboxState(sandboxState));
144
+ }
145
+ // Add session guidance
146
+ const guidance = `=== Session Guidance ===
147
+
148
+ Recalling Memories:
149
+ - Before answering questions about preferences, setup, conventions, or past decisions, call \`memory_recall\` first
150
+ - For broad context gathering, use \`memory_recall\` with relevant keywords
151
+ - Directives (subtype: directive) and preferences (subtype: preference) represent explicit user instructions — prioritize these
152
+
153
+ Storing Memories:
154
+ - \`memory_remember\`: store to long-term memory with type, subtype, and importance
155
+ - \`memory_session_commit\`: checkpoint working memory mid-session (without ending it)
156
+
157
+ Importance Guide: directives/preferences -> 0.9 | decisions/architecture -> 0.7-0.8 | fixes/solutions -> 0.7 | patterns -> 0.5-0.6
158
+ Types: semantic (facts), procedural (how-to), episodic (events), working (current context, auto-expires)
159
+ Subtypes: directive, decision, fix, solution, code_pattern, error, workflow, preference, problem
160
+
161
+ Context Environment (sandbox survives compaction):
162
+ - Load + analyze memories server-side: \`memory_context_load\` -> \`memory_context_query\` or \`memory_context_rlm\`
163
+ - Run computations on loaded data: \`memory_context_exec\` (Python sandbox)
164
+ - After compaction, call \`memory_context_inspect\` to re-orient with existing sandbox variables`;
165
+ sections.push(guidance);
166
+ if (sections.length === 1) {
167
+ // Only guidance, no prior context
168
+ return "MemoryLayer: No prior context found for this session.\n\n" + guidance;
169
+ }
170
+ return sections.join("\n\n");
171
+ }
172
+ /**
173
+ * Format guidance for storing memories after tool use
174
+ */
175
+ export function formatStorageGuidance(toolName, isSignificant) {
176
+ if (!isSignificant) {
177
+ return "";
178
+ }
179
+ const guidance = {
180
+ task: "Consider storing exploration/research findings with `memory_remember` (subtype: decision/problem/entity).",
181
+ bash: "If this was a significant git commit, test result, or build output, store with `memory_remember` (subtype: workflow).",
182
+ edit: "If this edit completes a milestone, store with `memory_remember` (type: working, tags: [active-file]).",
183
+ write: "If this file represents a significant deliverable, store with `memory_remember` (type: working, tags: [active-file]).",
184
+ };
185
+ return guidance[toolName] || "";
186
+ }
187
+ //# sourceMappingURL=formatters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatters.js","sourceRoot":"","sources":["../../../src/shared/formatters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Observation extraction for auto-capture hooks.
3
+ * Extracts structured observation data from tool usage.
4
+ *
5
+ * Adapted from the CC plugin with OpenCode tool name mappings.
6
+ * OpenCode uses lowercase tool names: bash, edit, write, read, glob, grep,
7
+ * task, webfetch, websearch, multiedit, ls, apply_patch, codesearch, etc.
8
+ */
9
+ export declare function shouldSkipTool(toolName: string): boolean;
10
+ export type ObservationType = "read" | "write" | "execute" | "search" | "other";
11
+ export interface ObservationData {
12
+ type: ObservationType;
13
+ title: string;
14
+ toolName: string;
15
+ filesRead: string[];
16
+ filesModified: string[];
17
+ facts: string[];
18
+ concepts: string[];
19
+ intent: string | null;
20
+ contentHash: string;
21
+ summary: string;
22
+ }
23
+ /**
24
+ * Classify tool as read/write/execute/search/other.
25
+ * Uses OpenCode's lowercase tool names.
26
+ */
27
+ export declare function getObservationType(toolName: string): ObservationType;
28
+ /**
29
+ * Extract file paths from tool args, classified as read or modified
30
+ */
31
+ export declare function extractFilePaths(toolName: string, toolArgs: Record<string, unknown>): {
32
+ filesRead: string[];
33
+ filesModified: string[];
34
+ };
35
+ /**
36
+ * Generate observation title from tool usage
37
+ */
38
+ export declare function generateTitle(toolName: string, toolArgs: Record<string, unknown>): string;
39
+ /**
40
+ * Extract facts from tool args/output
41
+ */
42
+ export declare function extractFacts(toolName: string, toolArgs: Record<string, unknown>, toolOutput?: string): string[];
43
+ /**
44
+ * Extract concepts/topics from tool usage
45
+ */
46
+ export declare function extractConcepts(toolName: string, toolArgs: Record<string, unknown>): string[];
47
+ /**
48
+ * Detect intent from tool usage and user prompt
49
+ */
50
+ export declare function detectIntent(toolName: string, toolArgs: Record<string, unknown>, prompt?: string): string | null;
51
+ /**
52
+ * Compute content hash for deduplication
53
+ */
54
+ export declare function computeContentHash(...parts: string[]): string;
55
+ /**
56
+ * Build observation from tool execution data
57
+ */
58
+ export declare function buildObservation(toolName: string, toolArgs: Record<string, unknown>, toolOutput?: string, currentPrompt?: string): ObservationData | null;
59
+ //# sourceMappingURL=observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../../../src/shared/observation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAoBH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGxD;AAGD,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEhF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,eAAe,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAWpE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAgClD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAiCzF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,EAAE,CAsDV;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAwF7F;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CA0Cf;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAK7D;AAUD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,aAAa,CAAC,EAAE,MAAM,GACrB,eAAe,GAAG,IAAI,CAiCxB"}
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Observation extraction for auto-capture hooks.
3
+ * Extracts structured observation data from tool usage.
4
+ *
5
+ * Adapted from the CC plugin with OpenCode tool name mappings.
6
+ * OpenCode uses lowercase tool names: bash, edit, write, read, glob, grep,
7
+ * task, webfetch, websearch, multiedit, ls, apply_patch, codesearch, etc.
8
+ */
9
+ import { createHash } from "crypto";
10
+ // Tools to skip (internal/noisy/self-referential)
11
+ const SKIP_TOOLS = new Set([
12
+ "todo",
13
+ "question",
14
+ "plan",
15
+ "skill",
16
+ "batch",
17
+ ]);
18
+ // Also skip any tool matching these prefixes (memory tools)
19
+ const SKIP_PREFIXES = [
20
+ "memorylayer",
21
+ "memory_",
22
+ "mcp__memorylayer",
23
+ ];
24
+ export function shouldSkipTool(toolName) {
25
+ if (SKIP_TOOLS.has(toolName))
26
+ return true;
27
+ return SKIP_PREFIXES.some(prefix => toolName.startsWith(prefix));
28
+ }
29
+ /**
30
+ * Classify tool as read/write/execute/search/other.
31
+ * Uses OpenCode's lowercase tool names.
32
+ */
33
+ export function getObservationType(toolName) {
34
+ const readTools = ["read", "glob", "grep", "ls", "codesearch"];
35
+ const writeTools = ["write", "edit", "multiedit", "apply_patch"];
36
+ const executeTools = ["bash", "task"];
37
+ const searchTools = ["websearch", "webfetch"];
38
+ if (readTools.includes(toolName))
39
+ return "read";
40
+ if (writeTools.includes(toolName))
41
+ return "write";
42
+ if (executeTools.includes(toolName))
43
+ return "execute";
44
+ if (searchTools.includes(toolName))
45
+ return "search";
46
+ return "other";
47
+ }
48
+ /**
49
+ * Extract file paths from tool args, classified as read or modified
50
+ */
51
+ export function extractFilePaths(toolName, toolArgs) {
52
+ const filesRead = [];
53
+ const filesModified = [];
54
+ try {
55
+ const filePath = (toolArgs.file_path || toolArgs.path || toolArgs.file || "");
56
+ if (!filePath) {
57
+ // Try to extract paths from bash commands
58
+ if (toolName === "bash") {
59
+ const command = (toolArgs.command || "");
60
+ const pathMatches = command.match(/(?:^|\s)([^\s]+\.(ts|js|py|json|yaml|yml|md|txt|go|rs|java|c|cpp|h))\b/gi);
61
+ if (pathMatches) {
62
+ filesRead.push(...pathMatches.map((p) => p.trim()));
63
+ }
64
+ }
65
+ return { filesRead, filesModified };
66
+ }
67
+ const type = getObservationType(toolName);
68
+ if (type === "write") {
69
+ filesModified.push(filePath);
70
+ }
71
+ else if (type === "read") {
72
+ filesRead.push(filePath);
73
+ }
74
+ }
75
+ catch {
76
+ // Ignore parse errors
77
+ }
78
+ return { filesRead, filesModified };
79
+ }
80
+ /**
81
+ * Generate observation title from tool usage
82
+ */
83
+ export function generateTitle(toolName, toolArgs) {
84
+ try {
85
+ switch (toolName) {
86
+ case "read":
87
+ return `Read ${toolArgs.file_path || toolArgs.path || "file"}`;
88
+ case "write":
89
+ return `Write ${toolArgs.file_path || toolArgs.path || "file"}`;
90
+ case "edit":
91
+ case "multiedit":
92
+ return `Edit ${toolArgs.file_path || toolArgs.path || "file"}`;
93
+ case "apply_patch":
94
+ return `Patch ${toolArgs.file_path || toolArgs.path || "file"}`;
95
+ case "bash": {
96
+ const cmd = (toolArgs.command || "");
97
+ return `Run: ${cmd.substring(0, 50)}${cmd.length > 50 ? "..." : ""}`;
98
+ }
99
+ case "glob":
100
+ return `Find ${toolArgs.pattern || "files"}`;
101
+ case "grep":
102
+ case "codesearch":
103
+ return `Search "${toolArgs.pattern || ""}"`;
104
+ case "task":
105
+ return `Task: ${toolArgs.description || "agent"}`;
106
+ case "websearch":
107
+ return `Search: ${toolArgs.query || ""}`;
108
+ case "webfetch":
109
+ return `Fetch: ${toolArgs.url || ""}`;
110
+ default:
111
+ return toolName;
112
+ }
113
+ }
114
+ catch {
115
+ return toolName;
116
+ }
117
+ }
118
+ /**
119
+ * Extract facts from tool args/output
120
+ */
121
+ export function extractFacts(toolName, toolArgs, toolOutput) {
122
+ const facts = [];
123
+ try {
124
+ const filePath = (toolArgs.file_path || toolArgs.path || toolArgs.file || "");
125
+ switch (toolName) {
126
+ case "read":
127
+ if (filePath)
128
+ facts.push(`File read: ${filePath}`);
129
+ break;
130
+ case "write":
131
+ if (filePath)
132
+ facts.push(`File created/updated: ${filePath}`);
133
+ break;
134
+ case "edit":
135
+ case "multiedit":
136
+ case "apply_patch":
137
+ if (filePath)
138
+ facts.push(`File modified: ${filePath}`);
139
+ if (toolArgs.old_string) {
140
+ facts.push(`Code replaced in ${filePath.split(/[/\\]/).pop() || "file"}`);
141
+ }
142
+ break;
143
+ case "bash": {
144
+ const cmd = (toolArgs.command || "");
145
+ facts.push(`Command executed: ${cmd.substring(0, 100)}`);
146
+ if (toolOutput) {
147
+ if (toolOutput.includes("passed") || toolOutput.includes("\u2713"))
148
+ facts.push("Tests passed");
149
+ if (toolOutput.includes("failed") || toolOutput.includes("\u2717"))
150
+ facts.push("Tests failed");
151
+ if (toolOutput.includes("error") || toolOutput.includes("Error"))
152
+ facts.push("Errors encountered");
153
+ }
154
+ break;
155
+ }
156
+ case "glob":
157
+ if (toolArgs.pattern)
158
+ facts.push(`Pattern searched: ${toolArgs.pattern}`);
159
+ break;
160
+ case "grep":
161
+ case "codesearch":
162
+ if (toolArgs.pattern)
163
+ facts.push(`Code pattern searched: ${toolArgs.pattern}`);
164
+ if (toolArgs.path)
165
+ facts.push(`Search scope: ${toolArgs.path}`);
166
+ break;
167
+ case "websearch":
168
+ if (toolArgs.query)
169
+ facts.push(`Web search: ${toolArgs.query}`);
170
+ break;
171
+ case "webfetch":
172
+ if (toolArgs.url)
173
+ facts.push(`URL fetched: ${toolArgs.url}`);
174
+ break;
175
+ case "task":
176
+ if (toolArgs.description)
177
+ facts.push(`Sub-task: ${toolArgs.description}`);
178
+ break;
179
+ }
180
+ }
181
+ catch {
182
+ // Ignore parse errors
183
+ }
184
+ return facts;
185
+ }
186
+ /**
187
+ * Extract concepts/topics from tool usage
188
+ */
189
+ export function extractConcepts(toolName, toolArgs) {
190
+ const concepts = new Set();
191
+ try {
192
+ const filePath = (toolArgs.file_path || toolArgs.path || toolArgs.file || "");
193
+ // Extract concepts from file paths
194
+ if (filePath) {
195
+ const parts = filePath.split(/[/\\]/);
196
+ for (const part of parts) {
197
+ if (["src", "lib", "dist", "node_modules", ".", ".."].includes(part))
198
+ continue;
199
+ if (part.includes(".")) {
200
+ const ext = part.split(".").pop();
201
+ const extMap = {
202
+ ts: "typescript", tsx: "react", js: "javascript", jsx: "react",
203
+ py: "python", rs: "rust", go: "golang", css: "styling", scss: "styling",
204
+ html: "html", json: "configuration", yaml: "configuration", yml: "configuration",
205
+ md: "documentation", test: "testing", spec: "testing", sql: "database",
206
+ };
207
+ if (ext && extMap[ext])
208
+ concepts.add(extMap[ext]);
209
+ }
210
+ const dirMap = {
211
+ tests: "testing", __tests__: "testing", test: "testing", spec: "testing",
212
+ hooks: "hooks", api: "api", auth: "authentication", db: "database",
213
+ components: "components", pages: "pages", routes: "routing", utils: "utilities",
214
+ services: "services", middleware: "middleware", models: "models", types: "types",
215
+ cli: "cli", config: "configuration", migrations: "database", schemas: "schemas",
216
+ };
217
+ if (dirMap[part])
218
+ concepts.add(dirMap[part]);
219
+ }
220
+ }
221
+ // Extract function/class names from edit tools
222
+ if (toolName === "edit" || toolName === "multiedit") {
223
+ const oldStr = (toolArgs.old_string || "");
224
+ const newStr = (toolArgs.new_string || "");
225
+ const combined = oldStr + "\n" + newStr;
226
+ const funcMatches = combined.match(/(?:function|async function|const|let|var)\s+(\w{3,})/g);
227
+ if (funcMatches) {
228
+ for (const m of funcMatches.slice(0, 3)) {
229
+ const name = m.replace(/(?:function|async function|const|let|var)\s+/, "");
230
+ concepts.add(`fn:${name}`);
231
+ }
232
+ }
233
+ const classMatches = combined.match(/class\s+(\w{3,})/g);
234
+ if (classMatches) {
235
+ for (const m of classMatches.slice(0, 2)) {
236
+ concepts.add(`class:${m.replace("class ", "")}`);
237
+ }
238
+ }
239
+ if (/\bimport\b/.test(combined))
240
+ concepts.add("pattern:import");
241
+ if (/\bexport\b/.test(combined))
242
+ concepts.add("pattern:export");
243
+ if (/\binterface\b/.test(combined))
244
+ concepts.add("pattern:interface");
245
+ if (/\benum\b/.test(combined))
246
+ concepts.add("pattern:enum");
247
+ if (/\btry\s*\{/.test(combined))
248
+ concepts.add("pattern:error-handling");
249
+ if (/\basync\b/.test(combined))
250
+ concepts.add("pattern:async");
251
+ }
252
+ // Tool-based concepts
253
+ switch (toolName) {
254
+ case "bash": {
255
+ const cmd = (toolArgs.command || "");
256
+ if (cmd.includes("test") || cmd.includes("vitest") || cmd.includes("jest"))
257
+ concepts.add("testing");
258
+ if (cmd.includes("build") || cmd.includes("tsc"))
259
+ concepts.add("build");
260
+ if (cmd.includes("git"))
261
+ concepts.add("version-control");
262
+ if (cmd.includes("npm") || cmd.includes("yarn") || cmd.includes("pnpm") || cmd.includes("bun"))
263
+ concepts.add("package-management");
264
+ if (cmd.includes("docker"))
265
+ concepts.add("containerization");
266
+ if (cmd.includes("lint") || cmd.includes("eslint") || cmd.includes("biome"))
267
+ concepts.add("linting");
268
+ break;
269
+ }
270
+ case "websearch":
271
+ concepts.add("research");
272
+ break;
273
+ case "webfetch":
274
+ concepts.add("web-content");
275
+ break;
276
+ case "task":
277
+ concepts.add("delegation");
278
+ break;
279
+ }
280
+ }
281
+ catch {
282
+ // Ignore parse errors
283
+ }
284
+ return Array.from(concepts);
285
+ }
286
+ /**
287
+ * Detect intent from tool usage and user prompt
288
+ */
289
+ export function detectIntent(toolName, toolArgs, prompt) {
290
+ const intentPatterns = {
291
+ bugfix: /fix|bug|error|issue|broken|crash|repair/i,
292
+ feature: /add|feature|implement|create|new|build/i,
293
+ refactor: /refactor|clean|rename|reorganize|restructure/i,
294
+ testing: /test|spec|coverage|verify/i,
295
+ investigation: /find|search|investigate|debug|analyze|explore/i,
296
+ documentation: /document|comment|readme|doc|explain/i,
297
+ };
298
+ if (prompt) {
299
+ for (const [intent, pattern] of Object.entries(intentPatterns)) {
300
+ if (pattern.test(prompt)) {
301
+ return intent;
302
+ }
303
+ }
304
+ }
305
+ try {
306
+ const inputStr = JSON.stringify(toolArgs);
307
+ for (const [intent, pattern] of Object.entries(intentPatterns)) {
308
+ if (pattern.test(inputStr)) {
309
+ return intent;
310
+ }
311
+ }
312
+ }
313
+ catch {
314
+ // Ignore parse errors
315
+ }
316
+ switch (toolName) {
317
+ case "read":
318
+ case "glob":
319
+ case "grep":
320
+ case "codesearch":
321
+ return "investigation";
322
+ case "write":
323
+ return "feature";
324
+ case "edit":
325
+ return null; // Too ambiguous without context
326
+ default:
327
+ return null;
328
+ }
329
+ }
330
+ /**
331
+ * Compute content hash for deduplication
332
+ */
333
+ export function computeContentHash(...parts) {
334
+ return createHash("sha256")
335
+ .update(parts.join("|"))
336
+ .digest("hex")
337
+ .slice(0, 16);
338
+ }
339
+ /**
340
+ * Truncate string to max length
341
+ */
342
+ function truncate(str, maxLen = 500) {
343
+ if (str.length <= maxLen)
344
+ return str;
345
+ return str.substring(0, maxLen) + "...";
346
+ }
347
+ /**
348
+ * Build observation from tool execution data
349
+ */
350
+ export function buildObservation(toolName, toolArgs, toolOutput, currentPrompt) {
351
+ const type = getObservationType(toolName);
352
+ const title = generateTitle(toolName, toolArgs);
353
+ const { filesRead, filesModified } = extractFilePaths(toolName, toolArgs);
354
+ const facts = extractFacts(toolName, toolArgs, toolOutput);
355
+ const concepts = extractConcepts(toolName, toolArgs);
356
+ const intent = detectIntent(toolName, toolArgs, currentPrompt);
357
+ const summary = buildSummary(toolName, toolArgs, toolOutput, filesRead, filesModified);
358
+ const contentHash = computeContentHash(toolName, JSON.stringify(toolArgs), summary);
359
+ // Skip if empty observation (no meaningful data extracted)
360
+ if (filesRead.length === 0 && filesModified.length === 0 && facts.length === 0 && concepts.length === 0) {
361
+ return null;
362
+ }
363
+ return {
364
+ type,
365
+ title,
366
+ toolName,
367
+ filesRead,
368
+ filesModified,
369
+ facts,
370
+ concepts,
371
+ intent,
372
+ contentHash,
373
+ summary,
374
+ };
375
+ }
376
+ /**
377
+ * Build human-readable summary
378
+ */
379
+ function buildSummary(toolName, toolArgs, _toolOutput, filesRead, filesModified) {
380
+ const parts = [];
381
+ if (filesRead.length > 0) {
382
+ parts.push(`Read: ${filesRead.join(", ")}`);
383
+ }
384
+ if (filesModified.length > 0) {
385
+ parts.push(`Modified: ${filesModified.join(", ")}`);
386
+ }
387
+ try {
388
+ if (toolName === "bash") {
389
+ const cmd = (toolArgs.command || "");
390
+ parts.push(`Command: ${truncate(cmd, 100)}`);
391
+ }
392
+ else if (toolName === "grep" || toolName === "codesearch") {
393
+ const pattern = (toolArgs.pattern || "");
394
+ parts.push(`Pattern: ${pattern}`);
395
+ }
396
+ else if (toolName === "task") {
397
+ const desc = (toolArgs.description || "");
398
+ parts.push(`Task: ${truncate(desc, 100)}`);
399
+ }
400
+ }
401
+ catch {
402
+ // Ignore
403
+ }
404
+ return parts.length > 0 ? parts.join("; ") : `Used ${toolName}`;
405
+ }
406
+ //# sourceMappingURL=observation.js.map