@linzumi/cli 0.0.4-beta → 0.0.5-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 +3 -3
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/phoenix.ts +17 -1
- package/src/runner.ts +24 -0
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ runner. It wraps the same local runner that was previously started with
|
|
|
27
27
|
Install the exact beta version:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npm install -g @linzumi/cli@0.0.
|
|
30
|
+
npm install -g @linzumi/cli@0.0.5-beta
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Or install the current beta tag:
|
|
@@ -47,7 +47,7 @@ linzumi --version
|
|
|
47
47
|
Expected CLI output:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
linzumi 0.0.
|
|
50
|
+
linzumi 0.0.5-beta
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
## Prod Quick Start
|
|
@@ -55,7 +55,7 @@ linzumi 0.0.4-beta
|
|
|
55
55
|
For the Linzumi workspace in prod, this is the first command to try:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npm install -g @linzumi/cli@0.0.
|
|
58
|
+
npm install -g @linzumi/cli@0.0.5-beta
|
|
59
59
|
|
|
60
60
|
linzumi connect \
|
|
61
61
|
--kandan-url wss://serve.kandanai.com \
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -66,7 +66,7 @@ async function main(args: readonly string[]): Promise<void> {
|
|
|
66
66
|
process.stdout.write(connectGuideText());
|
|
67
67
|
return;
|
|
68
68
|
case "version":
|
|
69
|
-
process.stdout.write("linzumi 0.0.
|
|
69
|
+
process.stdout.write("linzumi 0.0.5-beta\n");
|
|
70
70
|
return;
|
|
71
71
|
case "auth":
|
|
72
72
|
await runAuthCommand(parsed.args);
|
|
@@ -144,7 +144,7 @@ export async function parseRunnerArgs(args: readonly string[]): Promise<RunnerOp
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
if (values.get("version") === true) {
|
|
147
|
-
process.stdout.write("linzumi 0.0.
|
|
147
|
+
process.stdout.write("linzumi 0.0.5-beta\n");
|
|
148
148
|
process.exit(0);
|
|
149
149
|
}
|
|
150
150
|
|
package/src/phoenix.ts
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
|
|
14
14
|
Relationship: Treats the Kandan websocket as a reconnectable transport so
|
|
15
15
|
server restarts do not terminate the durable local Codex session.
|
|
16
|
+
|
|
17
|
+
- Date: 2026-04-26
|
|
18
|
+
Spec: plans/2026_04_26_linzumi_cli_review_followups_note.md
|
|
19
|
+
Relationship: Treats non-ok Phoenix replies for runner pushes as transport
|
|
20
|
+
failures so failed Kandan writes cannot be mistaken for synchronized state.
|
|
16
21
|
*/
|
|
17
22
|
import {
|
|
18
23
|
type JsonObject,
|
|
@@ -23,6 +28,7 @@ import {
|
|
|
23
28
|
} from "./protocol";
|
|
24
29
|
|
|
25
30
|
type PendingPush = {
|
|
31
|
+
readonly event: string;
|
|
26
32
|
readonly resolve: (payload: JsonValue) => void;
|
|
27
33
|
readonly reject: (error: Error) => void;
|
|
28
34
|
};
|
|
@@ -112,6 +118,8 @@ export async function connectPhoenixClient(
|
|
|
112
118
|
|
|
113
119
|
if (name === "phx_error") {
|
|
114
120
|
pendingPush.reject(new Error("phoenix push failed"));
|
|
121
|
+
} else if (isNonOkPushReply(payload) && pendingPush.event !== "phx_join") {
|
|
122
|
+
pendingPush.reject(new Error(`phoenix push failed: ${replyErrorMessage(payload)}`));
|
|
115
123
|
} else {
|
|
116
124
|
pendingPush.resolve(payload);
|
|
117
125
|
}
|
|
@@ -143,7 +151,7 @@ export async function connectPhoenixClient(
|
|
|
143
151
|
const frame: PhoenixFrame = [null, ref, topic, event, payload];
|
|
144
152
|
|
|
145
153
|
return new Promise((resolve, reject) => {
|
|
146
|
-
pending.set(ref, { resolve, reject });
|
|
154
|
+
pending.set(ref, { event, resolve, reject });
|
|
147
155
|
websocket.send(JSON.stringify(frame));
|
|
148
156
|
});
|
|
149
157
|
};
|
|
@@ -304,6 +312,10 @@ function isJoinReply(value: JsonValue): value is {
|
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
function joinErrorMessage(value: JsonValue): string {
|
|
315
|
+
return replyErrorMessage(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function replyErrorMessage(value: JsonValue): string {
|
|
307
319
|
if (!isJsonObject(value)) {
|
|
308
320
|
return "invalid reply";
|
|
309
321
|
}
|
|
@@ -321,6 +333,10 @@ function joinErrorMessage(value: JsonValue): string {
|
|
|
321
333
|
return "unknown";
|
|
322
334
|
}
|
|
323
335
|
|
|
336
|
+
function isNonOkPushReply(value: JsonValue): boolean {
|
|
337
|
+
return isJsonObject(value) && value.status !== "ok";
|
|
338
|
+
}
|
|
339
|
+
|
|
324
340
|
function isKandanControl(value: JsonValue): value is KandanControl {
|
|
325
341
|
return isJsonObject(value) && typeof value.type === "string";
|
|
326
342
|
}
|
package/src/runner.ts
CHANGED
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
Relationship: Leaves channel-scoped approval controls to `channelSession.ts`
|
|
31
31
|
so the process runner remains lifecycle-only while Kandan safely resolves
|
|
32
32
|
Codex app-server approval requests from the thread UI.
|
|
33
|
+
|
|
34
|
+
- Date: 2026-04-26
|
|
35
|
+
Spec: plans/2026_04_26_linzumi_cli_review_followups_note.md
|
|
36
|
+
Relationship: Rejects stale Kandan controls for previous local runner
|
|
37
|
+
instances before they can mutate the current Codex app-server session.
|
|
33
38
|
*/
|
|
34
39
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
35
40
|
import { randomUUID } from "node:crypto";
|
|
@@ -270,6 +275,16 @@ async function openLocalCodexRunner(
|
|
|
270
275
|
|
|
271
276
|
kandan.onControl(control => {
|
|
272
277
|
log("kandan.control", { control });
|
|
278
|
+
if (!controlTargetsInstance(control, instanceId)) {
|
|
279
|
+
log("kandan.control_ignored", {
|
|
280
|
+
reason: "instance_id_mismatch",
|
|
281
|
+
instanceId,
|
|
282
|
+
controlInstanceId: control.instanceId,
|
|
283
|
+
controlType: control.type,
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
273
288
|
void (channelSession?.handleControl(control) ?? Promise.resolve(undefined))
|
|
274
289
|
.then(handled => {
|
|
275
290
|
if (handled !== undefined) {
|
|
@@ -286,12 +301,21 @@ async function openLocalCodexRunner(
|
|
|
286
301
|
instanceId,
|
|
287
302
|
message: error instanceof Error ? error.message : String(error),
|
|
288
303
|
});
|
|
304
|
+
})
|
|
305
|
+
.catch(error => {
|
|
306
|
+
log("kandan.control_response_push_failed", {
|
|
307
|
+
message: error instanceof Error ? error.message : String(error),
|
|
308
|
+
});
|
|
289
309
|
});
|
|
290
310
|
});
|
|
291
311
|
|
|
292
312
|
return { instanceId, codexUrl, close };
|
|
293
313
|
}
|
|
294
314
|
|
|
315
|
+
function controlTargetsInstance(control: KandanControl, instanceId: string): boolean {
|
|
316
|
+
return control.instanceId === undefined || control.instanceId === instanceId;
|
|
317
|
+
}
|
|
318
|
+
|
|
295
319
|
async function closeCleanupStack(cleanup: CleanupStack): Promise<void> {
|
|
296
320
|
if (cleanup.closePromise !== undefined) {
|
|
297
321
|
return cleanup.closePromise;
|