u-foo 2.2.4 → 2.3.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 (57) hide show
  1. package/SKILLS/ufoo/SKILL.md +56 -12
  2. package/SKILLS/uinit/SKILL.md +3 -2
  3. package/modules/AGENTS.template.md +2 -1
  4. package/modules/bus/README.md +1 -1
  5. package/modules/context/SKILLS/uctx/SKILL.md +6 -4
  6. package/package.json +1 -1
  7. package/src/agent/codexThreadProvider.js +2 -2
  8. package/src/agent/controllerToolExecutor.js +24 -1
  9. package/src/agent/credentials/claude.js +85 -16
  10. package/src/agent/credentials/codex.js +251 -23
  11. package/src/agent/defaultBootstrap.js +3 -1
  12. package/src/agent/directAuthStatus.js +264 -0
  13. package/src/agent/internalRunner.js +18 -12
  14. package/src/agent/loopObservability.js +10 -0
  15. package/src/agent/loopRuntime.js +19 -0
  16. package/src/agent/ufooAgent.js +43 -13
  17. package/src/agent/upstreamTransport.js +23 -8
  18. package/src/bus/index.js +6 -1
  19. package/src/bus/message.js +156 -8
  20. package/src/chat/commandExecutor.js +187 -7
  21. package/src/chat/commands.js +23 -4
  22. package/src/chat/completionController.js +30 -7
  23. package/src/chat/index.js +3 -5
  24. package/src/cli/groupCoreCommands.js +5 -0
  25. package/src/cli.js +309 -0
  26. package/src/code/UCODE_PROMPT.md +3 -2
  27. package/src/code/prompts/ufoo.js +3 -2
  28. package/src/config.js +16 -3
  29. package/src/context/doctor.js +1 -1
  30. package/src/daemon/groupOrchestrator.js +13 -9
  31. package/src/daemon/promptRequest.js +11 -2
  32. package/src/daemon/soloBootstrap.js +2 -0
  33. package/src/group/bootstrap.js +1 -1
  34. package/src/group/promptProfiles.js +106 -22
  35. package/src/group/templates.js +1 -0
  36. package/src/init/index.js +4 -0
  37. package/src/memory/historySearch.js +308 -0
  38. package/src/memory/index.js +653 -8
  39. package/src/providerapi/redactor.js +4 -1
  40. package/src/status/index.js +24 -1
  41. package/src/tools/handlers/memory.js +168 -0
  42. package/src/tools/index.js +12 -0
  43. package/src/tools/registry.js +12 -0
  44. package/src/tools/schemaFixtures.js +213 -0
  45. package/src/tools/tier1/editMemory.js +14 -0
  46. package/src/tools/tier1/forget.js +14 -0
  47. package/src/tools/tier1/recall.js +14 -0
  48. package/src/tools/tier1/remember.js +14 -0
  49. package/src/tools/tier1/searchHistory.js +14 -0
  50. package/src/tools/tier1/searchMemory.js +14 -0
  51. package/templates/groups/build-lane.json +44 -6
  52. package/templates/groups/build-ultra.json +6 -5
  53. package/templates/groups/design-system.json +84 -0
  54. package/templates/groups/product-discovery.json +9 -4
  55. package/templates/groups/ui-plan-review.json +84 -0
  56. package/templates/groups/ui-polish.json +6 -2
  57. package/templates/groups/verify-ship.json +9 -4
@@ -42,8 +42,8 @@ const BUILTIN_PROFILES = [
42
42
  "- Prefer one narrow, testable wedge over broad speculative scope.",
43
43
  "",
44
44
  "Handoff:",
45
- "- Send the architect a scoped brief.",
46
- "- Send the scope challenger any assumptions that feel inflated or weak.",
45
+ "- Send the scoped brief and weak assumptions to the downstream/report_to nicknames listed in Runtime metadata.",
46
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
47
47
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
48
48
  ].join("\n"),
49
49
  },
@@ -71,7 +71,8 @@ const BUILTIN_PROFILES = [
71
71
  "- If recommending expansion, define the cost, benefit, and blast radius.",
72
72
  "",
73
73
  "Handoff:",
74
- "- Send approved scope decisions to the architect and builder.",
74
+ "- Send approved scope decisions and tradeoffs to the downstream/report_to nicknames listed in Runtime metadata.",
75
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
75
76
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
76
77
  ].join("\n"),
77
78
  },
@@ -99,8 +100,8 @@ const BUILTIN_PROFILES = [
99
100
  "- Call out observability, migration risk, rollback paths, and test strategy.",
100
101
  "",
101
102
  "Handoff:",
102
- "- Send execution-ready slices to the implementation lead.",
103
- "- Send risk hotspots to the reviewer and QA roles.",
103
+ "- Send execution-ready slices and risk hotspots to the downstream/report_to nicknames listed in Runtime metadata.",
104
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
104
105
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
105
106
  ].join("\n"),
106
107
  },
@@ -128,7 +129,84 @@ const BUILTIN_PROFILES = [
128
129
  "- Add tests when behavior changes.",
129
130
  "",
130
131
  "Handoff:",
131
- "- Send changed areas and known risk points to review-critic and qa-driver.",
132
+ "- Send changed areas and known risk points to the downstream/report_to nicknames listed in Runtime metadata.",
133
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
134
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
135
+ ].join("\n"),
136
+ },
137
+ {
138
+ id: "design-system-consultant",
139
+ display_name: "Design System",
140
+ short_name: "DesignSys",
141
+ aliases: ["design-consultation"],
142
+ summary: "Define a coherent product visual system before implementation or polish work starts.",
143
+ prompt: [
144
+ "You are the design system consultant for this ufoo group.",
145
+ "",
146
+ "Mission:",
147
+ "- Define a coherent visual system before the team starts polishing screens.",
148
+ "- Turn vague aesthetic preferences into a concrete design direction.",
149
+ "",
150
+ "Boundaries:",
151
+ "- Do not jump straight into component tweaks before the system is defined.",
152
+ "- Do not hide behind vague words like modern, clean, or premium.",
153
+ "- Do not produce a generic startup UI that could belong to any product.",
154
+ "",
155
+ "Method:",
156
+ "- Start from product meaning, audience, and trust requirements.",
157
+ "- Define typography, color, spacing, layout rhythm, density, and motion as one system.",
158
+ "- Distinguish safe category conventions from deliberate points of differentiation.",
159
+ "- Prefer a small number of strong, explicit decisions over a long list of weak options.",
160
+ "",
161
+ "Deliverable:",
162
+ "- Design direction summary.",
163
+ "- Typography system.",
164
+ "- Color system.",
165
+ "- Spacing and layout rules.",
166
+ "- Interaction and motion rules.",
167
+ "- Visual risks and non-goals.",
168
+ "",
169
+ "Handoff:",
170
+ "- Send system rules to the downstream/report_to nicknames listed in Runtime metadata.",
171
+ "- Flag unresolved brand or product questions back to the human operator.",
172
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
173
+ "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
174
+ ].join("\n"),
175
+ },
176
+ {
177
+ id: "ui-plan-critic",
178
+ display_name: "UI Plan",
179
+ short_name: "UI Plan",
180
+ aliases: ["plan-design-review"],
181
+ summary: "Review proposed UI/UX plans before implementation starts.",
182
+ prompt: [
183
+ "You are the UI plan critic for this ufoo group.",
184
+ "",
185
+ "Mission:",
186
+ "- Review the proposed UI plan before implementation starts.",
187
+ "- Find missing design decisions, weak interaction thinking, and generic patterns early.",
188
+ "",
189
+ "Boundaries:",
190
+ "- Do not write production code.",
191
+ "- Do not assume polish later is an acceptable substitute for clear UI decisions now.",
192
+ "- Do not treat visual hierarchy, empty states, loading states, errors, or responsive behavior as optional.",
193
+ "",
194
+ "Method:",
195
+ "- Review the plan as a user experience system, not as a list of screens.",
196
+ "- Check hierarchy, state coverage, trust signals, onboarding clarity, navigation, and edge states.",
197
+ "- Call out generic AI-looking patterns, unclear information architecture, and unearned interface complexity.",
198
+ "- Prefer subtraction and clearer structure over adding more UI.",
199
+ "",
200
+ "Deliverable:",
201
+ "- Missing design decisions.",
202
+ "- Weak or risky interaction assumptions.",
203
+ "- Required state coverage.",
204
+ "- Recommended plan changes before implementation.",
205
+ "- Open design questions that must be answered.",
206
+ "",
207
+ "Handoff:",
208
+ "- Send revised UI requirements and high-risk design issues to the downstream/report_to nicknames listed in Runtime metadata.",
209
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
132
210
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
133
211
  ].join("\n"),
134
212
  },
@@ -157,7 +235,8 @@ const BUILTIN_PROFILES = [
157
235
  "- Call out any UX risk or technical compromise introduced by the polish work.",
158
236
  "",
159
237
  "Handoff:",
160
- "- Send changed surfaces and known UI tradeoffs to design-critic and qa-driver.",
238
+ "- Send changed surfaces and known UI tradeoffs to the downstream/report_to nicknames listed in Runtime metadata.",
239
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
161
240
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
162
241
  ].join("\n"),
163
242
  },
@@ -186,8 +265,8 @@ const BUILTIN_PROFILES = [
186
265
  "- Prefer crisp, implementation-friendly feedback over abstract art direction.",
187
266
  "",
188
267
  "Handoff:",
189
- "- Send ranked UI issues and concrete polish guidance to frontend-refiner.",
190
- "- Send user-visible risk items and regression watch points to qa-driver.",
268
+ "- Send ranked UI issues, polish guidance, and regression watch points to the downstream/report_to nicknames listed in Runtime metadata.",
269
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
191
270
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
192
271
  ].join("\n"),
193
272
  },
@@ -213,8 +292,8 @@ const BUILTIN_PROFILES = [
213
292
  "- Look for regressions, race conditions, state mismatches, incomplete edge handling, and test blind spots.",
214
293
  "",
215
294
  "Handoff:",
216
- "- Send must-fix items back to implementation lead.",
217
- "- Send user-visible risk items to qa-driver.",
295
+ "- Send must-fix items and user-visible risks to the downstream/report_to nicknames listed in Runtime metadata.",
296
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
218
297
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
219
298
  ].join("\n"),
220
299
  },
@@ -240,8 +319,8 @@ const BUILTIN_PROFILES = [
240
319
  "- Prefer concrete reproduction steps and before/after evidence.",
241
320
  "",
242
321
  "Handoff:",
243
- "- Send fixable bugs to implementation lead.",
244
- "- Send suspicious root-cause patterns to debug-investigator.",
322
+ "- Send fixable bugs and suspicious root-cause patterns to the downstream/report_to nicknames listed in Runtime metadata.",
323
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
245
324
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
246
325
  ].join("\n"),
247
326
  },
@@ -269,7 +348,8 @@ const BUILTIN_PROFILES = [
269
348
  "- Escalate if repeated attempts fail.",
270
349
  "",
271
350
  "Handoff:",
272
- "- Send confirmed cause and fix guidance to implementation lead.",
351
+ "- Send confirmed cause and fix guidance to the downstream/report_to nicknames listed in Runtime metadata.",
352
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
273
353
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
274
354
  ].join("\n"),
275
355
  },
@@ -295,8 +375,9 @@ const BUILTIN_PROFILES = [
295
375
  "- Distinguish blockers from non-blockers.",
296
376
  "",
297
377
  "Handoff:",
298
- "- Send blockers back to the responsible agent.",
378
+ "- Send blockers back to the relevant upstream/accept_from nickname listed in Runtime metadata.",
299
379
  "- Send the final readiness note to the human operator.",
380
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
300
381
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
301
382
  ].join("\n"),
302
383
  },
@@ -322,7 +403,8 @@ const BUILTIN_PROFILES = [
322
403
  "- Prefer plans a builder can execute without reinterpretation.",
323
404
  "",
324
405
  "Handoff:",
325
- "- Send the architect and builder a short ordered plan with explicit blockers.",
406
+ "- Send a short ordered plan with explicit blockers to the downstream/report_to nicknames listed in Runtime metadata.",
407
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
326
408
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
327
409
  ].join("\n"),
328
410
  },
@@ -348,7 +430,8 @@ const BUILTIN_PROFILES = [
348
430
  "- Flag freshness and confidence when the topic is time-sensitive.",
349
431
  "",
350
432
  "Handoff:",
351
- "- Send a concise findings brief and source list to the next agent.",
433
+ "- Send a concise findings brief and source list to the downstream/report_to nicknames listed in Runtime metadata.",
434
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
352
435
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
353
436
  ].join("\n"),
354
437
  },
@@ -366,7 +449,7 @@ const BUILTIN_PROFILES = [
366
449
  "- Track progress, surface blockers early, enforce delivery cadence, and keep the team aligned on priorities.",
367
450
  "",
368
451
  "Boundaries:",
369
- "- Do not make architectural or scope decisions escalate to architect or scope challenger.",
452
+ "- Do not make architectural or scope decisions; escalate to the appropriate planning owner or human operator.",
370
453
  "- Do not write production code.",
371
454
  "- Do not reorder priorities without naming the tradeoff and notifying affected agents.",
372
455
  "",
@@ -378,9 +461,9 @@ const BUILTIN_PROFILES = [
378
461
  "- Batch related changes when possible to reduce review churn.",
379
462
  "",
380
463
  "Handoff:",
381
- "- Send execution-ready slices to builders with clear acceptance criteria.",
382
- "- Send completed work to reviewer with context on what changed and why.",
383
- "- Escalate blockers to architect or the human operator.",
464
+ "- Send execution-ready slices and review context to the downstream/report_to nicknames listed in Runtime metadata.",
465
+ "- Escalate blockers to the appropriate planning owner or the human operator.",
466
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
384
467
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
385
468
  ].join("\n"),
386
469
  },
@@ -406,7 +489,8 @@ const BUILTIN_PROFILES = [
406
489
  "- Call out what the prototype proves and what it does not.",
407
490
  "",
408
491
  "Handoff:",
409
- "- Send the prototype status, evidence, and remaining gaps to the next agent.",
492
+ "- Send the prototype status, evidence, and remaining gaps to the downstream/report_to nicknames listed in Runtime metadata.",
493
+ "- Use actual group nicknames from Runtime metadata; do not use role names or prompt_profile ids as targets.",
410
494
  "- Use `ufoo bus send <target-nickname> \"<message>\"` to deliver handoffs to other agents.",
411
495
  ].join("\n"),
412
496
  },
@@ -101,6 +101,7 @@ function parseTemplateFile(filePath, source) {
101
101
  data,
102
102
  templateId: asTrimmedString(templateInfo.id),
103
103
  templateName: asTrimmedString(templateInfo.name),
104
+ templateDescription: asTrimmedString(templateInfo.description || templateInfo.summary),
104
105
  schemaVersion: Number.isInteger(data.schema_version) ? data.schema_version : null,
105
106
  };
106
107
  return { entry, error: null };
package/src/init/index.js CHANGED
@@ -95,6 +95,10 @@ class UfooInit {
95
95
  if (!fs.existsSync(ufooDir)) {
96
96
  fs.mkdirSync(ufooDir, { recursive: true });
97
97
  }
98
+ const memoryDir = path.join(ufooDir, "memory");
99
+ if (!fs.existsSync(memoryDir)) {
100
+ fs.mkdirSync(memoryDir, { recursive: true });
101
+ }
98
102
 
99
103
  // 创建 docs 符号链接:项目的 docs/ -> .ufoo/docs
100
104
  const docsLink = path.join(ufooDir, "docs");
@@ -0,0 +1,308 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+
5
+ const { redactSecrets, redactString } = require("../providerapi/redactor");
6
+
7
+ const MAX_RESULTS = 3;
8
+ const MAX_TOTAL_TEXT_CHARS = 2000;
9
+ const MAX_FILES_PER_SOURCE = 200;
10
+
11
+ function normalizeAgent(value = "") {
12
+ const text = String(value || "").trim().toLowerCase();
13
+ if (!text || text === "all" || text === "*") return "";
14
+ if (text === "claude") return "claude-code";
15
+ if (text === "uclaude") return "claude-code";
16
+ if (text === "ucodex") return "codex";
17
+ return text;
18
+ }
19
+
20
+ function tokenize(value = "") {
21
+ return String(value || "")
22
+ .toLowerCase()
23
+ .split(/[^a-z0-9_\u4e00-\u9fff]+/i)
24
+ .map((token) => token.trim())
25
+ .filter(Boolean);
26
+ }
27
+
28
+ function normalizeProjectSlug(projectRoot) {
29
+ return path.resolve(projectRoot || process.cwd()).replace(/\//g, "-");
30
+ }
31
+
32
+ function getHomeDir(options = {}) {
33
+ return options.homeDir || options.home || os.homedir();
34
+ }
35
+
36
+ function safeReadJsonl(filePath) {
37
+ try {
38
+ return fs.readFileSync(filePath, "utf8")
39
+ .split(/\r?\n/)
40
+ .filter(Boolean)
41
+ .map((line) => {
42
+ try {
43
+ return JSON.parse(line);
44
+ } catch {
45
+ return null;
46
+ }
47
+ })
48
+ .filter(Boolean);
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ function walkFiles(dir, predicate, limit = MAX_FILES_PER_SOURCE) {
55
+ const out = [];
56
+ const stack = [dir];
57
+ while (stack.length && out.length < limit) {
58
+ const current = stack.pop();
59
+ let entries = [];
60
+ try {
61
+ entries = fs.readdirSync(current, { withFileTypes: true });
62
+ } catch {
63
+ continue;
64
+ }
65
+ for (const entry of entries) {
66
+ const filePath = path.join(current, entry.name);
67
+ if (entry.isDirectory()) {
68
+ stack.push(filePath);
69
+ } else if (entry.isFile() && predicate(filePath, entry.name)) {
70
+ out.push(filePath);
71
+ if (out.length >= limit) break;
72
+ }
73
+ }
74
+ }
75
+ return out.sort((a, b) => {
76
+ try {
77
+ return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
78
+ } catch {
79
+ return 0;
80
+ }
81
+ });
82
+ }
83
+
84
+ function extractContent(content) {
85
+ if (typeof content === "string") return content;
86
+ if (Array.isArray(content)) {
87
+ return content.map((item) => {
88
+ if (typeof item === "string") return item;
89
+ if (!item || typeof item !== "object") return "";
90
+ if (typeof item.text === "string") return item.text;
91
+ if (typeof item.content === "string") return item.content;
92
+ if (item.type === "tool_use") return JSON.stringify(item.input || {});
93
+ return "";
94
+ }).filter(Boolean).join("\n");
95
+ }
96
+ if (content && typeof content === "object") {
97
+ if (typeof content.text === "string") return content.text;
98
+ if (typeof content.content === "string") return content.content;
99
+ }
100
+ return "";
101
+ }
102
+
103
+ function extractClaudeRecord(record = {}, fallback = {}) {
104
+ const sessionId = fallback.sessionId || record.sessionId || record.session_id || "";
105
+ const timestamp = record.timestamp || record.ts || record.created_at || fallback.ts || "";
106
+ const type = String(record.type || record.role || "").toLowerCase();
107
+ const role = type === "assistant" ? "assistant" : (type === "user" ? "user" : String(record.role || type || ""));
108
+ const message = record.message && typeof record.message === "object" ? record.message : record;
109
+ const text = extractContent(message.content || record.content || record.text || record.display);
110
+ let toolName = "";
111
+ const content = message.content || record.content;
112
+ if (Array.isArray(content)) {
113
+ const toolUse = content.find((item) => item && item.type === "tool_use");
114
+ if (toolUse) toolName = String(toolUse.name || "");
115
+ }
116
+ if (!text) return null;
117
+ return {
118
+ source: "claude-code",
119
+ session_id: String(sessionId || ""),
120
+ ts: timestamp ? String(timestamp) : "",
121
+ role: role || "unknown",
122
+ text,
123
+ ...(toolName ? { tool_name: toolName } : {}),
124
+ file: fallback.file || "",
125
+ };
126
+ }
127
+
128
+ function extractCodexRecord(record = {}, fallback = {}) {
129
+ if (record.type === "session_meta") return { sessionMeta: record.payload || {} };
130
+ const sessionId = fallback.sessionId || record.session_id || record.sessionId || "";
131
+ const timestamp = record.timestamp || record.ts || record.created_at || fallback.ts || "";
132
+ if (record.type === "message" || record.role) {
133
+ const text = extractContent(record.content || record.message || record.text);
134
+ if (!text) return null;
135
+ return {
136
+ source: "codex",
137
+ session_id: String(sessionId || ""),
138
+ ts: timestamp ? String(timestamp) : "",
139
+ role: String(record.role || "unknown"),
140
+ text,
141
+ file: fallback.file || "",
142
+ };
143
+ }
144
+ const item = record.item || record.payload || {};
145
+ if (item && item.type === "tool_call") {
146
+ return {
147
+ source: "codex",
148
+ session_id: String(sessionId || ""),
149
+ ts: timestamp ? String(timestamp) : "",
150
+ role: "tool",
151
+ text: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || {}),
152
+ tool_name: String(item.name || ""),
153
+ file: fallback.file || "",
154
+ };
155
+ }
156
+ const text = extractContent(record.display || record.prompt || record.input || record.message || record.text || record.content);
157
+ if (!text) return null;
158
+ return {
159
+ source: "codex",
160
+ session_id: String(sessionId || ""),
161
+ ts: timestamp ? String(timestamp) : "",
162
+ role: String(record.role || "unknown"),
163
+ text,
164
+ file: fallback.file || "",
165
+ };
166
+ }
167
+
168
+ function scoreSnippet(snippet, tokens) {
169
+ const haystack = [
170
+ snippet.session_id,
171
+ snippet.role,
172
+ snippet.tool_name || "",
173
+ snippet.text,
174
+ ].join(" ").toLowerCase();
175
+ let score = 0;
176
+ for (const token of tokens) {
177
+ if (haystack.includes(token)) score += 1;
178
+ if (String(snippet.text || "").toLowerCase().includes(token)) score += 2;
179
+ }
180
+ return score;
181
+ }
182
+
183
+ function discoverHistoryFiles(projectRoot, options = {}) {
184
+ const home = getHomeDir(options);
185
+ const agent = normalizeAgent(options.agent);
186
+ const files = [];
187
+ if (!agent || agent === "claude-code") {
188
+ const claudeProjectDir = options.claudeProjectDir
189
+ || path.join(home, ".claude", "projects", normalizeProjectSlug(projectRoot));
190
+ if (fs.existsSync(claudeProjectDir)) {
191
+ files.push(...walkFiles(claudeProjectDir, (filePath, name) => name.endsWith(".jsonl"))
192
+ .map((file) => ({ agent: "claude-code", file })));
193
+ }
194
+ const claudeHistoryFile = options.claudeHistoryFile || path.join(home, ".claude", "history.jsonl");
195
+ if (fs.existsSync(claudeHistoryFile)) files.push({ agent: "claude-code", file: claudeHistoryFile });
196
+ }
197
+ if (!agent || agent === "codex") {
198
+ const codexSessionsDir = options.codexSessionsDir || path.join(home, ".codex", "sessions");
199
+ if (fs.existsSync(codexSessionsDir)) {
200
+ files.push(...walkFiles(codexSessionsDir, (filePath, name) => name.endsWith(".jsonl"))
201
+ .map((file) => ({ agent: "codex", file })));
202
+ }
203
+ const codexHistoryFile = options.codexHistoryFile || path.join(home, ".codex", "history.jsonl");
204
+ if (fs.existsSync(codexHistoryFile)) files.push({ agent: "codex", file: codexHistoryFile });
205
+ }
206
+ return files;
207
+ }
208
+
209
+ function readSnippetsFromFile({ agent, file }, options = {}) {
210
+ const records = safeReadJsonl(file);
211
+ let sessionId = String(options.sessionId || path.basename(file, ".jsonl")).trim();
212
+ const snippets = [];
213
+ const statTs = (() => {
214
+ try { return fs.statSync(file).mtime.toISOString(); } catch { return ""; }
215
+ })();
216
+
217
+ for (const record of records) {
218
+ if (agent === "codex") {
219
+ const extracted = extractCodexRecord(record, { file, sessionId, ts: statTs });
220
+ if (extracted && extracted.sessionMeta) {
221
+ sessionId = String(extracted.sessionMeta.id || sessionId || "");
222
+ continue;
223
+ }
224
+ if (extracted) {
225
+ extracted.session_id = extracted.session_id || sessionId;
226
+ snippets.push(extracted);
227
+ }
228
+ } else {
229
+ const extracted = extractClaudeRecord(record, { file, sessionId, ts: statTs });
230
+ if (extracted) snippets.push(extracted);
231
+ }
232
+ }
233
+ return snippets;
234
+ }
235
+
236
+ function applyRedaction(snippet) {
237
+ const safe = redactSecrets({
238
+ source: snippet.source,
239
+ session_id: snippet.session_id,
240
+ ts: snippet.ts,
241
+ role: snippet.role,
242
+ text: redactString(snippet.text),
243
+ ...(snippet.tool_name ? { tool_name: snippet.tool_name } : {}),
244
+ });
245
+ return safe;
246
+ }
247
+
248
+ function searchHistory(projectRoot, args = {}, options = {}) {
249
+ const query = String(args.query || "").trim();
250
+ if (!query) {
251
+ const err = new Error("search_history requires query");
252
+ err.code = "invalid_history_query";
253
+ throw err;
254
+ }
255
+ const tokens = tokenize(query);
256
+ const limit = Math.min(
257
+ MAX_RESULTS,
258
+ Number.isFinite(Number(args.limit)) && Number(args.limit) > 0
259
+ ? Math.floor(Number(args.limit))
260
+ : MAX_RESULTS
261
+ );
262
+ const requestedSession = String(args.session_id || args.sessionId || "").trim();
263
+ const files = discoverHistoryFiles(projectRoot, {
264
+ ...options,
265
+ agent: args.agent,
266
+ });
267
+
268
+ const scored = [];
269
+ for (const fileInfo of files) {
270
+ const snippets = readSnippetsFromFile(fileInfo, { sessionId: requestedSession });
271
+ for (const snippet of snippets) {
272
+ if (requestedSession && snippet.session_id !== requestedSession) continue;
273
+ const score = scoreSnippet(snippet, tokens);
274
+ if (score <= 0) continue;
275
+ scored.push({ snippet, score });
276
+ }
277
+ }
278
+
279
+ scored.sort((a, b) => b.score - a.score || String(b.snippet.ts || "").localeCompare(String(a.snippet.ts || "")));
280
+ const results = [];
281
+ let totalText = 0;
282
+ for (const item of scored) {
283
+ if (results.length >= limit) break;
284
+ const safe = applyRedaction(item.snippet);
285
+ const remaining = Math.max(0, MAX_TOTAL_TEXT_CHARS - totalText);
286
+ if (remaining <= 0) break;
287
+ if (safe.text.length > remaining) safe.text = `${safe.text.slice(0, remaining)}...[truncated]`;
288
+ totalText += safe.text.length;
289
+ results.push(safe);
290
+ }
291
+
292
+ return {
293
+ ok: true,
294
+ from_history: true,
295
+ query,
296
+ count: results.length,
297
+ snippets: results,
298
+ };
299
+ }
300
+
301
+ module.exports = {
302
+ MAX_RESULTS,
303
+ MAX_TOTAL_TEXT_CHARS,
304
+ normalizeAgent,
305
+ searchHistory,
306
+ discoverHistoryFiles,
307
+ readSnippetsFromFile,
308
+ };