super-dsh 0.1.0 → 0.1.2
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/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- package/client.js +0 -4
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { OmpSdkSidecar } from "./sidecar-client.js";
|
|
2
|
+
import { OMP_HOME } from "./knobs.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse the CLI args the RPC line passed to `omp --mode rpc`. Only the subset
|
|
5
|
+
* the bridge actually uses is understood; unknown flags fail loudly rather
|
|
6
|
+
* than being silently dropped.
|
|
7
|
+
*/
|
|
8
|
+
function parseSpawnArgs(args) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === "--approval-mode" && args[i + 1] !== undefined) {
|
|
12
|
+
out.approvalMode = args[++i];
|
|
13
|
+
}
|
|
14
|
+
else if (args[i] === "--resume" && args[i + 1] !== undefined) {
|
|
15
|
+
out.resumeFile = args[++i];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
throw new Error(`OmpSdkClient: unsupported spawn arg "${args[i]}" (SDK line)`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** Response-shape shim so RpcResponse-typed call sites keep compiling. */
|
|
24
|
+
function ok(command) {
|
|
25
|
+
return { type: "response", command, success: true };
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Shared sidecar (module-level singleton, refcounted)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
let shared = null;
|
|
31
|
+
let sharedStart = null;
|
|
32
|
+
let refcount = 0;
|
|
33
|
+
/**
|
|
34
|
+
* Acquire the process-wide shared sidecar (one per world root, refcounted).
|
|
35
|
+
*
|
|
36
|
+
* Exactly ONE construction per acquire cycle: a leaked merge once left a
|
|
37
|
+
* duplicate pair here, so every acquire started a second child with `OMP_HOME`
|
|
38
|
+
* unset and abandoned the first (started, unreferenced, never stopped) — 350
|
|
39
|
+
* live processes down the line. Whatever else changes, this block must build
|
|
40
|
+
* the child at most once and must pin the app home on it.
|
|
41
|
+
*/
|
|
42
|
+
function acquire() {
|
|
43
|
+
if (shared === null) {
|
|
44
|
+
shared = new OmpSdkSidecar({ env: { OMP_HOME } });
|
|
45
|
+
sharedStart = shared.start().then(() => undefined);
|
|
46
|
+
}
|
|
47
|
+
refcount++;
|
|
48
|
+
return shared;
|
|
49
|
+
}
|
|
50
|
+
function release() {
|
|
51
|
+
refcount = Math.max(0, refcount - 1);
|
|
52
|
+
if (refcount === 0 && shared !== null) {
|
|
53
|
+
const dying = shared;
|
|
54
|
+
shared = null;
|
|
55
|
+
sharedStart = null;
|
|
56
|
+
void dying.stop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Whether the shared sidecar exists right now (someone holds/wants it). */
|
|
60
|
+
export function isSharedSidecarLive() {
|
|
61
|
+
return shared !== null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Run one bridge-level call ONLY when the shared sidecar is already alive
|
|
65
|
+
* (a live session or an in-flight on-demand op holds it). Cold → `undefined`
|
|
66
|
+
* with ZERO processes: this is the periodic path's entry (model-catalog
|
|
67
|
+
* refresh) so the reconcile tick can never conjure a sidecar.
|
|
68
|
+
*/
|
|
69
|
+
export async function callSharedIfLive(method, params) {
|
|
70
|
+
if (shared === null)
|
|
71
|
+
return undefined;
|
|
72
|
+
const sidecar = acquire();
|
|
73
|
+
try {
|
|
74
|
+
await sharedStart;
|
|
75
|
+
return await sidecar.call(method, params);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run one bridge-level (non-session) data-plane call on the shared sidecar:
|
|
83
|
+
* acquire the refcounted singleton, await startup, call `method`, then release.
|
|
84
|
+
* Used by `omp-cli.ts` for `models.list` and `settings.modelRoles.get/set` so
|
|
85
|
+
* those paths share the SAME sidecar process as live sessions — never a second
|
|
86
|
+
* one. Fails with a rejection when the sidecar cannot start or the method
|
|
87
|
+
* throws; callers translate that into `undefined`/`false` (fail-soft).
|
|
88
|
+
*/
|
|
89
|
+
export async function callShared(method, params) {
|
|
90
|
+
const sidecar = acquire();
|
|
91
|
+
try {
|
|
92
|
+
await sharedStart;
|
|
93
|
+
return await sidecar.call(method, params);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
release();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Discover OMP skills for a workspace (fail-soft: [] on sidecar error). */
|
|
100
|
+
export async function listSkills(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const data = await callShared("skills.list", { cwd });
|
|
103
|
+
return data?.skills ?? [];
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Discover OMP slash commands for a workspace (fail-soft: [] on sidecar error). */
|
|
110
|
+
export async function listSlashCommands(cwd) {
|
|
111
|
+
try {
|
|
112
|
+
const data = await callShared("slashCommands.list", { cwd });
|
|
113
|
+
return data?.commands ?? [];
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
export class OmpSdkClient {
|
|
121
|
+
#sidecar;
|
|
122
|
+
#handle;
|
|
123
|
+
#listeners = new Set();
|
|
124
|
+
#titleListeners = new Set();
|
|
125
|
+
#failureListeners = new Set();
|
|
126
|
+
#closed = false;
|
|
127
|
+
#failure = null;
|
|
128
|
+
#unsub;
|
|
129
|
+
constructor(sidecar, handle) {
|
|
130
|
+
this.#sidecar = sidecar;
|
|
131
|
+
this.#handle = handle;
|
|
132
|
+
this.#unsub = sidecar.onEvent((frame) => {
|
|
133
|
+
if (frame.sessionId !== this.#handle)
|
|
134
|
+
return;
|
|
135
|
+
// OMP's own session title (session file `title` entry, `source: "auto"`).
|
|
136
|
+
// It is not part of the session event stream, so the sidecar sends it on a
|
|
137
|
+
// dedicated frame; Dash never generates a title for an OMP session.
|
|
138
|
+
if (frame.event === "session:title") {
|
|
139
|
+
const title = frame.payload?.title;
|
|
140
|
+
if (typeof title !== "string")
|
|
141
|
+
return;
|
|
142
|
+
for (const listener of [...this.#titleListeners]) {
|
|
143
|
+
try {
|
|
144
|
+
listener(title);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
process.stderr.write(`omp sdk title listener error: ${String(error)}\n`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const event = frame.payload;
|
|
153
|
+
if (event === null || typeof event !== "object" || typeof event.type !== "string")
|
|
154
|
+
return;
|
|
155
|
+
for (const listener of [...this.#listeners]) {
|
|
156
|
+
try {
|
|
157
|
+
listener(event);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
process.stderr.write(`omp sdk listener error: ${String(error)}\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Start a session on the shared sidecar and resolve once it is ready.
|
|
167
|
+
* Mirrors `OmpRpcClient.spawn(args, cwd)`.
|
|
168
|
+
*/
|
|
169
|
+
static async spawn(args = [], cwd) {
|
|
170
|
+
const { approvalMode, resumeFile } = parseSpawnArgs(args);
|
|
171
|
+
const sidecar = acquire();
|
|
172
|
+
try {
|
|
173
|
+
await sharedStart;
|
|
174
|
+
const created = await sidecar.call("session.create", {
|
|
175
|
+
cwd,
|
|
176
|
+
persistence: "file",
|
|
177
|
+
...(approvalMode === undefined ? {} : { approvalMode }),
|
|
178
|
+
...(resumeFile === undefined ? {} : { resumeFile }),
|
|
179
|
+
});
|
|
180
|
+
return new OmpSdkClient(sidecar, created.handle);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
release();
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Register an OMP session-title listener; returns the unsubscriber. */
|
|
188
|
+
onTitle(listener) {
|
|
189
|
+
this.#titleListeners.add(listener);
|
|
190
|
+
return () => {
|
|
191
|
+
this.#titleListeners.delete(listener);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Register an OMP event listener; returns the unsubscriber. */
|
|
195
|
+
on(listener) {
|
|
196
|
+
this.#listeners.add(listener);
|
|
197
|
+
return () => {
|
|
198
|
+
this.#listeners.delete(listener);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Register a failure listener; returns the unsubscriber. */
|
|
202
|
+
onFailure(listener) {
|
|
203
|
+
this.#failureListeners.add(listener);
|
|
204
|
+
return () => {
|
|
205
|
+
this.#failureListeners.delete(listener);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Raw-command escape hatch. The SDK line has no stdio command channel, so
|
|
210
|
+
* the RPC-specific `extension_ui_response` (approval-card answers) is
|
|
211
|
+
* accepted and logged — approval flows route through SDK-side approval
|
|
212
|
+
* events instead (parity work item, see README).
|
|
213
|
+
*/
|
|
214
|
+
sendRaw(command) {
|
|
215
|
+
if (this.#failure !== null || this.#closed)
|
|
216
|
+
return;
|
|
217
|
+
if (process.env.OMP_TRACE === "1") {
|
|
218
|
+
process.stderr.write(`[omp-sdk] sendRaw (no-op on SDK line): ${JSON.stringify(command).slice(0, 160)}\n`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Generic command sender for compatibility. The RPC line's free-form
|
|
223
|
+
* command channel does not exist here; supported commands are mapped onto
|
|
224
|
+
* sidecar methods, everything else fails loudly.
|
|
225
|
+
*/
|
|
226
|
+
async send(command) {
|
|
227
|
+
switch (command.type) {
|
|
228
|
+
case "prompt":
|
|
229
|
+
await this.prompt(String(command.message));
|
|
230
|
+
return ok("prompt");
|
|
231
|
+
case "steer":
|
|
232
|
+
await this.steer(String(command.message));
|
|
233
|
+
return ok("steer");
|
|
234
|
+
case "follow_up":
|
|
235
|
+
await this.followUp(String(command.message));
|
|
236
|
+
return ok("follow_up");
|
|
237
|
+
case "abort":
|
|
238
|
+
await this.abort();
|
|
239
|
+
return ok("abort");
|
|
240
|
+
case "set_model":
|
|
241
|
+
await this.setModel(String(command.provider), String(command.modelId));
|
|
242
|
+
return ok("set_model");
|
|
243
|
+
case "new_session":
|
|
244
|
+
await this.newSession();
|
|
245
|
+
return ok("new_session");
|
|
246
|
+
case "get_state":
|
|
247
|
+
return { type: "response", command: "get_state", success: true, data: await this.getState() };
|
|
248
|
+
case "get_messages":
|
|
249
|
+
return { type: "response", command: "get_messages", success: true, data: { messages: await this.getMessages() } };
|
|
250
|
+
default:
|
|
251
|
+
throw new Error(`OmpSdkClient.send: unsupported command "${String(command.type)}" (SDK line)`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Query session state; returns the `get_state`-shaped payload. */
|
|
255
|
+
async getState() {
|
|
256
|
+
const state = await this.#call("session.state");
|
|
257
|
+
return state ?? { isStreaming: false };
|
|
258
|
+
}
|
|
259
|
+
/** Query the full conversation. */
|
|
260
|
+
async getMessages() {
|
|
261
|
+
const data = await this.#call("session.messages");
|
|
262
|
+
return data?.messages ?? [];
|
|
263
|
+
}
|
|
264
|
+
/** Live token/context/cost accounting (live sessions only). */
|
|
265
|
+
async getSessionStats() {
|
|
266
|
+
return (await this.#call("session.stats")) ?? {};
|
|
267
|
+
}
|
|
268
|
+
/** Live subagent registry (fail-soft: [] until the SDK exposes it). */
|
|
269
|
+
async getSubagents() {
|
|
270
|
+
try {
|
|
271
|
+
const data = await this.#call("session.subagents");
|
|
272
|
+
return data?.subagents ?? [];
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Render the live session's full base system prompt (fail-soft). */
|
|
279
|
+
async getSystemPrompt() {
|
|
280
|
+
try {
|
|
281
|
+
const data = await this.#call("session.systemPrompt");
|
|
282
|
+
return data?.systemPrompt ?? undefined;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Start a turn with a user prompt (fire-and-forget acceptance). */
|
|
289
|
+
async prompt(message) {
|
|
290
|
+
await this.#call("session.prompt", { text: message });
|
|
291
|
+
return ok("prompt");
|
|
292
|
+
}
|
|
293
|
+
/** Queue a follow-up on a busy session. */
|
|
294
|
+
async followUp(message) {
|
|
295
|
+
await this.#call("session.followUp", { text: message });
|
|
296
|
+
return ok("follow_up");
|
|
297
|
+
}
|
|
298
|
+
/** Steer an in-flight turn. */
|
|
299
|
+
async steer(message) {
|
|
300
|
+
await this.#call("session.steer", { text: message });
|
|
301
|
+
return ok("steer");
|
|
302
|
+
}
|
|
303
|
+
/** Select the active model by provider + model id. */
|
|
304
|
+
async setModel(provider, modelId) {
|
|
305
|
+
await this.#call("session.setModel", { provider, modelId });
|
|
306
|
+
return ok("set_model");
|
|
307
|
+
}
|
|
308
|
+
/** Abort the current streaming run. */
|
|
309
|
+
async abort() {
|
|
310
|
+
await this.#call("session.abort");
|
|
311
|
+
return ok("abort");
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Run OMP's own context compaction on the live session (the TUI /compact
|
|
315
|
+
* handler's exact call). Resolves when the compaction settles.
|
|
316
|
+
*/
|
|
317
|
+
async compact(instructions) {
|
|
318
|
+
await this.#call("session.compact", instructions === undefined ? {} : { instructions });
|
|
319
|
+
return ok("compact");
|
|
320
|
+
}
|
|
321
|
+
/** Context-usage snapshot for pre/post compact metering (fail-soft). */
|
|
322
|
+
async contextUsage() {
|
|
323
|
+
try {
|
|
324
|
+
const data = await this.#call("session.contextUsage");
|
|
325
|
+
return data?.usage ?? undefined;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Discard the current conversation and start a fresh session (same handle). */
|
|
332
|
+
async newSession() {
|
|
333
|
+
await this.#call("session.new");
|
|
334
|
+
return ok("new_session");
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Lazy-surface parity: a real client is born live, so "ensure started" is a
|
|
338
|
+
* no-op (LazyOmpRpc implements the real deferred spawn behind the same
|
|
339
|
+
* OmpAgentRpc interface).
|
|
340
|
+
*/
|
|
341
|
+
ensureStarted() {
|
|
342
|
+
return Promise.resolve();
|
|
343
|
+
}
|
|
344
|
+
/** Always true for a real client (see OmpAgentRpc.spawned). */
|
|
345
|
+
get spawned() {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
/** Discard the current conversation and start a fresh session (same handle). */
|
|
349
|
+
close() {
|
|
350
|
+
if (this.#closed)
|
|
351
|
+
return;
|
|
352
|
+
this.#closed = true;
|
|
353
|
+
this.#unsub();
|
|
354
|
+
void this.#sidecar.call("session.dispose", { handle: this.#handle }).catch(() => { });
|
|
355
|
+
release();
|
|
356
|
+
this.#fail(new Error("omp sdk client closed"));
|
|
357
|
+
}
|
|
358
|
+
#call(method, params) {
|
|
359
|
+
if (this.#failure !== null)
|
|
360
|
+
return Promise.reject(this.#failure);
|
|
361
|
+
if (this.#closed)
|
|
362
|
+
return Promise.reject(new Error("omp sdk client is closed"));
|
|
363
|
+
return this.#sidecar.call(method, { handle: this.#handle, ...params });
|
|
364
|
+
}
|
|
365
|
+
#fail(error) {
|
|
366
|
+
if (this.#failure !== null)
|
|
367
|
+
return;
|
|
368
|
+
this.#failure = error;
|
|
369
|
+
for (const listener of [...this.#failureListeners]) {
|
|
370
|
+
try {
|
|
371
|
+
listener(error);
|
|
372
|
+
}
|
|
373
|
+
catch (listenerError) {
|
|
374
|
+
process.stderr.write(`omp sdk failure listener error: ${String(listenerError)}\n`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-side client for the omp-web bun sidecar.
|
|
3
|
+
* Spawns `bun run sidecar/main.ts`, speaks the v0 JSON-lines protocol,
|
|
4
|
+
* correlates requests by id and dispatches pushed event frames.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { PROTOCOL_VERSION, } from "./protocol.js";
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkgDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
/**
|
|
15
|
+
* Locate the bun binary for the sidecar spawn.
|
|
16
|
+
*
|
|
17
|
+
* Layout-dependent: a checkout has it at `<pkg>/node_modules/.bin/bun`, but a
|
|
18
|
+
* pnpm-hoisted profile install (prod) puts binaries in the profile root's
|
|
19
|
+
* `node_modules/.bin` with the package itself symlinked under
|
|
20
|
+
* `node_modules/<name>` — so walk the ancestors' .bin directories, and fall
|
|
21
|
+
* back to a plain `bun` on PATH.
|
|
22
|
+
*/
|
|
23
|
+
function resolveBunBinary(fromDir) {
|
|
24
|
+
const local = path.join(fromDir, "node_modules", ".bin", "bun");
|
|
25
|
+
if (existsSync(local))
|
|
26
|
+
return local;
|
|
27
|
+
let dir = fromDir;
|
|
28
|
+
for (let i = 0; i < 8; i++) {
|
|
29
|
+
const parent = path.dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
break;
|
|
32
|
+
dir = parent;
|
|
33
|
+
const candidate = path.join(dir, "node_modules", ".bin", "bun");
|
|
34
|
+
if (existsSync(candidate))
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
return "bun";
|
|
38
|
+
}
|
|
39
|
+
export class OmpSdkSidecar {
|
|
40
|
+
options;
|
|
41
|
+
proc = null;
|
|
42
|
+
nextId = 1;
|
|
43
|
+
pending = new Map();
|
|
44
|
+
eventHandlers = new Set();
|
|
45
|
+
readyPromise = null;
|
|
46
|
+
buffer = "";
|
|
47
|
+
startError = null;
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
}
|
|
51
|
+
get started() {
|
|
52
|
+
return this.proc !== null;
|
|
53
|
+
}
|
|
54
|
+
onEvent(handler) {
|
|
55
|
+
this.eventHandlers.add(handler);
|
|
56
|
+
return () => this.eventHandlers.delete(handler);
|
|
57
|
+
}
|
|
58
|
+
async start() {
|
|
59
|
+
if (this.readyPromise)
|
|
60
|
+
return this.readyPromise;
|
|
61
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
62
|
+
const bunBin = resolveBunBinary(pkgDir);
|
|
63
|
+
const sidecarEntry = path.join(pkgDir, "sidecar", "main.ts");
|
|
64
|
+
const env = {
|
|
65
|
+
...process.env,
|
|
66
|
+
...this.options.env,
|
|
67
|
+
};
|
|
68
|
+
let proc;
|
|
69
|
+
try {
|
|
70
|
+
proc = spawn(bunBin, ["run", sidecarEntry], {
|
|
71
|
+
cwd: this.options.cwd ?? pkgDir,
|
|
72
|
+
env,
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
reject(new Error(`failed to spawn bun sidecar: ${err.message}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.proc = proc;
|
|
81
|
+
const timeout = setTimeout(() => reject(new Error(`sidecar ready timeout (60s): ${this.startError ?? "no output"}`)), 60_000);
|
|
82
|
+
const settle = (fn) => { clearTimeout(timeout); fn(); };
|
|
83
|
+
// A dead sidecar surfaces as EPIPE on stdin writes; keep a permanent
|
|
84
|
+
// no-op error sink so no window exists where the stream throws out of the
|
|
85
|
+
// event loop (per-call degradation is handled by #writeSafe).
|
|
86
|
+
proc.stdin.on("error", () => { });
|
|
87
|
+
proc.stdout.setEncoding("utf8");
|
|
88
|
+
proc.stdout.on("data", (chunk) => this.consume(chunk, (r) => settle(() => resolve(r))));
|
|
89
|
+
proc.stderr.setEncoding("utf8");
|
|
90
|
+
proc.stderr.on("data", (chunk) => {
|
|
91
|
+
this.startError = (this.startError ?? "") + chunk;
|
|
92
|
+
process.stderr.write(`[omp-sdk-sidecar] ${chunk}`);
|
|
93
|
+
});
|
|
94
|
+
proc.on("exit", (code, signal) => {
|
|
95
|
+
settle(() => reject(new Error(`sidecar exited before ready (${code ?? signal}): ${this.startError ?? ""}`)));
|
|
96
|
+
for (const [, p] of this.pending)
|
|
97
|
+
p.reject(new Error(`sidecar exited (${code ?? signal})`));
|
|
98
|
+
this.pending.clear();
|
|
99
|
+
this.proc = null;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return this.readyPromise;
|
|
103
|
+
}
|
|
104
|
+
consume(chunk, onReady) {
|
|
105
|
+
this.buffer += chunk;
|
|
106
|
+
let nl;
|
|
107
|
+
while ((nl = this.buffer.indexOf("\n")) >= 0) {
|
|
108
|
+
const raw = this.buffer.slice(0, nl).trim();
|
|
109
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
110
|
+
if (!raw)
|
|
111
|
+
continue;
|
|
112
|
+
let frame;
|
|
113
|
+
try {
|
|
114
|
+
frame = JSON.parse(raw);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (frame.event === "ready") {
|
|
120
|
+
onReady?.({ protocol: frame.payload?.protocol ?? PROTOCOL_VERSION });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (typeof frame.id === "number" && ("ok" in frame)) {
|
|
124
|
+
const p = this.pending.get(frame.id);
|
|
125
|
+
if (p) {
|
|
126
|
+
this.pending.delete(frame.id);
|
|
127
|
+
if (frame.ok)
|
|
128
|
+
p.resolve(frame.result);
|
|
129
|
+
else
|
|
130
|
+
p.reject(new Error(frame.error));
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (frame.event) {
|
|
135
|
+
for (const h of this.eventHandlers)
|
|
136
|
+
h(frame);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
call(method, params) {
|
|
141
|
+
if (!this.proc?.stdin)
|
|
142
|
+
return Promise.reject(new Error("sidecar not started"));
|
|
143
|
+
const id = this.nextId++;
|
|
144
|
+
const frame = { id, method, params };
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
this.pending.set(id, { resolve, reject });
|
|
147
|
+
this.#writeSafe(JSON.stringify(frame) + "\n", () => {
|
|
148
|
+
this.pending.delete(id);
|
|
149
|
+
reject(new Error(`sidecar write failed: ${method}`));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** Write to the sidecar's stdin without ever throwing EPIPE out of the loop. */
|
|
154
|
+
#writeSafe(chunk, onLost) {
|
|
155
|
+
const stdin = this.proc?.stdin;
|
|
156
|
+
if (!stdin)
|
|
157
|
+
return onLost();
|
|
158
|
+
const fail = (err) => {
|
|
159
|
+
// EPIPE/ECONNRESET when the sidecar died between checks — a lost frame
|
|
160
|
+
// must degrade the caller, never crash the host process.
|
|
161
|
+
stdin.removeListener("error", fail);
|
|
162
|
+
try {
|
|
163
|
+
stdin.destroy();
|
|
164
|
+
}
|
|
165
|
+
catch { /* already gone */ }
|
|
166
|
+
onLost();
|
|
167
|
+
};
|
|
168
|
+
stdin.once("error", fail);
|
|
169
|
+
try {
|
|
170
|
+
stdin.write(chunk, () => stdin.removeListener("error", fail));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
fail(new Error("write threw"));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
notify(method, params) {
|
|
177
|
+
if (!this.proc?.stdin)
|
|
178
|
+
return;
|
|
179
|
+
this.#writeSafe(JSON.stringify({ method, params }) + "\n", () => { });
|
|
180
|
+
}
|
|
181
|
+
async stop() {
|
|
182
|
+
const proc = this.proc;
|
|
183
|
+
if (!proc)
|
|
184
|
+
return;
|
|
185
|
+
this.proc = null;
|
|
186
|
+
await new Promise((resolve) => {
|
|
187
|
+
proc.once("exit", () => resolve());
|
|
188
|
+
proc.stdin.end();
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
proc.kill("SIGTERM");
|
|
191
|
+
setTimeout(() => proc.kill("SIGKILL"), 3000);
|
|
192
|
+
}, 3000).unref?.();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite driver adapter — one thin interface over whichever builtin the
|
|
3
|
+
* runtime ships. Bun → `bun:sqlite`; Node (≥22.5) → `node:sqlite`. The store
|
|
4
|
+
* never cares which: it sees exec / prepare→(run|get|all) / close.
|
|
5
|
+
*
|
|
6
|
+
* Both builtins load through `createRequire` (never a static ESM import) so
|
|
7
|
+
* neither `bun:` nor `node:sqlite` is resolved at module load: under Node the
|
|
8
|
+
* `bun:` branch is dead code, under Bun the `node:sqlite` branch is dead code.
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
12
|
+
function openNode(path) {
|
|
13
|
+
const { DatabaseSync: Driver } = nodeRequire("node:sqlite");
|
|
14
|
+
const db = new Driver(path);
|
|
15
|
+
return {
|
|
16
|
+
exec: (sql) => db.exec(sql),
|
|
17
|
+
prepare: (sql) => {
|
|
18
|
+
const statement = db.prepare(sql);
|
|
19
|
+
return {
|
|
20
|
+
run: (...params) => statement.run(...params),
|
|
21
|
+
get: (...params) => statement.get(...params),
|
|
22
|
+
all: (...params) => statement.all(...params),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
close: () => db.close(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function openBun(path) {
|
|
29
|
+
const { Database: Driver } = nodeRequire("bun:sqlite");
|
|
30
|
+
const db = new Driver(path, { create: true });
|
|
31
|
+
return {
|
|
32
|
+
exec: (sql) => db.exec(sql),
|
|
33
|
+
prepare: (sql) => {
|
|
34
|
+
const query = db.query(sql);
|
|
35
|
+
return {
|
|
36
|
+
run: (...params) => query.run(...params),
|
|
37
|
+
get: (...params) => query.get(...params),
|
|
38
|
+
all: (...params) => query.all(...params),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
close: () => db.close(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Open a SQLite connection on whichever builtin the runtime provides. */
|
|
45
|
+
export function openDb(path) {
|
|
46
|
+
return globalThis.Bun !== undefined ? openBun(path) : openNode(path);
|
|
47
|
+
}
|