@dashup/connector 0.1.5 → 0.1.6
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 +4 -2
- package/dist/index.js +148 -40
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ dashup alert --project PROJECT_ID --title "Dashup is connected"
|
|
|
22
22
|
|
|
23
23
|
The connector automatically reads existing Claude Code, Codex, and OpenCode conversations alongside Zed workspaces and VS Code chat history. It honors the harness's existing data directory, authentication, and native session IDs. Native stores are opened read-only; discovery never launches an agent, creates or resumes a conversation, or changes editor settings. Only a session the phone starts runs an agent (see below). Opening the same supported harness session through Zed and its native history uses one Dashup thread when their actual session identities match. Matching directory names alone never merges local and SSH projects.
|
|
24
24
|
|
|
25
|
-
A thread's title is the agent's own (a rename, Codex's stored title, Claude's title), else the first message the person typed: Codex's injected AGENTS.md and environment messages and Claude Code's slash commands are skipped, in titles and in the conversation, and a Codex subagent is named for its task. A thread's time is its last conversation message in the agent's own history; editor bookkeeping such as Zed's sidebar restamps never moves it. Subagent sessions (OpenCode children, Codex spawned agents) carry their parent thread. Tool calls are joined with their results into one step with status, duration and, for file edits, a unified diff. Saved history has unknown live status
|
|
25
|
+
A thread's title is the agent's own (a rename, Codex's stored title, Claude's title), else the first message the person typed: Codex's injected AGENTS.md and environment messages and Claude Code's slash commands are skipped, in titles and in the conversation, and a Codex subagent is named for its task. A thread's time is its last conversation message in the agent's own history; editor bookkeeping such as Zed's sidebar restamps never moves it. Subagent sessions (OpenCode children, Codex spawned agents) carry their parent thread. Tool calls are joined with their results into one step with status, duration and, for file edits, a unified diff. Saved history has unknown live status; the phone can answer it by resuming it (see Starting sessions from the phone). File changes publish project metadata; only an approval, a question, an error or a finished turn sends an alert. A workspace becomes a project once it has a conversation. After every change the connector publishes its overview: every project, and the conversations Home shows (anything that needs the person or is working, then the newest 20). The phone renders Home from the overview alone and decodes a project's full snapshot only when a page opens it. Only a scan that read every source retires a project it published earlier and left off. Each phone read request reads a thread's native history once and publishes a snapshot of separately encrypted message fragments to the database. The phone reads bounded latest and earlier pages; realtime thread updates carry only summary and approval metadata. Native history remains the source, and an incomplete upload never replaces the previous published snapshot. Native live interfaces provide actual status and pending choices. Tool integrations can also queue explicit encrypted alerts with `alert`.
|
|
26
26
|
|
|
27
27
|
Project paths, SSH details, thread content, approvals, and alerts are encrypted separately for every paired phone. The encrypted outbox survives a connector restart. Parcae handles realtime delivery and reconnect; replay counters are saved before commands execute. Revoking a pairing stops its viewer and discards its connector key and queued content.
|
|
28
28
|
|
|
@@ -53,7 +53,9 @@ Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the la
|
|
|
53
53
|
|
|
54
54
|
## Starting sessions from the phone
|
|
55
55
|
|
|
56
|
-
The phone starts a session in a local project, or in a new folder inside one of the folders that already hold this computer's projects, with Claude Code, Codex or OpenCode: whichever of `claude`, `codex` and `opencode` runs here. The agent works with its own settings and permission rules, and asks the phone before anything they don't allow. Claude Code runs headless (`claude -p`) for each turn and resumes the session for the next; Codex threads start in Codex's own app-server daemon (`codex app-server daemon start`) and outlive the connector; OpenCode sessions run on an `opencode serve` the connector starts for that folder. Claude Code
|
|
56
|
+
The phone starts a session in a local project, or in a new folder inside one of the folders that already hold this computer's projects, with Claude Code, Codex or OpenCode: whichever of `claude`, `codex` and `opencode` runs here. The agent works with its own settings and permission rules, and asks the phone before anything they don't allow. Claude Code runs headless (`claude -p`) for each turn and resumes the session for the next; Codex threads start in Codex's own app-server daemon (`codex app-server daemon start`) and outlive the connector; OpenCode sessions run on an `opencode serve` the connector starts for that folder. Claude Code's permission requests arrive with the command, or the diff an edit or write would make.
|
|
57
|
+
|
|
58
|
+
A saved conversation is answered the same way when its agent runs here, its project is local, and nothing else has it open: Claude Code resumes it (`claude -p --resume`), Codex loads it in its daemon (`thread/resume`), and OpenCode serves its folder. One a native Codex process owns takes queued messages instead. A start expires with other commands after 60 seconds.
|
|
57
59
|
|
|
58
60
|
## Optional ACP wrapper
|
|
59
61
|
|
package/dist/index.js
CHANGED
|
@@ -4017,7 +4017,7 @@ import * as ui from "@clack/prompts";
|
|
|
4017
4017
|
import { z } from "zod";
|
|
4018
4018
|
|
|
4019
4019
|
// package.json
|
|
4020
|
-
var version = "0.1.
|
|
4020
|
+
var version = "0.1.6";
|
|
4021
4021
|
|
|
4022
4022
|
// src/pair.ts
|
|
4023
4023
|
var Invitation = z.object({
|
|
@@ -4644,17 +4644,17 @@ function step(id, name, input, result) {
|
|
|
4644
4644
|
}
|
|
4645
4645
|
var claudeHome = () => process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
|
|
4646
4646
|
var claudeMessage = (row) => (row.type === "user" || row.type === "assistant") && !row.isSidechain;
|
|
4647
|
-
function claudeChange(
|
|
4648
|
-
if (typeof
|
|
4649
|
-
const hunks = list(
|
|
4647
|
+
function claudeChange(change2) {
|
|
4648
|
+
if (typeof change2?.filePath !== "string") return void 0;
|
|
4649
|
+
const hunks = list(change2.structuredPatch);
|
|
4650
4650
|
if (!hunks.length)
|
|
4651
|
-
return
|
|
4651
|
+
return change2.type === "create" && typeof change2.content === "string" ? added(change2.filePath, change2.content) : void 0;
|
|
4652
4652
|
return [
|
|
4653
|
-
`--- ${
|
|
4654
|
-
`+++ ${
|
|
4655
|
-
...hunks.flatMap((
|
|
4656
|
-
`@@ -${
|
|
4657
|
-
...list(
|
|
4653
|
+
`--- ${change2.filePath}`,
|
|
4654
|
+
`+++ ${change2.filePath}`,
|
|
4655
|
+
...hunks.flatMap((hunk2) => [
|
|
4656
|
+
`@@ -${hunk2?.oldStart},${hunk2?.oldLines} +${hunk2?.newStart},${hunk2?.newLines} @@`,
|
|
4657
|
+
...list(hunk2?.lines)
|
|
4658
4658
|
])
|
|
4659
4659
|
].join("\n");
|
|
4660
4660
|
}
|
|
@@ -16677,12 +16677,12 @@ var Client = class {
|
|
|
16677
16677
|
this.requests.set(id, result);
|
|
16678
16678
|
return result;
|
|
16679
16679
|
}
|
|
16680
|
-
// Starts a thread in `cwd` with Codex's own settings, then
|
|
16681
|
-
async start(cwd, text3) {
|
|
16680
|
+
// Starts a thread in `cwd` with Codex's own settings, or loads a saved one, then a turn.
|
|
16681
|
+
async start(cwd, text3, saved) {
|
|
16682
16682
|
await this.connect();
|
|
16683
16683
|
if (!this.controls) throw new Error("Update Codex to start threads from Dashup");
|
|
16684
|
-
const id = (await this.call("thread/start", { cwd })).thread?.id;
|
|
16685
|
-
if (typeof id !== "string") throw new Error("Codex did not
|
|
16684
|
+
const id = saved ? (await this.call("thread/resume", { threadId: saved, excludeTurns: true })).thread?.id : (await this.call("thread/start", { cwd })).thread?.id;
|
|
16685
|
+
if (typeof id !== "string") throw new Error("Codex did not open the thread");
|
|
16686
16686
|
await this.call("turn/start", { threadId: id, input: [{ type: "text", text: text3, text_elements: [] }] });
|
|
16687
16687
|
this.changed();
|
|
16688
16688
|
return id;
|
|
@@ -16771,8 +16771,8 @@ var Codex = class {
|
|
|
16771
16771
|
clients = /* @__PURE__ */ new Map();
|
|
16772
16772
|
// Control sockets of daemons this connector started, which discovery may not look for.
|
|
16773
16773
|
started = /* @__PURE__ */ new Set();
|
|
16774
|
-
// Codex's own daemon runs the threads Dashup starts, so they outlive this connector.
|
|
16775
|
-
async start(cwd, text3) {
|
|
16774
|
+
// Codex's own daemon runs the threads Dashup starts or resumes, so they outlive this connector.
|
|
16775
|
+
async start(cwd, text3, saved) {
|
|
16776
16776
|
const path = process.env.DASHUP_CODEX_SOCKET || join4(codexHome(), ...SOCKET_PARTS);
|
|
16777
16777
|
await promisify3(execFile3)("codex", ["app-server", "daemon", "start"], {
|
|
16778
16778
|
timeout: 6e4,
|
|
@@ -16784,7 +16784,10 @@ var Codex = class {
|
|
|
16784
16784
|
client = new Client(path, this.changed);
|
|
16785
16785
|
this.clients.set(path, client);
|
|
16786
16786
|
}
|
|
16787
|
-
return client.start(cwd, text3);
|
|
16787
|
+
return client.start(cwd, text3, saved);
|
|
16788
|
+
}
|
|
16789
|
+
async resume(id, cwd, text3) {
|
|
16790
|
+
await this.start(cwd, text3, id);
|
|
16788
16791
|
}
|
|
16789
16792
|
async refresh() {
|
|
16790
16793
|
const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
|
|
@@ -16813,6 +16816,7 @@ var Codex = class {
|
|
|
16813
16816
|
|
|
16814
16817
|
// src/harness/headless.ts
|
|
16815
16818
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
16819
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
16816
16820
|
import { createInterface as createInterface3 } from "node:readline";
|
|
16817
16821
|
import { spawn as spawn3 } from "node:child_process";
|
|
16818
16822
|
var CHOICES = [
|
|
@@ -16822,12 +16826,63 @@ var CHOICES = [
|
|
|
16822
16826
|
var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
|
|
16823
16827
|
var MAX_REQUESTS = 1e3;
|
|
16824
16828
|
var NOTIFY_MS = 100;
|
|
16829
|
+
function hunk(before, after, offset = 0) {
|
|
16830
|
+
const old = before ? before.replace(/\n$/, "").split("\n") : [];
|
|
16831
|
+
const next = after ? after.replace(/\n$/, "").split("\n") : [];
|
|
16832
|
+
let start = 0;
|
|
16833
|
+
while (start < old.length && start < next.length && old[start] === next[start]) start++;
|
|
16834
|
+
let end = 0;
|
|
16835
|
+
while (end < old.length - start && end < next.length - start && old[old.length - 1 - end] === next[next.length - 1 - end])
|
|
16836
|
+
end++;
|
|
16837
|
+
const removed = old.slice(start, old.length - end);
|
|
16838
|
+
const added2 = next.slice(start, next.length - end);
|
|
16839
|
+
return {
|
|
16840
|
+
lines: [
|
|
16841
|
+
`@@ -${offset + start + 1},${removed.length} +${offset + start + 1},${added2.length} @@`,
|
|
16842
|
+
...old.slice(Math.max(0, start - 3), start).map((line) => ` ${line}`),
|
|
16843
|
+
...removed.map((line) => `-${line}`),
|
|
16844
|
+
...added2.map((line) => `+${line}`),
|
|
16845
|
+
...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
|
|
16846
|
+
],
|
|
16847
|
+
added: added2.length,
|
|
16848
|
+
deleted: removed.length
|
|
16849
|
+
};
|
|
16850
|
+
}
|
|
16851
|
+
function change(tool, input) {
|
|
16852
|
+
const path = input.file_path;
|
|
16853
|
+
if (typeof path !== "string") return void 0;
|
|
16854
|
+
let current = "";
|
|
16855
|
+
try {
|
|
16856
|
+
current = readFileSync3(path, "utf8");
|
|
16857
|
+
} catch {
|
|
16858
|
+
}
|
|
16859
|
+
const edit = (value2) => {
|
|
16860
|
+
const before = String(value2?.old_string ?? "");
|
|
16861
|
+
const at = before ? current.indexOf(before) : -1;
|
|
16862
|
+
return hunk(
|
|
16863
|
+
before,
|
|
16864
|
+
String(value2?.new_string ?? ""),
|
|
16865
|
+
at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
|
|
16866
|
+
);
|
|
16867
|
+
};
|
|
16868
|
+
const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
|
|
16869
|
+
if (!hunks.length) return void 0;
|
|
16870
|
+
const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
|
|
16871
|
+
if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
|
|
16872
|
+
return {
|
|
16873
|
+
path,
|
|
16874
|
+
diff,
|
|
16875
|
+
added: hunks.reduce((sum, item) => sum + item.added, 0),
|
|
16876
|
+
deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
|
|
16877
|
+
};
|
|
16878
|
+
}
|
|
16825
16879
|
function approval2(id, request2) {
|
|
16826
16880
|
const input = request2.input ?? {};
|
|
16827
16881
|
const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
|
|
16882
|
+
const diff = change(request2.tool_name, input);
|
|
16828
16883
|
let detail = [
|
|
16829
16884
|
typeof request2.description === "string" ? request2.description : "",
|
|
16830
|
-
typeof input.command === "string" ? input.command : JSON.stringify(input, null, 2)
|
|
16885
|
+
typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
|
|
16831
16886
|
].filter(Boolean).join("\n\n");
|
|
16832
16887
|
if (Buffer.byteLength(detail) > MAX_PREVIEW)
|
|
16833
16888
|
detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
|
|
@@ -16835,28 +16890,30 @@ function approval2(id, request2) {
|
|
|
16835
16890
|
id,
|
|
16836
16891
|
title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
|
|
16837
16892
|
detail,
|
|
16893
|
+
...diff ? { change: diff } : {},
|
|
16838
16894
|
choices: CHOICES.map((choice) => ({ ...choice }))
|
|
16839
16895
|
};
|
|
16840
16896
|
}
|
|
16841
16897
|
var Session = class {
|
|
16842
|
-
|
|
16898
|
+
// A resumed session is `saved` and keeps the title its history gives.
|
|
16899
|
+
constructor(id, cwd, title2, changed, saved = false) {
|
|
16843
16900
|
this.id = id;
|
|
16844
16901
|
this.cwd = cwd;
|
|
16845
16902
|
this.title = title2;
|
|
16846
16903
|
this.changed = changed;
|
|
16904
|
+
this.saved = saved;
|
|
16847
16905
|
}
|
|
16848
16906
|
id;
|
|
16849
16907
|
cwd;
|
|
16850
16908
|
title;
|
|
16851
16909
|
changed;
|
|
16910
|
+
saved;
|
|
16852
16911
|
messages = [];
|
|
16853
16912
|
status = "idle";
|
|
16854
16913
|
updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16855
16914
|
turn;
|
|
16856
16915
|
// Messages written to this turn's process that Claude Code has not finished answering.
|
|
16857
16916
|
open = 0;
|
|
16858
|
-
// Once Claude Code has saved the session, later turns resume it.
|
|
16859
|
-
saved = false;
|
|
16860
16917
|
approvals = /* @__PURE__ */ new Map();
|
|
16861
16918
|
requests = /* @__PURE__ */ new Map();
|
|
16862
16919
|
write(value2) {
|
|
@@ -17024,6 +17081,18 @@ var Headless = class {
|
|
|
17024
17081
|
}
|
|
17025
17082
|
return session.id;
|
|
17026
17083
|
}
|
|
17084
|
+
// Continues a saved session with a message; Claude Code resumes it from its own transcript.
|
|
17085
|
+
async resume(id, cwd, text3) {
|
|
17086
|
+
const known = this.sessions.get(id);
|
|
17087
|
+
const session = known ?? new Session(id, cwd, void 0, this.soon, true);
|
|
17088
|
+
this.sessions.set(id, session);
|
|
17089
|
+
try {
|
|
17090
|
+
await session.say(text3);
|
|
17091
|
+
} catch (error) {
|
|
17092
|
+
if (!known) this.sessions.delete(id);
|
|
17093
|
+
throw error;
|
|
17094
|
+
}
|
|
17095
|
+
}
|
|
17027
17096
|
async refresh() {
|
|
17028
17097
|
return [...this.sessions.values()].map((session) => session.live());
|
|
17029
17098
|
}
|
|
@@ -17804,7 +17873,7 @@ function permissionApproval(permission, history, cwd) {
|
|
|
17804
17873
|
const command2 = typeof input?.command === "string" ? input.command : metadata2?.command;
|
|
17805
17874
|
const path = typeof metadata2?.filepath === "string" ? metadata2.filepath : input?.filePath;
|
|
17806
17875
|
const diff = typeof metadata2?.diff === "string" ? metadata2.diff : "";
|
|
17807
|
-
const
|
|
17876
|
+
const change2 = typeof path === "string" && path.trim() && diff.trim() ? { path, diff } : void 0;
|
|
17808
17877
|
const patterns = Array.isArray(permission.patterns) ? permission.patterns.filter((value2) => typeof value2 === "string") : [];
|
|
17809
17878
|
const always = Array.isArray(permission.always) ? permission.always.filter((value2) => typeof value2 === "string") : [];
|
|
17810
17879
|
let detail = [
|
|
@@ -17812,7 +17881,7 @@ function permissionApproval(permission, history, cwd) {
|
|
|
17812
17881
|
${command2}` : "",
|
|
17813
17882
|
hasInput ? `Tool input:
|
|
17814
17883
|
${JSON.stringify(input, null, 2)}` : "",
|
|
17815
|
-
diff && !
|
|
17884
|
+
diff && !change2 ? `Proposed diff:
|
|
17816
17885
|
${diff}` : "",
|
|
17817
17886
|
`Working directory: ${cwd}`,
|
|
17818
17887
|
patterns.length ? `Permission scope:
|
|
@@ -17821,10 +17890,10 @@ ${patterns.join("\n")}` : "",
|
|
|
17821
17890
|
${always.join("\n")}` : ""
|
|
17822
17891
|
].filter(Boolean).join("\n\n");
|
|
17823
17892
|
const oversized = Buffer.byteLength(detail) > MAX_PREVIEW || Buffer.byteLength(diff) > MAX_PREVIEW;
|
|
17824
|
-
const unavailable = !(hasInput ||
|
|
17893
|
+
const unavailable = !(hasInput || change2 || typeof command2 === "string" && command2.trim()) || oversized;
|
|
17825
17894
|
if (oversized) {
|
|
17826
17895
|
detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
|
|
17827
|
-
if (
|
|
17896
|
+
if (change2) change2.diff = Buffer.from(diff).subarray(0, MAX_PREVIEW).toString("utf8");
|
|
17828
17897
|
}
|
|
17829
17898
|
if (unavailable) detail += `
|
|
17830
17899
|
|
|
@@ -17834,7 +17903,7 @@ ${SCREEN_PREVIEW}`;
|
|
|
17834
17903
|
title: `Allow ${String(permission.permission ?? "tool")}?`,
|
|
17835
17904
|
detail,
|
|
17836
17905
|
choices: PERMISSION_CHOICES.filter((choice) => !unavailable || choice.id === "reject").map((choice) => ({ ...choice })),
|
|
17837
|
-
...
|
|
17906
|
+
...change2 ? { change: change2 } : {}
|
|
17838
17907
|
};
|
|
17839
17908
|
}
|
|
17840
17909
|
function promptContext(history) {
|
|
@@ -17897,14 +17966,20 @@ var Server = class {
|
|
|
17897
17966
|
this.requests.set(id, pending);
|
|
17898
17967
|
return pending;
|
|
17899
17968
|
}
|
|
17900
|
-
// A
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17969
|
+
// A message to one of this folder's sessions, in the agent and model it last used; a new
|
|
17970
|
+
// session takes OpenCode's defaults.
|
|
17971
|
+
async prompt(id, cwd, text3) {
|
|
17972
|
+
const existing = await this.request(`/session/${encodeURIComponent(id)}/message?limit=200`, cwd);
|
|
17904
17973
|
await this.request(`/session/${encodeURIComponent(id)}/prompt_async`, cwd, {
|
|
17974
|
+
...promptContext(existing),
|
|
17905
17975
|
parts: [{ type: "text", text: text3 }]
|
|
17906
17976
|
});
|
|
17907
17977
|
this.changed();
|
|
17978
|
+
}
|
|
17979
|
+
async start(cwd, text3) {
|
|
17980
|
+
const id = (await this.request("/session", cwd, {}))?.id;
|
|
17981
|
+
if (typeof id !== "string") throw new Error("OpenCode did not start a session");
|
|
17982
|
+
await this.prompt(id, cwd, text3);
|
|
17908
17983
|
return id;
|
|
17909
17984
|
}
|
|
17910
17985
|
async read() {
|
|
@@ -18139,7 +18214,8 @@ var OpenCode = class {
|
|
|
18139
18214
|
servers = /* @__PURE__ */ new Map();
|
|
18140
18215
|
// Servers this connector started, by folder.
|
|
18141
18216
|
started = /* @__PURE__ */ new Map();
|
|
18142
|
-
|
|
18217
|
+
// The server this connector runs for a folder, started when first needed.
|
|
18218
|
+
async served(cwd) {
|
|
18143
18219
|
let served = this.started.get(cwd);
|
|
18144
18220
|
if (!served) this.started.set(cwd, served = serve(cwd));
|
|
18145
18221
|
const endpoint = await served.endpoint.catch((error) => {
|
|
@@ -18151,7 +18227,13 @@ var OpenCode = class {
|
|
|
18151
18227
|
server = new Server(endpoint, this.changed);
|
|
18152
18228
|
this.servers.set(endpoint.url, server);
|
|
18153
18229
|
}
|
|
18154
|
-
return server
|
|
18230
|
+
return server;
|
|
18231
|
+
}
|
|
18232
|
+
async start(cwd, text3) {
|
|
18233
|
+
return (await this.served(cwd)).start(cwd, text3);
|
|
18234
|
+
}
|
|
18235
|
+
async resume(id, cwd, text3) {
|
|
18236
|
+
await (await this.served(cwd)).prompt(id, cwd, text3);
|
|
18155
18237
|
}
|
|
18156
18238
|
async refresh() {
|
|
18157
18239
|
const discovered = await endpoints().catch(() => []);
|
|
@@ -18245,6 +18327,14 @@ var LiveHarnesses = class {
|
|
|
18245
18327
|
text3
|
|
18246
18328
|
);
|
|
18247
18329
|
}
|
|
18330
|
+
// Continues a saved session with a message, and resolves once its agent takes it.
|
|
18331
|
+
resume(harness, id, cwd, text3) {
|
|
18332
|
+
return { claude: this.headless, codex: this.codex, opencode: this.opencode }[harness].resume(
|
|
18333
|
+
id,
|
|
18334
|
+
cwd,
|
|
18335
|
+
text3
|
|
18336
|
+
);
|
|
18337
|
+
}
|
|
18248
18338
|
watch(onChange) {
|
|
18249
18339
|
this.listeners.add(onChange);
|
|
18250
18340
|
if (!this.timer && process.env.DASHUP_LIVE_DISCOVERY !== "0") {
|
|
@@ -18276,7 +18366,7 @@ import { randomUUID as randomUUID5 } from "node:crypto";
|
|
|
18276
18366
|
import { spawn as spawn5 } from "node:child_process";
|
|
18277
18367
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
18278
18368
|
import { join as join8, dirname as dirname5, basename as basename3 } from "node:path";
|
|
18279
|
-
import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as
|
|
18369
|
+
import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
18280
18370
|
var CODEX_VERSION = "0.160.1";
|
|
18281
18371
|
var REQUEST_MS5 = 1e4;
|
|
18282
18372
|
var MAX_BYTES4 = 1024 * 1024;
|
|
@@ -18298,7 +18388,7 @@ function packageVersion(executable) {
|
|
|
18298
18388
|
for (let depth = 0; depth < 8; depth++, directory2 = dirname5(directory2)) {
|
|
18299
18389
|
const path = join8(directory2, "package.json");
|
|
18300
18390
|
if (!existsSync5(path)) continue;
|
|
18301
|
-
const value2 = JSON.parse(
|
|
18391
|
+
const value2 = JSON.parse(readFileSync4(path, "utf8"));
|
|
18302
18392
|
if (value2.name?.startsWith("@openai/codex-")) return value2.version;
|
|
18303
18393
|
}
|
|
18304
18394
|
return void 0;
|
|
@@ -18308,7 +18398,7 @@ function processOwners(pid) {
|
|
|
18308
18398
|
if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
|
|
18309
18399
|
const executable = realpathSync2.native(join8(root, "exe"));
|
|
18310
18400
|
if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
|
|
18311
|
-
const status2 =
|
|
18401
|
+
const status2 = readFileSync4(join8(root, "stat"), "utf8");
|
|
18312
18402
|
const start = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
|
|
18313
18403
|
if (!start || !/^\d+$/.test(start)) return [];
|
|
18314
18404
|
const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
|
|
@@ -18324,7 +18414,7 @@ function processOwners(pid) {
|
|
|
18324
18414
|
const thread = basename3(path, ".lock");
|
|
18325
18415
|
if (!THREAD_ID.test(thread)) return [];
|
|
18326
18416
|
try {
|
|
18327
|
-
const lock =
|
|
18417
|
+
const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
|
|
18328
18418
|
if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
|
|
18329
18419
|
const home = realpathSync2.native(dirname5(dirname5(path)));
|
|
18330
18420
|
const directory2 = statSync2(home);
|
|
@@ -18725,7 +18815,8 @@ var Connector = class {
|
|
|
18725
18815
|
this.nativeSnapshots.set(thread.id, structuredClone(this.threadState(project, thread)));
|
|
18726
18816
|
}
|
|
18727
18817
|
this.refreshQueues();
|
|
18728
|
-
|
|
18818
|
+
this.refreshResumes();
|
|
18819
|
+
void this.findAgents().then(() => this.queueChange());
|
|
18729
18820
|
this.stopWatching = this.discovery.watch(this.rescan);
|
|
18730
18821
|
this.releases.push(this.harnesses.watch(this.rescan));
|
|
18731
18822
|
this.releases.push(
|
|
@@ -18954,7 +19045,6 @@ var Connector = class {
|
|
|
18954
19045
|
// Agents change only when someone installs or removes one, so a refresh asks again.
|
|
18955
19046
|
async findAgents() {
|
|
18956
19047
|
this.startable = await this.harnesses.agents();
|
|
18957
|
-
this.refreshOverview();
|
|
18958
19048
|
}
|
|
18959
19049
|
async listProjects(pair) {
|
|
18960
19050
|
const overview = this.overview();
|
|
@@ -18985,6 +19075,7 @@ var Connector = class {
|
|
|
18985
19075
|
const native = await this.harnesses.refresh();
|
|
18986
19076
|
const live = native.map((session) => ({ ...this.discovery.observe(session), session }));
|
|
18987
19077
|
this.refreshQueues();
|
|
19078
|
+
this.refreshResumes();
|
|
18988
19079
|
for (const agent of this.agents.values()) if (!agent.socket.destroyed) this.adopt(agent);
|
|
18989
19080
|
const current = new Set(live.map(({ thread }) => thread.id));
|
|
18990
19081
|
for (const [id, snapshot] of this.nativeSnapshots) {
|
|
@@ -19036,6 +19127,17 @@ var Connector = class {
|
|
|
19036
19127
|
for (const pair of this.activePairs()) await this.listProjects(pair);
|
|
19037
19128
|
await this.publishQueueReceipts();
|
|
19038
19129
|
}
|
|
19130
|
+
// A saved session nothing else has open continues headless when its agent runs here. One a
|
|
19131
|
+
// native process owns takes queued messages instead.
|
|
19132
|
+
refreshResumes() {
|
|
19133
|
+
for (const project of this.discovery.projects.values())
|
|
19134
|
+
for (const thread of project.threads) {
|
|
19135
|
+
const location2 = this.discovery.locations.get(thread.id);
|
|
19136
|
+
const harness = thread.agent?.harness;
|
|
19137
|
+
if (location2?.history && !location2.live && !project.remote && !thread.capabilities.queue && harness && harness !== "other" && this.startable.includes(harness))
|
|
19138
|
+
thread.capabilities = { ...thread.capabilities, send: true };
|
|
19139
|
+
}
|
|
19140
|
+
}
|
|
19039
19141
|
refreshQueues() {
|
|
19040
19142
|
this.nativeQueue.refresh();
|
|
19041
19143
|
for (const project of this.discovery.projects.values())
|
|
@@ -19267,9 +19369,15 @@ var Connector = class {
|
|
|
19267
19369
|
throw new Error(
|
|
19268
19370
|
"This session has no live reply connection. Open Screen to use the existing agent."
|
|
19269
19371
|
);
|
|
19270
|
-
const
|
|
19271
|
-
if (
|
|
19372
|
+
const location2 = this.discovery.locations.get(thread.id);
|
|
19373
|
+
if (location2?.live?.send) return location2.live.send(command2.requestId, command2.action.text);
|
|
19272
19374
|
const agent = this.attached(thread);
|
|
19375
|
+
const saved = thread.agent;
|
|
19376
|
+
if (!agent && location2?.history && saved && saved.harness !== "other") {
|
|
19377
|
+
await this.harnesses.resume(saved.harness, saved.sessionId, project.path, command2.action.text);
|
|
19378
|
+
await this.changed();
|
|
19379
|
+
return;
|
|
19380
|
+
}
|
|
19273
19381
|
if (!agent || agent.socket.destroyed || agent.status === "working")
|
|
19274
19382
|
throw new Error("Wait for the current ACP turn to finish");
|
|
19275
19383
|
return commandAcp(agent, command2.requestId, { kind: "prompt", text: command2.action.text });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashup/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dashup": "dist/index.js"
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"tsup": "^8.5.1",
|
|
30
30
|
"tsx": "^4.22.4",
|
|
31
31
|
"typescript": "^5.9.3",
|
|
32
|
-
"@dashup/models": "0.1.0",
|
|
33
32
|
"@dashup/protocol": "0.1.0",
|
|
34
33
|
"@dashup/typescript-config": "0.0.0",
|
|
35
|
-
"@
|
|
36
|
-
"@parcae/sdk": "0.8.1"
|
|
34
|
+
"@dashup/models": "0.1.0",
|
|
35
|
+
"@parcae/sdk": "0.8.1",
|
|
36
|
+
"@parcae/lynx": "0.1.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"dashup": "tsx src/index.ts",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"start": "node dist/index.js watch",
|
|
42
42
|
"build": "tsup",
|
|
43
43
|
"check-types": "tsc --noEmit",
|
|
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
|
+
"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/headless.test.ts tests/stop.test.ts tests/viewer.test.ts",
|
|
45
45
|
"test:workflow": "node --import tsx tests/workflow.test.ts"
|
|
46
46
|
}
|
|
47
47
|
}
|