@dashup/connector 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -34,10 +34,10 @@ Each history read stages a new encrypted database snapshot and publishes it only
34
34
 
35
35
  Dashup discovers existing local OpenCode listeners, Codex control sockets, and VS Code Agent Host endpoints. These connections remain local; their tokens are never sent to Parcae or the phone. Native endpoints are checked for local ownership or loopback access, and redirects and remote endpoint URLs are rejected.
36
36
 
37
- - **OpenCode:** the existing TUI/server supplies conversations, live events, prompts, native tool permissions, and supported single-choice questions. A reply targets the original session ID and preserves its latest native agent, model, and variant. For an existing session, Dashup does not run `opencode serve` or create another one.
37
+ - **OpenCode:** the existing TUI/server supplies conversations, live events, prompts, native tool permissions, and questions, which the phone answers or skips. A reply targets the original session ID and preserves its latest native agent, model, and variant. For an existing session, Dashup does not run `opencode serve` or create another one.
38
38
  - **Codex:** the existing control socket supplies already-loaded threads. Replies require a recognized native server and a thread that is still loaded and idle immediately before sending. For an existing thread, Dashup never calls `thread/start`, `thread/resume`, or `session/load`. Approval controls appear only for native requests routed to this connection; approvals owned by another client may remain available only in the original UI.
39
39
  - **VS Code:** Microsoft's official AHP client subscribes to existing host sessions and chats. Native choices are retained, and delivery is acknowledged by the host. Older extension-host chats without an accessible Agent Host remain saved history. AHP chat IDs are namespaced rather than guessed to equal CLI thread IDs. Non-file remote AHP workspace URIs are not presented as local execution environments.
40
- - **Claude Code:** existing transcripts are readable without starting Claude. `dashup install` adds Dashup's plugin to Claude Code (`dashup@dashup`, read in place from this package's `claude/` folder), and `dashup uninstall` removes it. A session that starts with the plugin holds a stream open to `~/.dashup/claude.sock` through `curl`, and takes each reply as the person's own words; a reply that starts with `/` runs that command. Claude Code's inbox socket is not used, because Claude presents anything posted there as another session's message, without the person's authority. Sessions opened before the plugin was installed, and sessions on Windows, stay read-only.
40
+ - **Claude Code:** existing transcripts are readable without starting Claude. `dashup install` adds Dashup's plugin to Claude Code (`dashup@dashup`, read in place from this package's `claude/` folder), and `dashup uninstall` removes it. A session that starts with the plugin holds a stream open to `~/.dashup/claude.sock` through `curl`, and takes each reply as the person's own words; a reply that starts with `/` runs that command. The plugin also puts each permission request the session waits on to the phone: a tool to allow once or under the rule Claude Code suggests, questions, or a plan. The dialog on the computer, or the editor hosting the session, stays open, and the first answer wins. Claude Code lets only the person there or its host approve a plan, so the phone can only send a running session's plan back with what to change. Claude Code's inbox socket is not used, because Claude presents anything posted there as another session's message, without the person's authority. Sessions opened before the plugin was installed, and sessions on Windows, stay read-only.
41
41
 
42
42
  Native listener discovery supports the host's available process/socket interfaces. File and socket formats are versioned implementation boundaries; unavailable or unfamiliar interfaces leave history readable and controls disabled. Existing editor and agent processes continue independently when the Dashup connector disconnects. The PC's saved SSH relationship is retained; reading it does not grant access to a remote machine's CLI store.
43
43
 
@@ -47,7 +47,7 @@ Only an explicit Queue reply starts a temporary official app-server helper to in
47
47
 
48
48
  Delivery requires that exact typed client-message ID in the original thread's native history database. A response receipt requires a later native assistant item in the same turn; it does not mean the agent finished. Receipts remain encrypted through the ordinary Dashup relay. History is read on request; the phone asks again while a thread is open and its session changes. Native approvals owned by the private-stdio process remain unsupported and require its original UI. Queue text never substitutes for a native approval.
49
49
 
50
- A running Claude Code session shows the model its newest reply ran on and, where that model takes them, fast mode and ultracode, as its transcript records them. The choices and their names are the models `claude` offers hosts in its initialize answer. The phone changes them through the plugin with Claude Code's own `/model`, `/fast` and `/effort ultracode` commands, and in a session the phone started with control requests at the start of each turn. Claude Code records a change when the next turn starts, so it shows from the next reply. A change expires with other commands after 60 seconds.
50
+ A running Claude Code session shows the model its newest reply ran on and, where that model takes them, fast mode and ultracode, as its transcript records them. The choices and their names are the models the session's own Claude Code binary offers hosts in its initialize answer, or `claude`'s where the system does not name the binary. The phone changes them through the plugin with Claude Code's own `/model`, `/fast` and `/effort ultracode` commands, and in a session the phone started with control requests at the start of each turn. Claude Code records a change when the next turn starts, so it shows from the next reply. A change expires with other commands after 60 seconds.
51
51
 
52
52
  Stop interrupts the running turn through each live interface: OpenCode's session abort, Codex `turn/interrupt`, the AHP client's turn cancellation, ACP `session/cancel`, and the Claude Code plugin's turn abort. It refuses when nothing is running and expires with other commands after 60 seconds, so a late stop cannot end a newer turn.
53
53
 
@@ -55,7 +55,7 @@ Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the la
55
55
 
56
56
  ## Starting sessions from the phone
57
57
 
58
- The phone starts a session in a local project, or in a new folder inside one of the folders that already hold this computer's projects, with Claude Code, Codex or OpenCode: whichever of `claude`, `codex` and `opencode` runs here. The agent works with its own settings and permission rules, and asks the phone before anything they don't allow. Claude Code runs headless (`claude -p`) for each turn and resumes the session for the next; Codex threads start in Codex's own app-server daemon (`codex app-server daemon start`) and outlive the connector; OpenCode sessions run on an `opencode serve` the connector starts for that folder. Claude Code's permission requests arrive with the command, or the diff an edit or write would make.
58
+ The phone starts a session in a local project, or in a new folder inside one of the folders that already hold this computer's projects, with Claude Code, Codex or OpenCode: whichever of `claude`, `codex` and `opencode` runs here. The agent works with its own settings and permission rules, and asks the phone before anything they don't allow. Claude Code runs headless (`claude -p`) for each turn and resumes the session for the next; Codex threads start in Codex's own app-server daemon (`codex app-server daemon start`) and outlive the connector; OpenCode sessions run on an `opencode serve` the connector starts for that folder. Claude Code's permission requests arrive with the command, or the diff an edit or write would make, and the rule that would stop it asking again; the phone answers its questions and plans as Claude Code's host.
59
59
 
60
60
  A saved conversation is answered the same way when its agent runs here, its project is local, and nothing else has it open: Claude Code resumes it (`claude -p --resume`), Codex loads it in its daemon (`thread/resume`), and OpenCode serves its folder. One a native Codex process owns takes queued messages instead. A start expires with other commands after 60 seconds.
61
61
 
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "name": "dashup",
3
- "description": "Takes what you send this session from the Dashup app as your own words"
3
+ "description": "Takes what you send this session from the Dashup app as your own words, and lets you answer its approvals, questions and plans there"
4
4
  }
@@ -13,9 +13,85 @@ type Delivery = { text?: string; now?: true };
13
13
 
14
14
  const RETRY_MS = 10_000;
15
15
 
16
+ // =============================================================================
17
+ // Permission requests
18
+ // =============================================================================
19
+
20
+ // The connector holds each request this session waits on open until the phone answers it. The
21
+ // dialog here, the editor hosting the session or the person's own hooks may answer first. No hook
22
+ // hears the dialog's answer, so the call it settles closes the request: both hooks see the call,
23
+ // with its fields in different orders.
24
+ function call(agent: string | undefined, tool: string, input: unknown): string {
25
+ const sorted = (value: unknown): unknown =>
26
+ Array.isArray(value)
27
+ ? value.map(sorted)
28
+ : value && typeof value === 'object'
29
+ ? Object.fromEntries(
30
+ Object.keys(value)
31
+ .sort()
32
+ .map((key) => [key, sorted((value as Record<string, unknown>)[key])]),
33
+ )
34
+ : value;
35
+ return JSON.stringify([agent ?? '', tool, sorted(input)]);
36
+ }
37
+
16
38
  export const register: Register = (on) => {
17
39
  let turnId: string | undefined;
18
40
  let stream: AsyncGenerator<unknown, unknown> | undefined;
41
+ // The requests the phone is asked, by the call each waits on.
42
+ const asks = new Map<string, AsyncGenerator<unknown, unknown>>();
43
+ on('classic.PermissionRequest', async ($, e, next) => {
44
+ const key = call(e.agent_id, e.tool_name, e.tool_input);
45
+ const ask = $.process.spawn({
46
+ argv: [
47
+ 'curl',
48
+ '-sfN',
49
+ '--unix-socket',
50
+ `${await $.env.get('HOME')}/.dashup/claude.sock`,
51
+ '--data-binary',
52
+ '@-',
53
+ `http://dashup/sessions/${await $.session.id()}/prompts`,
54
+ ],
55
+ input: JSON.stringify({
56
+ tool : e.tool_name,
57
+ input : e.tool_input,
58
+ suggestions : e.permission_suggestions,
59
+ }),
60
+ });
61
+ const open: AsyncGenerator<unknown, unknown> = ask;
62
+ asks.set(key, open);
63
+ const beneath = next(e).then((result) => {
64
+ if (result.decision) void open.return(undefined);
65
+ return result;
66
+ });
67
+ let answer = '';
68
+ try {
69
+ for await (const { stream: name, text } of ask) if (name === 'stdout') answer += text;
70
+ } catch {
71
+ // curl is missing.
72
+ } finally {
73
+ if (asks.get(key) === open) asks.delete(key);
74
+ }
75
+ try {
76
+ return answer ? { decision: JSON.parse(answer) } : beneath;
77
+ } catch {
78
+ // The connector stopped mid-answer.
79
+ return beneath;
80
+ }
81
+ });
82
+ on('tool.call', async ($, e, next) => {
83
+ try {
84
+ return await next(e);
85
+ } finally {
86
+ if (asks.size) {
87
+ const { tool, tool_use_id, consent, agentId, requestMeta, ...input } = e as Record<
88
+ string,
89
+ unknown
90
+ >;
91
+ void asks.get(call(agentId as string | undefined, String(tool), input))?.return(undefined);
92
+ }
93
+ }
94
+ });
19
95
  on('turn.start', ($, e, next) => {
20
96
  if (e.agentId === undefined) turnId = e.turnId;
21
97
  return next(e);
@@ -23,6 +99,7 @@ export const register: Register = (on) => {
23
99
  // `/clear` starts another conversation in this process, which streams under its own id.
24
100
  on('session.end', ($, e, next) => {
25
101
  void stream?.return(undefined);
102
+ for (const ask of asks.values()) void ask.return(undefined);
26
103
  return next(e);
27
104
  });
28
105
  on('session.start', async ($, e, next) => {
package/dist/index.js CHANGED
@@ -3802,7 +3802,7 @@ function bridgeAcp(socket, onUpdate) {
3802
3802
  if (session && direction === "agent" && message.method === methods.client.session.requestPermission) {
3803
3803
  const tool = message.params.toolCall ?? {};
3804
3804
  const diff = (tool.content ?? []).find((part) => part.type === "diff");
3805
- const approval3 = {
3805
+ const approval2 = {
3806
3806
  id: randomUUID(),
3807
3807
  title: String(message.params.title ?? tool.title ?? "Agent needs approval"),
3808
3808
  detail: String(message.params.description ?? ""),
@@ -3820,10 +3820,10 @@ ${String(diff.newText).split("\n").map((line) => "+" + line).join("\n")}`
3820
3820
  }
3821
3821
  } : {}
3822
3822
  };
3823
- session.approvals.set(String(message.id), approval3);
3823
+ session.approvals.set(String(message.id), approval2);
3824
3824
  permissions.set(String(message.id), session);
3825
3825
  session.status = "attention";
3826
- publish(session, approval3);
3826
+ publish(session, approval2);
3827
3827
  }
3828
3828
  if (direction === "editor" && !message.method) {
3829
3829
  const pending = permissions.get(String(message.id));
@@ -4048,7 +4048,7 @@ import * as ui from "@clack/prompts";
4048
4048
  import { z } from "zod";
4049
4049
 
4050
4050
  // package.json
4051
- var version = "0.1.14";
4051
+ var version = "0.1.16";
4052
4052
 
4053
4053
  // src/pair.ts
4054
4054
  var Invitation = z.object({
@@ -4215,6 +4215,24 @@ var attached = (id, mime, data, name) => ({
4215
4215
  });
4216
4216
  var MAX_PREVIEW = 64 * 1024;
4217
4217
  var NO_PREVIEW = "Review the full request on your computer before approving.";
4218
+ function questionApproval(id, items, multiple) {
4219
+ const asked = items.map((item) => ({
4220
+ ...typeof item?.header === "string" ? { header: item.header } : {},
4221
+ question: String(item?.question ?? ""),
4222
+ options: (Array.isArray(item?.options) ? item.options : []).filter((option2) => typeof option2?.label === "string").map((option2) => ({
4223
+ label: String(option2.label),
4224
+ ...typeof option2.description === "string" ? { description: option2.description } : {}
4225
+ })),
4226
+ ...multiple(item) ? { multiple: true } : {}
4227
+ }));
4228
+ return {
4229
+ id,
4230
+ title: asked.length === 1 ? asked[0].question : `${asked.length} questions`,
4231
+ detail: "",
4232
+ choices: [{ id: "skip", label: "Skip" }],
4233
+ questions: asked
4234
+ };
4235
+ }
4218
4236
 
4219
4237
  // src/harness/history.ts
4220
4238
  var SAMPLE_BYTES = 128 * 1024;
@@ -13246,22 +13264,22 @@ var FrontendAdapter = class {
13246
13264
  if (options.subscribe === false) requestData.__subscribe = false;
13247
13265
  const result = await this.transport.get(path, requestData);
13248
13266
  const items = result?.[(0, import_pluralize.default)(modelClass.type)] ?? result?.[modelClass.type + "s"] ?? result?.items ?? [];
13249
- const models2 = items.map(
13267
+ const models = items.map(
13250
13268
  (row) => modelClass.hydrate(this, row)
13251
13269
  );
13252
13270
  if (result?.__queryHash) {
13253
- Object.defineProperty(models2, "__queryHash", {
13271
+ Object.defineProperty(models, "__queryHash", {
13254
13272
  value: result.__queryHash,
13255
13273
  enumerable: false
13256
13274
  });
13257
13275
  }
13258
13276
  if (typeof result?.totalCount === "number") {
13259
- Object.defineProperty(models2, "__totalCount", {
13277
+ Object.defineProperty(models, "__totalCount", {
13260
13278
  value: result.totalCount,
13261
13279
  enumerable: false
13262
13280
  });
13263
13281
  }
13264
- return models2;
13282
+ return models;
13265
13283
  };
13266
13284
  chain.first = async () => {
13267
13285
  const all = await chain.find();
@@ -16728,13 +16746,14 @@ var Client = class {
16728
16746
  });
16729
16747
  this.changed();
16730
16748
  }) : void 0,
16731
- approve: approvals.length ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16732
- const approval3 = this.approvals.get(approvalId);
16749
+ approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
16750
+ const approval2 = this.approvals.get(approvalId);
16751
+ const choice = "choice" in reply ? reply.choice : void 0;
16733
16752
  const socket = this.socket;
16734
- if (!approval3 || approval3.threadId !== id || !approval3.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16753
+ if (!approval2 || approval2.threadId !== id || !approval2.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16735
16754
  throw new Error("Codex approval is no longer pending");
16736
16755
  await new Promise((resolve3, reject) => socket.send(
16737
- JSON.stringify({ id: approval3.requestId, result: { decision: choice } }),
16756
+ JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
16738
16757
  (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
16739
16758
  ));
16740
16759
  this.approvals.delete(approvalId);
@@ -16810,18 +16829,18 @@ var Codex = class {
16810
16829
  };
16811
16830
 
16812
16831
  // src/harness/headless.ts
16813
- import { randomUUID as randomUUID3 } from "node:crypto";
16814
- import { readFileSync as readFileSync3 } from "node:fs";
16832
+ import { randomUUID as randomUUID4 } from "node:crypto";
16815
16833
  import { createInterface as createInterface3 } from "node:readline";
16816
16834
  import { spawn as spawn3 } from "node:child_process";
16817
16835
 
16818
16836
  // src/harness/claude.ts
16819
16837
  import { homedir as homedir5 } from "node:os";
16820
- import { dirname as dirname4, join as join5 } from "node:path";
16838
+ import { randomUUID as randomUUID3 } from "node:crypto";
16839
+ import { dirname as dirname4, join as join5, basename as basename2 } from "node:path";
16821
16840
  import { spawn as spawn2 } from "node:child_process";
16822
16841
  import { createInterface as createInterface2 } from "node:readline";
16823
- import { watch as watch2 } from "node:fs";
16824
- import { mkdir, readdir as readdir2, readFile as readFile2, rm } from "node:fs/promises";
16842
+ import { watch as watch2, readFileSync as readFileSync3 } from "node:fs";
16843
+ import { mkdir, readdir as readdir2, readFile as readFile2, readlink, rm } from "node:fs/promises";
16825
16844
  import { createServer as createServer2 } from "node:http";
16826
16845
  var MODELS_MS = 3e4;
16827
16846
  var COMMANDS = {
@@ -16829,32 +16848,48 @@ var COMMANDS = {
16829
16848
  fast: (value2) => `/fast ${value2 ? "on" : "off"}`,
16830
16849
  ultracode: (value2) => `/effort ultracode ${value2 ? "on" : "off"}`
16831
16850
  };
16832
- var models = [];
16833
- function askModels() {
16851
+ var offered = /* @__PURE__ */ new Map();
16852
+ var asking = /* @__PURE__ */ new Set();
16853
+ var forgetModels = () => offered.clear();
16854
+ function claudeModels(changed, command2 = "claude", version2 = "") {
16855
+ const key = `${command2}\0${version2}`;
16856
+ if (!offered.has(key) && !asking.has(key)) {
16857
+ asking.add(key);
16858
+ void listModels(command2).then((models) => {
16859
+ asking.delete(key);
16860
+ if (!models.length) return;
16861
+ offered.set(key, models);
16862
+ changed();
16863
+ });
16864
+ }
16865
+ return offered.get(key) ?? [];
16866
+ }
16867
+ function listModels(command2) {
16834
16868
  return new Promise((resolve3) => {
16835
16869
  const child = spawn2(
16836
- "claude",
16870
+ command2,
16837
16871
  ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
16838
16872
  { cwd: homedir5(), stdio: "pipe", shell: process.platform === "win32" }
16839
16873
  );
16840
16874
  const timer = setTimeout(() => child.kill(), MODELS_MS);
16841
16875
  child.stdin.on("error", () => {
16842
16876
  });
16843
- child.once("error", () => resolve3());
16877
+ child.once("error", () => resolve3([]));
16844
16878
  child.once("exit", () => {
16845
16879
  clearTimeout(timer);
16846
- resolve3();
16880
+ resolve3([]);
16847
16881
  });
16848
16882
  createInterface2({ input: child.stdout }).on("line", (line) => {
16849
16883
  try {
16850
16884
  const message = JSON.parse(line);
16851
16885
  if (message?.type !== "control_response" || message.response?.request_id !== "models")
16852
16886
  return;
16853
- const offered = message.response.response?.models;
16854
- if (Array.isArray(offered))
16855
- models = offered.filter(
16887
+ const models = message.response.response?.models;
16888
+ resolve3(
16889
+ (Array.isArray(models) ? models : []).filter(
16856
16890
  (model) => typeof model?.value === "string" && typeof model.displayName === "string"
16857
- );
16891
+ )
16892
+ );
16858
16893
  child.stdin.end();
16859
16894
  } catch {
16860
16895
  }
@@ -16865,7 +16900,7 @@ function askModels() {
16865
16900
  );
16866
16901
  });
16867
16902
  }
16868
- function claudeOptions(recorded) {
16903
+ function claudeOptions(models, recorded) {
16869
16904
  const ran = typeof recorded.model === "string" ? recorded.model : "";
16870
16905
  const current = models.find((model) => model.value !== "default" && model.resolvedModel === ran) ?? models.find((model) => model.value === ran);
16871
16906
  const choices = models.map((model) => ({ value: model.value, name: model.displayName }));
@@ -16877,9 +16912,148 @@ function claudeOptions(recorded) {
16877
16912
  ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16878
16913
  ];
16879
16914
  }
16915
+ var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16916
+ var SCOPE = 32;
16917
+ var preview2 = (text3) => Buffer.byteLength(text3) > MAX_PREVIEW ? Buffer.from(text3).subarray(0, MAX_PREVIEW).toString("utf8") : text3;
16918
+ function hunk(before, after, offset = 0) {
16919
+ const old = before ? before.replace(/\n$/, "").split("\n") : [];
16920
+ const next = after ? after.replace(/\n$/, "").split("\n") : [];
16921
+ let start2 = 0;
16922
+ while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
16923
+ let end = 0;
16924
+ while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
16925
+ end++;
16926
+ const removed = old.slice(start2, old.length - end);
16927
+ const added2 = next.slice(start2, next.length - end);
16928
+ return {
16929
+ lines: [
16930
+ `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
16931
+ ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
16932
+ ...removed.map((line) => `-${line}`),
16933
+ ...added2.map((line) => `+${line}`),
16934
+ ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
16935
+ ],
16936
+ added: added2.length,
16937
+ deleted: removed.length
16938
+ };
16939
+ }
16940
+ function change(tool, input) {
16941
+ const path = input.file_path;
16942
+ if (typeof path !== "string") return void 0;
16943
+ let current = "";
16944
+ try {
16945
+ current = readFileSync3(path, "utf8");
16946
+ } catch {
16947
+ }
16948
+ const edit = (value2) => {
16949
+ const before = String(value2?.old_string ?? "");
16950
+ const at = before ? current.indexOf(before) : -1;
16951
+ return hunk(
16952
+ before,
16953
+ String(value2?.new_string ?? ""),
16954
+ at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
16955
+ );
16956
+ };
16957
+ const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
16958
+ if (!hunks.length) return void 0;
16959
+ const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
16960
+ if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
16961
+ return {
16962
+ path,
16963
+ diff,
16964
+ added: hunks.reduce((sum, item) => sum + item.added, 0),
16965
+ deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
16966
+ };
16967
+ }
16968
+ function remember(suggestions = []) {
16969
+ const updates = suggestions.filter(
16970
+ (update) => update?.type === "addRules" && update.behavior === "allow" || update?.type === "addDirectories"
16971
+ );
16972
+ const rule = updates.flatMap((update) => update.rules ?? [])[0];
16973
+ const folder = updates.flatMap((update) => update.directories ?? [])[0];
16974
+ const scope2 = rule ? String(rule.ruleContent ?? rule.toolName) : folder ? `access to ${basename2(String(folder))}` : "";
16975
+ if (scope2)
16976
+ return {
16977
+ label: `Always allow ${scope2.length > SCOPE ? `${scope2.slice(0, SCOPE - 1)}\u2026` : scope2}`,
16978
+ updates
16979
+ };
16980
+ const edits = suggestions.find(
16981
+ (update) => update?.type === "setMode" && update.mode === "acceptEdits"
16982
+ );
16983
+ return edits ? { label: "Allow all edits", updates: [edits] } : void 0;
16984
+ }
16985
+ function ask(id, request2, host) {
16986
+ const { tool, input } = request2;
16987
+ if (tool === "AskUserQuestion" && Array.isArray(input.questions))
16988
+ return questionApproval(id, input.questions, (item) => item.multiSelect === true);
16989
+ if (tool === "ExitPlanMode")
16990
+ return {
16991
+ id,
16992
+ title: "Approve this plan?",
16993
+ detail: host ? "" : "Approve it on your computer, or tell Claude what to change.",
16994
+ choices: [
16995
+ ...host ? [{ id: "accept", label: "Approve" }] : [],
16996
+ { id: "revise", label: "Keep planning", text: true }
16997
+ ],
16998
+ plan: preview2(String(input.plan ?? ""))
16999
+ };
17000
+ const diff = change(tool, input);
17001
+ const always = remember(request2.suggestions);
17002
+ return {
17003
+ id,
17004
+ title: tool === "Bash" ? "Allow command?" : EDITS.has(tool) ? "Allow file changes?" : `Allow ${request2.label ?? tool}?`,
17005
+ detail: preview2(
17006
+ [
17007
+ request2.description ?? (typeof input.description === "string" ? input.description : ""),
17008
+ typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
17009
+ ].filter(Boolean).join("\n\n")
17010
+ ),
17011
+ ...diff ? { change: diff } : {},
17012
+ choices: [
17013
+ { id: "accept", label: "Allow once" },
17014
+ ...always ? [{ id: "always", label: always.label }] : [],
17015
+ { id: "decline", label: "Reject" }
17016
+ ]
17017
+ };
17018
+ }
17019
+ function decide(request2, approval2, reply) {
17020
+ const input = request2.input;
17021
+ if ("answers" in reply)
17022
+ return {
17023
+ behavior: "allow",
17024
+ updatedInput: {
17025
+ ...input,
17026
+ answers: Object.fromEntries(
17027
+ (approval2.questions ?? []).map((item, index) => [
17028
+ item.question,
17029
+ reply.answers[index].join(", ")
17030
+ ])
17031
+ )
17032
+ }
17033
+ };
17034
+ if (reply.choice === "accept")
17035
+ return {
17036
+ behavior: "allow",
17037
+ updatedInput: input,
17038
+ ...approval2.plan === void 0 ? {} : { updatedPermissions: [{ type: "setMode", mode: "default", destination: "session" }] }
17039
+ };
17040
+ if (reply.choice === "always")
17041
+ return {
17042
+ behavior: "allow",
17043
+ updatedInput: input,
17044
+ updatedPermissions: remember(request2.suggestions)?.updates ?? []
17045
+ };
17046
+ if (reply.choice === "revise")
17047
+ return { behavior: "deny", message: reply.text?.trim() || "Keep planning." };
17048
+ return {
17049
+ behavior: "deny",
17050
+ message: reply.choice === "skip" ? "The person skipped these questions." : "The person rejected this from their phone."
17051
+ };
17052
+ }
16880
17053
  var MAX_SESSIONS2 = 100;
16881
17054
  var MAX_REQUESTS = 1e3;
16882
- var SESSION = /^\/sessions\/([\w-]{1,100})$/;
17055
+ var MAX_PROMPT = 4 * 1024 * 1024;
17056
+ var SESSION = /^\/sessions\/([\w-]{1,100})(\/prompts)?$/;
16883
17057
  var STATUS = {
16884
17058
  busy: "working",
16885
17059
  idle: "idle",
@@ -16896,7 +17070,8 @@ async function registrations() {
16896
17070
  const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
16897
17071
  if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
16898
17072
  process.kill(value2.pid, 0);
16899
- return [value2];
17073
+ const binary2 = await readlink(`/proc/${value2.pid}/exe`).catch(() => "");
17074
+ return [{ ...value2, command: /[\\/]claude(?:\.exe)?$/.test(binary2) ? binary2 : "claude" }];
16900
17075
  } catch {
16901
17076
  return [];
16902
17077
  }
@@ -16915,23 +17090,32 @@ var Claude = class {
16915
17090
  notify;
16916
17091
  server;
16917
17092
  streams = /* @__PURE__ */ new Map();
17093
+ prompts = /* @__PURE__ */ new Map();
16918
17094
  requests = /* @__PURE__ */ new Map();
16919
17095
  async refresh() {
16920
17096
  this.watch();
16921
17097
  return (await registrations()).map((session) => {
16922
17098
  const plugin = this.streams.has(session.sessionId);
17099
+ const models = claudeModels(this.changed, session.command, session.version);
17100
+ const approvals = [...this.prompts.values()].filter((prompt) => prompt.session === session.sessionId).map((prompt) => prompt.approval);
16923
17101
  return {
16924
17102
  id: session.sessionId,
16925
17103
  harness: "claude",
16926
17104
  cwd: session.cwd,
16927
17105
  updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
16928
- status: STATUS[session.status ?? ""] ?? "unknown",
17106
+ status: approvals.length ? "attention" : STATUS[session.status ?? ""] ?? "unknown",
16929
17107
  messages: [],
16930
- approvals: [],
17108
+ approvals,
17109
+ approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17110
+ const prompt = this.prompts.get(approvalId);
17111
+ if (prompt?.session !== session.sessionId)
17112
+ throw new Error("Claude Code approval is no longer pending");
17113
+ prompt.answer(decide(prompt.request, prompt.approval, reply));
17114
+ }) : void 0,
16931
17115
  send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
16932
17116
  stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
16933
17117
  now: plugin,
16934
- options: claudeOptions,
17118
+ options: (recorded) => claudeOptions(models, recorded),
16935
17119
  configure: plugin ? (requestId, option2, value2) => this.once(requestId, () => this.deliver(session.sessionId, { text: COMMANDS[option2](value2) })) : void 0
16936
17120
  };
16937
17121
  });
@@ -16940,8 +17124,9 @@ var Claude = class {
16940
17124
  async listen() {
16941
17125
  if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16942
17126
  const server = this.server = createServer2((request2, response) => {
16943
- const id = request2.method === "GET" ? SESSION.exec(request2.url ?? "")?.[1] : void 0;
16944
- if (!id) return void response.writeHead(404).end();
17127
+ const [, id, prompt] = SESSION.exec(request2.url ?? "") ?? [];
17128
+ if (id && prompt && request2.method === "POST") return this.prompt(id, request2, response);
17129
+ if (!id || prompt || request2.method !== "GET") return void response.writeHead(404).end();
16945
17130
  this.streams.get(id)?.end();
16946
17131
  this.streams.set(id, response);
16947
17132
  response.once("close", () => {
@@ -16959,6 +17144,46 @@ var Claude = class {
16959
17144
  server.listen(this.socket, resolve3);
16960
17145
  });
16961
17146
  }
17147
+ // Asks the phone what the session's permission request asks the person.
17148
+ prompt(session, request2, response) {
17149
+ const chunks = [];
17150
+ let bytes = 0;
17151
+ request2.on("data", (chunk) => {
17152
+ bytes += chunk.length;
17153
+ if (bytes > MAX_PROMPT) request2.destroy();
17154
+ else chunks.push(chunk);
17155
+ });
17156
+ request2.once("end", () => {
17157
+ let asked;
17158
+ try {
17159
+ asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
17160
+ } catch {
17161
+ }
17162
+ if (typeof asked?.tool !== "string" || !asked.input || typeof asked.input !== "object")
17163
+ return void response.writeHead(400).end();
17164
+ const id = randomUUID3();
17165
+ const prompted = {
17166
+ tool: asked.tool,
17167
+ input: asked.input,
17168
+ ...Array.isArray(asked.suggestions) ? { suggestions: asked.suggestions } : {}
17169
+ };
17170
+ this.prompts.set(id, {
17171
+ session,
17172
+ request: prompted,
17173
+ approval: ask(id, prompted, false),
17174
+ answer: (decision) => {
17175
+ this.prompts.delete(id);
17176
+ response.end(JSON.stringify(decision));
17177
+ this.changed();
17178
+ }
17179
+ });
17180
+ response.once("close", () => {
17181
+ if (this.prompts.delete(id)) this.changed();
17182
+ });
17183
+ response.writeHead(200, { "Content-Type": "application/json" }).flushHeaders();
17184
+ this.changed();
17185
+ });
17186
+ }
16962
17187
  deliver(id, delivery) {
16963
17188
  const stream = this.streams.get(id);
16964
17189
  if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
@@ -16997,6 +17222,7 @@ var Claude = class {
16997
17222
  clearTimeout(this.notify);
16998
17223
  this.watcher?.close();
16999
17224
  this.watcher = void 0;
17225
+ this.prompts.clear();
17000
17226
  this.server?.closeAllConnections();
17001
17227
  this.server?.close();
17002
17228
  this.server = void 0;
@@ -17004,82 +17230,9 @@ var Claude = class {
17004
17230
  };
17005
17231
 
17006
17232
  // src/harness/headless.ts
17007
- var CHOICES = [
17008
- { id: "accept", label: "Allow once" },
17009
- { id: "decline", label: "Reject" }
17010
- ];
17011
- var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
17012
17233
  var MAX_REQUESTS2 = 1e3;
17013
17234
  var NOTIFY_MS = 100;
17014
17235
  var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
17015
- function hunk(before, after, offset = 0) {
17016
- const old = before ? before.replace(/\n$/, "").split("\n") : [];
17017
- const next = after ? after.replace(/\n$/, "").split("\n") : [];
17018
- let start2 = 0;
17019
- while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
17020
- let end = 0;
17021
- while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
17022
- end++;
17023
- const removed = old.slice(start2, old.length - end);
17024
- const added2 = next.slice(start2, next.length - end);
17025
- return {
17026
- lines: [
17027
- `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
17028
- ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
17029
- ...removed.map((line) => `-${line}`),
17030
- ...added2.map((line) => `+${line}`),
17031
- ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
17032
- ],
17033
- added: added2.length,
17034
- deleted: removed.length
17035
- };
17036
- }
17037
- function change(tool, input) {
17038
- const path = input.file_path;
17039
- if (typeof path !== "string") return void 0;
17040
- let current = "";
17041
- try {
17042
- current = readFileSync3(path, "utf8");
17043
- } catch {
17044
- }
17045
- const edit = (value2) => {
17046
- const before = String(value2?.old_string ?? "");
17047
- const at = before ? current.indexOf(before) : -1;
17048
- return hunk(
17049
- before,
17050
- String(value2?.new_string ?? ""),
17051
- at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
17052
- );
17053
- };
17054
- const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
17055
- if (!hunks.length) return void 0;
17056
- const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
17057
- if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
17058
- return {
17059
- path,
17060
- diff,
17061
- added: hunks.reduce((sum, item) => sum + item.added, 0),
17062
- deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
17063
- };
17064
- }
17065
- function approval2(id, request2) {
17066
- const input = request2.input ?? {};
17067
- const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
17068
- const diff = change(request2.tool_name, input);
17069
- let detail = [
17070
- typeof request2.description === "string" ? request2.description : "",
17071
- typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
17072
- ].filter(Boolean).join("\n\n");
17073
- if (Buffer.byteLength(detail) > MAX_PREVIEW)
17074
- detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17075
- return {
17076
- id,
17077
- title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
17078
- detail,
17079
- ...diff ? { change: diff } : {},
17080
- choices: CHOICES.map((choice) => ({ ...choice }))
17081
- };
17082
- }
17083
17236
  var Session = class {
17084
17237
  // A resumed session is `saved` and keeps the title its history gives.
17085
17238
  constructor(id, cwd, title2, changed, saved = false) {
@@ -17113,14 +17266,14 @@ var Session = class {
17113
17266
  interrupt() {
17114
17267
  this.write({
17115
17268
  type: "control_request",
17116
- request_id: randomUUID3(),
17269
+ request_id: randomUUID4(),
17117
17270
  request: { subtype: "interrupt" }
17118
17271
  });
17119
17272
  }
17120
17273
  apply(option2, value2) {
17121
17274
  this.write({
17122
17275
  type: "control_request",
17123
- request_id: randomUUID3(),
17276
+ request_id: randomUUID4(),
17124
17277
  request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
17125
17278
  });
17126
17279
  }
@@ -17136,7 +17289,7 @@ var Session = class {
17136
17289
  // Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
17137
17290
  // turn under way, so Claude Code takes the message at once.
17138
17291
  say(text3, now = false) {
17139
- this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
17292
+ this.messages.push({ id: randomUUID4(), role: "user", text: text3 });
17140
17293
  this.typing = "";
17141
17294
  this.status = "working";
17142
17295
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -17205,15 +17358,27 @@ var Session = class {
17205
17358
  else if (message.type === "assistant") {
17206
17359
  const text3 = (Array.isArray(message.message?.content) ? message.message.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n\n");
17207
17360
  if (!text3) return false;
17208
- this.messages.push({ id: String(message.uuid ?? randomUUID3()), role: "assistant", text: text3 });
17361
+ this.messages.push({ id: String(message.uuid ?? randomUUID4()), role: "assistant", text: text3 });
17209
17362
  } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17210
- const id = randomUUID3();
17363
+ const id = randomUUID4();
17364
+ const asked = message.request;
17365
+ const request2 = {
17366
+ tool: String(asked.tool_name),
17367
+ input: asked.input ?? {},
17368
+ label: typeof asked.display_name === "string" ? asked.display_name : void 0,
17369
+ description: typeof asked.description === "string" ? asked.description : void 0,
17370
+ suggestions: Array.isArray(asked.permission_suggestions) ? asked.permission_suggestions : void 0
17371
+ };
17211
17372
  this.approvals.set(id, {
17212
- request: String(message.request_id),
17213
- input: message.request.input ?? {},
17214
- approval: approval2(id, message.request)
17373
+ control: String(message.request_id),
17374
+ request: request2,
17375
+ approval: ask(id, request2, true)
17215
17376
  });
17216
17377
  this.status = "attention";
17378
+ } else if (message.type === "control_cancel_request") {
17379
+ for (const [id, pending] of this.approvals)
17380
+ if (pending.control === message.request_id) this.approvals.delete(id);
17381
+ if (this.status === "attention" && !this.approvals.size) this.status = "working";
17217
17382
  } else if (message.type === "result") {
17218
17383
  if (--this.open <= 0) this.turn?.stdin.end();
17219
17384
  } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
@@ -17229,7 +17394,7 @@ var Session = class {
17229
17394
  this.changed();
17230
17395
  return init;
17231
17396
  }
17232
- live() {
17397
+ live(models) {
17233
17398
  return {
17234
17399
  id: this.id,
17235
17400
  cwd: this.cwd,
@@ -17242,7 +17407,7 @@ var Session = class {
17242
17407
  approvals: [...this.approvals.values()].map((pending) => pending.approval),
17243
17408
  send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17244
17409
  now: true,
17245
- options: claudeOptions,
17410
+ options: (recorded) => claudeOptions(models, recorded),
17246
17411
  configure: (requestId, option2, value2) => this.once(requestId, async () => {
17247
17412
  this.chosen[option2] = value2;
17248
17413
  this.apply(option2, value2);
@@ -17251,16 +17416,16 @@ var Session = class {
17251
17416
  if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17252
17417
  this.interrupt();
17253
17418
  }) : void 0,
17254
- approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17419
+ approve: this.approvals.size ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17255
17420
  const pending = this.approvals.get(approvalId);
17256
- if (!pending || !this.turn || !CHOICES.some((value2) => value2.id === choice))
17421
+ if (!pending || !this.turn)
17257
17422
  throw new Error("Claude Code approval is no longer pending");
17258
17423
  this.write({
17259
17424
  type: "control_response",
17260
17425
  response: {
17261
17426
  subtype: "success",
17262
- request_id: pending.request,
17263
- response: choice === "accept" ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The person rejected this from their phone." }
17427
+ request_id: pending.control,
17428
+ response: decide(pending.request, pending.approval, reply)
17264
17429
  }
17265
17430
  });
17266
17431
  this.approvals.delete(approvalId);
@@ -17291,7 +17456,7 @@ var Headless = class {
17291
17456
  // Starts a session in `cwd` with its first message, and resolves with the session's id.
17292
17457
  async start(cwd, text3) {
17293
17458
  const title2 = text3.trim().split("\n")[0].slice(0, 80);
17294
- const session = new Session(randomUUID3(), cwd, title2, this.soon);
17459
+ const session = new Session(randomUUID4(), cwd, title2, this.soon);
17295
17460
  this.sessions.set(session.id, session);
17296
17461
  try {
17297
17462
  await session.say(text3);
@@ -17314,7 +17479,8 @@ var Headless = class {
17314
17479
  }
17315
17480
  }
17316
17481
  async refresh() {
17317
- return [...this.sessions.values()].map((session) => session.live());
17482
+ const models = claudeModels(this.changed);
17483
+ return [...this.sessions.values()].map((session) => session.live(models));
17318
17484
  }
17319
17485
  stop() {
17320
17486
  clearTimeout(this.notify);
@@ -17329,9 +17495,9 @@ import { connect as connect2 } from "node:net";
17329
17495
  import { promisify as promisify3 } from "node:util";
17330
17496
  import { fileURLToPath } from "node:url";
17331
17497
  import { execFile as execFile3 } from "node:child_process";
17332
- import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17498
+ import { createHash as createHash3, randomUUID as randomUUID5 } from "node:crypto";
17333
17499
  import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17334
- import { join as join6, dirname as dirname5, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
17500
+ import { join as join6, dirname as dirname5, basename as basename3, isAbsolute as isAbsolute3 } from "node:path";
17335
17501
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
17336
17502
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
17337
17503
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
@@ -17349,7 +17515,7 @@ var CONFIRMATION_CHOICES = [
17349
17515
  function roots() {
17350
17516
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
17351
17517
  if (override !== void 0)
17352
- return override ? [basename2(override) === "User" ? dirname5(override) : override] : [];
17518
+ return override ? [basename3(override) === "User" ? dirname5(override) : override] : [];
17353
17519
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17354
17520
  const base = process.platform === "darwin" ? join6(homedir6(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join6(homedir6(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join6(homedir6(), ".config");
17355
17521
  return ["Code", "Code - Insiders"].map((name) => join6(base, name));
@@ -17391,12 +17557,12 @@ async function entries() {
17391
17557
  try {
17392
17558
  if (!await privateFile(path, MAX_METADATA)) continue;
17393
17559
  const parsed = JSON.parse(await readFile3(path, "utf8"));
17394
- const rows = basename2(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
17560
+ const rows = basename3(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
17395
17561
  for (const row of rows.slice(0, MAX_HOSTS)) {
17396
17562
  if (row?.schemaVersion !== 2 || !["editor", "standalone"].includes(row.type) || !Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.instanceId !== "string" || !row.instanceId || typeof row.connectionToken !== "string" || !row.connectionToken || row.connectionToken.length > 1024 || row.endpoint?.type !== "socket" || typeof row.endpoint.path !== "string" || !isAbsolute3(row.endpoint.path))
17397
17563
  continue;
17398
17564
  const identity2 = createHash3("sha256").update(`${row.type}\0${row.pid}\0${row.instanceId}`).digest("hex");
17399
- if (basename2(path) !== "metadata.json" && basename2(path) !== `${identity2}.json`) continue;
17565
+ if (basename3(path) !== "metadata.json" && basename3(path) !== `${identity2}.json`) continue;
17400
17566
  try {
17401
17567
  process.kill(row.pid, 0);
17402
17568
  } catch {
@@ -17522,7 +17688,7 @@ var Host = class {
17522
17688
  client;
17523
17689
  connecting;
17524
17690
  stopped = false;
17525
- clientId = `dashup-${randomUUID4()}`;
17691
+ clientId = `dashup-${randomUUID5()}`;
17526
17692
  resources = /* @__PURE__ */ new Map();
17527
17693
  subscriptions = /* @__PURE__ */ new Set();
17528
17694
  receipts = /* @__PURE__ */ new Map();
@@ -17703,12 +17869,12 @@ var Host = class {
17703
17869
  for (const pending of Array.isArray(session.inputNeeded) ? session.inputNeeded : []) {
17704
17870
  if (pending.kind !== "toolConfirmation" || pending.chat !== item.resource || pending.toolCall?.status !== "pending-confirmation")
17705
17871
  continue;
17706
- const preview2 = confirmation(pending.toolCall);
17872
+ const preview3 = confirmation(pending.toolCall);
17707
17873
  approvals.push({
17708
17874
  id: pending.id,
17709
17875
  title: text2(pending.toolCall.confirmationTitle) || pending.toolCall.displayName || "Native agent approval",
17710
- detail: preview2.detail,
17711
- choices: preview2.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17876
+ detail: preview3.detail,
17877
+ choices: preview3.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17712
17878
  });
17713
17879
  }
17714
17880
  const id = `vscode-ahp:${createHash3("sha256").update(this.entry.root).digest("hex").slice(0, 16)}:${item.resource}`;
@@ -17750,7 +17916,8 @@ var Host = class {
17750
17916
  message: { text: value2, origin: { kind: "user" } }
17751
17917
  });
17752
17918
  } : void 0,
17753
- approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => {
17919
+ approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => {
17920
+ const choice = "choice" in reply ? reply.choice : void 0;
17754
17921
  const prior = this.requests.get(requestId);
17755
17922
  if (prior) return prior;
17756
17923
  const current = this.resources.get(summary.resource)?.state;
@@ -17825,7 +17992,7 @@ import { join as join7 } from "node:path";
17825
17992
  import { promisify as promisify4 } from "node:util";
17826
17993
  import { createHash as createHash4 } from "node:crypto";
17827
17994
  import { execFile as execFile4, spawn as spawn4 } from "node:child_process";
17828
- import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17995
+ import { stat as stat3, readlink as readlink2, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17829
17996
  var run2 = promisify4(execFile4);
17830
17997
  var REQUEST_MS3 = 4e3;
17831
17998
  var MAX_BYTES3 = 8 * 1024 * 1024;
@@ -17880,7 +18047,7 @@ async function linuxEndpoints() {
17880
18047
  })
17881
18048
  );
17882
18049
  for (const fd of (await readdir4(join7(directory2, "fd"))).slice(0, 1024)) {
17883
- const target = await readlink(join7(directory2, "fd", fd)).catch(() => "");
18050
+ const target = await readlink2(join7(directory2, "fd", fd)).catch(() => "");
17884
18051
  const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
17885
18052
  const url2 = inode ? listeners.get(inode) : void 0;
17886
18053
  if (url2) result.push({ url: url2, authorization: authorization(environment) });
@@ -18123,21 +18290,14 @@ var Server = class {
18123
18290
  (question) => question?.sessionID === id && typeof question.id === "string"
18124
18291
  ) : [];
18125
18292
  const approvals = pending.map((permission) => permissionApproval(permission, history, cwd));
18126
- for (const question of asks) {
18127
- const first = question.questions?.[0];
18128
- const options = first?.multiple !== true && question.questions?.length === 1 && Array.isArray(first?.options) ? first.options.filter(
18129
- (option2) => typeof option2?.label === "string"
18130
- ) : [];
18131
- approvals.push({
18132
- id: `question:${question.id}`,
18133
- title: typeof first?.header === "string" ? first.header : "OpenCode needs an answer",
18134
- detail: typeof first?.question === "string" ? first.question : "Open this question in the computer\u2019s native interface.",
18135
- choices: options.map((option2, index) => ({
18136
- id: `answer:${index}`,
18137
- label: option2.label
18138
- }))
18139
- });
18140
- }
18293
+ for (const question of asks)
18294
+ approvals.push(
18295
+ questionApproval(
18296
+ `question:${question.id}`,
18297
+ Array.isArray(question.questions) ? question.questions : [],
18298
+ (item) => item.multiple === true
18299
+ )
18300
+ );
18141
18301
  result.push({
18142
18302
  id,
18143
18303
  cwd,
@@ -18173,23 +18333,21 @@ var Server = class {
18173
18333
  });
18174
18334
  this.changed();
18175
18335
  }),
18176
- approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
18336
+ approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => this.once(requestId, async () => {
18177
18337
  if (this.stopped) throw new Error("OpenCode is disconnected");
18338
+ const choice = "choice" in reply ? reply.choice : void 0;
18178
18339
  if (approvalId.startsWith("question:")) {
18179
18340
  const requestID = approvalId.slice("question:".length);
18180
18341
  const current = await this.request("/question", cwd);
18181
- const question = Array.isArray(current) ? current.find(
18342
+ if (!Array.isArray(current) || !current.some(
18182
18343
  (value2) => value2?.id === requestID && value2.sessionID === id
18183
- ) : void 0;
18184
- const index = /^answer:(\d+)$/.exec(choice)?.[1];
18185
- const option2 = index !== void 0 ? question?.questions?.[0]?.options?.[Number(index)] : void 0;
18186
- if (question?.questions?.length !== 1 || question.questions[0]?.multiple === true || typeof option2?.label !== "string")
18187
- throw new Error(
18188
- "Question is no longer pending or requires the native interface"
18189
- );
18190
- await this.request(`/question/${encodeURIComponent(requestID)}/reply`, cwd, {
18191
- answers: [[option2.label]]
18192
- });
18344
+ ))
18345
+ throw new Error("Question is no longer pending");
18346
+ await this.request(
18347
+ `/question/${encodeURIComponent(requestID)}/${"answers" in reply ? "reply" : "reject"}`,
18348
+ cwd,
18349
+ "answers" in reply ? { answers: reply.answers } : {}
18350
+ );
18193
18351
  } else {
18194
18352
  const current = await this.request("/permission", cwd);
18195
18353
  const permission = Array.isArray(current) ? current.find(
@@ -18413,10 +18571,10 @@ var LiveHarnesses = class {
18413
18571
  });
18414
18572
  return this.refreshing;
18415
18573
  }
18416
- // The agents whose own command runs on this computer, and the models Claude Code offers.
18574
+ // The agents whose own command runs on this computer. Claude Code is asked its models again.
18417
18575
  async agents() {
18418
18576
  const harnesses = ["claude", "codex", "opencode"];
18419
- const asked = askModels();
18577
+ forgetModels();
18420
18578
  const runs = await Promise.all(
18421
18579
  harnesses.map((harness) => promisify5(execFile5)(harness, ["--version"], {
18422
18580
  timeout: 1e4,
@@ -18426,7 +18584,6 @@ var LiveHarnesses = class {
18426
18584
  () => false
18427
18585
  ))
18428
18586
  );
18429
- await asked;
18430
18587
  return harnesses.filter((_, index) => runs[index]);
18431
18588
  }
18432
18589
  // Starts a session in `cwd` with its first message, and resolves with the session's native id.
@@ -18475,10 +18632,10 @@ var LiveHarnesses = class {
18475
18632
  };
18476
18633
 
18477
18634
  // src/harness/queue.ts
18478
- import { randomUUID as randomUUID5 } from "node:crypto";
18635
+ import { randomUUID as randomUUID6 } from "node:crypto";
18479
18636
  import { spawn as spawn5 } from "node:child_process";
18480
18637
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18481
- import { join as join8, dirname as dirname6, basename as basename3 } from "node:path";
18638
+ import { join as join8, dirname as dirname6, basename as basename4 } from "node:path";
18482
18639
  import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18483
18640
  var CODEX_VERSION = "0.160.1";
18484
18641
  var REQUEST_MS4 = 1e4;
@@ -18510,7 +18667,7 @@ function processOwners(pid) {
18510
18667
  const root = `/proc/${pid}`;
18511
18668
  if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
18512
18669
  const executable = realpathSync2.native(join8(root, "exe"));
18513
- if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18670
+ if (basename4(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18514
18671
  const status2 = readFileSync4(join8(root, "stat"), "utf8");
18515
18672
  const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18516
18673
  if (!start2 || !/^\d+$/.test(start2)) return [];
@@ -18523,8 +18680,8 @@ function processOwners(pid) {
18523
18680
  }
18524
18681
  });
18525
18682
  return files2.flatMap(({ fd, path }) => {
18526
- if (basename3(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18527
- const thread = basename3(path, ".lock");
18683
+ if (basename4(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18684
+ const thread = basename4(path, ".lock");
18528
18685
  if (!THREAD_ID.test(thread)) return [];
18529
18686
  try {
18530
18687
  const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
@@ -18760,7 +18917,7 @@ var NativeQueue = class {
18760
18917
  if (existing) return existing;
18761
18918
  if (this.stopped) throw new Error("The connector is stopping");
18762
18919
  validateOwner(scope2.owner);
18763
- const clientUserMessageId = randomUUID5();
18920
+ const clientUserMessageId = randomUUID6();
18764
18921
  const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
18765
18922
  let writer;
18766
18923
  try {
@@ -18834,6 +18991,7 @@ var EPHEMERAL_TTL_MS = 6e4;
18834
18991
  var HOME_RECENT = 20;
18835
18992
  var FOLLOW_MS = 2 * 6e4;
18836
18993
  var RESCAN_MS = 1e3;
18994
+ var answers = (approval2, reply) => "answers" in reply ? reply.answers.length === approval2.questions?.length : approval2.choices.some((choice) => choice.id === reply.choice);
18837
18995
  var Command = z2.object({
18838
18996
  type: z2.literal("command"),
18839
18997
  requestId: z2.string().min(1),
@@ -18855,7 +19013,15 @@ var Command = z2.object({
18855
19013
  option: z2.string().min(1).max(100),
18856
19014
  value: z2.union([z2.string().min(1).max(200), z2.boolean()])
18857
19015
  }).strict(),
18858
- z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
19016
+ z2.object({
19017
+ kind: z2.literal("approval.respond"),
19018
+ approvalId: z2.string(),
19019
+ choice: z2.string().optional(),
19020
+ text: z2.string().max(1e5).optional(),
19021
+ answers: z2.array(z2.array(z2.string().min(1).max(1e5)).min(1)).min(1).optional()
19022
+ }).strict().refine(
19023
+ (action) => action.choice === void 0 !== (action.answers === void 0) && (action.text === void 0 || action.choice !== void 0)
19024
+ ),
18859
19025
  z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18860
19026
  z2.object({
18861
19027
  kind: z2.literal("session.start"),
@@ -18971,15 +19137,15 @@ var Connector = class {
18971
19137
  for (const project of this.discovery.projects.values())
18972
19138
  for (const thread of project.threads) {
18973
19139
  const native = this.discovery.locations.get(thread.id)?.live;
18974
- const approval3 = native?.approvals[0];
18975
- if (!approval3) continue;
19140
+ const approval2 = native?.approvals[0];
19141
+ if (!approval2) continue;
18976
19142
  await this.thread(pair, project, thread);
18977
19143
  await this.send(pair, {
18978
19144
  type: "attention",
18979
19145
  projectId: project.id,
18980
19146
  threadId: thread.id,
18981
- title: approval3.title,
18982
- body: approval3.detail,
19147
+ title: approval2.title,
19148
+ body: approval2.detail,
18983
19149
  reason: "approval"
18984
19150
  });
18985
19151
  }
@@ -19233,9 +19399,9 @@ var Connector = class {
19233
19399
  if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
19234
19400
  this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
19235
19401
  for (const pair of this.activePairs()) await this.thread(pair, project, thread);
19236
- for (const approval3 of session.approvals)
19237
- if (!prior?.approvals.some((item) => item.id === approval3.id))
19238
- await this.attention(project.id, thread.id, approval3.title, approval3.detail, "approval");
19402
+ for (const approval2 of session.approvals)
19403
+ if (!prior?.approvals.some((item) => item.id === approval2.id))
19404
+ await this.attention(project.id, thread.id, approval2.title, approval2.detail, "approval");
19239
19405
  if (prior?.thread.status === "working" && thread.status === "idle")
19240
19406
  await this.attention(
19241
19407
  project.id,
@@ -19423,7 +19589,7 @@ var Connector = class {
19423
19589
  }
19424
19590
  }
19425
19591
  attach(socket) {
19426
- const sessions = bridgeAcp(socket, (session, approval3) => {
19592
+ const sessions = bridgeAcp(socket, (session, approval2) => {
19427
19593
  this.agents.set(session.id, session);
19428
19594
  const { project, thread } = this.adopt(session);
19429
19595
  if (!this.snapshots.has(session.id)) {
@@ -19438,15 +19604,15 @@ var Connector = class {
19438
19604
  }
19439
19605
  }).catch((error) => this.report(error));
19440
19606
  }
19441
- if (approval3)
19607
+ if (approval2)
19442
19608
  this.work = this.work.then(async () => {
19443
- if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval3))
19609
+ if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval2))
19444
19610
  return;
19445
19611
  await this.attention(
19446
19612
  project.id,
19447
19613
  thread.id,
19448
- approval3.title,
19449
- approval3.detail,
19614
+ approval2.title,
19615
+ approval2.detail,
19450
19616
  "approval"
19451
19617
  );
19452
19618
  }).catch((error) => this.report(error));
@@ -19612,24 +19778,24 @@ var Connector = class {
19612
19778
  }
19613
19779
  case "approval.respond": {
19614
19780
  if (!thread) throw new Error("Thread not found");
19615
- const action = command2.action;
19781
+ const { kind, approvalId, ...reply } = command2.action;
19616
19782
  const native = this.discovery.locations.get(thread.id)?.live;
19617
19783
  if (native?.approve) {
19618
- const approval4 = native.approvals.find((item) => item.id === action.approvalId);
19619
- if (!approval4?.choices.some((choice) => choice.id === action.choice))
19784
+ const approval3 = native.approvals.find((item) => item.id === approvalId);
19785
+ if (!approval3 || !answers(approval3, reply))
19620
19786
  throw new Error("Approval is no longer available");
19621
- return native.approve(command2.requestId, approval4.id, action.choice);
19787
+ return native.approve(command2.requestId, approval3.id, reply);
19622
19788
  }
19623
19789
  const agent = this.attached(thread);
19624
19790
  if (!agent)
19625
19791
  throw new Error("Approval requires an attached ACP session. Answer it on your computer.");
19626
- const [request2, approval3] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
19627
- if (!approval3?.choices.some((choice) => choice.id === action.choice))
19792
+ const [request2, approval2] = [...agent.approvals].find(([, item]) => item.id === approvalId) ?? [];
19793
+ if (!approval2 || !("choice" in reply) || !answers(approval2, reply))
19628
19794
  throw new Error("Approval is no longer available");
19629
19795
  return commandAcp(agent, command2.requestId, {
19630
19796
  kind: "approval",
19631
19797
  id: request2,
19632
- choice: action.choice
19798
+ choice: reply.choice
19633
19799
  });
19634
19800
  }
19635
19801
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dashup": "dist/index.js"
@@ -30,11 +30,11 @@
30
30
  "tsup": "^8.5.1",
31
31
  "tsx": "^4.22.4",
32
32
  "typescript": "^5.9.3",
33
+ "@dashup/models": "0.1.0",
33
34
  "@dashup/protocol": "0.1.0",
34
35
  "@dashup/typescript-config": "0.0.0",
35
- "@dashup/models": "0.1.0",
36
- "@parcae/sdk": "0.8.1",
37
- "@parcae/lynx": "0.1.0"
36
+ "@parcae/lynx": "0.1.0",
37
+ "@parcae/sdk": "0.8.1"
38
38
  },
39
39
  "scripts": {
40
40
  "dashup": "tsx src/index.ts",