pikiloom 0.4.16 → 0.4.17
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/README.md +2 -1
- package/dashboard/dist/assets/{AgentTab-B5tmLxa7.js → AgentTab-CDVhy5K1.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-CBp5nyfS.js → DirBrowser-BElI1-4D.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-w4pkrNas.js → ExtensionsTab-BB8ipJ77.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-37Po5LP1.js → IMAccessTab-IZt_yXoG.js} +1 -1
- package/dashboard/dist/assets/{Modal-CBMO5UcS.js → Modal-C1EAGSL1.js} +1 -1
- package/dashboard/dist/assets/{Modals-DMlEjJUG.js → Modals-DYUV5yR9.js} +1 -1
- package/dashboard/dist/assets/{Select-BiSTkS_t.js → Select-BnsbE6Qv.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-Ca_TVTT1.js +1 -0
- package/dashboard/dist/assets/{SystemTab-Brzt5wTT.js → SystemTab-Dk6k2OTt.js} +1 -1
- package/dashboard/dist/assets/{index-5Q-Q7ByM.js → index-CK-3CNRp.js} +2 -2
- package/dashboard/dist/assets/index-CnJsD381.js +23 -0
- package/dashboard/dist/assets/index-dzfjF9Js.css +1 -0
- package/dashboard/dist/assets/{shared-P-W1OYQ6.js → shared-CZVD0MJD.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/artifacts.js +160 -0
- package/dist/agent/images.js +51 -24
- package/dist/agent/index.js +4 -2
- package/dist/agent/mcp/tools/workspace.js +4 -3
- package/dist/bot/bot.js +83 -4
- package/dist/bot/commands.js +48 -2
- package/dist/bot/menu.js +1 -0
- package/dist/bot/session-hub.js +1 -1
- package/dist/channels/dingtalk/bot.js +9 -1
- package/dist/channels/discord/bot.js +9 -1
- package/dist/channels/feishu/bot.js +8 -1
- package/dist/channels/slack/bot.js +9 -1
- package/dist/channels/telegram/bot.js +8 -1
- package/dist/channels/wecom/bot.js +9 -1
- package/dist/channels/weixin/bot.js +9 -1
- package/dist/cli/main.js +1 -0
- package/dist/dashboard/routes/sessions.js +108 -27
- package/package.json +1 -1
- package/dashboard/dist/assets/SessionPanel-BVC7kwlX.js +0 -1
- package/dashboard/dist/assets/index-Dw3ty4QY.js +0 -23
- package/dashboard/dist/assets/index-FD86DEDF.css +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifacts.ts — delivered-artifact manifest (the single source of truth for
|
|
3
|
+
* "files the agent handed to the user during a session").
|
|
4
|
+
*
|
|
5
|
+
* When the agent calls `im_send_file`, the bot materializes the file into the
|
|
6
|
+
* session attachments dir and appends a record here. IM channels additionally
|
|
7
|
+
* push the bytes to their chat; the dashboard serves the materialized copy over
|
|
8
|
+
* HTTP and renders it. Recording is terminal-agnostic, so a session watched
|
|
9
|
+
* from BOTH a chat and the dashboard shows the same deliveries on either side,
|
|
10
|
+
* and they survive a page reload / workspace cleanup.
|
|
11
|
+
*
|
|
12
|
+
* The manifest lives next to the MCP-buffered image attachments
|
|
13
|
+
* (`sessionAttachmentsDir`), which is already an allowlist root for the
|
|
14
|
+
* dashboard attachment endpoint — so materialized artifacts are servable with
|
|
15
|
+
* no new trust boundary.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { sessionAttachmentsDir } from './images.js';
|
|
20
|
+
import { agentWarn } from './utils.js';
|
|
21
|
+
const PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']);
|
|
22
|
+
const MIME_BY_EXT = {
|
|
23
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
24
|
+
'.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
25
|
+
'.pdf': 'application/pdf', '.txt': 'text/plain; charset=utf-8', '.md': 'text/markdown; charset=utf-8',
|
|
26
|
+
'.csv': 'text/csv; charset=utf-8', '.json': 'application/json', '.xml': 'application/xml',
|
|
27
|
+
'.html': 'text/html; charset=utf-8', '.zip': 'application/zip', '.gz': 'application/gzip',
|
|
28
|
+
'.tar': 'application/x-tar', '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
|
|
29
|
+
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.log': 'text/plain; charset=utf-8',
|
|
30
|
+
};
|
|
31
|
+
/** Best-effort MIME for a delivered artifact; `application/octet-stream` when unknown. */
|
|
32
|
+
export function mimeForArtifact(filePath) {
|
|
33
|
+
return MIME_BY_EXT[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
34
|
+
}
|
|
35
|
+
function manifestPath(agent, sessionId) {
|
|
36
|
+
return path.join(sessionAttachmentsDir(agent, sessionId), 'delivered.jsonl');
|
|
37
|
+
}
|
|
38
|
+
function deliveredDir(agent, sessionId) {
|
|
39
|
+
return path.join(sessionAttachmentsDir(agent, sessionId), 'delivered');
|
|
40
|
+
}
|
|
41
|
+
function sanitizeName(name) {
|
|
42
|
+
return (name || 'file').replace(/[/\\\0]+/g, '_').replace(/^\.+/, '').slice(0, 200) || 'file';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Materialize `srcPath` into the session's delivered-artifacts dir so it is
|
|
46
|
+
* (a) servable by the dashboard attachment endpoint (the dir is an allowlist
|
|
47
|
+
* root) and (b) durable across workspace cleanup. Hardlinks when possible,
|
|
48
|
+
* falling back to a copy across filesystems. The on-disk name is stamped to
|
|
49
|
+
* avoid collisions; the pristine basename travels in the manifest.
|
|
50
|
+
*/
|
|
51
|
+
export function stageDeliveredArtifact(agent, sessionId, srcPath) {
|
|
52
|
+
const fileName = sanitizeName(path.basename(srcPath));
|
|
53
|
+
const stat = fs.statSync(srcPath);
|
|
54
|
+
const dir = deliveredDir(agent, sessionId);
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
const stamp = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
57
|
+
const dest = path.join(dir, `${stamp}-${fileName}`);
|
|
58
|
+
try {
|
|
59
|
+
fs.linkSync(srcPath, dest);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
fs.copyFileSync(srcPath, dest);
|
|
63
|
+
}
|
|
64
|
+
return { path: dest, fileName, fileSize: stat.size, fileMime: mimeForArtifact(fileName) };
|
|
65
|
+
}
|
|
66
|
+
/** Append a record to the session's delivered-artifact manifest. */
|
|
67
|
+
export function recordDeliveredArtifact(agent, sessionId, entry) {
|
|
68
|
+
try {
|
|
69
|
+
const file = manifestPath(agent, sessionId);
|
|
70
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
71
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
agentWarn(`[artifacts] record failed: ${e?.message || e}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Materialize + record a delivered artifact in one step. Best-effort: returns
|
|
79
|
+
* the stored record (with the materialized path), or null on failure — delivery
|
|
80
|
+
* must never crash the stream.
|
|
81
|
+
*/
|
|
82
|
+
export function deliverArtifact(agent, sessionId, srcPath, opts) {
|
|
83
|
+
try {
|
|
84
|
+
const staged = stageDeliveredArtifact(agent, sessionId, srcPath);
|
|
85
|
+
const record = {
|
|
86
|
+
ts: Date.now(),
|
|
87
|
+
...(opts.taskId ? { taskId: opts.taskId } : {}),
|
|
88
|
+
path: staged.path,
|
|
89
|
+
fileName: staged.fileName,
|
|
90
|
+
fileMime: staged.fileMime,
|
|
91
|
+
fileSize: staged.fileSize,
|
|
92
|
+
kind: opts.kind,
|
|
93
|
+
...(opts.caption ? { caption: opts.caption } : {}),
|
|
94
|
+
};
|
|
95
|
+
recordDeliveredArtifact(agent, sessionId, record);
|
|
96
|
+
return record;
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
agentWarn(`[artifacts] deliver failed for ${srcPath}: ${e?.message || e}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Read the delivered-artifact manifest for a session (tolerant of partial lines). */
|
|
104
|
+
export function readDeliveredArtifacts(agent, sessionId) {
|
|
105
|
+
const file = manifestPath(agent, sessionId);
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const line of raw.split('\n')) {
|
|
115
|
+
const t = line.trim();
|
|
116
|
+
if (!t)
|
|
117
|
+
continue;
|
|
118
|
+
try {
|
|
119
|
+
const rec = JSON.parse(t);
|
|
120
|
+
if (rec && typeof rec.path === 'string' && rec.fileName)
|
|
121
|
+
out.push(rec);
|
|
122
|
+
}
|
|
123
|
+
catch { /* skip malformed line */ }
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Project the delivered-artifact manifest into pre-transport MessageBlocks
|
|
129
|
+
* (content = `file://<abs>`). The dashboard read path rewrites these to
|
|
130
|
+
* attachment HTTP URLs via `rewriteAttachmentBlocksForTransport`. Records whose
|
|
131
|
+
* file no longer exists on disk are dropped.
|
|
132
|
+
*/
|
|
133
|
+
export function deliveredArtifactBlocks(agent, sessionId) {
|
|
134
|
+
const blocks = [];
|
|
135
|
+
for (const a of readDeliveredArtifacts(agent, sessionId)) {
|
|
136
|
+
if (!fs.existsSync(a.path))
|
|
137
|
+
continue;
|
|
138
|
+
if (a.kind === 'photo' && PHOTO_EXTS.has(path.extname(a.fileName).toLowerCase())) {
|
|
139
|
+
blocks.push({
|
|
140
|
+
type: 'image',
|
|
141
|
+
content: `file://${a.path}`,
|
|
142
|
+
imagePath: a.path,
|
|
143
|
+
imageMime: a.fileMime,
|
|
144
|
+
...(a.caption ? { imageCaption: a.caption } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
blocks.push({
|
|
149
|
+
type: 'file',
|
|
150
|
+
content: `file://${a.path}`,
|
|
151
|
+
filePath: a.path,
|
|
152
|
+
fileMime: a.fileMime,
|
|
153
|
+
fileName: a.fileName,
|
|
154
|
+
fileSize: a.fileSize,
|
|
155
|
+
...(a.caption ? { fileCaption: a.caption } : {}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return blocks;
|
|
160
|
+
}
|
package/dist/agent/images.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* images.ts — unified image pipeline for the agent
|
|
2
|
+
* images.ts — unified image (and on-disk attachment) pipeline for the agent
|
|
3
|
+
* layer.
|
|
3
4
|
*
|
|
4
5
|
* Every driver produces image blocks through `attachAgentImage`, every IM
|
|
5
6
|
* channel consumes them through `materializeImage`, and every dashboard
|
|
6
|
-
* response routes
|
|
7
|
-
*
|
|
7
|
+
* response routes on-disk image/file blocks through the attachment HTTP
|
|
8
|
+
* endpoint via `rewriteAttachmentBlocksForTransport` (delivered non-image files
|
|
9
|
+
* — see agent/artifacts.ts — ride the same transport). The shape isolates
|
|
10
|
+
* *where the bytes live*
|
|
8
11
|
* from *how a renderer wants to consume them*, so adding a new driver source
|
|
9
12
|
* (Codex `image_generation_call`, Claude MCP `tool_result` image, Gemini Imagen,
|
|
10
13
|
* future) or a new transport (a fourth IM channel, a CLI exporter, an OG-image
|
|
@@ -25,7 +28,7 @@
|
|
|
25
28
|
* - `content` always carries a directly-renderable reference: a `data:` URL
|
|
26
29
|
* for inline bytes, or an attachment HTTP URL for files on disk.
|
|
27
30
|
* - Below `INLINE_THRESHOLD_BYTES`, drivers may inline as `data:`. Above the
|
|
28
|
-
* threshold, `
|
|
31
|
+
* threshold, `rewriteAttachmentBlocksForTransport` substitutes a relative
|
|
29
32
|
* `/api/sessions/:agent/:id/attachment?...` URL — keeps RichMessage
|
|
30
33
|
* payloads small even when a session has many large images.
|
|
31
34
|
* - IM channels prefer `imagePath` over decoding `content`, avoiding wasted
|
|
@@ -192,7 +195,7 @@ export function attachAgentImage(opts) {
|
|
|
192
195
|
}
|
|
193
196
|
else {
|
|
194
197
|
// Sentinel: the transport layer rewrites this into an HTTP URL with the
|
|
195
|
-
// right session context. Renderers that bypass `
|
|
198
|
+
// right session context. Renderers that bypass `rewriteAttachmentBlocksForTransport`
|
|
196
199
|
// see a `file://` URL and can still resolve it via `materializeImage` since
|
|
197
200
|
// `imagePath` is also populated.
|
|
198
201
|
content = `file://${abs}`;
|
|
@@ -321,29 +324,53 @@ export function decodeAttachmentPathParam(value) {
|
|
|
321
324
|
return Buffer.from(normalized, 'base64').toString('utf-8');
|
|
322
325
|
}
|
|
323
326
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
327
|
+
* Build the dashboard attachment URL that serves `absPath` for a given session.
|
|
328
|
+
* Single source for the `/api/sessions/:agent/:id/attachment?p=…` shape so the
|
|
329
|
+
* transport rewrite, live stream snapshots, and any future caller stay in sync.
|
|
330
|
+
* `downloadName` is carried as an opaque `&n=` hint the endpoint uses for the
|
|
331
|
+
* Content-Disposition filename (the pristine basename, not the on-disk one).
|
|
332
|
+
*/
|
|
333
|
+
export function attachmentUrl(agent, sessionId, absPath, opts = {}) {
|
|
334
|
+
const base = (opts.apiBase || '/api/sessions').replace(/\/+$/, '');
|
|
335
|
+
const encoded = encodePathForUrl(absPath);
|
|
336
|
+
const name = opts.downloadName ? `&n=${encodeURIComponent(opts.downloadName)}` : '';
|
|
337
|
+
return `${base}/${encodeURIComponent(agent)}/${encodeURIComponent(sessionId)}/attachment?p=${encoded}${name}`;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Rewrite a block array for transport. Image and file blocks whose `content`
|
|
341
|
+
* is a `file://` sentinel (or which carry an on-disk `imagePath`/`filePath`)
|
|
342
|
+
* become attachment HTTP URLs; inline `data:` image URLs are left untouched so
|
|
343
|
+
* the dashboard renders them directly. Keeps RichMessage payloads compact even
|
|
344
|
+
* when a session delivered many large artifacts.
|
|
328
345
|
*
|
|
329
346
|
* Pure: returns a new array; the input is not mutated.
|
|
330
347
|
*/
|
|
331
|
-
export function
|
|
332
|
-
const base = (ctx.apiBase || '/api/sessions').replace(/\/+$/, '');
|
|
348
|
+
export function rewriteAttachmentBlocksForTransport(blocks, ctx) {
|
|
333
349
|
return blocks.map(block => {
|
|
334
|
-
if (block.type
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
if (block.type === 'image') {
|
|
351
|
+
const sourcePath = block.imagePath
|
|
352
|
+
|| (block.content?.startsWith('file://') ? block.content.slice('file://'.length) : '');
|
|
353
|
+
if (!sourcePath)
|
|
354
|
+
return block;
|
|
355
|
+
// Inline content under the threshold: leave the data URL alone — the
|
|
356
|
+
// dashboard renders it directly, no extra request.
|
|
357
|
+
if (block.content?.startsWith('data:'))
|
|
358
|
+
return block;
|
|
359
|
+
const url = attachmentUrl(ctx.agent, ctx.sessionId, sourcePath, { apiBase: ctx.apiBase });
|
|
360
|
+
return { ...block, content: url, imagePath: sourcePath };
|
|
361
|
+
}
|
|
362
|
+
if (block.type === 'file') {
|
|
363
|
+
const sourcePath = block.filePath
|
|
364
|
+
|| (block.content?.startsWith('file://') ? block.content.slice('file://'.length) : '');
|
|
365
|
+
if (!sourcePath)
|
|
366
|
+
return block;
|
|
367
|
+
const url = attachmentUrl(ctx.agent, ctx.sessionId, sourcePath, {
|
|
368
|
+
apiBase: ctx.apiBase,
|
|
369
|
+
downloadName: block.fileName || undefined,
|
|
370
|
+
});
|
|
371
|
+
return { ...block, content: url, filePath: sourcePath };
|
|
372
|
+
}
|
|
373
|
+
return block;
|
|
347
374
|
});
|
|
348
375
|
}
|
|
349
376
|
// ---------------------------------------------------------------------------
|
package/dist/agent/index.js
CHANGED
|
@@ -18,8 +18,10 @@ import './drivers/codex.js';
|
|
|
18
18
|
import './drivers/gemini.js';
|
|
19
19
|
import './drivers/hermes.js';
|
|
20
20
|
export { IMAGE_EXTS } from './types.js';
|
|
21
|
-
// ── Re-export:
|
|
22
|
-
export { attachAgentImage, attachInlineImage, materializeImage,
|
|
21
|
+
// ── Re-export: attachment pipeline (images + delivered files) ───────────────
|
|
22
|
+
export { attachAgentImage, attachInlineImage, materializeImage, rewriteAttachmentBlocksForTransport, attachmentUrl, resolveAllowedAttachmentPath, allowedAttachmentRoots, decodeAttachmentPathParam, sessionAttachmentsDir, codexHome, } from './images.js';
|
|
23
|
+
// ── Re-export: delivered-artifact manifest ──────────────────────────────────
|
|
24
|
+
export { deliverArtifact, readDeliveredArtifacts, deliveredArtifactBlocks, mimeForArtifact, } from './artifacts.js';
|
|
23
25
|
// ── Re-export: utilities ────────────────────────────────────────────────────
|
|
24
26
|
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
27
|
// ── Re-export: session management ───────────────────────────────────────────
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tools/workspace.ts — Workspace file tools.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* im_list_files — returns workspace path, staged files, and directory listing
|
|
5
|
+
* im_send_file — delivers a file to the user via the active terminal
|
|
6
|
+
* (IM chat push and/or the dashboard attachment endpoint)
|
|
6
7
|
*/
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
@@ -28,7 +29,7 @@ const tools = [
|
|
|
28
29
|
},
|
|
29
30
|
{
|
|
30
31
|
name: 'im_send_file',
|
|
31
|
-
description: 'Send a file to the user
|
|
32
|
+
description: 'Send a file to the user through the active terminal (IM chat or web dashboard). Use this to hand over screenshots, reports, archives, or generated assets — the file is delivered and stays retrievable even when the user is connected remotely. Prefer this over printing a local filesystem path, which a remote user cannot open.',
|
|
32
33
|
inputSchema: {
|
|
33
34
|
type: 'object',
|
|
34
35
|
properties: {
|
package/dist/bot/bot.js
CHANGED
|
@@ -7,7 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { execSync, spawn } from 'node:child_process';
|
|
9
9
|
import { getActiveUserConfig, loadWorkspaces, onUserConfigChange, resolveUserWorkdir, setUserWorkdir, updateUserConfig } from '../core/config/user-config.js';
|
|
10
|
-
import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, isPendingSessionId, } from '../agent/index.js';
|
|
10
|
+
import { doStream, ensureManagedSession, findManagedThreadSession, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, resolveDefaultAgent, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, deliverArtifact, attachmentUrl, isPendingSessionId, } from '../agent/index.js';
|
|
11
11
|
import { compactForHandover, describeHandoverRef } from '../agent/handover.js';
|
|
12
12
|
import { getActiveProfileId, setActiveProfile } from '../model/index.js';
|
|
13
13
|
import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
|
|
@@ -58,10 +58,14 @@ function appendExtraPrompt(base, extra) {
|
|
|
58
58
|
return lhs;
|
|
59
59
|
return `${lhs}\n\n${rhs}`;
|
|
60
60
|
}
|
|
61
|
+
// NOTE: the `[Artifact Return]` header is a load-bearing marker — both
|
|
62
|
+
// `stripInjectedPrompts` (bot/streaming.ts) and Gemini's
|
|
63
|
+
// `GEMINI_SYSTEM_BLOCK_SENTINELS` key on it to strip this injected block from
|
|
64
|
+
// displayed user messages. Keep the token in sync if you ever change it.
|
|
61
65
|
function buildMcpDeliveryPrompt() {
|
|
62
66
|
return [
|
|
63
67
|
'[Artifact Return]',
|
|
64
|
-
'
|
|
68
|
+
'To hand a file to the user — a screenshot, report, archive, generated asset, anything they asked you to "send" — call the `im_send_file` tool with the file path and a short caption. It is delivered through whatever terminal the user is on (an IM chat or the web dashboard) and stays retrievable even when they are connected remotely. Do NOT just print a local filesystem path: a remote user cannot open paths on this machine.',
|
|
65
69
|
].join('\n');
|
|
66
70
|
}
|
|
67
71
|
function buildClaudeAskUserPrompt() {
|
|
@@ -360,6 +364,14 @@ export class Bot {
|
|
|
360
364
|
}
|
|
361
365
|
break;
|
|
362
366
|
}
|
|
367
|
+
case 'artifact': {
|
|
368
|
+
const snap = this.streamSnapshots.get(sessionKey);
|
|
369
|
+
if (snap) {
|
|
370
|
+
snap.artifacts = [...(snap.artifacts || []), event.artifact];
|
|
371
|
+
snap.updatedAt = now;
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
363
375
|
case 'done': {
|
|
364
376
|
const prev = this.streamSnapshots.get(sessionKey);
|
|
365
377
|
this.streamSnapshots.set(sessionKey, {
|
|
@@ -375,6 +387,7 @@ export class Bot {
|
|
|
375
387
|
model: prev?.model ?? null,
|
|
376
388
|
effort: prev?.effort ?? null,
|
|
377
389
|
previewMeta: prev?.previewMeta ?? null,
|
|
390
|
+
artifacts: prev?.artifacts,
|
|
378
391
|
startedAt: prev?.startedAt,
|
|
379
392
|
queuedTaskIds: prev?.queuedTaskIds,
|
|
380
393
|
updatedAt: now,
|
|
@@ -482,6 +495,58 @@ export class Bot {
|
|
|
482
495
|
emitStreamCancelled(taskId, fallbackKey) {
|
|
483
496
|
this.emitStream(this.liveSessionKey(taskId, fallbackKey), { type: 'cancelled', taskId });
|
|
484
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Wrap a per-turn `im_send_file` callback so every artifact delivery is
|
|
500
|
+
* recorded to the session's durable manifest (agent/artifacts.ts) and
|
|
501
|
+
* mirrored live into the running turn's snapshot — on top of whatever
|
|
502
|
+
* terminal-specific push the caller supplied (`inner`, e.g. an IM chat
|
|
503
|
+
* upload). The returned callback is always safe to register, so the tool is
|
|
504
|
+
* available for dashboard / headless turns that have no `inner` (the
|
|
505
|
+
* dashboard serves the recorded copy over HTTP). Recording is best-effort and
|
|
506
|
+
* never propagates an error back into the delivery result.
|
|
507
|
+
*/
|
|
508
|
+
buildArtifactSendFile(agent, sessionKey, cs, inner) {
|
|
509
|
+
return async (filePath, sendOpts) => {
|
|
510
|
+
// Terminal-specific push first (IM chat). Dashboard / headless has no inner.
|
|
511
|
+
const result = inner ? await inner(filePath, sendOpts) : { ok: true };
|
|
512
|
+
if (!result.ok)
|
|
513
|
+
return result;
|
|
514
|
+
try {
|
|
515
|
+
// Resolve the freshest session id at delivery time — send_file fires
|
|
516
|
+
// late in a turn, after a pending→native promotion has normally already
|
|
517
|
+
// happened, so the manifest lands under the canonical id.
|
|
518
|
+
let sid = '';
|
|
519
|
+
if (sessionKey) {
|
|
520
|
+
const rt = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
521
|
+
if (rt?.sessionId)
|
|
522
|
+
sid = rt.sessionId;
|
|
523
|
+
}
|
|
524
|
+
if (!sid && typeof cs.sessionId === 'string')
|
|
525
|
+
sid = cs.sessionId;
|
|
526
|
+
if (!sid || isPendingSessionId(sid))
|
|
527
|
+
return result;
|
|
528
|
+
const kind = sendOpts?.kind === 'photo' ? 'photo' : 'document';
|
|
529
|
+
const record = deliverArtifact(agent, sid, filePath, { kind, caption: sendOpts?.caption });
|
|
530
|
+
if (record && sessionKey) {
|
|
531
|
+
this.emitStream(this.resolveSessionKey(sessionKey), {
|
|
532
|
+
type: 'artifact',
|
|
533
|
+
artifact: {
|
|
534
|
+
url: attachmentUrl(agent, sid, record.path, { downloadName: record.fileName }),
|
|
535
|
+
fileName: record.fileName,
|
|
536
|
+
fileSize: record.fileSize,
|
|
537
|
+
mime: record.fileMime,
|
|
538
|
+
kind: record.kind,
|
|
539
|
+
...(record.caption ? { caption: record.caption } : {}),
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
this.warn(`[runStream] artifact record failed: ${e?.message || e}`);
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
};
|
|
549
|
+
}
|
|
485
550
|
keepAliveProc = null;
|
|
486
551
|
keepAlivePulseTimer = null;
|
|
487
552
|
sessionChains = new Map();
|
|
@@ -2376,7 +2441,21 @@ export class Bot {
|
|
|
2376
2441
|
// falls back to the agent's in-memory flag (IM /mode) when unspecified.
|
|
2377
2442
|
// Default off — never read from a persisted config default.
|
|
2378
2443
|
const workflowEnabled = cs.agent === 'claude' && (extras?.workflowEnabled ?? this.claudeWorkflowEnabled);
|
|
2379
|
-
|
|
2444
|
+
// ── Artifact delivery (terminal-agnostic) ──
|
|
2445
|
+
// `im_send_file` is the single "hand a file to the user" verb. Whatever the
|
|
2446
|
+
// caller wired as `mcpSendFile` (an IM channel push, or nothing for a
|
|
2447
|
+
// dashboard / headless turn) is wrapped so every delivery ALSO records the
|
|
2448
|
+
// file into the session's delivered-artifact manifest — the durable SSOT
|
|
2449
|
+
// that survives reload + workspace cleanup and is servable over HTTP — and
|
|
2450
|
+
// surfaces it live in the running turn's snapshot for any watching
|
|
2451
|
+
// dashboard. Always provided, so the tool + delivery prompt light up even
|
|
2452
|
+
// when there is no IM channel (the dashboard is the delivery surface then).
|
|
2453
|
+
const deliverySessionKey = ('key' in cs && typeof cs.key === 'string') ? cs.key : null;
|
|
2454
|
+
const wrappedSendFile = this.buildArtifactSendFile(cs.agent, deliverySessionKey, cs, mcpSendFile);
|
|
2455
|
+
const mcpSystemPrompt = appendExtraPrompt(appendExtraPrompt(appendExtraPrompt(
|
|
2456
|
+
// Always-on: `wrappedSendFile` is provided for every turn (see above),
|
|
2457
|
+
// so artifact delivery is available regardless of terminal.
|
|
2458
|
+
buildMcpDeliveryPrompt(), onInteraction && cs.agent === 'claude' ? buildClaudeAskUserPrompt() : ''), buildBrowserAutomationPrompt(browserEnabled)), workflowEnabled ? buildWorkflowOptInPrompt() : '');
|
|
2380
2459
|
// mcpSystemPrompt carries behaviour directives (use im_ask_user instead of
|
|
2381
2460
|
// built-in AskUserQuestion, browser automation status, artifact delivery)
|
|
2382
2461
|
// that must apply on every turn, not just the first — on resume the CLI
|
|
@@ -2433,7 +2512,7 @@ export class Bot {
|
|
|
2433
2512
|
// a Profile is bound).
|
|
2434
2513
|
hermesModel: cs.agent === 'hermes' && resolvedModel ? resolvedModel : undefined,
|
|
2435
2514
|
// MCP bridge
|
|
2436
|
-
mcpSendFile,
|
|
2515
|
+
mcpSendFile: wrappedSendFile,
|
|
2437
2516
|
abortSignal,
|
|
2438
2517
|
onInteraction,
|
|
2439
2518
|
onSteerReady,
|
package/dist/bot/commands.js
CHANGED
|
@@ -247,6 +247,52 @@ export async function getSessionsPageData(bot, chatId, page, pageSize = 5) {
|
|
|
247
247
|
sessions: entries,
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
|
+
export async function getSessionsDigestData(bot, chatId, limit = 8) {
|
|
251
|
+
const pageData = await getSessionsPageData(bot, chatId, 0, Math.max(1, limit));
|
|
252
|
+
const entries = pageData.sessions.map((session, index) => ({
|
|
253
|
+
index: index + 1,
|
|
254
|
+
agent: session.agent,
|
|
255
|
+
title: session.title,
|
|
256
|
+
time: session.time,
|
|
257
|
+
runState: session.runState,
|
|
258
|
+
runDetail: session.runDetail,
|
|
259
|
+
isCurrent: session.isCurrent,
|
|
260
|
+
sessionKey: session.key,
|
|
261
|
+
}));
|
|
262
|
+
return {
|
|
263
|
+
workspaceName: pageData.workspaceName,
|
|
264
|
+
agentTotals: pageData.agentTotals,
|
|
265
|
+
total: pageData.total,
|
|
266
|
+
entries,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export function formatSessionsDigestText(data) {
|
|
270
|
+
if (!data.entries.length) {
|
|
271
|
+
return data.workspaceName
|
|
272
|
+
? `No sessions in ${data.workspaceName} yet. Send a message to start.`
|
|
273
|
+
: 'No sessions yet. Send a message to start.';
|
|
274
|
+
}
|
|
275
|
+
const agentBits = Object.entries(data.agentTotals)
|
|
276
|
+
.map(([agent, count]) => `${agent}×${count}`)
|
|
277
|
+
.join(' · ');
|
|
278
|
+
const lines = [
|
|
279
|
+
`Session digest — ${data.workspaceName || 'workspace'} (${data.total} total${agentBits ? ` · ${agentBits}` : ''})`,
|
|
280
|
+
'',
|
|
281
|
+
];
|
|
282
|
+
for (const entry of data.entries) {
|
|
283
|
+
const flags = [
|
|
284
|
+
entry.isCurrent ? 'current' : null,
|
|
285
|
+
entry.runState === 'running' ? 'running' : null,
|
|
286
|
+
entry.runState === 'incomplete' ? 'unfinished' : null,
|
|
287
|
+
].filter(Boolean).join(', ');
|
|
288
|
+
const flagSuffix = flags ? ` [${flags}]` : '';
|
|
289
|
+
lines.push(`${entry.index}. ${entry.agent} · ${entry.title}${flagSuffix}`);
|
|
290
|
+
const detail = entry.runDetail ? ` · ${entry.runDetail}` : '';
|
|
291
|
+
lines.push(` ${entry.time}${detail}`);
|
|
292
|
+
}
|
|
293
|
+
lines.push('', 'Switch: /sessions <#> · Browse: /sessions');
|
|
294
|
+
return lines.join('\n');
|
|
295
|
+
}
|
|
250
296
|
export function extractLastSessionTurn(messages) {
|
|
251
297
|
if (!messages.length)
|
|
252
298
|
return null;
|
|
@@ -479,10 +525,10 @@ export function resolveSkillPrompt(bot, chatId, cmd, args) {
|
|
|
479
525
|
const paths = getProjectSkillPaths(wd, skill.name);
|
|
480
526
|
const skillFile = paths.claudeSkillFile || paths.sharedSkillFile || paths.agentsSkillFile;
|
|
481
527
|
if (skillFile) {
|
|
482
|
-
prompt = `${workdirHint}Read the skill definition at \`${skillFile}\` and execute the instructions defined there.${suffix}`;
|
|
528
|
+
prompt = `${workdirHint}Read the skill definition at \`${relSkillPath(wd, skillFile)}\` and execute the instructions defined there.${suffix}`;
|
|
483
529
|
}
|
|
484
530
|
else {
|
|
485
|
-
const fallbackPath =
|
|
531
|
+
const fallbackPath = relSkillPath(wd, path.join(wd, '.pikiloom', 'skills', skill.name, 'SKILL.md'));
|
|
486
532
|
prompt = `${workdirHint}Read the skill definition at \`${fallbackPath}\` and execute the instructions defined there.${suffix}`;
|
|
487
533
|
}
|
|
488
534
|
return { prompt, skillName: skill.name };
|
package/dist/bot/menu.js
CHANGED
|
@@ -31,6 +31,7 @@ export function indexSkillsByCommand(skills) {
|
|
|
31
31
|
export function buildDefaultMenuCommands(agentCount, skills = []) {
|
|
32
32
|
const commands = [
|
|
33
33
|
{ command: 'sessions', description: 'Switch sessions' },
|
|
34
|
+
{ command: 'digest', description: 'Recent session digest' },
|
|
34
35
|
];
|
|
35
36
|
if (agentCount > 1) {
|
|
36
37
|
commands.push({ command: 'agents', description: 'Switch agents' });
|
package/dist/bot/session-hub.js
CHANGED
|
@@ -121,7 +121,7 @@ function imageBlocksFromManagedRecord(record) {
|
|
|
121
121
|
const abs = path.isAbsolute(rel) ? rel : path.join(record.workspacePath, rel);
|
|
122
122
|
blocks.push({
|
|
123
123
|
type: 'image',
|
|
124
|
-
// `file://` sentinel — `
|
|
124
|
+
// `file://` sentinel — `rewriteAttachmentBlocksForTransport` (dashboard
|
|
125
125
|
// response layer) converts it to a proper /attachment URL.
|
|
126
126
|
content: `file://${abs}`,
|
|
127
127
|
imagePath: abs,
|
|
@@ -9,7 +9,7 @@ import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orches
|
|
|
9
9
|
import { shutdownAllDrivers } from '../../agent/driver.js';
|
|
10
10
|
import { expandTilde } from '../../core/platform.js';
|
|
11
11
|
import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
|
|
12
|
-
import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, getWorkspacesData, } from '../../bot/commands.js';
|
|
12
|
+
import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getSessionsDigestData, formatSessionsDigestText, getStartData, getWorkspacesData, } from '../../bot/commands.js';
|
|
13
13
|
import { DingtalkChannel } from './channel.js';
|
|
14
14
|
import { getActiveUserConfig } from '../../core/config/user-config.js';
|
|
15
15
|
const SHUTDOWN_EXIT_CODE = {
|
|
@@ -127,6 +127,7 @@ export class DingtalkBot extends Bot {
|
|
|
127
127
|
'/switch [path] - Change workdir',
|
|
128
128
|
'/workspaces [#] - Pick saved workspace',
|
|
129
129
|
'/sessions [new|#] - List/switch sessions',
|
|
130
|
+
'/digest - Recent session digest',
|
|
130
131
|
'/skills - List project skills',
|
|
131
132
|
'/stop - Stop current task',
|
|
132
133
|
'/restart - Restart pikiloom',
|
|
@@ -160,6 +161,9 @@ export class DingtalkBot extends Bot {
|
|
|
160
161
|
case 'sessions':
|
|
161
162
|
await this.cmdSessions(ctx, args);
|
|
162
163
|
return true;
|
|
164
|
+
case 'digest':
|
|
165
|
+
await this.cmdDigest(ctx);
|
|
166
|
+
return true;
|
|
163
167
|
case 'skills':
|
|
164
168
|
await this.cmdSkills(ctx);
|
|
165
169
|
return true;
|
|
@@ -187,6 +191,10 @@ export class DingtalkBot extends Bot {
|
|
|
187
191
|
lines.push('', 'Ready. Send a message to start.');
|
|
188
192
|
await ctx.reply(lines.join('\n'));
|
|
189
193
|
}
|
|
194
|
+
async cmdDigest(ctx) {
|
|
195
|
+
const data = await getSessionsDigestData(this, ctx.chatId);
|
|
196
|
+
await ctx.reply(formatSessionsDigestText(data));
|
|
197
|
+
}
|
|
190
198
|
async cmdStatus(ctx) {
|
|
191
199
|
const d = await getStatusDataAsync(this, ctx.chatId);
|
|
192
200
|
const gitLine = formatGitStatusLine(d.git);
|
|
@@ -13,7 +13,7 @@ import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from '../../bot/orches
|
|
|
13
13
|
import { shutdownAllDrivers } from '../../agent/driver.js';
|
|
14
14
|
import { expandTilde } from '../../core/platform.js';
|
|
15
15
|
import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
|
|
16
|
-
import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getStartData, getWorkspacesData, } from '../../bot/commands.js';
|
|
16
|
+
import { getStatusDataAsync, getHostDataSync, getAgentsListData, getSkillsListData, getModelsListData, getSessionsPageData, getSessionsDigestData, formatSessionsDigestText, getStartData, getWorkspacesData, } from '../../bot/commands.js';
|
|
17
17
|
import { DiscordChannel } from './channel.js';
|
|
18
18
|
import { getActiveUserConfig } from '../../core/config/user-config.js';
|
|
19
19
|
const SHUTDOWN_EXIT_CODE = {
|
|
@@ -123,6 +123,7 @@ export class DiscordBot extends Bot {
|
|
|
123
123
|
'/switch [path] - Change workdir',
|
|
124
124
|
'/workspaces [#] - Pick saved workspace',
|
|
125
125
|
'/sessions [new|#] - List/switch sessions',
|
|
126
|
+
'/digest - Recent session digest',
|
|
126
127
|
'/skills - List project skills',
|
|
127
128
|
'/stop - Stop current task',
|
|
128
129
|
'/restart - Restart pikiloom',
|
|
@@ -156,6 +157,9 @@ export class DiscordBot extends Bot {
|
|
|
156
157
|
case 'sessions':
|
|
157
158
|
await this.cmdSessions(ctx, args);
|
|
158
159
|
return true;
|
|
160
|
+
case 'digest':
|
|
161
|
+
await this.cmdDigest(ctx);
|
|
162
|
+
return true;
|
|
159
163
|
case 'skills':
|
|
160
164
|
await this.cmdSkills(ctx);
|
|
161
165
|
return true;
|
|
@@ -183,6 +187,10 @@ export class DiscordBot extends Bot {
|
|
|
183
187
|
lines.push('', 'Ready. Send a message to start.');
|
|
184
188
|
await ctx.reply(lines.join('\n'));
|
|
185
189
|
}
|
|
190
|
+
async cmdDigest(ctx) {
|
|
191
|
+
const data = await getSessionsDigestData(this, ctx.chatId);
|
|
192
|
+
await ctx.reply(formatSessionsDigestText(data));
|
|
193
|
+
}
|
|
186
194
|
async cmdStatus(ctx) {
|
|
187
195
|
const d = await getStatusDataAsync(this, ctx.chatId);
|
|
188
196
|
const gitLine = formatGitStatusLine(d.git);
|
|
@@ -15,7 +15,7 @@ import { stageSessionFiles, } from '../../agent/index.js';
|
|
|
15
15
|
import { shutdownAllDrivers } from '../../agent/driver.js';
|
|
16
16
|
import { expandTilde } from '../../core/platform.js';
|
|
17
17
|
import { SKILL_CMD_PREFIX, } from '../../bot/menu.js';
|
|
18
|
-
import { getStartData, getSessionsPageData, getModelsListData, getSessionTurnPreviewData, getStatusDataAsync, getHostDataSync, getWorkspacesData, resolveSkillPrompt, handleGoalCommand, } from '../../bot/commands.js';
|
|
18
|
+
import { getStartData, getSessionsPageData, getModelsListData, getSessionTurnPreviewData, getStatusDataAsync, getHostDataSync, getWorkspacesData, resolveSkillPrompt, handleGoalCommand, getSessionsDigestData, formatSessionsDigestText, } from '../../bot/commands.js';
|
|
19
19
|
import { buildAgentsCommandView, buildModelsCommandView, buildModeCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from '../../bot/command-ui.js';
|
|
20
20
|
import { LivePreview } from '../telegram/live-preview.js';
|
|
21
21
|
import { registerProcessRuntime, requestProcessRestart, } from '../../core/process-control.js';
|
|
@@ -355,6 +355,10 @@ export class FeishuBot extends Bot {
|
|
|
355
355
|
}
|
|
356
356
|
await this.sendCommandView(ctx, await buildSessionsCommandView(this, ctx.chatId, 0, this.sessionsPageSize));
|
|
357
357
|
}
|
|
358
|
+
async cmdDigest(ctx) {
|
|
359
|
+
const data = await getSessionsDigestData(this, ctx.chatId);
|
|
360
|
+
await ctx.reply(formatSessionsDigestText(data));
|
|
361
|
+
}
|
|
358
362
|
async cmdStatus(ctx) {
|
|
359
363
|
const d = await getStatusDataAsync(this, ctx.chatId);
|
|
360
364
|
await ctx.reply(renderStatus(d));
|
|
@@ -962,6 +966,9 @@ export class FeishuBot extends Bot {
|
|
|
962
966
|
case 'sessions':
|
|
963
967
|
await this.cmdSessions(ctx, args);
|
|
964
968
|
return;
|
|
969
|
+
case 'digest':
|
|
970
|
+
await this.cmdDigest(ctx);
|
|
971
|
+
return;
|
|
965
972
|
case 'agents':
|
|
966
973
|
await this.cmdAgents(ctx, args);
|
|
967
974
|
return;
|