kimaki 0.29.0 → 0.30.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/dist/agent-model.e2e.test.js +33 -33
- package/dist/anthropic-account-identity.js +1 -1
- package/dist/anthropic-auth-plugin.js +5 -5
- package/dist/anthropic-auth-plugin.test.js +30 -2
- package/dist/anthropic-auth-state.js +1 -1
- package/dist/bin.js +7 -3
- package/dist/bot-lifecycle.test.js +206 -0
- package/dist/cache-drift-plugin.js +25 -18
- package/dist/cache-rewrite.js +39 -0
- package/dist/cache-rewrite.test.js +41 -0
- package/dist/cli-commands/session.js +37 -6
- package/dist/cli-parsing.test.js +3 -3
- package/dist/cli-runner.js +11 -1
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +4 -4
- package/dist/commands/ask-question.js +72 -23
- package/dist/commands/queue.js +7 -7
- package/dist/database.js +73 -6
- package/dist/db.test.js +104 -1
- package/dist/discord-bot.js +212 -144
- package/dist/external-directory-permissions.e2e.test.js +3 -3
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/forum-sync/watchers.js +15 -0
- package/dist/gateway-proxy.e2e.test.js +9 -9
- package/dist/hrana-server.js +68 -14
- package/dist/markdown.js +164 -102
- package/dist/markdown.test.js +88 -20
- package/dist/message-formatting.js +20 -2
- package/dist/message-formatting.test.js +20 -1
- package/dist/opencode.js +27 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-resume.e2e.test.js +100 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/question-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +3 -3
- package/dist/queue-advanced-action-buttons.e2e.test.js +5 -5
- package/dist/queue-advanced-e2e-setup.js +63 -0
- package/dist/queue-advanced-footer.e2e.test.js +18 -18
- package/dist/queue-advanced-model-switch.e2e.test.js +3 -4
- package/dist/queue-advanced-permissions-typing.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +4 -4
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +3 -3
- package/dist/queue-interrupt-drain.e2e.test.js +10 -10
- package/dist/queue-question-select-drain.e2e.test.js +5 -5
- package/dist/runtime-lifecycle.e2e.test.js +95 -11
- package/dist/session-handler/event-stream-state.js +187 -1
- package/dist/session-handler/event-stream-state.test.js +764 -1
- package/dist/session-handler/thread-session-runtime.js +185 -16
- package/dist/sleep.e2e.test.js +7 -7
- package/dist/store.js +1 -1
- package/dist/system-message.js +9 -10
- package/dist/system-message.test.js +19 -10
- package/dist/task-runner.js +7 -0
- package/dist/task-schedule.test.js +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/thread-message-queue.e2e.test.js +30 -31
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-message.e2e.test.js +64 -26
- package/dist/voice-routing-input.e2e.test.js +4 -4
- package/dist/voice-routing-question.e2e.test.js +4 -4
- package/dist/voice.js +107 -21
- package/dist/voice.test.js +52 -1
- package/dist/wait-session.js +24 -2
- package/dist/worktree-lifecycle.e2e.test.js +8 -8
- package/package.json +5 -5
- package/skills/egaki/SKILL.md +14 -0
- package/skills/holocron/SKILL.md +125 -42
- package/skills/opensrc/SKILL.md +20 -3
- package/skills/spiceflow/SKILL.md +53 -172
- package/src/agent-model.e2e.test.ts +33 -33
- package/src/anthropic-account-identity.ts +1 -1
- package/src/anthropic-auth-plugin.test.ts +37 -2
- package/src/anthropic-auth-plugin.ts +9 -7
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bin.ts +7 -3
- package/src/bot-lifecycle.test.ts +252 -0
- package/src/cache-drift-plugin.ts +34 -24
- package/src/cache-rewrite.test.ts +49 -0
- package/src/cache-rewrite.ts +96 -0
- package/src/cli-commands/session.ts +48 -8
- package/src/cli-parsing.test.ts +3 -3
- package/src/cli-runner.ts +11 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +6 -6
- package/src/commands/ask-question.ts +88 -25
- package/src/commands/queue.ts +7 -7
- package/src/database.ts +105 -6
- package/src/db.test.ts +124 -0
- package/src/discord-bot.ts +261 -183
- package/src/external-directory-permissions.e2e.test.ts +3 -3
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/forum-sync/watchers.ts +20 -0
- package/src/gateway-proxy.e2e.test.ts +9 -9
- package/src/hrana-server.ts +96 -16
- package/src/markdown.test.ts +108 -22
- package/src/markdown.ts +216 -103
- package/src/message-formatting.test.ts +24 -1
- package/src/message-formatting.ts +21 -2
- package/src/opencode.ts +28 -13
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-resume.e2e.test.ts +121 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/question-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +3 -3
- package/src/queue-advanced-action-buttons.e2e.test.ts +5 -5
- package/src/queue-advanced-e2e-setup.ts +65 -0
- package/src/queue-advanced-footer.e2e.test.ts +18 -18
- package/src/queue-advanced-model-switch.e2e.test.ts +3 -4
- package/src/queue-advanced-permissions-typing.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +4 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +3 -3
- package/src/queue-interrupt-drain.e2e.test.ts +10 -10
- package/src/queue-question-select-drain.e2e.test.ts +5 -5
- package/src/runtime-lifecycle.e2e.test.ts +104 -10
- package/src/session-handler/event-stream-state.test.ts +825 -0
- package/src/session-handler/event-stream-state.ts +265 -3
- package/src/session-handler/thread-session-runtime.ts +217 -14
- package/src/sleep.e2e.test.ts +7 -7
- package/src/store.ts +2 -2
- package/src/system-message.test.ts +20 -10
- package/src/system-message.ts +9 -10
- package/src/task-runner.ts +8 -0
- package/src/task-schedule.test.ts +1 -1
- package/src/test-utils.ts +5 -3
- package/src/thread-message-queue.e2e.test.ts +30 -31
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-message.e2e.test.ts +70 -26
- package/src/voice-routing-input.e2e.test.ts +4 -4
- package/src/voice-routing-question.e2e.test.ts +4 -4
- package/src/voice.test.ts +56 -0
- package/src/voice.ts +135 -25
- package/src/wait-session.ts +25 -2
- package/src/worktree-lifecycle.e2e.test.ts +8 -8
|
@@ -323,9 +323,9 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
323
323
|
"--- from: user (proxy-tester)
|
|
324
324
|
hello from gateway proxy test
|
|
325
325
|
--- from: assistant (TestBot)
|
|
326
|
-
|
|
326
|
+
-# *using deterministic-provider/deterministic-v2*
|
|
327
327
|
> gateway-proxy-reply
|
|
328
|
-
|
|
328
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
329
329
|
`);
|
|
330
330
|
expect(reply).toBeDefined();
|
|
331
331
|
expect(getMessageVisibleText(reply).trim().length).toBeGreaterThan(0);
|
|
@@ -350,14 +350,14 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
350
350
|
"--- from: user (proxy-tester)
|
|
351
351
|
hello from gateway proxy test
|
|
352
352
|
--- from: assistant (TestBot)
|
|
353
|
-
|
|
353
|
+
-# *using deterministic-provider/deterministic-v2*
|
|
354
354
|
> gateway-proxy-reply
|
|
355
|
-
|
|
355
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
356
356
|
--- from: user (proxy-tester)
|
|
357
357
|
follow up through proxy
|
|
358
358
|
--- from: assistant (TestBot)
|
|
359
359
|
> gateway-proxy-reply
|
|
360
|
-
|
|
360
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
361
361
|
`);
|
|
362
362
|
expect(reply).toBeDefined();
|
|
363
363
|
expect(getMessageVisibleText(reply).trim().length).toBeGreaterThan(0);
|
|
@@ -382,14 +382,14 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
382
382
|
"--- from: user (proxy-tester)
|
|
383
383
|
hello from gateway proxy test
|
|
384
384
|
--- from: assistant (TestBot)
|
|
385
|
-
|
|
385
|
+
-# *using deterministic-provider/deterministic-v2*
|
|
386
386
|
> gateway-proxy-reply
|
|
387
|
-
|
|
387
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
388
388
|
--- from: user (proxy-tester)
|
|
389
389
|
follow up through proxy
|
|
390
390
|
--- from: assistant (TestBot)
|
|
391
391
|
> gateway-proxy-reply
|
|
392
|
-
|
|
392
|
+
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
393
393
|
--- from: user (proxy-tester)
|
|
394
394
|
!echo proxy-shell-test
|
|
395
395
|
--- from: assistant (TestBot)
|
|
@@ -417,7 +417,7 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
417
417
|
"--- from: user (proxy-tester)
|
|
418
418
|
second message through proxy
|
|
419
419
|
--- from: assistant (TestBot)
|
|
420
|
-
|
|
420
|
+
-# *using deterministic-provider/deterministic-v2*"
|
|
421
421
|
`);
|
|
422
422
|
expect(reply).toBeDefined();
|
|
423
423
|
expect(getMessageVisibleText(reply).trim().length).toBeGreaterThan(0);
|
package/dist/hrana-server.js
CHANGED
|
@@ -148,7 +148,7 @@ export async function startHranaServer({ dbPath, bindAll = false, }) {
|
|
|
148
148
|
// Health check — no auth required
|
|
149
149
|
if (pathname === '/health') {
|
|
150
150
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
151
|
-
res.end(JSON.stringify({ status: 'ok', pid: process.pid }));
|
|
151
|
+
res.end(JSON.stringify({ status: 'ok', pid: process.pid, wrapperPid: getWrapperPid() }));
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
// OpenCode server port discovery — no auth required (localhost only).
|
|
@@ -230,8 +230,12 @@ export async function stopHranaServer() {
|
|
|
230
230
|
* Evict a previous kimaki instance on the lock port.
|
|
231
231
|
* Fetches /health to get the running process PID, then kills it directly.
|
|
232
232
|
* No lsof/netstat/spawnSync needed — the PID comes from the health response.
|
|
233
|
+
*
|
|
234
|
+
* SIGTERM first so the old bot can clean up. Its own shutdown deadline is
|
|
235
|
+
* 15s, so after a longer grace period we SIGKILL: a stuck process must never
|
|
236
|
+
* block every future start. Safe because the PID comes from our own /health.
|
|
233
237
|
*/
|
|
234
|
-
export async function evictExistingInstance({ port }) {
|
|
238
|
+
export async function evictExistingInstance({ port, gracePeriodMs = 20_000, }) {
|
|
235
239
|
const url = `http://127.0.0.1:${port}/health`;
|
|
236
240
|
const probe = await fetch(url, { signal: AbortSignal.timeout(1000) }).catch((e) => new FetchError({ url, cause: e }));
|
|
237
241
|
if (probe instanceof Error)
|
|
@@ -242,9 +246,15 @@ export async function evictExistingInstance({ port }) {
|
|
|
242
246
|
const targetPid = body.pid;
|
|
243
247
|
if (!targetPid || targetPid === process.pid)
|
|
244
248
|
return;
|
|
245
|
-
|
|
249
|
+
// Signal the bin.ts wrapper, not only the child: killing just the child
|
|
250
|
+
// looks like a crash and the old wrapper respawns it, which then evicts us.
|
|
251
|
+
// The wrapper forwards SIGTERM and does not restart after it.
|
|
252
|
+
const wrapperPid = body.wrapperPid && body.wrapperPid !== process.ppid
|
|
253
|
+
? body.wrapperPid
|
|
254
|
+
: null;
|
|
255
|
+
hranaLogger.log(`Evicting existing kimaki process (PID: ${targetPid}, wrapper: ${wrapperPid ?? 'none'}) on port ${port}`);
|
|
246
256
|
const killResult = errore.try(() => {
|
|
247
|
-
process.kill(targetPid, 'SIGTERM');
|
|
257
|
+
process.kill(wrapperPid ?? targetPid, 'SIGTERM');
|
|
248
258
|
}, (e) => new Error('Failed to send SIGTERM to existing kimaki process', {
|
|
249
259
|
cause: e,
|
|
250
260
|
}));
|
|
@@ -252,17 +262,61 @@ export async function evictExistingInstance({ port }) {
|
|
|
252
262
|
hranaLogger.log(`Failed to kill PID ${targetPid}: ${killResult.message}`);
|
|
253
263
|
return;
|
|
254
264
|
}
|
|
255
|
-
for
|
|
265
|
+
// Wait for process exit, not just a failed probe: a process that already
|
|
266
|
+
// closed its HTTP server can still be alive and holding the socket briefly.
|
|
267
|
+
if (await waitForProcessExit({ pid: targetPid, timeoutMs: gracePeriodMs })) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
hranaLogger.log(`PID ${targetPid} still alive after ${gracePeriodMs / 1000}s SIGTERM grace period, sending SIGKILL`);
|
|
271
|
+
// Wrapper first so it cannot respawn the child we are about to kill.
|
|
272
|
+
const wrapperKillResult = wrapperPid
|
|
273
|
+
? errore.try(() => {
|
|
274
|
+
process.kill(wrapperPid, 'SIGKILL');
|
|
275
|
+
}, (e) => new Error('Failed to send SIGKILL to kimaki wrapper', { cause: e }))
|
|
276
|
+
: null;
|
|
277
|
+
if (wrapperKillResult instanceof Error) {
|
|
278
|
+
hranaLogger.log(`Failed to kill wrapper PID ${wrapperPid}: ${wrapperKillResult.message}`);
|
|
279
|
+
}
|
|
280
|
+
const forceKillResult = errore.try(() => {
|
|
281
|
+
process.kill(targetPid, 'SIGKILL');
|
|
282
|
+
}, (e) => new Error('Failed to send SIGKILL to existing kimaki process', {
|
|
283
|
+
cause: e,
|
|
284
|
+
}));
|
|
285
|
+
if (forceKillResult instanceof Error) {
|
|
286
|
+
hranaLogger.log(`Failed to kill PID ${targetPid}: ${forceKillResult.message}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (await waitForProcessExit({ pid: targetPid, timeoutMs: 5_000 })) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
hranaLogger.log(`PID ${targetPid} still alive after SIGKILL`);
|
|
293
|
+
}
|
|
294
|
+
// PID of the bin.ts respawn wrapper, only while it is alive (IPC connected).
|
|
295
|
+
// Without the connected check an orphan would report ppid 1.
|
|
296
|
+
function getWrapperPid() {
|
|
297
|
+
if (!process.env.__KIMAKI_CHILD || !process.connected) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return process.ppid;
|
|
301
|
+
}
|
|
302
|
+
function isProcessAlive(pid) {
|
|
303
|
+
const result = errore.try(() => {
|
|
304
|
+
process.kill(pid, 0);
|
|
305
|
+
}, (e) => new Error('Process liveness check failed', { cause: e }));
|
|
306
|
+
if (result instanceof Error) {
|
|
307
|
+
// EPERM means the PID exists but belongs to another user.
|
|
308
|
+
return result.cause instanceof Error && Reflect.get(result.cause, 'code') === 'EPERM';
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
async function waitForProcessExit({ pid, timeoutMs, }) {
|
|
313
|
+
const deadline = Date.now() + timeoutMs;
|
|
314
|
+
while (Date.now() < deadline) {
|
|
315
|
+
if (!isProcessAlive(pid))
|
|
316
|
+
return true;
|
|
256
317
|
await new Promise((resolve) => {
|
|
257
|
-
setTimeout(resolve,
|
|
318
|
+
setTimeout(resolve, 200);
|
|
258
319
|
});
|
|
259
|
-
// Verify it's gone. Some shutdown paths need a few seconds to run cleanup,
|
|
260
|
-
// so we avoid SIGKILL and just poll for up to 10 seconds.
|
|
261
|
-
const secondProbe = await fetch(url, {
|
|
262
|
-
signal: AbortSignal.timeout(2000),
|
|
263
|
-
}).catch((e) => new FetchError({ url, cause: e }));
|
|
264
|
-
if (secondProbe instanceof Error)
|
|
265
|
-
return;
|
|
266
320
|
}
|
|
267
|
-
|
|
321
|
+
return !isProcessAlive(pid);
|
|
268
322
|
}
|
package/dist/markdown.js
CHANGED
|
@@ -15,6 +15,112 @@ class UnexpectedError extends errore.createTaggedError({
|
|
|
15
15
|
}
|
|
16
16
|
const markdownLogger = createLogger(LogPrefix.MARKDOWN);
|
|
17
17
|
const TOOL_OUTPUT_MAX_CHARS = 30_000;
|
|
18
|
+
export const DEFAULT_TOOL_INPUT_MAX_CHARS = 80;
|
|
19
|
+
function stringifyToolValue(value) {
|
|
20
|
+
if (typeof value === 'string')
|
|
21
|
+
return value;
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
export function truncateChars(value, maxChars) {
|
|
25
|
+
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
26
|
+
if (maxChars <= 0)
|
|
27
|
+
return '';
|
|
28
|
+
if (collapsed.length <= maxChars)
|
|
29
|
+
return collapsed;
|
|
30
|
+
if (maxChars === 1)
|
|
31
|
+
return '…';
|
|
32
|
+
return `${collapsed.slice(0, maxChars - 1)}…`;
|
|
33
|
+
}
|
|
34
|
+
export function formatDuration(ms) {
|
|
35
|
+
if (ms < 1000)
|
|
36
|
+
return `${ms}ms`;
|
|
37
|
+
if (ms < 60_000)
|
|
38
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
39
|
+
const minutes = Math.floor(ms / 60_000);
|
|
40
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
41
|
+
return `${minutes}m ${seconds}s`;
|
|
42
|
+
}
|
|
43
|
+
function messageCreatedAt(message) {
|
|
44
|
+
const created = message.time?.created;
|
|
45
|
+
return typeof created === 'number' ? created : undefined;
|
|
46
|
+
}
|
|
47
|
+
function messageEndedAt(message) {
|
|
48
|
+
const completed = message.time?.completed;
|
|
49
|
+
if (typeof completed === 'number')
|
|
50
|
+
return completed;
|
|
51
|
+
return messageCreatedAt(message);
|
|
52
|
+
}
|
|
53
|
+
/** User sent → last assistant finished, until the next user message. */
|
|
54
|
+
export function userPromptDurationMs({ messages, userIndex, }) {
|
|
55
|
+
const userCreated = messageCreatedAt(messages[userIndex]?.info ?? {});
|
|
56
|
+
if (userCreated === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
let end = userCreated;
|
|
59
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
60
|
+
const info = messages[i]?.info;
|
|
61
|
+
if (info?.role === 'user')
|
|
62
|
+
break;
|
|
63
|
+
if (info?.role !== 'assistant')
|
|
64
|
+
continue;
|
|
65
|
+
const ended = messageEndedAt(info);
|
|
66
|
+
if (ended !== undefined && ended > end)
|
|
67
|
+
end = ended;
|
|
68
|
+
}
|
|
69
|
+
const ms = end - userCreated;
|
|
70
|
+
return ms > 0 ? ms : undefined;
|
|
71
|
+
}
|
|
72
|
+
export function fileBaseName(filePath) {
|
|
73
|
+
const normalized = filePath.replaceAll('\\', '/');
|
|
74
|
+
const segments = normalized.split('/');
|
|
75
|
+
return segments[segments.length - 1] || filePath;
|
|
76
|
+
}
|
|
77
|
+
function readTaskSessionId(metadata) {
|
|
78
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
79
|
+
return undefined;
|
|
80
|
+
const sessionId = metadata.sessionId;
|
|
81
|
+
return typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : undefined;
|
|
82
|
+
}
|
|
83
|
+
function taskChildSessionId({ input, metadata, }) {
|
|
84
|
+
const sessionId = readTaskSessionId(metadata);
|
|
85
|
+
if (sessionId)
|
|
86
|
+
return sessionId;
|
|
87
|
+
const taskId = input?.task_id;
|
|
88
|
+
if (typeof taskId === 'string' && taskId.startsWith('ses'))
|
|
89
|
+
return taskId;
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Compact tool input for session markdown. Keep this greppable and short. */
|
|
93
|
+
export function formatCompactToolSummary({ tool, input, maxChars = DEFAULT_TOOL_INPUT_MAX_CHARS, metadata, }) {
|
|
94
|
+
const record = input ?? {};
|
|
95
|
+
if (tool === 'read') {
|
|
96
|
+
const path = record.filePath ?? record.path;
|
|
97
|
+
return typeof path === 'string' && path.length > 0 ? fileBaseName(path) : '';
|
|
98
|
+
}
|
|
99
|
+
if (tool === 'task') {
|
|
100
|
+
const description = typeof record.description === 'string' ? record.description : '';
|
|
101
|
+
const sessionId = taskChildSessionId({ input: record, metadata });
|
|
102
|
+
if (!sessionId)
|
|
103
|
+
return truncateChars(description, maxChars);
|
|
104
|
+
const separator = description.trim() ? 1 : 0;
|
|
105
|
+
const descBudget = Math.max(0, maxChars - sessionId.length - separator);
|
|
106
|
+
const desc = truncateChars(description, descBudget);
|
|
107
|
+
return [desc, sessionId].filter(Boolean).join(' ');
|
|
108
|
+
}
|
|
109
|
+
if (tool === 'bash') {
|
|
110
|
+
const command = typeof record.command === 'string' ? record.command : '';
|
|
111
|
+
const collapsedCommand = command.replace(/\s+/g, ' ').trim();
|
|
112
|
+
if (collapsedCommand.length > 0 && collapsedCommand.length <= maxChars) {
|
|
113
|
+
return collapsedCommand;
|
|
114
|
+
}
|
|
115
|
+
const description = typeof record.description === 'string' ? record.description : '';
|
|
116
|
+
return truncateChars(description || collapsedCommand, maxChars);
|
|
117
|
+
}
|
|
118
|
+
const parts = [];
|
|
119
|
+
for (const [key, value] of Object.entries(record)) {
|
|
120
|
+
parts.push(`${key}=${stringifyToolValue(value)}`);
|
|
121
|
+
}
|
|
122
|
+
return truncateChars(parts.join(' '), maxChars);
|
|
123
|
+
}
|
|
18
124
|
export class ShareMarkdown {
|
|
19
125
|
client;
|
|
20
126
|
constructor(client) {
|
|
@@ -26,7 +132,7 @@ export class ShareMarkdown {
|
|
|
26
132
|
* @returns Error or markdown string
|
|
27
133
|
*/
|
|
28
134
|
async generate(options) {
|
|
29
|
-
const { sessionID, includeSystemInfo, lastAssistantOnly, compactTools = true } = options;
|
|
135
|
+
const { sessionID, includeSystemInfo, lastAssistantOnly, compactTools = true, includeThinking = false, toolInputMaxChars = DEFAULT_TOOL_INPUT_MAX_CHARS, } = options;
|
|
30
136
|
// Get session info
|
|
31
137
|
const sessionResponse = await this.client.session.get({
|
|
32
138
|
sessionID,
|
|
@@ -45,12 +151,7 @@ export class ShareMarkdown {
|
|
|
45
151
|
const messages = messagesResponse.data;
|
|
46
152
|
// If lastAssistantOnly, filter to only the last assistant message
|
|
47
153
|
const messagesToRender = lastAssistantOnly
|
|
48
|
-
? (() =>
|
|
49
|
-
const assistantMessages = messages.filter((m) => m.info.role === 'assistant');
|
|
50
|
-
return assistantMessages.length > 0
|
|
51
|
-
? [assistantMessages[assistantMessages.length - 1]]
|
|
52
|
-
: [];
|
|
53
|
-
})()
|
|
154
|
+
? messages.filter((m) => m.info.role === 'assistant').slice(-1)
|
|
54
155
|
: messages;
|
|
55
156
|
// Build markdown
|
|
56
157
|
const lines = [];
|
|
@@ -74,17 +175,37 @@ export class ShareMarkdown {
|
|
|
74
175
|
lines.push('## Conversation');
|
|
75
176
|
lines.push('');
|
|
76
177
|
}
|
|
77
|
-
for (const message of messagesToRender) {
|
|
78
|
-
const messageLines = this.renderMessage(message.info, message.parts, {
|
|
178
|
+
for (const [index, message] of messagesToRender.entries()) {
|
|
179
|
+
const messageLines = this.renderMessage(message.info, message.parts, {
|
|
180
|
+
compactTools,
|
|
181
|
+
includeThinking,
|
|
182
|
+
toolInputMaxChars,
|
|
183
|
+
});
|
|
79
184
|
lines.push(...messageLines);
|
|
80
185
|
lines.push('');
|
|
186
|
+
const nextRole = messagesToRender[index + 1]?.info.role;
|
|
187
|
+
if (message.info.role !== 'assistant' || nextRole === 'assistant')
|
|
188
|
+
continue;
|
|
189
|
+
for (let userIndex = index - 1; userIndex >= 0; userIndex--) {
|
|
190
|
+
if (messagesToRender[userIndex]?.info.role !== 'user')
|
|
191
|
+
continue;
|
|
192
|
+
const durationMs = userPromptDurationMs({
|
|
193
|
+
messages: messagesToRender,
|
|
194
|
+
userIndex,
|
|
195
|
+
});
|
|
196
|
+
if (durationMs !== undefined) {
|
|
197
|
+
lines.push(`duration: ${formatDuration(durationMs)}`);
|
|
198
|
+
lines.push('');
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
81
202
|
}
|
|
82
203
|
return lines.join('\n');
|
|
83
204
|
}
|
|
84
205
|
renderMessage(message, parts, opts) {
|
|
85
206
|
const lines = [];
|
|
86
207
|
if (message.role === 'user') {
|
|
87
|
-
lines.push('###
|
|
208
|
+
lines.push('### user');
|
|
88
209
|
lines.push('');
|
|
89
210
|
for (const part of parts) {
|
|
90
211
|
if (part.type === 'text' && part.text) {
|
|
@@ -95,7 +216,7 @@ export class ShareMarkdown {
|
|
|
95
216
|
}
|
|
96
217
|
}
|
|
97
218
|
else if (part.type === 'file') {
|
|
98
|
-
lines.push(
|
|
219
|
+
lines.push(`file: ${part.filename || 'unnamed file'}`);
|
|
99
220
|
if (part.url) {
|
|
100
221
|
lines.push(` - URL: ${part.url}`);
|
|
101
222
|
}
|
|
@@ -104,47 +225,38 @@ export class ShareMarkdown {
|
|
|
104
225
|
}
|
|
105
226
|
}
|
|
106
227
|
else if (message.role === 'assistant') {
|
|
107
|
-
const modelId = [message.providerID, message.modelID].filter(Boolean).join('/') ||
|
|
108
|
-
'unknown model';
|
|
109
|
-
lines.push(`### 🤖 Assistant (${modelId})`);
|
|
110
|
-
lines.push('');
|
|
111
|
-
// Filter and process parts
|
|
112
228
|
const filteredParts = parts.filter((part) => {
|
|
113
|
-
if (part.type === 'step-start'
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (part.type === 'patch')
|
|
229
|
+
if (part.type === 'step-start' ||
|
|
230
|
+
part.type === 'step-finish' ||
|
|
231
|
+
part.type === 'snapshot' ||
|
|
232
|
+
part.type === 'patch')
|
|
118
233
|
return false;
|
|
119
|
-
if (part.type === '
|
|
120
|
-
return false;
|
|
121
|
-
if (part.type === 'text' && part.synthetic === true)
|
|
234
|
+
if (part.type === 'text' && (part.synthetic === true || !part.text))
|
|
122
235
|
return false;
|
|
123
236
|
if (part.type === 'tool' && part.tool === 'todoread')
|
|
124
237
|
return false;
|
|
125
|
-
if (part.type === 'text' && !part.text)
|
|
126
|
-
return false;
|
|
127
238
|
if (part.type === 'tool' &&
|
|
128
239
|
(part.state.status === 'pending' || part.state.status === 'running'))
|
|
129
240
|
return false;
|
|
130
241
|
return true;
|
|
131
242
|
});
|
|
243
|
+
const body = [];
|
|
132
244
|
for (const part of filteredParts) {
|
|
133
|
-
|
|
134
|
-
? this.renderPartCompact(part,
|
|
135
|
-
: this.renderPart(part,
|
|
136
|
-
lines.push(...partLines);
|
|
137
|
-
}
|
|
138
|
-
// Add completion time if available
|
|
139
|
-
if (message.time?.completed) {
|
|
140
|
-
const duration = message.time.completed - message.time.created;
|
|
141
|
-
lines.push('');
|
|
142
|
-
lines.push(`*Completed in ${this.formatDuration(duration)}*`);
|
|
245
|
+
body.push(...(opts.compactTools
|
|
246
|
+
? this.renderPartCompact(part, opts)
|
|
247
|
+
: this.renderPart(part, opts)));
|
|
143
248
|
}
|
|
249
|
+
if (body.length === 0)
|
|
250
|
+
return lines;
|
|
251
|
+
const modelId = [message.providerID, message.modelID].filter(Boolean).join('/') ||
|
|
252
|
+
'unknown model';
|
|
253
|
+
lines.push(`### assistant (${modelId})`);
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push(...body);
|
|
144
256
|
}
|
|
145
257
|
return lines;
|
|
146
258
|
}
|
|
147
|
-
renderPart(part,
|
|
259
|
+
renderPart(part, opts) {
|
|
148
260
|
const lines = [];
|
|
149
261
|
switch (part.type) {
|
|
150
262
|
case 'text':
|
|
@@ -154,14 +266,11 @@ export class ShareMarkdown {
|
|
|
154
266
|
}
|
|
155
267
|
break;
|
|
156
268
|
case 'reasoning':
|
|
157
|
-
if (part.text) {
|
|
158
|
-
lines.push('
|
|
159
|
-
lines.push('<summary>💭 Thinking</summary>');
|
|
269
|
+
if (opts.includeThinking && part.text) {
|
|
270
|
+
lines.push('thinking:');
|
|
160
271
|
lines.push('');
|
|
161
272
|
lines.push(part.text);
|
|
162
273
|
lines.push('');
|
|
163
|
-
lines.push('</details>');
|
|
164
|
-
lines.push('');
|
|
165
274
|
}
|
|
166
275
|
break;
|
|
167
276
|
case 'tool':
|
|
@@ -169,10 +278,10 @@ export class ShareMarkdown {
|
|
|
169
278
|
const output = part.state.output || '';
|
|
170
279
|
const isOversized = output.length > TOOL_OUTPUT_MAX_CHARS;
|
|
171
280
|
if (isOversized) {
|
|
172
|
-
lines.push(`>
|
|
281
|
+
lines.push(`> Large tool output (${output.length.toLocaleString()} chars, truncated to ${TOOL_OUTPUT_MAX_CHARS.toLocaleString()})`);
|
|
173
282
|
lines.push('');
|
|
174
283
|
}
|
|
175
|
-
lines.push(`####
|
|
284
|
+
lines.push(`#### tool: ${part.tool}`);
|
|
176
285
|
lines.push('');
|
|
177
286
|
// Render input parameters in YAML
|
|
178
287
|
if (part.state.input && Object.keys(part.state.input).length > 0) {
|
|
@@ -193,17 +302,9 @@ export class ShareMarkdown {
|
|
|
193
302
|
lines.push('```');
|
|
194
303
|
lines.push('');
|
|
195
304
|
}
|
|
196
|
-
// Add timing info if significant
|
|
197
|
-
if (part.state.time?.start && part.state.time?.end) {
|
|
198
|
-
const duration = part.state.time.end - part.state.time.start;
|
|
199
|
-
if (duration > 2000) {
|
|
200
|
-
lines.push(`*Duration: ${this.formatDuration(duration)}*`);
|
|
201
|
-
lines.push('');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
305
|
}
|
|
205
306
|
else if (part.state.status === 'error') {
|
|
206
|
-
lines.push(`####
|
|
307
|
+
lines.push(`#### tool-error: ${part.tool}`);
|
|
207
308
|
lines.push('');
|
|
208
309
|
lines.push('```');
|
|
209
310
|
lines.push(part.state.error || 'Unknown error');
|
|
@@ -211,75 +312,36 @@ export class ShareMarkdown {
|
|
|
211
312
|
lines.push('');
|
|
212
313
|
}
|
|
213
314
|
break;
|
|
214
|
-
case 'step-start':
|
|
215
|
-
lines.push(`**Started using ${message.providerID}/${message.modelID}**`);
|
|
216
|
-
lines.push('');
|
|
217
|
-
break;
|
|
218
315
|
}
|
|
219
316
|
return lines;
|
|
220
317
|
}
|
|
221
318
|
/** Compact rendering: tool calls become a single line with line count instead of full output. */
|
|
222
|
-
renderPartCompact(part,
|
|
223
|
-
// Non-tool parts render the same as verbose
|
|
319
|
+
renderPartCompact(part, opts) {
|
|
224
320
|
if (part.type !== 'tool') {
|
|
225
|
-
return this.renderPart(part,
|
|
321
|
+
return this.renderPart(part, opts);
|
|
226
322
|
}
|
|
227
323
|
const lines = [];
|
|
228
324
|
if (part.state.status === 'completed') {
|
|
229
325
|
const output = part.state.output || '';
|
|
230
326
|
const lineCount = output ? output.split('\n').length : 0;
|
|
231
|
-
const inputSummary =
|
|
327
|
+
const inputSummary = formatCompactToolSummary({
|
|
328
|
+
tool: part.tool,
|
|
329
|
+
input: part.state.input,
|
|
330
|
+
maxChars: opts.toolInputMaxChars,
|
|
331
|
+
metadata: part.state.metadata,
|
|
332
|
+
});
|
|
232
333
|
const outputLabel = lineCount > 0 ? `(${lineCount} lines)` : '';
|
|
233
334
|
const parts = [inputSummary, outputLabel].filter(Boolean).join(' ');
|
|
234
|
-
lines.push(
|
|
335
|
+
lines.push(`tool: ${part.tool}${parts ? ` ${parts}` : ''}`);
|
|
235
336
|
lines.push('');
|
|
236
337
|
}
|
|
237
338
|
else if (part.state.status === 'error') {
|
|
238
339
|
const errorText = (part.state.error || 'Unknown error').split('\n')[0].slice(0, 120);
|
|
239
|
-
lines.push(
|
|
340
|
+
lines.push(`tool-error: ${part.tool} ${errorText}`);
|
|
240
341
|
lines.push('');
|
|
241
342
|
}
|
|
242
343
|
return lines;
|
|
243
344
|
}
|
|
244
|
-
/** Build a compact key=value summary of tool input, max 2 params, 80 chars each. */
|
|
245
|
-
compactInputSummary(input) {
|
|
246
|
-
if (!input)
|
|
247
|
-
return '';
|
|
248
|
-
const entries = Object.entries(input);
|
|
249
|
-
if (entries.length === 0)
|
|
250
|
-
return '';
|
|
251
|
-
const parts = [];
|
|
252
|
-
const maxParams = 2;
|
|
253
|
-
for (let i = 0; i < Math.min(entries.length, maxParams); i++) {
|
|
254
|
-
const [key, value] = entries[i];
|
|
255
|
-
let val;
|
|
256
|
-
try {
|
|
257
|
-
val = typeof value === 'string'
|
|
258
|
-
? value
|
|
259
|
-
: (JSON.stringify(value) ?? String(value));
|
|
260
|
-
}
|
|
261
|
-
catch {
|
|
262
|
-
val = String(value);
|
|
263
|
-
}
|
|
264
|
-
// Collapse whitespace for readability
|
|
265
|
-
const collapsed = val.replace(/\s+/g, ' ').trim();
|
|
266
|
-
const normalized = collapsed.length > 80 ? `${collapsed.slice(0, 80)}…` : collapsed;
|
|
267
|
-
parts.push(`${key}=${normalized}`);
|
|
268
|
-
}
|
|
269
|
-
if (entries.length > maxParams) {
|
|
270
|
-
parts.push('...');
|
|
271
|
-
}
|
|
272
|
-
return parts.join(', ');
|
|
273
|
-
}
|
|
274
|
-
formatDuration(ms) {
|
|
275
|
-
if (ms < 1000)
|
|
276
|
-
return `${ms}ms`;
|
|
277
|
-
if (ms < 60000)
|
|
278
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
279
|
-
const minutes = Math.floor(ms / 60000);
|
|
280
|
-
const seconds = Math.floor((ms % 60000) / 1000);
|
|
281
|
-
return `${minutes}m ${seconds}s`;
|
|
282
|
-
}
|
|
283
345
|
}
|
|
284
346
|
/**
|
|
285
347
|
* Generate compact session context for voice transcription.
|