@developerz.ai/ai-claude-compat 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,24 +29,56 @@ can't read or write outside the root you give it.
29
29
  import {
30
30
  readFileTool, writeFileTool, // Read (offset/limit window) + Write
31
31
  editFileTool, multiEditTool, // exact-string Edit + batched MultiEdit
32
- bashTool, // streaming shell, scoped to cwd
32
+ bashTool, multiBashTool, // one shell command / an ordered sequence, scoped to cwd
33
33
  globTool, grepTool, // file glob + content search
34
34
  } from '@developerz.ai/ai-claude-compat';
35
35
 
36
36
  const cwd = process.cwd();
37
37
  const tools = {
38
- read: readFileTool({ cwd }),
39
- write: writeFileTool({ cwd }),
40
- edit: editFileTool({ cwd }),
41
- bash: bashTool({ cwd }),
42
- grep: grepTool({ cwd }),
43
- glob: globTool({ cwd }),
38
+ read: readFileTool({ cwd }),
39
+ write: writeFileTool({ cwd }),
40
+ edit: editFileTool({ cwd }),
41
+ bash: bashTool({ cwd }),
42
+ multiBash: multiBashTool({ cwd }),
43
+ grep: grepTool({ cwd }),
44
+ glob: globTool({ cwd }),
44
45
  };
45
46
  ```
46
47
 
47
48
  Pass `tools` straight into a `generateText` / `streamText` call or an
48
49
  `Agent`/`ToolLoopAgent`.
49
50
 
51
+ ## Background processes
52
+
53
+ `bashTool`/`multiBashTool` block until the command exits, so they can't hold a
54
+ dev server open. `backgroundProcessTools` does: it spawns `bash -c …` without
55
+ awaiting, returns a process id immediately, and lets the agent tail output, kill
56
+ one process, or kill them all on teardown. **The caller owns lifecycle** — keep
57
+ the returned `manager` and call `killAll()` when the run ends, or processes leak.
58
+
59
+ ```ts
60
+ import { backgroundProcessTools } from '@developerz.ai/ai-claude-compat';
61
+
62
+ const { manager, backgroundBash, bashOutput, killBash, listBackground } =
63
+ backgroundProcessTools({ cwd });
64
+
65
+ const agentTools = { ...tools, backgroundBash, bashOutput, killBash, listBackground };
66
+ // agent: backgroundBash("npm run dev") -> { id }
67
+ // bashOutput(id) -> new stdout/stderr since last poll, running, exitCode
68
+ // killBash(id)
69
+ try {
70
+ await agent.generate({ prompt: 'start the dev server and check it serves /health' });
71
+ } finally {
72
+ manager.killAll(); // teardown — nothing else guarantees the server is stopped
73
+ }
74
+ ```
75
+
76
+ `bashOutput` is **incremental** (like Claude Code's BashOutput): each call returns
77
+ only the bytes produced since the last call. Buffers are capped (256 KiB/stream by
78
+ default); past the cap the oldest bytes are dropped and `truncated` is set.
79
+ Browser/CDP automation is intentionally **out of scope** — drive a browser with a
80
+ dedicated MCP server (e.g. Playwright MCP), not from this library.
81
+
50
82
  ## Subagents (subagent-as-tool)
51
83
 
52
84
  `createSubagent` wraps the boilerplate of a `ToolLoopAgent` (model + tools +
@@ -99,7 +131,9 @@ for (const dir of claudeDirs(process.cwd())) {
99
131
  | --- | --- |
100
132
  | `readFileTool`, `writeFileTool` | Read (offset/limit window) + Write, cwd-scoped |
101
133
  | `editFileTool`, `multiEditTool`, `applyEdit` | Exact-string Edit, batched MultiEdit, pure edit helper |
102
- | `bashTool` | Streaming shell tool, scoped to cwd |
134
+ | `bashTool` | Run one shell command, cwd as initial dir; returns stdout/stderr/exitCode |
135
+ | `multiBashTool` | Run an ordered sequence of commands; stops at the first non-zero exit |
136
+ | `backgroundProcessTools`, `ProcessManager` | Non-blocking background commands (dev servers): start / tail output / kill / killAll |
103
137
  | `globTool`, `grepTool`, `globToRegExp` | File glob + content search |
104
138
  | `composeSystemPrompt`, `createSubagent` | System-prompt composer + subagent-as-tool factory |
105
139
  | `envBlock` | Render the `<env>` system-context block from `EnvInfo` |
@@ -116,6 +150,30 @@ ESM only. Runs unchanged on **Node ≥ 20, Bun, and Deno ≥ 1.40**. Peer dep: `
116
150
  (AI SDK v6). No Anthropic SDK — "Claude-compat" refers to the *conventions*, not
117
151
  the provider.
118
152
 
153
+ ## Platform: Linux only
154
+
155
+ The shell tools (`bashTool`, `multiBashTool`) target **Linux**. They spawn
156
+ `bash -c …`, so they need a POSIX `bash` on `PATH` — they are **not** supported
157
+ on native Windows (use WSL) and are only best-effort on macOS. The pure
158
+ filesystem/edit/search tools are platform-neutral, but the package as a whole is
159
+ developed and tested on Linux; treat anything else as unsupported.
160
+
161
+ ### Shell environment (rbenv / nvm / asdf, `~/.bashrc`)
162
+
163
+ Commands run via a **non-login, non-interactive** `bash -c` with `BASH_ENV`
164
+ scrubbed. That is deliberate: a login/interactive shell would source
165
+ `/etc/profile` and `~/.bashrc`, which often `cd` away and would defeat the
166
+ cwd lock. The consequence:
167
+
168
+ - **PATH-based tools work** — `rbenv`/`asdf` shims, and any binary already on the
169
+ `PATH` of the process that launched the agent, are inherited (the child gets
170
+ `process.env`). If you can run `ruby`/`node` from the shell you start the agent
171
+ in, the agent can too.
172
+ - **Shell-function tools do not** — `nvm`, and anything that exists only as a
173
+ function defined in `~/.bashrc`, is **not** loaded. Source it yourself inside
174
+ the command (`source ~/.nvm/nvm.sh && nvm use && …`) or put the resolved
175
+ binary on `PATH` before launching.
176
+
119
177
  ## License
120
178
 
121
179
  MIT · part of [`developerz-ai/ai-task-master`](https://github.com/developerz-ai/ai-task-master)
@@ -0,0 +1,62 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { type Tool } from 'ai';
3
+ export type SpawnFn = typeof nodeSpawn;
4
+ export type BackgroundProcessInit = {
5
+ cwd: string;
6
+ maxBufferBytes?: number;
7
+ spawn?: SpawnFn;
8
+ };
9
+ export type ProcessStatus = {
10
+ id: string;
11
+ command: string;
12
+ running: boolean;
13
+ exitCode: number | null;
14
+ pid: number | null;
15
+ };
16
+ export type ProcessOutput = {
17
+ id: string;
18
+ stdout: string;
19
+ stderr: string;
20
+ running: boolean;
21
+ exitCode: number | null;
22
+ truncated: boolean;
23
+ };
24
+ export declare class ProcessManager {
25
+ private readonly cwd;
26
+ private readonly cap;
27
+ private readonly spawn;
28
+ private readonly procs;
29
+ private counter;
30
+ constructor(init: BackgroundProcessInit);
31
+ start(command: string): ProcessStatus;
32
+ output(id: string): ProcessOutput | null;
33
+ kill(id: string, signal?: NodeJS.Signals): boolean;
34
+ killAll(signal?: NodeJS.Signals): void;
35
+ list(): ProcessStatus[];
36
+ private statusOf;
37
+ }
38
+ export type BackgroundBashInput = {
39
+ command: string;
40
+ };
41
+ export type BackgroundBashOutput = ProcessStatus;
42
+ export type BashOutputInput = {
43
+ id: string;
44
+ };
45
+ export type KillBashInput = {
46
+ id: string;
47
+ };
48
+ export type KillBashOutput = {
49
+ id: string;
50
+ killed: boolean;
51
+ };
52
+ export type ListBackgroundOutput = {
53
+ processes: ProcessStatus[];
54
+ };
55
+ export type BackgroundProcessTools = {
56
+ manager: ProcessManager;
57
+ backgroundBash: Tool<BackgroundBashInput, BackgroundBashOutput>;
58
+ bashOutput: Tool<BashOutputInput, ProcessOutput>;
59
+ killBash: Tool<KillBashInput, KillBashOutput>;
60
+ listBackground: Tool<Record<string, never>, ListBackgroundOutput>;
61
+ };
62
+ export declare function backgroundProcessTools(init: BackgroundProcessInit): BackgroundProcessTools;
@@ -0,0 +1,146 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { tool } from 'ai';
3
+ import { z } from 'zod';
4
+ const DEFAULT_MAX_BUFFER_BYTES = 256 * 1024;
5
+ class CappedStream {
6
+ cap;
7
+ retained = '';
8
+ produced = 0;
9
+ returned = 0;
10
+ constructor(cap) {
11
+ this.cap = cap;
12
+ }
13
+ append(chunk) {
14
+ this.produced += chunk.length;
15
+ this.retained += chunk;
16
+ if (this.retained.length > this.cap) {
17
+ this.retained = this.retained.slice(this.retained.length - this.cap);
18
+ }
19
+ }
20
+ read() {
21
+ const retainedStart = this.produced - this.retained.length;
22
+ const from = Math.max(this.returned, retainedStart);
23
+ const chunk = this.retained.slice(from - retainedStart);
24
+ const truncated = retainedStart > this.returned;
25
+ this.returned = this.produced;
26
+ return { chunk, truncated };
27
+ }
28
+ }
29
+ export class ProcessManager {
30
+ cwd;
31
+ cap;
32
+ spawn;
33
+ procs = new Map();
34
+ counter = 0;
35
+ constructor(init) {
36
+ this.cwd = init.cwd;
37
+ this.cap = init.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
38
+ this.spawn = init.spawn ?? nodeSpawn;
39
+ }
40
+ start(command) {
41
+ const id = `bg-${++this.counter}`;
42
+ const proc = this.spawn('bash', ['-c', command], {
43
+ cwd: this.cwd,
44
+ env: { ...process.env, BASH_ENV: '' },
45
+ });
46
+ const entry = {
47
+ command,
48
+ proc,
49
+ stdout: new CappedStream(this.cap),
50
+ stderr: new CappedStream(this.cap),
51
+ running: true,
52
+ exitCode: null,
53
+ };
54
+ proc.stdout?.on('data', (d) => entry.stdout.append(d.toString()));
55
+ proc.stderr?.on('data', (d) => entry.stderr.append(d.toString()));
56
+ proc.on('error', (err) => {
57
+ entry.stderr.append(err.message);
58
+ entry.running = false;
59
+ if (entry.exitCode === null)
60
+ entry.exitCode = 1;
61
+ });
62
+ proc.on('exit', (code, signal) => {
63
+ entry.running = false;
64
+ entry.exitCode = code ?? (signal ? 1 : 0);
65
+ });
66
+ this.procs.set(id, entry);
67
+ return this.statusOf(id, entry);
68
+ }
69
+ output(id) {
70
+ const entry = this.procs.get(id);
71
+ if (!entry)
72
+ return null;
73
+ const out = entry.stdout.read();
74
+ const err = entry.stderr.read();
75
+ return {
76
+ id,
77
+ stdout: out.chunk,
78
+ stderr: err.chunk,
79
+ running: entry.running,
80
+ exitCode: entry.exitCode,
81
+ truncated: out.truncated || err.truncated,
82
+ };
83
+ }
84
+ kill(id, signal = 'SIGTERM') {
85
+ const entry = this.procs.get(id);
86
+ if (!entry)
87
+ return false;
88
+ if (entry.running)
89
+ entry.proc.kill(signal);
90
+ return true;
91
+ }
92
+ killAll(signal = 'SIGTERM') {
93
+ for (const entry of this.procs.values()) {
94
+ if (entry.running)
95
+ entry.proc.kill(signal);
96
+ }
97
+ }
98
+ list() {
99
+ return [...this.procs.entries()].map(([id, entry]) => this.statusOf(id, entry));
100
+ }
101
+ statusOf(id, entry) {
102
+ return {
103
+ id,
104
+ command: entry.command,
105
+ running: entry.running,
106
+ exitCode: entry.exitCode,
107
+ pid: entry.proc.pid ?? null,
108
+ };
109
+ }
110
+ }
111
+ const backgroundBashInputSchema = z.object({ command: z.string().min(1) });
112
+ const idInputSchema = z.object({ id: z.string().min(1) });
113
+ export function backgroundProcessTools(init) {
114
+ const manager = new ProcessManager(init);
115
+ const backgroundBash = tool({
116
+ description: 'Start a long-running shell command in the background (e.g. a dev server) and return immediately with a process id. Does NOT wait for it to exit. Poll its output with bashOutput(id) and stop it with killBash(id). For a command that finishes quickly, use the blocking bash tool instead.',
117
+ inputSchema: backgroundBashInputSchema,
118
+ execute: async (input) => manager.start(input.command),
119
+ });
120
+ const bashOutput = tool({
121
+ description: 'Read new stdout/stderr produced since the last bashOutput call for a background process id, plus whether it is still running and its exit code once finished.',
122
+ inputSchema: idInputSchema,
123
+ execute: async (input) => manager.output(input.id) ?? {
124
+ id: input.id,
125
+ stdout: '',
126
+ stderr: `no background process with id ${input.id}`,
127
+ running: false,
128
+ exitCode: 1,
129
+ truncated: false,
130
+ },
131
+ });
132
+ const killBash = tool({
133
+ description: 'Stop a background process by id (sends SIGTERM). Idempotent.',
134
+ inputSchema: idInputSchema,
135
+ execute: async (input) => ({
136
+ id: input.id,
137
+ killed: manager.kill(input.id),
138
+ }),
139
+ });
140
+ const listBackground = tool({
141
+ description: 'List all background processes started this session with their running state.',
142
+ inputSchema: z.object({}),
143
+ execute: async () => ({ processes: manager.list() }),
144
+ });
145
+ return { manager, backgroundBash, bashOutput, killBash, listBackground };
146
+ }
@@ -5,16 +5,29 @@ declare const bashInputSchema: z.ZodObject<{
5
5
  command: z.ZodString;
6
6
  timeoutMs: z.ZodOptional<z.ZodNumber>;
7
7
  }, z.core.$strip>;
8
+ declare const multiBashInputSchema: z.ZodObject<{
9
+ commands: z.ZodArray<z.ZodString>;
10
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
11
+ }, z.core.$strip>;
8
12
  export type BashInput = z.infer<typeof bashInputSchema>;
9
13
  export type BashOutput = {
10
14
  stdout: string;
11
15
  stderr: string;
12
16
  exitCode: number;
13
17
  };
18
+ export type MultiBashInput = z.infer<typeof multiBashInputSchema>;
19
+ export type MultiBashOutput = {
20
+ results: Array<{
21
+ command: string;
22
+ } & BashOutput>;
23
+ exitCode: number;
24
+ failedAt: number | null;
25
+ };
14
26
  export type BashToolInit = {
15
27
  cwd: string;
16
28
  defaultTimeoutMs?: number;
17
29
  exec?: typeof execa;
18
30
  };
19
31
  export declare function bashTool(init: BashToolInit): Tool<BashInput, BashOutput>;
32
+ export declare function multiBashTool(init: BashToolInit): Tool<MultiBashInput, MultiBashOutput>;
20
33
  export {};
package/dist/bash-tool.js CHANGED
@@ -5,42 +5,67 @@ const bashInputSchema = z.object({
5
5
  command: z.string().min(1),
6
6
  timeoutMs: z.number().int().positive().optional(),
7
7
  });
8
+ const multiBashInputSchema = z.object({
9
+ commands: z.array(z.string().min(1)).min(1),
10
+ timeoutMs: z.number().int().positive().optional(),
11
+ });
8
12
  const DEFAULT_BASH_TIMEOUT_MS = 60_000;
9
13
  const MAX_BASH_TIMEOUT_MS = 600_000;
14
+ async function runBash(exec, cwd, command, timeout) {
15
+ try {
16
+ const r = await exec('bash', ['-c', command], {
17
+ cwd,
18
+ timeout,
19
+ env: { ...process.env, BASH_ENV: '' },
20
+ });
21
+ return {
22
+ stdout: typeof r.stdout === 'string' ? r.stdout : '',
23
+ stderr: typeof r.stderr === 'string' ? r.stderr : '',
24
+ exitCode: r.exitCode ?? 0,
25
+ };
26
+ }
27
+ catch (err) {
28
+ if (err instanceof ExecaError) {
29
+ return {
30
+ stdout: typeof err.stdout === 'string' ? err.stdout : '',
31
+ stderr: typeof err.stderr === 'string' ? err.stderr : err.message,
32
+ exitCode: err.exitCode ?? 1,
33
+ };
34
+ }
35
+ return {
36
+ stdout: '',
37
+ stderr: err instanceof Error ? err.message : String(err),
38
+ exitCode: 1,
39
+ };
40
+ }
41
+ }
10
42
  export function bashTool(init) {
11
43
  const exec = init.exec ?? execa;
12
44
  const defaultTimeout = init.defaultTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS;
13
45
  return tool({
14
46
  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.',
15
47
  inputSchema: bashInputSchema,
48
+ execute: (input) => runBash(exec, init.cwd, input.command, Math.min(input.timeoutMs ?? defaultTimeout, MAX_BASH_TIMEOUT_MS)),
49
+ });
50
+ }
51
+ export function multiBashTool(init) {
52
+ const exec = init.exec ?? execa;
53
+ const defaultTimeout = init.defaultTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS;
54
+ return tool({
55
+ description: 'Run a sequence of shell commands inside the current worktree, one after another. Stops at the first command that exits non-zero — the remaining commands are not run. Each command runs in its own `bash -c` with cwd reset to the worktree, so chain `cd x && …` within a single command if you need a directory change to persist. Returns one result per command that ran, the overall exit code, and the index of the failing command (failedAt) if any.',
56
+ inputSchema: multiBashInputSchema,
16
57
  execute: async (input) => {
17
58
  const timeout = Math.min(input.timeoutMs ?? defaultTimeout, MAX_BASH_TIMEOUT_MS);
18
- try {
19
- const r = await exec('bash', ['-c', input.command], {
20
- cwd: init.cwd,
21
- timeout,
22
- env: { ...process.env, BASH_ENV: '' },
23
- });
24
- return {
25
- stdout: typeof r.stdout === 'string' ? r.stdout : '',
26
- stderr: typeof r.stderr === 'string' ? r.stderr : '',
27
- exitCode: r.exitCode ?? 0,
28
- };
29
- }
30
- catch (err) {
31
- if (err instanceof ExecaError) {
32
- return {
33
- stdout: typeof err.stdout === 'string' ? err.stdout : '',
34
- stderr: typeof err.stderr === 'string' ? err.stderr : err.message,
35
- exitCode: err.exitCode ?? 1,
36
- };
59
+ const results = [];
60
+ for (let i = 0; i < input.commands.length; i++) {
61
+ const command = input.commands[i] ?? '';
62
+ const out = await runBash(exec, init.cwd, command, timeout);
63
+ results.push({ command, ...out });
64
+ if (out.exitCode !== 0) {
65
+ return { results, exitCode: out.exitCode, failedAt: i };
37
66
  }
38
- return {
39
- stdout: '',
40
- stderr: err instanceof Error ? err.message : String(err),
41
- exitCode: 1,
42
- };
43
67
  }
68
+ return { results, exitCode: 0, failedAt: null };
44
69
  },
45
70
  });
46
71
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { type AgentDefinition, claudeDirs, loadAgents } from './agents-loader.ts';
2
- export { type BashInput, type BashOutput, type BashToolInit, bashTool } from './bash-tool.ts';
2
+ export { type BackgroundBashInput, type BackgroundBashOutput, type BackgroundProcessInit, type BackgroundProcessTools, type BashOutputInput, backgroundProcessTools, type KillBashInput, type KillBashOutput, type ListBackgroundOutput, ProcessManager, type ProcessOutput, type ProcessStatus, type SpawnFn, } from './background-process.ts';
3
+ export { type BashInput, type BashOutput, type BashToolInit, bashTool, type MultiBashInput, type MultiBashOutput, multiBashTool, } from './bash-tool.ts';
3
4
  export { applyEdit, type EditFileInput, type EditFileOutput, type EditSpec, editFileTool, type MultiEditInput, type MultiEditOutput, multiEditTool, } from './edit-tools.ts';
4
5
  export { type EnvInfo, envBlock } from './env-block.ts';
5
6
  export { asString, asStringArray, type Frontmatter, type FrontmatterValue, parseFrontmatter, } from './frontmatter.ts';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { claudeDirs, loadAgents } from "./agents-loader.js";
2
- export { bashTool } from "./bash-tool.js";
2
+ export { backgroundProcessTools, ProcessManager, } from "./background-process.js";
3
+ export { bashTool, multiBashTool, } from "./bash-tool.js";
3
4
  export { applyEdit, editFileTool, multiEditTool, } from "./edit-tools.js";
4
5
  export { envBlock } from "./env-block.js";
5
6
  export { asString, asStringArray, parseFrontmatter, } from "./frontmatter.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developerz.ai/ai-claude-compat",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
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
5
  "license": "MIT",
6
6
  "type": "module",