@cjhyy/code-shell-core 0.6.0-rc.14 → 0.6.0-rc.15
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-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/external-agent-bindings.js +1 -1
- package/dist/cc-orchestrator/external-agent-session-store.js +3 -1
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +7 -2
- package/dist/context/manager.d.ts +11 -2
- package/dist/context/manager.js +64 -3
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +29 -10
- package/dist/credentials/use-credential-tool.d.ts +1 -0
- package/dist/credentials/use-credential-tool.js +3 -0
- package/dist/engine/engine.d.ts +9 -0
- package/dist/engine/engine.js +96 -17
- package/dist/engine/turn-loop.d.ts +3 -1
- package/dist/engine/turn-loop.js +3 -1
- 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 -127
- package/dist/git/worktree.js +5 -464
- package/dist/index.d.ts +28 -27
- package/dist/index.js +24 -24
- 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/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- 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/preset/index.d.ts +1 -0
- package/dist/preset/index.js +6 -0
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.js +7 -0
- package/dist/protocol/server.d.ts +10 -0
- package/dist/protocol/server.js +88 -4
- package/dist/protocol/types.d.ts +6 -0
- package/dist/protocol/types.js +2 -0
- package/dist/runtime/background-shell.js +2 -3
- 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 +56 -17
- package/dist/session/memory.js +337 -78
- package/dist/session/session-manager.d.ts +1 -1
- package/dist/session/session-manager.js +6 -6
- package/dist/settings/manager.js +43 -12
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/tool-system/builtin/index.js +18 -8
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/worktree.d.ts +2 -0
- package/dist/tool-system/builtin/worktree.js +59 -11
- package/dist/tool-system/context.d.ts +11 -1
- package/dist/tool-system/mcp-manager.d.ts +8 -5
- package/dist/tool-system/mcp-manager.js +85 -55
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +28 -12
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/settings/manager.js
CHANGED
|
@@ -51,14 +51,35 @@ export function isProtectedSettingKey(key) {
|
|
|
51
51
|
return PROTECTED_SETTING_ROOTS.has(root);
|
|
52
52
|
}
|
|
53
53
|
const FORBIDDEN_SETTING_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
54
|
+
function isForbiddenSettingKeySegment(key) {
|
|
55
|
+
return FORBIDDEN_SETTING_KEY_SEGMENTS.has(key);
|
|
56
|
+
}
|
|
54
57
|
function parseDottedSettingKey(key) {
|
|
55
58
|
const parts = key.split(".");
|
|
56
59
|
if (parts.length === 0 ||
|
|
57
|
-
parts.some((seg) => seg.length === 0 ||
|
|
60
|
+
parts.some((seg) => seg.length === 0 || isForbiddenSettingKeySegment(seg))) {
|
|
58
61
|
throw new Error(`invalid setting key: ${key}`);
|
|
59
62
|
}
|
|
60
63
|
return parts;
|
|
61
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
|
+
}
|
|
62
83
|
function isOwnPlainObject(parent, key) {
|
|
63
84
|
if (!Object.prototype.hasOwnProperty.call(parent, key))
|
|
64
85
|
return false;
|
|
@@ -174,7 +195,7 @@ export class SettingsManager {
|
|
|
174
195
|
}
|
|
175
196
|
// 5. CLI flags (highest priority)
|
|
176
197
|
if (flagOverrides && Object.keys(flagOverrides).length > 0) {
|
|
177
|
-
this.sources.push({ name: "flag", priority: 4, data: flagOverrides });
|
|
198
|
+
this.sources.push({ name: "flag", priority: 4, data: sanitizeSettingsObject(flagOverrides) });
|
|
178
199
|
}
|
|
179
200
|
// Sort by priority ascending (merge in order, later wins)
|
|
180
201
|
this.sources.sort((a, b) => a.priority - b.priority);
|
|
@@ -215,7 +236,7 @@ export class SettingsManager {
|
|
|
215
236
|
const userPath = join(userHome(), ".code-shell", "settings.json");
|
|
216
237
|
if (readUser && existsSync(userPath)) {
|
|
217
238
|
try {
|
|
218
|
-
const userRaw = JSON.parse(readFileSync(userPath, "utf-8"));
|
|
239
|
+
const userRaw = sanitizeSettingsObject(JSON.parse(readFileSync(userPath, "utf-8")));
|
|
219
240
|
const result = migrateModels({
|
|
220
241
|
providers: userRaw.providers ?? [],
|
|
221
242
|
models: userRaw.models ?? [],
|
|
@@ -227,14 +248,15 @@ export class SettingsManager {
|
|
|
227
248
|
providers: result.providers,
|
|
228
249
|
models: result.models,
|
|
229
250
|
};
|
|
251
|
+
const sanitized = sanitizeSettingsObject(migrated);
|
|
230
252
|
// Atomic write (tmp+rename) — a concurrent load must not see a
|
|
231
253
|
// half-written file. File exists here (existsSync guard above).
|
|
232
|
-
this.atomicWriteJson(userPath,
|
|
254
|
+
this.atomicWriteJson(userPath, sanitized);
|
|
233
255
|
// Re-deep-merge with the migrated user data so the validate
|
|
234
256
|
// call sees the new shape rather than the legacy one.
|
|
235
257
|
const userSource = this.sources.find((s) => s.name === "user");
|
|
236
258
|
if (userSource)
|
|
237
|
-
userSource.data =
|
|
259
|
+
userSource.data = sanitized;
|
|
238
260
|
const remerged = this.deepMerge();
|
|
239
261
|
this.merged = validateSettings(remerged);
|
|
240
262
|
return this.merged;
|
|
@@ -263,7 +285,7 @@ export class SettingsManager {
|
|
|
263
285
|
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
264
286
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
265
287
|
return;
|
|
266
|
-
const raw = parsed;
|
|
288
|
+
const raw = sanitizeSettingsObject(parsed);
|
|
267
289
|
const result = migrateConfig(raw);
|
|
268
290
|
if (!result.changed)
|
|
269
291
|
return;
|
|
@@ -279,10 +301,11 @@ export class SettingsManager {
|
|
|
279
301
|
// Atomic write (tmp+rename) so a concurrent load can't read a half-written
|
|
280
302
|
// migrated file — matches the normal save path (atomicWriteJson). The file
|
|
281
303
|
// exists here (existsSync guard above), so the recursive mkdir is a no-op.
|
|
282
|
-
|
|
304
|
+
const sanitized = sanitizeSettingsObject(result.config);
|
|
305
|
+
this.atomicWriteJson(path, sanitized);
|
|
283
306
|
const source = this.sources.find((s) => s.name === sourceName);
|
|
284
307
|
if (source)
|
|
285
|
-
source.data =
|
|
308
|
+
source.data = sanitized;
|
|
286
309
|
}
|
|
287
310
|
catch {
|
|
288
311
|
// Best-effort — fall through to normal merge/validate.
|
|
@@ -318,7 +341,7 @@ export class SettingsManager {
|
|
|
318
341
|
const raw = readFileSync(path, "utf-8");
|
|
319
342
|
const parsed = JSON.parse(raw);
|
|
320
343
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
321
|
-
current = parsed;
|
|
344
|
+
current = sanitizeSettingsObject(parsed);
|
|
322
345
|
}
|
|
323
346
|
}
|
|
324
347
|
catch {
|
|
@@ -500,7 +523,7 @@ function parseConfigFile(path) {
|
|
|
500
523
|
const ext = extname(path).toLowerCase();
|
|
501
524
|
const parsed = ext === ".yaml" || ext === ".yml" ? parseYaml(content) : JSON.parse(content);
|
|
502
525
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
503
|
-
return parsed;
|
|
526
|
+
return sanitizeSettingsObject(parsed);
|
|
504
527
|
}
|
|
505
528
|
}
|
|
506
529
|
catch {
|
|
@@ -527,9 +550,17 @@ function resolveConfigPath(jsonPath) {
|
|
|
527
550
|
return null;
|
|
528
551
|
}
|
|
529
552
|
function merge(base, override) {
|
|
530
|
-
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
|
+
}
|
|
531
559
|
for (const [key, value] of Object.entries(override)) {
|
|
532
|
-
if (
|
|
560
|
+
if (isForbiddenSettingKeySegment(key)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
else if (value === null) {
|
|
533
564
|
delete result[key];
|
|
534
565
|
}
|
|
535
566
|
else if (typeof value === "object" &&
|
|
@@ -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
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readToolDef, readTool } from "./read.js";
|
|
5
5
|
import { writeToolDef, writeTool } from "./write.js";
|
|
6
|
-
import { generateImageToolDef, generateImageTool, isGenerateImageAvailable } from "./generate-image.js";
|
|
6
|
+
import { generateImageToolDef, generateImageTool, isGenerateImageAvailable, } from "./generate-image.js";
|
|
7
7
|
import { editModelCatalogToolDef, editModelCatalogTool } from "./edit-model-catalog.js";
|
|
8
|
-
import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable } from "./generate-video.js";
|
|
8
|
+
import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable, } from "./generate-video.js";
|
|
9
9
|
import { viewImageToolDef, viewImageTool } from "./view-image.js";
|
|
10
10
|
import { editToolDef, editTool } from "./edit.js";
|
|
11
11
|
import { applyPatchToolDef, applyPatchTool } from "./apply-patch/index.js";
|
|
@@ -16,19 +16,19 @@ import { webSearchToolDef, webSearchTool, isWebSearchAvailable } from "./web-sea
|
|
|
16
16
|
import { webFetchToolDef, webFetchTool } from "./web-fetch.js";
|
|
17
17
|
import { askUserToolDef, askUserTool } from "./ask-user.js";
|
|
18
18
|
import { agentToolDef, agentTool, agentStatusToolDef, agentStatusTool, agentCancelToolDef, agentCancelTool, agentSendInputToolDef, agentSendInputTool, } from "./agent.js";
|
|
19
|
-
import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanModeTool } from "./plan.js";
|
|
19
|
+
import { enterPlanModeToolDef, enterPlanModeTool, exitPlanModeToolDef, exitPlanModeTool, } from "./plan.js";
|
|
20
20
|
import { toolSearchToolDef, toolSearchTool } from "./tool-search.js";
|
|
21
21
|
import { todoWriteToolDef, todoWriteTool } from "./task.js";
|
|
22
|
-
import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool } from "./worktree.js";
|
|
22
|
+
import { enterWorktreeToolDef, enterWorktreeTool, exitWorktreeToolDef, exitWorktreeTool, switchSessionWorkspaceToolDef, switchSessionWorkspaceTool, } from "./worktree.js";
|
|
23
23
|
import { sleepToolDef, sleepTool } from "./sleep.js";
|
|
24
24
|
import { configToolDef, configTool } from "./config.js";
|
|
25
25
|
import { notebookEditToolDef, notebookEditTool } from "./notebook-edit.js";
|
|
26
26
|
import { lspToolDef, lspTool } from "./lsp.js";
|
|
27
|
-
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool } from "./cron.js";
|
|
28
|
-
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS } from "./drive-claude-code.js";
|
|
27
|
+
import { cronCreateToolDef, cronCreateTool, cronDeleteToolDef, cronDeleteTool, cronListToolDef, cronListTool, } from "./cron.js";
|
|
28
|
+
import { driveClaudeCodeToolDef, driveClaudeCodeTool, driveAgentToolDef, driveAgentTool, DRIVE_AGENT_TOOL_TIMEOUT_MS, } from "./drive-claude-code.js";
|
|
29
29
|
import { checkQuotaToolDef, checkQuotaTool } from "./check-quota.js";
|
|
30
30
|
import { skillToolDef, skillTool } from "./skill.js";
|
|
31
|
-
import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool } from "./mcp-tools.js";
|
|
31
|
+
import { mcpToolDef, mcpToolExecute, listMcpResourcesToolDef, listMcpResourcesTool, readMcpResourceToolDef, readMcpResourceTool, } from "./mcp-tools.js";
|
|
32
32
|
import { replToolDef, replTool } from "./repl.js";
|
|
33
33
|
import { briefToolDef, briefTool } from "./brief.js";
|
|
34
34
|
import { powershellToolDef, powershellTool } from "./powershell.js";
|
|
@@ -344,6 +344,16 @@ export const BUILTIN_TOOLS = [
|
|
|
344
344
|
},
|
|
345
345
|
execute: exitWorktreeTool,
|
|
346
346
|
},
|
|
347
|
+
{
|
|
348
|
+
definition: {
|
|
349
|
+
...switchSessionWorkspaceToolDef,
|
|
350
|
+
source: "builtin",
|
|
351
|
+
permissionDefault: "ask",
|
|
352
|
+
isReadOnly: false,
|
|
353
|
+
isConcurrencySafe: false,
|
|
354
|
+
},
|
|
355
|
+
execute: switchSessionWorkspaceTool,
|
|
356
|
+
},
|
|
347
357
|
// ─── Phase 5: Utility Tools ────────────────────────────────────
|
|
348
358
|
{
|
|
349
359
|
definition: {
|
|
@@ -723,7 +733,7 @@ export const BUILTIN_TOOL_GUARDS = new Map([
|
|
|
723
733
|
[useCredentialToolDef.name, (ctx) => isUseCredentialAvailable(ctx.cwd)],
|
|
724
734
|
// InjectCredential hidden until ≥1 cookie credential exists (browser injection
|
|
725
735
|
// is cookie-only). Also degrades at call time if no browser bridge is wired.
|
|
726
|
-
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd)],
|
|
736
|
+
[injectCredentialToolDef.name, (ctx) => isInjectCredentialAvailable(ctx.cwd, ctx.settingsScope)],
|
|
727
737
|
[completeGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
728
738
|
[cancelGoalToolDef.name, (ctx) => ctx.hasGoal === true],
|
|
729
739
|
]);
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import { MemoryManager } from "../../session/memory.js";
|
|
18
18
|
const VALID_SCOPES = ["user", "dream"];
|
|
19
19
|
const VALID_TYPES = ["user", "feedback", "project", "reference"];
|
|
20
|
+
const VALID_ORIGINS = ["auto", "manual", "dream"];
|
|
20
21
|
function parseScope(raw) {
|
|
21
22
|
if (typeof raw !== "string")
|
|
22
|
-
return
|
|
23
|
+
return 'Error: scope is required ("user" or "dream")';
|
|
23
24
|
if (!VALID_SCOPES.includes(raw)) {
|
|
24
25
|
return `Error: scope must be "user" or "dream", got "${raw}"`;
|
|
25
26
|
}
|
|
@@ -30,6 +31,11 @@ function parseScope(raw) {
|
|
|
30
31
|
function parseLocation(raw) {
|
|
31
32
|
return raw === "global" ? "global" : "project";
|
|
32
33
|
}
|
|
34
|
+
function parseOrigin(raw) {
|
|
35
|
+
return typeof raw === "string" && VALID_ORIGINS.includes(raw)
|
|
36
|
+
? raw
|
|
37
|
+
: undefined;
|
|
38
|
+
}
|
|
33
39
|
function mmFor(ctx, scope, location = "project") {
|
|
34
40
|
// location=global → omit projectDir so the manager points at the global store.
|
|
35
41
|
return new MemoryManager({
|
|
@@ -46,7 +52,7 @@ const LOCATION_SCHEMA = {
|
|
|
46
52
|
export const memoryListToolDef = {
|
|
47
53
|
name: "MemoryList",
|
|
48
54
|
description: "List persistent memory entries from one scope. " +
|
|
49
|
-
"Returns each entry's name, type, and short description (not the full content — use MemoryRead for that). " +
|
|
55
|
+
"Returns each entry's id, origin, counts, name, type, and short description (not the full content — use MemoryRead for that). " +
|
|
50
56
|
"Use this before MemorySave/MemoryDelete to find the exact name to target. " +
|
|
51
57
|
'Scopes: "user" (entries the user owns; you need permission to modify) or "dream" (auto-consolidation workspace; you may freely modify).',
|
|
52
58
|
inputSchema: {
|
|
@@ -73,7 +79,9 @@ export async function memoryListTool(args, ctx) {
|
|
|
73
79
|
if (entries.length === 0)
|
|
74
80
|
return `(no memories in scope "${scope}")`;
|
|
75
81
|
return entries
|
|
76
|
-
.map((e) => `- [${e.type}] ${e.name}
|
|
82
|
+
.map((e) => `- [${e.type}] ${e.name} ` +
|
|
83
|
+
`(id:${e.id ?? "(none)"}, origin:${e.origin ?? "manual"}, use:${e.useCount ?? 0}, updates:${e.updateCount ?? 0}) — ` +
|
|
84
|
+
e.description)
|
|
77
85
|
.join("\n");
|
|
78
86
|
}
|
|
79
87
|
catch (err) {
|
|
@@ -127,15 +135,24 @@ export async function memoryReadTool(args, ctx) {
|
|
|
127
135
|
if (cb) {
|
|
128
136
|
// scope here is always "user"|"dream" (validated above); pending is
|
|
129
137
|
// never tool-readable, so the narrower event type is correct.
|
|
130
|
-
void cb({
|
|
138
|
+
void cb({
|
|
139
|
+
type: "memory_recalled",
|
|
140
|
+
name: entry.name,
|
|
141
|
+
scope: scope,
|
|
142
|
+
location,
|
|
143
|
+
});
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
catch {
|
|
134
147
|
// ignore — the read result below is what matters
|
|
135
148
|
}
|
|
136
149
|
return (`name: ${entry.name}\n` +
|
|
150
|
+
`id: ${entry.id ?? ""}\n` +
|
|
137
151
|
`description: ${entry.description}\n` +
|
|
138
152
|
`type: ${entry.type}\n` +
|
|
153
|
+
`origin: ${entry.origin ?? "manual"}\n` +
|
|
154
|
+
`useCount: ${entry.useCount ?? 0}\n` +
|
|
155
|
+
`updateCount: ${entry.updateCount ?? 0}\n` +
|
|
139
156
|
`\n${entry.content}`);
|
|
140
157
|
}
|
|
141
158
|
catch (err) {
|
|
@@ -145,7 +162,7 @@ export async function memoryReadTool(args, ctx) {
|
|
|
145
162
|
// ─── MemorySave ────────────────────────────────────────────────────────────
|
|
146
163
|
export const memorySaveToolDef = {
|
|
147
164
|
name: "MemorySave",
|
|
148
|
-
description: "Create or
|
|
165
|
+
description: "Create or update a memory entry. Before saving, scan the injected memory index and/or call MemoryList; for the same durable topic, update the existing entry by id instead of creating date-stamped variants. " +
|
|
149
166
|
'Saving to scope "user" requires user permission (you will see a confirmation prompt). ' +
|
|
150
167
|
'Saving to scope "dream" is automatic — it is your auto-consolidation workspace. ' +
|
|
151
168
|
"Pick `type` carefully: " +
|
|
@@ -154,7 +171,7 @@ export const memorySaveToolDef = {
|
|
|
154
171
|
"project (non-obvious facts about ongoing work), " +
|
|
155
172
|
"reference (pointers to external resources). " +
|
|
156
173
|
"Pick `location`: global (a lesson/preference true in ANY project) vs project (this repo only). " +
|
|
157
|
-
"If a memory in the injected index is now stale or wrong,
|
|
174
|
+
"If a memory in the injected index is now stale or wrong, update it by id or MemoryDelete it — keep the store correct rather than letting contradictions pile up. " +
|
|
158
175
|
"The `description` is a one-line summary shown in the index; the `content` is the full body.",
|
|
159
176
|
inputSchema: {
|
|
160
177
|
type: "object",
|
|
@@ -165,9 +182,13 @@ export const memorySaveToolDef = {
|
|
|
165
182
|
description: "Which scope to save to",
|
|
166
183
|
},
|
|
167
184
|
location: LOCATION_SCHEMA,
|
|
185
|
+
id: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "Stable id from MemoryList/MemoryRead when updating an existing memory. Omit only for genuinely new topics.",
|
|
188
|
+
},
|
|
168
189
|
name: {
|
|
169
190
|
type: "string",
|
|
170
|
-
description: "Short
|
|
191
|
+
description: "Short stable topic identifier; avoid dates, versions, or batch suffixes",
|
|
171
192
|
},
|
|
172
193
|
description: {
|
|
173
194
|
type: "string",
|
|
@@ -192,11 +213,14 @@ export async function memorySaveTool(args, ctx) {
|
|
|
192
213
|
return scope;
|
|
193
214
|
}
|
|
194
215
|
const name = args.name;
|
|
216
|
+
const id = args.id;
|
|
195
217
|
const description = args.description;
|
|
196
218
|
const type = args.type;
|
|
197
219
|
const content = args.content;
|
|
198
220
|
if (typeof name !== "string" || !name)
|
|
199
221
|
return "Error: name is required";
|
|
222
|
+
if (id !== undefined && typeof id !== "string")
|
|
223
|
+
return "Error: id must be a string";
|
|
200
224
|
if (typeof description !== "string")
|
|
201
225
|
return "Error: description is required";
|
|
202
226
|
if (typeof content !== "string")
|
|
@@ -207,12 +231,20 @@ export async function memorySaveTool(args, ctx) {
|
|
|
207
231
|
try {
|
|
208
232
|
const location = parseLocation(args.location);
|
|
209
233
|
const mm = mmFor(ctx, scope, location);
|
|
234
|
+
const dreamLoop = ctx?.__dreamLoop === true;
|
|
235
|
+
const forcedOrigin = dreamLoop
|
|
236
|
+
? "dream"
|
|
237
|
+
: scope === "user"
|
|
238
|
+
? "manual"
|
|
239
|
+
: (parseOrigin(args.origin) ?? "manual");
|
|
210
240
|
const fileName = mm.save({
|
|
241
|
+
id,
|
|
211
242
|
name,
|
|
212
243
|
description,
|
|
213
244
|
type: type,
|
|
214
245
|
content,
|
|
215
|
-
|
|
246
|
+
origin: forcedOrigin,
|
|
247
|
+
}, { forceOrigin: forcedOrigin });
|
|
216
248
|
return `Saved memory "${name}" → ${location}/${scope}/${fileName}`;
|
|
217
249
|
}
|
|
218
250
|
catch (err) {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
+
export declare const switchSessionWorkspaceToolDef: ToolDefinition;
|
|
7
|
+
export declare function switchSessionWorkspaceTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
6
8
|
export declare const enterWorktreeToolDef: ToolDefinition;
|
|
7
9
|
export declare function enterWorktreeTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
|
|
8
10
|
export declare const exitWorktreeToolDef: ToolDefinition;
|
|
@@ -4,6 +4,49 @@
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { createWorktree, removeWorktree, listWorktrees, validateWorktreeSlug, selectPlatformScript, runWorktreeSetup, worktreeHasUncommittedOrAheadChanges, currentBranch, } from "../../git/worktree.js";
|
|
7
|
+
export const switchSessionWorkspaceToolDef = {
|
|
8
|
+
name: "SwitchSessionWorkspace",
|
|
9
|
+
description: "Switch this current conversation session into or out of a workspace through the host UI path. " +
|
|
10
|
+
"Use this when you need isolated or parallel work in a git worktree, when you need to move " +
|
|
11
|
+
"this conversation to an existing worktree path/branch, or when you are done and should return " +
|
|
12
|
+
"this current conversation to main. This is the correct way to move THIS conversation's session " +
|
|
13
|
+
"into or out of a worktree on desktop.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
target: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Workspace target: 'main', a new slug, an existing worktree path, or an existing branch name.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ["target"],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export async function switchSessionWorkspaceTool(args, ctx) {
|
|
26
|
+
const target = stringArg(args.target);
|
|
27
|
+
if (!target)
|
|
28
|
+
return "Error: target is required";
|
|
29
|
+
const bridge = ctx?.workspace;
|
|
30
|
+
if (!bridge) {
|
|
31
|
+
return ("SwitchSessionWorkspace is not available in this host. " +
|
|
32
|
+
"Use the host's supported workspace controls instead.");
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const workspace = await bridge.switch(target);
|
|
36
|
+
ctx?.setSessionWorkspace?.(workspace);
|
|
37
|
+
const details = workspace.kind === "worktree" && workspace.worktree
|
|
38
|
+
? `\n Branch: ${workspace.worktree.branch}`
|
|
39
|
+
: "";
|
|
40
|
+
return (`Switched session workspace:\n` +
|
|
41
|
+
` Path: ${workspace.root}` +
|
|
42
|
+
details +
|
|
43
|
+
`\n\n` +
|
|
44
|
+
nextTurnNotice(workspace.root, ctx?.cwd ?? workspace.root));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return `Error switching workspace: ${err.message}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
7
50
|
export const enterWorktreeToolDef = {
|
|
8
51
|
name: "EnterWorktree",
|
|
9
52
|
description: "Switch the current session workspace. Target can be a new worktree slug, " +
|
|
@@ -40,6 +83,7 @@ export async function enterWorktreeTool(args, ctx) {
|
|
|
40
83
|
};
|
|
41
84
|
const fromRoot = fromWorkspace.root;
|
|
42
85
|
const currentTurnRoot = ctx?.cwd ?? fromRoot;
|
|
86
|
+
const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
|
|
43
87
|
try {
|
|
44
88
|
if (target === "main") {
|
|
45
89
|
const workspace = { root: mainRoot, kind: "main" };
|
|
@@ -50,12 +94,13 @@ export async function enterWorktreeTool(args, ctx) {
|
|
|
50
94
|
` From: ${fromRoot}\n\n` +
|
|
51
95
|
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
52
96
|
}
|
|
53
|
-
const selected = resolveWorktreeTarget({
|
|
97
|
+
const selected = await resolveWorktreeTarget({
|
|
54
98
|
target,
|
|
55
99
|
cwd: ctx?.cwd ?? mainRoot,
|
|
56
100
|
mainRoot,
|
|
57
101
|
sessionId,
|
|
58
102
|
currentWorkspace: fromWorkspace,
|
|
103
|
+
branchPrefix,
|
|
59
104
|
});
|
|
60
105
|
const workspace = toSessionWorkspace(selected, fromWorkspace);
|
|
61
106
|
persistSessionWorkspace(sessionManager, sessionId, workspace, ctx);
|
|
@@ -125,7 +170,7 @@ export async function exitWorktreeTool(args, ctx) {
|
|
|
125
170
|
return `Error: unknown action "${requested}" (expected keep, detach, or discard).`;
|
|
126
171
|
}
|
|
127
172
|
const hasPendingChanges = existsSync(workspace.worktree.path) &&
|
|
128
|
-
worktreeHasUncommittedOrAheadChanges(workspace.worktree.path, workspace.worktree.baseRef);
|
|
173
|
+
(await worktreeHasUncommittedOrAheadChanges(workspace.worktree.path, workspace.worktree.baseRef));
|
|
129
174
|
const action = requested ?? (hasPendingChanges ? undefined : "detach");
|
|
130
175
|
if (!action) {
|
|
131
176
|
return (`Error: worktree has uncommitted changes or new commits. Choose action "keep" to preserve ` +
|
|
@@ -137,10 +182,11 @@ export async function exitWorktreeTool(args, ctx) {
|
|
|
137
182
|
}
|
|
138
183
|
const mainRoot = sessionManager.readCwd(sessionId) ?? workspace.root;
|
|
139
184
|
const currentTurnRoot = ctx?.cwd ?? workspace.root;
|
|
185
|
+
const branchPrefix = ctx?.engine?.readWorktreeBranchPrefix?.(mainRoot);
|
|
140
186
|
try {
|
|
141
187
|
let removal;
|
|
142
188
|
if (action === "discard" || action === "detach") {
|
|
143
|
-
const otherOwners = otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, workspace.worktree.path);
|
|
189
|
+
const otherOwners = await otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, workspace.worktree.path);
|
|
144
190
|
if (otherOwners.length > 0) {
|
|
145
191
|
const mainWorkspace = { root: mainRoot, kind: "main" };
|
|
146
192
|
persistSessionWorkspace(sessionManager, sessionId, mainWorkspace, ctx);
|
|
@@ -149,7 +195,7 @@ export async function exitWorktreeTool(args, ctx) {
|
|
|
149
195
|
}
|
|
150
196
|
}
|
|
151
197
|
if (action === "discard") {
|
|
152
|
-
removal = removeWorktree(workspace.worktree.path, true);
|
|
198
|
+
removal = removeWorktree(workspace.worktree.path, true, { prefix: branchPrefix });
|
|
153
199
|
}
|
|
154
200
|
else if (action === "detach") {
|
|
155
201
|
removal = removeWorktree(workspace.worktree.path, false);
|
|
@@ -196,15 +242,15 @@ function sessionServices(ctx) {
|
|
|
196
242
|
}
|
|
197
243
|
return { ok: true, sessionId, sessionManager };
|
|
198
244
|
}
|
|
199
|
-
function otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, worktreePath) {
|
|
245
|
+
async function otherSessionOwnersForWorktree(sessionManager, sessionId, mainRoot, worktreePath) {
|
|
200
246
|
const workspaceOwners = sessionManager
|
|
201
247
|
.list(Number.MAX_SAFE_INTEGER)
|
|
202
248
|
.map((session) => ({ sessionId: session.sessionId, workspace: session.workspace }))
|
|
203
249
|
.filter((owner) => owner.workspace !== undefined);
|
|
204
|
-
const entry = listWorktrees(mainRoot, {
|
|
250
|
+
const entry = (await listWorktrees(mainRoot, {
|
|
205
251
|
currentSessionId: sessionId,
|
|
206
252
|
workspaceOwners,
|
|
207
|
-
}).find((worktree) => resolve(worktree.path) === resolve(worktreePath));
|
|
253
|
+
})).find((worktree) => resolve(worktree.path) === resolve(worktreePath));
|
|
208
254
|
return (entry?.occupiedBySessionIds ?? []).filter((owner) => owner !== sessionId);
|
|
209
255
|
}
|
|
210
256
|
function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnRoot) {
|
|
@@ -213,8 +259,8 @@ function sharedWorktreeRemovalSkippedMessage(otherOwners, mainRoot, currentTurnR
|
|
|
213
259
|
`Back to ${mainRoot} starting next turn.\n` +
|
|
214
260
|
nextTurnNotice(mainRoot, currentTurnRoot));
|
|
215
261
|
}
|
|
216
|
-
function resolveWorktreeTarget(opts) {
|
|
217
|
-
const entries = listWorktrees(opts.mainRoot);
|
|
262
|
+
async function resolveWorktreeTarget(opts) {
|
|
263
|
+
const entries = await listWorktrees(opts.mainRoot);
|
|
218
264
|
const pathTarget = pathLike(opts.target) ? resolvePathTarget(opts.target, opts.cwd) : undefined;
|
|
219
265
|
const branchTarget = normalizeBranchName(opts.target);
|
|
220
266
|
const match = entries.find((entry) => {
|
|
@@ -230,7 +276,7 @@ function resolveWorktreeTarget(opts) {
|
|
|
230
276
|
worktreePath: match.path,
|
|
231
277
|
worktreeName: match.path.split(/[\\/]/).pop() ?? match.branch,
|
|
232
278
|
worktreeBranch: match.branch,
|
|
233
|
-
originalBranch: currentBranch(opts.mainRoot),
|
|
279
|
+
originalBranch: await currentBranch(opts.mainRoot),
|
|
234
280
|
sessionId: opts.sessionId,
|
|
235
281
|
createdAt: Date.now(),
|
|
236
282
|
},
|
|
@@ -241,7 +287,9 @@ function resolveWorktreeTarget(opts) {
|
|
|
241
287
|
throw new Error(`no existing worktree found at ${opts.target}`);
|
|
242
288
|
}
|
|
243
289
|
validateWorktreeSlug(opts.target);
|
|
244
|
-
const created = createWorktree(opts.mainRoot, opts.target, opts.sessionId
|
|
290
|
+
const created = await createWorktree(opts.mainRoot, opts.target, opts.sessionId, {
|
|
291
|
+
prefix: opts.branchPrefix,
|
|
292
|
+
});
|
|
245
293
|
return { created: true, session: created, from: created.originalBranch ?? "HEAD" };
|
|
246
294
|
}
|
|
247
295
|
function toSessionWorkspace(selected, currentWorkspace) {
|
|
@@ -40,6 +40,8 @@ export interface ToolRuntimeHost {
|
|
|
40
40
|
linux?: string;
|
|
41
41
|
windows?: string;
|
|
42
42
|
} | undefined;
|
|
43
|
+
/** Branch prefix used for CodeShell-managed worktree branches. */
|
|
44
|
+
readWorktreeBranchPrefix?(cwd?: string): string | undefined;
|
|
43
45
|
/** Resolve a setup-only sandbox for a newly-created worktree root. */
|
|
44
46
|
resolveWorktreeSetupSandbox?(cwd: string): Promise<SandboxBackend | undefined>;
|
|
45
47
|
/** Resolve setup-only shell env for a newly-created worktree root. */
|
|
@@ -180,6 +182,7 @@ export interface SubAgentSpawner {
|
|
|
180
182
|
export interface ToolVisibilityContext {
|
|
181
183
|
cwd: string;
|
|
182
184
|
hasGoal: boolean;
|
|
185
|
+
settingsScope?: import("../settings/manager.js").SettingsScope;
|
|
183
186
|
}
|
|
184
187
|
export interface ToolContext {
|
|
185
188
|
/** Active working directory for this Engine. */
|
|
@@ -348,6 +351,13 @@ export interface ToolContext {
|
|
|
348
351
|
* docs/superpowers/specs/2026-06-16-browser-automation-mvp.md.
|
|
349
352
|
*/
|
|
350
353
|
browser?: import("./browser-bridge.js").BrowserBridge;
|
|
354
|
+
/**
|
|
355
|
+
* Host-backed workspace switch bridge. Desktop wires this to Electron main so
|
|
356
|
+
* the model's SwitchSessionWorkspace tool goes through the same path as the UI
|
|
357
|
+
* switcher. Undefined outside hosts that can switch this conversation's
|
|
358
|
+
* session workspace.
|
|
359
|
+
*/
|
|
360
|
+
workspace?: import("./workspace-bridge.js").WorkspaceBridge;
|
|
351
361
|
/**
|
|
352
362
|
* Inject a stored cookie credential into the built-in browser (restore its
|
|
353
363
|
* login state so the AI can drive the page as that account). The host
|
|
@@ -359,7 +369,7 @@ export interface ToolContext {
|
|
|
359
369
|
injectCredentialToBrowser?: InjectCredentialFn;
|
|
360
370
|
}
|
|
361
371
|
/** Inject a cookie credential into the built-in browser (host-implemented). */
|
|
362
|
-
export type InjectCredentialFn = (credentialId: string) => Promise<{
|
|
372
|
+
export type InjectCredentialFn = (credentialId: string, credentialScope?: "full" | "project") => Promise<{
|
|
363
373
|
ok: boolean;
|
|
364
374
|
count?: number;
|
|
365
375
|
error?: string;
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
import type { Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import type { MCPServerConfig, RegisteredTool } from "../types.js";
|
|
8
8
|
import { ToolRegistry } from "./registry.js";
|
|
9
|
+
interface MCPResourceInfo {
|
|
10
|
+
uri: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
serverName: string;
|
|
14
|
+
}
|
|
9
15
|
/**
|
|
10
16
|
* Read a required secret from `process.env` by NAME (Codex-style env-secret
|
|
11
17
|
* handling — the value is never persisted in MCP config). A referenced env var
|
|
@@ -125,13 +131,10 @@ export declare class MCPManager {
|
|
|
125
131
|
/**
|
|
126
132
|
* List resources from MCP servers.
|
|
127
133
|
*/
|
|
128
|
-
listResources(serverName?: string, signal?: AbortSignal): Promise<
|
|
129
|
-
uri: string;
|
|
130
|
-
name: string;
|
|
131
|
-
description?: string;
|
|
132
|
-
}>>;
|
|
134
|
+
listResources(serverName?: string, signal?: AbortSignal): Promise<MCPResourceInfo[]>;
|
|
133
135
|
/**
|
|
134
136
|
* Read a resource from an MCP server.
|
|
135
137
|
*/
|
|
136
138
|
readResource(serverName: string, uri: string, signal?: AbortSignal): Promise<string>;
|
|
137
139
|
}
|
|
140
|
+
export {};
|