@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.
- package/README.md +69 -0
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/bin/agent-server-stdio.d.ts +1 -0
- package/dist/bin/agent-server-stdio.js +4 -0
- package/dist/capability-runtime.d.ts +22 -0
- package/dist/capability-runtime.js +88 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
- package/dist/cc-orchestrator/agent-adapter.js +143 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
- package/dist/cc-orchestrator/cc-capability.js +57 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
- package/dist/cc-orchestrator/codex-session-history.js +249 -0
- package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
- package/dist/cc-orchestrator/codex-user-text.js +25 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +231 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-driver.js +284 -0
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
- package/dist/cc-orchestrator/index.d.ts +10 -0
- package/dist/cc-orchestrator/index.js +10 -0
- package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
- package/dist/cc-orchestrator/related-session-discovery.js +77 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
- package/dist/cc-orchestrator/relevance-judge.js +38 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
- package/dist/cc-orchestrator/session-discovery.js +295 -0
- package/dist/cc-orchestrator/session-history.d.ts +55 -0
- package/dist/cc-orchestrator/session-history.js +150 -0
- package/dist/external-agents/config.d.ts +3 -0
- package/dist/external-agents/config.js +16 -0
- package/dist/external-agents/types.d.ts +32 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
- package/dist/external-runtimes/claude-code/event-translator.js +201 -0
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
- package/dist/external-runtimes/claude-code/runtime.js +168 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
- package/dist/external-runtimes/codex/app-server-client.js +244 -0
- package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
- package/dist/external-runtimes/codex/event-translator.js +187 -0
- package/dist/external-runtimes/codex/index.d.ts +7 -0
- package/dist/external-runtimes/codex/index.js +6 -0
- package/dist/external-runtimes/codex/runtime.d.ts +128 -0
- package/dist/external-runtimes/codex/runtime.js +216 -0
- package/dist/external-runtimes/index.d.ts +37 -0
- package/dist/external-runtimes/index.js +27 -0
- package/dist/external-runtimes/session-factory.d.ts +97 -0
- package/dist/external-runtimes/session-factory.js +110 -0
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
- package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
- package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
- package/dist/external-runtimes/shared/session-context-store.js +114 -0
- package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
- package/dist/external-runtimes/shared/spawn-env.js +41 -0
- package/dist/git/parse-log.d.ts +14 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +49 -0
- package/dist/git/utils.js +161 -0
- package/dist/git/worktree/crud.d.ts +108 -0
- package/dist/git/worktree/crud.js +415 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +83 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +86 -0
- package/dist/git/worktree/index.d.ts +6 -0
- package/dist/git/worktree/index.js +6 -0
- package/dist/git/worktree/query.d.ts +43 -0
- package/dist/git/worktree/query.js +128 -0
- package/dist/git/worktree/slug.d.ts +12 -0
- package/dist/git/worktree/slug.js +59 -0
- package/dist/git/worktree.d.ts +1 -0
- package/dist/git/worktree.js +5 -0
- package/dist/index.capability.d.ts +7 -0
- package/dist/index.capability.js +190 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.git.d.ts +4 -0
- package/dist/index.git.js +4 -0
- package/dist/index.js +18 -0
- package/dist/index.orchestration.d.ts +7 -0
- package/dist/index.orchestration.js +5 -0
- package/dist/lsp/client.d.ts +41 -0
- package/dist/lsp/client.js +192 -0
- package/dist/lsp/manager.d.ts +53 -0
- package/dist/lsp/manager.js +248 -0
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/lsp/servers.d.ts +16 -0
- package/dist/lsp/servers.js +60 -0
- package/dist/prompt/coding.md +42 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +161 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/tools/apply-patch/LICENSE-codex +201 -0
- package/dist/tools/apply-patch/NOTICE.md +29 -0
- package/dist/tools/apply-patch/applier.d.ts +26 -0
- package/dist/tools/apply-patch/applier.js +306 -0
- package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tools/apply-patch/backup-targets.js +30 -0
- package/dist/tools/apply-patch/eol.d.ts +4 -0
- package/dist/tools/apply-patch/eol.js +9 -0
- package/dist/tools/apply-patch/index.d.ts +20 -0
- package/dist/tools/apply-patch/index.js +106 -0
- package/dist/tools/apply-patch/parser.d.ts +17 -0
- package/dist/tools/apply-patch/parser.js +209 -0
- package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
- package/dist/tools/apply-patch/seek-sequence.js +123 -0
- package/dist/tools/apply-patch/types.d.ts +49 -0
- package/dist/tools/apply-patch/types.js +13 -0
- package/dist/tools/brief.d.ts +6 -0
- package/dist/tools/brief.js +41 -0
- package/dist/tools/check-quota.d.ts +14 -0
- package/dist/tools/check-quota.js +38 -0
- package/dist/tools/drive-agent.d.ts +52 -0
- package/dist/tools/drive-agent.js +1184 -0
- package/dist/tools/lsp.d.ts +6 -0
- package/dist/tools/lsp.js +220 -0
- package/dist/tools/notebook-edit.d.ts +6 -0
- package/dist/tools/notebook-edit.js +127 -0
- package/dist/tools/worktree.d.ts +10 -0
- package/dist/tools/worktree.js +363 -0
- 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
|
+
}
|