clementine-agent 1.0.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 (190) hide show
  1. package/.env.example +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +795 -0
  4. package/dist/agent/agent-manager.d.ts +69 -0
  5. package/dist/agent/agent-manager.js +441 -0
  6. package/dist/agent/assistant.d.ts +225 -0
  7. package/dist/agent/assistant.js +3888 -0
  8. package/dist/agent/auto-update.d.ts +32 -0
  9. package/dist/agent/auto-update.js +186 -0
  10. package/dist/agent/daily-planner.d.ts +24 -0
  11. package/dist/agent/daily-planner.js +379 -0
  12. package/dist/agent/execution-advisor.d.ts +10 -0
  13. package/dist/agent/execution-advisor.js +272 -0
  14. package/dist/agent/hooks.d.ts +45 -0
  15. package/dist/agent/hooks.js +564 -0
  16. package/dist/agent/insight-engine.d.ts +66 -0
  17. package/dist/agent/insight-engine.js +225 -0
  18. package/dist/agent/intent-classifier.d.ts +48 -0
  19. package/dist/agent/intent-classifier.js +214 -0
  20. package/dist/agent/link-extractor.d.ts +19 -0
  21. package/dist/agent/link-extractor.js +90 -0
  22. package/dist/agent/mcp-bridge.d.ts +62 -0
  23. package/dist/agent/mcp-bridge.js +435 -0
  24. package/dist/agent/metacognition.d.ts +66 -0
  25. package/dist/agent/metacognition.js +221 -0
  26. package/dist/agent/orchestrator.d.ts +81 -0
  27. package/dist/agent/orchestrator.js +790 -0
  28. package/dist/agent/profiles.d.ts +22 -0
  29. package/dist/agent/profiles.js +91 -0
  30. package/dist/agent/prompt-cache.d.ts +24 -0
  31. package/dist/agent/prompt-cache.js +68 -0
  32. package/dist/agent/prompt-evolver.d.ts +28 -0
  33. package/dist/agent/prompt-evolver.js +279 -0
  34. package/dist/agent/role-scaffolds.d.ts +28 -0
  35. package/dist/agent/role-scaffolds.js +433 -0
  36. package/dist/agent/safe-restart.d.ts +41 -0
  37. package/dist/agent/safe-restart.js +150 -0
  38. package/dist/agent/self-improve.d.ts +66 -0
  39. package/dist/agent/self-improve.js +1706 -0
  40. package/dist/agent/session-event-log.d.ts +114 -0
  41. package/dist/agent/session-event-log.js +233 -0
  42. package/dist/agent/skill-extractor.d.ts +72 -0
  43. package/dist/agent/skill-extractor.js +435 -0
  44. package/dist/agent/source-mods.d.ts +61 -0
  45. package/dist/agent/source-mods.js +230 -0
  46. package/dist/agent/source-preflight.d.ts +25 -0
  47. package/dist/agent/source-preflight.js +100 -0
  48. package/dist/agent/stall-guard.d.ts +62 -0
  49. package/dist/agent/stall-guard.js +109 -0
  50. package/dist/agent/strategic-planner.d.ts +60 -0
  51. package/dist/agent/strategic-planner.js +352 -0
  52. package/dist/agent/team-bus.d.ts +89 -0
  53. package/dist/agent/team-bus.js +556 -0
  54. package/dist/agent/team-router.d.ts +26 -0
  55. package/dist/agent/team-router.js +37 -0
  56. package/dist/agent/tool-loop-detector.d.ts +59 -0
  57. package/dist/agent/tool-loop-detector.js +242 -0
  58. package/dist/agent/workflow-runner.d.ts +36 -0
  59. package/dist/agent/workflow-runner.js +317 -0
  60. package/dist/agent/workflow-variables.d.ts +16 -0
  61. package/dist/agent/workflow-variables.js +62 -0
  62. package/dist/channels/discord-agent-bot.d.ts +101 -0
  63. package/dist/channels/discord-agent-bot.js +881 -0
  64. package/dist/channels/discord-bot-manager.d.ts +80 -0
  65. package/dist/channels/discord-bot-manager.js +262 -0
  66. package/dist/channels/discord-utils.d.ts +51 -0
  67. package/dist/channels/discord-utils.js +293 -0
  68. package/dist/channels/discord.d.ts +12 -0
  69. package/dist/channels/discord.js +1832 -0
  70. package/dist/channels/slack-agent-bot.d.ts +73 -0
  71. package/dist/channels/slack-agent-bot.js +320 -0
  72. package/dist/channels/slack-bot-manager.d.ts +66 -0
  73. package/dist/channels/slack-bot-manager.js +236 -0
  74. package/dist/channels/slack-utils.d.ts +39 -0
  75. package/dist/channels/slack-utils.js +189 -0
  76. package/dist/channels/slack.d.ts +11 -0
  77. package/dist/channels/slack.js +196 -0
  78. package/dist/channels/telegram.d.ts +10 -0
  79. package/dist/channels/telegram.js +235 -0
  80. package/dist/channels/webhook.d.ts +9 -0
  81. package/dist/channels/webhook.js +78 -0
  82. package/dist/channels/whatsapp.d.ts +11 -0
  83. package/dist/channels/whatsapp.js +181 -0
  84. package/dist/cli/chat.d.ts +14 -0
  85. package/dist/cli/chat.js +220 -0
  86. package/dist/cli/cron.d.ts +17 -0
  87. package/dist/cli/cron.js +552 -0
  88. package/dist/cli/dashboard.d.ts +15 -0
  89. package/dist/cli/dashboard.js +17677 -0
  90. package/dist/cli/index.d.ts +3 -0
  91. package/dist/cli/index.js +2474 -0
  92. package/dist/cli/routes/delegations.d.ts +19 -0
  93. package/dist/cli/routes/delegations.js +154 -0
  94. package/dist/cli/routes/digest.d.ts +17 -0
  95. package/dist/cli/routes/digest.js +375 -0
  96. package/dist/cli/routes/goals.d.ts +14 -0
  97. package/dist/cli/routes/goals.js +258 -0
  98. package/dist/cli/routes/workflows.d.ts +18 -0
  99. package/dist/cli/routes/workflows.js +97 -0
  100. package/dist/cli/setup.d.ts +8 -0
  101. package/dist/cli/setup.js +619 -0
  102. package/dist/cli/tunnel.d.ts +35 -0
  103. package/dist/cli/tunnel.js +141 -0
  104. package/dist/config.d.ts +145 -0
  105. package/dist/config.js +278 -0
  106. package/dist/events/bus.d.ts +43 -0
  107. package/dist/events/bus.js +136 -0
  108. package/dist/gateway/cron-scheduler.d.ts +166 -0
  109. package/dist/gateway/cron-scheduler.js +1767 -0
  110. package/dist/gateway/delivery-queue.d.ts +30 -0
  111. package/dist/gateway/delivery-queue.js +110 -0
  112. package/dist/gateway/heartbeat-scheduler.d.ts +99 -0
  113. package/dist/gateway/heartbeat-scheduler.js +1298 -0
  114. package/dist/gateway/heartbeat.d.ts +3 -0
  115. package/dist/gateway/heartbeat.js +3 -0
  116. package/dist/gateway/lanes.d.ts +24 -0
  117. package/dist/gateway/lanes.js +76 -0
  118. package/dist/gateway/notifications.d.ts +29 -0
  119. package/dist/gateway/notifications.js +75 -0
  120. package/dist/gateway/router.d.ts +210 -0
  121. package/dist/gateway/router.js +1330 -0
  122. package/dist/index.d.ts +12 -0
  123. package/dist/index.js +1015 -0
  124. package/dist/memory/chunker.d.ts +28 -0
  125. package/dist/memory/chunker.js +226 -0
  126. package/dist/memory/consolidation.d.ts +44 -0
  127. package/dist/memory/consolidation.js +171 -0
  128. package/dist/memory/context-assembler.d.ts +50 -0
  129. package/dist/memory/context-assembler.js +149 -0
  130. package/dist/memory/embeddings.d.ts +38 -0
  131. package/dist/memory/embeddings.js +180 -0
  132. package/dist/memory/graph-store.d.ts +66 -0
  133. package/dist/memory/graph-store.js +613 -0
  134. package/dist/memory/mmr.d.ts +21 -0
  135. package/dist/memory/mmr.js +75 -0
  136. package/dist/memory/search.d.ts +26 -0
  137. package/dist/memory/search.js +67 -0
  138. package/dist/memory/store.d.ts +530 -0
  139. package/dist/memory/store.js +2022 -0
  140. package/dist/security/integrity.d.ts +24 -0
  141. package/dist/security/integrity.js +58 -0
  142. package/dist/security/patterns.d.ts +34 -0
  143. package/dist/security/patterns.js +110 -0
  144. package/dist/security/scanner.d.ts +32 -0
  145. package/dist/security/scanner.js +263 -0
  146. package/dist/tools/admin-tools.d.ts +12 -0
  147. package/dist/tools/admin-tools.js +1278 -0
  148. package/dist/tools/external-tools.d.ts +11 -0
  149. package/dist/tools/external-tools.js +1327 -0
  150. package/dist/tools/goal-tools.d.ts +9 -0
  151. package/dist/tools/goal-tools.js +159 -0
  152. package/dist/tools/mcp-server.d.ts +13 -0
  153. package/dist/tools/mcp-server.js +141 -0
  154. package/dist/tools/memory-tools.d.ts +10 -0
  155. package/dist/tools/memory-tools.js +568 -0
  156. package/dist/tools/session-tools.d.ts +6 -0
  157. package/dist/tools/session-tools.js +146 -0
  158. package/dist/tools/shared.d.ts +216 -0
  159. package/dist/tools/shared.js +340 -0
  160. package/dist/tools/team-tools.d.ts +6 -0
  161. package/dist/tools/team-tools.js +447 -0
  162. package/dist/tools/tool-meta.d.ts +34 -0
  163. package/dist/tools/tool-meta.js +133 -0
  164. package/dist/tools/vault-tools.d.ts +8 -0
  165. package/dist/tools/vault-tools.js +457 -0
  166. package/dist/types.d.ts +716 -0
  167. package/dist/types.js +16 -0
  168. package/dist/vault-migrations/0001-add-execution-framework.d.ts +10 -0
  169. package/dist/vault-migrations/0001-add-execution-framework.js +47 -0
  170. package/dist/vault-migrations/0002-add-agentic-communication.d.ts +12 -0
  171. package/dist/vault-migrations/0002-add-agentic-communication.js +79 -0
  172. package/dist/vault-migrations/0003-update-execution-pipeline-narration.d.ts +11 -0
  173. package/dist/vault-migrations/0003-update-execution-pipeline-narration.js +73 -0
  174. package/dist/vault-migrations/helpers.d.ts +14 -0
  175. package/dist/vault-migrations/helpers.js +44 -0
  176. package/dist/vault-migrations/runner.d.ts +14 -0
  177. package/dist/vault-migrations/runner.js +139 -0
  178. package/dist/vault-migrations/types.d.ts +42 -0
  179. package/dist/vault-migrations/types.js +9 -0
  180. package/install.sh +320 -0
  181. package/package.json +84 -0
  182. package/scripts/postinstall.js +125 -0
  183. package/vault/00-System/AGENTS.md +66 -0
  184. package/vault/00-System/CRON.md +71 -0
  185. package/vault/00-System/HEARTBEAT.md +58 -0
  186. package/vault/00-System/MEMORY.md +16 -0
  187. package/vault/00-System/SOUL.md +96 -0
  188. package/vault/05-Tasks/TASKS.md +19 -0
  189. package/vault/06-Templates/_Daily-Template.md +28 -0
  190. package/vault/06-Templates/_People-Template.md +22 -0
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Clementine TypeScript — Procedural Memory (Skill Extraction + Retrieval).
3
+ *
4
+ * Extracts reusable skill documents from successful multi-step executions
5
+ * (unleashed jobs, cron runs, complex chat interactions) and stores them
6
+ * as markdown files in vault/00-System/skills/ (global) or
7
+ * vault/00-System/agents/{slug}/skills/ (agent-scoped).
8
+ *
9
+ * New skills land in a pending queue first. The owner approves or rejects
10
+ * them via chat or dashboard before they become active.
11
+ *
12
+ * Skills are automatically indexed by the memory store FTS5 and retrieved
13
+ * during context search to avoid re-deriving procedures from scratch.
14
+ */
15
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, unlinkSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import matter from 'gray-matter';
18
+ import pino from 'pino';
19
+ import { VAULT_DIR, AGENTS_DIR, PENDING_SKILLS_DIR } from '../config.js';
20
+ const logger = pino({ name: 'clementine.skills' });
21
+ const GLOBAL_SKILLS_DIR = path.join(VAULT_DIR, '00-System', 'skills');
22
+ function agentSkillsDir(agentSlug) {
23
+ return path.join(AGENTS_DIR, agentSlug, 'skills');
24
+ }
25
+ function ensureDirs() {
26
+ for (const dir of [GLOBAL_SKILLS_DIR, PENDING_SKILLS_DIR]) {
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true });
29
+ }
30
+ }
31
+ // ── Skill Extraction ────────────────────────────────────────────────
32
+ /**
33
+ * Extract a reusable skill from a successful execution.
34
+ * New skills go to the pending queue — owner must approve before they activate.
35
+ * Merges into existing approved skills directly (no re-approval needed).
36
+ */
37
+ export async function extractSkill(assistant, context) {
38
+ try {
39
+ const extractionPrompt = `You are a skill extraction agent. Analyze this successful task execution and distill it into a reusable procedure.\n\n` +
40
+ `## Original Task\n${context.prompt.slice(0, 2000)}\n\n` +
41
+ `## Successful Output\n${context.output.slice(0, 3000)}\n\n` +
42
+ `## Tools Used\n${context.toolsUsed.join(', ') || '(none)'}\n\n` +
43
+ `## Instructions\n` +
44
+ `Extract a reusable skill document. The skill should be general enough to apply to similar future tasks, ` +
45
+ `but specific enough to be actionable.\n\n` +
46
+ `Output ONLY a JSON object (no markdown, no explanation):\n` +
47
+ `{\n` +
48
+ ` "title": "Short descriptive title",\n` +
49
+ ` "description": "1-2 sentence description of what this skill does",\n` +
50
+ ` "triggers": ["keyword1", "keyword2", "phrase that should activate this"],\n` +
51
+ ` "steps": "Step-by-step markdown procedure with numbered steps",\n` +
52
+ ` "toolsUsed": ["tool1", "tool2"]\n` +
53
+ `}\n\n` +
54
+ `If this task is too trivial or one-off to be worth saving as a skill, output: { "skip": true }`;
55
+ const result = await assistant.runPlanStep('skill-extract', extractionPrompt, {
56
+ tier: 1,
57
+ maxTurns: 1,
58
+ disableTools: true,
59
+ });
60
+ // Parse JSON response
61
+ const jsonMatch = result.match(/\{[\s\S]*\}/);
62
+ if (!jsonMatch)
63
+ return null;
64
+ const parsed = JSON.parse(jsonMatch[0]);
65
+ if (parsed.skip) {
66
+ logger.debug({ source: context.source, sourceJob: context.sourceJob }, 'Skill extraction skipped — too trivial');
67
+ return null;
68
+ }
69
+ const name = slugify(parsed.title);
70
+ const now = new Date().toISOString();
71
+ const skill = {
72
+ name,
73
+ title: parsed.title,
74
+ description: parsed.description,
75
+ triggers: parsed.triggers ?? [],
76
+ source: context.source,
77
+ sourceJob: context.sourceJob,
78
+ agentSlug: context.agentSlug,
79
+ steps: parsed.steps,
80
+ toolsUsed: parsed.toolsUsed ?? context.toolsUsed,
81
+ useCount: 0,
82
+ createdAt: now,
83
+ updatedAt: now,
84
+ };
85
+ // Check for duplicate/similar skills in active dirs (and pending) before saving
86
+ const existing = findSimilarActiveSkill(skill.triggers, context.agentSlug);
87
+ if (existing) {
88
+ logger.info({ name: existing.name, newTitle: skill.title }, 'Similar skill exists — merging');
89
+ return mergeSkill(assistant, existing, skill);
90
+ }
91
+ // Check pending for duplicates too — don't queue it twice
92
+ const existingPending = findSimilarPendingSkill(skill.triggers);
93
+ if (existingPending) {
94
+ logger.info({ name: existingPending.name, newTitle: skill.title }, 'Similar pending skill exists — skipping');
95
+ return null;
96
+ }
97
+ // Save to pending queue — owner approves before it goes live
98
+ savePendingSkill(skill);
99
+ // Notify owner via callback if wired
100
+ const cb = assistant.onSkillProposed;
101
+ if (cb) {
102
+ try {
103
+ cb(skill);
104
+ }
105
+ catch { /* non-fatal */ }
106
+ }
107
+ return skill;
108
+ }
109
+ catch (err) {
110
+ logger.error({ err, source: context.source }, 'Skill extraction failed');
111
+ return null;
112
+ }
113
+ }
114
+ // ── Skill Storage ───────────────────────────────────────────────────
115
+ /** Save a skill to the pending queue (JSON, awaiting approval). */
116
+ function savePendingSkill(skill) {
117
+ ensureDirs();
118
+ const filePath = path.join(PENDING_SKILLS_DIR, `${skill.name}.json`);
119
+ writeFileSync(filePath, JSON.stringify(skill, null, 2));
120
+ logger.info({ name: skill.name, source: skill.source }, 'Skill queued for approval');
121
+ }
122
+ /** Save an approved skill as a formatted markdown file. Agent-scoped if agentSlug set. */
123
+ function saveActiveSkill(skill) {
124
+ ensureDirs();
125
+ const targetDir = skill.agentSlug ? agentSkillsDir(skill.agentSlug) : GLOBAL_SKILLS_DIR;
126
+ if (!existsSync(targetDir))
127
+ mkdirSync(targetDir, { recursive: true });
128
+ // gray-matter's YAML dumper throws on undefined values — omit them
129
+ const frontmatter = {
130
+ title: skill.title,
131
+ description: skill.description,
132
+ triggers: skill.triggers,
133
+ source: skill.source,
134
+ toolsUsed: skill.toolsUsed,
135
+ useCount: skill.useCount,
136
+ createdAt: skill.createdAt,
137
+ updatedAt: skill.updatedAt,
138
+ };
139
+ if (skill.sourceJob)
140
+ frontmatter.sourceJob = skill.sourceJob;
141
+ if (skill.agentSlug)
142
+ frontmatter.agentSlug = skill.agentSlug;
143
+ if (skill.lastUsed)
144
+ frontmatter.lastUsed = skill.lastUsed;
145
+ const content = matter.stringify(`\n# ${skill.title}\n\n${skill.description}\n\n## Procedure\n\n${skill.steps}\n`, frontmatter);
146
+ const filePath = path.join(targetDir, `${skill.name}.md`);
147
+ // Backup existing before overwrite
148
+ if (existsSync(filePath)) {
149
+ try {
150
+ copyFileSync(filePath, filePath.replace(/\.md$/, '.md.bak'));
151
+ }
152
+ catch { /* best-effort */ }
153
+ }
154
+ writeFileSync(filePath, content);
155
+ logger.info({ name: skill.name, source: skill.source, agentSlug: skill.agentSlug ?? 'global' }, 'Skill saved');
156
+ }
157
+ // ── Pending Skill Management ────────────────────────────────────────
158
+ /** Move a pending skill to the active skills directory. */
159
+ export function approvePendingSkill(name) {
160
+ ensureDirs();
161
+ const pendingFile = path.join(PENDING_SKILLS_DIR, `${name}.json`);
162
+ if (!existsSync(pendingFile)) {
163
+ return { ok: false, message: `Pending skill not found: ${name}` };
164
+ }
165
+ try {
166
+ const skill = JSON.parse(readFileSync(pendingFile, 'utf-8'));
167
+ skill.updatedAt = new Date().toISOString();
168
+ saveActiveSkill(skill);
169
+ unlinkSync(pendingFile);
170
+ logger.info({ name }, 'Pending skill approved and activated');
171
+ return { ok: true, message: `Skill **${skill.title}** is now active${skill.agentSlug ? ` for ${skill.agentSlug}` : ' (global)'}.` };
172
+ }
173
+ catch (err) {
174
+ logger.error({ err, name }, 'Failed to approve pending skill');
175
+ return { ok: false, message: `Failed to approve skill: ${err instanceof Error ? err.message : String(err)}` };
176
+ }
177
+ }
178
+ /** Delete a pending skill (reject it). */
179
+ export function rejectPendingSkill(name) {
180
+ const pendingFile = path.join(PENDING_SKILLS_DIR, `${name}.json`);
181
+ if (!existsSync(pendingFile)) {
182
+ return { ok: false, message: `Pending skill not found: ${name}` };
183
+ }
184
+ try {
185
+ unlinkSync(pendingFile);
186
+ logger.info({ name }, 'Pending skill rejected');
187
+ return { ok: true, message: `Skill **${name}** rejected and removed.` };
188
+ }
189
+ catch (err) {
190
+ return { ok: false, message: `Failed to reject skill: ${err instanceof Error ? err.message : String(err)}` };
191
+ }
192
+ }
193
+ /** List all skills waiting for approval. */
194
+ export function listPendingSkills() {
195
+ if (!existsSync(PENDING_SKILLS_DIR))
196
+ return [];
197
+ return readdirSync(PENDING_SKILLS_DIR)
198
+ .filter(f => f.endsWith('.json'))
199
+ .map(f => {
200
+ try {
201
+ const skill = JSON.parse(readFileSync(path.join(PENDING_SKILLS_DIR, f), 'utf-8'));
202
+ return {
203
+ name: skill.name,
204
+ title: skill.title,
205
+ description: skill.description,
206
+ source: skill.source,
207
+ agentSlug: skill.agentSlug,
208
+ createdAt: skill.createdAt,
209
+ };
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ })
215
+ .filter(Boolean);
216
+ }
217
+ // ── Similarity Detection ────────────────────────────────────────────
218
+ /** Find an active skill (global or agent-scoped) with overlapping triggers. */
219
+ function findSimilarActiveSkill(triggers, agentSlug) {
220
+ const dirs = [];
221
+ if (agentSlug) {
222
+ const ad = agentSkillsDir(agentSlug);
223
+ if (existsSync(ad))
224
+ dirs.push(ad);
225
+ }
226
+ if (existsSync(GLOBAL_SKILLS_DIR))
227
+ dirs.push(GLOBAL_SKILLS_DIR);
228
+ return findSimilarInDirs(triggers, dirs, false);
229
+ }
230
+ /** Find a pending skill with overlapping triggers (to avoid duplicates in queue). */
231
+ function findSimilarPendingSkill(triggers) {
232
+ if (!existsSync(PENDING_SKILLS_DIR))
233
+ return null;
234
+ const triggerSet = new Set(triggers.map(t => t.toLowerCase()));
235
+ for (const f of readdirSync(PENDING_SKILLS_DIR).filter(f => f.endsWith('.json'))) {
236
+ try {
237
+ const skill = JSON.parse(readFileSync(path.join(PENDING_SKILLS_DIR, f), 'utf-8'));
238
+ const overlap = skill.triggers.filter(t => triggerSet.has(t.toLowerCase()));
239
+ if (overlap.length >= 2)
240
+ return skill;
241
+ }
242
+ catch { /* skip */ }
243
+ }
244
+ return null;
245
+ }
246
+ /** Shared similarity logic across markdown skill dirs. */
247
+ function findSimilarInDirs(triggers, dirs, _isPending) {
248
+ const triggerSet = new Set(triggers.map(t => t.toLowerCase()));
249
+ for (const dir of dirs) {
250
+ for (const file of readdirSync(dir).filter(f => f.endsWith('.md'))) {
251
+ try {
252
+ const content = readFileSync(path.join(dir, file), 'utf-8');
253
+ const parsed = matter(content);
254
+ const existingTriggers = parsed.data.triggers ?? [];
255
+ const overlap = existingTriggers.filter(t => triggerSet.has(t.toLowerCase()));
256
+ if (overlap.length >= 2) {
257
+ return {
258
+ name: file.replace('.md', ''),
259
+ title: parsed.data.title ?? file,
260
+ description: parsed.data.description ?? '',
261
+ triggers: existingTriggers,
262
+ source: parsed.data.source ?? 'manual',
263
+ sourceJob: parsed.data.sourceJob,
264
+ agentSlug: parsed.data.agentSlug,
265
+ steps: parsed.content,
266
+ toolsUsed: parsed.data.toolsUsed ?? [],
267
+ useCount: parsed.data.useCount ?? 0,
268
+ lastUsed: parsed.data.lastUsed,
269
+ createdAt: parsed.data.createdAt ?? new Date().toISOString(),
270
+ updatedAt: parsed.data.updatedAt ?? new Date().toISOString(),
271
+ };
272
+ }
273
+ }
274
+ catch { /* skip malformed */ }
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+ /** Merge a new skill into an existing approved one by refining the procedure. */
280
+ async function mergeSkill(assistant, existing, incoming) {
281
+ try {
282
+ const mergePrompt = `You have an existing skill and a new execution of a similar task. Merge them into a single improved skill.\n\n` +
283
+ `## Existing Skill: ${existing.title}\n${existing.steps}\n\n` +
284
+ `## New Execution\nTitle: ${incoming.title}\n${incoming.steps}\n\n` +
285
+ `Produce an improved procedure that incorporates lessons from both. ` +
286
+ `Keep what works, add new steps or improvements from the new execution, ` +
287
+ `remove anything that was shown to be unnecessary.\n\n` +
288
+ `Output ONLY a JSON object:\n` +
289
+ `{ "steps": "improved markdown procedure", "triggers": ["merged trigger list"] }`;
290
+ const result = await assistant.runPlanStep('skill-merge', mergePrompt, {
291
+ tier: 1,
292
+ maxTurns: 1,
293
+ disableTools: true,
294
+ });
295
+ const jsonMatch = result.match(/\{[\s\S]*\}/);
296
+ if (!jsonMatch)
297
+ return null;
298
+ const parsed = JSON.parse(jsonMatch[0]);
299
+ // Merge triggers (union)
300
+ const allTriggers = [...new Set([...existing.triggers, ...incoming.triggers, ...(parsed.triggers ?? [])])];
301
+ const merged = {
302
+ ...existing,
303
+ steps: parsed.steps ?? existing.steps,
304
+ triggers: allTriggers,
305
+ toolsUsed: [...new Set([...existing.toolsUsed, ...incoming.toolsUsed])],
306
+ useCount: existing.useCount,
307
+ updatedAt: new Date().toISOString(),
308
+ };
309
+ // Merges go directly to active (existing skill was already approved)
310
+ saveActiveSkill(merged);
311
+ logger.info({ name: merged.name }, 'Skill merged and updated');
312
+ return merged;
313
+ }
314
+ catch (err) {
315
+ logger.error({ err }, 'Skill merge failed');
316
+ return null;
317
+ }
318
+ }
319
+ export function searchSkills(query, limit = 3, agentSlug) {
320
+ const dirs = [];
321
+ // Agent-scoped skills get priority (boost=2)
322
+ if (agentSlug) {
323
+ const agentDir = agentSkillsDir(agentSlug);
324
+ if (existsSync(agentDir))
325
+ dirs.push({ dir: agentDir, boost: 2 });
326
+ }
327
+ // Global skills (no boost)
328
+ if (existsSync(GLOBAL_SKILLS_DIR))
329
+ dirs.push({ dir: GLOBAL_SKILLS_DIR, boost: 0 });
330
+ if (dirs.length === 0)
331
+ return [];
332
+ const queryWords = query.toLowerCase().split(/\s+/).filter(w => w.length > 2);
333
+ const results = [];
334
+ const seen = new Set();
335
+ for (const { dir, boost } of dirs) {
336
+ const files = readdirSync(dir).filter(f => f.endsWith('.md'));
337
+ for (const file of files) {
338
+ const name = file.replace('.md', '');
339
+ if (seen.has(name))
340
+ continue;
341
+ seen.add(name);
342
+ try {
343
+ const raw = readFileSync(path.join(dir, file), 'utf-8');
344
+ const parsed = matter(raw);
345
+ const triggers = parsed.data.triggers ?? [];
346
+ const title = parsed.data.title ?? '';
347
+ const description = parsed.data.description ?? '';
348
+ // Score: trigger matches (high weight) + title/description word overlap + agent boost
349
+ let score = 0;
350
+ const triggerLower = triggers.map(t => t.toLowerCase());
351
+ for (const word of queryWords) {
352
+ for (const trigger of triggerLower) {
353
+ if (trigger.includes(word) || word.includes(trigger))
354
+ score += 3;
355
+ }
356
+ if (title.toLowerCase().includes(word))
357
+ score += 2;
358
+ if (description.toLowerCase().includes(word))
359
+ score += 1;
360
+ }
361
+ if (score > 0) {
362
+ results.push({
363
+ name,
364
+ title,
365
+ content: parsed.content.slice(0, 1500),
366
+ score: score + boost,
367
+ toolsUsed: parsed.data.toolsUsed ?? [],
368
+ attachments: parsed.data.attachments ?? [],
369
+ skillDir: dir,
370
+ });
371
+ }
372
+ }
373
+ catch { /* skip */ }
374
+ }
375
+ }
376
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
377
+ }
378
+ /** Record that a skill was used (bump use count). */
379
+ export function recordSkillUse(skillName, agentSlug) {
380
+ try {
381
+ // Check agent dir first, then global
382
+ const dirs = agentSlug ? [agentSkillsDir(agentSlug), GLOBAL_SKILLS_DIR] : [GLOBAL_SKILLS_DIR];
383
+ for (const dir of dirs) {
384
+ const filePath = path.join(dir, `${skillName}.md`);
385
+ if (!existsSync(filePath))
386
+ continue;
387
+ const raw = readFileSync(filePath, 'utf-8');
388
+ const parsed = matter(raw);
389
+ parsed.data.useCount = (parsed.data.useCount ?? 0) + 1;
390
+ parsed.data.lastUsed = new Date().toISOString();
391
+ writeFileSync(filePath, matter.stringify(parsed.content, parsed.data));
392
+ return;
393
+ }
394
+ }
395
+ catch { /* non-fatal */ }
396
+ }
397
+ /** List all active skills (global + all agent-scoped). */
398
+ export function listSkills(agentSlug) {
399
+ const results = [];
400
+ const dirs = [];
401
+ if (agentSlug) {
402
+ dirs.push({ dir: agentSkillsDir(agentSlug), slug: agentSlug });
403
+ }
404
+ else {
405
+ dirs.push({ dir: GLOBAL_SKILLS_DIR });
406
+ }
407
+ for (const { dir, slug } of dirs) {
408
+ if (!existsSync(dir))
409
+ continue;
410
+ for (const f of readdirSync(dir).filter(f => f.endsWith('.md'))) {
411
+ try {
412
+ const parsed = matter(readFileSync(path.join(dir, f), 'utf-8'));
413
+ results.push({
414
+ name: f.replace('.md', ''),
415
+ title: parsed.data.title ?? f,
416
+ source: parsed.data.source ?? 'unknown',
417
+ useCount: parsed.data.useCount ?? 0,
418
+ updatedAt: parsed.data.updatedAt ?? '',
419
+ agentSlug: slug,
420
+ });
421
+ }
422
+ catch { /* skip */ }
423
+ }
424
+ }
425
+ return results;
426
+ }
427
+ // ── Helpers ─────────────────────────────────────────────────────────
428
+ function slugify(text) {
429
+ return text
430
+ .toLowerCase()
431
+ .replace(/[^a-z0-9]+/g, '-')
432
+ .replace(/^-+|-+$/g, '')
433
+ .slice(0, 60);
434
+ }
435
+ //# sourceMappingURL=skill-extractor.js.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Clementine TypeScript — Source Modification Registry.
3
+ *
4
+ * Tracks self-improve source edits in ~/.clementine/ (not in git).
5
+ * When `clementine update` pulls new code, the reconciliation step
6
+ * re-applies active modifications that are still needed.
7
+ *
8
+ * This decouples user-local improvements from the upstream repo,
9
+ * so `git pull` is always clean and user customizations survive.
10
+ */
11
+ export interface SourceModRecord {
12
+ id: string;
13
+ files: string[];
14
+ reason: string;
15
+ description: string;
16
+ experimentId?: string;
17
+ appliedAt: string;
18
+ status: 'active' | 'superseded' | 'needs-reconciliation' | 'failed';
19
+ }
20
+ /** Record a new source modification with before/after file snapshots. */
21
+ export declare function recordSourceMod(id: string, files: Array<{
22
+ relativePath: string;
23
+ beforeContent: string;
24
+ afterContent: string;
25
+ }>, opts: {
26
+ reason: string;
27
+ description: string;
28
+ experimentId?: string;
29
+ }): void;
30
+ /** Load all source mod records. */
31
+ export declare function loadSourceMods(): SourceModRecord[];
32
+ /** Load only active mods. */
33
+ export declare function loadActiveSourceMods(): SourceModRecord[];
34
+ /** Update a mod's status. */
35
+ export declare function updateModStatus(id: string, status: SourceModRecord['status']): void;
36
+ /** Remove a mod and its snapshots entirely. */
37
+ export declare function removeSourceMod(id: string): void;
38
+ /** Read the stored "after" content for a mod's file. */
39
+ export declare function readModAfterContent(id: string, relativePath: string): string | null;
40
+ /** Read the stored "before" content for a mod's file (for rollback). */
41
+ export declare function readModBeforeContent(id: string, relativePath: string): string | null;
42
+ /** Rollback a source mod by restoring the "before" snapshots. */
43
+ export declare function rollbackSourceMod(id: string, pkgDir: string): boolean;
44
+ export interface ReconcileResult {
45
+ reapplied: string[];
46
+ superseded: string[];
47
+ needsReconciliation: string[];
48
+ failed: string[];
49
+ }
50
+ /**
51
+ * Reconcile active source mods after an upstream update.
52
+ *
53
+ * For each active mod:
54
+ * 1. If the current file already matches our "after" content → superseded
55
+ * 2. If the current file matches our "before" content → re-apply directly
56
+ * 3. If the current file is different from both → needs LLM reconciliation
57
+ *
58
+ * After re-applying, runs a typecheck. Failures get reverted.
59
+ */
60
+ export declare function reconcileSourceMods(pkgDir: string): ReconcileResult;
61
+ //# sourceMappingURL=source-mods.d.ts.map