@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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a Codex thread to the CodeShell session tool host that serves it.
|
|
3
|
+
*
|
|
4
|
+
* One loopback MCP bridge serves every concurrent Codex thread, so this map is
|
|
5
|
+
* what keeps two sessions apart. Identity arrives as `_meta.threadId`, injected
|
|
6
|
+
* by the Codex app-server and — verified empirically against codex-cli 0.145.0,
|
|
7
|
+
* see `docs/todo/evidence/` — not influenceable by the model. Tool *arguments*
|
|
8
|
+
* are the opposite: fully model-controlled, and therefore never consulted here.
|
|
9
|
+
*
|
|
10
|
+
* Every failure mode is a refusal. There is no "use the foreground session",
|
|
11
|
+
* no "most recent thread", and no "there's only one, so it must be that one" —
|
|
12
|
+
* §11.3 and §22.5 reject all of those, because each turns a background run into
|
|
13
|
+
* a cross-session action.
|
|
14
|
+
*
|
|
15
|
+
* Memory-only by design (§13.6): hosts hold live executors and approval routes,
|
|
16
|
+
* so the map is rebuilt after a restart rather than persisted.
|
|
17
|
+
*/
|
|
18
|
+
/** Anything the bridge can dispatch a tool call to. Structural on purpose so
|
|
19
|
+
* this file stays free of a hard dependency on the host implementation. */
|
|
20
|
+
export interface ToolHostRef {
|
|
21
|
+
readonly businessSessionId: string;
|
|
22
|
+
}
|
|
23
|
+
export type SessionContextMissReason = "missing_thread_id" | "unknown_thread" | "stale_generation" | "ambiguous_thread";
|
|
24
|
+
export type SessionContextResult<T> = {
|
|
25
|
+
ok: true;
|
|
26
|
+
host: T;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: SessionContextMissReason;
|
|
30
|
+
};
|
|
31
|
+
export interface ResolveRequest {
|
|
32
|
+
/** From `_meta.threadId` (or `_meta["x-codex-turn-metadata"].thread_id`). */
|
|
33
|
+
threadId: string | undefined;
|
|
34
|
+
/** App-server generation the request belongs to; see {@link bumpGeneration}. */
|
|
35
|
+
generation: number;
|
|
36
|
+
}
|
|
37
|
+
export declare class SessionContextStore<T extends ToolHostRef = ToolHostRef> {
|
|
38
|
+
/** Non-enumerable so a stray `JSON.stringify(store)` cannot leak live hosts. */
|
|
39
|
+
private readonly byThread;
|
|
40
|
+
private currentGeneration;
|
|
41
|
+
get size(): number;
|
|
42
|
+
get generation(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Bind a thread to its host, in the store's CURRENT generation.
|
|
45
|
+
*
|
|
46
|
+
* The generation is a property of the store (one app-server lifetime), not of
|
|
47
|
+
* an individual entry, and callers deliberately cannot choose it. An earlier
|
|
48
|
+
* revision took it as a parameter and raised the counter to match, which made
|
|
49
|
+
* one registration reorder reachability for every other thread: registering at
|
|
50
|
+
* a lower generation stranded the new thread immediately, and registering at a
|
|
51
|
+
* higher one mass-evicted every healthy thread. Both were invisible to the
|
|
52
|
+
* suite because its only fencing test walked the happy sequence.
|
|
53
|
+
*/
|
|
54
|
+
register(threadId: string, host: T): void;
|
|
55
|
+
/** Drop a thread. Call this BEFORE closing the host, so a late request finds
|
|
56
|
+
* nothing rather than a disposed host (§13.4 ordering). */
|
|
57
|
+
unregister(threadId: string): void;
|
|
58
|
+
clear(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Advance the generation, e.g. after an app-server restart. Requests stamped
|
|
61
|
+
* with an older generation are refused even if their thread id was re-used,
|
|
62
|
+
* which is what stops a late reply from the dead process landing on the new
|
|
63
|
+
* session (§13.6).
|
|
64
|
+
*/
|
|
65
|
+
bumpGeneration(): number;
|
|
66
|
+
resolve(request: ResolveRequest): SessionContextResult<T>;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a batch that must belong to exactly one thread.
|
|
69
|
+
*
|
|
70
|
+
* Resolving each item separately would let a mixed batch touch two sessions
|
|
71
|
+
* on the strength of a single authorization, so a batch spanning threads is
|
|
72
|
+
* refused as a whole. An empty batch is refused too: there is no thread to
|
|
73
|
+
* attribute it to, and "nothing to do" is not a reason to hand back a host.
|
|
74
|
+
*/
|
|
75
|
+
resolveBatch(threadIds: readonly (string | undefined)[], generation: number): SessionContextResult<T>;
|
|
76
|
+
/** Threads currently bound, for diagnostics. Ids only — never hosts. */
|
|
77
|
+
threadIds(): readonly string[];
|
|
78
|
+
/** Keep live hosts out of any accidental serialization (§12.4 logging rules). */
|
|
79
|
+
toJSON(): {
|
|
80
|
+
generation: number;
|
|
81
|
+
threadCount: number;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a Codex thread to the CodeShell session tool host that serves it.
|
|
3
|
+
*
|
|
4
|
+
* One loopback MCP bridge serves every concurrent Codex thread, so this map is
|
|
5
|
+
* what keeps two sessions apart. Identity arrives as `_meta.threadId`, injected
|
|
6
|
+
* by the Codex app-server and — verified empirically against codex-cli 0.145.0,
|
|
7
|
+
* see `docs/todo/evidence/` — not influenceable by the model. Tool *arguments*
|
|
8
|
+
* are the opposite: fully model-controlled, and therefore never consulted here.
|
|
9
|
+
*
|
|
10
|
+
* Every failure mode is a refusal. There is no "use the foreground session",
|
|
11
|
+
* no "most recent thread", and no "there's only one, so it must be that one" —
|
|
12
|
+
* §11.3 and §22.5 reject all of those, because each turns a background run into
|
|
13
|
+
* a cross-session action.
|
|
14
|
+
*
|
|
15
|
+
* Memory-only by design (§13.6): hosts hold live executors and approval routes,
|
|
16
|
+
* so the map is rebuilt after a restart rather than persisted.
|
|
17
|
+
*/
|
|
18
|
+
export class SessionContextStore {
|
|
19
|
+
/** Non-enumerable so a stray `JSON.stringify(store)` cannot leak live hosts. */
|
|
20
|
+
byThread = new Map();
|
|
21
|
+
currentGeneration = 1;
|
|
22
|
+
get size() {
|
|
23
|
+
return this.byThread.size;
|
|
24
|
+
}
|
|
25
|
+
get generation() {
|
|
26
|
+
return this.currentGeneration;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Bind a thread to its host, in the store's CURRENT generation.
|
|
30
|
+
*
|
|
31
|
+
* The generation is a property of the store (one app-server lifetime), not of
|
|
32
|
+
* an individual entry, and callers deliberately cannot choose it. An earlier
|
|
33
|
+
* revision took it as a parameter and raised the counter to match, which made
|
|
34
|
+
* one registration reorder reachability for every other thread: registering at
|
|
35
|
+
* a lower generation stranded the new thread immediately, and registering at a
|
|
36
|
+
* higher one mass-evicted every healthy thread. Both were invisible to the
|
|
37
|
+
* suite because its only fencing test walked the happy sequence.
|
|
38
|
+
*/
|
|
39
|
+
register(threadId, host) {
|
|
40
|
+
this.byThread.set(threadId, { host, generation: this.currentGeneration });
|
|
41
|
+
}
|
|
42
|
+
/** Drop a thread. Call this BEFORE closing the host, so a late request finds
|
|
43
|
+
* nothing rather than a disposed host (§13.4 ordering). */
|
|
44
|
+
unregister(threadId) {
|
|
45
|
+
this.byThread.delete(threadId);
|
|
46
|
+
}
|
|
47
|
+
clear() {
|
|
48
|
+
this.byThread.clear();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Advance the generation, e.g. after an app-server restart. Requests stamped
|
|
52
|
+
* with an older generation are refused even if their thread id was re-used,
|
|
53
|
+
* which is what stops a late reply from the dead process landing on the new
|
|
54
|
+
* session (§13.6).
|
|
55
|
+
*/
|
|
56
|
+
bumpGeneration() {
|
|
57
|
+
this.currentGeneration += 1;
|
|
58
|
+
// Drop the entries the bump just made unreachable. Leaving them would pin
|
|
59
|
+
// live SessionToolHost objects — each holding an executor and an approval
|
|
60
|
+
// route — against GC for the process lifetime, growing with every restart.
|
|
61
|
+
// Callers re-register the threads they successfully resume.
|
|
62
|
+
this.byThread.clear();
|
|
63
|
+
return this.currentGeneration;
|
|
64
|
+
}
|
|
65
|
+
resolve(request) {
|
|
66
|
+
if (!request.threadId)
|
|
67
|
+
return { ok: false, reason: "missing_thread_id" };
|
|
68
|
+
const entry = this.byThread.get(request.threadId);
|
|
69
|
+
if (!entry)
|
|
70
|
+
return { ok: false, reason: "unknown_thread" };
|
|
71
|
+
// The load-bearing half is `request.generation`: it fences a request that was
|
|
72
|
+
// already in flight when the app-server restarted. Comparing the request
|
|
73
|
+
// against `entry.generation` instead would let a caller resurrect a stale
|
|
74
|
+
// entry by handing the stale number back in — the caller's word is not
|
|
75
|
+
// evidence about which generation is live.
|
|
76
|
+
//
|
|
77
|
+
// The `entry.generation` half is unreachable belt-and-braces today, because
|
|
78
|
+
// `bumpGeneration()` clears the map, so a surviving entry is always current.
|
|
79
|
+
// It is kept so that a future change which stops clearing cannot silently
|
|
80
|
+
// reopen the hole — but a mutation test will NOT flag its removal, and
|
|
81
|
+
// claiming otherwise would overstate the guard.
|
|
82
|
+
if (entry.generation !== this.currentGeneration ||
|
|
83
|
+
request.generation !== this.currentGeneration) {
|
|
84
|
+
return { ok: false, reason: "stale_generation" };
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, host: entry.host };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a batch that must belong to exactly one thread.
|
|
90
|
+
*
|
|
91
|
+
* Resolving each item separately would let a mixed batch touch two sessions
|
|
92
|
+
* on the strength of a single authorization, so a batch spanning threads is
|
|
93
|
+
* refused as a whole. An empty batch is refused too: there is no thread to
|
|
94
|
+
* attribute it to, and "nothing to do" is not a reason to hand back a host.
|
|
95
|
+
*/
|
|
96
|
+
resolveBatch(threadIds, generation) {
|
|
97
|
+
if (threadIds.length === 0)
|
|
98
|
+
return { ok: false, reason: "missing_thread_id" };
|
|
99
|
+
if (threadIds.some((id) => !id))
|
|
100
|
+
return { ok: false, reason: "missing_thread_id" };
|
|
101
|
+
const unique = new Set(threadIds);
|
|
102
|
+
if (unique.size > 1)
|
|
103
|
+
return { ok: false, reason: "ambiguous_thread" };
|
|
104
|
+
return this.resolve({ threadId: [...unique][0], generation });
|
|
105
|
+
}
|
|
106
|
+
/** Threads currently bound, for diagnostics. Ids only — never hosts. */
|
|
107
|
+
threadIds() {
|
|
108
|
+
return [...this.byThread.keys()];
|
|
109
|
+
}
|
|
110
|
+
/** Keep live hosts out of any accidental serialization (§12.4 logging rules). */
|
|
111
|
+
toJSON() {
|
|
112
|
+
return { generation: this.currentGeneration, threadCount: this.byThread.size };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for a spawned Codex process.
|
|
3
|
+
*
|
|
4
|
+
* The one non-obvious entry is `NO_PROXY`. Codex's Rust MCP client (reqwest)
|
|
5
|
+
* honours `HTTP_PROXY`/`HTTPS_PROXY`, and on a corporate network or behind a PAC
|
|
6
|
+
* file it will route even a `127.0.0.1` request through the upstream proxy. The
|
|
7
|
+
* proxy does not understand localhost, answers with an HTML error page, and every
|
|
8
|
+
* MCP transport dies with `UnexpectedContentType`. The user-visible symptom is
|
|
9
|
+
* "Codex cannot see any CodeShell tools" with nothing in the bridge log, because
|
|
10
|
+
* the request never arrives.
|
|
11
|
+
*
|
|
12
|
+
* Learned from the `makecindy/cindy` reference implementation (design §16)
|
|
13
|
+
* rather than the hard way.
|
|
14
|
+
*/
|
|
15
|
+
export interface RuntimeSpawnEnvOptions {
|
|
16
|
+
/** Base environment; defaults to the current process env. */
|
|
17
|
+
base?: NodeJS.ProcessEnv;
|
|
18
|
+
/** Bearer token env var name and value for the loopback MCP bridge. */
|
|
19
|
+
bridgeToken?: {
|
|
20
|
+
name: string;
|
|
21
|
+
value: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Merge the loopback hosts into `NO_PROXY`, preserving whatever the user already
|
|
26
|
+
* set. The lowercase `no_proxy` twin is deleted rather than also updated: Rust
|
|
27
|
+
* and Go read both, and leaving two spellings behind lets one silently override
|
|
28
|
+
* the other depending on which library looks first.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildRuntimeSpawnEnv(options?: RuntimeSpawnEnvOptions): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for a spawned Codex process.
|
|
3
|
+
*
|
|
4
|
+
* The one non-obvious entry is `NO_PROXY`. Codex's Rust MCP client (reqwest)
|
|
5
|
+
* honours `HTTP_PROXY`/`HTTPS_PROXY`, and on a corporate network or behind a PAC
|
|
6
|
+
* file it will route even a `127.0.0.1` request through the upstream proxy. The
|
|
7
|
+
* proxy does not understand localhost, answers with an HTML error page, and every
|
|
8
|
+
* MCP transport dies with `UnexpectedContentType`. The user-visible symptom is
|
|
9
|
+
* "Codex cannot see any CodeShell tools" with nothing in the bridge log, because
|
|
10
|
+
* the request never arrives.
|
|
11
|
+
*
|
|
12
|
+
* Learned from the `makecindy/cindy` reference implementation (design §16)
|
|
13
|
+
* rather than the hard way.
|
|
14
|
+
*/
|
|
15
|
+
const LOOPBACK_NO_PROXY = ["127.0.0.1", "localhost", "::1"];
|
|
16
|
+
/**
|
|
17
|
+
* Merge the loopback hosts into `NO_PROXY`, preserving whatever the user already
|
|
18
|
+
* set. The lowercase `no_proxy` twin is deleted rather than also updated: Rust
|
|
19
|
+
* and Go read both, and leaving two spellings behind lets one silently override
|
|
20
|
+
* the other depending on which library looks first.
|
|
21
|
+
*/
|
|
22
|
+
export function buildRuntimeSpawnEnv(options = {}) {
|
|
23
|
+
const base = options.base ?? process.env;
|
|
24
|
+
const env = { ...base };
|
|
25
|
+
const existing = [env.NO_PROXY, env.no_proxy]
|
|
26
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
27
|
+
.flatMap((value) => value.split(","))
|
|
28
|
+
.map((entry) => entry.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
const merged = [...existing];
|
|
31
|
+
for (const host of LOOPBACK_NO_PROXY) {
|
|
32
|
+
if (!merged.some((entry) => entry.toLowerCase() === host))
|
|
33
|
+
merged.push(host);
|
|
34
|
+
}
|
|
35
|
+
env.NO_PROXY = merged.join(",");
|
|
36
|
+
delete env.no_proxy;
|
|
37
|
+
if (options.bridgeToken) {
|
|
38
|
+
env[options.bridgeToken.name] = options.bridgeToken.value;
|
|
39
|
+
}
|
|
40
|
+
return env;
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One coding-repository git log entry. */
|
|
2
|
+
export interface GitLogEntry {
|
|
3
|
+
hash: string;
|
|
4
|
+
message: string;
|
|
5
|
+
author: string;
|
|
6
|
+
date: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse `git log --format=%H|%s|%an|%ci` output into entries. Tolerates
|
|
10
|
+
* malformed/short lines (missing `|` separators) by defaulting absent fields
|
|
11
|
+
* to "" rather than destructuring `undefined` and crashing on `.slice`
|
|
12
|
+
* (review-2026-05-30). Blank lines are skipped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseGitLog(raw: string): GitLogEntry[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `git log --format=%H|%s|%an|%ci` output into entries. Tolerates
|
|
3
|
+
* malformed/short lines (missing `|` separators) by defaulting absent fields
|
|
4
|
+
* to "" rather than destructuring `undefined` and crashing on `.slice`
|
|
5
|
+
* (review-2026-05-30). Blank lines are skipped.
|
|
6
|
+
*/
|
|
7
|
+
export function parseGitLog(raw) {
|
|
8
|
+
return raw
|
|
9
|
+
.split("\n")
|
|
10
|
+
.filter((line) => line.trim() !== "")
|
|
11
|
+
.map((line) => {
|
|
12
|
+
const parts = line.split("|");
|
|
13
|
+
const hash = parts[0] ?? "";
|
|
14
|
+
return {
|
|
15
|
+
hash: hash.slice(0, 8),
|
|
16
|
+
message: parts[1] ?? "",
|
|
17
|
+
author: parts[2] ?? "",
|
|
18
|
+
date: parts[3] ?? "",
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utility functions for the git workflow commands.
|
|
3
|
+
*
|
|
4
|
+
* Every call goes through execFileSync with an argv array — no command
|
|
5
|
+
* strings, no shell interpolation. Even arguments that look "safe" (already
|
|
6
|
+
* quoted, hard-coded) are passed as separate argv tokens so a future caller
|
|
7
|
+
* can't accidentally widen the attack surface by interpolating user input
|
|
8
|
+
* into the string form.
|
|
9
|
+
*/
|
|
10
|
+
import { type GitLogEntry } from "./parse-log.js";
|
|
11
|
+
export interface GitStatusEntry {
|
|
12
|
+
status: string;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
export type { GitLogEntry };
|
|
16
|
+
export declare function isGitRepo(cwd: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a directory to its PROJECT ROOT: the enclosing git repository's
|
|
19
|
+
* top-level dir if `cwd` is inside a git repo, otherwise `cwd` unchanged.
|
|
20
|
+
*
|
|
21
|
+
* This is the project-boundary rule the desktop uses when adding/identifying a
|
|
22
|
+
* project: picking a SUBDIRECTORY of a git repo should belong to that one repo
|
|
23
|
+
* (its root), not spawn a separate project per subdir — mirrors how editors
|
|
24
|
+
* (and Claude Code) treat a repo as one workspace. A non-git folder is its own
|
|
25
|
+
* project (returned as-is). Never throws; on any git failure falls back to cwd.
|
|
26
|
+
* Returns the git-reported toplevel (already absolute, forward-slashed on win).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveProjectRoot(cwd: string): string;
|
|
29
|
+
export declare function getCurrentBranch(cwd: string): string;
|
|
30
|
+
export declare function getGitStatus(cwd: string): GitStatusEntry[];
|
|
31
|
+
export declare function getGitDiff(cwd: string, opts?: {
|
|
32
|
+
staged?: boolean;
|
|
33
|
+
file?: string;
|
|
34
|
+
}): string;
|
|
35
|
+
export declare function getGitDiffStat(cwd: string, opts?: {
|
|
36
|
+
staged?: boolean;
|
|
37
|
+
file?: string;
|
|
38
|
+
}): string;
|
|
39
|
+
export declare function getGitLog(cwd: string, n?: number): GitLogEntry[];
|
|
40
|
+
export declare function getRemoteUrl(cwd: string): string | undefined;
|
|
41
|
+
export declare function gitAdd(cwd: string, files?: string[]): void;
|
|
42
|
+
export declare function gitCommit(cwd: string, message: string): string;
|
|
43
|
+
export declare function gitListBranches(cwd: string): {
|
|
44
|
+
name: string;
|
|
45
|
+
current: boolean;
|
|
46
|
+
}[];
|
|
47
|
+
export declare function gitCheckout(cwd: string, branch: string, create?: boolean): void;
|
|
48
|
+
export declare function ghAvailable(): boolean;
|
|
49
|
+
export declare function ghPrComments(cwd: string, prUrl: string): string;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utility functions for the git workflow commands.
|
|
3
|
+
*
|
|
4
|
+
* Every call goes through execFileSync with an argv array — no command
|
|
5
|
+
* strings, no shell interpolation. Even arguments that look "safe" (already
|
|
6
|
+
* quoted, hard-coded) are passed as separate argv tokens so a future caller
|
|
7
|
+
* can't accidentally widen the attack surface by interpolating user input
|
|
8
|
+
* into the string form.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { realpathSync } from "node:fs";
|
|
12
|
+
import { parseGitLog } from "./parse-log.js";
|
|
13
|
+
import { resolveExecutable, resolveGit } from "@cjhyy/code-shell-core/extension";
|
|
14
|
+
// Resolve git/gh through PATH×PATHEXT on Windows so a .cmd/.exe shim is found
|
|
15
|
+
// (bare execFile doesn't walk PATHEXT). No-op on POSIX. See utils/exec.ts.
|
|
16
|
+
const GH_BIN = resolveExecutable("gh");
|
|
17
|
+
/** Run git with an argv array and return its trimmed stdout. */
|
|
18
|
+
function git(cwd, args, timeoutMs = 10000) {
|
|
19
|
+
return execFileSync(resolveGit(), args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
|
|
20
|
+
}
|
|
21
|
+
/** Run gh with an argv array and return its trimmed stdout. */
|
|
22
|
+
function gh(cwd, args, timeoutMs = 10000) {
|
|
23
|
+
return execFileSync(GH_BIN, args, { cwd, encoding: "utf-8", timeout: timeoutMs }).trim();
|
|
24
|
+
}
|
|
25
|
+
export function isGitRepo(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
git(cwd, ["rev-parse", "--is-inside-work-tree"], 5000);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a directory to its PROJECT ROOT: the enclosing git repository's
|
|
36
|
+
* top-level dir if `cwd` is inside a git repo, otherwise `cwd` unchanged.
|
|
37
|
+
*
|
|
38
|
+
* This is the project-boundary rule the desktop uses when adding/identifying a
|
|
39
|
+
* project: picking a SUBDIRECTORY of a git repo should belong to that one repo
|
|
40
|
+
* (its root), not spawn a separate project per subdir — mirrors how editors
|
|
41
|
+
* (and Claude Code) treat a repo as one workspace. A non-git folder is its own
|
|
42
|
+
* project (returned as-is). Never throws; on any git failure falls back to cwd.
|
|
43
|
+
* Returns the git-reported toplevel (already absolute, forward-slashed on win).
|
|
44
|
+
*/
|
|
45
|
+
export function resolveProjectRoot(cwd) {
|
|
46
|
+
let realCwd = cwd;
|
|
47
|
+
try {
|
|
48
|
+
realCwd = realpathSync(cwd);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Non-existent paths are allowed to fall back unchanged below.
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const top = git(realCwd, ["rev-parse", "--show-toplevel"], 5000);
|
|
55
|
+
return top ? realpathSync(top) : realCwd;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return realCwd;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function getCurrentBranch(cwd) {
|
|
62
|
+
return git(cwd, ["branch", "--show-current"], 5000);
|
|
63
|
+
}
|
|
64
|
+
export function getGitStatus(cwd) {
|
|
65
|
+
const raw = git(cwd, ["status", "--porcelain"], 10000);
|
|
66
|
+
if (!raw)
|
|
67
|
+
return [];
|
|
68
|
+
return raw.split("\n").map((line) => ({
|
|
69
|
+
status: line.slice(0, 2).trim(),
|
|
70
|
+
path: line.slice(3),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
export function getGitDiff(cwd, opts) {
|
|
74
|
+
const args = ["diff", "--no-color"];
|
|
75
|
+
if (opts?.staged)
|
|
76
|
+
args.push("--staged");
|
|
77
|
+
// `--` terminates option parsing so a file path starting with `-` can't
|
|
78
|
+
// be re-interpreted as a flag.
|
|
79
|
+
if (opts?.file)
|
|
80
|
+
args.push("--", opts.file);
|
|
81
|
+
return git(cwd, args, 30000);
|
|
82
|
+
}
|
|
83
|
+
export function getGitDiffStat(cwd, opts) {
|
|
84
|
+
const args = ["diff", "--stat", "--no-color"];
|
|
85
|
+
if (opts?.staged)
|
|
86
|
+
args.push("--staged");
|
|
87
|
+
if (opts?.file)
|
|
88
|
+
args.push("--", opts.file);
|
|
89
|
+
return git(cwd, args, 10000);
|
|
90
|
+
}
|
|
91
|
+
export function getGitLog(cwd, n = 10) {
|
|
92
|
+
// `n` is numeric — coerce/validate to keep the argv clean even if a caller
|
|
93
|
+
// hands us a string.
|
|
94
|
+
const count = Number.isFinite(n) && n > 0 ? Math.floor(n) : 10;
|
|
95
|
+
const raw = git(cwd, ["log", "--oneline", "--format=%H|%s|%an|%ci", `-${count}`], 10000);
|
|
96
|
+
if (!raw)
|
|
97
|
+
return [];
|
|
98
|
+
return parseGitLog(raw);
|
|
99
|
+
}
|
|
100
|
+
export function getRemoteUrl(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
return git(cwd, ["remote", "get-url", "origin"], 5000);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function gitAdd(cwd, files = ["."]) {
|
|
109
|
+
// `--` ensures a path starting with `-` cannot be parsed as a flag.
|
|
110
|
+
// Each file is its own argv token, so spaces / quotes / non-ASCII pass
|
|
111
|
+
// through verbatim with no shell parsing.
|
|
112
|
+
execFileSync(resolveGit(), ["add", "--", ...files], { cwd, timeout: 10000 });
|
|
113
|
+
}
|
|
114
|
+
export function gitCommit(cwd, message) {
|
|
115
|
+
// Pre-fix this used `JSON.stringify(message)` which only happened to be
|
|
116
|
+
// safe because JSON.stringify covers most shell metacharacters — but it's
|
|
117
|
+
// not real escaping. The argv form is.
|
|
118
|
+
return execFileSync(resolveGit(), ["commit", "-m", message], {
|
|
119
|
+
cwd,
|
|
120
|
+
encoding: "utf-8",
|
|
121
|
+
timeout: 30000,
|
|
122
|
+
}).trim();
|
|
123
|
+
}
|
|
124
|
+
export function gitListBranches(cwd) {
|
|
125
|
+
const raw = git(cwd, ["branch", "--no-color"], 5000);
|
|
126
|
+
if (!raw)
|
|
127
|
+
return [];
|
|
128
|
+
return raw.split("\n").map((line) => ({
|
|
129
|
+
current: line.startsWith("*"),
|
|
130
|
+
name: line.replace(/^\*?\s+/, "").trim(),
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
export function gitCheckout(cwd, branch, create = false) {
|
|
134
|
+
// argv form defeats shell injection. We additionally reject branch names
|
|
135
|
+
// that start with `-` so a value like `--orphan` can't slip through git's
|
|
136
|
+
// own option parser (the trailing `--` trick doesn't help on checkout:
|
|
137
|
+
// git interprets it as the option's value, not as an option terminator).
|
|
138
|
+
if (typeof branch !== "string" || branch.length === 0) {
|
|
139
|
+
throw new Error("branch must be a non-empty string");
|
|
140
|
+
}
|
|
141
|
+
if (branch.startsWith("-")) {
|
|
142
|
+
throw new Error(`refusing branch name that starts with '-': ${branch}`);
|
|
143
|
+
}
|
|
144
|
+
const args = create ? ["checkout", "-b", branch] : ["checkout", branch];
|
|
145
|
+
execFileSync(resolveGit(), args, { cwd, timeout: 10000 });
|
|
146
|
+
}
|
|
147
|
+
export function ghAvailable() {
|
|
148
|
+
try {
|
|
149
|
+
execFileSync(GH_BIN, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export function ghPrComments(cwd, prUrl) {
|
|
157
|
+
// Pre-fix `gh pr view ${prUrl}` interpolated the URL directly. argv form
|
|
158
|
+
// means even a value like "$(rm -rf ~)" is sent to gh as a literal
|
|
159
|
+
// positional argument (which gh will then reject as a bad PR URL).
|
|
160
|
+
return gh(cwd, ["pr", "view", prUrl, "--comments", "--json", "comments", "-q", ".comments[].body"], 30000);
|
|
161
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type SandboxBackend } from "@cjhyy/code-shell-core/extension";
|
|
2
|
+
export interface WorktreeSession {
|
|
3
|
+
originalCwd: string;
|
|
4
|
+
worktreePath: string;
|
|
5
|
+
worktreeName: string;
|
|
6
|
+
worktreeBranch: string;
|
|
7
|
+
originalBranch?: string;
|
|
8
|
+
/** Immutable commit used to create the branch. Safe for later ahead checks. */
|
|
9
|
+
baseRef?: string;
|
|
10
|
+
/** User-facing ref selector (`head`, `fresh`, or an explicit ref). */
|
|
11
|
+
baseRefLabel?: string;
|
|
12
|
+
/** Gitignored files copied from `.worktreeinclude` / DriveAgent include patterns. */
|
|
13
|
+
includedFiles?: string[];
|
|
14
|
+
sessionId: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
/** Per-platform setup/cleanup scripts (a project's localEnvironment). */
|
|
18
|
+
export interface PlatformScripts {
|
|
19
|
+
default?: string;
|
|
20
|
+
macos?: string;
|
|
21
|
+
linux?: string;
|
|
22
|
+
windows?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CreateWorktreeOptions {
|
|
25
|
+
prefix?: string;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
/** `head`, `fresh` (local origin/HEAD), or an explicit git ref. */
|
|
28
|
+
baseRef?: string;
|
|
29
|
+
/** Extra gitignore-style patterns, combined with a root `.worktreeinclude`. */
|
|
30
|
+
include?: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Best-effort rollback for an aborted `git worktree add`.
|
|
34
|
+
*
|
|
35
|
+
* `allowRecursiveDelete` gates the `rm -rf` fallback. It must stay `false`
|
|
36
|
+
* whenever the worktree path already existed before this call: a `git worktree
|
|
37
|
+
* add` that fails with "already exists" never registered the path, so the
|
|
38
|
+
* fallback would recursively delete a directory (and any uncommitted work in
|
|
39
|
+
* it) that this call did not create. Only genuine partial-creation states —
|
|
40
|
+
* where `worktree add` succeeded and a later step failed — may fall back to a
|
|
41
|
+
* recursive delete.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cleanupAbortedWorktree(gitRoot: string, worktreePath: string, branchName: string, allowRecursiveDelete?: boolean): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Pick the setup/cleanup script for the running platform, falling back to
|
|
46
|
+
* `default`. Empty/whitespace-only scripts are treated as absent so a project
|
|
47
|
+
* can leave a platform key blank without spawning an empty shell. `platform`
|
|
48
|
+
* defaults to `process.platform` so callers usually omit it; tests pass a
|
|
49
|
+
* fixed value.
|
|
50
|
+
*/
|
|
51
|
+
export declare function selectPlatformScript(scripts: PlatformScripts | undefined, platform?: NodeJS.Platform): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Create an isolated git worktree for an agent session.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createWorktree(cwd: string, slug: string, sessionId: string, opts?: CreateWorktreeOptions): Promise<WorktreeSession>;
|
|
56
|
+
/** Copy gitignored configuration selected by `.worktreeinclude` and explicit
|
|
57
|
+
* patterns. Only regular files reported by `git ls-files --ignored` are copied,
|
|
58
|
+
* so patterns cannot escape the repository or overwrite tracked checkout data. */
|
|
59
|
+
export declare function copyWorktreeIncludes(sourceRoot: string, worktreePath: string, explicitPatterns: readonly string[] | undefined): string[];
|
|
60
|
+
export interface WorktreeSetupResult {
|
|
61
|
+
/** True if no setup script was configured for this platform (nothing ran). */
|
|
62
|
+
skipped: boolean;
|
|
63
|
+
/** True if a script ran and exited 0. */
|
|
64
|
+
ok: boolean;
|
|
65
|
+
/** Combined stdout/stderr, for surfacing to the user on failure. */
|
|
66
|
+
output: string;
|
|
67
|
+
/** Exit code when a script ran; undefined when skipped. */
|
|
68
|
+
exitCode?: number | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run a project's `localEnvironment.setupScripts` once, in the freshly-created
|
|
72
|
+
* worktree's root, right after `git worktree add`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function runWorktreeSetup(worktreePath: string, script: string | undefined, opts?: {
|
|
75
|
+
sandbox?: SandboxBackend;
|
|
76
|
+
shellEnv?: Record<string, string>;
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<WorktreeSetupResult>;
|
|
80
|
+
export interface RemoveWorktreeResult {
|
|
81
|
+
/** True once `git worktree remove` completed and the directory is gone. */
|
|
82
|
+
dirRemoved: boolean;
|
|
83
|
+
/** The branch targeted for deletion when removeBranch=true. */
|
|
84
|
+
branch?: string;
|
|
85
|
+
/** True when removeBranch=true and branch deletion completed. */
|
|
86
|
+
branchDeleted?: boolean;
|
|
87
|
+
/** Non-empty when the worktree directory is gone but branch deletion failed. */
|
|
88
|
+
branchError?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface RemoveWorktreeOptions {
|
|
91
|
+
prefix?: string;
|
|
92
|
+
}
|
|
93
|
+
export interface WorktreeChangeState {
|
|
94
|
+
uncommitted: boolean;
|
|
95
|
+
commitsAhead: number;
|
|
96
|
+
hasChanges: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Inspect both working-tree changes and commits made since the immutable base
|
|
99
|
+
* commit captured at creation time. */
|
|
100
|
+
export declare function inspectWorktreeChanges(worktreePath: string, baseRef?: string): WorktreeChangeState;
|
|
101
|
+
/** Prevent pruning/removal while an external agent owns the worktree. */
|
|
102
|
+
export declare function lockWorktree(worktreePath: string, reason: string): void;
|
|
103
|
+
/** Unlock is idempotent for lifecycle cleanup/keep paths. */
|
|
104
|
+
export declare function unlockWorktree(worktreePath: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Remove a worktree and optionally its branch.
|
|
107
|
+
*/
|
|
108
|
+
export declare function removeWorktree(worktreePath: string, removeBranch?: boolean, opts?: RemoveWorktreeOptions): RemoveWorktreeResult;
|