c0de-agent 0.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli/commands/acp.d.ts +19 -0
- package/dist/cli/commands/acp.js +50 -0
- package/dist/cli/commands/chat.d.ts +13 -0
- package/dist/cli/commands/chat.js +26 -0
- package/dist/cli/commands/config.d.ts +17 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +14 -0
- package/dist/cli/commands/plugin.d.ts +11 -0
- package/dist/cli/commands/plugin.js +31 -0
- package/dist/cli/commands/serve.d.ts +18 -0
- package/dist/cli/commands/serve.js +35 -0
- package/dist/cli/commands/update.d.ts +24 -0
- package/dist/cli/commands/update.js +40 -0
- package/dist/cli/deps.d.ts +30 -0
- package/dist/cli/deps.js +70 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +155 -0
- package/dist/cli/modes/acp.d.ts +18 -0
- package/dist/cli/modes/acp.js +57 -0
- package/dist/cli/modes/print.d.ts +16 -0
- package/dist/cli/modes/print.js +30 -0
- package/dist/cli/parser.d.ts +19 -0
- package/dist/cli/parser.js +28 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +23 -0
- package/dist/cli/utils/prompt.d.ts +12 -0
- package/dist/cli/utils/prompt.js +18 -0
- package/dist/core/agent.d.ts +12 -0
- package/dist/core/agent.js +100 -0
- package/dist/core/agents/builtin.d.ts +6 -0
- package/dist/core/agents/builtin.js +76 -0
- package/dist/core/agents/discovery.d.ts +6 -0
- package/dist/core/agents/discovery.js +96 -0
- package/dist/core/agents/index.d.ts +6 -0
- package/dist/core/agents/index.js +4 -0
- package/dist/core/agents/parallel.d.ts +13 -0
- package/dist/core/agents/parallel.js +37 -0
- package/dist/core/agents/registry.d.ts +4 -0
- package/dist/core/agents/registry.js +23 -0
- package/dist/core/agents/types.d.ts +37 -0
- package/dist/core/agents/types.js +1 -0
- package/dist/core/compact.d.ts +11 -0
- package/dist/core/compact.js +31 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +85 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.js +93 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +11 -0
- package/dist/core/loop.d.ts +35 -0
- package/dist/core/loop.js +915 -0
- package/dist/core/metrics.d.ts +27 -0
- package/dist/core/metrics.js +94 -0
- package/dist/core/prompt-registry.d.ts +21 -0
- package/dist/core/prompt-registry.js +200 -0
- package/dist/core/prompt.d.ts +11 -0
- package/dist/core/prompt.js +13 -0
- package/dist/core/secret.d.ts +12 -0
- package/dist/core/secret.js +51 -0
- package/dist/core/slash.d.ts +15 -0
- package/dist/core/slash.js +113 -0
- package/dist/core/steering.d.ts +8 -0
- package/dist/core/steering.js +15 -0
- package/dist/core/title.d.ts +42 -0
- package/dist/core/title.js +116 -0
- package/dist/core/tool-exec.d.ts +20 -0
- package/dist/core/tool-exec.js +64 -0
- package/dist/core/types.d.ts +97 -0
- package/dist/core/types.js +1 -0
- package/dist/core/worktree.d.ts +24 -0
- package/dist/core/worktree.js +70 -0
- package/dist/dap/index.d.ts +6 -0
- package/dist/dap/index.js +4 -0
- package/dist/dap/protocol.d.ts +40 -0
- package/dist/dap/protocol.js +132 -0
- package/dist/dap/session.d.ts +23 -0
- package/dist/dap/session.js +130 -0
- package/dist/dap/transport.d.ts +8 -0
- package/dist/dap/transport.js +30 -0
- package/dist/dap/types.d.ts +50 -0
- package/dist/dap/types.js +2 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +39 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +15 -0
- package/dist/db/schema.d.ts +965 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/types.d.ts +11 -0
- package/dist/db/types.js +1 -0
- package/dist/llm/index.d.ts +13 -0
- package/dist/llm/index.js +8 -0
- package/dist/llm/protocols/openai-compat.d.ts +139 -0
- package/dist/llm/protocols/openai-compat.js +214 -0
- package/dist/llm/protocols/utils/index.d.ts +2 -0
- package/dist/llm/protocols/utils/index.js +2 -0
- package/dist/llm/protocols/utils/lifecycle.d.ts +23 -0
- package/dist/llm/protocols/utils/lifecycle.js +46 -0
- package/dist/llm/protocols/utils/tool-stream.d.ts +46 -0
- package/dist/llm/protocols/utils/tool-stream.js +94 -0
- package/dist/llm/provider-error.d.ts +5 -0
- package/dist/llm/provider-error.js +33 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.js +195 -0
- package/dist/llm/registry.d.ts +44 -0
- package/dist/llm/registry.js +107 -0
- package/dist/llm/retry.d.ts +37 -0
- package/dist/llm/retry.js +82 -0
- package/dist/llm/routing.d.ts +36 -0
- package/dist/llm/routing.js +53 -0
- package/dist/llm/schema/errors.d.ts +88 -0
- package/dist/llm/schema/errors.js +33 -0
- package/dist/llm/schema/events.d.ts +135 -0
- package/dist/llm/schema/events.js +35 -0
- package/dist/llm/schema/ids.d.ts +22 -0
- package/dist/llm/schema/ids.js +3 -0
- package/dist/llm/schema/index.d.ts +5 -0
- package/dist/llm/schema/index.js +5 -0
- package/dist/llm/schema/messages.d.ts +97 -0
- package/dist/llm/schema/messages.js +18 -0
- package/dist/llm/schema/options.d.ts +54 -0
- package/dist/llm/schema/options.js +54 -0
- package/dist/llm/token.d.ts +2 -0
- package/dist/llm/token.js +13 -0
- package/dist/llm/transport.d.ts +31 -0
- package/dist/llm/transport.js +165 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/plugins/builtin.d.ts +6 -0
- package/dist/plugins/builtin.js +54 -0
- package/dist/plugins/hooks.d.ts +3 -0
- package/dist/plugins/hooks.js +79 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/init.d.ts +23 -0
- package/dist/plugins/init.js +30 -0
- package/dist/plugins/lifecycle.d.ts +6 -0
- package/dist/plugins/lifecycle.js +74 -0
- package/dist/plugins/loader.d.ts +16 -0
- package/dist/plugins/loader.js +69 -0
- package/dist/plugins/logger.d.ts +4 -0
- package/dist/plugins/logger.js +31 -0
- package/dist/plugins/registry.d.ts +7 -0
- package/dist/plugins/registry.js +24 -0
- package/dist/plugins/types.d.ts +113 -0
- package/dist/plugins/types.js +1 -0
- package/dist/project/detect.d.ts +4 -0
- package/dist/project/detect.js +131 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +2 -0
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +87 -0
- package/dist/project/resolve.d.ts +8 -0
- package/dist/project/resolve.js +81 -0
- package/dist/server/agent-manager.d.ts +34 -0
- package/dist/server/agent-manager.js +62 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.js +78 -0
- package/dist/server/context.d.ts +20 -0
- package/dist/server/context.js +49 -0
- package/dist/server/dev.d.ts +14 -0
- package/dist/server/dev.js +148 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +7 -0
- package/dist/server/middleware/auth.d.ts +15 -0
- package/dist/server/middleware/auth.js +29 -0
- package/dist/server/middleware/cors.d.ts +16 -0
- package/dist/server/middleware/cors.js +44 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.js +11 -0
- package/dist/server/permission/interactive.d.ts +24 -0
- package/dist/server/permission/interactive.js +56 -0
- package/dist/server/permission/store.d.ts +32 -0
- package/dist/server/permission/store.js +56 -0
- package/dist/server/routes/agent.d.ts +4 -0
- package/dist/server/routes/agent.js +19 -0
- package/dist/server/routes/catalog.d.ts +42 -0
- package/dist/server/routes/catalog.js +194 -0
- package/dist/server/routes/chat.d.ts +8 -0
- package/dist/server/routes/chat.js +318 -0
- package/dist/server/routes/commands.d.ts +5 -0
- package/dist/server/routes/commands.js +17 -0
- package/dist/server/routes/config.d.ts +4 -0
- package/dist/server/routes/config.js +27 -0
- package/dist/server/routes/files.d.ts +4 -0
- package/dist/server/routes/files.js +183 -0
- package/dist/server/routes/filesystem.d.ts +29 -0
- package/dist/server/routes/filesystem.js +108 -0
- package/dist/server/routes/health.d.ts +3 -0
- package/dist/server/routes/health.js +13 -0
- package/dist/server/routes/permissions.d.ts +5 -0
- package/dist/server/routes/permissions.js +23 -0
- package/dist/server/routes/project.d.ts +4 -0
- package/dist/server/routes/project.js +51 -0
- package/dist/server/routes/provider.d.ts +16 -0
- package/dist/server/routes/provider.js +66 -0
- package/dist/server/routes/session.d.ts +4 -0
- package/dist/server/routes/session.js +204 -0
- package/dist/server/routes/terminal.d.ts +5 -0
- package/dist/server/routes/terminal.js +66 -0
- package/dist/server/routes/tool.d.ts +4 -0
- package/dist/server/routes/tool.js +28 -0
- package/dist/server/routes/update.d.ts +14 -0
- package/dist/server/routes/update.js +49 -0
- package/dist/server/server.d.ts +56 -0
- package/dist/server/server.js +269 -0
- package/dist/server/terminal/pty-manager.d.ts +53 -0
- package/dist/server/terminal/pty-manager.js +160 -0
- package/dist/server/types.d.ts +76 -0
- package/dist/server/types.js +2 -0
- package/dist/server/util/safe-path.d.ts +7 -0
- package/dist/server/util/safe-path.js +7 -0
- package/dist/session/archive.d.ts +15 -0
- package/dist/session/archive.js +129 -0
- package/dist/session/branch.d.ts +9 -0
- package/dist/session/branch.js +63 -0
- package/dist/session/compaction.d.ts +33 -0
- package/dist/session/compaction.js +261 -0
- package/dist/session/context.d.ts +16 -0
- package/dist/session/context.js +173 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.js +10 -0
- package/dist/session/message.d.ts +22 -0
- package/dist/session/message.js +145 -0
- package/dist/session/session.d.ts +37 -0
- package/dist/session/session.js +144 -0
- package/dist/session/shake.d.ts +66 -0
- package/dist/session/shake.js +304 -0
- package/dist/session/snapshot.d.ts +11 -0
- package/dist/session/snapshot.js +60 -0
- package/dist/session/squash.d.ts +9 -0
- package/dist/session/squash.js +88 -0
- package/dist/session/token.d.ts +10 -0
- package/dist/session/token.js +33 -0
- package/dist/session/types.d.ts +122 -0
- package/dist/session/types.js +1 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/types/agent.d.ts +229 -0
- package/dist/shared/types/agent.js +1 -0
- package/dist/shared/types/base.d.ts +32 -0
- package/dist/shared/types/base.js +1 -0
- package/dist/shared/types/config.d.ts +114 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/index.d.ts +6 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/llm.d.ts +123 -0
- package/dist/shared/types/llm.js +1 -0
- package/dist/shared/types/message.d.ts +71 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/tool.d.ts +137 -0
- package/dist/shared/types/tool.js +1 -0
- package/dist/shared/utils/path.d.ts +15 -0
- package/dist/shared/utils/path.js +23 -0
- package/dist/shared/utils.d.ts +5 -0
- package/dist/shared/utils.js +10 -0
- package/dist/tools/builtin/bash.d.ts +12 -0
- package/dist/tools/builtin/bash.js +121 -0
- package/dist/tools/builtin/dap.d.ts +23 -0
- package/dist/tools/builtin/dap.js +231 -0
- package/dist/tools/builtin/edit.d.ts +12 -0
- package/dist/tools/builtin/edit.js +151 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +120 -0
- package/dist/tools/builtin/grep.d.ts +6 -0
- package/dist/tools/builtin/grep.js +141 -0
- package/dist/tools/builtin/read.d.ts +6 -0
- package/dist/tools/builtin/read.js +73 -0
- package/dist/tools/builtin/resolvers.d.ts +12 -0
- package/dist/tools/builtin/resolvers.js +79 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +100 -0
- package/dist/tools/builtin/write.d.ts +6 -0
- package/dist/tools/builtin/write.js +36 -0
- package/dist/tools/builtin/yield.d.ts +9 -0
- package/dist/tools/builtin/yield.js +40 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.js +54 -0
- package/dist/tools/hashline/index.d.ts +2 -0
- package/dist/tools/hashline/index.js +3 -0
- package/dist/tools/hashline/patch.d.ts +48 -0
- package/dist/tools/hashline/patch.js +201 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.js +55 -0
- package/dist/tools/permission.d.ts +13 -0
- package/dist/tools/permission.js +39 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.js +54 -0
- package/dist/tools/resolver.d.ts +15 -0
- package/dist/tools/resolver.js +47 -0
- package/dist/tools/truncate.d.ts +8 -0
- package/dist/tools/truncate.js +50 -0
- package/dist/tools/types.d.ts +101 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/validate.d.ts +8 -0
- package/dist/tools/validate.js +130 -0
- package/dist/tools/websearch/fetch.d.ts +11 -0
- package/dist/tools/websearch/fetch.js +21 -0
- package/dist/tools/websearch/index.d.ts +19 -0
- package/dist/tools/websearch/index.js +78 -0
- package/dist/tools/websearch/providers/brave.d.ts +5 -0
- package/dist/tools/websearch/providers/brave.js +53 -0
- package/dist/tools/websearch/providers/duckduckgo.d.ts +3 -0
- package/dist/tools/websearch/providers/duckduckgo.js +75 -0
- package/dist/tools/websearch/providers/tavily.d.ts +11 -0
- package/dist/tools/websearch/providers/tavily.js +89 -0
- package/dist/tools/websearch/types.d.ts +51 -0
- package/dist/tools/websearch/types.js +18 -0
- package/dist/tools/websearch/websearch.d.ts +10 -0
- package/dist/tools/websearch/websearch.js +48 -0
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +6 -0
- package/dist/update/ipc.d.ts +15 -0
- package/dist/update/ipc.js +41 -0
- package/dist/update/scheduler.d.ts +34 -0
- package/dist/update/scheduler.js +57 -0
- package/dist/update/snapshot.d.ts +39 -0
- package/dist/update/snapshot.js +99 -0
- package/dist/update/updater.d.ts +48 -0
- package/dist/update/updater.js +83 -0
- package/dist/update/version.d.ts +18 -0
- package/dist/update/version.js +49 -0
- package/drizzle/0000_magical_nightshade.sql +55 -0
- package/drizzle/0001_chemical_doctor_faustus.sql +13 -0
- package/drizzle/0002_amusing_micromacro.sql +12 -0
- package/drizzle/0003_regular_purifiers.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +448 -0
- package/drizzle/meta/0001_snapshot.json +539 -0
- package/drizzle/meta/0002_snapshot.json +633 -0
- package/drizzle/meta/0003_snapshot.json +645 -0
- package/drizzle/meta/_journal.json +34 -0
- package/package.json +108 -8
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { safeResolve } from '../../shared/utils/path.js';
|
|
3
|
+
import { applyPatch, parsePatch } from '../hashline/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* edit tool: file editing in two modes (spec §16.4).
|
|
6
|
+
* - diff mode: search-and-replace with fuzzy whitespace matching. Provide
|
|
7
|
+
* `oldText` + `newText`. Returns error if oldText is absent or ambiguous.
|
|
8
|
+
* - hashline mode: content-hash-anchored patch language (spec §16). Provide
|
|
9
|
+
* `patch`. If the file changed since the patch was generated, the hash
|
|
10
|
+
* mismatch is rejected rather than misapplied.
|
|
11
|
+
*
|
|
12
|
+
* Permission: ask (modifies filesystem).
|
|
13
|
+
*/
|
|
14
|
+
export const editTool = {
|
|
15
|
+
name: 'edit',
|
|
16
|
+
description: 'Edit a file. Two modes: (1) diff — provide oldText+newText for fuzzy search-and-replace; (2) hashline — provide `patch`, a content-hash-anchored patch that fails safely if the file changed since the patch was generated.',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
path: { type: 'string', description: 'File path (relative to cwd or absolute).' },
|
|
21
|
+
oldText: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'diff mode: text to find in the file (fuzzy whitespace matching).',
|
|
24
|
+
},
|
|
25
|
+
newText: { type: 'string', description: 'diff mode: replacement text.' },
|
|
26
|
+
patch: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'hashline mode: a patch block like `[path#hash]\\nSWAP start-end\\nnew content\\n---`. Generate the hash from the CURRENT file content; a stale hash is rejected.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['path'],
|
|
32
|
+
},
|
|
33
|
+
permission: 'ask',
|
|
34
|
+
execute: async (input, ctx) => {
|
|
35
|
+
const raw = input;
|
|
36
|
+
const path = raw.path;
|
|
37
|
+
const fullPath = safeResolve(ctx.cwd, path);
|
|
38
|
+
if (fullPath === null) {
|
|
39
|
+
return { _tag: 'error', error: `Path "${path}" escapes the working directory` };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
// ── hashline 模式 ──────────────────────────────────
|
|
43
|
+
if ('patch' in raw && typeof raw.patch === 'string') {
|
|
44
|
+
const patches = parsePatch(raw.patch);
|
|
45
|
+
if (patches.length === 0) {
|
|
46
|
+
return { _tag: 'error', error: `hashline: empty patch for "${path}"` };
|
|
47
|
+
}
|
|
48
|
+
let current = await readFile(fullPath, 'utf-8');
|
|
49
|
+
for (const p of patches) {
|
|
50
|
+
const r = applyPatch(current, p);
|
|
51
|
+
if (r._tag !== 'success') {
|
|
52
|
+
return { _tag: 'error', error: formatHashlineError(path, r) };
|
|
53
|
+
}
|
|
54
|
+
current = r.content;
|
|
55
|
+
}
|
|
56
|
+
await writeFile(fullPath, current, 'utf-8');
|
|
57
|
+
return {
|
|
58
|
+
_tag: 'success',
|
|
59
|
+
output: `Edited "${path}" via hashline patch (${patches.length} block(s))`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// ── diff 模式 ──────────────────────────────────────
|
|
63
|
+
if (!('oldText' in raw) || !('newText' in raw)) {
|
|
64
|
+
return {
|
|
65
|
+
_tag: 'error',
|
|
66
|
+
error: `edit: provide either 'patch' (hashline) or 'oldText'+'newText' (diff) for "${path}"`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const oldText = raw.oldText;
|
|
70
|
+
const newText = raw.newText;
|
|
71
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
72
|
+
// Fuzzy whitespace matching: normalize whitespace runs
|
|
73
|
+
const normalize = (s) => s.replace(/[ \t]+/g, ' ');
|
|
74
|
+
const normalizedContent = normalize(content);
|
|
75
|
+
const normalizedOld = normalize(oldText);
|
|
76
|
+
// Find all match positions
|
|
77
|
+
const matches = [];
|
|
78
|
+
let searchFrom = 0;
|
|
79
|
+
while (true) {
|
|
80
|
+
const idx = normalizedContent.indexOf(normalizedOld, searchFrom);
|
|
81
|
+
if (idx === -1)
|
|
82
|
+
break;
|
|
83
|
+
matches.push(idx);
|
|
84
|
+
searchFrom = idx + normalizedOld.length;
|
|
85
|
+
}
|
|
86
|
+
if (matches.length === 0) {
|
|
87
|
+
return { _tag: 'error', error: `oldText not found in "${path}"` };
|
|
88
|
+
}
|
|
89
|
+
if (matches.length > 1) {
|
|
90
|
+
return {
|
|
91
|
+
_tag: 'error',
|
|
92
|
+
error: `oldText matches ${matches.length} times in "${path}" — multiple matches found, provide more context to disambiguate`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Map normalized match back to original content
|
|
96
|
+
const matchIdx = matches[0] ?? 0;
|
|
97
|
+
const prefix = normalizedContent.slice(0, matchIdx);
|
|
98
|
+
const charCount = prefix.length;
|
|
99
|
+
const mapping = buildPositionMapping(content, normalizedContent);
|
|
100
|
+
const origStart = mapping.get(charCount) ?? charCount;
|
|
101
|
+
const origEnd = mapping.get(charCount + normalizedOld.length) ?? charCount + normalizedOld.length;
|
|
102
|
+
const newContent = content.slice(0, origStart) + newText + content.slice(origEnd);
|
|
103
|
+
await writeFile(fullPath, newContent, 'utf-8');
|
|
104
|
+
return {
|
|
105
|
+
_tag: 'success',
|
|
106
|
+
output: `Edited "${path}": replaced ${origEnd - origStart} chars with ${newText.length} chars`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
111
|
+
return { _tag: 'error', error: `Failed to edit "${path}": ${message}` };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
/** 把 hashline ApplyResult 失败分支格式化为对 agent 有指导意义的错误信息。 */
|
|
116
|
+
function formatHashlineError(path, result) {
|
|
117
|
+
if (result._tag === 'hash_mismatch') {
|
|
118
|
+
return `"${path}": hashline hash mismatch — the file changed since this patch was generated (expected ${result.expected}, actual ${result.actual}). Re-read the file and regenerate the patch with the current hash.`;
|
|
119
|
+
}
|
|
120
|
+
const op = result.operation;
|
|
121
|
+
const range = 'start' in op ? `${op.start}${op.end !== op.start ? `-${op.end}` : ''}` : '';
|
|
122
|
+
return `"${path}": hashline line range ${range} out of bounds for ${op._tag} operation.`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build a mapping from normalized string positions to original string positions.
|
|
126
|
+
* Used to map fuzzy match results back to the original content.
|
|
127
|
+
*/
|
|
128
|
+
function buildPositionMapping(original, normalized) {
|
|
129
|
+
const map = new Map();
|
|
130
|
+
let origIdx = 0;
|
|
131
|
+
let normIdx = 0;
|
|
132
|
+
while (origIdx < original.length && normIdx < normalized.length) {
|
|
133
|
+
map.set(normIdx, origIdx);
|
|
134
|
+
if (original[origIdx] === normalized[normIdx]) {
|
|
135
|
+
origIdx++;
|
|
136
|
+
normIdx++;
|
|
137
|
+
}
|
|
138
|
+
else if (original[origIdx] === ' ' || original[origIdx] === '\t') {
|
|
139
|
+
// Original has whitespace that was collapsed
|
|
140
|
+
origIdx++;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Shouldn't happen with proper normalization
|
|
144
|
+
origIdx++;
|
|
145
|
+
normIdx++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Map the end position
|
|
149
|
+
map.set(normIdx, origIdx);
|
|
150
|
+
return map;
|
|
151
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDef } from '../../shared/types/tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a glob pattern to a RegExp.
|
|
4
|
+
* Supports: * (single segment), ** (across segments), ? (single char), {a,b} (alternation), [abc] (char class).
|
|
5
|
+
*/
|
|
6
|
+
export declare function globToRegex(pattern: string): RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* glob tool: find files matching a glob pattern.
|
|
9
|
+
* Permission: auto (read-only).
|
|
10
|
+
*/
|
|
11
|
+
export declare const globTool: ToolDef;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
/** Directories always skipped during glob traversal. */
|
|
4
|
+
const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.turbo']);
|
|
5
|
+
/**
|
|
6
|
+
* Convert a glob pattern to a RegExp.
|
|
7
|
+
* Supports: * (single segment), ** (across segments), ? (single char), {a,b} (alternation), [abc] (char class).
|
|
8
|
+
*/
|
|
9
|
+
export function globToRegex(pattern) {
|
|
10
|
+
let re = '';
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < pattern.length) {
|
|
13
|
+
const c = pattern[i] ?? '';
|
|
14
|
+
if (c === '*') {
|
|
15
|
+
if (pattern[i + 1] === '*') {
|
|
16
|
+
re += '.*';
|
|
17
|
+
i += 2;
|
|
18
|
+
if (pattern[i] === '/')
|
|
19
|
+
i++; // skip separator after **
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
re += '[^/]*';
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (c === '?') {
|
|
27
|
+
re += '[^/]';
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
else if (c === '{') {
|
|
31
|
+
const end = pattern.indexOf('}', i);
|
|
32
|
+
if (end === -1) {
|
|
33
|
+
re += '\\{';
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const inner = pattern.slice(i + 1, end);
|
|
38
|
+
re += `(?:${inner.split(',').map(escapeRegex).join('|')})`;
|
|
39
|
+
i = end + 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (c === '[') {
|
|
43
|
+
const end = pattern.indexOf(']', i);
|
|
44
|
+
if (end === -1) {
|
|
45
|
+
re += '\\[';
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
re += pattern.slice(i, end + 1);
|
|
50
|
+
i = end + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if ('.+^$()|\\'.includes(c)) {
|
|
54
|
+
re += `\\${c}`;
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
re += c;
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return new RegExp(`^${re}$`);
|
|
63
|
+
}
|
|
64
|
+
function escapeRegex(s) {
|
|
65
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
66
|
+
}
|
|
67
|
+
/** Recursively walk a directory, skipping IGNORE_DIRS. Returns relative file paths. */
|
|
68
|
+
async function walkDir(dir, base) {
|
|
69
|
+
const results = [];
|
|
70
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
71
|
+
if (!entries)
|
|
72
|
+
return [];
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
76
|
+
continue;
|
|
77
|
+
const sub = await walkDir(join(dir, entry.name), base);
|
|
78
|
+
results.push(...sub);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
results.push(relative(base, join(dir, entry.name)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* glob tool: find files matching a glob pattern.
|
|
88
|
+
* Permission: auto (read-only).
|
|
89
|
+
*/
|
|
90
|
+
export const globTool = {
|
|
91
|
+
name: 'glob',
|
|
92
|
+
description: 'Find files matching a glob pattern. Supports *, **, ?, {a,b}. Searches recursively, skipping node_modules/.git/dist.',
|
|
93
|
+
parameters: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
pattern: { type: 'string', description: 'Glob pattern (e.g. "src/**/*.ts").' },
|
|
97
|
+
path: { type: 'string', description: 'Base directory to search (default: cwd).' },
|
|
98
|
+
},
|
|
99
|
+
required: ['pattern'],
|
|
100
|
+
},
|
|
101
|
+
permission: 'auto',
|
|
102
|
+
execute: async (input, ctx) => {
|
|
103
|
+
const { pattern, path } = input;
|
|
104
|
+
const basePath = path ? resolve(ctx.cwd, path) : ctx.cwd;
|
|
105
|
+
try {
|
|
106
|
+
const regex = globToRegex(pattern);
|
|
107
|
+
const files = await walkDir(basePath, basePath);
|
|
108
|
+
const matched = files.filter((f) => regex.test(f)).sort();
|
|
109
|
+
return {
|
|
110
|
+
_tag: 'success',
|
|
111
|
+
output: matched.join('\n'),
|
|
112
|
+
metadata: { count: matched.length },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
return { _tag: 'error', error: `Glob failed: ${message}` };
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
/** Directories always skipped during search. */
|
|
4
|
+
const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.turbo']);
|
|
5
|
+
/** File extensions treated as text (skip binary files). */
|
|
6
|
+
const TEXT_EXTENSIONS = new Set([
|
|
7
|
+
'.ts',
|
|
8
|
+
'.tsx',
|
|
9
|
+
'.js',
|
|
10
|
+
'.jsx',
|
|
11
|
+
'.mjs',
|
|
12
|
+
'.cjs',
|
|
13
|
+
'.json',
|
|
14
|
+
'.md',
|
|
15
|
+
'.mdx',
|
|
16
|
+
'.txt',
|
|
17
|
+
'.css',
|
|
18
|
+
'.scss',
|
|
19
|
+
'.html',
|
|
20
|
+
'.htm',
|
|
21
|
+
'.xml',
|
|
22
|
+
'.yaml',
|
|
23
|
+
'.yml',
|
|
24
|
+
'.toml',
|
|
25
|
+
'.ini',
|
|
26
|
+
'.env',
|
|
27
|
+
'.sh',
|
|
28
|
+
'.bash',
|
|
29
|
+
'.zsh',
|
|
30
|
+
'.py',
|
|
31
|
+
'.rb',
|
|
32
|
+
'.go',
|
|
33
|
+
'.rs',
|
|
34
|
+
'.java',
|
|
35
|
+
'.kt',
|
|
36
|
+
'.c',
|
|
37
|
+
'.cpp',
|
|
38
|
+
'.h',
|
|
39
|
+
'.hpp',
|
|
40
|
+
'.cs',
|
|
41
|
+
'.php',
|
|
42
|
+
'.swift',
|
|
43
|
+
'.sql',
|
|
44
|
+
'.graphql',
|
|
45
|
+
'.gql',
|
|
46
|
+
'.vue',
|
|
47
|
+
'.svelte',
|
|
48
|
+
'.astro',
|
|
49
|
+
]);
|
|
50
|
+
/** Maximum file size to search (skip files > 1MB). */
|
|
51
|
+
const MAX_FILE_SIZE = 1024 * 1024;
|
|
52
|
+
/** Recursively walk a directory for text files. */
|
|
53
|
+
async function walkForFiles(dir, base) {
|
|
54
|
+
const results = [];
|
|
55
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
56
|
+
if (!entries)
|
|
57
|
+
return [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
61
|
+
continue;
|
|
62
|
+
const sub = await walkForFiles(join(dir, entry.name), base);
|
|
63
|
+
results.push(...sub);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const ext = entry.name.slice(entry.name.lastIndexOf('.'));
|
|
67
|
+
if (TEXT_EXTENSIONS.has(ext) || ext === '') {
|
|
68
|
+
results.push(join(dir, entry.name));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* grep tool: search file contents with regex.
|
|
76
|
+
* Permission: auto (read-only).
|
|
77
|
+
*/
|
|
78
|
+
export const grepTool = {
|
|
79
|
+
name: 'grep',
|
|
80
|
+
description: 'Search file contents using regex. Searches recursively across text files, skipping node_modules/.git. Returns matching lines with file and line number.',
|
|
81
|
+
parameters: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
pattern: { type: 'string', description: 'Regular expression pattern.' },
|
|
85
|
+
path: { type: 'string', description: 'Base directory to search (default: cwd).' },
|
|
86
|
+
caseSensitive: { type: 'boolean', description: 'Case-sensitive search (default: true).' },
|
|
87
|
+
maxResults: { type: 'number', description: 'Maximum number of matches to return.' },
|
|
88
|
+
},
|
|
89
|
+
required: ['pattern'],
|
|
90
|
+
},
|
|
91
|
+
permission: 'auto',
|
|
92
|
+
execute: async (input, ctx) => {
|
|
93
|
+
const { pattern, path, caseSensitive = true, maxResults = 200 } = input;
|
|
94
|
+
const basePath = path ? resolve(ctx.cwd, path) : ctx.cwd;
|
|
95
|
+
let regex;
|
|
96
|
+
try {
|
|
97
|
+
regex = new RegExp(pattern, caseSensitive ? '' : 'i');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
return { _tag: 'error', error: `Invalid regex pattern: ${message}` };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const files = await walkForFiles(basePath, basePath);
|
|
105
|
+
const matches = [];
|
|
106
|
+
const max = maxResults;
|
|
107
|
+
outer: for (const filePath of files) {
|
|
108
|
+
const stat = await readFile(filePath);
|
|
109
|
+
if (stat.length > MAX_FILE_SIZE)
|
|
110
|
+
continue;
|
|
111
|
+
const content = stat.toString('utf-8');
|
|
112
|
+
const lines = content.split('\n');
|
|
113
|
+
const relPath = relative(basePath, filePath);
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const line = lines[i] ?? '';
|
|
116
|
+
const match = line.match(regex);
|
|
117
|
+
if (match) {
|
|
118
|
+
matches.push({
|
|
119
|
+
file: relPath,
|
|
120
|
+
line: i + 1,
|
|
121
|
+
text: line.trim(),
|
|
122
|
+
match: match[0] ?? '',
|
|
123
|
+
});
|
|
124
|
+
if (matches.length >= max)
|
|
125
|
+
break outer;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const output = matches.map((m) => `${m.file}:${m.line}: ${m.text}`).join('\n');
|
|
130
|
+
return {
|
|
131
|
+
_tag: 'success',
|
|
132
|
+
output,
|
|
133
|
+
metadata: { count: matches.length, truncated: matches.length >= max },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
return { _tag: 'error', error: `Grep failed: ${message}` };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { safeResolve } from '../../shared/utils/path.js';
|
|
3
|
+
import { isURLPath, resolveURL } from '../resolver.js';
|
|
4
|
+
/** Apply optional offset/limit (1-indexed) to text content. */
|
|
5
|
+
function applyRange(content, offset, limit) {
|
|
6
|
+
if (offset === undefined && limit === undefined)
|
|
7
|
+
return content;
|
|
8
|
+
const lines = content.split('\n');
|
|
9
|
+
const start = (offset ?? 1) - 1; // convert to 0-indexed
|
|
10
|
+
const end = limit !== undefined ? start + limit : lines.length;
|
|
11
|
+
return lines.slice(start, end).join('\n');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* read tool: read file content with optional line range.
|
|
15
|
+
* Permission: auto (read-only).
|
|
16
|
+
*/
|
|
17
|
+
export const readTool = {
|
|
18
|
+
name: 'read',
|
|
19
|
+
description: 'Read file content, or list a directory (entries with trailing `/` on subdirectories). Supports optional offset (1-indexed line number) and limit (number of lines) for files. Also resolves internal URL schemes (skill://, agent://, pr://, issue://) when a resolver registry is configured.',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
path: { type: 'string', description: 'File path (relative to cwd or absolute).' },
|
|
24
|
+
offset: { type: 'number', description: 'Starting line number (1-indexed). Default: 1.' },
|
|
25
|
+
limit: { type: 'number', description: 'Maximum number of lines to read.' },
|
|
26
|
+
},
|
|
27
|
+
required: ['path'],
|
|
28
|
+
},
|
|
29
|
+
permission: 'auto',
|
|
30
|
+
execute: async (input, ctx) => {
|
|
31
|
+
const { path, offset, limit } = input;
|
|
32
|
+
// 内部 URL scheme(spec §3.10):skill://, agent://, pr:// 等。
|
|
33
|
+
// 命中时走 resolver 拿到文本内容,再统一应用 offset/limit;目录列举对 URL 无意义。
|
|
34
|
+
if (isURLPath(path)) {
|
|
35
|
+
if (!ctx.urlRegistry) {
|
|
36
|
+
return {
|
|
37
|
+
_tag: 'error',
|
|
38
|
+
error: `Cannot resolve URL "${path}": no resolver registry available`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const res = await resolveURL(ctx.urlRegistry, path, { cwd: ctx.cwd, session: ctx.session });
|
|
42
|
+
if (res._tag === 'error')
|
|
43
|
+
return { _tag: 'error', error: res.error };
|
|
44
|
+
return { _tag: 'success', output: applyRange(res.content, offset, limit) };
|
|
45
|
+
}
|
|
46
|
+
const fullPath = safeResolve(ctx.cwd, path);
|
|
47
|
+
if (fullPath === null) {
|
|
48
|
+
return { _tag: 'error', error: `Path "${path}" escapes the working directory` };
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
// 目录:列出条目(子目录加 `/` 后缀),与 system prompt 的「listing a
|
|
52
|
+
// directory → read」契约一致。否则 readFile 遇到目录会抛 EISDIR,
|
|
53
|
+
// 而 prompt 却告诉模型 read 能列目录——模型拿到这个矛盾的错误信号后
|
|
54
|
+
// 会在 glob/read/bash 之间反复试探,最终放弃调用工具而提前 stop。
|
|
55
|
+
const info = await stat(fullPath);
|
|
56
|
+
if (info.isDirectory()) {
|
|
57
|
+
const entries = await readdir(fullPath, { withFileTypes: true });
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
return { _tag: 'success', output: `(empty directory) ${path}` };
|
|
60
|
+
}
|
|
61
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
62
|
+
const lines = entries.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
63
|
+
return { _tag: 'success', output: lines.join('\n') };
|
|
64
|
+
}
|
|
65
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
66
|
+
return { _tag: 'success', output: applyRange(content, offset, limit) };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return { _tag: 'error', error: `Failed to read "${path}": ${message}` };
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { URLRegistry, URLResolver } from '../../shared/types/tool.js';
|
|
2
|
+
/** file:// 解析器:`file://rel/path` 相对 cwd;`file:///abs/path` 用绝对路径。 */
|
|
3
|
+
declare function createFileResolver(): URLResolver;
|
|
4
|
+
/** skill:// 解析器:技能文件候选路径(项目优先于全局)。 */
|
|
5
|
+
declare function createSkillResolver(opts?: {
|
|
6
|
+
homeDir?: string;
|
|
7
|
+
}): URLResolver;
|
|
8
|
+
/** 内置 URL 解析器注册表:预装 file + skill。 */
|
|
9
|
+
declare function createDefaultURLRegistry(opts?: {
|
|
10
|
+
homeDir?: string;
|
|
11
|
+
}): URLRegistry;
|
|
12
|
+
export { createDefaultURLRegistry, createFileResolver, createSkillResolver };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// 内置 URL 解析器(spec §3.10)。
|
|
2
|
+
//
|
|
3
|
+
// registry 框架(createURLRegistry / registerURLResolver / resolveURL 分发)见
|
|
4
|
+
// ../resolver.ts。这里实现具体 scheme 的内容来源:
|
|
5
|
+
// - file:// → 本地文件(相对 ctx.cwd 或绝对路径)
|
|
6
|
+
// - skill:// → 技能文件,项目 .c0de/skills/<name>(.md|/SKILL.md) 优先于
|
|
7
|
+
// 全局 ~/.c0de/skills/<name>(.md|/SKILL.md)
|
|
8
|
+
//
|
|
9
|
+
// agent:// pr:// issue:// 依赖未实现的子 agent 输出与 GitHub 访问,暂不内置。
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { resolve as resolvePath } from 'node:path';
|
|
13
|
+
import { createURLRegistry, registerURLResolver } from '../resolver.js';
|
|
14
|
+
/** 从 `scheme://rest` 中取出 rest(scheme 已校验过,直接切首个 `://`)。 */
|
|
15
|
+
function stripScheme(url) {
|
|
16
|
+
const idx = url.indexOf('://');
|
|
17
|
+
return idx < 0 ? url : url.slice(idx + 3);
|
|
18
|
+
}
|
|
19
|
+
/** 读文件,读不到抛错(由 resolveURL 包装成 error result)。 */
|
|
20
|
+
async function readOrFail(path) {
|
|
21
|
+
return readFile(path, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
/** file:// 解析器:`file://rel/path` 相对 cwd;`file:///abs/path` 用绝对路径。 */
|
|
24
|
+
function createFileResolver() {
|
|
25
|
+
return {
|
|
26
|
+
scheme: 'file',
|
|
27
|
+
resolve: async (url, ctx) => {
|
|
28
|
+
const rest = stripScheme(url);
|
|
29
|
+
// file:///abs → rest 形如 /abs(已含前导斜杠);file://rel → rest 形如 rel。
|
|
30
|
+
const path = resolvePath(ctx.cwd, rest);
|
|
31
|
+
return readOrFail(path);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 按优先级返回第一个可读候选文件的路径,全部缺失时返回 null。 */
|
|
36
|
+
async function firstExistingFile(candidates) {
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
try {
|
|
39
|
+
// 用 readFile 探测:能读到即返回。失败的候选静默跳过(ENOENT 等)。
|
|
40
|
+
await readFile(c, 'utf-8');
|
|
41
|
+
return c;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// 继续尝试下一个候选路径
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** skill:// 解析器:技能文件候选路径(项目优先于全局)。 */
|
|
50
|
+
function createSkillResolver(opts) {
|
|
51
|
+
const home = opts?.homeDir ?? homedir();
|
|
52
|
+
return {
|
|
53
|
+
scheme: 'skill',
|
|
54
|
+
resolve: async (url, ctx) => {
|
|
55
|
+
const name = stripScheme(url);
|
|
56
|
+
if (!name)
|
|
57
|
+
throw new Error('skill URL requires a name, e.g. skill://brainstorming');
|
|
58
|
+
const candidates = [
|
|
59
|
+
resolvePath(ctx.cwd, '.c0de', 'skills', `${name}.md`),
|
|
60
|
+
resolvePath(ctx.cwd, '.c0de', 'skills', name, 'SKILL.md'),
|
|
61
|
+
resolvePath(home, '.c0de', 'skills', `${name}.md`),
|
|
62
|
+
resolvePath(home, '.c0de', 'skills', name, 'SKILL.md'),
|
|
63
|
+
];
|
|
64
|
+
const found = await firstExistingFile(candidates);
|
|
65
|
+
if (!found) {
|
|
66
|
+
throw new Error(`Skill "${name}" not found. Looked in:\n${candidates.map((c) => ` - ${c}`).join('\n')}`);
|
|
67
|
+
}
|
|
68
|
+
return readOrFail(found);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** 内置 URL 解析器注册表:预装 file + skill。 */
|
|
73
|
+
function createDefaultURLRegistry(opts) {
|
|
74
|
+
const reg = createURLRegistry();
|
|
75
|
+
registerURLResolver(reg, createFileResolver());
|
|
76
|
+
registerURLResolver(reg, createSkillResolver(opts));
|
|
77
|
+
return reg;
|
|
78
|
+
}
|
|
79
|
+
export { createDefaultURLRegistry, createFileResolver, createSkillResolver };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolDef } from '../../shared/types/tool.js';
|
|
2
|
+
/**
|
|
3
|
+
* task 工具:按 agent 类型派发子 agent。
|
|
4
|
+
*
|
|
5
|
+
* 两种形态:单任务(subagent_type + prompt)或批量并行(subagent_type + context + tasks[])。
|
|
6
|
+
* 依赖反转:实际子 agent 运行由 host(core loop 的 runSubAgent)通过 ctx.runSubAgent 执行。
|
|
7
|
+
* permission: auto(子 agent 是隔离 session,不扩宽父权限)。
|
|
8
|
+
*/
|
|
9
|
+
export declare const taskTool: ToolDef;
|