@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,57 @@
1
+ import { spawn } from "node:child_process";
2
+ import { delimiter } from "node:path";
3
+ /** macOS GUI-launched Electron has a minimal PATH (no Homebrew). Prepend common
4
+ * CLI dirs so `claude` resolves. Mirrors resident-agent.ts's fix. */
5
+ export function pathWithCommonBins(env = process.env) {
6
+ const extra = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
7
+ const current = (env.PATH ?? "").split(delimiter).filter(Boolean);
8
+ const merged = [];
9
+ for (const dir of [...extra, ...current])
10
+ if (!merged.includes(dir))
11
+ merged.push(dir);
12
+ return merged.join(delimiter);
13
+ }
14
+ /** Default runner: `<command> --version` with PATH fix. */
15
+ const defaultRunner = (command) => new Promise((resolve, reject) => {
16
+ const child = spawn(command, ["--version"], {
17
+ env: { ...process.env, PATH: pathWithCommonBins() },
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ });
20
+ let stdout = "";
21
+ child.stdout?.on("data", (c) => (stdout += String(c)));
22
+ child.on("error", reject);
23
+ child.on("exit", (code) => resolve({ ok: code === 0, stdout }));
24
+ });
25
+ /** Probe a CLI's availability. Injectable runner for tests. */
26
+ export async function probeCli(command, runner = defaultRunner) {
27
+ try {
28
+ const { ok, stdout } = await runner(command);
29
+ if (!ok)
30
+ return { available: false, command, reason: "not-executable" };
31
+ return { available: true, command, version: stdout.trim() || undefined };
32
+ }
33
+ catch (err) {
34
+ const code = err.code;
35
+ return {
36
+ available: false,
37
+ command,
38
+ reason: code === "ENOENT" ? "not-found" : "not-executable",
39
+ };
40
+ }
41
+ }
42
+ let cached;
43
+ /** Cached probe; pass force=true to re-detect (user installed CLI mid-session). */
44
+ export async function probeClaudeCli(force = false) {
45
+ if (cached && !force)
46
+ return cached;
47
+ cached = await probeCli("claude");
48
+ return cached;
49
+ }
50
+ let cachedCodex;
51
+ /** Cached probe for the OpenAI Codex CLI; force=true re-detects. */
52
+ export async function probeCodexCli(force = false) {
53
+ if (cachedCodex && !force)
54
+ return cachedCodex;
55
+ cachedCodex = await probeCli("codex");
56
+ return cachedCodex;
57
+ }
@@ -0,0 +1,45 @@
1
+ import type { DiscoveredSession, DiscoverOptions, ExternalSessionDiscoveryScope, RecentExternalSession } from "./session-discovery.js";
2
+ /**
3
+ * Discover codex CLI sessions for a given `cwd`, mirroring the claude-side
4
+ * `discoverSessions` but for codex's storage layout, which is fundamentally
5
+ * different:
6
+ *
7
+ * ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
8
+ *
9
+ * Unlike claude (one dir per project), codex stores rollouts by DATE, not by
10
+ * cwd. The cwd lives INSIDE the file: the first line is a `session_meta` event
11
+ * whose `payload` carries `{ id, cwd, timestamp }` (`id` is the thread id we
12
+ * resume by). So discovery means: walk every rollout file, read just its first
13
+ * line to learn its cwd, and keep the ones matching the requested project.
14
+ *
15
+ * Codex rollout files can be large, so we deliberately AVOID `readFileSync` on
16
+ * the whole file (the claude version reads the whole file because claude
17
+ * sessions are small). We read the first line for the meta, and — only for a
18
+ * matching file — read a bounded prefix to find the first real user message for
19
+ * the title. Returns the same `DiscoveredSession` shape so the UI is CLI-blind.
20
+ */
21
+ /** Discover Codex CLI sessions for the coding host. */
22
+ export declare function discoverCodexSessions(cwd: string, codexHome?: string, opts?: DiscoverOptions): DiscoveredSession[];
23
+ /** Discover several cwd buckets in one rollout walk. This matters for the
24
+ * room list: a CodeShell project and the worktrees delegated from it are one
25
+ * user-visible scope, while Codex stores all of them in a single global tree. */
26
+ export declare function discoverCodexSessionsForCwds(cwds: readonly string[], codexHome?: string, opts?: DiscoverOptions, accept?: (session: {
27
+ sessionId: string;
28
+ cwd: string;
29
+ }) => boolean): DiscoveredSession[];
30
+ /** Count codex sessions for `cwd` (reads first-line meta of every rollout; no
31
+ * window). Used so the UI knows whether a bounded list left older ones hidden. */
32
+ export declare function countCodexSessions(cwd: string, codexHome?: string): number;
33
+ export declare function countCodexSessionsForCwds(cwds: readonly string[], codexHome?: string): number;
34
+ /** One Codex session discovered globally (no cwd filter), for the Pet
35
+ * external-session adapter. `file` is the newest rollout for this thread.
36
+ * Alias of the shared shape so codex + claude paths stay isomorphic. */
37
+ export type RecentCodexSession = RecentExternalSession;
38
+ /**
39
+ * Discover ALL recent codex sessions regardless of cwd. Same bounded-read
40
+ * strategy as `discoverCodexSessionsForCwds` (stat pass → recency window →
41
+ * first-line meta), plus per-thread dedup: a resumed thread appends a NEW
42
+ * rollout file with the same session_meta id, so keep only the newest file
43
+ * (windowed list is already mtime-descending — first hit wins).
44
+ */
45
+ export declare function discoverRecentCodexSessions(opts?: DiscoverOptions, codexHome?: string, scope?: ExternalSessionDiscoveryScope): RecentCodexSession[];
@@ -0,0 +1,251 @@
1
+ import { readdirSync, statSync, existsSync, openSync, readSync, closeSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { externalSessionCwdEnabled, selectRecentStats } from "./session-discovery.js";
5
+ import { codexUserText } from "./codex-user-text.js";
6
+ /**
7
+ * Discover codex CLI sessions for a given `cwd`, mirroring the claude-side
8
+ * `discoverSessions` but for codex's storage layout, which is fundamentally
9
+ * different:
10
+ *
11
+ * ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
12
+ *
13
+ * Unlike claude (one dir per project), codex stores rollouts by DATE, not by
14
+ * cwd. The cwd lives INSIDE the file: the first line is a `session_meta` event
15
+ * whose `payload` carries `{ id, cwd, timestamp }` (`id` is the thread id we
16
+ * resume by). So discovery means: walk every rollout file, read just its first
17
+ * line to learn its cwd, and keep the ones matching the requested project.
18
+ *
19
+ * Codex rollout files can be large, so we deliberately AVOID `readFileSync` on
20
+ * the whole file (the claude version reads the whole file because claude
21
+ * sessions are small). We read the first line for the meta, and — only for a
22
+ * matching file — read a bounded prefix to find the first real user message for
23
+ * the title. Returns the same `DiscoveredSession` shape so the UI is CLI-blind.
24
+ */
25
+ /** Discover Codex CLI sessions for the coding host. */
26
+ export function discoverCodexSessions(cwd, codexHome = join(homedir(), ".codex"), opts = {}) {
27
+ return discoverCodexSessionsForCwds([cwd], codexHome, opts);
28
+ }
29
+ /** Discover several cwd buckets in one rollout walk. This matters for the
30
+ * room list: a CodeShell project and the worktrees delegated from it are one
31
+ * user-visible scope, while Codex stores all of them in a single global tree. */
32
+ export function discoverCodexSessionsForCwds(cwds, codexHome = join(homedir(), ".codex"), opts = {}, accept) {
33
+ const root = join(codexHome, "sessions");
34
+ if (!existsSync(root))
35
+ return [];
36
+ const cwdSet = new Set(cwds);
37
+ if (cwdSet.size === 0)
38
+ return [];
39
+ // Cheap pass: stat every rollout (no read). Apply the recency window here so
40
+ // the meta read (first line) only runs on in-window files. The limit can't be
41
+ // applied yet — we don't know which files match `cwd` until we read the meta,
42
+ // so we'd otherwise drop matching-but-not-yet-seen sessions.
43
+ const windowed = selectRecentStats(statAllRollouts(root), {
44
+ sinceMs: opts.sinceMs,
45
+ now: opts.now,
46
+ });
47
+ const limit = opts.limit && opts.limit > 0 ? opts.limit : Infinity;
48
+ const out = [];
49
+ for (const s of windowed) {
50
+ if (out.length >= limit)
51
+ break;
52
+ let meta;
53
+ try {
54
+ meta = readSessionMeta(s.file);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ if (!meta || !meta.id || !meta.cwd || !cwdSet.has(meta.cwd))
60
+ continue;
61
+ if (accept && !accept({ sessionId: meta.id, cwd: meta.cwd }))
62
+ continue;
63
+ out.push({
64
+ sessionId: meta.id,
65
+ cwd: meta.cwd,
66
+ firstMessage: readFirstUserMessage(s.file),
67
+ lastModified: s.mtimeMs,
68
+ messageCount: 0, // not tracked for codex (would need a full scan); UI shows time + title.
69
+ });
70
+ }
71
+ return out;
72
+ }
73
+ /** Count codex sessions for `cwd` (reads first-line meta of every rollout; no
74
+ * window). Used so the UI knows whether a bounded list left older ones hidden. */
75
+ export function countCodexSessions(cwd, codexHome = join(homedir(), ".codex")) {
76
+ return countCodexSessionsForCwds([cwd], codexHome);
77
+ }
78
+ export function countCodexSessionsForCwds(cwds, codexHome = join(homedir(), ".codex")) {
79
+ const root = join(codexHome, "sessions");
80
+ if (!existsSync(root))
81
+ return 0;
82
+ const cwdSet = new Set(cwds);
83
+ let n = 0;
84
+ for (const file of walkRollouts(root)) {
85
+ let meta;
86
+ try {
87
+ meta = readSessionMeta(file);
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ if (meta && meta.id && meta.cwd && cwdSet.has(meta.cwd))
93
+ n++;
94
+ }
95
+ return n;
96
+ }
97
+ /**
98
+ * Discover ALL recent codex sessions regardless of cwd. Same bounded-read
99
+ * strategy as `discoverCodexSessionsForCwds` (stat pass → recency window →
100
+ * first-line meta), plus per-thread dedup: a resumed thread appends a NEW
101
+ * rollout file with the same session_meta id, so keep only the newest file
102
+ * (windowed list is already mtime-descending — first hit wins).
103
+ */
104
+ export function discoverRecentCodexSessions(opts = {}, codexHome = join(homedir(), ".codex"), scope) {
105
+ const root = join(codexHome, "sessions");
106
+ if (!existsSync(root))
107
+ return [];
108
+ const windowed = selectRecentStats(statAllRollouts(root), {
109
+ sinceMs: opts.sinceMs,
110
+ now: opts.now,
111
+ });
112
+ const limit = opts.limit && opts.limit > 0 ? opts.limit : Infinity;
113
+ const out = new Map();
114
+ for (const s of windowed) {
115
+ if (out.size >= limit)
116
+ break;
117
+ let meta;
118
+ try {
119
+ // Codex stores every cwd in one date-partitioned tree, so the bounded
120
+ // first-line session_meta is the earliest possible project boundary.
121
+ // Apply scope immediately here: a disabled root is never title/content
122
+ // probed, returned to the adapter, or tailed.
123
+ meta = readSessionMeta(s.file);
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ if (!meta?.id || !meta.cwd || out.has(meta.id) || !externalSessionCwdEnabled(meta.cwd, scope)) {
129
+ continue;
130
+ }
131
+ out.set(meta.id, {
132
+ sessionId: meta.id,
133
+ cwd: meta.cwd,
134
+ file: s.file,
135
+ lastModified: s.mtimeMs,
136
+ firstMessage: readFirstUserMessage(s.file),
137
+ });
138
+ }
139
+ return [...out.values()];
140
+ }
141
+ /** Stat every rollout under <root>/sessions, skipping unreadable files. */
142
+ function statAllRollouts(root) {
143
+ const stats = [];
144
+ for (const file of walkRollouts(root)) {
145
+ try {
146
+ stats.push({ file, mtimeMs: statSync(file).mtimeMs });
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ }
152
+ return stats;
153
+ }
154
+ /** Recursively yield every `rollout-*.jsonl` file under `root`. */
155
+ function* walkRollouts(root) {
156
+ let entries;
157
+ try {
158
+ entries = readdirSync(root);
159
+ }
160
+ catch {
161
+ return;
162
+ }
163
+ for (const name of entries) {
164
+ const full = join(root, name);
165
+ let st;
166
+ try {
167
+ st = statSync(full);
168
+ }
169
+ catch {
170
+ continue;
171
+ }
172
+ if (st.isDirectory()) {
173
+ yield* walkRollouts(full);
174
+ }
175
+ else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
176
+ yield full;
177
+ }
178
+ }
179
+ }
180
+ /** Read just the first line of a file (bounded), without loading the whole file. */
181
+ function readFirstLine(file, maxBytes = 1 << 16) {
182
+ const fd = openSync(file, "r");
183
+ try {
184
+ const buf = Buffer.alloc(maxBytes);
185
+ const n = readSync(fd, buf, 0, maxBytes, 0);
186
+ const text = buf.toString("utf-8", 0, n);
187
+ const nl = text.indexOf("\n");
188
+ return nl === -1 ? text : text.slice(0, nl);
189
+ }
190
+ finally {
191
+ closeSync(fd);
192
+ }
193
+ }
194
+ /** Parse the first-line `session_meta` event → `{ id, cwd }`. */
195
+ function readSessionMeta(file) {
196
+ const first = readFirstLine(file).trim();
197
+ if (!first)
198
+ return undefined;
199
+ const d = JSON.parse(first);
200
+ if (d.type !== "session_meta" || !d.payload)
201
+ return undefined;
202
+ return { id: d.payload.id, cwd: d.payload.cwd };
203
+ }
204
+ /**
205
+ * Find the first *real* user message text for the session title. Codex user
206
+ * messages look like:
207
+ * {type:"response_item", payload:{type:"message", role:"user",
208
+ * content:[{type:"input_text", text}]}}
209
+ * A user message can contain several parts: host-injected plugin/AGENTS/env
210
+ * context followed by the actual prompt. `codexUserText` drops the injected
211
+ * parts without discarding the prompt beside them.
212
+ *
213
+ * Reads a bounded prefix of the file rather than the whole thing.
214
+ */
215
+ function readFirstUserMessage(file, maxBytes = 1 << 20) {
216
+ let chunk;
217
+ try {
218
+ const fd = openSync(file, "r");
219
+ try {
220
+ const buf = Buffer.alloc(maxBytes);
221
+ const n = readSync(fd, buf, 0, maxBytes, 0);
222
+ chunk = buf.toString("utf-8", 0, n);
223
+ }
224
+ finally {
225
+ closeSync(fd);
226
+ }
227
+ }
228
+ catch {
229
+ return "";
230
+ }
231
+ for (const line of chunk.split("\n")) {
232
+ if (!line.trim())
233
+ continue;
234
+ let d;
235
+ try {
236
+ d = JSON.parse(line);
237
+ }
238
+ catch {
239
+ // Truncated last line of the bounded read — stop scanning.
240
+ break;
241
+ }
242
+ const p = d?.payload;
243
+ if (d?.type !== "response_item" || p?.type !== "message" || p?.role !== "user")
244
+ continue;
245
+ const t = codexUserText(p.content).trim();
246
+ if (!t)
247
+ continue;
248
+ return t.slice(0, 200);
249
+ }
250
+ return "";
251
+ }
@@ -0,0 +1,39 @@
1
+ import type { HistoryMessage, SessionTailEvent } from "./session-history.js";
2
+ /**
3
+ * Read the last `limit` user/assistant messages from a codex CLI session,
4
+ * returning the SAME shape as the claude-side `readRecentHistory` so the room
5
+ * UI stays CLI-blind.
6
+ *
7
+ * Codex storage differs from claude fundamentally (see `codex-session-discovery`):
8
+ * rollouts live under `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`, keyed by
9
+ * DATE not cwd, with the thread id inside the file's `session_meta` first line.
10
+ * So we first locate the rollout file whose `session_meta` matches both
11
+ * `threadId` and `cwd`, then parse its event stream:
12
+ *
13
+ * - {type:"response_item", payload:{type:"message", role:"user"|"assistant",
14
+ * content:[{type:"input_text"|"output_text", text}]}} → message text
15
+ * - {type:"response_item", payload:{type:"function_call", name, arguments}} → tool
16
+ * - {type:"response_item", payload:{type:"custom_tool_call", name, input}} → tool
17
+ *
18
+ * `developer`-role messages and host-injected plugin/AGENTS/environment user
19
+ * parts are skipped, while real input parts beside them remain visible.
20
+ */
21
+ /** Read recent Codex CLI history for the coding host. */
22
+ export declare function readCodexRecentHistory(cwd: string, threadId: string, limit: number, codexHome?: string): {
23
+ messages: HistoryMessage[];
24
+ hasMore: boolean;
25
+ totalCount: number;
26
+ };
27
+ /** Parse a bounded/raw Codex rollout snapshot. Shared with the desktop tail
28
+ * follower so its initial history and EOF cursor are atomic. */
29
+ export declare function parseCodexRecentHistory(raw: string, limit: number): {
30
+ messages: HistoryMessage[];
31
+ hasMore: boolean;
32
+ totalCount: number;
33
+ };
34
+ /** Parse one newly-appended Codex rollout JSONL line. `response_item` is the
35
+ * authoritative rendered stream; parallel `event_msg.agent_message` records
36
+ * are deliberately ignored to avoid duplicate assistant bubbles. */
37
+ export declare function parseCodexTranscriptLine(line: string): SessionTailEvent[];
38
+ /** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`. */
39
+ export declare function findCodexRolloutFile(codexHome: string, cwd: string, threadId: string): string | undefined;
@@ -0,0 +1,249 @@
1
+ import { readdirSync, readFileSync, statSync, existsSync, openSync, readSync, closeSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { codexUserText } from "./codex-user-text.js";
5
+ /**
6
+ * Read the last `limit` user/assistant messages from a codex CLI session,
7
+ * returning the SAME shape as the claude-side `readRecentHistory` so the room
8
+ * UI stays CLI-blind.
9
+ *
10
+ * Codex storage differs from claude fundamentally (see `codex-session-discovery`):
11
+ * rollouts live under `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`, keyed by
12
+ * DATE not cwd, with the thread id inside the file's `session_meta` first line.
13
+ * So we first locate the rollout file whose `session_meta` matches both
14
+ * `threadId` and `cwd`, then parse its event stream:
15
+ *
16
+ * - {type:"response_item", payload:{type:"message", role:"user"|"assistant",
17
+ * content:[{type:"input_text"|"output_text", text}]}} → message text
18
+ * - {type:"response_item", payload:{type:"function_call", name, arguments}} → tool
19
+ * - {type:"response_item", payload:{type:"custom_tool_call", name, input}} → tool
20
+ *
21
+ * `developer`-role messages and host-injected plugin/AGENTS/environment user
22
+ * parts are skipped, while real input parts beside them remain visible.
23
+ */
24
+ /** Read recent Codex CLI history for the coding host. */
25
+ export function readCodexRecentHistory(cwd, threadId, limit, codexHome = join(homedir(), ".codex")) {
26
+ const empty = { messages: [], hasMore: false, totalCount: 0 };
27
+ const file = findCodexRolloutFile(codexHome, cwd, threadId);
28
+ if (!file)
29
+ return empty;
30
+ let raw;
31
+ try {
32
+ raw = readFileSync(file, "utf-8");
33
+ }
34
+ catch {
35
+ return empty;
36
+ }
37
+ return parseCodexRecentHistory(raw, limit);
38
+ }
39
+ /** Parse a bounded/raw Codex rollout snapshot. Shared with the desktop tail
40
+ * follower so its initial history and EOF cursor are atomic. */
41
+ export function parseCodexRecentHistory(raw, limit) {
42
+ const all = [];
43
+ for (const line of raw.split("\n")) {
44
+ if (!line.trim())
45
+ continue;
46
+ let d;
47
+ try {
48
+ d = JSON.parse(line);
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ if (d?.type !== "response_item")
54
+ continue;
55
+ const p = d.payload;
56
+ if (!p)
57
+ continue;
58
+ if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
59
+ const t = (p.role === "user" ? codexUserText(p.content) : textOf(p.content)).trim();
60
+ if (!t)
61
+ continue;
62
+ all.push({ role: p.role, text: t });
63
+ }
64
+ else if (p.type === "function_call" || p.type === "custom_tool_call") {
65
+ const tool = {
66
+ name: typeof p.name === "string" ? p.name : "tool",
67
+ summary: summaryOf(p),
68
+ args: argsOf(p),
69
+ };
70
+ const last = all[all.length - 1];
71
+ // Attach the tool to the preceding assistant turn; otherwise start one.
72
+ if (last && last.role === "assistant") {
73
+ (last.tools ??= []).push(tool);
74
+ }
75
+ else {
76
+ all.push({ role: "assistant", text: "", tools: [tool] });
77
+ }
78
+ }
79
+ }
80
+ const lim = limit > 0 ? limit : 20;
81
+ const start = Math.max(0, all.length - lim);
82
+ return { messages: all.slice(start), hasMore: start > 0, totalCount: all.length };
83
+ }
84
+ /** Parse one newly-appended Codex rollout JSONL line. `response_item` is the
85
+ * authoritative rendered stream; parallel `event_msg.agent_message` records
86
+ * are deliberately ignored to avoid duplicate assistant bubbles. */
87
+ export function parseCodexTranscriptLine(line) {
88
+ let d;
89
+ try {
90
+ d = JSON.parse(line);
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ if (d?.type === "event_msg" && d.payload?.type === "task_complete") {
96
+ return [{ type: "turn_end", reason: "completed" }];
97
+ }
98
+ if (d?.type !== "response_item" || !d.payload)
99
+ return [];
100
+ const p = d.payload;
101
+ if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
102
+ const text = (p.role === "user" ? codexUserText(p.content) : textOf(p.content)).trim();
103
+ if (!text)
104
+ return [];
105
+ return [{ type: p.role === "user" ? "user" : "assistant", text }];
106
+ }
107
+ if (p.type === "function_call" || p.type === "custom_tool_call") {
108
+ return [
109
+ {
110
+ type: "tool",
111
+ id: typeof p.call_id === "string" ? p.call_id : undefined,
112
+ name: typeof p.name === "string" ? p.name : "tool",
113
+ summary: summaryOf(p),
114
+ args: argsOf(p),
115
+ },
116
+ ];
117
+ }
118
+ if (p.type === "function_call_output" || p.type === "custom_tool_call_output") {
119
+ const result = typeof p.output === "string"
120
+ ? p.output
121
+ : typeof p.content === "string"
122
+ ? p.content
123
+ : JSON.stringify(p.output ?? p.content ?? "");
124
+ return [
125
+ {
126
+ type: "tool_result",
127
+ id: typeof p.call_id === "string" ? p.call_id : undefined,
128
+ result: result.slice(0, 4000),
129
+ isError: Boolean(p.is_error),
130
+ },
131
+ ];
132
+ }
133
+ return [];
134
+ }
135
+ /** Join the text of codex content parts (`input_text`/`output_text`). */
136
+ function textOf(content) {
137
+ if (typeof content === "string")
138
+ return content;
139
+ if (Array.isArray(content))
140
+ return content.map((c) => (typeof c?.text === "string" ? c.text : "")).join("");
141
+ return "";
142
+ }
143
+ /** Build a short summary for a codex tool event from its arguments/input. */
144
+ function summaryOf(payload) {
145
+ // function_call carries `arguments` as a JSON string; custom_tool_call carries `input` as a string.
146
+ if (typeof payload.arguments === "string") {
147
+ try {
148
+ const a = JSON.parse(payload.arguments);
149
+ const s = a?.cmd ?? a?.command ?? a?.file_path ?? a?.path ?? a?.workdir ?? "";
150
+ if (s)
151
+ return String(s).slice(0, 120);
152
+ }
153
+ catch {
154
+ /* fall through to raw */
155
+ }
156
+ return payload.arguments.slice(0, 120);
157
+ }
158
+ if (typeof payload.input === "string")
159
+ return payload.input.slice(0, 120);
160
+ return "";
161
+ }
162
+ /**
163
+ * Build the FULL structured tool args for a codex tool event so a replayed tool
164
+ * card can show the real parameters (not just the lossy one-field summary).
165
+ * `function_call.arguments` is a JSON string → parse it; `custom_tool_call.input`
166
+ * is a raw string → keep it under `{input}`. Returns undefined when neither is
167
+ * present or the JSON is malformed.
168
+ */
169
+ function argsOf(payload) {
170
+ if (typeof payload.arguments === "string") {
171
+ try {
172
+ const a = JSON.parse(payload.arguments);
173
+ if (a && typeof a === "object")
174
+ return a;
175
+ }
176
+ catch {
177
+ /* malformed JSON → fall through */
178
+ }
179
+ return { arguments: payload.arguments };
180
+ }
181
+ if (typeof payload.input === "string")
182
+ return { input: payload.input };
183
+ return undefined;
184
+ }
185
+ /** Find the rollout file whose `session_meta` matches both `threadId` and `cwd`. */
186
+ export function findCodexRolloutFile(codexHome, cwd, threadId) {
187
+ const root = join(codexHome, "sessions");
188
+ if (!existsSync(root))
189
+ return undefined;
190
+ for (const file of walkRollouts(root)) {
191
+ let meta;
192
+ try {
193
+ meta = readSessionMeta(file);
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ if (meta && meta.id === threadId && meta.cwd === cwd)
199
+ return file;
200
+ }
201
+ return undefined;
202
+ }
203
+ /** Recursively yield every `rollout-*.jsonl` file under `root`. */
204
+ function* walkRollouts(root) {
205
+ let entries;
206
+ try {
207
+ entries = readdirSync(root);
208
+ }
209
+ catch {
210
+ return;
211
+ }
212
+ for (const name of entries) {
213
+ const full = join(root, name);
214
+ let st;
215
+ try {
216
+ st = statSync(full);
217
+ }
218
+ catch {
219
+ continue;
220
+ }
221
+ if (st.isDirectory()) {
222
+ yield* walkRollouts(full);
223
+ }
224
+ else if (name.startsWith("rollout-") && name.endsWith(".jsonl")) {
225
+ yield full;
226
+ }
227
+ }
228
+ }
229
+ /** Parse the first-line `session_meta` event → `{ id, cwd }`, reading a bounded prefix. */
230
+ function readSessionMeta(file, maxBytes = 1 << 16) {
231
+ const fd = openSync(file, "r");
232
+ let text;
233
+ try {
234
+ const buf = Buffer.alloc(maxBytes);
235
+ const n = readSync(fd, buf, 0, maxBytes, 0);
236
+ text = buf.toString("utf-8", 0, n);
237
+ }
238
+ finally {
239
+ closeSync(fd);
240
+ }
241
+ const nl = text.indexOf("\n");
242
+ const first = (nl === -1 ? text : text.slice(0, nl)).trim();
243
+ if (!first)
244
+ return undefined;
245
+ const d = JSON.parse(first);
246
+ if (d.type !== "session_meta" || !d.payload)
247
+ return undefined;
248
+ return { id: d.payload.id, cwd: d.payload.cwd };
249
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Codex can store host-provided context and the user's actual prompt as
3
+ * separate `input_text` parts on the SAME user message. Joining every part
4
+ * makes room titles start with AGENTS/plugin boilerplate; dropping the whole
5
+ * message when its joined text starts with `<environment_context>` drops the
6
+ * real prompt too. Keep user-authored parts and discard only known host
7
+ * context parts.
8
+ */
9
+ export declare function codexUserText(content: unknown): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Codex can store host-provided context and the user's actual prompt as
3
+ * separate `input_text` parts on the SAME user message. Joining every part
4
+ * makes room titles start with AGENTS/plugin boilerplate; dropping the whole
5
+ * message when its joined text starts with `<environment_context>` drops the
6
+ * real prompt too. Keep user-authored parts and discard only known host
7
+ * context parts.
8
+ */
9
+ export function codexUserText(content) {
10
+ if (typeof content === "string") {
11
+ return isInjectedCodexContext(content) ? "" : content;
12
+ }
13
+ if (!Array.isArray(content))
14
+ return "";
15
+ return content
16
+ .map((part) => (typeof part?.text === "string" ? part.text : ""))
17
+ .filter((text) => text.trim() && !isInjectedCodexContext(text))
18
+ .join("\n");
19
+ }
20
+ function isInjectedCodexContext(text) {
21
+ const trimmed = text.trimStart();
22
+ return (trimmed.startsWith("<environment_context>") ||
23
+ trimmed.startsWith("<recommended_plugins>") ||
24
+ /^# AGENTS\.md instructions for\s/.test(trimmed));
25
+ }