@dashup/connector 0.1.12 → 0.1.14

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
@@ -22,7 +22,7 @@ dashup alert --project PROJECT_ID --title "Dashup is connected"
22
22
 
23
23
  The connector automatically reads existing Claude Code, Codex, and OpenCode conversations alongside Zed workspaces and VS Code chat history. It honors the harness's existing data directory, authentication, and native session IDs. Native stores are opened read-only; discovery never launches an agent, creates or resumes a conversation, or changes editor settings. Only a session the phone starts runs an agent (see below). Opening the same supported harness session through Zed and its native history uses one Dashup thread when their actual session identities match. Matching directory names alone never merges local and SSH projects.
24
24
 
25
- A thread's title is the agent's own (a rename, Codex's stored title, Claude's title), else the first message the person typed: Codex's injected AGENTS.md and environment messages and Claude Code's slash commands are skipped, in titles and in the conversation, and a Codex subagent is named for its task. A thread's time is its last conversation message in the agent's own history; editor bookkeeping such as Zed's sidebar restamps never moves it. Subagent sessions (OpenCode children, Codex spawned agents) carry their parent thread. Tool calls are joined with their results into one step with status, duration and, for file edits, a unified diff. Saved history has unknown live status; the phone can answer it by resuming it (see Starting sessions from the phone). File changes publish project metadata; only an approval, a question, an error or a finished turn sends an alert. A workspace becomes a project once it has a conversation. After every change the connector publishes its overview: every project, and the conversations Home shows (anything that needs the person or is working, then the newest 20). The phone renders Home from the overview alone and decodes a project's full snapshot only when a page opens it. Only a scan that read every source retires a project it published earlier and left off. Each phone read request reads a thread's native history once and publishes a snapshot of separately encrypted message fragments to the database. The phone reads bounded latest and earlier pages; realtime thread updates carry only summary and approval metadata. Native history remains the source, and an incomplete upload never replaces the previous published snapshot. Native live interfaces provide actual status and pending choices. Tool integrations can also queue explicit encrypted alerts with `alert`.
25
+ A thread's title is the agent's own (a rename, Codex's stored title, Claude's title), else the first message the person typed: Codex's injected AGENTS.md and environment messages and Claude Code's slash commands are skipped, and a Codex subagent is named for its task. In the conversation, Codex's injected messages and Claude Code's own meta rows stay out, and a Claude Code slash command reads as typed, followed by its output. A thread's time is its last conversation message in the agent's own history; editor bookkeeping such as Zed's sidebar restamps never moves it. Subagent sessions (OpenCode children, Codex spawned agents) carry their parent thread. Tool calls are joined with their results into one step with status, duration and, for file edits, a unified diff. Saved history has unknown live status; the phone can answer it by resuming it (see Starting sessions from the phone). File changes publish project metadata; only an approval, a question, an error or a finished turn sends an alert. A workspace becomes a project once it has a conversation. After every change the connector publishes its overview: every project, and the conversations Home shows (anything that needs the person or is working, then the newest 20). The phone renders Home from the overview alone and decodes a project's full snapshot only when a page opens it. Only a scan that read every source retires a project it published earlier and left off. Each phone read request reads a thread's native history once and publishes a snapshot of separately encrypted message fragments to the database. The phone reads bounded latest and earlier pages; realtime thread updates carry only summary and approval metadata. Native history remains the source, and an incomplete upload never replaces the previous published snapshot. Native live interfaces provide actual status and pending choices. Tool integrations can also queue explicit encrypted alerts with `alert`.
26
26
 
27
27
  Project paths, SSH details, thread content, approvals, and alerts are encrypted separately for every paired phone. The encrypted outbox survives a connector restart. Parcae handles realtime delivery and reconnect; replay counters are saved before commands execute. Revoking a pairing discards its connector key and queued content.
28
28
 
@@ -47,6 +47,8 @@ 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.
51
+
50
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.
51
53
 
52
54
  Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the latest 200 messages; the VS Code host supplies its current chat snapshot. Paged history prefers the native saved store when available and otherwise uses the existing live snapshot. Approvals whose actual commands or file contents cannot be previewed are approved on the computer; rejection remains available when the native protocol supports it.
@@ -1 +1 @@
1
- { "modules": ["./register.ts"] }
1
+ { "hooks": {}, "modules": ["./register.ts"] }
@@ -7,7 +7,8 @@ import type { EngineInterface, Register } from 'claude-code';
7
7
  // Claude Code reads what other processes post to a session's inbox as another session's request,
8
8
  // never the person's. Dashup's connector instead streams what the person sends this session from
9
9
  // their phone, one delivery per line, and the session takes each message as their own. `now`
10
- // first ends the running turn, so a delivery without text only stops it.
10
+ // first ends the running turn, so a delivery without text only stops it. The empty `hooks` in
11
+ // hooks.json lets Claude Code from before function hooks load the plugin, which then does nothing.
11
12
  type Delivery = { text?: string; now?: true };
12
13
 
13
14
  const RETRY_MS = 10_000;
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var require_XMLHttpRequest = __commonJS({
42
42
  "use strict";
43
43
  var fs = __require("fs");
44
44
  var Url = __require("url");
45
- var spawn6 = __require("child_process").spawn;
45
+ var spawn7 = __require("child_process").spawn;
46
46
  module.exports = XMLHttpRequest3;
47
47
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
48
48
  function XMLHttpRequest3(opts) {
@@ -338,7 +338,7 @@ var require_XMLHttpRequest = __commonJS({
338
338
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
339
339
  fs.writeFileSync(syncFile, "", "utf8");
340
340
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
341
- var syncProc = spawn6(process.argv[0], ["-e", execString]);
341
+ var syncProc = spawn7(process.argv[0], ["-e", execString]);
342
342
  var statusText;
343
343
  while (fs.existsSync(syncFile)) {
344
344
  }
@@ -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.12";
4051
+ var version = "0.1.14";
4052
4052
 
4053
4053
  // src/pair.ts
4054
4054
  var Invitation = z.object({
@@ -4459,6 +4459,13 @@ function step(id, name, input, result, started) {
4459
4459
  }
4460
4460
  var claudeHome = () => process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
4461
4461
  var claudeMessage = (row) => (row.type === "user" || row.type === "assistant") && !row.isSidechain;
4462
+ var OUTPUT_TAGS = /<\/?local-command-std\w+>/g;
4463
+ var claudeOutput = (row) => row.type === "system" && row.subtype === "local_command" ? text(row.content).replace(OUTPUT_TAGS, "").trim() : void 0;
4464
+ function claudeCommand(value2) {
4465
+ const name = /<command-name>([^<]*)<\/command-name>/.exec(value2)?.[1];
4466
+ const args2 = /<command-args>([^<]*)<\/command-args>/.exec(value2)?.[1] ?? "";
4467
+ return name === void 0 ? void 0 : `${name} ${args2}`.trim();
4468
+ }
4462
4469
  function claudeChange(change2) {
4463
4470
  if (typeof change2?.filePath !== "string") return void 0;
4464
4471
  const hunks = list(change2.structuredPatch);
@@ -4480,7 +4487,8 @@ function claudeMessages(path) {
4480
4487
  for (const row of rows) {
4481
4488
  if (typeof row.uuid === "string" && !row.isSidechain) {
4482
4489
  chain.set(row.uuid, row);
4483
- if (row.type === "user" || row.type === "assistant") leaf = row.uuid;
4490
+ if (row.type === "user" || row.type === "assistant" || claudeOutput(row) !== void 0)
4491
+ leaf = row.uuid;
4484
4492
  }
4485
4493
  if (typeof row.leafUuid === "string" && chain.has(row.leafUuid)) leaf = row.leafUuid;
4486
4494
  }
@@ -4502,9 +4510,13 @@ function claudeConversation(conversation, rows) {
4502
4510
  for (const part of row.type === "user" ? list(row.message?.content) : [])
4503
4511
  if (part?.type === "tool_result") results.set(part.tool_use_id, { part, row });
4504
4512
  return conversation.flatMap((row, index) => {
4505
- if (row.type !== "user" && row.type !== "assistant") return [];
4506
4513
  const id = String(row.uuid ?? row.message?.id ?? index);
4507
4514
  const content = row.message?.content;
4515
+ const output = claudeOutput(row);
4516
+ const command2 = row.type === "user" ? claudeCommand(text(content)) : void 0;
4517
+ if (output !== void 0) return prose(id, "system", output);
4518
+ if (row.type !== "user" && row.type !== "assistant" || row.isMeta) return [];
4519
+ if (command2 !== void 0) return [{ id, role: "user", text: command2 }];
4508
4520
  return [
4509
4521
  ...prose(id, row.type, content),
4510
4522
  ...list(content).flatMap((part, position) => {
@@ -4530,6 +4542,23 @@ function claudeConversation(conversation, rows) {
4530
4542
  ];
4531
4543
  });
4532
4544
  }
4545
+ function claudeSettings(path) {
4546
+ const rows = journal(path, true);
4547
+ const settings = {};
4548
+ for (let index = rows.length - 1; index >= 0; index--) {
4549
+ const row = rows[index];
4550
+ const message = object(row.message);
4551
+ const marker = row.type === "attachment" ? object(row.attachment)?.type : void 0;
4552
+ if (settings.model === void 0 && row.type === "assistant" && !row.isSidechain && typeof message?.model === "string" && message.model !== "<synthetic>") {
4553
+ settings.model = message.model;
4554
+ settings.fast = object(message.usage)?.speed === "fast";
4555
+ }
4556
+ if (settings.ultracode === void 0 && (marker === "ultra_effort_enter" || marker === "ultra_effort_exit"))
4557
+ settings.ultracode = marker === "ultra_effort_enter";
4558
+ if (settings.model !== void 0 && settings.ultracode !== void 0) break;
4559
+ }
4560
+ return { ultracode: false, ...settings };
4561
+ }
4533
4562
  function claude(root, scan) {
4534
4563
  for (const path of files(join2(root, "projects"), scan, 1).filter((file) => file.endsWith(".jsonl"))) {
4535
4564
  try {
@@ -4559,7 +4588,8 @@ function claude(root, scan) {
4559
4588
  harness: "claude",
4560
4589
  ...session,
4561
4590
  files: [path],
4562
- read: () => claudeMessages(path)
4591
+ read: () => claudeMessages(path),
4592
+ settings: () => claudeSettings(path)
4563
4593
  });
4564
4594
  } catch {
4565
4595
  scan.warnings.push(`Cannot read Claude Code session: ${path}`);
@@ -4748,8 +4778,8 @@ function databaseMessages(path, session) {
4748
4778
  }
4749
4779
  }
4750
4780
  function codexSubagent(row) {
4751
- const spawn6 = object(object(parse(String(row.source ?? "")))?.subagent)?.thread_spawn;
4752
- const task = typeof spawn6?.agent_path === "string" ? spawn6.agent_path.split("/").pop() : "";
4781
+ const spawn7 = object(object(parse(String(row.source ?? "")))?.subagent)?.thread_spawn;
4782
+ const task = typeof spawn7?.agent_path === "string" ? spawn7.agent_path.split("/").pop() : "";
4753
4783
  return [row.agent_nickname, task?.replace(/_/g, " ")].filter(Boolean).join(" \xB7 ");
4754
4784
  }
4755
4785
  var codexState;
@@ -5069,13 +5099,15 @@ var Discovery = class {
5069
5099
  title: session.title ?? found.thread.title,
5070
5100
  status: session.status,
5071
5101
  updatedAt: location2.history?.updatedAt ?? session.updatedAt,
5102
+ options: session.options?.(location2.history?.settings?.() ?? {}),
5072
5103
  capabilities: {
5073
5104
  ...found.thread.capabilities,
5074
5105
  read: true,
5075
5106
  send: Boolean(session.send),
5076
5107
  stop: Boolean(session.stop),
5077
5108
  now: Boolean(session.now),
5078
- approve: Boolean(session.approve)
5109
+ approve: Boolean(session.approve),
5110
+ configure: Boolean(session.configure)
5079
5111
  }
5080
5112
  });
5081
5113
  return found;
@@ -13214,22 +13246,22 @@ var FrontendAdapter = class {
13214
13246
  if (options.subscribe === false) requestData.__subscribe = false;
13215
13247
  const result = await this.transport.get(path, requestData);
13216
13248
  const items = result?.[(0, import_pluralize.default)(modelClass.type)] ?? result?.[modelClass.type + "s"] ?? result?.items ?? [];
13217
- const models = items.map(
13249
+ const models2 = items.map(
13218
13250
  (row) => modelClass.hydrate(this, row)
13219
13251
  );
13220
13252
  if (result?.__queryHash) {
13221
- Object.defineProperty(models, "__queryHash", {
13253
+ Object.defineProperty(models2, "__queryHash", {
13222
13254
  value: result.__queryHash,
13223
13255
  enumerable: false
13224
13256
  });
13225
13257
  }
13226
13258
  if (typeof result?.totalCount === "number") {
13227
- Object.defineProperty(models, "__totalCount", {
13259
+ Object.defineProperty(models2, "__totalCount", {
13228
13260
  value: result.totalCount,
13229
13261
  enumerable: false
13230
13262
  });
13231
13263
  }
13232
- return models;
13264
+ return models2;
13233
13265
  };
13234
13266
  chain.first = async () => {
13235
13267
  const all = await chain.find();
@@ -16780,15 +16812,206 @@ var Codex = class {
16780
16812
  // src/harness/headless.ts
16781
16813
  import { randomUUID as randomUUID3 } from "node:crypto";
16782
16814
  import { readFileSync as readFileSync3 } from "node:fs";
16783
- import { createInterface as createInterface2 } from "node:readline";
16815
+ import { createInterface as createInterface3 } from "node:readline";
16816
+ import { spawn as spawn3 } from "node:child_process";
16817
+
16818
+ // src/harness/claude.ts
16819
+ import { homedir as homedir5 } from "node:os";
16820
+ import { dirname as dirname4, join as join5 } from "node:path";
16784
16821
  import { spawn as spawn2 } from "node:child_process";
16822
+ 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";
16825
+ import { createServer as createServer2 } from "node:http";
16826
+ var MODELS_MS = 3e4;
16827
+ var COMMANDS = {
16828
+ model: (value2) => `/model ${value2}`,
16829
+ fast: (value2) => `/fast ${value2 ? "on" : "off"}`,
16830
+ ultracode: (value2) => `/effort ultracode ${value2 ? "on" : "off"}`
16831
+ };
16832
+ var models = [];
16833
+ function askModels() {
16834
+ return new Promise((resolve3) => {
16835
+ const child = spawn2(
16836
+ "claude",
16837
+ ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
16838
+ { cwd: homedir5(), stdio: "pipe", shell: process.platform === "win32" }
16839
+ );
16840
+ const timer = setTimeout(() => child.kill(), MODELS_MS);
16841
+ child.stdin.on("error", () => {
16842
+ });
16843
+ child.once("error", () => resolve3());
16844
+ child.once("exit", () => {
16845
+ clearTimeout(timer);
16846
+ resolve3();
16847
+ });
16848
+ createInterface2({ input: child.stdout }).on("line", (line) => {
16849
+ try {
16850
+ const message = JSON.parse(line);
16851
+ if (message?.type !== "control_response" || message.response?.request_id !== "models")
16852
+ return;
16853
+ const offered = message.response.response?.models;
16854
+ if (Array.isArray(offered))
16855
+ models = offered.filter(
16856
+ (model) => typeof model?.value === "string" && typeof model.displayName === "string"
16857
+ );
16858
+ child.stdin.end();
16859
+ } catch {
16860
+ }
16861
+ });
16862
+ child.stdin.write(
16863
+ `${JSON.stringify({ type: "control_request", request_id: "models", request: { subtype: "initialize" } })}
16864
+ `
16865
+ );
16866
+ });
16867
+ }
16868
+ function claudeOptions(recorded) {
16869
+ const ran = typeof recorded.model === "string" ? recorded.model : "";
16870
+ const current = models.find((model) => model.value !== "default" && model.resolvedModel === ran) ?? models.find((model) => model.value === ran);
16871
+ const choices = models.map((model) => ({ value: model.value, name: model.displayName }));
16872
+ if (ran && !current) choices.push({ value: ran, name: ran });
16873
+ if (!choices.length) return [];
16874
+ return [
16875
+ { id: "model", name: "Model", value: current?.value ?? ran, choices },
16876
+ ...current?.supportsFastMode ? [{ id: "fast", name: "Fast", value: recorded.fast === true }] : [],
16877
+ ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16878
+ ];
16879
+ }
16880
+ var MAX_SESSIONS2 = 100;
16881
+ var MAX_REQUESTS = 1e3;
16882
+ var SESSION = /^\/sessions\/([\w-]{1,100})$/;
16883
+ var STATUS = {
16884
+ busy: "working",
16885
+ idle: "idle",
16886
+ shell: "idle",
16887
+ waiting: "attention"
16888
+ };
16889
+ async function registrations() {
16890
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16891
+ const directory2 = join5(claudeHome(), "sessions");
16892
+ const names = await readdir2(directory2).catch(() => []);
16893
+ const found = await Promise.all(
16894
+ names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
16895
+ try {
16896
+ const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
16897
+ if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
16898
+ process.kill(value2.pid, 0);
16899
+ return [value2];
16900
+ } catch {
16901
+ return [];
16902
+ }
16903
+ })
16904
+ );
16905
+ return found.flat();
16906
+ }
16907
+ var Claude = class {
16908
+ constructor(changed, socket = join5(dirname4(configPath()), "claude.sock")) {
16909
+ this.changed = changed;
16910
+ this.socket = socket;
16911
+ }
16912
+ changed;
16913
+ socket;
16914
+ watcher;
16915
+ notify;
16916
+ server;
16917
+ streams = /* @__PURE__ */ new Map();
16918
+ requests = /* @__PURE__ */ new Map();
16919
+ async refresh() {
16920
+ this.watch();
16921
+ return (await registrations()).map((session) => {
16922
+ const plugin = this.streams.has(session.sessionId);
16923
+ return {
16924
+ id: session.sessionId,
16925
+ harness: "claude",
16926
+ cwd: session.cwd,
16927
+ updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
16928
+ status: STATUS[session.status ?? ""] ?? "unknown",
16929
+ messages: [],
16930
+ approvals: [],
16931
+ send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
16932
+ stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
16933
+ now: plugin,
16934
+ options: claudeOptions,
16935
+ configure: plugin ? (requestId, option2, value2) => this.once(requestId, () => this.deliver(session.sessionId, { text: COMMANDS[option2](value2) })) : void 0
16936
+ };
16937
+ });
16938
+ }
16939
+ // Only the connector serves the plugin; one-off commands such as `dashup doctor` read alone.
16940
+ async listen() {
16941
+ if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16942
+ 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();
16945
+ this.streams.get(id)?.end();
16946
+ this.streams.set(id, response);
16947
+ response.once("close", () => {
16948
+ if (this.streams.get(id) === response) this.streams.delete(id);
16949
+ this.changed();
16950
+ });
16951
+ response.writeHead(200, { "Content-Type": "application/x-ndjson" }).flushHeaders();
16952
+ this.changed();
16953
+ });
16954
+ await mkdir(dirname4(this.socket), { recursive: true, mode: 448 });
16955
+ await rm(this.socket, { force: true });
16956
+ if (this.server !== server) return;
16957
+ await new Promise((resolve3, reject) => {
16958
+ server.once("error", reject);
16959
+ server.listen(this.socket, resolve3);
16960
+ });
16961
+ }
16962
+ deliver(id, delivery) {
16963
+ const stream = this.streams.get(id);
16964
+ if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
16965
+ return new Promise((resolve3, reject) => stream.write(`${JSON.stringify(delivery)}
16966
+ `, (error) => error ? reject(error) : resolve3()));
16967
+ }
16968
+ // Each request delivers at most once.
16969
+ once(id, action) {
16970
+ const known = this.requests.get(id);
16971
+ if (known) return known;
16972
+ if (this.requests.size >= MAX_REQUESTS)
16973
+ this.requests.delete(this.requests.keys().next().value);
16974
+ const result = action();
16975
+ this.requests.set(id, result);
16976
+ return result;
16977
+ }
16978
+ watch() {
16979
+ if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16980
+ try {
16981
+ const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
16982
+ if (this.notify) return;
16983
+ this.notify = setTimeout(() => {
16984
+ this.notify = void 0;
16985
+ this.changed();
16986
+ }, 100);
16987
+ this.notify.unref();
16988
+ });
16989
+ watcher.on("error", () => {
16990
+ watcher.close();
16991
+ if (this.watcher === watcher) this.watcher = void 0;
16992
+ });
16993
+ } catch {
16994
+ }
16995
+ }
16996
+ stop() {
16997
+ clearTimeout(this.notify);
16998
+ this.watcher?.close();
16999
+ this.watcher = void 0;
17000
+ this.server?.closeAllConnections();
17001
+ this.server?.close();
17002
+ this.server = void 0;
17003
+ }
17004
+ };
17005
+
17006
+ // src/harness/headless.ts
16785
17007
  var CHOICES = [
16786
17008
  { id: "accept", label: "Allow once" },
16787
17009
  { id: "decline", label: "Reject" }
16788
17010
  ];
16789
17011
  var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16790
- var MAX_REQUESTS = 1e3;
17012
+ var MAX_REQUESTS2 = 1e3;
16791
17013
  var NOTIFY_MS = 100;
17014
+ var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
16792
17015
  function hunk(before, after, offset = 0) {
16793
17016
  const old = before ? before.replace(/\n$/, "").split("\n") : [];
16794
17017
  const next = after ? after.replace(/\n$/, "").split("\n") : [];
@@ -16881,6 +17104,8 @@ var Session = class {
16881
17104
  typing = "";
16882
17105
  approvals = /* @__PURE__ */ new Map();
16883
17106
  requests = /* @__PURE__ */ new Map();
17107
+ // What the person chose for this session, applied to each turn as it starts.
17108
+ chosen = {};
16884
17109
  write(value2) {
16885
17110
  this.turn?.stdin.write(`${JSON.stringify(value2)}
16886
17111
  `);
@@ -16892,10 +17117,17 @@ var Session = class {
16892
17117
  request: { subtype: "interrupt" }
16893
17118
  });
16894
17119
  }
17120
+ apply(option2, value2) {
17121
+ this.write({
17122
+ type: "control_request",
17123
+ request_id: randomUUID3(),
17124
+ request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
17125
+ });
17126
+ }
16895
17127
  once(id, action) {
16896
17128
  const known = this.requests.get(id);
16897
17129
  if (known) return known;
16898
- if (this.requests.size >= MAX_REQUESTS)
17130
+ if (this.requests.size >= MAX_REQUESTS2)
16899
17131
  throw new Error("Claude Code command history is full; restart Dashup before sending");
16900
17132
  const result = action();
16901
17133
  this.requests.set(id, result);
@@ -16916,7 +17148,7 @@ var Session = class {
16916
17148
  this.changed();
16917
17149
  return Promise.resolve();
16918
17150
  }
16919
- const turn = this.turn = spawn2(
17151
+ const turn = this.turn = spawn3(
16920
17152
  "claude",
16921
17153
  [
16922
17154
  "-p",
@@ -16940,7 +17172,7 @@ var Session = class {
16940
17172
  errors = (errors + String(chunk)).slice(-2e3);
16941
17173
  });
16942
17174
  const started = new Promise((resolve3, reject) => {
16943
- createInterface2({ input: turn.stdout }).on("line", (line) => {
17175
+ createInterface3({ input: turn.stdout }).on("line", (line) => {
16944
17176
  if (this.receive(line)) resolve3();
16945
17177
  });
16946
17178
  const end = (reason) => {
@@ -16955,6 +17187,7 @@ var Session = class {
16955
17187
  turn.once("error", () => end("Claude Code is not installed on this computer"));
16956
17188
  turn.once("exit", () => end(errors.trim() || "Claude Code stopped before starting"));
16957
17189
  });
17190
+ for (const [option2, value2] of Object.entries(this.chosen)) this.apply(option2, value2);
16958
17191
  this.write(message);
16959
17192
  this.changed();
16960
17193
  return started;
@@ -17009,6 +17242,11 @@ var Session = class {
17009
17242
  approvals: [...this.approvals.values()].map((pending) => pending.approval),
17010
17243
  send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17011
17244
  now: true,
17245
+ options: claudeOptions,
17246
+ configure: (requestId, option2, value2) => this.once(requestId, async () => {
17247
+ this.chosen[option2] = value2;
17248
+ this.apply(option2, value2);
17249
+ }),
17012
17250
  stop: this.turn ? (requestId) => this.once(requestId, async () => {
17013
17251
  if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17014
17252
  this.interrupt();
@@ -17084,138 +17322,9 @@ var Headless = class {
17084
17322
  }
17085
17323
  };
17086
17324
 
17087
- // src/harness/claude.ts
17088
- import { dirname as dirname4, join as join5 } from "node:path";
17089
- import { watch as watch2 } from "node:fs";
17090
- import { mkdir, readdir as readdir2, readFile as readFile2, rm } from "node:fs/promises";
17091
- import { createServer as createServer2 } from "node:http";
17092
- var MAX_SESSIONS2 = 100;
17093
- var MAX_REQUESTS2 = 1e3;
17094
- var SESSION = /^\/sessions\/([\w-]{1,100})$/;
17095
- var STATUS = {
17096
- busy: "working",
17097
- idle: "idle",
17098
- shell: "idle",
17099
- waiting: "attention"
17100
- };
17101
- async function registrations() {
17102
- if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17103
- const directory2 = join5(claudeHome(), "sessions");
17104
- const names = await readdir2(directory2).catch(() => []);
17105
- const found = await Promise.all(
17106
- names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
17107
- try {
17108
- const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
17109
- if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
17110
- process.kill(value2.pid, 0);
17111
- return [value2];
17112
- } catch {
17113
- return [];
17114
- }
17115
- })
17116
- );
17117
- return found.flat();
17118
- }
17119
- var Claude = class {
17120
- constructor(changed, socket = join5(dirname4(configPath()), "claude.sock")) {
17121
- this.changed = changed;
17122
- this.socket = socket;
17123
- }
17124
- changed;
17125
- socket;
17126
- watcher;
17127
- notify;
17128
- server;
17129
- streams = /* @__PURE__ */ new Map();
17130
- requests = /* @__PURE__ */ new Map();
17131
- async refresh() {
17132
- this.watch();
17133
- return (await registrations()).map((session) => {
17134
- const plugin = this.streams.has(session.sessionId);
17135
- return {
17136
- id: session.sessionId,
17137
- harness: "claude",
17138
- cwd: session.cwd,
17139
- updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
17140
- status: STATUS[session.status ?? ""] ?? "unknown",
17141
- messages: [],
17142
- approvals: [],
17143
- send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
17144
- stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
17145
- now: plugin
17146
- };
17147
- });
17148
- }
17149
- // Only the connector serves the plugin; one-off commands such as `dashup doctor` read alone.
17150
- async listen() {
17151
- if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
17152
- const server = this.server = createServer2((request2, response) => {
17153
- const id = request2.method === "GET" ? SESSION.exec(request2.url ?? "")?.[1] : void 0;
17154
- if (!id) return void response.writeHead(404).end();
17155
- this.streams.get(id)?.end();
17156
- this.streams.set(id, response);
17157
- response.once("close", () => {
17158
- if (this.streams.get(id) === response) this.streams.delete(id);
17159
- this.changed();
17160
- });
17161
- response.writeHead(200, { "Content-Type": "application/x-ndjson" }).flushHeaders();
17162
- this.changed();
17163
- });
17164
- await mkdir(dirname4(this.socket), { recursive: true, mode: 448 });
17165
- await rm(this.socket, { force: true });
17166
- if (this.server !== server) return;
17167
- await new Promise((resolve3, reject) => {
17168
- server.once("error", reject);
17169
- server.listen(this.socket, resolve3);
17170
- });
17171
- }
17172
- deliver(id, delivery) {
17173
- const stream = this.streams.get(id);
17174
- if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
17175
- return new Promise((resolve3, reject) => stream.write(`${JSON.stringify(delivery)}
17176
- `, (error) => error ? reject(error) : resolve3()));
17177
- }
17178
- // Each request delivers at most once.
17179
- once(id, action) {
17180
- const known = this.requests.get(id);
17181
- if (known) return known;
17182
- if (this.requests.size >= MAX_REQUESTS2)
17183
- this.requests.delete(this.requests.keys().next().value);
17184
- const result = action();
17185
- this.requests.set(id, result);
17186
- return result;
17187
- }
17188
- watch() {
17189
- if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
17190
- try {
17191
- const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
17192
- if (this.notify) return;
17193
- this.notify = setTimeout(() => {
17194
- this.notify = void 0;
17195
- this.changed();
17196
- }, 100);
17197
- this.notify.unref();
17198
- });
17199
- watcher.on("error", () => {
17200
- watcher.close();
17201
- if (this.watcher === watcher) this.watcher = void 0;
17202
- });
17203
- } catch {
17204
- }
17205
- }
17206
- stop() {
17207
- clearTimeout(this.notify);
17208
- this.watcher?.close();
17209
- this.watcher = void 0;
17210
- this.server?.closeAllConnections();
17211
- this.server?.close();
17212
- this.server = void 0;
17213
- }
17214
- };
17215
-
17216
17325
  // src/harness/vscode.ts
17217
17326
  import Socket3 from "ws";
17218
- import { homedir as homedir5 } from "node:os";
17327
+ import { homedir as homedir6 } from "node:os";
17219
17328
  import { connect as connect2 } from "node:net";
17220
17329
  import { promisify as promisify3 } from "node:util";
17221
17330
  import { fileURLToPath } from "node:url";
@@ -17242,7 +17351,7 @@ function roots() {
17242
17351
  if (override !== void 0)
17243
17352
  return override ? [basename2(override) === "User" ? dirname5(override) : override] : [];
17244
17353
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17245
- const base = process.platform === "darwin" ? join6(homedir5(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join6(homedir5(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join6(homedir5(), ".config");
17354
+ 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");
17246
17355
  return ["Code", "Code - Insiders"].map((name) => join6(base, name));
17247
17356
  }
17248
17357
  async function privateFile(path, limit) {
@@ -17715,7 +17824,7 @@ var VSCode = class {
17715
17824
  import { join as join7 } from "node:path";
17716
17825
  import { promisify as promisify4 } from "node:util";
17717
17826
  import { createHash as createHash4 } from "node:crypto";
17718
- import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
17827
+ import { execFile as execFile4, spawn as spawn4 } from "node:child_process";
17719
17828
  import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17720
17829
  var run2 = promisify4(execFile4);
17721
17830
  var REQUEST_MS3 = 4e3;
@@ -18183,7 +18292,7 @@ var Server = class {
18183
18292
  }
18184
18293
  };
18185
18294
  function serve(cwd) {
18186
- const child = spawn3("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18295
+ const child = spawn4("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18187
18296
  cwd,
18188
18297
  stdio: ["ignore", "pipe", "ignore"],
18189
18298
  shell: process.platform === "win32"
@@ -18304,9 +18413,10 @@ var LiveHarnesses = class {
18304
18413
  });
18305
18414
  return this.refreshing;
18306
18415
  }
18307
- // The agents whose own command runs on this computer.
18416
+ // The agents whose own command runs on this computer, and the models Claude Code offers.
18308
18417
  async agents() {
18309
18418
  const harnesses = ["claude", "codex", "opencode"];
18419
+ const asked = askModels();
18310
18420
  const runs = await Promise.all(
18311
18421
  harnesses.map((harness) => promisify5(execFile5)(harness, ["--version"], {
18312
18422
  timeout: 1e4,
@@ -18316,6 +18426,7 @@ var LiveHarnesses = class {
18316
18426
  () => false
18317
18427
  ))
18318
18428
  );
18429
+ await asked;
18319
18430
  return harnesses.filter((_, index) => runs[index]);
18320
18431
  }
18321
18432
  // Starts a session in `cwd` with its first message, and resolves with the session's native id.
@@ -18365,7 +18476,7 @@ var LiveHarnesses = class {
18365
18476
 
18366
18477
  // src/harness/queue.ts
18367
18478
  import { randomUUID as randomUUID5 } from "node:crypto";
18368
- import { spawn as spawn4 } from "node:child_process";
18479
+ import { spawn as spawn5 } from "node:child_process";
18369
18480
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18370
18481
  import { join as join8, dirname as dirname6, basename as basename3 } from "node:path";
18371
18482
  import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
@@ -18562,7 +18673,7 @@ var Writer = class {
18562
18673
  tail = "";
18563
18674
  pending = /* @__PURE__ */ new Map();
18564
18675
  constructor(owner) {
18565
- this.process = spawn4(owner.executable, ["app-server", "--listen", "stdio://"], {
18676
+ this.process = spawn5(owner.executable, ["app-server", "--listen", "stdio://"], {
18566
18677
  cwd: owner.cwd,
18567
18678
  env: { ...process.env, CODEX_HOME: owner.home },
18568
18679
  stdio: ["pipe", "pipe", "ignore"]
@@ -18739,6 +18850,11 @@ var Command = z2.object({
18739
18850
  }).strict(),
18740
18851
  z2.object({ kind: z2.literal("thread.queue"), text: z2.string().min(1).max(1e5) }).strict(),
18741
18852
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18853
+ z2.object({
18854
+ kind: z2.literal("thread.configure"),
18855
+ option: z2.string().min(1).max(100),
18856
+ value: z2.union([z2.string().min(1).max(200), z2.boolean()])
18857
+ }).strict(),
18742
18858
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18743
18859
  z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18744
18860
  z2.object({
@@ -19078,13 +19194,15 @@ var Connector = class {
19078
19194
  const detached = {
19079
19195
  ...snapshot.thread,
19080
19196
  status: "unknown",
19197
+ options: void 0,
19081
19198
  capabilities: {
19082
19199
  ...snapshot.thread.capabilities,
19083
19200
  send: false,
19084
19201
  approve: false,
19085
19202
  // A separate queue owner can remain valid after its live socket detaches.
19086
19203
  queue: thread?.capabilities.queue ?? false,
19087
- stop: false
19204
+ stop: false,
19205
+ configure: false
19088
19206
  }
19089
19207
  };
19090
19208
  if (thread) Object.assign(thread, detached);
@@ -19254,7 +19372,9 @@ var Connector = class {
19254
19372
  async execute(pair, content, envelope) {
19255
19373
  let queue = void 0;
19256
19374
  try {
19257
- if (["approval.respond", "thread.stop", "session.start"].includes(content.action.kind) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19375
+ if (["approval.respond", "thread.stop", "thread.configure", "session.start"].includes(
19376
+ content.action.kind
19377
+ ) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19258
19378
  throw new Error("Command expired. Try again while this computer is online.");
19259
19379
  const outcome = await this.command(pair, content);
19260
19380
  delete this.config.unconfirmed[content.requestId];
@@ -19461,6 +19581,16 @@ var Connector = class {
19461
19581
  );
19462
19582
  return commandAcp(agent, command2.requestId, { kind: "cancel" });
19463
19583
  }
19584
+ case "thread.configure": {
19585
+ const { option: option2, value: value2 } = command2.action;
19586
+ const native = thread && this.discovery.locations.get(thread.id)?.live;
19587
+ const known = thread?.options?.find((item) => item.id === option2);
19588
+ if (!native?.configure || !known)
19589
+ throw new Error("This session can only be changed on your computer.");
19590
+ if (typeof value2 !== typeof known.value || known.choices && !known.choices.some((choice) => choice.value === value2))
19591
+ throw new Error(`${known.name} can't be set to that.`);
19592
+ return native.configure(command2.requestId, option2, value2);
19593
+ }
19464
19594
  case "thread.queue": {
19465
19595
  if (!thread) throw new Error("Thread not found");
19466
19596
  this.nativeQueue.refresh();
@@ -19543,10 +19673,10 @@ var Connector = class {
19543
19673
 
19544
19674
  // src/install.ts
19545
19675
  import { existsSync as existsSync5 } from "node:fs";
19546
- import { homedir as homedir6, userInfo } from "node:os";
19676
+ import { homedir as homedir7, userInfo } from "node:os";
19547
19677
  import { promisify as promisify6 } from "node:util";
19548
19678
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19549
- import { execFile as execFile6, spawn as spawn5 } from "node:child_process";
19679
+ import { execFile as execFile6, spawn as spawn6 } from "node:child_process";
19550
19680
  import { cp, mkdir as mkdir3, rm as rm2, writeFile } from "node:fs/promises";
19551
19681
  import { dirname as dirname7, join as join10, relative, sep } from "node:path";
19552
19682
  var run3 = promisify6(execFile6);
@@ -19555,10 +19685,10 @@ var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
19555
19685
  var systemctl = (...args2) => run3("systemctl", ["--user", ...args2], {
19556
19686
  env: { ...process.env, XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${userInfo().uid}` }
19557
19687
  });
19558
- var serviceFile = () => process.platform === "darwin" ? join10(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join10(
19559
- process.env.APPDATA ?? join10(homedir6(), "AppData/Roaming"),
19688
+ var serviceFile = () => process.platform === "darwin" ? join10(homedir7(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join10(
19689
+ process.env.APPDATA ?? join10(homedir7(), "AppData/Roaming"),
19560
19690
  "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
19561
- ) : join10(homedir6(), ".config/systemd/user/dashup.service");
19691
+ ) : join10(homedir7(), ".config/systemd/user/dashup.service");
19562
19692
  var startupInstalled = () => existsSync5(serviceFile());
19563
19693
  function service(platform, node, entry, config2) {
19564
19694
  const command2 = [node, ...NODE_FLAGS, entry];
@@ -19627,7 +19757,7 @@ async function start(path) {
19627
19757
  });
19628
19758
  await run3("launchctl", ["load", path]);
19629
19759
  } else if (process.platform === "win32") {
19630
- spawn5("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19760
+ spawn6("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19631
19761
  } else {
19632
19762
  const note = process.env.DISPLAY || process.env.WAYLAND_DISPLAY ? void 0 : await run3("loginctl", ["enable-linger"]).then(
19633
19763
  () => void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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/protocol": "0.1.0",
34
+ "@dashup/typescript-config": "0.0.0",
33
35
  "@dashup/models": "0.1.0",
34
- "@parcae/lynx": "0.1.0",
35
36
  "@parcae/sdk": "0.8.1",
36
- "@dashup/typescript-config": "0.0.0",
37
- "@dashup/protocol": "0.1.0"
37
+ "@parcae/lynx": "0.1.0"
38
38
  },
39
39
  "scripts": {
40
40
  "dashup": "tsx src/index.ts",