pikiloop 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * images.ts — unified image pipeline for the agent layer.
3
+ *
4
+ * Every driver produces image blocks through `attachAgentImage`, every IM
5
+ * channel consumes them through `materializeImage`, and every dashboard
6
+ * response routes large images through the attachment HTTP endpoint via
7
+ * `rewriteImageBlocksForTransport`. The shape isolates *where the bytes live*
8
+ * from *how a renderer wants to consume them*, so adding a new driver source
9
+ * (Codex `image_generation_call`, Claude MCP `tool_result` image, Gemini Imagen,
10
+ * future) or a new transport (a fourth IM channel, a CLI exporter, an OG-image
11
+ * preview) doesn't require touching every other site.
12
+ *
13
+ * Storage model
14
+ * -------------
15
+ * - Agent-native sources (`~/.codex/generated_images/...`, `~/.claude/...`)
16
+ * keep their files in place. The block's `imagePath` is the authoritative
17
+ * pointer; the dashboard and channels read directly from there.
18
+ * - MCP-bridge-buffered sources (image content embedded in MCP tool results)
19
+ * are persisted under the per-session attachments directory (see
20
+ * `sessionAttachmentsDir`) so they survive the originating CLI process and
21
+ * are reachable by an absolute path.
22
+ *
23
+ * Transport model
24
+ * ---------------
25
+ * - `content` always carries a directly-renderable reference: a `data:` URL
26
+ * for inline bytes, or an attachment HTTP URL for files on disk.
27
+ * - Below `INLINE_THRESHOLD_BYTES`, drivers may inline as `data:`. Above the
28
+ * threshold, `rewriteImageBlocksForTransport` substitutes a relative
29
+ * `/api/sessions/:agent/:id/attachment?...` URL — keeps RichMessage
30
+ * payloads small even when a session has many large images.
31
+ * - IM channels prefer `imagePath` over decoding `content`, avoiding wasted
32
+ * base64 round-trips.
33
+ */
34
+ import fs from 'node:fs';
35
+ import os from 'node:os';
36
+ import path from 'node:path';
37
+ import { getHome } from '../core/platform.js';
38
+ import { agentLog, agentWarn } from './utils.js';
39
+ import { mimeForExt } from './utils.js';
40
+ // ---------------------------------------------------------------------------
41
+ // Constants & policy
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * Maximum on-disk size we'll read into memory when materializing image bytes.
45
+ * Larger files are skipped (transport surfaces the `imagePath` reference but
46
+ * doesn't blast a multi-megabyte buffer through the channel send path).
47
+ */
48
+ const MAX_IMAGE_BYTES = 16 * 1024 * 1024;
49
+ /**
50
+ * Below this size we inline the image as a `data:` URL inside the block's
51
+ * `content`. Larger images travel as on-disk references; the dashboard fetches
52
+ * them lazily through the attachment HTTP endpoint and IM channels read from
53
+ * disk via `imagePath`.
54
+ */
55
+ const INLINE_THRESHOLD_BYTES = 256 * 1024;
56
+ const IMAGE_MIME_PREFIX = 'image/';
57
+ const IMAGE_EXT_BY_MIME = {
58
+ 'image/png': '.png',
59
+ 'image/jpeg': '.jpg',
60
+ 'image/jpg': '.jpg',
61
+ 'image/gif': '.gif',
62
+ 'image/webp': '.webp',
63
+ 'image/svg+xml': '.svg',
64
+ };
65
+ // ---------------------------------------------------------------------------
66
+ // Path helpers
67
+ // ---------------------------------------------------------------------------
68
+ /** `$CODEX_HOME` or fallback `~/.codex`. */
69
+ export function codexHome() {
70
+ return process.env.CODEX_HOME || path.join(getHome(), '.codex');
71
+ }
72
+ /** Per-session attachments directory used by MCP bridge / tool buffering.
73
+ * Lives next to the user's pikiloop config so it survives across workdirs. */
74
+ export function sessionAttachmentsDir(agent, sessionId) {
75
+ return path.join(getHome(), '.pikiloop', 'attachments', agent, sessionId);
76
+ }
77
+ /**
78
+ * Path roots that the dashboard attachment endpoint is allowed to serve.
79
+ * Every entry is real-resolved at request time; a candidate file must live
80
+ * inside one of them (post-realpath) to be served.
81
+ *
82
+ * - `~/.codex/generated_images` — Codex built-in `image_gen` outputs.
83
+ * - `~/.codex/sessions` — rollout-adjacent assets (rare but legal).
84
+ * - `~/.claude/projects` — Claude attached images written to JSONL.
85
+ * - `~/.gemini` — Gemini CLI managed dirs.
86
+ * - `~/.pikiloop/attachments` — MCP-bridge-buffered tool result images.
87
+ * - workspace tree(s) — files generated under the project workdir.
88
+ * - OS tmpdir — short-lived staging by drivers / skills.
89
+ *
90
+ * `workdir` accepts a list because a multi-workspace setup serves sessions
91
+ * from several project trees through one endpoint — every entry must come
92
+ * from server-side config (registered workspaces / managed session records),
93
+ * never from request input.
94
+ */
95
+ export function allowedAttachmentRoots(workdir) {
96
+ const home = getHome();
97
+ const roots = [
98
+ path.join(codexHome(), 'generated_images'),
99
+ path.join(codexHome(), 'sessions'),
100
+ path.join(home, '.claude', 'projects'),
101
+ path.join(home, '.gemini'),
102
+ path.join(home, '.pikiloop', 'attachments'),
103
+ os.tmpdir(),
104
+ ];
105
+ const workdirs = typeof workdir === 'string' ? [workdir] : (workdir ?? []);
106
+ for (const wd of workdirs) {
107
+ if (wd && wd.trim())
108
+ roots.push(path.resolve(wd));
109
+ }
110
+ return roots;
111
+ }
112
+ /**
113
+ * Verify a resolved file path lives under one of the allowed roots, defending
114
+ * against `..` traversal AND symlinks that escape the allowlist. Returns the
115
+ * realpath when the file is allowed, or null when not.
116
+ */
117
+ export function resolveAllowedAttachmentPath(requested, workdir) {
118
+ if (!requested)
119
+ return null;
120
+ let real;
121
+ try {
122
+ real = fs.realpathSync(requested);
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ for (const root of allowedAttachmentRoots(workdir)) {
128
+ let realRoot;
129
+ try {
130
+ realRoot = fs.realpathSync(root);
131
+ }
132
+ catch {
133
+ continue;
134
+ }
135
+ const rel = path.relative(realRoot, real);
136
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel))
137
+ return real;
138
+ if (rel === '')
139
+ return real;
140
+ }
141
+ return null;
142
+ }
143
+ // ---------------------------------------------------------------------------
144
+ // MIME helpers
145
+ // ---------------------------------------------------------------------------
146
+ function mimeFromPath(filePath) {
147
+ return mimeForExt(path.extname(filePath).toLowerCase());
148
+ }
149
+ function extFromMime(mime) {
150
+ return IMAGE_EXT_BY_MIME[mime.toLowerCase()] || '.bin';
151
+ }
152
+ function isImageMime(mime) {
153
+ return mime.toLowerCase().startsWith(IMAGE_MIME_PREFIX);
154
+ }
155
+ /**
156
+ * Build a `MessageBlock` of type `image` from an on-disk file produced by an
157
+ * agent (Codex built-in `image_gen`, MCP tool buffered to attachments dir, …).
158
+ * Returns null when the file is missing or unreadable — drivers should treat
159
+ * that as a soft failure and continue without the block.
160
+ */
161
+ export function attachAgentImage(opts) {
162
+ const abs = path.resolve(opts.imagePath);
163
+ let stat;
164
+ try {
165
+ stat = fs.statSync(abs);
166
+ }
167
+ catch (err) {
168
+ agentLog(`[images] attachAgentImage: stat failed for ${abs}: ${err?.message || err}`);
169
+ return null;
170
+ }
171
+ if (!stat.isFile())
172
+ return null;
173
+ if (stat.size > MAX_IMAGE_BYTES) {
174
+ agentWarn(`[images] attachAgentImage: ${abs} too large (${stat.size} bytes > ${MAX_IMAGE_BYTES})`);
175
+ return null;
176
+ }
177
+ const mime = (opts.mime || mimeFromPath(abs)).toLowerCase();
178
+ if (!isImageMime(mime))
179
+ return null;
180
+ const threshold = opts.inlineThresholdBytes ?? INLINE_THRESHOLD_BYTES;
181
+ let content;
182
+ if (stat.size <= threshold) {
183
+ let bytes;
184
+ try {
185
+ bytes = fs.readFileSync(abs);
186
+ }
187
+ catch (err) {
188
+ agentLog(`[images] attachAgentImage: read failed for ${abs}: ${err?.message || err}`);
189
+ return null;
190
+ }
191
+ content = `data:${mime};base64,${bytes.toString('base64')}`;
192
+ }
193
+ else {
194
+ // Sentinel: the transport layer rewrites this into an HTTP URL with the
195
+ // right session context. Renderers that bypass `rewriteImageBlocksForTransport`
196
+ // see a `file://` URL and can still resolve it via `materializeImage` since
197
+ // `imagePath` is also populated.
198
+ content = `file://${abs}`;
199
+ }
200
+ const block = {
201
+ type: 'image',
202
+ content,
203
+ imagePath: abs,
204
+ imageMime: mime,
205
+ };
206
+ if (opts.caption?.trim())
207
+ block.imageCaption = opts.caption.trim();
208
+ if (opts.phase)
209
+ block.phase = opts.phase;
210
+ return block;
211
+ }
212
+ /**
213
+ * Build an image MessageBlock from an in-memory byte buffer. When `persist`
214
+ * is supplied, the bytes are also written to the session's attachments dir
215
+ * and the resulting path attached as `imagePath` — this is the path the MCP
216
+ * bridge uses when a tool returns an image (so downstream IM channels and the
217
+ * dashboard attachment endpoint can serve it without re-encoding).
218
+ */
219
+ export function attachInlineImage(opts) {
220
+ if (!opts.bytes?.length)
221
+ return null;
222
+ const mime = opts.mime.toLowerCase();
223
+ if (!isImageMime(mime))
224
+ return null;
225
+ if (opts.bytes.length > MAX_IMAGE_BYTES) {
226
+ agentWarn(`[images] attachInlineImage: ${opts.bytes.length} bytes > ${MAX_IMAGE_BYTES}`);
227
+ return null;
228
+ }
229
+ let imagePath;
230
+ if (opts.persist) {
231
+ const dir = sessionAttachmentsDir(opts.persist.agent, opts.persist.sessionId);
232
+ try {
233
+ fs.mkdirSync(dir, { recursive: true });
234
+ const stamp = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
235
+ const hint = (opts.persist.hint || 'image').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'image';
236
+ const candidate = path.join(dir, `${hint}-${stamp}${extFromMime(mime)}`);
237
+ fs.writeFileSync(candidate, opts.bytes);
238
+ imagePath = candidate;
239
+ }
240
+ catch (err) {
241
+ agentLog(`[images] attachInlineImage: persist failed: ${err?.message || err}`);
242
+ }
243
+ }
244
+ const threshold = opts.inlineThresholdBytes ?? INLINE_THRESHOLD_BYTES;
245
+ const content = opts.bytes.length <= threshold || !imagePath
246
+ ? `data:${mime};base64,${opts.bytes.toString('base64')}`
247
+ : `file://${imagePath}`;
248
+ const block = { type: 'image', content, imageMime: mime };
249
+ if (imagePath)
250
+ block.imagePath = imagePath;
251
+ if (opts.caption?.trim())
252
+ block.imageCaption = opts.caption.trim();
253
+ if (opts.phase)
254
+ block.phase = opts.phase;
255
+ return block;
256
+ }
257
+ /**
258
+ * Resolve an image block to raw bytes for transport (IM channel image send,
259
+ * export bundling, …). Preference order:
260
+ * 1. `imagePath` — read straight from disk, no base64 decode.
261
+ * 2. `content` is `data:image/...;base64,...` — decode the URL.
262
+ * 3. `content` is `file://...` — read that path from disk.
263
+ * 4. otherwise — return null.
264
+ */
265
+ export function materializeImage(block) {
266
+ if (block.type !== 'image')
267
+ return null;
268
+ const caption = block.imageCaption?.trim() || undefined;
269
+ if (block.imagePath) {
270
+ try {
271
+ const stat = fs.statSync(block.imagePath);
272
+ if (stat.isFile() && stat.size <= MAX_IMAGE_BYTES) {
273
+ const bytes = fs.readFileSync(block.imagePath);
274
+ const mime = (block.imageMime || mimeFromPath(block.imagePath)).toLowerCase();
275
+ return { bytes, mime, caption };
276
+ }
277
+ }
278
+ catch { /* fall through to content */ }
279
+ }
280
+ const content = block.content || '';
281
+ if (content.startsWith('data:')) {
282
+ const m = /^data:([^;,]+);base64,(.+)$/i.exec(content);
283
+ if (!m)
284
+ return null;
285
+ const mime = m[1].toLowerCase();
286
+ try {
287
+ const bytes = Buffer.from(m[2], 'base64');
288
+ if (bytes.length > MAX_IMAGE_BYTES)
289
+ return null;
290
+ return { bytes, mime, caption };
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ if (content.startsWith('file://')) {
297
+ const filePath = content.slice('file://'.length);
298
+ try {
299
+ const stat = fs.statSync(filePath);
300
+ if (!stat.isFile() || stat.size > MAX_IMAGE_BYTES)
301
+ return null;
302
+ const bytes = fs.readFileSync(filePath);
303
+ const mime = (block.imageMime || mimeFromPath(filePath)).toLowerCase();
304
+ return { bytes, mime, caption };
305
+ }
306
+ catch {
307
+ return null;
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+ function encodePathForUrl(value) {
313
+ // base64url makes the path opaque in URLs (no `?`, `#`, `/` collisions) and
314
+ // round-trips losslessly back to the original absolute path.
315
+ return Buffer.from(value, 'utf-8').toString('base64')
316
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
317
+ }
318
+ export function decodeAttachmentPathParam(value) {
319
+ const padded = value + '='.repeat((4 - (value.length % 4)) % 4);
320
+ const normalized = padded.replace(/-/g, '+').replace(/_/g, '/');
321
+ return Buffer.from(normalized, 'base64').toString('utf-8');
322
+ }
323
+ /**
324
+ * Rewrite a block array for transport: any image block whose `content` is a
325
+ * `file://` sentinel becomes an attachment HTTP URL, and any inline `data:`
326
+ * URL above the threshold is also promoted to an HTTP URL (keeping the
327
+ * RichMessage payload compact even when a session has many large images).
328
+ *
329
+ * Pure: returns a new array; the input is not mutated.
330
+ */
331
+ export function rewriteImageBlocksForTransport(blocks, ctx) {
332
+ const base = (ctx.apiBase || '/api/sessions').replace(/\/+$/, '');
333
+ return blocks.map(block => {
334
+ if (block.type !== 'image')
335
+ return block;
336
+ const sourcePath = block.imagePath
337
+ || (block.content?.startsWith('file://') ? block.content.slice('file://'.length) : '');
338
+ if (!sourcePath)
339
+ return block;
340
+ // Inline content under the threshold: leave the data URL alone — the
341
+ // dashboard renders it directly, no extra request.
342
+ if (block.content?.startsWith('data:'))
343
+ return block;
344
+ const encoded = encodePathForUrl(sourcePath);
345
+ const url = `${base}/${encodeURIComponent(ctx.agent)}/${encodeURIComponent(ctx.sessionId)}/attachment?p=${encoded}`;
346
+ return { ...block, content: url, imagePath: sourcePath };
347
+ });
348
+ }
349
+ // ---------------------------------------------------------------------------
350
+ // Constants exported for tests
351
+ // ---------------------------------------------------------------------------
352
+ export const _IMAGE_PIPELINE_INTERNALS = {
353
+ MAX_IMAGE_BYTES,
354
+ INLINE_THRESHOLD_BYTES,
355
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * agent/index.ts — Barrel export for the agent layer.
3
+ *
4
+ * This module loads all agent drivers (side-effect) and re-exports the public
5
+ * API from focused sub-modules:
6
+ *
7
+ * types.ts — Shared type definitions (StreamOpts, StreamResult, SessionInfo, …)
8
+ * utils.ts — Pure utility functions (Q, agentLog, normalizeErrorMessage, …)
9
+ * session.ts — Session workspace management, metadata, classification, export/import
10
+ * stream.ts — CLI spawn framework, stream orchestration, agent detection, delegation
11
+ * driver.ts — AgentDriver interface and registry
12
+ * skills.ts — Project skill discovery
13
+ * drivers/ — Per-agent driver implementations (claude, codex, gemini)
14
+ */
15
+ // ── Load all drivers (side-effect: each calls registerDriver) ───────────────
16
+ import './drivers/claude.js';
17
+ import './drivers/codex.js';
18
+ import './drivers/gemini.js';
19
+ import './drivers/hermes.js';
20
+ export { IMAGE_EXTS } from './types.js';
21
+ // ── Re-export: image pipeline ──────────────────────────────────────────────
22
+ export { attachAgentImage, attachInlineImage, materializeImage, rewriteImageBlocksForTransport, resolveAllowedAttachmentPath, allowedAttachmentRoots, decodeAttachmentPathParam, sessionAttachmentsDir, codexHome, } from './images.js';
23
+ // ── Re-export: utilities ────────────────────────────────────────────────────
24
+ export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
25
+ // ── Re-export: session management ───────────────────────────────────────────
26
+ export { updateSessionMeta, promoteSessionId, recordFork, listPikiloopSessions, findPikiloopSession, getSessionStoredConfig, ensureManagedSession, findManagedThreadSession, stageSessionFiles, mergeManagedAndNativeSessions, getSessions, getSessionTail, getSessionMessages, applyTurnWindow, applyTurnFilter, classifySession, deriveUserStatus, exportSession, importSession, deleteAgentSession, isProcessAlive, isRunningSessionStale, reconcileOrphanedRunningSessions, } from './session.js';
27
+ // ── Re-export: stream & detection ───────────────────────────────────────────
28
+ export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, listModels, resolveAgentModels, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
29
+ // ── Re-export: driver registry ──────────────────────────────────────────────
30
+ export { registerDriver, getDriver, getDriverCapabilities, allDrivers, allDriverIds, hasDriver, shutdownAllDrivers, } from './driver.js';
31
+ // ── Re-export: skills ───────────────────────────────────────────────────────
32
+ export { getProjectSkillPaths, initializeProjectSkills, listSkills, getGlobalSkillsRoot, collapseSkillPrompt, } from './skills.js';
33
+ // ── Re-export: goal (persistent thread objective) ────────────────────────────
34
+ export { readGoal, writeGoal, clearGoal, setGoal, pauseGoal, resumeGoal, completeGoal, accountTurn, bumpContinuationCount, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, sessionGoalPath, DEFAULT_MAX_CONTINUATIONS, } from './goal.js';
35
+ // ── Re-export: native codex goal bridge ──────────────────────────────────────
36
+ export { setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, } from './drivers/codex.js';
37
+ // ── Re-export: native claude goal bridge ─────────────────────────────────────
38
+ export { getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, } from './drivers/claude.js';
39
+ // ── Re-export: MCP extensions ───────────────────────────────────────────────
40
+ export { listAllMcpExtensions, addGlobalMcpExtension, removeGlobalMcpExtension, updateGlobalMcpExtension, addWorkspaceMcpExtension, removeWorkspaceMcpExtension, updateWorkspaceMcpExtension, loadGlobalMcpExtensions, loadWorkspaceMcpExtensions, getCatalogItems, getCatalogItem, buildInstalledConfigFromRecommended, checkMcpHealth, getCachedHealth, cacheHealth, } from './mcp/extensions.js';
41
+ export { getRecommendedMcpServers, getRecommendedMcpServer, getRecommendedSkillRepos, searchMcpServers, searchSkills as searchSkillRepos, } from './mcp/registry.js';
42
+ export { getMcpToken, saveMcpToken, deleteMcpToken, hasValidMcpToken, startAuthorization, completeAuthorization, refreshMcpToken, injectOAuthHeaders, } from './mcp/oauth.js';
43
+ export { installSkill, removeSkill, checkSkillUpdates, getGlobalSkillsDir, } from './skill-installer.js';
44
+ // ── Re-export: CLI extensions ───────────────────────────────────────────────
45
+ export { getRecommendedClis, getRecommendedCli, detectCli, getCachedCliStatus, invalidateCliStatus, currentPlatform, getCliCatalog, refreshCliStatus, startCliAuthSession, getAuthSession, cancelAuthSession, applyCliToken, logoutCli, startCliInstallSession, } from './cli/index.js';
46
+ // ── Re-export: driver-specific functions ────────────────────────────────────
47
+ export { doClaudeStream } from './drivers/claude.js';
48
+ export { doCodexStream, buildCodexTurnInput, shutdownCodexServer, getCodexUsageLive } from './drivers/codex.js';
49
+ export { doGeminiStream } from './drivers/gemini.js';
50
+ export { doHermesStream } from './drivers/hermes.js';