@phuetz/code-buddy 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -3
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/base-agent.d.ts +4 -0
- package/dist/agent/base-agent.js +6 -0
- package/dist/agent/facades/infrastructure-facade.d.ts +9 -2
- package/dist/agent/facades/infrastructure-facade.js +15 -6
- package/dist/agent/self-improvement/authored-artifact-gate.d.ts +18 -0
- package/dist/agent/self-improvement/authored-artifact-gate.js +42 -0
- package/dist/agent/self-improvement/authored-tool-runtime.d.ts +27 -0
- package/dist/agent/self-improvement/authored-tool-runtime.js +57 -0
- package/dist/agent/self-improvement/authored-tool-store.d.ts +24 -0
- package/dist/agent/self-improvement/authored-tool-store.js +57 -0
- package/dist/agent/self-improvement/llm-tool-proposer.d.ts +41 -0
- package/dist/agent/self-improvement/llm-tool-proposer.js +136 -0
- package/dist/agent/self-improvement/sandbox-scorer.d.ts +17 -0
- package/dist/agent/self-improvement/sandbox-scorer.js +43 -0
- package/dist/agent/self-improvement/self-knowledge.d.ts +8 -0
- package/dist/agent/self-improvement/self-knowledge.js +24 -0
- package/dist/agent/self-improvement/skill-benchmark.d.ts +9 -0
- package/dist/agent/self-improvement/skill-benchmark.js +22 -0
- package/dist/agent/self-improvement/skill-consolidator.d.ts +71 -0
- package/dist/agent/self-improvement/skill-consolidator.js +137 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +42 -0
- package/dist/agent/self-improvement/skill-engine.js +87 -0
- package/dist/agent/self-improvement/skill-gate.d.ts +19 -0
- package/dist/agent/self-improvement/skill-gate.js +62 -0
- package/dist/agent/self-improvement/skill-mutator.d.ts +74 -0
- package/dist/agent/self-improvement/skill-mutator.js +223 -0
- package/dist/agent/self-improvement/skill-proposer.d.ts +40 -0
- package/dist/agent/self-improvement/skill-proposer.js +82 -0
- package/dist/agent/self-improvement/skill-types.d.ts +41 -0
- package/dist/agent/self-improvement/skill-types.js +13 -0
- package/dist/agent/self-improvement/tool-benchmark.d.ts +10 -0
- package/dist/agent/self-improvement/tool-benchmark.js +37 -0
- package/dist/agent/self-improvement/tool-engine.d.ts +54 -0
- package/dist/agent/self-improvement/tool-engine.js +101 -0
- package/dist/agent/self-improvement/tool-gate.d.ts +20 -0
- package/dist/agent/self-improvement/tool-gate.js +78 -0
- package/dist/agent/self-improvement/tool-proposer.d.ts +31 -0
- package/dist/agent/self-improvement/tool-proposer.js +34 -0
- package/dist/agent/self-improvement/tool-skill-mutator.d.ts +40 -0
- package/dist/agent/self-improvement/tool-skill-mutator.js +79 -0
- package/dist/agent/self-improvement/tool-types.d.ts +48 -0
- package/dist/agent/self-improvement/tool-types.js +9 -0
- package/dist/agent/self-improvement/types.d.ts +3 -1
- package/dist/agent/tool-handler.js +3 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +6 -1
- package/dist/codebuddy/tools.d.ts +7 -0
- package/dist/codebuddy/tools.js +40 -0
- package/dist/commands/cli/improve-command.js +123 -0
- package/dist/commands/enhanced-command-handler.js +1 -1
- package/dist/commands/handlers/missing-handlers.d.ts +1 -1
- package/dist/commands/handlers/missing-handlers.js +26 -3
- package/dist/commands/skills-cli/index.js +123 -0
- package/dist/commands/slash/builtin-commands.js +1 -1
- package/dist/companion/percepts.js +11 -1
- package/dist/context/bootstrap-loader.js +6 -23
- package/dist/context/import-directive-parser.d.ts +4 -0
- package/dist/context/import-directive-parser.js +51 -6
- package/dist/context/instruction-excludes.d.ts +30 -1
- package/dist/context/instruction-excludes.js +71 -1
- package/dist/context/jit-context.d.ts +8 -10
- package/dist/context/jit-context.js +28 -106
- package/dist/context/project-context.d.ts +90 -0
- package/dist/context/project-context.js +295 -0
- package/dist/daemon/autonomous-loop.d.ts +31 -1
- package/dist/daemon/autonomous-loop.js +80 -2
- package/dist/harness/contract.d.ts +28 -28
- package/dist/identity/identity-manager.js +3 -2
- package/dist/index.js +17 -1
- package/dist/mcp/mcp-resources.js +2 -3
- package/dist/sensory/dreaming.d.ts +45 -0
- package/dist/sensory/dreaming.js +114 -0
- package/dist/sensory/heartbeat-scheduler.d.ts +38 -0
- package/dist/sensory/heartbeat-scheduler.js +72 -0
- package/dist/sensory/reactions.d.ts +24 -0
- package/dist/sensory/reactions.js +31 -0
- package/dist/sensory/screen-reaction.d.ts +23 -0
- package/dist/sensory/screen-reaction.js +59 -0
- package/dist/sensory/sensory-bridge.d.ts +23 -0
- package/dist/sensory/sensory-bridge.js +85 -0
- package/dist/sensory/sensory-memory.d.ts +20 -0
- package/dist/sensory/sensory-memory.js +39 -0
- package/dist/sensory/speech-reaction.d.ts +21 -0
- package/dist/sensory/speech-reaction.js +83 -0
- package/dist/sensory/vision-reaction.d.ts +31 -0
- package/dist/sensory/vision-reaction.js +74 -0
- package/dist/server/index.js +89 -0
- package/dist/services/prompt-builder.d.ts +10 -0
- package/dist/services/prompt-builder.js +75 -9
- package/dist/skills/parser.js +3 -0
- package/dist/skills/skill-importer.d.ts +58 -0
- package/dist/skills/skill-importer.js +261 -0
- package/dist/skills/skill-sources.d.ts +20 -0
- package/dist/skills/skill-sources.js +102 -0
- package/dist/skills/types.d.ts +6 -0
- package/dist/tools/register-tool-handler.d.ts +25 -0
- package/dist/tools/register-tool-handler.js +100 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +8 -0
- package/dist/utils/init-project.d.ts +7 -0
- package/dist/utils/init-project.js +37 -0
- package/dist/utils/settings-manager.d.ts +12 -0
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -844,6 +844,10 @@ function createApp(config) {
|
|
|
844
844
|
app.use(errorHandler);
|
|
845
845
|
return app;
|
|
846
846
|
}
|
|
847
|
+
/** Guards the sensory layer against double-wiring on a second in-process start. */
|
|
848
|
+
let sensoryWired = false;
|
|
849
|
+
/** Teardown fns for the sensory layer (bridge close, listener unsubscribes, scheduler stop). */
|
|
850
|
+
let sensoryTeardown = [];
|
|
847
851
|
/**
|
|
848
852
|
* Start the server
|
|
849
853
|
*/
|
|
@@ -1018,6 +1022,80 @@ export async function startServer(userConfig = {}) {
|
|
|
1018
1022
|
logger.info(`Metrics Dashboard: ${baseUrl}/api/metrics/dashboard`);
|
|
1019
1023
|
logger.info(`Docs: ${baseUrl}/api/docs`);
|
|
1020
1024
|
logger.info(`WebSocket: ${config.websocketEnabled ? 'Enabled (/ws)' : 'Disabled'}`);
|
|
1025
|
+
// Sensory nervous-system bridge (opt-in): ingress for the Rust buddy-sense
|
|
1026
|
+
// daemon → internal event bus → reactions. Loopback-only.
|
|
1027
|
+
if (process.env.CODEBUDDY_SENSORY === 'true' && !sensoryWired) {
|
|
1028
|
+
sensoryWired = true; // wire once per process (a 2nd start would double listeners + re-bind the port)
|
|
1029
|
+
try {
|
|
1030
|
+
const { startSensoryBridge } = await import('../sensory/sensory-bridge.js');
|
|
1031
|
+
const { wireSensoryReactions } = await import('../sensory/reactions.js');
|
|
1032
|
+
const { getHeartbeatScheduler } = await import('../sensory/heartbeat-scheduler.js');
|
|
1033
|
+
const sensoryBridgeHandle = startSensoryBridge();
|
|
1034
|
+
const unwireReactions = wireSensoryReactions();
|
|
1035
|
+
sensoryTeardown.push(() => sensoryBridgeHandle.close(), unwireReactions);
|
|
1036
|
+
// Vision reaction (opt-in) — vision/motion → camera_analyze (local gemma).
|
|
1037
|
+
// Requires a shared token: a frame can trigger the webcam, so refuse to
|
|
1038
|
+
// wire it on an unauthenticated bridge.
|
|
1039
|
+
const sensoryToken = process.env.CODEBUDDY_SENSORY_TOKEN;
|
|
1040
|
+
{
|
|
1041
|
+
const { shouldWireVisionReaction, wireVisionReaction } = await import('../sensory/vision-reaction.js');
|
|
1042
|
+
if (shouldWireVisionReaction({ camera: process.env.CODEBUDDY_SENSORY_CAMERA, token: sensoryToken })) {
|
|
1043
|
+
sensoryTeardown.push(wireVisionReaction());
|
|
1044
|
+
logger.info('Sensory vision reaction: Enabled (vision/motion → camera_analyze)');
|
|
1045
|
+
}
|
|
1046
|
+
else if (process.env.CODEBUDDY_SENSORY_CAMERA === 'true') {
|
|
1047
|
+
logger.warn('Sensory vision reaction NOT enabled: set CODEBUDDY_SENSORY_TOKEN to allow camera triggering.');
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// Screen reaction (opt-in) — also token-gated (an injected analyzer could capture the desktop).
|
|
1051
|
+
if (process.env.CODEBUDDY_SENSORY_SCREEN === 'true') {
|
|
1052
|
+
if (sensoryToken) {
|
|
1053
|
+
const { wireScreenReaction } = await import('../sensory/screen-reaction.js');
|
|
1054
|
+
sensoryTeardown.push(wireScreenReaction());
|
|
1055
|
+
logger.info('Sensory screen reaction: Enabled (screen/change → percept)');
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
logger.warn('Sensory screen reaction NOT enabled: set CODEBUDDY_SENSORY_TOKEN.');
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// Speech reaction (opt-in) — speech_end → STT → 'hearing' percept (+ onHeard hook).
|
|
1062
|
+
if (process.env.CODEBUDDY_SENSORY_SPEECH === 'true') {
|
|
1063
|
+
const { wireSpeechReaction } = await import('../sensory/speech-reaction.js');
|
|
1064
|
+
sensoryTeardown.push(wireSpeechReaction());
|
|
1065
|
+
logger.info('Sensory speech reaction: Enabled (speech_end → STT → percept)');
|
|
1066
|
+
}
|
|
1067
|
+
// Privacy: camera/screen descriptions land in percepts.jsonl — warn if not encrypted at rest.
|
|
1068
|
+
if ((process.env.CODEBUDDY_SENSORY_CAMERA === 'true' || process.env.CODEBUDDY_SENSORY_SCREEN === 'true') &&
|
|
1069
|
+
!process.env.CODEBUDDY_COMPANION_ENCRYPTION_KEY &&
|
|
1070
|
+
!process.env.CODEBUDDY_MEMORY_KEY) {
|
|
1071
|
+
logger.warn('Sensory camera/screen percepts are written UNENCRYPTED — set CODEBUDDY_COMPANION_ENCRYPTION_KEY to encrypt scene/screen descriptions at rest.');
|
|
1072
|
+
}
|
|
1073
|
+
// Heartbeat pacemaker — heartbeats trigger periodic processing (every N beats).
|
|
1074
|
+
const heart = getHeartbeatScheduler();
|
|
1075
|
+
const everyBeats = Math.max(1, Number(process.env.CODEBUDDY_HEARTBEAT_EVERY ?? 10));
|
|
1076
|
+
heart.register({
|
|
1077
|
+
name: 'pacemaker-tick',
|
|
1078
|
+
everyBeats,
|
|
1079
|
+
handler: (ctx) => logger.info(`[heartbeat] pacemaker tick — beat ${ctx.beat} (load ${ctx.load1 ?? '?'})`),
|
|
1080
|
+
});
|
|
1081
|
+
// Dreaming — consolidate short-term sensory memory every N beats.
|
|
1082
|
+
const dreamEvery = Math.max(1, Number(process.env.CODEBUDDY_DREAM_EVERY ?? 30));
|
|
1083
|
+
heart.register({
|
|
1084
|
+
name: 'dreaming',
|
|
1085
|
+
everyBeats: dreamEvery,
|
|
1086
|
+
handler: async () => {
|
|
1087
|
+
const { runDreamingPass } = await import('../sensory/dreaming.js');
|
|
1088
|
+
await runDreamingPass();
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
heart.start();
|
|
1092
|
+
sensoryTeardown.push(() => heart.stop());
|
|
1093
|
+
logger.info(`Sensory bridge: Enabled (buddy-sense → event bus; heartbeat treatments every ${everyBeats} beats)`);
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
logger.warn(`Sensory bridge failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1021
1099
|
logger.info(`Auth: ${config.authEnabled ? 'Enabled' : 'Disabled'}`);
|
|
1022
1100
|
if (!isLoopbackHost(config.host)) {
|
|
1023
1101
|
logger.warn(`Server is bound to ${config.host} (non-loopback) and is reachable from the network. ` +
|
|
@@ -1072,6 +1150,17 @@ export async function startServer(userConfig = {}) {
|
|
|
1072
1150
|
* Stop the server gracefully
|
|
1073
1151
|
*/
|
|
1074
1152
|
export async function stopServer(server) {
|
|
1153
|
+
// Tear down the sensory layer (WS bridge, bus listeners, heartbeat scheduler) so
|
|
1154
|
+
// an in-process restart doesn't leak listeners or EADDRINUSE the bridge port.
|
|
1155
|
+
for (const teardown of sensoryTeardown.splice(0)) {
|
|
1156
|
+
try {
|
|
1157
|
+
await teardown();
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
/* never throw on shutdown */
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
sensoryWired = false;
|
|
1075
1164
|
return new Promise((resolve, reject) => {
|
|
1076
1165
|
// Phase (d).9 — cancel the heartbeat timer so it doesn't keep
|
|
1077
1166
|
// emitting against a half-shut server. Idempotent.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { EnhancedMemory, PersistentMemoryManager } from "../memory/index.js";
|
|
13
13
|
import { PromptCacheManager } from "../optimization/prompt-cache.js";
|
|
14
14
|
import { MoltbotHooksManager } from "../hooks/moltbot-hooks.js";
|
|
15
|
+
import { type ContextRegistry } from "../context/project-context.js";
|
|
15
16
|
import { type QueryComplexity } from "../agent/execution/query-classifier.js";
|
|
16
17
|
export interface PromptBuilderConfig {
|
|
17
18
|
yoloMode: boolean;
|
|
@@ -48,7 +49,16 @@ export declare class PromptBuilder {
|
|
|
48
49
|
private memory?;
|
|
49
50
|
private moltbotHooksManager?;
|
|
50
51
|
private persistentMemory?;
|
|
52
|
+
/**
|
|
53
|
+
* Dedup registry for project-instruction files, recreated fresh at the start
|
|
54
|
+
* of each system-prompt build. The JIT context pass reads it (via
|
|
55
|
+
* `getContextRegistry()`) so files already injected at startup are not
|
|
56
|
+
* re-injected when a tool later touches a file in the same tree.
|
|
57
|
+
*/
|
|
58
|
+
private contextRegistry;
|
|
51
59
|
constructor(config: PromptBuilderConfig, promptCacheManager: PromptCacheManager, memory?: EnhancedMemory | undefined, moltbotHooksManager?: MoltbotHooksManager | undefined, persistentMemory?: PersistentMemoryManager | undefined);
|
|
60
|
+
/** The registry from the most recent system-prompt build (for the JIT pass). */
|
|
61
|
+
getContextRegistry(): ContextRegistry | null;
|
|
52
62
|
/**
|
|
53
63
|
* Build the system prompt for the agent. The optional `options`
|
|
54
64
|
* parameter gates per-block injection. All gates default to `true`
|
|
@@ -13,6 +13,7 @@ import { logger } from "../utils/logger.js";
|
|
|
13
13
|
import { getErrorMessage } from "../errors/index.js";
|
|
14
14
|
import { getSystemPromptForMode, getPromptManager, autoSelectPromptId, getChatOnlySystemPrompt, } from "../prompts/index.js";
|
|
15
15
|
import { getModelToolConfig } from "../config/model-tools.js";
|
|
16
|
+
import { resolveProjectContext, createContextRegistry, setActiveContextRegistry } from "../context/project-context.js";
|
|
16
17
|
import { classifyQuery } from "../agent/execution/query-classifier.js";
|
|
17
18
|
import { filterToolNames, getToolFilter, isToolNameAllowed, } from "../utils/tool-filter.js";
|
|
18
19
|
const ALL_BLOCKS = {
|
|
@@ -65,6 +66,13 @@ export class PromptBuilder {
|
|
|
65
66
|
memory;
|
|
66
67
|
moltbotHooksManager;
|
|
67
68
|
persistentMemory;
|
|
69
|
+
/**
|
|
70
|
+
* Dedup registry for project-instruction files, recreated fresh at the start
|
|
71
|
+
* of each system-prompt build. The JIT context pass reads it (via
|
|
72
|
+
* `getContextRegistry()`) so files already injected at startup are not
|
|
73
|
+
* re-injected when a tool later touches a file in the same tree.
|
|
74
|
+
*/
|
|
75
|
+
contextRegistry = null;
|
|
68
76
|
constructor(config, promptCacheManager, memory, moltbotHooksManager, persistentMemory) {
|
|
69
77
|
this.config = config;
|
|
70
78
|
this.promptCacheManager = promptCacheManager;
|
|
@@ -72,6 +80,10 @@ export class PromptBuilder {
|
|
|
72
80
|
this.moltbotHooksManager = moltbotHooksManager;
|
|
73
81
|
this.persistentMemory = persistentMemory;
|
|
74
82
|
}
|
|
83
|
+
/** The registry from the most recent system-prompt build (for the JIT pass). */
|
|
84
|
+
getContextRegistry() {
|
|
85
|
+
return this.contextRegistry;
|
|
86
|
+
}
|
|
75
87
|
/**
|
|
76
88
|
* Build the system prompt for the agent. The optional `options`
|
|
77
89
|
* parameter gates per-block injection. All gates default to `true`
|
|
@@ -248,18 +260,36 @@ export class PromptBuilder {
|
|
|
248
260
|
logger.warn('Failed to inject execution-discipline block', { error: getErrorMessage(err) });
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
|
-
// Inject
|
|
263
|
+
// Inject project-instruction context (AGENTS.md / CODEBUDDY.md / CLAUDE.md
|
|
264
|
+
// / GEMINI.md / CONTEXT.md / INSTRUCTIONS.md) via the unified hierarchical
|
|
265
|
+
// loader, then soul/bootstrap files. A fresh dedup registry is created per
|
|
266
|
+
// build and reused by the JIT pass (`getContextRegistry`).
|
|
252
267
|
if (gates.includeBootstrap) {
|
|
268
|
+
this.contextRegistry = createContextRegistry();
|
|
269
|
+
// Publish for the JIT pass so it skips files injected here at startup.
|
|
270
|
+
setActiveContextRegistry(this.contextRegistry);
|
|
271
|
+
try {
|
|
272
|
+
const ctx = resolveProjectContext({ cwd: this.config.cwd, registry: this.contextRegistry });
|
|
273
|
+
if (ctx.text) {
|
|
274
|
+
systemPrompt += '\n\n# Workspace Context\n\n' + ctx.text;
|
|
275
|
+
logger.debug(`Loaded project context from ${ctx.sources.length} file(s)`, {
|
|
276
|
+
sources: ctx.sources.map((s) => s.relPath),
|
|
277
|
+
chars: ctx.bytes,
|
|
278
|
+
truncated: ctx.truncated,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
logger.warn("Failed to load project context", { error: getErrorMessage(err) });
|
|
284
|
+
}
|
|
285
|
+
// Soul/identity bootstrap files (SOUL.md, USER.md, …) + PROJECT_KNOWLEDGE.md.
|
|
286
|
+
// Instruction files are handled above by the unified loader, not here.
|
|
253
287
|
try {
|
|
254
288
|
const { BootstrapLoader } = await import('../context/bootstrap-loader.js');
|
|
255
289
|
const bootstrap = await new BootstrapLoader().load(this.config.cwd);
|
|
256
290
|
if (bootstrap.content) {
|
|
257
|
-
systemPrompt += '\n\n
|
|
258
|
-
logger.debug(`Loaded bootstrap context from ${bootstrap.sources.length} file(s)
|
|
259
|
-
sources: bootstrap.sources,
|
|
260
|
-
chars: bootstrap.tokenCount,
|
|
261
|
-
truncated: bootstrap.truncated,
|
|
262
|
-
});
|
|
291
|
+
systemPrompt += '\n\n' + bootstrap.content;
|
|
292
|
+
logger.debug(`Loaded bootstrap/soul context from ${bootstrap.sources.length} file(s)`);
|
|
263
293
|
}
|
|
264
294
|
}
|
|
265
295
|
catch (err) {
|
|
@@ -325,6 +355,31 @@ export class PromptBuilder {
|
|
|
325
355
|
}
|
|
326
356
|
catch { /* rules module optional */ }
|
|
327
357
|
}
|
|
358
|
+
// Steer toward Code Explorer (gitnexus) when it is connected. Conditional:
|
|
359
|
+
// when Code Explorer is absent this injects nothing, so the built-in
|
|
360
|
+
// code_graph/codebase_map behaviour is unchanged. Presence is session-
|
|
361
|
+
// stable, so this stays in the cache-stable prefix.
|
|
362
|
+
try {
|
|
363
|
+
const { isCodeExplorerAvailable } = await import('../codebuddy/tools.js');
|
|
364
|
+
if (isCodeExplorerAvailable()) {
|
|
365
|
+
systemPrompt +=
|
|
366
|
+
`\n\n<code_explorer_priority>\n` +
|
|
367
|
+
`Code Explorer (gitnexus) is connected. For ANY question about code relationships — ` +
|
|
368
|
+
`callers/callees, blast radius / impact ("what breaks if I change X"), dead code, cycles, ` +
|
|
369
|
+
`coupling, complexity — PREFER its MCP tools (\`mcp__gitnexus__impact\`, ` +
|
|
370
|
+
`\`mcp__gitnexus__context\`, \`mcp__gitnexus__query\`, \`mcp__gitnexus__find_cycles\`, …) ` +
|
|
371
|
+
`over the built-in \`code_graph\` / \`codebase_map\`: the gitnexus graph is broader and more ` +
|
|
372
|
+
`complete (whole-repo, 14 languages).\n` +
|
|
373
|
+
`Usage: first call \`mcp__gitnexus__list_repos\` once to get the repo \`path\`/\`id\`, then call ` +
|
|
374
|
+
`\`mcp__gitnexus__impact\` with the REQUIRED \`target\` = the symbol name (e.g. \`target: "executePlan"\`, ` +
|
|
375
|
+
`optionally \`direction: "both"\`) and \`repo\` = that path; or \`mcp__gitnexus__context\` with \`name\` = ` +
|
|
376
|
+
`the symbol. Always include \`target\`/\`name\` — never call these tools with empty arguments. ` +
|
|
377
|
+
`Use the built-in \`code_graph\`/\`codebase_map\` only as a fallback if a gitnexus tool errors.\n` +
|
|
378
|
+
`</code_explorer_priority>`;
|
|
379
|
+
logger.debug('Injected Code Explorer priority directive');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch { /* tools module optional */ }
|
|
328
383
|
// Inject active skill prompt enhancement
|
|
329
384
|
if (gates.includeSkills) {
|
|
330
385
|
try {
|
|
@@ -338,10 +393,11 @@ export class PromptBuilder {
|
|
|
338
393
|
}
|
|
339
394
|
catch { /* skills module optional */ }
|
|
340
395
|
}
|
|
341
|
-
// Inject identity (SOUL.md, USER.md,
|
|
396
|
+
// Inject identity (SOUL.md, USER.md, …) — skip if already present to avoid duplication.
|
|
397
|
+
// AGENTS.md/INSTRUCTIONS.md are owned by the unified loader, not identity.
|
|
342
398
|
if (gates.includeIdentity) {
|
|
343
399
|
try {
|
|
344
|
-
if (!systemPrompt.includes('## SOUL.md')
|
|
400
|
+
if (!systemPrompt.includes('## SOUL.md')) {
|
|
345
401
|
const { getIdentityManager } = await import('../identity/identity-manager.js');
|
|
346
402
|
const identityMgr = getIdentityManager();
|
|
347
403
|
await identityMgr.load(this.config.cwd);
|
|
@@ -475,6 +531,16 @@ Lessons complement \`remember\`: \`remember\` stores facts (preferences, decisio
|
|
|
475
531
|
</lessons_directive>`;
|
|
476
532
|
logger.debug('Injected lessons directive into system prompt');
|
|
477
533
|
}
|
|
534
|
+
// Self-knowledge — only when self-improvement is explicitly opted in. Makes
|
|
535
|
+
// the agent aware it can author its own tools/skills (and the hard `src/` limit).
|
|
536
|
+
if (process.env.CODEBUDDY_SELF_IMPROVE === 'true') {
|
|
537
|
+
try {
|
|
538
|
+
const { buildSelfKnowledgeBlock } = await import('../agent/self-improvement/self-knowledge.js');
|
|
539
|
+
systemPrompt += `\n\n<self_knowledge>\n${buildSelfKnowledgeBlock()}\n</self_knowledge>`;
|
|
540
|
+
logger.debug('Injected self-knowledge block into system prompt');
|
|
541
|
+
}
|
|
542
|
+
catch { /* self-improvement module optional */ }
|
|
543
|
+
}
|
|
478
544
|
if (this.config.memoryEnabled && gates.includeUserModelDirective) {
|
|
479
545
|
systemPrompt += `\n\n<user_model_directive>
|
|
480
546
|
You have a persistent user model that builds a deepening profile of who you are, your traits, preferences, expertise, and working style.
|
package/dist/skills/parser.js
CHANGED
|
@@ -92,6 +92,9 @@ function parseMetadata(yamlContent, sourcePath) {
|
|
|
92
92
|
// CC11: context fork and disable-model-invocation
|
|
93
93
|
contextFork: parsed.context === 'fork' || parsed.contextFork === true,
|
|
94
94
|
disableModelInvocation: parsed['disable-model-invocation'] === true || parsed.disableModelInvocation === true,
|
|
95
|
+
pinned: parsed.pinned === true,
|
|
96
|
+
imported: parsed.imported === true,
|
|
97
|
+
source: parsed.source,
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
catch (error) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill importer — bring EXTERNAL skills (a Hermes repo, any skills directory)
|
|
3
|
+
* into Code Buddy, safely. External skills are untrusted and get injected into the
|
|
4
|
+
* agent's context, so the firewall gates every one (SKILL.md + its scripts) before
|
|
5
|
+
* install. Hermes nests 1–3 levels but our registry walks 1 level → flatten; Hermes
|
|
6
|
+
* tags live under `metadata.hermes.tags` but our discovery scores top-level `tags` +
|
|
7
|
+
* `nativeEngine.triggers` → remap, else an imported skill is invisible to matching.
|
|
8
|
+
*
|
|
9
|
+
* Imported skills are namespaced `imported-*` (distinct provenance; never touched by
|
|
10
|
+
* the self-improvement engine) and installed flat (1 level) under a tier root.
|
|
11
|
+
*
|
|
12
|
+
* @module skills/skill-importer
|
|
13
|
+
*/
|
|
14
|
+
export declare const IMPORTED_PREFIX = "imported-";
|
|
15
|
+
export interface ImportOptions {
|
|
16
|
+
/** Tier dir to install under. Default ~/.codebuddy/skills/managed. */
|
|
17
|
+
destRoot?: string;
|
|
18
|
+
/** Provenance label written to frontmatter (e.g. "hermes"). */
|
|
19
|
+
source?: string;
|
|
20
|
+
/** When true, scan + report but write nothing. */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** Import skills the firewall flags as 'review' (default: skip them). */
|
|
23
|
+
includeReview?: boolean;
|
|
24
|
+
/** Overwrite an existing imported-<name> (default: skip). */
|
|
25
|
+
overwrite?: boolean;
|
|
26
|
+
/** Only import skills whose source path contains this substring. */
|
|
27
|
+
category?: string;
|
|
28
|
+
/** Pin imported skills so curation leaves them alone (default true). */
|
|
29
|
+
pinByDefault?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ImportedSkill {
|
|
32
|
+
name: string;
|
|
33
|
+
sourcePath: string;
|
|
34
|
+
verdict: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SkippedSkill {
|
|
37
|
+
sourcePath: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
verdict?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ImportReport {
|
|
42
|
+
imported: ImportedSkill[];
|
|
43
|
+
quarantined: SkippedSkill[];
|
|
44
|
+
review: SkippedSkill[];
|
|
45
|
+
skipped: SkippedSkill[];
|
|
46
|
+
total: number;
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Recursively find skill directories (those containing a SKILL.md). Skips operational dirs. */
|
|
50
|
+
export declare function findSkillDirs(root: string): string[];
|
|
51
|
+
/** Build a Code-Buddy-shaped SKILL.md from a raw (e.g. Hermes) frontmatter object + body. */
|
|
52
|
+
export declare function remapSkill(rawFm: Record<string, unknown>, body: string, opts: {
|
|
53
|
+
slug: string;
|
|
54
|
+
source: string;
|
|
55
|
+
pinned: boolean;
|
|
56
|
+
}): string;
|
|
57
|
+
/** Import skills from a directory. Pure-ish: writes nothing when dryRun. */
|
|
58
|
+
export declare function importSkills(sourceDir: string, options?: ImportOptions): ImportReport;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill importer — bring EXTERNAL skills (a Hermes repo, any skills directory)
|
|
3
|
+
* into Code Buddy, safely. External skills are untrusted and get injected into the
|
|
4
|
+
* agent's context, so the firewall gates every one (SKILL.md + its scripts) before
|
|
5
|
+
* install. Hermes nests 1–3 levels but our registry walks 1 level → flatten; Hermes
|
|
6
|
+
* tags live under `metadata.hermes.tags` but our discovery scores top-level `tags` +
|
|
7
|
+
* `nativeEngine.triggers` → remap, else an imported skill is invisible to matching.
|
|
8
|
+
*
|
|
9
|
+
* Imported skills are namespaced `imported-*` (distinct provenance; never touched by
|
|
10
|
+
* the self-improvement engine) and installed flat (1 level) under a tier root.
|
|
11
|
+
*
|
|
12
|
+
* @module skills/skill-importer
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import * as yaml from 'yaml';
|
|
18
|
+
import { scanSkillFirewall } from '../security/skill-scanner.js';
|
|
19
|
+
import { parseSkillFile, validateSkill } from './parser.js';
|
|
20
|
+
import { logger } from '../utils/logger.js';
|
|
21
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
22
|
+
const SUPPORT_DIRS = ['references', 'templates', 'scripts', 'assets', 'workflows', 'tests'];
|
|
23
|
+
const SKIP_DIRS = new Set(['.git', 'index-cache', '.archive', 'node_modules', '.sources-cache']);
|
|
24
|
+
export const IMPORTED_PREFIX = 'imported-';
|
|
25
|
+
function defaultDestRoot() {
|
|
26
|
+
return path.join(os.homedir(), '.codebuddy', 'skills', 'managed');
|
|
27
|
+
}
|
|
28
|
+
/** Recursively find skill directories (those containing a SKILL.md). Skips operational dirs. */
|
|
29
|
+
export function findSkillDirs(root) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const walk = (dir) => {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (entries.some((e) => e.isFile() && e.name.toLowerCase() === 'skill.md')) {
|
|
40
|
+
out.push(dir);
|
|
41
|
+
// don't descend into a skill's own support dirs
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
if (!e.isDirectory() || e.name.startsWith('.') || SKIP_DIRS.has(e.name))
|
|
46
|
+
continue;
|
|
47
|
+
walk(path.join(dir, e.name));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(root);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function slugify(raw) {
|
|
54
|
+
const base = String(raw).trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
55
|
+
return base.startsWith(IMPORTED_PREFIX) ? base : `${IMPORTED_PREFIX}${base || 'skill'}`;
|
|
56
|
+
}
|
|
57
|
+
function normalizeTags(raw) {
|
|
58
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const t of list) {
|
|
62
|
+
const s = String(t).trim().toLowerCase();
|
|
63
|
+
if (s && !seen.has(s)) {
|
|
64
|
+
seen.add(s);
|
|
65
|
+
out.push(s);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
const STOPWORDS = new Set([
|
|
71
|
+
'the', 'and', 'for', 'with', 'your', 'via', 'into', 'from', 'that', 'this', 'use', 'using', 'create',
|
|
72
|
+
'creates', 'creating', 'a', 'an', 'or', 'to', 'of', 'in', 'on', 'as', 'by', 'it', 'is', 'are', 'add',
|
|
73
|
+
'list', 'get', 'set', 'run', 'when', 'how', 'you', 'can', 'will', 'their', 'them', 'they', 'about',
|
|
74
|
+
]);
|
|
75
|
+
/** Derive discovery triggers (the primary matcher) from the name + tags + description keywords. */
|
|
76
|
+
function deriveTriggers(rawName, tags, description = '') {
|
|
77
|
+
const nameWords = String(rawName).toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3);
|
|
78
|
+
const descWords = String(description)
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.split(/[^a-z0-9]+/)
|
|
81
|
+
.filter((w) => w.length >= 4 && !STOPWORDS.has(w));
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const t of [String(rawName).toLowerCase(), ...tags, ...nameWords, ...descWords]) {
|
|
85
|
+
if (t && !seen.has(t)) {
|
|
86
|
+
seen.add(t);
|
|
87
|
+
out.push(t);
|
|
88
|
+
}
|
|
89
|
+
if (out.length >= 12)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** Extract discovery tags from any source layout: top-level `tags`, or `metadata.<source>.tags`. */
|
|
95
|
+
function extractTags(rawFm) {
|
|
96
|
+
if (Array.isArray(rawFm.tags))
|
|
97
|
+
return rawFm.tags;
|
|
98
|
+
const meta = rawFm.metadata;
|
|
99
|
+
if (meta && typeof meta === 'object') {
|
|
100
|
+
for (const v of Object.values(meta)) {
|
|
101
|
+
if (v && typeof v === 'object' && Array.isArray(v.tags)) {
|
|
102
|
+
return v.tags;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
/** Map prerequisites/requires from any source → our SkillRequirements.tools. */
|
|
109
|
+
function extractRequiresTools(rawFm) {
|
|
110
|
+
const out = new Set();
|
|
111
|
+
const add = (v) => {
|
|
112
|
+
if (Array.isArray(v))
|
|
113
|
+
for (const x of v)
|
|
114
|
+
if (typeof x === 'string')
|
|
115
|
+
out.add(x);
|
|
116
|
+
};
|
|
117
|
+
// Hermes: prerequisites.commands
|
|
118
|
+
const prereq = rawFm.prerequisites;
|
|
119
|
+
if (prereq)
|
|
120
|
+
add(prereq.commands);
|
|
121
|
+
// OpenClaw (and others): metadata.<source>.requires.bins
|
|
122
|
+
const meta = rawFm.metadata;
|
|
123
|
+
if (meta && typeof meta === 'object') {
|
|
124
|
+
for (const v of Object.values(meta)) {
|
|
125
|
+
const req = v?.requires;
|
|
126
|
+
if (req)
|
|
127
|
+
add(req.bins);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...out];
|
|
131
|
+
}
|
|
132
|
+
/** Build a Code-Buddy-shaped SKILL.md from a raw (e.g. Hermes) frontmatter object + body. */
|
|
133
|
+
export function remapSkill(rawFm, body, opts) {
|
|
134
|
+
const description = String(rawFm.description ?? '').trim() || `Imported skill ${opts.slug}`;
|
|
135
|
+
const tags = normalizeTags(extractTags(rawFm));
|
|
136
|
+
const requiresTools = extractRequiresTools(rawFm);
|
|
137
|
+
const author = Array.isArray(rawFm.author) ? rawFm.author.join(', ') : rawFm.author;
|
|
138
|
+
const meta = {
|
|
139
|
+
name: opts.slug,
|
|
140
|
+
description,
|
|
141
|
+
...(rawFm.version ? { version: rawFm.version } : {}),
|
|
142
|
+
...(author ? { author } : {}),
|
|
143
|
+
...(rawFm.license ? { license: rawFm.license } : {}),
|
|
144
|
+
...(rawFm.platforms ? { platforms: rawFm.platforms } : {}),
|
|
145
|
+
tags,
|
|
146
|
+
nativeEngine: { triggers: deriveTriggers(String(rawFm.name ?? opts.slug), tags, description) },
|
|
147
|
+
...(requiresTools.length ? { requires: { tools: requiresTools } } : {}),
|
|
148
|
+
imported: true,
|
|
149
|
+
source: opts.source,
|
|
150
|
+
...(opts.pinned ? { pinned: true } : {}),
|
|
151
|
+
};
|
|
152
|
+
return `---\n${yaml.stringify(meta)}---\n\n${body.trim()}\n`;
|
|
153
|
+
}
|
|
154
|
+
function copySupportDirs(srcDir, destDir) {
|
|
155
|
+
for (const sub of SUPPORT_DIRS) {
|
|
156
|
+
const from = path.join(srcDir, sub);
|
|
157
|
+
if (fs.existsSync(from) && fs.statSync(from).isDirectory()) {
|
|
158
|
+
fs.cpSync(from, path.join(destDir, sub), {
|
|
159
|
+
recursive: true,
|
|
160
|
+
// never follow symlinks out of the source tree
|
|
161
|
+
filter: (s) => {
|
|
162
|
+
try {
|
|
163
|
+
return !fs.lstatSync(s).isSymbolicLink();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Import skills from a directory. Pure-ish: writes nothing when dryRun. */
|
|
174
|
+
export function importSkills(sourceDir, options = {}) {
|
|
175
|
+
const destRoot = options.destRoot ?? defaultDestRoot();
|
|
176
|
+
const source = options.source ?? 'import';
|
|
177
|
+
const dryRun = options.dryRun ?? false;
|
|
178
|
+
const pinByDefault = options.pinByDefault ?? true;
|
|
179
|
+
const report = { imported: [], quarantined: [], review: [], skipped: [], total: 0, dryRun };
|
|
180
|
+
const skillDirs = findSkillDirs(sourceDir);
|
|
181
|
+
report.total = skillDirs.length;
|
|
182
|
+
for (const skillDir of skillDirs) {
|
|
183
|
+
const rel = path.relative(sourceDir, skillDir);
|
|
184
|
+
if (options.category && !rel.includes(options.category)) {
|
|
185
|
+
report.skipped.push({ sourcePath: rel, reason: 'filtered by --category' });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const skillMd = fs.existsSync(path.join(skillDir, 'SKILL.md'))
|
|
189
|
+
? path.join(skillDir, 'SKILL.md')
|
|
190
|
+
: path.join(skillDir, 'skill.md');
|
|
191
|
+
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
192
|
+
// Compatibility check.
|
|
193
|
+
try {
|
|
194
|
+
const skill = parseSkillFile(content, skillMd, 'managed');
|
|
195
|
+
const v = validateSkill(skill);
|
|
196
|
+
if (!v.valid) {
|
|
197
|
+
report.skipped.push({ sourcePath: rel, reason: `invalid: ${v.errors.join('; ')}` });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
report.skipped.push({ sourcePath: rel, reason: `parse error: ${err instanceof Error ? err.message : String(err)}` });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Firewall gate (scans SKILL.md + scripts/support files recursively).
|
|
206
|
+
const fw = scanSkillFirewall(skillDir);
|
|
207
|
+
if (fw.quarantineRequired) {
|
|
208
|
+
report.quarantined.push({ sourcePath: rel, reason: fw.summary, verdict: String(fw.verdict) });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (String(fw.verdict) === 'review' && !options.includeReview) {
|
|
212
|
+
report.review.push({ sourcePath: rel, reason: fw.summary, verdict: 'review' });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Remap + flatten + install.
|
|
216
|
+
const m = content.match(FRONTMATTER_RE);
|
|
217
|
+
if (!m) {
|
|
218
|
+
report.skipped.push({ sourcePath: rel, reason: 'missing frontmatter' });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let rawFm;
|
|
222
|
+
try {
|
|
223
|
+
rawFm = (yaml.parse(m[1]) ?? {});
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
report.skipped.push({ sourcePath: rel, reason: 'unparseable frontmatter' });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const slug = slugify(String(rawFm.name ?? path.basename(skillDir)));
|
|
230
|
+
const destDir = path.join(destRoot, slug);
|
|
231
|
+
if (fs.existsSync(destDir) && !options.overwrite) {
|
|
232
|
+
report.skipped.push({ sourcePath: rel, reason: `conflict: ${slug} already imported` });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!dryRun) {
|
|
236
|
+
try {
|
|
237
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
238
|
+
fs.writeFileSync(path.join(destDir, 'SKILL.md'), remapSkill(rawFm, m[2], { slug, source, pinned: pinByDefault }), 'utf-8');
|
|
239
|
+
copySupportDirs(skillDir, destDir);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
report.skipped.push({ sourcePath: rel, reason: `write error: ${err instanceof Error ? err.message : String(err)}` });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
report.imported.push({ name: slug, sourcePath: rel, verdict: String(fw.verdict) });
|
|
247
|
+
}
|
|
248
|
+
if (!dryRun && report.imported.length > 0) {
|
|
249
|
+
void (async () => {
|
|
250
|
+
try {
|
|
251
|
+
const { getSkillRegistry } = await import('./registry.js');
|
|
252
|
+
await getSkillRegistry().reloadAll();
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
logger.debug(`skill reload after import failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
}
|
|
259
|
+
return report;
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=skill-importer.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill sources — a small "referential" of named places skills can be imported
|
|
3
|
+
* from. A source is a local directory or a git repo. Persisted to
|
|
4
|
+
* `~/.codebuddy/skill-sources.json`. The importer resolves a source to a local
|
|
5
|
+
* directory (cloning/pulling a git source into a cache) and then runs the same
|
|
6
|
+
* firewall-gated import path.
|
|
7
|
+
*
|
|
8
|
+
* @module skills/skill-sources
|
|
9
|
+
*/
|
|
10
|
+
export interface SkillSource {
|
|
11
|
+
name: string;
|
|
12
|
+
type: 'dir' | 'git';
|
|
13
|
+
location: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function listSources(): SkillSource[];
|
|
16
|
+
export declare function getSource(name: string): SkillSource | undefined;
|
|
17
|
+
export declare function addSource(name: string, location: string, type?: 'dir' | 'git'): SkillSource;
|
|
18
|
+
export declare function removeSource(name: string): boolean;
|
|
19
|
+
/** Resolve a source to a local directory (clone/pull a git source into the cache). */
|
|
20
|
+
export declare function resolveSourceDir(source: SkillSource): string;
|