@cjhyy/code-shell-core 0.8.13 → 0.9.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.
Files changed (91) hide show
  1. package/dist/automation/desktop-authority-client.d.ts +9 -0
  2. package/dist/automation/desktop-authority-client.js +47 -0
  3. package/dist/automation/scheduler.d.ts +8 -0
  4. package/dist/automation/scheduler.js +37 -1
  5. package/dist/automation/store.js +14 -1
  6. package/dist/capabilities/index.d.ts +1 -0
  7. package/dist/cli/agent-server-stdio.js +8 -4
  8. package/dist/engine/engine-workspace-authority.d.ts +35 -0
  9. package/dist/engine/engine-workspace-authority.js +136 -0
  10. package/dist/engine/engine.d.ts +5 -10
  11. package/dist/engine/engine.js +108 -112
  12. package/dist/engine/input-attachments.d.ts +1 -0
  13. package/dist/engine/input-attachments.js +6 -2
  14. package/dist/engine/run-environment.d.ts +8 -3
  15. package/dist/engine/run-environment.js +33 -8
  16. package/dist/engine/run-image-input.d.ts +1 -0
  17. package/dist/engine/run-image-input.js +1 -0
  18. package/dist/engine/run-session-open.d.ts +2 -1
  19. package/dist/engine/run-session-open.js +7 -1
  20. package/dist/engine/run-setup.d.ts +1 -0
  21. package/dist/engine/run-setup.js +2 -1
  22. package/dist/engine/run-tooling.d.ts +2 -0
  23. package/dist/engine/run-tooling.js +3 -0
  24. package/dist/engine/run-types.d.ts +4 -0
  25. package/dist/engine/run-workspace.d.ts +6 -1
  26. package/dist/engine/run-workspace.js +47 -0
  27. package/dist/engine/subagent-spawner.d.ts +1 -0
  28. package/dist/engine/subagent-spawner.js +1 -0
  29. package/dist/engine/turn-loop.js +17 -0
  30. package/dist/engine/types.d.ts +2 -0
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.extension.d.ts +1 -1
  33. package/dist/index.internal.d.ts +3 -2
  34. package/dist/index.internal.js +2 -0
  35. package/dist/index.js +2 -2
  36. package/dist/llm/client-base.d.ts +2 -1
  37. package/dist/llm/client-base.js +3 -1
  38. package/dist/llm/providers/anthropic.js +24 -25
  39. package/dist/llm/providers/openai.d.ts +4 -1
  40. package/dist/llm/providers/openai.js +76 -13
  41. package/dist/panel-apps/index.d.ts +1 -1
  42. package/dist/panel-apps/index.js +1 -1
  43. package/dist/panel-apps/installer.d.ts +29 -0
  44. package/dist/panel-apps/installer.js +124 -15
  45. package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
  46. package/dist/plugins/pluginAutomationTemplates.js +4 -0
  47. package/dist/plugins/pluginCatalog.d.ts +6 -0
  48. package/dist/plugins/pluginCatalog.js +7 -2
  49. package/dist/plugins/pluginContent.d.ts +1 -1
  50. package/dist/plugins/pluginContent.js +2 -10
  51. package/dist/prompt/composer.d.ts +4 -1
  52. package/dist/prompt/composer.js +11 -3
  53. package/dist/protocol/chat-session-manager.d.ts +42 -1
  54. package/dist/protocol/chat-session-manager.js +167 -4
  55. package/dist/protocol/chat-session.d.ts +11 -1
  56. package/dist/protocol/chat-session.js +20 -2
  57. package/dist/protocol/mobile-remote-types.d.ts +15 -0
  58. package/dist/protocol/server.d.ts +1 -2
  59. package/dist/protocol/server.js +28 -51
  60. package/dist/protocol/session-workspace-rpc.d.ts +23 -0
  61. package/dist/protocol/session-workspace-rpc.js +171 -0
  62. package/dist/protocol/types.d.ts +47 -1
  63. package/dist/protocol/types.js +4 -0
  64. package/dist/session/session-manager.d.ts +25 -0
  65. package/dist/session/session-manager.js +106 -6
  66. package/dist/session/transcript.d.ts +9 -0
  67. package/dist/session/transcript.js +81 -0
  68. package/dist/settings/manager.d.ts +12 -0
  69. package/dist/settings/manager.js +31 -0
  70. package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
  71. package/dist/tool-system/builtin/configure-model-connection.js +396 -0
  72. package/dist/tool-system/builtin/cron.d.ts +11 -0
  73. package/dist/tool-system/builtin/cron.js +27 -2
  74. package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
  75. package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
  76. package/dist/tool-system/builtin/edit.js +5 -3
  77. package/dist/tool-system/builtin/index.js +14 -0
  78. package/dist/tool-system/builtin/install-capability.js +22 -8
  79. package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
  80. package/dist/tool-system/builtin/settings-changed.js +18 -0
  81. package/dist/tool-system/builtin/write.js +5 -3
  82. package/dist/tool-system/context.d.ts +12 -3
  83. package/dist/tool-system/executor.js +1 -1
  84. package/dist/tool-system/path-policy.d.ts +11 -3
  85. package/dist/tool-system/path-policy.js +56 -36
  86. package/dist/types.d.ts +11 -0
  87. package/dist/workspace/canonical-key.d.ts +7 -0
  88. package/dist/workspace/canonical-key.js +39 -0
  89. package/dist/workspace/workspace-context.d.ts +26 -0
  90. package/dist/workspace/workspace-context.js +112 -0
  91. package/package.json +1 -1
@@ -41,6 +41,24 @@ function cachedTokensOf(usage) {
41
41
  out.cacheCreationTokens = details.cache_write_tokens;
42
42
  return out;
43
43
  }
44
+ /** Read the effective output-token ceiling from a built Chat Completions request. */
45
+ function outputTokenLimitOf(requestBody) {
46
+ const value = requestBody.max_completion_tokens ?? requestBody.max_tokens;
47
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
48
+ }
49
+ /**
50
+ * Some OpenAI-compatible gateways return finish_reason `tool_calls` when the
51
+ * output ceiling cuts a function-argument JSON string off. The malformed JSON
52
+ * plus exact cap usage is the evidence that distinguishes this from an ordinary
53
+ * completed tool call.
54
+ */
55
+ function didHitToolArgumentOutputLimit(input) {
56
+ return (input.finishReason === "tool_calls" &&
57
+ input.malformedToolArguments &&
58
+ input.outputTokenLimit !== undefined &&
59
+ input.completionTokens !== undefined &&
60
+ input.completionTokens >= input.outputTokenLimit);
61
+ }
44
62
  /**
45
63
  * Consume an async iterable of stream chunks with an idle watchdog.
46
64
  * Returns the accumulated text — either from onChunk return values, or
@@ -193,6 +211,7 @@ function normalizeOpenAIToolMessagePairs(messages) {
193
211
  }
194
212
  export class OpenAIClient extends LLMClientBase {
195
213
  _client = null;
214
+ dangerouslyAllowBrowser;
196
215
  // Sticky override: once the endpoint tells us `max_tokens` is rejected for
197
216
  // this model, switch to `max_completion_tokens` for the lifetime of the
198
217
  // client. Cheaper and more reliable than re-deriving from the model id when
@@ -205,8 +224,9 @@ export class OpenAIClient extends LLMClientBase {
205
224
  // succeed. Omitting the field just means "model default reasoning", which is
206
225
  // fine for our background/aux calls.
207
226
  _dropReasoningEffort = false;
208
- constructor(config, defaults) {
227
+ constructor(config, defaults, runtimeOptions = {}) {
209
228
  super(config, defaults);
229
+ this.dangerouslyAllowBrowser = runtimeOptions.dangerouslyAllowBrowser === true;
210
230
  }
211
231
  initClient() {
212
232
  // Lazy init — client created on first use
@@ -222,6 +242,8 @@ export class OpenAIClient extends LLMClientBase {
222
242
  (Object.keys(headers).length > 0 ? "x-headers-auth" : undefined),
223
243
  ...(this.config.baseUrl ? { baseURL: this.config.baseUrl } : {}),
224
244
  ...(Object.keys(headers).length > 0 ? { defaultHeaders: headers } : {}),
245
+ ...(this.fetch ? { fetch: this.fetch } : {}),
246
+ ...(this.dangerouslyAllowBrowser ? { dangerouslyAllowBrowser: true } : {}),
225
247
  timeout: this.timeout,
226
248
  });
227
249
  }
@@ -473,7 +495,8 @@ export class OpenAIClient extends LLMClientBase {
473
495
  }
474
496
  async nonStreamMessage(options, messages, tools, reasoning, requestSignal) {
475
497
  try {
476
- const response = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, false), { signal: requestSignal ?? options.signal });
498
+ const requestBody = this.buildRequestBody(options, messages, tools, reasoning, false);
499
+ const response = await this.client.chat.completions.create(requestBody, { signal: requestSignal ?? options.signal });
477
500
  const choice = response.choices[0];
478
501
  if (!choice)
479
502
  throw new LLMError("No response from OpenAI", "openai");
@@ -486,7 +509,7 @@ export class OpenAIClient extends LLMClientBase {
486
509
  ...cachedTokensOf(response.usage),
487
510
  };
488
511
  this.recordUsage(usage, options);
489
- return this.processChoice(choice, usage);
512
+ return this.processChoice(choice, usage, outputTokenLimitOf(requestBody));
490
513
  }
491
514
  catch (err) {
492
515
  this.handleApiError(err);
@@ -496,7 +519,9 @@ export class OpenAIClient extends LLMClientBase {
496
519
  async streamMessage(options, messages, tools, reasoning, requestSignal) {
497
520
  const sdkSignal = requestSignal ?? options.signal;
498
521
  try {
499
- const stream = await this.client.chat.completions.create(this.buildRequestBody(options, messages, tools, reasoning, true), { signal: sdkSignal });
522
+ const requestBody = this.buildRequestBody(options, messages, tools, reasoning, true);
523
+ const stream = await this.client.chat.completions.create(requestBody, { signal: sdkSignal });
524
+ const outputTokenLimit = outputTokenLimitOf(requestBody);
500
525
  let text = "";
501
526
  let reasoningContent = "";
502
527
  const toolCallsMap = new Map();
@@ -605,6 +630,7 @@ export class OpenAIClient extends LLMClientBase {
605
630
  signal: sdkSignal,
606
631
  });
607
632
  const toolCalls = [];
633
+ let malformedToolArguments = false;
608
634
  for (const [, tc] of toolCallsMap) {
609
635
  // Drop incomplete tool calls: an empty id or name (the fallbacks set
610
636
  // when a delta never delivered them) would become a malformed call
@@ -621,9 +647,16 @@ export class OpenAIClient extends LLMClientBase {
621
647
  args = JSON.parse(tc.args || "{}");
622
648
  }
623
649
  catch {
624
- /* intentional: model emitted malformed tool-call JSON — fall back to
625
- empty args rather than crashing the stream; the tool layer reports
626
- the resulting validation error back to the model. */
650
+ malformedToolArguments = true;
651
+ // Keep the historical empty-args fallback for genuinely malformed
652
+ // model output. If usage also proves the configured output ceiling was
653
+ // reached, the stop reason is normalized to "length" below so the turn
654
+ // loop retries instead of executing this misleading empty call.
655
+ logger.warn("openai.malformed_tool_arguments", {
656
+ id: tc.id,
657
+ name: tc.name,
658
+ argumentChars: tc.args.length,
659
+ });
627
660
  }
628
661
  toolCalls.push({ id: tc.id, toolName: tc.name, args });
629
662
  }
@@ -634,11 +667,31 @@ export class OpenAIClient extends LLMClientBase {
634
667
  ...cachedTokensOf(streamUsage),
635
668
  };
636
669
  this.recordUsage(usage, options);
670
+ const inferredToolArgumentTruncation = didHitToolArgumentOutputLimit({
671
+ finishReason,
672
+ malformedToolArguments,
673
+ completionTokens: usage.completionTokens,
674
+ outputTokenLimit,
675
+ });
676
+ if (inferredToolArgumentTruncation) {
677
+ logger.warn("openai.tool_arguments_truncation_inferred", {
678
+ provider: this.provider,
679
+ model: this.model,
680
+ finishReason,
681
+ completionTokens: usage.completionTokens,
682
+ outputTokenLimit,
683
+ toolCount: toolCalls.length,
684
+ });
685
+ }
637
686
  return {
638
687
  text,
639
688
  toolCalls,
640
689
  usage,
641
- stopReason: finishReason ?? "stop",
690
+ // OpenRouter can report `tool_calls` even when max_tokens cuts a tool's
691
+ // argument JSON off. Normalize that proven cap hit to the standard
692
+ // OpenAI `length` spelling so the shared continuation guard can skip
693
+ // execution and ask the model to retry with a smaller call.
694
+ stopReason: inferredToolArgumentTruncation ? "length" : (finishReason ?? "stop"),
642
695
  ...(reasoningContent ? { reasoningContent } : {}),
643
696
  };
644
697
  }
@@ -647,9 +700,10 @@ export class OpenAIClient extends LLMClientBase {
647
700
  throw err;
648
701
  }
649
702
  }
650
- processChoice(choice, usage) {
703
+ processChoice(choice, usage, outputTokenLimit) {
651
704
  const text = choice.message.content ?? "";
652
705
  const toolCalls = [];
706
+ let malformedToolArguments = false;
653
707
  if (choice.message.tool_calls) {
654
708
  for (const tc of choice.message.tool_calls) {
655
709
  let args = {};
@@ -657,9 +711,12 @@ export class OpenAIClient extends LLMClientBase {
657
711
  args = JSON.parse(tc.function.arguments || "{}");
658
712
  }
659
713
  catch {
660
- /* intentional: model emitted malformed tool-call JSON — fall back to
661
- empty args rather than crashing; the tool layer reports the
662
- resulting validation error back to the model. */
714
+ malformedToolArguments = true;
715
+ logger.warn("openai.malformed_tool_arguments", {
716
+ id: tc.id,
717
+ name: tc.function.name,
718
+ argumentChars: tc.function.arguments?.length ?? 0,
719
+ });
663
720
  }
664
721
  toolCalls.push({
665
722
  id: tc.id,
@@ -669,11 +726,17 @@ export class OpenAIClient extends LLMClientBase {
669
726
  }
670
727
  }
671
728
  const reasoningContent = extractReasoningContent(choice.message);
729
+ const inferredToolArgumentTruncation = didHitToolArgumentOutputLimit({
730
+ finishReason: choice.finish_reason ?? undefined,
731
+ malformedToolArguments,
732
+ completionTokens: usage.completionTokens,
733
+ outputTokenLimit,
734
+ });
672
735
  return {
673
736
  text,
674
737
  toolCalls,
675
738
  usage,
676
- stopReason: choice.finish_reason ?? undefined,
739
+ stopReason: inferredToolArgumentTruncation ? "length" : (choice.finish_reason ?? undefined),
677
740
  ...(reasoningContent ? { reasoningContent } : {}),
678
741
  };
679
742
  }
@@ -1,4 +1,4 @@
1
1
  export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppAgentContribution, PanelAppAgentTool, PanelAppManifest, type PanelAppAgentContribution as PanelAppAgentContributionData, type PanelAppAgentTool as PanelAppAgentToolData, type PanelAppManifest as PanelAppManifestData, } from "./manifest.js";
2
2
  export { PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, } from "./paths.js";
3
- export { installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, type InstalledPanelApp, type InstalledPanelAppSource, type GitPanelAppSourceInput, type LocalPanelAppSourceInput, type PanelAppSourceInput, type PanelAppPreview, } from "./installer.js";
3
+ export { discoverGitPanelApps, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, type InstalledPanelApp, type InstalledPanelAppSource, type GitPanelAppSourceInput, type GitPanelAppDiscovery, type GitPanelAppDiscoveryCandidate, type GitPanelAppDiscoveryIssue, type LocalPanelAppSourceInput, type PanelAppSourceInput, type PanelAppPreview, } from "./installer.js";
4
4
  export { isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, type PanelAppBindingPolicy, } from "./bindings.js";
@@ -1,4 +1,4 @@
1
1
  export { PANEL_APP_ICONS, PANEL_APP_MANIFEST_FILE, PANEL_APP_PERMISSIONS, PanelAppAgentContribution, PanelAppAgentTool, PanelAppManifest, } from "./manifest.js";
2
2
  export { PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, panelAppInstallDir, panelAppsRegistryPath, panelAppsRoot, } from "./paths.js";
3
- export { installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, } from "./installer.js";
3
+ export { discoverGitPanelApps, installReviewedLocalPanelApp, installReviewedPanelAppUpdate, listInstalledPanelApps, previewInstalledPanelAppUpdate, previewLocalPanelApp, uninstallPanelApp, } from "./installer.js";
4
4
  export { isPanelAppBound, resolvePanelAppBindingPolicy, resolvePanelAppBindingProjectPath, } from "./bindings.js";
@@ -33,6 +33,29 @@ export interface PanelAppPreview {
33
33
  };
34
34
  warnings: string[];
35
35
  }
36
+ export interface GitPanelAppDiscoveryCandidate {
37
+ /** Repository-relative path, or "." when the repository root is the app. */
38
+ subdir: string;
39
+ source: GitPanelAppSourceInput;
40
+ id: string;
41
+ version: string;
42
+ title: {
43
+ default: string;
44
+ en?: string;
45
+ "zh-CN"?: string;
46
+ };
47
+ description?: string;
48
+ icon: PanelAppManifest["icon"];
49
+ }
50
+ export interface GitPanelAppDiscoveryIssue {
51
+ subdir: string;
52
+ error: string;
53
+ }
54
+ export interface GitPanelAppDiscovery {
55
+ source: GitPanelAppSourceInput;
56
+ panels: GitPanelAppDiscoveryCandidate[];
57
+ issues: GitPanelAppDiscoveryIssue[];
58
+ }
36
59
  export interface InstalledPanelApp {
37
60
  id: string;
38
61
  version: string;
@@ -52,6 +75,12 @@ export interface InstalledPanelApp {
52
75
  installedAt: string;
53
76
  lastUpdated: string;
54
77
  }
78
+ /**
79
+ * Download a public GitHub repository once and enumerate every independently
80
+ * installable Panel App beneath it. Discovery is read-only: selecting a result
81
+ * still runs the normal full review and digest-bound install flow.
82
+ */
83
+ export declare function discoverGitPanelApps(input: GitPanelAppSourceInput): Promise<GitPanelAppDiscovery>;
55
84
  export declare function previewLocalPanelApp(input: PanelAppSourceInput): Promise<PanelAppPreview>;
56
85
  /**
57
86
  * Re-open the original folder, archive, or GitHub source through the same
@@ -16,6 +16,9 @@ const MAX_FILE_BYTES = 16 * 1024 * 1024;
16
16
  const MAX_DEPTH = 16;
17
17
  const MAX_MANIFEST_BYTES = 1024 * 1024;
18
18
  const MAX_AGENT_SKILL_BYTES = 256 * 1024;
19
+ const MAX_PANEL_DISCOVERY_DEPTH = 4;
20
+ const MAX_PANEL_DISCOVERY_DIRECTORIES = 512;
21
+ const MAX_PANEL_DISCOVERY_RESULTS = 16;
19
22
  const REVIEWED_GIT_SNAPSHOT_TTL_MS = 10 * 60 * 1000;
20
23
  const MAX_REVIEWED_GIT_SNAPSHOTS = 8;
21
24
  const ALLOWED_ASSET_EXTENSIONS = new Set([
@@ -61,9 +64,14 @@ function normalizeGitPanelAppSource(input) {
61
64
  if (!raw || raw.length > MAX_SOURCE_PATH || raw.includes("\0")) {
62
65
  throw new PanelAppInstallError("GitHub repository URL is invalid");
63
66
  }
67
+ const urlText = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}(?:\.git)?\/?$/.test(raw)
68
+ ? `https://github.com/${raw}`
69
+ : /^github\.com\//i.test(raw)
70
+ ? `https://${raw}`
71
+ : raw;
64
72
  let parsed;
65
73
  try {
66
- parsed = new URL(raw);
74
+ parsed = new URL(urlText);
67
75
  }
68
76
  catch {
69
77
  throw new PanelAppInstallError("GitHub repository URL is invalid");
@@ -135,19 +143,52 @@ function normalizedGitSourceKey(input) {
135
143
  function looksLikePanelAppRoot(directory) {
136
144
  return existsSync(join(directory, PANEL_APP_MANIFEST_FILE));
137
145
  }
146
+ async function discoverPanelAppRoots(directory) {
147
+ const found = [];
148
+ const pending = [{ directory, depth: 0 }];
149
+ let visited = 0;
150
+ while (pending.length > 0 &&
151
+ visited < MAX_PANEL_DISCOVERY_DIRECTORIES &&
152
+ found.length < MAX_PANEL_DISCOVERY_RESULTS) {
153
+ const current = pending.shift();
154
+ visited += 1;
155
+ if (looksLikePanelAppRoot(current.directory)) {
156
+ found.push(current.directory);
157
+ continue;
158
+ }
159
+ if (current.depth >= MAX_PANEL_DISCOVERY_DEPTH)
160
+ continue;
161
+ const entries = await readdir(current.directory, { withFileTypes: true });
162
+ for (const entry of entries) {
163
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") {
164
+ continue;
165
+ }
166
+ pending.push({ directory: join(current.directory, entry.name), depth: current.depth + 1 });
167
+ }
168
+ }
169
+ return found;
170
+ }
138
171
  async function findPanelAppRoot(directory) {
139
172
  if (looksLikePanelAppRoot(directory))
140
173
  return directory;
141
- const entries = await readdir(directory, { withFileTypes: true });
142
- const children = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
143
- if (children.length === 1) {
144
- const nested = join(directory, children[0].name);
145
- if (looksLikePanelAppRoot(nested))
146
- return nested;
147
- }
148
- throw new PanelAppInstallError(`no Panel App found (expected ${PANEL_APP_MANIFEST_FILE})`);
174
+ const found = await discoverPanelAppRoots(directory);
175
+ if (found.length === 1)
176
+ return found[0];
177
+ if (found.length > 1) {
178
+ const candidates = found
179
+ .map((root) => relative(directory, root).split(sep).join(posix.sep))
180
+ .sort()
181
+ .join(", ");
182
+ throw new PanelAppInstallError(`multiple Panel Apps found; choose an app subdirectory: ${candidates}`);
183
+ }
184
+ throw new PanelAppInstallError(`no Panel App found (expected ${PANEL_APP_MANIFEST_FILE}); ` +
185
+ "for a monorepo, provide the app subdirectory or a GitHub /tree/<ref>/<path> URL");
149
186
  }
150
- async function openGitPanelAppSource(input) {
187
+ function joinedPanelAppSubdirectory(base, nested) {
188
+ const parts = [base, nested].filter((part) => Boolean(part));
189
+ return parts.length > 0 ? parts.join("/") : undefined;
190
+ }
191
+ async function openGitPanelAppArchive(input) {
151
192
  const source = normalizeGitPanelAppSource(input);
152
193
  const temporaryRoot = await mkdtemp(join(tmpdir(), "cs-panel-app-git-"));
153
194
  try {
@@ -157,18 +198,86 @@ async function openGitPanelAppSource(input) {
157
198
  await downloadGitHubPanelAppArchive(source, archivePath);
158
199
  await extractZipSubdirectory(archivePath, extractedRoot, source.subdir);
159
200
  await rm(archivePath, { force: true });
201
+ return { source, extractedRoot, temporaryRoot };
202
+ }
203
+ catch (error) {
204
+ await rm(temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
205
+ throw error;
206
+ }
207
+ }
208
+ async function openGitPanelAppSource(input) {
209
+ const opened = await openGitPanelAppArchive(input);
210
+ try {
160
211
  return {
161
- source,
162
- sourceKey: JSON.stringify(source),
163
- sourceRoot: await findPanelAppRoot(extractedRoot),
164
- temporaryRoot,
212
+ source: opened.source,
213
+ sourceKey: JSON.stringify(opened.source),
214
+ sourceRoot: await findPanelAppRoot(opened.extractedRoot),
215
+ temporaryRoot: opened.temporaryRoot,
165
216
  };
166
217
  }
167
218
  catch (error) {
168
- await rm(temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
219
+ await rm(opened.temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
169
220
  throw error;
170
221
  }
171
222
  }
223
+ /**
224
+ * Download a public GitHub repository once and enumerate every independently
225
+ * installable Panel App beneath it. Discovery is read-only: selecting a result
226
+ * still runs the normal full review and digest-bound install flow.
227
+ */
228
+ export async function discoverGitPanelApps(input) {
229
+ const opened = await openGitPanelAppArchive(input);
230
+ try {
231
+ const roots = await discoverPanelAppRoots(opened.extractedRoot);
232
+ if (roots.length === 0) {
233
+ throw new PanelAppInstallError(`no Panel App found (expected ${PANEL_APP_MANIFEST_FILE}); ` +
234
+ "check the repository, branch, or optional search subdirectory");
235
+ }
236
+ const panels = [];
237
+ const issues = [];
238
+ for (const root of roots) {
239
+ const nested = relative(opened.extractedRoot, root).split(sep).join(posix.sep);
240
+ const subdir = joinedPanelAppSubdirectory(opened.source.subdir, nested);
241
+ const label = subdir ?? ".";
242
+ try {
243
+ const inspected = await inspectPanelAppSource(root);
244
+ panels.push({
245
+ subdir: label,
246
+ source: {
247
+ kind: "git",
248
+ url: opened.source.url,
249
+ ...(opened.source.ref ? { ref: opened.source.ref } : {}),
250
+ ...(subdir ? { subdir } : {}),
251
+ },
252
+ id: inspected.manifest.id,
253
+ version: inspected.manifest.version,
254
+ title: inspected.manifest.title,
255
+ ...(inspected.manifest.description
256
+ ? { description: inspected.manifest.description }
257
+ : {}),
258
+ icon: inspected.manifest.icon,
259
+ });
260
+ }
261
+ catch (error) {
262
+ issues.push({
263
+ subdir: label,
264
+ error: error instanceof Error ? error.message : String(error),
265
+ });
266
+ }
267
+ }
268
+ panels.sort((left, right) => left.subdir.localeCompare(right.subdir));
269
+ issues.sort((left, right) => left.subdir.localeCompare(right.subdir));
270
+ if (panels.length === 0) {
271
+ throw new PanelAppInstallError(`found ${issues.length} Panel App manifest(s), but none passed validation: ${issues
272
+ .map((issue) => `${issue.subdir}: ${issue.error}`)
273
+ .join("; ")}`);
274
+ }
275
+ return { source: opened.source, panels, issues };
276
+ }
277
+ finally {
278
+ await rm(opened.temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
279
+ }
280
+ }
172
281
  async function disposeReviewedGitSnapshot(snapshot) {
173
282
  clearTimeout(snapshot.expiryTimer);
174
283
  await rm(snapshot.temporaryRoot, { recursive: true, force: true }).catch(() => undefined);
@@ -6,6 +6,8 @@ export interface InstantiatePluginAutomationTemplateOptions {
6
6
  templateId: string;
7
7
  expectedRevision: string;
8
8
  cwd?: string;
9
+ projectId?: string;
10
+ rootId?: string;
9
11
  /** Effective project/global disable list computed by the host for this cwd. */
10
12
  disabledPluginNames: ReadonlySet<string>;
11
13
  maxJobs?: number;
@@ -38,6 +38,10 @@ export function instantiatePluginAutomationTemplate(options) {
38
38
  const template = contribution.template;
39
39
  return options.scheduler.create(template.title.default, template.schedule, template.prompt, {
40
40
  ...(template.workspace === "current" && options.cwd ? { cwd: options.cwd } : {}),
41
+ ...(template.workspace === "current" && options.projectId
42
+ ? { projectId: options.projectId }
43
+ : {}),
44
+ ...(template.workspace === "current" && options.rootId ? { rootId: options.rootId } : {}),
41
45
  ...(template.timezone ? { timezone: template.timezone } : {}),
42
46
  permissionLevel: template.permissionLevel,
43
47
  templateSource: {
@@ -33,6 +33,12 @@ export interface PluginAutomationTemplateContribution {
33
33
  * instantiated later. Install paths and timestamps are intentionally excluded.
34
34
  */
35
35
  export declare function pluginAutomationTemplateRevision(installKey: string, template: PluginAutomationTemplate): string;
36
+ /**
37
+ * Read an installed plugin manifest without following links or accepting an
38
+ * unbounded/non-regular file. Shared by the runtime catalog and UI inventory so
39
+ * those views cannot disagree about which manifest bytes are trusted.
40
+ */
41
+ export declare function readCatalogPluginManifest(installPath: string): CanonicalPluginManifestData | null;
36
42
  /**
37
43
  * Load the installed plugin catalog at the core boundary.
38
44
  *
@@ -19,7 +19,12 @@ export function pluginAutomationTemplateRevision(installKey, template) {
19
19
  .update(JSON.stringify(template))
20
20
  .digest("hex");
21
21
  }
22
- function readCanonicalManifest(installPath) {
22
+ /**
23
+ * Read an installed plugin manifest without following links or accepting an
24
+ * unbounded/non-regular file. Shared by the runtime catalog and UI inventory so
25
+ * those views cannot disagree about which manifest bytes are trusted.
26
+ */
27
+ export function readCatalogPluginManifest(installPath) {
23
28
  const file = join(installPath, CANONICAL_PLUGIN_MANIFEST_FILE);
24
29
  let descriptor;
25
30
  try {
@@ -69,7 +74,7 @@ export function loadPluginCatalog(options = {}) {
69
74
  const installPath = resolveSafePluginPath(entry.installPath, root);
70
75
  if (!installPath)
71
76
  continue;
72
- const manifest = readCanonicalManifest(installPath);
77
+ const manifest = readCatalogPluginManifest(installPath);
73
78
  const identity = identityFromInstallKey(installKey, manifest);
74
79
  catalog.push({
75
80
  installKey,
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { type PluginHookEntry } from "./loadPluginHooks.js";
9
9
  import { type PluginHookReview } from "./pluginHookApproval.js";
10
- import { type PluginAutomationTemplate } from "./installer/types.js";
10
+ import type { PluginAutomationTemplate } from "./installer/types.js";
11
11
  export type PluginAutomationTemplateDescriptor = PluginAutomationTemplate & {
12
12
  revision: string;
13
13
  };
@@ -11,8 +11,7 @@ import { parseFrontmatter } from "../skills/frontmatter.js";
11
11
  import { listPluginHooks } from "./loadPluginHooks.js";
12
12
  import { reviewPluginHooks } from "./pluginHookApproval.js";
13
13
  import { readPluginMcp } from "./installer/loadPluginMcp.js";
14
- import { CANONICAL_PLUGIN_MANIFEST_FILE, CanonicalPluginManifest, } from "./installer/types.js";
15
- import { pluginAutomationTemplateRevision } from "./pluginCatalog.js";
14
+ import { pluginAutomationTemplateRevision, readCatalogPluginManifest } from "./pluginCatalog.js";
16
15
  function resolveContainedPath(root, candidate) {
17
16
  try {
18
17
  const realRoot = realpathSync(root);
@@ -117,14 +116,7 @@ export function describePluginContent(pluginName, installPath, installKey) {
117
116
  return undefined;
118
117
  }
119
118
  })();
120
- const manifest = (() => {
121
- try {
122
- return CanonicalPluginManifest.parse(JSON.parse(readFileSync(join(installPath, CANONICAL_PLUGIN_MANIFEST_FILE), "utf-8")));
123
- }
124
- catch {
125
- return null;
126
- }
127
- })();
119
+ const manifest = readCatalogPluginManifest(installPath);
128
120
  return {
129
121
  skills: listSkills(installPath),
130
122
  commands: listMdNames(installPath, "commands"),
@@ -10,8 +10,11 @@ import { type ScanOptions } from "./instruction-scanner.js";
10
10
  import { type AgentPreset } from "../preset/index.js";
11
11
  import type { BuiltinTool } from "../tool-system/builtin/index.js";
12
12
  import type { CapabilityDynamicContextProvider } from "../capabilities/index.js";
13
+ import { type WorkspaceContext } from "../workspace/workspace-context.js";
13
14
  export interface ComposerOptions {
14
15
  cwd: string;
16
+ /** Run workspace; omitted by legacy callers that only provide cwd. */
17
+ workspace?: WorkspaceContext;
15
18
  model: string;
16
19
  instructionOptions?: ScanOptions;
17
20
  /** Resolved preset — used to load section-based prompt. */
@@ -106,9 +109,9 @@ export interface ComposerOptions {
106
109
  disableSourcesContext?: boolean;
107
110
  }
108
111
  export declare class PromptComposer {
109
- private readonly options;
110
112
  private sectionCache;
111
113
  private cachedInstructions;
114
+ private readonly options;
112
115
  constructor(options: ComposerOptions);
113
116
  /**
114
117
  * Build the system prompt from sections.
@@ -11,12 +11,18 @@ import { MemoryManager } from "../session/memory.js";
11
11
  import { scanSkills } from "../skills/index.js";
12
12
  import { buildSkillListing } from "../tool-system/builtin/skill-prompt.js";
13
13
  import { resolveAgentPreset, buildPresetSystemPrompt } from "../preset/index.js";
14
+ import { legacySingleRootWorkspace, validateWorkspaceContext, } from "../workspace/workspace-context.js";
14
15
  export class PromptComposer {
15
- options;
16
16
  sectionCache = new SectionCache();
17
17
  cachedInstructions = null;
18
+ options;
18
19
  constructor(options) {
19
- this.options = options;
20
+ this.options = {
21
+ ...options,
22
+ workspace: options.workspace === undefined
23
+ ? legacySingleRootWorkspace(options.cwd)
24
+ : validateWorkspaceContext(options.workspace),
25
+ };
20
26
  }
21
27
  /**
22
28
  * Build the system prompt from sections.
@@ -58,7 +64,7 @@ export class PromptComposer {
58
64
  const preset = this.options.preset ?? resolveAgentPreset();
59
65
  const parts = await Promise.all((this.options.dynamicContextProviders ?? []).map(async (provider) => {
60
66
  try {
61
- return await provider({ cwd: this.options.cwd, preset });
67
+ return await provider({ cwd: this.options.cwd, workspace: this.options.workspace, preset });
62
68
  }
63
69
  catch {
64
70
  // A capability's optional context must not make a turn fail.
@@ -164,6 +170,8 @@ export class PromptComposer {
164
170
  const lines = [
165
171
  `You are an AI agent powered by ${this.options.model}.`,
166
172
  `Working directory: ${this.options.cwd}`,
173
+ `Workspace roots (${this.options.workspace.roots.length}):`,
174
+ ...this.options.workspace.roots.map((root) => `- [${root.role}] ${root.path} (rootId: ${root.id})`),
167
175
  `Platform: ${process.platform}`,
168
176
  `Shell: ${process.env.SHELL ?? "unknown"}`,
169
177
  ];
@@ -1,9 +1,10 @@
1
1
  import { ChatSession } from "./chat-session.js";
2
2
  import type { SessionKind } from "../types.js";
3
+ import type { SessionProjectBinding, SessionWorkspace } from "../types.js";
3
4
  import type { Engine } from "../engine/engine.js";
4
5
  import type { EngineRuntime } from "../engine/runtime.js";
5
6
  import type { EngineConfig } from "../engine/types.js";
6
- export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "goal" | "maxTurns" | "maxContextTokens" | "cwd" | "projectTrusted" | "sessionStorageDir">;
7
+ export type EngineConfigSlice = Pick<EngineConfig, "permissionMode" | "preset" | "customSystemPrompt" | "appendSystemPrompt" | "goal" | "maxTurns" | "maxContextTokens" | "cwd" | "workspaceContext" | "projectTrusted" | "sessionStorageDir">;
7
8
  /** Identity scope every ChatSessionManager belongs to when none is injected. */
8
9
  export declare const LOCAL_CHAT_IDENTITY = "local";
9
10
  export interface ChatSessionManagerOptions {
@@ -48,11 +49,30 @@ export interface LiveChatSessionSnapshot {
48
49
  }>;
49
50
  }
50
51
  export declare const CLOSED_CHAT_SESSION_TOMBSTONE_LIMIT = 4096;
52
+ export interface ResidentSessionMainRootMigration {
53
+ project: SessionProjectBinding;
54
+ mainRoot: string;
55
+ workspaceContext: import("../workspace/workspace-context.js").WorkspaceContext;
56
+ projectTrusted: boolean;
57
+ }
58
+ export type SessionMigrationOwnership = {
59
+ status: "resident";
60
+ session: ChatSession;
61
+ } | {
62
+ status: "not-resident";
63
+ ownershipToken: string;
64
+ } | {
65
+ status: "failed";
66
+ error: string;
67
+ };
51
68
  export declare class ChatSessionManager {
52
69
  private readonly sessions;
53
70
  private readonly closingSessions;
54
71
  private readonly closedSessions;
55
72
  private readonly sessionGeneration;
73
+ private readonly sessionSlices;
74
+ private readonly migrationClaims;
75
+ private readonly residentMigrations;
56
76
  readonly runtime: EngineRuntime;
57
77
  /** Identity scope this manager serves ("local" unless injected). */
58
78
  readonly identity: string;
@@ -76,6 +96,26 @@ export declare class ChatSessionManager {
76
96
  */
77
97
  forIdentity(identity: string): ChatSessionManager;
78
98
  getOrCreate(sessionId: string, slice: EngineConfigSlice): Promise<ChatSession>;
99
+ /**
100
+ * Prove whether this worker currently owns a resident Engine, or fence the
101
+ * Session so Main can perform one durable migration without a re-resume race.
102
+ */
103
+ beginSessionMigration(sessionId: string, ownershipToken: string): SessionMigrationOwnership;
104
+ /** Release only the exact claim minted for this Session. */
105
+ completeSessionMigration(sessionId: string, ownershipToken: string): boolean;
106
+ /**
107
+ * Rebuild an idle resident Engine against a new authoritative main root.
108
+ *
109
+ * Transaction order is intentional:
110
+ * 1. construct and restore the candidate while durable state still points at
111
+ * the old owner (factory/restore failure is therefore a no-op),
112
+ * 2. atomically commit durable state through the old owning Engine,
113
+ * 3. synchronously swap the Engine inside the existing ChatSession,
114
+ * 4. dispose the unreachable old Engine before reporting success.
115
+ *
116
+ * No await occurs between the idle check, durable commit and owner swap.
117
+ */
118
+ migrateResidentSessionMainRoot(sessionId: string, target: ResidentSessionMainRootMigration): Promise<SessionWorkspace>;
79
119
  /**
80
120
  * Cold-resume a persisted session using its own cwd. The first detached
81
121
  * Engine is only a storage probe; all actual work runs on the second Engine
@@ -121,6 +161,7 @@ export declare class ChatSessionManager {
121
161
  startIdleSweeper(intervalMs?: number): void;
122
162
  stopIdleSweeper(): void;
123
163
  private unregisterMcpOwner;
164
+ private disposeEngine;
124
165
  private engineSessionManager;
125
166
  }
126
167
  /**