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,29 +1,6 @@
1
- /**
2
- * Provider model-list cache.
3
- *
4
- * Backs the GET /api/models/providers/:id/models endpoint and the agent-status
5
- * + IM /models surfaces. Each entry is a list of model ids the provider's
6
- * /models endpoint reported, plus a fetch timestamp for TTL invalidation.
7
- *
8
- * Cache is in-memory only — providers' validation state already persists in
9
- * setting.json; the model list itself can be re-fetched cheaply on demand.
10
- */
11
1
  import { getProvider } from './store.js';
12
2
  import { validateProvider } from './validation.js';
13
- const TTL_MS = 30 * 60 * 1000; // 30 minutes
14
- // Cache is pinned to globalThis so module re-instantiation under tsx ESM
15
- // (e.g. when the same file is imported via different specifier strings) can't
16
- // fragment it into multiple disjoint maps. Without this, `getProviderModelList`
17
- // from `dashboard/routes/models.ts` (barrel) and `peekProviderModelList` from
18
- // `dashboard/routes/agents.ts` (also barrel) were writing/reading two
19
- // different Map instances, making the peek perpetually miss.
20
- // Cache pinned to globalThis so module re-instantiation under tsx ESM
21
- // (where the same file imported via different specifier strings ends up as
22
- // separate ESM module instances) can't fragment it into multiple disjoint
23
- // maps. Without this, `getProviderModelList` from `dashboard/routes/models.ts`
24
- // and `peekProviderModelList` from `dashboard/routes/agents.ts` would be
25
- // writing/reading two different Map instances and the peek would
26
- // perpetually miss.
3
+ const TTL_MS = 30 * 60 * 1000;
27
4
  const GLOBAL_KEY = Symbol.for('pikiloom.providerModelsCache');
28
5
  const _existing = globalThis[GLOBAL_KEY];
29
6
  const cache = _existing || new Map();
@@ -34,10 +11,6 @@ function isFresh(entry, provider) {
34
11
  return false;
35
12
  return (Date.now() - entry.fetchedAt) < TTL_MS;
36
13
  }
37
- /**
38
- * Get the model list for a provider, fetching from /models on cache miss or
39
- * when the provider config has been updated since the last fetch.
40
- */
41
14
  export async function getProviderModelList(providerId, opts = {}) {
42
15
  const provider = getProvider(providerId);
43
16
  if (!provider)
@@ -66,19 +39,9 @@ export async function getProviderModelList(providerId, opts = {}) {
66
39
  fromCache: false,
67
40
  };
68
41
  }
69
- /**
70
- * Invalidate cached model list (e.g. after a provider edit/delete).
71
- */
72
42
  export function invalidateProviderModels(providerId) {
73
43
  cache.delete(providerId);
74
44
  }
75
- /**
76
- * Synchronous peek for a single model's cached metadata (context length,
77
- * pricing). Returns `null` on cache miss or when the entry is stale relative
78
- * to the provider's `updatedAt` — callers should treat that as "unknown" and
79
- * fall back to whatever the agent CLI reports. Pair with
80
- * `prefetchProviderModels` to populate the cache lazily for the next call.
81
- */
82
45
  export function peekProviderModelInfo(providerId, modelId) {
83
46
  const provider = getProvider(providerId);
84
47
  if (!provider)
@@ -88,11 +51,6 @@ export function peekProviderModelInfo(providerId, modelId) {
88
51
  return null;
89
52
  return entry.modelInfos.find(info => info.id === modelId) ?? null;
90
53
  }
91
- /**
92
- * Synchronous peek for the full cached model list of a provider. Returns
93
- * `null` on cache miss / stale entry — callers should fall back and let a
94
- * background refresh populate it (`prefetchProviderModels`).
95
- */
96
54
  export function peekProviderModelList(providerId) {
97
55
  const provider = getProvider(providerId);
98
56
  if (!provider)
@@ -102,11 +60,6 @@ export function peekProviderModelList(providerId) {
102
60
  return null;
103
61
  return entry.modelInfos;
104
62
  }
105
- /**
106
- * Fire-and-forget cache fill. Safe to call repeatedly: no-ops when the cache
107
- * is already fresh, otherwise triggers a single in-flight fetch and discards
108
- * the result (the next sync peek will see the populated cache).
109
- */
110
63
  export function prefetchProviderModels(providerId) {
111
64
  void getProviderModelList(providerId).catch(() => { });
112
65
  }
@@ -1,27 +1,3 @@
1
- /**
2
- * Responses↔Chat bridge.
3
- *
4
- * Codex 0.140+ speaks ONLY the OpenAI Responses API (`wire_api = "chat"` was
5
- * removed). Many OpenAI-compatible providers — DeepSeek, Kimi/Moonshot,
6
- * MiniMax, 豆包/Doubao, Qwen/DashScope, Zhipu, … — implement ONLY the Chat
7
- * Completions API. This in-process HTTP server bridges the two so codex can
8
- * drive any chat-only provider:
9
- *
10
- * codex ──(Responses API)──▶ bridge ──(Chat Completions)──▶ upstream provider
11
- *
12
- * One server instance routes every upstream: the upstream base URL is encoded
13
- * (base64url) into the request path (`/u/<token>/responses`). The caller's
14
- * Authorization header is forwarded verbatim, so the bridge never reads or
15
- * stores credentials — codex injects `Authorization: Bearer <key>` from the
16
- * provider's `env_key`, and we relay it upstream.
17
- *
18
- * Translation is intentionally NON-incremental: we call the upstream with
19
- * `stream:false`, then synthesise a complete, spec-shaped Responses SSE stream.
20
- * Codex rebuilds a turn from `response.output_item.done` items plus the final
21
- * `response.completed`, so a fully-populated terminal payload is authoritative;
22
- * this sidesteps fragile per-token delta bookkeeping while still surfacing
23
- * assistant text AND tool/function calls (apply_patch, shell, MCP tools).
24
- */
25
1
  import http from 'node:http';
26
2
  import { writeScopedLog } from '../core/logging.js';
27
3
  const SCOPE = 'model-bridge';
@@ -36,7 +12,6 @@ function genId(prefix) {
36
12
  return `${prefix}_${Date.now().toString(36)}${idCounter.toString(36)}`;
37
13
  }
38
14
  function num(v) { return typeof v === 'number' && Number.isFinite(v) ? v : 0; }
39
- /** base64url-encode an upstream base URL so it survives as a single path segment. */
40
15
  export function upstreamToken(baseURL) {
41
16
  return Buffer.from(baseURL, 'utf8').toString('base64url');
42
17
  }
@@ -48,7 +23,6 @@ function decodeUpstream(token) {
48
23
  return null;
49
24
  }
50
25
  }
51
- /** Start (or reuse) the singleton bridge server; resolves to its localhost port. */
52
26
  export async function ensureResponsesBridge() {
53
27
  if (server && listenPort)
54
28
  return listenPort;
@@ -76,13 +50,10 @@ export function shutdownResponsesBridge() {
76
50
  try {
77
51
  server?.close();
78
52
  }
79
- catch { /* ignore */ }
53
+ catch { }
80
54
  server = null;
81
55
  listenPort = 0;
82
56
  }
83
- // ---------------------------------------------------------------------------
84
- // HTTP handling
85
- // ---------------------------------------------------------------------------
86
57
  function handleRequest(req, res) {
87
58
  const url = new URL(req.url || '/', 'http://127.0.0.1');
88
59
  const m = url.pathname.match(/^\/u\/([^/]+)\/(responses|models)$/);
@@ -96,8 +67,6 @@ function handleRequest(req, res) {
96
67
  return;
97
68
  }
98
69
  if (m[2] === 'models') {
99
- // Codex's model-catalog refresh is best-effort; an empty list keeps it quiet
100
- // and never blocks the turn.
101
70
  res.writeHead(200, { 'content-type': 'application/json' });
102
71
  res.end(JSON.stringify({ object: 'list', data: [], models: [] }));
103
72
  return;
@@ -148,11 +117,6 @@ async function handleResponses(req, res, upstreamBase, body) {
148
117
  sendResponsesError(res, `upstream ${upstreamResp.status}: ${raw.slice(0, 500)}`);
149
118
  return;
150
119
  }
151
- // Forward the upstream Chat Completions SSE incrementally, translating each
152
- // delta into Responses events, so codex (and the dashboard) render the answer
153
- // progressively — token-by-token — instead of popping it in at once. Codex
154
- // still rebuilds the turn from the terminal `response.output_item.done` +
155
- // `response.completed`, which we always emit.
156
120
  res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
157
121
  res.flushHeaders?.();
158
122
  let seq = 0;
@@ -285,7 +249,7 @@ function sendResponsesError(res, message) {
285
249
  try {
286
250
  res.end();
287
251
  }
288
- catch { /* ignore */ }
252
+ catch { }
289
253
  return;
290
254
  }
291
255
  res.writeHead(200, {
@@ -300,9 +264,6 @@ function sendResponsesError(res, message) {
300
264
  emit({ type: 'response.failed', response: { id, object: 'response', status: 'failed', error: { code: 'bridge_error', message }, output: [] } });
301
265
  res.end();
302
266
  }
303
- // ---------------------------------------------------------------------------
304
- // Request translation: Responses → Chat Completions
305
- // ---------------------------------------------------------------------------
306
267
  function asText(content) {
307
268
  if (typeof content === 'string')
308
269
  return content;
@@ -352,7 +313,6 @@ function toChatRequest(body) {
352
313
  });
353
314
  }
354
315
  else if (type === 'reasoning') {
355
- // Chat models cannot ingest prior reasoning items — drop.
356
316
  }
357
317
  }
358
318
  const tools = Array.isArray(body.tools)
@@ -388,13 +348,11 @@ function toChatTool(t) {
388
348
  },
389
349
  };
390
350
  }
391
- // Codex built-in custom tools (e.g. local_shell) and web_search aren't
392
- // expressible as chat functions — drop; codex falls back to its function tools.
393
351
  return null;
394
352
  }
395
353
  function toChatToolChoice(tc) {
396
354
  if (typeof tc === 'string')
397
- return tc; // auto | none | required
355
+ return tc;
398
356
  if (tc?.type === 'function' && tc.name)
399
357
  return { type: 'function', function: { name: tc.name } };
400
358
  if (tc?.type === 'function' && tc.function)
@@ -1,13 +1,6 @@
1
- /**
2
- * Persistence for Provider / Profile / activeProfileByAgent.
3
- * Reads and writes the `models` section of ~/.pikiloom/setting.json.
4
- */
5
1
  import { randomUUID } from 'node:crypto';
6
2
  import { loadUserConfig, saveUserConfig } from '../core/config/user-config.js';
7
3
  import { persistSecret, forgetSecret } from '../core/secrets/index.js';
8
- // ---------------------------------------------------------------------------
9
- // Read helpers
10
- // ---------------------------------------------------------------------------
11
4
  function getModelLayer() {
12
5
  const config = loadUserConfig();
13
6
  return config.models || {};
@@ -62,11 +55,9 @@ export async function updateProvider(id, patch) {
62
55
  credential = patch.credentialRef;
63
56
  else if (patch.apiKey !== undefined) {
64
57
  if (existing.credential.source === 'keychain') {
65
- // overwrite same keychain slot
66
58
  credential = await persistSecret(existing.credential.account, patch.apiKey);
67
59
  }
68
60
  else {
69
- // upgrade from inline/env/command to keychain
70
61
  credential = await persistSecret(`provider/${id}`, patch.apiKey);
71
62
  }
72
63
  }
@@ -92,13 +83,11 @@ export async function removeProvider(id) {
92
83
  if (!existing)
93
84
  return false;
94
85
  delete providers[id];
95
- // also drop any profiles bound to this provider
96
86
  const profiles = { ...(layer.profiles || {}) };
97
87
  for (const [pid, prof] of Object.entries(profiles)) {
98
88
  if (prof.providerId === id)
99
89
  delete profiles[pid];
100
90
  }
101
- // and any active bindings
102
91
  const bindings = { ...(layer.activeProfileByAgent || {}) };
103
92
  for (const agentId of Object.keys(bindings)) {
104
93
  const profileId = bindings[agentId];
@@ -127,9 +116,6 @@ export function getProfile(id) {
127
116
  return layer.profiles?.[id] || null;
128
117
  }
129
118
  function defaultProfileName(_providerName, modelId, _effort) {
130
- // Keep the auto-generated label short: the brand icon already carries the
131
- // provider, and effort lives in its own pill on the card. Just the model id
132
- // gives the cleanest tile + IM list, with the user free to override.
133
119
  return modelId;
134
120
  }
135
121
  export function addProfile(input) {
@@ -188,9 +174,6 @@ export function removeProfile(id) {
188
174
  writeModelLayer({ ...layer, profiles, activeProfileByAgent: bindings });
189
175
  return true;
190
176
  }
191
- // ---------------------------------------------------------------------------
192
- // Active binding
193
- // ---------------------------------------------------------------------------
194
177
  export function getActiveProfileId(agentId) {
195
178
  const layer = getModelLayer();
196
179
  return layer.activeProfileByAgent?.[agentId] || null;
@@ -1,13 +1 @@
1
- /**
2
- * Pikiloom "Model" layer — Provider/Profile data model.
3
- *
4
- * Provider = a configured endpoint + credential reference (e.g. "OpenRouter
5
- * personal", "Anthropic direct"). Holds baseURL + extra headers.
6
- * Profile = a Provider + modelId + tuning params (effort, max output).
7
- * This is the unit a user binds to an agent.
8
- *
9
- * Profiles are the smallest selectable unit because Hermes (and most coding
10
- * agents) only support one model per session. Binding `activeProfileByAgent`
11
- * tells the driver which Profile to use when spawning that agent.
12
- */
13
1
  export {};
@@ -1,10 +1,3 @@
1
- /**
2
- * Provider credential validation — Feishu-style explicit check.
3
- *
4
- * Validation strategy: GET ${baseURL}/models with the resolved API key.
5
- * Status semantics mirror ChannelSetupState in core/config/validation.ts:
6
- * missing | invalid | error | ready
7
- */
8
1
  import { request } from 'undici';
9
2
  import { resolveCredential } from '../core/secrets/index.js';
10
3
  const VALIDATION_TIMEOUT_MS = 10_000;
@@ -12,13 +5,10 @@ function buildHeaders(provider, apiKey) {
12
5
  const base = { 'Accept': 'application/json' };
13
6
  switch (provider.kind) {
14
7
  case 'anthropic':
15
- // Anthropic models endpoint is /v1/models with x-api-key + anthropic-version
16
8
  base['x-api-key'] = apiKey;
17
9
  base['anthropic-version'] = '2023-06-01';
18
10
  break;
19
11
  case 'google':
20
- // Google AI Studio uses ?key= query param, not Authorization header.
21
- // Caller appends ?key= in URL builder below.
22
12
  break;
23
13
  case 'openai':
24
14
  case 'openai-compatible':
@@ -33,14 +23,12 @@ function buildHeaders(provider, apiKey) {
33
23
  function modelsUrl(provider, apiKey) {
34
24
  const base = provider.baseURL.replace(/\/+$/, '');
35
25
  if (provider.kind === 'google') {
36
- // Google AI Studio: GET https://generativelanguage.googleapis.com/v1beta/models?key=API_KEY
37
26
  const sep = base.includes('?') ? '&' : '?';
38
27
  return `${base}/models${sep}key=${encodeURIComponent(apiKey)}`;
39
28
  }
40
29
  if (provider.kind === 'anthropic') {
41
30
  return `${base}/v1/models`;
42
31
  }
43
- // OpenAI-compatible
44
32
  return `${base}/models`;
45
33
  }
46
34
  function toNumber(value) {
@@ -53,11 +41,6 @@ function toNumber(value) {
53
41
  }
54
42
  return undefined;
55
43
  }
56
- /**
57
- * Convert a per-token USD price (the format OpenRouter & most OpenAI-compatible
58
- * APIs use) to USD per 1M tokens. We preserve up to 4 decimals so micro-prices
59
- * don't round to zero.
60
- */
61
44
  function perTokenToPerMillion(value) {
62
45
  const n = toNumber(value);
63
46
  if (n === undefined)
@@ -177,7 +160,7 @@ export async function validateProvider(provider) {
177
160
  try {
178
161
  parsed = JSON.parse(bodyText);
179
162
  }
180
- catch { /* unparsed body — still treat 2xx as ready */ }
163
+ catch { }
181
164
  const modelInfos = extractModelInfos(parsed);
182
165
  const models = modelInfos.map(info => info.id);
183
166
  return {
@@ -1,19 +1,3 @@
1
- /**
2
- * pikichannel/adapter-pikiloom.ts — the reference {@link SessionSource}.
3
- *
4
- * Binds the universal protocol to pikiloom's live runtime *without touching the
5
- * bot*. It is a peer consumer of the very same surfaces the Web Dashboard uses:
6
- * - data plane: the `runtime.events` 'dashboard-event' bus (same bus the
7
- * dashboard WebSocket layer listens to). We must NOT call
8
- * `bot.onStreamSnapshot` — that is a single-slot callback already owned by
9
- * the runtime; re-registering it would silently break the dashboard.
10
- * - control plane: the exported session-control functions
11
- * (`queueDashboardSessionTask`, `stopSessionTasks`, …) — the same entry
12
- * points the dashboard REST routes call.
13
- *
14
- * The StreamSnapshot → UniversalSnapshot projection lives here, keeping the
15
- * protocol package free of any pikiloom-internal type.
16
- */
17
1
  import { EventEmitter } from 'node:events';
18
2
  import { runtime } from '../dashboard/runtime.js';
19
3
  import { queueDashboardSessionTask, stopSessionTasks, steerSessionTask, cancelSessionTask, interactionSelectOption, interactionSubmitText, interactionSkip, interactionCancel, } from '../dashboard/session-control.js';
@@ -45,8 +29,6 @@ function projectInteractions(snap) {
45
29
  })),
46
30
  }));
47
31
  }
48
- /** Strip empty optionals so the projection is lean AND deterministic (the diff
49
- * baseline must be stable: a field is either consistently present or absent). */
50
32
  function compact(obj) {
51
33
  for (const k of Object.keys(obj)) {
52
34
  const v = obj[k];
@@ -57,7 +39,6 @@ function compact(obj) {
57
39
  }
58
40
  return obj;
59
41
  }
60
- /** Project pikiloom's StreamSnapshot into the agent-agnostic wire snapshot. */
61
42
  export function projectSnapshot(sessionKey, snap) {
62
43
  const { agent } = splitKey(sessionKey);
63
44
  const meta = snap.previewMeta;
@@ -105,34 +86,24 @@ function metaFromSnapshot(sessionKey, snap) {
105
86
  const title = (snap.prompt || snap.text || '').slice(0, 80) || null;
106
87
  return { sessionKey, agent: snap.agent, title, phase: snap.phase, updatedAt: snap.updatedAt };
107
88
  }
108
- /**
109
- * The pikiloom SessionSource. One instance per process; it attaches to the
110
- * runtime event bus lazily on first `onUpdate`.
111
- */
112
89
  export class PikiloomSessionSource {
113
90
  forwarder;
114
91
  seqs = new Map();
115
92
  known = new Map();
116
93
  bus = new EventEmitter();
117
94
  wired = false;
118
- /** @param forwarder forwards tunneled `/api/*` requests to the HTTP router. */
119
95
  constructor(forwarder) {
120
96
  this.forwarder = forwarder;
121
97
  }
122
98
  hostInfo() {
123
99
  return { name: 'pikiloom', version: VERSION, capabilities: CAPABILITIES, authRequired: true };
124
100
  }
125
- /** Control-plane HTTP tunnel — delegates to the embedder-supplied forwarder
126
- * (the dashboard's Hono router). The host has already enforced auth + the
127
- * `/api/*` allowlist before we get here. */
128
101
  async handleRequest(req) {
129
102
  if (!this.forwarder)
130
103
  return { status: 503, body: 'tunnel forwarder not configured' };
131
104
  return this.forwarder(req);
132
105
  }
133
106
  listSessions() {
134
- // Most-recent first, bounded so a long-lived host never ships an unbounded
135
- // list over the wire (remote clients want a recent picker, not all history).
136
107
  return Array.from(this.known.values()).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)).slice(0, 50);
137
108
  }
138
109
  getSnapshot(sessionKey) {
@@ -154,7 +125,6 @@ export class PikiloomSessionSource {
154
125
  this.bus.on('sessions', handler);
155
126
  return () => this.bus.off('sessions', handler);
156
127
  }
157
- /** Subscribe once to the runtime's dashboard-event bus and fan out internally. */
158
128
  ensureWired() {
159
129
  if (this.wired)
160
130
  return;
@@ -172,7 +142,6 @@ export class PikiloomSessionSource {
172
142
  }
173
143
  });
174
144
  }
175
- // -- control plane (delegates to the shared session-control surface) -----
176
145
  async prompt(cmd) {
177
146
  const config = loadUserConfig();
178
147
  const workdir = cmd.workdir || runtime.getRequestWorkdir(config);
@@ -1,15 +1,3 @@
1
- /**
2
- * pikichannel/code.ts — the shareable connection code (server side).
3
- *
4
- * One short string a host shows and a client pastes. Same format the browser
5
- * `endpoint.ts` decodes — keep the keys (h/r/n/t) in lockstep:
6
- * h = direct host authority r = rendezvous URL n = nodeId t = token
7
- *
8
- * `buildServerCode` picks the best reachability for THIS host:
9
- * - public address set → DIRECT code {h,t} (public-IP server; no NAT hop)
10
- * - else rendezvous on → REMOTE code {r,n,t} (NAT traversal)
11
- * - else → none (only reachable locally)
12
- */
13
1
  export function encodeConnectionCode(d) {
14
2
  const lean = {};
15
3
  if (d.host)
@@ -1,22 +1,10 @@
1
- /**
2
- * pikichannel/codec.ts — frame (de)serialization for the L2 protocol.
3
- *
4
- * The transport layer (L1) moves opaque frames; this module is the only place
5
- * that knows how a {@link ServerMessage} / {@link ClientMessage} becomes a frame
6
- * and back. Today that is newline-free JSON text. The indirection is deliberate:
7
- * swapping to a binary codec (CBOR / protobuf) later is a change here alone, with
8
- * the wire-format tag negotiated in the handshake — no transport or host edits.
9
- */
10
1
  export const DEFAULT_WIRE_FORMAT = 'json';
11
- /** Encode a host→client message into a transport frame. */
12
2
  export function encodeServer(msg, _fmt = DEFAULT_WIRE_FORMAT) {
13
3
  return JSON.stringify(msg);
14
4
  }
15
- /** Encode a client→host message into a transport frame. */
16
5
  export function encodeClient(msg, _fmt = DEFAULT_WIRE_FORMAT) {
17
6
  return JSON.stringify(msg);
18
7
  }
19
- /** Coerce a transport frame (string / Buffer / ArrayBuffer) into text. */
20
8
  function frameToText(raw) {
21
9
  if (typeof raw === 'string')
22
10
  return raw;
@@ -24,7 +12,6 @@ function frameToText(raw) {
24
12
  return raw.toString('utf8');
25
13
  return Buffer.from(new Uint8Array(raw)).toString('utf8');
26
14
  }
27
- /** Decode a client→host frame. Returns null on malformed input (never throws). */
28
15
  export function decodeClient(raw, _fmt = DEFAULT_WIRE_FORMAT) {
29
16
  try {
30
17
  const value = JSON.parse(frameToText(raw));
@@ -36,7 +23,6 @@ export function decodeClient(raw, _fmt = DEFAULT_WIRE_FORMAT) {
36
23
  return null;
37
24
  }
38
25
  }
39
- /** Decode a host→client frame. Returns null on malformed input (never throws). */
40
26
  export function decodeServer(raw, _fmt = DEFAULT_WIRE_FORMAT) {
41
27
  try {
42
28
  const value = JSON.parse(frameToText(raw));
@@ -1,19 +1,3 @@
1
- /**
2
- * pikichannel/host.ts — the host SDK (transport- and agent-agnostic).
3
- *
4
- * `PikichannelHost` speaks the L2 protocol over any {@link ChannelConnection},
5
- * driven by a {@link SessionSource} the embedder supplies. The host knows
6
- * nothing about pikiloom: porting pikichannel to another agent runtime is a
7
- * matter of implementing SessionSource (see adapter-pikiloom.ts).
8
- *
9
- * It owns three cross-cutting concerns the protocol requires:
10
- * - Auth: a peer must pass `authenticate(token, remote)` (loopback exempt by
11
- * policy) before ANY session data or control is processed.
12
- * - Delta: it holds the latest full snapshot per session and emits a `full`
13
- * patch on (re)subscribe / resync, deltas thereafter — O(n) per stream.
14
- * - Fan-out: one delta per update is broadcast to every caught-up subscriber;
15
- * a fresh subscriber gets a `full` so it shares the same baseline.
16
- */
17
1
  import { PROTOCOL_VERSION, diffSnapshot, } from './protocol.js';
18
2
  import { encodeServer, decodeClient } from './codec.js';
19
3
  const SUBSCRIBE_ALL = '*';
@@ -24,7 +8,6 @@ export class PikichannelHost {
24
8
  peers = new Set();
25
9
  unsubscribers = [];
26
10
  started = false;
27
- /** Latest full snapshot per session — the delta baseline shared by all peers. */
28
11
  lastFull = new Map();
29
12
  lastSeq = new Map();
30
13
  constructor(source, authenticate = () => true, log = () => { }) {
@@ -32,7 +15,6 @@ export class PikichannelHost {
32
15
  this.authenticate = authenticate;
33
16
  this.log = log;
34
17
  }
35
- /** Wire host-level subscriptions to the source (once). */
36
18
  start() {
37
19
  if (this.started)
38
20
  return;
@@ -45,20 +27,19 @@ export class PikichannelHost {
45
27
  try {
46
28
  u();
47
29
  }
48
- catch { /* ignore */ }
30
+ catch { }
49
31
  }
50
32
  for (const peer of this.peers) {
51
33
  try {
52
34
  peer.conn.close();
53
35
  }
54
- catch { /* ignore */ }
36
+ catch { }
55
37
  }
56
38
  this.peers.clear();
57
39
  this.lastFull.clear();
58
40
  this.lastSeq.clear();
59
41
  this.started = false;
60
42
  }
61
- /** Adopt a freshly-established connection from any transport binding. */
62
43
  handleConnection(conn) {
63
44
  if (!this.started)
64
45
  this.start();
@@ -78,7 +59,6 @@ export class PikichannelHost {
78
59
  this.log(`peer disconnected id=${conn.id} via=${conn.kind}`);
79
60
  });
80
61
  }
81
- // -- delta baseline ------------------------------------------------------
82
62
  onSourceUpdate(sessionKey, full, seq) {
83
63
  const prev = this.lastFull.get(sessionKey);
84
64
  const patch = prev ? diffSnapshot(prev, full) : { full };
@@ -90,7 +70,6 @@ export class PikichannelHost {
90
70
  this.send(peer, frame);
91
71
  }
92
72
  }
93
- /** Send the current full snapshot for a session to one peer (subscribe / resync). */
94
73
  sendFull(peer, sessionKey) {
95
74
  let full = this.lastFull.get(sessionKey);
96
75
  let seq = this.lastSeq.get(sessionKey);
@@ -105,9 +84,7 @@ export class PikichannelHost {
105
84
  }
106
85
  this.send(peer, { type: 'session', sessionKey, seq: seq || 0, patch: { full } });
107
86
  }
108
- // -- inbound -------------------------------------------------------------
109
87
  async handleClientMessage(peer, msg) {
110
- // Ping is always allowed (RTT / keepalive). Everything else is gated on auth.
111
88
  if (msg.type === 'ping') {
112
89
  this.send(peer, { type: 'pong', t: msg.t });
113
90
  return;
@@ -145,7 +122,7 @@ export class PikichannelHost {
145
122
  }
146
123
  switch (msg.type) {
147
124
  case 'hello':
148
- return; // already authenticated; ignore duplicate hello
125
+ return;
149
126
  case 'subscribe': {
150
127
  peer.subs.add(msg.sessionKey);
151
128
  if (msg.sessionKey === SUBSCRIBE_ALL) {
@@ -207,9 +184,6 @@ export class PikichannelHost {
207
184
  }
208
185
  case 'request': {
209
186
  const id = msg.id;
210
- // Only the management API is tunnelable — never static, the SPA shell,
211
- // or loopback-gated endpoints (e.g. /pikichannel/pair would otherwise
212
- // leak the token to a remote peer via the host's own fetch).
213
187
  if (!msg.path || !msg.path.startsWith('/api/')) {
214
188
  this.send(peer, { type: 'response', id, status: 403, error: 'only /api/* is tunnelable' });
215
189
  return;
@@ -229,7 +203,6 @@ export class PikichannelHost {
229
203
  }
230
204
  }
231
205
  }
232
- // -- outbound ------------------------------------------------------------
233
206
  broadcast(frame, authedOnly = false) {
234
207
  for (const peer of this.peers)
235
208
  if (!authedOnly || peer.authed)
@@ -241,11 +214,9 @@ export class PikichannelHost {
241
214
  try {
242
215
  peer.conn.send(encodeServer(frame));
243
216
  }
244
- catch { /* drop on closed pipe */ }
217
+ catch { }
245
218
  }
246
- /** Number of live peers (for status / metrics). */
247
219
  get peerCount() { return this.peers.size; }
248
- /** Number of authenticated peers. */
249
220
  get authedPeerCount() { let n = 0; for (const p of this.peers)
250
221
  if (p.authed)
251
222
  n++; return n; }
@@ -1,58 +1,10 @@
1
- /**
2
- * pikichannel/protocol.ts — THE universal, agent-agnostic wire protocol (L2).
3
- *
4
- * This file is the single source of truth for the pikichannel contract. It is
5
- * deliberately free of any pikiloom-internal or agent-specific type: a client
6
- * SDK on any platform (Web / iOS / Android) only needs these shapes to speak to
7
- * any pikichannel host. The browser SDK in `web/pikichannel-sdk.js` mirrors the
8
- * string literals and shapes documented here — keep the two in lockstep.
9
- *
10
- * Layering:
11
- * transport (L1) — moves opaque frames (a byte/string pipe): WebSocket, WebRTC
12
- * datachannel, relay tunnel. Pluggable; see transport.ts.
13
- * protocol (L2) — THIS file. The session/event semantics framed over L1.
14
- *
15
- * Design rules:
16
- * - Every message is a flat discriminated union on `type`. No nested envelopes
17
- * so clients can `switch (msg.type)` directly.
18
- * - The host→client `session` event carries a {@link SnapshotPatch}: a `full`
19
- * snapshot on first send / resync, then deltas (append-only suffixes for the
20
- * unbounded text/reasoning fields + changed scalar/struct fields). The host
21
- * remains the single source of truth — it holds the full snapshot and emits
22
- * `full` on (re)subscribe or whenever a client reports a `seq` gap, so the
23
- * wire is O(n) for a stream of total size n instead of O(n²).
24
- * - `seq` is monotonic per session for ordering / gap detection.
25
- * - Auth: a client authenticates in `hello` (token); loopback peers are exempt
26
- * by host policy. No session data or control is processed before auth.
27
- * - Versioned via PROTOCOL_VERSION; the handshake negotiates it.
28
- *
29
- * Extensibility (how this grows without forking the wire):
30
- * - ADDITIVE by default: new optional fields on existing messages and brand-new
31
- * `type`s are backward-compatible. Both host and client MUST ignore unknown
32
- * message `type`s and unknown fields (the switch statements fall through, not
33
- * throw) — so an old peer talks to a new peer safely.
34
- * - Optional features are negotiated via `HostCapability` (advertised in
35
- * `welcome`), not by version bumps — a client checks `host.capabilities`
36
- * before using a non-core verb.
37
- * - PROTOCOL_VERSION bumps only for a BREAKING change; `hello.v` lets either
38
- * side detect a mismatch and degrade.
39
- * - The transport (L1) is fully decoupled: a new binding (relay, WebTransport,
40
- * QUIC) implements ChannelConnection and carries this protocol unchanged.
41
- */
42
1
  export const PROTOCOL_VERSION = 1;
43
- /** An empty baseline snapshot, used as the starting point before any patch. */
44
2
  export function emptySnapshot() {
45
3
  return { phase: 'idle', updatedAt: 0 };
46
4
  }
47
5
  const APPEND_FIELDS = ['text', 'reasoning'];
48
6
  const STRUCT_FIELDS = ['plan', 'toolCalls', 'subAgents', 'usage', 'artifacts', 'interactions', 'queued'];
49
7
  const SCALAR_FIELDS = ['phase', 'taskId', 'agent', 'model', 'effort', 'prompt', 'activity', 'error', 'incomplete', 'startedAt', 'updatedAt'];
50
- /**
51
- * Diff two full snapshots into a minimal patch. text/reasoning are encoded as
52
- * append suffixes when the next value extends the prev (the streaming common
53
- * case); any non-extension falls back to a `set`. Bounded fields are compared by
54
- * value and sent whole when changed.
55
- */
56
8
  export function diffSnapshot(prev, next) {
57
9
  const patch = {};
58
10
  let set;
@@ -84,7 +36,6 @@ export function diffSnapshot(prev, next) {
84
36
  patch.set = set;
85
37
  return patch;
86
38
  }
87
- /** Apply a patch onto a prior snapshot, returning the new cumulative snapshot. */
88
39
  export function applySnapshotPatch(prev, patch) {
89
40
  if (patch.full)
90
41
  return patch.full;
@@ -97,9 +48,6 @@ export function applySnapshotPatch(prev, patch) {
97
48
  Object.assign(next, patch.set);
98
49
  return next;
99
50
  }
100
- // ---------------------------------------------------------------------------
101
- // Type guards (host-side convenience)
102
- // ---------------------------------------------------------------------------
103
51
  export function isClientMessage(value) {
104
52
  return !!value && typeof value === 'object' && typeof value.type === 'string';
105
53
  }