@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,99 @@
|
|
|
1
|
+
/** Encode a cwd to claude's project dir name: every non-[A-Za-z0-9] char → '-'.
|
|
2
|
+
* Mirrors `~/.claude/projects/<encoded>` (verified against real layout). */
|
|
3
|
+
export declare function encodeCwd(cwd: string): string;
|
|
4
|
+
/** Session discovered from an external coding CLI. */
|
|
5
|
+
export interface DiscoveredSession {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
/** The cwd recorded by the external CLI for this session. Consumers must
|
|
8
|
+
* open/resume the session with this path instead of assuming the currently
|
|
9
|
+
* selected CodeShell project has the same cwd (DriveAgent worktrees often
|
|
10
|
+
* do not). */
|
|
11
|
+
cwd: string;
|
|
12
|
+
firstMessage: string;
|
|
13
|
+
lastModified: number;
|
|
14
|
+
messageCount: number;
|
|
15
|
+
}
|
|
16
|
+
/** A session discovered globally from an external CLI's own storage, with the
|
|
17
|
+
* file path so the Pet adapter can tail it. Shared by codex + claude paths. */
|
|
18
|
+
export interface RecentExternalSession {
|
|
19
|
+
sessionId: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
file: string;
|
|
22
|
+
lastModified: number;
|
|
23
|
+
firstMessage: string;
|
|
24
|
+
}
|
|
25
|
+
/** One registered CodeShell project root and its effective visibility. */
|
|
26
|
+
export interface ExternalSessionProjectRoot {
|
|
27
|
+
cwd: string;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Source-level discovery scope. Registered roots use longest-ancestor matching;
|
|
32
|
+
* sessions outside every registered root follow includeUnregistered (the user
|
|
33
|
+
* global baseline).
|
|
34
|
+
*/
|
|
35
|
+
export interface ExternalSessionDiscoveryScope {
|
|
36
|
+
includeUnregistered: boolean;
|
|
37
|
+
projectRoots: readonly ExternalSessionProjectRoot[];
|
|
38
|
+
}
|
|
39
|
+
/** Resolve aliases such as a trailing slash before comparing project roots. */
|
|
40
|
+
export declare function normalizeExternalSessionCwd(cwd: string): string;
|
|
41
|
+
/** Pick the most specific registered ancestor for a session cwd. */
|
|
42
|
+
export declare function externalSessionProjectRootFor(cwd: string, roots: readonly ExternalSessionProjectRoot[]): ExternalSessionProjectRoot | undefined;
|
|
43
|
+
/** Apply a discovery scope to one real cwd. */
|
|
44
|
+
export declare function externalSessionCwdEnabled(cwd: string, scope?: ExternalSessionDiscoveryScope): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Cheap Claude-directory prefilter. Because encodeCwd is lossy, true only means
|
|
47
|
+
* "this directory may contain an enabled cwd". The real cwd identity is always
|
|
48
|
+
* checked before transcript content is read.
|
|
49
|
+
*/
|
|
50
|
+
export declare function claudeProjectDirEnabled(encodedProjectDir: string, scope?: ExternalSessionDiscoveryScope): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Bound how many sessions a discovery scan deep-reads + returns. Without this,
|
|
53
|
+
* `discoverSessions` read EVERY `.jsonl` file fully (title + message count) on
|
|
54
|
+
* every list open — O(all history) work that makes the CC/Codex room list slow
|
|
55
|
+
* and unwieldy once a project accumulates sessions. The default surfaces only
|
|
56
|
+
* the recent, useful slice; the UI fetches older ones on demand by passing a
|
|
57
|
+
* larger `limit` (or `sinceMs: 0`).
|
|
58
|
+
*/
|
|
59
|
+
export interface DiscoverOptions {
|
|
60
|
+
/** Max sessions to return (after the recency filter), newest first.
|
|
61
|
+
* undefined or <= 0 = no cap. */
|
|
62
|
+
limit?: number;
|
|
63
|
+
/** Only include sessions modified within this many ms of `now`.
|
|
64
|
+
* undefined or <= 0 = no window. */
|
|
65
|
+
sinceMs?: number;
|
|
66
|
+
/** Injectable clock for tests. Defaults to Date.now(). */
|
|
67
|
+
now?: number;
|
|
68
|
+
}
|
|
69
|
+
/** Default room-list window: last 2 weeks AND at most 20 — the intersection. */
|
|
70
|
+
export declare const DEFAULT_DISCOVER_LIMIT = 20;
|
|
71
|
+
export declare const DEFAULT_DISCOVER_SINCE_MS: number;
|
|
72
|
+
/**
|
|
73
|
+
* Apply the recency window + limit to mtime-sorted stats, returning the slice to
|
|
74
|
+
* deep-read. Shared by the claude + codex discovery paths so they bound work
|
|
75
|
+
* identically. `total` (caller-side) lets the UI show a "load more" affordance.
|
|
76
|
+
*/
|
|
77
|
+
export declare function selectRecentStats<T extends {
|
|
78
|
+
mtimeMs: number;
|
|
79
|
+
}>(stats: T[], opts?: DiscoverOptions): T[];
|
|
80
|
+
/** List claude sessions for `cwd`. `claudeHome` defaults to ~/.claude (override
|
|
81
|
+
* for tests). Read-only, on-demand scan; no index. Stats every file cheaply,
|
|
82
|
+
* then deep-reads (title + count) ONLY the recent slice (opts) so an old,
|
|
83
|
+
* large history doesn't get fully read on every list open. */
|
|
84
|
+
export declare function discoverSessions(cwd: string, claudeHome?: string, opts?: DiscoverOptions): DiscoveredSession[];
|
|
85
|
+
export interface ClaudeSessionDiscoveryDiagnostics {
|
|
86
|
+
/** Called immediately before the 1 MiB title/content prefix is read. */
|
|
87
|
+
onContentRead?: (file: string) => void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Discover ALL recent Claude Code sessions across every project dir. Claude
|
|
91
|
+
* encodes cwd into the dir name lossily (encodeCwd), so the real cwd comes from
|
|
92
|
+
* the session file's first-line `cwd` field. After the stat/recency pass, read a
|
|
93
|
+
* small identity prefix first and apply scope. Only enabled identities receive
|
|
94
|
+
* the larger bounded content read used to find the card title.
|
|
95
|
+
*/
|
|
96
|
+
export declare function discoverRecentClaudeSessions(opts?: DiscoverOptions, claudeHome?: string, scope?: ExternalSessionDiscoveryScope, diagnostics?: ClaudeSessionDiscoveryDiagnostics): RecentExternalSession[];
|
|
97
|
+
/** Count of all session files for `cwd`, cheaply (stat only). Lets the UI know
|
|
98
|
+
* whether a bounded `discoverSessions` left older sessions unshown. */
|
|
99
|
+
export declare function countSessions(cwd: string, claudeHome?: string): number;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, existsSync, openSync, readSync, closeSync, } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
/** Encode a cwd to claude's project dir name: every non-[A-Za-z0-9] char → '-'.
|
|
5
|
+
* Mirrors `~/.claude/projects/<encoded>` (verified against real layout). */
|
|
6
|
+
export function encodeCwd(cwd) {
|
|
7
|
+
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
8
|
+
}
|
|
9
|
+
/** Resolve aliases such as a trailing slash before comparing project roots. */
|
|
10
|
+
export function normalizeExternalSessionCwd(cwd) {
|
|
11
|
+
return resolve(cwd);
|
|
12
|
+
}
|
|
13
|
+
function isWithinRoot(cwd, root) {
|
|
14
|
+
const child = relative(root, cwd);
|
|
15
|
+
return child === "" || (!child.startsWith("..") && !isAbsolute(child));
|
|
16
|
+
}
|
|
17
|
+
/** Pick the most specific registered ancestor for a session cwd. */
|
|
18
|
+
export function externalSessionProjectRootFor(cwd, roots) {
|
|
19
|
+
const normalizedCwd = normalizeExternalSessionCwd(cwd);
|
|
20
|
+
let best;
|
|
21
|
+
for (const candidate of roots) {
|
|
22
|
+
const normalizedRoot = normalizeExternalSessionCwd(candidate.cwd);
|
|
23
|
+
if (!isWithinRoot(normalizedCwd, normalizedRoot))
|
|
24
|
+
continue;
|
|
25
|
+
if (!best || normalizedRoot.length > normalizeExternalSessionCwd(best.cwd).length) {
|
|
26
|
+
best = { ...candidate, cwd: normalizedRoot };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return best;
|
|
30
|
+
}
|
|
31
|
+
/** Apply a discovery scope to one real cwd. */
|
|
32
|
+
export function externalSessionCwdEnabled(cwd, scope) {
|
|
33
|
+
if (!scope)
|
|
34
|
+
return true;
|
|
35
|
+
return (externalSessionProjectRootFor(cwd, scope.projectRoots)?.enabled ?? scope.includeUnregistered);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Cheap Claude-directory prefilter. Because encodeCwd is lossy, true only means
|
|
39
|
+
* "this directory may contain an enabled cwd". The real cwd identity is always
|
|
40
|
+
* checked before transcript content is read.
|
|
41
|
+
*/
|
|
42
|
+
export function claudeProjectDirEnabled(encodedProjectDir, scope) {
|
|
43
|
+
if (!scope)
|
|
44
|
+
return true;
|
|
45
|
+
// With an enabled unregistered baseline, an encoded name can never prove a
|
|
46
|
+
// directory is disabled: `/work/repo-other` looks like a descendant of
|
|
47
|
+
// `/work/repo`, and `/foo-bar` collides exactly with `/foo/bar`. Probe only
|
|
48
|
+
// the small identity prefix and decide from the real cwd later.
|
|
49
|
+
if (scope.includeUnregistered)
|
|
50
|
+
return true;
|
|
51
|
+
return scope.projectRoots.some((candidate) => {
|
|
52
|
+
if (!candidate.enabled)
|
|
53
|
+
return false;
|
|
54
|
+
const normalizedRoot = normalizeExternalSessionCwd(candidate.cwd);
|
|
55
|
+
const exact = encodeCwd(normalizedRoot);
|
|
56
|
+
const descendantPrefix = encodeCwd(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`);
|
|
57
|
+
return encodedProjectDir === exact || encodedProjectDir.startsWith(descendantPrefix);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Default room-list window: last 2 weeks AND at most 20 — the intersection. */
|
|
61
|
+
export const DEFAULT_DISCOVER_LIMIT = 20;
|
|
62
|
+
export const DEFAULT_DISCOVER_SINCE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
63
|
+
function claudeProjectsDir(claudeHome) {
|
|
64
|
+
return join(claudeHome, "projects");
|
|
65
|
+
}
|
|
66
|
+
/** Extract first *real* user message text, skipping caveat/command noise. */
|
|
67
|
+
function firstUserMessage(lines) {
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
if (!line.trim())
|
|
70
|
+
continue;
|
|
71
|
+
let d;
|
|
72
|
+
try {
|
|
73
|
+
d = JSON.parse(line);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (d.type !== "user")
|
|
79
|
+
continue;
|
|
80
|
+
const c = d.message?.content;
|
|
81
|
+
const text = typeof c === "string"
|
|
82
|
+
? c
|
|
83
|
+
: Array.isArray(c)
|
|
84
|
+
? c.map((p) => (typeof p?.text === "string" ? p.text : "")).join("")
|
|
85
|
+
: "";
|
|
86
|
+
const t = text.trim();
|
|
87
|
+
if (!t)
|
|
88
|
+
continue;
|
|
89
|
+
if (t.startsWith("<local-command-caveat>") || t.startsWith("<command-name>"))
|
|
90
|
+
continue;
|
|
91
|
+
return t.slice(0, 200);
|
|
92
|
+
}
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
function countUserMessages(lines) {
|
|
96
|
+
let n = 0;
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (!line.trim())
|
|
99
|
+
continue;
|
|
100
|
+
try {
|
|
101
|
+
if (JSON.parse(line).type === "user")
|
|
102
|
+
n++;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* skip */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return n;
|
|
109
|
+
}
|
|
110
|
+
/** Cheap (sessionId, mtime) pairs for every session file — stat only, no read. */
|
|
111
|
+
function listSessionStats(dir) {
|
|
112
|
+
const stats = [];
|
|
113
|
+
for (const name of readdirSync(dir)) {
|
|
114
|
+
if (!name.endsWith(".jsonl"))
|
|
115
|
+
continue;
|
|
116
|
+
const file = join(dir, name);
|
|
117
|
+
let st;
|
|
118
|
+
try {
|
|
119
|
+
st = statSync(file);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
stats.push({ sessionId: name.replace(/\.jsonl$/, ""), file, mtimeMs: st.mtimeMs });
|
|
125
|
+
}
|
|
126
|
+
return stats;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Apply the recency window + limit to mtime-sorted stats, returning the slice to
|
|
130
|
+
* deep-read. Shared by the claude + codex discovery paths so they bound work
|
|
131
|
+
* identically. `total` (caller-side) lets the UI show a "load more" affordance.
|
|
132
|
+
*/
|
|
133
|
+
export function selectRecentStats(stats, opts = {}) {
|
|
134
|
+
const now = opts.now ?? Date.now();
|
|
135
|
+
let sel = stats.slice().sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
136
|
+
if (opts.sinceMs && opts.sinceMs > 0) {
|
|
137
|
+
const cutoff = now - opts.sinceMs;
|
|
138
|
+
sel = sel.filter((s) => s.mtimeMs >= cutoff);
|
|
139
|
+
}
|
|
140
|
+
if (opts.limit && opts.limit > 0)
|
|
141
|
+
sel = sel.slice(0, opts.limit);
|
|
142
|
+
return sel;
|
|
143
|
+
}
|
|
144
|
+
/** List claude sessions for `cwd`. `claudeHome` defaults to ~/.claude (override
|
|
145
|
+
* for tests). Read-only, on-demand scan; no index. Stats every file cheaply,
|
|
146
|
+
* then deep-reads (title + count) ONLY the recent slice (opts) so an old,
|
|
147
|
+
* large history doesn't get fully read on every list open. */
|
|
148
|
+
export function discoverSessions(cwd, claudeHome = join(homedir(), ".claude"), opts = {}) {
|
|
149
|
+
const dir = join(claudeProjectsDir(claudeHome), encodeCwd(cwd));
|
|
150
|
+
if (!existsSync(dir))
|
|
151
|
+
return [];
|
|
152
|
+
const selected = selectRecentStats(listSessionStats(dir), opts);
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const s of selected) {
|
|
155
|
+
let lines;
|
|
156
|
+
try {
|
|
157
|
+
lines = readFileSync(s.file, "utf-8").split("\n");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
out.push({
|
|
163
|
+
sessionId: s.sessionId,
|
|
164
|
+
cwd,
|
|
165
|
+
firstMessage: firstUserMessage(lines),
|
|
166
|
+
lastModified: s.mtimeMs,
|
|
167
|
+
messageCount: countUserMessages(lines),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/** Read up to maxBytes of a file as UTF-8 (may span many lines), without
|
|
173
|
+
* loading the whole transcript. Newlines are preserved so callers can split. */
|
|
174
|
+
function readBoundedPrefix(file, maxBytes) {
|
|
175
|
+
const fd = openSync(file, "r");
|
|
176
|
+
try {
|
|
177
|
+
const buf = Buffer.alloc(maxBytes);
|
|
178
|
+
const n = readSync(fd, buf, 0, maxBytes, 0);
|
|
179
|
+
return buf.toString("utf-8", 0, n);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
closeSync(fd);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Read only enough of the first line to recover Claude's top-level cwd
|
|
187
|
+
* identity. The field precedes message content in Claude's records; if a
|
|
188
|
+
* nonstandard record puts it after a large payload, fail closed instead of
|
|
189
|
+
* reading transcript content merely to classify visibility.
|
|
190
|
+
*/
|
|
191
|
+
function readSessionIdentityCwd(file, maxBytes = 4 * 1024) {
|
|
192
|
+
const fd = openSync(file, "r");
|
|
193
|
+
try {
|
|
194
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
195
|
+
let total = 0;
|
|
196
|
+
while (total < maxBytes) {
|
|
197
|
+
const read = readSync(fd, buffer, total, Math.min(256, maxBytes - total), total);
|
|
198
|
+
if (read === 0)
|
|
199
|
+
return undefined;
|
|
200
|
+
total += read;
|
|
201
|
+
const text = buffer.subarray(0, total).toString("utf-8");
|
|
202
|
+
const newline = text.indexOf("\n");
|
|
203
|
+
const firstLine = newline >= 0 ? text.slice(0, newline) : text;
|
|
204
|
+
const cwdIndex = firstLine.indexOf('"cwd"');
|
|
205
|
+
const messageIndex = firstLine.indexOf('"message"');
|
|
206
|
+
if (messageIndex >= 0 && (cwdIndex < 0 || cwdIndex > messageIndex))
|
|
207
|
+
return undefined;
|
|
208
|
+
if (cwdIndex < 0) {
|
|
209
|
+
if (newline >= 0)
|
|
210
|
+
return undefined;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const match = firstLine.slice(cwdIndex).match(/^"cwd"\s*:\s*("(?:\\.|[^"\\])*")/);
|
|
214
|
+
if (!match)
|
|
215
|
+
continue;
|
|
216
|
+
try {
|
|
217
|
+
const cwd = JSON.parse(match[1]);
|
|
218
|
+
return typeof cwd === "string" ? cwd : undefined;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
closeSync(fd);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Discover ALL recent Claude Code sessions across every project dir. Claude
|
|
232
|
+
* encodes cwd into the dir name lossily (encodeCwd), so the real cwd comes from
|
|
233
|
+
* the session file's first-line `cwd` field. After the stat/recency pass, read a
|
|
234
|
+
* small identity prefix first and apply scope. Only enabled identities receive
|
|
235
|
+
* the larger bounded content read used to find the card title.
|
|
236
|
+
*/
|
|
237
|
+
export function discoverRecentClaudeSessions(opts = {}, claudeHome = join(homedir(), ".claude"), scope, diagnostics) {
|
|
238
|
+
const projects = claudeProjectsDir(claudeHome);
|
|
239
|
+
if (!existsSync(projects))
|
|
240
|
+
return [];
|
|
241
|
+
const stats = [];
|
|
242
|
+
for (const projectDir of readdirSync(projects)) {
|
|
243
|
+
// Skip directories that cannot contain an enabled root. Ambiguous lossy
|
|
244
|
+
// names proceed only to the small per-file identity read below.
|
|
245
|
+
if (!claudeProjectDirEnabled(projectDir, scope))
|
|
246
|
+
continue;
|
|
247
|
+
const dir = join(projects, projectDir);
|
|
248
|
+
let dirStat;
|
|
249
|
+
try {
|
|
250
|
+
dirStat = statSync(dir);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (!dirStat.isDirectory())
|
|
256
|
+
continue;
|
|
257
|
+
for (const entry of listSessionStats(dir)) {
|
|
258
|
+
stats.push({ file: entry.file, sessionId: entry.sessionId, mtimeMs: entry.mtimeMs });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const windowed = selectRecentStats(stats, { sinceMs: opts.sinceMs, now: opts.now });
|
|
262
|
+
const limit = opts.limit && opts.limit > 0 ? opts.limit : Infinity;
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const s of windowed) {
|
|
265
|
+
if (out.length >= limit)
|
|
266
|
+
break;
|
|
267
|
+
let cwd;
|
|
268
|
+
try {
|
|
269
|
+
cwd = readSessionIdentityCwd(s.file);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (!cwd || !externalSessionCwdEnabled(cwd, scope))
|
|
275
|
+
continue;
|
|
276
|
+
let firstMessage = "";
|
|
277
|
+
try {
|
|
278
|
+
diagnostics?.onContentRead?.(s.file);
|
|
279
|
+
firstMessage = firstUserMessage(readBoundedPrefix(s.file, 1 << 20).split("\n"));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
out.push({ sessionId: s.sessionId, cwd, file: s.file, lastModified: s.mtimeMs, firstMessage });
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
/** Count of all session files for `cwd`, cheaply (stat only). Lets the UI know
|
|
289
|
+
* whether a bounded `discoverSessions` left older sessions unshown. */
|
|
290
|
+
export function countSessions(cwd, claudeHome = join(homedir(), ".claude")) {
|
|
291
|
+
const dir = join(claudeProjectsDir(claudeHome), encodeCwd(cwd));
|
|
292
|
+
if (!existsSync(dir))
|
|
293
|
+
return 0;
|
|
294
|
+
return listSessionStats(dir).length;
|
|
295
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Normalized external coding-agent history message. */
|
|
2
|
+
export interface HistoryMessage {
|
|
3
|
+
role: "user" | "assistant";
|
|
4
|
+
text: string;
|
|
5
|
+
/** `summary` is a lossy one-field preview; `args` is the full tool_use input
|
|
6
|
+
* (e.g. a sub-agent's `prompt`) so a replayed tool card can show the real
|
|
7
|
+
* parameters, not just the whitelisted field. */
|
|
8
|
+
tools?: {
|
|
9
|
+
name: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
args?: Record<string, unknown>;
|
|
12
|
+
}[];
|
|
13
|
+
ts?: number;
|
|
14
|
+
}
|
|
15
|
+
/** One render-relevant event parsed from an appended external-CLI transcript
|
|
16
|
+
* line. Kept CLI-neutral so the desktop room follower can feed Claude Code and
|
|
17
|
+
* Codex through the same RoomManager push path. */
|
|
18
|
+
export type SessionTailEvent = {
|
|
19
|
+
type: "user";
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "assistant";
|
|
23
|
+
text: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: "tool";
|
|
26
|
+
id?: string;
|
|
27
|
+
name: string;
|
|
28
|
+
summary: string;
|
|
29
|
+
args?: Record<string, unknown>;
|
|
30
|
+
} | {
|
|
31
|
+
type: "tool_result";
|
|
32
|
+
id?: string;
|
|
33
|
+
result: string;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
} | {
|
|
36
|
+
type: "turn_end";
|
|
37
|
+
reason: string;
|
|
38
|
+
};
|
|
39
|
+
/** Parse a bounded/raw Claude Code transcript snapshot. Exported so a live
|
|
40
|
+
* follower can take its initial snapshot and EOF cursor from the same read,
|
|
41
|
+
* eliminating the snapshot→subscribe race. */
|
|
42
|
+
export declare function parseRecentHistory(raw: string, limit: number): {
|
|
43
|
+
messages: HistoryMessage[];
|
|
44
|
+
hasMore: boolean;
|
|
45
|
+
totalCount: number;
|
|
46
|
+
};
|
|
47
|
+
/** Parse one newly-appended Claude Code transcript JSONL line into the compact
|
|
48
|
+
* event vocabulary consumed by the desktop room follower. */
|
|
49
|
+
export declare function parseClaudeTranscriptLine(line: string): SessionTailEvent[];
|
|
50
|
+
/** Read the last `limit` user/assistant messages from a claude session jsonl. */
|
|
51
|
+
export declare function readRecentHistory(cwd: string, sessionId: string, limit: number, claudeHome?: string): {
|
|
52
|
+
messages: HistoryMessage[];
|
|
53
|
+
hasMore: boolean;
|
|
54
|
+
totalCount: number;
|
|
55
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { encodeCwd } from "./session-discovery.js";
|
|
5
|
+
const NOISE = ["<local-command-caveat>", "<command-name>"];
|
|
6
|
+
function textOf(content) {
|
|
7
|
+
if (typeof content === "string")
|
|
8
|
+
return content;
|
|
9
|
+
if (Array.isArray(content))
|
|
10
|
+
return content.map((p) => (typeof p?.text === "string" ? p.text : "")).join("");
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
function toolsOf(content) {
|
|
14
|
+
if (!Array.isArray(content))
|
|
15
|
+
return [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const p of content) {
|
|
18
|
+
if (p?.type === "tool_use") {
|
|
19
|
+
const inp = p.input ?? {};
|
|
20
|
+
const summary = inp.command ?? inp.file_path ?? inp.path ?? inp.url ?? inp.pattern ?? inp.query ?? "";
|
|
21
|
+
const args = inp && typeof inp === "object" && Object.keys(inp).length > 0 ? inp : undefined;
|
|
22
|
+
out.push({
|
|
23
|
+
name: typeof p.name === "string" ? p.name : "tool",
|
|
24
|
+
summary: String(summary).slice(0, 120),
|
|
25
|
+
args,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function toolResultText(content) {
|
|
32
|
+
if (typeof content === "string")
|
|
33
|
+
return content;
|
|
34
|
+
if (!Array.isArray(content))
|
|
35
|
+
return "";
|
|
36
|
+
return content
|
|
37
|
+
.map((part) => typeof part === "string" ? part : typeof part?.text === "string" ? part.text : "")
|
|
38
|
+
.join("");
|
|
39
|
+
}
|
|
40
|
+
/** Parse a bounded/raw Claude Code transcript snapshot. Exported so a live
|
|
41
|
+
* follower can take its initial snapshot and EOF cursor from the same read,
|
|
42
|
+
* eliminating the snapshot→subscribe race. */
|
|
43
|
+
export function parseRecentHistory(raw, limit) {
|
|
44
|
+
const all = [];
|
|
45
|
+
for (const line of raw.split("\n")) {
|
|
46
|
+
if (!line.trim())
|
|
47
|
+
continue;
|
|
48
|
+
let d;
|
|
49
|
+
try {
|
|
50
|
+
d = JSON.parse(line);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (d.type === "user") {
|
|
56
|
+
const t = textOf(d.message?.content).trim();
|
|
57
|
+
if (!t || NOISE.some((n) => t.startsWith(n)))
|
|
58
|
+
continue;
|
|
59
|
+
all.push({ role: "user", text: t });
|
|
60
|
+
}
|
|
61
|
+
else if (d.type === "assistant") {
|
|
62
|
+
const t = textOf(d.message?.content).trim();
|
|
63
|
+
const tools = toolsOf(d.message?.content);
|
|
64
|
+
if (!t && tools.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
all.push({ role: "assistant", text: t, tools: tools.length ? tools : undefined });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const lim = limit > 0 ? limit : 20;
|
|
70
|
+
const start = Math.max(0, all.length - lim);
|
|
71
|
+
return { messages: all.slice(start), hasMore: start > 0, totalCount: all.length };
|
|
72
|
+
}
|
|
73
|
+
/** Parse one newly-appended Claude Code transcript JSONL line into the compact
|
|
74
|
+
* event vocabulary consumed by the desktop room follower. */
|
|
75
|
+
export function parseClaudeTranscriptLine(line) {
|
|
76
|
+
let d;
|
|
77
|
+
try {
|
|
78
|
+
d = JSON.parse(line);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const out = [];
|
|
84
|
+
if (d.type === "user") {
|
|
85
|
+
const content = d.message?.content;
|
|
86
|
+
if (Array.isArray(content)) {
|
|
87
|
+
for (const part of content) {
|
|
88
|
+
if (part?.type !== "tool_result")
|
|
89
|
+
continue;
|
|
90
|
+
out.push({
|
|
91
|
+
type: "tool_result",
|
|
92
|
+
id: typeof part.tool_use_id === "string" ? part.tool_use_id : undefined,
|
|
93
|
+
result: toolResultText(part.content).slice(0, 4000),
|
|
94
|
+
isError: Boolean(part.is_error),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const text = textOf(content).trim();
|
|
99
|
+
if (text && !NOISE.some((noise) => text.startsWith(noise))) {
|
|
100
|
+
out.unshift({ type: "user", text });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
if (d.type === "assistant" && Array.isArray(d.message?.content)) {
|
|
105
|
+
for (const part of d.message.content) {
|
|
106
|
+
if (part?.type === "text" && typeof part.text === "string" && part.text) {
|
|
107
|
+
out.push({ type: "assistant", text: part.text });
|
|
108
|
+
}
|
|
109
|
+
else if (part?.type === "tool_use") {
|
|
110
|
+
const input = part.input && typeof part.input === "object"
|
|
111
|
+
? part.input
|
|
112
|
+
: undefined;
|
|
113
|
+
out.push({
|
|
114
|
+
type: "tool",
|
|
115
|
+
id: typeof part.id === "string" ? part.id : undefined,
|
|
116
|
+
name: typeof part.name === "string" ? part.name : "tool",
|
|
117
|
+
summary: toolsOf([part])[0]?.summary ?? "",
|
|
118
|
+
args: input,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (d.message.stop_reason === "end_turn") {
|
|
123
|
+
out.push({ type: "turn_end", reason: "completed" });
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
if (d.type === "result") {
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
type: "turn_end",
|
|
131
|
+
reason: typeof d.subtype === "string" ? d.subtype : "completed",
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
/** Read the last `limit` user/assistant messages from a claude session jsonl. */
|
|
138
|
+
export function readRecentHistory(cwd, sessionId, limit, claudeHome = join(homedir(), ".claude")) {
|
|
139
|
+
const file = join(claudeHome, "projects", encodeCwd(cwd), `${sessionId}.jsonl`);
|
|
140
|
+
if (!existsSync(file))
|
|
141
|
+
return { messages: [], hasMore: false, totalCount: 0 };
|
|
142
|
+
let raw;
|
|
143
|
+
try {
|
|
144
|
+
raw = readFileSync(file, "utf-8");
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return { messages: [], hasMore: false, totalCount: 0 };
|
|
148
|
+
}
|
|
149
|
+
return parseRecentHistory(raw, limit);
|
|
150
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ExternalAgentsSettings, ResolvedExternalAgentsConfig } from "./types.js";
|
|
2
|
+
/** Resolve coding CLI defaults without involving the agent kernel. */
|
|
3
|
+
export declare function resolveExternalAgentConfig(settings: ExternalAgentsSettings | undefined): ResolvedExternalAgentsConfig;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Resolve coding CLI defaults without involving the agent kernel. */
|
|
2
|
+
export function resolveExternalAgentConfig(settings) {
|
|
3
|
+
return {
|
|
4
|
+
claudeCode: {
|
|
5
|
+
command: settings?.claudeCode?.command ?? "claude",
|
|
6
|
+
defaultMode: settings?.claudeCode?.defaultMode ?? "safe",
|
|
7
|
+
dangerousArgs: settings?.claudeCode?.dangerousArgs ?? [],
|
|
8
|
+
trustedWorkspaces: settings?.claudeCode?.trustedWorkspaces ?? [],
|
|
9
|
+
autoStartInTrustedWorkspaces: settings?.claudeCode?.autoStartInTrustedWorkspaces ?? false,
|
|
10
|
+
},
|
|
11
|
+
codex: {
|
|
12
|
+
command: settings?.codex?.command ?? "codex",
|
|
13
|
+
args: settings?.codex?.args ?? [],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Permission profile for an external coding agent. */
|
|
2
|
+
export type ExternalAgentMode = "safe" | "dangerous";
|
|
3
|
+
export interface ClaudeCodeSettings {
|
|
4
|
+
command?: string;
|
|
5
|
+
defaultMode?: ExternalAgentMode;
|
|
6
|
+
dangerousArgs?: string[];
|
|
7
|
+
trustedWorkspaces?: string[];
|
|
8
|
+
autoStartInTrustedWorkspaces?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CodexSettings {
|
|
11
|
+
command?: string;
|
|
12
|
+
args?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ExternalAgentsSettings {
|
|
15
|
+
claudeCode?: ClaudeCodeSettings;
|
|
16
|
+
codex?: CodexSettings;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedClaudeCodeSettings {
|
|
19
|
+
command: string;
|
|
20
|
+
defaultMode: ExternalAgentMode;
|
|
21
|
+
dangerousArgs: string[];
|
|
22
|
+
trustedWorkspaces: string[];
|
|
23
|
+
autoStartInTrustedWorkspaces: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolvedCodexSettings {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface ResolvedExternalAgentsConfig {
|
|
30
|
+
claudeCode: ResolvedClaudeCodeSettings;
|
|
31
|
+
codex: ResolvedCodexSettings;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|