specrails-desktop 2.6.0 → 2.8.0
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/client/dist/assets/{ActivityFeedPage-CoWwVcty.js → ActivityFeedPage-LKqd18-G.js} +1 -1
- package/client/dist/assets/{AgentsPage-CgPvynWc.js → AgentsPage-Cb-b-6Ot.js} +1 -1
- package/client/dist/assets/AnalyticsPage-HVxQQ1wy.js +1 -0
- package/client/dist/assets/{BarChart-BKXQPcoW.js → BarChart-BOyHB0dw.js} +1 -1
- package/client/dist/assets/{CodePage-CYhXRKiI.js → CodePage-DnOnwKGB.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-CBfPCT3q.js → DesktopAnalyticsPage-D2auU39x.js} +1 -1
- package/client/dist/assets/{DocsDialog-uRTBV-3T.js → DocsDialog-CTuDX3GK.js} +1 -1
- package/client/dist/assets/{DocsPage-gH0Lc54I.js → DocsPage-DRyMmu0Z.js} +1 -1
- package/client/dist/assets/{ExportDropdown-DAp7zWib.js → ExportDropdown-DO-GGiMh.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-D40Si_7s.js → IntegrationsPage-BhbO4jFT.js} +1 -1
- package/client/dist/assets/{JobDetailPage-DSxAvB1n.js → JobDetailPage-DJooEg1s.js} +1 -1
- package/client/dist/assets/{JobsPage-ZMBc1BHE.js → JobsPage-BbaC-YOg.js} +1 -1
- package/client/dist/assets/{addspec-DeDOztDr.js → addspec-B-BKlvDj.js} +1 -1
- package/client/dist/assets/{addspec-v8j6A7CD.js → addspec-BErjOdNK.js} +1 -1
- package/client/dist/assets/{addspec-B1FTtI2a.js → addspec-CIGb34PS.js} +1 -1
- package/client/dist/assets/{addspec-GWm4ffKl.js → addspec-C_3NBarY.js} +1 -1
- package/client/dist/assets/{addspec-Dw-0Dg-4.js → addspec-DDvvnE6N.js} +1 -1
- package/client/dist/assets/{addspec-DpRgmfmx.js → addspec-RuL8Zd7w.js} +1 -1
- package/client/dist/assets/{addspec-rp496P_F.js → addspec-rmhOaH7N.js} +1 -1
- package/client/dist/assets/{addspec-BCT9vm_c.js → addspec-xjDbYZWL.js} +1 -1
- package/client/dist/assets/{dist-js-CKqmDyXR.js → dist-js-CiIVMsx3.js} +1 -1
- package/client/dist/assets/{dist-js-bTZuok_W.js → dist-js-Xc2lRKp2.js} +1 -1
- package/client/dist/assets/{index-B9IKK_QQ.js → index-DK214dak.js} +34 -34
- package/client/dist/assets/index-DgKfQFcf.css +2 -0
- package/client/dist/assets/{lib-B5mjOeEi.js → lib-Bo5s6xpe.js} +1 -1
- package/client/dist/assets/{useProjectCache-Cf83MBQh.js → useProjectCache-DVNypkmR.js} +1 -1
- package/client/dist/index.html +3 -3
- package/docs/adding-a-provider.md +107 -0
- package/docs/agy-cli-provider-study.md +179 -0
- package/docs/gemini-cli-provider-study.md +301 -0
- package/docs/gemini-core-support-evaluation.md +160 -0
- package/docs/gemini.md +106 -0
- package/package.json +1 -1
- package/server/dist/chat-manager.js +1 -1
- package/server/dist/core-package.js +6 -1
- package/server/dist/desktop-router.js +29 -8
- package/server/dist/explore-cwd-manager.js +1 -1
- package/server/dist/mobile/mobile-datachannel.js +65 -4
- package/server/dist/pricing.js +13 -0
- package/server/dist/project-router-tickets.js +63 -18
- package/server/dist/providers/gemini-adapter.js +238 -0
- package/server/dist/providers/gemini-agent-ack.js +65 -0
- package/server/dist/providers/index.js +4 -1
- package/server/dist/queue-manager.js +13 -0
- package/server/dist/setup-manager.js +13 -7
- package/server/dist/setup-prerequisites.js +4 -0
- package/server/dist/spec-models.js +17 -3
- package/server/dist/util/cli-prompt.js +17 -1
- package/server/dist/util/stream-display.js +18 -3
- package/client/dist/assets/AnalyticsPage-ioz3Ub2D.js +0 -1
- package/client/dist/assets/index-BqAXaTbC.css +0 -2
|
@@ -8,7 +8,12 @@ exports.CORE_PACKAGE_SPEC = void 0;
|
|
|
8
8
|
* minute it is published — adopting a new major is a deliberate one-line
|
|
9
9
|
* bump here, shipped through the app's own release pipeline.
|
|
10
10
|
*
|
|
11
|
+
* Floor bumped to 4.8.0 — the version that ships the Gemini provider target
|
|
12
|
+
* (`.gemini/` commands + agents). It also changes the `npx` package-spec string,
|
|
13
|
+
* which invalidates any stale `_npx` exec cache that npx would otherwise reuse
|
|
14
|
+
* for the old `^4.6.0` range (so users actually resolve the newer core).
|
|
15
|
+
*
|
|
11
16
|
* `SPECRAILS_CORE_BIN` remains the escape hatch for local/linked builds
|
|
12
17
|
* (see getCoreCommand in setup-manager.ts).
|
|
13
18
|
*/
|
|
14
|
-
exports.CORE_PACKAGE_SPEC = 'specrails-core@^4.
|
|
19
|
+
exports.CORE_PACKAGE_SPEC = 'specrails-core@^4.8.0';
|
|
@@ -29,6 +29,16 @@ function isCodexBetaDisabled() {
|
|
|
29
29
|
const v = process.env.SPECRAILS_CODEX_BETA ?? process.env.SPECRAILS_HUB_CODEX_BETA;
|
|
30
30
|
return v === '0';
|
|
31
31
|
}
|
|
32
|
+
// Gemini is enabled by DEFAULT now that its stream-json schema and the full rails
|
|
33
|
+
// pipeline (architect→developer→reviewer delegation, headless agent loading, the
|
|
34
|
+
// MAX_TURNS resume + reviewer gate) are validated against the live binary
|
|
35
|
+
// (0.46/0.47). Emergency rollback: SPECRAILS_GEMINI_BETA=0 forces it back to
|
|
36
|
+
// "unavailable" without redeploying — parity with codex's SPECRAILS_CODEX_BETA.
|
|
37
|
+
// The adapter is always registered (pricing/getAdapter work); only project
|
|
38
|
+
// selection is gated.
|
|
39
|
+
function isGeminiBetaDisabled() {
|
|
40
|
+
return process.env.SPECRAILS_GEMINI_BETA === '0';
|
|
41
|
+
}
|
|
32
42
|
// Theme allow-list. Mirror of THEME_IDS in `client/src/lib/themes.ts` —
|
|
33
43
|
// kept duplicated to avoid pulling client code into the server bundle.
|
|
34
44
|
const THEME_ID_ALLOWLIST = new Set(['dracula', 'aurora-light', 'obsidian-dark', 'matrix', 'specrails']);
|
|
@@ -144,14 +154,19 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
144
154
|
const providers = (0, core_compat_1.detectAvailableCLIs)();
|
|
145
155
|
// tiers: quick install is always available (app-driven config); full requires an AI CLI
|
|
146
156
|
const tiers = ['quick'];
|
|
147
|
-
if (
|
|
157
|
+
if (Object.values(providers).some(Boolean))
|
|
148
158
|
tiers.push('full');
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// Return the full detected map (registry-driven) so a newly-registered
|
|
160
|
+
// provider surfaces here with no edit. Apply per-provider beta gates: codex
|
|
161
|
+
// is forced unavailable when SPECRAILS_CODEX_BETA=0 (emergency rollback).
|
|
162
|
+
const gated = { ...providers };
|
|
163
|
+
if (isCodexBetaDisabled())
|
|
164
|
+
gated.codex = false;
|
|
165
|
+
// Gemini: enabled by default; forced unavailable only when
|
|
166
|
+
// SPECRAILS_GEMINI_BETA=0 (emergency rollback, parity with codex).
|
|
167
|
+
if (isGeminiBetaDisabled())
|
|
168
|
+
gated.gemini = false;
|
|
169
|
+
res.json({ ...gated, tiers });
|
|
155
170
|
});
|
|
156
171
|
router.get('/setup-prerequisites', (req, res) => {
|
|
157
172
|
const status = (0, setup_prerequisites_1.getSetupPrerequisitesStatus)();
|
|
@@ -218,6 +233,12 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
218
233
|
});
|
|
219
234
|
return;
|
|
220
235
|
}
|
|
236
|
+
if (providers.includes('gemini') && isGeminiBetaDisabled()) {
|
|
237
|
+
res.status(400).json({
|
|
238
|
+
error: 'Gemini provider is currently disabled (SPECRAILS_GEMINI_BETA=0). Unset or set to 1 to enable.',
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
221
242
|
const resolvedPath = path_1.default.resolve(projectPath);
|
|
222
243
|
// Validate path exists
|
|
223
244
|
if (!fs_1.default.existsSync(resolvedPath)) {
|
|
@@ -243,7 +264,7 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
243
264
|
name: derivedName,
|
|
244
265
|
path: canonicalPath,
|
|
245
266
|
provider: providers[0],
|
|
246
|
-
providers
|
|
267
|
+
providers,
|
|
247
268
|
});
|
|
248
269
|
broadcast({
|
|
249
270
|
type: 'desktop.project_added',
|
|
@@ -127,7 +127,7 @@ function ensureExploreCwd(input, baseDir) {
|
|
|
127
127
|
// — provider is immutable post-creation but the lifecycle code MUST handle
|
|
128
128
|
// the edge per spec), remove any stale instructions file authored by a
|
|
129
129
|
// different provider so the explore-cwd doesn't carry both.
|
|
130
|
-
const STALE_INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md'];
|
|
130
|
+
const STALE_INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
|
|
131
131
|
for (const stale of STALE_INSTRUCTION_FILES) {
|
|
132
132
|
if (stale === adapter.instructionsFilename)
|
|
133
133
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DataChannelPeer = exports.DataChannelSocket = void 0;
|
|
3
|
+
exports.DataChannelPeer = exports.DataChannelSocket = exports.ChunkReassembler = void 0;
|
|
4
4
|
// WebRTC DataChannel transport for the web companion (serverless double-QR
|
|
5
5
|
// pairing). Instead of a LAN WSS gateway, the companion connects peer-to-peer
|
|
6
6
|
// over a WebRTC DataChannel; this module bridges that channel onto the EXISTING
|
|
@@ -15,6 +15,63 @@ exports.DataChannelPeer = exports.DataChannelSocket = void 0;
|
|
|
15
15
|
// without a real WebRTC stack, the database, or the loopback HTTP forward.
|
|
16
16
|
const WS_OPEN = 1;
|
|
17
17
|
const WS_CLOSED = 3;
|
|
18
|
+
// WebRTC DataChannels carry an SCTP per-message size cap (werift advertises a
|
|
19
|
+
// modest maxMessageSize; oversized sends throw). A large rpc_result — e.g. a
|
|
20
|
+
// JIRA-backed project's full ticket list — easily exceeds it, and the throw was
|
|
21
|
+
// swallowed silently, so the companion just timed out and rendered "0 tickets".
|
|
22
|
+
// We split any oversized frame into ordered `__chunk` envelopes and reassemble
|
|
23
|
+
// them on the far side. The channel is ordered+reliable, so chunks arrive in
|
|
24
|
+
// order; `g` (group id) keeps concurrent large messages from interleaving.
|
|
25
|
+
const CHUNK_SIZE = 16000;
|
|
26
|
+
let _chunkGroup = 0;
|
|
27
|
+
/** Send `data` over `ch`, splitting into `__chunk` frames when it exceeds the
|
|
28
|
+
* safe per-message size. Small frames go out verbatim (no envelope overhead). */
|
|
29
|
+
function framedSend(ch, data) {
|
|
30
|
+
if (data.length <= CHUNK_SIZE) {
|
|
31
|
+
ch.send(data);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const g = _chunkGroup++;
|
|
35
|
+
const n = Math.ceil(data.length / CHUNK_SIZE);
|
|
36
|
+
for (let i = 0; i < n; i++) {
|
|
37
|
+
const d = data.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
|
|
38
|
+
ch.send(JSON.stringify({ type: '__chunk', g, i, n, d }));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Buffers inbound `__chunk` frames and yields the reassembled payload once a
|
|
42
|
+
* group is complete. Non-chunk frames pass straight through. */
|
|
43
|
+
class ChunkReassembler {
|
|
44
|
+
_groups = new Map();
|
|
45
|
+
/** Returns the string to handle (the frame itself, or a completed
|
|
46
|
+
* reassembly), or null when more chunks of the group are still pending. */
|
|
47
|
+
push(data) {
|
|
48
|
+
if (!data.startsWith('{"type":"__chunk"'))
|
|
49
|
+
return data;
|
|
50
|
+
let p;
|
|
51
|
+
try {
|
|
52
|
+
p = JSON.parse(data);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
if (!p || p.n == null)
|
|
58
|
+
return data;
|
|
59
|
+
let grp = this._groups.get(p.g);
|
|
60
|
+
if (!grp) {
|
|
61
|
+
grp = { n: p.n, parts: new Array(p.n).fill(''), got: 0 };
|
|
62
|
+
this._groups.set(p.g, grp);
|
|
63
|
+
}
|
|
64
|
+
if (grp.parts[p.i] === '' && grp.got < grp.n) {
|
|
65
|
+
grp.parts[p.i] = p.d;
|
|
66
|
+
grp.got++;
|
|
67
|
+
}
|
|
68
|
+
if (grp.got < grp.n)
|
|
69
|
+
return null;
|
|
70
|
+
this._groups.delete(p.g);
|
|
71
|
+
return grp.parts.join('');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.ChunkReassembler = ChunkReassembler;
|
|
18
75
|
/** Presents a DataChannel to MobileWsBridge as if it were a ws.WebSocket, so the
|
|
19
76
|
* existing fan-out (redaction, per-subscription filtering, log batching) is
|
|
20
77
|
* reused unchanged. WebRTC has no ping/pong frames, so ping() reports liveness
|
|
@@ -30,7 +87,7 @@ class DataChannelSocket {
|
|
|
30
87
|
}
|
|
31
88
|
send(data) {
|
|
32
89
|
try {
|
|
33
|
-
this._ch
|
|
90
|
+
framedSend(this._ch, data);
|
|
34
91
|
}
|
|
35
92
|
catch {
|
|
36
93
|
/* channel went away mid-send — onClose will reap it */
|
|
@@ -88,12 +145,16 @@ class DataChannelPeer {
|
|
|
88
145
|
_sock;
|
|
89
146
|
_authed = false;
|
|
90
147
|
_token = '';
|
|
148
|
+
_reasm = new ChunkReassembler();
|
|
91
149
|
constructor(_ch, _deps) {
|
|
92
150
|
this._ch = _ch;
|
|
93
151
|
this._deps = _deps;
|
|
94
152
|
this._sock = new DataChannelSocket(_ch);
|
|
95
153
|
_ch.onMessage((data) => {
|
|
96
|
-
|
|
154
|
+
const full = this._reasm.push(data);
|
|
155
|
+
if (full === null)
|
|
156
|
+
return;
|
|
157
|
+
void this._onMessage(full);
|
|
97
158
|
});
|
|
98
159
|
_ch.onClose(() => this._sock.closed());
|
|
99
160
|
}
|
|
@@ -102,7 +163,7 @@ class DataChannelPeer {
|
|
|
102
163
|
}
|
|
103
164
|
_send(obj) {
|
|
104
165
|
try {
|
|
105
|
-
this._ch
|
|
166
|
+
framedSend(this._ch, JSON.stringify(obj));
|
|
106
167
|
}
|
|
107
168
|
catch {
|
|
108
169
|
/* ignore */
|
package/server/dist/pricing.js
CHANGED
|
@@ -32,6 +32,19 @@ exports.PRICING = {
|
|
|
32
32
|
'codex:gpt-5.4': { inputPer1M: 2.50, outputPer1M: 10.00, cacheReadPer1M: 0.25, lastReviewedAt: '2026-05-17' },
|
|
33
33
|
'codex:gpt-5.4-mini': { inputPer1M: 0.25, outputPer1M: 2.00, cacheReadPer1M: 0.025, lastReviewedAt: '2026-05-17' },
|
|
34
34
|
'codex:gpt-5.3-codex': { inputPer1M: 1.50, outputPer1M: 6.00, cacheReadPer1M: 0.15, lastReviewedAt: '2026-05-17' },
|
|
35
|
+
// Gemini (Google). Reference: https://ai.google.dev/gemini-api/docs/pricing
|
|
36
|
+
// Standard paid tier, <=200k-context prices (Gemini Pro is context-tiered;
|
|
37
|
+
// prompts >200k are under-estimated in v1 — see docs/gemini-cli-provider-study.md §2).
|
|
38
|
+
// Current catalog (June 2026): 3.5-flash flagship default, 3.1-pro-preview max,
|
|
39
|
+
// 3.1-flash-lite budget, 2.5-flash-lite cheapest. Older 2.5-pro/2.5-flash rows
|
|
40
|
+
// are retained so historic invocations still price.
|
|
41
|
+
'gemini:gemini-3.5-flash': { inputPer1M: 1.50, outputPer1M: 9.00, cacheReadPer1M: 0.15, lastReviewedAt: '2026-06-17' },
|
|
42
|
+
'gemini:gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00, cacheReadPer1M: 0.20, lastReviewedAt: '2026-06-17' },
|
|
43
|
+
'gemini:gemini-3.1-flash-lite': { inputPer1M: 0.25, outputPer1M: 1.50, cacheReadPer1M: 0.025, lastReviewedAt: '2026-06-17' },
|
|
44
|
+
'gemini:gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00, cacheReadPer1M: 0.05, lastReviewedAt: '2026-06-17' },
|
|
45
|
+
'gemini:gemini-2.5-pro': { inputPer1M: 1.25, outputPer1M: 10.00, cacheReadPer1M: 0.125, lastReviewedAt: '2026-06-17' },
|
|
46
|
+
'gemini:gemini-2.5-flash': { inputPer1M: 0.30, outputPer1M: 2.50, cacheReadPer1M: 0.03, lastReviewedAt: '2026-06-17' },
|
|
47
|
+
'gemini:gemini-2.5-flash-lite': { inputPer1M: 0.10, outputPer1M: 0.40, cacheReadPer1M: 0.01, lastReviewedAt: '2026-06-17' },
|
|
35
48
|
};
|
|
36
49
|
/**
|
|
37
50
|
* Estimate cost in USD from a token usage breakdown. Returns `null` when:
|
|
@@ -393,24 +393,44 @@ function registerTicketsRoutes(deps) {
|
|
|
393
393
|
broadcast(msg);
|
|
394
394
|
return;
|
|
395
395
|
}
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
if (provider === 'claude') {
|
|
397
|
+
// Claude path.
|
|
398
|
+
if (parsed.type === 'result') {
|
|
399
|
+
lastResultEvent = parsed;
|
|
400
|
+
}
|
|
401
|
+
if (parsed.type === 'assistant') {
|
|
402
|
+
const msg = parsed.message;
|
|
403
|
+
const texts = (msg?.content ?? [])
|
|
404
|
+
.filter((c) => c.type === 'text')
|
|
405
|
+
.map((c) => c.text ?? '');
|
|
406
|
+
const newText = texts.join('');
|
|
407
|
+
if (newText) {
|
|
408
|
+
buffer += newText;
|
|
409
|
+
const wsMsg = {
|
|
410
|
+
type: 'spec_gen_stream', projectId, requestId,
|
|
411
|
+
delta: newText, timestamp: new Date().toISOString(),
|
|
412
|
+
};
|
|
413
|
+
broadcast(wsMsg);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Adapter-driven path (gemini + future providers): the adapter already
|
|
419
|
+
// parsed this line into a structured AdapterEvent above (`adapterEv`).
|
|
420
|
+
// Accumulate assistant text deltas into the buffer and capture the result
|
|
421
|
+
// event for usage extraction — mirrors the ai-edit endpoint's else branch.
|
|
422
|
+
// Without this, gemini (which emits `type:'message'`, not `type:'assistant'`)
|
|
423
|
+
// would never accumulate text → empty buffer → "Empty response from AI".
|
|
424
|
+
if (adapterEv?.kind === 'result') {
|
|
398
425
|
lastResultEvent = parsed;
|
|
399
426
|
}
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
buffer += newText;
|
|
408
|
-
const wsMsg = {
|
|
409
|
-
type: 'spec_gen_stream', projectId, requestId,
|
|
410
|
-
delta: newText, timestamp: new Date().toISOString(),
|
|
411
|
-
};
|
|
412
|
-
broadcast(wsMsg);
|
|
413
|
-
}
|
|
427
|
+
if (adapterEv?.kind === 'text-delta' && adapterEv.text) {
|
|
428
|
+
buffer += adapterEv.text;
|
|
429
|
+
const wsMsg = {
|
|
430
|
+
type: 'spec_gen_stream', projectId, requestId,
|
|
431
|
+
delta: adapterEv.text, timestamp: new Date().toISOString(),
|
|
432
|
+
};
|
|
433
|
+
broadcast(wsMsg);
|
|
414
434
|
}
|
|
415
435
|
});
|
|
416
436
|
child.on('close', async (code) => {
|
|
@@ -1476,7 +1496,7 @@ function registerTicketsRoutes(deps) {
|
|
|
1476
1496
|
// Use gpt-5.5 (default for Codex per CODEX_MODELS/PRESET_DEFAULTS in ModelSelector); never hardcode o4-mini
|
|
1477
1497
|
args = ['exec', `${systemPrompt}\n\n${userPrompt}`, '--model', 'gpt-5.5'];
|
|
1478
1498
|
}
|
|
1479
|
-
else {
|
|
1499
|
+
else if (provider === 'claude') {
|
|
1480
1500
|
binary = 'claude';
|
|
1481
1501
|
args = [
|
|
1482
1502
|
'--dangerously-skip-permissions',
|
|
@@ -1489,6 +1509,18 @@ function registerTicketsRoutes(deps) {
|
|
|
1489
1509
|
'-p', userPrompt,
|
|
1490
1510
|
];
|
|
1491
1511
|
}
|
|
1512
|
+
else {
|
|
1513
|
+
// Adapter-driven path (gemini + any future provider): binary, argv, and
|
|
1514
|
+
// stream parsing all come from the registered adapter — no per-provider
|
|
1515
|
+
// hardcoding. The claude/codex shapes above are kept byte-identical.
|
|
1516
|
+
const adapter = (0, providers_1.getAdapter)(provider);
|
|
1517
|
+
binary = adapter.binary;
|
|
1518
|
+
args = adapter.buildArgs('agent-refine', {
|
|
1519
|
+
prompt: userPrompt,
|
|
1520
|
+
systemPrompt,
|
|
1521
|
+
model: adapter.defaultModel(),
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1492
1524
|
// spawnAiCli reroutes multi-line argv values through stdin on Windows.
|
|
1493
1525
|
console.log(`[project-router] ai-edit spawn: ${binary} (cwd=${project.path}, requestId=${requestId})`);
|
|
1494
1526
|
const child = (0, cli_prompt_1.spawnAiCli)(binary, args, {
|
|
@@ -1534,7 +1566,7 @@ function registerTicketsRoutes(deps) {
|
|
|
1534
1566
|
broadcast(msg);
|
|
1535
1567
|
}
|
|
1536
1568
|
}
|
|
1537
|
-
else {
|
|
1569
|
+
else if (provider === 'claude') {
|
|
1538
1570
|
let parsed = null;
|
|
1539
1571
|
try {
|
|
1540
1572
|
parsed = JSON.parse(line);
|
|
@@ -1558,6 +1590,19 @@ function registerTicketsRoutes(deps) {
|
|
|
1558
1590
|
}
|
|
1559
1591
|
}
|
|
1560
1592
|
}
|
|
1593
|
+
else {
|
|
1594
|
+
// Adapter-driven parse (gemini + future providers): uniform AdapterEvent
|
|
1595
|
+
// stream — accumulate assistant text deltas.
|
|
1596
|
+
const ev = (0, providers_1.getAdapter)(provider).parseStreamLine(line);
|
|
1597
|
+
if (ev?.kind === 'text-delta' && ev.text) {
|
|
1598
|
+
buffer += ev.text;
|
|
1599
|
+
const wsMsg = {
|
|
1600
|
+
type: 'ticket_ai_edit_stream', projectId, ticketId: Number(ticketId),
|
|
1601
|
+
requestId, delta: ev.text, timestamp: new Date().toISOString(),
|
|
1602
|
+
};
|
|
1603
|
+
broadcast(wsMsg);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1561
1606
|
});
|
|
1562
1607
|
child.on('close', (code) => {
|
|
1563
1608
|
_aiEditProcesses.delete(requestId);
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Gemini CLI (Google) adapter for gemini-cli 0.11.0+.
|
|
3
|
+
//
|
|
4
|
+
// Stream format (gemini `-p "<prompt>" --output-format stream-json`), one minified
|
|
5
|
+
// JSON object per line — pinned to gemini-cli >= 0.11 and locked by the fixtures
|
|
6
|
+
// under __fixtures__/gemini-*.ndjson:
|
|
7
|
+
// {"type":"init","session_id":"<UUID>","model":"gemini-3.5-flash"}
|
|
8
|
+
// {"type":"message","role":"assistant","content":"...","delta":true}
|
|
9
|
+
// {"type":"tool_use","tool_name":"read_file","tool_id":"t0","parameters":{...}}
|
|
10
|
+
// {"type":"tool_result","tool_id":"t0","status":"ok","output":"..."}
|
|
11
|
+
// {"type":"result","status":"success","stats":{"input_tokens":N,"output_tokens":N,
|
|
12
|
+
// "cached":N,"total_tokens":N,"duration_ms":N}}
|
|
13
|
+
//
|
|
14
|
+
// Differences vs codex: Gemini reports tokens but NOT cost, so `nativeCostUsd`
|
|
15
|
+
// is false and cost is estimated downstream via server/pricing.ts. Gemini DOES
|
|
16
|
+
// emit OTLP natively via `GEMINI_TELEMETRY_*` env (set by QueueManager), so
|
|
17
|
+
// `nativeOtelEnv` is true — no synthetic bridge. There is no `--system-prompt`
|
|
18
|
+
// flag (the CLI uses the `GEMINI_SYSTEM_MD` env), so `systemPromptArg` is false
|
|
19
|
+
// and the system prompt is folded for non-Explore actions; Explore turns trust
|
|
20
|
+
// the app-managed GEMINI.md in explore-cwd, exactly like codex/AGENTS.md.
|
|
21
|
+
//
|
|
22
|
+
// NOTE (pre-1.0): gemini-cli changes weekly. The exact argv/event field names
|
|
23
|
+
// here follow the documented 0.11 contract; they are guarded by the beta flag
|
|
24
|
+
// (SPECRAILS_GEMINI_BETA) and MUST be re-validated against a real binary via the
|
|
25
|
+
// fixtures before the gate is removed. Parsing is tolerant of missing fields.
|
|
26
|
+
//
|
|
27
|
+
// Spec: openspec/specs/multi-provider-architecture/spec.md
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports._GEMINI_MIN_VERSION = exports.geminiAdapter = void 0;
|
|
30
|
+
exports._compareSemver = compareSemver;
|
|
31
|
+
const child_process_1 = require("child_process");
|
|
32
|
+
const gemini_agent_ack_1 = require("./gemini-agent-ack");
|
|
33
|
+
const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
34
|
+
// Floor where `--output-format stream-json` + headless `--resume` are available.
|
|
35
|
+
const GEMINI_MIN_VERSION = '0.11.0';
|
|
36
|
+
exports._GEMINI_MIN_VERSION = GEMINI_MIN_VERSION;
|
|
37
|
+
// Curated GA-id catalog (see docs/gemini-cli-provider-study.md §2). Preview ids
|
|
38
|
+
// rotate, so we pin concrete ids that pricing.ts has rows for.
|
|
39
|
+
const GEMINI_MODELS = [
|
|
40
|
+
{ value: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash', default: true },
|
|
41
|
+
{ value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro (preview)' },
|
|
42
|
+
{ value: 'gemini-3.1-flash-lite', label: 'Gemini 3.1 Flash Lite' },
|
|
43
|
+
{ value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
|
|
44
|
+
];
|
|
45
|
+
const STREAM_JSON_FLAGS = ['--output-format', 'stream-json'];
|
|
46
|
+
// Auto-approve tool calls so headless turns never block on an approval prompt.
|
|
47
|
+
// Parity with codex's workspace-write/full-access sandbox; the non-destructive
|
|
48
|
+
// stance for Explore is carried by GEMINI.md + the system prompt, not a sandbox.
|
|
49
|
+
const YOLO_FLAG = '--yolo';
|
|
50
|
+
/** Fold system prompt into the user prompt for providers without --system-prompt. */
|
|
51
|
+
function fold(systemPrompt, prompt) {
|
|
52
|
+
if (!systemPrompt)
|
|
53
|
+
return prompt;
|
|
54
|
+
return `${systemPrompt}\n\n---\n\n${prompt}`;
|
|
55
|
+
}
|
|
56
|
+
function buildGeminiArgs(action, opts) {
|
|
57
|
+
const model = ['--model', opts.model];
|
|
58
|
+
switch (action) {
|
|
59
|
+
case 'chat-turn': {
|
|
60
|
+
// Explore turns spawn from the app-managed explore-cwd, which ships a
|
|
61
|
+
// GEMINI.md with the Explore stance. Folding the long system prompt into a
|
|
62
|
+
// short user message ("quiero hacer un tetris") makes the model answer the
|
|
63
|
+
// system instructions instead of the user — so pass user text only and
|
|
64
|
+
// trust GEMINI.md, exactly like the codex adapter trusts AGENTS.md.
|
|
65
|
+
return ['-p', opts.prompt, ...model, ...STREAM_JSON_FLAGS, YOLO_FLAG, ...(opts.extraArgs ?? [])];
|
|
66
|
+
}
|
|
67
|
+
case 'chat-resume': {
|
|
68
|
+
if (!opts.sessionId)
|
|
69
|
+
throw new Error(`${action} requires sessionId`);
|
|
70
|
+
return ['-p', opts.prompt, ...model, ...STREAM_JSON_FLAGS, '--resume', opts.sessionId, YOLO_FLAG, ...(opts.extraArgs ?? [])];
|
|
71
|
+
}
|
|
72
|
+
case 'chat-stream': {
|
|
73
|
+
// Gemini has no persistent-stdin multi-turn transport; the Explore
|
|
74
|
+
// fast-path gates on capabilities.persistentStdin (omitted), so this is
|
|
75
|
+
// never reached. Throw defensively rather than emit a broken argv.
|
|
76
|
+
throw new Error('gemini does not support persistent stdin streaming (chat-stream)');
|
|
77
|
+
}
|
|
78
|
+
case 'spec-gen':
|
|
79
|
+
case 'agent-refine':
|
|
80
|
+
case 'auto-title':
|
|
81
|
+
case 'setup-enrich':
|
|
82
|
+
case 'rail-job': {
|
|
83
|
+
return ['-p', fold(opts.systemPrompt, opts.prompt), ...model, ...STREAM_JSON_FLAGS, YOLO_FLAG, ...(opts.extraArgs ?? [])];
|
|
84
|
+
}
|
|
85
|
+
case 'setup-enrich-resume': {
|
|
86
|
+
if (!opts.sessionId)
|
|
87
|
+
throw new Error(`${action} requires sessionId`);
|
|
88
|
+
return ['-p', fold(opts.systemPrompt, opts.prompt), ...model, ...STREAM_JSON_FLAGS, '--resume', opts.sessionId, YOLO_FLAG, ...(opts.extraArgs ?? [])];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function parseGeminiStreamLine(line) {
|
|
93
|
+
if (line.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(line);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const type = parsed.type;
|
|
103
|
+
if (!type)
|
|
104
|
+
return { kind: 'other', type: '<missing>', raw: parsed };
|
|
105
|
+
if (type === 'init') {
|
|
106
|
+
const sid = parsed.session_id;
|
|
107
|
+
if (sid)
|
|
108
|
+
return { kind: 'session-started', sessionId: sid };
|
|
109
|
+
return { kind: 'other', type, raw: parsed };
|
|
110
|
+
}
|
|
111
|
+
if (type === 'result') {
|
|
112
|
+
return { kind: 'result', payload: parsed };
|
|
113
|
+
}
|
|
114
|
+
if (type === 'message') {
|
|
115
|
+
// Only assistant text is a delta; the user echo (role 'user') is ignored.
|
|
116
|
+
const role = parsed.role;
|
|
117
|
+
if (role === 'assistant') {
|
|
118
|
+
const text = parsed.content ?? '';
|
|
119
|
+
if (text)
|
|
120
|
+
return { kind: 'text-delta', text };
|
|
121
|
+
}
|
|
122
|
+
return { kind: 'other', type, raw: parsed };
|
|
123
|
+
}
|
|
124
|
+
if (type === 'tool_use') {
|
|
125
|
+
const name = parsed.tool_name ?? '<unnamed>';
|
|
126
|
+
const params = parsed.parameters;
|
|
127
|
+
const inputPreview = params !== undefined ? JSON.stringify(params).slice(0, 200) : '';
|
|
128
|
+
return { kind: 'tool-use', name, inputPreview };
|
|
129
|
+
}
|
|
130
|
+
// tool_result, error, and any unknown event type carry no normalised meaning.
|
|
131
|
+
return { kind: 'other', type, raw: parsed };
|
|
132
|
+
}
|
|
133
|
+
function extractGeminiResult(events) {
|
|
134
|
+
let sessionId;
|
|
135
|
+
let resultPayload = null;
|
|
136
|
+
let turnCount = 0;
|
|
137
|
+
for (const ev of events) {
|
|
138
|
+
if (ev.kind === 'session-started')
|
|
139
|
+
sessionId = ev.sessionId;
|
|
140
|
+
else if (ev.kind === 'result') {
|
|
141
|
+
resultPayload = ev.payload;
|
|
142
|
+
turnCount += 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!resultPayload) {
|
|
146
|
+
return { session_id: sessionId };
|
|
147
|
+
}
|
|
148
|
+
const stats = resultPayload.stats;
|
|
149
|
+
return {
|
|
150
|
+
tokens_in: stats?.input_tokens,
|
|
151
|
+
tokens_out: stats?.output_tokens,
|
|
152
|
+
// Gemini reports a single `cached` read tier; map it to cache_read. No
|
|
153
|
+
// separate cache-creation tier — `cached` is counted as a subset of
|
|
154
|
+
// input_tokens (consistent with codex `cached_input_tokens`), so pricing.ts
|
|
155
|
+
// must not double-count.
|
|
156
|
+
tokens_cache_read: stats?.cached,
|
|
157
|
+
tokens_cache_create: undefined,
|
|
158
|
+
// total_cost_usd intentionally absent — estimated via pricing.ts.
|
|
159
|
+
num_turns: turnCount || 1,
|
|
160
|
+
// Stream events do not carry the model; the manager wrapper stamps the
|
|
161
|
+
// requested model on the row.
|
|
162
|
+
model: undefined,
|
|
163
|
+
duration_ms: typeof stats?.duration_ms === 'number' ? stats.duration_ms : undefined,
|
|
164
|
+
duration_api_ms: undefined,
|
|
165
|
+
session_id: sessionId,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function compareSemver(a, b) {
|
|
169
|
+
const aParts = a.split('.').map((n) => parseInt(n, 10));
|
|
170
|
+
const bParts = b.split('.').map((n) => parseInt(n, 10));
|
|
171
|
+
for (let i = 0; i < 3; i++) {
|
|
172
|
+
const av = aParts[i] ?? 0;
|
|
173
|
+
const bv = bParts[i] ?? 0;
|
|
174
|
+
if (av > bv)
|
|
175
|
+
return 1;
|
|
176
|
+
if (av < bv)
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
async function detectGeminiInstalled() {
|
|
182
|
+
try {
|
|
183
|
+
(0, child_process_1.execSync)(`${WHICH_CMD} gemini`, { stdio: 'ignore' });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return { installed: false, executable: false };
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const raw = (0, child_process_1.execSync)('gemini --version', {
|
|
190
|
+
encoding: 'utf-8',
|
|
191
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
192
|
+
timeout: 3000,
|
|
193
|
+
}).trim();
|
|
194
|
+
const match = raw.match(/\d+\.\d+\.\d+/);
|
|
195
|
+
const version = match ? match[0] : raw;
|
|
196
|
+
const meetsMinimum = match ? compareSemver(version, GEMINI_MIN_VERSION) >= 0 : false;
|
|
197
|
+
const result = {
|
|
198
|
+
installed: true,
|
|
199
|
+
executable: true,
|
|
200
|
+
version,
|
|
201
|
+
meetsMinimum,
|
|
202
|
+
};
|
|
203
|
+
if (!meetsMinimum) {
|
|
204
|
+
result.error = `gemini ${version} is older than required ${GEMINI_MIN_VERSION}. Upgrade with: npm i -g @google/gemini-cli (or see https://github.com/google-gemini/gemini-cli).`;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { installed: true, executable: false };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
exports.geminiAdapter = {
|
|
213
|
+
id: 'gemini',
|
|
214
|
+
displayName: 'Gemini CLI',
|
|
215
|
+
binary: 'gemini',
|
|
216
|
+
minCliVersion: GEMINI_MIN_VERSION,
|
|
217
|
+
projectDirName: '.gemini',
|
|
218
|
+
instructionsFilename: 'GEMINI.md',
|
|
219
|
+
mcpRegistration: 'project-json',
|
|
220
|
+
capabilities: {
|
|
221
|
+
nativeResume: true,
|
|
222
|
+
nativeStreamJson: true,
|
|
223
|
+
nativeCostUsd: false,
|
|
224
|
+
nativeOtelEnv: true,
|
|
225
|
+
profileEnvSupport: true,
|
|
226
|
+
systemPromptArg: false,
|
|
227
|
+
},
|
|
228
|
+
modelCatalog: () => GEMINI_MODELS,
|
|
229
|
+
defaultModel: () => 'gemini-3.5-flash',
|
|
230
|
+
buildArgs: buildGeminiArgs,
|
|
231
|
+
parseStreamLine: parseGeminiStreamLine,
|
|
232
|
+
extractResult: extractGeminiResult,
|
|
233
|
+
baselineAgents: () => ['sr-architect', 'sr-developer', 'sr-reviewer'],
|
|
234
|
+
detectInstalled: detectGeminiInstalled,
|
|
235
|
+
// Pre-acknowledge the project's custom subagents so they load in headless
|
|
236
|
+
// `gemini -p` rail spawns (else invoke_agent reports "Subagent not found").
|
|
237
|
+
prepareHeadlessSpawn: gemini_agent_ack_1.acknowledgeGeminiProjectAgents,
|
|
238
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Gemini headless subagent pre-acknowledgment.
|
|
3
|
+
//
|
|
4
|
+
// gemini 0.46+ DISCOVERS `<project>/.gemini/agents/*.md` but only ENABLES a
|
|
5
|
+
// project's custom subagents after an interactive "New Agents Discovered →
|
|
6
|
+
// Acknowledge and Enable" prompt. That prompt never fires in headless
|
|
7
|
+
// (`gemini -p`) spawns — which is how the desktop runs every rail — so
|
|
8
|
+
// `invoke_agent sr-architect` returns "Subagent not found" and the implement
|
|
9
|
+
// orchestrator silently falls back to a generic agent (the specialised
|
|
10
|
+
// architect/developer/reviewer personas never run in isolation).
|
|
11
|
+
//
|
|
12
|
+
// specrails-core writes the acknowledgment file at install time; this is the
|
|
13
|
+
// defence-in-depth copy the desktop runs right before a gemini rail spawn, so a
|
|
14
|
+
// project installed with an older core (or whose agents changed since install)
|
|
15
|
+
// is still trusted headless. The file gemini reads is
|
|
16
|
+
// `~/.gemini/acknowledgments/agents.json`, shaped
|
|
17
|
+
// { [projectRoot]: { [agentName]: <sha256-hex of the agent .md file> } }
|
|
18
|
+
// where the hash is sha256 of the FULL agent markdown file (verified empirically
|
|
19
|
+
// against gemini 0.47). Entries are MERGED so other projects (and other agents)
|
|
20
|
+
// survive. Best-effort — callers swallow any error; a failure only means the
|
|
21
|
+
// agents need the one-time interactive acknowledge.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.acknowledgeGeminiProjectAgents = acknowledgeGeminiProjectAgents;
|
|
24
|
+
const crypto_1 = require("crypto");
|
|
25
|
+
const fs_1 = require("fs");
|
|
26
|
+
const os_1 = require("os");
|
|
27
|
+
const path_1 = require("path");
|
|
28
|
+
function ackFilePath() {
|
|
29
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.gemini', 'acknowledgments', 'agents.json');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pre-acknowledge every `<projectPath>/.gemini/agents/*.md` so gemini loads them
|
|
33
|
+
* in headless mode. No-op when the project has no `.gemini/agents` dir or no
|
|
34
|
+
* agent files. The `projectPath` is the key gemini uses (the spawn cwd / repo
|
|
35
|
+
* root), matching what `specrails-core` writes at install.
|
|
36
|
+
*/
|
|
37
|
+
function acknowledgeGeminiProjectAgents(projectPath) {
|
|
38
|
+
const agentsDir = (0, path_1.join)(projectPath, '.gemini', 'agents');
|
|
39
|
+
if (!(0, fs_1.existsSync)(agentsDir))
|
|
40
|
+
return;
|
|
41
|
+
const agentFiles = (0, fs_1.readdirSync)(agentsDir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
|
|
42
|
+
if (agentFiles.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
const ackPath = ackFilePath();
|
|
45
|
+
let store = {};
|
|
46
|
+
if ((0, fs_1.existsSync)(ackPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(ackPath, 'utf8'));
|
|
49
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
50
|
+
store = parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Corrupt/unreadable file — start fresh rather than crash the spawn.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const projectEntry = { ...(store[projectPath] ?? {}) };
|
|
58
|
+
for (const file of agentFiles) {
|
|
59
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(agentsDir, file), 'utf8');
|
|
60
|
+
projectEntry[file.slice(0, -3)] = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
store[projectPath] = projectEntry;
|
|
63
|
+
(0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.gemini', 'acknowledgments'), { recursive: true });
|
|
64
|
+
(0, fs_1.writeFileSync)(ackPath, `${JSON.stringify(store, null, 2)}\n`);
|
|
65
|
+
}
|
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Spec: openspec/specs/multi-provider-architecture/spec.md
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.UnknownProviderError = exports.codexAdapter = exports.claudeAdapter = exports.listAdapters = exports.hasAdapter = exports.getAdapter = void 0;
|
|
10
|
+
exports.UnknownProviderError = exports.geminiAdapter = exports.codexAdapter = exports.claudeAdapter = exports.listAdapters = exports.hasAdapter = exports.getAdapter = void 0;
|
|
11
11
|
const registry_1 = require("./registry");
|
|
12
12
|
const claude_adapter_1 = require("./claude-adapter");
|
|
13
13
|
Object.defineProperty(exports, "claudeAdapter", { enumerable: true, get: function () { return claude_adapter_1.claudeAdapter; } });
|
|
14
14
|
const codex_adapter_1 = require("./codex-adapter");
|
|
15
15
|
Object.defineProperty(exports, "codexAdapter", { enumerable: true, get: function () { return codex_adapter_1.codexAdapter; } });
|
|
16
|
+
const gemini_adapter_1 = require("./gemini-adapter");
|
|
17
|
+
Object.defineProperty(exports, "geminiAdapter", { enumerable: true, get: function () { return gemini_adapter_1.geminiAdapter; } });
|
|
16
18
|
(0, registry_1.register)(claude_adapter_1.claudeAdapter);
|
|
17
19
|
(0, registry_1.register)(codex_adapter_1.codexAdapter);
|
|
20
|
+
(0, registry_1.register)(gemini_adapter_1.geminiAdapter);
|
|
18
21
|
var registry_2 = require("./registry");
|
|
19
22
|
Object.defineProperty(exports, "getAdapter", { enumerable: true, get: function () { return registry_2.getAdapter; } });
|
|
20
23
|
Object.defineProperty(exports, "hasAdapter", { enumerable: true, get: function () { return registry_2.hasAdapter; } });
|