outsrc 0.2.0 → 0.2.1

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.
@@ -0,0 +1,136 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { promisify } from "node:util";
6
+ import { z } from "zod";
7
+ import { ensureOutsrcHome } from "./fs-home.js";
8
+ import { readJson, writeJson } from "./state.js";
9
+ import { isBehind, parseVersion } from "./version.js";
10
+ export const PRODUCTS = ["claude", "codex", "grok", "outsrc"];
11
+ const Check = z.object({ installed: z.string().nullable(), latest: z.string().nullable(), error: z.string().nullable() });
12
+ const Pending = z.object({ installed: z.string(), latest: z.string(), first_seen_at: z.string() });
13
+ const StateSchema = z.object({
14
+ version: z.literal(1),
15
+ checked_at: z.string().nullable(),
16
+ checks: z.partialRecord(z.enum(PRODUCTS), Check),
17
+ pending: z.partialRecord(z.enum(PRODUCTS), Pending),
18
+ });
19
+ const AckSchema = z.object({ version: z.literal(1), acked: z.array(z.string()) });
20
+ // refresh is the only writer of updates.json; ack is the only writer of updates-ack.json.
21
+ export function updatesFile(home) { return join(home, "updates.json"); }
22
+ export function ackFile(home) { return join(home, "updates-ack.json"); }
23
+ const EMPTY = { version: 1, checked_at: null, checks: {}, pending: {} };
24
+ export function readUpdates(home) {
25
+ const file = updatesFile(home);
26
+ if (!existsSync(file))
27
+ return EMPTY;
28
+ const parsed = StateSchema.safeParse(readJson(file));
29
+ return parsed.success ? parsed.data : EMPTY;
30
+ }
31
+ function readAcks(home) {
32
+ const file = ackFile(home);
33
+ if (!existsSync(file))
34
+ return [];
35
+ const parsed = AckSchema.safeParse(readJson(file));
36
+ return parsed.success ? parsed.data.acked : [];
37
+ }
38
+ export function notifications(state) {
39
+ return PRODUCTS.flatMap((product) => {
40
+ const entry = state.pending[product];
41
+ return entry ? [{ id: `${product}@${entry.latest}`, product, ...entry }] : [];
42
+ });
43
+ }
44
+ /** Pending updates the owner has not acknowledged yet. */
45
+ export function unread(home, state = readUpdates(home)) {
46
+ const acked = new Set(readAcks(home));
47
+ return notifications(state).filter((note) => !acked.has(note.id));
48
+ }
49
+ export function ack(home, ids) {
50
+ const chosen = ids === "all" ? unread(home).map((note) => note.id) : ids;
51
+ const acked = [...new Set([...readAcks(home), ...chosen])].slice(-200);
52
+ ensureOutsrcHome(home);
53
+ writeJson(ackFile(home), { version: 1, acked });
54
+ return chosen;
55
+ }
56
+ async function settle(read) {
57
+ try {
58
+ const text = await read();
59
+ const version = parseVersion(text);
60
+ return version ? { version: version.text, error: null } : { version: null, error: `no version in ${JSON.stringify(text.slice(0, 80))}`, missing: false };
61
+ }
62
+ catch (error) {
63
+ const missing = error.code === "ENOENT";
64
+ return { version: null, error: error instanceof Error ? error.message.split("\n")[0] ?? "failed" : String(error), missing };
65
+ }
66
+ }
67
+ /**
68
+ * Checks every product and records which are behind. Never installs anything.
69
+ * A pending entry keeps its first_seen_at until a different latest version appears, so reruns are idempotent,
70
+ * and a failed check keeps the previous entry instead of dropping it and announcing it again later.
71
+ */
72
+ export async function refresh(home, probe, now = new Date()) {
73
+ const previous = readUpdates(home);
74
+ const checkedAt = now.toISOString();
75
+ const results = await Promise.all(PRODUCTS.map(async (product) => {
76
+ const [installed, latest] = await Promise.all([settle(() => probe.installed(product)), settle(() => probe.latest(product))]);
77
+ return { product, installed, latest };
78
+ }));
79
+ const state = { version: 1, checked_at: checkedAt, checks: {}, pending: {} };
80
+ const fresh = [];
81
+ for (const { product, installed, latest } of results) {
82
+ state.checks[product] = { installed: installed.version, latest: latest.version, error: installed.error ?? latest.error };
83
+ const before = previous.pending[product];
84
+ if (installed.version === null || latest.version === null) {
85
+ // An uninstalled CLI has nothing to update; any other failure keeps what the last good check found.
86
+ const uninstalled = installed.version === null && installed.missing;
87
+ if (before && !uninstalled)
88
+ state.pending[product] = before;
89
+ continue;
90
+ }
91
+ if (!isBehind(installed.version, latest.version))
92
+ continue;
93
+ const entry = before?.latest === latest.version
94
+ ? { ...before, installed: installed.version }
95
+ : { installed: installed.version, latest: latest.version, first_seen_at: checkedAt };
96
+ state.pending[product] = entry;
97
+ if (before?.latest !== latest.version)
98
+ fresh.push({ id: `${product}@${entry.latest}`, product, ...entry });
99
+ }
100
+ ensureOutsrcHome(home);
101
+ writeJson(updatesFile(home), state);
102
+ return { state, fresh };
103
+ }
104
+ const run = promisify(execFile);
105
+ const TIMEOUT_MS = 20_000;
106
+ const NPM_PACKAGE = { claude: "@anthropic-ai/claude-code", codex: "@openai/codex", outsrc: "outsrc" };
107
+ const PACKAGE_JSON = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
108
+ async function stdout(command, args) {
109
+ return (await run(command, args, { timeout: TIMEOUT_MS, encoding: "utf8" })).stdout;
110
+ }
111
+ /** Version queries only: `<cli> --version`, the npm registry, and `grok update --check`. No model calls. */
112
+ export const defaultProbe = {
113
+ async installed(product) {
114
+ if (product === "outsrc")
115
+ return JSON.parse(readFileSync(PACKAGE_JSON, "utf8")).version;
116
+ return stdout(product, ["--version"]);
117
+ },
118
+ async latest(product) {
119
+ if (product === "grok") {
120
+ const body = JSON.parse(await stdout("grok", ["update", "--check", "--json"]));
121
+ if (typeof body.latestVersion !== "string")
122
+ throw new Error("grok update --check gave no latestVersion");
123
+ return body.latestVersion;
124
+ }
125
+ const name = NPM_PACKAGE[product];
126
+ if (!name)
127
+ throw new Error(`no registry package for ${product}`);
128
+ const response = await fetch(`https://registry.npmjs.org/${name.replace("/", "%2F")}/latest`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
129
+ if (!response.ok)
130
+ throw new Error(`npm registry ${response.status} for ${name}`);
131
+ const body = (await response.json());
132
+ if (typeof body.version !== "string")
133
+ throw new Error(`npm registry gave no version for ${name}`);
134
+ return body.version;
135
+ },
136
+ };
@@ -0,0 +1,105 @@
1
+ import { z } from "zod";
2
+ import type { Adapter } from "./adapters.js";
3
+ export declare const RunUsageSchema: z.ZodObject<{
4
+ tokens_in: z.ZodNullable<z.ZodNumber>;
5
+ tokens_out: z.ZodNullable<z.ZodNumber>;
6
+ estimated_cost_usd: z.ZodNullable<z.ZodNumber>;
7
+ wall_minutes: z.ZodNullable<z.ZodNumber>;
8
+ model: z.ZodNullable<z.ZodString>;
9
+ effort: z.ZodNullable<z.ZodString>;
10
+ }, z.core.$strip>;
11
+ export type RunUsage = z.infer<typeof RunUsageSchema>;
12
+ export declare function emptyUsage(): RunUsage;
13
+ export declare function readAdapterUsage(input: {
14
+ adapter: Adapter;
15
+ stdout: string;
16
+ }): RunUsage;
17
+ export type UsageSample = {
18
+ target: string;
19
+ created_at: string;
20
+ usage: RunUsage;
21
+ };
22
+ export declare function aggregateUsage(samples: UsageSample[], now?: Date): {
23
+ ok: boolean;
24
+ as_of: string;
25
+ today: {
26
+ runs: number;
27
+ tokens_in: {
28
+ total: number | null;
29
+ missing_runs: number;
30
+ };
31
+ tokens_out: {
32
+ total: number | null;
33
+ missing_runs: number;
34
+ };
35
+ estimated_cost_usd: {
36
+ total: number | null;
37
+ missing_runs: number;
38
+ };
39
+ wall_minutes: {
40
+ total: number | null;
41
+ missing_runs: number;
42
+ };
43
+ since: string;
44
+ by_target: {
45
+ runs: number;
46
+ tokens_in: {
47
+ total: number | null;
48
+ missing_runs: number;
49
+ };
50
+ tokens_out: {
51
+ total: number | null;
52
+ missing_runs: number;
53
+ };
54
+ estimated_cost_usd: {
55
+ total: number | null;
56
+ missing_runs: number;
57
+ };
58
+ wall_minutes: {
59
+ total: number | null;
60
+ missing_runs: number;
61
+ };
62
+ target: string;
63
+ }[];
64
+ };
65
+ last_7_days: {
66
+ runs: number;
67
+ tokens_in: {
68
+ total: number | null;
69
+ missing_runs: number;
70
+ };
71
+ tokens_out: {
72
+ total: number | null;
73
+ missing_runs: number;
74
+ };
75
+ estimated_cost_usd: {
76
+ total: number | null;
77
+ missing_runs: number;
78
+ };
79
+ wall_minutes: {
80
+ total: number | null;
81
+ missing_runs: number;
82
+ };
83
+ since: string;
84
+ by_target: {
85
+ runs: number;
86
+ tokens_in: {
87
+ total: number | null;
88
+ missing_runs: number;
89
+ };
90
+ tokens_out: {
91
+ total: number | null;
92
+ missing_runs: number;
93
+ };
94
+ estimated_cost_usd: {
95
+ total: number | null;
96
+ missing_runs: number;
97
+ };
98
+ wall_minutes: {
99
+ total: number | null;
100
+ missing_runs: number;
101
+ };
102
+ target: string;
103
+ }[];
104
+ };
105
+ };
package/dist/usage.js ADDED
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ const Tokens = z.number().int().nonnegative();
3
+ const Amount = z.number().nonnegative();
4
+ export const RunUsageSchema = z.object({
5
+ tokens_in: Tokens.nullable(),
6
+ tokens_out: Tokens.nullable(),
7
+ estimated_cost_usd: Amount.nullable(),
8
+ wall_minutes: Amount.nullable(),
9
+ model: z.string().min(1).nullable(),
10
+ effort: z.string().nullable(),
11
+ });
12
+ export function emptyUsage() {
13
+ return { tokens_in: null, tokens_out: null, estimated_cost_usd: null, wall_minutes: null, model: null, effort: null };
14
+ }
15
+ const Metadata = z.object({
16
+ type: z.string().optional(),
17
+ usage: z.unknown().optional(),
18
+ total_cost_usd: z.unknown().optional(),
19
+ model: z.unknown().optional(),
20
+ modelUsage: z.unknown().optional(),
21
+ });
22
+ function metadata(raw) {
23
+ const parsed = Metadata.safeParse(raw);
24
+ if (!parsed.success)
25
+ return emptyUsage();
26
+ const value = parsed.data;
27
+ const counters = z.object({ input_tokens: z.unknown().optional(), output_tokens: z.unknown().optional() }).safeParse(value.usage).data;
28
+ const models = Object.keys(z.record(z.string(), z.unknown()).safeParse(value.modelUsage).data ?? {});
29
+ return {
30
+ tokens_in: Tokens.safeParse(counters?.input_tokens).data ?? null,
31
+ tokens_out: Tokens.safeParse(counters?.output_tokens).data ?? null,
32
+ estimated_cost_usd: Amount.safeParse(value.total_cost_usd).data ?? null,
33
+ wall_minutes: null,
34
+ model: z.string().min(1).safeParse(value.model).data ??
35
+ z.string().min(1).safeParse(models.length === 1 ? models[0] : undefined).data ?? null,
36
+ effort: null,
37
+ };
38
+ }
39
+ export function readAdapterUsage(input) {
40
+ try {
41
+ if (input.adapter === "custom" || input.adapter === "codex-plugin")
42
+ return emptyUsage();
43
+ if (input.adapter === "codex") {
44
+ const events = input.stdout.split("\n").flatMap((line) => {
45
+ try {
46
+ return [JSON.parse(line)];
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ });
52
+ const completed = events.filter((event) => Metadata.safeParse(event).data?.type === "turn.completed");
53
+ if (completed.length === 0)
54
+ return emptyUsage();
55
+ const usage = completed.map(metadata);
56
+ const sum = (key) => usage.every((item) => item[key] !== null)
57
+ ? usage.reduce((total, item) => total + (item[key] ?? 0), 0) : null;
58
+ return { ...emptyUsage(), tokens_in: sum("tokens_in"), tokens_out: sum("tokens_out") };
59
+ }
60
+ const raw = JSON.parse(input.stdout);
61
+ if (input.adapter === "grok-plugin") {
62
+ const payload = z.object({ result: z.unknown().optional(), grok: z.object({ stdout: z.string().optional() }).optional() }).parse(raw);
63
+ const primary = metadata(payload.result);
64
+ let fallback = emptyUsage();
65
+ try {
66
+ if (payload.grok?.stdout)
67
+ fallback = metadata(JSON.parse(payload.grok.stdout));
68
+ }
69
+ catch { }
70
+ return { ...primary, tokens_in: primary.tokens_in ?? fallback.tokens_in, tokens_out: primary.tokens_out ?? fallback.tokens_out,
71
+ estimated_cost_usd: primary.estimated_cost_usd ?? fallback.estimated_cost_usd, model: primary.model ?? fallback.model };
72
+ }
73
+ const messages = z.array(z.unknown()).safeParse(raw);
74
+ return metadata(messages.success ? messages.data.findLast((item) => Metadata.safeParse(item).data?.type === "result") : raw);
75
+ }
76
+ catch {
77
+ return emptyUsage();
78
+ }
79
+ }
80
+ function totals(samples) {
81
+ function metric(key) {
82
+ const missing = samples.filter((sample) => sample.usage[key] === null).length;
83
+ return { total: missing === 0 ? samples.reduce((sum, sample) => sum + (sample.usage[key] ?? 0), 0) : null, missing_runs: missing };
84
+ }
85
+ return { runs: samples.length, tokens_in: metric("tokens_in"), tokens_out: metric("tokens_out"),
86
+ estimated_cost_usd: metric("estimated_cost_usd"), wall_minutes: metric("wall_minutes") };
87
+ }
88
+ export function aggregateUsage(samples, now = new Date()) {
89
+ const today = new Date(now);
90
+ today.setHours(0, 0, 0, 0);
91
+ const start7d = now.getTime() - 7 * 86400_000;
92
+ function window(start) {
93
+ const selected = samples.filter((sample) => {
94
+ const time = Date.parse(sample.created_at);
95
+ if (!Number.isFinite(time))
96
+ throw new Error("Invalid run created_at in usage records");
97
+ return time >= start && time <= now.getTime();
98
+ });
99
+ const targets = [...new Set(selected.map((sample) => sample.target))].sort();
100
+ return {
101
+ since: new Date(start).toISOString(), ...totals(selected),
102
+ by_target: targets.map((target) => ({ target, ...totals(selected.filter((sample) => sample.target === target)) })),
103
+ };
104
+ }
105
+ return { ok: true, as_of: now.toISOString(), today: window(today.getTime()), last_7_days: window(start7d) };
106
+ }
@@ -0,0 +1,13 @@
1
+ export type Version = {
2
+ major: number;
3
+ minor: number;
4
+ patch: number;
5
+ prerelease: string[];
6
+ text: string;
7
+ };
8
+ /** First x.y.z[-pre] in the text, or null. */
9
+ export declare function parseVersion(text: string): Version | null;
10
+ /** Negative when a is older than b, zero when equal, positive when newer. */
11
+ export declare function compareVersions(a: Version, b: Version): number;
12
+ /** True when both parse and latest is newer than installed. */
13
+ export declare function isBehind(installed: string, latest: string): boolean;
@@ -0,0 +1,45 @@
1
+ // Semver-shaped versions as the CLIs print them: "2.1.285 (Claude Code)", "codex-cli 0.159.2", "grok 1.0.44 (abc) [stable]".
2
+ const VERSION = /(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
3
+ /** First x.y.z[-pre] in the text, or null. */
4
+ export function parseVersion(text) {
5
+ const match = VERSION.exec(text);
6
+ if (!match)
7
+ return null;
8
+ const [whole, major, minor, patch, pre] = match;
9
+ return { major: Number(major), minor: Number(minor), patch: Number(patch), prerelease: pre ? pre.split(".") : [], text: whole };
10
+ }
11
+ function comparePrerelease(a, b) {
12
+ // A release sorts above any of its prereleases.
13
+ if (a.length === 0 || b.length === 0)
14
+ return b.length - a.length;
15
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
16
+ const x = a[i];
17
+ const y = b[i];
18
+ if (x === undefined)
19
+ return -1;
20
+ if (y === undefined)
21
+ return 1;
22
+ if (x === y)
23
+ continue;
24
+ const nx = /^\d+$/.test(x) ? Number(x) : null;
25
+ const ny = /^\d+$/.test(y) ? Number(y) : null;
26
+ if (nx !== null && ny !== null)
27
+ return nx - ny;
28
+ if (nx !== null)
29
+ return -1;
30
+ if (ny !== null)
31
+ return 1;
32
+ return x < y ? -1 : 1;
33
+ }
34
+ return 0;
35
+ }
36
+ /** Negative when a is older than b, zero when equal, positive when newer. */
37
+ export function compareVersions(a, b) {
38
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch || comparePrerelease(a.prerelease, b.prerelease);
39
+ }
40
+ /** True when both parse and latest is newer than installed. */
41
+ export function isBehind(installed, latest) {
42
+ const a = parseVersion(installed);
43
+ const b = parseVersion(latest);
44
+ return a !== null && b !== null && compareVersions(a, b) < 0;
45
+ }
package/dist/wrapper.js CHANGED
@@ -12,6 +12,7 @@ import { engineDir, isPluginAdapter, teardownInvocation } from "./plugins.js";
12
12
  import { writeJson } from "./state.js";
13
13
  import { parseThreadId } from "./types.js";
14
14
  import { collectWorkspaceDiff, commitWorkspace } from "./workspace.js";
15
+ import { emptyUsage, readAdapterUsage } from "./usage.js";
15
16
  const RunSpecSchema = z.object({
16
17
  threadId: z.string().transform(parseThreadId),
17
18
  runId: z.string(),
@@ -38,7 +39,7 @@ function failure(message, sessionId) {
38
39
  return { kind: "failed", message, sessionId, findings: [] };
39
40
  }
40
41
  function execute(input) {
41
- return new Promise((resolveResult, reject) => {
42
+ return new Promise((resolveResult) => {
42
43
  const child = spawn(input.command, input.args, {
43
44
  cwd: input.cwd,
44
45
  env: { ...process.env, ...input.env },
@@ -78,10 +79,8 @@ function execute(input) {
78
79
  return;
79
80
  settled = true;
80
81
  clearTimeout(timer);
81
- if (error !== undefined)
82
- reject(error);
83
- else
84
- resolveResult({ stdout: Buffer.concat(stdout).toString("utf8"), exitCode });
82
+ const output = { stdout: Buffer.concat(stdout).toString("utf8"), exitCode };
83
+ resolveResult(error === undefined ? { ...output, kind: "exited" } : { ...output, kind: "failed", error });
85
84
  }
86
85
  const timer = input.deadlineMs === null ? undefined : setTimeout(() => {
87
86
  timedOut = true;
@@ -94,11 +93,17 @@ function execute(input) {
94
93
  child.stdout.on("data", (chunk) => {
95
94
  stdout.push(chunk);
96
95
  append(chunk);
96
+ try {
97
+ input.onStdout?.(Buffer.concat(stdout).toString("utf8"));
98
+ }
99
+ catch (error) {
100
+ logError = error;
101
+ }
97
102
  });
98
103
  child.stderr.on("data", append);
99
104
  child.once("close", (exitCode) => {
100
105
  if (timedOut)
101
- finish(new Error(`Run exceeded the ${input.deadlineMs}ms deadline`), null);
106
+ finish(new Error(`Run exceeded the ${input.deadlineMs}ms deadline`), exitCode);
102
107
  else if (logError !== undefined)
103
108
  finish(logError, null);
104
109
  else
@@ -107,6 +112,8 @@ function execute(input) {
107
112
  });
108
113
  }
109
114
  export async function run(spec) {
115
+ const startedAt = performance.now();
116
+ let usage = emptyUsage();
110
117
  let output = failure("Agent did not run", spec.sessionId);
111
118
  let exitCode = null;
112
119
  let commit = null;
@@ -124,10 +131,13 @@ export async function run(spec) {
124
131
  return left;
125
132
  }
126
133
  function publish(result) {
134
+ const recorded = { ...result,
135
+ ...(existsSync(join(spec.runDir, "cancelled")) ? { kind: "cancelled", message: "Task cancelled" } : {}),
136
+ usage: { ...usage, wall_minutes: (performance.now() - startedAt) / 60_000 } };
127
137
  const temporary = join(spec.runDir, "result.json.tmp");
128
- writeHomeFile(temporary, JSON.stringify(result));
138
+ writeHomeFile(temporary, JSON.stringify(recorded));
129
139
  renameSync(temporary, join(spec.runDir, "result.json"));
130
- return result;
140
+ return recorded;
131
141
  }
132
142
  mkdirSync(spec.runDir, { recursive: true, mode: 0o700 });
133
143
  try {
@@ -150,6 +160,8 @@ export async function run(spec) {
150
160
  if (!executable)
151
161
  throw new Error("command is empty");
152
162
  const setup = await execute({ command: executable, args, cwd: spec.worktree, logPath, deadlineMs: timeLeft() });
163
+ if (setup.kind === "failed")
164
+ throw setup.error;
153
165
  if (setup.exitCode !== 0) {
154
166
  exitCode = setup.exitCode;
155
167
  throw new Error(`exited with ${setup.exitCode ?? "a signal"}`);
@@ -171,9 +183,41 @@ export async function run(spec) {
171
183
  dataDir,
172
184
  threadId: spec.threadId,
173
185
  });
186
+ const ignoresEffort = (spec.target.adapter === "codex-plugin" && spec.kind !== "task" && spec.base !== null) ||
187
+ (spec.target.adapter === "custom" && !spec.target.args.some((arg) => arg.includes("{effort}")));
188
+ usage.effort = ignoresEffort ? null : spec.effort;
189
+ const ignoresModel = (spec.target.adapter === "codex-plugin" && spec.kind !== "task" && spec.base !== null) ||
190
+ (spec.target.adapter === "custom" && !spec.target.args.some((arg) => arg.includes("{model}")));
191
+ usage.model = ignoresModel ? null : spec.model;
192
+ let savedUsage = JSON.stringify(usage);
193
+ function captureUsage(stdout) {
194
+ if (spec.target.adapter !== "codex") {
195
+ try {
196
+ JSON.parse(stdout);
197
+ }
198
+ catch {
199
+ return;
200
+ }
201
+ }
202
+ const reported = readAdapterUsage({ adapter: spec.target.adapter, stdout });
203
+ usage = { ...reported, model: reported.model ?? usage.model, effort: usage.effort };
204
+ const serialized = JSON.stringify(usage);
205
+ if (serialized === savedUsage)
206
+ return;
207
+ writeJson(join(spec.runDir, "usage.json"), usage);
208
+ savedUsage = serialized;
209
+ }
210
+ writeJson(join(spec.runDir, "usage.json"), usage);
174
211
  let executed;
175
212
  try {
176
- executed = await execute({ ...invocation, cwd: spec.worktree, logPath, deadlineMs: timeLeft() });
213
+ const supportsUsage = spec.target.adapter !== "custom" && spec.target.adapter !== "codex-plugin";
214
+ executed = await execute({ ...invocation, cwd: spec.worktree, logPath, deadlineMs: timeLeft(),
215
+ ...(supportsUsage ? { onStdout: captureUsage } : {}) });
216
+ exitCode = executed.exitCode;
217
+ if (supportsUsage)
218
+ captureUsage(executed.stdout);
219
+ if (executed.kind === "failed")
220
+ throw executed.error;
177
221
  }
178
222
  finally {
179
223
  if (isPluginAdapter(spec.target.adapter)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "outsrc",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local MCP mailbox that lets any agent hand coding tasks to Claude Code, Codex and Grok Build in git worktrees.",
5
5
  "homepage": "https://outsrc.ing",
6
6
  "keywords": [
@@ -37,15 +37,15 @@
37
37
  "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
38
38
  },
39
39
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "^1.25.1",
41
- "smol-toml": "^1.4.2",
40
+ "@modelcontextprotocol/sdk": "^1.30.1",
41
+ "smol-toml": "^1.9.0",
42
42
  "zod": "^4.6.5"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.15.0",
46
46
  "tsx": "^4.20.0",
47
47
  "typescript": "^7.0.2",
48
- "vitest": "^5.0.1"
48
+ "vitest": "^5.0.2"
49
49
  },
50
50
  "license": "MIT",
51
51
  "repository": {