lody 0.83.0 → 0.85.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/dist/codex-acp.js CHANGED
@@ -3,5 +3,5 @@
3
3
  console.error("CODEX_PATH is required for the bundled Codex ACP adapter.");
4
4
  process.exit(1);
5
5
  }
6
- await import("./chunks/index-CbTnvx5t.js");
6
+ await import("./chunks/index-ddMw90Gr.js");
7
7
  })();
@@ -2,13 +2,15 @@ import { randomUUID, createHash } from "node:crypto";
2
2
  import { isAbsolute } from "node:path";
3
3
  import { Readable, Writable } from "node:stream";
4
4
  import { n as ndJsonStream, A as AgentSideConnection, R as RequestError, P as PROTOCOL_VERSION } from "./chunks/acp-CrvzLY5A.js";
5
- import { b as DEEPSEEK_HARNESS_MODELS, D as DEEPSEEK_HARNESS_PERMISSION_MODES, c as DEEPSEEK_HARNESS_REASONING_OPTIONS, e as ACP_EXTENSION_DSH_VERSION, a as DEEPSEEK_HARNESS_AGENT_PRESETS } from "./chunks/profile-RfMzWliN.js";
5
+ import { D as DEEPSEEK_HARNESS_PERMISSION_MODES, c as DEEPSEEK_HARNESS_REASONING_OPTIONS, e as ACP_EXTENSION_DSH_VERSION, a as DEEPSEEK_HARNESS_AGENT_PRESETS } from "./chunks/profile-Cxxl5hCq.js";
6
6
  import "./chunks/schemas-Du3qLZPS.js";
7
7
  const name = "acp-extension-dsh";
8
8
  const inject = [
9
9
  "agents",
10
10
  "agentPresets",
11
+ "attachments",
11
12
  "loader",
13
+ "llm",
12
14
  "permissionPresets",
13
15
  "sessionPersistence",
14
16
  "sessionQuery"
@@ -22,7 +24,24 @@ const MCP_TOOL_CALL_TIMEOUT_MS = 6e4;
22
24
  const MCP_SERVER_NAME_MAX_LENGTH = 32;
23
25
  const MCP_SERVER_NAME_HASH_LENGTH = 8;
24
26
  const INVALID_MCP_SERVER_NAME_CHARS = /[^A-Za-z0-9_-]/gu;
25
- const MODEL_IDS = new Set(DEEPSEEK_HARNESS_MODELS.map((model) => model.modelId));
27
+ const IMAGE_MEDIA_TYPES = [
28
+ "image/png",
29
+ "image/jpeg",
30
+ "image/webp",
31
+ "image/gif"
32
+ ];
33
+ const CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
34
+ const IMAGE_ADMISSION_ERROR_CODES = /* @__PURE__ */ new Set([
35
+ "TOO_MANY_IMAGES",
36
+ "IMAGES_TOO_LARGE",
37
+ "UNSUPPORTED_IMAGE_TYPE",
38
+ "INVALID_IMAGE_BASE64",
39
+ "INVALID_IMAGE",
40
+ "IMAGE_TYPE_MISMATCH",
41
+ "IMAGE_TOO_LARGE",
42
+ "IMAGE_TOO_MANY_PIXELS",
43
+ "IMAGE_DIMENSION_TOO_LARGE"
44
+ ]);
26
45
  const PERMISSION_MODE_IDS = new Set(DEEPSEEK_HARNESS_PERMISSION_MODES.map((mode) => mode.id));
27
46
  const REASONING_EFFORT_IDS = new Set(DEEPSEEK_HARNESS_REASONING_OPTIONS.map((effort) => effort.value));
28
47
  function invalidParams(detail) {
@@ -38,9 +57,6 @@ function resolveAdapterConfig(config) {
38
57
  const provider = nonEmptyString(config?.provider, "deepseek-official");
39
58
  const model = nonEmptyString(config?.model, "deepseek-v4-pro");
40
59
  const reasoningEffort = config?.reasoningEffort ?? "max";
41
- if (!MODEL_IDS.has(model)) {
42
- throw new Error(`acp-extension-dsh: unsupported model ${JSON.stringify(model)}`);
43
- }
44
60
  if (!REASONING_EFFORT_IDS.has(reasoningEffort)) {
45
61
  throw new Error(`acp-extension-dsh: unsupported reasoning effort ${JSON.stringify(reasoningEffort)}`);
46
62
  }
@@ -51,6 +67,27 @@ function resolveAdapterConfig(config) {
51
67
  ...config?.stream ? { stream: config.stream } : {}
52
68
  };
53
69
  }
70
+ async function loadHarnessModels(ctx, provider) {
71
+ const llm = ctx.get("llm");
72
+ if (!llm)
73
+ throw new Error("acp-extension-dsh: no Harness LLM catalog is mounted");
74
+ const listed = await llm.listModels(provider);
75
+ const models = [];
76
+ const ids = /* @__PURE__ */ new Set();
77
+ for (const model of listed) {
78
+ if (model.provider !== provider || !model.id.trim())
79
+ continue;
80
+ if (ids.has(model.id)) {
81
+ throw new Error(`acp-extension-dsh: duplicate model ${JSON.stringify(model.id)} for provider ${JSON.stringify(provider)}`);
82
+ }
83
+ ids.add(model.id);
84
+ models.push({
85
+ ...model,
86
+ ...model.inputModalities ? { inputModalities: [...model.inputModalities] } : {}
87
+ });
88
+ }
89
+ return models;
90
+ }
54
91
  async function loadMcpClientPlugin(agentContext) {
55
92
  const module = agentContext.loader.unwrapExports(await agentContext.loader.import(MCP_CLIENT_PACKAGE));
56
93
  if (typeof module !== "object" || module === null || !("apply" in module) || typeof module.apply !== "function") {
@@ -202,9 +239,9 @@ function configOptions(record) {
202
239
  category: "model",
203
240
  type: "select",
204
241
  currentValue: record.selection.current.model,
205
- options: DEEPSEEK_HARNESS_MODELS.map((model) => ({
206
- value: model.modelId,
207
- name: model.name,
242
+ options: record.models.map((model) => ({
243
+ value: model.id,
244
+ name: model.name ?? model.id,
208
245
  description: model.description ?? null
209
246
  }))
210
247
  },
@@ -233,28 +270,97 @@ function modeState(record) {
233
270
  }))
234
271
  };
235
272
  }
236
- function acpPromptToText(prompt) {
237
- return prompt.flatMap((block) => {
238
- if (block.type === "text")
239
- return [block.text];
240
- if (block.type === "resource_link") {
241
- return [
242
- `
273
+ function modelSupportsImages(models, modelId) {
274
+ return models.some((model) => model.id === modelId && model.inputModalities?.includes("image"));
275
+ }
276
+ function imageMediaType(value) {
277
+ return IMAGE_MEDIA_TYPES.includes(value) ? value : void 0;
278
+ }
279
+ function decodePromptImage(block) {
280
+ const mediaType = imageMediaType(block.mimeType);
281
+ if (!mediaType) {
282
+ throw invalidParams("image mimeType must be image/png, image/jpeg, image/webp, or image/gif");
283
+ }
284
+ if (!block.data || !CANONICAL_BASE64.test(block.data)) {
285
+ throw invalidParams("image data must be canonical base64");
286
+ }
287
+ const decoded = Buffer.from(block.data, "base64");
288
+ if (decoded.toString("base64") !== block.data) {
289
+ throw invalidParams("image data must be canonical base64");
290
+ }
291
+ return { data: new Uint8Array(decoded), mediaType };
292
+ }
293
+ function isImageAdmissionError(error) {
294
+ return error instanceof Error && "code" in error && typeof error.code === "string" && IMAGE_ADMISSION_ERROR_CODES.has(error.code);
295
+ }
296
+ async function admitAcpPrompt(prompt, models, modelId, attachments) {
297
+ const images = [];
298
+ for (const block of prompt) {
299
+ switch (block.type) {
300
+ case "text":
301
+ case "resource_link":
302
+ break;
303
+ case "image":
304
+ if (!modelSupportsImages(models, modelId)) {
305
+ throw invalidParams(`model ${JSON.stringify(modelId)} does not support image input`);
306
+ }
307
+ images.push(decodePromptImage(block));
308
+ break;
309
+ case "audio":
310
+ throw invalidParams("audio prompt content is not supported");
311
+ case "resource":
312
+ throw invalidParams("embedded resource prompt content is not supported");
313
+ default:
314
+ throw invalidParams("unsupported ACP prompt content");
315
+ }
316
+ }
317
+ let refs = [];
318
+ if (images.length > 0) {
319
+ if (!attachments)
320
+ throw internalError("no Harness attachment store is mounted");
321
+ try {
322
+ refs = await attachments.saveImages(images);
323
+ } catch (error) {
324
+ if (isImageAdmissionError(error))
325
+ throw invalidParams(error.message);
326
+ throw internalError("unable to persist the prompt image batch");
327
+ }
328
+ }
329
+ const content = [];
330
+ let pendingText = "";
331
+ let imageIndex = 0;
332
+ const flushText = () => {
333
+ if (!pendingText)
334
+ return;
335
+ content.push({ type: "text", text: pendingText });
336
+ pendingText = "";
337
+ };
338
+ for (const block of prompt) {
339
+ if (block.type === "text") {
340
+ pendingText += block.text;
341
+ } else if (block.type === "resource_link") {
342
+ pendingText += `
243
343
  [resource_link name=${JSON.stringify(block.name)} uri=${JSON.stringify(block.uri)}]
244
- `
245
- ];
344
+ `;
345
+ } else if (block.type === "image") {
346
+ flushText();
347
+ const attachment = refs[imageIndex++];
348
+ if (!attachment)
349
+ throw internalError("the attachment store returned an incomplete image batch");
350
+ content.push({ type: "image", attachment });
246
351
  }
247
- return [];
248
- }).join("");
249
- }
250
- function promptHasUnsupportedContent(prompt) {
251
- return prompt.some((block) => block.type !== "text" && block.type !== "resource_link");
352
+ }
353
+ flushText();
354
+ if (!content.some((block) => block.type === "image" || block.type === "text" && block.text.trim().length > 0)) {
355
+ throw invalidParams("empty prompt");
356
+ }
357
+ return content;
252
358
  }
253
- function createUserMessage(text) {
359
+ function createUserMessage(id, content) {
254
360
  return Object.freeze({
255
- id: randomUUID(),
361
+ id,
256
362
  role: "user",
257
- content: [Object.freeze({ type: "text", text })],
363
+ content: Object.freeze(content.map((block) => Object.freeze(block))),
258
364
  source: Object.freeze({ kind: "user" })
259
365
  });
260
366
  }
@@ -482,7 +588,7 @@ function apply(ctx, rawConfig) {
482
588
  throw invalidParams(`failed to select agent preset ${JSON.stringify(value)}: ${errorChain(error)}`);
483
589
  });
484
590
  } else if (params.configId === MODEL_CONFIG_ID) {
485
- assertAllowed(value, MODEL_IDS, "model");
591
+ assertAllowed(value, new Set(record.models.map((model) => model.id)), "model");
486
592
  record.selection.current = { ...record.selection.current, model: value };
487
593
  } else if (params.configId === REASONING_EFFORT_CONFIG_ID) {
488
594
  assertAllowed(value, REASONING_EFFORT_IDS, "reasoning effort");
@@ -498,17 +604,22 @@ function apply(ctx, rawConfig) {
498
604
  const makeAgent = (connection) => {
499
605
  conn = connection;
500
606
  return {
501
- initialize(_params) {
502
- return Promise.resolve({
607
+ async initialize(_params) {
608
+ const models = await loadHarnessModels(ctx, config.provider);
609
+ return {
503
610
  protocolVersion: PROTOCOL_VERSION,
504
611
  agentInfo: { name: "acp-extension-dsh", version: ACP_EXTENSION_DSH_VERSION },
505
612
  agentCapabilities: {
506
- promptCapabilities: { image: false, audio: false, embeddedContext: false },
613
+ promptCapabilities: {
614
+ image: models.some((model) => modelSupportsImages(models, model.id)),
615
+ audio: false,
616
+ embeddedContext: false
617
+ },
507
618
  mcpCapabilities: { http: true },
508
619
  sessionCapabilities: { close: {} }
509
620
  },
510
621
  authMethods: []
511
- });
622
+ };
512
623
  },
513
624
  authenticate(_params) {
514
625
  return Promise.resolve();
@@ -516,6 +627,12 @@ function apply(ctx, rawConfig) {
516
627
  async newSession(params) {
517
628
  assertOpen();
518
629
  validateSessionParams(params);
630
+ let models;
631
+ try {
632
+ models = await loadHarnessModels(ctx, config.provider);
633
+ } catch (error) {
634
+ throw internalError(`failed to list models: ${errorChain(error)}`);
635
+ }
519
636
  const sessionId = randomUUID();
520
637
  const selection = {
521
638
  current: {
@@ -575,13 +692,22 @@ function apply(ctx, rawConfig) {
575
692
  permissionMode,
576
693
  agentPreset: mountedPreset,
577
694
  agentPresetOptions,
695
+ models,
578
696
  started: false
579
697
  };
580
698
  sessions.set(sessionId, record);
581
699
  return {
582
700
  sessionId,
583
701
  modes: modeState(record),
584
- configOptions: configOptions(record)
702
+ configOptions: configOptions(record),
703
+ models: {
704
+ currentModelId: record.selection.current.model,
705
+ availableModels: record.models.map((model) => ({
706
+ modelId: model.id,
707
+ name: model.name ?? model.id,
708
+ description: model.description ?? null
709
+ }))
710
+ }
585
711
  };
586
712
  },
587
713
  setSessionMode(params) {
@@ -596,20 +722,29 @@ function apply(ctx, rawConfig) {
596
722
  const record = requireSession(params.sessionId);
597
723
  if (record.inflight)
598
724
  throw invalidParams("a prompt is already in flight for this session");
599
- if (promptHasUnsupportedContent(params.prompt)) {
600
- throw invalidParams("only text and resource_link prompt content is supported");
601
- }
602
- const text = acpPromptToText(params.prompt);
603
- if (!text.trim())
604
- throw invalidParams("empty prompt");
605
725
  if (ctx.agents.get(record.agent.id) !== record.agent) {
606
726
  throw internalError("prompt was not queued: the agent was disposed outside the bridge");
607
727
  }
608
- const message = createUserMessage(text);
609
- record.started = true;
610
- const stopReason = await new Promise((resolve, reject) => {
611
- const inflight = { resolve, reject, messageId: message.id };
612
- record.inflight = inflight;
728
+ const attachments = ctx.get("attachments");
729
+ const messageId = randomUUID();
730
+ let resolvePrompt;
731
+ let rejectPrompt;
732
+ const completion = new Promise((resolve, reject) => {
733
+ resolvePrompt = resolve;
734
+ rejectPrompt = reject;
735
+ });
736
+ const inflight = {
737
+ resolve: resolvePrompt,
738
+ reject: rejectPrompt,
739
+ messageId
740
+ };
741
+ record.inflight = inflight;
742
+ try {
743
+ const content = await admitAcpPrompt(params.prompt, record.models, record.selection.current.model, attachments);
744
+ if (record.inflight !== inflight)
745
+ return { stopReason: await completion };
746
+ const message = createUserMessage(messageId, content);
747
+ record.started = true;
613
748
  try {
614
749
  record.agent.followup(message);
615
750
  } catch (error) {
@@ -624,8 +759,12 @@ function apply(ctx, rawConfig) {
624
759
  const end = inflight.endReason;
625
760
  inflight.resolve(end ? end.kind === "max-tokens" ? "end_turn" : turnEndToStopReason(end) : "cancelled");
626
761
  });
627
- });
628
- return { stopReason };
762
+ } catch (error) {
763
+ if (record.inflight === inflight)
764
+ record.inflight = void 0;
765
+ throw error;
766
+ }
767
+ return { stopReason: await completion };
629
768
  },
630
769
  cancel(params) {
631
770
  const record = sessions.get(params.sessionId);
@@ -198,9 +198,10 @@
198
198
  toolName: subagent_fork
199
199
  backgroundMode: continuable
200
200
 
201
- # Production dsh does not install these optional providers. An opting-in
202
- # Profile mounts each provider once on the host plane; copy this preset,
203
- # then remove `disabled` from the matching tool row.
201
+ # Production dsh does not install these optional providers. Install the
202
+ # matching Bundle in this Profile and restart the Host, then copy this
203
+ # preset and remove `disabled` from the matching tool row. Host availability
204
+ # alone grants no tool.
204
205
  - id: tool-subagent-codex
205
206
  name: '@deepseek-ai/dsh-tool-subagent'
206
207
  disabled: true
@@ -185,9 +185,10 @@
185
185
  toolName: subagent_fork
186
186
  backgroundMode: continuable
187
187
 
188
- # Production dsh does not install these optional providers. An opting-in
189
- # Profile mounts each provider once on the host plane; copy this preset,
190
- # then remove `disabled` from the matching tool row.
188
+ # Production dsh does not install these optional providers. Install the
189
+ # matching Bundle in this Profile and restart the Host, then copy this
190
+ # preset and remove `disabled` from the matching tool row. Host availability
191
+ # alone grants no tool.
191
192
  - id: tool-subagent-codex
192
193
  name: '@deepseek-ai/dsh-tool-subagent'
193
194
  disabled: true
@@ -123,7 +123,16 @@ After a clean mount-validation, ask the user to start a session on the new prese
123
123
 
124
124
  ## Native product subagents
125
125
 
126
- Codex and Claude Code providers belong on the host plane but are not installed by production `dsh`. The active Profile must install and mount the selected provider before a preset can expose its ordinary delegation-tool row; never move a product provider into the preset and never add a product-specific settings field.
126
+ Codex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers:
127
+
128
+ ```sh
129
+ dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
130
+ dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
131
+ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
132
+ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
133
+ ```
134
+
135
+ Each Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start.
127
136
 
128
137
  Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:
129
138
 
@@ -147,7 +156,9 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
147
156
  maxDepth: provider-managed
148
157
  ```
149
158
 
150
- The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install or mount either optional provider: before enabling a row, the Profile must install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` package and mount it once on the host plane. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. The host must also provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
159
+ For additional named Codex or Claude Code instances, mount a separate host-plane provider row for each instance with a unique `providerName`, then add a separate preset tool row whose `provider` exactly matches that name and whose `toolName` is also unique. Keep the shipped rows for the default `codex` and `claude-code` names; do not reuse one tool row for several providers or derive either name from permission or environment settings.
160
+
161
+ The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install either optional provider: before enabling a row, install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` Bundle in the Profile and restart it. Each Bundle registers its dormant default provider and exclusively uses its pinned package-local platform CLI; additional named instances use extra host-plane rows from the same installed package. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Installing a Bundle or composing a preset row does not start a product, authenticate an account, select a model, probe credentials, or manage native product settings.
151
162
 
152
163
  ## What not to move into a preset
153
164
 
@@ -3,7 +3,8 @@
3
3
  # The persona is the complete system prompt, so global identity, Web orientation,
4
4
  # tool guidance, and later assembly listeners cannot add prompt text. Runtime
5
5
  # context snapshots are suppressed for this preset, and the model composes only
6
- # persistent `bash` and `str_replace_editor`. Context compaction is absent.
6
+ # the persistent shell (`bash` on POSIX, `pwsh` on win32) and
7
+ # `str_replace_editor`. Context compaction is absent.
7
8
 
8
9
  - id: persona
9
10
  name: '@deepseek-ai/dsh-persona'
@@ -15,6 +16,8 @@
15
16
  # The PTY registry is an agent-owned service, so it lives in an entry-local
16
17
  # realm. The backend still consumes the host sandbox policy and subprocess
17
18
  # implementation, while the tool registers into this agent's scoped catalog.
19
+ # Exactly one shell stack mounts per host: the bash stack gates off win32 and
20
+ # its pwsh twin gates off POSIX, mirroring the one-shot shell rows.
18
21
  - id: persistent-shell
19
22
  name: cordis:group
20
23
  group: true
@@ -26,11 +29,13 @@
26
29
 
27
30
  - id: terminal-bash
28
31
  name: '@deepseek-ai/dsh-terminal-bash'
32
+ disabled: !!js process.platform === 'win32'
29
33
  config:
30
34
  timeoutMs: 300000
31
35
 
32
36
  - id: persistent-bash
33
37
  name: '@deepseek-ai/dsh-tool-bash-persistent'
38
+ disabled: !!js process.platform === 'win32'
34
39
  config:
35
40
  timeoutMs: 300000
36
41
  description: |-
@@ -43,6 +48,27 @@
43
48
  * Please avoid commands that may produce a very large amount of output.
44
49
  * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
45
50
 
51
+ - id: terminal-pwsh
52
+ name: '@deepseek-ai/dsh-terminal-bash'
53
+ disabled: !!js process.platform !== 'win32'
54
+ config:
55
+ shellDialect: pwsh
56
+ timeoutMs: 300000
57
+
58
+ - id: persistent-pwsh
59
+ name: '@deepseek-ai/dsh-tool-pwsh-persistent'
60
+ disabled: !!js process.platform !== 'win32'
61
+ config:
62
+ timeoutMs: 300000
63
+ description: |-
64
+ Run commands in a PowerShell shell
65
+ * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
66
+ * You don't have access to the internet via this tool.
67
+ * State is persistent across command calls and discussions with the user.
68
+ * Use native Windows paths (C:\...) and $env:NAME variables; this is PowerShell, not bash.
69
+ * Please avoid commands that may produce a very large amount of output.
70
+ * Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.
71
+
46
72
  # The bare local filesystem shadows the host's sandboxed provider only for this
47
73
  # preset. The editor shares that realm and requires absolute paths.
48
74
  - id: filesystem
@@ -197,9 +197,10 @@
197
197
  toolName: subagent_fork
198
198
  backgroundMode: continuable
199
199
 
200
- # Production dsh does not install these optional providers. An opting-in
201
- # Profile mounts each provider once on the host plane; copy this preset,
202
- # then remove `disabled` from the matching tool row.
200
+ # Production dsh does not install these optional providers. Install the
201
+ # matching Bundle in this Profile and restart the Host, then copy this
202
+ # preset and remove `disabled` from the matching tool row. Host availability
203
+ # alone grants no tool.
203
204
  - id: tool-subagent-codex
204
205
  name: '@deepseek-ai/dsh-tool-subagent'
205
206
  disabled: true
package/dist/grok-acp.js CHANGED
@@ -200,6 +200,45 @@ function permissionNotification(clientIdentifier, mode) {
200
200
  if (!mapped) return void 0;
201
201
  return extensionNotification("permissionNotification", { clientIdentifier, ...mapped });
202
202
  }
203
+ function stripLodySessionConfig(meta) {
204
+ if (!meta || typeof meta !== "object") return meta;
205
+ const lody = meta.lody;
206
+ if (!lody || typeof lody !== "object" || !Object.hasOwn(lody, "sessionConfig")) return meta;
207
+ const { sessionConfig: _sessionConfig, ...remainingLody } = lody;
208
+ const { lody: _lody, ...remainingMeta } = meta;
209
+ return Object.keys(remainingLody).length > 0 ? { ...remainingMeta, lody: remainingLody } : remainingMeta;
210
+ }
211
+ function readLodySessionConfigOption(meta, configId) {
212
+ if (!meta || typeof meta !== "object") return void 0;
213
+ const sessionConfig = meta.lody?.sessionConfig;
214
+ if (!sessionConfig || typeof sessionConfig !== "object" || sessionConfig.version !== 1 || !sessionConfig.configOptionValues || typeof sessionConfig.configOptionValues !== "object") {
215
+ return void 0;
216
+ }
217
+ const value = sessionConfig.configOptionValues[configId];
218
+ return typeof value === "string" || typeof value === "boolean" ? value : void 0;
219
+ }
220
+ function translateSessionStart(message) {
221
+ const params = message.params ?? {};
222
+ const permissionMode = readLodySessionConfigOption(params._meta, "permission_mode");
223
+ const mapped = typeof permissionMode === "string" ? PERMISSION_MODES[permissionMode] : void 0;
224
+ if (!mapped) return { message, permissionMode: void 0, notification: void 0 };
225
+ const clientIdentifier = params._meta?.clientIdentifier;
226
+ const translated = {
227
+ ...message,
228
+ params: {
229
+ ...params,
230
+ _meta: {
231
+ ...stripLodySessionConfig(params._meta),
232
+ yoloMode: mapped.yolo_mode
233
+ }
234
+ }
235
+ };
236
+ return {
237
+ message: translated,
238
+ permissionMode,
239
+ notification: mapped.auto_mode && typeof clientIdentifier === "string" ? permissionNotification(clientIdentifier, permissionMode) : void 0
240
+ };
241
+ }
203
242
  class GrokAcpCompatibilityProxy {
204
243
  constructor() {
205
244
  this.sessions = /* @__PURE__ */ new Map();
@@ -236,8 +275,12 @@ class GrokAcpCompatibilityProxy {
236
275
  handleClient(message) {
237
276
  if (!message || typeof message !== "object") return { toRuntime: [message], toClient: [] };
238
277
  const params = message.params ?? {};
239
- if (message.method === "session/new" || message.method === "session/load" || message.method === "session/resume") {
278
+ if (message.method === "session/new" || message.method === "session/load" || message.method === "session/resume" || message.method === "session/fork") {
240
279
  const clientIdentifier = params._meta?.clientIdentifier;
280
+ const translated2 = translateSessionStart(message);
281
+ if (translated2.permissionMode && typeof clientIdentifier === "string") {
282
+ this.permissionModes.set(clientIdentifier, translated2.permissionMode);
283
+ }
241
284
  if (message.id !== void 0) {
242
285
  this.pending.set(message.id, {
243
286
  kind: "session",
@@ -246,7 +289,10 @@ class GrokAcpCompatibilityProxy {
246
289
  clientIdentifier
247
290
  });
248
291
  }
249
- return { toRuntime: [message], toClient: [] };
292
+ return {
293
+ toRuntime: translated2.notification ? [translated2.notification, translated2.message] : [translated2.message],
294
+ toClient: []
295
+ };
250
296
  }
251
297
  if (message.method === "session/prompt") {
252
298
  if (message.id !== void 0) {