@dashup/connector 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +310 -162
  3. package/package.json +4 -4
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 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
+
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.
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.13";
4051
+ var version = "0.1.15";
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;
@@ -16780,15 +16812,224 @@ 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, readlink, 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 offered = /* @__PURE__ */ new Map();
16833
+ var asking = /* @__PURE__ */ new Set();
16834
+ var forgetModels = () => offered.clear();
16835
+ function claudeModels(changed, command2 = "claude", version2 = "") {
16836
+ const key = `${command2}\0${version2}`;
16837
+ if (!offered.has(key) && !asking.has(key)) {
16838
+ asking.add(key);
16839
+ void listModels(command2).then((models) => {
16840
+ asking.delete(key);
16841
+ if (!models.length) return;
16842
+ offered.set(key, models);
16843
+ changed();
16844
+ });
16845
+ }
16846
+ return offered.get(key) ?? [];
16847
+ }
16848
+ function listModels(command2) {
16849
+ return new Promise((resolve3) => {
16850
+ const child = spawn2(
16851
+ command2,
16852
+ ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
16853
+ { cwd: homedir5(), stdio: "pipe", shell: process.platform === "win32" }
16854
+ );
16855
+ const timer = setTimeout(() => child.kill(), MODELS_MS);
16856
+ child.stdin.on("error", () => {
16857
+ });
16858
+ child.once("error", () => resolve3([]));
16859
+ child.once("exit", () => {
16860
+ clearTimeout(timer);
16861
+ resolve3([]);
16862
+ });
16863
+ createInterface2({ input: child.stdout }).on("line", (line) => {
16864
+ try {
16865
+ const message = JSON.parse(line);
16866
+ if (message?.type !== "control_response" || message.response?.request_id !== "models")
16867
+ return;
16868
+ const models = message.response.response?.models;
16869
+ resolve3(
16870
+ (Array.isArray(models) ? models : []).filter(
16871
+ (model) => typeof model?.value === "string" && typeof model.displayName === "string"
16872
+ )
16873
+ );
16874
+ child.stdin.end();
16875
+ } catch {
16876
+ }
16877
+ });
16878
+ child.stdin.write(
16879
+ `${JSON.stringify({ type: "control_request", request_id: "models", request: { subtype: "initialize" } })}
16880
+ `
16881
+ );
16882
+ });
16883
+ }
16884
+ function claudeOptions(models, recorded) {
16885
+ const ran = typeof recorded.model === "string" ? recorded.model : "";
16886
+ const current = models.find((model) => model.value !== "default" && model.resolvedModel === ran) ?? models.find((model) => model.value === ran);
16887
+ const choices = models.map((model) => ({ value: model.value, name: model.displayName }));
16888
+ if (ran && !current) choices.push({ value: ran, name: ran });
16889
+ if (!choices.length) return [];
16890
+ return [
16891
+ { id: "model", name: "Model", value: current?.value ?? ran, choices },
16892
+ ...current?.supportsFastMode ? [{ id: "fast", name: "Fast", value: recorded.fast === true }] : [],
16893
+ ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16894
+ ];
16895
+ }
16896
+ var MAX_SESSIONS2 = 100;
16897
+ var MAX_REQUESTS = 1e3;
16898
+ var SESSION = /^\/sessions\/([\w-]{1,100})$/;
16899
+ var STATUS = {
16900
+ busy: "working",
16901
+ idle: "idle",
16902
+ shell: "idle",
16903
+ waiting: "attention"
16904
+ };
16905
+ async function registrations() {
16906
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16907
+ const directory2 = join5(claudeHome(), "sessions");
16908
+ const names = await readdir2(directory2).catch(() => []);
16909
+ const found = await Promise.all(
16910
+ names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
16911
+ try {
16912
+ const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
16913
+ if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
16914
+ process.kill(value2.pid, 0);
16915
+ const binary2 = await readlink(`/proc/${value2.pid}/exe`).catch(() => "");
16916
+ return [{ ...value2, command: /[\\/]claude(?:\.exe)?$/.test(binary2) ? binary2 : "claude" }];
16917
+ } catch {
16918
+ return [];
16919
+ }
16920
+ })
16921
+ );
16922
+ return found.flat();
16923
+ }
16924
+ var Claude = class {
16925
+ constructor(changed, socket = join5(dirname4(configPath()), "claude.sock")) {
16926
+ this.changed = changed;
16927
+ this.socket = socket;
16928
+ }
16929
+ changed;
16930
+ socket;
16931
+ watcher;
16932
+ notify;
16933
+ server;
16934
+ streams = /* @__PURE__ */ new Map();
16935
+ requests = /* @__PURE__ */ new Map();
16936
+ async refresh() {
16937
+ this.watch();
16938
+ return (await registrations()).map((session) => {
16939
+ const plugin = this.streams.has(session.sessionId);
16940
+ const models = claudeModels(this.changed, session.command, session.version);
16941
+ return {
16942
+ id: session.sessionId,
16943
+ harness: "claude",
16944
+ cwd: session.cwd,
16945
+ updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
16946
+ status: STATUS[session.status ?? ""] ?? "unknown",
16947
+ messages: [],
16948
+ approvals: [],
16949
+ send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
16950
+ stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
16951
+ now: plugin,
16952
+ options: (recorded) => claudeOptions(models, recorded),
16953
+ configure: plugin ? (requestId, option2, value2) => this.once(requestId, () => this.deliver(session.sessionId, { text: COMMANDS[option2](value2) })) : void 0
16954
+ };
16955
+ });
16956
+ }
16957
+ // Only the connector serves the plugin; one-off commands such as `dashup doctor` read alone.
16958
+ async listen() {
16959
+ if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16960
+ const server = this.server = createServer2((request2, response) => {
16961
+ const id = request2.method === "GET" ? SESSION.exec(request2.url ?? "")?.[1] : void 0;
16962
+ if (!id) return void response.writeHead(404).end();
16963
+ this.streams.get(id)?.end();
16964
+ this.streams.set(id, response);
16965
+ response.once("close", () => {
16966
+ if (this.streams.get(id) === response) this.streams.delete(id);
16967
+ this.changed();
16968
+ });
16969
+ response.writeHead(200, { "Content-Type": "application/x-ndjson" }).flushHeaders();
16970
+ this.changed();
16971
+ });
16972
+ await mkdir(dirname4(this.socket), { recursive: true, mode: 448 });
16973
+ await rm(this.socket, { force: true });
16974
+ if (this.server !== server) return;
16975
+ await new Promise((resolve3, reject) => {
16976
+ server.once("error", reject);
16977
+ server.listen(this.socket, resolve3);
16978
+ });
16979
+ }
16980
+ deliver(id, delivery) {
16981
+ const stream = this.streams.get(id);
16982
+ if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
16983
+ return new Promise((resolve3, reject) => stream.write(`${JSON.stringify(delivery)}
16984
+ `, (error) => error ? reject(error) : resolve3()));
16985
+ }
16986
+ // Each request delivers at most once.
16987
+ once(id, action) {
16988
+ const known = this.requests.get(id);
16989
+ if (known) return known;
16990
+ if (this.requests.size >= MAX_REQUESTS)
16991
+ this.requests.delete(this.requests.keys().next().value);
16992
+ const result = action();
16993
+ this.requests.set(id, result);
16994
+ return result;
16995
+ }
16996
+ watch() {
16997
+ if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16998
+ try {
16999
+ const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
17000
+ if (this.notify) return;
17001
+ this.notify = setTimeout(() => {
17002
+ this.notify = void 0;
17003
+ this.changed();
17004
+ }, 100);
17005
+ this.notify.unref();
17006
+ });
17007
+ watcher.on("error", () => {
17008
+ watcher.close();
17009
+ if (this.watcher === watcher) this.watcher = void 0;
17010
+ });
17011
+ } catch {
17012
+ }
17013
+ }
17014
+ stop() {
17015
+ clearTimeout(this.notify);
17016
+ this.watcher?.close();
17017
+ this.watcher = void 0;
17018
+ this.server?.closeAllConnections();
17019
+ this.server?.close();
17020
+ this.server = void 0;
17021
+ }
17022
+ };
17023
+
17024
+ // src/harness/headless.ts
16785
17025
  var CHOICES = [
16786
17026
  { id: "accept", label: "Allow once" },
16787
17027
  { id: "decline", label: "Reject" }
16788
17028
  ];
16789
17029
  var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16790
- var MAX_REQUESTS = 1e3;
17030
+ var MAX_REQUESTS2 = 1e3;
16791
17031
  var NOTIFY_MS = 100;
17032
+ var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
16792
17033
  function hunk(before, after, offset = 0) {
16793
17034
  const old = before ? before.replace(/\n$/, "").split("\n") : [];
16794
17035
  const next = after ? after.replace(/\n$/, "").split("\n") : [];
@@ -16881,6 +17122,8 @@ var Session = class {
16881
17122
  typing = "";
16882
17123
  approvals = /* @__PURE__ */ new Map();
16883
17124
  requests = /* @__PURE__ */ new Map();
17125
+ // What the person chose for this session, applied to each turn as it starts.
17126
+ chosen = {};
16884
17127
  write(value2) {
16885
17128
  this.turn?.stdin.write(`${JSON.stringify(value2)}
16886
17129
  `);
@@ -16892,10 +17135,17 @@ var Session = class {
16892
17135
  request: { subtype: "interrupt" }
16893
17136
  });
16894
17137
  }
17138
+ apply(option2, value2) {
17139
+ this.write({
17140
+ type: "control_request",
17141
+ request_id: randomUUID3(),
17142
+ request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
17143
+ });
17144
+ }
16895
17145
  once(id, action) {
16896
17146
  const known = this.requests.get(id);
16897
17147
  if (known) return known;
16898
- if (this.requests.size >= MAX_REQUESTS)
17148
+ if (this.requests.size >= MAX_REQUESTS2)
16899
17149
  throw new Error("Claude Code command history is full; restart Dashup before sending");
16900
17150
  const result = action();
16901
17151
  this.requests.set(id, result);
@@ -16916,7 +17166,7 @@ var Session = class {
16916
17166
  this.changed();
16917
17167
  return Promise.resolve();
16918
17168
  }
16919
- const turn = this.turn = spawn2(
17169
+ const turn = this.turn = spawn3(
16920
17170
  "claude",
16921
17171
  [
16922
17172
  "-p",
@@ -16940,7 +17190,7 @@ var Session = class {
16940
17190
  errors = (errors + String(chunk)).slice(-2e3);
16941
17191
  });
16942
17192
  const started = new Promise((resolve3, reject) => {
16943
- createInterface2({ input: turn.stdout }).on("line", (line) => {
17193
+ createInterface3({ input: turn.stdout }).on("line", (line) => {
16944
17194
  if (this.receive(line)) resolve3();
16945
17195
  });
16946
17196
  const end = (reason) => {
@@ -16955,6 +17205,7 @@ var Session = class {
16955
17205
  turn.once("error", () => end("Claude Code is not installed on this computer"));
16956
17206
  turn.once("exit", () => end(errors.trim() || "Claude Code stopped before starting"));
16957
17207
  });
17208
+ for (const [option2, value2] of Object.entries(this.chosen)) this.apply(option2, value2);
16958
17209
  this.write(message);
16959
17210
  this.changed();
16960
17211
  return started;
@@ -16996,7 +17247,7 @@ var Session = class {
16996
17247
  this.changed();
16997
17248
  return init;
16998
17249
  }
16999
- live() {
17250
+ live(models) {
17000
17251
  return {
17001
17252
  id: this.id,
17002
17253
  cwd: this.cwd,
@@ -17009,6 +17260,11 @@ var Session = class {
17009
17260
  approvals: [...this.approvals.values()].map((pending) => pending.approval),
17010
17261
  send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17011
17262
  now: true,
17263
+ options: (recorded) => claudeOptions(models, recorded),
17264
+ configure: (requestId, option2, value2) => this.once(requestId, async () => {
17265
+ this.chosen[option2] = value2;
17266
+ this.apply(option2, value2);
17267
+ }),
17012
17268
  stop: this.turn ? (requestId) => this.once(requestId, async () => {
17013
17269
  if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17014
17270
  this.interrupt();
@@ -17076,7 +17332,8 @@ var Headless = class {
17076
17332
  }
17077
17333
  }
17078
17334
  async refresh() {
17079
- return [...this.sessions.values()].map((session) => session.live());
17335
+ const models = claudeModels(this.changed);
17336
+ return [...this.sessions.values()].map((session) => session.live(models));
17080
17337
  }
17081
17338
  stop() {
17082
17339
  clearTimeout(this.notify);
@@ -17084,138 +17341,9 @@ var Headless = class {
17084
17341
  }
17085
17342
  };
17086
17343
 
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
17344
  // src/harness/vscode.ts
17217
17345
  import Socket3 from "ws";
17218
- import { homedir as homedir5 } from "node:os";
17346
+ import { homedir as homedir6 } from "node:os";
17219
17347
  import { connect as connect2 } from "node:net";
17220
17348
  import { promisify as promisify3 } from "node:util";
17221
17349
  import { fileURLToPath } from "node:url";
@@ -17242,7 +17370,7 @@ function roots() {
17242
17370
  if (override !== void 0)
17243
17371
  return override ? [basename2(override) === "User" ? dirname5(override) : override] : [];
17244
17372
  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");
17373
+ 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
17374
  return ["Code", "Code - Insiders"].map((name) => join6(base, name));
17247
17375
  }
17248
17376
  async function privateFile(path, limit) {
@@ -17715,8 +17843,8 @@ var VSCode = class {
17715
17843
  import { join as join7 } from "node:path";
17716
17844
  import { promisify as promisify4 } from "node:util";
17717
17845
  import { createHash as createHash4 } from "node:crypto";
17718
- import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
17719
- import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17846
+ import { execFile as execFile4, spawn as spawn4 } from "node:child_process";
17847
+ import { stat as stat3, readlink as readlink2, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17720
17848
  var run2 = promisify4(execFile4);
17721
17849
  var REQUEST_MS3 = 4e3;
17722
17850
  var MAX_BYTES3 = 8 * 1024 * 1024;
@@ -17771,7 +17899,7 @@ async function linuxEndpoints() {
17771
17899
  })
17772
17900
  );
17773
17901
  for (const fd of (await readdir4(join7(directory2, "fd"))).slice(0, 1024)) {
17774
- const target = await readlink(join7(directory2, "fd", fd)).catch(() => "");
17902
+ const target = await readlink2(join7(directory2, "fd", fd)).catch(() => "");
17775
17903
  const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
17776
17904
  const url2 = inode ? listeners.get(inode) : void 0;
17777
17905
  if (url2) result.push({ url: url2, authorization: authorization(environment) });
@@ -18183,7 +18311,7 @@ var Server = class {
18183
18311
  }
18184
18312
  };
18185
18313
  function serve(cwd) {
18186
- const child = spawn3("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18314
+ const child = spawn4("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18187
18315
  cwd,
18188
18316
  stdio: ["ignore", "pipe", "ignore"],
18189
18317
  shell: process.platform === "win32"
@@ -18304,9 +18432,10 @@ var LiveHarnesses = class {
18304
18432
  });
18305
18433
  return this.refreshing;
18306
18434
  }
18307
- // The agents whose own command runs on this computer.
18435
+ // The agents whose own command runs on this computer. Claude Code is asked its models again.
18308
18436
  async agents() {
18309
18437
  const harnesses = ["claude", "codex", "opencode"];
18438
+ forgetModels();
18310
18439
  const runs = await Promise.all(
18311
18440
  harnesses.map((harness) => promisify5(execFile5)(harness, ["--version"], {
18312
18441
  timeout: 1e4,
@@ -18365,7 +18494,7 @@ var LiveHarnesses = class {
18365
18494
 
18366
18495
  // src/harness/queue.ts
18367
18496
  import { randomUUID as randomUUID5 } from "node:crypto";
18368
- import { spawn as spawn4 } from "node:child_process";
18497
+ import { spawn as spawn5 } from "node:child_process";
18369
18498
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18370
18499
  import { join as join8, dirname as dirname6, basename as basename3 } from "node:path";
18371
18500
  import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
@@ -18562,7 +18691,7 @@ var Writer = class {
18562
18691
  tail = "";
18563
18692
  pending = /* @__PURE__ */ new Map();
18564
18693
  constructor(owner) {
18565
- this.process = spawn4(owner.executable, ["app-server", "--listen", "stdio://"], {
18694
+ this.process = spawn5(owner.executable, ["app-server", "--listen", "stdio://"], {
18566
18695
  cwd: owner.cwd,
18567
18696
  env: { ...process.env, CODEX_HOME: owner.home },
18568
18697
  stdio: ["pipe", "pipe", "ignore"]
@@ -18739,6 +18868,11 @@ var Command = z2.object({
18739
18868
  }).strict(),
18740
18869
  z2.object({ kind: z2.literal("thread.queue"), text: z2.string().min(1).max(1e5) }).strict(),
18741
18870
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18871
+ z2.object({
18872
+ kind: z2.literal("thread.configure"),
18873
+ option: z2.string().min(1).max(100),
18874
+ value: z2.union([z2.string().min(1).max(200), z2.boolean()])
18875
+ }).strict(),
18742
18876
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18743
18877
  z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18744
18878
  z2.object({
@@ -19078,13 +19212,15 @@ var Connector = class {
19078
19212
  const detached = {
19079
19213
  ...snapshot.thread,
19080
19214
  status: "unknown",
19215
+ options: void 0,
19081
19216
  capabilities: {
19082
19217
  ...snapshot.thread.capabilities,
19083
19218
  send: false,
19084
19219
  approve: false,
19085
19220
  // A separate queue owner can remain valid after its live socket detaches.
19086
19221
  queue: thread?.capabilities.queue ?? false,
19087
- stop: false
19222
+ stop: false,
19223
+ configure: false
19088
19224
  }
19089
19225
  };
19090
19226
  if (thread) Object.assign(thread, detached);
@@ -19254,7 +19390,9 @@ var Connector = class {
19254
19390
  async execute(pair, content, envelope) {
19255
19391
  let queue = void 0;
19256
19392
  try {
19257
- if (["approval.respond", "thread.stop", "session.start"].includes(content.action.kind) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19393
+ if (["approval.respond", "thread.stop", "thread.configure", "session.start"].includes(
19394
+ content.action.kind
19395
+ ) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19258
19396
  throw new Error("Command expired. Try again while this computer is online.");
19259
19397
  const outcome = await this.command(pair, content);
19260
19398
  delete this.config.unconfirmed[content.requestId];
@@ -19461,6 +19599,16 @@ var Connector = class {
19461
19599
  );
19462
19600
  return commandAcp(agent, command2.requestId, { kind: "cancel" });
19463
19601
  }
19602
+ case "thread.configure": {
19603
+ const { option: option2, value: value2 } = command2.action;
19604
+ const native = thread && this.discovery.locations.get(thread.id)?.live;
19605
+ const known = thread?.options?.find((item) => item.id === option2);
19606
+ if (!native?.configure || !known)
19607
+ throw new Error("This session can only be changed on your computer.");
19608
+ if (typeof value2 !== typeof known.value || known.choices && !known.choices.some((choice) => choice.value === value2))
19609
+ throw new Error(`${known.name} can't be set to that.`);
19610
+ return native.configure(command2.requestId, option2, value2);
19611
+ }
19464
19612
  case "thread.queue": {
19465
19613
  if (!thread) throw new Error("Thread not found");
19466
19614
  this.nativeQueue.refresh();
@@ -19543,10 +19691,10 @@ var Connector = class {
19543
19691
 
19544
19692
  // src/install.ts
19545
19693
  import { existsSync as existsSync5 } from "node:fs";
19546
- import { homedir as homedir6, userInfo } from "node:os";
19694
+ import { homedir as homedir7, userInfo } from "node:os";
19547
19695
  import { promisify as promisify6 } from "node:util";
19548
19696
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19549
- import { execFile as execFile6, spawn as spawn5 } from "node:child_process";
19697
+ import { execFile as execFile6, spawn as spawn6 } from "node:child_process";
19550
19698
  import { cp, mkdir as mkdir3, rm as rm2, writeFile } from "node:fs/promises";
19551
19699
  import { dirname as dirname7, join as join10, relative, sep } from "node:path";
19552
19700
  var run3 = promisify6(execFile6);
@@ -19555,10 +19703,10 @@ var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
19555
19703
  var systemctl = (...args2) => run3("systemctl", ["--user", ...args2], {
19556
19704
  env: { ...process.env, XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR ?? `/run/user/${userInfo().uid}` }
19557
19705
  });
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"),
19706
+ var serviceFile = () => process.platform === "darwin" ? join10(homedir7(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join10(
19707
+ process.env.APPDATA ?? join10(homedir7(), "AppData/Roaming"),
19560
19708
  "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
19561
- ) : join10(homedir6(), ".config/systemd/user/dashup.service");
19709
+ ) : join10(homedir7(), ".config/systemd/user/dashup.service");
19562
19710
  var startupInstalled = () => existsSync5(serviceFile());
19563
19711
  function service(platform, node, entry, config2) {
19564
19712
  const command2 = [node, ...NODE_FLAGS, entry];
@@ -19627,7 +19775,7 @@ async function start(path) {
19627
19775
  });
19628
19776
  await run3("launchctl", ["load", path]);
19629
19777
  } else if (process.platform === "win32") {
19630
- spawn5("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19778
+ spawn6("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19631
19779
  } else {
19632
19780
  const note = process.env.DISPLAY || process.env.WAYLAND_DISPLAY ? void 0 : await run3("loginctl", ["enable-linger"]).then(
19633
19781
  () => void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",
34
33
  "@dashup/protocol": "0.1.0",
35
- "@dashup/typescript-config": "0.0.0",
36
34
  "@parcae/lynx": "0.1.0",
37
- "@parcae/sdk": "0.8.1"
35
+ "@parcae/sdk": "0.8.1",
36
+ "@dashup/typescript-config": "0.0.0",
37
+ "@dashup/models": "0.1.0"
38
38
  },
39
39
  "scripts": {
40
40
  "dashup": "tsx src/index.ts",