flint-agent 1.14.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 (171) hide show
  1. package/.env.example +108 -0
  2. package/CHANGELOG.md +55 -0
  3. package/FEATURES.md +298 -0
  4. package/LICENSE +21 -0
  5. package/README.md +435 -0
  6. package/bin/flint.js +47 -0
  7. package/config/classifier-prompt.md +218 -0
  8. package/config/models-curated.json +4 -0
  9. package/config/providers.json +74 -0
  10. package/package.json +92 -0
  11. package/patches/ink+6.8.0.patch +78 -0
  12. package/profiles/desktop.md +65 -0
  13. package/profiles/generic.md +20 -0
  14. package/profiles/marketer.md +20 -0
  15. package/profiles/profiles.json +34 -0
  16. package/profiles/ux-reviewer.md +25 -0
  17. package/src/agent/agent.js +1743 -0
  18. package/src/agent/auto.js +346 -0
  19. package/src/agent/backoff.js +143 -0
  20. package/src/agent/compression.js +310 -0
  21. package/src/agent/content-resolver.js +180 -0
  22. package/src/agent/flow-controller.js +309 -0
  23. package/src/agent/intent-manifest.js +231 -0
  24. package/src/agent/intent-timeout.js +46 -0
  25. package/src/agent/intent.js +633 -0
  26. package/src/agent/knowledge.js +114 -0
  27. package/src/agent/learning.js +180 -0
  28. package/src/agent/modes.js +187 -0
  29. package/src/agent/outcome-ask.js +91 -0
  30. package/src/agent/project-context.js +76 -0
  31. package/src/agent/prompt-budget.js +117 -0
  32. package/src/agent/reflection-extractor.js +140 -0
  33. package/src/agent/steering.js +86 -0
  34. package/src/agent/supervisor.js +430 -0
  35. package/src/agent/swap.js +443 -0
  36. package/src/agent/system-prompt.js +446 -0
  37. package/src/agent/time-stamp.js +48 -0
  38. package/src/agent/tool-guard.js +201 -0
  39. package/src/agent/toolcall-text.js +162 -0
  40. package/src/agent/usage.js +297 -0
  41. package/src/agent/vision.js +94 -0
  42. package/src/agent/watchdog.js +139 -0
  43. package/src/agent/workspace-changes.js +177 -0
  44. package/src/api/address.js +14 -0
  45. package/src/api/client.js +280 -0
  46. package/src/api/server.js +535 -0
  47. package/src/api/stream-pipe.js +113 -0
  48. package/src/app-state.js +39 -0
  49. package/src/bootstrap.js +501 -0
  50. package/src/bus/drain-loop.js +497 -0
  51. package/src/bus/index.js +270 -0
  52. package/src/bus/plugins.js +65 -0
  53. package/src/child-idle.js +14 -0
  54. package/src/cli.js +118 -0
  55. package/src/commands/commands.js +1297 -0
  56. package/src/commands/registry.js +132 -0
  57. package/src/components/App.js +491 -0
  58. package/src/components/CarefulMenu.js +145 -0
  59. package/src/components/HistoryWriter.js +86 -0
  60. package/src/components/LineInput.js +69 -0
  61. package/src/components/LiveZone.js +294 -0
  62. package/src/components/OverlayMenu.js +179 -0
  63. package/src/components/SystemPanel.js +156 -0
  64. package/src/components/Table.js +54 -0
  65. package/src/config.js +249 -0
  66. package/src/free-models.js +230 -0
  67. package/src/index.js +1111 -0
  68. package/src/input-handler.js +13 -0
  69. package/src/input-text.js +123 -0
  70. package/src/launcher.js +129 -0
  71. package/src/logging/api-log.js +95 -0
  72. package/src/logging/chat-log-follower.js +113 -0
  73. package/src/logging/chat-log.js +15 -0
  74. package/src/logging/log-collector.js +182 -0
  75. package/src/logging/logger.js +112 -0
  76. package/src/logging/tool-log.js +20 -0
  77. package/src/mcp-client.js +314 -0
  78. package/src/memory/conversation-digest.js +113 -0
  79. package/src/memory/extract-facts.js +98 -0
  80. package/src/memory/facts.js +181 -0
  81. package/src/memory/inbox.js +63 -0
  82. package/src/memory/markdown.js +38 -0
  83. package/src/memory/patterns.js +185 -0
  84. package/src/memory/project.js +66 -0
  85. package/src/memory/reflections.js +74 -0
  86. package/src/memory/retrieval.js +84 -0
  87. package/src/memory/rules.js +105 -0
  88. package/src/memory/session-facts.js +125 -0
  89. package/src/memory/skills.js +191 -0
  90. package/src/memory/sqlite-store.js +653 -0
  91. package/src/memory/store.js +208 -0
  92. package/src/memory/tools.js +196 -0
  93. package/src/memory/user-model.js +86 -0
  94. package/src/message-handler.js +775 -0
  95. package/src/model-check.js +218 -0
  96. package/src/plugins/loader.js +120 -0
  97. package/src/plugins/manager.js +88 -0
  98. package/src/production-env.js +22 -0
  99. package/src/profiles.js +42 -0
  100. package/src/providers/adapters/anthropic.js +270 -0
  101. package/src/providers/adapters/openai.js +120 -0
  102. package/src/providers/keys-dpapi.js +41 -0
  103. package/src/providers/keys-fallback.js +31 -0
  104. package/src/providers/keys.js +132 -0
  105. package/src/providers/models.js +154 -0
  106. package/src/providers/registry.js +56 -0
  107. package/src/providers/state.js +56 -0
  108. package/src/registry.js +96 -0
  109. package/src/restart.js +29 -0
  110. package/src/sandbox/backend.js +130 -0
  111. package/src/security/api-auth.js +132 -0
  112. package/src/security/audit.js +98 -0
  113. package/src/security/child-policy.js +41 -0
  114. package/src/security/command-guard.js +173 -0
  115. package/src/security/content-fence.js +250 -0
  116. package/src/security/content-validator.js +132 -0
  117. package/src/security/index.js +143 -0
  118. package/src/security/network-guard.js +126 -0
  119. package/src/security/pairing.js +180 -0
  120. package/src/security/path-guard.js +140 -0
  121. package/src/security/persona-guard.js +67 -0
  122. package/src/security/policies.js +452 -0
  123. package/src/security/safety-constants.js +34 -0
  124. package/src/security/watchdog.js +107 -0
  125. package/src/sessions.js +130 -0
  126. package/src/spend.js +97 -0
  127. package/src/startup-watchdog.js +59 -0
  128. package/src/stdio/args.js +71 -0
  129. package/src/stdio/guard.js +59 -0
  130. package/src/stdio/protocol.js +167 -0
  131. package/src/stdio/run.js +106 -0
  132. package/src/stdio/session.js +180 -0
  133. package/src/store/agent-slice.js +306 -0
  134. package/src/store/dataset-slice.js +73 -0
  135. package/src/store/index.js +22 -0
  136. package/src/store/process-slice.js +135 -0
  137. package/src/store/session-slice.js +191 -0
  138. package/src/store/ui-slice.js +119 -0
  139. package/src/tasks/db.js +184 -0
  140. package/src/tasks/queries.js +589 -0
  141. package/src/tools/agent-tools.js +473 -0
  142. package/src/tools/checkpoint.js +152 -0
  143. package/src/tools/command-approvals.js +180 -0
  144. package/src/tools/dataset.js +50 -0
  145. package/src/tools/filesystem.js +682 -0
  146. package/src/tools/inbox-tools.js +48 -0
  147. package/src/tools/mesh.js +135 -0
  148. package/src/tools/own-env.js +136 -0
  149. package/src/tools/permissions.js +681 -0
  150. package/src/tools/plugin-tools.js +123 -0
  151. package/src/tools/process-tools.js +595 -0
  152. package/src/tools/registry.js +307 -0
  153. package/src/tools/swap-tools.js +72 -0
  154. package/src/tools/system.js +662 -0
  155. package/src/tools/tasks.js +532 -0
  156. package/src/tools/tool-search.js +171 -0
  157. package/src/ui/header.js +140 -0
  158. package/src/ui/input-cursor.js +23 -0
  159. package/src/ui/last-line.js +25 -0
  160. package/src/ui/line-edit.js +135 -0
  161. package/src/ui/output.js +399 -0
  162. package/src/ui/paste-tokens.js +131 -0
  163. package/src/ui/prompt-attention.js +134 -0
  164. package/src/ui/render-options.js +13 -0
  165. package/src/ui/replay.js +94 -0
  166. package/src/ui/splash.js +49 -0
  167. package/src/ui/status-level.js +36 -0
  168. package/src/ui/tool-ledger.js +203 -0
  169. package/src/ui/window-title.js +150 -0
  170. package/src/update.js +205 -0
  171. package/system.md +63 -0
@@ -0,0 +1,105 @@
1
+ // Rules memory — Layer 3 of memory architecture.
2
+ //
3
+ // Aggregates `better` bullets from ALL reflections (Layer 1) into a
4
+ // stable ruleset: groups semantically-similar lessons, counts frequency,
5
+ // surfaces top-N canonical rules.
6
+ //
7
+ // Purpose: Layer 1 injects last-5 reflections — random recent slice.
8
+ // Layer 3 injects the most-repeated lessons across the full history —
9
+ // load-bearing advice that shows up session after session. Answers the
10
+ // question "what has the agent learned most often to do differently?"
11
+
12
+ import { loadAll as loadAllReflections } from "./reflections.js";
13
+ import { tokenize } from "./patterns.js";
14
+
15
+ function jaccard(setA, setB) {
16
+ let inter = 0;
17
+ for (const x of setA) if (setB.has(x)) inter++;
18
+ const union = setA.size + setB.size - inter;
19
+ return union === 0 ? 0 : inter / union;
20
+ }
21
+
22
+ /**
23
+ * Cluster bullets by Jaccard token similarity (greedy agglomerative).
24
+ * Two bullets merge if overlap ≥ threshold with any cluster member.
25
+ */
26
+ function clusterByJaccard(bullets, threshold = 0.4) {
27
+ const items = bullets.map(b => ({ text: b.text, tags: b.tags, tokens: new Set(tokenize(b.text)) }));
28
+ const clusters = [];
29
+ for (const item of items) {
30
+ if (item.tokens.size === 0) continue;
31
+ let matched = null;
32
+ for (const c of clusters) {
33
+ for (const m of c.members) {
34
+ if (jaccard(item.tokens, m.tokens) >= threshold) {
35
+ matched = c;
36
+ break;
37
+ }
38
+ }
39
+ if (matched) break;
40
+ }
41
+ if (matched) {
42
+ matched.members.push(item);
43
+ for (const t of item.tags) matched.tags.add(t);
44
+ } else {
45
+ clusters.push({ members: [item], tags: new Set(item.tags) });
46
+ }
47
+ }
48
+ return clusters;
49
+ }
50
+
51
+ /**
52
+ * Compile top-N stable rules from all reflections.
53
+ * @param {object} opts
54
+ * @param {number} opts.topN — max rules to return (default 8)
55
+ * @param {number} opts.minCount — min occurrences to keep a cluster (default 2)
56
+ * @param {number} opts.similarity — Jaccard threshold for clustering (default 0.4)
57
+ * @returns {Array<{text:string, count:number, tags:string[]}>}
58
+ */
59
+ export function compileRules(opts = {}) {
60
+ const topN = opts.topN || 8;
61
+ const minCount = opts.minCount || 2;
62
+ const similarity = opts.similarity || 0.4;
63
+
64
+ const reflections = loadAllReflections();
65
+ if (!reflections.length) return [];
66
+
67
+ const bullets = [];
68
+ for (const r of reflections) {
69
+ const tags = Array.isArray(r.tags) ? r.tags : [];
70
+ for (const b of (r.better || [])) {
71
+ if (typeof b !== "string" || b.length < 15) continue;
72
+ bullets.push({ text: b, tags });
73
+ }
74
+ }
75
+ if (!bullets.length) return [];
76
+
77
+ const clusters = clusterByJaccard(bullets, similarity);
78
+
79
+ const result = [];
80
+ for (const c of clusters) {
81
+ if (c.members.length < minCount) continue;
82
+ // Canonical = shortest member ≥ 30 chars, fallback to first
83
+ let canonical = c.members[0].text;
84
+ for (const m of c.members) {
85
+ if (m.text.length >= 30 && m.text.length < canonical.length) canonical = m.text;
86
+ }
87
+ result.push({ text: canonical, count: c.members.length, tags: [...c.tags].slice(0, 3) });
88
+ }
89
+ result.sort((a, b) => b.count - a.count);
90
+ return result.slice(0, topN);
91
+ }
92
+
93
+ /**
94
+ * Format rules as a prompt block.
95
+ * @param {Array<{text,count,tags}>} rules
96
+ */
97
+ export function formatForPrompt(rules) {
98
+ if (!rules?.length) return "";
99
+ const lines = ["Stable rules distilled from past sessions (most-repeated lessons):"];
100
+ for (const r of rules) {
101
+ const tags = r.tags?.length ? ` [${r.tags.slice(0, 3).join(",")}]` : "";
102
+ lines.push(` - (${r.count}×)${tags} ${r.text}`);
103
+ }
104
+ return lines.join("\n");
105
+ }
@@ -0,0 +1,125 @@
1
+ // Per-session fact storage — each session has its own facts file
2
+ // Stored in sessions/{sessionId}.facts.jsonl
3
+ // HMAC integrity protection against tampering
4
+
5
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
6
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
7
+ import path from "node:path";
8
+ import { config } from "../config.js";
9
+
10
+ function factsPath(sessionId) {
11
+ return path.join(config.sessionsDir, `${sessionId}.facts.jsonl`);
12
+ }
13
+
14
+ function hmacPath(sessionId) {
15
+ return path.join(config.sessionsDir, `${sessionId}.facts.hmac`);
16
+ }
17
+
18
+ function getFactsKey() {
19
+ if (process.env.AGENT_MEMORY_HMAC_KEY) return process.env.AGENT_MEMORY_HMAC_KEY;
20
+ const keyFile = path.join(config.sessionsDir, ".hmac-key");
21
+ if (existsSync(keyFile)) return readFileSync(keyFile, "utf-8").trim();
22
+ mkdirSync(config.sessionsDir, { recursive: true });
23
+ const key = randomBytes(32).toString("hex");
24
+ writeFileSync(keyFile, key, { encoding: "utf-8", mode: 0o600 });
25
+ return key;
26
+ }
27
+
28
+ function computeFactsHmac(data) {
29
+ return createHmac("sha256", getFactsKey()).update(data, "utf-8").digest("hex");
30
+ }
31
+
32
+ function verifyFactsHmac(sessionId, data) {
33
+ const hp = hmacPath(sessionId);
34
+ if (!existsSync(hp)) {
35
+ // If facts file has data but no HMAC → auto-heal (migration)
36
+ if (data.trim().length > 0) {
37
+ updateFactsHmac(sessionId);
38
+ }
39
+ return true;
40
+ }
41
+ const stored = readFileSync(hp, "utf-8").trim();
42
+ const computed = computeFactsHmac(data);
43
+ try {
44
+ const storedBuf = Buffer.from(stored, "hex");
45
+ const computedBuf = Buffer.from(computed, "hex");
46
+ if (storedBuf.length !== computedBuf.length || !timingSafeEqual(storedBuf, computedBuf)) {
47
+ console.error(`[SECURITY] Session facts integrity FAILED for ${sessionId} — possible tampering`);
48
+ return false;
49
+ }
50
+ } catch {
51
+ console.error(`[SECURITY] Session facts HMAC error for ${sessionId} — rejecting`);
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ function updateFactsHmac(sessionId) {
58
+ const fp = factsPath(sessionId);
59
+ if (!existsSync(fp)) return;
60
+ const data = readFileSync(fp, "utf-8");
61
+ writeFileSync(hmacPath(sessionId), computeFactsHmac(data), "utf-8");
62
+ }
63
+
64
+ export function saveSessionFact(sessionId, fact) {
65
+ if (!sessionId || !fact?.content) return;
66
+ mkdirSync(config.sessionsDir, { recursive: true });
67
+ const entry = {
68
+ content: fact.content,
69
+ category: fact.category || "auto",
70
+ source: fact.source || "compression",
71
+ created_at: new Date().toISOString(),
72
+ };
73
+ appendFileSync(factsPath(sessionId), JSON.stringify(entry) + "\n", "utf-8");
74
+ updateFactsHmac(sessionId);
75
+ return entry;
76
+ }
77
+
78
+ export function saveSessionFacts(sessionId, facts) {
79
+ if (!sessionId || !facts?.length) return;
80
+ for (const fact of facts) {
81
+ saveSessionFact(sessionId, fact);
82
+ }
83
+ }
84
+
85
+ export function loadSessionFacts(sessionId) {
86
+ if (!sessionId) return [];
87
+ const fp = factsPath(sessionId);
88
+ if (!existsSync(fp)) return [];
89
+ const raw = readFileSync(fp, "utf-8");
90
+
91
+ if (!verifyFactsHmac(sessionId, raw)) {
92
+ return []; // tampering detected — return empty
93
+ }
94
+
95
+ const lines = raw.split("\n").filter(Boolean);
96
+ return lines.map((line) => {
97
+ try { return JSON.parse(line); } catch { return null; }
98
+ }).filter(Boolean);
99
+ }
100
+
101
+ export function getSessionFactsSummary(sessionId, maxLines = 30) {
102
+ const facts = loadSessionFacts(sessionId);
103
+ if (!facts.length) return "";
104
+
105
+ const grouped = {};
106
+ for (const f of facts) {
107
+ const cat = f.category || "auto";
108
+ if (!grouped[cat]) grouped[cat] = [];
109
+ grouped[cat].push(f);
110
+ }
111
+
112
+ let summary = `Session facts (${facts.length}):\n`;
113
+ for (const [cat, items] of Object.entries(grouped)) {
114
+ summary += `[${cat}]\n`;
115
+ for (const f of items.slice(-maxLines)) {
116
+ summary += `- ${f.content}\n`;
117
+ }
118
+ }
119
+ return summary;
120
+ }
121
+
122
+ export function clearSessionFacts(sessionId) {
123
+ const fp = factsPath(sessionId);
124
+ if (existsSync(fp)) writeFileSync(fp, "", "utf-8");
125
+ }
@@ -0,0 +1,191 @@
1
+ // Skills memory — Layer 3 of memory architecture.
2
+ //
3
+ // Reusable procedures stored as markdown files in ~/.flint/memory/skills/.
4
+ // Synced bidirectionally with SQLite skills table.
5
+ // Human-editable: user can write/edit .md files directly.
6
+ // Agent-editable: agent can create/update via tool actions.
7
+ //
8
+ // Internal task reference removed.
9
+
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
11
+ import { join, basename, resolve } from "node:path";
12
+ import { homedir } from "node:os";
13
+ import { insertSkill, getAllSkills, getSkill, deleteSkill, indexEntry } from "./sqlite-store.js";
14
+
15
+ const SKILLS_DIR = process.env.FLINT_DATA_DIR
16
+ ? join(resolve(process.env.FLINT_DATA_DIR), "memory", "skills")
17
+ : join(homedir(), ".flint", "memory", "skills");
18
+ const MAX_SKILLS = 50;
19
+
20
+ function ensureDir() {
21
+ if (!existsSync(SKILLS_DIR)) mkdirSync(SKILLS_DIR, { recursive: true });
22
+ }
23
+
24
+ /**
25
+ * Parse a markdown skill file. Optional YAML-like frontmatter:
26
+ * ---
27
+ * tags: deploy, aws
28
+ * ---
29
+ * # Skill title
30
+ * Content...
31
+ */
32
+ function parseSkillFile(filePath) {
33
+ const raw = readFileSync(filePath, "utf-8");
34
+ const fileName = basename(filePath, ".md");
35
+ let tags = [];
36
+ let content = raw;
37
+ let name = fileName;
38
+
39
+ // Extract frontmatter
40
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
41
+ if (fmMatch) {
42
+ const fm = fmMatch[1];
43
+ content = fmMatch[2].trim();
44
+ const tagsMatch = fm.match(/tags:\s*(.+)/);
45
+ if (tagsMatch) tags = tagsMatch[1].split(",").map(t => t.trim()).filter(Boolean);
46
+ }
47
+
48
+ // Extract title from first # heading
49
+ const titleMatch = content.match(/^#\s+(.+)/m);
50
+ if (titleMatch) name = titleMatch[1].trim();
51
+
52
+ return { id: `skill-${fileName}`, name, content, tags, source_file: filePath };
53
+ }
54
+
55
+ /**
56
+ * Sync markdown files → SQLite table.
57
+ * Called at startup. Adds new files, updates changed, removes deleted.
58
+ */
59
+ export function syncFromDisk() {
60
+ ensureDir();
61
+ const files = readdirSync(SKILLS_DIR).filter(f => f.endsWith(".md"));
62
+ const dbSkills = getAllSkills();
63
+ const dbMap = new Map(dbSkills.map(s => [s.id, s]));
64
+ const fileIds = new Set();
65
+
66
+ for (const f of files) {
67
+ const filePath = join(SKILLS_DIR, f);
68
+ try {
69
+ const parsed = parseSkillFile(filePath);
70
+ fileIds.add(parsed.id);
71
+ const existing = dbMap.get(parsed.id);
72
+ // Insert or update if content changed
73
+ if (!existing || existing.content !== parsed.content) {
74
+ insertSkill(parsed);
75
+ indexEntry("skills", parsed.id, [parsed.name, parsed.content].join(" "));
76
+ }
77
+ } catch {}
78
+ }
79
+
80
+ // Remove skills from DB that no longer have files
81
+ for (const s of dbSkills) {
82
+ if (s.source_file && !fileIds.has(s.id)) {
83
+ deleteSkill(s.id);
84
+ }
85
+ }
86
+
87
+ return { synced: fileIds.size, total: files.length };
88
+ }
89
+
90
+ /**
91
+ * Create a new skill from agent (writes both DB + file).
92
+ * @param {string} name — human-readable name
93
+ * @param {string} content — markdown content
94
+ * @param {string[]} tags — optional tags
95
+ * @returns {string} skill id
96
+ */
97
+ export function createSkill(name, content, tags = []) {
98
+ ensureDir();
99
+ // Enforce cap
100
+ const existing = getAllSkills();
101
+ if (existing.length >= MAX_SKILLS) {
102
+ // Evict oldest
103
+ const oldest = existing[existing.length - 1];
104
+ if (oldest) {
105
+ deleteSkill(oldest.id);
106
+ if (oldest.source_file && existsSync(oldest.source_file)) {
107
+ try { unlinkSync(oldest.source_file); } catch {}
108
+ }
109
+ }
110
+ }
111
+
112
+ const slug = name.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").slice(0, 60);
113
+ const id = `skill-${slug}`;
114
+ const filePath = join(SKILLS_DIR, `${slug}.md`);
115
+
116
+ // Build markdown with frontmatter
117
+ let md = "";
118
+ if (tags.length) {
119
+ md += `---\ntags: ${tags.join(", ")}\n---\n`;
120
+ }
121
+ md += `# ${name}\n\n${content}\n`;
122
+
123
+ writeFileSync(filePath, md, "utf-8");
124
+ insertSkill({ id, name, content, source_file: filePath, tags });
125
+ indexEntry("skills", id, [name, content].join(" "));
126
+ return id;
127
+ }
128
+
129
+ /**
130
+ * Update an existing skill content.
131
+ */
132
+ export function updateSkill(id, content) {
133
+ const skill = getSkill(id);
134
+ if (!skill) return false;
135
+ skill.content = content;
136
+ insertSkill(skill);
137
+ indexEntry("skills", id, [skill.name, content].join(" "));
138
+ // Update file
139
+ if (skill.source_file && existsSync(skill.source_file)) {
140
+ let md = "";
141
+ if (skill.tags?.length) {
142
+ md += `---\ntags: ${skill.tags.join(", ")}\n---\n`;
143
+ }
144
+ md += `# ${skill.name}\n\n${content}\n`;
145
+ writeFileSync(skill.source_file, md, "utf-8");
146
+ }
147
+ return true;
148
+ }
149
+
150
+ /**
151
+ * Remove a skill (DB + file).
152
+ */
153
+ export function removeSkill(id) {
154
+ const skill = getSkill(id);
155
+ if (!skill) return false;
156
+ deleteSkill(id);
157
+ if (skill.source_file && existsSync(skill.source_file)) {
158
+ try { unlinkSync(skill.source_file); } catch {}
159
+ }
160
+ return true;
161
+ }
162
+
163
+ /**
164
+ * List all skills as summaries (for prompt injection).
165
+ * @returns {Array<{id, name, tags}>}
166
+ */
167
+ export function listSkillSummaries() {
168
+ return getAllSkills().map(s => ({ id: s.id, name: s.name, tags: s.tags }));
169
+ }
170
+
171
+ /**
172
+ * Format skills as a system-prompt-ready block.
173
+ * Progressive disclosure: only names + tags, agent uses memory_expand to get full content.
174
+ * @returns {string}
175
+ */
176
+ export function formatForPrompt() {
177
+ const skills = getAllSkills();
178
+ if (!skills.length) return "";
179
+ const lines = ["Available skills (use load_skill or memory_expand to see full content):"];
180
+ for (const s of skills) {
181
+ const tagStr = s.tags?.length ? ` [${s.tags.join(", ")}]` : "";
182
+ // First non-empty line of content as description (~60 chars)
183
+ const desc = s.content
184
+ ? s.content.split("\n").find(l => l.trim() && !l.startsWith("#"))?.trim().slice(0, 60) || ""
185
+ : "";
186
+ lines.push(` § ${s.name}${tagStr}${desc ? " — " + desc : ""}`);
187
+ }
188
+ return lines.join("\n");
189
+ }
190
+
191
+ export const __internal = { SKILLS_DIR, MAX_SKILLS };