pikiloom 0.4.25 → 0.4.27

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-B8Ai-_zs.js → AgentTab-DIP-y4dl.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-CklI2Lh4.js → DirBrowser-Dot7ua9-.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BYLt52eV.js → ExtensionsTab-GioMx4S6.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-BlwB4Hje.js → IMAccessTab-C9_r8jJP.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-DoamIusy.js → Modal-M1AP1sgR.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-BGRFYHLW.js → Modals-CCm3UUfJ.js} +1 -1
  8. package/dashboard/dist/assets/{Select-DCGLDJPj.js → Select-YubDM16P.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-Cnv9o96n.js +1 -0
  10. package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
  11. package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
  12. package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
  13. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  14. package/dashboard/dist/assets/{shared-DsGKwuTP.js → shared-C0SAInnT.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 +90 -192
  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-YR4dc2lx.js +0 -1
  152. package/dashboard/dist/assets/SessionPanel-DZziN6ft.js +0 -1
  153. package/dashboard/dist/assets/SystemTab-DaJk9s8R.js +0 -1
  154. package/dashboard/dist/assets/index-ClrAJ5vv.js +0 -23
  155. package/dashboard/dist/assets/index-CoBaePpK.js +0 -3
  156. package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
@@ -1,23 +1,9 @@
1
- /**
2
- * Public session task control surface for dashboard and API routes.
3
- */
4
1
  import path from 'node:path';
5
2
  import { getProjectSkillPaths, listSkills, stageSessionFiles, ensureManagedSession, findPikiloomSession, getDriverCapabilities, isPendingSessionId } from '../agent/index.js';
6
3
  import { loadUserConfig } from '../core/config/user-config.js';
7
4
  import { decomposeEffortSelection } from '../core/config/runtime-config.js';
8
5
  import { runtime } from './runtime.js';
9
6
  const KNOWN_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
10
- /**
11
- * Parse a `/goal[ args]` prompt typed in the dashboard chat box. Returns null
12
- * when the prompt is not a goal slash command. Sub-commands mirror the IM
13
- * `handleGoalCommand` semantics (set / clear / pause / resume / status).
14
- *
15
- * Routing /goal through the native bridge is the dashboard's analog of what
16
- * channels/{telegram,feishu,weixin}/bot.ts do via `handleGoalCommand` — before
17
- * this hook, dashboard /goal was matched by the legacy `goal` skill resolver
18
- * and silently rewritten to "Read SKILL.md and execute", which bypassed both
19
- * the claude native /goal slash command and codex's thread/goal RPC.
20
- */
21
7
  function parseGoalSlash(prompt) {
22
8
  const trimmed = prompt.trim();
23
9
  const m = trimmed.match(/^\/goal(?:\s+([\s\S]*))?$/);
@@ -35,22 +21,16 @@ function parseGoalSlash(prompt) {
35
21
  return { action: 'resume', objective: '' };
36
22
  return { action: 'set', objective: args };
37
23
  }
38
- /**
39
- * Resolve a `/skill-name [args]` prompt into the full skill execution prompt.
40
- * Returns null if the prompt is not a skill invocation or the skill is not found.
41
- */
42
24
  function resolveSkillFromPrompt(workdir, prompt) {
43
25
  const trimmed = prompt.trim();
44
26
  if (!trimmed.startsWith('/'))
45
27
  return null;
46
- // Extract command name and args: "/skill-name some args" → name="skill-name", args="some args"
47
28
  const match = trimmed.match(/^\/([^\s]+)(?:\s+(.*))?$/s);
48
29
  if (!match)
49
30
  return null;
50
31
  const name = match[1];
51
32
  const args = (match[2] || '').trim();
52
33
  const { skills } = listSkills(workdir);
53
- // Match by exact skill name (case-insensitive)
54
34
  const skill = skills.find(s => s.name.toLowerCase() === name.toLowerCase());
55
35
  if (!skill)
56
36
  return null;
@@ -62,12 +42,6 @@ function resolveSkillFromPrompt(workdir, prompt) {
62
42
  const resolvedPrompt = `${workdirHint}Read the skill definition at \`${targetPath}\` and execute the instructions defined there.${extra}`;
63
43
  return { resolvedPrompt, skillName: skill.name };
64
44
  }
65
- /**
66
- * Resolve a `handoverFrom` ref from the request's `previousAgent` /
67
- * `previousSessionId` fields, validating that it points to a real, non-self,
68
- * different-agent session managed by pikiloom. Returns null when the inputs
69
- * are absent or invalid — handover is best-effort and silent-skip on bad data.
70
- */
71
45
  function resolveHandoverFrom(request, targetAgent) {
72
46
  const prevAgent = typeof request.previousAgent === 'string' ? request.previousAgent.trim() : '';
73
47
  const prevSessionId = typeof request.previousSessionId === 'string' ? request.previousSessionId.trim() : '';
@@ -76,9 +50,9 @@ function resolveHandoverFrom(request, targetAgent) {
76
50
  if (!KNOWN_AGENTS.has(prevAgent))
77
51
  return null;
78
52
  if (prevAgent === targetAgent)
79
- return null; // same-agent continuation goes via --resume, not handover
53
+ return null;
80
54
  if (isPendingSessionId(prevSessionId))
81
- return null; // no native history yet → nothing to compact
55
+ return null;
82
56
  const record = findPikiloomSession(request.workdir, prevAgent, prevSessionId);
83
57
  if (!record)
84
58
  return null;
@@ -96,21 +70,13 @@ export async function queueDashboardSessionTask(request) {
96
70
  ? request.agent
97
71
  : runtime.getRuntimeDefaultAgent(config);
98
72
  const modelId = typeof request.model === 'string' ? request.model.trim() : '';
99
- // "ultra" is a synthetic effort rung = max depth + Workflow orchestration;
100
- // decompose it so the spawn carries a real --effort value plus the workflow
101
- // flag (the per-send pick is the single knob — no separate workflow control).
102
73
  const { effort: splitEffort, workflow: ultraWorkflow } = decomposeEffortSelection(typeof request.effort === 'string' ? request.effort : '');
103
74
  const thinkingEffort = resolvedAgent === 'gemini' ? '' : splitEffort;
104
75
  const workflowEnabled = ultraWorkflow || request.workflow === true;
105
- // /goal — route directly to the goal bridge (claude native slash, codex RPC,
106
- // or portable goal.json for gemini/hermes). Must run BEFORE skill resolution
107
- // so the legacy `goal` skill doesn't grab the prompt and rewrite it into a
108
- // "Read SKILL.md" instruction.
109
76
  const goalCmd = parseGoalSlash(request.prompt || '');
110
77
  if (goalCmd && request.sessionId && !isPendingSessionId(request.sessionId)) {
111
78
  return runDashboardGoalSlash(bot, resolvedAgent, request, goalCmd, modelId, thinkingEffort);
112
79
  }
113
- // Resolve /skill-name prompts into full skill execution prompts
114
80
  let prompt = request.prompt;
115
81
  const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
116
82
  if (skillResult) {
@@ -119,13 +85,8 @@ export async function queueDashboardSessionTask(request) {
119
85
  }
120
86
  let sessionId = request.sessionId;
121
87
  let attachments = request.attachments || [];
122
- // Resolve handover source. Only meaningful when we're about to stage a fresh
123
- // session (sessionId blank or pending). For an existing session we never
124
- // replay handover — that session's own --resume history is canonical.
125
88
  const isFreshSession = !sessionId || isPendingSessionId(sessionId);
126
89
  const handoverFrom = isFreshSession ? resolveHandoverFrom(request, resolvedAgent) : null;
127
- // Stage files into the session workspace so temp uploads survive cleanup.
128
- // Also creates a new pending session when no sessionId is provided.
129
90
  if (!sessionId || attachments.length) {
130
91
  const staged = stageSessionFiles({
131
92
  agent: resolvedAgent,
@@ -150,9 +111,6 @@ export async function queueDashboardSessionTask(request) {
150
111
  attachments,
151
112
  ...(modelId ? { modelId } : {}),
152
113
  ...(thinkingEffort ? { thinkingEffort } : {}),
153
- // Always thread the per-send workflow choice (even when false) so the run
154
- // explicitly reflects the picked rung (Ultra ⇒ on) rather than any ambient
155
- // default.
156
114
  workflowEnabled,
157
115
  ...(handoverFrom ? { handoverFrom } : {}),
158
116
  });
@@ -160,11 +118,6 @@ export async function queueDashboardSessionTask(request) {
160
118
  async function runDashboardGoalSlash(bot, agent, request, cmd, modelId, thinkingEffort) {
161
119
  const opts = { chatId: 'dashboard', modelId: modelId || undefined, thinkingEffort: thinkingEffort || undefined };
162
120
  const sessionKey = `${agent}:${request.sessionId}`;
163
- // Synthetic task id — for set / clear / resume on agents that internally
164
- // submit a follow-up task (claude native slash, portable continuation),
165
- // the real task id is owned by submitSessionTask. The dashboard's SSE
166
- // stream listener picks that up via session events; this id is just to
167
- // give the HTTP caller a non-empty taskId field.
168
121
  const taskId = `goal-${cmd.action}-${Date.now().toString(36)}`;
169
122
  try {
170
123
  if (cmd.action === 'status') {
@@ -183,7 +136,6 @@ async function runDashboardGoalSlash(bot, agent, request, cmd, modelId, thinking
183
136
  const goal = await bot.resumeSessionGoal(request.workdir, agent, request.sessionId, opts);
184
137
  return { ok: true, taskId, sessionKey, queued: false, goal };
185
138
  }
186
- // set
187
139
  const goal = await bot.setSessionGoal(request.workdir, agent, request.sessionId, {
188
140
  objective: cmd.objective,
189
141
  ...opts,
@@ -209,26 +161,17 @@ export function forkDashboardSessionTask(request) {
209
161
  return { ok: false, error: `Agent ${agent} does not support fork` };
210
162
  }
211
163
  const modelId = typeof request.model === 'string' ? request.model.trim() : '';
212
- // Same "ultra" decomposition as the send path — a forked turn launched at
213
- // Ultra inherits max depth + Workflow orchestration.
214
164
  const { effort: splitEffort, workflow: ultraWorkflow } = decomposeEffortSelection(typeof request.effort === 'string' ? request.effort : '');
215
165
  const thinkingEffort = agent === 'gemini' ? '' : splitEffort;
216
- // Resolve /skill-name shorthand the same way send/queue does, so a forked
217
- // turn that starts with `/skill-name` runs the skill against the child.
218
166
  let prompt = request.prompt;
219
167
  const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
220
168
  if (skillResult)
221
169
  prompt = skillResult.resolvedPrompt;
222
- // Make sure the parent has a managed record so `recordFork` (called after the
223
- // child stream completes) can write the lineage on both sides. Native-only
224
- // sessions (started outside pikiloom) won't have a record yet.
225
170
  ensureManagedSession({
226
171
  agent,
227
172
  workdir: request.workdir,
228
173
  sessionId: request.parentSessionId,
229
174
  });
230
- // Always create a fresh pending session for the child. stageSessionFiles
231
- // also handles attachment imports into the new workspace.
232
175
  const staged = stageSessionFiles({
233
176
  agent,
234
177
  workdir: request.workdir,
@@ -265,13 +208,6 @@ export function cancelSessionTask(taskId) {
265
208
  const result = bot.cancelTask(taskId);
266
209
  return { ok: true, recalled: result.cancelled || result.interrupted };
267
210
  }
268
- /**
269
- * Stop only the currently running task for a session — queued follow-ups are
270
- * preserved and run normally once the chain advances. Works on (agent,
271
- * sessionId) rather than a single taskId so it still functions during the
272
- * brief window after send/before the queued WS snapshot reaches the client.
273
- * Per-row × buttons (→ cancelSessionTask) cancel one queued entry at a time.
274
- */
275
211
  export function stopSessionTasks(agent, sessionId) {
276
212
  const bot = runtime.getBotRef();
277
213
  if (!bot)
@@ -286,9 +222,6 @@ export async function steerSessionTask(taskId) {
286
222
  const result = await bot.steerTask(taskId);
287
223
  return { ok: true, steered: result.steered };
288
224
  }
289
- // ---------------------------------------------------------------------------
290
- // Interaction prompt control (human-in-the-loop)
291
- // ---------------------------------------------------------------------------
292
225
  export function interactionSelectOption(promptId, optionValue, opts) {
293
226
  const bot = runtime.getBotRef();
294
227
  if (!bot)
@@ -1,9 +1,3 @@
1
- /**
2
- * models.dev catalog — read-only metadata about LLM providers and their
3
- * models (pricing, context window, capabilities). We hit the public JSON
4
- * endpoint and cache the result locally for 24h, with a fallback to the
5
- * cached copy when offline.
6
- */
7
1
  import fs from 'node:fs';
8
2
  import os from 'node:os';
9
3
  import path from 'node:path';
@@ -45,10 +39,6 @@ async function fetchFromNetwork() {
45
39
  const text = await body.text();
46
40
  return JSON.parse(text);
47
41
  }
48
- /**
49
- * Get the catalog. Returns a cached copy if fresh; otherwise fetches in the
50
- * background and falls back to the stale cache on failure.
51
- */
52
42
  export async function getModelsDevCatalog(opts = {}) {
53
43
  const now = Date.now();
54
44
  if (!opts.forceRefresh && memCache && now - memCache.fetchedAt < CACHE_TTL_MS) {
@@ -70,7 +60,7 @@ export async function getModelsDevCatalog(opts = {}) {
70
60
  }
71
61
  catch (e) {
72
62
  if (memCache)
73
- return memCache.data; // fall back to stale cache
63
+ return memCache.data;
74
64
  throw e;
75
65
  }
76
66
  finally {
@@ -80,20 +70,14 @@ export async function getModelsDevCatalog(opts = {}) {
80
70
  }
81
71
  return inflight;
82
72
  }
83
- /** Lookup a single provider by its models.dev id (e.g. "openrouter"). */
84
73
  export async function getCatalogProvider(providerId) {
85
74
  const cat = await getModelsDevCatalog().catch(() => null);
86
75
  return cat?.[providerId] || null;
87
76
  }
88
- /** Lookup a model entry within a provider. */
89
77
  export async function getCatalogModel(providerId, modelId) {
90
78
  const provider = await getCatalogProvider(providerId);
91
79
  return provider?.models?.[modelId] || null;
92
80
  }
93
- /**
94
- * Lightweight search: returns providers whose id/name match the query.
95
- * If query is empty, returns all providers sorted by id.
96
- */
97
81
  export async function searchCatalogProviders(query) {
98
82
  const cat = await getModelsDevCatalog().catch(() => ({}));
99
83
  const all = Object.values(cat);
@@ -1,18 +1,3 @@
1
- /**
2
- * Pikiloom "Model" layer — barrel export.
3
- *
4
- * The Model layer is one of the four physical layers in pikiloom's
5
- * architecture (Terminal / Agent / **Model** / Tool). It centralises:
6
- * - Provider/Profile data model (types.ts)
7
- * - Read-only catalog of providers/models from models.dev (catalog.ts)
8
- * - Persistence (store.ts) over ~/.pikiloom/setting.json
9
- * - Feishu-style credential validation (validation.ts)
10
- * - Per-agent credential injection at spawn time (injector.ts)
11
- *
12
- * Adding a new agent driver only needs to:
13
- * 1. Define a new AgentInjector entry in injector.ts
14
- * 2. Read `resolveAgentInjection(agentId)` before spawning
15
- */
16
1
  export { getModelsDevCatalog, getCatalogProvider, getCatalogModel, searchCatalogProviders, } from './catalog.js';
17
2
  export { listProviders, getProvider, addProvider, updateProvider, removeProvider, setProviderValidation, listProfiles, getProfile, addProfile, updateProfile, removeProfile, getActiveProfileId, getActiveProfile, setActiveProfile, } from './store.js';
18
3
  export { validateProvider } from './validation.js';
@@ -1,20 +1,9 @@
1
- /**
2
- * Credential injector — turn an active Profile into the env vars and
3
- * additional argv that should be applied when spawning a specific agent.
4
- *
5
- * This is the single point where pikiloom's Profile abstraction is
6
- * translated into per-agent quirks. Adding a new agent (e.g. OpenCode)
7
- * = adding one entry to AGENT_INJECT_TABLE.
8
- */
9
1
  import { resolveCredential } from '../core/secrets/index.js';
10
2
  import { writeScopedLog } from '../core/logging.js';
11
3
  import { getActiveProfile, getProvider } from './store.js';
12
4
  import { peekProviderModelInfo, prefetchProviderModels } from './provider-models.js';
13
5
  import { ensureResponsesBridge, upstreamToken } from './responses-bridge.js';
14
6
  const EMPTY = { env: {}, argvAppend: [], detail: '' };
15
- // ---------------------------------------------------------------------------
16
- // Shared host-based provider identification
17
- // ---------------------------------------------------------------------------
18
7
  function providerHost(provider) {
19
8
  try {
20
9
  return new URL(provider.baseURL).host.toLowerCase();
@@ -23,12 +12,6 @@ function providerHost(provider) {
23
12
  return '';
24
13
  }
25
14
  }
26
- /**
27
- * Stable slug used to identify the provider in TOML-style configs (Codex
28
- * `model_providers.<slug>`, Hermes ACP `<slug>:<model>`). Host-aware so a
29
- * "DeepSeek personal" provider always resolves to `deepseek`, regardless of
30
- * its display name.
31
- */
32
15
  function providerSlug(provider) {
33
16
  if (provider.kind === 'anthropic')
34
17
  return 'anthropic';
@@ -55,19 +38,9 @@ function providerSlug(provider) {
55
38
  return 'doubao';
56
39
  if (host.includes('openrouter'))
57
40
  return 'openrouter';
58
- // Unknown host: derive a stable slug from the hostname's leading label. (The
59
- // old `return 'openrouter'` fallback mis-slugged every unrecognised provider —
60
- // including localhost Ollama — as openrouter.) This never collides with
61
- // codex's reserved built-in `openai`/`oss`/`ollama` ids, which are routed
62
- // before we ever reach providerSlug.
63
41
  const label = host.replace(/:\d+$/, '').replace(/^(www|api)\./, '').split('.')[0].replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
64
42
  return label || 'byok';
65
43
  }
66
- /**
67
- * Canonical env-var name(s) carrying the credential for a provider. Returned
68
- * as a map so e.g. Google fans out to both `GOOGLE_API_KEY` and
69
- * `GEMINI_API_KEY` for SDKs that expect either.
70
- */
71
44
  function providerCredentialEnv(provider, apiKey) {
72
45
  if (provider.kind === 'anthropic')
73
46
  return { ANTHROPIC_API_KEY: apiKey };
@@ -96,53 +69,18 @@ function providerCredentialEnv(provider, apiKey) {
96
69
  return { ARK_API_KEY: apiKey, DOUBAO_API_KEY: apiKey };
97
70
  return { OPENROUTER_API_KEY: apiKey };
98
71
  }
99
- /** Pick the single env var Codex's `model_providers.<slug>.env_key` should reference. */
100
72
  function codexEnvKey(provider) {
101
73
  const keys = Object.keys(providerCredentialEnv(provider, ''));
102
74
  return keys[0] || 'OPENROUTER_API_KEY';
103
75
  }
104
- /** Escape a string for embedding inside a TOML double-quoted string literal. */
105
76
  function tomlEscape(value) {
106
77
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
107
78
  }
108
- /**
109
- * OpenAI-compatible providers that expose their Anthropic-protocol API under a
110
- * dedicated namespace on the SAME origin (not the OpenAI base we store). Keyed
111
- * by `providerSlug`; the value maps the origin → the Anthropic base URL.
112
- *
113
- * slug stored OpenAI baseURL Anthropic base
114
- * ───────── ─────────────────────────────────── ───────────────────────────────
115
- * deepseek https://api.deepseek.com https://api.deepseek.com/anthropic
116
- * kimi https://api.moonshot.cn/v1 https://api.moonshot.cn/anthropic
117
- * zai https://open.bigmodel.cn/api/paas/v4 https://open.bigmodel.cn/api/anthropic
118
- *
119
- * The SDK appends `/v1/messages`, so e.g. DeepSeek lands on
120
- * `…/anthropic/v1/messages` — the endpoint DeepSeek actually serves the
121
- * Anthropic protocol on (cf. src/agent/drivers/claude.ts).
122
- */
123
79
  const ANTHROPIC_ENDPOINT_BY_SLUG = {
124
80
  deepseek: origin => `${origin}/anthropic`,
125
81
  kimi: origin => `${origin}/anthropic`,
126
82
  zai: origin => `${origin}/api/anthropic`,
127
83
  };
128
- /**
129
- * Anthropic-protocol baseURL for Claude BYOK. Claude Code speaks the Anthropic
130
- * Messages API and appends `/v1/messages` to `ANTHROPIC_BASE_URL` itself, so
131
- * the base must point at the provider's *Anthropic-compatible* root.
132
- *
133
- * The base must NOT carry a trailing `/v1` (otherwise requests land on
134
- * `/v1/v1/messages` and 404). The canonical stored form is the OpenAI base
135
- * (so `validateProvider`'s GET /models and the Codex/Hermes injectors keep
136
- * working); we translate it here:
137
- * - Anthropic-native (and Anthropic-shaped third parties): strip a trailing
138
- * `/v1` and pass through verbatim.
139
- * - Known OpenAI-compatible providers whose Anthropic API lives under a
140
- * separate path (DeepSeek `/anthropic`, Kimi `/anthropic`, Zhipu
141
- * `/api/anthropic`): rebuild from the origin via `ANTHROPIC_ENDPOINT_BY_SLUG`.
142
- * Rebuilding from the origin (not the stored path) is idempotent — a user
143
- * who already pasted the `/anthropic` URL doesn't get `/anthropic/anthropic`.
144
- * - Anything else: best-effort, strip a trailing `/v1` (historical default).
145
- */
146
84
  function claudeAnthropicBaseURL(provider) {
147
85
  const raw = provider.baseURL.replace(/\/+$/, '');
148
86
  if (provider.kind === 'anthropic')
@@ -160,14 +98,6 @@ function claudeAnthropicBaseURL(provider) {
160
98
  }
161
99
  return raw.replace(/\/v1$/, '');
162
100
  }
163
- /**
164
- * First-party Anthropic = the official API host (`api.anthropic.com` / any
165
- * `*.anthropic.com`). A Claude route counts as "direct" when it lands here —
166
- * both the subscription path and an own-key BYOK profile pointed at
167
- * api.anthropic.com. Everything else (OpenRouter, DeepSeek, domestic series, a
168
- * self-hosted relay, localhost) is a third-party proxy. Unparseable → treat as
169
- * proxy (safe default: suppressing attribution is harmless, churning isn't).
170
- */
171
101
  function isFirstPartyAnthropic(baseURL) {
172
102
  let host;
173
103
  try {
@@ -178,17 +108,6 @@ function isFirstPartyAnthropic(baseURL) {
178
108
  }
179
109
  return host === 'anthropic.com' || host.endsWith('.anthropic.com');
180
110
  }
181
- /**
182
- * Claude Code respects `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` (or
183
- * `ANTHROPIC_AUTH_TOKEN`) as a BYOK route. The CLI itself is unchanged.
184
- * The model is overridden via opts.claudeModel (handled in stream.ts).
185
- *
186
- * For OpenAI-compatible providers (OpenRouter, DeepSeek native, …), the
187
- * baseURL must point to an Anthropic-protocol-compatible endpoint
188
- * (`/v1/messages`-shaped). `claudeAnthropicBaseURL` translates the stored
189
- * OpenAI base into the right Anthropic root per provider (e.g. DeepSeek's
190
- * `https://api.deepseek.com` → `https://api.deepseek.com/anthropic`).
191
- */
192
111
  const claudeInjector = (provider, profile, apiKey) => {
193
112
  if (provider.kind !== 'anthropic' && provider.kind !== 'openai-compatible') {
194
113
  return {
@@ -202,16 +121,6 @@ const claudeInjector = (provider, profile, apiKey) => {
202
121
  ANTHROPIC_API_KEY: apiKey,
203
122
  ANTHROPIC_AUTH_TOKEN: apiKey,
204
123
  };
205
- // Claude Code >= 2.1.36 stamps a per-request `x-anthropic-billing-header`
206
- // (cc_version / cc_entrypoint / cch=… — the cch token churns every turn).
207
- // Third-party proxies (OpenRouter, DeepSeek /anthropic, domestic series, any
208
- // OpenAI-compat or self-hosted Anthropic-shaped front) often key their
209
- // prefix/KV cache on request headers, so the churn forces a full prompt
210
- // reprocess every turn — slow and expensive. `0` makes claude omit the header
211
- // (env-bool: 0/false/no/off). Only on proxy routes: first-party Anthropic
212
- // (api.anthropic.com — subscription OR own-key direct) is left exactly as
213
- // shipped; its cache is content/breakpoint based, so attribution is irrelevant
214
- // there and we don't touch it.
215
124
  if (!isFirstPartyAnthropic(baseURL)) {
216
125
  env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0';
217
126
  }
@@ -230,43 +139,24 @@ function providerHostname(provider) {
230
139
  return '';
231
140
  }
232
141
  }
233
- /** True for localhost endpoints (Ollama / LM Studio / llama.cpp). */
234
142
  function isLocalProvider(provider) {
235
143
  const h = providerHostname(provider);
236
144
  return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '::1';
237
145
  }
238
- /** Providers that natively implement the OpenAI Responses API (codex talks to them directly). */
239
146
  function isResponsesNativeProvider(provider) {
240
147
  return providerHost(provider).includes('openrouter');
241
148
  }
242
- /** codex's built-in local provider id for a localhost endpoint. */
243
149
  function codexLocalProvider(provider) {
244
150
  let port = '';
245
151
  try {
246
152
  port = new URL(provider.baseURL).port;
247
153
  }
248
- catch { /* ignore */ }
154
+ catch { }
249
155
  if (port === '1234' || /lm\s*studio/i.test(provider.name))
250
156
  return 'lmstudio';
251
157
  return 'ollama';
252
158
  }
253
- /** Ollama keeps a prewarmed model resident for this long (its `keep_alive`). */
254
159
  const PREWARM_KEEP_ALIVE = '30m';
255
- /**
256
- * Warm a localhost model backend so the user's first real turn doesn't pay the
257
- * model cold-load (weights → memory). Fire-and-forget: never blocks the caller,
258
- * never throws.
259
- *
260
- * - Ollama has a native load endpoint — `POST /api/generate {model, keep_alive}`
261
- * with no prompt loads the weights and returns immediately; `keep_alive`
262
- * keeps them resident across the seed + real turns of a session.
263
- * - LM Studio JIT-loads on first request, so we nudge it with a 1-token
264
- * completion against its OpenAI-compatible endpoint.
265
- *
266
- * Called when a local Profile is bound (warm while the user reads / types) and
267
- * again at spawn (re-assert keep_alive). Measured: a cold gemma3:4b spent ~12s
268
- * before its first token; prewarmed, generation starts in ~2s.
269
- */
270
160
  export function prewarmLocalModel(provider, modelId) {
271
161
  if (!modelId || !isLocalProvider(provider))
272
162
  return;
@@ -290,15 +180,6 @@ export function prewarmLocalModel(provider, modelId) {
290
180
  body: JSON.stringify({ model: modelId, keep_alive: PREWARM_KEEP_ALIVE }),
291
181
  }).then(r => { writeScopedLog('model-prewarm', `ollama load ${modelId} → ${r.status}`); }, e => { writeScopedLog('model-prewarm', `ollama load ${modelId} failed: ${e?.message || e}`, { level: 'warn', stream: 'stderr' }); });
292
182
  }
293
- /**
294
- * Decide how codex should reach a provider. Codex 0.140+ speaks ONLY the
295
- * Responses API, so the route depends on what the provider implements:
296
- * openai-native genuine OpenAI → built-in `openai` provider
297
- * local-oss localhost Ollama/LMStudio → built-in `ollama`/`lmstudio` (responses)
298
- * responses-native OpenRouter, … → custom provider, responses direct
299
- * bridge chat-only (DeepSeek, Kimi, MiniMax, 豆包, Qwen, Zhipu, …)
300
- * → local Responses↔Chat bridge
301
- */
302
183
  function codexRoute(provider) {
303
184
  if (provider.kind === 'openai')
304
185
  return 'openai-native';
@@ -308,19 +189,6 @@ function codexRoute(provider) {
308
189
  return 'responses-native';
309
190
  return 'bridge';
310
191
  }
311
- /**
312
- * Codex CLI honours `model_providers.<slug>` definitions in `config.toml` and
313
- * binds the active one via `model_provider="<slug>"`. The credential lives in
314
- * the env var named by `env_key`, picked host-aware (e.g. `DEEPSEEK_API_KEY`).
315
- *
316
- * Codex 0.140+ dropped Chat Completions (`wire_api = "chat"` is rejected at
317
- * config load) — it speaks ONLY the Responses API. So this injector routes per
318
- * `codexRoute()`: responses-capable providers (OpenAI, OpenRouter, local
319
- * Ollama/LM Studio) are reached directly with the default `responses` wire;
320
- * chat-only providers (DeepSeek and the domestic series) are routed through the
321
- * in-process Responses↔Chat bridge, which codex sees as just another
322
- * responses-speaking provider on localhost.
323
- */
324
192
  const codexInjector = async (provider, profile, apiKey) => {
325
193
  if (provider.kind !== 'openai' && provider.kind !== 'openai-compatible') {
326
194
  return {
@@ -330,10 +198,6 @@ const codexInjector = async (provider, profile, apiKey) => {
330
198
  }
331
199
  const model = profile.modelId;
332
200
  const route = codexRoute(provider);
333
- // Local Ollama / LM Studio: codex's built-in provider already speaks the
334
- // Responses API to the local server. Just select it — no custom provider, no
335
- // API key. (Defining `model_providers.<built-in>` is rejected: "Built-in
336
- // providers cannot be overridden.")
337
201
  if (route === 'local-oss') {
338
202
  const local = codexLocalProvider(provider);
339
203
  prewarmLocalModel(provider, model);
@@ -344,7 +208,6 @@ const codexInjector = async (provider, profile, apiKey) => {
344
208
  detail: `Codex local → ${provider.name} / ${model} (built-in ${local}, responses)`,
345
209
  };
346
210
  }
347
- // Genuine OpenAI: use the built-in `openai` provider; inject the key (+ base).
348
211
  if (route === 'openai-native') {
349
212
  const env = { OPENAI_API_KEY: apiKey };
350
213
  if (provider.baseURL)
@@ -358,9 +221,6 @@ const codexInjector = async (provider, profile, apiKey) => {
358
221
  }
359
222
  const slug = providerSlug(provider);
360
223
  const envKey = codexEnvKey(provider);
361
- // Chat-only providers: route through the local Responses↔Chat bridge. Codex
362
- // forwards `Authorization: Bearer <key>` (from env_key) to the bridge, which
363
- // relays it to the upstream chat endpoint — the bridge never stores secrets.
364
224
  if (route === 'bridge') {
365
225
  const port = await ensureResponsesBridge();
366
226
  const base = `http://127.0.0.1:${port}/u/${upstreamToken(provider.baseURL)}`;
@@ -377,8 +237,6 @@ const codexInjector = async (provider, profile, apiKey) => {
377
237
  detail: `Codex BYOK → ${provider.name} / ${model} via Responses↔Chat bridge (provider=${slug})`,
378
238
  };
379
239
  }
380
- // responses-native (OpenRouter, …): point codex straight at the provider's
381
- // Responses endpoint (wire_api omitted ⇒ codex default `responses`).
382
240
  return {
383
241
  env: { [envKey]: apiKey },
384
242
  argvAppend: [],
@@ -392,7 +250,6 @@ const codexInjector = async (provider, profile, apiKey) => {
392
250
  detail: `Codex BYOK → ${provider.name} / ${model} (provider=${slug}, native responses)`,
393
251
  };
394
252
  };
395
- /** Gemini CLI accepts `GEMINI_API_KEY` but does not allow custom baseURL. */
396
253
  const geminiInjector = (provider, profile, apiKey) => {
397
254
  if (provider.kind !== 'google') {
398
255
  return {
@@ -407,29 +264,9 @@ const geminiInjector = (provider, profile, apiKey) => {
407
264
  detail: `Gemini BYOK → ${provider.name} / ${profile.modelId}`,
408
265
  };
409
266
  };
410
- /**
411
- * Hermes injector. Two channels:
412
- * 1. Env vars carry the credential — `hermes acp` honours `OPENROUTER_API_KEY`,
413
- * `ANTHROPIC_API_KEY`, etc. just like the top-level `hermes` CLI.
414
- * 2. The model is bound *per-session* by the driver via the ACP
415
- * `session/set_model` request — `hermes acp` does NOT accept `-m` /
416
- * `--provider` (only `--accept-hooks`); appending `-m` here used to make
417
- * every BYOK-bound spawn die with `unrecognized arguments`.
418
- *
419
- * The model is handed to the driver via `modelOverride` (an ACP-style
420
- * `<provider>:<model>` string). The driver passes it to `session/set_model`
421
- * after `session/new` returns; if the user has no Profile bound, no
422
- * `set_model` call is made and Hermes uses its `~/.hermes/config.yaml`
423
- * default.
424
- */
425
267
  const hermesInjector = (provider, profile, apiKey) => {
426
268
  const env = providerCredentialEnv(provider, apiKey);
427
269
  const slug = providerSlug(provider);
428
- // Only strip a leading `<slug>/` or `<slug>:` if the user accidentally
429
- // stored a redundant provider prefix. Do NOT strip the *first segment* of
430
- // a slash-separated model id wholesale — for OpenRouter the canonical
431
- // model id is `vendor/model` (e.g. `deepseek/deepseek-v4-flash`), and
432
- // dropping the `vendor/` part yields a non-existent model.
433
270
  let bareModel = profile.modelId;
434
271
  if (bareModel.startsWith(`${slug}/`) || bareModel.startsWith(`${slug}:`)) {
435
272
  bareModel = bareModel.slice(slug.length + 1);
@@ -447,14 +284,6 @@ const AGENT_INJECT_TABLE = {
447
284
  gemini: geminiInjector,
448
285
  hermes: hermesInjector,
449
286
  };
450
- // ---------------------------------------------------------------------------
451
- // Public API
452
- // ---------------------------------------------------------------------------
453
- /**
454
- * Resolve the active Profile for an agent and return the spawn config to
455
- * inject. Returns `null` when no Profile is bound (caller should fall back
456
- * to the agent's native auth / default model).
457
- */
458
287
  export async function resolveAgentInjection(agentId) {
459
288
  const profile = getActiveProfile(agentId);
460
289
  if (!profile)
@@ -465,9 +294,6 @@ export async function resolveAgentInjection(agentId) {
465
294
  const injector = AGENT_INJECT_TABLE[agentId];
466
295
  if (!injector)
467
296
  return null;
468
- // Local providers (Ollama / LM Studio / llama.cpp) need no credential — codex
469
- // reaches them via its built-in localhost provider with no auth. Don't let a
470
- // missing/placeholder key block an otherwise-valid local binding.
471
297
  let apiKey = '';
472
298
  try {
473
299
  apiKey = await resolveCredential(provider.credential);
@@ -478,12 +304,7 @@ export async function resolveAgentInjection(agentId) {
478
304
  }
479
305
  }
480
306
  const result = await injector(provider, profile, apiKey);
481
- // Attach the provider display name so renders can surface "via <provider>"
482
- // — this is what tells the user the turn is going through a BYOK route.
483
307
  result.providerName = provider.name;
484
- // Attach the real context window from the provider's cached /models listing.
485
- // Sync peek — no network blocking; on miss we kick off a fetch so the *next*
486
- // session has it, and this turn falls back to the CLI's advertised value.
487
308
  const cached = peekProviderModelInfo(provider.id, profile.modelId);
488
309
  if (cached?.contextLength && cached.contextLength > 0) {
489
310
  result.contextWindow = cached.contextLength;
@@ -493,7 +314,6 @@ export async function resolveAgentInjection(agentId) {
493
314
  }
494
315
  return result;
495
316
  }
496
- /** Returns `true` if the given agent is bound to a Profile. */
497
317
  export function isAgentBoundToProfile(agentId) {
498
318
  return getActiveProfile(agentId) !== null;
499
319
  }