@dashup/connector 0.1.10 → 0.1.12

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
@@ -37,7 +37,7 @@ Dashup discovers existing local OpenCode listeners, Codex control sockets, and V
37
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.
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. Direct control of an ordinary foreground Claude process is not advertised when it exposes no supported attach interface.
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.
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
- Stop interrupts the running turn through each live interface: OpenCode's session abort, Codex `turn/interrupt`, the AHP client's turn cancellation, and ACP `session/cancel`. It refuses when nothing is running and expires with other commands after 60 seconds, so a late stop cannot end a newer turn.
50
+ 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
51
 
52
52
  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.
53
53
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "dashup",
3
+ "owner": { "name": "Dashup" },
4
+ "plugins": [{ "name": "dashup", "source": "./" }]
5
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "dashup",
3
+ "description": "Takes what you send this session from the Dashup app as your own words"
4
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.ts"] }
@@ -0,0 +1,69 @@
1
+ import type { EngineInterface, Register } from 'claude-code';
2
+
3
+ // =============================================================================
4
+ // Replies from the Dashup app
5
+ // =============================================================================
6
+
7
+ // Claude Code reads what other processes post to a session's inbox as another session's request,
8
+ // never the person's. Dashup's connector instead streams what the person sends this session from
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.
11
+ type Delivery = { text?: string; now?: true };
12
+
13
+ const RETRY_MS = 10_000;
14
+
15
+ export const register: Register = (on) => {
16
+ let turnId: string | undefined;
17
+ let stream: AsyncGenerator<unknown, unknown> | undefined;
18
+ on('turn.start', ($, e, next) => {
19
+ if (e.agentId === undefined) turnId = e.turnId;
20
+ return next(e);
21
+ });
22
+ // `/clear` starts another conversation in this process, which streams under its own id.
23
+ on('session.end', ($, e, next) => {
24
+ void stream?.return(undefined);
25
+ return next(e);
26
+ });
27
+ on('session.start', async ($, e, next) => {
28
+ const started = await next(e);
29
+ void (async () => {
30
+ const socketPath = `${await $.env.get('HOME')}/.dashup/claude.sock`;
31
+ for (;;) {
32
+ // A request held open here would hold the session's queue; a child's stream does not.
33
+ const lines = (stream = $.process.spawn({
34
+ argv: [
35
+ 'curl',
36
+ '-sfN',
37
+ '--unix-socket',
38
+ socketPath,
39
+ `http://dashup/sessions/${await $.session.id()}`,
40
+ ],
41
+ }));
42
+ let buffer = '';
43
+ try {
44
+ for await (const { stream: name, text } of lines) {
45
+ if (name !== 'stdout') continue;
46
+ buffer += text;
47
+ for (let end = buffer.indexOf('\n'); end !== -1; end = buffer.indexOf('\n')) {
48
+ await take($, JSON.parse(buffer.slice(0, end)) as Delivery, turnId);
49
+ buffer = buffer.slice(end + 1);
50
+ }
51
+ }
52
+ } catch {
53
+ // curl is missing, or the stream broke off mid-line.
54
+ }
55
+ // Dashup is not running on this computer, or is restarting.
56
+ await $.clock.sleep(RETRY_MS);
57
+ }
58
+ })();
59
+ return started;
60
+ });
61
+ };
62
+
63
+ // A message runs once the session is idle; the stream must not wait for that.
64
+ async function take($: EngineInterface, { text, now }: Delivery, turnId?: string): Promise<void> {
65
+ if (now && turnId) await $.turn.abort({ turnId }).catch(() => {});
66
+ const command = text?.match(/^\/(\S+)\s*([\s\S]*)$/);
67
+ if (command) void $.command.run({ command: command[1]!, args: command[2] }).catch(() => {});
68
+ else if (text) void $.prompt.submit({ text, asUser: true }).catch(() => {});
69
+ }
package/dist/index.js CHANGED
@@ -2149,15 +2149,15 @@ var require_short_unique_id = __commonJS({
2149
2149
  this.lowerBound = this.dictRange[0];
2150
2150
  this.upperBound = this.dictRange[1];
2151
2151
  const isAscending = this.lowerBound <= this.upperBound;
2152
- const start = this.lowerBound;
2152
+ const start2 = this.lowerBound;
2153
2153
  const end = this.upperBound;
2154
2154
  if (isAscending) {
2155
- for (let i = start; i < end; i++) {
2155
+ for (let i = start2; i < end; i++) {
2156
2156
  finalDict[dictIdx++] = String.fromCharCode(i);
2157
2157
  this.dictIndex = i;
2158
2158
  }
2159
2159
  } else {
2160
- for (let i = start; i > end; i--) {
2160
+ for (let i = start2; i > end; i--) {
2161
2161
  finalDict[dictIdx++] = String.fromCharCode(i);
2162
2162
  this.dictIndex = i;
2163
2163
  }
@@ -2706,7 +2706,7 @@ var require_pluralize = __commonJS({
2706
2706
 
2707
2707
  // src/index.ts
2708
2708
  import { randomBytes as randomBytes5, timingSafeEqual } from "node:crypto";
2709
- import { createServer as createServer2 } from "node:http";
2709
+ import { createServer as createServer3 } from "node:http";
2710
2710
  import * as ui2 from "@clack/prompts";
2711
2711
  import { z as z3 } from "zod";
2712
2712
 
@@ -2728,6 +2728,7 @@ var PROTOCOL_VERSION = 1;
2728
2728
  var MAX_ENVELOPE_BYTES = 8 * 1024 * 1024;
2729
2729
  var HISTORY_PAGE_ITEMS = 30;
2730
2730
  var HISTORY_TEXT_CHARS = 4e3;
2731
+ var HISTORY_TOOL_CHARS = 500;
2731
2732
  var HISTORY_PAGE_BYTES = 256 * 1024;
2732
2733
  var HISTORY_BATCH_ITEMS = 50;
2733
2734
  var HISTORY_BATCH_BYTES = HISTORY_PAGE_BYTES;
@@ -2741,9 +2742,11 @@ var MESSAGE_ROLES = ["user", "assistant", "system", "tool"];
2741
2742
  var HARNESSES = {
2742
2743
  claude: "Claude Code",
2743
2744
  codex: "Codex",
2745
+ kimi: "Kimi",
2744
2746
  opencode: "OpenCode",
2745
2747
  other: "Agent"
2746
2748
  };
2749
+ var isStartHarness = (harness) => harness !== "other" && harness !== "kimi";
2747
2750
 
2748
2751
  // ../../node_modules/.pnpm/@noble+ciphers@2.4.0/node_modules/@noble/ciphers/utils.js
2749
2752
  function isBytes(a) {
@@ -3868,16 +3871,16 @@ function attachAcp(socket, onEvent) {
3868
3871
  async function proxyAcp(config2, executable, args2) {
3869
3872
  if (!config2.localSecret) throw new Error("Start dashup watch before using the ACP proxy");
3870
3873
  const socket = await new Promise((resolve3, reject) => {
3871
- const connect4 = request({
3874
+ const connect3 = request({
3872
3875
  hostname: "127.0.0.1",
3873
3876
  port: connectorPort(),
3874
3877
  method: "CONNECT",
3875
3878
  path: "/acp",
3876
3879
  headers: { Authorization: `Bearer ${config2.localSecret}` }
3877
3880
  });
3878
- connect4.on("connect", (response, stream) => response.statusCode === 200 ? resolve3(stream) : reject(new Error("Dashup ACP connection was rejected")));
3879
- connect4.on("error", reject);
3880
- connect4.end();
3881
+ connect3.on("connect", (response, stream) => response.statusCode === 200 ? resolve3(stream) : reject(new Error("Dashup ACP connection was rejected")));
3882
+ connect3.on("error", reject);
3883
+ connect3.end();
3881
3884
  });
3882
3885
  const child = spawn(executable, args2, { stdio: ["pipe", "pipe", "inherit"], windowsHide: true });
3883
3886
  const local = /* @__PURE__ */ new Set();
@@ -4045,7 +4048,7 @@ import * as ui from "@clack/prompts";
4045
4048
  import { z } from "zod";
4046
4049
 
4047
4050
  // package.json
4048
- var version = "0.1.10";
4051
+ var version = "0.1.12";
4049
4052
 
4050
4053
  // src/pair.ts
4051
4054
  var Invitation = z.object({
@@ -4199,6 +4202,7 @@ var sessionKey = (session) => `${session.harness}\0${session.id}`;
4199
4202
  function agentHarness(name) {
4200
4203
  if (/claude/i.test(name)) return "claude";
4201
4204
  if (/codex/i.test(name)) return "codex";
4205
+ if (/kimi/i.test(name)) return "kimi";
4202
4206
  if (/opencode/i.test(name)) return "opencode";
4203
4207
  return void 0;
4204
4208
  }
@@ -4239,13 +4243,13 @@ function records(buffer) {
4239
4243
  function unchanged(left, right) {
4240
4244
  return left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
4241
4245
  }
4242
- function readRange(path, start, length) {
4246
+ function readRange(path, start2, length) {
4243
4247
  const fd = openSync2(path, "r");
4244
4248
  try {
4245
4249
  const buffer = Buffer.alloc(length);
4246
4250
  let offset = 0;
4247
4251
  while (offset < length) {
4248
- const count = readSync(fd, buffer, offset, length - offset, start + offset);
4252
+ const count = readSync(fd, buffer, offset, length - offset, start2 + offset);
4249
4253
  if (!count) break;
4250
4254
  offset += count;
4251
4255
  }
@@ -4273,12 +4277,12 @@ function described(path, until, describe2) {
4273
4277
  }
4274
4278
  let tail = [];
4275
4279
  for (let bytes = SAMPLE_BYTES; stamp.size > head.length; bytes *= 2) {
4276
- const start = Math.max(head.length, stamp.size - bytes);
4277
- const buffer = readRange(path, start, stamp.size - start);
4280
+ const start2 = Math.max(head.length, stamp.size - bytes);
4281
+ const buffer = readRange(path, start2, stamp.size - start2);
4278
4282
  tail = records(
4279
- readRange(path, start - 1, 1)[0] === 10 ? buffer : buffer.subarray(buffer.indexOf(10) + 1)
4283
+ readRange(path, start2 - 1, 1)[0] === 10 ? buffer : buffer.subarray(buffer.indexOf(10) + 1)
4280
4284
  );
4281
- if (start === head.length || tail.some(until)) break;
4285
+ if (start2 === head.length || tail.some(until)) break;
4282
4286
  }
4283
4287
  const value2 = describe2([...records(head), ...tail], stamp);
4284
4288
  metadata.set(path, { stamp, value: value2 });
@@ -4295,8 +4299,8 @@ function journal(path, latest = false) {
4295
4299
  return tail2 ? [...saved.rows, tail2] : saved.rows;
4296
4300
  }
4297
4301
  if (!saved || saved.stamp.ino !== stamp.ino || stamp.size <= saved.offset || latest && stamp.size - saved.start > 2 * LATEST_BYTES) {
4298
- const start = latest ? Math.max(0, stamp.size - LATEST_BYTES) : 0;
4299
- saved = { stamp, start, offset: start, tail: Buffer.alloc(0), rows: [] };
4302
+ const start2 = latest ? Math.max(0, stamp.size - LATEST_BYTES) : 0;
4303
+ saved = { stamp, start: start2, offset: start2, tail: Buffer.alloc(0), rows: [] };
4300
4304
  }
4301
4305
  while (saved.offset < stamp.size) {
4302
4306
  const chunk = readRange(path, saved.offset, Math.min(READ_BYTES, stamp.size - saved.offset));
@@ -4390,7 +4394,6 @@ function prose(id, role, content) {
4390
4394
  const files2 = attachments(id, content);
4391
4395
  return value2 || files2.length ? [{ id, role, text: value2, ...files2.length ? { attachments: files2 } : {} }] : [];
4392
4396
  }
4393
- var SUMMARY_LIMIT = 200;
4394
4397
  var SUMMARY_FIELDS = [
4395
4398
  "command",
4396
4399
  "cmd",
@@ -4412,11 +4415,15 @@ function describe(input) {
4412
4415
  );
4413
4416
  const value2 = typeof input === "string" ? input : named2 ?? (fields && Object.keys(fields).length ? JSON.stringify(fields) : "");
4414
4417
  const paths = [...value2.matchAll(PATCH_FILE)].map((match) => match[2]).join(", ");
4415
- const first = value2.split("\n").find((line) => line.trim());
4416
- return (paths || first?.trim())?.slice(0, SUMMARY_LIMIT);
4418
+ return (paths || value2.trim()).slice(0, HISTORY_TOOL_CHARS) || void 0;
4419
+ }
4420
+ function said(input, summary) {
4421
+ const value2 = object(input)?.description;
4422
+ const line = typeof value2 === "string" ? value2.trim().split("\n")[0] : "";
4423
+ return line && line !== summary ? line.slice(0, HISTORY_TOOL_CHARS) : void 0;
4417
4424
  }
4418
- function elapsed(start, end) {
4419
- const ms = instant(end) - instant(start);
4425
+ function elapsed(start2, end) {
4426
+ const ms = instant(end) - instant(start2);
4420
4427
  return Number.isFinite(ms) ? Math.max(0, Math.round(ms)) : void 0;
4421
4428
  }
4422
4429
  function added(path, content) {
@@ -4429,18 +4436,22 @@ function added(path, content) {
4429
4436
  ].join("\n");
4430
4437
  }
4431
4438
  var binary = (diff) => diff.slice(0, 8e3).includes("\0");
4432
- function step(id, name, input, result) {
4439
+ function step(id, name, input, result, started) {
4433
4440
  const summary = describe(input);
4441
+ const label = said(input, summary);
4442
+ const at = instant(started);
4434
4443
  const diff = result?.status === "ok" && result.diff && !binary(result.diff) ? result.diff : void 0;
4435
4444
  return {
4436
4445
  id,
4437
4446
  role: "tool",
4438
4447
  text: diff || result?.text || "",
4439
4448
  tool: {
4440
- name: typeof name === "string" && name ? name.slice(0, SUMMARY_LIMIT) : "Tool",
4449
+ name: typeof name === "string" && name ? name.slice(0, HISTORY_TOOL_CHARS) : "Tool",
4450
+ ...label ? { label } : {},
4441
4451
  ...summary ? { summary } : {},
4442
4452
  ...result?.status ? { status: result.status } : {},
4443
4453
  ...result?.status && result.ms !== void 0 ? { ms: result.ms } : {},
4454
+ ...!result?.status && Number.isFinite(at) ? { at: new Date(at).toISOString() } : {},
4444
4455
  ...diff ? { diff: true } : {}
4445
4456
  },
4446
4457
  ...result?.attachments?.length ? { attachments: result.attachments } : {}
@@ -4511,7 +4522,8 @@ function claudeConversation(conversation, rows) {
4511
4522
  ms: elapsed(row.timestamp, result.row.timestamp),
4512
4523
  diff: claudeChange(object(result.row.toolUseResult)),
4513
4524
  attachments: attachments(call, result.part.content)
4514
- }
4525
+ },
4526
+ row.timestamp
4515
4527
  )
4516
4528
  ];
4517
4529
  })
@@ -4600,7 +4612,8 @@ function codexMessages(path) {
4600
4612
  ms: elapsed(row.timestamp, output.timestamp),
4601
4613
  diff: patch && applyPatch(patch),
4602
4614
  attachments: attachments(call, output.payload.output)
4603
- }
4615
+ },
4616
+ row.timestamp
4604
4617
  )
4605
4618
  ];
4606
4619
  });
@@ -4699,7 +4712,8 @@ function openCodeParts(id, role, parts2) {
4699
4712
  ms: elapsed(state.time?.start, state.time?.end),
4700
4713
  diff: openCodeChange(state),
4701
4714
  attachments: attachments(key, state.attachments)
4702
- }
4715
+ },
4716
+ state.time?.start
4703
4717
  )
4704
4718
  ];
4705
4719
  });
@@ -5060,6 +5074,7 @@ var Discovery = class {
5060
5074
  read: true,
5061
5075
  send: Boolean(session.send),
5062
5076
  stop: Boolean(session.stop),
5077
+ now: Boolean(session.now),
5063
5078
  approve: Boolean(session.approve)
5064
5079
  }
5065
5080
  });
@@ -5276,7 +5291,7 @@ var Discovery = class {
5276
5291
  // src/connector.ts
5277
5292
  import { z as z2 } from "zod";
5278
5293
  import { join as join9 } from "node:path";
5279
- import { mkdir } from "node:fs/promises";
5294
+ import { mkdir as mkdir2 } from "node:fs/promises";
5280
5295
  import { randomBytes as randomBytes4 } from "node:crypto";
5281
5296
 
5282
5297
  // ../../node_modules/.pnpm/engine.io-client@6.6.7/node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
@@ -7421,10 +7436,10 @@ var Decoder = class _Decoder extends Emitter {
7421
7436
  throw new Error("unknown packet type " + p.type);
7422
7437
  }
7423
7438
  if (p.type === PacketType.BINARY_EVENT || p.type === PacketType.BINARY_ACK) {
7424
- const start = i + 1;
7439
+ const start2 = i + 1;
7425
7440
  while (str.charAt(++i) !== "-" && i != str.length) {
7426
7441
  }
7427
- const buf = str.substring(start, i);
7442
+ const buf = str.substring(start2, i);
7428
7443
  if (buf != Number(buf) || str.charAt(i) !== "-") {
7429
7444
  throw new Error("Illegal attachments");
7430
7445
  }
@@ -7437,7 +7452,7 @@ var Decoder = class _Decoder extends Emitter {
7437
7452
  p.attachments = n;
7438
7453
  }
7439
7454
  if ("/" === str.charAt(i + 1)) {
7440
- const start = i + 1;
7455
+ const start2 = i + 1;
7441
7456
  while (++i) {
7442
7457
  const c = str.charAt(i);
7443
7458
  if ("," === c)
@@ -7445,13 +7460,13 @@ var Decoder = class _Decoder extends Emitter {
7445
7460
  if (i === str.length)
7446
7461
  break;
7447
7462
  }
7448
- p.nsp = str.substring(start, i);
7463
+ p.nsp = str.substring(start2, i);
7449
7464
  } else {
7450
7465
  p.nsp = "/";
7451
7466
  }
7452
7467
  const next = str.charAt(i + 1);
7453
7468
  if ("" !== next && Number(next) == next) {
7454
- const start = i + 1;
7469
+ const start2 = i + 1;
7455
7470
  while (++i) {
7456
7471
  const c = str.charAt(i);
7457
7472
  if (null == c || Number(c) != c) {
@@ -7461,7 +7476,7 @@ var Decoder = class _Decoder extends Emitter {
7461
7476
  if (i === str.length)
7462
7477
  break;
7463
7478
  }
7464
- p.id = Number(str.substring(start, i + 1));
7479
+ p.id = Number(str.substring(start2, i + 1));
7465
7480
  }
7466
7481
  if (str.charAt(++i)) {
7467
7482
  const payload = this.tryParse(str.substr(i));
@@ -9671,7 +9686,7 @@ var putShortMSB = (s, b) => {
9671
9686
  s.pending_buf[s.pending++] = b >>> 8 & 255;
9672
9687
  s.pending_buf[s.pending++] = b & 255;
9673
9688
  };
9674
- var read_buf = (strm, buf, start, size) => {
9689
+ var read_buf = (strm, buf, start2, size) => {
9675
9690
  let len = strm.avail_in;
9676
9691
  if (len > size) {
9677
9692
  len = size;
@@ -9680,11 +9695,11 @@ var read_buf = (strm, buf, start, size) => {
9680
9695
  return 0;
9681
9696
  }
9682
9697
  strm.avail_in -= len;
9683
- buf.set(strm.input.subarray(strm.next_in, strm.next_in + len), start);
9698
+ buf.set(strm.input.subarray(strm.next_in, strm.next_in + len), start2);
9684
9699
  if (strm.state.wrap === 1) {
9685
- strm.adler = adler32_1(strm.adler, buf, len, start);
9700
+ strm.adler = adler32_1(strm.adler, buf, len, start2);
9686
9701
  } else if (strm.state.wrap === 2) {
9687
- strm.adler = crc32_1(strm.adler, buf, len, start);
9702
+ strm.adler = crc32_1(strm.adler, buf, len, start2);
9688
9703
  }
9689
9704
  strm.next_in += len;
9690
9705
  strm.total_in += len;
@@ -11049,7 +11064,7 @@ var deflate_1$1 = {
11049
11064
  };
11050
11065
  var BAD$1 = 16209;
11051
11066
  var TYPE$1 = 16191;
11052
- var inffast = function inflate_fast(strm, start) {
11067
+ var inffast = function inflate_fast(strm, start2) {
11053
11068
  let _in;
11054
11069
  let last;
11055
11070
  let _out;
@@ -11079,7 +11094,7 @@ var inffast = function inflate_fast(strm, start) {
11079
11094
  last = _in + (strm.avail_in - 5);
11080
11095
  _out = strm.next_out;
11081
11096
  output = strm.output;
11082
- beg = _out - (start - strm.avail_out);
11097
+ beg = _out - (start2 - strm.avail_out);
11083
11098
  end = _out + (strm.avail_out - 257);
11084
11099
  dmax = state.dmax;
11085
11100
  wsize = state.wsize;
@@ -16172,12 +16187,13 @@ var ID = /^[a-f0-9]{64}$/;
16172
16187
  var ROLES = new Set(MESSAGE_ROLES);
16173
16188
  var ROUTING_FIELDS = ["pairing", "sender", "recipient", "project", "thread"];
16174
16189
  var MIME = /^[\w.+-]{1,64}\/[\w.+-]{1,64}$/;
16175
- var TOOL_TEXT = 500;
16176
16190
  function validAttachment(value2) {
16177
16191
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.id === "string" && ID.test(value2.id) && typeof value2.mime === "string" && MIME.test(value2.mime) && Number.isSafeInteger(value2.bytes) && value2.bytes >= 0 && (value2.name === void 0 || typeof value2.name === "string" && value2.name.length <= ATTACHMENT_NAME_CHARS);
16178
16192
  }
16179
16193
  function validTool(value2) {
16180
- return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.name === "string" && value2.name.length <= TOOL_TEXT && (value2.summary === void 0 || typeof value2.summary === "string" && value2.summary.length <= TOOL_TEXT) && (value2.status === void 0 || value2.status === "ok" || value2.status === "error") && (value2.ms === void 0 || Number.isSafeInteger(value2.ms) && value2.ms >= 0) && (value2.diff === void 0 || typeof value2.diff === "boolean");
16194
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.name === "string" && value2.name.length <= HISTORY_TOOL_CHARS && [value2.label, value2.summary].every(
16195
+ (text3) => text3 === void 0 || typeof text3 === "string" && text3.length <= HISTORY_TOOL_CHARS
16196
+ ) && (value2.at === void 0 || typeof value2.at === "string" && !Number.isNaN(Date.parse(value2.at))) && (value2.status === void 0 || value2.status === "ok" || value2.status === "error") && (value2.ms === void 0 || Number.isSafeInteger(value2.ms) && value2.ms >= 0) && (value2.diff === void 0 || typeof value2.diff === "boolean");
16181
16197
  }
16182
16198
  function validMessage(value2) {
16183
16199
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2) && typeof value2.id === "string" && ID.test(value2.id) && ROLES.has(value2.role) && typeof value2.text === "string" && value2.text.length <= HISTORY_TEXT_CHARS && Number.isSafeInteger(value2.part) && Number.isSafeInteger(value2.parts) && value2.part >= 0 && value2.parts > value2.part && (value2.tool === void 0 || value2.part === 0 && validTool(value2.tool)) && (value2.full === void 0 || value2.part === 0 && validAttachment(value2.full)) && (value2.attachments === void 0 || value2.part === 0 && Array.isArray(value2.attachments) && value2.attachments.length <= HISTORY_ATTACHMENTS && value2.attachments.every(validAttachment));
@@ -16186,14 +16202,14 @@ var preview = (text3) => text3.split("\n", OUTPUT_PREVIEW.lines).join("\n").slic
16186
16202
  function splitHistoryMessages(messages5, opaqueId2) {
16187
16203
  return messages5.flatMap((message) => {
16188
16204
  const text3 = [];
16189
- let start = 0;
16205
+ let start2 = 0;
16190
16206
  do {
16191
- let end = Math.min(start + HISTORY_TEXT_CHARS, message.text.length);
16207
+ let end = Math.min(start2 + HISTORY_TEXT_CHARS, message.text.length);
16192
16208
  const code = message.text.charCodeAt(end - 1);
16193
16209
  if (end < message.text.length && code >= 55296 && code <= 56319) end--;
16194
- text3.push(message.text.slice(start, end));
16195
- start = end;
16196
- } while (start < message.text.length);
16210
+ text3.push(message.text.slice(start2, end));
16211
+ start2 = end;
16212
+ } while (start2 < message.text.length);
16197
16213
  const id = opaqueId2(message.id);
16198
16214
  return text3.map((part, index) => ({
16199
16215
  id,
@@ -16290,9 +16306,9 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16290
16306
  const generation = ++local.sequence;
16291
16307
  saveConfig(config2);
16292
16308
  const all = historyMessages(local.key, project, thread.id, messages5);
16293
- let start = Math.max(0, all.length - HISTORY_KEPT);
16294
- while (start < all.length && (all[start].part ?? 0) > 0) start++;
16295
- const fragments = start ? [...historyMessages(local.key, project, thread.id, [EARLIER]), ...all.slice(start)] : all;
16309
+ let start2 = Math.max(0, all.length - HISTORY_KEPT);
16310
+ while (start2 < all.length && (all[start2].part ?? 0) > 0) start2++;
16311
+ const fragments = start2 ? [...historyMessages(local.key, project, thread.id, [EARLIER]), ...all.slice(start2)] : all;
16296
16312
  const summary = { generation, total: fragments.length, updatedAt: thread.updatedAt };
16297
16313
  const batch = {
16298
16314
  pairing: pair.id,
@@ -16346,8 +16362,8 @@ function kept(message) {
16346
16362
  files2.push(attached(`${message.id}@${at}`, mime, data));
16347
16363
  return "";
16348
16364
  });
16349
- const start = message.role === "tool" && !message.tool?.diff ? preview(text3) : text3;
16350
- const full = start.length < text3.length ? {
16365
+ const start2 = message.role === "tool" && !message.tool?.diff ? preview(text3) : text3;
16366
+ const full = start2.length < text3.length ? {
16351
16367
  id: `${message.id}:full`,
16352
16368
  mime: "text/plain",
16353
16369
  bytes: Buffer.byteLength(text3),
@@ -16355,7 +16371,7 @@ function kept(message) {
16355
16371
  } : void 0;
16356
16372
  return {
16357
16373
  ...message,
16358
- text: start,
16374
+ text: start2,
16359
16375
  ...full ? { full } : {},
16360
16376
  ...files2.length ? { attachments: files2 } : {}
16361
16377
  };
@@ -16494,6 +16510,8 @@ var Client = class {
16494
16510
  pending = /* @__PURE__ */ new Map();
16495
16511
  approvals = /* @__PURE__ */ new Map();
16496
16512
  requests = /* @__PURE__ */ new Map();
16513
+ // The reply each thread is writing, as far as it has streamed, and the item it belongs to.
16514
+ typing = /* @__PURE__ */ new Map();
16497
16515
  async connect() {
16498
16516
  if (this.socket?.readyState === WebSocket2.OPEN) return;
16499
16517
  if (this.connecting) return this.connecting;
@@ -16528,13 +16546,20 @@ var Client = class {
16528
16546
  return;
16529
16547
  }
16530
16548
  if (typeof message.method !== "string") return;
16549
+ const params = message.params;
16550
+ if (message.method === "item/agentMessage/delta" && typeof params?.threadId === "string" && typeof params.delta === "string") {
16551
+ const known = this.typing.get(params.threadId);
16552
+ this.typing.set(params.threadId, {
16553
+ item: params.itemId,
16554
+ text: (known && known.item === params.itemId ? known.text : "") + params.delta
16555
+ });
16556
+ }
16531
16557
  if (message.method === "serverRequest/resolved") {
16532
16558
  for (const [id, pending] of this.approvals)
16533
16559
  if (pending.requestId === message.params?.requestId) this.approvals.delete(id);
16534
16560
  }
16535
16561
  if ((message.method === "item/commandExecution/requestApproval" || message.method === "item/fileChange/requestApproval") && (typeof message.id === "string" || typeof message.id === "number") && typeof message.params?.threadId === "string") {
16536
16562
  const id = randomUUID2();
16537
- const params = message.params;
16538
16563
  this.approvals.set(id, {
16539
16564
  requestId: message.id,
16540
16565
  threadId: params.threadId,
@@ -16644,6 +16669,7 @@ var Client = class {
16644
16669
  updatedAt: new Date(Number(thread.updatedAt ?? 0) * 1e3).toISOString(),
16645
16670
  status: approvals.length ? "attention" : status(thread),
16646
16671
  messages: messages2(thread),
16672
+ typing: this.typing.get(id)?.text,
16647
16673
  approvals,
16648
16674
  // An interrupt for a finished turn is never answered, so the active turn is read first.
16649
16675
  stop: this.controls ? (requestId) => this.once(requestId, async () => {
@@ -16766,17 +16792,17 @@ var NOTIFY_MS = 100;
16766
16792
  function hunk(before, after, offset = 0) {
16767
16793
  const old = before ? before.replace(/\n$/, "").split("\n") : [];
16768
16794
  const next = after ? after.replace(/\n$/, "").split("\n") : [];
16769
- let start = 0;
16770
- while (start < old.length && start < next.length && old[start] === next[start]) start++;
16795
+ let start2 = 0;
16796
+ while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
16771
16797
  let end = 0;
16772
- while (end < old.length - start && end < next.length - start && old[old.length - 1 - end] === next[next.length - 1 - end])
16798
+ while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
16773
16799
  end++;
16774
- const removed = old.slice(start, old.length - end);
16775
- const added2 = next.slice(start, next.length - end);
16800
+ const removed = old.slice(start2, old.length - end);
16801
+ const added2 = next.slice(start2, next.length - end);
16776
16802
  return {
16777
16803
  lines: [
16778
- `@@ -${offset + start + 1},${removed.length} +${offset + start + 1},${added2.length} @@`,
16779
- ...old.slice(Math.max(0, start - 3), start).map((line) => ` ${line}`),
16804
+ `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
16805
+ ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
16780
16806
  ...removed.map((line) => `-${line}`),
16781
16807
  ...added2.map((line) => `+${line}`),
16782
16808
  ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
@@ -16851,12 +16877,21 @@ var Session = class {
16851
16877
  turn;
16852
16878
  // Messages written to this turn's process that Claude Code has not finished answering.
16853
16879
  open = 0;
16880
+ // The text Claude Code is writing, as far as it has streamed.
16881
+ typing = "";
16854
16882
  approvals = /* @__PURE__ */ new Map();
16855
16883
  requests = /* @__PURE__ */ new Map();
16856
16884
  write(value2) {
16857
16885
  this.turn?.stdin.write(`${JSON.stringify(value2)}
16858
16886
  `);
16859
16887
  }
16888
+ interrupt() {
16889
+ this.write({
16890
+ type: "control_request",
16891
+ request_id: randomUUID3(),
16892
+ request: { subtype: "interrupt" }
16893
+ });
16894
+ }
16860
16895
  once(id, action) {
16861
16896
  const known = this.requests.get(id);
16862
16897
  if (known) return known;
@@ -16866,13 +16901,16 @@ var Session = class {
16866
16901
  this.requests.set(id, result);
16867
16902
  return result;
16868
16903
  }
16869
- // Resolves once Claude Code takes the message, or says why it could not start.
16870
- say(text3) {
16904
+ // Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
16905
+ // turn under way, so Claude Code takes the message at once.
16906
+ say(text3, now = false) {
16871
16907
  this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
16908
+ this.typing = "";
16872
16909
  this.status = "working";
16873
16910
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16874
16911
  const message = { type: "user", message: { role: "user", content: text3 } };
16875
16912
  if (this.turn) {
16913
+ if (now) this.interrupt();
16876
16914
  this.open++;
16877
16915
  this.write(message);
16878
16916
  this.changed();
@@ -16887,6 +16925,7 @@ var Session = class {
16887
16925
  "--output-format",
16888
16926
  "stream-json",
16889
16927
  "--verbose",
16928
+ "--include-partial-messages",
16890
16929
  "--permission-prompt-tool",
16891
16930
  "stdio",
16892
16931
  ...this.saved ? ["--resume", this.id] : ["--session-id", this.id]
@@ -16944,6 +16983,14 @@ var Session = class {
16944
16983
  this.status = "attention";
16945
16984
  } else if (message.type === "result") {
16946
16985
  if (--this.open <= 0) this.turn?.stdin.end();
16986
+ } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
16987
+ const event = message.event;
16988
+ if (event?.type === "content_block_start") this.typing = "";
16989
+ else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta")
16990
+ this.typing += String(event.delta.text ?? "");
16991
+ else return false;
16992
+ this.changed();
16993
+ return false;
16947
16994
  } else return false;
16948
16995
  this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16949
16996
  this.changed();
@@ -16958,15 +17005,13 @@ var Session = class {
16958
17005
  updatedAt: this.updatedAt,
16959
17006
  status: this.status,
16960
17007
  messages: [...this.messages],
17008
+ typing: this.typing,
16961
17009
  approvals: [...this.approvals.values()].map((pending) => pending.approval),
16962
- send: (requestId, text3) => this.once(requestId, () => this.say(text3)),
17010
+ send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17011
+ now: true,
16963
17012
  stop: this.turn ? (requestId) => this.once(requestId, async () => {
16964
17013
  if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
16965
- this.write({
16966
- type: "control_request",
16967
- request_id: randomUUID3(),
16968
- request: { subtype: "interrupt" }
16969
- });
17014
+ this.interrupt();
16970
17015
  }) : void 0,
16971
17016
  approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16972
17017
  const pending = this.approvals.get(approvalId);
@@ -17040,15 +17085,13 @@ var Headless = class {
17040
17085
  };
17041
17086
 
17042
17087
  // src/harness/claude.ts
17043
- import { join as join5 } from "node:path";
17044
- import { connect as connect2 } from "node:net";
17088
+ import { dirname as dirname4, join as join5 } from "node:path";
17045
17089
  import { watch as watch2 } from "node:fs";
17046
- import { lstat, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
17047
- var PEER_PROTOCOL = 1;
17090
+ import { mkdir, readdir as readdir2, readFile as readFile2, rm } from "node:fs/promises";
17091
+ import { createServer as createServer2 } from "node:http";
17048
17092
  var MAX_SESSIONS2 = 100;
17049
- var REQUEST_MS2 = 4e3;
17050
17093
  var MAX_REQUESTS2 = 1e3;
17051
- var ENVELOPE = "cross-session-message";
17094
+ var SESSION = /^\/sessions\/([\w-]{1,100})$/;
17052
17095
  var STATUS = {
17053
17096
  busy: "working",
17054
17097
  idle: "idle",
@@ -17073,39 +17116,22 @@ async function registrations() {
17073
17116
  );
17074
17117
  return found.flat();
17075
17118
  }
17076
- async function post(session, path, text3) {
17077
- const file = await lstat(path);
17078
- if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
17079
- throw new Error("Claude Code inbox is not owned by this user");
17080
- const body2 = text3.replace(new RegExp(`<(?=/?${ENVELOPE})`, "gi"), "<\\");
17081
- const line = JSON.stringify({
17082
- type: "user",
17083
- session_id: session.sessionId,
17084
- priority: "next",
17085
- message: { role: "user", content: `<${ENVELOPE} from-name="Dashup">
17086
- ${body2}
17087
- </${ENVELOPE}>` }
17088
- });
17089
- await new Promise((resolve3, reject) => {
17090
- const socket = connect2(path);
17091
- socket.setTimeout(REQUEST_MS2, () => socket.destroy(new Error("timed out")));
17092
- socket.once("error", () => reject(new Error("Claude Code is not accepting messages")));
17093
- socket.end(`${line}
17094
- `, resolve3);
17095
- });
17096
- }
17097
17119
  var Claude = class {
17098
- constructor(changed) {
17120
+ constructor(changed, socket = join5(dirname4(configPath()), "claude.sock")) {
17099
17121
  this.changed = changed;
17122
+ this.socket = socket;
17100
17123
  }
17101
17124
  changed;
17125
+ socket;
17102
17126
  watcher;
17103
17127
  notify;
17128
+ server;
17129
+ streams = /* @__PURE__ */ new Map();
17104
17130
  requests = /* @__PURE__ */ new Map();
17105
17131
  async refresh() {
17106
17132
  this.watch();
17107
17133
  return (await registrations()).map((session) => {
17108
- const path = session.peerProtocol === PEER_PROTOCOL ? session.messagingSocketPath : void 0;
17134
+ const plugin = this.streams.has(session.sessionId);
17109
17135
  return {
17110
17136
  id: session.sessionId,
17111
17137
  harness: "claude",
@@ -17114,14 +17140,47 @@ var Claude = class {
17114
17140
  status: STATUS[session.status ?? ""] ?? "unknown",
17115
17141
  messages: [],
17116
17142
  approvals: [],
17117
- send: path ? (requestId, text3) => this.once(requestId, () => post(session, path, text3)) : void 0
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
17118
17146
  };
17119
17147
  });
17120
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.
17121
17179
  once(id, action) {
17122
17180
  const known = this.requests.get(id);
17123
17181
  if (known) return known;
17124
- if (this.requests.size >= MAX_REQUESTS2) this.requests.delete(this.requests.keys().next().value);
17182
+ if (this.requests.size >= MAX_REQUESTS2)
17183
+ this.requests.delete(this.requests.keys().next().value);
17125
17184
  const result = action();
17126
17185
  this.requests.set(id, result);
17127
17186
  return result;
@@ -17148,23 +17207,26 @@ var Claude = class {
17148
17207
  clearTimeout(this.notify);
17149
17208
  this.watcher?.close();
17150
17209
  this.watcher = void 0;
17210
+ this.server?.closeAllConnections();
17211
+ this.server?.close();
17212
+ this.server = void 0;
17151
17213
  }
17152
17214
  };
17153
17215
 
17154
17216
  // src/harness/vscode.ts
17155
17217
  import Socket3 from "ws";
17156
17218
  import { homedir as homedir5 } from "node:os";
17157
- import { connect as connect3 } from "node:net";
17219
+ import { connect as connect2 } from "node:net";
17158
17220
  import { promisify as promisify3 } from "node:util";
17159
17221
  import { fileURLToPath } from "node:url";
17160
17222
  import { execFile as execFile3 } from "node:child_process";
17161
17223
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17162
17224
  import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17163
- import { join as join6, dirname as dirname4, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
17225
+ import { join as join6, dirname as dirname5, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
17164
17226
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
17165
17227
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
17166
17228
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
17167
- var REQUEST_MS3 = 4e3;
17229
+ var REQUEST_MS2 = 4e3;
17168
17230
  var MAX_BYTES2 = 8 * 1024 * 1024;
17169
17231
  var MAX_METADATA = 64 * 1024;
17170
17232
  var MAX_HOSTS = 16;
@@ -17178,7 +17240,7 @@ var CONFIRMATION_CHOICES = [
17178
17240
  function roots() {
17179
17241
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
17180
17242
  if (override !== void 0)
17181
- return override ? [basename2(override) === "User" ? dirname4(override) : override] : [];
17243
+ return override ? [basename2(override) === "User" ? dirname5(override) : override] : [];
17182
17244
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17183
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");
17184
17246
  return ["Code", "Code - Insiders"].map((name) => join6(base, name));
@@ -17198,7 +17260,7 @@ async function privateFile(path, limit) {
17198
17260
  "-EncodedCommand",
17199
17261
  Buffer.from(script, "utf16le").toString("base64")
17200
17262
  ],
17201
- { timeout: REQUEST_MS3, maxBuffer: MAX_METADATA }
17263
+ { timeout: REQUEST_MS2, maxBuffer: MAX_METADATA }
17202
17264
  );
17203
17265
  return stdout.trim().toLowerCase() === "true";
17204
17266
  } catch {
@@ -17239,7 +17301,7 @@ async function entries() {
17239
17301
  if (!socket?.isSocket() || process.getuid && socket.uid !== process.getuid())
17240
17302
  continue;
17241
17303
  }
17242
- if (!await privateFile(dirname4(path), MAX_BYTES2)) continue;
17304
+ if (!await privateFile(dirname5(path), MAX_BYTES2)) continue;
17243
17305
  const key = `${root}\0${identity2}`;
17244
17306
  if (!result.has(key))
17245
17307
  result.set(key, {
@@ -17403,9 +17465,9 @@ var Host = class {
17403
17465
  if (this.connecting) return this.connecting;
17404
17466
  this.connecting = (async () => {
17405
17467
  const socket = new Socket3(`ws://localhost/?tkn=${encodeURIComponent(this.entry.token)}`, {
17406
- createConnection: () => connect3(this.entry.path),
17468
+ createConnection: () => connect2(this.entry.path),
17407
17469
  maxPayload: MAX_BYTES2,
17408
- handshakeTimeout: REQUEST_MS3
17470
+ handshakeTimeout: REQUEST_MS2
17409
17471
  });
17410
17472
  socket.on("error", () => {
17411
17473
  });
@@ -17438,7 +17500,7 @@ var Host = class {
17438
17500
  }
17439
17501
  };
17440
17502
  const client = this.client = new AhpClient(transport, {
17441
- requestTimeoutMs: REQUEST_MS3,
17503
+ requestTimeoutMs: REQUEST_MS2,
17442
17504
  subscriptionBuffer: 1024
17443
17505
  });
17444
17506
  this.resources.clear();
@@ -17502,7 +17564,7 @@ var Host = class {
17502
17564
  const timer = setTimeout(() => {
17503
17565
  this.receipts.delete(handle.clientSeq);
17504
17566
  reject(new Error("Native AHP action was not acknowledged; delivery is unknown"));
17505
- }, REQUEST_MS3);
17567
+ }, REQUEST_MS2);
17506
17568
  this.receipts.set(handle.clientSeq, { resolve: resolve3, reject, timer });
17507
17569
  });
17508
17570
  this.requests.set(requestId, pending);
@@ -17656,7 +17718,7 @@ import { createHash as createHash4 } from "node:crypto";
17656
17718
  import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
17657
17719
  import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17658
17720
  var run2 = promisify4(execFile4);
17659
- var REQUEST_MS4 = 4e3;
17721
+ var REQUEST_MS3 = 4e3;
17660
17722
  var MAX_BYTES3 = 8 * 1024 * 1024;
17661
17723
  var MAX_SESSIONS4 = 100;
17662
17724
  var MAX_PROCESSES2 = 4096;
@@ -17738,7 +17800,7 @@ async function endpoints() {
17738
17800
  const { stdout } = await run2(
17739
17801
  "lsof",
17740
17802
  ["-nP", "-a", "-c", "opencode", "-iTCP", "-sTCP:LISTEN", "-Fn"],
17741
- { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17803
+ { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17742
17804
  );
17743
17805
  return stdout.split("\n").flatMap((line) => {
17744
17806
  const port = /^n(?:127\.0\.0\.1|\[::1\]|\*):(\d+)$/.exec(line)?.[1];
@@ -17755,7 +17817,7 @@ async function endpoints() {
17755
17817
  const { stdout } = await run2(
17756
17818
  "powershell.exe",
17757
17819
  ["-NoProfile", "-NonInteractive", "-Command", script],
17758
- { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17820
+ { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17759
17821
  );
17760
17822
  return stdout.split(/\r?\n/).filter((port) => /^\d+$/.test(port)).slice(0, MAX_ENDPOINTS).map((port) => ({
17761
17823
  url: localEndpoint(`http://127.0.0.1:${port}`),
@@ -17875,7 +17937,7 @@ var Server = class {
17875
17937
  };
17876
17938
  }
17877
17939
  async request(path, directory2, payload) {
17878
- const signal = AbortSignal.timeout(REQUEST_MS4);
17940
+ const signal = AbortSignal.timeout(REQUEST_MS3);
17879
17941
  const response = await fetch(this.endpoint.url + path, {
17880
17942
  method: payload === void 0 ? "GET" : "POST",
17881
17943
  headers: {
@@ -18051,7 +18113,7 @@ var Server = class {
18051
18113
  listen() {
18052
18114
  if (this.stream || this.retry || this.stopped) return;
18053
18115
  const controller = this.stream = new AbortController();
18054
- const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS4);
18116
+ const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS3);
18055
18117
  connectTimer.unref();
18056
18118
  void (async () => {
18057
18119
  const response = await fetch(this.endpoint.url + "/global/event", {
@@ -18273,6 +18335,10 @@ var LiveHarnesses = class {
18273
18335
  }
18274
18336
  watch(onChange) {
18275
18337
  this.listeners.add(onChange);
18338
+ this.claude.listen().catch((error) => process.stderr.write(
18339
+ `Claude Code sessions cannot take replies: ${error instanceof Error ? error.message : error}
18340
+ `
18341
+ ));
18276
18342
  if (!this.timer && process.env.DASHUP_LIVE_DISCOVERY !== "0") {
18277
18343
  this.timer = setInterval(() => this.changed(), DISCOVERY_MS);
18278
18344
  this.timer.unref();
@@ -18301,10 +18367,10 @@ var LiveHarnesses = class {
18301
18367
  import { randomUUID as randomUUID5 } from "node:crypto";
18302
18368
  import { spawn as spawn4 } from "node:child_process";
18303
18369
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18304
- import { join as join8, dirname as dirname5, basename as basename3 } from "node:path";
18370
+ import { join as join8, dirname as dirname6, basename as basename3 } from "node:path";
18305
18371
  import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18306
18372
  var CODEX_VERSION = "0.160.1";
18307
- var REQUEST_MS5 = 1e4;
18373
+ var REQUEST_MS4 = 1e4;
18308
18374
  var MAX_BYTES4 = 1024 * 1024;
18309
18375
  var MAX_PROCESSES3 = 4096;
18310
18376
  var MAX_FILES = 4096;
@@ -18320,8 +18386,8 @@ function sameFile(left, right) {
18320
18386
  return left.path === right.path && left.device === right.device && left.inode === right.inode;
18321
18387
  }
18322
18388
  function packageVersion(executable) {
18323
- let directory2 = dirname5(executable);
18324
- for (let depth = 0; depth < 8; depth++, directory2 = dirname5(directory2)) {
18389
+ let directory2 = dirname6(executable);
18390
+ for (let depth = 0; depth < 8; depth++, directory2 = dirname6(directory2)) {
18325
18391
  const path = join8(directory2, "package.json");
18326
18392
  if (!existsSync4(path)) continue;
18327
18393
  const value2 = JSON.parse(readFileSync4(path, "utf8"));
@@ -18335,8 +18401,8 @@ function processOwners(pid) {
18335
18401
  const executable = realpathSync2.native(join8(root, "exe"));
18336
18402
  if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18337
18403
  const status2 = readFileSync4(join8(root, "stat"), "utf8");
18338
- const start = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18339
- if (!start || !/^\d+$/.test(start)) return [];
18404
+ const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18405
+ if (!start2 || !/^\d+$/.test(start2)) return [];
18340
18406
  const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
18341
18407
  const files2 = fds.flatMap((fd) => {
18342
18408
  try {
@@ -18346,13 +18412,13 @@ function processOwners(pid) {
18346
18412
  }
18347
18413
  });
18348
18414
  return files2.flatMap(({ fd, path }) => {
18349
- if (basename3(dirname5(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18415
+ if (basename3(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18350
18416
  const thread = basename3(path, ".lock");
18351
18417
  if (!THREAD_ID.test(thread)) return [];
18352
18418
  try {
18353
18419
  const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
18354
18420
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
18355
- const home = realpathSync2.native(dirname5(dirname5(path)));
18421
+ const home = realpathSync2.native(dirname6(dirname6(path)));
18356
18422
  const directory2 = statSync2(home);
18357
18423
  if (process.getuid && directory2.uid !== process.getuid()) return [];
18358
18424
  const queue = identity(join8(home, "queue_1.sqlite"));
@@ -18371,7 +18437,7 @@ function processOwners(pid) {
18371
18437
  return [
18372
18438
  {
18373
18439
  pid,
18374
- start,
18440
+ start: start2,
18375
18441
  executable,
18376
18442
  home,
18377
18443
  thread,
@@ -18536,7 +18602,7 @@ var Writer = class {
18536
18602
  this.pending.delete(id);
18537
18603
  reject(new Error("Native queue acknowledgement was not received"));
18538
18604
  this.close();
18539
- }, REQUEST_MS5);
18605
+ }, REQUEST_MS4);
18540
18606
  this.pending.set(id, { resolve: resolve3, reject, timer });
18541
18607
  this.process.stdin.write(JSON.stringify({ id, method, params }) + "\n");
18542
18608
  });
@@ -18655,7 +18721,7 @@ var NativeQueue = class {
18655
18721
  // src/connector.ts
18656
18722
  var EPHEMERAL_TTL_MS = 6e4;
18657
18723
  var HOME_RECENT = 20;
18658
- var FOLLOW_MS = 5 * 6e4;
18724
+ var FOLLOW_MS = 2 * 6e4;
18659
18725
  var RESCAN_MS = 1e3;
18660
18726
  var Command = z2.object({
18661
18727
  type: z2.literal("command"),
@@ -18665,7 +18731,12 @@ var Command = z2.object({
18665
18731
  action: z2.discriminatedUnion("kind", [
18666
18732
  z2.object({ kind: z2.literal("projects.refresh") }).strict(),
18667
18733
  z2.object({ kind: z2.literal("thread.read") }).strict(),
18668
- z2.object({ kind: z2.literal("thread.send"), text: z2.string().min(1).max(1e5) }).strict(),
18734
+ z2.object({ kind: z2.literal("thread.follow") }).strict(),
18735
+ z2.object({
18736
+ kind: z2.literal("thread.send"),
18737
+ text: z2.string().min(1).max(1e5),
18738
+ now: z2.boolean().optional()
18739
+ }).strict(),
18669
18740
  z2.object({ kind: z2.literal("thread.queue"), text: z2.string().min(1).max(1e5) }).strict(),
18670
18741
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18671
18742
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
@@ -19059,6 +19130,27 @@ var Connector = class {
19059
19130
  for (const pair of this.activePairs()) await this.listProjects(pair);
19060
19131
  await this.publishQueueReceipts();
19061
19132
  }
19133
+ // Publishes a conversation for the phone, which then gets what changes in it.
19134
+ async read(pair, project, thread) {
19135
+ const messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19136
+ const history = await publishHistory(this.config, pair, project.id, thread, messages5);
19137
+ this.follows.set(`${pair.id}:${thread.id}`, {
19138
+ pair,
19139
+ project: project.id,
19140
+ thread: thread.id,
19141
+ until: Date.now() + FOLLOW_MS,
19142
+ copy: copy(messages5),
19143
+ updatedAt: thread.updatedAt,
19144
+ typing: ""
19145
+ });
19146
+ return this.thread(pair, project, thread, history);
19147
+ }
19148
+ // The reply an agent this computer runs is still writing, until the conversation holds it.
19149
+ typing(thread, messages5) {
19150
+ const typing = this.discovery.locations.get(thread.id)?.live?.typing ?? "";
19151
+ const last = messages5.filter((message) => message.role === "assistant").at(-1);
19152
+ return typing && !last?.text.startsWith(typing) ? typing : "";
19153
+ }
19062
19154
  // A phone reading a conversation gets its messages as they change, before anything that says the
19063
19155
  // conversation changed, so it never asks for a copy it already has.
19064
19156
  async stream() {
@@ -19067,18 +19159,19 @@ var Connector = class {
19067
19159
  let messages5;
19068
19160
  try {
19069
19161
  if (thread && follow.until > Date.now() && activePair(this.config, follow.pair))
19070
- messages5 = changes(
19071
- follow.copy,
19072
- this.discovery.read(thread, this.attached(thread)?.messages)
19073
- );
19162
+ messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19074
19163
  } catch {
19075
19164
  }
19076
19165
  if (!thread || !messages5) {
19077
19166
  this.follows.delete(key);
19078
19167
  continue;
19079
19168
  }
19080
- if (!messages5.length && thread.updatedAt === follow.updatedAt) continue;
19169
+ const changed = changes(follow.copy, messages5);
19170
+ const typing = this.typing(thread, messages5);
19171
+ if (!changed.length && thread.updatedAt === follow.updatedAt && typing === follow.typing)
19172
+ continue;
19081
19173
  follow.updatedAt = thread.updatedAt;
19174
+ follow.typing = typing;
19082
19175
  await this.send(follow.pair, {
19083
19176
  type: "thread-tail",
19084
19177
  projectId: follow.project,
@@ -19088,8 +19181,9 @@ var Connector = class {
19088
19181
  this.config.pairs[follow.pair.id].key,
19089
19182
  follow.project,
19090
19183
  follow.thread,
19091
- messages5
19092
- )
19184
+ changed
19185
+ ),
19186
+ ...typing ? { typing } : {}
19093
19187
  });
19094
19188
  }
19095
19189
  }
@@ -19100,7 +19194,7 @@ var Connector = class {
19100
19194
  for (const thread of project.threads) {
19101
19195
  const location2 = this.discovery.locations.get(thread.id);
19102
19196
  const harness = thread.agent?.harness;
19103
- if (location2?.history && !location2.live && !project.remote && !thread.capabilities.queue && harness && harness !== "other" && this.startable.includes(harness))
19197
+ if (location2?.history && !location2.live && !project.remote && !thread.capabilities.queue && harness && isStartHarness(harness) && this.startable.includes(harness))
19104
19198
  thread.capabilities = { ...thread.capabilities, send: true };
19105
19199
  }
19106
19200
  }
@@ -19164,6 +19258,7 @@ var Connector = class {
19164
19258
  throw new Error("Command expired. Try again while this computer is online.");
19165
19259
  const outcome = await this.command(pair, content);
19166
19260
  delete this.config.unconfirmed[content.requestId];
19261
+ if (content.action.kind === "thread.follow") return;
19167
19262
  if (outcome && "state" in outcome) {
19168
19263
  queue = outcome;
19169
19264
  await this.queueResult(pair, content.requestId, content.projectId, queue);
@@ -19177,6 +19272,7 @@ var Connector = class {
19177
19272
  );
19178
19273
  } catch (error) {
19179
19274
  delete this.config.unconfirmed[content.requestId];
19275
+ if (content.action.kind === "thread.follow") return;
19180
19276
  if (queue) {
19181
19277
  await this.queueResult(pair, content.requestId, content.projectId, queue);
19182
19278
  return;
@@ -19309,19 +19405,15 @@ var Connector = class {
19309
19405
  if (!project) throw new Error("Project is no longer open on this computer");
19310
19406
  const thread = project.threads.find((item) => item.id === command2.threadId);
19311
19407
  switch (command2.action.kind) {
19312
- case "thread.read": {
19408
+ case "thread.read":
19313
19409
  if (!thread) throw new Error("Thread not found");
19314
- const messages5 = this.discovery.read(thread, this.attached(thread)?.messages);
19315
- const history = await publishHistory(this.config, pair, project.id, thread, messages5);
19316
- this.follows.set(`${pair.id}:${thread.id}`, {
19317
- pair,
19318
- project: project.id,
19319
- thread: thread.id,
19320
- until: Date.now() + FOLLOW_MS,
19321
- copy: copy(messages5),
19322
- updatedAt: thread.updatedAt
19323
- });
19324
- return this.thread(pair, project, thread, history);
19410
+ return this.read(pair, project, thread);
19411
+ case "thread.follow": {
19412
+ if (!thread) throw new Error("Thread not found");
19413
+ const follow = this.follows.get(`${pair.id}:${thread.id}`);
19414
+ if (!follow) return this.read(pair, project, thread);
19415
+ follow.until = Date.now() + FOLLOW_MS;
19416
+ return;
19325
19417
  }
19326
19418
  case "attachment.read": {
19327
19419
  if (!thread) throw new Error("Thread not found");
@@ -19341,11 +19433,17 @@ var Connector = class {
19341
19433
  "This session has no live reply connection. Use the existing agent on your computer."
19342
19434
  );
19343
19435
  const location2 = this.discovery.locations.get(thread.id);
19344
- if (location2?.live?.send) return location2.live.send(command2.requestId, command2.action.text);
19436
+ if (location2?.live?.send)
19437
+ return location2.live.send(command2.requestId, command2.action.text, command2.action.now);
19345
19438
  const agent = this.attached(thread);
19346
19439
  const saved = thread.agent;
19347
- if (!agent && location2?.history && saved && saved.harness !== "other") {
19348
- await this.harnesses.resume(saved.harness, saved.sessionId, project.path, command2.action.text);
19440
+ if (!agent && location2?.history && saved && isStartHarness(saved.harness)) {
19441
+ await this.harnesses.resume(
19442
+ saved.harness,
19443
+ saved.sessionId,
19444
+ project.path,
19445
+ command2.action.text
19446
+ );
19349
19447
  await this.changed();
19350
19448
  return;
19351
19449
  }
@@ -19419,7 +19517,7 @@ var Connector = class {
19419
19517
  if (!name || name === "." || name === ".." || /[\\/:*?"<>|\x00-\x1f]/.test(name))
19420
19518
  throw new Error("Name the folder without slashes or special characters");
19421
19519
  cwd = join9(folder.parent, name);
19422
- await mkdir(cwd).catch((error) => {
19520
+ await mkdir2(cwd).catch((error) => {
19423
19521
  throw new Error(error.code === "EEXIST" ? `${name} already exists` : `Couldn't create ${name}`);
19424
19522
  });
19425
19523
  } else {
@@ -19449,8 +19547,8 @@ import { homedir as homedir6, userInfo } from "node:os";
19449
19547
  import { promisify as promisify6 } from "node:util";
19450
19548
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19451
19549
  import { execFile as execFile6, spawn as spawn5 } from "node:child_process";
19452
- import { cp, mkdir as mkdir2, rm, writeFile } from "node:fs/promises";
19453
- import { dirname as dirname6, join as join10, relative, sep } from "node:path";
19550
+ import { cp, mkdir as mkdir3, rm as rm2, writeFile } from "node:fs/promises";
19551
+ import { dirname as dirname7, join as join10, relative, sep } from "node:path";
19454
19552
  var run3 = promisify6(execFile6);
19455
19553
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
19456
19554
  var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
@@ -19496,21 +19594,34 @@ async function durableEntry() {
19496
19594
  );
19497
19595
  if (!["_npx", "dlx"].some((cache2) => entry.includes(`${sep}${cache2}${sep}`))) return entry;
19498
19596
  const root = entry.slice(0, entry.indexOf(`${sep}node_modules${sep}`));
19499
- const app = join10(dirname6(configPath()), "app");
19500
- await rm(app, { recursive: true, force: true });
19597
+ const app = join10(dirname7(configPath()), "app");
19598
+ await rm2(app, { recursive: true, force: true });
19501
19599
  await cp(join10(root, "node_modules"), join10(app, "node_modules"), {
19502
19600
  recursive: true,
19503
19601
  verbatimSymlinks: true
19504
19602
  });
19505
19603
  return join10(app, relative(root, entry));
19506
19604
  }
19605
+ var claude2 = (...args2) => run3("claude", ["plugin", ...args2], { timeout: 6e4 });
19606
+ async function installClaude(entry) {
19607
+ if (process.platform === "win32") return false;
19608
+ try {
19609
+ await claude2("marketplace", "add", join10(dirname7(entry), "..", "claude"));
19610
+ await claude2("install", "dashup@dashup", "--scope", "user");
19611
+ return true;
19612
+ } catch {
19613
+ return false;
19614
+ }
19615
+ }
19507
19616
  async function installStartup() {
19508
19617
  const path = serviceFile();
19509
- await mkdir2(dirname6(path), { recursive: true });
19510
- await writeFile(
19511
- path,
19512
- service(process.platform, process.execPath, await durableEntry(), configPath())
19513
- );
19618
+ const entry = await durableEntry();
19619
+ await mkdir3(dirname7(path), { recursive: true });
19620
+ await writeFile(path, service(process.platform, process.execPath, entry, configPath()));
19621
+ const restart = await installClaude(entry) ? "Restart Claude Code sessions that are already open to answer them from your phone." : void 0;
19622
+ return [await start(path), restart].filter(Boolean).join(" ") || void 0;
19623
+ }
19624
+ async function start(path) {
19514
19625
  if (process.platform === "darwin") {
19515
19626
  await run3("launchctl", ["unload", path]).catch(() => {
19516
19627
  });
@@ -19530,13 +19641,17 @@ async function installStartup() {
19530
19641
  }
19531
19642
  async function uninstallStartup() {
19532
19643
  const path = serviceFile();
19644
+ await claude2("uninstall", "dashup@dashup").catch(() => {
19645
+ });
19646
+ await claude2("marketplace", "remove", "dashup").catch(() => {
19647
+ });
19533
19648
  if (process.platform === "darwin") await run3("launchctl", ["unload", path]).catch(() => {
19534
19649
  });
19535
19650
  else if (process.platform !== "win32")
19536
19651
  await systemctl("disable", "dashup.service").catch(() => {
19537
19652
  });
19538
- await rm(path, { force: true });
19539
- await rm(join10(dirname6(configPath()), "app"), { recursive: true, force: true });
19653
+ await rm2(path, { force: true });
19654
+ await rm2(join10(dirname7(configPath()), "app"), { recursive: true, force: true });
19540
19655
  }
19541
19656
 
19542
19657
  // src/index.ts
@@ -19739,7 +19854,7 @@ async function main() {
19739
19854
  "To pair this computer, run dashup pair in a terminal, or add --account with your Dashup email."
19740
19855
  );
19741
19856
  let connector;
19742
- const server = createServer2(async (request2, response) => {
19857
+ const server = createServer3(async (request2, response) => {
19743
19858
  if (!authorized(request2)) {
19744
19859
  response.writeHead(403).end();
19745
19860
  return;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dashup": "dist/index.js"
7
7
  },
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "claude"
10
11
  ],
11
12
  "publishConfig": {
12
13
  "access": "public"
@@ -29,11 +30,11 @@
29
30
  "tsup": "^8.5.1",
30
31
  "tsx": "^4.22.4",
31
32
  "typescript": "^5.9.3",
32
- "@dashup/typescript-config": "0.0.0",
33
- "@dashup/protocol": "0.1.0",
33
+ "@dashup/models": "0.1.0",
34
34
  "@parcae/lynx": "0.1.0",
35
35
  "@parcae/sdk": "0.8.1",
36
- "@dashup/models": "0.1.0"
36
+ "@dashup/typescript-config": "0.0.0",
37
+ "@dashup/protocol": "0.1.0"
37
38
  },
38
39
  "scripts": {
39
40
  "dashup": "tsx src/index.ts",