@developerz.ai/ai-claude-compat 0.0.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 +25 -0
- package/dist/agents-loader.d.ts +10 -0
- package/dist/agents-loader.js +48 -0
- package/dist/agents-loader.js.map +1 -0
- package/dist/bash-tool.d.ts +20 -0
- package/dist/bash-tool.js +61 -0
- package/dist/bash-tool.js.map +1 -0
- package/dist/edit-tools.d.ts +40 -0
- package/dist/edit-tools.js +81 -0
- package/dist/edit-tools.js.map +1 -0
- package/dist/env-block.d.ts +6 -0
- package/dist/env-block.js +29 -0
- package/dist/env-block.js.map +1 -0
- package/dist/frontmatter.d.ts +8 -0
- package/dist/frontmatter.js +70 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/fs-tools.d.ts +25 -0
- package/dist/fs-tools.js +79 -0
- package/dist/fs-tools.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/safe-path.d.ts +1 -0
- package/dist/safe-path.js +71 -0
- package/dist/safe-path.js.map +1 -0
- package/dist/search-tools.d.ts +29 -0
- package/dist/search-tools.js +166 -0
- package/dist/search-tools.js.map +1 -0
- package/dist/skills-loader.d.ts +7 -0
- package/dist/skills-loader.js +33 -0
- package/dist/skills-loader.js.map +1 -0
- package/dist/subagent.d.ts +11 -0
- package/dist/subagent.js +25 -0
- package/dist/subagent.js.map +1 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 developerz-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @developerz.ai/ai-claude-compat
|
|
2
|
+
|
|
3
|
+
Claude-Code-style agent primitives for the [Vercel AI SDK](https://ai-sdk.dev):
|
|
4
|
+
FS/edit/search/bash tools, an `<env>` system-context block, a subagent-as-tool
|
|
5
|
+
factory, and `.claude/` skills/agents loading.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @developerz.ai/ai-claude-compat ai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What's inside
|
|
14
|
+
|
|
15
|
+
- **Tools** — filesystem, edit, search, and bash tools scoped to a working
|
|
16
|
+
directory, shaped like Claude Code's tool surface.
|
|
17
|
+
- **`composeSystemPrompt` / `envBlock`** — assemble a system prompt with an
|
|
18
|
+
`<env>` block (cwd, platform, OS, runtime, date).
|
|
19
|
+
- **`createSubagent`** — wrap an `experimental_Agent` as a callable tool
|
|
20
|
+
(the subagents-as-tools pattern).
|
|
21
|
+
- **`.claude/` loading** — discover and parse local skills and agents.
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type AgentDefinition = {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
tools?: string[];
|
|
5
|
+
model?: string;
|
|
6
|
+
systemPrompt: string;
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function loadAgents(claudeDir: string): Promise<AgentDefinition[]>;
|
|
10
|
+
export declare function claudeDirs(cwd: string): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Discover Claude-Code-style subagent definitions: `<claudeDir>/agents/<name>.md`, each with
|
|
2
|
+
// YAML frontmatter (name, description, tools, model) + a markdown body that is the subagent's
|
|
3
|
+
// system prompt. Mirrors how the harness scans `.claude/agents/` (see claude-code-knowledge
|
|
4
|
+
// skills-and-subagents.md). Also exposes the standard global + project `.claude` locations.
|
|
5
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { basename, join } from 'node:path';
|
|
8
|
+
import { asString, asStringArray, parseFrontmatter } from "./frontmatter.js";
|
|
9
|
+
// Load every agent under `<claudeDir>/agents/*.md`. `claudeDir` is a `.claude` directory.
|
|
10
|
+
// A missing dir yields []. Sorted by name.
|
|
11
|
+
export async function loadAgents(claudeDir) {
|
|
12
|
+
const agentsDir = join(claudeDir, 'agents');
|
|
13
|
+
const entries = await readdir(agentsDir, { withFileTypes: true }).catch(() => null);
|
|
14
|
+
if (entries === null)
|
|
15
|
+
return [];
|
|
16
|
+
const agents = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
19
|
+
continue;
|
|
20
|
+
const path = join(agentsDir, entry.name);
|
|
21
|
+
const content = await readFile(path, 'utf8').catch(() => null);
|
|
22
|
+
if (content === null)
|
|
23
|
+
continue;
|
|
24
|
+
const { data, body } = parseFrontmatter(content);
|
|
25
|
+
const def = {
|
|
26
|
+
name: asString(data.name) || basename(entry.name, '.md'),
|
|
27
|
+
description: asString(data.description),
|
|
28
|
+
systemPrompt: body.trim(),
|
|
29
|
+
path,
|
|
30
|
+
};
|
|
31
|
+
const tools = asStringArray(data.tools);
|
|
32
|
+
if (tools)
|
|
33
|
+
def.tools = tools;
|
|
34
|
+
const model = asString(data.model);
|
|
35
|
+
if (model)
|
|
36
|
+
def.model = model;
|
|
37
|
+
agents.push(def);
|
|
38
|
+
}
|
|
39
|
+
agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
+
return agents;
|
|
41
|
+
}
|
|
42
|
+
// The two `.claude` locations the harness scans, in increasing precedence: the global
|
|
43
|
+
// `~/.claude` first, then the project `<cwd>/.claude`. A caller merging by name should let the
|
|
44
|
+
// later (project) entry win.
|
|
45
|
+
export function claudeDirs(cwd) {
|
|
46
|
+
return [join(homedir(), '.claude'), join(cwd, '.claude')];
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=agents-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents-loader.js","sourceRoot":"","sources":["../src/agents-loader.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,8FAA8F;AAC9F,4FAA4F;AAC5F,4FAA4F;AAE5F,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAe7E,0FAA0F;AAC1F,2CAA2C;AAC3C,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAiB;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACpF,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAAE,SAAS;QAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,OAAO,KAAK,IAAI;YAAE,SAAS;QAC/B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,GAAG,GAAoB;YAC3B,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC;YACxD,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC;YACvC,YAAY,EAAE,IAAI,CAAC,IAAI,EAAE;YACzB,IAAI;SACL,CAAC;QACF,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,IAAI,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;QAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,sFAAsF;AACtF,+FAA+F;AAC/F,6BAA6B;AAC7B,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Tool } from 'ai';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
declare const bashInputSchema: z.ZodObject<{
|
|
5
|
+
command: z.ZodString;
|
|
6
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type BashInput = z.infer<typeof bashInputSchema>;
|
|
9
|
+
export type BashOutput = {
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
exitCode: number;
|
|
13
|
+
};
|
|
14
|
+
export type BashToolInit = {
|
|
15
|
+
cwd: string;
|
|
16
|
+
defaultTimeoutMs?: number;
|
|
17
|
+
exec?: typeof execa;
|
|
18
|
+
};
|
|
19
|
+
export declare function bashTool(init: BashToolInit): Tool<BashInput, BashOutput>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Shell tool, modeled on Claude Code's Bash. Runs a command via `bash -c` with its initial cwd
|
|
2
|
+
// set to the worktree. Unlike the FS tools it is intentionally NOT confined to the worktree:
|
|
3
|
+
// subagents need git, test and build commands, so `cd`, absolute paths and `git -C` are all
|
|
4
|
+
// legitimate. The trust boundary is "an agent is running on your repo" — same as Claude Code
|
|
5
|
+
// itself; sandboxing/containerization is out of scope. The worktree is the initial cwd as a
|
|
6
|
+
// convenience default, not a security boundary.
|
|
7
|
+
import { tool } from 'ai';
|
|
8
|
+
import { ExecaError, execa } from 'execa';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
const bashInputSchema = z.object({
|
|
11
|
+
command: z.string().min(1),
|
|
12
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
13
|
+
});
|
|
14
|
+
const DEFAULT_BASH_TIMEOUT_MS = 60_000;
|
|
15
|
+
// Hard ceiling on the effective timeout so a single tool call — via a huge per-call timeoutMs or
|
|
16
|
+
// an over-large configured default — can't pin the agent loop far longer than intended.
|
|
17
|
+
const MAX_BASH_TIMEOUT_MS = 600_000;
|
|
18
|
+
export function bashTool(init) {
|
|
19
|
+
const exec = init.exec ?? execa;
|
|
20
|
+
const defaultTimeout = init.defaultTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS;
|
|
21
|
+
return tool({
|
|
22
|
+
description: 'Run a shell command inside the current worktree. Returns stdout, stderr, and exit code. The command runs via `bash -c` with its initial cwd set to the worktree.',
|
|
23
|
+
inputSchema: bashInputSchema,
|
|
24
|
+
execute: async (input) => {
|
|
25
|
+
const timeout = Math.min(input.timeoutMs ?? defaultTimeout, MAX_BASH_TIMEOUT_MS);
|
|
26
|
+
try {
|
|
27
|
+
// Plain `-c`, not a login shell (`-lc`): a login shell sources /etc/profile and
|
|
28
|
+
// ~/.bash_profile, which in CI can `cd` away from `cwd` before the command runs. Also
|
|
29
|
+
// scrub BASH_ENV — even a non-login `bash -c` sources the file it points to at startup,
|
|
30
|
+
// which could `cd` away and defeat the cwd lock.
|
|
31
|
+
const r = await exec('bash', ['-c', input.command], {
|
|
32
|
+
cwd: init.cwd,
|
|
33
|
+
timeout,
|
|
34
|
+
env: { ...process.env, BASH_ENV: '' },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
stdout: typeof r.stdout === 'string' ? r.stdout : '',
|
|
38
|
+
stderr: typeof r.stderr === 'string' ? r.stderr : '',
|
|
39
|
+
exitCode: r.exitCode ?? 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof ExecaError) {
|
|
44
|
+
return {
|
|
45
|
+
stdout: typeof err.stdout === 'string' ? err.stdout : '',
|
|
46
|
+
stderr: typeof err.stderr === 'string' ? err.stderr : err.message,
|
|
47
|
+
exitCode: err.exitCode ?? 1,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Unknown failure (timeout, ENOENT on bash, etc.): surface as a non-zero exit with the
|
|
51
|
+
// error message in stderr so the LLM can read it and react.
|
|
52
|
+
return {
|
|
53
|
+
stdout: '',
|
|
54
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
55
|
+
exitCode: 1,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=bash-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bash-tool.js","sourceRoot":"","sources":["../src/bash-tool.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,6FAA6F;AAC7F,4FAA4F;AAC5F,6FAA6F;AAC7F,4FAA4F;AAC5F,gDAAgD;AAEhD,OAAO,EAAa,IAAI,EAAE,MAAM,IAAI,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAClD,CAAC,CAAC;AAkBH,MAAM,uBAAuB,GAAG,MAAM,CAAC;AACvC,iGAAiG;AACjG,wFAAwF;AACxF,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAEpC,MAAM,UAAU,QAAQ,CAAC,IAAkB;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC;IAChC,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,IAAI,uBAAuB,CAAC;IACxE,OAAO,IAAI,CAAC;QACV,WAAW,EACT,kKAAkK;QACpK,WAAW,EAAE,eAAe;QAC5B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAuB,EAAE;YAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,cAAc,EAAE,mBAAmB,CAAC,CAAC;YACjF,IAAI,CAAC;gBACH,gFAAgF;gBAChF,sFAAsF;gBACtF,wFAAwF;gBACxF,iDAAiD;gBACjD,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE;oBAClD,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,OAAO;oBACP,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE;iBACtC,CAAC,CAAC;gBACH,OAAO;oBACL,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;oBACpD,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;oBACpD,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC;iBAC1B,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;oBAC9B,OAAO;wBACL,MAAM,EAAE,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;wBACxD,MAAM,EAAE,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO;wBACjE,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,CAAC;qBAC5B,CAAC;gBACJ,CAAC;gBACD,uFAAuF;gBACvF,4DAA4D;gBAC5D,OAAO;oBACL,MAAM,EAAE,EAAE;oBACV,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;oBACxD,QAAQ,EAAE,CAAC;iBACZ,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ToolInit } from './fs-tools.ts';
|
|
4
|
+
declare const editSpecSchema: z.ZodObject<{
|
|
5
|
+
oldString: z.ZodString;
|
|
6
|
+
newString: z.ZodString;
|
|
7
|
+
replaceAll: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
declare const editFileInputSchema: z.ZodObject<{
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
oldString: z.ZodString;
|
|
12
|
+
newString: z.ZodString;
|
|
13
|
+
replaceAll: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
declare const multiEditInputSchema: z.ZodObject<{
|
|
16
|
+
path: z.ZodString;
|
|
17
|
+
edits: z.ZodArray<z.ZodObject<{
|
|
18
|
+
oldString: z.ZodString;
|
|
19
|
+
newString: z.ZodString;
|
|
20
|
+
replaceAll: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type EditSpec = z.infer<typeof editSpecSchema>;
|
|
24
|
+
export type EditFileInput = z.infer<typeof editFileInputSchema>;
|
|
25
|
+
export type EditFileOutput = {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
replacements: number;
|
|
28
|
+
};
|
|
29
|
+
export type MultiEditInput = z.infer<typeof multiEditInputSchema>;
|
|
30
|
+
export type MultiEditOutput = {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
replacements: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function editFileTool(init: ToolInit): Tool<EditFileInput, EditFileOutput>;
|
|
35
|
+
export declare function multiEditTool(init: ToolInit): Tool<MultiEditInput, MultiEditOutput>;
|
|
36
|
+
export declare function applyEdit(content: string, edit: EditSpec): {
|
|
37
|
+
next: string;
|
|
38
|
+
count: number;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// String-replace edit tools, modeled on Claude Code's Edit. `editFile` does one exact
|
|
2
|
+
// replacement; `multiEdit` applies a sequence atomically (all succeed and the file is written
|
|
3
|
+
// once, or nothing is written). Both reject an `oldString` that is absent or — unless
|
|
4
|
+
// `replaceAll` — ambiguous, so the model can't silently edit the wrong occurrence.
|
|
5
|
+
import { readFile as fsReadFile, writeFile as fsWriteFile } from 'node:fs/promises';
|
|
6
|
+
import { tool } from 'ai';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { resolveInside } from "./safe-path.js";
|
|
9
|
+
const editSpecSchema = z.object({
|
|
10
|
+
oldString: z.string().min(1),
|
|
11
|
+
newString: z.string(),
|
|
12
|
+
replaceAll: z.boolean().optional(),
|
|
13
|
+
});
|
|
14
|
+
const editFileInputSchema = z.object({ path: z.string().min(1) }).extend(editSpecSchema.shape);
|
|
15
|
+
const multiEditInputSchema = z.object({
|
|
16
|
+
path: z.string().min(1),
|
|
17
|
+
edits: z.array(editSpecSchema).min(1),
|
|
18
|
+
});
|
|
19
|
+
export function editFileTool(init) {
|
|
20
|
+
return tool({
|
|
21
|
+
description: 'Edit a file in the worktree by exact string replacement. `oldString` must occur exactly once unless `replaceAll` is true (then every occurrence is replaced). Errors if `oldString` is not found, or is ambiguous without `replaceAll`.',
|
|
22
|
+
inputSchema: editFileInputSchema,
|
|
23
|
+
execute: async (input) => {
|
|
24
|
+
const safe = await resolveInside(init.cwd, input.path);
|
|
25
|
+
const original = await fsReadFile(safe, 'utf8');
|
|
26
|
+
const { next, count } = applyEdit(original, input);
|
|
27
|
+
await fsWriteFile(safe, next, 'utf8');
|
|
28
|
+
return { ok: true, replacements: count };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function multiEditTool(init) {
|
|
33
|
+
return tool({
|
|
34
|
+
description: 'Apply a sequence of exact string replacements to a single file atomically. Edits apply in order to the in-memory contents; if any edit fails (string absent or ambiguous), nothing is written. Use for several related edits to the same file.',
|
|
35
|
+
inputSchema: multiEditInputSchema,
|
|
36
|
+
execute: async (input) => {
|
|
37
|
+
const safe = await resolveInside(init.cwd, input.path);
|
|
38
|
+
let content = await fsReadFile(safe, 'utf8');
|
|
39
|
+
let total = 0;
|
|
40
|
+
input.edits.forEach((edit, i) => {
|
|
41
|
+
try {
|
|
42
|
+
const { next, count } = applyEdit(content, edit);
|
|
43
|
+
content = next;
|
|
44
|
+
total += count;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw new Error(`edit ${i + 1}/${input.edits.length}: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
await fsWriteFile(safe, content, 'utf8');
|
|
51
|
+
return { ok: true, replacements: total };
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Pure string replacement with the uniqueness contract. Uses split/join so `newString` is
|
|
56
|
+
// inserted verbatim (no `$&`/`$1` substitution that String.prototype.replace would apply).
|
|
57
|
+
export function applyEdit(content, edit) {
|
|
58
|
+
// Guard the exported API: the tool schemas enforce min(1), but a direct caller could pass
|
|
59
|
+
// '' — which `split('')` would treat as a match at every character boundary.
|
|
60
|
+
if (edit.oldString.length === 0) {
|
|
61
|
+
throw new Error('oldString must be non-empty');
|
|
62
|
+
}
|
|
63
|
+
const occurrences = content.split(edit.oldString).length - 1;
|
|
64
|
+
if (occurrences === 0) {
|
|
65
|
+
throw new Error(`oldString not found: ${preview(edit.oldString)}`);
|
|
66
|
+
}
|
|
67
|
+
if (occurrences > 1 && !edit.replaceAll) {
|
|
68
|
+
throw new Error(`oldString is not unique (${occurrences} matches): ${preview(edit.oldString)} — add surrounding context or pass replaceAll`);
|
|
69
|
+
}
|
|
70
|
+
if (edit.replaceAll) {
|
|
71
|
+
return { next: content.split(edit.oldString).join(edit.newString), count: occurrences };
|
|
72
|
+
}
|
|
73
|
+
const at = content.indexOf(edit.oldString);
|
|
74
|
+
const next = content.slice(0, at) + edit.newString + content.slice(at + edit.oldString.length);
|
|
75
|
+
return { next, count: 1 };
|
|
76
|
+
}
|
|
77
|
+
function preview(s) {
|
|
78
|
+
const oneLine = s.replace(/\n/g, '\\n');
|
|
79
|
+
return oneLine.length > 60 ? `${oneLine.slice(0, 60)}…` : oneLine;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=edit-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-tools.js","sourceRoot":"","sources":["../src/edit-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The `<env>` system-context block injected into a subagent's system prompt so the model
|
|
2
|
+
// picks the right paths/commands. Portable: node:os + process only (Bun, Node >=20, Deno).
|
|
3
|
+
import { arch as osArch, release } from 'node:os';
|
|
4
|
+
function runtimeLabel() {
|
|
5
|
+
const v = process.versions;
|
|
6
|
+
if (v.bun)
|
|
7
|
+
return `bun ${v.bun}`;
|
|
8
|
+
if (v.deno)
|
|
9
|
+
return `deno ${v.deno}`;
|
|
10
|
+
return `node ${process.version}`;
|
|
11
|
+
}
|
|
12
|
+
export function envBlock(info) {
|
|
13
|
+
const lines = [
|
|
14
|
+
'<env>',
|
|
15
|
+
`Working directory: ${info.cwd}`,
|
|
16
|
+
...(info.isGitRepo === undefined
|
|
17
|
+
? []
|
|
18
|
+
: [`Is directory a git repo: ${info.isGitRepo ? 'Yes' : 'No'}`]),
|
|
19
|
+
`Platform: ${process.platform}`,
|
|
20
|
+
`OS version: ${release()}`,
|
|
21
|
+
`Arch: ${osArch()}`,
|
|
22
|
+
`Shell: ${process.env.SHELL ?? ''}`,
|
|
23
|
+
`Runtime: ${runtimeLabel()}`,
|
|
24
|
+
`Today's date: ${info.date ?? new Date().toISOString().slice(0, 10)}`,
|
|
25
|
+
'</env>',
|
|
26
|
+
];
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=env-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-block.js","sourceRoot":"","sources":["../src/env-block.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,2FAA2F;AAE3F,OAAO,EAAE,IAAI,IAAI,MAAM,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AASlD,SAAS,YAAY;IACnB,MAAM,CAAC,GAAG,OAAO,CAAC,QAA8C,CAAC;IACjE,IAAI,CAAC,CAAC,GAAG;QAAE,OAAO,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC;IACjC,IAAI,CAAC,CAAC,IAAI;QAAE,OAAO,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,OAAO,QAAQ,OAAO,CAAC,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAa;IACpC,MAAM,KAAK,GAAG;QACZ,OAAO;QACP,sBAAsB,IAAI,CAAC,GAAG,EAAE;QAChC,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS;YAC9B,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,4BAA4B,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAClE,aAAa,OAAO,CAAC,QAAQ,EAAE;QAC/B,eAAe,OAAO,EAAE,EAAE;QAC1B,SAAS,MAAM,EAAE,EAAE;QACnB,UAAU,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,EAAE;QACnC,YAAY,YAAY,EAAE,EAAE;QAC5B,iBAAiB,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE;QACrE,QAAQ;KACT,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type FrontmatterValue = string | string[];
|
|
2
|
+
export type Frontmatter = {
|
|
3
|
+
data: Record<string, FrontmatterValue>;
|
|
4
|
+
body: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function parseFrontmatter(content: string): Frontmatter;
|
|
7
|
+
export declare function asString(value: FrontmatterValue | undefined): string;
|
|
8
|
+
export declare function asStringArray(value: FrontmatterValue | undefined): string[] | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Minimal YAML-frontmatter parser for `.claude/` extension files (SKILL.md, agents/*.md).
|
|
2
|
+
// Handles only what those files use — scalar strings, flow arrays (`[a, b]`), and block
|
|
3
|
+
// sequences (`- a` lines) — so the lib stays dependency-free rather than pulling a full YAML
|
|
4
|
+
// engine. Anything outside that shape is ignored, not errored, so a malformed field never
|
|
5
|
+
// blocks discovery of the rest.
|
|
6
|
+
const FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n([\s\S]*))?$/;
|
|
7
|
+
export function parseFrontmatter(content) {
|
|
8
|
+
const match = FRONTMATTER.exec(content);
|
|
9
|
+
if (!match)
|
|
10
|
+
return { data: {}, body: content };
|
|
11
|
+
return { data: parseBlock(match[1] ?? ''), body: match[2] ?? '' };
|
|
12
|
+
}
|
|
13
|
+
function parseBlock(raw) {
|
|
14
|
+
const data = {};
|
|
15
|
+
const lines = raw.split('\n');
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
const line = lines[i] ?? '';
|
|
18
|
+
if (line.trim() === '' || line.trimStart().startsWith('#'))
|
|
19
|
+
continue;
|
|
20
|
+
const m = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(line);
|
|
21
|
+
if (!m)
|
|
22
|
+
continue;
|
|
23
|
+
const key = m[1] ?? '';
|
|
24
|
+
const rest = (m[2] ?? '').trim();
|
|
25
|
+
if (rest === '') {
|
|
26
|
+
// A bare `key:` may head a block sequence on the following `- item` lines.
|
|
27
|
+
const items = [];
|
|
28
|
+
while (i + 1 < lines.length && /^[ \t]*-[ \t]+/.test(lines[i + 1] ?? '')) {
|
|
29
|
+
i += 1;
|
|
30
|
+
items.push(stripQuotes((lines[i] ?? '').replace(/^[ \t]*-[ \t]+/, '').trim()));
|
|
31
|
+
}
|
|
32
|
+
data[key] = items;
|
|
33
|
+
}
|
|
34
|
+
else if (rest.startsWith('[') && rest.endsWith(']')) {
|
|
35
|
+
const inner = rest.slice(1, -1).trim();
|
|
36
|
+
data[key] = inner === '' ? [] : inner.split(',').map((s) => stripQuotes(s.trim()));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
data[key] = stripQuotes(rest);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
function stripQuotes(s) {
|
|
45
|
+
if (s.length >= 2) {
|
|
46
|
+
const first = s[0];
|
|
47
|
+
if ((first === '"' || first === "'") && s[s.length - 1] === first) {
|
|
48
|
+
return s.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
// Coerce a parsed value to a single string (scalars stay; arrays join). Empty when absent.
|
|
54
|
+
export function asString(value) {
|
|
55
|
+
if (typeof value === 'string')
|
|
56
|
+
return value;
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.join(', ');
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
// Coerce a parsed value to a string list. A scalar splits on commas; absent → undefined.
|
|
62
|
+
export function asStringArray(value) {
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return value;
|
|
65
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
66
|
+
return value.split(',').map((s) => s.trim());
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export type ToolInit = {
|
|
4
|
+
cwd: string;
|
|
5
|
+
};
|
|
6
|
+
declare const readFileInputSchema: z.ZodObject<{
|
|
7
|
+
path: z.ZodString;
|
|
8
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
declare const writeFileInputSchema: z.ZodObject<{
|
|
12
|
+
path: z.ZodString;
|
|
13
|
+
content: z.ZodString;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export type ReadFileInput = z.infer<typeof readFileInputSchema>;
|
|
16
|
+
export type ReadFileOutput = {
|
|
17
|
+
content: string;
|
|
18
|
+
};
|
|
19
|
+
export type WriteFileInput = z.infer<typeof writeFileInputSchema>;
|
|
20
|
+
export type WriteFileOutput = {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare function readFileTool(init: ToolInit): Tool<ReadFileInput, ReadFileOutput>;
|
|
24
|
+
export declare function writeFileTool(init: ToolInit): Tool<WriteFileInput, WriteFileOutput>;
|
|
25
|
+
export {};
|
package/dist/fs-tools.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Read/write filesystem tools, modeled on Claude Code's Read/Write. Every tool is scoped to a
|
|
2
|
+
// `cwd` (the active worktree) and rejects paths that escape it via the shared `resolveInside`
|
|
3
|
+
// guard. AI-SDK `Tool`-shaped so they drop straight into a subagent's tool set.
|
|
4
|
+
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { readFile as fsReadFile, writeFile as fsWriteFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { tool } from 'ai';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { resolveInside } from "./safe-path.js";
|
|
11
|
+
// offset is a 1-based starting line; limit is the max number of lines to return. Both optional
|
|
12
|
+
// — omitting them reads the whole file. Mirrors Claude Code's Read line window.
|
|
13
|
+
const readFileInputSchema = z.object({
|
|
14
|
+
path: z.string().min(1),
|
|
15
|
+
offset: z.number().int().positive().optional(),
|
|
16
|
+
limit: z.number().int().positive().optional(),
|
|
17
|
+
});
|
|
18
|
+
const writeFileInputSchema = z.object({
|
|
19
|
+
path: z.string().min(1),
|
|
20
|
+
content: z.string(),
|
|
21
|
+
});
|
|
22
|
+
export function readFileTool(init) {
|
|
23
|
+
return tool({
|
|
24
|
+
description: 'Read a UTF-8 text file from the current worktree. Path may be relative (resolved against the worktree root) or absolute (must still be inside the worktree). Optionally pass `offset` (1-based start line) and `limit` (line count) to read a window of a large file.',
|
|
25
|
+
inputSchema: readFileInputSchema,
|
|
26
|
+
execute: async (input) => {
|
|
27
|
+
const safe = await resolveInside(init.cwd, input.path);
|
|
28
|
+
// Whole-file read returns the bytes verbatim; a windowed read streams line-by-line so a
|
|
29
|
+
// small window of a huge file doesn't pull the whole thing into memory.
|
|
30
|
+
if (input.offset === undefined && input.limit === undefined) {
|
|
31
|
+
return { content: await fsReadFile(safe, 'utf8') };
|
|
32
|
+
}
|
|
33
|
+
return { content: await readWindow(safe, input.offset, input.limit) };
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function writeFileTool(init) {
|
|
38
|
+
return tool({
|
|
39
|
+
description: 'Write a UTF-8 text file inside the current worktree. Creates parent directories. Overwrites any existing file. Path must stay inside the worktree.',
|
|
40
|
+
inputSchema: writeFileInputSchema,
|
|
41
|
+
execute: async (input) => {
|
|
42
|
+
const safe = await resolveInside(init.cwd, input.path);
|
|
43
|
+
await mkdir(dirname(safe), { recursive: true });
|
|
44
|
+
await fsWriteFile(safe, input.content, 'utf8');
|
|
45
|
+
return { ok: true };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Stream the [offset, offset+limit) line window (1-based offset) without reading the whole file.
|
|
50
|
+
// readline strips line terminators, so when the window runs to the end of the file we re-attach a
|
|
51
|
+
// trailing newline — matching the whole-file read for to-EOF windows. A window capped early by
|
|
52
|
+
// `limit` returns just the joined lines (no trailing terminator).
|
|
53
|
+
async function readWindow(path, offset, limit) {
|
|
54
|
+
const start = offset ?? 1;
|
|
55
|
+
const stream = createReadStream(path, { encoding: 'utf8' });
|
|
56
|
+
try {
|
|
57
|
+
const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
58
|
+
const lines = [];
|
|
59
|
+
let lineNo = 0;
|
|
60
|
+
let cappedEarly = false;
|
|
61
|
+
for await (const line of rl) {
|
|
62
|
+
lineNo += 1;
|
|
63
|
+
if (lineNo < start)
|
|
64
|
+
continue;
|
|
65
|
+
lines.push(line);
|
|
66
|
+
if (limit !== undefined && lines.length >= limit) {
|
|
67
|
+
cappedEarly = true;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
rl.close();
|
|
72
|
+
const text = lines.join('\n');
|
|
73
|
+
return !cappedEarly && lines.length > 0 ? `${text}\n` : text;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
stream.destroy();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=fs-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-tools.js","sourceRoot":"","sources":["../src/fs-tools.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { type AgentDefinition, claudeDirs, loadAgents } from './agents-loader.ts';
|
|
2
|
+
export { type BashInput, type BashOutput, type BashToolInit, bashTool } from './bash-tool.ts';
|
|
3
|
+
export { applyEdit, type EditFileInput, type EditFileOutput, type EditSpec, editFileTool, type MultiEditInput, type MultiEditOutput, multiEditTool, } from './edit-tools.ts';
|
|
4
|
+
export { type EnvInfo, envBlock } from './env-block.ts';
|
|
5
|
+
export { asString, asStringArray, type Frontmatter, type FrontmatterValue, parseFrontmatter, } from './frontmatter.ts';
|
|
6
|
+
export { type ReadFileInput, type ReadFileOutput, readFileTool, type ToolInit, type WriteFileInput, type WriteFileOutput, writeFileTool, } from './fs-tools.ts';
|
|
7
|
+
export { resolveInside } from './safe-path.ts';
|
|
8
|
+
export { type GlobInput, type GlobOutput, type GrepInput, type GrepOutput, globTool, globToRegExp, grepTool, } from './search-tools.ts';
|
|
9
|
+
export { loadSkills, type SkillDefinition } from './skills-loader.ts';
|
|
10
|
+
export { composeSystemPrompt, createSubagent, type SubagentConfig } from './subagent.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Public API for @developerz.ai/ai-claude-compat. Claude-Code-style agent primitives for the
|
|
2
|
+
// Vercel AI SDK: cwd-scoped FS/edit/search/shell tools, an <env> system-context block, and
|
|
3
|
+
// .claude/ skills + agents loading.
|
|
4
|
+
export { claudeDirs, loadAgents } from "./agents-loader.js";
|
|
5
|
+
export { bashTool } from "./bash-tool.js";
|
|
6
|
+
export { applyEdit, editFileTool, multiEditTool, } from "./edit-tools.js";
|
|
7
|
+
export { envBlock } from "./env-block.js";
|
|
8
|
+
export { asString, asStringArray, parseFrontmatter, } from "./frontmatter.js";
|
|
9
|
+
export { readFileTool, writeFileTool, } from "./fs-tools.js";
|
|
10
|
+
export { resolveInside } from "./safe-path.js";
|
|
11
|
+
export { globTool, globToRegExp, grepTool, } from "./search-tools.js";
|
|
12
|
+
export { loadSkills } from "./skills-loader.js";
|
|
13
|
+
export { composeSystemPrompt, createSubagent } from "./subagent.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,2FAA2F;AAC3F,oCAAoC;AAEpC,OAAO,EAAwB,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAAsD,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC9F,OAAO,EACL,SAAS,EAIT,YAAY,EAGZ,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAgB,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,QAAQ,EACR,aAAa,EAGb,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGL,YAAY,EAIZ,aAAa,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAKL,QAAQ,EACR,YAAY,EACZ,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAwB,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAuB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveInside(root: string, requested: string): Promise<string>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Confine a requested path to a root directory. Every FS tool in this lib is exposed to a
|
|
2
|
+
// fallible LLM and may run against the user's real repo (not a sandbox), so a missing path
|
|
3
|
+
// check is an arbitrary read/write primitive — same severity as a path-traversal CVE in a web
|
|
4
|
+
// app. The guard uses two layers: (1) a cheap pre-resolve string check via `path.relative`
|
|
5
|
+
// that rejects `../`-style escapes, and (2) a realpath of the closest existing ancestor that
|
|
6
|
+
// defeats symlink trickery. We can't realpath the target itself (it may not exist yet for a
|
|
7
|
+
// write), so we realpath the parent and re-check the relative path from there.
|
|
8
|
+
import { realpath } from 'node:fs/promises';
|
|
9
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
10
|
+
// Resolve `requested` against `root` and assert the result stays inside `root`. Accepts a
|
|
11
|
+
// relative path (resolved against root) or an absolute path (must still be inside root).
|
|
12
|
+
// Throws if the path escapes, by string or via symlink.
|
|
13
|
+
export async function resolveInside(root, requested) {
|
|
14
|
+
const absRoot = resolve(root);
|
|
15
|
+
const target = isAbsolute(requested) ? resolve(requested) : resolve(absRoot, requested);
|
|
16
|
+
if (escapesRoot(absRoot, target)) {
|
|
17
|
+
throw new Error(`path escapes worktree: ${requested}`);
|
|
18
|
+
}
|
|
19
|
+
const realRoot = await safeRealpath(absRoot);
|
|
20
|
+
const realTarget = await realpathOfExisting(target);
|
|
21
|
+
if (escapesRoot(realRoot, realTarget)) {
|
|
22
|
+
throw new Error(`path escapes worktree via symlink: ${requested}`);
|
|
23
|
+
}
|
|
24
|
+
return target;
|
|
25
|
+
}
|
|
26
|
+
function escapesRoot(root, target) {
|
|
27
|
+
const rel = relative(root, target);
|
|
28
|
+
if (rel === '')
|
|
29
|
+
return false;
|
|
30
|
+
// Only a real parent-traversal token escapes — not an in-root name that merely starts with
|
|
31
|
+
// ".." (e.g. "..cache/file").
|
|
32
|
+
if (rel === '..' || rel.startsWith(`..${sep}`))
|
|
33
|
+
return true;
|
|
34
|
+
if (isAbsolute(rel))
|
|
35
|
+
return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// Only a genuinely-absent path (ENOENT) may fall back to the string form. EACCES/ELOOP/EIO and
|
|
39
|
+
// the like must propagate — swallowing them would silently downgrade the symlink guard to the
|
|
40
|
+
// weaker string-only check.
|
|
41
|
+
function isEnoent(err) {
|
|
42
|
+
return typeof err === 'object' && err !== null && err.code === 'ENOENT';
|
|
43
|
+
}
|
|
44
|
+
async function safeRealpath(p) {
|
|
45
|
+
try {
|
|
46
|
+
return await realpath(p);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (isEnoent(err))
|
|
50
|
+
return resolve(p);
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Walk up `target`'s ancestors until one exists, realpath it, then re-attach the
|
|
55
|
+
// non-existing suffix. Lets us safely check would-be-created paths.
|
|
56
|
+
async function realpathOfExisting(target) {
|
|
57
|
+
try {
|
|
58
|
+
return await realpath(target);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (!isEnoent(err))
|
|
62
|
+
throw err;
|
|
63
|
+
const parent = dirname(target);
|
|
64
|
+
if (parent === target)
|
|
65
|
+
return target;
|
|
66
|
+
const realParent = await realpathOfExisting(parent);
|
|
67
|
+
const suffix = relative(parent, target);
|
|
68
|
+
return resolve(realParent, suffix);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=safe-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-path.js","sourceRoot":"","sources":["../src/safe-path.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,2FAA2F;AAC3F,8FAA8F;AAC9F,2FAA2F;AAC3F,6FAA6F;AAC7F,4FAA4F;AAC5F,+EAA+E;AAE/E,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAExE,0FAA0F;AAC1F,yFAAyF;AACzF,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,SAAiB;IACjE,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACxF,IAAI,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,0BAA0B,SAAS,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACpD,IAAI,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,sCAAsC,SAAS,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,MAAc;IAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC7B,2FAA2F;IAC3F,8BAA8B;IAC9B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,IAAI,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+FAA+F;AAC/F,8FAA8F;AAC9F,4BAA4B;AAC5B,SAAS,QAAQ,CAAC,GAAY;IAC5B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAK,GAA0B,CAAC,IAAI,KAAK,QAAQ,CAAC;AAClG,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,CAAS;IACnC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,oEAAoE;AACpE,KAAK,UAAU,kBAAkB,CAAC,MAAc;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,GAAG,CAAC;QAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,MAAM,CAAC;QACrC,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACxC,OAAO,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ToolInit } from './fs-tools.ts';
|
|
4
|
+
declare const grepInputSchema: z.ZodObject<{
|
|
5
|
+
pattern: z.ZodString;
|
|
6
|
+
path: z.ZodOptional<z.ZodString>;
|
|
7
|
+
glob: z.ZodOptional<z.ZodString>;
|
|
8
|
+
ignoreCase: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
filesWithMatches: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
maxResults: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
declare const globInputSchema: z.ZodObject<{
|
|
13
|
+
pattern: z.ZodString;
|
|
14
|
+
path: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type GrepInput = z.infer<typeof grepInputSchema>;
|
|
17
|
+
export type GrepOutput = {
|
|
18
|
+
matches: string[];
|
|
19
|
+
truncated: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type GlobInput = z.infer<typeof globInputSchema>;
|
|
22
|
+
export type GlobOutput = {
|
|
23
|
+
files: string[];
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function grepTool(init: ToolInit): Tool<GrepInput, GrepOutput>;
|
|
27
|
+
export declare function globTool(init: ToolInit): Tool<GlobInput, GlobOutput>;
|
|
28
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Portable search tools, modeled on how Claude Code uses ripgrep + glob — but implemented in
|
|
2
|
+
// pure node so there's no `rg` dependency (it may be absent) and no reliance on `fs.glob`
|
|
3
|
+
// (not on Node 20). Both walk the tree under a cwd-confined root, skipping `.git` and
|
|
4
|
+
// `node_modules`, following no symlinks (which keeps the walk inside the worktree and loop-free).
|
|
5
|
+
import { readFile as fsReadFile, readdir, stat } from 'node:fs/promises';
|
|
6
|
+
import { join, relative, sep } from 'node:path';
|
|
7
|
+
import { tool } from 'ai';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { resolveInside } from "./safe-path.js";
|
|
10
|
+
const DEFAULT_MAX_RESULTS = 200;
|
|
11
|
+
const GLOB_MAX_FILES = 2000;
|
|
12
|
+
const MAX_FILE_BYTES = 5_000_000;
|
|
13
|
+
// Regex metacharacters escaped to literals when translating a glob (a Set, not a string, so the
|
|
14
|
+
// `${` pair doesn't read as a template placeholder).
|
|
15
|
+
const REGEX_META = new Set(['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']);
|
|
16
|
+
const grepInputSchema = z.object({
|
|
17
|
+
pattern: z.string().min(1),
|
|
18
|
+
path: z.string().optional(),
|
|
19
|
+
glob: z.string().optional(),
|
|
20
|
+
ignoreCase: z.boolean().optional(),
|
|
21
|
+
filesWithMatches: z.boolean().optional(),
|
|
22
|
+
maxResults: z.number().int().positive().optional(),
|
|
23
|
+
});
|
|
24
|
+
const globInputSchema = z.object({
|
|
25
|
+
pattern: z.string().min(1),
|
|
26
|
+
path: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
export function grepTool(init) {
|
|
29
|
+
return tool({
|
|
30
|
+
description: 'Search file contents under the worktree for a JavaScript regular expression. Returns `path:line:text` lines (or just file paths with filesWithMatches). Optionally restrict to files matching a glob, ignore case, and cap results.',
|
|
31
|
+
inputSchema: grepInputSchema,
|
|
32
|
+
execute: async (input) => {
|
|
33
|
+
const root = await resolveInside(init.cwd, input.path ?? '.');
|
|
34
|
+
const re = compileRegExp(input.pattern, input.ignoreCase ? 'i' : '');
|
|
35
|
+
const fileFilter = input.glob ? globToRegExp(input.glob) : null;
|
|
36
|
+
const cap = input.maxResults ?? DEFAULT_MAX_RESULTS;
|
|
37
|
+
const matches = [];
|
|
38
|
+
let truncated = false;
|
|
39
|
+
outer: for await (const file of walk(root, init.cwd)) {
|
|
40
|
+
if (fileFilter && !fileFilter.test(file.rel))
|
|
41
|
+
continue;
|
|
42
|
+
const text = await readTextOrSkip(file.abs);
|
|
43
|
+
if (text === null)
|
|
44
|
+
continue;
|
|
45
|
+
const lines = text.split('\n');
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i] ?? '';
|
|
48
|
+
if (!re.test(line))
|
|
49
|
+
continue;
|
|
50
|
+
matches.push(input.filesWithMatches ? file.rel : `${file.rel}:${i + 1}:${line}`);
|
|
51
|
+
if (matches.length >= cap) {
|
|
52
|
+
truncated = true;
|
|
53
|
+
break outer;
|
|
54
|
+
}
|
|
55
|
+
if (input.filesWithMatches)
|
|
56
|
+
continue outer; // one entry per file
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { matches, truncated };
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function globTool(init) {
|
|
64
|
+
return tool({
|
|
65
|
+
description: 'List files under the worktree whose path matches a glob (supports `*`, `**`, `?`). Paths are returned relative to the worktree, sorted. Skips `.git` and `node_modules`.',
|
|
66
|
+
inputSchema: globInputSchema,
|
|
67
|
+
execute: async (input) => {
|
|
68
|
+
const root = await resolveInside(init.cwd, input.path ?? '.');
|
|
69
|
+
const re = globToRegExp(input.pattern);
|
|
70
|
+
const files = [];
|
|
71
|
+
let truncated = false;
|
|
72
|
+
for await (const file of walk(root, init.cwd)) {
|
|
73
|
+
if (!re.test(file.rel))
|
|
74
|
+
continue;
|
|
75
|
+
files.push(file.rel);
|
|
76
|
+
if (files.length >= GLOB_MAX_FILES) {
|
|
77
|
+
truncated = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
files.sort();
|
|
82
|
+
return { files, truncated };
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Depth-first walk yielding regular files only. `rel` is POSIX-style and relative to `cwd`
|
|
87
|
+
// (so a caller's glob/regex sees forward slashes regardless of platform). Symlinks are not
|
|
88
|
+
// followed — that confines the walk to the worktree and avoids cycles.
|
|
89
|
+
async function* walk(dir, cwd) {
|
|
90
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
91
|
+
if (entries === null)
|
|
92
|
+
return;
|
|
93
|
+
// readdir order is filesystem-dependent; sort so capped grep/glob results (and which entries
|
|
94
|
+
// survive maxResults) stay deterministic across environments.
|
|
95
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (entry.isSymbolicLink())
|
|
98
|
+
continue;
|
|
99
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
100
|
+
continue;
|
|
101
|
+
const abs = join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
yield* walk(abs, cwd);
|
|
104
|
+
}
|
|
105
|
+
else if (entry.isFile()) {
|
|
106
|
+
yield { abs, rel: relative(cwd, abs).split(sep).join('/') };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function readTextOrSkip(abs) {
|
|
111
|
+
try {
|
|
112
|
+
const info = await stat(abs);
|
|
113
|
+
if (info.size > MAX_FILE_BYTES)
|
|
114
|
+
return null;
|
|
115
|
+
const text = await fsReadFile(abs, 'utf8');
|
|
116
|
+
if (text.includes(String.fromCharCode(0)))
|
|
117
|
+
return null; // crude binary guard: NUL byte
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function compileRegExp(pattern, flags) {
|
|
125
|
+
try {
|
|
126
|
+
return new RegExp(pattern, flags);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
throw new Error(`invalid regular expression: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Convert a glob to an anchored RegExp. Supports `**` (any run of path segments, incl. zero),
|
|
133
|
+
// `*` (any run within one segment), `?` (one non-slash char). Other regex metacharacters are
|
|
134
|
+
// escaped so they match literally.
|
|
135
|
+
export function globToRegExp(glob) {
|
|
136
|
+
let re = '';
|
|
137
|
+
for (let i = 0; i < glob.length; i++) {
|
|
138
|
+
const c = glob[i];
|
|
139
|
+
if (c === '*') {
|
|
140
|
+
if (glob[i + 1] === '*') {
|
|
141
|
+
i++;
|
|
142
|
+
if (glob[i + 1] === '/') {
|
|
143
|
+
i++;
|
|
144
|
+
re += '(?:[^/]+/)*'; // `**/` — zero or more directories
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
re += '.*'; // `**` — anything, including slashes
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
re += '[^/]*'; // `*` — within a segment
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (c === '?') {
|
|
155
|
+
re += '[^/]';
|
|
156
|
+
}
|
|
157
|
+
else if (c !== undefined && REGEX_META.has(c)) {
|
|
158
|
+
re += `\\${c}`;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
re += c;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return new RegExp(`^${re}$`);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=search-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-tools.js","sourceRoot":"","sources":["../src/search-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Discover Claude-Code-style skills: `<claudeDir>/skills/<name>/SKILL.md`, each with YAML
|
|
2
|
+
// frontmatter (name, description) + a markdown body of instructions. Mirrors how the harness
|
|
3
|
+
// scans `.claude/skills/` (see claude-code-knowledge skills-and-subagents.md).
|
|
4
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { asString, parseFrontmatter } from "./frontmatter.js";
|
|
7
|
+
// Load every skill under `<claudeDir>/skills/*/SKILL.md`. `claudeDir` is a `.claude` directory.
|
|
8
|
+
// A missing dir yields []; a skill folder without a SKILL.md is skipped. Sorted by name.
|
|
9
|
+
export async function loadSkills(claudeDir) {
|
|
10
|
+
const skillsDir = join(claudeDir, 'skills');
|
|
11
|
+
const entries = await readdir(skillsDir, { withFileTypes: true }).catch(() => null);
|
|
12
|
+
if (entries === null)
|
|
13
|
+
return [];
|
|
14
|
+
const skills = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (!entry.isDirectory())
|
|
17
|
+
continue;
|
|
18
|
+
const path = join(skillsDir, entry.name, 'SKILL.md');
|
|
19
|
+
const content = await readFile(path, 'utf8').catch(() => null);
|
|
20
|
+
if (content === null)
|
|
21
|
+
continue;
|
|
22
|
+
const { data, body } = parseFrontmatter(content);
|
|
23
|
+
skills.push({
|
|
24
|
+
name: asString(data.name) || entry.name,
|
|
25
|
+
description: asString(data.description),
|
|
26
|
+
body: body.trim(),
|
|
27
|
+
path,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
return skills;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=skills-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-loader.js","sourceRoot":"","sources":["../src/skills-loader.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,6FAA6F;AAC7F,+EAA+E;AAE/E,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAW9D,gGAAgG;AAChG,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAiB;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACpF,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,OAAO,KAAK,IAAI;YAAE,SAAS;QAC/B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI;YACvC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC;YACvC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YACjB,IAAI;SACL,CAAC,CAAC;IACL,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type LanguageModel, type Output, ToolLoopAgent, type ToolSet } from 'ai';
|
|
2
|
+
import { type EnvInfo } from './env-block.ts';
|
|
3
|
+
export declare function composeSystemPrompt(style: string, rolePrefix: string, env: string | EnvInfo): string;
|
|
4
|
+
export type SubagentConfig<TOOLS extends ToolSet, OUTPUT extends Output.Output> = {
|
|
5
|
+
model: LanguageModel;
|
|
6
|
+
tools: TOOLS;
|
|
7
|
+
systemPrompt: string;
|
|
8
|
+
output: OUTPUT;
|
|
9
|
+
maxSteps?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function createSubagent<TOOLS extends ToolSet, OUTPUT extends Output.Output>(config: SubagentConfig<TOOLS, OUTPUT>, defaultMaxSteps: number): ToolLoopAgent<never, TOOLS, OUTPUT>;
|
package/dist/subagent.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Provider-agnostic subagent scaffolding for the Vercel AI SDK: compose a system prompt with an
|
|
2
|
+
// <env> block, and wrap the `ToolLoopAgent` construction (the subagents-as-tools pattern) behind
|
|
3
|
+
// a single factory so callers don't repeat the model/tools/instructions/stopWhen boilerplate.
|
|
4
|
+
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
5
|
+
import { envBlock } from "./env-block.js";
|
|
6
|
+
// Build a subagent system prompt: caller style/context + role prefix + an <env> system-context
|
|
7
|
+
// block (so the model knows the worktree cwd, platform, runtime, date). The cwd is per-worktree,
|
|
8
|
+
// so this is composed at the wiring site rather than baked into a static role prefix. Pass a
|
|
9
|
+
// string cwd for the common case, or a full EnvInfo to control the git/date fields.
|
|
10
|
+
export function composeSystemPrompt(style, rolePrefix, env) {
|
|
11
|
+
const info = typeof env === 'string' ? { cwd: env, isGitRepo: true } : env;
|
|
12
|
+
return `${style}${rolePrefix}\n${envBlock(info)}`;
|
|
13
|
+
}
|
|
14
|
+
// Wrap a ToolLoopAgent. Maps systemPrompt → instructions and maxSteps → a stepCountIs stop
|
|
15
|
+
// condition, so each concrete subagent factory is a one-liner over its own tools + output type.
|
|
16
|
+
export function createSubagent(config, defaultMaxSteps) {
|
|
17
|
+
return new ToolLoopAgent({
|
|
18
|
+
model: config.model,
|
|
19
|
+
tools: config.tools,
|
|
20
|
+
instructions: config.systemPrompt,
|
|
21
|
+
output: config.output,
|
|
22
|
+
stopWhen: stepCountIs(config.maxSteps ?? defaultMaxSteps),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=subagent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subagent.js","sourceRoot":"","sources":["../src/subagent.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,iGAAiG;AACjG,8FAA8F;AAE9F,OAAO,EAAmC,WAAW,EAAE,aAAa,EAAgB,MAAM,IAAI,CAAC;AAC/F,OAAO,EAAgB,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAExD,+FAA+F;AAC/F,iGAAiG;AACjG,6FAA6F;AAC7F,oFAAoF;AACpF,MAAM,UAAU,mBAAmB,CACjC,KAAa,EACb,UAAkB,EAClB,GAAqB;IAErB,MAAM,IAAI,GAAY,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IACpF,OAAO,GAAG,KAAK,GAAG,UAAU,KAAK,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;AACpD,CAAC;AAaD,2FAA2F;AAC3F,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAC5B,MAAqC,EACrC,eAAuB;IAEvB,OAAO,IAAI,aAAa,CAAuB;QAC7C,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,YAAY,EAAE,MAAM,CAAC,YAAY;QACjC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,QAAQ,IAAI,eAAe,CAAC;KAC1D,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@developerz.ai/ai-claude-compat",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Claude-Code-style agent primitives for the Vercel AI SDK: FS/bash tools, an <env> system-context block, a subagent-as-tool factory, and .claude/ skills/agents loading.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"provenance": true
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
25
|
+
"test:node": "node --test --import tsx \"src/**/*.test.ts\""
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ai": "^6.0.182",
|
|
29
|
+
"execa": "^9.6.1",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
}
|
|
32
|
+
}
|