@cjhyy/code-shell-capability-coding 0.8.0

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 (134) hide show
  1. package/README.md +69 -0
  2. package/THIRD_PARTY_NOTICES.md +206 -0
  3. package/dist/bin/agent-server-stdio.d.ts +1 -0
  4. package/dist/bin/agent-server-stdio.js +4 -0
  5. package/dist/capability-runtime.d.ts +22 -0
  6. package/dist/capability-runtime.js +88 -0
  7. package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
  8. package/dist/cc-orchestrator/agent-adapter.js +143 -0
  9. package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
  10. package/dist/cc-orchestrator/cc-capability.js +57 -0
  11. package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
  12. package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
  13. package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
  14. package/dist/cc-orchestrator/codex-session-history.js +249 -0
  15. package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
  16. package/dist/cc-orchestrator/codex-user-text.js +25 -0
  17. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  18. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  19. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  20. package/dist/cc-orchestrator/external-agent-changes.js +231 -0
  21. package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
  22. package/dist/cc-orchestrator/external-agent-driver.js +284 -0
  23. package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
  24. package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
  25. package/dist/cc-orchestrator/index.d.ts +10 -0
  26. package/dist/cc-orchestrator/index.js +10 -0
  27. package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
  28. package/dist/cc-orchestrator/related-session-discovery.js +77 -0
  29. package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
  30. package/dist/cc-orchestrator/relevance-judge.js +38 -0
  31. package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
  32. package/dist/cc-orchestrator/session-discovery.js +295 -0
  33. package/dist/cc-orchestrator/session-history.d.ts +55 -0
  34. package/dist/cc-orchestrator/session-history.js +150 -0
  35. package/dist/external-agents/config.d.ts +3 -0
  36. package/dist/external-agents/config.js +16 -0
  37. package/dist/external-agents/types.d.ts +32 -0
  38. package/dist/external-agents/types.js +1 -0
  39. package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
  40. package/dist/external-runtimes/claude-code/event-translator.js +201 -0
  41. package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
  42. package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
  43. package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
  44. package/dist/external-runtimes/claude-code/runtime.js +168 -0
  45. package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
  46. package/dist/external-runtimes/codex/app-server-client.js +244 -0
  47. package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
  48. package/dist/external-runtimes/codex/event-translator.js +187 -0
  49. package/dist/external-runtimes/codex/index.d.ts +7 -0
  50. package/dist/external-runtimes/codex/index.js +6 -0
  51. package/dist/external-runtimes/codex/runtime.d.ts +128 -0
  52. package/dist/external-runtimes/codex/runtime.js +216 -0
  53. package/dist/external-runtimes/index.d.ts +37 -0
  54. package/dist/external-runtimes/index.js +27 -0
  55. package/dist/external-runtimes/session-factory.d.ts +97 -0
  56. package/dist/external-runtimes/session-factory.js +110 -0
  57. package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
  58. package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
  59. package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
  60. package/dist/external-runtimes/shared/session-context-store.js +114 -0
  61. package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
  62. package/dist/external-runtimes/shared/spawn-env.js +41 -0
  63. package/dist/git/parse-log.d.ts +14 -0
  64. package/dist/git/parse-log.js +21 -0
  65. package/dist/git/utils.d.ts +49 -0
  66. package/dist/git/utils.js +161 -0
  67. package/dist/git/worktree/crud.d.ts +108 -0
  68. package/dist/git/worktree/crud.js +415 -0
  69. package/dist/git/worktree/diff.d.ts +14 -0
  70. package/dist/git/worktree/diff.js +83 -0
  71. package/dist/git/worktree/git-exec.d.ts +7 -0
  72. package/dist/git/worktree/git-exec.js +86 -0
  73. package/dist/git/worktree/index.d.ts +6 -0
  74. package/dist/git/worktree/index.js +6 -0
  75. package/dist/git/worktree/query.d.ts +43 -0
  76. package/dist/git/worktree/query.js +128 -0
  77. package/dist/git/worktree/slug.d.ts +12 -0
  78. package/dist/git/worktree/slug.js +59 -0
  79. package/dist/git/worktree.d.ts +1 -0
  80. package/dist/git/worktree.js +5 -0
  81. package/dist/index.capability.d.ts +7 -0
  82. package/dist/index.capability.js +190 -0
  83. package/dist/index.d.ts +18 -0
  84. package/dist/index.git.d.ts +4 -0
  85. package/dist/index.git.js +4 -0
  86. package/dist/index.js +18 -0
  87. package/dist/index.orchestration.d.ts +7 -0
  88. package/dist/index.orchestration.js +5 -0
  89. package/dist/lsp/client.d.ts +41 -0
  90. package/dist/lsp/client.js +192 -0
  91. package/dist/lsp/manager.d.ts +53 -0
  92. package/dist/lsp/manager.js +248 -0
  93. package/dist/lsp/root-path.d.ts +9 -0
  94. package/dist/lsp/root-path.js +12 -0
  95. package/dist/lsp/servers.d.ts +16 -0
  96. package/dist/lsp/servers.js +60 -0
  97. package/dist/prompt/coding.md +42 -0
  98. package/dist/quota/credentials.d.ts +3 -0
  99. package/dist/quota/credentials.js +80 -0
  100. package/dist/quota/index.d.ts +36 -0
  101. package/dist/quota/index.js +161 -0
  102. package/dist/quota/types.d.ts +48 -0
  103. package/dist/quota/types.js +13 -0
  104. package/dist/review/review-prompt.d.ts +28 -0
  105. package/dist/review/review-prompt.js +81 -0
  106. package/dist/tools/apply-patch/LICENSE-codex +201 -0
  107. package/dist/tools/apply-patch/NOTICE.md +29 -0
  108. package/dist/tools/apply-patch/applier.d.ts +26 -0
  109. package/dist/tools/apply-patch/applier.js +306 -0
  110. package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
  111. package/dist/tools/apply-patch/backup-targets.js +30 -0
  112. package/dist/tools/apply-patch/eol.d.ts +4 -0
  113. package/dist/tools/apply-patch/eol.js +9 -0
  114. package/dist/tools/apply-patch/index.d.ts +20 -0
  115. package/dist/tools/apply-patch/index.js +106 -0
  116. package/dist/tools/apply-patch/parser.d.ts +17 -0
  117. package/dist/tools/apply-patch/parser.js +209 -0
  118. package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
  119. package/dist/tools/apply-patch/seek-sequence.js +123 -0
  120. package/dist/tools/apply-patch/types.d.ts +49 -0
  121. package/dist/tools/apply-patch/types.js +13 -0
  122. package/dist/tools/brief.d.ts +6 -0
  123. package/dist/tools/brief.js +41 -0
  124. package/dist/tools/check-quota.d.ts +14 -0
  125. package/dist/tools/check-quota.js +38 -0
  126. package/dist/tools/drive-agent.d.ts +52 -0
  127. package/dist/tools/drive-agent.js +1184 -0
  128. package/dist/tools/lsp.d.ts +6 -0
  129. package/dist/tools/lsp.js +220 -0
  130. package/dist/tools/notebook-edit.d.ts +6 -0
  131. package/dist/tools/notebook-edit.js +127 -0
  132. package/dist/tools/worktree.d.ts +10 -0
  133. package/dist/tools/worktree.js +363 -0
  134. package/package.json +60 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Claude Code as a CodeShell Agent Runtime.
3
+ *
4
+ * Drives `claude -p --output-format stream-json`, translates the stream into
5
+ * `StreamEvent`, and wires the loopback MCP bridge so the model can reach
6
+ * CodeShell tools mid-turn.
7
+ *
8
+ * Not an `LLMClientBase` (§6.1): Claude Code brings its own agent loop, tools and
9
+ * permission layer. This sits beside the Engine.
10
+ *
11
+ * One structural difference from Codex worth stating plainly: Codex's app-server
12
+ * is a long-lived process serving many turns, whereas `claude -p` is **one process
13
+ * per turn**. Continuity therefore comes from `--resume <sessionId>`, using the id
14
+ * learned from the first turn's `system/init` line — not from keeping a socket
15
+ * open. That is why there is no `start()` here: the first `send()` IS the start.
16
+ */
17
+ import { spawn } from "node:child_process";
18
+ import { createInterface } from "node:readline";
19
+ import { ClaudeEventTranslator } from "./event-translator.js";
20
+ import { claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME } from "./mcp-config.js";
21
+ import { buildRuntimeSpawnEnv } from "../shared/spawn-env.js";
22
+ export class ClaudeCodeRuntime {
23
+ options;
24
+ hooks;
25
+ kind = "claude-code";
26
+ log;
27
+ translator;
28
+ child;
29
+ claudeSessionId;
30
+ closed = false;
31
+ constructor(options, hooks = {}) {
32
+ this.options = options;
33
+ this.hooks = hooks;
34
+ this.log = options.log ?? (() => { });
35
+ this.translator = new ClaudeEventTranslator({
36
+ sessionId: options.businessSessionId,
37
+ codeshellServerName: options.serverName ?? CLAUDE_MCP_SERVER_NAME,
38
+ });
39
+ }
40
+ /** Claude session id, once the first turn has reported it. Resume key only. */
41
+ get runtimeSessionId() {
42
+ return this.claudeSessionId;
43
+ }
44
+ /**
45
+ * Run one turn. Resolves when the process exits, which is also when the turn is
46
+ * terminal — `claude -p` is one process per turn.
47
+ *
48
+ * The prompt goes on **stdin**, not argv: `--mcp-config` is variadic, so a
49
+ * positional prompt after it is swallowed as another config value (measured),
50
+ * and a prompt on the command line would also be visible in `ps`.
51
+ */
52
+ async send(text) {
53
+ if (this.closed)
54
+ throw new Error("ClaudeCodeRuntime is closed");
55
+ if (this.child)
56
+ throw new Error("a turn is already running");
57
+ const wiring = claudeBridgeArgs({
58
+ bridge: this.options.bridge,
59
+ exposedToolNames: this.options.exposedToolNames,
60
+ ...(this.options.serverName ? { serverName: this.options.serverName } : {}),
61
+ });
62
+ const args = [
63
+ "-p",
64
+ "--output-format",
65
+ "stream-json",
66
+ "--include-partial-messages",
67
+ // stream-json output requires --verbose; without it the CLI refuses.
68
+ "--verbose",
69
+ ...(this.options.model ? ["--model", this.options.model] : []),
70
+ // Continuity across turns: a fresh process per turn means the session id is
71
+ // the only thread of memory.
72
+ ...(this.claudeSessionId ? ["--resume", this.claudeSessionId] : []),
73
+ ...(this.options.extraArgs ?? []),
74
+ ...wiring.args,
75
+ ];
76
+ const child = spawn(this.options.command ?? "claude", args, {
77
+ cwd: this.options.cwd,
78
+ env: buildRuntimeSpawnEnv({
79
+ bridgeToken: {
80
+ name: this.options.bridge.tokenEnvVar,
81
+ value: this.options.bridge.token,
82
+ },
83
+ }),
84
+ stdio: ["pipe", "pipe", "pipe"],
85
+ shell: false,
86
+ });
87
+ this.child = child;
88
+ child.stdout.setEncoding("utf8");
89
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
90
+ lines.on("line", (line) => this.onLine(line));
91
+ child.stderr.setEncoding("utf8");
92
+ child.stderr.on("data", (chunk) => {
93
+ const trimmed = chunk.trim();
94
+ if (trimmed)
95
+ this.log("claude.stderr", { bytes: trimmed.length });
96
+ });
97
+ child.stdin.end(text);
98
+ const done = new Promise((resolve) => {
99
+ const finish = () => {
100
+ lines.close();
101
+ // Clean up the config file that carried the bearer token.
102
+ wiring.cleanup();
103
+ this.child = undefined;
104
+ resolve();
105
+ };
106
+ child.once("exit", finish);
107
+ child.once("error", (error) => {
108
+ this.log("claude.spawn_failed", { error: error.message.slice(0, 200) });
109
+ finish();
110
+ });
111
+ });
112
+ return { done };
113
+ }
114
+ onLine(line) {
115
+ if (!line.trim())
116
+ return;
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(line);
120
+ }
121
+ catch {
122
+ // The CLI may print non-JSON diagnostics; one bad line is not fatal.
123
+ this.log("claude.unparsable_line", { bytes: line.length });
124
+ return;
125
+ }
126
+ for (const event of this.translator.translate(parsed)) {
127
+ try {
128
+ this.hooks.onEvent?.(event);
129
+ }
130
+ catch (error) {
131
+ this.log("claude.event_handler_failed", {
132
+ error: error instanceof Error ? error.name : "unknown",
133
+ });
134
+ }
135
+ }
136
+ if (!this.claudeSessionId && this.translator.runtimeSessionId) {
137
+ this.claudeSessionId = this.translator.runtimeSessionId;
138
+ this.log("claude.session_started", {
139
+ businessSessionId: this.options.businessSessionId,
140
+ runtimeSessionIdPrefix: this.claudeSessionId.slice(0, 8),
141
+ });
142
+ }
143
+ }
144
+ /**
145
+ * Interrupt the active turn.
146
+ *
147
+ * `claude -p` has no interrupt RPC, so this is a signal. As with Codex, it
148
+ * cannot retract side effects already dispatched — it only stops further steps.
149
+ */
150
+ async interrupt() {
151
+ const child = this.child;
152
+ if (!child)
153
+ return;
154
+ child.kill("SIGTERM");
155
+ }
156
+ async close() {
157
+ this.closed = true;
158
+ await this.interrupt();
159
+ }
160
+ /** Start a fresh conversation on the next `send()`, discarding resume state. */
161
+ resetConversation() {
162
+ this.claudeSessionId = undefined;
163
+ this.translator = new ClaudeEventTranslator({
164
+ sessionId: this.options.businessSessionId,
165
+ codeshellServerName: this.options.serverName ?? CLAUDE_MCP_SERVER_NAME,
166
+ });
167
+ }
168
+ }
@@ -0,0 +1,66 @@
1
+ export interface AppServerClientOptions {
2
+ /** Executable; defaults to `codex` on PATH. */
3
+ command?: string;
4
+ /**
5
+ * Full argv after `command`. Defaults to `["app-server"]`.
6
+ *
7
+ * Configurable rather than hardcoded so a test can point the client at a fake
8
+ * server binary — a client that can only ever launch the real Codex is a client
9
+ * whose transport rules are untestable without a login.
10
+ */
11
+ args?: readonly string[];
12
+ cwd?: string;
13
+ env?: NodeJS.ProcessEnv;
14
+ /** Structured log sink. Never receives tokens or full tool arguments. */
15
+ log?: (event: string, data: Record<string, unknown>) => void;
16
+ }
17
+ export type NotificationHandler = (method: string, params: unknown) => void;
18
+ /**
19
+ * Answer a server→client request. Returning `undefined` means "not handled",
20
+ * which the client turns into a JSON-RPC method-not-found reply — the server is
21
+ * waiting either way.
22
+ */
23
+ export type ServerRequestHandler = (method: string, params: unknown) => Promise<unknown> | unknown;
24
+ export declare class CodexAppServerClient {
25
+ private readonly options;
26
+ private child?;
27
+ private lines?;
28
+ private nextId;
29
+ private readonly pending;
30
+ private notificationHandler?;
31
+ private requestHandler?;
32
+ private closed;
33
+ private closeReason?;
34
+ /** Buffer lines that arrive before a handler exists, then drain in order. */
35
+ private readonly preHandlerLines;
36
+ private handlersReady;
37
+ private readonly log;
38
+ constructor(options?: AppServerClientOptions);
39
+ /**
40
+ * Register handlers. MUST be called before {@link start} — see the note on
41
+ * ordering in the module header.
42
+ */
43
+ onNotification(handler: NotificationHandler): void;
44
+ onServerRequest(handler: ServerRequestHandler): void;
45
+ /** Spawn the app-server. Handlers must already be registered. */
46
+ start(): void;
47
+ private acceptLine;
48
+ private handleLine;
49
+ private settle;
50
+ /**
51
+ * Fire-and-forget so an awaiting client request never blocks inbound reads —
52
+ * a server request arriving mid-flight must not deadlock the transport.
53
+ */
54
+ private dispatchServerRequest;
55
+ /**
56
+ * Send a request. No timeout unless `timeoutMs` is given — see the module
57
+ * header on why a global timeout is harmful.
58
+ */
59
+ request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
60
+ notify(method: string, params?: unknown): void;
61
+ private write;
62
+ private failAll;
63
+ get isClosed(): boolean;
64
+ /** Graceful stop: stdin EOF (the Rust app-server exits on it), SIGTERM after. */
65
+ close(): Promise<void>;
66
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * NDJSON JSON-RPC client for `codex app-server`.
3
+ *
4
+ * Shapes verified against real `codex-cli 0.145.0` generated bindings; the
5
+ * ordering and framing rules below are the expensive lessons recorded in the
6
+ * `makecindy/cindy` reference implementation (design §16), which we read rather
7
+ * than rediscover:
8
+ *
9
+ * - **Codex's JSON-RPC does NOT send a `jsonrpc` field.** Messages must be
10
+ * discriminated by shape alone.
11
+ * - **Handlers must be registered before `initialize()`.** The server pushes
12
+ * notifications the moment the transport is up; registering afterwards drops
13
+ * them. There is no readiness banner — the `initialize` response IS readiness.
14
+ * - **A server→client request must always be answered**, even an unknown one, or
15
+ * the server blocks waiting.
16
+ * - **`request()` has no default timeout.** Per protocol a response may be
17
+ * arbitrarily late, and a global timeout manufactures orphan turns: a timed-out
18
+ * `turn/start` does NOT mean the server declined to create the turn. Callers
19
+ * opt in per call site.
20
+ * - **A single malformed line must not kill the session** (the server may emit a
21
+ * banner), but an oversized line must, to keep JSON.parse off the hot path.
22
+ */
23
+ import { spawn } from "node:child_process";
24
+ import { createInterface } from "node:readline";
25
+ /** Cap per NDJSON line. Generous for reasoning text; guards parse latency. */
26
+ const MAX_LINE_BYTES = 16 * 1024 * 1024;
27
+ export class CodexAppServerClient {
28
+ options;
29
+ child;
30
+ lines;
31
+ nextId = 1;
32
+ pending = new Map();
33
+ notificationHandler;
34
+ requestHandler;
35
+ closed = false;
36
+ closeReason;
37
+ /** Buffer lines that arrive before a handler exists, then drain in order. */
38
+ preHandlerLines = [];
39
+ handlersReady = false;
40
+ log;
41
+ constructor(options = {}) {
42
+ this.options = options;
43
+ this.log = options.log ?? (() => { });
44
+ }
45
+ /**
46
+ * Register handlers. MUST be called before {@link start} — see the note on
47
+ * ordering in the module header.
48
+ */
49
+ onNotification(handler) {
50
+ this.notificationHandler = handler;
51
+ }
52
+ onServerRequest(handler) {
53
+ this.requestHandler = handler;
54
+ }
55
+ /** Spawn the app-server. Handlers must already be registered. */
56
+ start() {
57
+ if (this.child)
58
+ return;
59
+ const command = this.options.command ?? "codex";
60
+ const args = [...(this.options.args ?? ["app-server"])];
61
+ // `shell: false` deliberately: a shell would reintroduce env-injection risk
62
+ // and make stdio piping unpredictable.
63
+ this.child = spawn(command, args, {
64
+ cwd: this.options.cwd,
65
+ env: this.options.env,
66
+ stdio: ["pipe", "pipe", "pipe"],
67
+ shell: false,
68
+ });
69
+ this.child.stdout.setEncoding("utf8");
70
+ this.lines = createInterface({ input: this.child.stdout, crlfDelay: Infinity });
71
+ this.lines.on("line", (line) => this.acceptLine(line));
72
+ this.child.stderr.setEncoding("utf8");
73
+ this.child.stderr.on("data", (chunk) => {
74
+ const text = chunk.trim();
75
+ // stderr is not protocol data. Log only a classification: it can carry
76
+ // model output and must not become an auth-failure oracle either — auth
77
+ // invalidation is detected ONLY from a correlated JSON-RPC error.
78
+ // Diagnostic preview only, bounded; stderr is never an auth oracle.
79
+ if (text)
80
+ this.log("appserver.stderr", { bytes: text.length, preview: text.slice(0, 300) });
81
+ });
82
+ const onGone = (why) => () => this.failAll(why);
83
+ this.child.on("exit", onGone("app-server exited"));
84
+ this.child.on("error", onGone("app-server failed to start"));
85
+ // Handlers are registered by contract before start(); release the buffer.
86
+ this.handlersReady = true;
87
+ for (const line of this.preHandlerLines.splice(0))
88
+ this.handleLine(line);
89
+ }
90
+ acceptLine(line) {
91
+ if (!this.handlersReady) {
92
+ this.preHandlerLines.push(line);
93
+ return;
94
+ }
95
+ this.handleLine(line);
96
+ }
97
+ handleLine(line) {
98
+ if (line.length > MAX_LINE_BYTES) {
99
+ // Oversized is a transport failure: parsing it would stall the event loop.
100
+ this.failAll("app-server sent an oversized line");
101
+ return;
102
+ }
103
+ let message;
104
+ try {
105
+ message = JSON.parse(line);
106
+ }
107
+ catch {
108
+ // One bad line does not kill the session — the server may emit a banner.
109
+ this.log("appserver.unparsable_line", { bytes: line.length });
110
+ return;
111
+ }
112
+ // Shape-based discrimination: Codex omits the `jsonrpc` field entirely.
113
+ const hasId = message.id !== undefined && message.id !== null;
114
+ const method = typeof message.method === "string" ? message.method : undefined;
115
+ if (hasId && method) {
116
+ void this.dispatchServerRequest(message.id, method, message.params);
117
+ return;
118
+ }
119
+ if (hasId) {
120
+ this.settle(message);
121
+ return;
122
+ }
123
+ if (method) {
124
+ try {
125
+ this.notificationHandler?.(method, message.params);
126
+ }
127
+ catch (error) {
128
+ // A handler throw must not take down the read loop.
129
+ this.log("appserver.notification_handler_failed", {
130
+ method,
131
+ error: error instanceof Error ? error.name : "unknown",
132
+ });
133
+ }
134
+ }
135
+ }
136
+ settle(message) {
137
+ const id = Number(message.id);
138
+ const entry = this.pending.get(id);
139
+ if (!entry)
140
+ return;
141
+ this.pending.delete(id);
142
+ if (entry.timer)
143
+ clearTimeout(entry.timer);
144
+ if (message.error !== undefined) {
145
+ entry.reject(new Error(`app-server error: ${JSON.stringify(message.error).slice(0, 400)}`));
146
+ return;
147
+ }
148
+ entry.resolve(message.result);
149
+ }
150
+ /**
151
+ * Fire-and-forget so an awaiting client request never blocks inbound reads —
152
+ * a server request arriving mid-flight must not deadlock the transport.
153
+ */
154
+ async dispatchServerRequest(id, method, params) {
155
+ try {
156
+ const result = this.requestHandler ? await this.requestHandler(method, params) : undefined;
157
+ if (result === undefined) {
158
+ // The server is waiting regardless; an unhandled request still gets an
159
+ // answer rather than a hang.
160
+ this.write({ id, error: { code: -32601, message: `unhandled: ${method}` } });
161
+ return;
162
+ }
163
+ this.write({ id, result });
164
+ }
165
+ catch (error) {
166
+ this.write({
167
+ id,
168
+ error: { code: -32603, message: error instanceof Error ? error.message : "handler failed" },
169
+ });
170
+ }
171
+ }
172
+ /**
173
+ * Send a request. No timeout unless `timeoutMs` is given — see the module
174
+ * header on why a global timeout is harmful.
175
+ */
176
+ request(method, params, timeoutMs) {
177
+ if (this.closed) {
178
+ return Promise.reject(new Error(this.closeReason ?? "app-server client is closed"));
179
+ }
180
+ const id = this.nextId++;
181
+ return new Promise((resolve, reject) => {
182
+ const entry = { resolve, reject };
183
+ if (timeoutMs !== undefined) {
184
+ entry.timer = setTimeout(() => {
185
+ this.pending.delete(id);
186
+ reject(new Error(`app-server ${method} did not answer within ${timeoutMs}ms. NOTE: the ` +
187
+ `request may still have taken effect — a timed-out turn/start does not ` +
188
+ `mean the turn was not created.`));
189
+ }, timeoutMs);
190
+ }
191
+ this.pending.set(id, entry);
192
+ this.write({ id, method, params: params ?? {} });
193
+ });
194
+ }
195
+ notify(method, params) {
196
+ if (this.closed)
197
+ return;
198
+ this.write({ method, params: params ?? {} });
199
+ }
200
+ write(payload) {
201
+ const child = this.child;
202
+ if (!child || child.stdin.destroyed)
203
+ return;
204
+ child.stdin.write(`${JSON.stringify(payload)}\n`);
205
+ }
206
+ failAll(reason) {
207
+ if (this.closed)
208
+ return;
209
+ this.closed = true;
210
+ this.closeReason = reason;
211
+ this.log("appserver.closed", { reason });
212
+ for (const [, entry] of this.pending) {
213
+ if (entry.timer)
214
+ clearTimeout(entry.timer);
215
+ entry.reject(new Error(reason));
216
+ }
217
+ this.pending.clear();
218
+ }
219
+ get isClosed() {
220
+ return this.closed;
221
+ }
222
+ /** Graceful stop: stdin EOF (the Rust app-server exits on it), SIGTERM after. */
223
+ async close() {
224
+ const child = this.child;
225
+ this.failAll("app-server client closed");
226
+ this.lines?.close();
227
+ if (!child)
228
+ return;
229
+ try {
230
+ child.stdin.end();
231
+ }
232
+ catch {
233
+ /* already gone */
234
+ }
235
+ const exited = new Promise((resolve) => {
236
+ if (child.exitCode !== null)
237
+ return resolve();
238
+ child.once("exit", () => resolve());
239
+ });
240
+ const timer = setTimeout(() => child.kill("SIGTERM"), 2_000);
241
+ await exited;
242
+ clearTimeout(timer);
243
+ }
244
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Codex app-server notifications → CodeShell `StreamEvent`.
3
+ *
4
+ * Shapes come from the generated bindings of a real codex-cli 0.145.0
5
+ * (`codex app-server generate-ts`). The ordering rules below are NOT inferred
6
+ * from the schema — they are the expensive lessons recorded in the
7
+ * `makecindy/cindy` reference implementation (design §16), which we read rather
8
+ * than rediscover:
9
+ *
10
+ * - `turn/completed` **can be delivered more than once**. Only the first may
11
+ * close the turn.
12
+ * - Items **arrive after `turn/completed`**. Clearing the current turn id is not
13
+ * enough; a late item would re-open a turn whose completion was already
14
+ * consumed, leaving the session permanently "busy".
15
+ * - `turn/started` for a turn can arrive **before** the `turn/start` RPC
16
+ * response, and so can `turn/completed`. Neither may resurrect a turn that has
17
+ * already reached a terminal state.
18
+ *
19
+ * Hence: a per-turn tombstone that outlives the turn, for completed AND errored
20
+ * turns alike. Dropping a stale event is always preferable to reopening a turn.
21
+ *
22
+ * §15.2 also applies: a CodeShell Host Tool already emits its own lifecycle
23
+ * through `ToolExecutor`, so this translator must NOT emit a second tool card for
24
+ * it. Third-party MCP servers have no CodeShell-side lifecycle and do get cards.
25
+ */
26
+ import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
27
+ export interface CodexEventTranslatorOptions {
28
+ /** The ONE Codex thread this translator serves. */
29
+ threadId: string;
30
+ /** CodeShell business session id, for correlating logs. */
31
+ sessionId: string;
32
+ /** Override the suppressed MCP server name (tests). */
33
+ codeshellServerName?: string;
34
+ }
35
+ export declare class CodexEventTranslator {
36
+ private readonly threadId;
37
+ private readonly codeshellServer;
38
+ private turnNumber;
39
+ private activeTurnId;
40
+ /** Turns that reached a terminal state. Late events for these are dropped. */
41
+ private readonly finishedTurns;
42
+ constructor(options: CodexEventTranslatorOptions);
43
+ /**
44
+ * Translate one notification. Returns zero or more events — zero is a normal,
45
+ * common outcome (unknown method, other thread, stale turn), never an error.
46
+ *
47
+ * Deliberately total: the app-server is experimental and adds notifications
48
+ * between versions, so an unrecognised or malformed one must not take a session
49
+ * down.
50
+ */
51
+ translate(notification: unknown): StreamEvent[];
52
+ /** True when an event belongs to a turn that has already finished. */
53
+ private isStale;
54
+ private remember;
55
+ private onTurnStarted;
56
+ private onTurnCompleted;
57
+ private onError;
58
+ private onAgentDelta;
59
+ /**
60
+ * A CodeShell Host Tool call must not produce a card here — `ToolExecutor`
61
+ * already emits one, and two unsynchronised sources for one operation is
62
+ * exactly what §15.2 forbids. Only OUR server is suppressed; a third-party MCP
63
+ * server has no CodeShell-side lifecycle, so dropping it would lose the card.
64
+ */
65
+ private isCodeshellHostTool;
66
+ private onItemStarted;
67
+ private onItemCompleted;
68
+ }