@zhijiewang/openharness 2.5.0 → 2.9.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 (62) hide show
  1. package/README.md +1 -1
  2. package/data/registry.json +262 -0
  3. package/data/skills/code-review.md +19 -0
  4. package/data/skills/commit.md +17 -0
  5. package/data/skills/debug.md +24 -0
  6. package/data/skills/diagnose.md +24 -0
  7. package/data/skills/plan.md +25 -0
  8. package/data/skills/simplify.md +24 -0
  9. package/data/skills/tdd.md +22 -0
  10. package/dist/agents/roles.d.ts +12 -2
  11. package/dist/agents/roles.js +65 -6
  12. package/dist/commands/ai.d.ts +6 -0
  13. package/dist/commands/ai.js +264 -0
  14. package/dist/commands/git.d.ts +6 -0
  15. package/dist/commands/git.js +167 -0
  16. package/dist/commands/index.d.ts +10 -31
  17. package/dist/commands/index.js +22 -1096
  18. package/dist/commands/info.d.ts +8 -0
  19. package/dist/commands/info.js +671 -0
  20. package/dist/commands/session.d.ts +6 -0
  21. package/dist/commands/session.js +214 -0
  22. package/dist/commands/settings.d.ts +6 -0
  23. package/dist/commands/settings.js +187 -0
  24. package/dist/commands/skills.d.ts +6 -0
  25. package/dist/commands/skills.js +162 -0
  26. package/dist/commands/types.d.ts +36 -0
  27. package/dist/commands/types.js +5 -0
  28. package/dist/components/App.js +7 -1
  29. package/dist/components/InitWizard.js +60 -62
  30. package/dist/harness/config.d.ts +11 -0
  31. package/dist/harness/hooks.d.ts +14 -0
  32. package/dist/harness/hooks.js +56 -10
  33. package/dist/harness/marketplace.d.ts +77 -2
  34. package/dist/harness/marketplace.js +260 -38
  35. package/dist/harness/memory.d.ts +34 -0
  36. package/dist/harness/memory.js +96 -0
  37. package/dist/harness/plugins.d.ts +13 -3
  38. package/dist/harness/plugins.js +98 -17
  39. package/dist/harness/session-db.d.ts +8 -1
  40. package/dist/harness/session-db.js +24 -3
  41. package/dist/harness/skill-registry.d.ts +26 -2
  42. package/dist/harness/skill-registry.js +42 -4
  43. package/dist/providers/anthropic.js +7 -8
  44. package/dist/providers/openai.js +3 -2
  45. package/dist/renderer/layout-sections.d.ts +56 -0
  46. package/dist/renderer/layout-sections.js +462 -0
  47. package/dist/renderer/layout.d.ts +4 -2
  48. package/dist/renderer/layout.js +25 -500
  49. package/dist/tools/AgentTool/index.d.ts +2 -2
  50. package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
  51. package/dist/tools/GrepTool/index.d.ts +6 -6
  52. package/dist/tools/MemoryTool/index.d.ts +6 -6
  53. package/dist/tools/MonitorTool/index.js +5 -1
  54. package/dist/tools/TodoWriteTool/index.d.ts +37 -0
  55. package/dist/tools/TodoWriteTool/index.js +78 -0
  56. package/dist/tools.js +2 -0
  57. package/dist/types/permissions.js +104 -42
  58. package/dist/utils/bash-safety.d.ts +19 -0
  59. package/dist/utils/bash-safety.js +179 -1
  60. package/dist/utils/safe-env.d.ts +5 -1
  61. package/dist/utils/safe-env.js +19 -1
  62. package/package.json +3 -1
@@ -7,59 +7,196 @@
7
7
  * Inspired by Claude Code's marketplace model.
8
8
  */
9
9
  import { execSync } from "node:child_process";
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { basename, join } from "node:path";
13
13
  const MARKETPLACE_DIR = join(homedir(), ".oh", "marketplaces");
14
14
  const PLUGIN_CACHE_DIR = join(homedir(), ".oh", "plugins", "cache");
15
15
  const INSTALLED_PLUGINS_FILE = join(homedir(), ".oh", "plugins", "installed.json");
16
+ // Claude Code plugin manifest path relative to plugin root
17
+ const CC_MANIFEST_PATH = join(".claude-plugin", "plugin.json");
18
+ /** Parse a `.claude-plugin/plugin.json` file at the given plugin root, or null if missing/invalid. */
19
+ export function parseCcPluginManifest(pluginRoot) {
20
+ const path = join(pluginRoot, CC_MANIFEST_PATH);
21
+ if (!existsSync(path))
22
+ return null;
23
+ try {
24
+ const m = JSON.parse(readFileSync(path, "utf-8"));
25
+ if (typeof m?.name !== "string" || typeof m?.description !== "string")
26
+ return null;
27
+ return m;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /** Convert a CcMarketplace to OH's internal Marketplace type (lossy: dropped fields like `owner` are not stored). */
34
+ export function ccMarketplaceToOh(cc) {
35
+ const plugins = [];
36
+ for (const p of cc.plugins) {
37
+ const ohSource = ccSourceToOh(p.source);
38
+ if (!ohSource)
39
+ continue; // Skip relative-path sources — only resolvable inside the marketplace repo
40
+ plugins.push({
41
+ name: p.name,
42
+ description: p.description ?? "",
43
+ version: p.version ?? "0.0.0",
44
+ author: typeof p.author === "string" ? p.author : p.author?.name,
45
+ source: ohSource,
46
+ });
47
+ }
48
+ return { name: cc.name, version: 1, description: cc.description, plugins };
49
+ }
50
+ function ccSourceToOh(src) {
51
+ if (typeof src === "string")
52
+ return null; // relative paths require marketplace-repo context
53
+ if (src.source === "github")
54
+ return { type: "github", repo: src.repo };
55
+ if (src.source === "url")
56
+ return { type: "url", url: src.url };
57
+ if (src.source === "npm")
58
+ return { type: "npm", package: src.package };
59
+ return null;
60
+ }
61
+ /** Parse marketplace JSON text. Tries CC format (.claude-plugin/marketplace.json shape) first, falls back to OH-native. */
62
+ export function parseMarketplaceJson(text) {
63
+ let raw;
64
+ try {
65
+ raw = JSON.parse(text);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ if (!raw || typeof raw !== "object")
71
+ return null;
72
+ const obj = raw;
73
+ if (!Array.isArray(obj.plugins))
74
+ return null;
75
+ // Detect CC format: any plugin entry has either a string `source` or `source.source` (kind tag)
76
+ const looksCc = obj.plugins.some((p) => {
77
+ if (!p || typeof p !== "object")
78
+ return false;
79
+ const s = p.source;
80
+ return typeof s === "string" || (s !== null && typeof s === "object" && "source" in s);
81
+ });
82
+ if (looksCc) {
83
+ return ccMarketplaceToOh(obj);
84
+ }
85
+ // OH-native format
86
+ return obj;
87
+ }
88
+ /** Discover plugin-shipped MCP servers from `cachePath/.mcp.json`. Returns raw object for the runtime to merge. */
89
+ export function getPluginMcpServers(cachePath) {
90
+ const path = join(cachePath, ".mcp.json");
91
+ if (!existsSync(path))
92
+ return null;
93
+ try {
94
+ const data = JSON.parse(readFileSync(path, "utf-8"));
95
+ // Claude Code shape: { "mcpServers": { name: { command, args, env, ... } } }
96
+ // Some plugins store the inner map directly. Accept both.
97
+ if (data && typeof data === "object") {
98
+ if ("mcpServers" in data && typeof data.mcpServers === "object")
99
+ return data.mcpServers;
100
+ return data;
101
+ }
102
+ return null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /** Discover plugin-shipped hooks from `cachePath/hooks/hooks.json`. Returns raw config for the runtime to register. */
109
+ export function getPluginHooks(cachePath) {
110
+ const path = join(cachePath, "hooks", "hooks.json");
111
+ if (!existsSync(path))
112
+ return null;
113
+ try {
114
+ const data = JSON.parse(readFileSync(path, "utf-8"));
115
+ if (data && typeof data === "object")
116
+ return data;
117
+ return null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /** Discover plugin-shipped LSP servers from `cachePath/.lsp.json`. Returns raw config for the runtime to register. */
124
+ export function getPluginLspServers(cachePath) {
125
+ const path = join(cachePath, ".lsp.json");
126
+ if (!existsSync(path))
127
+ return null;
128
+ try {
129
+ const data = JSON.parse(readFileSync(path, "utf-8"));
130
+ if (data && typeof data === "object") {
131
+ if ("lspServers" in data && typeof data.lspServers === "object")
132
+ return data.lspServers;
133
+ return data;
134
+ }
135
+ return null;
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
16
141
  // ── Marketplace Management ──
17
- /** Add a marketplace from a URL, GitHub repo, or local path */
142
+ /** Add a marketplace from a URL, GitHub repo, or local path.
143
+ * Probes for both OH-native `marketplace.json` and Claude Code `.claude-plugin/marketplace.json`.
144
+ */
18
145
  export function addMarketplace(nameOrUrl) {
19
146
  mkdirSync(MARKETPLACE_DIR, { recursive: true });
20
- // Fetch marketplace.json
21
- let data;
147
+ // Candidate filenames to try, in priority order
148
+ const candidatePaths = ["marketplace.json", ".claude-plugin/marketplace.json"];
149
+ let data = null;
22
150
  let marketplaceName;
23
151
  if (nameOrUrl.startsWith("http")) {
24
- // URL
25
- try {
26
- data = execSync(`curl -sL "${nameOrUrl}/marketplace.json"`, { encoding: "utf-8", timeout: 10_000 });
27
- marketplaceName = new URL(nameOrUrl).hostname;
28
- }
29
- catch {
30
- return null;
152
+ marketplaceName = new URL(nameOrUrl).hostname;
153
+ for (const fname of candidatePaths) {
154
+ try {
155
+ const text = execSync(`curl -sfL "${nameOrUrl}/${fname}"`, { encoding: "utf-8", timeout: 10_000 });
156
+ if (text.trim()) {
157
+ data = text;
158
+ break;
159
+ }
160
+ }
161
+ catch {
162
+ /* try next */
163
+ }
31
164
  }
32
165
  }
33
166
  else if (nameOrUrl.includes("/") && !nameOrUrl.startsWith(".")) {
34
- // GitHub repo (owner/repo format)
35
- try {
36
- const url = `https://raw.githubusercontent.com/${nameOrUrl}/main/marketplace.json`;
37
- data = execSync(`curl -sL "${url}"`, { encoding: "utf-8", timeout: 10_000 });
38
- marketplaceName = nameOrUrl.replace("/", "-");
39
- }
40
- catch {
41
- return null;
167
+ marketplaceName = nameOrUrl.replace("/", "-");
168
+ for (const fname of candidatePaths) {
169
+ try {
170
+ const url = `https://raw.githubusercontent.com/${nameOrUrl}/main/${fname}`;
171
+ const text = execSync(`curl -sfL "${url}"`, { encoding: "utf-8", timeout: 10_000 });
172
+ if (text.trim()) {
173
+ data = text;
174
+ break;
175
+ }
176
+ }
177
+ catch {
178
+ /* try next */
179
+ }
42
180
  }
43
181
  }
44
- else if (existsSync(join(nameOrUrl, "marketplace.json"))) {
45
- // Local path
46
- data = readFileSync(join(nameOrUrl, "marketplace.json"), "utf-8");
182
+ else {
47
183
  marketplaceName = basename(nameOrUrl);
184
+ for (const fname of candidatePaths) {
185
+ const path = join(nameOrUrl, fname);
186
+ if (existsSync(path)) {
187
+ data = readFileSync(path, "utf-8");
188
+ break;
189
+ }
190
+ }
48
191
  }
49
- else {
192
+ if (!data)
50
193
  return null;
51
- }
52
- try {
53
- const marketplace = JSON.parse(data);
54
- if (!marketplace.plugins || !Array.isArray(marketplace.plugins))
55
- return null;
56
- marketplace.name = marketplace.name ?? marketplaceName;
57
- writeFileSync(join(MARKETPLACE_DIR, `${marketplace.name}.json`), JSON.stringify(marketplace, null, 2));
58
- return marketplace;
59
- }
60
- catch {
194
+ const marketplace = parseMarketplaceJson(data);
195
+ if (!marketplace)
61
196
  return null;
62
- }
197
+ marketplace.name = marketplace.name ?? marketplaceName;
198
+ writeFileSync(join(MARKETPLACE_DIR, `${marketplace.name}.json`), JSON.stringify(marketplace, null, 2));
199
+ return marketplace;
63
200
  }
64
201
  /** Remove a marketplace */
65
202
  export function removeMarketplace(name) {
@@ -199,17 +336,102 @@ export function uninstallPlugin(name) {
199
336
  saveInstalledPluginList(remaining);
200
337
  return true;
201
338
  }
202
- /** Get all installed plugins */
339
+ /** Get all installed plugins.
340
+ * Sources merged in priority order:
341
+ * 1. installed.json (plugins installed via /plugin install or addMarketplace flow)
342
+ * 2. CC-style plugins discovered in PLUGIN_CACHE_DIR via .claude-plugin/plugin.json
343
+ * (covers plugins manually dropped in the cache, or installed by parallel tooling)
344
+ * Plugins from #1 are enriched with manifest data if their cachePath has one.
345
+ * De-duplication is by cachePath.
346
+ */
203
347
  export function getInstalledPlugins() {
204
- if (!existsSync(INSTALLED_PLUGINS_FILE))
205
- return [];
348
+ const recorded = (() => {
349
+ if (!existsSync(INSTALLED_PLUGINS_FILE))
350
+ return [];
351
+ try {
352
+ return JSON.parse(readFileSync(INSTALLED_PLUGINS_FILE, "utf-8"));
353
+ }
354
+ catch {
355
+ return [];
356
+ }
357
+ })();
358
+ // Enrich recorded plugins from their CC manifest if present
359
+ const enriched = recorded.map((p) => mergeManifest(p, parseCcPluginManifest(p.cachePath)));
360
+ const seenPaths = new Set(enriched.map((p) => p.cachePath));
361
+ // Discover CC-style plugins in PLUGIN_CACHE_DIR not already recorded
362
+ const discovered = [];
363
+ if (existsSync(PLUGIN_CACHE_DIR)) {
364
+ try {
365
+ for (const nameEntry of readdirSync(PLUGIN_CACHE_DIR)) {
366
+ const nameDir = join(PLUGIN_CACHE_DIR, nameEntry);
367
+ if (!safeIsDirectory(nameDir))
368
+ continue;
369
+ // Plugin can sit either at <name>/ or <name>/<version>/
370
+ const candidates = [nameDir, ...listSubdirs(nameDir)];
371
+ for (const root of candidates) {
372
+ if (seenPaths.has(root))
373
+ continue;
374
+ const manifest = parseCcPluginManifest(root);
375
+ if (!manifest)
376
+ continue;
377
+ discovered.push({
378
+ name: manifest.name,
379
+ version: manifest.version ?? "0.0.0",
380
+ marketplace: "discovered",
381
+ installedAt: tryStatTime(root),
382
+ cachePath: root,
383
+ ...projectManifestFields(manifest),
384
+ });
385
+ seenPaths.add(root);
386
+ }
387
+ }
388
+ }
389
+ catch {
390
+ /* ignore */
391
+ }
392
+ }
393
+ return [...enriched, ...discovered];
394
+ }
395
+ function safeIsDirectory(path) {
396
+ try {
397
+ return statSync(path).isDirectory();
398
+ }
399
+ catch {
400
+ return false;
401
+ }
402
+ }
403
+ function listSubdirs(dir) {
206
404
  try {
207
- return JSON.parse(readFileSync(INSTALLED_PLUGINS_FILE, "utf-8"));
405
+ return readdirSync(dir)
406
+ .map((entry) => join(dir, entry))
407
+ .filter((p) => safeIsDirectory(p));
208
408
  }
209
409
  catch {
210
410
  return [];
211
411
  }
212
412
  }
413
+ function tryStatTime(path) {
414
+ try {
415
+ return statSync(path).mtimeMs;
416
+ }
417
+ catch {
418
+ return Date.now();
419
+ }
420
+ }
421
+ function projectManifestFields(m) {
422
+ return {
423
+ description: m.description,
424
+ author: typeof m.author === "string" ? m.author : m.author?.name,
425
+ license: m.license,
426
+ homepage: m.homepage,
427
+ keywords: m.keywords,
428
+ };
429
+ }
430
+ function mergeManifest(plugin, manifest) {
431
+ if (!manifest)
432
+ return plugin;
433
+ return { ...plugin, ...projectManifestFields(manifest) };
434
+ }
213
435
  function saveInstalledPlugin(plugin) {
214
436
  const installed = getInstalledPlugins();
215
437
  // Replace existing version
@@ -31,6 +31,40 @@ export type MemoryEntry = {
31
31
  export declare function loadMemories(): MemoryEntry[];
32
32
  /** Build a system prompt section from loaded memories (capped at MEMORY_PROMPT_MAX_CHARS) */
33
33
  export declare function memoriesToPrompt(memories: MemoryEntry[]): string;
34
+ /** A single CLAUDE.md source with its resolved content (imports inlined). */
35
+ export type ClaudeMdEntry = {
36
+ path: string;
37
+ source: "project" | "project-local" | "user" | "claude-dir";
38
+ content: string;
39
+ };
40
+ /**
41
+ * Resolve `@path/to/file` imports inline. Relative paths resolve against
42
+ * `baseDir`. Absolute paths are read as-is. Each imported file can itself
43
+ * contain imports — recursion capped at `CLAUDE_MD_MAX_HOPS` to prevent
44
+ * cycles. Missing or unreadable imports are silently dropped.
45
+ *
46
+ * @internal exposed for testing
47
+ */
48
+ export declare function resolveClaudeMdImports(content: string, baseDir: string, hopsLeft?: number): string;
49
+ /**
50
+ * Load the hierarchical CLAUDE.md set in the order Anthropic documents:
51
+ * 1. `./.claude/CLAUDE.md` (project, checked in)
52
+ * 2. `./CLAUDE.md` (project, checked in)
53
+ * 3. `./CLAUDE.local.md` (project, gitignored)
54
+ * 4. `~/.claude/CLAUDE.md` (user-global)
55
+ *
56
+ * Each file is read, `@imports` are resolved, and the results are returned in
57
+ * load order. Missing files are skipped. The caller can format these into the
58
+ * system prompt alongside `memoriesToPrompt()` — the two systems are additive.
59
+ *
60
+ * @param root optional project root (defaults to cwd)
61
+ */
62
+ export declare function loadClaudeMdHierarchy(root?: string): ClaudeMdEntry[];
63
+ /**
64
+ * Render loaded CLAUDE.md entries as a system-prompt block. Empty when no
65
+ * CLAUDE.md files exist — caller should concatenate alongside `memoriesToPrompt`.
66
+ */
67
+ export declare function claudeMdToPrompt(entries: ClaudeMdEntry[]): string;
34
68
  /** Save a memory entry to the project memory directory */
35
69
  export declare function saveMemory(name: string, type: MemoryType, description: string, content: string, global?: boolean): string;
36
70
  /**
@@ -13,6 +13,10 @@ import { join, resolve, sep } from "node:path";
13
13
  import { createUserMessage } from "../types/message.js";
14
14
  const PROJECT_MEMORY_DIR = join(".oh", "memory");
15
15
  const GLOBAL_MEMORY_DIR = join(homedir(), ".oh", "memory");
16
+ // Maximum number of @-import hops before giving up (prevents cycles).
17
+ const CLAUDE_MD_MAX_HOPS = 5;
18
+ // Cap per loaded CLAUDE.md source to keep the system prompt bounded.
19
+ const CLAUDE_MD_PER_FILE_CAP = 20_000;
16
20
  // Version counter — incremented on every save, used by query loop for live injection
17
21
  let _memoryVersion = 0;
18
22
  export function memoryVersion() {
@@ -79,6 +83,98 @@ export function memoriesToPrompt(memories) {
79
83
  }
80
84
  return result.trimEnd();
81
85
  }
86
+ /**
87
+ * Resolve `@path/to/file` imports inline. Relative paths resolve against
88
+ * `baseDir`. Absolute paths are read as-is. Each imported file can itself
89
+ * contain imports — recursion capped at `CLAUDE_MD_MAX_HOPS` to prevent
90
+ * cycles. Missing or unreadable imports are silently dropped.
91
+ *
92
+ * @internal exposed for testing
93
+ */
94
+ export function resolveClaudeMdImports(content, baseDir, hopsLeft = CLAUDE_MD_MAX_HOPS) {
95
+ if (hopsLeft <= 0)
96
+ return content;
97
+ // Match `@path` on its own line or in a line-leading position. We avoid
98
+ // email addresses (`foo@bar.com`) by requiring whitespace/start-of-line
99
+ // before the `@` and a path-like token after.
100
+ const importRe = /(^|\s)@([^\s@]+)/g;
101
+ return content.replace(importRe, (match, leading, path) => {
102
+ // Skip obvious non-path tokens (e.g. `@user` mentions without slashes or extensions)
103
+ if (!path.includes("/") && !path.includes("."))
104
+ return match;
105
+ const resolved = path.startsWith("/") || path.startsWith("~") ? path.replace(/^~/, homedir()) : join(baseDir, path);
106
+ if (!existsSync(resolved))
107
+ return match;
108
+ try {
109
+ const raw = readFileSync(resolved, "utf-8");
110
+ const subDir = resolved.split(/[/\\]/).slice(0, -1).join("/");
111
+ const expanded = resolveClaudeMdImports(raw, subDir || baseDir, hopsLeft - 1);
112
+ return `${leading}<!-- imported from @${path} -->\n${expanded}\n<!-- end @${path} -->`;
113
+ }
114
+ catch {
115
+ return match;
116
+ }
117
+ });
118
+ }
119
+ /** Read a single CLAUDE.md candidate path if it exists. Returns null otherwise. */
120
+ function readClaudeMdIfExists(path, source) {
121
+ if (!existsSync(path))
122
+ return null;
123
+ try {
124
+ const raw = readFileSync(path, "utf-8").slice(0, CLAUDE_MD_PER_FILE_CAP);
125
+ const baseDir = path.split(/[/\\]/).slice(0, -1).join("/") || ".";
126
+ return { path, source, content: resolveClaudeMdImports(raw, baseDir) };
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * Load the hierarchical CLAUDE.md set in the order Anthropic documents:
134
+ * 1. `./.claude/CLAUDE.md` (project, checked in)
135
+ * 2. `./CLAUDE.md` (project, checked in)
136
+ * 3. `./CLAUDE.local.md` (project, gitignored)
137
+ * 4. `~/.claude/CLAUDE.md` (user-global)
138
+ *
139
+ * Each file is read, `@imports` are resolved, and the results are returned in
140
+ * load order. Missing files are skipped. The caller can format these into the
141
+ * system prompt alongside `memoriesToPrompt()` — the two systems are additive.
142
+ *
143
+ * @param root optional project root (defaults to cwd)
144
+ */
145
+ export function loadClaudeMdHierarchy(root = ".") {
146
+ const candidates = [
147
+ [join(root, ".claude", "CLAUDE.md"), "claude-dir"],
148
+ [join(root, "CLAUDE.md"), "project"],
149
+ [join(root, "CLAUDE.local.md"), "project-local"],
150
+ [join(homedir(), ".claude", "CLAUDE.md"), "user"],
151
+ ];
152
+ const entries = [];
153
+ const seen = new Set();
154
+ for (const [path, source] of candidates) {
155
+ if (seen.has(path))
156
+ continue;
157
+ seen.add(path);
158
+ const entry = readClaudeMdIfExists(path, source);
159
+ if (entry)
160
+ entries.push(entry);
161
+ }
162
+ return entries;
163
+ }
164
+ /**
165
+ * Render loaded CLAUDE.md entries as a system-prompt block. Empty when no
166
+ * CLAUDE.md files exist — caller should concatenate alongside `memoriesToPrompt`.
167
+ */
168
+ export function claudeMdToPrompt(entries) {
169
+ if (entries.length === 0)
170
+ return "";
171
+ const parts = ["# Project instructions (CLAUDE.md)"];
172
+ for (const e of entries) {
173
+ parts.push(`<!-- source: ${e.source} (${e.path}) -->`);
174
+ parts.push(e.content.trim());
175
+ }
176
+ return parts.join("\n\n").trimEnd();
177
+ }
82
178
  /** Save a memory entry to the project memory directory */
83
179
  export function saveMemory(name, type, description, content, global = false) {
84
180
  const dir = global ? GLOBAL_MEMORY_DIR : PROJECT_MEMORY_DIR;
@@ -16,9 +16,19 @@ export type SkillMetadata = {
16
16
  trigger: string | undefined;
17
17
  tools: string[] | undefined;
18
18
  args: string[] | undefined;
19
+ /** Optional natural-language hint for when this skill applies; concatenated to description for trigger matching */
20
+ whenToUse: string | undefined;
21
+ /** SPDX license identifier (e.g. "MIT", "Apache-2.0", "CC-BY-SA-4.0"). Used by install gate. */
22
+ license: string | undefined;
23
+ /** Glob patterns scoping skill auto-surfacing to specific file paths */
24
+ paths: string[] | undefined;
25
+ /** Execution context: "default" runs in the current agent, "fork" spawns a sub-agent (Anthropic extension) */
26
+ context: "default" | "fork" | undefined;
27
+ /** When `context: fork`, the sub-agent type to spawn (must match an AgentRole id) */
28
+ agent: string | undefined;
19
29
  content: string;
20
30
  filePath: string;
21
- source: "project" | "global" | "plugin";
31
+ source: "bundled" | "project" | "global" | "plugin";
22
32
  /** When false, skill is hidden from system prompt until explicitly invoked */
23
33
  invokeModel: boolean;
24
34
  };
@@ -44,11 +54,11 @@ export type AgentTeamConfig = {
44
54
  tools?: string[];
45
55
  }>;
46
56
  };
47
- /** Discover all available skills from project + global dirs + installed plugins */
57
+ /** Discover all available skills from bundled + project + global dirs + installed plugins */
48
58
  export declare function discoverSkills(): SkillMetadata[];
49
59
  /** Find a skill by name (case-insensitive) */
50
60
  export declare function findSkill(name: string): SkillMetadata | null;
51
- /** Find skills that match a trigger condition */
61
+ /** Find skills that match a trigger condition (substring match against `trigger` field). */
52
62
  export declare function findTriggeredSkills(userMessage: string): SkillMetadata[];
53
63
  /** Find a skill similar to a candidate (for patch-vs-create decision) */
54
64
  export declare function findSimilarSkill(candidateName: string, candidateDescription: string, skills: Array<{