@dashup/connector 0.1.1 → 0.1.3

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 +5 -13
  2. package/dist/index.js +275 -141
  3. package/package.json +6 -5
package/README.md CHANGED
@@ -5,8 +5,7 @@ Run beside the editor on each computer, including the PC whose Zed window connec
5
5
  With Node 24:
6
6
 
7
7
  ```sh
8
- npm install -g @dashup/connector
9
- dashup pair
8
+ npx @dashup/connector pair
10
9
  ```
11
10
 
12
11
  From a checkout, use pnpm 10.34.5 from the workspace root and `pnpm --filter @dashup/connector dashup` in place of `dashup`:
@@ -18,7 +17,7 @@ pnpm exec turbo build --filter=@dashup/connector...
18
17
 
19
18
  The published package bundles the workspace code, and CI publishes it when the package version is new. Screen and input need the native helper from `packages/desktop`, which the package does not include.
20
19
 
21
- `dashup pair` prints a QR code that expires in five minutes, then keeps the connector running; scan it in the phone app under Pair a computer. The first phone adds the computer to its account, so the terminal names that account and shares nothing until you answer `y`. Later codes pair more phones on the same account without asking, including through a connector that is already running. A phone that scans an expired code gets a pairing the computer revokes, because it never kept that key. It uses `https://api.dashup.com`; for a local API, add `--api http://localhost:3000`. A physical phone needs a reachable HTTPS API. After restarting the connector, use `dashup watch`.
20
+ `dashup pair` walks through pairing in the terminal: it shows a QR code that expires in five minutes, waits for a phone to scan it under Pair a computer, and offers to keep Dashup running in the background. The first phone adds the computer to its account, so the walkthrough names that account and shares nothing until you confirm it. Later codes pair more phones on the same account without asking, including through a connector that is already running. A phone that scans an expired code gets a pairing the computer revokes, because it never kept that key. Pairing needs a terminal: without one, an unpaired computer exits and says so. It uses `https://api.dashup.com`; for a local API, add `--api http://localhost:3000`. A physical phone needs a reachable HTTPS API. To run a paired connector in the foreground, use `dashup watch`.
22
21
 
23
22
  ## Existing projects and alerts
24
23
 
@@ -30,7 +29,7 @@ pnpm --filter @dashup/connector dashup alert --project PROJECT_ID --title "Dashu
30
29
 
31
30
  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. 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.
32
31
 
33
- 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 and read-only controls. File changes publish project metadata and can produce a rate-limited “updated” alert. A workspace becomes a project once it has a conversation. After a scan that read every source, the connector publishes its list of projects, which retires any 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`.
32
+ 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 and read-only controls. 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`.
34
33
 
35
34
  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 stops its viewer and discards its connector key and queued content.
36
35
 
@@ -89,16 +88,9 @@ macOS requires Screen Recording and Accessibility permission. Windows uses the s
89
88
 
90
89
  ## Start at login
91
90
 
92
- After pairing, stop the foreground connector and install the built entry point:
91
+ Answering yes to the walkthrough's background question, or running `dashup install` on a paired computer, registers a macOS LaunchAgent, Windows Startup entry, or Linux user systemd service and starts it, taking over from any connector already running for that state file. The service runs `watch` with the state file it was paired with (`DASHUP_CONFIG`). Linux requires a user systemd session. Under npx or pnpm dlx, whose caches can be cleared, the service runs a copy of that install kept in `app/` beside the state file; a checkout or global install runs in place. The Node executable must stay at its installed location. Signed desktop installers are not included.
93
92
 
94
- ```sh
95
- node apps/connector/dist/index.js stop
96
- node apps/connector/dist/index.js install
97
- ```
98
-
99
- Installation registers a macOS LaunchAgent, Windows Startup entry, or Linux user systemd service. Linux requires a user systemd session. These source startup paths require the checkout and Node executable to remain at their installed locations. Signed desktop installers are not included.
100
-
101
- Remove startup with `launchctl unload ~/Library/LaunchAgents/com.dashup.connector.plist` and delete that file on macOS; delete `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Dashup.vbs` on Windows; or run `systemctl --user disable --now dashup.service` and delete `~/.config/systemd/user/dashup.service` on Linux. A macOS KeepAlive job restarts after `stop`; unload it when stopping the service.
93
+ Remove startup with `launchctl unload ~/Library/LaunchAgents/com.dashup.connector.plist` and delete that file on macOS; delete `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Dashup.vbs` on Windows; or run `systemctl --user disable --now dashup.service` and delete `~/.config/systemd/user/dashup.service` on Linux. `dashup stop` leaves the service stopped until the next sign-in; a crash restarts it.
102
94
 
103
95
  ## Local state and checks
104
96
 
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 spawn4 = __require("child_process").spawn;
45
+ var spawn5 = __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 = spawn4(process.argv[0], ["-e", execString]);
341
+ var syncProc = spawn5(process.argv[0], ["-e", execString]);
342
342
  var statusText;
343
343
  while (fs.existsSync(syncFile)) {
344
344
  }
@@ -1028,7 +1028,7 @@ var require_node = __commonJS({
1028
1028
  var tty = __require("tty");
1029
1029
  var util = __require("util");
1030
1030
  exports.init = init;
1031
- exports.log = log2;
1031
+ exports.log = log4;
1032
1032
  exports.formatArgs = formatArgs;
1033
1033
  exports.save = save;
1034
1034
  exports.load = load;
@@ -1163,7 +1163,7 @@ var require_node = __commonJS({
1163
1163
  }
1164
1164
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
1165
1165
  }
1166
- function log2(...args2) {
1166
+ function log4(...args2) {
1167
1167
  return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args2) + "\n");
1168
1168
  }
1169
1169
  function save(namespaces) {
@@ -2707,6 +2707,7 @@ var require_pluralize = __commonJS({
2707
2707
  // src/index.ts
2708
2708
  import { randomBytes as randomBytes5, timingSafeEqual } from "node:crypto";
2709
2709
  import { createServer } from "node:http";
2710
+ import * as ui2 from "@clack/prompts";
2710
2711
  import { z as z3 } from "zod";
2711
2712
 
2712
2713
  // ../../packages/protocol/src/index.ts
@@ -4004,61 +4005,84 @@ async function proxyAcp(config2, executable, args2) {
4004
4005
  // src/pair.ts
4005
4006
  import { hostname } from "node:os";
4006
4007
  import { randomBytes as randomBytes2, createHash as createHash2 } from "node:crypto";
4007
- import { createInterface as createInterface2 } from "node:readline/promises";
4008
4008
  import QRCode from "qrcode";
4009
+ import * as ui from "@clack/prompts";
4009
4010
  import { z } from "zod";
4011
+
4012
+ // package.json
4013
+ var version = "0.1.3";
4014
+
4015
+ // src/pair.ts
4010
4016
  var Invitation = z.object({
4011
4017
  code: z.string().regex(/^[a-f0-9]{16}$/),
4012
4018
  key: z.string().regex(/^[a-f0-9]{64}$/),
4013
4019
  text: z.string().min(1)
4014
4020
  }).strict();
4021
+ var interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
4022
+ var intro2 = () => ui.intro(`Dashup ${version}`);
4015
4023
  function invitation(config2, secret = config2.secret) {
4016
4024
  const code = randomBytes2(8).toString("hex");
4017
4025
  const key = newSecret(randomBytes2);
4018
4026
  const computer = createHash2("sha256").update(secret).digest("hex");
4019
4027
  return { code, key, text: formatPairingCode({ api: config2.api, computer, code, key }) };
4020
4028
  }
4021
- async function scanned(config2, { code, text: text3 }, secret = config2.secret) {
4029
+ async function scanned(config2, { code, text: text3 }, done, secret = config2.secret) {
4022
4030
  const qr = await QRCode.toString(text3, {
4023
4031
  type: "terminal",
4024
4032
  small: true,
4025
4033
  errorCorrectionLevel: "L"
4026
4034
  });
4027
- process.stdout.write(
4028
- `
4035
+ const rows = qr.split("\n").filter((row) => row.replace(/\x1b\[\d+m/g, ""));
4036
+ if (interactive) {
4037
+ ui.log.step("Scan this in the Dashup app under Pair a computer. It expires in 5 minutes.");
4038
+ ui.log.message("Can't scan? Paste this code instead:", { spacing: 0 });
4039
+ process.stdout.write(` ${text3}
4040
+ `);
4041
+ ui.log.message(rows);
4042
+ } else {
4043
+ process.stdout.write(
4044
+ `
4029
4045
  Pairing code: ${text3}
4030
4046
 
4031
- ${qr}
4047
+ ${rows.join("\n")}
4032
4048
  Scan this in the Dashup app (Pair a computer). It expires in 5 minutes.
4033
4049
  `
4034
- );
4035
- for (const deadline = Date.now() + PAIRING_TTL_MS; Date.now() < deadline; ) {
4036
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4037
- const response = await fetch(`${config2.api}/v1/dashup/pairings?code=${code}`, {
4038
- headers: { Authorization: `Bearer ${secret}` }
4039
- });
4040
- if (response.status === 401 || response.status === 404) continue;
4041
- const value2 = await response.json();
4042
- if (!response.ok || !value2.success)
4043
- throw new Error(value2.error ?? `Dashup request failed (${response.status})`);
4044
- return value2.result;
4045
- }
4046
- throw new Error("The pairing code expired. Run dashup pair for a new one.");
4047
- }
4048
- async function confirm(question) {
4049
- const prompt = createInterface2({ input: process.stdin, output: process.stdout });
4050
- const answer = await new Promise((resolve3) => {
4051
- prompt.once("close", () => resolve3(""));
4052
- prompt.once("SIGINT", () => prompt.close());
4053
- prompt.question(question).then(resolve3, () => resolve3(""));
4054
- });
4055
- prompt.close();
4056
- return /^y(es)?$/i.test(answer.trim());
4050
+ );
4051
+ }
4052
+ const waiting = interactive ? ui.spinner() : void 0;
4053
+ waiting?.start("Waiting for your phone");
4054
+ try {
4055
+ for (const deadline = Date.now() + PAIRING_TTL_MS; Date.now() < deadline; ) {
4056
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4057
+ const response = await fetch(`${config2.api}/v1/dashup/pairings?code=${code}`, {
4058
+ headers: { Authorization: `Bearer ${secret}` }
4059
+ });
4060
+ if (response.status === 401 || response.status === 404) continue;
4061
+ const value2 = await response.json();
4062
+ if (!response.ok || !value2.success)
4063
+ throw new Error(value2.error ?? `Dashup request failed (${response.status})`);
4064
+ if (waiting) waiting.stop(done(value2.result.email));
4065
+ else process.stdout.write(`${done(value2.result.email)}.
4066
+ `);
4067
+ return value2.result;
4068
+ }
4069
+ throw new Error("The pairing code expired. Run dashup pair for a new one.");
4070
+ } catch (error) {
4071
+ if (!waiting) throw error;
4072
+ waiting.error(error instanceof Error ? error.message : "Pairing failed");
4073
+ throw Object.assign(error, { reported: true });
4074
+ }
4057
4075
  }
4076
+ var reported = (error) => error?.reported === true;
4058
4077
  async function link(config2) {
4059
4078
  const secret = newSecret(randomBytes2);
4060
4079
  const invite = invitation(config2, secret);
4061
- const { pairing, email } = await scanned(config2, invite, secret);
4080
+ const { pairing, email } = await scanned(
4081
+ config2,
4082
+ invite,
4083
+ (account) => `Scanned by ${account}`,
4084
+ secret
4085
+ );
4062
4086
  const linked = { ...config2, secret };
4063
4087
  const platform = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
4064
4088
  const { device } = await api(linked, "/dashup/devices", {
@@ -4067,11 +4091,15 @@ async function link(config2) {
4067
4091
  kind: "desktop",
4068
4092
  platform
4069
4093
  });
4070
- if (!await confirm(`
4071
- ${email} scanned the code. Pair this computer with that account? (y/N) `)) {
4094
+ const confirmed = await ui.confirm({
4095
+ message: `Pair this computer with ${email}?`,
4096
+ initialValue: false
4097
+ });
4098
+ if (confirmed !== true) {
4072
4099
  await api(linked, `/dashup/pairings/${pairing.id}`, void 0, "DELETE").catch(() => {
4073
4100
  });
4074
- throw new Error("Pairing cancelled. Nothing was shared.");
4101
+ ui.cancel("Pairing cancelled. Nothing was shared.");
4102
+ return false;
4075
4103
  }
4076
4104
  Object.assign(config2, {
4077
4105
  secret,
@@ -4082,12 +4110,13 @@ ${email} scanned the code. Pair this computer with that account? (y/N) `)) {
4082
4110
  unconfirmed: {}
4083
4111
  });
4084
4112
  saveConfig(config2);
4113
+ return true;
4085
4114
  }
4086
4115
 
4087
4116
  // src/viewer.ts
4088
4117
  import { existsSync as existsSync2 } from "node:fs";
4089
4118
  import { fileURLToPath } from "node:url";
4090
- import { createInterface as createInterface3 } from "node:readline";
4119
+ import { createInterface as createInterface2 } from "node:readline";
4091
4120
  import { spawn as spawn2 } from "node:child_process";
4092
4121
 
4093
4122
  // src/screen.ts
@@ -4268,7 +4297,7 @@ var Viewer = class _Viewer {
4268
4297
  const child = spawn2(DESKTOP_BIN, [], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
4269
4298
  this.process = child;
4270
4299
  let events = Promise.resolve();
4271
- createInterface3({ input: child.stdout }).on("error", () => this.failed(child)).on("line", (line) => {
4300
+ createInterface2({ input: child.stdout }).on("error", () => this.failed(child)).on("line", (line) => {
4272
4301
  events = events.then(() => {
4273
4302
  if (this.process === child) return this.receive(line);
4274
4303
  }).catch(() => {
@@ -5947,7 +5976,7 @@ var Polling = class extends Transport {
5947
5976
  */
5948
5977
  pause(onPause) {
5949
5978
  this.readyState = "pausing";
5950
- const pause = () => {
5979
+ const pause2 = () => {
5951
5980
  debug2("paused");
5952
5981
  this.readyState = "paused";
5953
5982
  onPause();
@@ -5959,7 +5988,7 @@ var Polling = class extends Transport {
5959
5988
  total++;
5960
5989
  this.once("pollComplete", function() {
5961
5990
  debug2("pre-pause polling complete");
5962
- --total || pause();
5991
+ --total || pause2();
5963
5992
  });
5964
5993
  }
5965
5994
  if (!this.writable) {
@@ -5967,11 +5996,11 @@ var Polling = class extends Transport {
5967
5996
  total++;
5968
5997
  this.once("drain", function() {
5969
5998
  debug2("pre-pause writing complete");
5970
- --total || pause();
5999
+ --total || pause2();
5971
6000
  });
5972
6001
  }
5973
6002
  } else {
5974
- pause();
6003
+ pause2();
5975
6004
  }
5976
6005
  }
5977
6006
  /**
@@ -15035,14 +15064,14 @@ var Model = class _Model extends import_index2.default {
15035
15064
  continue;
15036
15065
  }
15037
15066
  const pathStart = op.path.indexOf("/", 1);
15038
- const relative = {
15067
+ const relative2 = {
15039
15068
  ...op,
15040
15069
  path: op.path.slice(pathStart),
15041
15070
  ...from ? { from: from.slice(from.indexOf("/", 1)) } : {}
15042
15071
  };
15043
15072
  const grouped = refOps.get(segments[0]);
15044
- if (grouped) grouped.push(relative);
15045
- else refOps.set(segments[0], [relative]);
15073
+ if (grouped) grouped.push(relative2);
15074
+ else refOps.set(segments[0], [relative2]);
15046
15075
  }
15047
15076
  let didRefChange = false;
15048
15077
  const changedRefOps = [];
@@ -15217,7 +15246,7 @@ function prefix(ansi) {
15217
15246
  const code = codes[ansi] ?? "";
15218
15247
  return [`${code}${tag}\x1B[0m`];
15219
15248
  }
15220
- var log = {
15249
+ var log2 = {
15221
15250
  warn: (...args2) => isDev && console.warn(...prefix("#fb3"), ...args2),
15222
15251
  error: (...args2) => isDev && console.error(...prefix("#f44"), ...args2),
15223
15252
  debug: (...args2) => isDev && console.debug(...prefix("#888"), ...args2)
@@ -15257,7 +15286,7 @@ var SessionMachine = class {
15257
15286
  this.state.status = nextStatus;
15258
15287
  this.state.userId = userId;
15259
15288
  this.state.version++;
15260
- log.debug(
15289
+ log2.debug(
15261
15290
  userId ? `session: authenticated ${userId}` : "session: anonymous"
15262
15291
  );
15263
15292
  this._fireReady();
@@ -15272,7 +15301,7 @@ var SessionMachine = class {
15272
15301
  this.state.status = "terminated";
15273
15302
  this.state.userId = null;
15274
15303
  this.state.version++;
15275
- log.debug("session: terminated");
15304
+ log2.debug("session: terminated");
15276
15305
  this._fireReady();
15277
15306
  this._notify();
15278
15307
  }
@@ -15324,7 +15353,7 @@ var ConnectionMachine = class {
15324
15353
  this.state.status = status2;
15325
15354
  this.state.lastError = err2;
15326
15355
  this.state.version++;
15327
- log.debug(
15356
+ log2.debug(
15328
15357
  `connection: ${status2}${err2 ? ` (${err2.message})` : ""}`
15329
15358
  );
15330
15359
  for (const fn of this._listeners) fn();
@@ -15454,7 +15483,7 @@ var SocketTransport = class extends import_index2.default {
15454
15483
  this.helloState = "resolved";
15455
15484
  const userId = response.userId ?? null;
15456
15485
  const ms = (performance.now() - t0).toFixed(0);
15457
- log.debug(
15486
+ log2.debug(
15458
15487
  `hello: ${userId ? `userId=${userId}` : "anonymous"} (${ms}ms)`
15459
15488
  );
15460
15489
  this.session.resolve(userId);
@@ -15465,7 +15494,7 @@ var SocketTransport = class extends import_index2.default {
15465
15494
  },
15466
15495
  (err2) => {
15467
15496
  const error = err2 instanceof Error ? err2 : new Error(String(err2));
15468
- log.warn(`hello: token resolution failed (${error.message})`);
15497
+ log2.warn(`hello: token resolution failed (${error.message})`);
15469
15498
  this._rejectHandshake(generation, error);
15470
15499
  }
15471
15500
  );
@@ -15685,12 +15714,12 @@ var SocketTransport = class extends import_index2.default {
15685
15714
  const t0 = performance.now();
15686
15715
  const fullPath = `/${this.version}${path}`;
15687
15716
  const timeoutMs = options?.timeout ?? DEFAULT_TIMEOUT;
15688
- log.debug(`\u2192 ${method.toUpperCase()} ${fullPath}`);
15717
+ log2.debug(`\u2192 ${method.toUpperCase()} ${fullPath}`);
15689
15718
  return new Promise((resolve3, reject) => {
15690
15719
  const timeout = setTimeout(() => {
15691
15720
  this.socket.off(id);
15692
15721
  this.pendingCalls.delete(id);
15693
- log.debug(
15722
+ log2.debug(
15694
15723
  `\u2717 ${method.toUpperCase()} ${fullPath} timeout (${(timeoutMs / 1e3).toFixed(0)}s)`
15695
15724
  );
15696
15725
  reject(new Error(`RPC timeout: ${method} ${path}`));
@@ -15704,10 +15733,10 @@ var SocketTransport = class extends import_index2.default {
15704
15733
  const uncompressed = pako.ungzip(msg, { to: "string" });
15705
15734
  const parsed = (0, import_compress_json.decompress)(JSON.parse(uncompressed));
15706
15735
  if (parsed.success) {
15707
- log.debug(`\u2190 ${method.toUpperCase()} ${fullPath} (${ms}ms)`);
15736
+ log2.debug(`\u2190 ${method.toUpperCase()} ${fullPath} (${ms}ms)`);
15708
15737
  resolve3(parsed.result);
15709
15738
  } else {
15710
- log.debug(
15739
+ log2.debug(
15711
15740
  `\u2717 ${method.toUpperCase()} ${fullPath} (${ms}ms) ${parsed.error || parsed.message}`
15712
15741
  );
15713
15742
  const details = parsed.error && typeof parsed.error === "object" ? parsed.error : parsed;
@@ -15723,7 +15752,7 @@ var SocketTransport = class extends import_index2.default {
15723
15752
  reject(error);
15724
15753
  }
15725
15754
  } catch (err2) {
15726
- log.debug(
15755
+ log2.debug(
15727
15756
  `\u2717 ${method.toUpperCase()} ${fullPath} (${ms}ms) parse error`
15728
15757
  );
15729
15758
  reject(err2);
@@ -16187,7 +16216,6 @@ var Device = class extends Model {
16187
16216
  name = "";
16188
16217
  kind = "desktop";
16189
16218
  platform = "linux";
16190
- lastSeenAt = /* @__PURE__ */ new Date();
16191
16219
  pushToken = "";
16192
16220
  pushProvider = "";
16193
16221
  // A computer signs in with the secret this hashes; phones sign in with an account session.
@@ -16344,11 +16372,6 @@ import { connect } from "node:net";
16344
16372
  import { randomUUID as randomUUID2 } from "node:crypto";
16345
16373
  import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16346
16374
  import { stat, readdir, readFile } from "node:fs/promises";
16347
-
16348
- // package.json
16349
- var version = "0.1.1";
16350
-
16351
- // src/harness/codex.ts
16352
16375
  var REQUEST_MS = 4e3;
16353
16376
  var MAX_BYTES = 8 * 1024 * 1024;
16354
16377
  var MAX_SESSIONS = 100;
@@ -18079,7 +18102,7 @@ var NativeQueue = class {
18079
18102
 
18080
18103
  // src/connector.ts
18081
18104
  var EPHEMERAL_TTL_MS = 6e4;
18082
- var HISTORY_ALERT_INTERVAL_MS = 6e4;
18105
+ var HOME_RECENT = 20;
18083
18106
  var Command = z2.object({
18084
18107
  type: z2.literal("command"),
18085
18108
  requestId: z2.string().min(1),
@@ -18153,9 +18176,10 @@ var Connector = class {
18153
18176
  releases = [];
18154
18177
  announced = /* @__PURE__ */ new Set();
18155
18178
  listed = /* @__PURE__ */ new Map();
18179
+ listedProjects = /* @__PURE__ */ new Map();
18180
+ overviewQueued = false;
18156
18181
  snapshots = /* @__PURE__ */ new Set();
18157
18182
  nativeSnapshots = /* @__PURE__ */ new Map();
18158
- updatedAlerts = /* @__PURE__ */ new Map();
18159
18183
  queueReceipts = /* @__PURE__ */ new Map();
18160
18184
  // The content key behind each pairing code on show, until a phone pairs with it.
18161
18185
  invitations = /* @__PURE__ */ new Map();
@@ -18321,6 +18345,7 @@ var Connector = class {
18321
18345
  );
18322
18346
  this.config.outbox.push(envelope);
18323
18347
  saveConfig(this.config);
18348
+ if (content.type === "project" || content.type === "thread-state") this.refreshOverview();
18324
18349
  await this.flush();
18325
18350
  }
18326
18351
  discardLegacySnapshots() {
@@ -18360,13 +18385,48 @@ var Connector = class {
18360
18385
  await this.send(pair, { type: "project", project });
18361
18386
  await this.listProjects(pair);
18362
18387
  }
18363
- // Only a scan that read every source can tell which projects are gone.
18388
+ // The phone renders Home from this alone; it opens a project's full snapshot only on demand.
18389
+ overview() {
18390
+ const projects = [...this.discovery.projects.values()];
18391
+ const sessions = projects.flatMap((project) => project.threads.map((thread) => ({
18392
+ projectId: project.id,
18393
+ thread,
18394
+ approvals: this.threadState(project, thread).approvals
18395
+ }))).sort((a, b) => b.thread.updatedAt.localeCompare(a.thread.updatedAt));
18396
+ const busy = ({ thread, approvals }) => approvals.length > 0 || thread.status === "attention" || thread.status === "working";
18397
+ if (!this.discovery.warnings.length) this.listedProjects.clear();
18398
+ for (const { threads, ...project } of projects) {
18399
+ const started = threads.filter((thread) => !thread.parent);
18400
+ this.listedProjects.set(project.id, {
18401
+ ...project,
18402
+ active: started.reduce((newest, thread) => thread.updatedAt > newest ? thread.updatedAt : newest, ""),
18403
+ started: started.length
18404
+ });
18405
+ }
18406
+ return {
18407
+ type: "projects",
18408
+ projects: [...this.listedProjects.values()],
18409
+ sessions: [
18410
+ ...sessions.filter(busy),
18411
+ ...sessions.filter((session) => !busy(session) && !session.thread.parent).slice(0, HOME_RECENT)
18412
+ ]
18413
+ };
18414
+ }
18364
18415
  async listProjects(pair) {
18365
- const ids = [...this.discovery.projects.keys()].sort();
18366
- const list2 = JSON.stringify(ids);
18367
- if (this.discovery.warnings.length || this.listed.get(pair.id) === list2) return;
18368
- await this.send(pair, { type: "projects", ids });
18369
- this.listed.set(pair.id, list2);
18416
+ const overview = this.overview();
18417
+ const text3 = JSON.stringify(overview);
18418
+ if (this.listed.get(pair.id) === text3) return;
18419
+ await this.send(pair, overview);
18420
+ this.listed.set(pair.id, text3);
18421
+ }
18422
+ // Each change of a project or thread brings the overview up to date once it settles.
18423
+ refreshOverview() {
18424
+ if (this.overviewQueued) return;
18425
+ this.overviewQueued = true;
18426
+ this.work = this.work.then(async () => {
18427
+ this.overviewQueued = false;
18428
+ for (const pair of this.activePairs()) await this.listProjects(pair);
18429
+ }).catch((error) => this.report(error));
18370
18430
  }
18371
18431
  queueChange() {
18372
18432
  const run5 = this.work.then(() => this.changed());
@@ -18408,26 +18468,9 @@ var Connector = class {
18408
18468
  this.stopWatching();
18409
18469
  this.stopWatching = this.discovery.watch(this.rescan);
18410
18470
  for (const project of projects) {
18411
- const old = previous.get(project.id);
18412
- if (JSON.stringify(old) === JSON.stringify(project)) continue;
18471
+ if (JSON.stringify(previous.get(project.id)) === JSON.stringify(project)) continue;
18413
18472
  for (const pair of this.activePairs()) await this.send(pair, { type: "project", project });
18414
- for (const thread of project.threads) {
18415
- const before = old?.threads.find((item) => item.id === thread.id);
18416
- if (before && before.updatedAt !== thread.updatedAt && !this.discovery.locations.get(thread.id)?.live && !this.attached(thread)) {
18417
- const last = this.updatedAlerts.get(thread.id) ?? 0;
18418
- if (Date.now() - last < HISTORY_ALERT_INTERVAL_MS) continue;
18419
- this.updatedAlerts.set(thread.id, Date.now());
18420
- await this.attention(
18421
- project.id,
18422
- thread.id,
18423
- thread.title,
18424
- "The existing thread was updated.",
18425
- "updated"
18426
- );
18427
- }
18428
- }
18429
18473
  }
18430
- for (const pair of this.activePairs()) await this.listProjects(pair);
18431
18474
  for (const { project, thread, session } of live) {
18432
18475
  const snapshot = this.threadState(project, thread);
18433
18476
  const prior = this.nativeSnapshots.get(thread.id);
@@ -18446,6 +18489,7 @@ var Connector = class {
18446
18489
  "complete"
18447
18490
  );
18448
18491
  }
18492
+ for (const pair of this.activePairs()) await this.listProjects(pair);
18449
18493
  await this.publishQueueReceipts();
18450
18494
  }
18451
18495
  refreshQueues() {
@@ -18745,63 +18789,81 @@ var Connector = class {
18745
18789
  };
18746
18790
 
18747
18791
  // src/install.ts
18792
+ import { existsSync as existsSync6 } from "node:fs";
18748
18793
  import { homedir as homedir6 } from "node:os";
18749
- import { dirname as dirname5, join as join8 } from "node:path";
18750
- import { fileURLToPath as fileURLToPath3 } from "node:url";
18751
18794
  import { promisify as promisify4 } from "node:util";
18752
- import { execFile as execFile4 } from "node:child_process";
18753
- import { mkdir, writeFile } from "node:fs/promises";
18795
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
18796
+ import { execFile as execFile4, spawn as spawn4 } from "node:child_process";
18797
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
18798
+ import { dirname as dirname5, join as join8, relative, sep } from "node:path";
18754
18799
  var run4 = promisify4(execFile4);
18755
18800
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18756
- async function installStartup() {
18757
- const entry = fileURLToPath3(import.meta.url);
18758
- if (!entry.endsWith("/dist/index.js") && !entry.endsWith("\\dist\\index.js"))
18759
- throw new Error(
18760
- "Build the connector before installing startup: pnpm --filter @dashup/connector build"
18761
- );
18762
- const node = process.execPath;
18763
- if (process.platform === "darwin") {
18764
- const path = join8(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist");
18765
- await mkdir(dirname5(path), { recursive: true });
18766
- await writeFile(
18767
- path,
18768
- `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.dashup.connector</string><key>ProgramArguments</key><array><string>${xml(node)}</string><string>${xml(entry)}</string><string>watch</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/></dict></plist>`
18769
- );
18770
- await run4("launchctl", ["load", path]);
18771
- } else if (process.platform === "win32") {
18772
- const path = join8(
18773
- process.env.APPDATA ?? join8(homedir6(), "AppData/Roaming"),
18774
- "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18775
- );
18776
- const quote = (value2) => '""' + value2.replaceAll('"', '""') + '""';
18777
- await writeFile(
18778
- path,
18779
- `CreateObject("WScript.Shell").Run "${quote(node)} ${quote(entry)} watch", 0, False\r
18780
- `
18781
- );
18782
- } else {
18783
- const path = join8(homedir6(), ".config/systemd/user/dashup.service");
18784
- await mkdir(dirname5(path), { recursive: true });
18785
- const quote = (value2) => '"' + value2.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%") + '"';
18786
- await writeFile(
18787
- path,
18788
- `[Unit]
18801
+ var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
18802
+ var serviceFile = () => process.platform === "darwin" ? join8(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join8(
18803
+ process.env.APPDATA ?? join8(homedir6(), "AppData/Roaming"),
18804
+ "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18805
+ ) : join8(homedir6(), ".config/systemd/user/dashup.service");
18806
+ var startupInstalled = () => existsSync6(serviceFile());
18807
+ function service(platform, node, entry, config2) {
18808
+ const command2 = [node, ...NODE_FLAGS, entry];
18809
+ if (platform === "darwin")
18810
+ return `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.dashup.connector</string><key>EnvironmentVariables</key><dict><key>DASHUP_CONFIG</key><string>${xml(config2)}</string></dict><key>ProgramArguments</key><array>${[...command2, "watch"].map((value2) => `<string>${xml(value2)}</string>`).join("")}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict></dict></plist>`;
18811
+ if (platform === "win32") {
18812
+ const quote2 = (value2) => '""' + value2.replaceAll('"', '""') + '""';
18813
+ return `Set shell = CreateObject("WScript.Shell")\r
18814
+ shell.Environment("Process")("DASHUP_CONFIG") = "${config2.replaceAll('"', '""')}"\r
18815
+ shell.Run "${command2.map(quote2).join(" ")} watch", 0, False\r
18816
+ `;
18817
+ }
18818
+ const quote = (value2) => '"' + value2.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%") + '"';
18819
+ return `[Unit]
18789
18820
  Description=Dashup desktop connector
18790
18821
  After=network-online.target
18791
18822
 
18792
18823
  [Service]
18793
- ExecStart=${quote(node)} ${quote(entry)} watch
18824
+ Environment=${quote(`DASHUP_CONFIG=${config2}`)}
18825
+ ExecStart=${command2.map(quote).join(" ")} watch
18794
18826
  Restart=on-failure
18795
18827
  RestartSec=5
18796
18828
 
18797
18829
  [Install]
18798
18830
  WantedBy=default.target
18799
- `
18831
+ `;
18832
+ }
18833
+ async function durableEntry() {
18834
+ const entry = fileURLToPath3(import.meta.url);
18835
+ if (!entry.endsWith(`${sep}dist${sep}index.js`))
18836
+ throw new Error(
18837
+ "Build the connector before installing startup: pnpm --filter @dashup/connector build"
18800
18838
  );
18839
+ if (!["_npx", "dlx"].some((cache2) => entry.includes(`${sep}${cache2}${sep}`))) return entry;
18840
+ const root = entry.slice(0, entry.indexOf(`${sep}node_modules${sep}`));
18841
+ const app = join8(dirname5(configPath()), "app");
18842
+ await rm(app, { recursive: true, force: true });
18843
+ await cp(join8(root, "node_modules"), join8(app, "node_modules"), {
18844
+ recursive: true,
18845
+ verbatimSymlinks: true
18846
+ });
18847
+ return join8(app, relative(root, entry));
18848
+ }
18849
+ async function installStartup() {
18850
+ const path = serviceFile();
18851
+ await mkdir(dirname5(path), { recursive: true });
18852
+ await writeFile(
18853
+ path,
18854
+ service(process.platform, process.execPath, await durableEntry(), configPath())
18855
+ );
18856
+ if (process.platform === "darwin") {
18857
+ await run4("launchctl", ["unload", path]).catch(() => {
18858
+ });
18859
+ await run4("launchctl", ["load", path]);
18860
+ } else if (process.platform === "win32") {
18861
+ spawn4("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
18862
+ } else {
18801
18863
  await run4("systemctl", ["--user", "daemon-reload"]);
18802
- await run4("systemctl", ["--user", "enable", "--now", "dashup.service"]);
18864
+ await run4("systemctl", ["--user", "enable", "dashup.service"]);
18865
+ await run4("systemctl", ["--user", "restart", "dashup.service"]);
18803
18866
  }
18804
- process.stdout.write("Dashup will start when you sign in to this computer.\n");
18805
18867
  }
18806
18868
 
18807
18869
  // src/index.ts
@@ -18820,6 +18882,42 @@ var refused = (error) => {
18820
18882
  if (error.cause?.code === "ECONNREFUSED") return null;
18821
18883
  throw error;
18822
18884
  };
18885
+ var pause = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
18886
+ async function answering(expected) {
18887
+ for (let tries = 0; tries < 40; tries++) {
18888
+ const status2 = await running("/status", { signal: AbortSignal.timeout(1e3) }).catch(
18889
+ () => null
18890
+ );
18891
+ if (Boolean(status2?.ok) === expected) return true;
18892
+ await pause(250);
18893
+ }
18894
+ return false;
18895
+ }
18896
+ var BACKGROUND = "Run Dashup in the background and start it when you sign in?";
18897
+ var DONE2 = "Your projects appear on your phone.";
18898
+ async function background() {
18899
+ const waiting = interactive ? ui2.spinner() : void 0;
18900
+ waiting?.start("Starting Dashup in the background");
18901
+ let failure = "Dashup did not start in the background. Run dashup watch to see why.";
18902
+ try {
18903
+ await running("/stop", { method: "POST" }).catch(() => null);
18904
+ await answering(false);
18905
+ await installStartup();
18906
+ if (await answering(true)) {
18907
+ const message = "Dashup runs in the background and starts when you sign in.";
18908
+ if (waiting) waiting.stop(message);
18909
+ else process.stdout.write(`${message}
18910
+ `);
18911
+ return true;
18912
+ }
18913
+ } catch (error) {
18914
+ if (error instanceof Error) failure = error.message;
18915
+ }
18916
+ if (waiting) waiting.error(failure);
18917
+ else process.stderr.write(`${failure}
18918
+ `);
18919
+ return false;
18920
+ }
18823
18921
  var authorized = (request2) => {
18824
18922
  const expected = Buffer.from(`Bearer ${config.localSecret}`);
18825
18923
  const actual = Buffer.from(request2.headers.authorization ?? "");
@@ -18830,7 +18928,7 @@ var Attention = z3.object({
18830
18928
  threadId: z3.string().default(""),
18831
18929
  title: z3.string().min(1).max(200),
18832
18930
  body: z3.string().max(4e3).default(""),
18833
- reason: z3.enum(["input", "approval", "error", "complete", "updated"]).default("input")
18931
+ reason: z3.enum(["input", "approval", "error", "complete"]).default("input")
18834
18932
  }).strict();
18835
18933
  async function main() {
18836
18934
  if (option("--api")) {
@@ -18843,7 +18941,11 @@ async function main() {
18843
18941
  throw new Error("Use a separate DASHUP_CONFIG file to connect a different server");
18844
18942
  config.api = url2.origin;
18845
18943
  }
18846
- if (command === "install") return installStartup();
18944
+ if (command === "install") {
18945
+ if (!config.secret) throw new Error("Pair this computer first: run dashup pair.");
18946
+ if (!await background()) process.exitCode = 1;
18947
+ return;
18948
+ }
18847
18949
  if (command === "stop") {
18848
18950
  const response = await running("/stop", { method: "POST" }).catch(refused);
18849
18951
  if (!response) throw new Error("Dashup is not running on this computer");
@@ -18938,10 +19040,18 @@ async function main() {
18938
19040
  );
18939
19041
  return;
18940
19042
  }
18941
- const pairPhone = async (invite) => {
18942
- const { email } = await scanned(config, invite);
18943
- process.stdout.write(`Paired a phone signed in as ${email}.
18944
- `);
19043
+ const pairPhone = (invite) => scanned(config, invite, (email) => `Paired a phone signed in as ${email}`);
19044
+ const keepRunning = async (stopHere) => {
19045
+ if (!interactive) return;
19046
+ const answer = startupInstalled() || await ui2.confirm({ message: BACKGROUND });
19047
+ if (answer === true) {
19048
+ stopHere();
19049
+ const started = await background();
19050
+ ui2.outro(started ? DONE2 : "Run dashup to keep Dashup running in this terminal.");
19051
+ process.exit(started ? 0 : 1);
19052
+ }
19053
+ ui2.log.info("Dashup runs in this terminal. Keep it open; Ctrl+C stops it.");
19054
+ ui2.outro(DONE2);
18945
19055
  };
18946
19056
  const status2 = await running("/status", { signal: AbortSignal.timeout(1e3) }).catch(refused);
18947
19057
  if (status2?.ok) {
@@ -18956,8 +19066,17 @@ async function main() {
18956
19066
  throw new Error(
18957
19067
  "An older Dashup connector is running. Run dashup stop, then run dashup pair with the updated connector."
18958
19068
  );
18959
- return pairPhone(invite.data);
19069
+ if (interactive) intro2();
19070
+ await pairPhone(invite.data);
19071
+ if (interactive && !startupInstalled()) {
19072
+ const answer = await ui2.confirm({ message: BACKGROUND });
19073
+ if (answer === true && !await background()) process.exitCode = 1;
19074
+ }
19075
+ if (interactive) ui2.outro(DONE2);
19076
+ process.exit();
18960
19077
  }
19078
+ if (!config.secret && !interactive)
19079
+ throw new Error("Run dashup pair in a terminal to pair this computer.");
18961
19080
  let connector;
18962
19081
  const server = createServer(async (request2, response) => {
18963
19082
  if (!authorized(request2)) {
@@ -19036,12 +19155,26 @@ async function main() {
19036
19155
  });
19037
19156
  try {
19038
19157
  const linking = !config.secret;
19039
- if (linking) await link(config);
19158
+ if (linking) {
19159
+ intro2();
19160
+ if (!await link(config)) process.exit(1);
19161
+ await keepRunning(() => server.close());
19162
+ }
19040
19163
  connector = new Connector(config);
19041
19164
  await connector.start();
19042
- if (command === "pair" && !linking)
19043
- pairPhone(connector.invite()).catch((error) => process.stderr.write(`${error instanceof Error ? error.message : "Pairing failed"}
19044
- `));
19165
+ if (command === "pair" && !linking) {
19166
+ if (interactive) intro2();
19167
+ const paired = await pairPhone(connector.invite()).catch((error) => {
19168
+ if (!reported(error))
19169
+ process.stderr.write(`${error instanceof Error ? error.message : "Pairing failed"}
19170
+ `);
19171
+ });
19172
+ if (paired)
19173
+ await keepRunning(() => {
19174
+ connector?.stop();
19175
+ server.close();
19176
+ });
19177
+ }
19045
19178
  } catch (error) {
19046
19179
  connector?.stop();
19047
19180
  server.close();
@@ -19049,7 +19182,8 @@ async function main() {
19049
19182
  }
19050
19183
  }
19051
19184
  main().catch((error) => {
19052
- process.stderr.write(`${error instanceof Error ? error.message : "Dashup failed"}
19185
+ if (!reported(error))
19186
+ process.stderr.write(`${error instanceof Error ? error.message : "Dashup failed"}
19053
19187
  `);
19054
19188
  process.exit(1);
19055
19189
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dashup": "dist/index.js"
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@agentclientprotocol/sdk": "1.9.0",
19
+ "@clack/prompts": "^1.8.1",
19
20
  "@microsoft/agent-host-protocol": "1.0.0",
20
21
  "qrcode": "^1.5.4",
21
22
  "ws": "^8.21.3",
@@ -29,10 +30,10 @@
29
30
  "tsx": "^4.22.4",
30
31
  "typescript": "^5.9.3",
31
32
  "@dashup/models": "0.1.0",
32
- "@dashup/typescript-config": "0.0.0",
33
33
  "@dashup/protocol": "0.1.0",
34
- "@parcae/sdk": "0.8.1",
35
- "@parcae/lynx": "0.1.0"
34
+ "@dashup/typescript-config": "0.0.0",
35
+ "@parcae/lynx": "0.1.0",
36
+ "@parcae/sdk": "0.8.1"
36
37
  },
37
38
  "scripts": {
38
39
  "dashup": "tsx src/index.ts",
@@ -40,7 +41,7 @@
40
41
  "start": "node dist/index.js watch",
41
42
  "build": "tsup",
42
43
  "check-types": "tsc --noEmit",
43
- "test": "node --import tsx --test tests/connector.test.ts tests/history.test.ts tests/harness-history.test.ts tests/harness-live.test.ts tests/harness-queue.test.ts tests/harness-vscode.test.ts tests/stop.test.ts tests/viewer.test.ts",
44
+ "test": "node --import tsx --test tests/connector.test.ts tests/history.test.ts tests/harness-history.test.ts tests/harness-live.test.ts tests/harness-queue.test.ts tests/harness-vscode.test.ts tests/install.test.ts tests/stop.test.ts tests/viewer.test.ts",
44
45
  "test:workflow": "node --import tsx tests/workflow.test.ts"
45
46
  }
46
47
  }