pikiloom 0.4.26 → 0.4.28
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/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* driver-gemini.ts — Gemini CLI agent driver.
|
|
3
|
-
*
|
|
4
|
-
* Requires `gemini` CLI installed (https://github.com/google-gemini/gemini-cli).
|
|
5
|
-
* Stream protocol: spawns `gemini` with JSON output and parses stdout line-by-line.
|
|
6
|
-
*/
|
|
7
1
|
import { registerDriver } from '../driver.js';
|
|
8
2
|
import fs from 'node:fs';
|
|
9
3
|
import os from 'node:os';
|
|
@@ -12,9 +6,6 @@ import { execSync } from 'node:child_process';
|
|
|
12
6
|
import { GEMINI_USAGE_TIMEOUTS, SESSION_RUNNING_THRESHOLD_MS } from '../../core/constants.js';
|
|
13
7
|
import { run, agentLog, appendSystemPrompt, pushRecentActivity, firstNonEmptyLine, shortValue, normalizeErrorMessage, sanitizeSessionUserPreviewText, emitSessionIdUpdate, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, applyTurnWindow, stripInjectedPrompts, attachAgentImage, roundPercent, emptyUsage, Q, } from '../index.js';
|
|
14
8
|
import { getHome } from '../../core/platform.js';
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Command & parser
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
9
|
function hasGeminiFlag(args, names) {
|
|
19
10
|
if (!args?.length)
|
|
20
11
|
return false;
|
|
@@ -25,15 +16,9 @@ function hasGeminiFlag(args, names) {
|
|
|
25
16
|
return names.some(name => trimmed === name || trimmed.startsWith(`${name}=`));
|
|
26
17
|
});
|
|
27
18
|
}
|
|
28
|
-
// Gemini CLI's -p mode is text-only — there's no flag for binary inputs. The
|
|
29
|
-
// CLI does, however, parse `@<path>` references in the prompt and inlines the
|
|
30
|
-
// file's content (text or image) into the model's context. We splice those
|
|
31
|
-
// references at the front of the prompt so attachments survive the trip.
|
|
32
19
|
export function buildGeminiPromptText(prompt, attachments) {
|
|
33
20
|
if (!attachments.length)
|
|
34
21
|
return prompt;
|
|
35
|
-
// Quote paths that contain spaces — gemini's tokenizer reads `@"..."` as a
|
|
36
|
-
// single reference. Plain paths can be left bare for cleaner display.
|
|
37
22
|
const refs = attachments.map(p => /\s/.test(p) ? `@"${p}"` : `@${p}`).join(' ');
|
|
38
23
|
return prompt ? `${refs}\n\n${prompt}` : refs;
|
|
39
24
|
}
|
|
@@ -53,7 +38,6 @@ function geminiCmd(o) {
|
|
|
53
38
|
}
|
|
54
39
|
if (o.geminiExtraArgs?.length)
|
|
55
40
|
args.push(...o.geminiExtraArgs);
|
|
56
|
-
// gemini's -p requires the prompt as its value (not via stdin)
|
|
57
41
|
const userPrompt = buildGeminiPromptText(o.prompt, o.attachments || []);
|
|
58
42
|
const promptText = o.geminiSystemInstruction
|
|
59
43
|
? appendSystemPrompt(o.geminiSystemInstruction, userPrompt)
|
|
@@ -166,20 +150,13 @@ function geminiToolResultSummary(tool, ev) {
|
|
|
166
150
|
}
|
|
167
151
|
function geminiParse(ev, s) {
|
|
168
152
|
const t = ev.type || '';
|
|
169
|
-
// init event: {"type":"init","session_id":"...","model":"..."}
|
|
170
153
|
if (t === 'init') {
|
|
171
154
|
emitSessionIdUpdate(s, ev.session_id);
|
|
172
155
|
s.model = ev.model ?? s.model;
|
|
173
156
|
s.contextWindow = geminiContextWindowFromModel(s.model) ?? s.contextWindow;
|
|
174
|
-
// Gemini's stream-json drops `thought` parts and every `agent_*`/`tool_update`
|
|
175
|
-
// event, so between init and the first tool_use/message there's nothing to
|
|
176
|
-
// surface — easily 10–30s on Gemini 3 Pro with HIGH thinking, longer when
|
|
177
|
-
// 429 backoffs kick in. Plant a sentinel so the IM/dashboard activity area
|
|
178
|
-
// shows progress instead of staying blank.
|
|
179
157
|
pushRecentActivity(s.recentActivity, 'Thinking...');
|
|
180
158
|
s.activity = s.recentActivity.join('\n');
|
|
181
159
|
}
|
|
182
|
-
// message delta: {"type":"message","role":"assistant","content":"...","delta":true}
|
|
183
160
|
if (t === 'message' && ev.role === 'assistant') {
|
|
184
161
|
if (ev.delta)
|
|
185
162
|
s.text += ev.content || '';
|
|
@@ -211,7 +188,6 @@ function geminiParse(ev, s) {
|
|
|
211
188
|
s.activity = s.recentActivity.join('\n');
|
|
212
189
|
}
|
|
213
190
|
}
|
|
214
|
-
// result event: {"type":"result","status":"success","stats":{...}}
|
|
215
191
|
if (t === 'result') {
|
|
216
192
|
emitSessionIdUpdate(s, ev.session_id);
|
|
217
193
|
if (ev.status === 'error' || ev.status === 'failure') {
|
|
@@ -227,19 +203,12 @@ function geminiParse(ev, s) {
|
|
|
227
203
|
s.inputTokens = u.input_tokens ?? u.input ?? s.inputTokens;
|
|
228
204
|
s.outputTokens = u.output_tokens ?? u.output ?? s.outputTokens;
|
|
229
205
|
s.cachedInputTokens = u.cached ?? s.cachedInputTokens;
|
|
230
|
-
// Gemini's `input_tokens` is the full prompt size (cached portion is
|
|
231
|
-
// already a subset of it). Use it directly as the context-window
|
|
232
|
-
// occupancy — adding `cached` would double-count.
|
|
233
206
|
if (s.inputTokens != null)
|
|
234
207
|
s.contextUsedTokens = s.inputTokens;
|
|
235
208
|
}
|
|
236
209
|
s.contextWindow = geminiContextWindowFromModel(s.model) ?? s.contextWindow;
|
|
237
210
|
}
|
|
238
211
|
}
|
|
239
|
-
// Gemini-cli does an exponential backoff on 429s and other transient errors
|
|
240
|
-
// without emitting any stream-json event — only stderr gets a line like
|
|
241
|
-
// `Attempt 1 failed with status 429. Retrying with backoff...`. Surface those
|
|
242
|
-
// lines as activity so users don't see a frozen UI during MODEL_CAPACITY_EXHAUSTED.
|
|
243
212
|
const GEMINI_RETRY_RE = /^Attempt\s+(\d+)\s+failed\s+with\s+status\s+(\d+)/i;
|
|
244
213
|
function geminiParseStderrLine(line, s) {
|
|
245
214
|
const m = GEMINI_RETRY_RE.exec(line);
|
|
@@ -253,23 +222,6 @@ function geminiParseStderrLine(line, s) {
|
|
|
253
222
|
pushRecentActivity(s.recentActivity, `Retrying after ${reason} (attempt ${attempt})`);
|
|
254
223
|
s.activity = s.recentActivity.join('\n');
|
|
255
224
|
}
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// Thinking effort overlay
|
|
258
|
-
//
|
|
259
|
-
// Gemini CLI exposes thinking via two knobs depending on model family:
|
|
260
|
-
// - Gemini 3.x: thinkingLevel: "LOW" | "HIGH"
|
|
261
|
-
// - Gemini 2.5: thinkingBudget: number (0=off, 8192=default, -1=dynamic)
|
|
262
|
-
// There is no CLI flag — the only place the CLI reads them is settings.json
|
|
263
|
-
// under `agents.<chat-base*>.modelConfig.generateContentConfig.thinkingConfig`.
|
|
264
|
-
//
|
|
265
|
-
// We don't want to mutate the user's ~/.gemini/settings.json, so for streams
|
|
266
|
-
// where an effort is set we materialise a fake $HOME via GEMINI_CLI_HOME and
|
|
267
|
-
// place a synthetic `.gemini/` inside it: symlinks for everything in the
|
|
268
|
-
// real ~/.gemini/ (oauth, projects, history, tmp, …) plus our merged
|
|
269
|
-
// settings.json. Note that gemini-cli reads GEMINI_CLI_HOME as the *parent*
|
|
270
|
-
// of `.gemini/`, not as `.gemini/` itself — getting that wrong makes gemini
|
|
271
|
-
// fail with "Please set an Auth method" because it can't find any creds.
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
225
|
function geminiEffortOverlay(effort) {
|
|
274
226
|
const value = String(effort || '').trim().toLowerCase();
|
|
275
227
|
if (!value)
|
|
@@ -345,8 +297,6 @@ function prepareGeminiHomeOverlay(opts) {
|
|
|
345
297
|
catch { }
|
|
346
298
|
return null;
|
|
347
299
|
}
|
|
348
|
-
// Symlink every entry in ~/.gemini except settings.json so OAuth, projects,
|
|
349
|
-
// history, tmp/, etc. all stay shared with the user's real config.
|
|
350
300
|
try {
|
|
351
301
|
for (const entry of fs.readdirSync(userGeminiDir, { withFileTypes: true })) {
|
|
352
302
|
if (entry.name === 'settings.json')
|
|
@@ -354,10 +304,10 @@ function prepareGeminiHomeOverlay(opts) {
|
|
|
354
304
|
try {
|
|
355
305
|
fs.symlinkSync(path.join(userGeminiDir, entry.name), path.join(overlayGeminiDir, entry.name));
|
|
356
306
|
}
|
|
357
|
-
catch {
|
|
307
|
+
catch { }
|
|
358
308
|
}
|
|
359
309
|
}
|
|
360
|
-
catch {
|
|
310
|
+
catch { }
|
|
361
311
|
let userSettings = {};
|
|
362
312
|
const userSettingsPath = path.join(userGeminiDir, 'settings.json');
|
|
363
313
|
try {
|
|
@@ -365,7 +315,7 @@ function prepareGeminiHomeOverlay(opts) {
|
|
|
365
315
|
userSettings = JSON.parse(fs.readFileSync(userSettingsPath, 'utf-8'));
|
|
366
316
|
}
|
|
367
317
|
}
|
|
368
|
-
catch {
|
|
318
|
+
catch { }
|
|
369
319
|
const merged = { ...userSettings };
|
|
370
320
|
if (effortOverrides) {
|
|
371
321
|
merged.agents = deepMergeAgents(userSettings.agents, effortOverrides);
|
|
@@ -402,11 +352,7 @@ function prepareGeminiHomeOverlay(opts) {
|
|
|
402
352
|
catch { } },
|
|
403
353
|
};
|
|
404
354
|
}
|
|
405
|
-
// ---------------------------------------------------------------------------
|
|
406
|
-
// Stream
|
|
407
|
-
// ---------------------------------------------------------------------------
|
|
408
355
|
export async function doGeminiStream(opts) {
|
|
409
|
-
// Prompt is passed as -p argument; send empty stdin so run() doesn't duplicate it
|
|
410
356
|
const overlay = prepareGeminiHomeOverlay({
|
|
411
357
|
effort: opts.thinkingEffort,
|
|
412
358
|
hasAttachments: (opts.attachments?.length ?? 0) > 0,
|
|
@@ -422,10 +368,6 @@ export async function doGeminiStream(opts) {
|
|
|
422
368
|
overlay?.cleanup();
|
|
423
369
|
}
|
|
424
370
|
}
|
|
425
|
-
// ---------------------------------------------------------------------------
|
|
426
|
-
// Sessions / Tail
|
|
427
|
-
// ---------------------------------------------------------------------------
|
|
428
|
-
/** Resolve Gemini project name for a workdir from ~/.gemini/projects.json */
|
|
429
371
|
function geminiProjectName(workdir) {
|
|
430
372
|
const home = getHome();
|
|
431
373
|
if (!home)
|
|
@@ -437,7 +379,6 @@ function geminiProjectName(workdir) {
|
|
|
437
379
|
if (!projects || typeof projects !== 'object')
|
|
438
380
|
return null;
|
|
439
381
|
const resolved = path.resolve(workdir);
|
|
440
|
-
// Exact match first, then check entries
|
|
441
382
|
if (projects[resolved])
|
|
442
383
|
return projects[resolved];
|
|
443
384
|
for (const [dir, name] of Object.entries(projects)) {
|
|
@@ -445,7 +386,7 @@ function geminiProjectName(workdir) {
|
|
|
445
386
|
return name;
|
|
446
387
|
}
|
|
447
388
|
}
|
|
448
|
-
catch {
|
|
389
|
+
catch { }
|
|
449
390
|
return null;
|
|
450
391
|
}
|
|
451
392
|
function geminiChatsDir(workdir) {
|
|
@@ -475,15 +416,6 @@ function extractGeminiText(content) {
|
|
|
475
416
|
}
|
|
476
417
|
return parts.join('\n').trim();
|
|
477
418
|
}
|
|
478
|
-
// Gemini's -p mode is text-only, so pikiloom concatenates its system-prompt
|
|
479
|
-
// blocks ([Browser Automation], [Artifact Return], …) onto the user's prompt
|
|
480
|
-
// before invoking the CLI. That means the JSONL "user" message we read back
|
|
481
|
-
// later contains those orchestrator-injected blocks AND the gemini-CLI-emitted
|
|
482
|
-
// `--- Content from referenced files ---` markers it appends when expanding
|
|
483
|
-
// `@<path>` references. Both are noise for the dashboard / IM render path —
|
|
484
|
-
// the helpers below strip them so the displayed user bubble matches what the
|
|
485
|
-
// human actually typed, and surface staged image attachments as image blocks
|
|
486
|
-
// instead of raw `@<path>` text.
|
|
487
419
|
const GEMINI_SYSTEM_BLOCK_SENTINELS = [
|
|
488
420
|
'[Artifact Return]',
|
|
489
421
|
'[Asking the user]',
|
|
@@ -513,12 +445,6 @@ function cleanGeminiUserText(rawText) {
|
|
|
513
445
|
text = text.replace(GEMINI_REFERENCED_FILES_BLOCK_RE, '\n');
|
|
514
446
|
return text.trim();
|
|
515
447
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Build a (text, image blocks) pair for a rendered user bubble. `@<path>`
|
|
518
|
-
* references that resolve to readable image files are lifted into image
|
|
519
|
-
* blocks; refs that don't resolve are left in the text so the user can still
|
|
520
|
-
* see what they wrote.
|
|
521
|
-
*/
|
|
522
448
|
function buildGeminiUserMessageContent(rawText, workdir) {
|
|
523
449
|
const cleaned = cleanGeminiUserText(rawText);
|
|
524
450
|
if (!cleaned)
|
|
@@ -538,14 +464,9 @@ function buildGeminiUserMessageContent(rawText, workdir) {
|
|
|
538
464
|
});
|
|
539
465
|
return { text: textOnly.replace(/\n{3,}/g, '\n\n').trim(), blocks };
|
|
540
466
|
}
|
|
541
|
-
/** Drop the attachment `@<path>` refs entirely so they don't surface as raw
|
|
542
|
-
* paths in plain-text contexts (tail snippets, sidebar previews). Newlines
|
|
543
|
-
* in the surrounding prose are preserved. */
|
|
544
467
|
function dropGeminiFileRefs(text) {
|
|
545
468
|
return text.replace(GEMINI_FILE_REF_RE, '$1');
|
|
546
469
|
}
|
|
547
|
-
/** Single-line variant for session list titles where the bubble shape is a
|
|
548
|
-
* one-liner — collapses every whitespace run to a single space. */
|
|
549
470
|
function flattenGeminiUserText(rawText) {
|
|
550
471
|
return dropGeminiFileRefs(cleanGeminiUserText(rawText)).replace(/\s+/g, ' ').trim();
|
|
551
472
|
}
|
|
@@ -577,7 +498,7 @@ function findGeminiSessionFile(workdir, sessionId) {
|
|
|
577
498
|
if (data?.sessionId === sessionId)
|
|
578
499
|
return filePath;
|
|
579
500
|
}
|
|
580
|
-
catch {
|
|
501
|
+
catch { }
|
|
581
502
|
}
|
|
582
503
|
return null;
|
|
583
504
|
}
|
|
@@ -586,7 +507,6 @@ function loadGeminiSessionData(filePath) {
|
|
|
586
507
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
587
508
|
if (filePath.endsWith('.json'))
|
|
588
509
|
return JSON.parse(content);
|
|
589
|
-
// JSONL format: first line is metadata, subsequent lines are messages or $set updates
|
|
590
510
|
const lines = content.split('\n');
|
|
591
511
|
let data = {};
|
|
592
512
|
const messages = [];
|
|
@@ -606,7 +526,7 @@ function loadGeminiSessionData(filePath) {
|
|
|
606
526
|
messages.push(obj);
|
|
607
527
|
}
|
|
608
528
|
}
|
|
609
|
-
catch {
|
|
529
|
+
catch { }
|
|
610
530
|
}
|
|
611
531
|
data.messages = messages;
|
|
612
532
|
return data;
|
|
@@ -615,24 +535,14 @@ function loadGeminiSessionData(filePath) {
|
|
|
615
535
|
return null;
|
|
616
536
|
}
|
|
617
537
|
}
|
|
618
|
-
// Per-file cache of the derived fields. getNativeGeminiSessionsFromFiles read +
|
|
619
|
-
// JSON-parsed every chat file's full contents on every list request AND per
|
|
620
|
-
// workspace×agent in the overview fan-out. Keyed by (mtime,size) so unchanged
|
|
621
|
-
// chats are never re-read; `running` depends on Date.now() so it's recomputed
|
|
622
|
-
// per call. Stores only the small derived fields, never the full messages array.
|
|
623
538
|
const nativeGeminiContentCache = new Map();
|
|
624
539
|
function readNativeGeminiContent(filePath) {
|
|
625
540
|
const data = loadGeminiSessionData(filePath);
|
|
626
541
|
if (!data?.sessionId)
|
|
627
542
|
return null;
|
|
628
|
-
// Gemini CLI writes stub session files for internal bookkeeping — e.g.
|
|
629
|
-
// `sessionId: "a2a-server"` for its built-in a2a server, plus abandoned
|
|
630
|
-
// UUID-named sessions that never received a turn. Both share the same shape:
|
|
631
|
-
// metadata only, no `messages` array. Nothing to render, so skip them.
|
|
632
543
|
const messages = Array.isArray(data.messages) ? data.messages : [];
|
|
633
544
|
if (messages.length === 0)
|
|
634
545
|
return null;
|
|
635
|
-
// Extract title from first user message + last Q&A from tail.
|
|
636
546
|
let title = null;
|
|
637
547
|
let lastQuestion = null;
|
|
638
548
|
let lastAnswer = null;
|
|
@@ -668,7 +578,6 @@ function readNativeGeminiContent(filePath) {
|
|
|
668
578
|
numTurns: numTurns || null,
|
|
669
579
|
};
|
|
670
580
|
}
|
|
671
|
-
/** Read native Gemini CLI sessions from ~/.gemini/tmp/{projectName}/chats/ */
|
|
672
581
|
function getNativeGeminiSessionsFromFiles(workdir) {
|
|
673
582
|
const chatsDir = geminiChatsDir(workdir);
|
|
674
583
|
if (!chatsDir || !fs.existsSync(chatsDir))
|
|
@@ -702,7 +611,6 @@ function getNativeGeminiSessionsFromFiles(workdir) {
|
|
|
702
611
|
const content = cached.content;
|
|
703
612
|
if (!content)
|
|
704
613
|
continue;
|
|
705
|
-
// If we already saw this sessionId, only replace it if this file is newer.
|
|
706
614
|
const existing = sessionsById.get(content.sessionId);
|
|
707
615
|
if (existing && content.updatedAt && existing.runUpdatedAt && Date.parse(content.updatedAt) <= Date.parse(existing.runUpdatedAt)) {
|
|
708
616
|
continue;
|
|
@@ -739,9 +647,6 @@ function getNativeGeminiSessions(workdir) {
|
|
|
739
647
|
}
|
|
740
648
|
function getGeminiSessions(workdir, limit) {
|
|
741
649
|
const resolvedWorkdir = path.resolve(workdir);
|
|
742
|
-
// Merge pikiloom-tracked sessions with native Gemini sessions
|
|
743
|
-
// Canonical record→SessionInfo mapper (single source of truth) — see claude.ts.
|
|
744
|
-
// Hand-rolling dropped thinkingEffort/workflowEnabled/profileId from the merge.
|
|
745
650
|
const pikiloomSessions = listPikiloomSessions(resolvedWorkdir, 'gemini').map(managedRecordToSessionInfo);
|
|
746
651
|
const nativeSessions = getNativeGeminiSessions(resolvedWorkdir);
|
|
747
652
|
const merged = mergeManagedAndNativeSessions(pikiloomSessions, nativeSessions);
|
|
@@ -777,9 +682,6 @@ function getGeminiSessionTail(opts) {
|
|
|
777
682
|
return { ok: false, messages: [], error: e.message };
|
|
778
683
|
}
|
|
779
684
|
}
|
|
780
|
-
// ---------------------------------------------------------------------------
|
|
781
|
-
// Session messages (full content)
|
|
782
|
-
// ---------------------------------------------------------------------------
|
|
783
685
|
function getGeminiSessionMessages(opts) {
|
|
784
686
|
const filePath = findGeminiSessionFile(opts.workdir, opts.sessionId);
|
|
785
687
|
if (!filePath)
|
|
@@ -819,10 +721,6 @@ function getGeminiSessionMessages(opts) {
|
|
|
819
721
|
return { ok: false, messages: [], totalTurns: 0, error: e.message };
|
|
820
722
|
}
|
|
821
723
|
}
|
|
822
|
-
// ---------------------------------------------------------------------------
|
|
823
|
-
// Models — static list for now, can be extended with `gemini models list`
|
|
824
|
-
// ---------------------------------------------------------------------------
|
|
825
|
-
// Model IDs from gemini-cli-core (no CLI command to list them dynamically)
|
|
826
724
|
const GEMINI_MODELS = [
|
|
827
725
|
{ id: 'auto-gemini-3', alias: 'auto-3' },
|
|
828
726
|
{ id: 'auto-gemini-2.5', alias: 'auto' },
|
|
@@ -833,9 +731,6 @@ const GEMINI_MODELS = [
|
|
|
833
731
|
{ id: 'gemini-2.5-flash', alias: 'flash' },
|
|
834
732
|
{ id: 'gemini-2.5-flash-lite', alias: 'flash-lite' },
|
|
835
733
|
];
|
|
836
|
-
// ---------------------------------------------------------------------------
|
|
837
|
-
// Usage
|
|
838
|
-
// ---------------------------------------------------------------------------
|
|
839
734
|
const GEMINI_USAGE_TIMEOUT_MS = GEMINI_USAGE_TIMEOUTS.request;
|
|
840
735
|
const GEMINI_USAGE_URL = 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota';
|
|
841
736
|
let lastGeminiUsage = null;
|
|
@@ -993,9 +888,6 @@ async function getGeminiUsageLive() {
|
|
|
993
888
|
return cachedGeminiUsage(detail);
|
|
994
889
|
}
|
|
995
890
|
}
|
|
996
|
-
// ---------------------------------------------------------------------------
|
|
997
|
-
// Driver
|
|
998
|
-
// ---------------------------------------------------------------------------
|
|
999
891
|
class GeminiDriver {
|
|
1000
892
|
id = 'gemini';
|
|
1001
893
|
cmd = 'gemini';
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hermes Agent driver — speaks ACP (Agent Client Protocol) to `hermes acp`.
|
|
3
|
-
*
|
|
4
|
-
* Pikiloom owns Provider/Profile credentials in its own vault; this driver
|
|
5
|
-
* reads the active Profile via the model layer's injector and applies env
|
|
6
|
-
* vars when spawning the Hermes ACP child process. Hermes' own config and
|
|
7
|
-
* `hermes auth` command are not touched.
|
|
8
|
-
*
|
|
9
|
-
* Reference: https://agentclientprotocol.com — JSON-RPC 2.0 over stdio.
|
|
10
|
-
* Hermes implements ACP via `~/.hermes/hermes-agent/acp_adapter/server.py`.
|
|
11
|
-
*/
|
|
12
1
|
import { spawn } from 'node:child_process';
|
|
13
2
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
14
3
|
import { homedir } from 'node:os';
|
|
@@ -17,11 +6,6 @@ import { resolve as resolvePath } from 'node:path';
|
|
|
17
6
|
import { registerDriver } from '../driver.js';
|
|
18
7
|
import { AcpClient, toAcpMcpServers } from '../acp-client.js';
|
|
19
8
|
import { agentLog, agentWarn, emptyUsage, normalizeErrorMessage, listPikiloomSessions, managedRecordToSessionInfo, findPikiloomSession, buildStreamPreviewMeta, applyTurnWindow, pushRecentActivity, IMAGE_EXTS, mimeForExt, } from '../index.js';
|
|
20
|
-
// Build the ACP `prompt` content array from the user's text + staged
|
|
21
|
-
// attachments. Images become ImageContentBlocks (base64 + mimeType — the
|
|
22
|
-
// shape Hermes' acp_adapter accepts and converts to OpenAI multimodal
|
|
23
|
-
// content). Non-image attachments are referenced by path so the agent can
|
|
24
|
-
// open them with its own filesystem tools.
|
|
25
9
|
function buildHermesPromptBlocks(prompt, attachments) {
|
|
26
10
|
const blocks = [];
|
|
27
11
|
for (const filePath of attachments) {
|
|
@@ -75,9 +59,6 @@ function applySessionUpdate(state, update) {
|
|
|
75
59
|
return false;
|
|
76
60
|
}
|
|
77
61
|
case 'tool_call': {
|
|
78
|
-
// ACP tool_call kicks off a new invocation. Accumulate it (mirroring
|
|
79
|
-
// Claude/Codex) so the user sees the full chain in the live footer,
|
|
80
|
-
// not just the title of whatever tool is currently mid-flight.
|
|
81
62
|
const id = typeof update.toolCallId === 'string' ? update.toolCallId : '';
|
|
82
63
|
const title = (typeof update.title === 'string' && update.title.trim()) || 'tool';
|
|
83
64
|
if (id)
|
|
@@ -104,10 +85,6 @@ function applySessionUpdate(state, update) {
|
|
|
104
85
|
return true;
|
|
105
86
|
}
|
|
106
87
|
case 'usage_update': {
|
|
107
|
-
// ACP semantics: `size` = model context window, `used` = current
|
|
108
|
-
// context pressure. We previously mis-mapped `size` to
|
|
109
|
-
// contextUsedTokens, which made the chip show the full window
|
|
110
|
-
// (e.g. "1.0M tok" for a 1M-window model) regardless of actual use.
|
|
111
88
|
if (typeof update.size === 'number')
|
|
112
89
|
state.contextWindow = update.size;
|
|
113
90
|
if (typeof update.used === 'number')
|
|
@@ -129,21 +106,9 @@ function makeStreamResult(start, partial = {}) {
|
|
|
129
106
|
...partial,
|
|
130
107
|
};
|
|
131
108
|
}
|
|
132
|
-
// Empty / refusal heuristic — used to surface a hint when the model's reply
|
|
133
|
-
// is the canonical OpenAI safety refusal with no content. We don't paper
|
|
134
|
-
// over it (the user should see exactly what the model said), but a short
|
|
135
|
-
// pikiloom note tells them this came from the model itself, not a bug.
|
|
136
109
|
const REFUSAL_REGEX = /^(?:i'?m sorry|sorry),?[\s\w,'`]*?(?:can(?:not|'t)|unable to)\s+(?:assist|help)[\s\S]{0,40}$/i;
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// ACP-driven streaming
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
110
|
async function doHermesStream(opts) {
|
|
141
111
|
const start = Date.now();
|
|
142
|
-
// BYOK env is populated on opts by stream.ts via resolveAgentInjection('hermes');
|
|
143
|
-
// we just merge it into the spawn env. The bound model is delivered via the
|
|
144
|
-
// ACP `session/set_model` request below — `hermes acp` does NOT accept any
|
|
145
|
-
// CLI flags besides `--accept-hooks`, so we MUST NOT append byokArgvAppend
|
|
146
|
-
// here (doing so would crash the spawn with `unrecognized arguments`).
|
|
147
112
|
const baseEnv = { ...process.env, ...(opts.extraEnv || {}) };
|
|
148
113
|
if (!opts.hermesModel) {
|
|
149
114
|
agentLog(`[hermes] no active profile bound — running with hermes' native config default`);
|
|
@@ -156,16 +121,8 @@ async function doHermesStream(opts) {
|
|
|
156
121
|
});
|
|
157
122
|
let sessionId = opts.sessionId || null;
|
|
158
123
|
let stopReason = null;
|
|
159
|
-
// Per-turn streaming state. We reset it just before sending session/prompt
|
|
160
|
-
// so any session/update events from the prior session/load replay don't
|
|
161
|
-
// leak into the user-visible reply.
|
|
162
124
|
const state = makeStreamState();
|
|
163
|
-
// While true, sessionUpdate events flow into `state`. When false, they are
|
|
164
|
-
// counted but discarded — used during the session/load replay window so
|
|
165
|
-
// history-replay chunks don't pollute the new turn's reply.
|
|
166
125
|
let consumeUpdates = true;
|
|
167
|
-
// Agent-initiated requests we must respond to (we deny filesystem ops we
|
|
168
|
-
// don't support; pikiloom has its own sandbox/permission model elsewhere).
|
|
169
126
|
client.on('request', ({ id, method }) => {
|
|
170
127
|
if (method === 'session/request_permission') {
|
|
171
128
|
client.respond(id, { outcome: { outcome: 'cancelled' } });
|
|
@@ -196,7 +153,6 @@ async function doHermesStream(opts) {
|
|
|
196
153
|
}
|
|
197
154
|
};
|
|
198
155
|
client.on('sessionUpdate', onUpdate);
|
|
199
|
-
// Abort handling
|
|
200
156
|
const onAbort = () => {
|
|
201
157
|
stopReason = 'interrupted';
|
|
202
158
|
if (sessionId)
|
|
@@ -223,16 +179,6 @@ async function doHermesStream(opts) {
|
|
|
223
179
|
opts.onSessionId?.(sessionId);
|
|
224
180
|
}
|
|
225
181
|
else {
|
|
226
|
-
// Resumed session in a fresh `hermes acp` process. We MUST call
|
|
227
|
-
// `session/load`, not just rely on the implicit DB restore that
|
|
228
|
-
// `session/prompt` does internally:
|
|
229
|
-
// 1. ACP-registered MCP servers are NOT persisted on disk — Hermes'
|
|
230
|
-
// `_make_agent` (used during DB restore) recreates the agent with
|
|
231
|
-
// *only* its native toolset. Without re-registering, the model
|
|
232
|
-
// sees a different tool surface than turn 1.
|
|
233
|
-
// 2. `session/load` triggers Hermes' history-replay (it streams every
|
|
234
|
-
// prior user/assistant message back as session/update events).
|
|
235
|
-
// Discard them so they don't pollute the new turn's reply.
|
|
236
182
|
consumeUpdates = false;
|
|
237
183
|
try {
|
|
238
184
|
const result = await client.request('session/load', {
|
|
@@ -244,9 +190,6 @@ async function doHermesStream(opts) {
|
|
|
244
190
|
agentWarn(`[hermes] session/load returned null for ${sessionId} — session not found in Hermes DB; continuing with a fresh prompt against the existing id`);
|
|
245
191
|
}
|
|
246
192
|
else {
|
|
247
|
-
// Replay events arrive on Hermes' event loop AFTER session/load
|
|
248
|
-
// resolves. Wait until the stream goes quiet for ~150ms (or 3s
|
|
249
|
-
// hard cap) — that's a more robust signal than a fixed sleep.
|
|
250
193
|
const drained = await client.waitForQuiet(150, 3_000);
|
|
251
194
|
if (drained > 0)
|
|
252
195
|
agentLog(`[hermes] drained ${drained} replay event(s) after session/load`);
|
|
@@ -258,12 +201,6 @@ async function doHermesStream(opts) {
|
|
|
258
201
|
}
|
|
259
202
|
if (!sessionId)
|
|
260
203
|
throw new Error('Hermes did not return a session id');
|
|
261
|
-
// If a Profile is bound, override Hermes' config-default model via ACP.
|
|
262
|
-
// `set_model` is best-effort: a Hermes that doesn't recognise the model id
|
|
263
|
-
// (or doesn't have credentials for the requested provider) responds with
|
|
264
|
-
// an error, but we keep going — the user will see that error in the first
|
|
265
|
-
// prompt response and can re-pick a working profile, which is far better
|
|
266
|
-
// than crashing the whole spawn.
|
|
267
204
|
if (opts.hermesModel) {
|
|
268
205
|
try {
|
|
269
206
|
await client.request('session/set_model', {
|
|
@@ -276,20 +213,12 @@ async function doHermesStream(opts) {
|
|
|
276
213
|
agentWarn(`[hermes] session/set_model failed (${opts.hermesModel}): ${e?.message || e} — falling back to Hermes' config default`);
|
|
277
214
|
}
|
|
278
215
|
}
|
|
279
|
-
// Best-effort: forward the chosen reasoning effort via ACP `session/set_mode`.
|
|
280
|
-
// Current `hermes acp` accepts the request but only persists the value on
|
|
281
|
-
// the session record (it doesn't influence generation). When Hermes adds a
|
|
282
|
-
// real effort knob to the ACP surface, this call will start taking effect
|
|
283
|
-
// without any pikiloom change.
|
|
284
216
|
if (opts.thinkingEffort) {
|
|
285
217
|
await client.tryRequest('session/set_mode', {
|
|
286
218
|
sessionId,
|
|
287
219
|
modeId: opts.thinkingEffort,
|
|
288
220
|
});
|
|
289
221
|
}
|
|
290
|
-
// Reset the buffer one last time before we start consuming updates for
|
|
291
|
-
// the prompt. Anything that arrived during/after the drain (latency
|
|
292
|
-
// stragglers from the replay) gets discarded too.
|
|
293
222
|
state.text = '';
|
|
294
223
|
state.thinking = '';
|
|
295
224
|
state.activity = '';
|
|
@@ -301,9 +230,6 @@ async function doHermesStream(opts) {
|
|
|
301
230
|
prompt: buildHermesPromptBlocks(opts.prompt, opts.attachments || []),
|
|
302
231
|
}, Math.max(opts.timeout * 1000, 30_000));
|
|
303
232
|
stopReason = promptResponse?.stopReason || 'end_turn';
|
|
304
|
-
// `PromptResponse.usage` (ACP) carries real per-turn token counts from the
|
|
305
|
-
// provider. `usage_update` notifications, by contrast, describe the
|
|
306
|
-
// session's *context window pressure* (size/used), not this turn's I/O.
|
|
307
233
|
const usage = promptResponse?.usage;
|
|
308
234
|
if (usage && typeof usage === 'object') {
|
|
309
235
|
const input = usage.inputTokens ?? usage.input_tokens;
|
|
@@ -333,8 +259,6 @@ async function doHermesStream(opts) {
|
|
|
333
259
|
stopReason,
|
|
334
260
|
incomplete: stopReason !== 'end_turn',
|
|
335
261
|
activity: null,
|
|
336
|
-
// When the model itself refuses, mark as incomplete and add a hint so
|
|
337
|
-
// the user can tell it's the model's choice (not a pikiloom error).
|
|
338
262
|
error: isRefusalOnly
|
|
339
263
|
? `Model returned a safety refusal. Try a different model on the agent card (e.g. claude-haiku-4.5 via OpenRouter), or check ~/.hermes/config.yaml.`
|
|
340
264
|
: null,
|
|
@@ -362,16 +286,8 @@ async function doHermesStream(opts) {
|
|
|
362
286
|
await client.close().catch(() => { });
|
|
363
287
|
}
|
|
364
288
|
}
|
|
365
|
-
// ---------------------------------------------------------------------------
|
|
366
|
-
// Sessions / models / usage — minimal surface
|
|
367
|
-
// ---------------------------------------------------------------------------
|
|
368
289
|
async function getHermesSessions(workdir, limit) {
|
|
369
|
-
// Hermes' own session list lives in a SQLite DB inside ~/.hermes — useful
|
|
370
|
-
// for the `hermes sessions` CLI but irrelevant to pikiloom, which always
|
|
371
|
-
// creates its own ACP session per turn and records it under .pikiloom.
|
|
372
290
|
const resolvedWorkdir = resolvePath(workdir);
|
|
373
|
-
// Canonical record→SessionInfo mapper (single source of truth) — see claude.ts.
|
|
374
|
-
// Hand-rolling dropped thinkingEffort/workflowEnabled/profileId.
|
|
375
291
|
const records = listPikiloomSessions(resolvedWorkdir, 'hermes');
|
|
376
292
|
const sessions = records.map(managedRecordToSessionInfo);
|
|
377
293
|
sessions.sort((a, b) => Date.parse(b.createdAt || '') - Date.parse(a.createdAt || ''));
|
|
@@ -382,19 +298,12 @@ async function getHermesSessions(workdir, limit) {
|
|
|
382
298
|
async function getHermesSessionTail(_opts) {
|
|
383
299
|
return { ok: true, messages: [], error: null };
|
|
384
300
|
}
|
|
385
|
-
// Hermes mirrors every ACP/CLI session to ~/.hermes/sessions/session_<id>.json
|
|
386
|
-
// (in addition to the SQLite store at ~/.hermes/state.db). The JSON file is
|
|
387
|
-
// authoritative for full conversation replay: it carries the OpenAI-style
|
|
388
|
-
// `messages[]` stream, including assistant `reasoning_content` and any
|
|
389
|
-
// `tool_calls[] / role:"tool"` pairs. Reading it is ~20ms vs. a 4+ s ACP
|
|
390
|
-
// `session/load` replay round-trip, so we use it directly.
|
|
391
301
|
function hermesSessionJsonPath(sessionId) {
|
|
392
302
|
return join(homedir(), '.hermes', 'sessions', `session_${sessionId}.json`);
|
|
393
303
|
}
|
|
394
304
|
function extractHermesContentText(content) {
|
|
395
305
|
if (typeof content === 'string')
|
|
396
306
|
return content;
|
|
397
|
-
// OpenAI multimodal: array of { type, text | image_url, ... }.
|
|
398
307
|
if (Array.isArray(content)) {
|
|
399
308
|
const parts = [];
|
|
400
309
|
for (const part of content) {
|
|
@@ -417,7 +326,6 @@ function formatHermesArgs(raw) {
|
|
|
417
326
|
const trimmed = raw.trim();
|
|
418
327
|
if (!trimmed)
|
|
419
328
|
return '';
|
|
420
|
-
// tool_calls.function.arguments is a JSON string — pretty-print when valid.
|
|
421
329
|
try {
|
|
422
330
|
const parsed = JSON.parse(trimmed);
|
|
423
331
|
return JSON.stringify(parsed, null, 2);
|
|
@@ -537,16 +445,11 @@ function getHermesSessionMessagesFromJson(opts) {
|
|
|
537
445
|
});
|
|
538
446
|
continue;
|
|
539
447
|
}
|
|
540
|
-
// Unknown role — ignore silently.
|
|
541
448
|
}
|
|
542
449
|
flushAssistant();
|
|
543
450
|
return applyTurnWindow(allMsgs, opts, opts.rich !== false ? richMsgs : undefined);
|
|
544
451
|
}
|
|
545
452
|
function getHermesSessionMessagesFromRecord(opts) {
|
|
546
|
-
// Fallback for sessions that pre-date the JSON store (or that Hermes wrote
|
|
547
|
-
// somewhere we can't see). Synthesizes the most recent turn from the
|
|
548
|
-
// pikiloom session record so the dashboard still has *something* after the
|
|
549
|
-
// live snapshot expires.
|
|
550
453
|
const record = findPikiloomSession(opts.workdir, 'hermes', opts.sessionId);
|
|
551
454
|
if (!record || (!record.lastQuestion && !record.lastAnswer && !record.lastThinking)) {
|
|
552
455
|
return { ok: true, messages: [], totalTurns: 0, error: null };
|
|
@@ -597,10 +500,6 @@ async function getHermesSessionMessages(opts) {
|
|
|
597
500
|
return getHermesSessionMessagesFromRecord(opts);
|
|
598
501
|
}
|
|
599
502
|
async function listHermesModels(_opts) {
|
|
600
|
-
// When Hermes has its own working config (typical case — user ran `hermes auth`
|
|
601
|
-
// and `hermes config`), surface the configured default model so the dashboard
|
|
602
|
-
// and IM /models reflect what Hermes will actually use without forcing the
|
|
603
|
-
// user to re-configure inside pikiloom.
|
|
604
503
|
const native = readHermesNativeConfig();
|
|
605
504
|
const models = native?.model
|
|
606
505
|
? [{ id: native.model, alias: `${native.provider} (Hermes config)` }]
|
|
@@ -614,24 +513,11 @@ async function listHermesModels(_opts) {
|
|
|
614
513
|
: `Run \`hermes config\` to set a default model, or bind a pikiloom Provider on the agent card.`,
|
|
615
514
|
};
|
|
616
515
|
}
|
|
617
|
-
// ---------------------------------------------------------------------------
|
|
618
|
-
// Native config reader
|
|
619
|
-
//
|
|
620
|
-
// Pikiloom never writes to ~/.hermes/config.yaml — that is Hermes' own state.
|
|
621
|
-
// We just *read* a few fields so the dashboard can show what Hermes will run
|
|
622
|
-
// with when no BYOK Profile is bound, and so the UI doesn't pretend that an
|
|
623
|
-
// already-configured Hermes "needs" to be re-configured inside pikiloom.
|
|
624
|
-
//
|
|
625
|
-
// Limited surface: only the top-level `model.*` and `agent.reasoning_effort`
|
|
626
|
-
// keys we care about. Implemented with simple string matching (no YAML
|
|
627
|
-
// dependency) since the schema is stable and shallow.
|
|
628
|
-
// ---------------------------------------------------------------------------
|
|
629
516
|
let cachedNativeConfig = null;
|
|
630
517
|
function readHermesNativeConfig() {
|
|
631
518
|
const path = join(homedir(), '.hermes', 'config.yaml');
|
|
632
519
|
if (!existsSync(path))
|
|
633
520
|
return null;
|
|
634
|
-
// Cheap mtime check — re-read only when the file has changed.
|
|
635
521
|
let mtimeMs;
|
|
636
522
|
try {
|
|
637
523
|
mtimeMs = statSync(path).mtimeMs;
|
|
@@ -650,14 +536,10 @@ function readHermesNativeConfig() {
|
|
|
650
536
|
return null;
|
|
651
537
|
}
|
|
652
538
|
const blockOf = (name) => {
|
|
653
|
-
// Captures consecutive indented lines under `name:` (a top-level mapping).
|
|
654
|
-
// Stops at the first non-indented line, which marks the next top-level
|
|
655
|
-
// key. Plain (no `m` flag) so `.` matches across lines via [^\n].
|
|
656
539
|
const re = new RegExp(`(?:^|\\n)${name}:[ \\t]*\\n((?:[ \\t]+[^\\n]*\\n?)+)`);
|
|
657
540
|
return text.match(re)?.[1] || '';
|
|
658
541
|
};
|
|
659
542
|
const valueOf = (block, key) => {
|
|
660
|
-
// The key must be at any indentation level deeper than the parent.
|
|
661
543
|
const m = block.match(new RegExp(`(?:^|\\n)[ \\t]+${key}:[ \\t]*([^\\n]*)`));
|
|
662
544
|
if (!m)
|
|
663
545
|
return null;
|
|
@@ -682,7 +564,6 @@ function getHermesUsage(_opts) {
|
|
|
682
564
|
return emptyUsage('hermes', 'Run `hermes insights` for token analytics.');
|
|
683
565
|
}
|
|
684
566
|
async function getHermesUsageLive(_opts) {
|
|
685
|
-
// Spawn `hermes insights --days 30 --source tool` and parse output.
|
|
686
567
|
return new Promise(resolve => {
|
|
687
568
|
let stdout = '';
|
|
688
569
|
let stderr = '';
|
|
@@ -725,7 +606,6 @@ async function getHermesUsageLive(_opts) {
|
|
|
725
606
|
});
|
|
726
607
|
}
|
|
727
608
|
function parseHermesInsightsOutput(text) {
|
|
728
|
-
// Minimal parser: extract Sessions / Total tokens summary line.
|
|
729
609
|
const out = [];
|
|
730
610
|
const sessionsMatch = text.match(/Sessions:\s+(\d+)/);
|
|
731
611
|
const totalTokensMatch = text.match(/Total tokens:\s+([\d,]+)/);
|
|
@@ -744,18 +624,11 @@ function parseHermesInsightsOutput(text) {
|
|
|
744
624
|
}
|
|
745
625
|
return out;
|
|
746
626
|
}
|
|
747
|
-
// ---------------------------------------------------------------------------
|
|
748
|
-
// Driver registration
|
|
749
|
-
// ---------------------------------------------------------------------------
|
|
750
627
|
const HermesDriver = {
|
|
751
628
|
id: 'hermes',
|
|
752
629
|
cmd: 'hermes',
|
|
753
630
|
thinkLabel: 'Reasoning',
|
|
754
|
-
// Hermes locks the model at profile-binding time. The ACP `session/set_model`
|
|
755
|
-
// hook exists but is unreliable across providers in current Hermes builds, so
|
|
756
|
-
// pikiloom treats the model as fixed for the session and hides the picker.
|
|
757
631
|
capabilities: { fork: false, modelSwitch: false, workflow: false },
|
|
758
|
-
// Hermes is BYOK-only — every Profile kind is fair game.
|
|
759
632
|
acceptedProviderKinds: ['anthropic', 'openai', 'openai-compatible', 'google'],
|
|
760
633
|
doStream: doHermesStream,
|
|
761
634
|
getSessions: getHermesSessions,
|
|
@@ -766,7 +639,6 @@ const HermesDriver = {
|
|
|
766
639
|
getUsageLive: getHermesUsageLive,
|
|
767
640
|
getNativeConfig: getHermesNativeConfig,
|
|
768
641
|
shutdown() {
|
|
769
|
-
/* Per-stream AcpClient is closed in doStream finally; nothing process-wide. */
|
|
770
642
|
},
|
|
771
643
|
};
|
|
772
644
|
registerDriver(HermesDriver);
|