dsh-agy-link 0.2.9 → 0.3.1
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 -2
- package/dist/index.js +515 -130
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,7 +145,8 @@ Bring **Google Antigravity models into DeepSeek Harness (DSH)** — chat, thinki
|
|
|
145
145
|
| Capability | Description |
|
|
146
146
|
| ---- | ---- |
|
|
147
147
|
| 🔌 **A model route, not a proxy** | Registers the `antigravity` provider in DSH's model config; pick any Antigravity model (Gemini / Claude / GPT-OSS) from the `/model` picker |
|
|
148
|
-
| 🌊 **Full streaming** | Text, thinking (reasoning), and
|
|
148
|
+
| 🌊 **Full streaming** | Text, thinking (reasoning), and token usage mapped onto DSH's native chunk protocol |
|
|
149
|
+
| 🃏 **Native tool cards (v0.3)** | agy's tool activity renders with DSH's own tool-card UI — terminal cards for `run_command`, inline diffs for file writes — via the internal `agy_tool` mirror riding the real agent loop |
|
|
149
150
|
| 🔗 **Session continuity** | Each DSH session binds to a native agy conversation (`--conversation`); multi-turn context rides agy history instead of re-sending everything |
|
|
150
151
|
| 📊 **Token usage** | Input/output/thinking/cache tokens surface in DSH usage accounting |
|
|
151
152
|
| ⚙️ **Settings status** | DSH Settings → Antigravity page shows agy connection/login/workspace/bindings/last-run state |
|
|
@@ -222,7 +223,7 @@ DSH-side tools and permissions are unaffected — this only governs what the spa
|
|
|
222
223
|
|
|
223
224
|
## 🧩 How it works
|
|
224
225
|
|
|
225
|
-
One DSH
|
|
226
|
+
One DSH turn = one short-lived `agy -p --output-format stream-json` process. The NDJSON event stream is parsed, normalized, and recorded. Spans of that recording are mapped to DSH StreamChunks — thinking → reasoning blocks, text → text blocks, result envelope → usage + finish — and each **completed agy tool step cuts the span** with a `tool-calls` finish addressed to the internal `agy_tool` mirror. DSH's agent loop dispatches the mirror (it instantly replays the recorded output), writes real `tool/call` + `tool/result` session events, and re-calls the provider to continue the run — so tool activity renders with DSH's **native tool-card UI** (terminal cards, diffs, read/search icons) instead of text annotations. Conversation ids come from the stream itself, with a conversations-directory snapshot diff as fallback. **No reverse-engineered database scraping, no protobuf decoding, no token-file access** — only the official unmodified agy binary is spawned.
|
|
226
227
|
|
|
227
228
|
## 📋 What it cannot do (honest list)
|
|
228
229
|
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
2
2
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { CallId, LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
6
|
import { mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
7
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
9
|
import { createServer } from "node:http";
|
|
9
|
-
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
//#region src/common/types.ts
|
|
12
12
|
const PROVIDER_ID = "antigravity";
|
|
@@ -178,7 +178,7 @@ function readJson(file) {
|
|
|
178
178
|
function readOverrides(file = overridesPath()) {
|
|
179
179
|
return readJson(file);
|
|
180
180
|
}
|
|
181
|
-
function asString(v) {
|
|
181
|
+
function asString$1(v) {
|
|
182
182
|
return typeof v === "string" ? v : void 0;
|
|
183
183
|
}
|
|
184
184
|
function asBool(v) {
|
|
@@ -207,11 +207,11 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
207
207
|
const cfg = {
|
|
208
208
|
...base,
|
|
209
209
|
enabled: asBool(get("enabled")) ?? base.enabled,
|
|
210
|
-
agyBin: asString(get("agyBin")) ?? base.agyBin,
|
|
210
|
+
agyBin: asString$1(get("agyBin")) ?? base.agyBin,
|
|
211
211
|
extraArgs: Array.isArray(get("extraArgs")) ? get("extraArgs").filter((x) => typeof x === "string") : base.extraArgs,
|
|
212
212
|
permissionMode: asMode(get("permissionMode")) ?? base.permissionMode,
|
|
213
|
-
defaultModel: asString(get("defaultModel")) ?? base.defaultModel,
|
|
214
|
-
defaultEffort: asString(get("defaultEffort")) ?? base.defaultEffort,
|
|
213
|
+
defaultModel: asString$1(get("defaultModel")) ?? base.defaultModel,
|
|
214
|
+
defaultEffort: asString$1(get("defaultEffort")) ?? base.defaultEffort,
|
|
215
215
|
timeoutMs: asNum(get("timeoutMs")) ?? base.timeoutMs,
|
|
216
216
|
maxConcurrent: asNum(get("maxConcurrent")) ?? base.maxConcurrent,
|
|
217
217
|
contextWindowDefault: asNum(get("contextWindowDefault")) ?? base.contextWindowDefault,
|
|
@@ -221,15 +221,15 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
221
221
|
modelsCacheTtlMs: asNum(get("modelsCacheTtlMs")) ?? base.modelsCacheTtlMs,
|
|
222
222
|
allowAuxiliary: asBool(get("allowAuxiliary")) ?? base.allowAuxiliary,
|
|
223
223
|
compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
|
|
224
|
-
workspaceRoot: asString(get("workspaceRoot")) ?? base.workspaceRoot,
|
|
224
|
+
workspaceRoot: asString$1(get("workspaceRoot")) ?? base.workspaceRoot,
|
|
225
225
|
fallbackModels: Array.isArray(get("fallbackModels")) ? get("fallbackModels").filter((x) => !!x && typeof x === "object" && typeof x.id === "string") : base.fallbackModels,
|
|
226
226
|
askTool: asBool(get("askTool")) ?? base.askTool,
|
|
227
|
-
mediaDir: asString(get("mediaDir")) ?? base.mediaDir,
|
|
227
|
+
mediaDir: asString$1(get("mediaDir")) ?? base.mediaDir,
|
|
228
228
|
mediaTtlMs: asNum(get("mediaTtlMs")) ?? base.mediaTtlMs,
|
|
229
229
|
mediaMaxBytes: asNum(get("mediaMaxBytes")) ?? base.mediaMaxBytes,
|
|
230
230
|
mediaMaxImages: asNum(get("mediaMaxImages")) ?? base.mediaMaxImages,
|
|
231
231
|
mcpBridge: asBool(get("mcpBridge")) ?? base.mcpBridge,
|
|
232
|
-
mcpToolAllowlist: asString(get("mcpToolAllowlist")) ?? base.mcpToolAllowlist
|
|
232
|
+
mcpToolAllowlist: asString$1(get("mcpToolAllowlist")) ?? base.mcpToolAllowlist
|
|
233
233
|
};
|
|
234
234
|
if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
|
|
235
235
|
if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
|
|
@@ -300,31 +300,158 @@ function diffConversations(before, dir = defaultConversationsDir()) {
|
|
|
300
300
|
};
|
|
301
301
|
}
|
|
302
302
|
//#endregion
|
|
303
|
-
//#region src/host/
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
//#region src/host/recording.ts
|
|
304
|
+
const waiters = Symbol("waiters");
|
|
305
|
+
/** One agy run's append-only event log plus its settlement state. */
|
|
306
|
+
var RunRecording = class {
|
|
307
|
+
runId;
|
|
308
|
+
events = [];
|
|
309
|
+
settled = false;
|
|
310
|
+
failure = null;
|
|
311
|
+
resultConversationId = null;
|
|
312
|
+
[waiters] = /* @__PURE__ */ new Set();
|
|
313
|
+
constructor(runId = randomUUID()) {
|
|
314
|
+
this.runId = runId;
|
|
315
|
+
}
|
|
316
|
+
/** Append one pump event and wake every span consumer. */
|
|
317
|
+
append(ev) {
|
|
318
|
+
if (this.settled) return;
|
|
319
|
+
this.events.push(ev);
|
|
320
|
+
if (ev.kind === "result") this.resultConversationId = ev.conversationId ?? null;
|
|
321
|
+
this.wake();
|
|
322
|
+
}
|
|
323
|
+
/** Settle the recording: no further events will arrive. */
|
|
324
|
+
settle(failure) {
|
|
325
|
+
if (this.settled) return;
|
|
326
|
+
this.settled = true;
|
|
327
|
+
this.failure = failure;
|
|
328
|
+
this.wake();
|
|
329
|
+
}
|
|
330
|
+
wake() {
|
|
331
|
+
for (const w of this[waiters]) w();
|
|
332
|
+
this[waiters].clear();
|
|
333
|
+
}
|
|
334
|
+
get isSettled() {
|
|
335
|
+
return this.settled;
|
|
336
|
+
}
|
|
337
|
+
/** Terminal failure, when the run ended without a consumable result. */
|
|
338
|
+
get failureInfo() {
|
|
339
|
+
return this.failure;
|
|
340
|
+
}
|
|
341
|
+
/** Conversation id from the result envelope, once seen. */
|
|
342
|
+
get conversationId() {
|
|
343
|
+
return this.resultConversationId;
|
|
344
|
+
}
|
|
345
|
+
/** Whether a result event (ok or error-with-response) was recorded. */
|
|
346
|
+
get hasResult() {
|
|
347
|
+
return this.resultConversationId !== null || this.events.some((e) => e.kind === "result");
|
|
348
|
+
}
|
|
349
|
+
/** The last result envelope's finish-relevant projection, when one arrived. */
|
|
350
|
+
getResultEvent() {
|
|
351
|
+
for (let i = this.events.length - 1; i >= 0; i--) {
|
|
352
|
+
const ev = this.events[i];
|
|
353
|
+
if (ev !== void 0 && ev.kind === "result") return {
|
|
354
|
+
ok: ev.ok,
|
|
355
|
+
response: ev.response
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
311
359
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
let s;
|
|
320
|
-
try {
|
|
321
|
-
s = typeof output === "string" ? output : JSON.stringify(output);
|
|
322
|
-
} catch {
|
|
323
|
-
s = String(output);
|
|
360
|
+
/** Event at an absolute index (bounds-checked). */
|
|
361
|
+
eventAt(index) {
|
|
362
|
+
return this.events[index];
|
|
363
|
+
}
|
|
364
|
+
/** Number of recorded events so far. */
|
|
365
|
+
get length() {
|
|
366
|
+
return this.events.length;
|
|
324
367
|
}
|
|
325
|
-
|
|
326
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Whether any assistant-visible text streamed before (not including) the
|
|
370
|
+
* given index. Carried into each span's mapper so the result-envelope
|
|
371
|
+
* fallback never duplicates text earlier spans already streamed.
|
|
372
|
+
*/
|
|
373
|
+
sawTextBefore(index) {
|
|
374
|
+
for (let i = 0; i < Math.min(index, this.events.length); i++) {
|
|
375
|
+
const ev = this.events[i];
|
|
376
|
+
if (ev !== void 0 && ev.kind === "step" && ev.stepKind === "text" && ev.text !== "") return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Yield events at or after `from`, waiting for the pump while the run is
|
|
382
|
+
* live. The iterator ends once every recorded event was yielded AND the
|
|
383
|
+
* recording settled — spans then inspect failureInfo to finish or fail.
|
|
384
|
+
*/
|
|
385
|
+
async *eventsFrom(from) {
|
|
386
|
+
let cursor = from;
|
|
387
|
+
for (;;) {
|
|
388
|
+
while (cursor < this.events.length) {
|
|
389
|
+
yield this.events[cursor];
|
|
390
|
+
cursor++;
|
|
391
|
+
}
|
|
392
|
+
if (this.settled) return;
|
|
393
|
+
await new Promise((resolve) => {
|
|
394
|
+
this[waiters].add(resolve);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Recorded tool step for a mirror-tool call: the event at `eventIndex` must
|
|
400
|
+
* be the completed tool step the callId was minted from.
|
|
401
|
+
*/
|
|
402
|
+
toolEventAt(eventIndex) {
|
|
403
|
+
const ev = this.events[eventIndex];
|
|
404
|
+
if (ev === void 0 || ev.kind !== "step" || ev.stepKind !== "tool" || !ev.tool) return null;
|
|
405
|
+
return ev.tool;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
/** Prefix every mirrored agy tool callId carries; continuation detection key. */
|
|
409
|
+
const AGY_CALL_PREFIX = "agytc-";
|
|
410
|
+
/** Mint the callId for one recorded tool event. */
|
|
411
|
+
function mirrorCallId(runId, eventIndex) {
|
|
412
|
+
return AGY_CALL_PREFIX + runId + "-" + String(eventIndex);
|
|
413
|
+
}
|
|
414
|
+
/** Parse a callId back into its run coordinates; null when not ours. */
|
|
415
|
+
function parseMirrorCallId(callId) {
|
|
416
|
+
if (!callId.startsWith("agytc-")) return null;
|
|
417
|
+
const rest = callId.slice(6);
|
|
418
|
+
const idx = rest.lastIndexOf("-");
|
|
419
|
+
if (idx <= 0) return null;
|
|
420
|
+
const runId = rest.slice(0, idx);
|
|
421
|
+
const n = Number(rest.slice(idx + 1));
|
|
422
|
+
if (!Number.isSafeInteger(n) || n < 0) return null;
|
|
423
|
+
return {
|
|
424
|
+
runId,
|
|
425
|
+
eventIndex: n
|
|
426
|
+
};
|
|
327
427
|
}
|
|
428
|
+
const MAX_RETAINED_RUNS = 8;
|
|
429
|
+
/** Bounded registry keeping the most recent runs for continuation spans. */
|
|
430
|
+
var RunRegistry = class {
|
|
431
|
+
runs = /* @__PURE__ */ new Map();
|
|
432
|
+
create() {
|
|
433
|
+
const rec = new RunRecording();
|
|
434
|
+
this.remember(rec);
|
|
435
|
+
return rec;
|
|
436
|
+
}
|
|
437
|
+
remember(rec) {
|
|
438
|
+
this.runs.set(rec.runId, rec);
|
|
439
|
+
while (this.runs.size > MAX_RETAINED_RUNS) {
|
|
440
|
+
const oldest = this.runs.keys().next().value;
|
|
441
|
+
if (oldest === void 0) break;
|
|
442
|
+
this.runs.delete(oldest);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
get(runId) {
|
|
446
|
+
return this.runs.get(runId);
|
|
447
|
+
}
|
|
448
|
+
/** Drop a settled run early (called when its final span finished stop). */
|
|
449
|
+
forget(runId) {
|
|
450
|
+
this.runs.delete(runId);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/host/mapper.ts
|
|
328
455
|
function usageFromRaw(raw) {
|
|
329
456
|
const usage = {
|
|
330
457
|
inputTokens: raw.input_tokens ?? 0,
|
|
@@ -348,12 +475,13 @@ var EventMapper = class {
|
|
|
348
475
|
openType = null;
|
|
349
476
|
openAcc = "";
|
|
350
477
|
emittedByKey = /* @__PURE__ */ new Map();
|
|
351
|
-
|
|
478
|
+
announcedTools = /* @__PURE__ */ new Set();
|
|
352
479
|
thinkingAnnounced = /* @__PURE__ */ new Set();
|
|
353
|
-
sawTextStep
|
|
480
|
+
sawTextStep;
|
|
354
481
|
finished = false;
|
|
355
|
-
constructor(opts
|
|
482
|
+
constructor(opts) {
|
|
356
483
|
this.opts = opts;
|
|
484
|
+
this.sawTextStep = opts.initialSawText === true;
|
|
357
485
|
}
|
|
358
486
|
/** Whether a terminal finish chunk has been emitted. */
|
|
359
487
|
get isFinished() {
|
|
@@ -403,7 +531,12 @@ var EventMapper = class {
|
|
|
403
531
|
text: delta
|
|
404
532
|
};
|
|
405
533
|
}
|
|
406
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Map one event. `absIndex` is the event's position in the run recording;
|
|
536
|
+
* it mints the mirror callId and is what continuation detection parses
|
|
537
|
+
* back out of the DSH message list.
|
|
538
|
+
*/
|
|
539
|
+
*map(ev, absIndex) {
|
|
407
540
|
if (this.finished) return;
|
|
408
541
|
if (ev.kind === "init") return;
|
|
409
542
|
if (ev.kind === "garbage") return;
|
|
@@ -447,29 +580,48 @@ var EventMapper = class {
|
|
|
447
580
|
return;
|
|
448
581
|
}
|
|
449
582
|
if (ev.stepKind === "tool" && ev.tool) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
583
|
+
if (!(ev.tool.output !== void 0 || ev.tool.error !== void 0)) return;
|
|
584
|
+
if (this.announcedTools.has(ev.stepKey)) return;
|
|
585
|
+
this.announcedTools.add(ev.stepKey);
|
|
586
|
+
if (!this.opts.cutOnTool) return;
|
|
587
|
+
const close = this.closeOpen();
|
|
588
|
+
if (close) yield close;
|
|
589
|
+
const idx = this.blockIdx;
|
|
590
|
+
const input = ev.tool.args === void 0 ? {} : { input: ev.tool.args };
|
|
591
|
+
const argumentsJson = JSON.stringify({
|
|
592
|
+
run: this.opts.runId,
|
|
593
|
+
step: absIndex,
|
|
594
|
+
tool: ev.tool.name,
|
|
595
|
+
...input
|
|
596
|
+
});
|
|
597
|
+
yield {
|
|
598
|
+
type: "block-start",
|
|
599
|
+
index: idx,
|
|
600
|
+
blockType: "tool-call"
|
|
601
|
+
};
|
|
602
|
+
yield {
|
|
603
|
+
type: "block-end",
|
|
604
|
+
index: idx,
|
|
605
|
+
block: {
|
|
606
|
+
type: "tool-call",
|
|
607
|
+
id: CallId(mirrorCallId(this.opts.runId, absIndex)),
|
|
608
|
+
name: "agy_tool",
|
|
609
|
+
arguments: argumentsJson
|
|
471
610
|
}
|
|
472
|
-
}
|
|
611
|
+
};
|
|
612
|
+
this.blockIdx++;
|
|
613
|
+
yield {
|
|
614
|
+
type: "usage",
|
|
615
|
+
usage: {
|
|
616
|
+
inputTokens: 0,
|
|
617
|
+
outputTokens: 0
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
yield {
|
|
621
|
+
type: "finish",
|
|
622
|
+
reason: { kind: "tool-calls" }
|
|
623
|
+
};
|
|
624
|
+
this.finished = true;
|
|
473
625
|
return;
|
|
474
626
|
}
|
|
475
627
|
return;
|
|
@@ -1551,6 +1703,32 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1551
1703
|
if (isAux && !cfg.allowAuxiliary) throw new LlmError("auxiliary calls are disabled for the antigravity route (allowAuxiliary: false)", Err.AUX_DISABLED);
|
|
1552
1704
|
const sessionKey = options.sessionId !== void 0 ? String(options.sessionId) : "";
|
|
1553
1705
|
const workspaceRoot = cfg.workspaceRoot !== "" ? cfg.workspaceRoot : this.deps.sessionCwd?.(sessionKey) || process.cwd();
|
|
1706
|
+
const continuation = detectContinuation(options.messages);
|
|
1707
|
+
if (continuation !== null) {
|
|
1708
|
+
const rec = this.deps.runs.get(continuation.runId);
|
|
1709
|
+
if (rec === void 0) {
|
|
1710
|
+
yield {
|
|
1711
|
+
type: "usage",
|
|
1712
|
+
usage: {
|
|
1713
|
+
inputTokens: 0,
|
|
1714
|
+
outputTokens: 0
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
yield {
|
|
1718
|
+
type: "finish",
|
|
1719
|
+
reason: {
|
|
1720
|
+
kind: "error",
|
|
1721
|
+
failure: {
|
|
1722
|
+
message: "agy run " + continuation.runId + " is no longer available (server restarted?) — please resend your message",
|
|
1723
|
+
code: Err.AGY_ERROR
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
yield* this.driveSpan(rec, continuation.eventIndex + 1, true);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1554
1732
|
const catalog = this.deps.catalog.get();
|
|
1555
1733
|
const model = options.model;
|
|
1556
1734
|
const entry = findEntry(catalog, model);
|
|
@@ -1620,11 +1798,9 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1620
1798
|
if (prompt.trim() === "") throw new LlmError("request carries no user text or images to forward to agy", Err.AGY_ERROR);
|
|
1621
1799
|
} else if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
|
|
1622
1800
|
const before = snapshotConversations();
|
|
1623
|
-
const
|
|
1624
|
-
const mapper = new EventMapper({ toolOutput: toolOutput ? (name, args, output) => toolOutput(name, args, output, workspaceRoot) : void 0 });
|
|
1801
|
+
const rec = this.deps.runs.create();
|
|
1625
1802
|
const parser = new StreamJsonParser();
|
|
1626
1803
|
this.deps.onParser?.(parser);
|
|
1627
|
-
const queue = new ChunkQueue();
|
|
1628
1804
|
let streamCid = null;
|
|
1629
1805
|
const args = this.buildArgs({
|
|
1630
1806
|
prompt,
|
|
@@ -1655,7 +1831,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1655
1831
|
for (const ev of parser.feed(line + "\n")) {
|
|
1656
1832
|
if (ev.kind === "init" && ev.conversationId) streamCid = ev.conversationId;
|
|
1657
1833
|
if (ev.kind === "result" && ev.conversationId !== "") streamCid = ev.conversationId;
|
|
1658
|
-
|
|
1834
|
+
rec.append(ev);
|
|
1659
1835
|
}
|
|
1660
1836
|
}
|
|
1661
1837
|
});
|
|
@@ -1668,10 +1844,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1668
1844
|
releaseOnce();
|
|
1669
1845
|
for (const ev of parser.flush()) {
|
|
1670
1846
|
if (ev.kind === "result" && ev.conversationId !== "") streamCid = ev.conversationId;
|
|
1671
|
-
|
|
1847
|
+
rec.append(ev);
|
|
1672
1848
|
}
|
|
1673
1849
|
const diffed = diffConversations(before).conversationId;
|
|
1674
1850
|
const conversationId = streamCid ?? diffed;
|
|
1851
|
+
const r = rec.getResultEvent();
|
|
1852
|
+
const consumable = r !== null && (r.ok || r.response !== "");
|
|
1675
1853
|
let failure = null;
|
|
1676
1854
|
if (outcome.aborted) failure = {
|
|
1677
1855
|
kind: "aborted",
|
|
@@ -1688,7 +1866,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1688
1866
|
code: Err.AUTH,
|
|
1689
1867
|
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
1690
1868
|
};
|
|
1691
|
-
else if (!
|
|
1869
|
+
else if (!consumable) {
|
|
1692
1870
|
if (outcome.code !== 0) failure = {
|
|
1693
1871
|
kind: "error",
|
|
1694
1872
|
code: Err.PROCESS_EXIT,
|
|
@@ -1705,7 +1883,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1705
1883
|
message: "agy produced no result event (" + parser.stats.garbage + " unparseable lines)"
|
|
1706
1884
|
};
|
|
1707
1885
|
}
|
|
1708
|
-
|
|
1886
|
+
rec.settle(failure);
|
|
1709
1887
|
if (!isAux && sessionKey !== "" && failure === null) {
|
|
1710
1888
|
const finalId = binding !== void 0 ? binding.conversationId : conversationId;
|
|
1711
1889
|
if (finalId) this.deps.store.set(sessionKey, {
|
|
@@ -1721,15 +1899,63 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1721
1899
|
durationMs: outcome.durationMs,
|
|
1722
1900
|
model
|
|
1723
1901
|
});
|
|
1724
|
-
queue.close();
|
|
1725
1902
|
})().catch((err) => {
|
|
1726
1903
|
releaseOnce();
|
|
1727
|
-
|
|
1728
|
-
|
|
1904
|
+
rec.settle({
|
|
1905
|
+
kind: "error",
|
|
1906
|
+
code: Err.PROCESS_EXIT,
|
|
1907
|
+
message: "internal error: " + brief(String(err))
|
|
1908
|
+
});
|
|
1729
1909
|
});
|
|
1910
|
+
yield* this.driveSpan(rec, 0, !isAux);
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Stream one span of a recording: map events from `from` until the mapper
|
|
1914
|
+
* finishes (tool-calls cut or result stop), then let the queue drain. When
|
|
1915
|
+
* the recording settles without a consumable result, surface its failure
|
|
1916
|
+
* as this span's terminal chunk — the turn ends exactly like a native
|
|
1917
|
+
* provider error.
|
|
1918
|
+
*/
|
|
1919
|
+
async *driveSpan(rec, from, cutOnTool) {
|
|
1920
|
+
const queue = new ChunkQueue();
|
|
1921
|
+
(async () => {
|
|
1922
|
+
const mapper = new EventMapper({
|
|
1923
|
+
runId: rec.runId,
|
|
1924
|
+
cutOnTool,
|
|
1925
|
+
initialSawText: rec.sawTextBefore(from)
|
|
1926
|
+
});
|
|
1927
|
+
let i = from;
|
|
1928
|
+
try {
|
|
1929
|
+
for await (const ev of rec.eventsFrom(from)) {
|
|
1930
|
+
for (const ch of mapper.map(ev, i)) queue.push(ch);
|
|
1931
|
+
i++;
|
|
1932
|
+
if (mapper.isFinished) break;
|
|
1933
|
+
}
|
|
1934
|
+
if (!mapper.isFinished) {
|
|
1935
|
+
const f = rec.failureInfo;
|
|
1936
|
+
if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message)) queue.push(ch);
|
|
1937
|
+
else for (const ch of mapper.emitFailure("error", Err.INVALID_OUTPUT, "agy stream ended without a result event")) queue.push(ch);
|
|
1938
|
+
}
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
|
|
1941
|
+
}
|
|
1942
|
+
queue.close();
|
|
1943
|
+
})();
|
|
1730
1944
|
yield* queue.drain();
|
|
1731
1945
|
}
|
|
1732
1946
|
};
|
|
1947
|
+
/**
|
|
1948
|
+
* Detect a continuation span: the request's LAST message is the tool result
|
|
1949
|
+
* of one of our mirrored agy tool calls. Its callId encodes the recording
|
|
1950
|
+
* run and the event index to resume after.
|
|
1951
|
+
*/
|
|
1952
|
+
function detectContinuation(messages) {
|
|
1953
|
+
const last = messages[messages.length - 1];
|
|
1954
|
+
if (last === void 0 || last.role !== "user") return null;
|
|
1955
|
+
const src = last.source;
|
|
1956
|
+
if (src === void 0 || src.kind !== "tool" || typeof src.callId !== "string") return null;
|
|
1957
|
+
return parseMirrorCallId(src.callId);
|
|
1958
|
+
}
|
|
1733
1959
|
//#endregion
|
|
1734
1960
|
//#region src/host/oneshot.ts
|
|
1735
1961
|
/** Per-file inline cap; larger files are truncated. */
|
|
@@ -2395,73 +2621,220 @@ function writeDoctorReport(deps) {
|
|
|
2395
2621
|
return file;
|
|
2396
2622
|
}
|
|
2397
2623
|
//#endregion
|
|
2398
|
-
//#region src/host/
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
"
|
|
2405
|
-
"
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
const o = args;
|
|
2411
|
-
for (const k of FILE_KEYS) {
|
|
2412
|
-
const v = o[k];
|
|
2413
|
-
if (typeof v === "string" && v !== "") return v;
|
|
2624
|
+
//#region src/host/mirror-tool.ts
|
|
2625
|
+
const MIRROR_TOOL_NAME = "agy_tool";
|
|
2626
|
+
/** agy serializes some tool args as a JSON string; presenters get an object. */
|
|
2627
|
+
function toolInput(args) {
|
|
2628
|
+
const raw = args.input;
|
|
2629
|
+
if (raw === void 0 || raw === null) return {};
|
|
2630
|
+
if (typeof raw === "object") return raw;
|
|
2631
|
+
if (typeof raw === "string") try {
|
|
2632
|
+
const parsed = JSON.parse(raw);
|
|
2633
|
+
return typeof parsed === "object" && parsed !== null ? parsed : { value: parsed };
|
|
2634
|
+
} catch {
|
|
2635
|
+
return { value: raw };
|
|
2414
2636
|
}
|
|
2415
|
-
return
|
|
2637
|
+
return { value: raw };
|
|
2416
2638
|
}
|
|
2417
|
-
function
|
|
2418
|
-
|
|
2419
|
-
return n.includes("write") || n.includes("edit") || n.includes("replace") || n.includes("str_replace");
|
|
2639
|
+
function asString(v) {
|
|
2640
|
+
return typeof v === "string" ? v : void 0;
|
|
2420
2641
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2642
|
+
/** Native pending-card projection for one mirrored agy tool call. */
|
|
2643
|
+
function presentMirrorCall(args) {
|
|
2644
|
+
const a = args;
|
|
2645
|
+
const name = typeof a?.tool === "string" ? a.tool : "";
|
|
2646
|
+
const input = toolInput(a);
|
|
2647
|
+
switch (name) {
|
|
2648
|
+
case "run_command":
|
|
2649
|
+
case "bash":
|
|
2650
|
+
case "execute_command": return {
|
|
2651
|
+
card: "terminal",
|
|
2652
|
+
title: asString(input.command) ?? asString(input.cmd) ?? JSON.stringify(input),
|
|
2653
|
+
...asString(input.description) !== void 0 ? { description: asString(input.description) } : {},
|
|
2654
|
+
...asString(input.cwd) !== void 0 ? { cwd: asString(input.cwd) } : {}
|
|
2655
|
+
};
|
|
2656
|
+
case "write_to_file":
|
|
2657
|
+
case "write_file":
|
|
2658
|
+
case "create_file": {
|
|
2659
|
+
const path = asString(input.path) ?? asString(input.file_path) ?? asString(input.filename) ?? "file";
|
|
2660
|
+
const content = asString(input.content) ?? "";
|
|
2661
|
+
return {
|
|
2662
|
+
card: "diff",
|
|
2663
|
+
title: "Write " + path,
|
|
2664
|
+
diffs: [{
|
|
2665
|
+
path,
|
|
2666
|
+
oldText: null,
|
|
2667
|
+
newText: content
|
|
2668
|
+
}],
|
|
2669
|
+
locations: [{ path }]
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
case "edit_file":
|
|
2673
|
+
case "replace_in_file":
|
|
2674
|
+
case "edit": {
|
|
2675
|
+
const path = asString(input.path) ?? asString(input.file_path) ?? "file";
|
|
2676
|
+
const newText = asString(input.new_string) ?? asString(input.newText) ?? asString(input.content) ?? "";
|
|
2677
|
+
const oldText = asString(input.old_string) ?? asString(input.oldText) ?? null;
|
|
2678
|
+
return {
|
|
2679
|
+
card: "diff",
|
|
2680
|
+
title: "Edit " + path,
|
|
2681
|
+
diffs: [{
|
|
2682
|
+
path,
|
|
2683
|
+
oldText,
|
|
2684
|
+
newText
|
|
2685
|
+
}],
|
|
2686
|
+
locations: [{ path }]
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
case "read_file":
|
|
2690
|
+
case "view_file":
|
|
2691
|
+
case "read":
|
|
2692
|
+
case "open_file": {
|
|
2693
|
+
const path = asString(input.path) ?? asString(input.file_path) ?? asString(input.filename) ?? "";
|
|
2694
|
+
const offset = typeof input.offset === "number" ? input.offset : void 0;
|
|
2695
|
+
return {
|
|
2696
|
+
card: "generic",
|
|
2697
|
+
title: "Read " + path,
|
|
2698
|
+
kind: "read",
|
|
2699
|
+
...path !== "" ? { locations: [{
|
|
2700
|
+
path,
|
|
2701
|
+
...offset !== void 0 ? { line: offset + 1 } : {}
|
|
2702
|
+
}] } : {}
|
|
2703
|
+
};
|
|
2428
2704
|
}
|
|
2705
|
+
case "find_by_name":
|
|
2706
|
+
case "glob":
|
|
2707
|
+
case "search_files":
|
|
2708
|
+
case "search":
|
|
2709
|
+
case "search_file_content":
|
|
2710
|
+
case "grep": {
|
|
2711
|
+
const q = asString(input.pattern) ?? asString(input.query) ?? asString(input.regex) ?? "";
|
|
2712
|
+
return {
|
|
2713
|
+
card: "generic",
|
|
2714
|
+
title: q !== "" ? "Search " + q : "Search",
|
|
2715
|
+
kind: "search"
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
case "list_dir":
|
|
2719
|
+
case "ls": {
|
|
2720
|
+
const path = asString(input.path) ?? asString(input.directory) ?? "";
|
|
2721
|
+
return {
|
|
2722
|
+
card: "generic",
|
|
2723
|
+
title: path !== "" ? "List " + path : "List directory"
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
case "delete_file":
|
|
2727
|
+
case "remove_file":
|
|
2728
|
+
case "rm": return {
|
|
2729
|
+
card: "generic",
|
|
2730
|
+
title: "Delete " + (asString(input.path) ?? asString(input.file_path) ?? ""),
|
|
2731
|
+
kind: "delete"
|
|
2732
|
+
};
|
|
2733
|
+
default: return {
|
|
2734
|
+
card: "generic",
|
|
2735
|
+
title: name !== "" ? name : "agy tool",
|
|
2736
|
+
rawInput: input
|
|
2737
|
+
};
|
|
2429
2738
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2739
|
+
}
|
|
2740
|
+
/** Native completed-card projection for one mirrored agy tool call. */
|
|
2741
|
+
function presentMirrorResult(args, result) {
|
|
2742
|
+
const a = args;
|
|
2743
|
+
const name = typeof a?.tool === "string" ? a.tool : "";
|
|
2744
|
+
const text = resultText$1(result.content);
|
|
2745
|
+
switch (name) {
|
|
2746
|
+
case "run_command":
|
|
2747
|
+
case "bash":
|
|
2748
|
+
case "execute_command": return {
|
|
2749
|
+
card: "terminal",
|
|
2750
|
+
output: text
|
|
2751
|
+
};
|
|
2752
|
+
case "write_to_file":
|
|
2753
|
+
case "write_file":
|
|
2754
|
+
case "create_file":
|
|
2755
|
+
case "edit_file":
|
|
2756
|
+
case "replace_in_file":
|
|
2757
|
+
case "edit": {
|
|
2758
|
+
const call = presentMirrorCall(args);
|
|
2759
|
+
return {
|
|
2760
|
+
card: "diff",
|
|
2761
|
+
diffs: call !== void 0 && call.card === "diff" ? call.diffs : []
|
|
2762
|
+
};
|
|
2436
2763
|
}
|
|
2437
|
-
|
|
2764
|
+
default: return {
|
|
2765
|
+
card: "generic",
|
|
2766
|
+
content: [{
|
|
2767
|
+
type: "text",
|
|
2768
|
+
text: clip(text, 4e3)
|
|
2769
|
+
}]
|
|
2770
|
+
};
|
|
2438
2771
|
}
|
|
2439
|
-
return parts.length === 0 ? null : parts.join("\n") + "\n";
|
|
2440
2772
|
}
|
|
2441
|
-
function
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
"diff",
|
|
2447
|
-
"HEAD",
|
|
2448
|
-
"--",
|
|
2449
|
-
file
|
|
2450
|
-
], {
|
|
2451
|
-
encoding: "utf8",
|
|
2452
|
-
timeout: 5e3,
|
|
2453
|
-
maxBuffer: 1e6,
|
|
2454
|
-
stdio: [
|
|
2455
|
-
"ignore",
|
|
2456
|
-
"pipe",
|
|
2457
|
-
"ignore"
|
|
2458
|
-
]
|
|
2459
|
-
});
|
|
2460
|
-
if (out.trim() === "") return null;
|
|
2461
|
-
return out.split("\n").slice(0, 100).join("\n");
|
|
2462
|
-
} catch {
|
|
2463
|
-
return null;
|
|
2773
|
+
function resultText$1(content) {
|
|
2774
|
+
const parts = [];
|
|
2775
|
+
for (const b of Array.isArray(content) ? content : []) {
|
|
2776
|
+
const blk = b;
|
|
2777
|
+
if (blk && blk.type === "text" && typeof blk.text === "string") parts.push(blk.text);
|
|
2464
2778
|
}
|
|
2779
|
+
return parts.join("\n");
|
|
2780
|
+
}
|
|
2781
|
+
function clip(s, max) {
|
|
2782
|
+
return s.length > max ? s.slice(0, max) + "… (+" + (s.length - max) + " chars)" : s;
|
|
2783
|
+
}
|
|
2784
|
+
function defineAgyMirrorTool(deps) {
|
|
2785
|
+
return defineTool({
|
|
2786
|
+
name: MIRROR_TOOL_NAME,
|
|
2787
|
+
description: "Internal to the dsh-agy-link bridge: replays one tool activity recorded from a Google Antigravity (agy) CLI run so it renders as a native tool card and rides the agent loop. Emitted automatically by the antigravity provider — do not call it directly.",
|
|
2788
|
+
parameters: {
|
|
2789
|
+
run: {
|
|
2790
|
+
type: "string",
|
|
2791
|
+
required: true,
|
|
2792
|
+
description: "Recording run id."
|
|
2793
|
+
},
|
|
2794
|
+
step: {
|
|
2795
|
+
type: "number",
|
|
2796
|
+
required: true,
|
|
2797
|
+
description: "Recorded event index of the tool step."
|
|
2798
|
+
},
|
|
2799
|
+
tool: {
|
|
2800
|
+
type: "string",
|
|
2801
|
+
required: true,
|
|
2802
|
+
description: "agy tool name that ran."
|
|
2803
|
+
},
|
|
2804
|
+
input: {
|
|
2805
|
+
type: "json",
|
|
2806
|
+
description: "agy tool arguments as recorded."
|
|
2807
|
+
}
|
|
2808
|
+
},
|
|
2809
|
+
output: {
|
|
2810
|
+
schema: { type: "string" },
|
|
2811
|
+
render: (_args, value) => [{
|
|
2812
|
+
type: "text",
|
|
2813
|
+
text: value
|
|
2814
|
+
}]
|
|
2815
|
+
},
|
|
2816
|
+
presentCall: (args) => presentMirrorCall(args),
|
|
2817
|
+
presentResult: (args, result) => presentMirrorResult(args, result),
|
|
2818
|
+
async execute(args, exec) {
|
|
2819
|
+
exec.signal;
|
|
2820
|
+
const runId = typeof args.run === "string" ? args.run : "";
|
|
2821
|
+
const step = typeof args.step === "number" ? args.step : -1;
|
|
2822
|
+
typeof args.tool === "string" && args.tool;
|
|
2823
|
+
const rec = deps.runs.get(runId);
|
|
2824
|
+
if (rec === void 0) throw new Error("agy_tool: no recorded agy run \"" + runId + "\" — this tool only replays bridge-recorded activity");
|
|
2825
|
+
const t = rec.toolEventAt(step);
|
|
2826
|
+
if (t === null) throw new Error("agy_tool: event " + step + " of run " + runId + " is not a completed tool step");
|
|
2827
|
+
if (t.error !== void 0) throw new Error("agy tool " + t.name + " failed: " + t.error);
|
|
2828
|
+
const out = t.output;
|
|
2829
|
+
if (out === void 0 || out === null) return "";
|
|
2830
|
+
if (typeof out === "string") return out;
|
|
2831
|
+
try {
|
|
2832
|
+
return JSON.stringify(out, null, 2);
|
|
2833
|
+
} catch {
|
|
2834
|
+
return String(out);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2465
2838
|
}
|
|
2466
2839
|
//#endregion
|
|
2467
2840
|
//#region src/host/sessions.ts
|
|
@@ -2753,6 +3126,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
2753
3126
|
};
|
|
2754
3127
|
}, getConfig().fallbackModels, 3e5);
|
|
2755
3128
|
const auth = new AuthHelper(bin);
|
|
3129
|
+
const runs = new RunRegistry();
|
|
2756
3130
|
const adapter = new AgyAdapter({
|
|
2757
3131
|
getConfig,
|
|
2758
3132
|
catalog,
|
|
@@ -2760,10 +3134,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
2760
3134
|
bin,
|
|
2761
3135
|
acquire: () => semaphore.acquire(),
|
|
2762
3136
|
log,
|
|
2763
|
-
|
|
2764
|
-
const ws = getConfig().workspaceRoot;
|
|
2765
|
-
return renderToolActivity(name, args, output, cwd ?? (ws !== "" ? ws : process.cwd()));
|
|
2766
|
-
},
|
|
3137
|
+
runs,
|
|
2767
3138
|
sessionCwd: (sessionId) => {
|
|
2768
3139
|
return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
|
|
2769
3140
|
},
|
|
@@ -2861,6 +3232,18 @@ function apply(ctx, entryConfig = {}) {
|
|
|
2861
3232
|
}
|
|
2862
3233
|
};
|
|
2863
3234
|
syncAskTool();
|
|
3235
|
+
const mirrorToolDispose = { current: null };
|
|
3236
|
+
const syncMirrorTool = () => {
|
|
3237
|
+
const want = getConfig().enabled && bin() !== null;
|
|
3238
|
+
if (want && mirrorToolDispose.current === null && toolsSvc) {
|
|
3239
|
+
const reg = toolsSvc.register(defineAgyMirrorTool({ runs }));
|
|
3240
|
+
mirrorToolDispose.current = typeof reg === "function" ? reg : null;
|
|
3241
|
+
} else if (!want && mirrorToolDispose.current !== null) {
|
|
3242
|
+
mirrorToolDispose.current();
|
|
3243
|
+
mirrorToolDispose.current = null;
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
syncMirrorTool();
|
|
2864
3247
|
const registerRoutes = (webServer) => {
|
|
2865
3248
|
const disposers = [];
|
|
2866
3249
|
const reg = (route) => {
|
|
@@ -2979,6 +3362,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
2979
3362
|
}
|
|
2980
3363
|
setOverride(key, body.value);
|
|
2981
3364
|
syncAskTool();
|
|
3365
|
+
syncMirrorTool();
|
|
2982
3366
|
sendJson(res, 200, {
|
|
2983
3367
|
ok: true,
|
|
2984
3368
|
key,
|
|
@@ -3075,6 +3459,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
3075
3459
|
ctx.effect(() => {
|
|
3076
3460
|
auth.dispose();
|
|
3077
3461
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
3462
|
+
if (mirrorToolDispose.current !== null) mirrorToolDispose.current();
|
|
3078
3463
|
bridgeState.restore?.();
|
|
3079
3464
|
bridgeState.bridge?.close();
|
|
3080
3465
|
return () => void 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|