@cjhyy/code-shell-capability-coding 0.9.44 → 0.9.47
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/dist/capability-runtime.js +4 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +3 -1
- package/dist/cc-orchestrator/codex-session-history.js +11 -4
- package/dist/cc-orchestrator/external-agent-changes.js +7 -65
- package/dist/cc-orchestrator/external-agent-driver.js +61 -3
- package/dist/cc-orchestrator/session-discovery.d.ts +4 -2
- package/dist/cc-orchestrator/session-discovery.js +63 -6
- package/dist/cc-orchestrator/session-history.js +5 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +7 -0
- package/dist/external-runtimes/claude-code/event-translator.js +58 -9
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +8 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +16 -3
- package/dist/external-runtimes/claude-code/permissions.d.ts +69 -0
- package/dist/external-runtimes/claude-code/permissions.js +120 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +43 -4
- package/dist/external-runtimes/claude-code/runtime.js +155 -39
- package/dist/external-runtimes/claude-code/text-inference.d.ts +22 -0
- package/dist/external-runtimes/claude-code/text-inference.js +240 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +15 -0
- package/dist/external-runtimes/codex/app-server-client.js +69 -6
- package/dist/external-runtimes/codex/event-translator.js +16 -2
- package/dist/external-runtimes/codex/runtime.d.ts +14 -0
- package/dist/external-runtimes/codex/runtime.js +152 -16
- package/dist/external-runtimes/index.d.ts +4 -0
- package/dist/external-runtimes/index.js +2 -0
- package/dist/external-runtimes/session-factory.d.ts +16 -0
- package/dist/external-runtimes/session-factory.js +43 -1
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +36 -1
- package/dist/external-runtimes/shared/mcp-bridge.js +93 -13
- package/dist/git/parse-log.d.ts +6 -2
- package/dist/git/parse-log.js +7 -3
- package/dist/git/utils.d.ts +4 -0
- package/dist/git/utils.js +29 -10
- package/dist/git/worktree/crud.d.ts +2 -0
- package/dist/git/worktree/crud.js +29 -1
- package/dist/git/worktree/diff.js +57 -29
- package/dist/index.capability.js +14 -1
- package/dist/index.git.d.ts +1 -0
- package/dist/index.git.js +1 -0
- package/dist/lsp/client.d.ts +3 -0
- package/dist/lsp/client.js +96 -25
- package/dist/lsp/manager.d.ts +2 -0
- package/dist/lsp/manager.js +56 -15
- package/dist/prompt/coding.md +1 -0
- package/dist/quota/index.d.ts +2 -1
- package/dist/quota/index.js +27 -7
- package/dist/quota/types.d.ts +5 -2
- package/dist/review/review-prompt.d.ts +2 -0
- package/dist/review/review-prompt.js +70 -29
- package/dist/review/review-target.d.ts +29 -0
- package/dist/review/review-target.js +353 -0
- package/dist/tools/apply-patch/applier.js +64 -39
- package/dist/tools/apply-patch/backup-targets.d.ts +8 -0
- package/dist/tools/apply-patch/backup-targets.js +24 -0
- package/dist/tools/apply-patch/parser.js +4 -2
- package/dist/tools/check-quota.js +3 -1
- package/dist/tools/lsp.js +36 -5
- package/package.json +2 -2
package/dist/git/utils.js
CHANGED
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { realpathSync } from "node:fs";
|
|
12
|
-
import { parseGitLog } from "./parse-log.js";
|
|
12
|
+
import { GIT_LOG_FORMAT, parseGitLog } from "./parse-log.js";
|
|
13
13
|
import { resolveExecutable, resolveGit } from "@cjhyy/code-shell-core/extension";
|
|
14
14
|
// Resolve git/gh through PATH×PATHEXT on Windows so a .cmd/.exe shim is found
|
|
15
15
|
// (bare execFile doesn't walk PATHEXT). No-op on POSIX. See utils/exec.ts.
|
|
16
16
|
const GH_BIN = resolveExecutable("gh");
|
|
17
|
+
/** Run git with an argv array and return its untrimmed stdout. */
|
|
18
|
+
function gitRaw(cwd, args, timeoutMs = 10000) {
|
|
19
|
+
return execFileSync(resolveGit(), args, { cwd, encoding: "utf-8", timeout: timeoutMs });
|
|
20
|
+
}
|
|
17
21
|
/** Run git with an argv array and return its trimmed stdout. */
|
|
18
22
|
function git(cwd, args, timeoutMs = 10000) {
|
|
19
|
-
return
|
|
23
|
+
return gitRaw(cwd, args, timeoutMs).trim();
|
|
20
24
|
}
|
|
21
25
|
/** Run gh with an argv array and return its trimmed stdout. */
|
|
22
26
|
function gh(cwd, args, timeoutMs = 10000) {
|
|
@@ -62,13 +66,22 @@ export function getCurrentBranch(cwd) {
|
|
|
62
66
|
return git(cwd, ["branch", "--show-current"], 5000);
|
|
63
67
|
}
|
|
64
68
|
export function getGitStatus(cwd) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// `-z` output is NUL-terminated and unquoted. It must not be trimmed: the
|
|
70
|
+
// fixed-width `XY ` prefix of an unstaged entry starts with a space.
|
|
71
|
+
const records = gitRaw(cwd, ["status", "--porcelain", "-z"], 10000).split("\0");
|
|
72
|
+
const entries = [];
|
|
73
|
+
for (let i = 0; i < records.length; i++) {
|
|
74
|
+
const record = records[i];
|
|
75
|
+
if (record.length < 4)
|
|
76
|
+
continue;
|
|
77
|
+
const xy = record.slice(0, 2);
|
|
78
|
+
const entry = { status: xy.trim(), path: record.slice(3) };
|
|
79
|
+
// Rename/copy entries are followed by a separate source-path record.
|
|
80
|
+
if (xy.includes("R") || xy.includes("C"))
|
|
81
|
+
entry.origPath = records[++i] ?? "";
|
|
82
|
+
entries.push(entry);
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
72
85
|
}
|
|
73
86
|
export function getGitDiff(cwd, opts) {
|
|
74
87
|
const args = ["diff", "--no-color"];
|
|
@@ -88,11 +101,17 @@ export function getGitDiffStat(cwd, opts) {
|
|
|
88
101
|
args.push("--", opts.file);
|
|
89
102
|
return git(cwd, args, 10000);
|
|
90
103
|
}
|
|
104
|
+
/** Untracked, non-ignored files under `cwd`, relative to `cwd` (what `git add .` would add). */
|
|
105
|
+
export function getUntrackedFiles(cwd) {
|
|
106
|
+
return gitRaw(cwd, ["ls-files", "--others", "--exclude-standard", "-z"], 10000)
|
|
107
|
+
.split("\0")
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
}
|
|
91
110
|
export function getGitLog(cwd, n = 10) {
|
|
92
111
|
// `n` is numeric — coerce/validate to keep the argv clean even if a caller
|
|
93
112
|
// hands us a string.
|
|
94
113
|
const count = Number.isFinite(n) && n > 0 ? Math.floor(n) : 10;
|
|
95
|
-
const raw = git(cwd, ["log",
|
|
114
|
+
const raw = git(cwd, ["log", `--format=${GIT_LOG_FORMAT}`, `-${count}`], 10000);
|
|
96
115
|
if (!raw)
|
|
97
116
|
return [];
|
|
98
117
|
return parseGitLog(raw);
|
|
@@ -89,6 +89,8 @@ export interface RemoveWorktreeResult {
|
|
|
89
89
|
}
|
|
90
90
|
export interface RemoveWorktreeOptions {
|
|
91
91
|
prefix?: string;
|
|
92
|
+
/** False lets Git reject working-tree changes made after the caller's preflight. */
|
|
93
|
+
force?: boolean;
|
|
92
94
|
}
|
|
93
95
|
export interface WorktreeChangeState {
|
|
94
96
|
uncommitted: boolean;
|
|
@@ -48,6 +48,30 @@ export function selectPlatformScript(scripts, platform = process.platform) {
|
|
|
48
48
|
const trimmed = candidate?.trim();
|
|
49
49
|
return trimmed ? candidate : undefined;
|
|
50
50
|
}
|
|
51
|
+
/** Tail of the pending creations per canonical repository root. */
|
|
52
|
+
const creationQueues = new Map();
|
|
53
|
+
/**
|
|
54
|
+
* Run one worktree creation at a time per repository. The prechecks and the
|
|
55
|
+
* rollback below assume that an absent branch/path seen by this call stays
|
|
56
|
+
* this call's to create; two concurrent identical requests would otherwise
|
|
57
|
+
* both pass the prechecks, and the loser's rollback would force-remove the
|
|
58
|
+
* winner's worktree, branch and uncommitted files.
|
|
59
|
+
*/
|
|
60
|
+
async function withRepositoryCreationLock(gitRoot, run) {
|
|
61
|
+
const previous = creationQueues.get(gitRoot) ?? Promise.resolve();
|
|
62
|
+
let release;
|
|
63
|
+
const tail = previous.then(() => new Promise((resolveRelease) => (release = resolveRelease)));
|
|
64
|
+
creationQueues.set(gitRoot, tail);
|
|
65
|
+
await previous;
|
|
66
|
+
try {
|
|
67
|
+
return await run();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
release();
|
|
71
|
+
if (creationQueues.get(gitRoot) === tail)
|
|
72
|
+
creationQueues.delete(gitRoot);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
51
75
|
/**
|
|
52
76
|
* Create an isolated git worktree for an agent session.
|
|
53
77
|
*/
|
|
@@ -55,6 +79,10 @@ export async function createWorktree(cwd, slug, sessionId, opts = {}) {
|
|
|
55
79
|
validateWorktreeSlug(slug);
|
|
56
80
|
throwIfAborted(opts.signal);
|
|
57
81
|
const gitRoot = await findGitRoot(cwd);
|
|
82
|
+
throwIfAborted(opts.signal);
|
|
83
|
+
return await withRepositoryCreationLock(gitRoot, () => createWorktreeLocked(cwd, gitRoot, slug, sessionId, opts));
|
|
84
|
+
}
|
|
85
|
+
async function createWorktreeLocked(cwd, gitRoot, slug, sessionId, opts) {
|
|
58
86
|
throwIfAborted(opts.signal);
|
|
59
87
|
const branchName = applyPrefix(opts.prefix, slug, sessionId);
|
|
60
88
|
const worktreePath = resolve(gitRoot, "..", `.worktrees/${slug}-${sessionId.slice(0, 8)}`);
|
|
@@ -328,7 +356,7 @@ export function removeWorktree(worktreePath, removeBranch = false, opts = {}) {
|
|
|
328
356
|
}
|
|
329
357
|
}
|
|
330
358
|
try {
|
|
331
|
-
execGitSync(mainRoot, ["worktree", "remove", worktreePath, "--force"], 30000);
|
|
359
|
+
execGitSync(mainRoot, ["worktree", "remove", worktreePath, ...(opts.force === false ? [] : ["--force"])], 30000);
|
|
332
360
|
}
|
|
333
361
|
catch (err) {
|
|
334
362
|
throw new Error(`failed to remove worktree ${worktreePath}: ${gitErrorMessage(err)}`, {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/** Git worktree diff support owned by the coding capability. */
|
|
2
|
-
import { gitOutput } from "./git-exec.js";
|
|
2
|
+
import { execGit, gitErrorMessage, gitOutput } from "./git-exec.js";
|
|
3
3
|
export async function getWorktreeDiff(worktreePath, baseRef) {
|
|
4
|
-
const comparisonBase =
|
|
5
|
-
? baseRef
|
|
6
|
-
: await findComparisonBaseRef(worktreePath);
|
|
4
|
+
const comparisonBase = await findSummaryComparisonBase(worktreePath, baseRef);
|
|
7
5
|
return diffSummary(worktreePath, comparisonBase);
|
|
8
6
|
}
|
|
9
7
|
export async function worktreeHasUncommittedChanges(worktreePath) {
|
|
@@ -29,11 +27,33 @@ async function commitRefExists(cwd, ref) {
|
|
|
29
27
|
const out = await gitOutput(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], 5000);
|
|
30
28
|
return out !== undefined;
|
|
31
29
|
}
|
|
30
|
+
async function findSummaryComparisonBase(cwd, baseRef) {
|
|
31
|
+
const candidates = new Set([
|
|
32
|
+
...(baseRef ? [baseRef] : []),
|
|
33
|
+
"main",
|
|
34
|
+
"master",
|
|
35
|
+
"origin/main",
|
|
36
|
+
"origin/master",
|
|
37
|
+
]);
|
|
38
|
+
for (const ref of candidates) {
|
|
39
|
+
try {
|
|
40
|
+
await execGit(cwd, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], 5000);
|
|
41
|
+
return ref;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// --verify --quiet exits with 1 for an absent ref. Infrastructure errors
|
|
45
|
+
// must reach the UI instead of being reported as a clean workspace.
|
|
46
|
+
if (error.code !== 1)
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
32
52
|
async function diffSummary(cwd, baseRef) {
|
|
33
53
|
const [dirtyFiles, committedFiles, aheadCommits] = await Promise.all([
|
|
34
54
|
statusFileSet(cwd),
|
|
35
55
|
baseRef ? changedFilesSinceBase(cwd, baseRef) : Promise.resolve(new Set()),
|
|
36
|
-
baseRef ?
|
|
56
|
+
baseRef ? readAheadCommitCount(cwd, baseRef) : Promise.resolve(0),
|
|
37
57
|
]);
|
|
38
58
|
const changedFiles = new Set([...dirtyFiles, ...committedFiles]);
|
|
39
59
|
return {
|
|
@@ -44,12 +64,26 @@ async function diffSummary(cwd, baseRef) {
|
|
|
44
64
|
};
|
|
45
65
|
}
|
|
46
66
|
async function changedFilesSinceBase(cwd, baseRef) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await execGit(cwd, ["diff", "--name-only", "-z", `${baseRef}...HEAD`]);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// Unrelated histories have no merge base. Keep the existing two-dot
|
|
73
|
+
// comparison for that case, while surfacing timeouts and other failures.
|
|
74
|
+
if (!/no merge base/i.test(gitErrorMessage(error)))
|
|
75
|
+
throw error;
|
|
76
|
+
raw = await execGit(cwd, ["diff", "--name-only", "-z", `${baseRef}..HEAD`]);
|
|
77
|
+
}
|
|
78
|
+
return new Set(raw.split("\0").filter(Boolean));
|
|
79
|
+
}
|
|
80
|
+
async function readAheadCommitCount(cwd, baseRef) {
|
|
81
|
+
const out = await execGit(cwd, ["rev-list", "--count", `${baseRef}..HEAD`]);
|
|
82
|
+
const count = Number(out.trim());
|
|
83
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
84
|
+
throw new Error("Git returned an invalid ahead commit count");
|
|
85
|
+
}
|
|
86
|
+
return count;
|
|
53
87
|
}
|
|
54
88
|
async function aheadCommitCount(cwd, baseRef) {
|
|
55
89
|
const out = await gitOutput(cwd, ["rev-list", "--count", `${baseRef}..HEAD`]);
|
|
@@ -57,27 +91,21 @@ async function aheadCommitCount(cwd, baseRef) {
|
|
|
57
91
|
return Number.isFinite(n) ? n : 0;
|
|
58
92
|
}
|
|
59
93
|
async function statusFileSet(cwd) {
|
|
60
|
-
|
|
94
|
+
// Background summaries must not contend with a foreground Git operation for
|
|
95
|
+
// the optional index refresh lock. NUL records preserve arbitrary file names.
|
|
96
|
+
const raw = await execGit(cwd, ["--no-optional-locks", "status", "--porcelain=v1", "-z"]);
|
|
61
97
|
const files = new Set();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const path =
|
|
98
|
+
const records = raw.split("\0");
|
|
99
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
100
|
+
const record = records[i];
|
|
101
|
+
const path = record.slice(3);
|
|
66
102
|
if (!path)
|
|
67
103
|
continue;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
104
|
+
files.add(path);
|
|
105
|
+
// In porcelain -z, the destination comes first and the following NUL
|
|
106
|
+
// record contains the source of a rename or copy.
|
|
107
|
+
if (/[RC]/.test(record.slice(0, 2)))
|
|
108
|
+
i += 1;
|
|
71
109
|
}
|
|
72
110
|
return files;
|
|
73
111
|
}
|
|
74
|
-
function unquoteGitPath(path) {
|
|
75
|
-
if (!path.startsWith('"') || !path.endsWith('"'))
|
|
76
|
-
return path;
|
|
77
|
-
try {
|
|
78
|
-
return JSON.parse(path);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return path.slice(1, -1);
|
|
82
|
-
}
|
|
83
|
-
}
|
package/dist/index.capability.js
CHANGED
|
@@ -5,7 +5,7 @@ import { lspTool, lspToolDef } from "./tools/lsp.js";
|
|
|
5
5
|
import { notebookEditTool, notebookEditToolDef } from "./tools/notebook-edit.js";
|
|
6
6
|
import { applyPatchTool, applyPatchToolDef } from "./tools/apply-patch/index.js";
|
|
7
7
|
import { parsePatch } from "./tools/apply-patch/parser.js";
|
|
8
|
-
import { patchBackupTargets } from "./tools/apply-patch/backup-targets.js";
|
|
8
|
+
import { patchBackupTargets, patchDiffTargets } from "./tools/apply-patch/backup-targets.js";
|
|
9
9
|
import { enterWorktreeTool, enterWorktreeToolDef, exitWorktreeTool, exitWorktreeToolDef, switchSessionWorkspaceTool, switchSessionWorkspaceToolDef, } from "./tools/worktree.js";
|
|
10
10
|
import { branchExists, isGitWorktreeRoot } from "./git/worktree.js";
|
|
11
11
|
import { checkQuotaTool, checkQuotaToolDef } from "./tools/check-quota.js";
|
|
@@ -235,10 +235,23 @@ export const TERMINAL_CODING_PRESET = {
|
|
|
235
235
|
const CODING_PROMPT_SECTIONS = {
|
|
236
236
|
coding: readFileSync(new URL("./prompt/coding.md", import.meta.url), "utf-8"),
|
|
237
237
|
};
|
|
238
|
+
function notebookMutationTargets(args, cwd) {
|
|
239
|
+
return typeof args.file_path === "string" &&
|
|
240
|
+
args.file_path.length > 0 &&
|
|
241
|
+
(args.action === "insert" || args.action === "replace" || args.action === "delete")
|
|
242
|
+
? [resolve(cwd, args.file_path)]
|
|
243
|
+
: [];
|
|
244
|
+
}
|
|
238
245
|
const CODING_FILE_HISTORY = [
|
|
239
246
|
{
|
|
240
247
|
toolName: "ApplyPatch",
|
|
241
248
|
resolveTargets: (args, cwd) => typeof args.patch === "string" ? patchBackupTargets(args.patch, cwd) : [],
|
|
249
|
+
resolveDiffTargets: (args, cwd) => typeof args.patch === "string" ? patchDiffTargets(args.patch, cwd) : [],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
toolName: "NotebookEdit",
|
|
253
|
+
resolveTargets: notebookMutationTargets,
|
|
254
|
+
resolveDiffTargets: notebookMutationTargets,
|
|
242
255
|
},
|
|
243
256
|
];
|
|
244
257
|
const CODING_SESSION_WORKSPACE = {
|
package/dist/index.git.d.ts
CHANGED
package/dist/index.git.js
CHANGED
package/dist/lsp/client.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare class LSPClient extends EventEmitter {
|
|
|
28
28
|
private buffer;
|
|
29
29
|
private contentLength;
|
|
30
30
|
private initialized;
|
|
31
|
+
private failure;
|
|
31
32
|
constructor(command: string, args?: string[], cwd?: string | undefined);
|
|
32
33
|
start(): Promise<void>;
|
|
33
34
|
initialize(rootUri: string): Promise<unknown>;
|
|
@@ -36,6 +37,8 @@ export declare class LSPClient extends EventEmitter {
|
|
|
36
37
|
shutdown(): Promise<void>;
|
|
37
38
|
get isInitialized(): boolean;
|
|
38
39
|
get isAlive(): boolean;
|
|
40
|
+
private assertWritable;
|
|
41
|
+
private fail;
|
|
39
42
|
private handleData;
|
|
40
43
|
private rejectAll;
|
|
41
44
|
}
|
package/dist/lsp/client.js
CHANGED
|
@@ -18,6 +18,7 @@ export class LSPClient extends EventEmitter {
|
|
|
18
18
|
buffer = Buffer.alloc(0);
|
|
19
19
|
contentLength = -1;
|
|
20
20
|
initialized = false;
|
|
21
|
+
failure;
|
|
21
22
|
constructor(command, args = [], cwd) {
|
|
22
23
|
super();
|
|
23
24
|
this.command = command;
|
|
@@ -25,21 +26,56 @@ export class LSPClient extends EventEmitter {
|
|
|
25
26
|
this.cwd = cwd;
|
|
26
27
|
}
|
|
27
28
|
async start() {
|
|
28
|
-
|
|
29
|
+
if (this.isAlive)
|
|
30
|
+
return;
|
|
31
|
+
this.failure = undefined;
|
|
32
|
+
this.initialized = false;
|
|
33
|
+
this.buffer = Buffer.alloc(0);
|
|
34
|
+
this.contentLength = -1;
|
|
35
|
+
const child = spawn(this.command, this.args, {
|
|
29
36
|
cwd: this.cwd,
|
|
30
37
|
stdio: ["pipe", "pipe", "pipe"],
|
|
31
38
|
});
|
|
32
|
-
this.process
|
|
33
|
-
|
|
39
|
+
this.process = child;
|
|
40
|
+
const fail = (error) => {
|
|
41
|
+
if (this.process !== child)
|
|
42
|
+
return;
|
|
43
|
+
this.fail(error);
|
|
44
|
+
};
|
|
45
|
+
child.stdout?.on("data", (data) => {
|
|
46
|
+
if (this.process === child && !this.failure)
|
|
47
|
+
this.handleData(data);
|
|
48
|
+
});
|
|
49
|
+
child.stderr?.on("data", (_data) => {
|
|
34
50
|
// LSP servers often log to stderr — ignore
|
|
35
51
|
});
|
|
36
|
-
|
|
52
|
+
// Pipe errors are emitted on the streams, not on ChildProcess. Keep these
|
|
53
|
+
// listeners through teardown so late EPIPE events cannot escape the host.
|
|
54
|
+
child.stdin?.on("error", fail);
|
|
55
|
+
child.stdout?.on("error", fail);
|
|
56
|
+
child.stderr?.on("error", fail);
|
|
57
|
+
child.on("error", fail);
|
|
58
|
+
child.on("exit", (code, signal) => {
|
|
59
|
+
if (this.process !== child)
|
|
60
|
+
return;
|
|
61
|
+
this.fail(new Error(`LSP server exited with ${signal ? `signal ${signal}` : `code ${code}`}`));
|
|
37
62
|
this.emit("exit", code);
|
|
38
|
-
this.rejectAll(new Error(`LSP server exited with code ${code}`));
|
|
39
63
|
});
|
|
40
64
|
// Don't keep the event loop alive solely for the LSP child.
|
|
41
65
|
// Call after listeners are wired so event registration doesn't re-ref.
|
|
42
|
-
|
|
66
|
+
child.unref();
|
|
67
|
+
await new Promise((resolve, reject) => {
|
|
68
|
+
const onSpawn = () => {
|
|
69
|
+
child.off("error", onError);
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
const onError = (error) => {
|
|
73
|
+
child.off("spawn", onSpawn);
|
|
74
|
+
reject(error);
|
|
75
|
+
};
|
|
76
|
+
child.once("spawn", onSpawn);
|
|
77
|
+
child.once("error", onError);
|
|
78
|
+
});
|
|
43
79
|
}
|
|
44
80
|
async initialize(rootUri) {
|
|
45
81
|
const result = await this.request("initialize", {
|
|
@@ -63,8 +99,7 @@ export class LSPClient extends EventEmitter {
|
|
|
63
99
|
return result;
|
|
64
100
|
}
|
|
65
101
|
async request(method, params) {
|
|
66
|
-
|
|
67
|
-
throw new Error("LSP server not started");
|
|
102
|
+
this.assertWritable();
|
|
68
103
|
const id = this.nextId++;
|
|
69
104
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
70
105
|
const content = JSON.stringify(message);
|
|
@@ -88,43 +123,79 @@ export class LSPClient extends EventEmitter {
|
|
|
88
123
|
reject(err);
|
|
89
124
|
},
|
|
90
125
|
});
|
|
91
|
-
|
|
126
|
+
try {
|
|
127
|
+
this.process.stdin.write(header + content);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
131
|
+
}
|
|
92
132
|
});
|
|
93
133
|
}
|
|
94
134
|
async notify(method, params) {
|
|
95
|
-
|
|
96
|
-
throw new Error("LSP server not started");
|
|
135
|
+
this.assertWritable();
|
|
97
136
|
const message = { jsonrpc: "2.0", method, params };
|
|
98
137
|
const content = JSON.stringify(message);
|
|
99
138
|
const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
|
|
100
|
-
|
|
139
|
+
try {
|
|
140
|
+
this.process.stdin.write(header + content);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
101
146
|
}
|
|
102
147
|
async shutdown() {
|
|
103
|
-
|
|
148
|
+
const child = this.process;
|
|
149
|
+
if (!child)
|
|
104
150
|
return;
|
|
105
151
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
152
|
+
if (this.isAlive && this.initialized) {
|
|
153
|
+
await this.request("shutdown", {});
|
|
154
|
+
await this.notify("exit", {});
|
|
155
|
+
}
|
|
108
156
|
}
|
|
109
157
|
catch {
|
|
110
158
|
// Force kill
|
|
111
159
|
}
|
|
112
|
-
this.process
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
this.process.kill();
|
|
160
|
+
if (this.process !== child)
|
|
161
|
+
return;
|
|
162
|
+
this.fail(new Error("LSP server shut down"));
|
|
116
163
|
this.process = undefined;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
this.pending.clear();
|
|
164
|
+
child.stdin?.destroy();
|
|
165
|
+
child.stdout?.destroy();
|
|
166
|
+
child.stderr?.destroy();
|
|
122
167
|
}
|
|
123
168
|
get isInitialized() {
|
|
124
169
|
return this.initialized;
|
|
125
170
|
}
|
|
126
171
|
get isAlive() {
|
|
127
|
-
return this.process
|
|
172
|
+
return (!!this.process &&
|
|
173
|
+
!this.failure &&
|
|
174
|
+
!this.process.killed &&
|
|
175
|
+
this.process.exitCode === null &&
|
|
176
|
+
this.process.signalCode === null);
|
|
177
|
+
}
|
|
178
|
+
assertWritable() {
|
|
179
|
+
if (!this.isAlive || !this.process?.stdin || this.process.stdin.destroyed)
|
|
180
|
+
throw this.failure ?? new Error("LSP server is not running");
|
|
181
|
+
}
|
|
182
|
+
fail(error) {
|
|
183
|
+
if (this.failure)
|
|
184
|
+
return;
|
|
185
|
+
this.failure = error;
|
|
186
|
+
this.initialized = false;
|
|
187
|
+
this.rejectAll(error);
|
|
188
|
+
const child = this.process;
|
|
189
|
+
if (child?.pid && child.exitCode === null && child.signalCode === null) {
|
|
190
|
+
child.kill();
|
|
191
|
+
const timer = setTimeout(() => {
|
|
192
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
193
|
+
child.kill("SIGKILL");
|
|
194
|
+
}, 500);
|
|
195
|
+
timer.unref();
|
|
196
|
+
child.once("exit", () => clearTimeout(timer));
|
|
197
|
+
}
|
|
198
|
+
this.emit("failure", error);
|
|
128
199
|
}
|
|
129
200
|
handleData(data) {
|
|
130
201
|
// Append the new chunk as bytes. Buffer.concat avoids the UTF-8
|
package/dist/lsp/manager.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ type ServerState = "stopped" | "starting" | "ready" | "error";
|
|
|
7
7
|
export declare class LSPServerManager {
|
|
8
8
|
private servers;
|
|
9
9
|
private rootUri;
|
|
10
|
+
private stopping;
|
|
10
11
|
constructor(cwd: string);
|
|
11
12
|
/**
|
|
12
13
|
* Get or start a language server by name.
|
|
@@ -33,6 +34,7 @@ export declare class LSPServerManager {
|
|
|
33
34
|
* Shutdown all servers.
|
|
34
35
|
*/
|
|
35
36
|
shutdownAll(): Promise<void>;
|
|
37
|
+
private stopServers;
|
|
36
38
|
}
|
|
37
39
|
export declare function isCommandAvailable(command: string, env?: NodeJS.ProcessEnv): boolean;
|
|
38
40
|
export declare function initializeLSPManager(cwd: string): LSPServerManager;
|
package/dist/lsp/manager.js
CHANGED
|
@@ -11,6 +11,7 @@ import { rootUriToPath } from "./root-path.js";
|
|
|
11
11
|
export class LSPServerManager {
|
|
12
12
|
servers = new Map();
|
|
13
13
|
rootUri;
|
|
14
|
+
stopping;
|
|
14
15
|
constructor(cwd) {
|
|
15
16
|
this.rootUri = pathToFileURL(cwd).href;
|
|
16
17
|
// Register all built-in servers
|
|
@@ -22,19 +23,25 @@ export class LSPServerManager {
|
|
|
22
23
|
* Get or start a language server by name.
|
|
23
24
|
*/
|
|
24
25
|
async getClient(serverName) {
|
|
26
|
+
if (this.stopping)
|
|
27
|
+
return undefined;
|
|
25
28
|
const managed = this.servers.get(serverName);
|
|
26
29
|
if (!managed)
|
|
27
30
|
return undefined;
|
|
28
31
|
if (managed.state === "ready" && managed.client?.isAlive) {
|
|
29
32
|
return managed.client;
|
|
30
33
|
}
|
|
31
|
-
if (managed.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
if (managed.starting)
|
|
35
|
+
return managed.starting;
|
|
36
|
+
const starting = this.startServer(serverName);
|
|
37
|
+
managed.starting = starting;
|
|
38
|
+
try {
|
|
39
|
+
return await starting;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (managed.starting === starting)
|
|
43
|
+
managed.starting = undefined;
|
|
35
44
|
}
|
|
36
|
-
// Try to start
|
|
37
|
-
return this.startServer(serverName);
|
|
38
45
|
}
|
|
39
46
|
/**
|
|
40
47
|
* Start a specific language server.
|
|
@@ -50,17 +57,37 @@ export class LSPServerManager {
|
|
|
50
57
|
return undefined;
|
|
51
58
|
}
|
|
52
59
|
managed.state = "starting";
|
|
60
|
+
managed.error = undefined;
|
|
61
|
+
const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
|
|
62
|
+
managed.client = client;
|
|
63
|
+
client.on("failure", (error) => {
|
|
64
|
+
if (managed.client !== client)
|
|
65
|
+
return;
|
|
66
|
+
managed.state = "error";
|
|
67
|
+
managed.error = error.message;
|
|
68
|
+
});
|
|
53
69
|
try {
|
|
54
|
-
const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
|
|
55
70
|
await client.start();
|
|
71
|
+
if (managed.client !== client) {
|
|
72
|
+
await client.shutdown();
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
56
75
|
await client.initialize(this.rootUri);
|
|
57
|
-
|
|
76
|
+
// Shutdown can retire a startup while initialize is in flight.
|
|
77
|
+
if (managed.client !== client) {
|
|
78
|
+
await client.shutdown();
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
58
81
|
managed.state = "ready";
|
|
59
82
|
return client;
|
|
60
83
|
}
|
|
61
84
|
catch (err) {
|
|
62
|
-
managed.
|
|
63
|
-
|
|
85
|
+
if (managed.client === client) {
|
|
86
|
+
managed.state = "error";
|
|
87
|
+
managed.error = err.message;
|
|
88
|
+
managed.client = undefined;
|
|
89
|
+
}
|
|
90
|
+
await client.shutdown();
|
|
64
91
|
return undefined;
|
|
65
92
|
}
|
|
66
93
|
}
|
|
@@ -88,18 +115,32 @@ export class LSPServerManager {
|
|
|
88
115
|
/**
|
|
89
116
|
* Shutdown all servers.
|
|
90
117
|
*/
|
|
91
|
-
|
|
118
|
+
shutdownAll() {
|
|
119
|
+
// Fence new starts for the entire sweep, including while a previous
|
|
120
|
+
// server's shutdown handshake is pending. Registry eviction must not leave
|
|
121
|
+
// a replacement process in a manager that no longer has an owner.
|
|
122
|
+
if (!this.stopping) {
|
|
123
|
+
this.stopping = Promise.resolve()
|
|
124
|
+
.then(() => this.stopServers())
|
|
125
|
+
.finally(() => {
|
|
126
|
+
this.stopping = undefined;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return this.stopping;
|
|
130
|
+
}
|
|
131
|
+
async stopServers() {
|
|
92
132
|
for (const server of this.servers.values()) {
|
|
93
|
-
|
|
133
|
+
const client = server.client;
|
|
134
|
+
server.state = "stopped";
|
|
135
|
+
server.client = undefined;
|
|
136
|
+
if (client) {
|
|
94
137
|
try {
|
|
95
|
-
await
|
|
138
|
+
await client.shutdown();
|
|
96
139
|
}
|
|
97
140
|
catch {
|
|
98
141
|
// Force killed
|
|
99
142
|
}
|
|
100
143
|
}
|
|
101
|
-
server.state = "stopped";
|
|
102
|
-
server.client = undefined;
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
}
|
package/dist/prompt/coding.md
CHANGED
|
@@ -30,6 +30,7 @@ The default job in this preset is software engineering in the current working di
|
|
|
30
30
|
|
|
31
31
|
- When referencing specific functions or pieces of code include the pattern file_path:line_number to allow the user to easily navigate to the source code location.
|
|
32
32
|
- When referencing GitHub issues or pull requests, use the owner/repo#123 format so they render as clickable links.
|
|
33
|
+
- Before deleting a task branch, check both its merge target and configured upstream. `git branch -d` can refuse deletion when the branch is merged into local `HEAD` but its upstream still lags. Preserve that protection and report local versus remote integration accurately; do not automatically retry with `-D` or treat the refusal as a failed merge.
|
|
33
34
|
|
|
34
35
|
## Opening URLs and local previews
|
|
35
36
|
|
package/dist/quota/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Both lookups are verified against the real backends (2026-07-01):
|
|
5
5
|
* - Codex: GET https://chatgpt.com/backend-api/wham/usage → JSON
|
|
6
|
-
* rate_limit.{primary_window,secondary_window}
|
|
6
|
+
* rate_limit.{primary_window,secondary_window} contains
|
|
7
|
+
* limit_window_seconds, used_percent and reset_at.
|
|
7
8
|
* Zero cost (no message sent).
|
|
8
9
|
* - Claude: POST /v1/messages (max_tokens:1) → response headers
|
|
9
10
|
* anthropic-ratelimit-unified-<window>-{utilization,reset}, where
|