@linzumi/cli 0.0.11-beta → 0.0.12-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 CHANGED
@@ -1,21 +1,29 @@
1
1
  # Linzumi CLI
2
2
 
3
- Run an AI coding session on the code that is already on your computer, then
4
- open the editor and app preview from a shared browser workspace.
5
-
6
- Linzumi is for the common team workflow that is still too painful:
7
-
8
- - your branch is only on your laptop
9
- - your app only runs with your local setup, secrets, database, or devices
10
- - you want an AI agent to read, edit, run, and explain that code
11
- - you want a teammate to see the editor or preview without cloning the repo
12
- - you do not want to move the project into a cloud VM just to collaborate
13
-
14
- The CLI connects your machine to Kandan, Linzumi's shared browser workspace.
15
- Your code, terminal commands, dev servers, and editor runtime stay local. Kandan
16
- handles sign-in, sharing, HTTPS browser access, and permission checks.
17
-
18
- ## Try It
3
+ ```text
4
+ ▓▓╗ ▓▓╗▓▓▓╗ ▓▓╗▓▓▓▓▓▓▓╗▓▓╗ ▓▓╗▓▓▓╗ ▓▓▓╗▓▓╗
5
+ ▓▓║ ▓▓║▓▓▓▓╗ ▓▓║╚══▓▓▓╔╝▓▓║ ▓▓║▓▓▓▓╗ ▓▓▓▓║▓▓║
6
+ ▓▓║ ▓▓║▓▓╔▓▓╗ ▓▓║ ▓▓▓╔╝ ▓▓║ ▓▓║▓▓╔▓▓▓▓╔▓▓║▓▓║
7
+ ▓▓║ ▓▓║▓▓║╚▓▓╗▓▓║ ▓▓▓╔╝ ▓▓║ ▓▓║▓▓║╚▓▓╔╝▓▓║▓▓║
8
+ ▓▓▓▓▓▓▓░░░░░░░░╚▓▓▓▓║▓▓▓▓▓▓▓╗╚▓▓▓▓▓▓╔╝░░░░░░░░▓▓║▓▓║
9
+ ╚═══▒▒▒@@@@@@@@▒▒═══╝╚══════╝ ╚════▒▒▒@@@@@@@@▒▒╝╚═╝
10
+ ▒@@@@@@@@@@@@@@@@▒ ▒@@@@@@@@@@@@@@@@▒
11
+ @@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@
12
+ @@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@
13
+ ║║ ║║
14
+ ║║ ║║
15
+ ║║ ()-().----. ║║
16
+ ║║ \"/` ___ ;_________║║_.'
17
+ ║║ ` ^^ ^^ ║║
18
+ ──────────────╨╨─────────────··────··────────╨╨──────────────
19
+ multiplayer Codex, on your own code
20
+ ```
21
+
22
+ Linzumi turns a folder on your laptop into a shared workspace. You and your teammates open the same browser app, point AI coding agents at the real code on your machines, and watch each other work — no cloud VM, no pushing a branch just to show someone a preview.
23
+
24
+ Your code, your terminal, and your dev server stay on your laptop. Linzumi handles sign-in, sharing controls, and the secure browser link your teammates use to join you.
25
+
26
+ ## Quick start
19
27
 
20
28
  Use Chrome, Edge, Arc, Brave, or another Chromium-based browser.
21
29
 
@@ -24,191 +32,99 @@ npm install -g @linzumi/cli@beta
24
32
  linzumi start ~/code/my-app
25
33
  ```
26
34
 
27
- Then in the browser:
28
-
29
- ```text
30
- Explain this project, open the editor, and tell me how to run it.
31
- ```
32
-
33
- If the project has a dev server, start it locally:
34
-
35
- ```bash
36
- npm run dev
37
- ```
38
-
39
- Open the forwarded preview from Kandan. You should get a normal HTTPS URL that
40
- you can share with an approved teammate, without exposing a raw `localhost`
41
- address.
35
+ That is it. Here is what happens next:
42
36
 
43
- To pin this exact beta:
37
+ 1. Your browser opens to Linzumi.
38
+ 2. Sign in (or sign up — one click).
39
+ 3. Linzumi asks if it can connect to this computer. Click allow.
40
+ 4. Your computer shows up as available in your workspace.
41
+ 5. Type something in chat — like *"Explain this project and tell me how to run it."* — and Codex picks it up on your machine.
44
42
 
45
- ```bash
46
- npm install -g @linzumi/cli@0.0.11-beta
47
- linzumi start ~/code/my-app
48
- ```
43
+ The rest of this README is detail.
49
44
 
50
- ## What You Get
45
+ ## What you can do with it
51
46
 
52
- Linzumi turns your local project into a shared coding workspace:
47
+ - **Onboard yourself to a new repo.** Ask Codex to map the project for you what it does, where to start reading, how to run it.
48
+ - **Make a real change without context-switching.** Codex edits the files on your disk. You watch the diff land in chat, or jump into the browser editor (it is VS Code, in your browser, pointed at your folder).
49
+ - **Show someone what is on your screen.** Open the browser editor or share your local dev server through a normal HTTPS link. No exposing `localhost` to the internet, no copy-pasting IPs.
50
+ - **Work with a teammate on the same code.** They join your channel, see the same threads, and can start their own Codex run on their own machine.
53
51
 
54
- 1. Codex can inspect and edit the folder you allowed.
55
- 2. You can open a browser editor for that local folder.
56
- 3. Local app previews are reachable through Kandan HTTPS URLs.
57
- 4. Approved teammates can join the editor or preview.
58
- 5. Kandan enforces auth, sharing grants, and allowed-port policy.
52
+ ## Working as a team
59
53
 
60
- The important part: your project does not have to leave your computer for this
61
- to work.
54
+ This is the part that makes Linzumi different from running an AI coding agent alone in a terminal.
62
55
 
63
- ## What Runs Where
56
+ - **Your computers are always in sight.** Every machine you have run `linzumi start` on shows up as a runner in your workspace. From any channel you are in, you can see how many of your runners are reachable right now and which ones are listening on that channel.
64
57
 
65
- On your computer, the CLI starts a runner. The runner can:
58
+ - **Put Codex on the job from the channel.** Pick an available runner and trusted folder from the channel menu, enter what Codex should work on, and start the session. Linzumi asks that runner to attach a fresh Codex to the folder you picked, using the runner, folder, and Codex settings shown in the menu.
66
59
 
67
- - launch or connect to Codex
68
- - download the Kandan-approved editor runtime
69
- - start code-server for the folder you chose
70
- - connect to explicitly approved local ports
71
- - stream editor and preview traffic through Kandan
60
+ - **One Codex per thread.** Once a Codex picks up a thread, that thread belongs to it — no second Codex can step in and trample the work. (A future release will allow handing a thread off; for now the lock holds for the life of the thread.)
72
61
 
73
- In the browser, Kandan provides:
62
+ - **Browse what your computers have been up to.** Open the runners dropdown and you see, across all your devices, the most recently active threads — each with its title and a short AI-written summary of what is happening. Pop into any of them and keep working.
74
63
 
75
- - sign-in and workspace UI
76
- - local-runner status
77
- - editor and preview links
78
- - sharing controls
79
- - HTTPS termination
80
- - access checks before traffic reaches your machine
64
+ - **Chat with humans without waking up Codex.** Start a message with `&` and Codex will not see it. Use it for side conversations with teammates inside a thread, without nudging the agent.
81
65
 
82
- When docs mention `127.0.0.1` or `localhost` on the local-service hop, that
83
- means your computer, not the Kandan server.
84
-
85
- ## Requirements
86
-
87
- Install these before running the beta:
66
+ ## Pin a specific version
88
67
 
89
68
  ```bash
90
- node --version
91
- npm --version
92
- bun --version
93
- codex --version
94
- ```
95
-
96
- Expected:
97
-
98
- - Node.js 20 or newer
99
- - npm
100
- - Bun 1.2 or newer
101
- - Codex CLI
102
- - Chrome, Edge, Arc, Brave, or another Chromium-based browser
103
-
104
- Safari is semi-supported for now. Use Chromium for the best editor and
105
- collaboration behavior.
106
-
107
- ## First Run
108
-
109
- ```bash
110
- linzumi start ~/code/my-app
69
+ npm install -g @linzumi/cli@0.0.12-beta
70
+ linzumi --version
111
71
  ```
112
72
 
113
- What happens:
114
-
115
- 1. The CLI opens Kandan.
116
- 2. You sign up or sign in.
117
- 3. Kandan asks permission to connect this computer.
118
- 4. The CLI stores a scoped local-runner token.
119
- 5. The CLI checks Bun, Codex, and the Kandan editor runtime.
120
- 6. Kandan shows the computer as connected.
121
-
122
- The first editor launch can download a Kandan-approved runtime archive. Later
123
- runs reuse the verified runtime from:
124
-
125
- ```text
126
- ~/.linzumi/editor-runtimes
127
- ```
73
+ ## What You Get
128
74
 
129
- The production path does not use a random local `code-server` install. Kandan
130
- publishes a checksummed runtime manifest, and the CLI only advertises editor
131
- readiness after that runtime is verified locally.
75
+ Run an AI coding session on the code that is already on your computer. Linzumi is for the moments when you do not want to move the project into a cloud VM just to let teammates watch Codex work, open the browser editor, or review a forwarded local preview.
132
76
 
133
77
  ## Good Things To Try
134
78
 
135
- Ask Codex to map the project:
136
-
137
- ```text
138
- What does this app do? Where should I start reading?
139
- ```
140
-
141
- Ask Codex to make a small change:
142
-
143
- ```text
144
- Find the main settings page and add a clear empty state.
145
- ```
79
+ - Start in a repo and ask Codex to explain how to run it.
80
+ - Open the Codex editor from the runner controls once the folder is trusted.
81
+ - Forward a local dev server and share the preview with a teammate.
146
82
 
147
- Open the editor from Kandan and inspect the changed files.
83
+ ## Trusted folders
148
84
 
149
- Start a local dev server:
85
+ Linzumi only starts Codex or opens the Codex editor inside folders you trust. The default trusted folder list lives at `~/.linzumi/config.json`.
150
86
 
151
87
  ```bash
152
- npm run dev
88
+ linzumi paths list
89
+ linzumi paths add ~/code/my-app
90
+ linzumi paths remove ~/code/my-app
153
91
  ```
154
92
 
155
- If a port is not auto-detected, restart with an explicit approved port:
93
+ `linzumi connect` uses those trusted folders by default. Pass `--allowed-cwd <paths>` when you want one runner process to use an explicit comma-separated folder list instead of `~/.linzumi/config.json`.
156
94
 
157
- ```bash
158
- linzumi start ~/code/my-app --forward-port 3000
159
- ```
95
+ ## What you need installed
160
96
 
161
- For a collaboration test, invite another user, open the same file in the local
162
- editor, and type in Chromium. Remote cursor labels and selections should be
163
- visible.
97
+ - Node.js 20 or newer
98
+ - Bun 1.2 or newer
99
+ - The Codex CLI
100
+ - A Chromium-based browser (Chrome, Edge, Arc, or Brave)
164
101
 
165
- ## Hosted Kandan
102
+ Safari is semi-supported. Live editing and live collaboration are smoother in Chromium.
166
103
 
167
- For a hosted Kandan deployment, point the CLI at the hosted websocket URL:
104
+ ## If something looks wrong
168
105
 
169
- ```bash
170
- linzumi start ~/code/my-app --kandan-url wss://<your-kandan-host>
171
- ```
106
+ - **`linzumi: command not found`** — your global npm bin folder is not on `PATH`. Run `npm prefix -g` and add the `bin` folder under that path to your shell `PATH`.
107
+ - **`bun: command not found`** — install Bun, then rerun `linzumi start`.
108
+ - **`codex: command not found`** — install or configure the Codex CLI, or pass `--codex-bin <path>` to `linzumi start`.
109
+ - **The browser sign-in opens but never returns to the CLI** — your browser cannot reach the address the CLI is listening on. Pass `--oauth-callback-host <ip-or-host-your-browser-can-reach>`.
110
+ - **The browser editor never says it is ready** — rerun `linzumi start` and watch the console for the editor download step. Slow networks can make the very first launch take a minute or two.
111
+ - **Collaboration looks off in Safari** — switch to a Chromium browser for now.
172
112
 
173
- For Render-hosted Kandan, public TLS should work without local certificate
174
- flags. The CLI derives the HTTPS API origin from the websocket URL for OAuth,
175
- runtime manifest download, and runtime archive download.
113
+ ## Advanced
176
114
 
177
- For local development with a private CA:
115
+ Most people never need anything in this section.
178
116
 
179
- ```bash
180
- KANDAN_TLS_CA_FILE=/path/to/ca.crt linzumi start ~/code/my-app \
181
- --kandan-url wss://linzumi.io:4140
182
- ```
183
-
184
- ## Tailscale Development
185
-
186
- If your browser needs to reach a local Kandan server through Tailscale:
117
+ ### Point at a self-hosted Linzumi
187
118
 
188
119
  ```bash
189
- linzumi start ~/code/my-app \
190
- --kandan-url ws://100.71.192.98:4162 \
191
- --oauth-callback-host 100.71.192.98
120
+ linzumi start ~/code/my-app --kandan-url wss://your-host
192
121
  ```
193
122
 
194
- Use your own Tailscale IP.
195
-
196
- ## Commands
197
-
198
- ```bash
199
- linzumi
200
- linzumi --help
201
- linzumi --version
202
- linzumi start <folder>
203
- linzumi connect --help
204
- linzumi connect [runner options]
205
- linzumi auth [auth options]
206
- ```
123
+ Public TLS hosts work without extra flags. For local development against a private CA, set `KANDAN_TLS_CA_FILE` to your CA bundle and pass `--kandan-url` as usual.
207
124
 
208
- Most people should use `linzumi start`.
125
+ ### Lower-level connect
209
126
 
210
- `linzumi connect` is the lower-level command for connecting to an explicit
211
- workspace and channel:
127
+ `linzumi start` is what you want almost every time. The lower-level form, useful when you already know your workspace and channel:
212
128
 
213
129
  ```bash
214
130
  linzumi connect \
@@ -218,71 +134,29 @@ linzumi connect \
218
134
  --cwd ~/code/my-app
219
135
  ```
220
136
 
221
- ## Useful Options
137
+ ### All the flags
222
138
 
223
- ```bash
224
- --kandan-url <ws-url> Kandan websocket URL
225
- --oauth-callback-host <ip> Callback host reachable by your browser
139
+ ```text
140
+ --kandan-url <ws-url> Linzumi base URL (defaults to the hosted service)
141
+ --oauth-callback-host <ip> Sign-in callback host your browser can reach
226
142
  --runner-id <id> Stable id for this computer
227
- --codex-bin <path> Codex executable, default codex
228
- --model <name> Codex model metadata shown in Kandan
229
- --reasoning-effort <value> Codex reasoning metadata shown in Kandan
143
+ --codex-bin <path> Codex executable, default `codex`
144
+ --model <name> Codex model metadata shown in Linzumi
145
+ --reasoning-effort <value> Codex reasoning metadata shown in Linzumi
230
146
  --fast Mark this runner as low-latency
231
- --forward-port <ports> Comma-separated local ports Kandan may expose
232
- --allowed-cwd <paths> Comma-separated roots Kandan may use
147
+ --forward-port <ports> Comma-separated local ports Linzumi may share
148
+ --allowed-cwd <paths> Override ~/.linzumi/config.json with comma-separated trusted roots
233
149
  --log-file <path> JSONL runner event log
234
150
  ```
235
151
 
236
- `--code-server-bin` exists only as a development override. It is not the
237
- supported production editor path.
238
-
239
- ## Troubleshooting
240
-
241
- Check the CLI version:
242
-
243
- ```bash
244
- linzumi --version
245
- ```
246
-
247
- Expected:
248
-
249
- ```text
250
- linzumi 0.0.11-beta
251
- ```
252
-
253
- If `linzumi` is not found, your global npm bin directory is not on `PATH`.
254
-
255
- If `bun` is not found, install Bun and rerun `linzumi start`.
152
+ ### Tailscale
256
153
 
257
- If `codex` is not found, install or configure the Codex CLI, or pass
258
- `--codex-bin`.
259
-
260
- If OAuth opens but does not return to the CLI, pass an
261
- `--oauth-callback-host` that your browser can reach.
262
-
263
- If the editor does not become ready, do not install a local code-server package
264
- as a workaround. The server-managed runtime must download and verify cleanly.
265
- Rerun the CLI and check the runner log.
266
-
267
- If collaboration behaves oddly in Safari, retry in Chromium. Safari is currently
268
- semi-supported.
269
-
270
- ## For Kandan Release Engineers
271
-
272
- The production contract is:
273
-
274
- 1. Build the server-approved editor runtime archive.
275
- 2. Publish the manifest and archive from Kandan.
276
- 3. Publish the CLI beta.
277
- 4. The CLI downloads only the approved runtime archive.
278
- 5. The CLI verifies the archive SHA before advertising editor readiness.
279
-
280
- For local package verification:
154
+ If your browser reaches your local Linzumi server through Tailscale, pass both your Tailscale IP for the URL and for the sign-in callback:
281
155
 
282
156
  ```bash
283
- bun test
284
- npm pack --dry-run
157
+ linzumi start ~/code/my-app \
158
+ --kandan-url ws://100.71.192.98:4162 \
159
+ --oauth-callback-host 100.71.192.98
285
160
  ```
286
161
 
287
- The npm package must include this README, `bin/linzumi.js`, and the `src`
288
- runtime files.
162
+ Use your own Tailscale IP, not that one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.11-beta",
3
+ "version": "0.0.12-beta",
4
4
  "description": "Connect your computer to Kandan for local Codex sessions, editors, and forwarded previews",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1171,6 +1171,20 @@ async function handleKandanChatEvent(
1171
1171
  return;
1172
1172
  }
1173
1173
 
1174
+ if (event.body.trimStart().startsWith("&")) {
1175
+ args.log("kandan.message_ignored", {
1176
+ seq: event.seq,
1177
+ actor_slug: event.actorSlug ?? null,
1178
+ actor_user_id: event.actorUserId ?? null,
1179
+ reason: "human_only",
1180
+ });
1181
+ await publishKandanMessageState(args, event, {
1182
+ status: "ignored",
1183
+ reason: "human_only",
1184
+ });
1185
+ return;
1186
+ }
1187
+
1174
1188
  const portForwardDecision = parsePortForwardDecision(event.body);
1175
1189
  if (portForwardDecision !== undefined) {
1176
1190
  const result = await resolvePendingPortForwardRequest(args, state, payloadContext, {
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  capability metadata without creating an implicit tunnel.
23
23
  */
24
24
  import { randomUUID } from "node:crypto";
25
+ import { realpathSync } from "node:fs";
25
26
  import { homedir } from "node:os";
26
27
  import { resolve } from "node:path";
27
28
  import { runLocalCodexRunner, type RunnerOptions } from "./runner";
@@ -30,9 +31,17 @@ import { resolveLocalRunnerToken } from "./authResolution";
30
31
  import { identityFromAccessToken } from "./channelSessionSupport";
31
32
  import {
32
33
  assertConfiguredAllowedCwds,
34
+ expandUserPath,
33
35
  parseAllowedCwdList,
34
36
  parseAllowedPortList,
35
37
  } from "./localCapabilities";
38
+ import {
39
+ addAllowedCwd,
40
+ localConfigPath,
41
+ readConfiguredAllowedCwds,
42
+ readLocalConfig,
43
+ removeAllowedCwd,
44
+ } from "./localConfig";
36
45
  import {
37
46
  acquireLocalRunnerTokenDetails,
38
47
  fetchLocalRunnerStartTarget,
@@ -101,11 +110,14 @@ async function main(args: readonly string[]): Promise<void> {
101
110
  process.stdout.write(connectGuideText());
102
111
  return;
103
112
  case "version":
104
- process.stdout.write("linzumi 0.0.11-beta\n");
113
+ process.stdout.write("linzumi 0.0.12-beta\n");
105
114
  return;
106
115
  case "auth":
107
116
  await runAuthCommand(parsed.args);
108
117
  return;
118
+ case "paths":
119
+ runPathsCommand(parsed.args);
120
+ return;
109
121
  case "start": {
110
122
  const options = await parseStartRunnerArgs(parsed.args);
111
123
  await runLocalCodexRunner(options);
@@ -123,6 +135,7 @@ type ParsedCommand =
123
135
  | { readonly command: "guide"; readonly args: readonly string[] }
124
136
  | { readonly command: "version"; readonly args: readonly string[] }
125
137
  | { readonly command: "auth"; readonly args: readonly string[] }
138
+ | { readonly command: "paths"; readonly args: readonly string[] }
126
139
  | { readonly command: "start"; readonly args: readonly string[] }
127
140
  | { readonly command: "run"; readonly args: readonly string[] };
128
141
 
@@ -143,6 +156,8 @@ function parseCommand(args: readonly string[]): ParsedCommand {
143
156
  return { command: "run", args: ["--help"] };
144
157
  case "auth":
145
158
  return { command: "auth", args: rest };
159
+ case "paths":
160
+ return { command: "paths", args: rest };
146
161
  case "start":
147
162
  return { command: "start", args: rest };
148
163
  case "run":
@@ -152,6 +167,53 @@ function parseCommand(args: readonly string[]): ParsedCommand {
152
167
  }
153
168
  }
154
169
 
170
+ function runPathsCommand(args: readonly string[]): void {
171
+ const [subcommand, pathValue, ...rest] = args;
172
+
173
+ if (subcommand === undefined || subcommand === "help" || subcommand === "--help") {
174
+ process.stdout.write(pathsHelpText());
175
+ return;
176
+ }
177
+
178
+ if (rest.length > 0) {
179
+ throw new Error("linzumi paths accepts one path argument");
180
+ }
181
+
182
+ switch (subcommand) {
183
+ case "list": {
184
+ const config = readLocalConfig();
185
+ if (config.allowedCwds.length === 0) {
186
+ process.stdout.write(`No trusted paths configured in ${localConfigPath()}\n`);
187
+ return;
188
+ }
189
+
190
+ process.stdout.write(`${config.allowedCwds.join("\n")}\n`);
191
+ return;
192
+ }
193
+ case "add": {
194
+ if (pathValue === undefined || pathValue.trim() === "") {
195
+ throw new Error("missing path for linzumi paths add");
196
+ }
197
+
198
+ const trustedPath = realpathSync(resolve(expandUserPath(pathValue)));
199
+ addAllowedCwd(pathValue);
200
+ process.stdout.write(`Trusted ${trustedPath}\n`);
201
+ return;
202
+ }
203
+ case "remove": {
204
+ if (pathValue === undefined || pathValue.trim() === "") {
205
+ throw new Error("missing path for linzumi paths remove");
206
+ }
207
+
208
+ removeAllowedCwd(pathValue);
209
+ process.stdout.write(`Removed trusted path ${pathValue}\n`);
210
+ return;
211
+ }
212
+ default:
213
+ throw new Error(`invalid paths command: ${subcommand}`);
214
+ }
215
+ }
216
+
155
217
  async function runAuthCommand(args: readonly string[]): Promise<void> {
156
218
  const values = strictFlagValues(args);
157
219
 
@@ -210,7 +272,9 @@ export async function parseStartRunnerArgs(
210
272
 
211
273
  const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.kandanai.com";
212
274
  const requestedCwd = resolveUserPath(cwdArg ?? process.cwd());
213
- const allowedCwds = assertConfiguredAllowedCwds([requestedCwd]);
275
+ const allowedCwds = values.has("allowed-cwd")
276
+ ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
277
+ : assertConfiguredAllowedCwds([requestedCwd]);
214
278
  const cwd = allowedCwds[0] ?? requestedCwd;
215
279
  const codexBin = stringValue(values, "codex-bin") ?? "codex";
216
280
  const customCodeServerBin = stringValue(values, "code-server-bin");
@@ -347,7 +411,7 @@ export async function parseRunnerArgs(
347
411
  }
348
412
 
349
413
  if (values.get("version") === true) {
350
- process.stdout.write("linzumi 0.0.11-beta\n");
414
+ process.stdout.write("linzumi 0.0.12-beta\n");
351
415
  process.exit(0);
352
416
  }
353
417
 
@@ -395,9 +459,9 @@ export async function parseRunnerArgs(
395
459
  launchTui: values.get("launch-tui") === true,
396
460
  fast: values.get("fast") === true,
397
461
  logFile: stringValue(values, "log-file"),
398
- allowedCwds: assertConfiguredAllowedCwds(
399
- parseAllowedCwdList(stringValue(values, "allowed-cwd")),
400
- ),
462
+ allowedCwds: values.has("allowed-cwd")
463
+ ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
464
+ : readConfiguredAllowedCwds(),
401
465
  allowedForwardPorts: parseAllowedPortList(
402
466
  stringValue(values, "forward-port"),
403
467
  ),
@@ -647,6 +711,7 @@ function helpText(): string {
647
711
  Usage:
648
712
  linzumi
649
713
  linzumi start <folder> [options]
714
+ linzumi paths list|add|remove [path]
650
715
  linzumi connect --kandan-url <ws-url> --workspace <slug> --channel <slug> [options]
651
716
  linzumi auth --kandan-url <ws-url> [--workspace <slug> --channel <slug>]
652
717
 
@@ -686,6 +751,8 @@ Examples:
686
751
  linzumi connect --kandan-url wss://serve.kandanai.com --workspace linzumi --channel seans-playground --codex-bin codex --model gpt-5.5 --reasoning-effort low --fast --launch-tui
687
752
  linzumi auth --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
688
753
  linzumi auth --kandan-url ws://100.71.192.98:4160 --oauth-callback-host 100.71.192.98 --workspace default --channel seans-playground
754
+ linzumi paths add ~/code/linzumi
755
+ linzumi paths list
689
756
  linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --workspace default --channel seans-playground --cwd /tmp/kandan-runner-a
690
757
  linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
691
758
  linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --channel default/seans-playground --listen-user all --launch-tui
@@ -705,6 +772,19 @@ Examples:
705
772
  `;
706
773
  }
707
774
 
775
+ function pathsHelpText(): string {
776
+ return `Linzumi trusted paths
777
+
778
+ Usage:
779
+ linzumi paths list
780
+ linzumi paths add <path>
781
+ linzumi paths remove <path>
782
+
783
+ Trusted paths are stored in ~/.linzumi/config.json. linzumi connect uses them
784
+ unless --allowed-cwd is passed for that runner process.
785
+ `;
786
+ }
787
+
708
788
  function startHelpText(): string {
709
789
  return `Linzumi one-command local runner
710
790
 
@@ -748,6 +828,7 @@ This opens Kandan in your browser, creates or reuses your personal coding
748
828
  space, and starts this computer as a local Codex runner.
749
829
 
750
830
  Advanced:
831
+ linzumi paths add "$PWD"
751
832
  linzumi connect
752
833
 
753
834
  Connect this computer to Kandan as a local Codex runner.
@@ -7,6 +7,7 @@
7
7
  forwarding is advertised only for explicitly configured local ports.
8
8
  */
9
9
  import { realpathSync } from "node:fs";
10
+ import { homedir } from "node:os";
10
11
  import { isAbsolute, relative, resolve } from "node:path";
11
12
 
12
13
  export type CwdCapabilityDecision =
@@ -66,13 +67,25 @@ export function assertConfiguredAllowedCwds(
66
67
  ): string[] {
67
68
  return paths.map((path) => {
68
69
  try {
69
- return realpathSync(resolve(path));
70
+ return realpathSync(resolve(expandUserPath(path)));
70
71
  } catch (_error) {
71
72
  throw new Error(`invalid --allowed-cwd: ${path} does not exist`);
72
73
  }
73
74
  });
74
75
  }
75
76
 
77
+ export function expandUserPath(pathValue: string): string {
78
+ if (pathValue === "~") {
79
+ return homedir();
80
+ }
81
+
82
+ if (pathValue.startsWith("~/")) {
83
+ return resolve(homedir(), pathValue.slice(2));
84
+ }
85
+
86
+ return pathValue;
87
+ }
88
+
76
89
  export function resolveAllowedCwd(
77
90
  requestedCwd: string | undefined,
78
91
  allowedRoots: readonly string[],
@@ -0,0 +1,99 @@
1
+ /*
2
+ - Date: 2026-05-01
3
+ Spec: ../../kandan/server_v2/plans/2026-05-01-runner-editor-dropdown-and-thread-controls.md
4
+ Relationship: Owns the npm-first local runner's trusted-folder config at
5
+ ~/.linzumi/config.json so README path-management commands are product truth.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { dirname, resolve } from "node:path";
10
+ import { expandUserPath } from "./localCapabilities";
11
+
12
+ export type LinzumiConfig = {
13
+ readonly version: 1;
14
+ readonly allowedCwds: readonly string[];
15
+ };
16
+
17
+ export function localConfigPath(env: NodeJS.ProcessEnv = process.env): string {
18
+ const override = env.LINZUMI_CONFIG_FILE;
19
+
20
+ return override !== undefined && override.trim() !== ""
21
+ ? resolve(expandUserPath(override))
22
+ : resolve(homedir(), ".linzumi", "config.json");
23
+ }
24
+
25
+ export function readLocalConfig(path: string = localConfigPath()): LinzumiConfig {
26
+ if (!existsSync(path)) {
27
+ return { version: 1, allowedCwds: [] };
28
+ }
29
+
30
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
31
+
32
+ if (!isConfigPayload(parsed)) {
33
+ throw new Error(`invalid Linzumi config: ${path}`);
34
+ }
35
+
36
+ return {
37
+ version: 1,
38
+ allowedCwds: uniqueStrings(parsed.allowedCwds),
39
+ };
40
+ }
41
+
42
+ export function readConfiguredAllowedCwds(path: string = localConfigPath()): string[] {
43
+ return readLocalConfig(path).allowedCwds.map((cwd) => {
44
+ try {
45
+ return realpathSync(resolve(expandUserPath(cwd)));
46
+ } catch (_error) {
47
+ throw new Error(`invalid Linzumi config allowed path: ${cwd} does not exist`);
48
+ }
49
+ });
50
+ }
51
+
52
+ export function addAllowedCwd(pathValue: string, path: string = localConfigPath()): string[] {
53
+ const normalizedPath = realpathSync(resolve(expandUserPath(pathValue)));
54
+ const config = readLocalConfig(path);
55
+ const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
56
+ writeLocalConfig({ version: 1, allowedCwds }, path);
57
+ return allowedCwds;
58
+ }
59
+
60
+ export function removeAllowedCwd(pathValue: string, path: string = localConfigPath()): string[] {
61
+ const requestedPath = resolve(expandUserPath(pathValue));
62
+ const normalizedRequest = realpathOrResolved(requestedPath);
63
+ const config = readLocalConfig(path);
64
+ const allowedCwds = config.allowedCwds.filter((cwd) => {
65
+ const normalizedExisting = realpathOrResolved(cwd);
66
+ return cwd !== pathValue && normalizedExisting !== normalizedRequest;
67
+ });
68
+ writeLocalConfig({ version: 1, allowedCwds }, path);
69
+ return allowedCwds;
70
+ }
71
+
72
+ export function writeLocalConfig(config: LinzumiConfig, path: string = localConfigPath()): void {
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
75
+ }
76
+
77
+ function isConfigPayload(value: unknown): value is LinzumiConfig {
78
+ return (
79
+ typeof value === "object" &&
80
+ value !== null &&
81
+ (value as { readonly version?: unknown }).version === 1 &&
82
+ Array.isArray((value as { readonly allowedCwds?: unknown }).allowedCwds) &&
83
+ (value as { readonly allowedCwds: readonly unknown[] }).allowedCwds.every(
84
+ (cwd) => typeof cwd === "string" && cwd.trim() !== "",
85
+ )
86
+ );
87
+ }
88
+
89
+ function uniqueStrings(values: readonly string[]): string[] {
90
+ return [...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))];
91
+ }
92
+
93
+ function realpathOrResolved(pathValue: string): string {
94
+ try {
95
+ return realpathSync(resolve(expandUserPath(pathValue)));
96
+ } catch (_error) {
97
+ return resolve(expandUserPath(pathValue));
98
+ }
99
+ }
@@ -592,7 +592,7 @@ export function prepareLocalEditorCollaboration(
592
592
  return undefined;
593
593
  }
594
594
 
595
- const targetPath = `/local-codex-runner-targets/${encodeURIComponent(runnerId)}/forwards/${serverPort}`;
595
+ const targetPath = `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${serverPort}/preview-target`;
596
596
  const serverUrl = new URL(targetPath, `${browserBaseUrl}/`).toString();
597
597
  const bootstrapServerUrl =
598
598
  collaboration.bootstrapToken === undefined || collaboration.bootstrapToken === ""
package/src/protocol.ts CHANGED
@@ -133,6 +133,7 @@ export type KandanControl =
133
133
  readonly type: "start_instance";
134
134
  readonly instanceId?: string;
135
135
  readonly cwd?: string;
136
+ readonly workDescription?: string;
136
137
  readonly launchTui?: boolean;
137
138
  readonly model?: string;
138
139
  readonly reasoningEffort?: string;
package/src/runner.ts CHANGED
@@ -60,7 +60,6 @@ import { join } from "node:path";
60
60
  import { attachChannelSession } from "./channelSession";
61
61
  import { connectCodexAppServer, startCodexAppServer } from "./codexAppServer";
62
62
  import { arrayValue, integerValue, objectValue, stringValue } from "./json";
63
- import { connectForwardTunnel } from "./forwardTunnel";
64
63
  import { resolveAllowedCwd } from "./localCapabilities";
65
64
  import {
66
65
  createForwardWebSocketManager,
@@ -82,6 +81,7 @@ import {
82
81
  import { connectPhoenixClient } from "./phoenix";
83
82
  import {
84
83
  type JsonObject,
84
+ type JsonRpcResponse,
85
85
  type JsonValue,
86
86
  type KandanChannelSessionOptions,
87
87
  type KandanControl,
@@ -183,8 +183,6 @@ async function openLocalCodexRunner(
183
183
  allowedPorts: Array.from(liveForwardPorts).sort(
184
184
  (left, right) => left - right,
185
185
  ),
186
- streamingForwarding: true,
187
- streamingForwardingVersion: 1,
188
186
  toolStatus:
189
187
  options.dependencyStatus === undefined
190
188
  ? null
@@ -209,18 +207,11 @@ async function openLocalCodexRunner(
209
207
  const joinPayload = (): JsonObject => ({
210
208
  clientName: "kandan-local-codex-runner",
211
209
  version: "0.0.1",
210
+ workspace: options.channelSession?.workspaceSlug ?? null,
211
+ channel: options.channelSession?.channelSlug ?? null,
212
212
  capabilities: capabilitiesPayload(),
213
213
  });
214
214
  await kandan.join(topic, joinPayload(), { rejoinPayload: joinPayload });
215
- const forwardTunnel = await connectForwardTunnel({
216
- kandanUrl: options.kandanUrl,
217
- token: options.token,
218
- runnerId: options.runnerId,
219
- allowedPorts: () => Array.from(liveForwardPorts),
220
- log,
221
- socketFactory: options.socketFactory,
222
- });
223
- cleanup.actions.push(() => forwardTunnel.close());
224
215
 
225
216
  const started =
226
217
  options.codexUrl === undefined
@@ -661,6 +652,22 @@ async function discoverCodexThreads(
661
652
  .filter((thread) => thread.id !== "");
662
653
  }
663
654
 
655
+ function extractStartedThreadId(response: JsonRpcResponse): string | undefined {
656
+ if ("error" in response) {
657
+ return undefined;
658
+ }
659
+
660
+ return stringValue(objectValue(objectValue(response.result)?.thread)?.id);
661
+ }
662
+
663
+ function normalizedWorkDescription(value: string | undefined): string | undefined {
664
+ const normalized = value?.trim();
665
+
666
+ return normalized === undefined || normalized === ""
667
+ ? undefined
668
+ : normalized;
669
+ }
670
+
664
671
  function makeRunnerLogger(options: RunnerOptions): RunnerLogger {
665
672
  return createRunnerLogger(
666
673
  options.logFile ?? join(options.cwd, ".kandan-local-codex-runner.log"),
@@ -808,6 +815,15 @@ async function applyControl(
808
815
  ...(control.sandbox === undefined ? {} : { sandbox: control.sandbox }),
809
816
  ...(control.fast === true ? { serviceTier: "fast" } : {}),
810
817
  });
818
+ const codexThreadId = extractStartedThreadId(response);
819
+ const workDescription = normalizedWorkDescription(control.workDescription);
820
+
821
+ if (codexThreadId !== undefined && workDescription !== undefined) {
822
+ await codex.request("turn/start", {
823
+ threadId: codexThreadId,
824
+ input: [{ type: "text", text: workDescription }],
825
+ });
826
+ }
811
827
 
812
828
  return {
813
829
  instanceId,