kimaki 0.26.0 → 0.28.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 +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
3
3
|
import { getChannelVerbosity, getPartMessageIds, getThreadIdBySessionId, getThreadSessionSource, listTrackedTextChannels, setPartMessagesBatch, upsertThreadSession, } from './database.js';
|
|
4
|
-
import {
|
|
4
|
+
import { sendSessionPartBatches } from './discord-utils.js';
|
|
5
5
|
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
-
import { formatPart, collectSessionChunks, batchChunksForDiscord, } from './message-formatting.js';
|
|
6
|
+
import { formatPart, collectSessionChunks, batchChunksForDiscord, QUEUE_PREFIX, } from './message-formatting.js';
|
|
7
7
|
import { initializeOpencodeForDirectory, } from './opencode.js';
|
|
8
|
-
import { isEssentialToolPart } from './session-handler/thread-session-runtime.js';
|
|
8
|
+
import { getRuntime, isEssentialToolPart } from './session-handler/thread-session-runtime.js';
|
|
9
9
|
import { notifyError } from './sentry.js';
|
|
10
10
|
import { store } from './store.js';
|
|
11
11
|
import { extractNonXmlContent } from './xml.js';
|
|
@@ -20,8 +20,7 @@ const SYNC_PER_DIRECTORY_TIMEOUT_MS = 30_000;
|
|
|
20
20
|
const CLI_START_MS = Date.now() - 5 * 60 * 1000;
|
|
21
21
|
let externalSyncInterval = null;
|
|
22
22
|
function isSyntheticTextPart(part) {
|
|
23
|
-
|
|
24
|
-
return candidate.synthetic === true;
|
|
23
|
+
return part.synthetic === true;
|
|
25
24
|
}
|
|
26
25
|
function parseDiscordOriginMetadata(text) {
|
|
27
26
|
const match = text.match(/<discord-user\s+([^>]+)\s*\/>/);
|
|
@@ -64,7 +63,7 @@ function getDiscordOriginMetadataFromMessage({ message, }) {
|
|
|
64
63
|
}
|
|
65
64
|
return null;
|
|
66
65
|
}
|
|
67
|
-
function getRenderableUserTextParts({ message, }) {
|
|
66
|
+
export function getRenderableUserTextParts({ message, }) {
|
|
68
67
|
if (message.info.role !== 'user') {
|
|
69
68
|
return [];
|
|
70
69
|
}
|
|
@@ -72,7 +71,7 @@ function getRenderableUserTextParts({ message, }) {
|
|
|
72
71
|
if (part.type !== 'text') {
|
|
73
72
|
return [];
|
|
74
73
|
}
|
|
75
|
-
if (isSyntheticTextPart(part)) {
|
|
74
|
+
if (isSyntheticTextPart(part) || part.ignored === true) {
|
|
76
75
|
return [];
|
|
77
76
|
}
|
|
78
77
|
const cleanedText = extractNonXmlContent(part.text || '').trim();
|
|
@@ -82,8 +81,49 @@ function getRenderableUserTextParts({ message, }) {
|
|
|
82
81
|
return [{ id: part.id, text: cleanedText }];
|
|
83
82
|
});
|
|
84
83
|
}
|
|
84
|
+
function hasCompactionContinueMetadata(part) {
|
|
85
|
+
return part.metadata?.compaction_continue === true;
|
|
86
|
+
}
|
|
87
|
+
export function isInternalOpenCodeUserMessage({ message, }) {
|
|
88
|
+
if (message.info.role !== 'user') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return message.parts.some((part) => {
|
|
92
|
+
if (part.type === 'compaction') {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (part.type !== 'text') {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return isSyntheticTextPart(part) && hasCompactionContinueMetadata(part);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function shouldSkipExternalAssistantMessage({ message, }) {
|
|
102
|
+
if (message.info.role !== 'assistant') {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return message.info.summary === true;
|
|
106
|
+
}
|
|
107
|
+
export function getIgnoredNoticeTextParts({ message, }) {
|
|
108
|
+
if (message.info.role !== 'user') {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return message.parts.flatMap((part) => {
|
|
112
|
+
if (part.type !== 'text') {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (part.ignored !== true || isSyntheticTextPart(part)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const text = part.text?.trim();
|
|
119
|
+
if (!text) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return [{ id: part.id, text }];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
85
125
|
function getExternalUserMirrorText({ username, prompt, }) {
|
|
86
|
-
return
|
|
126
|
+
return `${QUEUE_PREFIX}**${username}:** ${prompt.slice(0, 1000)}${prompt.length > 1000 ? '...' : ''}`;
|
|
87
127
|
}
|
|
88
128
|
// Pure derivation: is the latest user turn from Discord?
|
|
89
129
|
// Checks the newest user message with renderable text for a <discord-user />
|
|
@@ -91,12 +131,15 @@ function getExternalUserMirrorText({ username, prompt, }) {
|
|
|
91
131
|
// (kimaki manages it) and external sync should skip it. If absent (CLI/TUI),
|
|
92
132
|
// external sync should mirror it — this naturally handles the "reclaim" case
|
|
93
133
|
// (external → discord → external) without any DB source toggling.
|
|
94
|
-
function isLatestUserTurnFromDiscord({ messages, }) {
|
|
134
|
+
export function isLatestUserTurnFromDiscord({ messages, }) {
|
|
95
135
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
96
136
|
const message = messages[i];
|
|
97
137
|
if (message.info.role !== 'user') {
|
|
98
138
|
continue;
|
|
99
139
|
}
|
|
140
|
+
if (isInternalOpenCodeUserMessage({ message })) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
100
143
|
const renderableParts = getRenderableUserTextParts({ message });
|
|
101
144
|
if (renderableParts.length === 0) {
|
|
102
145
|
continue;
|
|
@@ -234,6 +277,23 @@ function collectUnsyncedChunks({ messages, syncedPartIds, verbosity, thread, })
|
|
|
234
277
|
const directMappings = [];
|
|
235
278
|
for (const message of messages) {
|
|
236
279
|
if (message.info.role === 'user') {
|
|
280
|
+
if (isInternalOpenCodeUserMessage({ message })) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const ignoredNoticeParts = getIgnoredNoticeTextParts({ message }).filter((part) => {
|
|
284
|
+
return !syncedPartIds.has(part.id);
|
|
285
|
+
});
|
|
286
|
+
if (ignoredNoticeParts.length > 0) {
|
|
287
|
+
chunks.push({
|
|
288
|
+
partIds: ignoredNoticeParts.map((part) => {
|
|
289
|
+
return part.id;
|
|
290
|
+
}),
|
|
291
|
+
content: ignoredNoticeParts.map((part) => {
|
|
292
|
+
return part.text;
|
|
293
|
+
}).join('\n\n'),
|
|
294
|
+
kind: 'text',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
237
297
|
const renderableParts = getRenderableUserTextParts({ message });
|
|
238
298
|
const unsyncedParts = renderableParts.filter((p) => {
|
|
239
299
|
return !syncedPartIds.has(p.id);
|
|
@@ -265,12 +325,16 @@ function collectUnsyncedChunks({ messages, syncedPartIds, verbosity, thread, })
|
|
|
265
325
|
return p.id;
|
|
266
326
|
}),
|
|
267
327
|
content: getExternalUserMirrorText({ username: 'user', prompt: promptText }),
|
|
328
|
+
kind: 'text',
|
|
268
329
|
});
|
|
269
330
|
continue;
|
|
270
331
|
}
|
|
271
332
|
if (message.info.role !== 'assistant') {
|
|
272
333
|
continue;
|
|
273
334
|
}
|
|
335
|
+
if (shouldSkipExternalAssistantMessage({ message })) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
274
338
|
// Filter assistant parts by verbosity before passing to shared collector
|
|
275
339
|
const filteredParts = message.parts.filter((part) => {
|
|
276
340
|
return shouldMirrorAssistantPart({ part, verbosity });
|
|
@@ -314,6 +378,10 @@ async function syncSessionToThread({ client, discordClient, directory, channelId
|
|
|
314
378
|
if (isLatestUserTurnFromDiscord({ messages })) {
|
|
315
379
|
return;
|
|
316
380
|
}
|
|
381
|
+
const existingThreadId = await getThreadIdBySessionId(sessionId);
|
|
382
|
+
if (existingThreadId && getRuntime(existingThreadId)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
317
385
|
const thread = await ensureExternalSessionThread({
|
|
318
386
|
discordClient,
|
|
319
387
|
channelId,
|
|
@@ -340,13 +408,15 @@ async function syncSessionToThread({ client, discordClient, directory, channelId
|
|
|
340
408
|
await setPartMessagesBatch(directMappings);
|
|
341
409
|
}
|
|
342
410
|
const batched = batchChunksForDiscord(chunks);
|
|
343
|
-
|
|
411
|
+
if (signal.aborted)
|
|
412
|
+
return;
|
|
413
|
+
const sent = await sendSessionPartBatches({ thread, batches: batched });
|
|
414
|
+
for (const batch of sent) {
|
|
344
415
|
if (signal.aborted)
|
|
345
416
|
return;
|
|
346
|
-
const sentMessage = await sendThreadMessage(thread, batch.content);
|
|
347
417
|
await setPartMessagesBatch(batch.partIds.map((partId) => ({
|
|
348
418
|
partId,
|
|
349
|
-
messageId:
|
|
419
|
+
messageId: batch.message.id,
|
|
350
420
|
threadId: thread.id,
|
|
351
421
|
})));
|
|
352
422
|
}
|
|
@@ -377,6 +447,15 @@ async function pulseTypingForBusySessions({ discordClient, statuses, }) {
|
|
|
377
447
|
}
|
|
378
448
|
}
|
|
379
449
|
const EXTERNAL_SYNC_MAX_SESSIONS = 50;
|
|
450
|
+
export function isExternalSyncRootSession({ title, parentID, }) {
|
|
451
|
+
if (parentID) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (/^new session\s*-/i.test(title || '')) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
return !/subagent\)\s*$/i.test(title || '');
|
|
458
|
+
}
|
|
380
459
|
// Tracks directories with an in-flight sync. When a directory times out,
|
|
381
460
|
// its AbortController is aborted so the inner work stops producing side
|
|
382
461
|
// effects (Discord messages, DB writes). The next poll tick skips the
|
|
@@ -449,11 +528,7 @@ async function syncDirectoryInner({ directory, channelId, startMs, discordClient
|
|
|
449
528
|
if (signal.aborted)
|
|
450
529
|
return;
|
|
451
530
|
const sessions = (sessionsResponse.data || []).filter((session) => {
|
|
452
|
-
|
|
453
|
-
if (/^new session\s*-/i.test(title)) {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
return !/subagent\)\s*$/i.test(title);
|
|
531
|
+
return isExternalSyncRootSession(session);
|
|
457
532
|
});
|
|
458
533
|
const sorted = sortSessionsByRecency(sessions);
|
|
459
534
|
for (const session of sorted) {
|
|
@@ -540,4 +615,6 @@ export const externalOpencodeSyncInternals = {
|
|
|
540
615
|
parseDiscordOriginMetadata,
|
|
541
616
|
getDiscordOriginMetadataFromMessage,
|
|
542
617
|
isLatestUserTurnFromDiscord,
|
|
618
|
+
isInternalOpenCodeUserMessage,
|
|
619
|
+
shouldSkipExternalAssistantMessage,
|
|
543
620
|
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** Tests external OpenCode user-message filtering for Discord mirroring and ownership. */
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { getIgnoredNoticeTextParts, getRenderableUserTextParts, isExternalSyncRootSession, isInternalOpenCodeUserMessage, isLatestUserTurnFromDiscord, shouldSkipExternalAssistantMessage, } from './external-opencode-sync.js';
|
|
4
|
+
function textMessage({ text, ignored, synthetic, }) {
|
|
5
|
+
return {
|
|
6
|
+
info: { role: 'user' },
|
|
7
|
+
parts: [
|
|
8
|
+
{
|
|
9
|
+
id: 'part-1',
|
|
10
|
+
sessionID: 'session-1',
|
|
11
|
+
messageID: 'message-1',
|
|
12
|
+
type: 'text',
|
|
13
|
+
text,
|
|
14
|
+
ignored,
|
|
15
|
+
synthetic,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe('external OpenCode user-message filtering', () => {
|
|
21
|
+
test('does not sync task child sessions into Discord', () => {
|
|
22
|
+
expect(isExternalSyncRootSession({
|
|
23
|
+
title: 'Design shader visual stack',
|
|
24
|
+
parentID: 'ses_parent',
|
|
25
|
+
})).toBe(false);
|
|
26
|
+
expect(isExternalSyncRootSession({
|
|
27
|
+
title: 'Main session',
|
|
28
|
+
})).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
test('keeps normal external user text renderable', () => {
|
|
31
|
+
const message = textMessage({ text: 'Run the tests' });
|
|
32
|
+
expect(getRenderableUserTextParts({ message })).toEqual([
|
|
33
|
+
{ id: 'part-1', text: 'Run the tests' },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
test('skips ignored plugin notices from user mirroring', () => {
|
|
37
|
+
const message = textMessage({
|
|
38
|
+
text: 'Subrouter: xai/grok-4.6 was rate limited.',
|
|
39
|
+
ignored: true,
|
|
40
|
+
});
|
|
41
|
+
expect(getRenderableUserTextParts({ message })).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
test('collects ignored plugin notices as bot text', () => {
|
|
44
|
+
const message = textMessage({
|
|
45
|
+
text: 'Subrouter: Using openai/gpt-5.6-sol because xai/grok-4.6 is rate limited.',
|
|
46
|
+
ignored: true,
|
|
47
|
+
});
|
|
48
|
+
expect(getIgnoredNoticeTextParts({ message })).toEqual([
|
|
49
|
+
{
|
|
50
|
+
id: 'part-1',
|
|
51
|
+
text: 'Subrouter: Using openai/gpt-5.6-sol because xai/grok-4.6 is rate limited.',
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
test('does not collect synthetic or normal user text as ignored notices', () => {
|
|
56
|
+
expect(getIgnoredNoticeTextParts({
|
|
57
|
+
message: textMessage({ text: 'Run the tests' }),
|
|
58
|
+
})).toEqual([]);
|
|
59
|
+
expect(getIgnoredNoticeTextParts({
|
|
60
|
+
message: textMessage({
|
|
61
|
+
text: '<discord-user name="Tommy" />',
|
|
62
|
+
synthetic: true,
|
|
63
|
+
}),
|
|
64
|
+
})).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
test('skips synthetic context parts', () => {
|
|
67
|
+
const message = textMessage({
|
|
68
|
+
text: '<discord-user name="Tommy" />',
|
|
69
|
+
synthetic: true,
|
|
70
|
+
});
|
|
71
|
+
expect(getRenderableUserTextParts({ message })).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
test('does not treat an ignored notice as an external takeover', () => {
|
|
74
|
+
const discordMessage = {
|
|
75
|
+
info: { role: 'user' },
|
|
76
|
+
parts: [
|
|
77
|
+
{
|
|
78
|
+
id: 'part-2',
|
|
79
|
+
sessionID: 'session-1',
|
|
80
|
+
messageID: 'message-2',
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: '<discord-user name="Tommy" />',
|
|
83
|
+
synthetic: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'part-3',
|
|
87
|
+
sessionID: 'session-1',
|
|
88
|
+
messageID: 'message-2',
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: 'What failed?',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
const notice = textMessage({
|
|
95
|
+
text: 'Subrouter: xai/grok-4.6 was rate limited.',
|
|
96
|
+
ignored: true,
|
|
97
|
+
});
|
|
98
|
+
expect(isLatestUserTurnFromDiscord({ messages: [discordMessage, notice] })).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
test('skips compaction user messages when deciding Discord ownership', () => {
|
|
101
|
+
const discordMessage = {
|
|
102
|
+
info: { role: 'user' },
|
|
103
|
+
parts: [
|
|
104
|
+
{
|
|
105
|
+
id: 'part-2',
|
|
106
|
+
sessionID: 'session-1',
|
|
107
|
+
messageID: 'message-2',
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: '<discord-user name="Tommy" />',
|
|
110
|
+
synthetic: true,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'part-3',
|
|
114
|
+
sessionID: 'session-1',
|
|
115
|
+
messageID: 'message-2',
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: 'Fix the tabs',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
const compactionUser = {
|
|
122
|
+
info: { role: 'user' },
|
|
123
|
+
parts: [
|
|
124
|
+
{
|
|
125
|
+
id: 'part-4',
|
|
126
|
+
sessionID: 'session-1',
|
|
127
|
+
messageID: 'message-4',
|
|
128
|
+
type: 'compaction',
|
|
129
|
+
auto: true,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
expect(isInternalOpenCodeUserMessage({ message: compactionUser })).toBe(true);
|
|
134
|
+
expect(isLatestUserTurnFromDiscord({ messages: [discordMessage, compactionUser] })).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
test('skips compaction continue users when deciding Discord ownership', () => {
|
|
137
|
+
const discordMessage = {
|
|
138
|
+
info: { role: 'user' },
|
|
139
|
+
parts: [
|
|
140
|
+
{
|
|
141
|
+
id: 'part-2',
|
|
142
|
+
sessionID: 'session-1',
|
|
143
|
+
messageID: 'message-2',
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: '<discord-user name="Tommy" />',
|
|
146
|
+
synthetic: true,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'part-3',
|
|
150
|
+
sessionID: 'session-1',
|
|
151
|
+
messageID: 'message-2',
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: 'Fix the tabs',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
const continueUser = {
|
|
158
|
+
info: { role: 'user' },
|
|
159
|
+
parts: [
|
|
160
|
+
{
|
|
161
|
+
id: 'part-5',
|
|
162
|
+
sessionID: 'session-1',
|
|
163
|
+
messageID: 'message-5',
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: 'Continue if you have next steps',
|
|
166
|
+
synthetic: true,
|
|
167
|
+
metadata: { compaction_continue: true },
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
expect(isInternalOpenCodeUserMessage({ message: continueUser })).toBe(true);
|
|
172
|
+
expect(isLatestUserTurnFromDiscord({ messages: [discordMessage, continueUser] })).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
test('skips compaction summary assistants from external mirroring', () => {
|
|
175
|
+
expect(shouldSkipExternalAssistantMessage({
|
|
176
|
+
message: {
|
|
177
|
+
info: { role: 'assistant', summary: true },
|
|
178
|
+
parts: [
|
|
179
|
+
{
|
|
180
|
+
id: 'part-6',
|
|
181
|
+
sessionID: 'session-1',
|
|
182
|
+
messageID: 'message-6',
|
|
183
|
+
type: 'text',
|
|
184
|
+
text: 'internal compaction summary must not reach Discord',
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
})).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
test('does not skip a user-facing assistant just because the agent is named compaction', () => {
|
|
191
|
+
expect(shouldSkipExternalAssistantMessage({
|
|
192
|
+
message: {
|
|
193
|
+
info: { role: 'assistant', agent: 'compaction' },
|
|
194
|
+
parts: [
|
|
195
|
+
{
|
|
196
|
+
id: 'part-7',
|
|
197
|
+
sessionID: 'session-1',
|
|
198
|
+
messageID: 'message-7',
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: 'user-facing reply',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
})).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { extractSdkErrorMessage } from './opencode.js';
|
|
3
|
+
describe('extractSdkErrorMessage', () => {
|
|
4
|
+
test('includes UnknownError name, message, and ref from session.create', () => {
|
|
5
|
+
expect(extractSdkErrorMessage({
|
|
6
|
+
name: 'UnknownError',
|
|
7
|
+
data: {
|
|
8
|
+
message: 'Unexpected server error. Check server logs for details.',
|
|
9
|
+
ref: 'err_58d6c6cf',
|
|
10
|
+
},
|
|
11
|
+
})).toMatchInlineSnapshot(`"UnknownError: Unexpected server error. Check server logs for details. (err_58d6c6cf)"`);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Tracks which OpenCode sessions last edited each file.
|
|
2
|
+
// Plugin appends JSONL events; CLI derives the per-file session list on read.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import * as errore from 'errore';
|
|
6
|
+
import { FilesystemOperationError } from './errors.js';
|
|
7
|
+
import { extractPatchFilePaths } from './patch-text-parser.js';
|
|
8
|
+
import { createPluginLogger, setPluginLogFilePath } from './plugin-logger.js';
|
|
9
|
+
const logger = createPluginLogger('FILEEDIT');
|
|
10
|
+
export const FILE_EDIT_EVENTS_FILENAME = 'file-edit-events.jsonl';
|
|
11
|
+
const DEFAULT_MAX_EVENTS = 10_000;
|
|
12
|
+
const DEFAULT_COMPACT_AFTER_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
function isFileEditTool(tool) {
|
|
14
|
+
return tool === 'edit' || tool === 'write' || tool === 'apply_patch';
|
|
15
|
+
}
|
|
16
|
+
function getStringField({ args, key }) {
|
|
17
|
+
const value = args[key];
|
|
18
|
+
return typeof value === 'string' ? value : '';
|
|
19
|
+
}
|
|
20
|
+
export function extractEditedFiles({ tool, args, directory, }) {
|
|
21
|
+
const name = tool.toLowerCase();
|
|
22
|
+
if (name === 'edit' || name === 'write') {
|
|
23
|
+
const filePath = getStringField({ args, key: 'filePath' });
|
|
24
|
+
if (!filePath)
|
|
25
|
+
return [];
|
|
26
|
+
return [path.resolve(directory, filePath)];
|
|
27
|
+
}
|
|
28
|
+
if (name !== 'apply_patch')
|
|
29
|
+
return [];
|
|
30
|
+
const patchText = getStringField({ args, key: 'patchText' });
|
|
31
|
+
if (!patchText)
|
|
32
|
+
return [];
|
|
33
|
+
return extractPatchFilePaths(patchText).map((filePath) => {
|
|
34
|
+
return path.resolve(directory, filePath);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function editorsForFile({ events, filePath, cwd, }) {
|
|
38
|
+
const resolved = path.resolve(cwd, filePath);
|
|
39
|
+
const latestBySession = new Map();
|
|
40
|
+
for (const event of events) {
|
|
41
|
+
if (path.resolve(event.file) !== resolved)
|
|
42
|
+
continue;
|
|
43
|
+
const previous = latestBySession.get(event.sessionId);
|
|
44
|
+
if (previous === undefined || event.at > previous) {
|
|
45
|
+
latestBySession.set(event.sessionId, event.at);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...latestBySession.entries()]
|
|
49
|
+
.map(([sessionId, at]) => {
|
|
50
|
+
return { sessionId, at };
|
|
51
|
+
})
|
|
52
|
+
.sort((left, right) => {
|
|
53
|
+
return right.at - left.at;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function parseFileEditEvent(line) {
|
|
57
|
+
const parsed = errore.try(() => JSON.parse(line), (cause) => new FilesystemOperationError({ operation: 'parse file edit event', cause }));
|
|
58
|
+
if (parsed instanceof Error)
|
|
59
|
+
return null;
|
|
60
|
+
if (!parsed || typeof parsed !== 'object')
|
|
61
|
+
return null;
|
|
62
|
+
if (parsed.v !== 1)
|
|
63
|
+
return null;
|
|
64
|
+
if (typeof parsed.at !== 'number' || !Number.isFinite(parsed.at))
|
|
65
|
+
return null;
|
|
66
|
+
if (typeof parsed.sessionId !== 'string' || parsed.sessionId.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
if (typeof parsed.file !== 'string' || parsed.file.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
if (typeof parsed.tool !== 'string' || !isFileEditTool(parsed.tool))
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
v: 1,
|
|
74
|
+
at: parsed.at,
|
|
75
|
+
sessionId: parsed.sessionId,
|
|
76
|
+
file: parsed.file,
|
|
77
|
+
tool: parsed.tool,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function nodeErrorCode(error) {
|
|
81
|
+
const cause = error.cause;
|
|
82
|
+
if (!cause || typeof cause !== 'object')
|
|
83
|
+
return '';
|
|
84
|
+
const code = Reflect.get(cause, 'code');
|
|
85
|
+
return typeof code === 'string' ? code : '';
|
|
86
|
+
}
|
|
87
|
+
export function loadFileEditEvents({ dataDir }) {
|
|
88
|
+
const logPath = path.join(dataDir, FILE_EDIT_EVENTS_FILENAME);
|
|
89
|
+
const raw = errore.try(() => fs.readFileSync(logPath, 'utf8'), (cause) => new FilesystemOperationError({ operation: 'read file edit log', cause }));
|
|
90
|
+
if (raw instanceof Error) {
|
|
91
|
+
if (nodeErrorCode(raw) === 'ENOENT')
|
|
92
|
+
return [];
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
const events = [];
|
|
96
|
+
for (const line of raw.split('\n')) {
|
|
97
|
+
if (!line.trim())
|
|
98
|
+
continue;
|
|
99
|
+
const event = parseFileEditEvent(line);
|
|
100
|
+
if (event)
|
|
101
|
+
events.push(event);
|
|
102
|
+
}
|
|
103
|
+
return events;
|
|
104
|
+
}
|
|
105
|
+
function collapseFileEditEvents(events) {
|
|
106
|
+
const latest = new Map();
|
|
107
|
+
for (const event of events) {
|
|
108
|
+
const key = `${event.sessionId}\0${event.file}`;
|
|
109
|
+
const previous = latest.get(key);
|
|
110
|
+
if (!previous || event.at >= previous.at)
|
|
111
|
+
latest.set(key, event);
|
|
112
|
+
}
|
|
113
|
+
return [...latest.values()].sort((left, right) => {
|
|
114
|
+
return left.at - right.at;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function compactFileEditLog({ dataDir, maxEvents, }) {
|
|
118
|
+
const loaded = loadFileEditEvents({ dataDir });
|
|
119
|
+
if (loaded instanceof Error)
|
|
120
|
+
return loaded;
|
|
121
|
+
const collapsed = collapseFileEditEvents(loaded);
|
|
122
|
+
const kept = collapsed.length > maxEvents ? collapsed.slice(-maxEvents) : collapsed;
|
|
123
|
+
const logPath = path.join(dataDir, FILE_EDIT_EVENTS_FILENAME);
|
|
124
|
+
const tempPath = `${logPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
125
|
+
const body = kept.map((event) => `${JSON.stringify(event)}\n`).join('');
|
|
126
|
+
const written = errore.try(() => {
|
|
127
|
+
fs.writeFileSync(tempPath, body);
|
|
128
|
+
fs.renameSync(tempPath, logPath);
|
|
129
|
+
}, (cause) => new FilesystemOperationError({ operation: 'compact file edit log', cause }));
|
|
130
|
+
if (written instanceof Error) {
|
|
131
|
+
fs.rmSync(tempPath, { force: true });
|
|
132
|
+
return written;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function appendFileEditEvents({ dataDir, events, maxEvents = DEFAULT_MAX_EVENTS, compactAfterBytes = DEFAULT_COMPACT_AFTER_BYTES, }) {
|
|
136
|
+
if (events.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
const logPath = path.join(dataDir, FILE_EDIT_EVENTS_FILENAME);
|
|
139
|
+
const prepared = errore.try(() => {
|
|
140
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
141
|
+
fs.appendFileSync(logPath, events.map((event) => `${JSON.stringify(event)}\n`).join(''));
|
|
142
|
+
}, (cause) => new FilesystemOperationError({ operation: 'append file edit log', cause }));
|
|
143
|
+
if (prepared instanceof Error)
|
|
144
|
+
return prepared;
|
|
145
|
+
const size = errore.try(() => fs.statSync(logPath).size, (cause) => new FilesystemOperationError({ operation: 'stat file edit log', cause }));
|
|
146
|
+
if (size instanceof Error)
|
|
147
|
+
return size;
|
|
148
|
+
if (size < compactAfterBytes)
|
|
149
|
+
return;
|
|
150
|
+
return compactFileEditLog({ dataDir, maxEvents });
|
|
151
|
+
}
|
|
152
|
+
function recordToolEdits({ dataDir, directory, sessionId, tool, args, }) {
|
|
153
|
+
const name = tool.toLowerCase();
|
|
154
|
+
if (!isFileEditTool(name))
|
|
155
|
+
return;
|
|
156
|
+
const files = extractEditedFiles({ tool: name, args, directory });
|
|
157
|
+
if (files.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const at = Date.now();
|
|
160
|
+
const events = files.map((file) => {
|
|
161
|
+
return {
|
|
162
|
+
v: 1,
|
|
163
|
+
at,
|
|
164
|
+
sessionId,
|
|
165
|
+
file,
|
|
166
|
+
tool: name,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const result = appendFileEditEvents({ dataDir, events });
|
|
170
|
+
if (result instanceof Error) {
|
|
171
|
+
logger.warn('Failed to record file edit', result.message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export function createFileEditHooks({ dataDir, directory, }) {
|
|
175
|
+
return {
|
|
176
|
+
'tool.execute.after': async (input) => {
|
|
177
|
+
recordToolEdits({
|
|
178
|
+
dataDir,
|
|
179
|
+
directory,
|
|
180
|
+
sessionId: input.sessionID,
|
|
181
|
+
tool: input.tool,
|
|
182
|
+
args: input.args,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export const fileEditTrackerPlugin = async ({ directory }) => {
|
|
188
|
+
const dataDir = process.env.KIMAKI_DATA_DIR;
|
|
189
|
+
if (!dataDir)
|
|
190
|
+
return {};
|
|
191
|
+
setPluginLogFilePath(dataDir);
|
|
192
|
+
const created = errore.try(() => fs.mkdirSync(dataDir, { recursive: true }), (cause) => new FilesystemOperationError({ operation: 'create file edit log dir', cause }));
|
|
193
|
+
if (created instanceof Error) {
|
|
194
|
+
logger.warn('Failed to create file edit log dir', created.message);
|
|
195
|
+
}
|
|
196
|
+
return createFileEditHooks({ dataDir, directory });
|
|
197
|
+
};
|