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.
- package/README.md +103 -1
- package/dist/cli.js +128 -7
- package/dist/mailbox.d.ts +106 -1
- package/dist/mailbox.js +98 -33
- package/dist/model-cache.d.ts +29 -0
- package/dist/model-cache.js +62 -0
- package/dist/models.d.ts +10 -0
- package/dist/models.js +108 -0
- package/dist/server.js +7 -2
- package/dist/state.d.ts +24 -0
- package/dist/state.js +10 -0
- package/dist/streams.d.ts +49 -0
- package/dist/streams.js +388 -0
- package/dist/types.d.ts +12 -2
- package/dist/update-check.d.ts +18 -0
- package/dist/update-check.js +84 -0
- package/dist/updates.d.ts +78 -0
- package/dist/updates.js +136 -0
- package/dist/usage.d.ts +105 -0
- package/dist/usage.js +106 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +45 -0
- package/dist/wrapper.js +53 -9
- package/package.json +4 -4
package/dist/updates.js
ADDED
|
@@ -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
|
+
};
|
package/dist/usage.d.ts
ADDED
|
@@ -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;
|
package/dist/version.js
ADDED
|
@@ -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
|
|
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
|
-
|
|
82
|
-
|
|
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`),
|
|
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(
|
|
138
|
+
writeHomeFile(temporary, JSON.stringify(recorded));
|
|
129
139
|
renameSync(temporary, join(spec.runDir, "result.json"));
|
|
130
|
-
return
|
|
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
|
-
|
|
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.
|
|
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.
|
|
41
|
-
"smol-toml": "^1.
|
|
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.
|
|
48
|
+
"vitest": "^5.0.2"
|
|
49
49
|
},
|
|
50
50
|
"license": "MIT",
|
|
51
51
|
"repository": {
|