@dna113p/machines 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -4
- package/claude/statusline.mjs +244 -0
- package/dist/examples/chatgpt-web.d.ts +1 -0
- package/dist/examples/chatgpt-web.js +36 -0
- package/dist/examples/claude.d.ts +1 -0
- package/dist/examples/claude.js +30 -0
- package/dist/examples/codex.d.ts +1 -0
- package/dist/examples/codex.js +30 -0
- package/dist/examples/decision.d.ts +1 -0
- package/dist/examples/decision.js +66 -0
- package/dist/examples/deepseek.d.ts +1 -0
- package/dist/examples/deepseek.js +27 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +72 -10
- package/dist/pi-extension/index.js +16 -9
- package/dist/src/acp.js +2 -1
- package/dist/src/agent-bindings.d.ts +6 -0
- package/dist/src/agent-bindings.js +35 -5
- package/dist/src/agent-limit.d.ts +25 -0
- package/dist/src/agent-limit.js +34 -0
- package/dist/src/agent-limits.d.ts +37 -0
- package/dist/src/agent-limits.js +212 -0
- package/dist/src/agent-protocol.js +3 -1
- package/dist/src/catalog.d.ts +3 -0
- package/dist/src/catalog.js +84 -12
- package/dist/src/chatgpt-web.d.ts +143 -0
- package/dist/src/chatgpt-web.js +946 -0
- package/dist/src/claude.d.ts +35 -0
- package/dist/src/claude.js +294 -0
- package/dist/src/cli.js +194 -16
- package/dist/src/codex.d.ts +16 -0
- package/dist/src/codex.js +176 -0
- package/dist/src/conversation.d.ts +74 -0
- package/dist/src/conversation.js +46 -0
- package/dist/src/coordinator-guidance.d.ts +3 -0
- package/dist/src/coordinator-guidance.js +17 -0
- package/dist/src/decision.d.ts +29 -0
- package/dist/src/decision.js +67 -0
- package/dist/src/deepseek.d.ts +12 -0
- package/dist/src/deepseek.js +16 -0
- package/dist/src/host.d.ts +3 -0
- package/dist/src/host.js +68 -7
- package/dist/src/index.d.ts +39 -2
- package/dist/src/index.js +62 -5
- package/dist/src/jev.d.ts +15 -0
- package/dist/src/jev.js +90 -0
- package/dist/src/json.d.ts +6 -0
- package/dist/src/json.js +28 -0
- package/dist/src/launcher.d.ts +11 -1
- package/dist/src/launcher.js +72 -3
- package/dist/src/laya.d.ts +15 -0
- package/dist/src/laya.js +13 -0
- package/dist/src/openrouter.d.ts +16 -0
- package/dist/src/openrouter.js +97 -0
- package/dist/src/run-inbox.d.ts +49 -0
- package/dist/src/run-inbox.js +229 -0
- package/dist/src/run-status.d.ts +97 -0
- package/dist/src/run-status.js +285 -0
- package/dist/src/session.d.ts +13 -0
- package/dist/src/session.js +41 -3
- package/dist/src/system-one-http.d.ts +25 -0
- package/dist/src/system-one-http.js +146 -0
- package/dist/src/terminal-human.d.ts +8 -1
- package/dist/src/terminal-human.js +36 -8
- package/dist/src/von.d.ts +15 -0
- package/dist/src/von.js +13 -0
- package/docs/architecture.md +67 -2
- package/docs/authoring.md +809 -1
- package/docs/integrations.md +243 -3
- package/docs/local-decisions.md +212 -0
- package/package.json +21 -4
- package/services/laya/server.py +230 -0
- package/skills/machine-builder/SKILL.md +56 -6
- package/skills/machine-delegation/SKILL.md +123 -0
- package/skills/machine-delegation/references/delegation-machine.md +90 -0
- package/skills/machine-project-setup/SKILL.md +181 -0
- package/skills/machine-project-setup/references/project-workflows.md +50 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentRunner } from "./index.ts";
|
|
2
|
+
export interface ClaudeAgentOptions {
|
|
3
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
4
|
+
readonly harness?: string;
|
|
5
|
+
/** A Claude Code model alias (for example "opus", "sonnet", "haiku") or a full model name. */
|
|
6
|
+
readonly model?: string;
|
|
7
|
+
/** An effort level supported by the installed Claude Code and selected model. */
|
|
8
|
+
readonly effort?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Defaults to "dontAsk": tools run only when Claude Code's own permission
|
|
11
|
+
* rules already allow them, and anything that would prompt is denied.
|
|
12
|
+
*/
|
|
13
|
+
readonly permissionMode?: "acceptEdits" | "auto" | "bypassPermissions" | "dontAsk" | "manual" | "plan";
|
|
14
|
+
/** Restricts the built-in tool set, for example ["Read"]. */
|
|
15
|
+
readonly tools?: readonly string[];
|
|
16
|
+
/** Permission rules to allow in addition to Claude Code's settings, for example ["Bash(npm test)"]. */
|
|
17
|
+
readonly allowedTools?: readonly string[];
|
|
18
|
+
/** Tools or permission rules to deny regardless of Claude Code's settings. */
|
|
19
|
+
readonly disallowedTools?: readonly string[];
|
|
20
|
+
/** Defaults to false, so each invocation does not save a resumable Claude Code session. */
|
|
21
|
+
readonly sessionPersistence?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Defaults to "limit": once the plan limit is used up and Claude Code starts
|
|
24
|
+
* spending paid extra usage, the turn stops with an `AgentLimitError`.
|
|
25
|
+
* "allow" lets the turn continue on extra usage.
|
|
26
|
+
*/
|
|
27
|
+
readonly extraUsage?: "limit" | "allow";
|
|
28
|
+
readonly output?: "capture" | "stream";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Runs one fresh, non-interactive Claude Code turn using the installed CLI and
|
|
32
|
+
* its existing login. Authentication, settings, and permission rules stay in
|
|
33
|
+
* Claude Code; this runner never reads or forwards credentials.
|
|
34
|
+
*/
|
|
35
|
+
export declare function claudeAgent(command?: string, args?: readonly string[], options?: ClaudeAgentOptions): AgentRunner;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { AgentLimitError } from "./agent-limit.js";
|
|
5
|
+
import { agentPrompt, readAgentEvent } from "./agent-protocol.js";
|
|
6
|
+
/**
|
|
7
|
+
* Runs one fresh, non-interactive Claude Code turn using the installed CLI and
|
|
8
|
+
* its existing login. Authentication, settings, and permission rules stay in
|
|
9
|
+
* Claude Code; this runner never reads or forwards credentials.
|
|
10
|
+
*/
|
|
11
|
+
export function claudeAgent(command = "claude", args = [], options = {}) {
|
|
12
|
+
return async (request, report) => {
|
|
13
|
+
if (request.cwd === undefined)
|
|
14
|
+
throw new Error("Claude Agent requires a working directory");
|
|
15
|
+
if (request.outcomes.length === 0)
|
|
16
|
+
throw new Error("Claude Agent requires at least one allowed outcome");
|
|
17
|
+
const harness = options.harness ?? command;
|
|
18
|
+
const env = { ...process.env, ...options.env };
|
|
19
|
+
// Claude Code exports its own CLAUDE_* variables (including CLAUDE_EFFORT) to
|
|
20
|
+
// child processes, so runner settings use names it does not own.
|
|
21
|
+
const model = options.model ?? env.MACHINES_CLAUDE_MODEL ?? env.MACHINES_AGENT_MODEL;
|
|
22
|
+
const effort = options.effort ?? env.MACHINES_CLAUDE_EFFORT ?? env.MACHINES_AGENT_EFFORT;
|
|
23
|
+
const outputMode = options.output ?? "stream";
|
|
24
|
+
report?.({
|
|
25
|
+
type: "identity",
|
|
26
|
+
harness,
|
|
27
|
+
...(model === undefined ? {} : { model }),
|
|
28
|
+
...(effort === undefined ? {} : { thinking: effort }),
|
|
29
|
+
});
|
|
30
|
+
let stderr = "";
|
|
31
|
+
try {
|
|
32
|
+
const child = spawn(command, [
|
|
33
|
+
...args,
|
|
34
|
+
"--print", "--output-format", "stream-json", "--verbose",
|
|
35
|
+
"--permission-mode", options.permissionMode ?? "dontAsk",
|
|
36
|
+
...(options.sessionPersistence === true ? [] : ["--no-session-persistence"]),
|
|
37
|
+
...(model === undefined ? [] : ["--model", model]),
|
|
38
|
+
...(effort === undefined ? [] : ["--effort", effort]),
|
|
39
|
+
...(options.tools === undefined ? [] : ["--tools", options.tools.join(",")]),
|
|
40
|
+
...(options.allowedTools === undefined ? [] : ["--allowed-tools", options.allowedTools.join(",")]),
|
|
41
|
+
...(options.disallowedTools === undefined ? [] : ["--disallowed-tools", options.disallowedTools.join(",")]),
|
|
42
|
+
], { cwd: request.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
43
|
+
const exited = new Promise((resolve, reject) => {
|
|
44
|
+
child.once("error", reject);
|
|
45
|
+
child.stdin.once("error", reject);
|
|
46
|
+
child.once("close", (code, signal) => {
|
|
47
|
+
if (code === 0)
|
|
48
|
+
resolve();
|
|
49
|
+
else
|
|
50
|
+
reject(new Error(signal === null
|
|
51
|
+
? `Process exited with code ${code}`
|
|
52
|
+
: `Process terminated by ${signal}`));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// Attach handlers before reading stdout: errors can arrive while it drains.
|
|
56
|
+
void exited.catch(() => { });
|
|
57
|
+
child.stderr.setEncoding("utf8");
|
|
58
|
+
child.stderr.on("data", (chunk) => {
|
|
59
|
+
stderr += chunk;
|
|
60
|
+
if (outputMode === "stream")
|
|
61
|
+
process.stderr.write(chunk);
|
|
62
|
+
});
|
|
63
|
+
const lines = createInterface({ input: child.stdout });
|
|
64
|
+
const toolTitles = new Map();
|
|
65
|
+
let result;
|
|
66
|
+
// The CLI reports a limit beside its messages: on the last top-level
|
|
67
|
+
// assistant record and in the latest rate-limit information.
|
|
68
|
+
let assistantError = {};
|
|
69
|
+
let rateLimit;
|
|
70
|
+
// The CLI streams one record per content block, and its final result
|
|
71
|
+
// joins the text blocks of the closing message.
|
|
72
|
+
let closingMessage;
|
|
73
|
+
let closingText = "";
|
|
74
|
+
const emitMessage = (text) => {
|
|
75
|
+
if (text === "")
|
|
76
|
+
return;
|
|
77
|
+
report?.({ type: "output", text: `${text}${text.endsWith("\n") ? "" : "\n"}` });
|
|
78
|
+
if (outputMode === "stream")
|
|
79
|
+
process.stdout.write(`${text}${text.endsWith("\n") ? "" : "\n"}`);
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
child.stdin.end(agentPrompt(request));
|
|
83
|
+
for await (const line of lines) {
|
|
84
|
+
const record = decodeRecord(line);
|
|
85
|
+
if (record === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
if (record.type === "malformed-result") {
|
|
88
|
+
// An unreadable final result must not leave an earlier one in effect.
|
|
89
|
+
result = { failed: true, text: "Claude returned a malformed final result" };
|
|
90
|
+
}
|
|
91
|
+
else if (record.type === "system") {
|
|
92
|
+
// The CLI resolves aliases and defaults; report what actually runs.
|
|
93
|
+
report?.({
|
|
94
|
+
type: "identity",
|
|
95
|
+
harness,
|
|
96
|
+
model: record.model,
|
|
97
|
+
...(effort === undefined ? {} : { thinking: effort }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (record.type === "rate_limit_event") {
|
|
101
|
+
const info = record.rate_limit_info;
|
|
102
|
+
if (info?.isUsingOverage === true && options.extraUsage !== "allow") {
|
|
103
|
+
// The plan limit is already used up; continuing would spend paid credits.
|
|
104
|
+
throw new AgentLimitError("Claude plan limit reached; extra usage is not used", agentLimit(harness, info));
|
|
105
|
+
}
|
|
106
|
+
// Information without a recognised status does not replace what is known.
|
|
107
|
+
if (info?.status !== undefined)
|
|
108
|
+
rateLimit = info;
|
|
109
|
+
}
|
|
110
|
+
else if (record.type === "result") {
|
|
111
|
+
result = {
|
|
112
|
+
failed: record.is_error === true || record.subtype !== "success",
|
|
113
|
+
text: record.result ?? record.errors?.join("\n") ?? record.subtype,
|
|
114
|
+
...(record.is_error === true
|
|
115
|
+
? { apiError: { name: record.api_error, rateLimited: record.api_error_status === 429 } }
|
|
116
|
+
: {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
else if (record.parent_tool_use_id !== undefined && record.parent_tool_use_id !== null) {
|
|
120
|
+
// Subagent activity belongs to the parent's tool call, not the final answer.
|
|
121
|
+
}
|
|
122
|
+
else if (record.type === "assistant") {
|
|
123
|
+
assistantError = { name: record.api_error, rateLimited: record.error === "rate_limit" };
|
|
124
|
+
const info = record.api_error_params?.rate_limit_info;
|
|
125
|
+
if (info?.status !== undefined)
|
|
126
|
+
rateLimit = info;
|
|
127
|
+
const message = record.message.id ?? undefined;
|
|
128
|
+
if (message === undefined || message !== closingMessage)
|
|
129
|
+
closingText = "";
|
|
130
|
+
closingMessage = message;
|
|
131
|
+
for (const block of record.message.content) {
|
|
132
|
+
if (block.type === "text") {
|
|
133
|
+
closingText += block.text;
|
|
134
|
+
emitMessage(block.text);
|
|
135
|
+
}
|
|
136
|
+
else if (block.type === "tool_use") {
|
|
137
|
+
toolTitles.set(block.id, block.name);
|
|
138
|
+
report?.({ type: "tool", id: block.id, title: block.name, status: "in_progress" });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (typeof record.message.content !== "string") {
|
|
143
|
+
for (const block of record.message.content) {
|
|
144
|
+
if (block.type !== "tool_result")
|
|
145
|
+
continue;
|
|
146
|
+
const title = toolTitles.get(block.tool_use_id);
|
|
147
|
+
report?.({
|
|
148
|
+
type: "tool",
|
|
149
|
+
id: block.tool_use_id,
|
|
150
|
+
...(title === undefined ? {} : { title }),
|
|
151
|
+
status: block.is_error === true ? "failed" : "completed",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Only a final result that ended on an API error can be a limit. The latest
|
|
157
|
+
// rate-limit information names it and its reset, whatever its own status.
|
|
158
|
+
const limit = result?.apiError !== undefined
|
|
159
|
+
&& reachedLimit(result.apiError, assistantError, rateLimit, result.text)
|
|
160
|
+
? agentLimit(harness, rateLimit)
|
|
161
|
+
: undefined;
|
|
162
|
+
try {
|
|
163
|
+
await exited;
|
|
164
|
+
}
|
|
165
|
+
catch (cause) {
|
|
166
|
+
// An empty result adds nothing to the exit, but a limit it carries is still reported.
|
|
167
|
+
if (result === undefined || (result.text === "" && limit === undefined))
|
|
168
|
+
throw cause;
|
|
169
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
170
|
+
throw turnFailure(result.text === "" ? message : `${message}: ${result.text}`, limit, { cause });
|
|
171
|
+
}
|
|
172
|
+
if (result === undefined)
|
|
173
|
+
throw new Error("Claude exited without a final result");
|
|
174
|
+
if (result.failed)
|
|
175
|
+
throw turnFailure(result.text, limit);
|
|
176
|
+
// Earlier messages may contain progress or a preliminary event. Only the
|
|
177
|
+
// CLI's final result determines the outcome, never accumulated activity.
|
|
178
|
+
if (withoutSpace(result.text) !== withoutSpace(closingText))
|
|
179
|
+
emitMessage(result.text);
|
|
180
|
+
return readAgentEvent(result.text, { adapter: "Claude", includeMessage: outputMode === "capture" });
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
lines.close();
|
|
184
|
+
child.kill();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (cause) {
|
|
188
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
189
|
+
const detail = stderr.trim();
|
|
190
|
+
const text = `Claude Agent "${command}" failed in "${request.cwd}": ${message}${detail === "" ? "" : `\n${detail}`}`;
|
|
191
|
+
throw cause instanceof AgentLimitError
|
|
192
|
+
? new AgentLimitError(text, cause.limit, { cause })
|
|
193
|
+
: new Error(text, { cause });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const planLimitErrors = new Set(["usage_limit_reached", "model_requires_usage_credits"]);
|
|
198
|
+
const limitText = /^(?:You've (?:hit|reached) your |You're out of (?:usage credits|extra usage))/u;
|
|
199
|
+
const maximumResetDelay = 8 * 24 * 60 * 60 * 1000;
|
|
200
|
+
// A 429 alone may be a temporary throttle; a plan limit also names itself, is
|
|
201
|
+
// rejected by the rate-limit information, or uses Claude Code's limit wording.
|
|
202
|
+
function reachedLimit(result, assistant, rateLimit, text) {
|
|
203
|
+
if ([result.name, assistant.name].some((name) => name !== undefined && planLimitErrors.has(name)))
|
|
204
|
+
return true;
|
|
205
|
+
if (result.rateLimited !== true && assistant.rateLimited !== true)
|
|
206
|
+
return false;
|
|
207
|
+
return (rateLimit?.status === "rejected" && rateLimit.isUsingOverage !== true) || limitText.test(text);
|
|
208
|
+
}
|
|
209
|
+
function agentLimit(harness, info) {
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
const resetsAt = info?.resetsAt === undefined ? undefined : info.resetsAt * 1000;
|
|
212
|
+
return {
|
|
213
|
+
harness,
|
|
214
|
+
...(info?.rateLimitType === undefined ? {} : { kind: info.rateLimitType }),
|
|
215
|
+
// A reset time that is past or implausibly distant is not reported.
|
|
216
|
+
...(resetsAt !== undefined && Number.isFinite(resetsAt) && resetsAt > now && resetsAt <= now + maximumResetDelay
|
|
217
|
+
? { resetsAt }
|
|
218
|
+
: {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function turnFailure(message, limit, options) {
|
|
222
|
+
return limit === undefined ? new Error(message, options) : new AgentLimitError(message, limit, options);
|
|
223
|
+
}
|
|
224
|
+
const parentSchema = v.nullish(v.string());
|
|
225
|
+
// Unlisted block types (thinking, images, future additions) are skipped, not rejected.
|
|
226
|
+
const otherBlockSchema = v.pipe(v.object({ type: v.string() }), v.transform(() => ({ type: "other" })));
|
|
227
|
+
const assistantBlockSchema = v.union([
|
|
228
|
+
v.object({ type: v.literal("text"), text: v.string() }),
|
|
229
|
+
v.object({ type: v.literal("tool_use"), id: v.string(), name: v.string() }),
|
|
230
|
+
otherBlockSchema,
|
|
231
|
+
]);
|
|
232
|
+
const userBlockSchema = v.union([
|
|
233
|
+
v.object({ type: v.literal("tool_result"), tool_use_id: v.string(), is_error: v.nullish(v.boolean()) }),
|
|
234
|
+
otherBlockSchema,
|
|
235
|
+
]);
|
|
236
|
+
// The limit fields were read from the Claude Code bundle, not observed at a real
|
|
237
|
+
// limit, so a malformed one is dropped without rejecting the record that carries it.
|
|
238
|
+
// A value outside the listed ones counts as malformed.
|
|
239
|
+
const tolerant = (schema) => v.fallback(v.optional(schema), undefined);
|
|
240
|
+
const rateLimitInfoSchema = v.object({
|
|
241
|
+
status: tolerant(v.picklist(["allowed", "allowed_warning", "rejected"])),
|
|
242
|
+
// Whole Unix seconds.
|
|
243
|
+
resetsAt: tolerant(v.pipe(v.number(), v.safeInteger())),
|
|
244
|
+
rateLimitType: tolerant(v.picklist([
|
|
245
|
+
"five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet", "seven_day_overage_included", "overage",
|
|
246
|
+
])),
|
|
247
|
+
isUsingOverage: tolerant(v.boolean()),
|
|
248
|
+
});
|
|
249
|
+
const recordSchema = v.variant("type", [
|
|
250
|
+
v.object({ type: v.literal("system"), subtype: v.literal("init"), model: v.string() }),
|
|
251
|
+
v.object({ type: v.literal("rate_limit_event"), rate_limit_info: tolerant(rateLimitInfoSchema) }),
|
|
252
|
+
v.object({
|
|
253
|
+
type: v.literal("assistant"),
|
|
254
|
+
parent_tool_use_id: parentSchema,
|
|
255
|
+
message: v.object({ id: v.nullish(v.string()), content: v.array(assistantBlockSchema) }),
|
|
256
|
+
error: tolerant(v.string()),
|
|
257
|
+
api_error: tolerant(v.string()),
|
|
258
|
+
api_error_params: tolerant(v.object({ rate_limit_info: tolerant(rateLimitInfoSchema) })),
|
|
259
|
+
}),
|
|
260
|
+
v.object({
|
|
261
|
+
type: v.literal("user"),
|
|
262
|
+
parent_tool_use_id: parentSchema,
|
|
263
|
+
message: v.object({ content: v.union([v.string(), v.array(userBlockSchema)]) }),
|
|
264
|
+
}),
|
|
265
|
+
v.object({
|
|
266
|
+
type: v.literal("result"),
|
|
267
|
+
subtype: v.string(),
|
|
268
|
+
is_error: v.nullish(v.boolean()),
|
|
269
|
+
result: v.nullish(v.string()),
|
|
270
|
+
errors: v.nullish(v.array(v.string())),
|
|
271
|
+
api_error: tolerant(v.string()),
|
|
272
|
+
api_error_status: tolerant(v.number()),
|
|
273
|
+
}),
|
|
274
|
+
]);
|
|
275
|
+
// Ignore unsupported events and malformed records without exposing raw protocol data.
|
|
276
|
+
function decodeRecord(line) {
|
|
277
|
+
let value;
|
|
278
|
+
try {
|
|
279
|
+
value = JSON.parse(line);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const parsed = v.safeParse(recordSchema, value);
|
|
285
|
+
if (parsed.success)
|
|
286
|
+
return parsed.output;
|
|
287
|
+
return value !== null && typeof value === "object" && "type" in value && value.type === "result"
|
|
288
|
+
? { type: "malformed-result" }
|
|
289
|
+
: undefined;
|
|
290
|
+
}
|
|
291
|
+
// Block boundaries differ between streamed messages and the joined result.
|
|
292
|
+
function withoutSpace(text) {
|
|
293
|
+
return text.replace(/\s+/gu, "");
|
|
294
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
4
5
|
import { emitKeypressEvents } from "node:readline";
|
|
5
|
-
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
import { inspect, stripVTControlCharacters } from "node:util";
|
|
6
7
|
import yoctoSpinner from "yocto-spinner";
|
|
7
8
|
import { bold, cyan, dim, green, magenta, red, yellow } from "yoctocolors";
|
|
9
|
+
import { assertJsonValue, isJsonValue } from "./json.js";
|
|
8
10
|
import { discoverMachines, machinesUserHome } from "./discovery.js";
|
|
9
11
|
import { listAgentPresets, listMachines, prepareMachineRun, } from "./launcher.js";
|
|
12
|
+
import { inboxHuman, respondToRun } from "./run-inbox.js";
|
|
13
|
+
import { createRunStatusPublisher, isProcessAlive, readRunStatuses, runStatusDirectory, } from "./run-status.js";
|
|
10
14
|
const [command, selector, ...argumentsAfterSelector] = process.argv.slice(2);
|
|
11
15
|
try {
|
|
12
16
|
if (command === "list" && selector === undefined) {
|
|
@@ -35,13 +39,21 @@ try {
|
|
|
35
39
|
else if (command === "run" && selector !== undefined) {
|
|
36
40
|
await runMachine(await resolveMachine(selector), argumentsAfterSelector);
|
|
37
41
|
}
|
|
42
|
+
else if (command === "runs" && (selector === undefined || selector === "--json") && argumentsAfterSelector.length === 0) {
|
|
43
|
+
listRuns(selector === "--json");
|
|
44
|
+
}
|
|
45
|
+
else if (command === "respond" && selector !== undefined) {
|
|
46
|
+
await respond([selector, ...argumentsAfterSelector]);
|
|
47
|
+
}
|
|
38
48
|
else {
|
|
39
49
|
throw new Error([
|
|
40
50
|
"Usage:",
|
|
41
51
|
" machine list",
|
|
42
52
|
" machine agents",
|
|
43
53
|
" machine show <name-or-file>",
|
|
44
|
-
" machine run <name-or-file> [--agent role=preset]... [--] [input]",
|
|
54
|
+
" machine run <name-or-file> [--agent role=preset]... [--input-file <file|->] [--] [input]",
|
|
55
|
+
" machine runs [--json]",
|
|
56
|
+
" machine respond [--question] <run-id> <request-id> [--] <response>",
|
|
45
57
|
].join("\n"));
|
|
46
58
|
}
|
|
47
59
|
}
|
|
@@ -61,7 +73,7 @@ async function resolveMachine(selector) {
|
|
|
61
73
|
return found.path;
|
|
62
74
|
}
|
|
63
75
|
async function runMachine(machinePath, args) {
|
|
64
|
-
const invocation = parseRunArguments(args);
|
|
76
|
+
const invocation = await parseRunArguments(args);
|
|
65
77
|
printMachineHeader(machinePath);
|
|
66
78
|
const prepared = await prepareMachineRun({
|
|
67
79
|
cwd: process.cwd(),
|
|
@@ -70,16 +82,78 @@ async function runMachine(machinePath, args) {
|
|
|
70
82
|
agents: Object.fromEntries(invocation.agentOverrides),
|
|
71
83
|
});
|
|
72
84
|
const status = createRunStatus(machinePath);
|
|
85
|
+
const runId = randomUUID();
|
|
86
|
+
const published = createRunStatusPublisher({ id: runId, machine: prepared.name, path: prepared.path, cwd: process.cwd() });
|
|
87
|
+
const statusDirectory = runStatusDirectory();
|
|
88
|
+
// Machines started by this run's Operations and Agents are its sub-runs.
|
|
89
|
+
process.env.MACHINES_RUN_PARENT = runId;
|
|
90
|
+
let publishedState;
|
|
91
|
+
// A runner reports its identity as it starts. For a state entered by a
|
|
92
|
+
// transition that is before the state is announced, so an identity is held
|
|
93
|
+
// for the rest of the tick and published with the state it belongs to.
|
|
94
|
+
let arriving;
|
|
73
95
|
try {
|
|
74
96
|
const result = await prepared.start({
|
|
75
|
-
onAgentUpdate
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
onAgentUpdate(update) {
|
|
98
|
+
if (update.type === "identity") {
|
|
99
|
+
arriving = update;
|
|
100
|
+
queueMicrotask(() => {
|
|
101
|
+
if (arriving !== update)
|
|
102
|
+
return;
|
|
103
|
+
arriving = undefined;
|
|
104
|
+
published.update({ agent: update });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else if (update.type === "limit" && update.status !== "swapped") {
|
|
108
|
+
// A swap while every preset is being tried again leaves the wait as it is published.
|
|
109
|
+
published.update({
|
|
110
|
+
limit: update.status === "resumed"
|
|
111
|
+
? undefined
|
|
112
|
+
: { text: update.text, ...(update.until === undefined ? {} : { until: update.until }) },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
status.onAgentUpdate(update);
|
|
116
|
+
},
|
|
117
|
+
// A published run can also be answered through its inbox by `machine respond`.
|
|
118
|
+
...(statusDirectory === undefined ? {} : {
|
|
119
|
+
human: inboxHuman({
|
|
120
|
+
directory: statusDirectory,
|
|
121
|
+
runId,
|
|
122
|
+
publish: (request) => published.update({ status: "waiting", human: request }),
|
|
123
|
+
acknowledge: (delivery) => published.update({ delivery }),
|
|
124
|
+
onTerminalRefused: (reason) => console.error(reason),
|
|
125
|
+
onTerminalClosed(requestId) {
|
|
126
|
+
// Finish the unanswered prompt line before saying where the answer can come from.
|
|
127
|
+
process.stdout.write("\n");
|
|
128
|
+
console.error(`No terminal input. Answer with: machine respond ${runId} ${requestId} <response>`);
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
}),
|
|
132
|
+
onHumanInput(active) {
|
|
133
|
+
published.update(active ? { status: "waiting" } : { status: "running", human: undefined });
|
|
134
|
+
status.onHumanInput(active);
|
|
135
|
+
},
|
|
136
|
+
onState(state) {
|
|
137
|
+
const entered = formatState(state);
|
|
138
|
+
if (entered === publishedState)
|
|
139
|
+
published.update({ state });
|
|
140
|
+
else {
|
|
141
|
+
published.update({ state, agent: arriving });
|
|
142
|
+
arriving = undefined;
|
|
143
|
+
}
|
|
144
|
+
publishedState = entered;
|
|
145
|
+
status.onState(state);
|
|
146
|
+
},
|
|
78
147
|
});
|
|
148
|
+
published.finish({ state: result.value });
|
|
79
149
|
status.succeed();
|
|
80
150
|
console.log(`--> ${String(result.value)}`);
|
|
151
|
+
if (result.output !== undefined) {
|
|
152
|
+
console.log(isJsonValue(result.output) ? JSON.stringify(result.output, null, 2) : inspect(result.output));
|
|
153
|
+
}
|
|
81
154
|
}
|
|
82
155
|
catch (cause) {
|
|
156
|
+
published.finish({ error: cause instanceof Error ? cause.message : String(cause) });
|
|
83
157
|
status.fail();
|
|
84
158
|
throw cause;
|
|
85
159
|
}
|
|
@@ -87,7 +161,46 @@ async function runMachine(machinePath, args) {
|
|
|
87
161
|
status.close();
|
|
88
162
|
}
|
|
89
163
|
}
|
|
90
|
-
function
|
|
164
|
+
function listRuns(json) {
|
|
165
|
+
const runs = readRunStatuses(requireStatusDirectory())
|
|
166
|
+
.filter((record) => (record.status === "running" || record.status === "waiting") && isProcessAlive(record.pid))
|
|
167
|
+
.sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt));
|
|
168
|
+
if (json) {
|
|
169
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (runs.length === 0)
|
|
173
|
+
console.log("No Machine runs are running or waiting.");
|
|
174
|
+
runs.forEach((record, index) => {
|
|
175
|
+
printRunListing(record, index > 0);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function respond(args) {
|
|
179
|
+
const question = args[0] === "--question";
|
|
180
|
+
const [runId, requestId, ...words] = question ? args.slice(1) : args;
|
|
181
|
+
if (words[0] === "--")
|
|
182
|
+
words.shift();
|
|
183
|
+
if (runId === undefined || requestId === undefined || words.length === 0) {
|
|
184
|
+
throw new Error("Usage: machine respond [--question] <run-id> <request-id> [--] <response>");
|
|
185
|
+
}
|
|
186
|
+
const text = words.join(" ");
|
|
187
|
+
await respondToRun({
|
|
188
|
+
directory: requireStatusDirectory(),
|
|
189
|
+
runId,
|
|
190
|
+
requestId,
|
|
191
|
+
response: question ? { type: "question", text } : text,
|
|
192
|
+
});
|
|
193
|
+
console.log(`Machine run ${runId} took the ${question ? "question" : "response"}.`);
|
|
194
|
+
}
|
|
195
|
+
function requireStatusDirectory() {
|
|
196
|
+
const directory = runStatusDirectory();
|
|
197
|
+
if (directory === undefined) {
|
|
198
|
+
throw new Error("Run status publication is off: set MACHINES_RUN_STATUS_DIR to the directory the runs publish to.");
|
|
199
|
+
}
|
|
200
|
+
return directory;
|
|
201
|
+
}
|
|
202
|
+
async function parseRunArguments(args) {
|
|
203
|
+
let inputFile;
|
|
91
204
|
const agentOverrides = new Map();
|
|
92
205
|
let index = 0;
|
|
93
206
|
while (index < args.length) {
|
|
@@ -96,6 +209,13 @@ function parseRunArguments(args) {
|
|
|
96
209
|
index += 1;
|
|
97
210
|
break;
|
|
98
211
|
}
|
|
212
|
+
if (argument === "--input-file") {
|
|
213
|
+
if (inputFile !== undefined || args[index + 1] === undefined)
|
|
214
|
+
throw new Error("Expected one --input-file <file|->");
|
|
215
|
+
inputFile = args[index + 1];
|
|
216
|
+
index += 2;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
99
219
|
if (argument !== "--agent")
|
|
100
220
|
break;
|
|
101
221
|
const assignment = args[index + 1];
|
|
@@ -117,10 +237,24 @@ function parseRunArguments(args) {
|
|
|
117
237
|
agentOverrides.set(role, preset);
|
|
118
238
|
index += 2;
|
|
119
239
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
240
|
+
let input = args.slice(index).join(" ");
|
|
241
|
+
if (inputFile !== undefined) {
|
|
242
|
+
if (args.length > index)
|
|
243
|
+
throw new Error("Use positional input or --input-file, not both");
|
|
244
|
+
let text;
|
|
245
|
+
if (inputFile === "-") {
|
|
246
|
+
const chunks = [];
|
|
247
|
+
for await (const chunk of process.stdin)
|
|
248
|
+
chunks.push(Buffer.from(chunk));
|
|
249
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
text = await readFile(inputFile, "utf8");
|
|
253
|
+
const parsed = JSON.parse(text);
|
|
254
|
+
assertJsonValue(parsed, "Machine input");
|
|
255
|
+
input = parsed;
|
|
256
|
+
}
|
|
257
|
+
return { agentOverrides, input };
|
|
124
258
|
}
|
|
125
259
|
function printMachineListing(found, separate) {
|
|
126
260
|
if (!process.stdout.isTTY) {
|
|
@@ -153,6 +287,7 @@ function printAgentListing(preset, separate) {
|
|
|
153
287
|
preset.model ?? "",
|
|
154
288
|
preset.thinking ?? "",
|
|
155
289
|
preset.source,
|
|
290
|
+
preset.fallback?.join(", ") ?? "",
|
|
156
291
|
].join("\t"));
|
|
157
292
|
return;
|
|
158
293
|
}
|
|
@@ -162,8 +297,35 @@ function printAgentListing(preset, separate) {
|
|
|
162
297
|
console.log(` ${preset.description}`);
|
|
163
298
|
if (identity.length > 0)
|
|
164
299
|
console.log(` ${magenta(identity.join(" · "))}`);
|
|
300
|
+
if (preset.fallback !== undefined)
|
|
301
|
+
console.log(` fallback: ${preset.fallback.join(", ") || "none"}`);
|
|
165
302
|
console.log(` ${dim(preset.source)}`);
|
|
166
303
|
}
|
|
304
|
+
function printRunListing(record, separate) {
|
|
305
|
+
const state = record.state ?? "starting";
|
|
306
|
+
const human = record.status === "waiting" ? record.human : undefined;
|
|
307
|
+
// Recorded text must not be able to move the cursor or restyle the terminal.
|
|
308
|
+
const plain = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").trim();
|
|
309
|
+
const details = human === undefined ? (record.limit === undefined ? [] : [plain(record.limit.text)]) : [
|
|
310
|
+
...human.prompt.split("\n").map(plain).filter((line) => line !== ""),
|
|
311
|
+
...(human.requestId === undefined ? [] : [`request: ${plain(human.requestId)}`]),
|
|
312
|
+
...(human.choices === undefined ? [] : [`choices: ${human.choices.map(plain).join(", ")}`]),
|
|
313
|
+
...(human.suggestions === undefined ? [] : [`suggestions: ${human.suggestions.map(plain).join(", ")}`]),
|
|
314
|
+
...(human.discussion === true ? ["discussion: questions allowed"] : []),
|
|
315
|
+
];
|
|
316
|
+
if (!process.stdout.isTTY) {
|
|
317
|
+
console.log([record.id, record.machine, record.status, state].map(plain).join("\t"));
|
|
318
|
+
for (const line of details)
|
|
319
|
+
console.log(` ${line}`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (separate)
|
|
323
|
+
console.log();
|
|
324
|
+
const status = record.status === "waiting" ? yellow("waiting") : cyan("running");
|
|
325
|
+
console.log(`${bold(plain(record.id))} ${bold(cyan(plain(record.machine)))} › ${status} · ${plain(state)}`);
|
|
326
|
+
for (const line of details)
|
|
327
|
+
console.log(` ${line.startsWith("request: ") ? dim(line) : line}`);
|
|
328
|
+
}
|
|
167
329
|
function printMachineHeader(machinePath) {
|
|
168
330
|
if (!process.stdout.isTTY) {
|
|
169
331
|
console.log(`Machine: ${machinePath}`);
|
|
@@ -179,7 +341,10 @@ function createRunStatus(machinePath) {
|
|
|
179
341
|
onState(state) {
|
|
180
342
|
console.error(`● ${name} · ${formatState(state)}`);
|
|
181
343
|
},
|
|
182
|
-
onAgentUpdate() {
|
|
344
|
+
onAgentUpdate(update) {
|
|
345
|
+
if (update.type === "limit")
|
|
346
|
+
console.error(update.text.replace(/\s+/gu, " "));
|
|
347
|
+
},
|
|
183
348
|
onHumanInput() { },
|
|
184
349
|
succeed() { },
|
|
185
350
|
fail() { },
|
|
@@ -192,6 +357,8 @@ function createRunStatus(machinePath) {
|
|
|
192
357
|
let activityExpanded = false;
|
|
193
358
|
let agentOutput = "";
|
|
194
359
|
let agentIdentity = {};
|
|
360
|
+
let agentLimit = "";
|
|
361
|
+
let agentWaiting = false;
|
|
195
362
|
let refresh;
|
|
196
363
|
const tools = new Map();
|
|
197
364
|
const hotkeysAvailable = Boolean(process.stdin.isTTY && process.stdin.setRawMode);
|
|
@@ -211,12 +378,14 @@ function createRunStatus(machinePath) {
|
|
|
211
378
|
? undefined
|
|
212
379
|
: `thinking ${agentIdentity.thinking}`,
|
|
213
380
|
].filter(Boolean).join(" · ");
|
|
214
|
-
const
|
|
381
|
+
const width = Math.max(20, (process.stderr.columns ?? 100) - 4);
|
|
382
|
+
const identityLine = identity === "" ? "" : `\n ${dim(truncate(identity, width))}`;
|
|
383
|
+
const limitLine = agentLimit === ""
|
|
215
384
|
? ""
|
|
216
|
-
: `\n ${
|
|
385
|
+
: `\n ${yellow(truncate(stripVTControlCharacters(agentLimit).replace(/\s+/gu, " "), width))}`;
|
|
217
386
|
return activityExpanded
|
|
218
|
-
? `${status}${identityLine}\n${activityText()}`
|
|
219
|
-
: `${status}${identityLine}`;
|
|
387
|
+
? `${status}${identityLine}${limitLine}\n${activityText()}`
|
|
388
|
+
: `${status}${identityLine}${limitLine}`;
|
|
220
389
|
}
|
|
221
390
|
function completedText() {
|
|
222
391
|
return `${green(displayState(currentState))} · ${dim(formatElapsed(Date.now() - enteredAt))}`;
|
|
@@ -317,6 +486,13 @@ function createRunStatus(machinePath) {
|
|
|
317
486
|
else if (update.type === "output") {
|
|
318
487
|
agentOutput = (agentOutput + update.text).slice(-32_768);
|
|
319
488
|
}
|
|
489
|
+
else if (update.type === "limit") {
|
|
490
|
+
// A wait stays on screen while its presets are tried again, until one of them resumes.
|
|
491
|
+
if (update.status !== "swapped")
|
|
492
|
+
agentWaiting = update.status === "waiting";
|
|
493
|
+
if (update.status !== "swapped" || !agentWaiting)
|
|
494
|
+
agentLimit = update.text;
|
|
495
|
+
}
|
|
320
496
|
else {
|
|
321
497
|
const previous = tools.get(update.id);
|
|
322
498
|
tools.set(update.id, {
|
|
@@ -344,6 +520,8 @@ function createRunStatus(machinePath) {
|
|
|
344
520
|
enteredAt = Date.now();
|
|
345
521
|
agentOutput = "";
|
|
346
522
|
agentIdentity = {};
|
|
523
|
+
agentLimit = "";
|
|
524
|
+
agentWaiting = false;
|
|
347
525
|
tools.clear();
|
|
348
526
|
if (humanInputActive)
|
|
349
527
|
return;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentRunner } from "./index.ts";
|
|
2
|
+
export interface CodexAgentOptions {
|
|
3
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
4
|
+
readonly harness?: string;
|
|
5
|
+
readonly model?: string;
|
|
6
|
+
/** A reasoning effort supported by the selected Codex model. */
|
|
7
|
+
readonly effort?: string;
|
|
8
|
+
/** Defaults to read-only; writes and unrestricted access require an explicit choice. */
|
|
9
|
+
readonly sandbox?: "read-only" | "workspace-write" | "danger-full-access";
|
|
10
|
+
readonly skipGitRepoCheck?: boolean;
|
|
11
|
+
/** Defaults to true, so each invocation does not persist a Codex session. */
|
|
12
|
+
readonly ephemeral?: boolean;
|
|
13
|
+
readonly output?: "capture" | "stream";
|
|
14
|
+
}
|
|
15
|
+
/** Runs one fresh, non-interactive Codex turn using the installed CLI and its authentication. */
|
|
16
|
+
export declare function codexAgent(command?: string, args?: readonly string[], options?: CodexAgentOptions): AgentRunner;
|