@zhijiewang/openharness 2.0.0 → 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 (235) hide show
  1. package/README.md +4 -4
  2. package/dist/DeferredTool.js +3 -1
  3. package/dist/Tool.d.ts +1 -1
  4. package/dist/agents/roles.js +58 -62
  5. package/dist/commands/cybergotchi.d.ts +1 -1
  6. package/dist/commands/cybergotchi.js +30 -30
  7. package/dist/commands/index.js +360 -122
  8. package/dist/components/App.d.ts +1 -1
  9. package/dist/components/App.js +6 -6
  10. package/dist/components/CompanionFooter.d.ts +1 -1
  11. package/dist/components/CompanionFooter.js +6 -8
  12. package/dist/components/CybergotchiBubble.js +5 -5
  13. package/dist/components/CybergotchiPanel.d.ts +1 -1
  14. package/dist/components/CybergotchiPanel.js +7 -7
  15. package/dist/components/CybergotchiPanelConnected.js +2 -2
  16. package/dist/components/CybergotchiSetup.js +26 -24
  17. package/dist/components/CybergotchiSprite.d.ts +1 -1
  18. package/dist/components/CybergotchiSprite.js +8 -12
  19. package/dist/components/DiffView.d.ts +1 -1
  20. package/dist/components/DiffView.js +10 -10
  21. package/dist/components/ErrorBoundary.d.ts +1 -1
  22. package/dist/components/ErrorBoundary.js +1 -1
  23. package/dist/components/InitWizard.js +65 -33
  24. package/dist/components/Markdown.js +2 -4
  25. package/dist/components/Messages.js +4 -4
  26. package/dist/components/PermissionPrompt.d.ts +1 -1
  27. package/dist/components/PermissionPrompt.js +15 -17
  28. package/dist/components/REPL.d.ts +1 -1
  29. package/dist/components/REPL.js +74 -49
  30. package/dist/components/Spinner.js +2 -2
  31. package/dist/components/TextInput.js +35 -29
  32. package/dist/components/ToolCallDisplay.js +3 -5
  33. package/dist/cybergotchi/bones.d.ts +1 -1
  34. package/dist/cybergotchi/bones.js +8 -8
  35. package/dist/cybergotchi/config.d.ts +2 -2
  36. package/dist/cybergotchi/config.js +13 -13
  37. package/dist/cybergotchi/events.d.ts +5 -5
  38. package/dist/cybergotchi/events.js +7 -7
  39. package/dist/cybergotchi/needs.d.ts +2 -2
  40. package/dist/cybergotchi/needs.js +7 -9
  41. package/dist/cybergotchi/personality.d.ts +2 -2
  42. package/dist/cybergotchi/personality.js +2 -2
  43. package/dist/cybergotchi/species.d.ts +1 -1
  44. package/dist/cybergotchi/species.js +145 -217
  45. package/dist/cybergotchi/speech.d.ts +2 -2
  46. package/dist/cybergotchi/speech.js +43 -43
  47. package/dist/cybergotchi/types.d.ts +4 -4
  48. package/dist/cybergotchi/types.js +26 -26
  49. package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
  50. package/dist/cybergotchi/useCybergotchi.js +29 -25
  51. package/dist/git/index.js +11 -9
  52. package/dist/harness/checkpoints.js +29 -21
  53. package/dist/harness/config.d.ts +12 -2
  54. package/dist/harness/config.js +15 -9
  55. package/dist/harness/context-warning.d.ts +1 -1
  56. package/dist/harness/context-warning.js +1 -1
  57. package/dist/harness/cost.js +1 -1
  58. package/dist/harness/credentials.js +13 -13
  59. package/dist/harness/hooks.js +7 -5
  60. package/dist/harness/keybindings.js +20 -18
  61. package/dist/harness/marketplace.d.ts +3 -3
  62. package/dist/harness/marketplace.js +55 -42
  63. package/dist/harness/memory.d.ts +23 -5
  64. package/dist/harness/memory.js +142 -41
  65. package/dist/harness/onboarding.js +30 -10
  66. package/dist/harness/plugins.d.ts +9 -1
  67. package/dist/harness/plugins.js +54 -30
  68. package/dist/harness/rules.js +12 -7
  69. package/dist/harness/sandbox.d.ts +34 -0
  70. package/dist/harness/sandbox.js +104 -0
  71. package/dist/harness/session-db.d.ts +55 -0
  72. package/dist/harness/session-db.js +165 -0
  73. package/dist/harness/session.d.ts +1 -1
  74. package/dist/harness/session.js +34 -15
  75. package/dist/harness/store.d.ts +3 -3
  76. package/dist/harness/store.js +6 -4
  77. package/dist/harness/submit-handler.d.ts +4 -4
  78. package/dist/harness/submit-handler.js +57 -21
  79. package/dist/harness/telemetry.d.ts +1 -1
  80. package/dist/harness/telemetry.js +23 -19
  81. package/dist/harness/traces.d.ts +2 -2
  82. package/dist/harness/traces.js +44 -33
  83. package/dist/harness/verification.d.ts +1 -1
  84. package/dist/harness/verification.js +50 -44
  85. package/dist/lsp/client.js +44 -40
  86. package/dist/main.js +100 -59
  87. package/dist/mcp/DeferredMcpTool.d.ts +4 -4
  88. package/dist/mcp/DeferredMcpTool.js +9 -5
  89. package/dist/mcp/McpTool.d.ts +4 -4
  90. package/dist/mcp/McpTool.js +8 -4
  91. package/dist/mcp/client.d.ts +2 -2
  92. package/dist/mcp/client.js +21 -21
  93. package/dist/mcp/loader.d.ts +1 -1
  94. package/dist/mcp/loader.js +17 -12
  95. package/dist/mcp/registry.d.ts +3 -3
  96. package/dist/mcp/registry.js +97 -97
  97. package/dist/mcp/schema.d.ts +1 -1
  98. package/dist/mcp/schema.js +16 -16
  99. package/dist/mcp/server.d.ts +1 -1
  100. package/dist/mcp/server.js +21 -21
  101. package/dist/mcp/types.d.ts +3 -3
  102. package/dist/providers/anthropic.d.ts +2 -2
  103. package/dist/providers/anthropic.js +10 -9
  104. package/dist/providers/base.d.ts +1 -1
  105. package/dist/providers/index.js +10 -3
  106. package/dist/providers/llamacpp.d.ts +2 -2
  107. package/dist/providers/llamacpp.js +1 -3
  108. package/dist/providers/ollama.d.ts +2 -2
  109. package/dist/providers/ollama.js +3 -4
  110. package/dist/providers/openai.d.ts +2 -2
  111. package/dist/providers/openai.js +3 -5
  112. package/dist/providers/openrouter.d.ts +2 -2
  113. package/dist/providers/router.d.ts +1 -1
  114. package/dist/providers/router.js +7 -7
  115. package/dist/query/compress.d.ts +2 -2
  116. package/dist/query/compress.js +22 -21
  117. package/dist/query/context-manager.d.ts +2 -2
  118. package/dist/query/context-manager.js +8 -11
  119. package/dist/query/errors.js +1 -1
  120. package/dist/query/index.d.ts +1 -1
  121. package/dist/query/index.js +30 -22
  122. package/dist/query/tools.js +15 -12
  123. package/dist/query/types.d.ts +1 -1
  124. package/dist/query.d.ts +1 -1
  125. package/dist/query.js +1 -1
  126. package/dist/remote/auth.d.ts +2 -2
  127. package/dist/remote/auth.js +8 -8
  128. package/dist/remote/server.d.ts +3 -3
  129. package/dist/remote/server.js +60 -60
  130. package/dist/renderer/cells.js +9 -9
  131. package/dist/renderer/colors.js +24 -6
  132. package/dist/renderer/diff.d.ts +2 -2
  133. package/dist/renderer/diff.js +27 -19
  134. package/dist/renderer/differ.d.ts +1 -1
  135. package/dist/renderer/differ.js +9 -9
  136. package/dist/renderer/image.js +19 -19
  137. package/dist/renderer/index.d.ts +6 -6
  138. package/dist/renderer/index.js +163 -93
  139. package/dist/renderer/input.js +66 -48
  140. package/dist/renderer/layout.d.ts +6 -6
  141. package/dist/renderer/layout.js +163 -124
  142. package/dist/renderer/markdown.d.ts +2 -2
  143. package/dist/renderer/markdown.js +173 -54
  144. package/dist/renderer/session-browser.d.ts +2 -2
  145. package/dist/renderer/session-browser.js +19 -21
  146. package/dist/repl.d.ts +5 -5
  147. package/dist/repl.js +300 -198
  148. package/dist/sdk/index.d.ts +8 -7
  149. package/dist/sdk/index.js +59 -42
  150. package/dist/services/AgentDispatcher.d.ts +3 -3
  151. package/dist/services/AgentDispatcher.js +33 -29
  152. package/dist/services/CronExecutor.d.ts +4 -4
  153. package/dist/services/CronExecutor.js +12 -8
  154. package/dist/services/EvaluatorLoop.d.ts +3 -3
  155. package/dist/services/EvaluatorLoop.js +29 -21
  156. package/dist/services/MetaHarness.d.ts +1 -1
  157. package/dist/services/MetaHarness.js +41 -33
  158. package/dist/services/PipelineExecutor.d.ts +1 -1
  159. package/dist/services/PipelineExecutor.js +23 -25
  160. package/dist/services/SkillExtractor.d.ts +43 -0
  161. package/dist/services/SkillExtractor.js +143 -0
  162. package/dist/services/StreamingToolExecutor.d.ts +2 -2
  163. package/dist/services/StreamingToolExecutor.js +11 -7
  164. package/dist/services/a2a.d.ts +8 -8
  165. package/dist/services/a2a.js +44 -34
  166. package/dist/services/agent-messaging.d.ts +33 -15
  167. package/dist/services/agent-messaging.js +65 -13
  168. package/dist/services/cron.js +16 -16
  169. package/dist/tools/AgentTool/index.d.ts +5 -2
  170. package/dist/tools/AgentTool/index.js +35 -15
  171. package/dist/tools/AskUserTool/index.js +1 -1
  172. package/dist/tools/BashTool/index.d.ts +2 -2
  173. package/dist/tools/BashTool/index.js +18 -10
  174. package/dist/tools/CronTool/index.d.ts +2 -2
  175. package/dist/tools/CronTool/index.js +30 -12
  176. package/dist/tools/DiagnosticsTool/index.js +28 -22
  177. package/dist/tools/EnterPlanModeTool/index.js +93 -14
  178. package/dist/tools/EnterWorktreeTool/index.js +7 -3
  179. package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
  180. package/dist/tools/ExitPlanModeTool/index.js +20 -5
  181. package/dist/tools/ExitWorktreeTool/index.js +11 -4
  182. package/dist/tools/FileEditTool/index.js +3 -5
  183. package/dist/tools/FileReadTool/index.js +16 -10
  184. package/dist/tools/FileWriteTool/index.js +2 -2
  185. package/dist/tools/GlobTool/index.js +5 -9
  186. package/dist/tools/GrepTool/index.d.ts +2 -2
  187. package/dist/tools/GrepTool/index.js +14 -9
  188. package/dist/tools/ImageReadTool/index.js +2 -2
  189. package/dist/tools/KillProcessTool/index.js +11 -7
  190. package/dist/tools/LSTool/index.js +3 -3
  191. package/dist/tools/MemoryTool/index.d.ts +11 -11
  192. package/dist/tools/MemoryTool/index.js +28 -14
  193. package/dist/tools/MonitorTool/index.d.ts +2 -2
  194. package/dist/tools/MonitorTool/index.js +24 -19
  195. package/dist/tools/MultiEditTool/index.js +9 -5
  196. package/dist/tools/NotebookEditTool/index.js +3 -3
  197. package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
  198. package/dist/tools/ParallelAgentTool/index.js +12 -6
  199. package/dist/tools/PipelineTool/index.d.ts +4 -4
  200. package/dist/tools/PipelineTool/index.js +3 -3
  201. package/dist/tools/PowerShellTool/index.js +10 -6
  202. package/dist/tools/RemoteTriggerTool/index.js +8 -4
  203. package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
  204. package/dist/tools/ScheduleWakeupTool/index.js +115 -0
  205. package/dist/tools/SendMessageTool/index.js +25 -7
  206. package/dist/tools/SessionSearchTool/index.d.ts +15 -0
  207. package/dist/tools/SessionSearchTool/index.js +36 -0
  208. package/dist/tools/SkillTool/index.d.ts +3 -0
  209. package/dist/tools/SkillTool/index.js +39 -9
  210. package/dist/tools/TaskCreateTool/index.d.ts +2 -2
  211. package/dist/tools/TaskCreateTool/index.js +2 -2
  212. package/dist/tools/TaskGetTool/index.js +2 -2
  213. package/dist/tools/TaskListTool/index.js +3 -5
  214. package/dist/tools/TaskOutputTool/index.js +2 -2
  215. package/dist/tools/TaskStopTool/index.js +3 -3
  216. package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
  217. package/dist/tools/TaskUpdateTool/index.js +2 -2
  218. package/dist/tools/ToolSearchTool/index.js +9 -6
  219. package/dist/tools/WebFetchTool/index.js +1 -1
  220. package/dist/tools/WebSearchTool/index.js +2 -6
  221. package/dist/tools.js +31 -30
  222. package/dist/types/permissions.js +15 -9
  223. package/dist/utils/bash-safety.d.ts +1 -1
  224. package/dist/utils/bash-safety.js +64 -54
  225. package/dist/utils/diff-algorithm.d.ts +3 -3
  226. package/dist/utils/diff-algorithm.js +7 -7
  227. package/dist/utils/fs.js +3 -3
  228. package/dist/utils/safe-env.js +1 -1
  229. package/dist/utils/theme-data.d.ts +1 -1
  230. package/dist/utils/theme-data.js +1 -1
  231. package/dist/utils/theme.d.ts +1 -1
  232. package/dist/utils/theme.js +1 -1
  233. package/dist/utils/tool-summary.d.ts +1 -1
  234. package/dist/utils/tool-summary.js +27 -9
  235. package/package.json +10 -3
@@ -7,27 +7,29 @@
7
7
  * The system detects learnable patterns from assistant responses and saves them
8
8
  * without user intervention.
9
9
  */
10
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, unlinkSync } from 'node:fs';
11
- import { join, resolve, sep } from 'node:path';
12
- import { homedir } from 'node:os';
13
- import { createUserMessage } from '../types/message.js';
14
- const PROJECT_MEMORY_DIR = join('.oh', 'memory');
15
- const GLOBAL_MEMORY_DIR = join(homedir(), '.oh', 'memory');
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join, resolve, sep } from "node:path";
13
+ import { createUserMessage } from "../types/message.js";
14
+ const PROJECT_MEMORY_DIR = join(".oh", "memory");
15
+ const GLOBAL_MEMORY_DIR = join(homedir(), ".oh", "memory");
16
16
  /** Load all memories from project and global dirs */
17
17
  export function loadMemories() {
18
18
  const entries = [];
19
19
  for (const dir of [PROJECT_MEMORY_DIR, GLOBAL_MEMORY_DIR]) {
20
20
  if (!existsSync(dir))
21
21
  continue;
22
- for (const file of readdirSync(dir).filter(f => f.endsWith('.md'))) {
22
+ for (const file of readdirSync(dir).filter((f) => f.endsWith(".md"))) {
23
23
  try {
24
24
  const filePath = join(dir, file);
25
- const raw = readFileSync(filePath, 'utf-8');
25
+ const raw = readFileSync(filePath, "utf-8");
26
26
  const entry = parseMemory(raw, filePath);
27
27
  if (entry)
28
28
  entries.push(entry);
29
29
  }
30
- catch { /* skip */ }
30
+ catch {
31
+ /* skip */
32
+ }
31
33
  }
32
34
  }
33
35
  return entries;
@@ -40,36 +42,39 @@ function parseMemory(raw, filePath) {
40
42
  if (!nameMatch)
41
43
  return null;
42
44
  // Content is everything after the frontmatter closing ---
43
- const fmEnd = raw.indexOf('---', raw.indexOf('---') + 3);
44
- const content = fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : '';
45
+ const fmEnd = raw.indexOf("---", raw.indexOf("---") + 3);
46
+ const content = fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : "";
45
47
  const relevanceMatch = raw.match(/^relevance:\s*([0-9.]+)$/m);
46
48
  const lastAccessedMatch = raw.match(/^lastAccessed:\s*(\d+)$/m);
47
49
  const createdAtMatch = raw.match(/^createdAt:\s*(\d+)$/m);
48
50
  const accessCountMatch = raw.match(/^accessCount:\s*(\d+)$/m);
49
51
  return {
50
52
  name: nameMatch[1].trim(),
51
- type: (typeMatch?.[1]?.trim() ?? 'convention'),
52
- description: descMatch?.[1]?.trim() ?? '',
53
+ type: (typeMatch?.[1]?.trim() ?? "user"),
54
+ description: descMatch?.[1]?.trim() ?? "",
53
55
  content,
54
56
  filePath,
55
57
  relevance: relevanceMatch ? parseFloat(relevanceMatch[1]) : 0.5,
56
- lastAccessed: lastAccessedMatch ? parseInt(lastAccessedMatch[1]) : undefined,
57
- createdAt: createdAtMatch ? parseInt(createdAtMatch[1]) : undefined,
58
- accessCount: accessCountMatch ? parseInt(accessCountMatch[1]) : 0,
58
+ lastAccessed: lastAccessedMatch ? parseInt(lastAccessedMatch[1], 10) : undefined,
59
+ createdAt: createdAtMatch ? parseInt(createdAtMatch[1], 10) : undefined,
60
+ accessCount: accessCountMatch ? parseInt(accessCountMatch[1], 10) : 0,
59
61
  };
60
62
  }
61
63
  /** Build a system prompt section from loaded memories */
62
64
  export function memoriesToPrompt(memories) {
63
65
  if (memories.length === 0)
64
- return '';
65
- const lines = memories.map(m => `- **${m.name}** (${m.type}): ${m.content.slice(0, 200)}`);
66
- return `# Remembered Context\n${lines.join('\n')}`;
66
+ return "";
67
+ const lines = memories.map((m) => `- **${m.name}** (${m.type}): ${m.content.slice(0, 200)}`);
68
+ return `# Remembered Context\n${lines.join("\n")}`;
67
69
  }
68
70
  /** Save a memory entry to the project memory directory */
69
71
  export function saveMemory(name, type, description, content, global = false) {
70
72
  const dir = global ? GLOBAL_MEMORY_DIR : PROJECT_MEMORY_DIR;
71
73
  mkdirSync(dir, { recursive: true });
72
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50);
74
+ const slug = name
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9]+/g, "-")
77
+ .slice(0, 50);
73
78
  const filePath = join(dir, `${slug}.md`);
74
79
  const now = Date.now();
75
80
  const md = `---
@@ -85,45 +90,95 @@ accessCount: 0
85
90
  ${content}
86
91
  `;
87
92
  writeFileSync(filePath, md);
93
+ updateMemoryIndex(dir);
88
94
  return filePath;
89
95
  }
96
+ /**
97
+ * Update or create MEMORY.md index file in the given memory directory.
98
+ * The index is always loaded into context, providing instant awareness of all stored memories.
99
+ * Each entry is a one-liner pointer to the individual memory file (~200 line cap).
100
+ */
101
+ export function updateMemoryIndex(dir = PROJECT_MEMORY_DIR) {
102
+ if (!existsSync(dir))
103
+ return;
104
+ const files = readdirSync(dir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
105
+ const entries = [];
106
+ for (const file of files) {
107
+ try {
108
+ const raw = readFileSync(join(dir, file), "utf-8");
109
+ const nameMatch = raw.match(/^name:\s*(.+)$/m);
110
+ const descMatch = raw.match(/^description:\s*(.+)$/m);
111
+ if (nameMatch) {
112
+ entries.push({
113
+ name: nameMatch[1].trim(),
114
+ file,
115
+ description: descMatch?.[1]?.trim() ?? "",
116
+ });
117
+ }
118
+ }
119
+ catch {
120
+ /* skip */
121
+ }
122
+ }
123
+ const lines = ["# Memory Index", ""];
124
+ for (const e of entries) {
125
+ // Keep each line under ~150 chars for readability
126
+ const hook = e.description.length > 100 ? `${e.description.slice(0, 97)}...` : e.description;
127
+ lines.push(`- [${e.name}](${e.file}) — ${hook}`);
128
+ }
129
+ lines.push("");
130
+ writeFileSync(join(dir, "MEMORY.md"), lines.join("\n"));
131
+ }
90
132
  /** Mark a memory as accessed — updates lastAccessed and accessCount in the file */
91
133
  export function touchMemory(entry) {
92
134
  try {
93
- let raw = readFileSync(entry.filePath, 'utf-8');
135
+ let raw = readFileSync(entry.filePath, "utf-8");
94
136
  const now = Date.now();
95
137
  const newCount = (entry.accessCount ?? 0) + 1;
96
- // Update or insert metadata fields in frontmatter
138
+ // Update existing fields in frontmatter, or insert before closing ---
97
139
  if (raw.match(/^lastAccessed:/m)) {
98
140
  raw = raw.replace(/^lastAccessed:\s*\d+$/m, `lastAccessed: ${now}`);
99
141
  }
100
142
  else {
101
- raw = raw.replace(/^---\s*$/m, `lastAccessed: ${now}\n---`);
143
+ // Insert before the CLOSING --- (second occurrence)
144
+ const firstIdx = raw.indexOf("---");
145
+ const closingIdx = raw.indexOf("---", firstIdx + 3);
146
+ if (closingIdx > 0) {
147
+ raw = `${raw.slice(0, closingIdx)}lastAccessed: ${now}\n${raw.slice(closingIdx)}`;
148
+ }
102
149
  }
103
150
  if (raw.match(/^accessCount:/m)) {
104
151
  raw = raw.replace(/^accessCount:\s*\d+$/m, `accessCount: ${newCount}`);
105
152
  }
106
153
  else {
107
- raw = raw.replace(/^---\s*$/m, `accessCount: ${newCount}\n---`);
154
+ const firstIdx = raw.indexOf("---");
155
+ const closingIdx = raw.indexOf("---", firstIdx + 3);
156
+ if (closingIdx > 0) {
157
+ raw = `${raw.slice(0, closingIdx)}accessCount: ${newCount}\n${raw.slice(closingIdx)}`;
158
+ }
108
159
  }
109
160
  writeFileSync(entry.filePath, raw);
110
161
  entry.lastAccessed = now;
111
162
  entry.accessCount = newCount;
112
163
  }
113
- catch { /* ignore write errors */ }
164
+ catch {
165
+ /* ignore write errors */
166
+ }
114
167
  }
115
168
  /** Boost a memory's relevance score (capped at 1.0) */
116
169
  export function boostRelevance(entry, amount = 0.1) {
117
170
  const newRelevance = Math.min(1.0, (entry.relevance ?? 0.5) + amount);
118
171
  try {
119
- let raw = readFileSync(entry.filePath, 'utf-8');
172
+ let raw = readFileSync(entry.filePath, "utf-8");
120
173
  if (raw.match(/^relevance:/m)) {
121
174
  raw = raw.replace(/^relevance:\s*[0-9.]+$/m, `relevance: ${newRelevance.toFixed(2)}`);
122
175
  }
123
176
  writeFileSync(entry.filePath, raw);
124
177
  entry.relevance = newRelevance;
125
178
  }
126
- catch { /* ignore */ }
179
+ catch {
180
+ /* ignore */
181
+ }
127
182
  }
128
183
  /**
129
184
  * Apply temporal decay to memory relevance.
@@ -164,30 +219,34 @@ export function loadActiveMemories() {
164
219
  /** Delete memory files that have been pruned (relevance < 0.1) */
165
220
  export function deletePrunedMemories(pruned) {
166
221
  // Guard: only delete files within known memory directories
167
- const allowedDirs = [PROJECT_MEMORY_DIR, GLOBAL_MEMORY_DIR].map(d => resolve(d));
222
+ const allowedDirs = [PROJECT_MEMORY_DIR, GLOBAL_MEMORY_DIR].map((d) => resolve(d));
168
223
  let count = 0;
169
224
  for (const m of pruned) {
170
225
  const resolved = resolve(m.filePath);
171
- if (!allowedDirs.some(d => resolved.startsWith(d + sep)))
226
+ if (!allowedDirs.some((d) => resolved.startsWith(d + sep)))
172
227
  continue;
173
228
  try {
174
229
  unlinkSync(m.filePath);
175
230
  count++;
176
231
  }
177
- catch { /* ignore */ }
232
+ catch {
233
+ /* ignore */
234
+ }
178
235
  }
179
236
  return count;
180
237
  }
181
238
  /** Write back decayed relevance score to file frontmatter */
182
239
  function persistDecayedRelevance(entry) {
183
240
  try {
184
- let raw = readFileSync(entry.filePath, 'utf-8');
241
+ let raw = readFileSync(entry.filePath, "utf-8");
185
242
  if (raw.match(/^relevance:/m)) {
186
243
  raw = raw.replace(/^relevance:\s*[0-9.]+$/m, `relevance: ${(entry.relevance ?? 0.5).toFixed(2)}`);
187
244
  writeFileSync(entry.filePath, raw);
188
245
  }
189
246
  }
190
- catch { /* ignore */ }
247
+ catch {
248
+ /* ignore */
249
+ }
191
250
  }
192
251
  /**
193
252
  * Run full memory consolidation: apply decay, delete pruned files,
@@ -208,8 +267,54 @@ export function consolidateMemories() {
208
267
  decayedCount++;
209
268
  }
210
269
  }
270
+ // Refresh MEMORY.md index after pruning
271
+ updateMemoryIndex(PROJECT_MEMORY_DIR);
272
+ updateMemoryIndex(GLOBAL_MEMORY_DIR);
211
273
  return { total: all.length, pruned: prunedCount, decayed: decayedCount };
212
274
  }
275
+ // ── User Profile ──
276
+ const USER_PROFILE_FILE = "USER.md";
277
+ const USER_PROFILE_MAX_CHARS = 2000;
278
+ /** Load the user profile from .oh/memory/USER.md */
279
+ export function loadUserProfile() {
280
+ const filePath = join(PROJECT_MEMORY_DIR, USER_PROFILE_FILE);
281
+ if (!existsSync(filePath))
282
+ return "";
283
+ try {
284
+ const raw = readFileSync(filePath, "utf-8");
285
+ const fmEnd = raw.indexOf("---", raw.indexOf("---") + 3);
286
+ return fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : raw.trim();
287
+ }
288
+ catch {
289
+ return "";
290
+ }
291
+ }
292
+ /** Update the user profile, truncating to max chars */
293
+ export function updateUserProfile(content) {
294
+ mkdirSync(PROJECT_MEMORY_DIR, { recursive: true });
295
+ // Truncate at last newline before max chars to avoid cutting mid-sentence
296
+ let truncated = content;
297
+ if (truncated.length > USER_PROFILE_MAX_CHARS) {
298
+ const lastNewline = content.lastIndexOf("\n", USER_PROFILE_MAX_CHARS);
299
+ truncated = lastNewline > 0 ? content.slice(0, lastNewline) : content.slice(0, USER_PROFILE_MAX_CHARS);
300
+ }
301
+ const md = `---
302
+ name: User Profile
303
+ type: user_profile
304
+ updatedAt: ${Date.now()}
305
+ ---
306
+
307
+ ${truncated}
308
+ `;
309
+ writeFileSync(join(PROJECT_MEMORY_DIR, USER_PROFILE_FILE), md);
310
+ }
311
+ /** Format user profile for system prompt injection */
312
+ export function userProfileToPrompt() {
313
+ const profile = loadUserProfile();
314
+ if (!profile)
315
+ return "";
316
+ return `# User Profile\n${profile}`;
317
+ }
213
318
  /**
214
319
  * Detect if recent assistant messages contain learnable patterns.
215
320
  * Returns structured memories to save, or empty array.
@@ -219,30 +324,26 @@ export async function detectMemories(provider, recentMessages, model) {
219
324
  if (recentMessages.length < 4)
220
325
  return [];
221
326
  // Extract assistant messages from recent turns
222
- const assistantMsgs = recentMessages
223
- .filter(m => m.role === 'assistant' && m.content.length > 50)
224
- .slice(-3);
327
+ const assistantMsgs = recentMessages.filter((m) => m.role === "assistant" && m.content.length > 50).slice(-3);
225
328
  if (assistantMsgs.length === 0)
226
329
  return [];
227
- const contextText = assistantMsgs
228
- .map(m => m.content.slice(0, 500))
229
- .join('\n---\n');
330
+ const contextText = assistantMsgs.map((m) => m.content.slice(0, 500)).join("\n---\n");
230
331
  const prompt = `Analyze this conversation snippet. If there are reusable learnings (coding conventions, project patterns, user preferences, debugging insights), extract them. Respond ONLY with a JSON array of objects with {name, type, description, content} or [] if nothing worth remembering.
231
332
 
232
- Types: "convention" (code style/patterns), "preference" (user likes/dislikes), "project" (architecture/decisions), "debugging" (solutions to problems)
333
+ Types: "user" (role/preferences), "feedback" (corrections/confirmations), "project" (goals/decisions), "reference" (external pointers)
233
334
 
234
335
  Keep each memory concise (1-2 sentences). Only extract non-obvious learnings.
235
336
 
236
337
  ${contextText}`;
237
338
  try {
238
- const response = await provider.complete([createUserMessage(prompt)], 'You are a memory extraction system. Respond ONLY with valid JSON.', undefined, model);
339
+ const response = await provider.complete([createUserMessage(prompt)], "You are a memory extraction system. Respond ONLY with valid JSON.", undefined, model);
239
340
  const jsonMatch = response.content.match(/\[[\s\S]*\]/);
240
341
  if (!jsonMatch)
241
342
  return [];
242
343
  const parsed = JSON.parse(jsonMatch[0]);
243
344
  if (!Array.isArray(parsed))
244
345
  return [];
245
- return parsed.filter((m) => m.name && m.type && m.content && typeof m.content === 'string');
346
+ return parsed.filter((m) => m.name && m.type && m.content && typeof m.content === "string");
246
347
  }
247
348
  catch {
248
349
  return [];
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Project auto-detection — detect language, framework, test runner, git state.
3
3
  */
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
4
  import { execSync } from "node:child_process";
5
+ import { existsSync, readFileSync } from "node:fs";
7
6
  import { platform, release } from "node:os";
7
+ import { join } from "node:path";
8
8
  const DETECTORS = [
9
9
  // [indicator, language, framework, packageManager, testRunner]
10
10
  ["pyproject.toml", "python", "", "pip", "pytest"],
@@ -30,7 +30,7 @@ const FRAMEWORKS = {
30
30
  "svelte.config.js": "Svelte",
31
31
  "manage.py": "Django",
32
32
  "tailwind.config.js": "Tailwind CSS",
33
- "Dockerfile": "Docker",
33
+ Dockerfile: "Docker",
34
34
  "docker-compose.yml": "Docker Compose",
35
35
  };
36
36
  export function detectProject(root) {
@@ -63,7 +63,9 @@ export function detectProject(root) {
63
63
  gitBranch = head.slice("ref: refs/heads/".length);
64
64
  }
65
65
  }
66
- catch { /* ignore */ }
66
+ catch {
67
+ /* ignore */
68
+ }
67
69
  }
68
70
  const hasReadme = ["README.md", "README.rst", "README.txt", "README"].some((f) => existsSync(join(projectRoot, f)));
69
71
  let description = "";
@@ -81,7 +83,17 @@ export function detectProject(root) {
81
83
  break;
82
84
  }
83
85
  }
84
- return { root: projectRoot, language, framework, packageManager, testRunner, hasGit, gitBranch, hasReadme, description };
86
+ return {
87
+ root: projectRoot,
88
+ language,
89
+ framework,
90
+ packageManager,
91
+ testRunner,
92
+ hasGit,
93
+ gitBranch,
94
+ hasReadme,
95
+ description,
96
+ };
85
97
  }
86
98
  export function projectContextToPrompt(ctx, model) {
87
99
  const parts = [];
@@ -123,7 +135,9 @@ export function projectContextToPrompt(ctx, model) {
123
135
  else if (refs.includes("master"))
124
136
  mainBranch = "master";
125
137
  }
126
- catch { /* ignore */ }
138
+ catch {
139
+ /* ignore */
140
+ }
127
141
  parts.push(`Main branch: ${mainBranch}`);
128
142
  // Git user
129
143
  try {
@@ -131,7 +145,9 @@ export function projectContextToPrompt(ctx, model) {
131
145
  if (user)
132
146
  parts.push(`Git user: ${user}`);
133
147
  }
134
- catch { /* ignore */ }
148
+ catch {
149
+ /* ignore */
150
+ }
135
151
  // Git status (brief)
136
152
  try {
137
153
  const status = execSync("git status --porcelain", { cwd: ctx.root, stdio: "pipe" }).toString().trim();
@@ -140,15 +156,19 @@ export function projectContextToPrompt(ctx, model) {
140
156
  parts.push(`\nStatus:\n${lines.join("\n")}${status.split("\n").length > 20 ? "\n..." : ""}`);
141
157
  }
142
158
  }
143
- catch { /* ignore */ }
159
+ catch {
160
+ /* ignore */
161
+ }
144
162
  // Recent commits
145
163
  try {
146
164
  const log = execSync("git log --oneline -5", { cwd: ctx.root, stdio: "pipe" }).toString().trim();
147
165
  if (log)
148
166
  parts.push(`\nRecent commits:\n${log}`);
149
167
  }
150
- catch { /* ignore */ }
168
+ catch {
169
+ /* ignore */
170
+ }
151
171
  }
152
- return "# Environment\n" + parts.map((p) => `- ${p}`).join("\n");
172
+ return `# Environment\n${parts.map((p) => `- ${p}`).join("\n")}`;
153
173
  }
154
174
  //# sourceMappingURL=onboarding.js.map
@@ -18,7 +18,7 @@ export type SkillMetadata = {
18
18
  args: string[] | undefined;
19
19
  content: string;
20
20
  filePath: string;
21
- source: 'project' | 'global' | 'plugin';
21
+ source: "project" | "global" | "plugin";
22
22
  };
23
23
  export type PluginManifest = {
24
24
  name: string;
@@ -48,6 +48,14 @@ export declare function discoverSkills(): SkillMetadata[];
48
48
  export declare function findSkill(name: string): SkillMetadata | null;
49
49
  /** Find skills that match a trigger condition */
50
50
  export declare function findTriggeredSkills(userMessage: string): SkillMetadata[];
51
+ /** Find a skill similar to a candidate (for patch-vs-create decision) */
52
+ export declare function findSimilarSkill(candidateName: string, candidateDescription: string, skills: Array<{
53
+ name: string;
54
+ description: string;
55
+ }>): {
56
+ name: string;
57
+ description: string;
58
+ } | null;
51
59
  /** Load a plugin manifest from a directory */
52
60
  export declare function loadPluginManifest(dir: string): PluginManifest | null;
53
61
  /** Discover plugins from node_modules */
@@ -10,11 +10,11 @@
10
10
  * 2. ~/.oh/skills/ (global)
11
11
  * 3. node_modules packages with "openharness-plugin" keyword
12
12
  */
13
- import { readFileSync, readdirSync, existsSync } from 'node:fs';
14
- import { join, basename } from 'node:path';
15
- import { homedir } from 'node:os';
16
- const PROJECT_SKILLS_DIR = join('.oh', 'skills');
17
- const GLOBAL_SKILLS_DIR = join(homedir(), '.oh', 'skills');
13
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { basename, join } from "node:path";
16
+ const PROJECT_SKILLS_DIR = join(".oh", "skills");
17
+ const GLOBAL_SKILLS_DIR = join(homedir(), ".oh", "skills");
18
18
  /** Parse YAML frontmatter from a skill markdown file */
19
19
  function parseSkillFrontmatter(content) {
20
20
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -33,10 +33,10 @@ function parseSkillFrontmatter(content) {
33
33
  result.trigger = triggerMatch[1].trim();
34
34
  const toolsMatch = frontmatter.match(/^tools:\s*\[(.+)\]$/m);
35
35
  if (toolsMatch)
36
- result.tools = toolsMatch[1].split(',').map(t => t.trim());
36
+ result.tools = toolsMatch[1].split(",").map((t) => t.trim());
37
37
  const argsMatch = frontmatter.match(/^args:\s*\[(.+)\]$/m);
38
38
  if (argsMatch)
39
- result.args = argsMatch[1].split(',').map(a => a.trim());
39
+ result.args = argsMatch[1].split(",").map((a) => a.trim());
40
40
  return result;
41
41
  }
42
42
  /** Load skills from a directory */
@@ -44,15 +44,15 @@ function loadSkillsFromDir(dir, source) {
44
44
  if (!existsSync(dir))
45
45
  return [];
46
46
  return readdirSync(dir)
47
- .filter(f => f.endsWith('.md'))
48
- .map(f => {
47
+ .filter((f) => f.endsWith(".md"))
48
+ .map((f) => {
49
49
  const filePath = join(dir, f);
50
50
  try {
51
- const content = readFileSync(filePath, 'utf-8');
51
+ const content = readFileSync(filePath, "utf-8");
52
52
  const meta = parseSkillFrontmatter(content);
53
53
  return {
54
- name: meta.name || basename(f, '.md'),
55
- description: meta.description || '',
54
+ name: meta.name || basename(f, ".md"),
55
+ description: meta.description || "",
56
56
  trigger: meta.trigger,
57
57
  tools: meta.tools,
58
58
  args: meta.args,
@@ -70,14 +70,14 @@ function loadSkillsFromDir(dir, source) {
70
70
  /** Discover all available skills from project + global dirs + installed plugins */
71
71
  export function discoverSkills() {
72
72
  const skills = [];
73
- skills.push(...loadSkillsFromDir(PROJECT_SKILLS_DIR, 'project'));
74
- skills.push(...loadSkillsFromDir(GLOBAL_SKILLS_DIR, 'global'));
73
+ skills.push(...loadSkillsFromDir(PROJECT_SKILLS_DIR, "project"));
74
+ skills.push(...loadSkillsFromDir(GLOBAL_SKILLS_DIR, "global"));
75
75
  // Load skills from installed marketplace plugins (namespaced as plugin-name:skill-name)
76
76
  try {
77
- const { getInstalledPlugins } = require('./marketplace.js');
77
+ const { getInstalledPlugins } = require("./marketplace.js");
78
78
  for (const plugin of getInstalledPlugins()) {
79
- const pluginSkillsDir = join(plugin.cachePath, 'skills');
80
- const pluginSkills = loadSkillsFromDir(pluginSkillsDir, 'plugin');
79
+ const pluginSkillsDir = join(plugin.cachePath, "skills");
80
+ const pluginSkills = loadSkillsFromDir(pluginSkillsDir, "plugin");
81
81
  // Namespace: prefix skill name with plugin name
82
82
  for (const skill of pluginSkills) {
83
83
  skill.name = `${plugin.name}:${skill.name}`;
@@ -85,30 +85,50 @@ export function discoverSkills() {
85
85
  skills.push(...pluginSkills);
86
86
  }
87
87
  }
88
- catch { /* marketplace module may not be loaded yet */ }
88
+ catch {
89
+ /* marketplace module may not be loaded yet */
90
+ }
89
91
  return skills;
90
92
  }
91
93
  /** Find a skill by name (case-insensitive) */
92
94
  export function findSkill(name) {
93
95
  const skills = discoverSkills();
94
- return skills.find(s => s.name.toLowerCase() === name.toLowerCase()) ?? null;
96
+ return skills.find((s) => s.name.toLowerCase() === name.toLowerCase()) ?? null;
95
97
  }
96
98
  /** Find skills that match a trigger condition */
97
99
  export function findTriggeredSkills(userMessage) {
98
100
  const skills = discoverSkills();
99
- return skills.filter(s => {
101
+ return skills.filter((s) => {
100
102
  if (!s.trigger)
101
103
  return false;
102
104
  return userMessage.toLowerCase().includes(s.trigger.toLowerCase());
103
105
  });
104
106
  }
107
+ /** Find a skill similar to a candidate (for patch-vs-create decision) */
108
+ export function findSimilarSkill(candidateName, candidateDescription, skills) {
109
+ const nameWords = new Set(candidateName.toLowerCase().split(/[-_ ]+/));
110
+ for (const skill of skills) {
111
+ const skillWords = new Set(skill.name.toLowerCase().split(/[-_ ]+/));
112
+ const overlap = [...nameWords].filter((w) => skillWords.has(w)).length;
113
+ if (overlap >= Math.ceil(nameWords.size * 0.5))
114
+ return skill;
115
+ const descWords = new Set(skill.description.toLowerCase().split(/\s+/));
116
+ const descOverlap = candidateDescription
117
+ .toLowerCase()
118
+ .split(/\s+/)
119
+ .filter((w) => descWords.has(w)).length;
120
+ if (descOverlap >= 3)
121
+ return skill;
122
+ }
123
+ return null;
124
+ }
105
125
  /** Load a plugin manifest from a directory */
106
126
  export function loadPluginManifest(dir) {
107
- const manifestPath = join(dir, 'openharness-plugin.json');
127
+ const manifestPath = join(dir, "openharness-plugin.json");
108
128
  if (!existsSync(manifestPath))
109
129
  return null;
110
130
  try {
111
- return JSON.parse(readFileSync(manifestPath, 'utf-8'));
131
+ return JSON.parse(readFileSync(manifestPath, "utf-8"));
112
132
  }
113
133
  catch {
114
134
  return null;
@@ -118,19 +138,19 @@ export function loadPluginManifest(dir) {
118
138
  export function discoverPlugins() {
119
139
  const plugins = [];
120
140
  // Check node_modules for packages with openharness-plugin.json
121
- const nodeModules = join('.', 'node_modules');
141
+ const nodeModules = join(".", "node_modules");
122
142
  if (!existsSync(nodeModules))
123
143
  return plugins;
124
144
  try {
125
145
  for (const pkg of readdirSync(nodeModules)) {
126
- if (pkg.startsWith('.'))
146
+ if (pkg.startsWith("."))
127
147
  continue;
128
148
  const pkgDir = join(nodeModules, pkg);
129
149
  const manifest = loadPluginManifest(pkgDir);
130
150
  if (manifest)
131
151
  plugins.push(manifest);
132
152
  // Scoped packages
133
- if (pkg.startsWith('@')) {
153
+ if (pkg.startsWith("@")) {
134
154
  try {
135
155
  for (const sub of readdirSync(pkgDir)) {
136
156
  const subDir = join(pkgDir, sub);
@@ -139,18 +159,22 @@ export function discoverPlugins() {
139
159
  plugins.push(subManifest);
140
160
  }
141
161
  }
142
- catch { /* ignore */ }
162
+ catch {
163
+ /* ignore */
164
+ }
143
165
  }
144
166
  }
145
167
  }
146
- catch { /* ignore */ }
168
+ catch {
169
+ /* ignore */
170
+ }
147
171
  return plugins;
148
172
  }
149
173
  /** Build a prompt listing available skills for the LLM */
150
174
  export function skillsToPrompt(skills) {
151
175
  if (skills.length === 0)
152
- return '';
153
- const lines = skills.map(s => `- ${s.name}: ${s.description}${s.trigger ? ` (auto-trigger: "${s.trigger}")` : ''}`);
154
- return `# Available Skills\nUse the Skill tool to invoke these:\n${lines.join('\n')}`;
176
+ return "";
177
+ const lines = skills.map((s) => `- ${s.name}: ${s.description}${s.trigger ? ` (auto-trigger: "${s.trigger}")` : ""}`);
178
+ return `# Available Skills\nUse the Skill tool to invoke these:\n${lines.join("\n")}`;
155
179
  }
156
180
  //# sourceMappingURL=plugins.js.map