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.
Files changed (156) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
  10. package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
  11. package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
  12. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  13. package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
  153. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  154. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  155. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  156. 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 { /* ignore individual symlink failures */ }
307
+ catch { }
358
308
  }
359
309
  }
360
- catch { /* readdir failure → fall through with whatever we managed */ }
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 { /* malformed user settings — start fresh */ }
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 { /* skip */ }
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 { /* skip */ }
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 { /* skip */ }
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);