@zhijiewang/openharness 2.8.0 → 2.10.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.
- package/data/registry.json +262 -0
- package/data/skills/code-review.md +19 -0
- package/data/skills/commit.md +17 -0
- package/data/skills/debug.md +24 -0
- package/data/skills/diagnose.md +24 -0
- package/data/skills/plan.md +25 -0
- package/data/skills/simplify.md +24 -0
- package/data/skills/tdd.md +22 -0
- package/dist/agents/roles.d.ts +12 -2
- package/dist/agents/roles.js +65 -6
- package/dist/commands/ai.js +27 -7
- package/dist/commands/skills.d.ts +1 -1
- package/dist/commands/skills.js +51 -6
- package/dist/components/App.js +7 -1
- package/dist/harness/config.d.ts +24 -0
- package/dist/harness/hooks.d.ts +14 -0
- package/dist/harness/hooks.js +205 -11
- package/dist/harness/marketplace.d.ts +77 -2
- package/dist/harness/marketplace.js +260 -38
- package/dist/harness/memory.d.ts +34 -0
- package/dist/harness/memory.js +96 -0
- package/dist/harness/plugins.d.ts +13 -3
- package/dist/harness/plugins.js +98 -17
- package/dist/harness/session-db.d.ts +8 -1
- package/dist/harness/session-db.js +24 -3
- package/dist/harness/skill-registry.d.ts +26 -2
- package/dist/harness/skill-registry.js +42 -4
- package/dist/tools/AgentTool/index.d.ts +2 -2
- package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
- package/dist/tools/GrepTool/index.d.ts +6 -6
- package/dist/tools/MemoryTool/index.d.ts +4 -4
- package/dist/tools/MonitorTool/index.js +5 -1
- package/dist/types/permissions.js +104 -42
- package/dist/utils/bash-safety.d.ts +19 -0
- package/dist/utils/bash-safety.js +179 -1
- package/dist/utils/safe-env.d.ts +5 -1
- package/dist/utils/safe-env.js +19 -1
- 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
|
-
//
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
192
|
+
if (!data)
|
|
50
193
|
return null;
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
|
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
|
package/dist/harness/memory.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/harness/memory.js
CHANGED
|
@@ -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<{
|