outsrc 0.2.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/config.example.toml +46 -0
  4. package/dist/adapters.d.ts +110 -0
  5. package/dist/adapters.js +246 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +302 -0
  8. package/dist/config.d.ts +46 -0
  9. package/dist/config.js +87 -0
  10. package/dist/engines.d.ts +44 -0
  11. package/dist/engines.js +69 -0
  12. package/dist/fs-home.d.ts +7 -0
  13. package/dist/fs-home.js +28 -0
  14. package/dist/git.d.ts +9 -0
  15. package/dist/git.js +27 -0
  16. package/dist/init.d.ts +54 -0
  17. package/dist/init.js +322 -0
  18. package/dist/job.d.ts +19 -0
  19. package/dist/job.js +112 -0
  20. package/dist/limits.d.ts +13 -0
  21. package/dist/limits.js +10 -0
  22. package/dist/mailbox.d.ts +122 -0
  23. package/dist/mailbox.js +503 -0
  24. package/dist/migrate.d.ts +11 -0
  25. package/dist/migrate.js +122 -0
  26. package/dist/paths.d.ts +7 -0
  27. package/dist/paths.js +36 -0
  28. package/dist/permissions.d.ts +2 -0
  29. package/dist/permissions.js +29 -0
  30. package/dist/plugin-contract.d.ts +12 -0
  31. package/dist/plugin-contract.js +79 -0
  32. package/dist/plugins.d.ts +44 -0
  33. package/dist/plugins.js +215 -0
  34. package/dist/prompt.d.ts +2 -0
  35. package/dist/prompt.js +20 -0
  36. package/dist/server.d.ts +12 -0
  37. package/dist/server.js +127 -0
  38. package/dist/settings.d.ts +247 -0
  39. package/dist/settings.js +214 -0
  40. package/dist/state.d.ts +79 -0
  41. package/dist/state.js +61 -0
  42. package/dist/types.d.ts +181 -0
  43. package/dist/types.js +35 -0
  44. package/dist/workspace.d.ts +18 -0
  45. package/dist/workspace.js +66 -0
  46. package/dist/wrapper.d.ts +2 -0
  47. package/dist/wrapper.js +246 -0
  48. package/engines.lock.json +14 -0
  49. package/package.json +59 -0
@@ -0,0 +1,181 @@
1
+ import type { Limits } from "./limits.js";
2
+ import type { AdapterTarget, Finding } from "./adapters.js";
3
+ export type ThreadId = string & {
4
+ readonly __brand: "ThreadId";
5
+ };
6
+ export type RunId = string & {
7
+ readonly __brand: "RunId";
8
+ };
9
+ export type Alias = string & {
10
+ readonly __brand: "Alias";
11
+ };
12
+ export type TargetName = string & {
13
+ readonly __brand: "TargetName";
14
+ };
15
+ export declare const MINTED_THREAD_ID: RegExp;
16
+ export declare const STORAGE_ID: RegExp;
17
+ export declare const CALLER: RegExp;
18
+ export declare const DEFAULT_CALLER = "local";
19
+ export declare function parseCaller(raw: string): string;
20
+ export declare function parseThreadId(raw: string): ThreadId;
21
+ export declare function parseRunId(raw: string): RunId;
22
+ export declare function parseAlias(raw: string): Alias;
23
+ export declare function parseTargetName(raw: string): TargetName;
24
+ export type PermissionMode = "auto" | "ask";
25
+ export type TaskKind = "task" | "review" | "adversarial_review";
26
+ export type RepoConfig = {
27
+ alias: string;
28
+ path: string;
29
+ setup?: string[][];
30
+ autoCommit?: boolean;
31
+ retentionDays?: number;
32
+ };
33
+ export type TargetConfig = {
34
+ adapter?: AdapterTarget["adapter"];
35
+ command: string;
36
+ args: string[];
37
+ permissions?: PermissionMode;
38
+ models?: {
39
+ default: string;
40
+ allowed: string[];
41
+ };
42
+ effort?: {
43
+ default: string;
44
+ allowed: string[];
45
+ };
46
+ description?: string;
47
+ costNote?: string;
48
+ };
49
+ export type Config = {
50
+ repos: RepoConfig[];
51
+ targets: Record<string, TargetConfig>;
52
+ limits?: Limits;
53
+ };
54
+ export type ThreadRecord = {
55
+ version: 2;
56
+ id: ThreadId;
57
+ caller: string;
58
+ repo: Alias;
59
+ target: TargetName;
60
+ model: string | null;
61
+ effort: string | null;
62
+ taskKind: TaskKind;
63
+ base: string | null;
64
+ worktree: string;
65
+ branch: string;
66
+ baseCommit: string;
67
+ createdAt: string;
68
+ latestRunId: string;
69
+ };
70
+ export type RunRecord = {
71
+ id: string;
72
+ threadId: ThreadId;
73
+ requestId: string | null;
74
+ message: string;
75
+ createdAt: string;
76
+ sessionId: string | null;
77
+ };
78
+ export type RunSpec = {
79
+ threadId: ThreadId;
80
+ runId: string;
81
+ runDir: string;
82
+ worktree: string;
83
+ baseCommit: string;
84
+ target: AdapterTarget;
85
+ model: string | null;
86
+ effort: string | null;
87
+ permissions: PermissionMode;
88
+ sessionId: string | null;
89
+ prompt: string;
90
+ setup: string[][];
91
+ autoCommit: boolean;
92
+ kind: TaskKind;
93
+ base: string | null;
94
+ deadlineMs?: number | null;
95
+ };
96
+ export type RunResult = {
97
+ kind: "completed" | "needs_input" | "failed" | "cancelled";
98
+ message: string;
99
+ sessionId: string | null;
100
+ findings: Finding[];
101
+ exitCode: number | null;
102
+ finishedAt: string;
103
+ diffstat: string;
104
+ commit: string | null;
105
+ };
106
+ export type SendInput = {
107
+ message: string;
108
+ repo?: string;
109
+ target?: string;
110
+ thread_id?: string;
111
+ request_id?: string;
112
+ model?: string;
113
+ effort?: string;
114
+ kind?: TaskKind;
115
+ base?: string;
116
+ ref?: string;
117
+ };
118
+ export type SendResult = {
119
+ ok: true;
120
+ delivered: true;
121
+ thread_id: ThreadId;
122
+ run_id: string;
123
+ } | {
124
+ ok: false;
125
+ error: string;
126
+ };
127
+ export type ThreadStatus = "working" | "needs_input" | "succeeded" | "failed" | "cancelled";
128
+ export type InboxResult = {
129
+ ok: true;
130
+ status: "working";
131
+ retry_after_seconds: number;
132
+ run_id: string;
133
+ progress: string;
134
+ } | {
135
+ ok: true;
136
+ status: "needs_input";
137
+ retry_after_seconds: number;
138
+ run_id: string;
139
+ message: string;
140
+ question_id: string;
141
+ session_id: string | null;
142
+ } | {
143
+ ok: true;
144
+ status: "succeeded" | "failed" | "cancelled";
145
+ run_id: string;
146
+ message: string;
147
+ branch: string;
148
+ worktree: string;
149
+ diffstat: {
150
+ raw: string;
151
+ };
152
+ exit_code: number | null;
153
+ session_id: string | null;
154
+ findings: Finding[];
155
+ commit: string | null;
156
+ } | {
157
+ ok: false;
158
+ error: string;
159
+ };
160
+ export type LogResult = {
161
+ ok: true;
162
+ text: string;
163
+ } | {
164
+ ok: false;
165
+ error: string;
166
+ };
167
+ export type StopResult = {
168
+ ok: true;
169
+ stopped: true;
170
+ } | {
171
+ ok: false;
172
+ error: string;
173
+ };
174
+ export type MailboxContext = {
175
+ home: string;
176
+ caller?: string;
177
+ config: Config;
178
+ retryAfterSeconds?: number;
179
+ wrapperCommand?: string[];
180
+ nodeExecutable?: string;
181
+ };
package/dist/types.js ADDED
@@ -0,0 +1,35 @@
1
+ export const MINTED_THREAD_ID = /^[0-9a-f]{16}$/;
2
+ export const STORAGE_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
3
+ // Who is connected: each registration of the server names its caller, and a caller sees only its own threads.
4
+ export const CALLER = /^[a-z0-9][a-z0-9-]{0,31}$/;
5
+ export const DEFAULT_CALLER = "local";
6
+ export function parseCaller(raw) {
7
+ const caller = raw.trim();
8
+ if (!CALLER.test(caller))
9
+ throw new Error("caller must match ^[a-z0-9][a-z0-9-]{0,31}$");
10
+ return caller;
11
+ }
12
+ export function parseThreadId(raw) {
13
+ const id = raw.trim();
14
+ if (!MINTED_THREAD_ID.test(id))
15
+ throw new Error("thread_id must be 16 lowercase hex characters");
16
+ return id;
17
+ }
18
+ export function parseRunId(raw) {
19
+ const id = raw.trim();
20
+ if (!STORAGE_ID.test(id))
21
+ throw new Error("run_id must be a single storage id");
22
+ return id;
23
+ }
24
+ export function parseAlias(raw) {
25
+ const alias = raw.trim();
26
+ if (!alias)
27
+ throw new Error("repo is required");
28
+ return alias;
29
+ }
30
+ export function parseTargetName(raw) {
31
+ const target = raw.trim();
32
+ if (!target)
33
+ throw new Error("target is required");
34
+ return target;
35
+ }
@@ -0,0 +1,18 @@
1
+ export type WorkspaceDiff = {
2
+ patch: string;
3
+ stat: string;
4
+ truncated: boolean;
5
+ };
6
+ export declare function getBaseCommit(repo: string): string;
7
+ export declare function collectWorkspaceDiff(input: {
8
+ workspace: string;
9
+ baseCommit: string;
10
+ maxBytes?: number;
11
+ }): WorkspaceDiff;
12
+ export declare function commitWorkspace(input: {
13
+ workspace: string;
14
+ message: string;
15
+ }): {
16
+ commit: string;
17
+ changed: boolean;
18
+ };
@@ -0,0 +1,66 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ const DEFAULT_DIFF_BYTES = 256 * 1024;
6
+ function git(args, workspace, env = process.env) {
7
+ return execFileSync("git", args, {
8
+ cwd: workspace,
9
+ env,
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "pipe"],
12
+ }).trim();
13
+ }
14
+ export function getBaseCommit(repo) {
15
+ return git(["rev-parse", "HEAD"], repo);
16
+ }
17
+ function boundedGit(args, workspace, env, limit) {
18
+ const result = spawnSync("git", args, {
19
+ cwd: workspace,
20
+ env,
21
+ maxBuffer: limit + 1,
22
+ });
23
+ let truncated = result.stdout.length > limit || result.error?.message.includes("ENOBUFS") === true;
24
+ if (!truncated && (result.error || result.status !== 0)) {
25
+ throw result.error ?? new Error(result.stderr.toString().trim() || "git diff failed");
26
+ }
27
+ const text = result.stdout.subarray(0, limit).toString("utf8");
28
+ const encoded = Buffer.from(text);
29
+ truncated ||= encoded.length > limit;
30
+ return {
31
+ text: new TextDecoder().decode(encoded.subarray(0, limit), { stream: true }),
32
+ truncated,
33
+ };
34
+ }
35
+ export function collectWorkspaceDiff(input) {
36
+ const maxBytes = input.maxBytes ?? DEFAULT_DIFF_BYTES;
37
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
38
+ throw new Error("maxBytes must be a positive integer");
39
+ }
40
+ const temporary = mkdtempSync(join(tmpdir(), "outsrc-diff-"));
41
+ try {
42
+ const index = join(temporary, "index");
43
+ const currentIndex = resolve(input.workspace, git(["rev-parse", "--git-path", "index"], input.workspace));
44
+ const env = { ...process.env, GIT_INDEX_FILE: index };
45
+ if (existsSync(currentIndex))
46
+ copyFileSync(currentIndex, index);
47
+ else
48
+ git(["read-tree", "HEAD"], input.workspace, env);
49
+ git(["add", "-A", "--", "."], input.workspace, env);
50
+ const args = ["diff", "--cached", "--no-color"];
51
+ const patch = boundedGit([...args, input.baseCommit, "--"], input.workspace, env, maxBytes);
52
+ const stat = boundedGit([...args, "--stat", input.baseCommit, "--"], input.workspace, env, maxBytes);
53
+ return { patch: patch.text, stat: stat.text, truncated: patch.truncated || stat.truncated };
54
+ }
55
+ finally {
56
+ rmSync(temporary, { recursive: true, force: true });
57
+ }
58
+ }
59
+ export function commitWorkspace(input) {
60
+ git(["add", "-A", "--", "."], input.workspace);
61
+ const tree = git(["write-tree"], input.workspace);
62
+ const changed = tree !== git(["rev-parse", "HEAD^{tree}"], input.workspace);
63
+ if (changed)
64
+ git(["commit", "-m", input.message], input.workspace);
65
+ return { commit: getBaseCommit(input.workspace), changed };
66
+ }
@@ -0,0 +1,2 @@
1
+ import { type RunResult, type RunSpec } from "./types.js";
2
+ export declare function run(spec: RunSpec): Promise<RunResult>;
@@ -0,0 +1,246 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { writeHomeFile } from "./fs-home.js";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseArgs } from "node:util";
7
+ import { z } from "zod";
8
+ import { AGENT_OUTPUT_SCHEMA, buildInvocation, parseAgentOutput } from "./adapters.js";
9
+ import { captureProcess } from "./job.js";
10
+ import { LOG_TRUNCATED, MAX_LOG_BYTES, MAX_RUN_MS } from "./limits.js";
11
+ import { engineDir, isPluginAdapter, teardownInvocation } from "./plugins.js";
12
+ import { writeJson } from "./state.js";
13
+ import { parseThreadId } from "./types.js";
14
+ import { collectWorkspaceDiff, commitWorkspace } from "./workspace.js";
15
+ const RunSpecSchema = z.object({
16
+ threadId: z.string().transform(parseThreadId),
17
+ runId: z.string(),
18
+ runDir: z.string(),
19
+ worktree: z.string(),
20
+ baseCommit: z.string(),
21
+ target: z.object({
22
+ adapter: z.enum(["claude", "codex", "grok", "custom", "codex-plugin", "grok-plugin"]),
23
+ command: z.string(),
24
+ args: z.array(z.string()),
25
+ }),
26
+ model: z.string().nullable(),
27
+ effort: z.string().nullable(),
28
+ permissions: z.enum(["auto", "ask"]),
29
+ sessionId: z.string().nullable(),
30
+ prompt: z.string(),
31
+ setup: z.array(z.array(z.string())),
32
+ autoCommit: z.boolean(),
33
+ kind: z.enum(["task", "review", "adversarial_review"]),
34
+ base: z.string().nullable(),
35
+ });
36
+ const DeadlineSchema = z.object({ deadlineMs: z.number().int().positive().nullable().optional() });
37
+ function failure(message, sessionId) {
38
+ return { kind: "failed", message, sessionId, findings: [] };
39
+ }
40
+ function execute(input) {
41
+ return new Promise((resolveResult, reject) => {
42
+ const child = spawn(input.command, input.args, {
43
+ cwd: input.cwd,
44
+ env: { ...process.env, ...input.env },
45
+ stdio: ["ignore", "pipe", "pipe"],
46
+ });
47
+ const stdout = [];
48
+ let logError;
49
+ let logged = 0;
50
+ let truncated = false;
51
+ let settled = false;
52
+ let timedOut = false;
53
+ const marker = Buffer.from(LOG_TRUNCATED);
54
+ function append(chunk) {
55
+ try {
56
+ if (truncated)
57
+ return;
58
+ const room = MAX_LOG_BYTES - logged;
59
+ if (room <= 0) {
60
+ appendFileSync(input.logPath, marker);
61
+ truncated = true;
62
+ return;
63
+ }
64
+ const slice = chunk.subarray(0, room);
65
+ appendFileSync(input.logPath, slice);
66
+ logged += slice.length;
67
+ if (slice.length < chunk.length) {
68
+ appendFileSync(input.logPath, marker);
69
+ truncated = true;
70
+ }
71
+ }
72
+ catch (error) {
73
+ logError = error;
74
+ }
75
+ }
76
+ function finish(error, exitCode) {
77
+ if (settled)
78
+ return;
79
+ settled = true;
80
+ clearTimeout(timer);
81
+ if (error !== undefined)
82
+ reject(error);
83
+ else
84
+ resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), exitCode });
85
+ }
86
+ const timer = input.deadlineMs === null ? undefined : setTimeout(() => {
87
+ timedOut = true;
88
+ child.kill("SIGTERM");
89
+ const killTimer = setTimeout(() => child.kill("SIGKILL"), 500);
90
+ killTimer.unref();
91
+ }, input.deadlineMs);
92
+ timer?.unref();
93
+ child.once("error", (error) => finish(error, null));
94
+ child.stdout.on("data", (chunk) => {
95
+ stdout.push(chunk);
96
+ append(chunk);
97
+ });
98
+ child.stderr.on("data", append);
99
+ child.once("close", (exitCode) => {
100
+ if (timedOut)
101
+ finish(new Error(`Run exceeded the ${input.deadlineMs}ms deadline`), null);
102
+ else if (logError !== undefined)
103
+ finish(logError, null);
104
+ else
105
+ finish(undefined, exitCode);
106
+ });
107
+ });
108
+ }
109
+ export async function run(spec) {
110
+ let output = failure("Agent did not run", spec.sessionId);
111
+ let exitCode = null;
112
+ let commit = null;
113
+ let diffstat = "";
114
+ let phase = "Run preparation";
115
+ const logPath = join(spec.runDir, "run.log");
116
+ const deadlineMs = spec.deadlineMs === undefined ? MAX_RUN_MS : spec.deadlineMs;
117
+ const deadlineAt = deadlineMs === null ? null : Date.now() + deadlineMs;
118
+ function timeLeft() {
119
+ if (deadlineAt === null)
120
+ return null;
121
+ const left = deadlineAt - Date.now();
122
+ if (left <= 0)
123
+ throw new Error(`Run exceeded the ${deadlineMs}ms deadline`);
124
+ return left;
125
+ }
126
+ function publish(result) {
127
+ const temporary = join(spec.runDir, "result.json.tmp");
128
+ writeHomeFile(temporary, JSON.stringify(result));
129
+ renameSync(temporary, join(spec.runDir, "result.json"));
130
+ return result;
131
+ }
132
+ mkdirSync(spec.runDir, { recursive: true, mode: 0o700 });
133
+ try {
134
+ chmodSync(spec.runDir, 0o700);
135
+ }
136
+ catch { }
137
+ if (existsSync(join(spec.runDir, "cancelled"))) {
138
+ return publish({
139
+ kind: "cancelled", message: "Task cancelled", sessionId: spec.sessionId, findings: [],
140
+ exitCode: null, finishedAt: new Date().toISOString(), diffstat: "", commit: null,
141
+ });
142
+ }
143
+ try {
144
+ writeFileSync(logPath, "");
145
+ const schemaPath = join(spec.runDir, "output-schema.json");
146
+ writeFileSync(schemaPath, JSON.stringify(AGENT_OUTPUT_SCHEMA));
147
+ for (const [index, command] of spec.setup.entries()) {
148
+ phase = `Setup command ${index + 1}`;
149
+ const [executable, ...args] = command;
150
+ if (!executable)
151
+ throw new Error("command is empty");
152
+ const setup = await execute({ command: executable, args, cwd: spec.worktree, logPath, deadlineMs: timeLeft() });
153
+ if (setup.exitCode !== 0) {
154
+ exitCode = setup.exitCode;
155
+ throw new Error(`exited with ${setup.exitCode ?? "a signal"}`);
156
+ }
157
+ }
158
+ phase = "Agent execution";
159
+ const dataDir = isPluginAdapter(spec.target.adapter) ? engineDir(dirname(dirname(spec.runDir)), spec.target.adapter) : "";
160
+ const invocation = buildInvocation({
161
+ target: spec.target,
162
+ worktree: spec.worktree,
163
+ prompt: spec.prompt,
164
+ model: spec.model,
165
+ effort: spec.effort,
166
+ sessionId: spec.sessionId,
167
+ schemaPath,
168
+ permissions: spec.permissions,
169
+ kind: spec.kind,
170
+ base: spec.base,
171
+ dataDir,
172
+ threadId: spec.threadId,
173
+ });
174
+ let executed;
175
+ try {
176
+ executed = await execute({ ...invocation, cwd: spec.worktree, logPath, deadlineMs: timeLeft() });
177
+ }
178
+ finally {
179
+ if (isPluginAdapter(spec.target.adapter)) {
180
+ const teardown = teardownInvocation(spec.target.adapter, spec.target.command, spec.worktree, dataDir);
181
+ if (teardown) {
182
+ spawnSync(teardown.command, teardown.args, {
183
+ cwd: spec.worktree, input: teardown.input, env: { ...process.env, ...teardown.env }, stdio: ["pipe", "ignore", "ignore"], timeout: 15_000,
184
+ });
185
+ }
186
+ }
187
+ }
188
+ exitCode = executed.exitCode;
189
+ output = parseAgentOutput({ adapter: spec.target.adapter, stdout: executed.stdout, exitCode });
190
+ output.sessionId ??= spec.sessionId;
191
+ if (output.kind === "needs_input" && spec.target.adapter === "custom" &&
192
+ !spec.target.args.some((arg) => arg.includes("{session_id}"))) {
193
+ output = failure("Custom adapter requested input but has no {session_id} argument for resuming", output.sessionId);
194
+ }
195
+ if (output.kind === "completed" && spec.autoCommit) {
196
+ phase = "Automatic commit";
197
+ commit = commitWorkspace({
198
+ workspace: spec.worktree,
199
+ message: `outsrc: complete ${spec.threadId}`,
200
+ }).commit;
201
+ }
202
+ }
203
+ catch (error) {
204
+ output = failure(`${phase} failed: ${String(error)}`, output.sessionId);
205
+ }
206
+ try {
207
+ diffstat = collectWorkspaceDiff({ workspace: spec.worktree, baseCommit: spec.baseCommit }).stat;
208
+ }
209
+ catch (error) {
210
+ const context = output.kind === "failed" ? `${output.message}\n` : "";
211
+ output = failure(`${context}Result collection failed: ${String(error)}`, output.sessionId);
212
+ }
213
+ return publish({
214
+ ...output,
215
+ exitCode,
216
+ finishedAt: new Date().toISOString(),
217
+ diffstat,
218
+ commit,
219
+ });
220
+ }
221
+ async function main() {
222
+ const { values } = parseArgs({ options: { spec: { type: "string" } }, strict: true });
223
+ if (!values.spec)
224
+ throw new Error("wrapper: --spec is required");
225
+ const raw = JSON.parse(readFileSync(values.spec, "utf8"));
226
+ const parsed = RunSpecSchema.parse(raw);
227
+ const deadlineMs = DeadlineSchema.parse(raw).deadlineMs;
228
+ const spec = deadlineMs === undefined ? parsed : { ...parsed, deadlineMs };
229
+ mkdirSync(spec.runDir, { recursive: true, mode: 0o700 });
230
+ try {
231
+ chmodSync(spec.runDir, 0o700);
232
+ }
233
+ catch { }
234
+ const recorded = captureProcess(process.pid);
235
+ if (!recorded)
236
+ throw new Error("process start time is unavailable");
237
+ writeJson(join(spec.runDir, "process.json"), recorded);
238
+ const result = await run(spec);
239
+ process.exitCode = result.kind === "failed" ? 1 : 0;
240
+ }
241
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
242
+ main().catch((error) => {
243
+ process.stderr.write(`${String(error)}\n`);
244
+ process.exitCode = 1;
245
+ });
246
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "codex-plugin": {
3
+ "source": "https://github.com/openai/codex-plugin-cc.git",
4
+ "path": "plugins/codex",
5
+ "version": "1.0.5",
6
+ "commit": "80c31f99570876c3ef40327838b0a2ca1ae2cd9c"
7
+ },
8
+ "grok-plugin": {
9
+ "source": "https://github.com/xai-org/grok-build-plugin-cc.git",
10
+ "path": "plugins/grok-build",
11
+ "version": "0.2.0",
12
+ "commit": "5a9f924a8d1ca802b3e6dc0ce0e1a602fb35ec9e"
13
+ }
14
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "outsrc",
3
+ "version": "0.2.0",
4
+ "description": "Local MCP mailbox that lets any agent hand coding tasks to Claude Code, Codex and Grok Build in git worktrees.",
5
+ "homepage": "https://outsrc.ing",
6
+ "keywords": [
7
+ "mcp",
8
+ "claude-code",
9
+ "codex",
10
+ "grok",
11
+ "agents",
12
+ "worktree"
13
+ ],
14
+ "type": "module",
15
+ "bin": {
16
+ "outsrc-mcp": "dist/server.js",
17
+ "outsrc": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "engines.lock.json",
22
+ "config.example.toml",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "node scripts/build.mjs",
28
+ "test": "vitest run",
29
+ "mcp": "node --import tsx src/server.ts",
30
+ "verify": "node scripts/verify.mjs",
31
+ "typecheck": "tsc --project tsconfig.tests.json",
32
+ "smoke:native": "npm run build && node scripts/native-smoke.mjs",
33
+ "smoke:plugins": "npm run build && node scripts/plugin-smoke.mjs",
34
+ "prepublishOnly": "npm run build"
35
+ },
36
+ "engines": {
37
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.25.1",
41
+ "smol-toml": "^1.4.2",
42
+ "zod": "^4.6.5"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.15.0",
46
+ "tsx": "^4.20.0",
47
+ "typescript": "^7.0.2",
48
+ "vitest": "^5.0.1"
49
+ },
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/proticom/outsrc.ing.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/proticom/outsrc.ing/issues"
57
+ },
58
+ "author": "Proticom"
59
+ }