@linzumi/cli 0.0.25-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.
Files changed (3) hide show
  1. package/README.md +51 -41
  2. package/dist/index.js +547 -28
  3. 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 -- 'Get me up and running with Linzumi from https://linzumi.com/agents.md. Read it with curl.'
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
- starts that hot-reload app on your computer, creates the shared support
32
- channel, starts a Linzumi Codex session in a work thread, and opens the
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 runner in your workspace, and
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`, start its hot-reload Node server, start the
119
- Commander for that folder, start a Linzumi Codex session in a work thread, and
120
- open the browser editor. The Linzumi Codex session then adds confetti to the
121
- demo page while you watch. Workspace names are plain display names from 2 to
122
- 100 characters; Linzumi generates the URL-safe workspace slug.
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
- (cd "$project_dir" && npm run dev > "$project_dir/dev.log" 2>&1 &)
137
- npx -y @linzumi/cli@latest commander "$project_dir" \
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, trusts only the selected
147
- folder, marks that same folder trusted in Codex's normal project config so
148
- Codex does not stop for an interactive trust prompt, advertises the explicit
149
- preview port, and listens only to the approving human unless `--listen-user` is
150
- explicitly passed. Use a unique
151
- Commander id per launch thread; Linzumi stores trusted-folder config per
152
- Commander id, so reusing an old fixed id can pick up stale allowed-cwd config.
153
- The bootstrap agent waits for `Runner connected:` before starting Codex in the Linzumi
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 start
171
- Codex and open the browser editor for the same thread and folder:
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 <thread_id> \
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 "Work only in the canonical Hello Linzumi project directory printed by pwd -P for /tmp/hello_linzumi. Add tasteful confetti when the Hello Linzumi page loads, keep the hot-reload app working on port 8787, and post the exact files changed."
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
- npx -y @linzumi/cli@latest editor open <thread_id> \
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 runner.** Your laptop, listed in the workspace, advertising
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 runners
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 runner. From any channel, you can
223
- see how many of your runners are reachable right now and which ones
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 runner
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 attaches a fresh Codex to that runner
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 runners dropdown
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 runner to start
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.25-beta
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 runner as low-latency in the workspace
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 runner event log
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-runner orchestration, custom Codex deployments, and CI
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 existsSync9, readFileSync as readFileSync8, realpathSync as realpathSync5 } from "node:fs";
4
- import { homedir as homedir7 } from "node:os";
5
- import { resolve as resolve7 } from "node:path";
6
- import { fileURLToPath as fileURLToPath2 } from "node:url";
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";
@@ -4244,9 +4244,9 @@ trust_level = "trusted"
4244
4244
  }
4245
4245
  const table = config.slice(range.start, range.end);
4246
4246
  const trustLine = /^(\s*trust_level\s*=\s*).*(\r?)$/m;
4247
- const nextTable = trustLine.test(table) ? table.replace(trustLine, '$1"trusted"$2') : table.replace(/\r?\n/, `
4247
+ const nextTable = trustLine.test(table) ? table.replace(trustLine, '$1"trusted"$2') : `${trimTrailingNewlines(table)}
4248
4248
  trust_level = "trusted"
4249
- `);
4249
+ `;
4250
4250
  return `${config.slice(0, range.start)}${nextTable}${config.slice(range.end)}`;
4251
4251
  }
4252
4252
  function findTableRange(config, header) {
@@ -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 fileURLToPath2(import.meta.url) === resolve7(scriptPath);
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.25-beta
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(resolve7(expandUserPath(pathValue)));
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 requestedCwd = resolveUserPath(cwdArg ?? stringValue3(values, "cwd") ?? process.cwd());
8770
- const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : assertConfiguredAllowedCwds([requestedCwd]);
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 existsSync9(path) ? readFileSync8(path, "utf8") : undefined;
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.25-beta
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 homedir7();
9491
+ return homedir8();
9013
9492
  }
9014
9493
  if (pathValue.startsWith("~/")) {
9015
- return resolve7(homedir7(), pathValue.slice(2));
9494
+ return resolve8(homedir8(), pathValue.slice(2));
9016
9495
  }
9017
- return resolve7(pathValue);
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 commander ~/code/my-app --runner-id launch-commander
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, trusts
9258
- only the selected folder by default, and listens only to the owning human
9259
- recorded during claim unless --listen-user is passed.
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 the selected folder with comma-separated trusted roots
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 commander "$PWD" --runner-id hello-world-commander
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.25-beta",
3
+ "version": "0.0.27-beta",
4
4
  "description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {