@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// OpenCode message part formatting for Discord.
|
|
2
|
+
// Converts SDK message parts (text, tools, reasoning) to Discord-friendly format,
|
|
3
|
+
// handles file attachments, and provides tool summary generation.
|
|
4
|
+
import * as errore from 'errore';
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
+
import { FetchError } from './errors.js';
|
|
7
|
+
import { processImage } from './image-utils.js';
|
|
8
|
+
const logger = createLogger(LogPrefix.FORMATTING);
|
|
9
|
+
/**
|
|
10
|
+
* Resolves Discord mentions in message content to human-readable names.
|
|
11
|
+
* Replaces <@userId> with @displayName, <@&roleId> with @roleName, <#channelId> with #channelName.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveMentions(message) {
|
|
14
|
+
let content = message.content || '';
|
|
15
|
+
// Replace user mentions <@userId> or <@!userId> with @displayName
|
|
16
|
+
for (const [userId, user] of message.mentions.users) {
|
|
17
|
+
const member = message.guild?.members.cache.get(userId);
|
|
18
|
+
const displayName = member?.displayName || user.displayName || user.username;
|
|
19
|
+
content = content.replace(new RegExp(`<@!?${userId}>`, 'g'), `@${displayName}`);
|
|
20
|
+
}
|
|
21
|
+
// Replace role mentions <@&roleId> with @roleName
|
|
22
|
+
for (const [roleId, role] of message.mentions.roles) {
|
|
23
|
+
content = content.replace(new RegExp(`<@&${roleId}>`, 'g'), `@${role.name}`);
|
|
24
|
+
}
|
|
25
|
+
// Replace channel mentions <#channelId> with #channelName
|
|
26
|
+
for (const [channelId, channel] of message.mentions.channels) {
|
|
27
|
+
const name = 'name' in channel ? channel.name : channelId;
|
|
28
|
+
content = content.replace(new RegExp(`<#${channelId}>`, 'g'), `#${name}`);
|
|
29
|
+
}
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Escapes Discord inline markdown characters so dynamic content
|
|
34
|
+
* doesn't break formatting when wrapped in *, _, **, etc.
|
|
35
|
+
*/
|
|
36
|
+
function escapeInlineMarkdown(text) {
|
|
37
|
+
return text.replace(/([*_~|`\\])/g, '\\$1');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses a patchText string (apply_patch format) and counts additions/deletions per file.
|
|
41
|
+
* Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
|
|
42
|
+
* with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
|
|
43
|
+
*/
|
|
44
|
+
function parsePatchCounts(patchText) {
|
|
45
|
+
const counts = new Map();
|
|
46
|
+
const lines = patchText.split('\n');
|
|
47
|
+
let currentFile = '';
|
|
48
|
+
let currentType = '';
|
|
49
|
+
let inHunk = false;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/);
|
|
52
|
+
const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/);
|
|
53
|
+
const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/);
|
|
54
|
+
if (addMatch || updateMatch || deleteMatch) {
|
|
55
|
+
const match = addMatch || updateMatch || deleteMatch;
|
|
56
|
+
currentFile = (match?.[1] ?? '').trim();
|
|
57
|
+
currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete';
|
|
58
|
+
counts.set(currentFile, { additions: 0, deletions: 0 });
|
|
59
|
+
inHunk = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (line.startsWith('@@')) {
|
|
63
|
+
inHunk = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (line.startsWith('*** ')) {
|
|
67
|
+
inHunk = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!currentFile) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const entry = counts.get(currentFile);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (currentType === 'add') {
|
|
78
|
+
// all content lines in Add File are additions
|
|
79
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
80
|
+
entry.additions++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (currentType === 'delete') {
|
|
84
|
+
// all content lines in Delete File are deletions
|
|
85
|
+
if (line.length > 0 && !line.startsWith('*** ')) {
|
|
86
|
+
entry.deletions++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (inHunk) {
|
|
90
|
+
if (line.startsWith('+')) {
|
|
91
|
+
entry.additions++;
|
|
92
|
+
}
|
|
93
|
+
else if (line.startsWith('-')) {
|
|
94
|
+
entry.deletions++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return counts;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
|
|
102
|
+
*/
|
|
103
|
+
function normalizeWhitespace(text) {
|
|
104
|
+
return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Collects and formats the last N assistant parts from session messages.
|
|
108
|
+
* Used by both /resume and /fork to show recent assistant context.
|
|
109
|
+
*/
|
|
110
|
+
export function collectLastAssistantParts({ messages, limit = 30, }) {
|
|
111
|
+
const allAssistantParts = [];
|
|
112
|
+
for (const message of messages) {
|
|
113
|
+
if (message.info.role === 'assistant') {
|
|
114
|
+
for (const part of message.parts) {
|
|
115
|
+
const content = formatPart(part);
|
|
116
|
+
if (content.trim()) {
|
|
117
|
+
allAssistantParts.push({ id: part.id, content: content.trimEnd() });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const partsToRender = allAssistantParts.slice(-limit);
|
|
123
|
+
const partIds = partsToRender.map((p) => p.id);
|
|
124
|
+
const content = partsToRender.map((p) => p.content).join('\n');
|
|
125
|
+
const skippedCount = allAssistantParts.length - partsToRender.length;
|
|
126
|
+
return { partIds, content, skippedCount };
|
|
127
|
+
}
|
|
128
|
+
export const TEXT_MIME_TYPES = [
|
|
129
|
+
'text/',
|
|
130
|
+
'application/json',
|
|
131
|
+
'application/xml',
|
|
132
|
+
'application/javascript',
|
|
133
|
+
'application/typescript',
|
|
134
|
+
'application/x-yaml',
|
|
135
|
+
'application/toml',
|
|
136
|
+
];
|
|
137
|
+
export function isTextMimeType(contentType) {
|
|
138
|
+
if (!contentType) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return TEXT_MIME_TYPES.some((prefix) => contentType.startsWith(prefix));
|
|
142
|
+
}
|
|
143
|
+
export async function getTextAttachments(message) {
|
|
144
|
+
const textAttachments = Array.from(message.attachments.values()).filter((attachment) => isTextMimeType(attachment.contentType));
|
|
145
|
+
if (textAttachments.length === 0) {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
const textContents = await Promise.all(textAttachments.map(async (attachment) => {
|
|
149
|
+
const response = await errore.tryAsync({
|
|
150
|
+
try: () => fetch(attachment.url),
|
|
151
|
+
catch: (e) => new FetchError({ url: attachment.url, cause: e }),
|
|
152
|
+
});
|
|
153
|
+
if (response instanceof Error) {
|
|
154
|
+
return `<attachment filename="${attachment.name}" error="${response.message}" />`;
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
return `<attachment filename="${attachment.name}" error="Failed to fetch: ${response.status}" />`;
|
|
158
|
+
}
|
|
159
|
+
const text = await response.text();
|
|
160
|
+
return `<attachment filename="${attachment.name}" mime="${attachment.contentType}">\n${text}\n</attachment>`;
|
|
161
|
+
}));
|
|
162
|
+
return textContents.join('\n\n');
|
|
163
|
+
}
|
|
164
|
+
export async function getFileAttachments(message) {
|
|
165
|
+
const fileAttachments = Array.from(message.attachments.values()).filter((attachment) => {
|
|
166
|
+
const contentType = attachment.contentType || '';
|
|
167
|
+
return (contentType.startsWith('image/') || contentType === 'application/pdf');
|
|
168
|
+
});
|
|
169
|
+
if (fileAttachments.length === 0) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const results = await Promise.all(fileAttachments.map(async (attachment) => {
|
|
173
|
+
const response = await errore.tryAsync({
|
|
174
|
+
try: () => fetch(attachment.url),
|
|
175
|
+
catch: (e) => new FetchError({ url: attachment.url, cause: e }),
|
|
176
|
+
});
|
|
177
|
+
if (response instanceof Error) {
|
|
178
|
+
logger.error(`Error downloading attachment ${attachment.name}:`, response.message);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
logger.error(`Failed to fetch attachment ${attachment.name}: ${response.status}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const rawBuffer = Buffer.from(await response.arrayBuffer());
|
|
186
|
+
const originalMime = attachment.contentType || 'application/octet-stream';
|
|
187
|
+
// Process image (resize if needed, convert to JPEG)
|
|
188
|
+
const { buffer, mime } = await processImage(rawBuffer, originalMime);
|
|
189
|
+
const base64 = buffer.toString('base64');
|
|
190
|
+
const dataUrl = `data:${mime};base64,${base64}`;
|
|
191
|
+
logger.log(`Attachment ${attachment.name}: ${rawBuffer.length} → ${buffer.length} bytes, ${mime}`);
|
|
192
|
+
return {
|
|
193
|
+
type: 'file',
|
|
194
|
+
mime,
|
|
195
|
+
filename: attachment.name,
|
|
196
|
+
url: dataUrl,
|
|
197
|
+
sourceUrl: attachment.url,
|
|
198
|
+
};
|
|
199
|
+
}));
|
|
200
|
+
return results.filter((r) => r !== null);
|
|
201
|
+
}
|
|
202
|
+
const MAX_BASH_COMMAND_INLINE_LENGTH = 100;
|
|
203
|
+
export function getToolSummaryText(part) {
|
|
204
|
+
if (part.type !== 'tool')
|
|
205
|
+
return '';
|
|
206
|
+
if (part.tool === 'edit') {
|
|
207
|
+
const filePath = part.state.input?.filePath || '';
|
|
208
|
+
const newString = part.state.input?.newString || '';
|
|
209
|
+
const oldString = part.state.input?.oldString || '';
|
|
210
|
+
const added = newString.split('\n').length;
|
|
211
|
+
const removed = oldString.split('\n').length;
|
|
212
|
+
const fileName = filePath.split('/').pop() || '';
|
|
213
|
+
return fileName
|
|
214
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
|
|
215
|
+
: `(+${added}-${removed})`;
|
|
216
|
+
}
|
|
217
|
+
if (part.tool === 'apply_patch') {
|
|
218
|
+
// Only inputs are available when parts are sent during streaming (output/metadata not yet populated)
|
|
219
|
+
const patchText = part.state.input?.patchText || '';
|
|
220
|
+
if (!patchText) {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
const patchCounts = parsePatchCounts(patchText);
|
|
224
|
+
return [...patchCounts.entries()]
|
|
225
|
+
.map(([filePath, { additions, deletions }]) => {
|
|
226
|
+
const fileName = filePath.split('/').pop() || '';
|
|
227
|
+
return fileName
|
|
228
|
+
? `*${escapeInlineMarkdown(fileName)}* (+${additions}-${deletions})`
|
|
229
|
+
: `(+${additions}-${deletions})`;
|
|
230
|
+
})
|
|
231
|
+
.join(', ');
|
|
232
|
+
}
|
|
233
|
+
if (part.tool === 'write') {
|
|
234
|
+
const filePath = part.state.input?.filePath || '';
|
|
235
|
+
const content = part.state.input?.content || '';
|
|
236
|
+
const lines = content.split('\n').length;
|
|
237
|
+
const fileName = filePath.split('/').pop() || '';
|
|
238
|
+
return fileName
|
|
239
|
+
? `*${escapeInlineMarkdown(fileName)}* (${lines} line${lines === 1 ? '' : 's'})`
|
|
240
|
+
: `(${lines} line${lines === 1 ? '' : 's'})`;
|
|
241
|
+
}
|
|
242
|
+
if (part.tool === 'webfetch') {
|
|
243
|
+
const url = part.state.input?.url || '';
|
|
244
|
+
const urlWithoutProtocol = url.replace(/^https?:\/\//, '');
|
|
245
|
+
return urlWithoutProtocol
|
|
246
|
+
? `*${escapeInlineMarkdown(urlWithoutProtocol)}*`
|
|
247
|
+
: '';
|
|
248
|
+
}
|
|
249
|
+
if (part.tool === 'read') {
|
|
250
|
+
const filePath = part.state.input?.filePath || '';
|
|
251
|
+
const fileName = filePath.split('/').pop() || '';
|
|
252
|
+
return fileName ? `*${escapeInlineMarkdown(fileName)}*` : '';
|
|
253
|
+
}
|
|
254
|
+
if (part.tool === 'list') {
|
|
255
|
+
const path = part.state.input?.path || '';
|
|
256
|
+
const dirName = path.split('/').pop() || path;
|
|
257
|
+
return dirName ? `*${escapeInlineMarkdown(dirName)}*` : '';
|
|
258
|
+
}
|
|
259
|
+
if (part.tool === 'glob') {
|
|
260
|
+
const pattern = part.state.input?.pattern || '';
|
|
261
|
+
return pattern ? `*${escapeInlineMarkdown(pattern)}*` : '';
|
|
262
|
+
}
|
|
263
|
+
if (part.tool === 'grep') {
|
|
264
|
+
const pattern = part.state.input?.pattern || '';
|
|
265
|
+
return pattern ? `*${escapeInlineMarkdown(pattern)}*` : '';
|
|
266
|
+
}
|
|
267
|
+
if (part.tool === 'bash' ||
|
|
268
|
+
part.tool === 'todoread' ||
|
|
269
|
+
part.tool === 'todowrite') {
|
|
270
|
+
return '';
|
|
271
|
+
}
|
|
272
|
+
// Task tool display is handled via subtask part in session-handler (shows name + agent)
|
|
273
|
+
if (part.tool === 'task') {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
if (part.tool === 'skill') {
|
|
277
|
+
const name = part.state.input?.name || '';
|
|
278
|
+
return name ? `_${escapeInlineMarkdown(name)}_` : '';
|
|
279
|
+
}
|
|
280
|
+
// File upload tool - show the prompt
|
|
281
|
+
if (part.tool.endsWith('kimaki_file_upload')) {
|
|
282
|
+
const prompt = part.state.input?.prompt || '';
|
|
283
|
+
return prompt ? `*${escapeInlineMarkdown(prompt.slice(0, 60))}*` : '';
|
|
284
|
+
}
|
|
285
|
+
if (!part.state.input)
|
|
286
|
+
return '';
|
|
287
|
+
const inputFields = Object.entries(part.state.input)
|
|
288
|
+
.map(([key, value]) => {
|
|
289
|
+
if (value === null || value === undefined)
|
|
290
|
+
return null;
|
|
291
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
292
|
+
const normalized = normalizeWhitespace(stringValue);
|
|
293
|
+
const truncatedValue = normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized;
|
|
294
|
+
return `${key}: ${truncatedValue}`;
|
|
295
|
+
})
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
if (inputFields.length === 0)
|
|
298
|
+
return '';
|
|
299
|
+
return `(${inputFields.join(', ')})`;
|
|
300
|
+
}
|
|
301
|
+
export function formatTodoList(part) {
|
|
302
|
+
if (part.type !== 'tool' || part.tool !== 'todowrite')
|
|
303
|
+
return '';
|
|
304
|
+
const todos = part.state.input?.todos || [];
|
|
305
|
+
const activeIndex = todos.findIndex((todo) => {
|
|
306
|
+
return todo.status === 'in_progress';
|
|
307
|
+
});
|
|
308
|
+
const activeTodo = todos[activeIndex];
|
|
309
|
+
if (activeIndex === -1 || !activeTodo)
|
|
310
|
+
return '';
|
|
311
|
+
// digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
|
|
312
|
+
const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛';
|
|
313
|
+
const todoNumber = activeIndex + 1;
|
|
314
|
+
const num = todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`;
|
|
315
|
+
const content = activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1);
|
|
316
|
+
return `${num} **${escapeInlineMarkdown(content)}**`;
|
|
317
|
+
}
|
|
318
|
+
export function formatPart(part, prefix) {
|
|
319
|
+
const pfx = prefix ? `${prefix} ⋅ ` : '';
|
|
320
|
+
if (part.type === 'text') {
|
|
321
|
+
const text = part.text?.trim();
|
|
322
|
+
if (!text)
|
|
323
|
+
return '';
|
|
324
|
+
// For subtask text, always use bullet with prefix
|
|
325
|
+
if (prefix) {
|
|
326
|
+
return `⬥ ${pfx}${text}`;
|
|
327
|
+
}
|
|
328
|
+
const firstChar = text[0] || '';
|
|
329
|
+
const markdownStarters = ['#', '*', '_', '-', '>', '`', '[', '|'];
|
|
330
|
+
const startsWithMarkdown = markdownStarters.includes(firstChar) || /^\d+\./.test(text);
|
|
331
|
+
if (startsWithMarkdown) {
|
|
332
|
+
return `\n${text}`;
|
|
333
|
+
}
|
|
334
|
+
return `⬥ ${text}`;
|
|
335
|
+
}
|
|
336
|
+
if (part.type === 'reasoning') {
|
|
337
|
+
if (!part.text?.trim())
|
|
338
|
+
return '';
|
|
339
|
+
return `┣ ${pfx}thinking`;
|
|
340
|
+
}
|
|
341
|
+
if (part.type === 'file') {
|
|
342
|
+
return prefix
|
|
343
|
+
? `📄 ${pfx}${part.filename || 'File'}`
|
|
344
|
+
: `📄 ${part.filename || 'File'}`;
|
|
345
|
+
}
|
|
346
|
+
if (part.type === 'step-start' ||
|
|
347
|
+
part.type === 'step-finish' ||
|
|
348
|
+
part.type === 'patch') {
|
|
349
|
+
return '';
|
|
350
|
+
}
|
|
351
|
+
if (part.type === 'agent') {
|
|
352
|
+
return `┣ ${pfx}agent ${part.id}`;
|
|
353
|
+
}
|
|
354
|
+
if (part.type === 'snapshot') {
|
|
355
|
+
return `┣ ${pfx}snapshot ${part.snapshot}`;
|
|
356
|
+
}
|
|
357
|
+
if (part.type === 'tool') {
|
|
358
|
+
if (part.tool === 'todowrite') {
|
|
359
|
+
const formatted = formatTodoList(part);
|
|
360
|
+
return prefix && formatted ? `┣ ${pfx}${formatted}` : formatted;
|
|
361
|
+
}
|
|
362
|
+
// Question tool is handled via Discord dropdowns, not text
|
|
363
|
+
if (part.tool === 'question') {
|
|
364
|
+
return '';
|
|
365
|
+
}
|
|
366
|
+
// File upload tool is handled via Discord button + modal, not text
|
|
367
|
+
if (part.tool.endsWith('kimaki_file_upload')) {
|
|
368
|
+
return '';
|
|
369
|
+
}
|
|
370
|
+
// Action buttons tool is handled via Discord buttons, not text
|
|
371
|
+
if (part.tool.endsWith('kimaki_action_buttons')) {
|
|
372
|
+
return '';
|
|
373
|
+
}
|
|
374
|
+
// Task tool display is handled in session-handler with proper label
|
|
375
|
+
if (part.tool === 'task') {
|
|
376
|
+
return '';
|
|
377
|
+
}
|
|
378
|
+
if (part.state.status === 'pending') {
|
|
379
|
+
if (part.tool !== 'bash') {
|
|
380
|
+
return '';
|
|
381
|
+
}
|
|
382
|
+
const command = part.state.input?.command || '';
|
|
383
|
+
const description = part.state.input?.description || '';
|
|
384
|
+
const isSingleLine = !command.includes('\n');
|
|
385
|
+
const toolTitle = isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH
|
|
386
|
+
? ` _${escapeInlineMarkdown(command)}_`
|
|
387
|
+
: description
|
|
388
|
+
? ` _${escapeInlineMarkdown(description)}_`
|
|
389
|
+
: '';
|
|
390
|
+
return `┣ ${pfx}bash${toolTitle}`;
|
|
391
|
+
}
|
|
392
|
+
const summaryText = getToolSummaryText(part);
|
|
393
|
+
const stateTitle = 'title' in part.state ? part.state.title : undefined;
|
|
394
|
+
let toolTitle = '';
|
|
395
|
+
if (part.state.status === 'error') {
|
|
396
|
+
toolTitle = part.state.error || 'error';
|
|
397
|
+
}
|
|
398
|
+
else if (part.tool === 'bash') {
|
|
399
|
+
const command = part.state.input?.command || '';
|
|
400
|
+
const description = part.state.input?.description || '';
|
|
401
|
+
const isSingleLine = !command.includes('\n');
|
|
402
|
+
if (isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH) {
|
|
403
|
+
toolTitle = `_${escapeInlineMarkdown(command)}_`;
|
|
404
|
+
}
|
|
405
|
+
else if (description) {
|
|
406
|
+
toolTitle = `_${escapeInlineMarkdown(description)}_`;
|
|
407
|
+
}
|
|
408
|
+
else if (stateTitle) {
|
|
409
|
+
toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else if (stateTitle) {
|
|
413
|
+
toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`;
|
|
414
|
+
}
|
|
415
|
+
const icon = (() => {
|
|
416
|
+
if (part.state.status === 'error') {
|
|
417
|
+
return '⨯';
|
|
418
|
+
}
|
|
419
|
+
if (part.tool === 'edit' ||
|
|
420
|
+
part.tool === 'write' ||
|
|
421
|
+
part.tool === 'apply_patch') {
|
|
422
|
+
return '◼︎';
|
|
423
|
+
}
|
|
424
|
+
return '┣';
|
|
425
|
+
})();
|
|
426
|
+
const toolParts = [part.tool, toolTitle, summaryText]
|
|
427
|
+
.filter(Boolean)
|
|
428
|
+
.join(' ');
|
|
429
|
+
return `${icon} ${pfx}${toolParts}`;
|
|
430
|
+
}
|
|
431
|
+
logger.warn('Unknown part type:', part);
|
|
432
|
+
return '';
|
|
433
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { formatTodoList } from './message-formatting.js';
|
|
3
|
+
describe('formatTodoList', () => {
|
|
4
|
+
test('formats active todo with monospace numbers', () => {
|
|
5
|
+
const part = {
|
|
6
|
+
id: 'test',
|
|
7
|
+
type: 'tool',
|
|
8
|
+
tool: 'todowrite',
|
|
9
|
+
sessionID: 'ses_test',
|
|
10
|
+
messageID: 'msg_test',
|
|
11
|
+
callID: 'call_test',
|
|
12
|
+
state: {
|
|
13
|
+
status: 'completed',
|
|
14
|
+
input: {
|
|
15
|
+
todos: [
|
|
16
|
+
{ content: 'First task', status: 'completed' },
|
|
17
|
+
{ content: 'Second task', status: 'in_progress' },
|
|
18
|
+
{ content: 'Third task', status: 'pending' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
output: '',
|
|
22
|
+
title: 'todowrite',
|
|
23
|
+
metadata: {},
|
|
24
|
+
time: { start: 0, end: 0 },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒉ **second task**"`);
|
|
28
|
+
});
|
|
29
|
+
test('formats double digit todo numbers', () => {
|
|
30
|
+
const todos = Array.from({ length: 12 }, (_, i) => ({
|
|
31
|
+
content: `Task ${i + 1}`,
|
|
32
|
+
status: i === 11 ? 'in_progress' : 'completed',
|
|
33
|
+
}));
|
|
34
|
+
const part = {
|
|
35
|
+
id: 'test',
|
|
36
|
+
type: 'tool',
|
|
37
|
+
tool: 'todowrite',
|
|
38
|
+
sessionID: 'ses_test',
|
|
39
|
+
messageID: 'msg_test',
|
|
40
|
+
callID: 'call_test',
|
|
41
|
+
state: {
|
|
42
|
+
status: 'completed',
|
|
43
|
+
input: { todos },
|
|
44
|
+
output: '',
|
|
45
|
+
title: 'todowrite',
|
|
46
|
+
metadata: {},
|
|
47
|
+
time: { start: 0, end: 0 },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒓ **task 12**"`);
|
|
51
|
+
});
|
|
52
|
+
test('lowercases first letter of content', () => {
|
|
53
|
+
const part = {
|
|
54
|
+
id: 'test',
|
|
55
|
+
type: 'tool',
|
|
56
|
+
tool: 'todowrite',
|
|
57
|
+
sessionID: 'ses_test',
|
|
58
|
+
messageID: 'msg_test',
|
|
59
|
+
callID: 'call_test',
|
|
60
|
+
state: {
|
|
61
|
+
status: 'completed',
|
|
62
|
+
input: {
|
|
63
|
+
todos: [{ content: 'Fix the bug', status: 'in_progress' }],
|
|
64
|
+
},
|
|
65
|
+
output: '',
|
|
66
|
+
title: 'todowrite',
|
|
67
|
+
metadata: {},
|
|
68
|
+
time: { start: 0, end: 0 },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
expect(formatTodoList(part)).toMatchInlineSnapshot(`"⒈ **fix the bug**"`);
|
|
72
|
+
});
|
|
73
|
+
});
|