@dashup/connector 0.1.15 → 0.1.17
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 +6 -4
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/hooks/register.ts +77 -0
- package/dist/index.js +1144 -872
- package/package.json +3 -3
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
|
|
|
@@ -47,7 +47,9 @@ Only an explicit Queue reply starts a temporary official app-server helper to in
|
|
|
47
47
|
|
|
48
48
|
Delivery requires that exact typed client-message ID in the original thread's native history database. A response receipt requires a later native assistant item in the same turn; it does not mean the agent finished. Receipts remain encrypted through the ordinary Dashup relay. History is read on request; the phone asks again while a thread is open and its session changes. Native approvals owned by the private-stdio process remain unsupported and require its original UI. Queue text never substitutes for a native approval.
|
|
49
49
|
|
|
50
|
-
A
|
|
50
|
+
Each thread shows what its agent's own record says it last ran with. A Claude Code session shows the model of its newest reply and, where that model takes them, fast mode and ultracode; the choices and their names are the models the session's own Claude Code binary offers hosts in its initialize answer, or `claude`'s where the system does not name the binary. A Codex thread shows the model and reasoning effort of its newest turn and, where the model has the priority tier, fast mode, named from Codex's own `models_cache.json`. A setting an agent records only when it changes, such as ultracode or Codex's tier, is searched for however far back it lies. A saved thread carries these, without choices, only in its own published state, never in a project's snapshot.
|
|
51
|
+
|
|
52
|
+
The phone changes a running session's settings: through the Claude plugin with Claude Code's own `/model`, `/fast` and `/effort ultracode` commands; in a Claude session the phone started, with control requests at the start of each turn; and in a Codex thread the connector holds, with the next `turn/start` it sends. An agent records a change when its next turn starts, so it shows from the next reply. A change expires with other commands after 60 seconds.
|
|
51
53
|
|
|
52
54
|
Stop interrupts the running turn through each live interface: OpenCode's session abort, Codex `turn/interrupt`, the AHP client's turn cancellation, ACP `session/cancel`, and the Claude Code plugin's turn abort. It refuses when nothing is running and expires with other commands after 60 seconds, so a late stop cannot end a newer turn.
|
|
53
55
|
|
|
@@ -55,7 +57,7 @@ Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the la
|
|
|
55
57
|
|
|
56
58
|
## Starting sessions from the phone
|
|
57
59
|
|
|
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.
|
|
60
|
+
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
61
|
|
|
60
62
|
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
63
|
|
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) => {
|