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.
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- 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';
|