@cjhyy/code-shell-core 0.6.0-rc.9 → 0.7.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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +4 -0
- package/dist/cc-orchestrator/agent-adapter.js +11 -1
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +65 -5
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -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.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +265 -50
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +98 -15
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +8 -1
- package/dist/context/compaction.js +49 -4
- package/dist/context/manager.d.ts +16 -2
- package/dist/context/manager.js +103 -19
- package/dist/credentials/access.d.ts +57 -0
- package/dist/credentials/access.js +185 -0
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/oauth.d.ts +20 -0
- package/dist/credentials/oauth.js +114 -0
- package/dist/credentials/store.d.ts +1 -0
- package/dist/credentials/store.js +3 -1
- package/dist/credentials/types.d.ts +49 -3
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +23 -3
- package/dist/engine/engine.js +380 -255
- package/dist/engine/goal.d.ts +17 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +398 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +4 -4
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +37 -2
- package/dist/engine/turn-loop.js +228 -43
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/hooks/goal-stop-hook.d.ts +33 -1
- package/dist/hooks/goal-stop-hook.js +202 -34
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +30 -13
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +3 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/client.d.ts +9 -4
- package/dist/protocol/client.js +18 -1
- package/dist/protocol/server.d.ts +30 -0
- package/dist/protocol/server.js +247 -47
- package/dist/protocol/types.d.ts +49 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +224 -4
- package/dist/session/transcript.d.ts +1 -1
- package/dist/session/transcript.js +17 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/background-jobs.d.ts +35 -6
- package/dist/tool-system/builtin/background-jobs.js +116 -7
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +23 -2
- package/dist/tool-system/builtin/drive-claude-code.js +589 -48
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +50 -22
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +41 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +31 -6
- package/dist/tool-system/mcp-manager.js +193 -78
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +78 -11
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session lifecycle manager.
|
|
3
3
|
*/
|
|
4
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { closeSync, existsSync, mkdirSync, lstatSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { Transcript } from "./transcript.js";
|
|
9
9
|
import { SessionError } from "../exceptions.js";
|
|
10
10
|
import { normalizeCumulativeUsageCounters } from "../engine/session-usage.js";
|
|
11
|
+
import { isSameGoalInstance } from "../engine/goal.js";
|
|
12
|
+
import { branchExists, isGitWorktreeRoot } from "../git/worktree.js";
|
|
11
13
|
/**
|
|
12
14
|
* Validate a session ID before it is joined into a filesystem path.
|
|
13
15
|
*
|
|
@@ -55,6 +57,44 @@ export function assertSafeSessionId(sessionId) {
|
|
|
55
57
|
export function codeShellHome() {
|
|
56
58
|
return process.env.CODE_SHELL_HOME || join(homedir(), ".code-shell");
|
|
57
59
|
}
|
|
60
|
+
function isSessionWorkspace(value) {
|
|
61
|
+
if (!value || typeof value !== "object")
|
|
62
|
+
return false;
|
|
63
|
+
const ws = value;
|
|
64
|
+
if (typeof ws.root !== "string" || ws.root.length === 0)
|
|
65
|
+
return false;
|
|
66
|
+
if (ws.kind !== "main" && ws.kind !== "worktree")
|
|
67
|
+
return false;
|
|
68
|
+
if (ws.kind === "main")
|
|
69
|
+
return ws.worktree === undefined;
|
|
70
|
+
const wt = ws.worktree;
|
|
71
|
+
return (!!wt &&
|
|
72
|
+
typeof wt.path === "string" &&
|
|
73
|
+
wt.path.length > 0 &&
|
|
74
|
+
typeof wt.branch === "string" &&
|
|
75
|
+
wt.branch.length > 0 &&
|
|
76
|
+
typeof wt.baseRef === "string" &&
|
|
77
|
+
wt.baseRef.length > 0 &&
|
|
78
|
+
wt.createdBy === "codeshell");
|
|
79
|
+
}
|
|
80
|
+
async function validateResumeWorktreeRoot(root) {
|
|
81
|
+
if (!existsSync(root))
|
|
82
|
+
return "no longer exists";
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = lstatSync(root);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return "no longer exists";
|
|
89
|
+
}
|
|
90
|
+
if (stat.isSymbolicLink())
|
|
91
|
+
return "is not a valid git worktree (symbolic link)";
|
|
92
|
+
if (!stat.isDirectory())
|
|
93
|
+
return "is not a valid git worktree (not a directory)";
|
|
94
|
+
if (!(await isGitWorktreeRoot(root)))
|
|
95
|
+
return "is not a valid git worktree";
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
58
98
|
export class SessionManager {
|
|
59
99
|
sessionsDir;
|
|
60
100
|
constructor(storageDir) {
|
|
@@ -76,10 +116,19 @@ export class SessionManager {
|
|
|
76
116
|
assertSafeSessionId(explicitSessionId);
|
|
77
117
|
const sessionId = explicitSessionId ?? nanoid(16);
|
|
78
118
|
const sessionDir = join(this.sessionsDir, sessionId);
|
|
79
|
-
|
|
119
|
+
try {
|
|
120
|
+
mkdirSync(sessionDir);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (err.code === "EEXIST") {
|
|
124
|
+
throw new SessionError(`Session already exists: ${sessionId}`);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
80
128
|
const state = {
|
|
81
129
|
sessionId,
|
|
82
130
|
cwd,
|
|
131
|
+
workspace: { root: cwd, kind: "main" },
|
|
83
132
|
startedAt: Date.now(),
|
|
84
133
|
model,
|
|
85
134
|
provider,
|
|
@@ -158,6 +207,143 @@ export class SessionManager {
|
|
|
158
207
|
return undefined;
|
|
159
208
|
}
|
|
160
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Disk-only workspace pointer reader. Legacy sessions written before
|
|
212
|
+
* `workspace` existed are treated as main-workspace sessions rooted at
|
|
213
|
+
* `state.cwd`; the read is intentionally non-mutating.
|
|
214
|
+
*/
|
|
215
|
+
getSessionWorkspace(sessionId) {
|
|
216
|
+
try {
|
|
217
|
+
assertSafeSessionId(sessionId);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
223
|
+
if (!existsSync(stateFile))
|
|
224
|
+
return undefined;
|
|
225
|
+
try {
|
|
226
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
227
|
+
if (isSessionWorkspace(state.workspace))
|
|
228
|
+
return state.workspace;
|
|
229
|
+
return typeof state.cwd === "string" && state.cwd.length > 0
|
|
230
|
+
? { root: state.cwd, kind: "main" }
|
|
231
|
+
: undefined;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Persist the current session workspace pointer without changing legacy
|
|
239
|
+
* `cwd`. P1 will teach ToolContext to resolve cwd from this field; for P0 it
|
|
240
|
+
* is a safety pointer and resume breadcrumb.
|
|
241
|
+
*/
|
|
242
|
+
setSessionWorkspace(sessionId, workspace) {
|
|
243
|
+
assertSafeSessionId(sessionId);
|
|
244
|
+
if (!isSessionWorkspace(workspace)) {
|
|
245
|
+
throw new SessionError(`invalid workspace for session ${sessionId}`);
|
|
246
|
+
}
|
|
247
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
248
|
+
if (!existsSync(stateFile)) {
|
|
249
|
+
throw new SessionError(`Session state file not found: ${sessionId}`);
|
|
250
|
+
}
|
|
251
|
+
let state;
|
|
252
|
+
try {
|
|
253
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
257
|
+
}
|
|
258
|
+
state.workspace = workspace;
|
|
259
|
+
this.saveState(state);
|
|
260
|
+
}
|
|
261
|
+
recordWorkspaceHandoff(sessionId, from, to) {
|
|
262
|
+
assertSafeSessionId(sessionId);
|
|
263
|
+
const transcriptFile = join(this.sessionsDir, sessionId, "transcript.jsonl");
|
|
264
|
+
if (!existsSync(transcriptFile))
|
|
265
|
+
return;
|
|
266
|
+
try {
|
|
267
|
+
const transcript = new Transcript(transcriptFile);
|
|
268
|
+
transcript.append("session_meta", {
|
|
269
|
+
sessionId,
|
|
270
|
+
cwd: to.root,
|
|
271
|
+
workspace: to,
|
|
272
|
+
handoffFrom: from?.root,
|
|
273
|
+
handoffAt: Date.now(),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Transcript handoff metadata is best-effort; state.workspace is the
|
|
278
|
+
// authoritative switch pointer.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Resolve the cwd a resumed session must run in from its persisted workspace
|
|
283
|
+
* pointer. A missing worktree directory is never silently treated as the main
|
|
284
|
+
* repo: if the branch still exists callers get a blocking recreate message;
|
|
285
|
+
* if the branch is gone the workspace pointer is reset to main and a warning
|
|
286
|
+
* message is returned for the host to surface before continuing.
|
|
287
|
+
*/
|
|
288
|
+
async resolveSessionWorkspaceForResume(sessionId) {
|
|
289
|
+
assertSafeSessionId(sessionId);
|
|
290
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
291
|
+
if (!existsSync(stateFile)) {
|
|
292
|
+
throw new SessionError(`Session state file not found: ${sessionId}`);
|
|
293
|
+
}
|
|
294
|
+
let state;
|
|
295
|
+
try {
|
|
296
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
300
|
+
}
|
|
301
|
+
const legacyMain = typeof state.cwd === "string" && state.cwd.length > 0
|
|
302
|
+
? { root: state.cwd, kind: "main" }
|
|
303
|
+
: undefined;
|
|
304
|
+
const workspace = isSessionWorkspace(state.workspace) ? state.workspace : legacyMain;
|
|
305
|
+
if (!workspace) {
|
|
306
|
+
throw new SessionError(`Session ${sessionId} has no recoverable cwd`);
|
|
307
|
+
}
|
|
308
|
+
if (workspace.kind === "main") {
|
|
309
|
+
return {
|
|
310
|
+
ok: true,
|
|
311
|
+
cwd: workspace.root,
|
|
312
|
+
workspace,
|
|
313
|
+
reason: isSessionWorkspace(state.workspace) ? "main" : "legacy",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const invalidWorktreeReason = await validateResumeWorktreeRoot(workspace.root);
|
|
317
|
+
if (!invalidWorktreeReason) {
|
|
318
|
+
return { ok: true, cwd: workspace.root, workspace, reason: "worktree" };
|
|
319
|
+
}
|
|
320
|
+
const branch = workspace.worktree?.branch;
|
|
321
|
+
const mainRoot = typeof state.cwd === "string" && state.cwd.length > 0 ? state.cwd : workspace.root;
|
|
322
|
+
if (branch && existsSync(mainRoot) && (await branchExists(mainRoot, branch))) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
cwd: mainRoot,
|
|
326
|
+
workspace,
|
|
327
|
+
reason: "worktree_missing_branch_exists",
|
|
328
|
+
message: `Session ${sessionId} is bound to worktree ${workspace.root}, but that directory ` +
|
|
329
|
+
`${invalidWorktreeReason}. Branch ${branch} still exists; recreate the worktree at ` +
|
|
330
|
+
`${workspace.worktree?.path ?? workspace.root} before resuming, or switch this session ` +
|
|
331
|
+
`back to main explicitly.`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const fallback = { root: mainRoot, kind: "main" };
|
|
335
|
+
state.workspace = fallback;
|
|
336
|
+
this.saveState(state);
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
cwd: mainRoot,
|
|
340
|
+
workspace: fallback,
|
|
341
|
+
reason: "worktree_missing_branch_gone",
|
|
342
|
+
message: `Session ${sessionId} was bound to worktree ${workspace.root}, but that directory ` +
|
|
343
|
+
`${invalidWorktreeReason}${branch ? ` and branch ${branch} is gone` : ""}; fell back to main ` +
|
|
344
|
+
`${mainRoot}. Re-run the request if you want to continue there.`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
161
347
|
/**
|
|
162
348
|
* Cheap "does this session have a persisted goal?" probe — reads only
|
|
163
349
|
* state.json, NOT the transcript (like readCwd). A persistent goal lives ONLY
|
|
@@ -181,7 +367,9 @@ export class SessionManager {
|
|
|
181
367
|
return undefined;
|
|
182
368
|
try {
|
|
183
369
|
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
184
|
-
return state.activeGoal
|
|
370
|
+
return isSameGoalInstance(state.activeGoal, state.goalTerminal)
|
|
371
|
+
? undefined
|
|
372
|
+
: state.activeGoal;
|
|
185
373
|
}
|
|
186
374
|
catch {
|
|
187
375
|
return undefined;
|
|
@@ -261,6 +449,27 @@ export class SessionManager {
|
|
|
261
449
|
// Atomic write: stage to .tmp, then rename. Protects against two processes
|
|
262
450
|
// clobbering each other's state.json mid-write.
|
|
263
451
|
const target = join(sessionDir, "state.json");
|
|
452
|
+
// Preserve the newest goal tombstone across whole-state writers. If an old
|
|
453
|
+
// detached bundle still carries the tombstoned goal, drop it before write;
|
|
454
|
+
// when disk already contains a newer replacement goal, retain that goal as
|
|
455
|
+
// well instead of letting the stale bundle erase it.
|
|
456
|
+
let persisted;
|
|
457
|
+
if (existsSync(target)) {
|
|
458
|
+
try {
|
|
459
|
+
persisted = JSON.parse(readFileSync(target, "utf-8"));
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// The atomic writer should make this rare. Preserve the existing
|
|
463
|
+
// behavior and overwrite malformed state with the caller's snapshot.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const terminal = newestGoalTerminal(state.goalTerminal, persisted?.goalTerminal);
|
|
467
|
+
if (terminal)
|
|
468
|
+
state.goalTerminal = terminal;
|
|
469
|
+
if (terminal && isSameGoalInstance(state.activeGoal, terminal)) {
|
|
470
|
+
const diskGoal = persisted?.activeGoal;
|
|
471
|
+
state.activeGoal = diskGoal && !isSameGoalInstance(diskGoal, terminal) ? diskGoal : undefined;
|
|
472
|
+
}
|
|
264
473
|
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
265
474
|
writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
266
475
|
renameSync(tmp, target);
|
|
@@ -302,7 +511,7 @@ export class SessionManager {
|
|
|
302
511
|
continue;
|
|
303
512
|
const transcriptFile = join(this.sessionsDir, dir, "transcript.jsonl");
|
|
304
513
|
let lastActiveAt;
|
|
305
|
-
let transcriptExists
|
|
514
|
+
let transcriptExists;
|
|
306
515
|
try {
|
|
307
516
|
transcriptExists = existsSync(transcriptFile);
|
|
308
517
|
if (transcriptExists) {
|
|
@@ -335,6 +544,15 @@ export class SessionManager {
|
|
|
335
544
|
return sessions;
|
|
336
545
|
}
|
|
337
546
|
}
|
|
547
|
+
function newestGoalTerminal(incoming, persisted) {
|
|
548
|
+
if (!incoming)
|
|
549
|
+
return persisted;
|
|
550
|
+
if (!persisted)
|
|
551
|
+
return incoming;
|
|
552
|
+
const incomingAt = incoming.terminatedAtMs ?? Number.NEGATIVE_INFINITY;
|
|
553
|
+
const persistedAt = persisted.terminatedAtMs ?? Number.NEGATIVE_INFINITY;
|
|
554
|
+
return incomingAt > persistedAt ? incoming : persisted;
|
|
555
|
+
}
|
|
338
556
|
/**
|
|
339
557
|
* Scan a transcript.jsonl for the LAST user message and return a short
|
|
340
558
|
* preview, reading the file from the END in 64 KiB chunks.
|
|
@@ -436,6 +654,8 @@ function parseUserPreview(line) {
|
|
|
436
654
|
return undefined;
|
|
437
655
|
if (event.data?.role !== "user")
|
|
438
656
|
return undefined;
|
|
657
|
+
if (event.data.injected === true)
|
|
658
|
+
return undefined;
|
|
439
659
|
const content = event.data.content;
|
|
440
660
|
const text = typeof content === "string"
|
|
441
661
|
? content
|
|
@@ -27,7 +27,7 @@ export declare class Transcript {
|
|
|
27
27
|
}): TranscriptEvent;
|
|
28
28
|
hasClientMessageId(clientMessageId: string): boolean;
|
|
29
29
|
appendToolUse(toolName: string, toolCallId: string, args: Record<string, unknown>): TranscriptEvent;
|
|
30
|
-
appendToolResult(toolCallId: string, toolName: string, result?: string, error?: string): TranscriptEvent;
|
|
30
|
+
appendToolResult(toolCallId: string, toolName: string, result?: string, error?: string, contentBlocks?: ContentBlock[]): TranscriptEvent;
|
|
31
31
|
/** Anchor for a spawned sub-agent (see TranscriptEventType "subagent").
|
|
32
32
|
* Written at spawn time so replay can rebuild the sub-agent's card from
|
|
33
33
|
* sessions/<agentId>/ — agentId === the sub-agent's session id. */
|
|
@@ -10,7 +10,9 @@ export class Transcript {
|
|
|
10
10
|
events = [];
|
|
11
11
|
filePath;
|
|
12
12
|
currentTurn = 0;
|
|
13
|
-
getFilePath() {
|
|
13
|
+
getFilePath() {
|
|
14
|
+
return this.filePath;
|
|
15
|
+
}
|
|
14
16
|
constructor(filePath) {
|
|
15
17
|
this.filePath = filePath;
|
|
16
18
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
@@ -66,8 +68,14 @@ export class Transcript {
|
|
|
66
68
|
appendToolUse(toolName, toolCallId, args) {
|
|
67
69
|
return this.append("tool_use", { toolName, toolCallId, args });
|
|
68
70
|
}
|
|
69
|
-
appendToolResult(toolCallId, toolName, result, error) {
|
|
70
|
-
return this.append("tool_result", {
|
|
71
|
+
appendToolResult(toolCallId, toolName, result, error, contentBlocks) {
|
|
72
|
+
return this.append("tool_result", {
|
|
73
|
+
toolCallId,
|
|
74
|
+
toolName,
|
|
75
|
+
result,
|
|
76
|
+
error,
|
|
77
|
+
...(contentBlocks && contentBlocks.length > 0 ? { contentBlocks } : {}),
|
|
78
|
+
});
|
|
71
79
|
}
|
|
72
80
|
/** Anchor for a spawned sub-agent (see TranscriptEventType "subagent").
|
|
73
81
|
* Written at spawn time so replay can rebuild the sub-agent's card from
|
|
@@ -125,13 +133,17 @@ export class Transcript {
|
|
|
125
133
|
break;
|
|
126
134
|
}
|
|
127
135
|
case "tool_result": {
|
|
128
|
-
const { toolCallId, result, error } = event.data;
|
|
136
|
+
const { toolCallId, result, error, contentBlocks } = event.data;
|
|
129
137
|
// Find if there's already a user message with tool_results to append to
|
|
130
138
|
const lastMsg = messages[messages.length - 1];
|
|
131
139
|
const block = {
|
|
132
140
|
type: "tool_result",
|
|
133
141
|
tool_use_id: toolCallId,
|
|
134
|
-
content: error
|
|
142
|
+
content: error
|
|
143
|
+
? `Error: ${error}`
|
|
144
|
+
: Array.isArray(contentBlocks) && contentBlocks.length > 0
|
|
145
|
+
? contentBlocks
|
|
146
|
+
: (result ?? "(no output)"),
|
|
135
147
|
};
|
|
136
148
|
if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
|
|
137
149
|
lastMsg.content.push(block);
|
|
@@ -18,6 +18,7 @@ export declare function userHome(): string;
|
|
|
18
18
|
* ("permissions.defaultMode", "env.FOO") are caught too.
|
|
19
19
|
*/
|
|
20
20
|
export declare function isProtectedSettingKey(key: string): boolean;
|
|
21
|
+
export declare function setDottedSetting(target: Record<string, unknown>, key: string, value: unknown): void;
|
|
21
22
|
/**
|
|
22
23
|
* The fixed cwd used for "no-repo" pure-chat conversations (a chat not bound to
|
|
23
24
|
* any code project). Same location as desktop's `resolveNoRepoCwd`
|
package/dist/settings/manager.js
CHANGED
|
@@ -50,6 +50,66 @@ export function isProtectedSettingKey(key) {
|
|
|
50
50
|
const root = key.split(".")[0] ?? "";
|
|
51
51
|
return PROTECTED_SETTING_ROOTS.has(root);
|
|
52
52
|
}
|
|
53
|
+
const FORBIDDEN_SETTING_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
54
|
+
function isForbiddenSettingKeySegment(key) {
|
|
55
|
+
return FORBIDDEN_SETTING_KEY_SEGMENTS.has(key);
|
|
56
|
+
}
|
|
57
|
+
function parseDottedSettingKey(key) {
|
|
58
|
+
const parts = key.split(".");
|
|
59
|
+
if (parts.length === 0 ||
|
|
60
|
+
parts.some((seg) => seg.length === 0 || isForbiddenSettingKeySegment(seg))) {
|
|
61
|
+
throw new Error(`invalid setting key: ${key}`);
|
|
62
|
+
}
|
|
63
|
+
return parts;
|
|
64
|
+
}
|
|
65
|
+
function sanitizeSettingsValue(value) {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return value.map((entry) => sanitizeSettingsValue(entry));
|
|
68
|
+
}
|
|
69
|
+
if (!value || typeof value !== "object") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [key, child] of Object.entries(value)) {
|
|
74
|
+
if (isForbiddenSettingKeySegment(key))
|
|
75
|
+
continue;
|
|
76
|
+
out[key] = sanitizeSettingsValue(child);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function sanitizeSettingsObject(data) {
|
|
81
|
+
return sanitizeSettingsValue(data);
|
|
82
|
+
}
|
|
83
|
+
function isOwnPlainObject(parent, key) {
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(parent, key))
|
|
85
|
+
return false;
|
|
86
|
+
const value = parent[key];
|
|
87
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
88
|
+
return false;
|
|
89
|
+
const proto = Object.getPrototypeOf(value);
|
|
90
|
+
return proto === Object.prototype || proto === null;
|
|
91
|
+
}
|
|
92
|
+
function descendForSettingWrite(target, key, fullKey) {
|
|
93
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
94
|
+
const inherited = target[key];
|
|
95
|
+
if (inherited && typeof inherited === "object") {
|
|
96
|
+
throw new Error(`invalid setting key: ${fullKey} (refusing to descend through inherited object segment: ${key})`);
|
|
97
|
+
}
|
|
98
|
+
target[key] = {};
|
|
99
|
+
}
|
|
100
|
+
else if (!isOwnPlainObject(target, key)) {
|
|
101
|
+
target[key] = {};
|
|
102
|
+
}
|
|
103
|
+
return target[key];
|
|
104
|
+
}
|
|
105
|
+
export function setDottedSetting(target, key, value) {
|
|
106
|
+
const parts = parseDottedSettingKey(key);
|
|
107
|
+
let current = target;
|
|
108
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
109
|
+
current = descendForSettingWrite(current, parts[i], key);
|
|
110
|
+
}
|
|
111
|
+
current[parts[parts.length - 1]] = value;
|
|
112
|
+
}
|
|
53
113
|
/**
|
|
54
114
|
* The fixed cwd used for "no-repo" pure-chat conversations (a chat not bound to
|
|
55
115
|
* any code project). Same location as desktop's `resolveNoRepoCwd`
|
|
@@ -135,7 +195,7 @@ export class SettingsManager {
|
|
|
135
195
|
}
|
|
136
196
|
// 5. CLI flags (highest priority)
|
|
137
197
|
if (flagOverrides && Object.keys(flagOverrides).length > 0) {
|
|
138
|
-
this.sources.push({ name: "flag", priority: 4, data: flagOverrides });
|
|
198
|
+
this.sources.push({ name: "flag", priority: 4, data: sanitizeSettingsObject(flagOverrides) });
|
|
139
199
|
}
|
|
140
200
|
// Sort by priority ascending (merge in order, later wins)
|
|
141
201
|
this.sources.sort((a, b) => a.priority - b.priority);
|
|
@@ -176,7 +236,7 @@ export class SettingsManager {
|
|
|
176
236
|
const userPath = join(userHome(), ".code-shell", "settings.json");
|
|
177
237
|
if (readUser && existsSync(userPath)) {
|
|
178
238
|
try {
|
|
179
|
-
const userRaw = JSON.parse(readFileSync(userPath, "utf-8"));
|
|
239
|
+
const userRaw = sanitizeSettingsObject(JSON.parse(readFileSync(userPath, "utf-8")));
|
|
180
240
|
const result = migrateModels({
|
|
181
241
|
providers: userRaw.providers ?? [],
|
|
182
242
|
models: userRaw.models ?? [],
|
|
@@ -188,14 +248,15 @@ export class SettingsManager {
|
|
|
188
248
|
providers: result.providers,
|
|
189
249
|
models: result.models,
|
|
190
250
|
};
|
|
251
|
+
const sanitized = sanitizeSettingsObject(migrated);
|
|
191
252
|
// Atomic write (tmp+rename) — a concurrent load must not see a
|
|
192
253
|
// half-written file. File exists here (existsSync guard above).
|
|
193
|
-
this.atomicWriteJson(userPath,
|
|
254
|
+
this.atomicWriteJson(userPath, sanitized);
|
|
194
255
|
// Re-deep-merge with the migrated user data so the validate
|
|
195
256
|
// call sees the new shape rather than the legacy one.
|
|
196
257
|
const userSource = this.sources.find((s) => s.name === "user");
|
|
197
258
|
if (userSource)
|
|
198
|
-
userSource.data =
|
|
259
|
+
userSource.data = sanitized;
|
|
199
260
|
const remerged = this.deepMerge();
|
|
200
261
|
this.merged = validateSettings(remerged);
|
|
201
262
|
return this.merged;
|
|
@@ -224,7 +285,7 @@ export class SettingsManager {
|
|
|
224
285
|
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
225
286
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
226
287
|
return;
|
|
227
|
-
const raw = parsed;
|
|
288
|
+
const raw = sanitizeSettingsObject(parsed);
|
|
228
289
|
const result = migrateConfig(raw);
|
|
229
290
|
if (!result.changed)
|
|
230
291
|
return;
|
|
@@ -240,10 +301,11 @@ export class SettingsManager {
|
|
|
240
301
|
// Atomic write (tmp+rename) so a concurrent load can't read a half-written
|
|
241
302
|
// migrated file — matches the normal save path (atomicWriteJson). The file
|
|
242
303
|
// exists here (existsSync guard above), so the recursive mkdir is a no-op.
|
|
243
|
-
|
|
304
|
+
const sanitized = sanitizeSettingsObject(result.config);
|
|
305
|
+
this.atomicWriteJson(path, sanitized);
|
|
244
306
|
const source = this.sources.find((s) => s.name === sourceName);
|
|
245
307
|
if (source)
|
|
246
|
-
source.data =
|
|
308
|
+
source.data = sanitized;
|
|
247
309
|
}
|
|
248
310
|
catch {
|
|
249
311
|
// Best-effort — fall through to normal merge/validate.
|
|
@@ -279,24 +341,14 @@ export class SettingsManager {
|
|
|
279
341
|
const raw = readFileSync(path, "utf-8");
|
|
280
342
|
const parsed = JSON.parse(raw);
|
|
281
343
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
282
|
-
current = parsed;
|
|
344
|
+
current = sanitizeSettingsObject(parsed);
|
|
283
345
|
}
|
|
284
346
|
}
|
|
285
347
|
catch {
|
|
286
348
|
// Corrupt file — overwrite rather than crash.
|
|
287
349
|
}
|
|
288
350
|
}
|
|
289
|
-
|
|
290
|
-
let target = current;
|
|
291
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
292
|
-
const seg = parts[i];
|
|
293
|
-
const next = target[seg];
|
|
294
|
-
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
295
|
-
target[seg] = {};
|
|
296
|
-
}
|
|
297
|
-
target = target[seg];
|
|
298
|
-
}
|
|
299
|
-
target[parts[parts.length - 1]] = value;
|
|
351
|
+
setDottedSetting(current, key, value);
|
|
300
352
|
mkdirSync(dirname(path), { recursive: true });
|
|
301
353
|
// Atomic write: stage to .tmp, then rename, so a concurrent read can't
|
|
302
354
|
// catch a half-written file. mode 0o600 — settings.json can hold plaintext
|
|
@@ -323,17 +375,7 @@ export class SettingsManager {
|
|
|
323
375
|
if (!existsSync(cwd))
|
|
324
376
|
return;
|
|
325
377
|
const current = this.readJsonObject(path);
|
|
326
|
-
|
|
327
|
-
let target = current;
|
|
328
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
329
|
-
const seg = parts[i];
|
|
330
|
-
const next = target[seg];
|
|
331
|
-
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
332
|
-
target[seg] = {};
|
|
333
|
-
}
|
|
334
|
-
target = target[seg];
|
|
335
|
-
}
|
|
336
|
-
target[parts[parts.length - 1]] = value;
|
|
378
|
+
setDottedSetting(current, key, value);
|
|
337
379
|
this.atomicWriteJson(path, current);
|
|
338
380
|
this.invalidate();
|
|
339
381
|
}
|
|
@@ -353,13 +395,13 @@ export class SettingsManager {
|
|
|
353
395
|
if (!resolveConfigPath(path))
|
|
354
396
|
return;
|
|
355
397
|
const current = this.readJsonObject(path);
|
|
356
|
-
const parts = key
|
|
398
|
+
const parts = parseDottedSettingKey(key);
|
|
357
399
|
let target = current;
|
|
358
400
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
359
|
-
const
|
|
360
|
-
if (!
|
|
401
|
+
const seg = parts[i];
|
|
402
|
+
if (!target || !isOwnPlainObject(target, seg))
|
|
361
403
|
return;
|
|
362
|
-
target =
|
|
404
|
+
target = target[seg];
|
|
363
405
|
}
|
|
364
406
|
if (target)
|
|
365
407
|
delete target[parts[parts.length - 1]];
|
|
@@ -481,7 +523,7 @@ function parseConfigFile(path) {
|
|
|
481
523
|
const ext = extname(path).toLowerCase();
|
|
482
524
|
const parsed = ext === ".yaml" || ext === ".yml" ? parseYaml(content) : JSON.parse(content);
|
|
483
525
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
484
|
-
return parsed;
|
|
526
|
+
return sanitizeSettingsObject(parsed);
|
|
485
527
|
}
|
|
486
528
|
}
|
|
487
529
|
catch {
|
|
@@ -508,9 +550,17 @@ function resolveConfigPath(jsonPath) {
|
|
|
508
550
|
return null;
|
|
509
551
|
}
|
|
510
552
|
function merge(base, override) {
|
|
511
|
-
const result = {
|
|
553
|
+
const result = {};
|
|
554
|
+
for (const [key, value] of Object.entries(base)) {
|
|
555
|
+
if (isForbiddenSettingKeySegment(key))
|
|
556
|
+
continue;
|
|
557
|
+
result[key] = value;
|
|
558
|
+
}
|
|
512
559
|
for (const [key, value] of Object.entries(override)) {
|
|
513
|
-
if (
|
|
560
|
+
if (isForbiddenSettingKeySegment(key)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
else if (value === null) {
|
|
514
564
|
delete result[key];
|
|
515
565
|
}
|
|
516
566
|
else if (typeof value === "object" &&
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This module is side-effect free: it only EXPOSES the generator and a writer.
|
|
9
9
|
* `manager.load()` deliberately does NOT call these — emitting a file during
|
|
10
|
-
* load would pollute test HOMEs and add disk I/O to every
|
|
11
|
-
*
|
|
12
|
-
* (wiring TBD — see the task return note).
|
|
10
|
+
* load would pollute test HOMEs and add disk I/O to every settings read. Hosts
|
|
11
|
+
* write it explicitly during startup as a best-effort editor aid.
|
|
13
12
|
*/
|
|
14
13
|
/**
|
|
15
14
|
* Generate the JSON Schema for the settings object. The `name` option makes
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* This module is side-effect free: it only EXPOSES the generator and a writer.
|
|
9
9
|
* `manager.load()` deliberately does NOT call these — emitting a file during
|
|
10
|
-
* load would pollute test HOMEs and add disk I/O to every
|
|
11
|
-
*
|
|
12
|
-
* (wiring TBD — see the task return note).
|
|
10
|
+
* load would pollute test HOMEs and add disk I/O to every settings read. Hosts
|
|
11
|
+
* write it explicitly during startup as a best-effort editor aid.
|
|
13
12
|
*/
|
|
14
13
|
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
15
14
|
import { dirname, join } from "node:path";
|
|
@@ -366,6 +366,13 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
366
366
|
storageDir?: string | undefined;
|
|
367
367
|
maxHistory?: number | undefined;
|
|
368
368
|
}>>;
|
|
369
|
+
worktree: z.ZodDefault<z.ZodObject<{
|
|
370
|
+
branchPrefix: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
371
|
+
}, "strip", z.ZodTypeAny, {
|
|
372
|
+
branchPrefix: string;
|
|
373
|
+
}, {
|
|
374
|
+
branchPrefix?: string | undefined;
|
|
375
|
+
}>>;
|
|
369
376
|
mcpServers: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
370
377
|
name: z.ZodString;
|
|
371
378
|
command: z.ZodOptional<z.ZodString>;
|
|
@@ -1240,6 +1247,13 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
1240
1247
|
storageDir?: string | undefined;
|
|
1241
1248
|
maxHistory?: number | undefined;
|
|
1242
1249
|
}>>;
|
|
1250
|
+
worktree: z.ZodDefault<z.ZodObject<{
|
|
1251
|
+
branchPrefix: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
1252
|
+
}, "strip", z.ZodTypeAny, {
|
|
1253
|
+
branchPrefix: string;
|
|
1254
|
+
}, {
|
|
1255
|
+
branchPrefix?: string | undefined;
|
|
1256
|
+
}>>;
|
|
1243
1257
|
mcpServers: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
1244
1258
|
name: z.ZodString;
|
|
1245
1259
|
command: z.ZodOptional<z.ZodString>;
|
|
@@ -2114,6 +2128,13 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
2114
2128
|
storageDir?: string | undefined;
|
|
2115
2129
|
maxHistory?: number | undefined;
|
|
2116
2130
|
}>>;
|
|
2131
|
+
worktree: z.ZodDefault<z.ZodObject<{
|
|
2132
|
+
branchPrefix: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
2133
|
+
}, "strip", z.ZodTypeAny, {
|
|
2134
|
+
branchPrefix: string;
|
|
2135
|
+
}, {
|
|
2136
|
+
branchPrefix?: string | undefined;
|
|
2137
|
+
}>>;
|
|
2117
2138
|
mcpServers: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
2118
2139
|
name: z.ZodString;
|
|
2119
2140
|
command: z.ZodOptional<z.ZodString>;
|
package/dist/settings/schema.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Settings schema with Zod validation.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { DEFAULT_WORKTREE_BRANCH_PREFIX, isValidWorktreeBranchPrefix, normalizeWorktreeBranchPrefix, } from "../git/worktree/slug.js";
|
|
5
6
|
/**
|
|
6
7
|
* Tri-state project capability overlay. Lives in PROJECT settings only and
|
|
7
8
|
* layers over the global baseline (disabledSkills / disabledPlugins /
|
|
@@ -30,6 +31,12 @@ export const CapabilityOverridesSchema = z
|
|
|
30
31
|
pluginHooks: z.record(CapabilityOverrideSchema).optional(),
|
|
31
32
|
})
|
|
32
33
|
.optional();
|
|
34
|
+
const WorktreeBranchPrefixSchema = z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(1)
|
|
38
|
+
.refine(isValidWorktreeBranchPrefix, "Invalid git branch prefix")
|
|
39
|
+
.transform((value) => normalizeWorktreeBranchPrefix(value));
|
|
33
40
|
export const SettingsSchema = z
|
|
34
41
|
.object({
|
|
35
42
|
agent: z
|
|
@@ -214,6 +221,11 @@ export const SettingsSchema = z
|
|
|
214
221
|
maxHistory: z.number().default(100),
|
|
215
222
|
})
|
|
216
223
|
.default({}),
|
|
224
|
+
worktree: z
|
|
225
|
+
.object({
|
|
226
|
+
branchPrefix: WorktreeBranchPrefixSchema.default(DEFAULT_WORKTREE_BRANCH_PREFIX),
|
|
227
|
+
})
|
|
228
|
+
.default({}),
|
|
217
229
|
// The record key IS the server name at runtime (MCPManager.connectAll
|
|
218
230
|
// uses Object.entries keys; desktop's persist strips the `name` field and
|
|
219
231
|
// keys by it via stripNameFromServer). So `name` here is optional and
|