@dashup/connector 0.1.15 → 0.1.16
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 +3 -3
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/hooks/register.ts +77 -0
- package/dist/index.js +314 -166
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -34,10 +34,10 @@ Each history read stages a new encrypted database snapshot and publishes it only
|
|
|
34
34
|
|
|
35
35
|
Dashup discovers existing local OpenCode listeners, Codex control sockets, and VS Code Agent Host endpoints. These connections remain local; their tokens are never sent to Parcae or the phone. Native endpoints are checked for local ownership or loopback access, and redirects and remote endpoint URLs are rejected.
|
|
36
36
|
|
|
37
|
-
- **OpenCode:** the existing TUI/server supplies conversations, live events, prompts, native tool permissions, and
|
|
37
|
+
- **OpenCode:** the existing TUI/server supplies conversations, live events, prompts, native tool permissions, and questions, which the phone answers or skips. 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. `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.
|
|
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. The plugin also puts each permission request the session waits on to the phone: a tool to allow once or under the rule Claude Code suggests, questions, or a plan. The dialog on the computer, or the editor hosting the session, stays open, and the first answer wins. Claude Code lets only the person there or its host approve a plan, so the phone can only send a running session's plan back with what to change. 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
|
|
|
@@ -55,7 +55,7 @@ Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the la
|
|
|
55
55
|
|
|
56
56
|
## Starting sessions from the phone
|
|
57
57
|
|
|
58
|
-
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.
|
|
58
|
+
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, and the rule that would stop it asking again; the phone answers its questions and plans as Claude Code's host.
|
|
59
59
|
|
|
60
60
|
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.
|
|
61
61
|
|
package/claude/hooks/register.ts
CHANGED
|
@@ -13,9 +13,85 @@ type Delivery = { text?: string; now?: true };
|
|
|
13
13
|
|
|
14
14
|
const RETRY_MS = 10_000;
|
|
15
15
|
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Permission requests
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
// The connector holds each request this session waits on open until the phone answers it. The
|
|
21
|
+
// dialog here, the editor hosting the session or the person's own hooks may answer first. No hook
|
|
22
|
+
// hears the dialog's answer, so the call it settles closes the request: both hooks see the call,
|
|
23
|
+
// with its fields in different orders.
|
|
24
|
+
function call(agent: string | undefined, tool: string, input: unknown): string {
|
|
25
|
+
const sorted = (value: unknown): unknown =>
|
|
26
|
+
Array.isArray(value)
|
|
27
|
+
? value.map(sorted)
|
|
28
|
+
: value && typeof value === 'object'
|
|
29
|
+
? Object.fromEntries(
|
|
30
|
+
Object.keys(value)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((key) => [key, sorted((value as Record<string, unknown>)[key])]),
|
|
33
|
+
)
|
|
34
|
+
: value;
|
|
35
|
+
return JSON.stringify([agent ?? '', tool, sorted(input)]);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
export const register: Register = (on) => {
|
|
17
39
|
let turnId: string | undefined;
|
|
18
40
|
let stream: AsyncGenerator<unknown, unknown> | undefined;
|
|
41
|
+
// The requests the phone is asked, by the call each waits on.
|
|
42
|
+
const asks = new Map<string, AsyncGenerator<unknown, unknown>>();
|
|
43
|
+
on('classic.PermissionRequest', async ($, e, next) => {
|
|
44
|
+
const key = call(e.agent_id, e.tool_name, e.tool_input);
|
|
45
|
+
const ask = $.process.spawn({
|
|
46
|
+
argv: [
|
|
47
|
+
'curl',
|
|
48
|
+
'-sfN',
|
|
49
|
+
'--unix-socket',
|
|
50
|
+
`${await $.env.get('HOME')}/.dashup/claude.sock`,
|
|
51
|
+
'--data-binary',
|
|
52
|
+
'@-',
|
|
53
|
+
`http://dashup/sessions/${await $.session.id()}/prompts`,
|
|
54
|
+
],
|
|
55
|
+
input: JSON.stringify({
|
|
56
|
+
tool : e.tool_name,
|
|
57
|
+
input : e.tool_input,
|
|
58
|
+
suggestions : e.permission_suggestions,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
const open: AsyncGenerator<unknown, unknown> = ask;
|
|
62
|
+
asks.set(key, open);
|
|
63
|
+
const beneath = next(e).then((result) => {
|
|
64
|
+
if (result.decision) void open.return(undefined);
|
|
65
|
+
return result;
|
|
66
|
+
});
|
|
67
|
+
let answer = '';
|
|
68
|
+
try {
|
|
69
|
+
for await (const { stream: name, text } of ask) if (name === 'stdout') answer += text;
|
|
70
|
+
} catch {
|
|
71
|
+
// curl is missing.
|
|
72
|
+
} finally {
|
|
73
|
+
if (asks.get(key) === open) asks.delete(key);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return answer ? { decision: JSON.parse(answer) } : beneath;
|
|
77
|
+
} catch {
|
|
78
|
+
// The connector stopped mid-answer.
|
|
79
|
+
return beneath;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
on('tool.call', async ($, e, next) => {
|
|
83
|
+
try {
|
|
84
|
+
return await next(e);
|
|
85
|
+
} finally {
|
|
86
|
+
if (asks.size) {
|
|
87
|
+
const { tool, tool_use_id, consent, agentId, requestMeta, ...input } = e as Record<
|
|
88
|
+
string,
|
|
89
|
+
unknown
|
|
90
|
+
>;
|
|
91
|
+
void asks.get(call(agentId as string | undefined, String(tool), input))?.return(undefined);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
19
95
|
on('turn.start', ($, e, next) => {
|
|
20
96
|
if (e.agentId === undefined) turnId = e.turnId;
|
|
21
97
|
return next(e);
|
|
@@ -23,6 +99,7 @@ export const register: Register = (on) => {
|
|
|
23
99
|
// `/clear` starts another conversation in this process, which streams under its own id.
|
|
24
100
|
on('session.end', ($, e, next) => {
|
|
25
101
|
void stream?.return(undefined);
|
|
102
|
+
for (const ask of asks.values()) void ask.return(undefined);
|
|
26
103
|
return next(e);
|
|
27
104
|
});
|
|
28
105
|
on('session.start', async ($, e, next) => {
|
package/dist/index.js
CHANGED
|
@@ -3802,7 +3802,7 @@ function bridgeAcp(socket, onUpdate) {
|
|
|
3802
3802
|
if (session && direction === "agent" && message.method === methods.client.session.requestPermission) {
|
|
3803
3803
|
const tool = message.params.toolCall ?? {};
|
|
3804
3804
|
const diff = (tool.content ?? []).find((part) => part.type === "diff");
|
|
3805
|
-
const
|
|
3805
|
+
const approval2 = {
|
|
3806
3806
|
id: randomUUID(),
|
|
3807
3807
|
title: String(message.params.title ?? tool.title ?? "Agent needs approval"),
|
|
3808
3808
|
detail: String(message.params.description ?? ""),
|
|
@@ -3820,10 +3820,10 @@ ${String(diff.newText).split("\n").map((line) => "+" + line).join("\n")}`
|
|
|
3820
3820
|
}
|
|
3821
3821
|
} : {}
|
|
3822
3822
|
};
|
|
3823
|
-
session.approvals.set(String(message.id),
|
|
3823
|
+
session.approvals.set(String(message.id), approval2);
|
|
3824
3824
|
permissions.set(String(message.id), session);
|
|
3825
3825
|
session.status = "attention";
|
|
3826
|
-
publish(session,
|
|
3826
|
+
publish(session, approval2);
|
|
3827
3827
|
}
|
|
3828
3828
|
if (direction === "editor" && !message.method) {
|
|
3829
3829
|
const pending = permissions.get(String(message.id));
|
|
@@ -4048,7 +4048,7 @@ import * as ui from "@clack/prompts";
|
|
|
4048
4048
|
import { z } from "zod";
|
|
4049
4049
|
|
|
4050
4050
|
// package.json
|
|
4051
|
-
var version = "0.1.
|
|
4051
|
+
var version = "0.1.16";
|
|
4052
4052
|
|
|
4053
4053
|
// src/pair.ts
|
|
4054
4054
|
var Invitation = z.object({
|
|
@@ -4215,6 +4215,24 @@ var attached = (id, mime, data, name) => ({
|
|
|
4215
4215
|
});
|
|
4216
4216
|
var MAX_PREVIEW = 64 * 1024;
|
|
4217
4217
|
var NO_PREVIEW = "Review the full request on your computer before approving.";
|
|
4218
|
+
function questionApproval(id, items, multiple) {
|
|
4219
|
+
const asked = items.map((item) => ({
|
|
4220
|
+
...typeof item?.header === "string" ? { header: item.header } : {},
|
|
4221
|
+
question: String(item?.question ?? ""),
|
|
4222
|
+
options: (Array.isArray(item?.options) ? item.options : []).filter((option2) => typeof option2?.label === "string").map((option2) => ({
|
|
4223
|
+
label: String(option2.label),
|
|
4224
|
+
...typeof option2.description === "string" ? { description: option2.description } : {}
|
|
4225
|
+
})),
|
|
4226
|
+
...multiple(item) ? { multiple: true } : {}
|
|
4227
|
+
}));
|
|
4228
|
+
return {
|
|
4229
|
+
id,
|
|
4230
|
+
title: asked.length === 1 ? asked[0].question : `${asked.length} questions`,
|
|
4231
|
+
detail: "",
|
|
4232
|
+
choices: [{ id: "skip", label: "Skip" }],
|
|
4233
|
+
questions: asked
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4218
4236
|
|
|
4219
4237
|
// src/harness/history.ts
|
|
4220
4238
|
var SAMPLE_BYTES = 128 * 1024;
|
|
@@ -16728,13 +16746,14 @@ var Client = class {
|
|
|
16728
16746
|
});
|
|
16729
16747
|
this.changed();
|
|
16730
16748
|
}) : void 0,
|
|
16731
|
-
approve: approvals.length ? (requestId, approvalId,
|
|
16732
|
-
const
|
|
16749
|
+
approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
|
|
16750
|
+
const approval2 = this.approvals.get(approvalId);
|
|
16751
|
+
const choice = "choice" in reply ? reply.choice : void 0;
|
|
16733
16752
|
const socket = this.socket;
|
|
16734
|
-
if (!
|
|
16753
|
+
if (!approval2 || approval2.threadId !== id || !approval2.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
|
|
16735
16754
|
throw new Error("Codex approval is no longer pending");
|
|
16736
16755
|
await new Promise((resolve3, reject) => socket.send(
|
|
16737
|
-
JSON.stringify({ id:
|
|
16756
|
+
JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
|
|
16738
16757
|
(error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
|
|
16739
16758
|
));
|
|
16740
16759
|
this.approvals.delete(approvalId);
|
|
@@ -16810,17 +16829,17 @@ var Codex = class {
|
|
|
16810
16829
|
};
|
|
16811
16830
|
|
|
16812
16831
|
// src/harness/headless.ts
|
|
16813
|
-
import { randomUUID as
|
|
16814
|
-
import { readFileSync as readFileSync3 } from "node:fs";
|
|
16832
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
16815
16833
|
import { createInterface as createInterface3 } from "node:readline";
|
|
16816
16834
|
import { spawn as spawn3 } from "node:child_process";
|
|
16817
16835
|
|
|
16818
16836
|
// src/harness/claude.ts
|
|
16819
16837
|
import { homedir as homedir5 } from "node:os";
|
|
16820
|
-
import {
|
|
16838
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
16839
|
+
import { dirname as dirname4, join as join5, basename as basename2 } from "node:path";
|
|
16821
16840
|
import { spawn as spawn2 } from "node:child_process";
|
|
16822
16841
|
import { createInterface as createInterface2 } from "node:readline";
|
|
16823
|
-
import { watch as watch2 } from "node:fs";
|
|
16842
|
+
import { watch as watch2, readFileSync as readFileSync3 } from "node:fs";
|
|
16824
16843
|
import { mkdir, readdir as readdir2, readFile as readFile2, readlink, rm } from "node:fs/promises";
|
|
16825
16844
|
import { createServer as createServer2 } from "node:http";
|
|
16826
16845
|
var MODELS_MS = 3e4;
|
|
@@ -16893,9 +16912,148 @@ function claudeOptions(models, recorded) {
|
|
|
16893
16912
|
...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
|
|
16894
16913
|
];
|
|
16895
16914
|
}
|
|
16915
|
+
var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
|
|
16916
|
+
var SCOPE = 32;
|
|
16917
|
+
var preview2 = (text3) => Buffer.byteLength(text3) > MAX_PREVIEW ? Buffer.from(text3).subarray(0, MAX_PREVIEW).toString("utf8") : text3;
|
|
16918
|
+
function hunk(before, after, offset = 0) {
|
|
16919
|
+
const old = before ? before.replace(/\n$/, "").split("\n") : [];
|
|
16920
|
+
const next = after ? after.replace(/\n$/, "").split("\n") : [];
|
|
16921
|
+
let start2 = 0;
|
|
16922
|
+
while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
|
|
16923
|
+
let end = 0;
|
|
16924
|
+
while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
|
|
16925
|
+
end++;
|
|
16926
|
+
const removed = old.slice(start2, old.length - end);
|
|
16927
|
+
const added2 = next.slice(start2, next.length - end);
|
|
16928
|
+
return {
|
|
16929
|
+
lines: [
|
|
16930
|
+
`@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
|
|
16931
|
+
...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
|
|
16932
|
+
...removed.map((line) => `-${line}`),
|
|
16933
|
+
...added2.map((line) => `+${line}`),
|
|
16934
|
+
...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
|
|
16935
|
+
],
|
|
16936
|
+
added: added2.length,
|
|
16937
|
+
deleted: removed.length
|
|
16938
|
+
};
|
|
16939
|
+
}
|
|
16940
|
+
function change(tool, input) {
|
|
16941
|
+
const path = input.file_path;
|
|
16942
|
+
if (typeof path !== "string") return void 0;
|
|
16943
|
+
let current = "";
|
|
16944
|
+
try {
|
|
16945
|
+
current = readFileSync3(path, "utf8");
|
|
16946
|
+
} catch {
|
|
16947
|
+
}
|
|
16948
|
+
const edit = (value2) => {
|
|
16949
|
+
const before = String(value2?.old_string ?? "");
|
|
16950
|
+
const at = before ? current.indexOf(before) : -1;
|
|
16951
|
+
return hunk(
|
|
16952
|
+
before,
|
|
16953
|
+
String(value2?.new_string ?? ""),
|
|
16954
|
+
at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
|
|
16955
|
+
);
|
|
16956
|
+
};
|
|
16957
|
+
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) : [];
|
|
16958
|
+
if (!hunks.length) return void 0;
|
|
16959
|
+
const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
|
|
16960
|
+
if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
|
|
16961
|
+
return {
|
|
16962
|
+
path,
|
|
16963
|
+
diff,
|
|
16964
|
+
added: hunks.reduce((sum, item) => sum + item.added, 0),
|
|
16965
|
+
deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
|
|
16966
|
+
};
|
|
16967
|
+
}
|
|
16968
|
+
function remember(suggestions = []) {
|
|
16969
|
+
const updates = suggestions.filter(
|
|
16970
|
+
(update) => update?.type === "addRules" && update.behavior === "allow" || update?.type === "addDirectories"
|
|
16971
|
+
);
|
|
16972
|
+
const rule = updates.flatMap((update) => update.rules ?? [])[0];
|
|
16973
|
+
const folder = updates.flatMap((update) => update.directories ?? [])[0];
|
|
16974
|
+
const scope2 = rule ? String(rule.ruleContent ?? rule.toolName) : folder ? `access to ${basename2(String(folder))}` : "";
|
|
16975
|
+
if (scope2)
|
|
16976
|
+
return {
|
|
16977
|
+
label: `Always allow ${scope2.length > SCOPE ? `${scope2.slice(0, SCOPE - 1)}\u2026` : scope2}`,
|
|
16978
|
+
updates
|
|
16979
|
+
};
|
|
16980
|
+
const edits = suggestions.find(
|
|
16981
|
+
(update) => update?.type === "setMode" && update.mode === "acceptEdits"
|
|
16982
|
+
);
|
|
16983
|
+
return edits ? { label: "Allow all edits", updates: [edits] } : void 0;
|
|
16984
|
+
}
|
|
16985
|
+
function ask(id, request2, host) {
|
|
16986
|
+
const { tool, input } = request2;
|
|
16987
|
+
if (tool === "AskUserQuestion" && Array.isArray(input.questions))
|
|
16988
|
+
return questionApproval(id, input.questions, (item) => item.multiSelect === true);
|
|
16989
|
+
if (tool === "ExitPlanMode")
|
|
16990
|
+
return {
|
|
16991
|
+
id,
|
|
16992
|
+
title: "Approve this plan?",
|
|
16993
|
+
detail: host ? "" : "Approve it on your computer, or tell Claude what to change.",
|
|
16994
|
+
choices: [
|
|
16995
|
+
...host ? [{ id: "accept", label: "Approve" }] : [],
|
|
16996
|
+
{ id: "revise", label: "Keep planning", text: true }
|
|
16997
|
+
],
|
|
16998
|
+
plan: preview2(String(input.plan ?? ""))
|
|
16999
|
+
};
|
|
17000
|
+
const diff = change(tool, input);
|
|
17001
|
+
const always = remember(request2.suggestions);
|
|
17002
|
+
return {
|
|
17003
|
+
id,
|
|
17004
|
+
title: tool === "Bash" ? "Allow command?" : EDITS.has(tool) ? "Allow file changes?" : `Allow ${request2.label ?? tool}?`,
|
|
17005
|
+
detail: preview2(
|
|
17006
|
+
[
|
|
17007
|
+
request2.description ?? (typeof input.description === "string" ? input.description : ""),
|
|
17008
|
+
typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
|
|
17009
|
+
].filter(Boolean).join("\n\n")
|
|
17010
|
+
),
|
|
17011
|
+
...diff ? { change: diff } : {},
|
|
17012
|
+
choices: [
|
|
17013
|
+
{ id: "accept", label: "Allow once" },
|
|
17014
|
+
...always ? [{ id: "always", label: always.label }] : [],
|
|
17015
|
+
{ id: "decline", label: "Reject" }
|
|
17016
|
+
]
|
|
17017
|
+
};
|
|
17018
|
+
}
|
|
17019
|
+
function decide(request2, approval2, reply) {
|
|
17020
|
+
const input = request2.input;
|
|
17021
|
+
if ("answers" in reply)
|
|
17022
|
+
return {
|
|
17023
|
+
behavior: "allow",
|
|
17024
|
+
updatedInput: {
|
|
17025
|
+
...input,
|
|
17026
|
+
answers: Object.fromEntries(
|
|
17027
|
+
(approval2.questions ?? []).map((item, index) => [
|
|
17028
|
+
item.question,
|
|
17029
|
+
reply.answers[index].join(", ")
|
|
17030
|
+
])
|
|
17031
|
+
)
|
|
17032
|
+
}
|
|
17033
|
+
};
|
|
17034
|
+
if (reply.choice === "accept")
|
|
17035
|
+
return {
|
|
17036
|
+
behavior: "allow",
|
|
17037
|
+
updatedInput: input,
|
|
17038
|
+
...approval2.plan === void 0 ? {} : { updatedPermissions: [{ type: "setMode", mode: "default", destination: "session" }] }
|
|
17039
|
+
};
|
|
17040
|
+
if (reply.choice === "always")
|
|
17041
|
+
return {
|
|
17042
|
+
behavior: "allow",
|
|
17043
|
+
updatedInput: input,
|
|
17044
|
+
updatedPermissions: remember(request2.suggestions)?.updates ?? []
|
|
17045
|
+
};
|
|
17046
|
+
if (reply.choice === "revise")
|
|
17047
|
+
return { behavior: "deny", message: reply.text?.trim() || "Keep planning." };
|
|
17048
|
+
return {
|
|
17049
|
+
behavior: "deny",
|
|
17050
|
+
message: reply.choice === "skip" ? "The person skipped these questions." : "The person rejected this from their phone."
|
|
17051
|
+
};
|
|
17052
|
+
}
|
|
16896
17053
|
var MAX_SESSIONS2 = 100;
|
|
16897
17054
|
var MAX_REQUESTS = 1e3;
|
|
16898
|
-
var
|
|
17055
|
+
var MAX_PROMPT = 4 * 1024 * 1024;
|
|
17056
|
+
var SESSION = /^\/sessions\/([\w-]{1,100})(\/prompts)?$/;
|
|
16899
17057
|
var STATUS = {
|
|
16900
17058
|
busy: "working",
|
|
16901
17059
|
idle: "idle",
|
|
@@ -16932,20 +17090,28 @@ var Claude = class {
|
|
|
16932
17090
|
notify;
|
|
16933
17091
|
server;
|
|
16934
17092
|
streams = /* @__PURE__ */ new Map();
|
|
17093
|
+
prompts = /* @__PURE__ */ new Map();
|
|
16935
17094
|
requests = /* @__PURE__ */ new Map();
|
|
16936
17095
|
async refresh() {
|
|
16937
17096
|
this.watch();
|
|
16938
17097
|
return (await registrations()).map((session) => {
|
|
16939
17098
|
const plugin = this.streams.has(session.sessionId);
|
|
16940
17099
|
const models = claudeModels(this.changed, session.command, session.version);
|
|
17100
|
+
const approvals = [...this.prompts.values()].filter((prompt) => prompt.session === session.sessionId).map((prompt) => prompt.approval);
|
|
16941
17101
|
return {
|
|
16942
17102
|
id: session.sessionId,
|
|
16943
17103
|
harness: "claude",
|
|
16944
17104
|
cwd: session.cwd,
|
|
16945
17105
|
updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
|
|
16946
|
-
status: STATUS[session.status ?? ""] ?? "unknown",
|
|
17106
|
+
status: approvals.length ? "attention" : STATUS[session.status ?? ""] ?? "unknown",
|
|
16947
17107
|
messages: [],
|
|
16948
|
-
approvals
|
|
17108
|
+
approvals,
|
|
17109
|
+
approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
|
|
17110
|
+
const prompt = this.prompts.get(approvalId);
|
|
17111
|
+
if (prompt?.session !== session.sessionId)
|
|
17112
|
+
throw new Error("Claude Code approval is no longer pending");
|
|
17113
|
+
prompt.answer(decide(prompt.request, prompt.approval, reply));
|
|
17114
|
+
}) : void 0,
|
|
16949
17115
|
send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
|
|
16950
17116
|
stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
|
|
16951
17117
|
now: plugin,
|
|
@@ -16958,8 +17124,9 @@ var Claude = class {
|
|
|
16958
17124
|
async listen() {
|
|
16959
17125
|
if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
|
|
16960
17126
|
const server = this.server = createServer2((request2, response) => {
|
|
16961
|
-
const id =
|
|
16962
|
-
if (
|
|
17127
|
+
const [, id, prompt] = SESSION.exec(request2.url ?? "") ?? [];
|
|
17128
|
+
if (id && prompt && request2.method === "POST") return this.prompt(id, request2, response);
|
|
17129
|
+
if (!id || prompt || request2.method !== "GET") return void response.writeHead(404).end();
|
|
16963
17130
|
this.streams.get(id)?.end();
|
|
16964
17131
|
this.streams.set(id, response);
|
|
16965
17132
|
response.once("close", () => {
|
|
@@ -16977,6 +17144,46 @@ var Claude = class {
|
|
|
16977
17144
|
server.listen(this.socket, resolve3);
|
|
16978
17145
|
});
|
|
16979
17146
|
}
|
|
17147
|
+
// Asks the phone what the session's permission request asks the person.
|
|
17148
|
+
prompt(session, request2, response) {
|
|
17149
|
+
const chunks = [];
|
|
17150
|
+
let bytes = 0;
|
|
17151
|
+
request2.on("data", (chunk) => {
|
|
17152
|
+
bytes += chunk.length;
|
|
17153
|
+
if (bytes > MAX_PROMPT) request2.destroy();
|
|
17154
|
+
else chunks.push(chunk);
|
|
17155
|
+
});
|
|
17156
|
+
request2.once("end", () => {
|
|
17157
|
+
let asked;
|
|
17158
|
+
try {
|
|
17159
|
+
asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
17160
|
+
} catch {
|
|
17161
|
+
}
|
|
17162
|
+
if (typeof asked?.tool !== "string" || !asked.input || typeof asked.input !== "object")
|
|
17163
|
+
return void response.writeHead(400).end();
|
|
17164
|
+
const id = randomUUID3();
|
|
17165
|
+
const prompted = {
|
|
17166
|
+
tool: asked.tool,
|
|
17167
|
+
input: asked.input,
|
|
17168
|
+
...Array.isArray(asked.suggestions) ? { suggestions: asked.suggestions } : {}
|
|
17169
|
+
};
|
|
17170
|
+
this.prompts.set(id, {
|
|
17171
|
+
session,
|
|
17172
|
+
request: prompted,
|
|
17173
|
+
approval: ask(id, prompted, false),
|
|
17174
|
+
answer: (decision) => {
|
|
17175
|
+
this.prompts.delete(id);
|
|
17176
|
+
response.end(JSON.stringify(decision));
|
|
17177
|
+
this.changed();
|
|
17178
|
+
}
|
|
17179
|
+
});
|
|
17180
|
+
response.once("close", () => {
|
|
17181
|
+
if (this.prompts.delete(id)) this.changed();
|
|
17182
|
+
});
|
|
17183
|
+
response.writeHead(200, { "Content-Type": "application/json" }).flushHeaders();
|
|
17184
|
+
this.changed();
|
|
17185
|
+
});
|
|
17186
|
+
}
|
|
16980
17187
|
deliver(id, delivery) {
|
|
16981
17188
|
const stream = this.streams.get(id);
|
|
16982
17189
|
if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
|
|
@@ -17015,6 +17222,7 @@ var Claude = class {
|
|
|
17015
17222
|
clearTimeout(this.notify);
|
|
17016
17223
|
this.watcher?.close();
|
|
17017
17224
|
this.watcher = void 0;
|
|
17225
|
+
this.prompts.clear();
|
|
17018
17226
|
this.server?.closeAllConnections();
|
|
17019
17227
|
this.server?.close();
|
|
17020
17228
|
this.server = void 0;
|
|
@@ -17022,82 +17230,9 @@ var Claude = class {
|
|
|
17022
17230
|
};
|
|
17023
17231
|
|
|
17024
17232
|
// src/harness/headless.ts
|
|
17025
|
-
var CHOICES = [
|
|
17026
|
-
{ id: "accept", label: "Allow once" },
|
|
17027
|
-
{ id: "decline", label: "Reject" }
|
|
17028
|
-
];
|
|
17029
|
-
var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
|
|
17030
17233
|
var MAX_REQUESTS2 = 1e3;
|
|
17031
17234
|
var NOTIFY_MS = 100;
|
|
17032
17235
|
var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
|
|
17033
|
-
function hunk(before, after, offset = 0) {
|
|
17034
|
-
const old = before ? before.replace(/\n$/, "").split("\n") : [];
|
|
17035
|
-
const next = after ? after.replace(/\n$/, "").split("\n") : [];
|
|
17036
|
-
let start2 = 0;
|
|
17037
|
-
while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
|
|
17038
|
-
let end = 0;
|
|
17039
|
-
while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
|
|
17040
|
-
end++;
|
|
17041
|
-
const removed = old.slice(start2, old.length - end);
|
|
17042
|
-
const added2 = next.slice(start2, next.length - end);
|
|
17043
|
-
return {
|
|
17044
|
-
lines: [
|
|
17045
|
-
`@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
|
|
17046
|
-
...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
|
|
17047
|
-
...removed.map((line) => `-${line}`),
|
|
17048
|
-
...added2.map((line) => `+${line}`),
|
|
17049
|
-
...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
|
|
17050
|
-
],
|
|
17051
|
-
added: added2.length,
|
|
17052
|
-
deleted: removed.length
|
|
17053
|
-
};
|
|
17054
|
-
}
|
|
17055
|
-
function change(tool, input) {
|
|
17056
|
-
const path = input.file_path;
|
|
17057
|
-
if (typeof path !== "string") return void 0;
|
|
17058
|
-
let current = "";
|
|
17059
|
-
try {
|
|
17060
|
-
current = readFileSync3(path, "utf8");
|
|
17061
|
-
} catch {
|
|
17062
|
-
}
|
|
17063
|
-
const edit = (value2) => {
|
|
17064
|
-
const before = String(value2?.old_string ?? "");
|
|
17065
|
-
const at = before ? current.indexOf(before) : -1;
|
|
17066
|
-
return hunk(
|
|
17067
|
-
before,
|
|
17068
|
-
String(value2?.new_string ?? ""),
|
|
17069
|
-
at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
|
|
17070
|
-
);
|
|
17071
|
-
};
|
|
17072
|
-
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) : [];
|
|
17073
|
-
if (!hunks.length) return void 0;
|
|
17074
|
-
const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
|
|
17075
|
-
if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
|
|
17076
|
-
return {
|
|
17077
|
-
path,
|
|
17078
|
-
diff,
|
|
17079
|
-
added: hunks.reduce((sum, item) => sum + item.added, 0),
|
|
17080
|
-
deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
|
|
17081
|
-
};
|
|
17082
|
-
}
|
|
17083
|
-
function approval2(id, request2) {
|
|
17084
|
-
const input = request2.input ?? {};
|
|
17085
|
-
const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
|
|
17086
|
-
const diff = change(request2.tool_name, input);
|
|
17087
|
-
let detail = [
|
|
17088
|
-
typeof request2.description === "string" ? request2.description : "",
|
|
17089
|
-
typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
|
|
17090
|
-
].filter(Boolean).join("\n\n");
|
|
17091
|
-
if (Buffer.byteLength(detail) > MAX_PREVIEW)
|
|
17092
|
-
detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
|
|
17093
|
-
return {
|
|
17094
|
-
id,
|
|
17095
|
-
title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
|
|
17096
|
-
detail,
|
|
17097
|
-
...diff ? { change: diff } : {},
|
|
17098
|
-
choices: CHOICES.map((choice) => ({ ...choice }))
|
|
17099
|
-
};
|
|
17100
|
-
}
|
|
17101
17236
|
var Session = class {
|
|
17102
17237
|
// A resumed session is `saved` and keeps the title its history gives.
|
|
17103
17238
|
constructor(id, cwd, title2, changed, saved = false) {
|
|
@@ -17131,14 +17266,14 @@ var Session = class {
|
|
|
17131
17266
|
interrupt() {
|
|
17132
17267
|
this.write({
|
|
17133
17268
|
type: "control_request",
|
|
17134
|
-
request_id:
|
|
17269
|
+
request_id: randomUUID4(),
|
|
17135
17270
|
request: { subtype: "interrupt" }
|
|
17136
17271
|
});
|
|
17137
17272
|
}
|
|
17138
17273
|
apply(option2, value2) {
|
|
17139
17274
|
this.write({
|
|
17140
17275
|
type: "control_request",
|
|
17141
|
-
request_id:
|
|
17276
|
+
request_id: randomUUID4(),
|
|
17142
17277
|
request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
|
|
17143
17278
|
});
|
|
17144
17279
|
}
|
|
@@ -17154,7 +17289,7 @@ var Session = class {
|
|
|
17154
17289
|
// Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
|
|
17155
17290
|
// turn under way, so Claude Code takes the message at once.
|
|
17156
17291
|
say(text3, now = false) {
|
|
17157
|
-
this.messages.push({ id:
|
|
17292
|
+
this.messages.push({ id: randomUUID4(), role: "user", text: text3 });
|
|
17158
17293
|
this.typing = "";
|
|
17159
17294
|
this.status = "working";
|
|
17160
17295
|
this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -17223,15 +17358,27 @@ var Session = class {
|
|
|
17223
17358
|
else if (message.type === "assistant") {
|
|
17224
17359
|
const text3 = (Array.isArray(message.message?.content) ? message.message.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n\n");
|
|
17225
17360
|
if (!text3) return false;
|
|
17226
|
-
this.messages.push({ id: String(message.uuid ??
|
|
17361
|
+
this.messages.push({ id: String(message.uuid ?? randomUUID4()), role: "assistant", text: text3 });
|
|
17227
17362
|
} else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
|
|
17228
|
-
const id =
|
|
17363
|
+
const id = randomUUID4();
|
|
17364
|
+
const asked = message.request;
|
|
17365
|
+
const request2 = {
|
|
17366
|
+
tool: String(asked.tool_name),
|
|
17367
|
+
input: asked.input ?? {},
|
|
17368
|
+
label: typeof asked.display_name === "string" ? asked.display_name : void 0,
|
|
17369
|
+
description: typeof asked.description === "string" ? asked.description : void 0,
|
|
17370
|
+
suggestions: Array.isArray(asked.permission_suggestions) ? asked.permission_suggestions : void 0
|
|
17371
|
+
};
|
|
17229
17372
|
this.approvals.set(id, {
|
|
17230
|
-
|
|
17231
|
-
|
|
17232
|
-
approval:
|
|
17373
|
+
control: String(message.request_id),
|
|
17374
|
+
request: request2,
|
|
17375
|
+
approval: ask(id, request2, true)
|
|
17233
17376
|
});
|
|
17234
17377
|
this.status = "attention";
|
|
17378
|
+
} else if (message.type === "control_cancel_request") {
|
|
17379
|
+
for (const [id, pending] of this.approvals)
|
|
17380
|
+
if (pending.control === message.request_id) this.approvals.delete(id);
|
|
17381
|
+
if (this.status === "attention" && !this.approvals.size) this.status = "working";
|
|
17235
17382
|
} else if (message.type === "result") {
|
|
17236
17383
|
if (--this.open <= 0) this.turn?.stdin.end();
|
|
17237
17384
|
} else if (message.type === "stream_event" && !message.parent_tool_use_id) {
|
|
@@ -17269,16 +17416,16 @@ var Session = class {
|
|
|
17269
17416
|
if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
|
|
17270
17417
|
this.interrupt();
|
|
17271
17418
|
}) : void 0,
|
|
17272
|
-
approve: this.approvals.size ? (requestId, approvalId,
|
|
17419
|
+
approve: this.approvals.size ? (requestId, approvalId, reply) => this.once(requestId, async () => {
|
|
17273
17420
|
const pending = this.approvals.get(approvalId);
|
|
17274
|
-
if (!pending || !this.turn
|
|
17421
|
+
if (!pending || !this.turn)
|
|
17275
17422
|
throw new Error("Claude Code approval is no longer pending");
|
|
17276
17423
|
this.write({
|
|
17277
17424
|
type: "control_response",
|
|
17278
17425
|
response: {
|
|
17279
17426
|
subtype: "success",
|
|
17280
|
-
request_id: pending.
|
|
17281
|
-
response:
|
|
17427
|
+
request_id: pending.control,
|
|
17428
|
+
response: decide(pending.request, pending.approval, reply)
|
|
17282
17429
|
}
|
|
17283
17430
|
});
|
|
17284
17431
|
this.approvals.delete(approvalId);
|
|
@@ -17309,7 +17456,7 @@ var Headless = class {
|
|
|
17309
17456
|
// Starts a session in `cwd` with its first message, and resolves with the session's id.
|
|
17310
17457
|
async start(cwd, text3) {
|
|
17311
17458
|
const title2 = text3.trim().split("\n")[0].slice(0, 80);
|
|
17312
|
-
const session = new Session(
|
|
17459
|
+
const session = new Session(randomUUID4(), cwd, title2, this.soon);
|
|
17313
17460
|
this.sessions.set(session.id, session);
|
|
17314
17461
|
try {
|
|
17315
17462
|
await session.say(text3);
|
|
@@ -17348,9 +17495,9 @@ import { connect as connect2 } from "node:net";
|
|
|
17348
17495
|
import { promisify as promisify3 } from "node:util";
|
|
17349
17496
|
import { fileURLToPath } from "node:url";
|
|
17350
17497
|
import { execFile as execFile3 } from "node:child_process";
|
|
17351
|
-
import { createHash as createHash3, randomUUID as
|
|
17498
|
+
import { createHash as createHash3, randomUUID as randomUUID5 } from "node:crypto";
|
|
17352
17499
|
import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
|
|
17353
|
-
import { join as join6, dirname as dirname5, basename as
|
|
17500
|
+
import { join as join6, dirname as dirname5, basename as basename3, isAbsolute as isAbsolute3 } from "node:path";
|
|
17354
17501
|
import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
|
|
17355
17502
|
import { AhpClient } from "@microsoft/agent-host-protocol/client";
|
|
17356
17503
|
import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
|
|
@@ -17368,7 +17515,7 @@ var CONFIRMATION_CHOICES = [
|
|
|
17368
17515
|
function roots() {
|
|
17369
17516
|
const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
|
|
17370
17517
|
if (override !== void 0)
|
|
17371
|
-
return override ? [
|
|
17518
|
+
return override ? [basename3(override) === "User" ? dirname5(override) : override] : [];
|
|
17372
17519
|
if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
|
|
17373
17520
|
const base = process.platform === "darwin" ? join6(homedir6(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join6(homedir6(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join6(homedir6(), ".config");
|
|
17374
17521
|
return ["Code", "Code - Insiders"].map((name) => join6(base, name));
|
|
@@ -17410,12 +17557,12 @@ async function entries() {
|
|
|
17410
17557
|
try {
|
|
17411
17558
|
if (!await privateFile(path, MAX_METADATA)) continue;
|
|
17412
17559
|
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
17413
|
-
const rows =
|
|
17560
|
+
const rows = basename3(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
|
|
17414
17561
|
for (const row of rows.slice(0, MAX_HOSTS)) {
|
|
17415
17562
|
if (row?.schemaVersion !== 2 || !["editor", "standalone"].includes(row.type) || !Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.instanceId !== "string" || !row.instanceId || typeof row.connectionToken !== "string" || !row.connectionToken || row.connectionToken.length > 1024 || row.endpoint?.type !== "socket" || typeof row.endpoint.path !== "string" || !isAbsolute3(row.endpoint.path))
|
|
17416
17563
|
continue;
|
|
17417
17564
|
const identity2 = createHash3("sha256").update(`${row.type}\0${row.pid}\0${row.instanceId}`).digest("hex");
|
|
17418
|
-
if (
|
|
17565
|
+
if (basename3(path) !== "metadata.json" && basename3(path) !== `${identity2}.json`) continue;
|
|
17419
17566
|
try {
|
|
17420
17567
|
process.kill(row.pid, 0);
|
|
17421
17568
|
} catch {
|
|
@@ -17541,7 +17688,7 @@ var Host = class {
|
|
|
17541
17688
|
client;
|
|
17542
17689
|
connecting;
|
|
17543
17690
|
stopped = false;
|
|
17544
|
-
clientId = `dashup-${
|
|
17691
|
+
clientId = `dashup-${randomUUID5()}`;
|
|
17545
17692
|
resources = /* @__PURE__ */ new Map();
|
|
17546
17693
|
subscriptions = /* @__PURE__ */ new Set();
|
|
17547
17694
|
receipts = /* @__PURE__ */ new Map();
|
|
@@ -17722,12 +17869,12 @@ var Host = class {
|
|
|
17722
17869
|
for (const pending of Array.isArray(session.inputNeeded) ? session.inputNeeded : []) {
|
|
17723
17870
|
if (pending.kind !== "toolConfirmation" || pending.chat !== item.resource || pending.toolCall?.status !== "pending-confirmation")
|
|
17724
17871
|
continue;
|
|
17725
|
-
const
|
|
17872
|
+
const preview3 = confirmation(pending.toolCall);
|
|
17726
17873
|
approvals.push({
|
|
17727
17874
|
id: pending.id,
|
|
17728
17875
|
title: text2(pending.toolCall.confirmationTitle) || pending.toolCall.displayName || "Native agent approval",
|
|
17729
|
-
detail:
|
|
17730
|
-
choices:
|
|
17876
|
+
detail: preview3.detail,
|
|
17877
|
+
choices: preview3.choices.map(({ id: id2, label }) => ({ id: id2, label }))
|
|
17731
17878
|
});
|
|
17732
17879
|
}
|
|
17733
17880
|
const id = `vscode-ahp:${createHash3("sha256").update(this.entry.root).digest("hex").slice(0, 16)}:${item.resource}`;
|
|
@@ -17769,7 +17916,8 @@ var Host = class {
|
|
|
17769
17916
|
message: { text: value2, origin: { kind: "user" } }
|
|
17770
17917
|
});
|
|
17771
17918
|
} : void 0,
|
|
17772
|
-
approve: approvals.some((
|
|
17919
|
+
approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => {
|
|
17920
|
+
const choice = "choice" in reply ? reply.choice : void 0;
|
|
17773
17921
|
const prior = this.requests.get(requestId);
|
|
17774
17922
|
if (prior) return prior;
|
|
17775
17923
|
const current = this.resources.get(summary.resource)?.state;
|
|
@@ -18142,21 +18290,14 @@ var Server = class {
|
|
|
18142
18290
|
(question) => question?.sessionID === id && typeof question.id === "string"
|
|
18143
18291
|
) : [];
|
|
18144
18292
|
const approvals = pending.map((permission) => permissionApproval(permission, history, cwd));
|
|
18145
|
-
for (const question of asks)
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
detail: typeof first?.question === "string" ? first.question : "Open this question in the computer\u2019s native interface.",
|
|
18154
|
-
choices: options.map((option2, index) => ({
|
|
18155
|
-
id: `answer:${index}`,
|
|
18156
|
-
label: option2.label
|
|
18157
|
-
}))
|
|
18158
|
-
});
|
|
18159
|
-
}
|
|
18293
|
+
for (const question of asks)
|
|
18294
|
+
approvals.push(
|
|
18295
|
+
questionApproval(
|
|
18296
|
+
`question:${question.id}`,
|
|
18297
|
+
Array.isArray(question.questions) ? question.questions : [],
|
|
18298
|
+
(item) => item.multiple === true
|
|
18299
|
+
)
|
|
18300
|
+
);
|
|
18160
18301
|
result.push({
|
|
18161
18302
|
id,
|
|
18162
18303
|
cwd,
|
|
@@ -18192,23 +18333,21 @@ var Server = class {
|
|
|
18192
18333
|
});
|
|
18193
18334
|
this.changed();
|
|
18194
18335
|
}),
|
|
18195
|
-
approve: approvals.some((
|
|
18336
|
+
approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => this.once(requestId, async () => {
|
|
18196
18337
|
if (this.stopped) throw new Error("OpenCode is disconnected");
|
|
18338
|
+
const choice = "choice" in reply ? reply.choice : void 0;
|
|
18197
18339
|
if (approvalId.startsWith("question:")) {
|
|
18198
18340
|
const requestID = approvalId.slice("question:".length);
|
|
18199
18341
|
const current = await this.request("/question", cwd);
|
|
18200
|
-
|
|
18342
|
+
if (!Array.isArray(current) || !current.some(
|
|
18201
18343
|
(value2) => value2?.id === requestID && value2.sessionID === id
|
|
18202
|
-
)
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
await this.request(`/question/${encodeURIComponent(requestID)}/reply`, cwd, {
|
|
18210
|
-
answers: [[option2.label]]
|
|
18211
|
-
});
|
|
18344
|
+
))
|
|
18345
|
+
throw new Error("Question is no longer pending");
|
|
18346
|
+
await this.request(
|
|
18347
|
+
`/question/${encodeURIComponent(requestID)}/${"answers" in reply ? "reply" : "reject"}`,
|
|
18348
|
+
cwd,
|
|
18349
|
+
"answers" in reply ? { answers: reply.answers } : {}
|
|
18350
|
+
);
|
|
18212
18351
|
} else {
|
|
18213
18352
|
const current = await this.request("/permission", cwd);
|
|
18214
18353
|
const permission = Array.isArray(current) ? current.find(
|
|
@@ -18493,10 +18632,10 @@ var LiveHarnesses = class {
|
|
|
18493
18632
|
};
|
|
18494
18633
|
|
|
18495
18634
|
// src/harness/queue.ts
|
|
18496
|
-
import { randomUUID as
|
|
18635
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
18497
18636
|
import { spawn as spawn5 } from "node:child_process";
|
|
18498
18637
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
18499
|
-
import { join as join8, dirname as dirname6, basename as
|
|
18638
|
+
import { join as join8, dirname as dirname6, basename as basename4 } from "node:path";
|
|
18500
18639
|
import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
18501
18640
|
var CODEX_VERSION = "0.160.1";
|
|
18502
18641
|
var REQUEST_MS4 = 1e4;
|
|
@@ -18528,7 +18667,7 @@ function processOwners(pid) {
|
|
|
18528
18667
|
const root = `/proc/${pid}`;
|
|
18529
18668
|
if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
|
|
18530
18669
|
const executable = realpathSync2.native(join8(root, "exe"));
|
|
18531
|
-
if (
|
|
18670
|
+
if (basename4(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
|
|
18532
18671
|
const status2 = readFileSync4(join8(root, "stat"), "utf8");
|
|
18533
18672
|
const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
|
|
18534
18673
|
if (!start2 || !/^\d+$/.test(start2)) return [];
|
|
@@ -18541,8 +18680,8 @@ function processOwners(pid) {
|
|
|
18541
18680
|
}
|
|
18542
18681
|
});
|
|
18543
18682
|
return files2.flatMap(({ fd, path }) => {
|
|
18544
|
-
if (
|
|
18545
|
-
const thread =
|
|
18683
|
+
if (basename4(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
|
|
18684
|
+
const thread = basename4(path, ".lock");
|
|
18546
18685
|
if (!THREAD_ID.test(thread)) return [];
|
|
18547
18686
|
try {
|
|
18548
18687
|
const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
|
|
@@ -18778,7 +18917,7 @@ var NativeQueue = class {
|
|
|
18778
18917
|
if (existing) return existing;
|
|
18779
18918
|
if (this.stopped) throw new Error("The connector is stopping");
|
|
18780
18919
|
validateOwner(scope2.owner);
|
|
18781
|
-
const clientUserMessageId =
|
|
18920
|
+
const clientUserMessageId = randomUUID6();
|
|
18782
18921
|
const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
|
|
18783
18922
|
let writer;
|
|
18784
18923
|
try {
|
|
@@ -18852,6 +18991,7 @@ var EPHEMERAL_TTL_MS = 6e4;
|
|
|
18852
18991
|
var HOME_RECENT = 20;
|
|
18853
18992
|
var FOLLOW_MS = 2 * 6e4;
|
|
18854
18993
|
var RESCAN_MS = 1e3;
|
|
18994
|
+
var answers = (approval2, reply) => "answers" in reply ? reply.answers.length === approval2.questions?.length : approval2.choices.some((choice) => choice.id === reply.choice);
|
|
18855
18995
|
var Command = z2.object({
|
|
18856
18996
|
type: z2.literal("command"),
|
|
18857
18997
|
requestId: z2.string().min(1),
|
|
@@ -18873,7 +19013,15 @@ var Command = z2.object({
|
|
|
18873
19013
|
option: z2.string().min(1).max(100),
|
|
18874
19014
|
value: z2.union([z2.string().min(1).max(200), z2.boolean()])
|
|
18875
19015
|
}).strict(),
|
|
18876
|
-
z2.object({
|
|
19016
|
+
z2.object({
|
|
19017
|
+
kind: z2.literal("approval.respond"),
|
|
19018
|
+
approvalId: z2.string(),
|
|
19019
|
+
choice: z2.string().optional(),
|
|
19020
|
+
text: z2.string().max(1e5).optional(),
|
|
19021
|
+
answers: z2.array(z2.array(z2.string().min(1).max(1e5)).min(1)).min(1).optional()
|
|
19022
|
+
}).strict().refine(
|
|
19023
|
+
(action) => action.choice === void 0 !== (action.answers === void 0) && (action.text === void 0 || action.choice !== void 0)
|
|
19024
|
+
),
|
|
18877
19025
|
z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
|
|
18878
19026
|
z2.object({
|
|
18879
19027
|
kind: z2.literal("session.start"),
|
|
@@ -18989,15 +19137,15 @@ var Connector = class {
|
|
|
18989
19137
|
for (const project of this.discovery.projects.values())
|
|
18990
19138
|
for (const thread of project.threads) {
|
|
18991
19139
|
const native = this.discovery.locations.get(thread.id)?.live;
|
|
18992
|
-
const
|
|
18993
|
-
if (!
|
|
19140
|
+
const approval2 = native?.approvals[0];
|
|
19141
|
+
if (!approval2) continue;
|
|
18994
19142
|
await this.thread(pair, project, thread);
|
|
18995
19143
|
await this.send(pair, {
|
|
18996
19144
|
type: "attention",
|
|
18997
19145
|
projectId: project.id,
|
|
18998
19146
|
threadId: thread.id,
|
|
18999
|
-
title:
|
|
19000
|
-
body:
|
|
19147
|
+
title: approval2.title,
|
|
19148
|
+
body: approval2.detail,
|
|
19001
19149
|
reason: "approval"
|
|
19002
19150
|
});
|
|
19003
19151
|
}
|
|
@@ -19251,9 +19399,9 @@ var Connector = class {
|
|
|
19251
19399
|
if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
|
|
19252
19400
|
this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
|
|
19253
19401
|
for (const pair of this.activePairs()) await this.thread(pair, project, thread);
|
|
19254
|
-
for (const
|
|
19255
|
-
if (!prior?.approvals.some((item) => item.id ===
|
|
19256
|
-
await this.attention(project.id, thread.id,
|
|
19402
|
+
for (const approval2 of session.approvals)
|
|
19403
|
+
if (!prior?.approvals.some((item) => item.id === approval2.id))
|
|
19404
|
+
await this.attention(project.id, thread.id, approval2.title, approval2.detail, "approval");
|
|
19257
19405
|
if (prior?.thread.status === "working" && thread.status === "idle")
|
|
19258
19406
|
await this.attention(
|
|
19259
19407
|
project.id,
|
|
@@ -19441,7 +19589,7 @@ var Connector = class {
|
|
|
19441
19589
|
}
|
|
19442
19590
|
}
|
|
19443
19591
|
attach(socket) {
|
|
19444
|
-
const sessions = bridgeAcp(socket, (session,
|
|
19592
|
+
const sessions = bridgeAcp(socket, (session, approval2) => {
|
|
19445
19593
|
this.agents.set(session.id, session);
|
|
19446
19594
|
const { project, thread } = this.adopt(session);
|
|
19447
19595
|
if (!this.snapshots.has(session.id)) {
|
|
@@ -19456,15 +19604,15 @@ var Connector = class {
|
|
|
19456
19604
|
}
|
|
19457
19605
|
}).catch((error) => this.report(error));
|
|
19458
19606
|
}
|
|
19459
|
-
if (
|
|
19607
|
+
if (approval2)
|
|
19460
19608
|
this.work = this.work.then(async () => {
|
|
19461
|
-
if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(
|
|
19609
|
+
if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval2))
|
|
19462
19610
|
return;
|
|
19463
19611
|
await this.attention(
|
|
19464
19612
|
project.id,
|
|
19465
19613
|
thread.id,
|
|
19466
|
-
|
|
19467
|
-
|
|
19614
|
+
approval2.title,
|
|
19615
|
+
approval2.detail,
|
|
19468
19616
|
"approval"
|
|
19469
19617
|
);
|
|
19470
19618
|
}).catch((error) => this.report(error));
|
|
@@ -19630,24 +19778,24 @@ var Connector = class {
|
|
|
19630
19778
|
}
|
|
19631
19779
|
case "approval.respond": {
|
|
19632
19780
|
if (!thread) throw new Error("Thread not found");
|
|
19633
|
-
const
|
|
19781
|
+
const { kind, approvalId, ...reply } = command2.action;
|
|
19634
19782
|
const native = this.discovery.locations.get(thread.id)?.live;
|
|
19635
19783
|
if (native?.approve) {
|
|
19636
|
-
const
|
|
19637
|
-
if (!
|
|
19784
|
+
const approval3 = native.approvals.find((item) => item.id === approvalId);
|
|
19785
|
+
if (!approval3 || !answers(approval3, reply))
|
|
19638
19786
|
throw new Error("Approval is no longer available");
|
|
19639
|
-
return native.approve(command2.requestId,
|
|
19787
|
+
return native.approve(command2.requestId, approval3.id, reply);
|
|
19640
19788
|
}
|
|
19641
19789
|
const agent = this.attached(thread);
|
|
19642
19790
|
if (!agent)
|
|
19643
19791
|
throw new Error("Approval requires an attached ACP session. Answer it on your computer.");
|
|
19644
|
-
const [request2,
|
|
19645
|
-
if (!
|
|
19792
|
+
const [request2, approval2] = [...agent.approvals].find(([, item]) => item.id === approvalId) ?? [];
|
|
19793
|
+
if (!approval2 || !("choice" in reply) || !answers(approval2, reply))
|
|
19646
19794
|
throw new Error("Approval is no longer available");
|
|
19647
19795
|
return commandAcp(agent, command2.requestId, {
|
|
19648
19796
|
kind: "approval",
|
|
19649
19797
|
id: request2,
|
|
19650
|
-
choice:
|
|
19798
|
+
choice: reply.choice
|
|
19651
19799
|
});
|
|
19652
19800
|
}
|
|
19653
19801
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashup/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dashup": "dist/index.js"
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"tsup": "^8.5.1",
|
|
31
31
|
"tsx": "^4.22.4",
|
|
32
32
|
"typescript": "^5.9.3",
|
|
33
|
+
"@dashup/models": "0.1.0",
|
|
33
34
|
"@dashup/protocol": "0.1.0",
|
|
34
|
-
"@parcae/lynx": "0.1.0",
|
|
35
|
-
"@parcae/sdk": "0.8.1",
|
|
36
35
|
"@dashup/typescript-config": "0.0.0",
|
|
37
|
-
"@
|
|
36
|
+
"@parcae/lynx": "0.1.0",
|
|
37
|
+
"@parcae/sdk": "0.8.1"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"dashup": "tsx src/index.ts",
|