@linzumi/cli 0.0.5-beta → 0.0.6-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.
@@ -0,0 +1,130 @@
1
+ /*
2
+ - Date: 2026-04-26
3
+ Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
4
+ Relationship: Defines the local machine capability boundary for Kandan-
5
+ requested Codex session starts and future preview forwarding. Remote starts
6
+ are allowed only inside explicitly configured local cwd roots, and preview
7
+ forwarding is advertised only for explicitly configured local ports.
8
+ */
9
+ import { realpathSync } from "node:fs";
10
+ import { isAbsolute, relative, resolve } from "node:path";
11
+
12
+ export type CwdCapabilityDecision =
13
+ | {
14
+ readonly ok: true;
15
+ readonly cwd: string;
16
+ readonly matchedRoot: string;
17
+ }
18
+ | {
19
+ readonly ok: false;
20
+ readonly reason:
21
+ | "missing_cwd"
22
+ | "no_allowed_cwd"
23
+ | "cwd_not_allowed"
24
+ | "cwd_not_found";
25
+ };
26
+
27
+ export function parseAllowedCwdList(value: string | undefined): string[] {
28
+ if (value === undefined) {
29
+ return [];
30
+ }
31
+
32
+ return value
33
+ .split(",")
34
+ .map((part) => part.trim())
35
+ .filter((part) => part !== "");
36
+ }
37
+
38
+ export function parseAllowedPortList(value: string | undefined): number[] {
39
+ if (value === undefined) {
40
+ return [];
41
+ }
42
+
43
+ const ports = new Set<number>();
44
+
45
+ for (const part of value.split(",")) {
46
+ const trimmed = part.trim();
47
+
48
+ if (trimmed === "") {
49
+ continue;
50
+ }
51
+
52
+ const port = Number(trimmed);
53
+
54
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
55
+ throw new Error(`invalid --forward-port: ${trimmed} is not a TCP port`);
56
+ }
57
+
58
+ ports.add(port);
59
+ }
60
+
61
+ return [...ports].sort((left, right) => left - right);
62
+ }
63
+
64
+ export function assertConfiguredAllowedCwds(
65
+ paths: readonly string[],
66
+ ): string[] {
67
+ return paths.map((path) => {
68
+ try {
69
+ return realpathSync(resolve(path));
70
+ } catch (_error) {
71
+ throw new Error(`invalid --allowed-cwd: ${path} does not exist`);
72
+ }
73
+ });
74
+ }
75
+
76
+ export function resolveAllowedCwd(
77
+ requestedCwd: string | undefined,
78
+ allowedRoots: readonly string[],
79
+ ): CwdCapabilityDecision {
80
+ if (requestedCwd === undefined || requestedCwd.trim() === "") {
81
+ return { ok: false, reason: "missing_cwd" };
82
+ }
83
+
84
+ if (allowedRoots.length === 0) {
85
+ return { ok: false, reason: "no_allowed_cwd" };
86
+ }
87
+
88
+ let cwd: string;
89
+
90
+ try {
91
+ cwd = realpathSync(resolve(requestedCwd));
92
+ } catch (_error) {
93
+ return { ok: false, reason: "cwd_not_found" };
94
+ }
95
+
96
+ const matchedRoot = allowedRoots.find((root) =>
97
+ cwdIsInsideNormalizedRoot(cwd, root),
98
+ );
99
+
100
+ return matchedRoot === undefined
101
+ ? { ok: false, reason: "cwd_not_allowed" }
102
+ : { ok: true, cwd, matchedRoot };
103
+ }
104
+
105
+ function cwdIsInsideNormalizedRoot(
106
+ cwd: string,
107
+ normalizedRoot: string,
108
+ ): boolean {
109
+ return relativePathIsInsideRoot(relative(normalizedRoot, cwd));
110
+ }
111
+
112
+ export function relativePathIsInsideRoot(pathRelativeToRoot: string): boolean {
113
+ return (
114
+ pathRelativeToRoot === "" ||
115
+ (!pathLooksAbsolute(pathRelativeToRoot) &&
116
+ pathRelativeToRoot !== ".." &&
117
+ !pathRelativeToRoot.startsWith("../") &&
118
+ !pathRelativeToRoot.startsWith("..\\") &&
119
+ !pathRelativeToRoot.includes("/../") &&
120
+ !pathRelativeToRoot.includes("\\..\\"))
121
+ );
122
+ }
123
+
124
+ function pathLooksAbsolute(pathValue: string): boolean {
125
+ return (
126
+ isAbsolute(pathValue) ||
127
+ /^[A-Za-z]:[\\/]/.test(pathValue) ||
128
+ pathValue.startsWith("\\\\")
129
+ );
130
+ }
@@ -0,0 +1,135 @@
1
+ /*
2
+ - Date: 2026-04-26
3
+ Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
4
+ Relationship: Defines the user-visible local Codex message state model and
5
+ the Codex notification-to-state mapping used by Kandan to show queued,
6
+ processing, approval, failed, ignored, and processed states consistently.
7
+
8
+ - Date: 2026-04-26
9
+ Spec: plans/2026-04-26-local-runner-port-forward-approval.md
10
+ Relationship: Allows local runner approval states to carry explicit
11
+ UI choices so descendant port-forward prompts can reuse the existing
12
+ Kandan approval bottom bar.
13
+ */
14
+ import { objectValue, stringValue } from "./json";
15
+ import type { JsonObject, JsonRpcRequest } from "./protocol";
16
+
17
+ export type LocalCodexProcessingReason =
18
+ | "starting turn"
19
+ | "streaming response"
20
+ | "running terminal command"
21
+ | "interrupt requested"
22
+ | "awaiting approval";
23
+
24
+ export type CodexApprovalMessageState = {
25
+ readonly requestId: string;
26
+ readonly kind: string;
27
+ readonly summary: string;
28
+ readonly reason?: string | undefined;
29
+ readonly choices?: readonly ApprovalChoice[] | undefined;
30
+ readonly allowedActorSlug?: string | undefined;
31
+ readonly allowedActorUserId?: number | undefined;
32
+ };
33
+
34
+ export type ApprovalChoice = {
35
+ readonly decision: string;
36
+ readonly label: string;
37
+ readonly description?: string | undefined;
38
+ };
39
+
40
+ export type ActiveProcessingState =
41
+ | {
42
+ readonly seq: number;
43
+ readonly reason: Exclude<LocalCodexProcessingReason, "awaiting approval">;
44
+ }
45
+ | {
46
+ readonly seq: number;
47
+ readonly reason: "awaiting approval";
48
+ readonly approval: CodexApprovalMessageState;
49
+ };
50
+
51
+ export type LocalCodexMessageState =
52
+ | { readonly status: "queued" }
53
+ | { readonly status: "processed" }
54
+ | {
55
+ readonly status: "processing";
56
+ readonly reason: LocalCodexProcessingReason;
57
+ readonly approval?: CodexApprovalMessageState | undefined;
58
+ }
59
+ | { readonly status: "ignored"; readonly reason: string }
60
+ | { readonly status: "failed"; readonly reason: string };
61
+
62
+ export function codexApprovalMessageState(
63
+ request: JsonRpcRequest,
64
+ ): CodexApprovalMessageState {
65
+ const params = objectValue(request.params) ?? {};
66
+ const command = stringValue(params.command) ?? stringValue(params.cmd);
67
+ const filePath =
68
+ stringValue(params.path) ??
69
+ stringValue(params.filePath) ??
70
+ stringValue(params.file);
71
+ const summary =
72
+ command ??
73
+ filePath ??
74
+ stringValue(params.reason) ??
75
+ stringValue(params.summary) ??
76
+ request.method;
77
+
78
+ return {
79
+ requestId: String(request.id),
80
+ kind: request.method,
81
+ summary,
82
+ };
83
+ }
84
+
85
+ export function approvalRequestKey(requestId: string, sourceSeq: number): string {
86
+ return `${sourceSeq}:${requestId}`;
87
+ }
88
+
89
+ export function processingReasonForCodexNotification(
90
+ method: string,
91
+ params: JsonObject,
92
+ ): Exclude<LocalCodexProcessingReason, "awaiting approval"> | undefined {
93
+ if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta") {
94
+ return "streaming response";
95
+ }
96
+
97
+ if (
98
+ method.startsWith("item/commandExecution/") ||
99
+ method === "command/exec/outputDelta"
100
+ ) {
101
+ return "running terminal command";
102
+ }
103
+
104
+ const item = objectValue(params.item) ?? params;
105
+ const itemType = stringValue(item.type);
106
+
107
+ switch (itemType) {
108
+ case "commandExecution":
109
+ case "terminalInput":
110
+ return "running terminal command";
111
+ case "agentMessage":
112
+ case "reasoning":
113
+ case "fileChange":
114
+ case "web_search_call":
115
+ case "webSearchCall":
116
+ case "webSearch":
117
+ return "streaming response";
118
+ default:
119
+ return undefined;
120
+ }
121
+ }
122
+
123
+ export function processingMessageStateFromActive(
124
+ state: ActiveProcessingState,
125
+ ): Extract<LocalCodexMessageState, { readonly status: "processing" }> {
126
+ switch (state.reason) {
127
+ case "awaiting approval":
128
+ return { status: "processing", reason: state.reason, approval: state.approval };
129
+ case "starting turn":
130
+ case "streaming response":
131
+ case "running terminal command":
132
+ case "interrupt requested":
133
+ return { status: "processing", reason: state.reason };
134
+ }
135
+ }
@@ -0,0 +1,108 @@
1
+ /*
2
+ - Date: 2026-04-26
3
+ Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
4
+ Relationship: Defines the pure local Codex turn lifecycle helpers used by
5
+ the channel-session orchestrator for interruption, active-turn lookup,
6
+ completion, failure cleanup, and forwarding eligibility.
7
+ */
8
+
9
+ export type TurnState =
10
+ | { readonly status: "idle" }
11
+ | {
12
+ readonly status: "starting";
13
+ readonly queuedSeq: number;
14
+ readonly interruptAfterStart: boolean;
15
+ }
16
+ | { readonly status: "active"; readonly turnId: string; readonly queuedSeq: number }
17
+ | { readonly status: "completing"; readonly turnId: string; readonly queuedSeq: number };
18
+
19
+ export function activeTurnId(turn: TurnState): string | undefined {
20
+ switch (turn.status) {
21
+ case "active":
22
+ case "completing":
23
+ return turn.turnId;
24
+ case "idle":
25
+ case "starting":
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ export function activeQueuedSeqForTurn(
31
+ turn: TurnState,
32
+ turnId: string,
33
+ ): number | undefined {
34
+ switch (turn.status) {
35
+ case "active":
36
+ case "completing":
37
+ return turn.turnId === turnId ? turn.queuedSeq : undefined;
38
+ case "idle":
39
+ case "starting":
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ export function interruptibleQueuedSeq(turn: TurnState): number | undefined {
45
+ switch (turn.status) {
46
+ case "active":
47
+ case "starting":
48
+ return turn.queuedSeq;
49
+ case "idle":
50
+ case "completing":
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ export function markInterruptAfterStart(turn: TurnState): TurnState {
56
+ switch (turn.status) {
57
+ case "starting":
58
+ return {
59
+ status: "starting",
60
+ queuedSeq: turn.queuedSeq,
61
+ interruptAfterStart: true,
62
+ };
63
+ case "idle":
64
+ case "active":
65
+ case "completing":
66
+ return turn;
67
+ }
68
+ }
69
+
70
+ export function turnCanForwardByState(turn: TurnState, turnId: string): boolean {
71
+ switch (turn.status) {
72
+ case "active":
73
+ case "completing":
74
+ return turn.turnId === turnId;
75
+ case "idle":
76
+ case "starting":
77
+ return false;
78
+ }
79
+ }
80
+
81
+ export function completingQueuedSeqForTurn(
82
+ turn: TurnState,
83
+ turnId: string,
84
+ ): number | undefined {
85
+ switch (turn.status) {
86
+ case "active":
87
+ return turn.turnId === turnId ? turn.queuedSeq : undefined;
88
+ case "idle":
89
+ case "starting":
90
+ case "completing":
91
+ return undefined;
92
+ }
93
+ }
94
+
95
+ export function shouldClearTurnAfterFailure(
96
+ turn: TurnState,
97
+ turnId: string,
98
+ ): boolean {
99
+ switch (turn.status) {
100
+ case "starting":
101
+ return true;
102
+ case "active":
103
+ case "completing":
104
+ return turn.turnId === turnId;
105
+ case "idle":
106
+ return false;
107
+ }
108
+ }