@linzumi/cli 0.0.26-beta → 0.0.27-beta
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 +51 -41
- package/dist/index.js +545 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,15 +22,20 @@
|
|
|
22
22
|
Copy this into your terminal:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
codex --ask-for-approval never --sandbox danger-full-access -- '
|
|
25
|
+
codex -c sandbox_workspace_write.network_access=true --ask-for-approval never --sandbox danger-full-access -- 'Follow https://linzumi.com/agents.md to get me up and running. Read the instructions with curl.'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
That's the launch path: the first Codex is only the bootstrapper. It
|
|
29
29
|
opens the agent instructions, confirms your email and workspace details,
|
|
30
30
|
asks you for the emailed code when it arrives, creates `/tmp/hello_linzumi`,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
browser editor pointed at the demo app.
|
|
31
|
+
creates the shared support channel, starts the Linzumi Commander for that
|
|
32
|
+
folder, asks the Commander to launch the inner Linzumi Codex session in a work
|
|
33
|
+
thread, and opens the browser editor pointed at the demo app. The
|
|
34
|
+
Commander-launched Codex session starts the hot-reload app and edits it.
|
|
35
|
+
When you ask the bootstrapper to get you to "wow" or get you up and running,
|
|
36
|
+
that means the complete first-run outcome: a running Linzumi workspace, browser
|
|
37
|
+
VS Code editor, editable `/tmp/hello_linzumi` app on your computer, and a
|
|
38
|
+
shareable Linzumi secure-tunnel preview URL for that app.
|
|
34
39
|
|
|
35
40
|
Terms:
|
|
36
41
|
|
|
@@ -101,7 +106,7 @@ Here's what the next 30 seconds look like:
|
|
|
101
106
|
1. Your browser opens to Linzumi.
|
|
102
107
|
2. Sign in (or sign up — one click).
|
|
103
108
|
3. Linzumi asks if it can connect to this computer. Click allow.
|
|
104
|
-
4. Your laptop appears as a
|
|
109
|
+
4. Your laptop appears as a Commander in your workspace, and
|
|
105
110
|
`~/code/my-app` is added to your trusted-paths list automatically.
|
|
106
111
|
5. Type something in chat — *"Explain this project and tell me how
|
|
107
112
|
to run it"* — and Linzumi Codex picks it up on your machine.
|
|
@@ -115,11 +120,13 @@ hosted at `https://linzumi.com/agents.md`; `https://linzumi.com/skills.md`
|
|
|
115
120
|
stays available for compatibility. The bootstrap agent will confirm your
|
|
116
121
|
email and workspace choice up front, ask for the emailed code after signup
|
|
117
122
|
sends it, say hello to `@sean` in the shared support channel,
|
|
118
|
-
generate `/tmp/hello_linzumi`,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
generate `/tmp/hello_linzumi`, trust that folder in `~/.linzumi/config.json`,
|
|
124
|
+
start the global Commander daemon, ask the Commander to launch a Linzumi Codex
|
|
125
|
+
session in a new work thread, and open the browser editor. The
|
|
126
|
+
Commander-launched Linzumi Codex session starts the
|
|
127
|
+
hot-reload app and adds confetti to the demo page while you watch. Workspace
|
|
128
|
+
names are plain display names from 2 to 100 characters; Linzumi generates the
|
|
129
|
+
URL-safe workspace slug.
|
|
123
130
|
|
|
124
131
|
Under the hood, the npm package exposes these commands for the bootstrap
|
|
125
132
|
agent to run. They are not extra human setup steps after the one pasted
|
|
@@ -129,29 +136,27 @@ Codex command.
|
|
|
129
136
|
npx -y @linzumi/cli@latest signup --email alice@example.com --workspace-name "Alice's Linzumi" --agent-name BuildBot
|
|
130
137
|
npx -y @linzumi/cli@latest claim --pending <pending_id> --code <XXXX-XXXX>
|
|
131
138
|
npx -y @linzumi/cli@latest channel post <support_channel_id> "Hello @sean, starting this launch run."
|
|
132
|
-
npx -y @linzumi/cli@latest thread new "Hello Linzumi confetti" --message "Preparing the Hello Linzumi demo. Next I will generate /tmp/hello_linzumi, start its hot-reload server bound to 0.0.0.0 on port 8787, and ask Linzumi Codex to add confetti when the page loads."
|
|
133
|
-
commander_id="hello-linzumi-commander-${thread_id%%-*}"
|
|
134
139
|
npx -y @linzumi/cli@latest init-hello-linzumi-demo-app --parent-dir /tmp --name hello_linzumi --host 0.0.0.0 --port 8787 --reset --json
|
|
135
140
|
project_dir="$(cd /tmp/hello_linzumi && pwd -P)"
|
|
136
|
-
(
|
|
137
|
-
npx -y @linzumi/cli@latest
|
|
141
|
+
commander_id="hello-linzumi-commander-$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -c1-8)"
|
|
142
|
+
npx -y @linzumi/cli@latest paths add "$project_dir"
|
|
143
|
+
npx -y @linzumi/cli@latest commander daemon \
|
|
138
144
|
--runner-id "$commander_id" \
|
|
139
|
-
--allowed-cwd "$project_dir" \
|
|
140
145
|
--forward-port 8787 \
|
|
141
146
|
--sandbox danger-full-access \
|
|
142
147
|
--approval-policy never
|
|
148
|
+
npx -y @linzumi/cli@latest commander wait --runner-id "$commander_id" --timeout-ms 30000
|
|
143
149
|
```
|
|
144
150
|
|
|
145
151
|
The agent-owned Commander reads `~/.linzumi/agent-token.json`, uses the
|
|
146
|
-
workspace/channel scope from the approval flow,
|
|
147
|
-
|
|
148
|
-
Codex does not stop for an interactive trust
|
|
149
|
-
preview port, and listens only to the
|
|
150
|
-
explicitly passed. Use a unique
|
|
151
|
-
Commander id per launch
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
thread.
|
|
152
|
+
workspace/channel scope from the approval flow, reads trusted folders from
|
|
153
|
+
`~/.linzumi/config.json`, marks approved project directories trusted in
|
|
154
|
+
Codex's normal project config so Codex does not stop for an interactive trust
|
|
155
|
+
prompt, advertises the explicit preview port, and listens only to the
|
|
156
|
+
approving human unless `--listen-user` is explicitly passed. Use a unique
|
|
157
|
+
Commander id per launch. `linzumi commander daemon` writes a status record and
|
|
158
|
+
log under `~/.linzumi/commanders`, and `linzumi commander wait` returns only
|
|
159
|
+
after the Commander is connected.
|
|
155
160
|
|
|
156
161
|
By default, the Commander downloads the Linzumi-approved `code-server`
|
|
157
162
|
runtime for your platform and verifies its checksum before enabling the
|
|
@@ -167,16 +172,21 @@ side; the bootstrap agent posts a hello there with the printed
|
|
|
167
172
|
Keep demo work in task threads; use the support channel when signup, Commander,
|
|
168
173
|
preview, or browser-editor setup gets stuck.
|
|
169
174
|
|
|
170
|
-
Once the Commander is online, the bootstrap agent can ask Linzumi to
|
|
171
|
-
|
|
175
|
+
Once the Commander is online, the bootstrap agent can ask Linzumi to create
|
|
176
|
+
the work thread, have the Commander start Codex, and open the browser editor
|
|
177
|
+
for the same thread and folder:
|
|
172
178
|
|
|
173
179
|
```bash
|
|
174
|
-
npx -y @linzumi/cli@latest codex start
|
|
180
|
+
npx -y @linzumi/cli@latest codex start-new \
|
|
181
|
+
--title "Hello Linzumi confetti" \
|
|
175
182
|
--runner "$commander_id" \
|
|
176
183
|
--cwd "$project_dir" \
|
|
177
|
-
--work-description "
|
|
184
|
+
--work-description "Start the Hello Linzumi hot-reload app on 0.0.0.0:8787, add tasteful confetti when the page loads, keep the preview working, and post the exact files changed." \
|
|
185
|
+
--developer-prompt "The Bootstrapper Codex generated the project and then handed off. You are the Commander-launched Linzumi Codex session. Start any dev server yourself as your descendant process, bind user-visible servers to 0.0.0.0, work only in the approved Hello Linzumi folder, and do not create a branch or PR for this demo." \
|
|
186
|
+
--idempotency-key "hello-linzumi-${commander_id}"
|
|
178
187
|
|
|
179
|
-
|
|
188
|
+
thread_id="<thread_id printed by codex start-new>"
|
|
189
|
+
npx -y @linzumi/cli@latest editor open "$thread_id" \
|
|
180
190
|
--runner "$commander_id" \
|
|
181
191
|
--cwd "$project_dir"
|
|
182
192
|
```
|
|
@@ -189,10 +199,10 @@ forwarded preview readiness, and the first visible Codex edit.
|
|
|
189
199
|
|
|
190
200
|
You now have:
|
|
191
201
|
|
|
192
|
-
- **A
|
|
202
|
+
- **A Commander.** Your laptop, listed in the workspace, advertising
|
|
193
203
|
`/tmp/hello_linzumi` and the explicit preview port.
|
|
194
204
|
- **A workspace** you can invite teammates into. They open the same
|
|
195
|
-
browser app, see the same threads, and can start their own
|
|
205
|
+
browser app, see the same threads, and can start their own Commanders
|
|
196
206
|
on their own machines.
|
|
197
207
|
- **A private Linzumi support channel.** We can see this channel; we
|
|
198
208
|
**cannot** see your repo contents, your tokens, your Codex
|
|
@@ -219,20 +229,20 @@ This is what makes Linzumi different from running an AI coding agent
|
|
|
219
229
|
alone in a terminal.
|
|
220
230
|
|
|
221
231
|
- **Your laptops are always in sight.** Every machine you've run
|
|
222
|
-
`linzumi start` on shows up as a
|
|
223
|
-
see how many of your
|
|
232
|
+
`linzumi start` on shows up as a Commander. From any channel, you can
|
|
233
|
+
see how many of your Commanders are reachable right now and which ones
|
|
224
234
|
are listening on that channel.
|
|
225
235
|
|
|
226
|
-
- **Put Codex on the job from the channel.** Pick an available
|
|
236
|
+
- **Put Codex on the job from the channel.** Pick an available Commander
|
|
227
237
|
and a trusted folder from the channel menu, type what Codex should
|
|
228
|
-
work on, hit start. Linzumi
|
|
238
|
+
work on, hit start. Linzumi asks that Commander to attach a fresh Codex
|
|
229
239
|
with the folder and settings you picked.
|
|
230
240
|
|
|
231
241
|
- **One Codex per thread.** Once a Linzumi Codex session picks up a
|
|
232
242
|
thread, it owns the thread — no second Codex can step in and trample
|
|
233
243
|
the work. The lock holds for the life of the thread.
|
|
234
244
|
|
|
235
|
-
- **Browse what your team's been up to.** Open the
|
|
245
|
+
- **Browse what your team's been up to.** Open the Commanders dropdown
|
|
236
246
|
to see, across every device, the most recently active threads with
|
|
237
247
|
short AI-written summaries. Jump in and keep working.
|
|
238
248
|
|
|
@@ -255,7 +265,7 @@ linzumi paths remove ~/code/my-app
|
|
|
255
265
|
on first use. `linzumi connect` uses the list as-is — or pass
|
|
256
266
|
`--allowed-cwd <paths>` for a one-off comma-separated override.
|
|
257
267
|
|
|
258
|
-
There's no credential escalation: Linzumi only asks the
|
|
268
|
+
There's no credential escalation: Linzumi only asks the Commander to start
|
|
259
269
|
Codex or the editor inside trusted folders. Those processes still run
|
|
260
270
|
with your shell's operating-system privileges, so choose trusted folders
|
|
261
271
|
intentionally. Every action is auditable from the thread.
|
|
@@ -279,7 +289,7 @@ intentionally. Every action is auditable from the thread.
|
|
|
279
289
|
## Pinning a version
|
|
280
290
|
|
|
281
291
|
```bash
|
|
282
|
-
npm install -g @linzumi/cli@0.0.
|
|
292
|
+
npm install -g @linzumi/cli@0.0.27-beta
|
|
283
293
|
linzumi --version
|
|
284
294
|
```
|
|
285
295
|
|
|
@@ -307,15 +317,15 @@ linzumi connect \
|
|
|
307
317
|
--codex-bin <path> Codex executable, default `codex`
|
|
308
318
|
--model <name> Model requested for Codex sessions and labelled in Linzumi
|
|
309
319
|
--reasoning-effort <value> Reasoning effort requested for Codex sessions and labelled in Linzumi
|
|
310
|
-
--fast Advertise this
|
|
320
|
+
--fast Advertise this Commander as low-latency in the workspace
|
|
311
321
|
--forward-port <ports> Comma-separated local TCP ports Linzumi may share as authenticated previews
|
|
312
322
|
--allowed-cwd <paths> Override ~/.linzumi/config.json with comma-separated trusted roots
|
|
313
|
-
--log-file <path> JSONL
|
|
323
|
+
--log-file <path> JSONL Commander event log
|
|
314
324
|
```
|
|
315
325
|
|
|
316
326
|
`--runner-id`, `--auth-file`, `--code-server-bin`, `--codex-url`,
|
|
317
327
|
`--launch-tui`, `--listen-user`, `--sandbox`, `--approval-policy`,
|
|
318
328
|
`--stream-flush-ms`, and `--token` are also accepted; `linzumi
|
|
319
329
|
--help` shows them all with brief descriptions. They exist for
|
|
320
|
-
multi-
|
|
330
|
+
multi-Commander orchestration, custom Codex deployments, and CI
|
|
321
331
|
scenarios — not for everyday use.
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3
|
-
import { existsSync as
|
|
4
|
-
import { homedir as
|
|
5
|
-
import { resolve as
|
|
6
|
-
import { fileURLToPath as
|
|
3
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync5 } from "node:fs";
|
|
4
|
+
import { homedir as homedir8 } from "node:os";
|
|
5
|
+
import { resolve as resolve8 } from "node:path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/runner.ts
|
|
9
9
|
import { spawn as spawn6 } from "node:child_process";
|
|
@@ -6781,7 +6781,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6781
6781
|
if (handled !== undefined) {
|
|
6782
6782
|
return handled;
|
|
6783
6783
|
}
|
|
6784
|
-
return applyControl(codex, instanceId, options, allowedCwds.value, control);
|
|
6784
|
+
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control);
|
|
6785
6785
|
}).then((response) => {
|
|
6786
6786
|
return kandan.push(topic, "codex_response", response);
|
|
6787
6787
|
}).catch((error) => {
|
|
@@ -6916,7 +6916,7 @@ async function prepareCodexThreadForTuiResume(codex, codexThreadId) {
|
|
|
6916
6916
|
throw new Error(`failed to verify Codex TUI resume: ${verified.error.message}`);
|
|
6917
6917
|
}
|
|
6918
6918
|
}
|
|
6919
|
-
async function applyControl(codex, instanceId, options, allowedCwds, control) {
|
|
6919
|
+
async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control) {
|
|
6920
6920
|
switch (control.type) {
|
|
6921
6921
|
case "start_instance": {
|
|
6922
6922
|
const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
|
|
@@ -6931,10 +6931,15 @@ async function applyControl(codex, instanceId, options, allowedCwds, control) {
|
|
|
6931
6931
|
if (options.codexUrl === undefined) {
|
|
6932
6932
|
ensureCodexProjectTrusted(cwd.cwd);
|
|
6933
6933
|
}
|
|
6934
|
+
const developerPrompt = normalizedWorkDescription(control.developerPrompt);
|
|
6934
6935
|
const response = await codex.request("thread/start", {
|
|
6935
6936
|
cwd: cwd.cwd,
|
|
6936
6937
|
serviceName: "kandan-local-runner",
|
|
6937
6938
|
personality: "pragmatic",
|
|
6939
|
+
developerInstructions: commanderDeveloperInstructions({
|
|
6940
|
+
cwd: cwd.cwd,
|
|
6941
|
+
developerPrompt
|
|
6942
|
+
}),
|
|
6938
6943
|
...control.model === undefined ? {} : { model: control.model },
|
|
6939
6944
|
...control.reasoningEffort === undefined ? {} : { reasoningEffort: control.reasoningEffort },
|
|
6940
6945
|
...control.approvalPolicy === undefined ? {} : { approvalPolicy: control.approvalPolicy },
|
|
@@ -6943,6 +6948,9 @@ async function applyControl(codex, instanceId, options, allowedCwds, control) {
|
|
|
6943
6948
|
});
|
|
6944
6949
|
const codexThreadId = extractStartedThreadId(response);
|
|
6945
6950
|
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
6951
|
+
if (codexThreadId !== undefined && developerPrompt !== undefined) {
|
|
6952
|
+
await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt);
|
|
6953
|
+
}
|
|
6946
6954
|
if (codexThreadId !== undefined && workDescription !== undefined) {
|
|
6947
6955
|
await codex.request("turn/start", {
|
|
6948
6956
|
threadId: codexThreadId,
|
|
@@ -7015,6 +7023,83 @@ async function applyControl(codex, instanceId, options, allowedCwds, control) {
|
|
|
7015
7023
|
return { instanceId, controlType: control.type, skipped: true };
|
|
7016
7024
|
}
|
|
7017
7025
|
}
|
|
7026
|
+
function commanderDeveloperInstructions(args) {
|
|
7027
|
+
const customPrompt = args.developerPrompt === undefined ? "" : `
|
|
7028
|
+
<invoker_developer_prompt>
|
|
7029
|
+
${args.developerPrompt}
|
|
7030
|
+
</invoker_developer_prompt>
|
|
7031
|
+
`;
|
|
7032
|
+
return `<context>
|
|
7033
|
+
You are a Linzumi Codex session launched by the Linzumi Commander.
|
|
7034
|
+
The Commander is the local bridge process that connects this computer to the
|
|
7035
|
+
user's Linzumi workspace, secure preview tunnel, browser VS Code editor, and
|
|
7036
|
+
thread transcript. Your work and progress are relayed into that Linzumi thread.
|
|
7037
|
+
</context>
|
|
7038
|
+
|
|
7039
|
+
<term_definitions>
|
|
7040
|
+
Bootstrapper Codex: The outer setup agent that claimed/signup, generated the
|
|
7041
|
+
demo folder when applicable, and asked Linzumi to start this session. It does
|
|
7042
|
+
not own implementation work.
|
|
7043
|
+
Linzumi Commander: The local long-running process that launched this Codex
|
|
7044
|
+
session, enforces the trusted folder, and forwards descendant preview ports to
|
|
7045
|
+
Linzumi.
|
|
7046
|
+
Linzumi Codex session: You, the inner Codex process that performs the actual
|
|
7047
|
+
work in the approved project folder.
|
|
7048
|
+
Approved project folder: ${args.cwd}
|
|
7049
|
+
</term_definitions>
|
|
7050
|
+
|
|
7051
|
+
<task_instructions>
|
|
7052
|
+
Work only in the approved project folder unless the human explicitly asks for
|
|
7053
|
+
something else in the Linzumi thread. Start, inspect, and modify the local app
|
|
7054
|
+
from that folder. Report concise progress and exact blockers in the thread.
|
|
7055
|
+
</task_instructions>
|
|
7056
|
+
|
|
7057
|
+
<rules>
|
|
7058
|
+
You MUST treat the Linzumi thread as the source of truth for user-facing
|
|
7059
|
+
progress.
|
|
7060
|
+
You MUST keep user-visible preview servers bound to 0.0.0.0, not 127.0.0.1 or
|
|
7061
|
+
localhost, so the Linzumi secure tunnel can reach them.
|
|
7062
|
+
You MUST keep any preview or dev server as your descendant process so the
|
|
7063
|
+
Commander can discover and forward it.
|
|
7064
|
+
You MUST NOT ask the Bootstrapper Codex to do implementation work.
|
|
7065
|
+
You MUST NOT inspect unrelated repositories or folders.
|
|
7066
|
+
You MUST report the exact blocker if a command, preview, editor, or tunnel step
|
|
7067
|
+
fails.
|
|
7068
|
+
</rules>
|
|
7069
|
+
|
|
7070
|
+
<examples>
|
|
7071
|
+
GOOD preview command: npm run dev -- --host 0.0.0.0 --port 8787
|
|
7072
|
+
BAD preview command: npm run dev -- --host 127.0.0.1
|
|
7073
|
+
</examples>
|
|
7074
|
+
${customPrompt}
|
|
7075
|
+
<task_reminder>
|
|
7076
|
+
You are the Commander-launched Linzumi Codex session. Do the implementation
|
|
7077
|
+
work in the approved project folder, keep preview servers reachable through the
|
|
7078
|
+
secure tunnel, and keep the Linzumi thread truthful.
|
|
7079
|
+
</task_reminder>`;
|
|
7080
|
+
}
|
|
7081
|
+
async function postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt) {
|
|
7082
|
+
const workspace = normalizedWorkDescription(control.workspace);
|
|
7083
|
+
const channel = normalizedWorkDescription(control.channel);
|
|
7084
|
+
const threadId = normalizedWorkDescription(control.threadId);
|
|
7085
|
+
if (workspace === undefined || channel === undefined || threadId === undefined) {
|
|
7086
|
+
return;
|
|
7087
|
+
}
|
|
7088
|
+
await kandan.push(topic, "session:post_thread_message", {
|
|
7089
|
+
workspace,
|
|
7090
|
+
channel,
|
|
7091
|
+
thread_id: threadId,
|
|
7092
|
+
body: `Starting a new Codex thread with instructions:
|
|
7093
|
+
|
|
7094
|
+
${developerPrompt}`,
|
|
7095
|
+
payload: {
|
|
7096
|
+
local_codex_runner: {
|
|
7097
|
+
event_type: "codex_start_instructions"
|
|
7098
|
+
}
|
|
7099
|
+
},
|
|
7100
|
+
client_message_id: `codex-start-instructions-${threadId}`
|
|
7101
|
+
});
|
|
7102
|
+
}
|
|
7018
7103
|
async function startOwnedCodexAppServer(options) {
|
|
7019
7104
|
ensureCodexProjectTrusted(options.cwd);
|
|
7020
7105
|
return await startCodexAppServer(options.codexBin, options.cwd, {
|
|
@@ -7325,6 +7410,9 @@ async function runAgentCliCommand(args, deps = {
|
|
|
7325
7410
|
case "codexStart":
|
|
7326
7411
|
await runCodexStart(command, deps);
|
|
7327
7412
|
return;
|
|
7413
|
+
case "codexStartNew":
|
|
7414
|
+
await runCodexStartNew(command, deps);
|
|
7415
|
+
return;
|
|
7328
7416
|
case "editorOpen":
|
|
7329
7417
|
await runEditorOpen(command, deps);
|
|
7330
7418
|
return;
|
|
@@ -7460,10 +7548,31 @@ function parseAgentCommand(args) {
|
|
|
7460
7548
|
}
|
|
7461
7549
|
case "codex": {
|
|
7462
7550
|
const [subcommand, ...subcommandArgs] = rest;
|
|
7463
|
-
if (subcommand !== "start") {
|
|
7464
|
-
throw new Error("linzumi codex supports: start");
|
|
7551
|
+
if (subcommand !== "start" && subcommand !== "start-new") {
|
|
7552
|
+
throw new Error("linzumi codex supports: start, start-new");
|
|
7465
7553
|
}
|
|
7466
7554
|
const parsed = parseAgentArgs(subcommandArgs);
|
|
7555
|
+
if (subcommand === "start-new") {
|
|
7556
|
+
if (parsed.positionals.length > 0) {
|
|
7557
|
+
throw new Error("linzumi codex start-new does not accept positional arguments");
|
|
7558
|
+
}
|
|
7559
|
+
return {
|
|
7560
|
+
kind: "codexStartNew",
|
|
7561
|
+
apiUrl: agentApiUrl(parsed.flags),
|
|
7562
|
+
title: requiredFlag(parsed.flags, "title"),
|
|
7563
|
+
runnerId: requiredFlag(parsed.flags, "runner"),
|
|
7564
|
+
cwd: requiredFlag(parsed.flags, "cwd"),
|
|
7565
|
+
workDescription: requiredFlag(parsed.flags, "work-description"),
|
|
7566
|
+
developerPrompt: optionalFlag(parsed.flags, "developer-prompt"),
|
|
7567
|
+
idempotencyKey: optionalFlag(parsed.flags, "idempotency-key"),
|
|
7568
|
+
model: optionalFlag(parsed.flags, "model"),
|
|
7569
|
+
reasoningEffort: optionalFlag(parsed.flags, "reasoning-effort"),
|
|
7570
|
+
approvalPolicy: optionalFlag(parsed.flags, "approval-policy"),
|
|
7571
|
+
sandbox: optionalFlag(parsed.flags, "sandbox"),
|
|
7572
|
+
fast: parsed.flags.get("fast") === "true",
|
|
7573
|
+
tokenFile: agentTokenFile(parsed.flags)
|
|
7574
|
+
};
|
|
7575
|
+
}
|
|
7467
7576
|
const [threadId, ...extra] = parsed.positionals;
|
|
7468
7577
|
if (threadId === undefined || threadId.trim() === "") {
|
|
7469
7578
|
throw new Error("missing thread id");
|
|
@@ -7478,6 +7587,7 @@ function parseAgentCommand(args) {
|
|
|
7478
7587
|
runnerId: requiredFlag(parsed.flags, "runner"),
|
|
7479
7588
|
cwd: requiredFlag(parsed.flags, "cwd"),
|
|
7480
7589
|
workDescription: requiredFlag(parsed.flags, "work-description"),
|
|
7590
|
+
developerPrompt: optionalFlag(parsed.flags, "developer-prompt"),
|
|
7481
7591
|
model: optionalFlag(parsed.flags, "model"),
|
|
7482
7592
|
reasoningEffort: optionalFlag(parsed.flags, "reasoning-effort"),
|
|
7483
7593
|
approvalPolicy: optionalFlag(parsed.flags, "approval-policy"),
|
|
@@ -7641,6 +7751,7 @@ async function runCodexStart(command, deps) {
|
|
|
7641
7751
|
putOptional(body, "reasoning_effort", command.reasoningEffort);
|
|
7642
7752
|
putOptional(body, "approval_policy", command.approvalPolicy);
|
|
7643
7753
|
putOptional(body, "sandbox", command.sandbox);
|
|
7754
|
+
putOptional(body, "developer_prompt", command.developerPrompt);
|
|
7644
7755
|
if (command.fast) {
|
|
7645
7756
|
body.fast = true;
|
|
7646
7757
|
}
|
|
@@ -7650,6 +7761,33 @@ async function runCodexStart(command, deps) {
|
|
|
7650
7761
|
deps.stdout.write(`runner_id: ${requiredString(response, "runner_id")}
|
|
7651
7762
|
`);
|
|
7652
7763
|
}
|
|
7764
|
+
async function runCodexStartNew(command, deps) {
|
|
7765
|
+
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
7766
|
+
const body = {
|
|
7767
|
+
title: command.title,
|
|
7768
|
+
runner_id: command.runnerId,
|
|
7769
|
+
cwd: command.cwd,
|
|
7770
|
+
work_description: command.workDescription
|
|
7771
|
+
};
|
|
7772
|
+
putOptional(body, "developer_prompt", command.developerPrompt);
|
|
7773
|
+
putOptional(body, "idempotency_key", command.idempotencyKey);
|
|
7774
|
+
putOptional(body, "model", command.model);
|
|
7775
|
+
putOptional(body, "reasoning_effort", command.reasoningEffort);
|
|
7776
|
+
putOptional(body, "approval_policy", command.approvalPolicy);
|
|
7777
|
+
putOptional(body, "sandbox", command.sandbox);
|
|
7778
|
+
if (command.fast) {
|
|
7779
|
+
body.fast = true;
|
|
7780
|
+
}
|
|
7781
|
+
const response = await postJson(command.apiUrl, "/agent/codex/start", body, tokenFile.agentToken, deps.fetchImpl);
|
|
7782
|
+
deps.stdout.write(`thread_id: ${requiredString(response, "thread_id")}
|
|
7783
|
+
`);
|
|
7784
|
+
deps.stdout.write(`thread_url: ${requiredString(response, "thread_url")}
|
|
7785
|
+
`);
|
|
7786
|
+
deps.stdout.write(`codex_status: ${requiredString(response, "status")}
|
|
7787
|
+
`);
|
|
7788
|
+
deps.stdout.write(`runner_id: ${requiredString(response, "runner_id")}
|
|
7789
|
+
`);
|
|
7790
|
+
}
|
|
7653
7791
|
async function runEditorOpen(command, deps) {
|
|
7654
7792
|
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
7655
7793
|
const response = await postJson(command.apiUrl, `/agent/threads/${encodeURIComponent(command.threadId)}/editor/open`, {
|
|
@@ -7831,7 +7969,8 @@ Usage:
|
|
|
7831
7969
|
linzumi post <thread_id> <message> [--kind progress|question]
|
|
7832
7970
|
linzumi inbox <thread_id> --since-last
|
|
7833
7971
|
linzumi done <thread_id> --message <message>
|
|
7834
|
-
linzumi codex start <thread_id> --runner <runner_id> --cwd <path> --work-description <text>
|
|
7972
|
+
linzumi codex start <thread_id> --runner <runner_id> --cwd <path> --work-description <text> [--developer-prompt <text>]
|
|
7973
|
+
linzumi codex start-new --title <title> --runner <runner_id> --cwd <path> --work-description <text> [--developer-prompt <text>] [--idempotency-key <key>]
|
|
7835
7974
|
linzumi editor open <thread_id> --runner <runner_id> --cwd <path>
|
|
7836
7975
|
|
|
7837
7976
|
Options:
|
|
@@ -8407,6 +8546,232 @@ To kick the agent off:
|
|
|
8407
8546
|
3. Paste into the thread. Codex will pick it up and start editing this folder.
|
|
8408
8547
|
`;
|
|
8409
8548
|
|
|
8549
|
+
// src/commanderDaemon.ts
|
|
8550
|
+
import {
|
|
8551
|
+
existsSync as existsSync9,
|
|
8552
|
+
closeSync,
|
|
8553
|
+
mkdirSync as mkdirSync9,
|
|
8554
|
+
openSync as openSync2,
|
|
8555
|
+
readFileSync as readFileSync8,
|
|
8556
|
+
watch,
|
|
8557
|
+
writeFileSync as writeFileSync8
|
|
8558
|
+
} from "node:fs";
|
|
8559
|
+
import { homedir as homedir7 } from "node:os";
|
|
8560
|
+
import { dirname as dirname8, join as join9, resolve as resolve7 } from "node:path";
|
|
8561
|
+
import { execFileSync, spawn as spawn7 } from "node:child_process";
|
|
8562
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8563
|
+
var connectedMarker = "Runner connected:";
|
|
8564
|
+
function commanderStatusDir() {
|
|
8565
|
+
return join9(homedir7(), ".linzumi", "commanders");
|
|
8566
|
+
}
|
|
8567
|
+
function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
|
|
8568
|
+
return join9(statusDir, `${safeRunnerId(runnerId)}.json`);
|
|
8569
|
+
}
|
|
8570
|
+
function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
|
|
8571
|
+
return join9(statusDir, `${safeRunnerId(runnerId)}.log`);
|
|
8572
|
+
}
|
|
8573
|
+
function startCommanderDaemon(options) {
|
|
8574
|
+
const statusDir = options.statusDir ?? commanderStatusDir();
|
|
8575
|
+
const statusFile = commanderStatusFile(options.runnerId, statusDir);
|
|
8576
|
+
const logFile = resolve7(options.logFile ?? defaultCommanderLogFile(options.runnerId, statusDir));
|
|
8577
|
+
const entrypoint = options.entrypoint ?? currentEntrypoint();
|
|
8578
|
+
const nodeBin = options.nodeBin ?? process.execPath;
|
|
8579
|
+
const command = [
|
|
8580
|
+
entrypoint,
|
|
8581
|
+
"commander",
|
|
8582
|
+
...options.cwd === undefined ? [] : [options.cwd],
|
|
8583
|
+
...options.args,
|
|
8584
|
+
"--runner-id",
|
|
8585
|
+
options.runnerId,
|
|
8586
|
+
"--log-file",
|
|
8587
|
+
logFile
|
|
8588
|
+
];
|
|
8589
|
+
mkdirSync9(statusDir, { recursive: true });
|
|
8590
|
+
mkdirSync9(dirname8(logFile), { recursive: true });
|
|
8591
|
+
const out = openSync2(logFile, "a");
|
|
8592
|
+
const err = openSync2(logFile, "a");
|
|
8593
|
+
const child = (options.spawnImpl ?? spawn7)(nodeBin, command, {
|
|
8594
|
+
detached: true,
|
|
8595
|
+
stdio: ["ignore", out, err],
|
|
8596
|
+
env: process.env
|
|
8597
|
+
});
|
|
8598
|
+
closeSync(out);
|
|
8599
|
+
closeSync(err);
|
|
8600
|
+
child.unref();
|
|
8601
|
+
if (child.pid === undefined) {
|
|
8602
|
+
throw new Error("commander daemon did not report a pid");
|
|
8603
|
+
}
|
|
8604
|
+
const processIdentity = readProcessIdentity(child.pid);
|
|
8605
|
+
const record = {
|
|
8606
|
+
runnerId: options.runnerId,
|
|
8607
|
+
pid: child.pid,
|
|
8608
|
+
...processIdentity?.startedAt === undefined ? {} : { processStartedAt: processIdentity.startedAt },
|
|
8609
|
+
cwd: process.cwd(),
|
|
8610
|
+
...options.cwd === undefined ? {} : { launchFolder: options.cwd },
|
|
8611
|
+
logFile,
|
|
8612
|
+
startedAt: new Date().toISOString(),
|
|
8613
|
+
command: [nodeBin, ...command]
|
|
8614
|
+
};
|
|
8615
|
+
writeFileSync8(statusFile, `${JSON.stringify(record, null, 2)}
|
|
8616
|
+
`);
|
|
8617
|
+
return record;
|
|
8618
|
+
}
|
|
8619
|
+
function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
|
|
8620
|
+
const statusFile = commanderStatusFile(runnerId, statusDir);
|
|
8621
|
+
if (!existsSync9(statusFile)) {
|
|
8622
|
+
return { status: "missing", runnerId, statusFile };
|
|
8623
|
+
}
|
|
8624
|
+
const record = parseRecord(readFileSync8(statusFile, "utf8"));
|
|
8625
|
+
return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
|
|
8626
|
+
}
|
|
8627
|
+
async function waitForCommanderDaemon(options) {
|
|
8628
|
+
const now = options.now ?? (() => Date.now());
|
|
8629
|
+
const readTextFile = options.readTextFile ?? ((path) => existsSync9(path) ? readFileSync8(path, "utf8") : undefined);
|
|
8630
|
+
const statusImpl = options.statusImpl ?? commanderDaemonStatus;
|
|
8631
|
+
const deadline = now() + options.timeoutMs;
|
|
8632
|
+
while (now() <= deadline) {
|
|
8633
|
+
const status = statusImpl(options.runnerId, options.statusDir);
|
|
8634
|
+
switch (status.status) {
|
|
8635
|
+
case "missing":
|
|
8636
|
+
return { ok: false, reason: "missing" };
|
|
8637
|
+
case "stopped":
|
|
8638
|
+
return { ok: false, reason: "stopped" };
|
|
8639
|
+
case "running": {
|
|
8640
|
+
const log = readTextFile(status.record.logFile);
|
|
8641
|
+
if (log === undefined) {
|
|
8642
|
+
return { ok: false, reason: "timeout" };
|
|
8643
|
+
}
|
|
8644
|
+
if (log.includes(connectedMarker)) {
|
|
8645
|
+
return { ok: true, record: status.record };
|
|
8646
|
+
}
|
|
8647
|
+
await waitForFileChangeOrTimeout(status.record.logFile, deadline, now, () => {
|
|
8648
|
+
const updatedLog = readTextFile(status.record.logFile);
|
|
8649
|
+
return updatedLog !== undefined && updatedLog.includes(connectedMarker);
|
|
8650
|
+
});
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
return { ok: false, reason: "timeout" };
|
|
8655
|
+
}
|
|
8656
|
+
function stopCommanderDaemon(runnerId, statusDir = commanderStatusDir()) {
|
|
8657
|
+
const status = commanderDaemonStatus(runnerId, statusDir);
|
|
8658
|
+
if (status.status === "running") {
|
|
8659
|
+
process.kill(status.record.pid, "SIGINT");
|
|
8660
|
+
}
|
|
8661
|
+
return status;
|
|
8662
|
+
}
|
|
8663
|
+
function currentEntrypoint() {
|
|
8664
|
+
const scriptPath = process.argv[1];
|
|
8665
|
+
if (scriptPath !== undefined) {
|
|
8666
|
+
return scriptPath;
|
|
8667
|
+
}
|
|
8668
|
+
return fileURLToPath2(import.meta.url);
|
|
8669
|
+
}
|
|
8670
|
+
function parseRecord(content) {
|
|
8671
|
+
const parsed = JSON.parse(content);
|
|
8672
|
+
const record = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8673
|
+
const pid = typeof record.pid === "number" ? record.pid : undefined;
|
|
8674
|
+
const runnerId = recordString(record.runnerId);
|
|
8675
|
+
const cwd = recordString(record.cwd);
|
|
8676
|
+
const launchFolder = recordString(record.launchFolder);
|
|
8677
|
+
const logFile = recordString(record.logFile);
|
|
8678
|
+
const startedAt = recordString(record.startedAt);
|
|
8679
|
+
const processStartedAt = recordString(record.processStartedAt);
|
|
8680
|
+
const commandValue = record.command;
|
|
8681
|
+
const command = Array.isArray(commandValue) ? commandValue.filter((value) => typeof value === "string") : [];
|
|
8682
|
+
if (pid === undefined || runnerId === undefined || cwd === undefined || logFile === undefined || startedAt === undefined) {
|
|
8683
|
+
throw new Error("commander daemon status file is invalid");
|
|
8684
|
+
}
|
|
8685
|
+
return {
|
|
8686
|
+
pid,
|
|
8687
|
+
...processStartedAt === undefined ? {} : { processStartedAt },
|
|
8688
|
+
runnerId,
|
|
8689
|
+
cwd,
|
|
8690
|
+
...launchFolder === undefined ? {} : { launchFolder },
|
|
8691
|
+
logFile,
|
|
8692
|
+
startedAt,
|
|
8693
|
+
command
|
|
8694
|
+
};
|
|
8695
|
+
}
|
|
8696
|
+
function recordString(value) {
|
|
8697
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
8698
|
+
}
|
|
8699
|
+
function processIsRunning(pid) {
|
|
8700
|
+
try {
|
|
8701
|
+
process.kill(pid, 0);
|
|
8702
|
+
return true;
|
|
8703
|
+
} catch (_error) {
|
|
8704
|
+
return false;
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
function processMatchesRecord(record, processIdentityReader) {
|
|
8708
|
+
const identity = processIdentityReader(record.pid);
|
|
8709
|
+
const commandLine = identity?.command;
|
|
8710
|
+
if (commandLine === undefined || commandLine.trim() === "") {
|
|
8711
|
+
return false;
|
|
8712
|
+
}
|
|
8713
|
+
const commandMatches = record.command.every((part) => commandLine.includes(part));
|
|
8714
|
+
if (!commandMatches) {
|
|
8715
|
+
return false;
|
|
8716
|
+
}
|
|
8717
|
+
return record.processStartedAt === undefined || identity?.startedAt === record.processStartedAt;
|
|
8718
|
+
}
|
|
8719
|
+
function readProcessIdentity(pid) {
|
|
8720
|
+
try {
|
|
8721
|
+
const output = execFileSync("ps", ["-p", String(pid), "-o", "lstart=", "-o", "command="], {
|
|
8722
|
+
encoding: "utf8",
|
|
8723
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
8724
|
+
}).trim();
|
|
8725
|
+
if (output === "") {
|
|
8726
|
+
return;
|
|
8727
|
+
}
|
|
8728
|
+
const match = output.match(/^(\S+\s+\S+\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+)$/);
|
|
8729
|
+
if (match === null) {
|
|
8730
|
+
return { command: output };
|
|
8731
|
+
}
|
|
8732
|
+
return { startedAt: match[1], command: match[2] };
|
|
8733
|
+
} catch (_error) {
|
|
8734
|
+
return;
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
function safeRunnerId(runnerId) {
|
|
8738
|
+
const safe = runnerId.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8739
|
+
if (safe === "") {
|
|
8740
|
+
throw new Error("runner id must contain a filesystem-safe character");
|
|
8741
|
+
}
|
|
8742
|
+
return safe;
|
|
8743
|
+
}
|
|
8744
|
+
async function waitForFileChangeOrTimeout(path, deadline, now, ready2 = () => false) {
|
|
8745
|
+
const remaining = Math.max(0, deadline - now());
|
|
8746
|
+
await new Promise((resolve8) => {
|
|
8747
|
+
let resolved = false;
|
|
8748
|
+
let watcher;
|
|
8749
|
+
const finish = () => {
|
|
8750
|
+
if (resolved) {
|
|
8751
|
+
return;
|
|
8752
|
+
}
|
|
8753
|
+
resolved = true;
|
|
8754
|
+
watcher?.close();
|
|
8755
|
+
clearTimeout(timer);
|
|
8756
|
+
resolve8();
|
|
8757
|
+
};
|
|
8758
|
+
const timer = setTimeout(finish, remaining);
|
|
8759
|
+
try {
|
|
8760
|
+
watcher = watch(path, finish);
|
|
8761
|
+
watcher.on("error", finish);
|
|
8762
|
+
if (ready2()) {
|
|
8763
|
+
finish();
|
|
8764
|
+
}
|
|
8765
|
+
} catch (_error) {
|
|
8766
|
+
if (ready2()) {
|
|
8767
|
+
finish();
|
|
8768
|
+
return;
|
|
8769
|
+
}
|
|
8770
|
+
finish();
|
|
8771
|
+
}
|
|
8772
|
+
});
|
|
8773
|
+
}
|
|
8774
|
+
|
|
8410
8775
|
// src/index.ts
|
|
8411
8776
|
var flagDefinitions = new Map([
|
|
8412
8777
|
["version", { kind: "boolean" }],
|
|
@@ -8431,6 +8796,8 @@ var flagDefinitions = new Map([
|
|
|
8431
8796
|
["code-server-bin", { kind: "value" }],
|
|
8432
8797
|
["fast", { kind: "boolean" }],
|
|
8433
8798
|
["log-file", { kind: "value" }],
|
|
8799
|
+
["status-dir", { kind: "value" }],
|
|
8800
|
+
["timeout-ms", { kind: "value" }],
|
|
8434
8801
|
["auth-file", { kind: "value" }],
|
|
8435
8802
|
["agent-token-file", { kind: "value" }],
|
|
8436
8803
|
["oauth-callback-host", { kind: "value" }],
|
|
@@ -8460,7 +8827,7 @@ function isMainModule() {
|
|
|
8460
8827
|
if (scriptPath === undefined) {
|
|
8461
8828
|
return false;
|
|
8462
8829
|
}
|
|
8463
|
-
return
|
|
8830
|
+
return fileURLToPath3(import.meta.url) === resolve8(scriptPath);
|
|
8464
8831
|
}
|
|
8465
8832
|
async function main(args) {
|
|
8466
8833
|
const parsed = parseCommand(args);
|
|
@@ -8469,7 +8836,7 @@ async function main(args) {
|
|
|
8469
8836
|
process.stdout.write(connectGuideText());
|
|
8470
8837
|
return;
|
|
8471
8838
|
case "version":
|
|
8472
|
-
process.stdout.write(`linzumi 0.0.
|
|
8839
|
+
process.stdout.write(`linzumi 0.0.27-beta
|
|
8473
8840
|
`);
|
|
8474
8841
|
return;
|
|
8475
8842
|
case "auth":
|
|
@@ -8484,6 +8851,9 @@ async function main(args) {
|
|
|
8484
8851
|
case "agent":
|
|
8485
8852
|
await runAgentCliCommand(parsed.args);
|
|
8486
8853
|
return;
|
|
8854
|
+
case "commanderDaemon":
|
|
8855
|
+
await runCommanderDaemonCommand(parsed.args);
|
|
8856
|
+
return;
|
|
8487
8857
|
case "agentRunner": {
|
|
8488
8858
|
const options = await parseAgentRunnerArgs(parsed.args);
|
|
8489
8859
|
await runLocalCodexRunner(options);
|
|
@@ -8524,8 +8894,17 @@ function parseCommand(args) {
|
|
|
8524
8894
|
case "agent":
|
|
8525
8895
|
return rest[0] === "runner" ? { command: "agentRunner", args: rest.slice(1) } : { command: "agent", args: rest };
|
|
8526
8896
|
case "agent-runner":
|
|
8527
|
-
case "commander":
|
|
8528
8897
|
return { command: "agentRunner", args: rest };
|
|
8898
|
+
case "commander":
|
|
8899
|
+
return ["daemon", "status", "wait", "stop"].includes(rest[0] ?? "") ? { command: "commanderDaemon", args: rest } : { command: "agentRunner", args: rest };
|
|
8900
|
+
case "commander-daemon":
|
|
8901
|
+
return { command: "commanderDaemon", args: ["daemon", ...rest] };
|
|
8902
|
+
case "commander-status":
|
|
8903
|
+
return { command: "commanderDaemon", args: ["status", ...rest] };
|
|
8904
|
+
case "commander-wait":
|
|
8905
|
+
return { command: "commanderDaemon", args: ["wait", ...rest] };
|
|
8906
|
+
case "commander-stop":
|
|
8907
|
+
return { command: "commanderDaemon", args: ["stop", ...rest] };
|
|
8529
8908
|
case "signup":
|
|
8530
8909
|
case "claim":
|
|
8531
8910
|
case "thread":
|
|
@@ -8604,7 +8983,7 @@ function runPathsCommand(args) {
|
|
|
8604
8983
|
if (pathValue === undefined || pathValue.trim() === "") {
|
|
8605
8984
|
throw new Error("missing path for linzumi paths add");
|
|
8606
8985
|
}
|
|
8607
|
-
const trustedPath = realpathSync5(
|
|
8986
|
+
const trustedPath = realpathSync5(resolve8(expandUserPath(pathValue)));
|
|
8608
8987
|
addAllowedCwd(pathValue);
|
|
8609
8988
|
process.stdout.write(`Trusted ${trustedPath}
|
|
8610
8989
|
`);
|
|
@@ -8623,6 +9002,79 @@ function runPathsCommand(args) {
|
|
|
8623
9002
|
throw new Error(`invalid paths command: ${subcommand}`);
|
|
8624
9003
|
}
|
|
8625
9004
|
}
|
|
9005
|
+
async function runCommanderDaemonCommand(args) {
|
|
9006
|
+
const [subcommand, ...rest] = args;
|
|
9007
|
+
switch (subcommand) {
|
|
9008
|
+
case "daemon": {
|
|
9009
|
+
const { cwdArg, flagArgs } = splitStartArgs(rest);
|
|
9010
|
+
const values = strictFlagValues(flagArgs);
|
|
9011
|
+
if (values.get("help") === true) {
|
|
9012
|
+
process.stdout.write(commanderDaemonHelpText());
|
|
9013
|
+
return;
|
|
9014
|
+
}
|
|
9015
|
+
const runnerId = required(values, "runner-id");
|
|
9016
|
+
const cwd = cwdArg === undefined || cwdArg.trim() === "" ? undefined : resolveUserPath(cwdArg);
|
|
9017
|
+
const record = startCommanderDaemon({
|
|
9018
|
+
runnerId,
|
|
9019
|
+
cwd,
|
|
9020
|
+
args: stripDaemonSupervisorFlags(flagArgs),
|
|
9021
|
+
logFile: stringValue3(values, "log-file"),
|
|
9022
|
+
statusDir: stringValue3(values, "status-dir")
|
|
9023
|
+
});
|
|
9024
|
+
process.stdout.write(`commander_status: daemon_started
|
|
9025
|
+
`);
|
|
9026
|
+
process.stdout.write(`runner_id: ${record.runnerId}
|
|
9027
|
+
`);
|
|
9028
|
+
process.stdout.write(`pid: ${record.pid}
|
|
9029
|
+
`);
|
|
9030
|
+
process.stdout.write(`log_file: ${record.logFile}
|
|
9031
|
+
`);
|
|
9032
|
+
if (record.launchFolder !== undefined) {
|
|
9033
|
+
process.stdout.write(`launch_folder: ${record.launchFolder}
|
|
9034
|
+
`);
|
|
9035
|
+
}
|
|
9036
|
+
return;
|
|
9037
|
+
}
|
|
9038
|
+
case "status": {
|
|
9039
|
+
const values = strictFlagValues(rest);
|
|
9040
|
+
const runnerId = required(values, "runner-id");
|
|
9041
|
+
const status = commanderDaemonStatus(runnerId, stringValue3(values, "status-dir"));
|
|
9042
|
+
process.stdout.write(`${JSON.stringify(status)}
|
|
9043
|
+
`);
|
|
9044
|
+
return;
|
|
9045
|
+
}
|
|
9046
|
+
case "wait": {
|
|
9047
|
+
const values = strictFlagValues(rest);
|
|
9048
|
+
const runnerId = required(values, "runner-id");
|
|
9049
|
+
const timeoutMs = positiveIntegerValue(values, "timeout-ms") ?? 30000;
|
|
9050
|
+
const result = await waitForCommanderDaemon({
|
|
9051
|
+
runnerId,
|
|
9052
|
+
timeoutMs,
|
|
9053
|
+
statusDir: stringValue3(values, "status-dir")
|
|
9054
|
+
});
|
|
9055
|
+
if (result.ok) {
|
|
9056
|
+
process.stdout.write(`commander_status: connected
|
|
9057
|
+
`);
|
|
9058
|
+
process.stdout.write(`runner_id: ${result.record.runnerId}
|
|
9059
|
+
`);
|
|
9060
|
+
process.stdout.write(`pid: ${result.record.pid}
|
|
9061
|
+
`);
|
|
9062
|
+
return;
|
|
9063
|
+
}
|
|
9064
|
+
throw new Error(`commander did not connect: ${result.reason}`);
|
|
9065
|
+
}
|
|
9066
|
+
case "stop": {
|
|
9067
|
+
const values = strictFlagValues(rest);
|
|
9068
|
+
const runnerId = required(values, "runner-id");
|
|
9069
|
+
const status = stopCommanderDaemon(runnerId, stringValue3(values, "status-dir"));
|
|
9070
|
+
process.stdout.write(`${JSON.stringify(status)}
|
|
9071
|
+
`);
|
|
9072
|
+
return;
|
|
9073
|
+
}
|
|
9074
|
+
default:
|
|
9075
|
+
process.stdout.write(commanderDaemonHelpText());
|
|
9076
|
+
}
|
|
9077
|
+
}
|
|
8626
9078
|
async function runAuthCommand(args) {
|
|
8627
9079
|
const values = strictFlagValues(args);
|
|
8628
9080
|
if (values.get("help") === true) {
|
|
@@ -8766,8 +9218,9 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
8766
9218
|
const channelSlug = requiredStoredAgentChannel(tokenFile.channelId);
|
|
8767
9219
|
const listenUser = stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
|
|
8768
9220
|
const kandanUrl = stringValue3(values, "kandan-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
|
|
8769
|
-
const
|
|
8770
|
-
const
|
|
9221
|
+
const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
|
|
9222
|
+
const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
|
|
9223
|
+
const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? readConfiguredAllowedCwds() : assertConfiguredAllowedCwds([requestedCwd]);
|
|
8771
9224
|
const cwd = allowedCwds[0] ?? requestedCwd;
|
|
8772
9225
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
8773
9226
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
@@ -8820,7 +9273,7 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
8820
9273
|
};
|
|
8821
9274
|
}
|
|
8822
9275
|
function readAgentTokenTextFile(path) {
|
|
8823
|
-
return
|
|
9276
|
+
return existsSync10(path) ? readFileSync9(path, "utf8") : undefined;
|
|
8824
9277
|
}
|
|
8825
9278
|
function rejectAgentRunnerTargetingFlags(values) {
|
|
8826
9279
|
const unsupportedFlags = ["workspace", "channel", "token", "auth-file", "oauth-callback-host"].filter((flag) => values.has(flag));
|
|
@@ -8890,7 +9343,7 @@ async function parseRunnerArgs(args, deps = {
|
|
|
8890
9343
|
process.exit(0);
|
|
8891
9344
|
}
|
|
8892
9345
|
if (values.get("version") === true) {
|
|
8893
|
-
process.stdout.write(`linzumi 0.0.
|
|
9346
|
+
process.stdout.write(`linzumi 0.0.27-beta
|
|
8894
9347
|
`);
|
|
8895
9348
|
process.exit(0);
|
|
8896
9349
|
}
|
|
@@ -9001,6 +9454,32 @@ function splitStartArgs(args) {
|
|
|
9001
9454
|
}
|
|
9002
9455
|
return { cwdArg, flagArgs };
|
|
9003
9456
|
}
|
|
9457
|
+
function stripDaemonSupervisorFlags(args) {
|
|
9458
|
+
const stripped = [];
|
|
9459
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
9460
|
+
const arg = args[index];
|
|
9461
|
+
if (arg === undefined) {
|
|
9462
|
+
continue;
|
|
9463
|
+
}
|
|
9464
|
+
const key = arg.startsWith("--") ? arg.slice(2) : undefined;
|
|
9465
|
+
const definition = key === undefined ? undefined : flagDefinitions.get(key);
|
|
9466
|
+
if (key === "runner-id" || key === "log-file" || key === "status-dir") {
|
|
9467
|
+
if (definition?.kind === "value") {
|
|
9468
|
+
index += 1;
|
|
9469
|
+
}
|
|
9470
|
+
continue;
|
|
9471
|
+
}
|
|
9472
|
+
stripped.push(arg);
|
|
9473
|
+
if (definition?.kind === "value") {
|
|
9474
|
+
const next = args[index + 1];
|
|
9475
|
+
if (next !== undefined) {
|
|
9476
|
+
stripped.push(next);
|
|
9477
|
+
index += 1;
|
|
9478
|
+
}
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
return stripped;
|
|
9482
|
+
}
|
|
9004
9483
|
function rejectStartTargetingFlags(values) {
|
|
9005
9484
|
const unsupportedFlags = ["workspace", "channel"].filter((flag) => values.has(flag));
|
|
9006
9485
|
if (unsupportedFlags.length > 0) {
|
|
@@ -9009,12 +9488,12 @@ function rejectStartTargetingFlags(values) {
|
|
|
9009
9488
|
}
|
|
9010
9489
|
function resolveUserPath(pathValue) {
|
|
9011
9490
|
if (pathValue === "~") {
|
|
9012
|
-
return
|
|
9491
|
+
return homedir8();
|
|
9013
9492
|
}
|
|
9014
9493
|
if (pathValue.startsWith("~/")) {
|
|
9015
|
-
return
|
|
9494
|
+
return resolve8(homedir8(), pathValue.slice(2));
|
|
9016
9495
|
}
|
|
9017
|
-
return
|
|
9496
|
+
return resolve8(pathValue);
|
|
9018
9497
|
}
|
|
9019
9498
|
function parseChannelSession(values, token, target) {
|
|
9020
9499
|
if (target === undefined) {
|
|
@@ -9112,6 +9591,9 @@ Usage:
|
|
|
9112
9591
|
linzumi inbox <thread_id> --since-last
|
|
9113
9592
|
linzumi done <thread_id> --message <message>
|
|
9114
9593
|
linzumi init-hello-linzumi-demo-app
|
|
9594
|
+
linzumi codex start-new --title <title> --runner <runner_id> --cwd <path> --work-description <text>
|
|
9595
|
+
linzumi commander daemon --runner-id <id>
|
|
9596
|
+
linzumi commander wait --runner-id <id>
|
|
9115
9597
|
linzumi commander <folder> [options]
|
|
9116
9598
|
linzumi start <folder> [options]
|
|
9117
9599
|
linzumi paths list|add|remove [path]
|
|
@@ -9154,7 +9636,8 @@ Examples:
|
|
|
9154
9636
|
linzumi post thr_abc123 "PR is open"
|
|
9155
9637
|
linzumi done thr_abc123 --message "Done: https://github.com/example/repo/pull/1"
|
|
9156
9638
|
linzumi init-hello-linzumi-demo-app
|
|
9157
|
-
linzumi
|
|
9639
|
+
linzumi paths add ~/code/my-app
|
|
9640
|
+
linzumi commander daemon --runner-id launch-commander
|
|
9158
9641
|
linzumi start ~/
|
|
9159
9642
|
linzumi start ~/code/my-app
|
|
9160
9643
|
linzumi connect --workspace <your-workspace> --channel <your-channel> --launch-tui
|
|
@@ -9201,6 +9684,35 @@ Options:
|
|
|
9201
9684
|
--json Print machine-readable project details.
|
|
9202
9685
|
`;
|
|
9203
9686
|
}
|
|
9687
|
+
function commanderDaemonHelpText() {
|
|
9688
|
+
return `Linzumi Commander daemon
|
|
9689
|
+
|
|
9690
|
+
Usage:
|
|
9691
|
+
linzumi commander daemon --runner-id <id> [options]
|
|
9692
|
+
linzumi commander daemon <folder> --runner-id <id> [options]
|
|
9693
|
+
linzumi commander status --runner-id <id>
|
|
9694
|
+
linzumi commander wait --runner-id <id> [--timeout-ms <ms>]
|
|
9695
|
+
linzumi commander stop --runner-id <id>
|
|
9696
|
+
|
|
9697
|
+
What it does:
|
|
9698
|
+
Starts the workspace Commander as a detached process, writes a status file,
|
|
9699
|
+
and gives the Bootstrapper Codex explicit status and readiness commands. With
|
|
9700
|
+
no folder argument, the Commander reads trusted folders from
|
|
9701
|
+
~/.linzumi/config.json so the Linzumi UI can launch Codex sessions in any
|
|
9702
|
+
configured folder.
|
|
9703
|
+
|
|
9704
|
+
Options:
|
|
9705
|
+
--runner-id <id> Stable Commander id, required
|
|
9706
|
+
--status-dir <path> Status directory, default ~/.linzumi/commanders
|
|
9707
|
+
--log-file <path> Commander log path, default in the status dir
|
|
9708
|
+
--timeout-ms <ms> Wait timeout, default 30000
|
|
9709
|
+
|
|
9710
|
+
All normal Commander options such as --agent-token-file, --allowed-cwd,
|
|
9711
|
+
--forward-port, --sandbox, --approval-policy, --codex-bin, and
|
|
9712
|
+
--code-server-bin can be passed to the daemon command. Passing --allowed-cwd is
|
|
9713
|
+
an explicit override; otherwise the global daemon uses ~/.linzumi/config.json.
|
|
9714
|
+
`;
|
|
9715
|
+
}
|
|
9204
9716
|
function pathsHelpText() {
|
|
9205
9717
|
return `Linzumi trusted paths
|
|
9206
9718
|
|
|
@@ -9250,13 +9762,19 @@ function agentRunnerHelpText() {
|
|
|
9250
9762
|
|
|
9251
9763
|
Usage:
|
|
9252
9764
|
linzumi commander <folder> [options]
|
|
9765
|
+
linzumi commander daemon [options]
|
|
9766
|
+
linzumi commander daemon <folder> [options]
|
|
9767
|
+
linzumi commander status --runner-id <id>
|
|
9768
|
+
linzumi commander wait --runner-id <id>
|
|
9769
|
+
linzumi commander stop --runner-id <id>
|
|
9253
9770
|
linzumi agent runner <folder> [options]
|
|
9254
9771
|
|
|
9255
9772
|
What it does:
|
|
9256
9773
|
Starts this computer as the claimed agent's scoped Linzumi Commander. The command
|
|
9257
|
-
reads ~/.linzumi/agent-token.json, uses its workspace/channel scope,
|
|
9258
|
-
|
|
9259
|
-
recorded during claim unless --listen-user is
|
|
9774
|
+
reads ~/.linzumi/agent-token.json, uses its workspace/channel scope, reads
|
|
9775
|
+
trusted folders from ~/.linzumi/config.json when no folder is passed, and
|
|
9776
|
+
listens only to the owning human recorded during claim unless --listen-user is
|
|
9777
|
+
passed.
|
|
9260
9778
|
|
|
9261
9779
|
Options:
|
|
9262
9780
|
--agent-token-file <path> Agent token cache, default ~/.linzumi/agent-token.json
|
|
@@ -9270,11 +9788,12 @@ Options:
|
|
|
9270
9788
|
--sandbox <value> Sandbox metadata shown in Kandan
|
|
9271
9789
|
--approval-policy <value> Approval-policy metadata shown in Kandan
|
|
9272
9790
|
--forward-port <ports> Comma-separated local TCP ports Kandan may expose as previews
|
|
9273
|
-
--allowed-cwd <paths> Override
|
|
9791
|
+
--allowed-cwd <paths> Override ~/.linzumi/config.json or selected folder with comma-separated trusted roots
|
|
9274
9792
|
--fast Mark this Commander as low-latency/fast in Linzumi
|
|
9275
9793
|
|
|
9276
9794
|
Examples:
|
|
9277
|
-
linzumi
|
|
9795
|
+
linzumi paths add "$PWD"
|
|
9796
|
+
linzumi commander daemon --runner-id hello-world-commander
|
|
9278
9797
|
linzumi commander ~/code/my-app --kandan-url ws://127.0.0.1:4162 --runner-id local-qa-commander
|
|
9279
9798
|
`;
|
|
9280
9799
|
}
|
package/package.json
CHANGED