kimaki 0.4.90 → 0.4.91

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.
Files changed (114) hide show
  1. package/dist/agent-model.e2e.test.js +80 -2
  2. package/dist/anthropic-auth-plugin.js +246 -195
  3. package/dist/anthropic-auth-plugin.test.js +125 -0
  4. package/dist/anthropic-auth-state.js +231 -0
  5. package/dist/bin.js +6 -3
  6. package/dist/cli-parsing.test.js +23 -0
  7. package/dist/cli-send-thread.e2e.test.js +2 -2
  8. package/dist/cli.js +72 -46
  9. package/dist/commands/merge-worktree.js +6 -3
  10. package/dist/commands/new-worktree.js +18 -7
  11. package/dist/commands/worktrees.js +71 -7
  12. package/dist/context-awareness-plugin.js +52 -50
  13. package/dist/context-awareness-plugin.test.js +68 -1
  14. package/dist/discord-bot.js +126 -54
  15. package/dist/discord-utils.test.js +19 -0
  16. package/dist/errors.js +0 -5
  17. package/dist/exec-async.js +26 -0
  18. package/dist/external-opencode-sync.js +33 -72
  19. package/dist/forum-sync/config.js +2 -2
  20. package/dist/forum-sync/markdown.js +4 -8
  21. package/dist/hrana-server.js +11 -3
  22. package/dist/image-optimizer-plugin.js +153 -0
  23. package/dist/ipc-tools-plugin.js +11 -4
  24. package/dist/kimaki-opencode-plugin.js +1 -0
  25. package/dist/logger.js +0 -1
  26. package/dist/markdown.js +2 -2
  27. package/dist/message-preprocessing.js +100 -16
  28. package/dist/onboarding-tutorial.js +1 -1
  29. package/dist/opencode-command-detection.js +70 -0
  30. package/dist/opencode-command-detection.test.js +210 -0
  31. package/dist/opencode-interrupt-plugin.js +64 -8
  32. package/dist/opencode-interrupt-plugin.test.js +23 -39
  33. package/dist/opencode.js +16 -20
  34. package/dist/pkce.js +23 -0
  35. package/dist/plugin-logger.js +59 -0
  36. package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -1
  37. package/dist/queue-advanced-question.e2e.test.js +127 -42
  38. package/dist/sentry.js +7 -114
  39. package/dist/session-handler/event-stream-state.js +1 -1
  40. package/dist/session-handler/thread-runtime-state.js +9 -0
  41. package/dist/session-handler/thread-session-runtime.js +197 -45
  42. package/dist/session-title-rename.test.js +80 -0
  43. package/dist/store.js +1 -2
  44. package/dist/system-message.js +105 -49
  45. package/dist/system-message.test.js +598 -15
  46. package/dist/task-runner.js +7 -4
  47. package/dist/task-schedule.js +2 -0
  48. package/dist/thread-message-queue.e2e.test.js +18 -11
  49. package/dist/unnest-code-blocks.js +11 -1
  50. package/dist/unnest-code-blocks.test.js +32 -0
  51. package/dist/voice-handler.js +15 -5
  52. package/dist/voice.js +53 -23
  53. package/dist/voice.test.js +2 -0
  54. package/dist/worktrees.js +111 -120
  55. package/package.json +15 -19
  56. package/skills/lintcn/SKILL.md +6 -1
  57. package/skills/new-skill/SKILL.md +211 -0
  58. package/skills/npm-package/SKILL.md +3 -2
  59. package/skills/spiceflow/SKILL.md +1 -1
  60. package/skills/usecomputer/SKILL.md +174 -249
  61. package/src/agent-model.e2e.test.ts +95 -2
  62. package/src/anthropic-auth-plugin.test.ts +159 -0
  63. package/src/anthropic-auth-plugin.ts +474 -403
  64. package/src/anthropic-auth-state.ts +282 -0
  65. package/src/bin.ts +6 -3
  66. package/src/cli-parsing.test.ts +32 -0
  67. package/src/cli-send-thread.e2e.test.ts +2 -2
  68. package/src/cli.ts +93 -62
  69. package/src/commands/merge-worktree.ts +8 -3
  70. package/src/commands/new-worktree.ts +22 -10
  71. package/src/commands/worktrees.ts +86 -5
  72. package/src/context-awareness-plugin.test.ts +77 -1
  73. package/src/context-awareness-plugin.ts +85 -64
  74. package/src/discord-bot.ts +135 -56
  75. package/src/discord-utils.test.ts +21 -0
  76. package/src/errors.ts +0 -6
  77. package/src/exec-async.ts +35 -0
  78. package/src/external-opencode-sync.ts +39 -85
  79. package/src/forum-sync/config.ts +2 -2
  80. package/src/forum-sync/markdown.ts +5 -9
  81. package/src/hrana-server.ts +15 -3
  82. package/src/image-optimizer-plugin.ts +194 -0
  83. package/src/ipc-tools-plugin.ts +16 -8
  84. package/src/kimaki-opencode-plugin.ts +1 -0
  85. package/src/logger.ts +0 -1
  86. package/src/markdown.ts +2 -2
  87. package/src/message-preprocessing.ts +117 -16
  88. package/src/onboarding-tutorial.ts +1 -1
  89. package/src/opencode-command-detection.test.ts +268 -0
  90. package/src/opencode-command-detection.ts +79 -0
  91. package/src/opencode-interrupt-plugin.test.ts +93 -50
  92. package/src/opencode-interrupt-plugin.ts +86 -9
  93. package/src/opencode.ts +16 -22
  94. package/src/plugin-logger.ts +68 -0
  95. package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -1
  96. package/src/queue-advanced-question.e2e.test.ts +243 -158
  97. package/src/sentry.ts +7 -120
  98. package/src/session-handler/event-stream-state.ts +1 -1
  99. package/src/session-handler/thread-runtime-state.ts +17 -0
  100. package/src/session-handler/thread-session-runtime.ts +232 -46
  101. package/src/session-title-rename.test.ts +112 -0
  102. package/src/store.ts +3 -8
  103. package/src/system-message.test.ts +612 -0
  104. package/src/system-message.ts +136 -63
  105. package/src/task-runner.ts +7 -4
  106. package/src/task-schedule.ts +3 -0
  107. package/src/thread-message-queue.e2e.test.ts +22 -11
  108. package/src/undici.d.ts +12 -0
  109. package/src/unnest-code-blocks.test.ts +34 -0
  110. package/src/unnest-code-blocks.ts +18 -1
  111. package/src/voice-handler.ts +18 -4
  112. package/src/voice.test.ts +2 -0
  113. package/src/voice.ts +68 -23
  114. package/src/worktrees.ts +152 -156
@@ -3,46 +3,94 @@
3
3
  // The user's message must appear as a real user message in the thread, not
4
4
  // get consumed as a tool result answer (which lost voice/image content).
5
5
  import { describe, test, expect, afterEach } from 'vitest';
6
- import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
7
- import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
8
- import { pendingQuestionContexts } from './commands/ask-question.js';
6
+ import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
7
+ import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
9
8
  import { store } from './store.js';
9
+ import { getOpencodeClient } from './opencode.js';
10
+ import { getThreadSession } from './database.js';
10
11
  const TEXT_CHANNEL_ID = '200000000000001007';
11
12
  const VOICE_CHANNEL_ID = '200000000000001017';
12
- async function waitForPendingQuestion({ threadId, timeoutMs, }) {
13
- const start = Date.now();
14
- while (Date.now() - start < timeoutMs) {
15
- const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
16
- return context.thread.id === threadId;
17
- });
18
- if (entry) {
19
- return { contextHash: entry[0] };
20
- }
21
- await new Promise((resolve) => {
22
- setTimeout(resolve, 100);
23
- });
13
+ function setDeterministicTranscription(config) {
14
+ store.setState({
15
+ test: { deterministicTranscription: config },
16
+ });
17
+ }
18
+ function getOpencodeClientForTest(projectDirectory) {
19
+ const client = getOpencodeClient(projectDirectory);
20
+ if (!client) {
21
+ throw new Error('OpenCode client not found for project directory');
24
22
  }
25
- throw new Error('Timed out waiting for pending question context');
23
+ return client;
24
+ }
25
+ function getTextFromParts(parts) {
26
+ return parts.flatMap((part) => {
27
+ if (part.type === 'text') {
28
+ return [part.text];
29
+ }
30
+ return [];
31
+ });
32
+ }
33
+ function normalizeSessionText(text) {
34
+ return text
35
+ .replace(/\[current git branch is [^\]]+\]/g, '')
36
+ .replace(/<discord-user[^>]*\/>/g, '<discord-user />')
37
+ .trim();
26
38
  }
27
- async function waitForNoPendingQuestion({ threadId, timeoutMs, }) {
39
+ function getSessionRoleTextTimeline(messages) {
40
+ return messages.flatMap((message) => {
41
+ const text = normalizeSessionText(getTextFromParts(message.parts).join(''));
42
+ if (!text.trim()) {
43
+ return [];
44
+ }
45
+ return [{ role: message.info.role, text }];
46
+ });
47
+ }
48
+ function getSessionMessageSummary(messages) {
49
+ return messages.map((message) => {
50
+ return {
51
+ role: message.info.role,
52
+ parts: message.parts.map((part) => {
53
+ if (part.type === 'text') {
54
+ return {
55
+ type: part.type,
56
+ text: normalizeSessionText(part.text),
57
+ };
58
+ }
59
+ if (part.type === 'tool') {
60
+ return {
61
+ type: part.type,
62
+ tool: part.tool,
63
+ status: part.state.status,
64
+ title: part.state.status === 'completed' ? part.state.title : undefined,
65
+ output: part.state.status === 'completed' ? part.state.output : undefined,
66
+ };
67
+ }
68
+ return { type: part.type };
69
+ }),
70
+ };
71
+ });
72
+ }
73
+ async function waitForSessionMessages({ projectDirectory, sessionId, timeoutMs, predicate, }) {
74
+ const client = getOpencodeClientForTest(projectDirectory);
28
75
  const start = Date.now();
29
76
  while (Date.now() - start < timeoutMs) {
30
- const stillPending = [...pendingQuestionContexts.values()].some((context) => {
31
- return context.thread.id === threadId;
77
+ const response = await client.session.messages({
78
+ sessionID: sessionId,
79
+ directory: projectDirectory,
32
80
  });
33
- if (!stillPending) {
34
- return;
81
+ const messages = response.data ?? [];
82
+ if (predicate(messages)) {
83
+ return messages;
35
84
  }
36
85
  await new Promise((resolve) => {
37
86
  setTimeout(resolve, 100);
38
87
  });
39
88
  }
40
- throw new Error('Timed out waiting for question context cleanup');
41
- }
42
- function setDeterministicTranscription(config) {
43
- store.setState({
44
- test: { deterministicTranscription: config },
89
+ const finalResponse = await client.session.messages({
90
+ sessionID: sessionId,
91
+ directory: projectDirectory,
45
92
  });
93
+ return finalResponse.data ?? [];
46
94
  }
47
95
  describe('queue advanced: question tool answer', () => {
48
96
  const ctx = setupQueueAdvancedSuite({
@@ -130,12 +178,16 @@ describe('queue advanced: voice message during pending question', () => {
130
178
  timeout: 12_000,
131
179
  });
132
180
  // Send a voice message while the question is pending.
133
- // message.content is "" for voice messages only the attachment exists.
181
+ // Reproduction: Discord voice messages can still carry non-empty
182
+ // message.content. The bug consumed that raw text before transcription,
183
+ // so the session never received the spoken content.
134
184
  setDeterministicTranscription({
135
185
  transcription: 'I want option Alpha please',
136
186
  queueMessage: false,
137
187
  });
138
- await th.user(TEST_USER_ID).sendVoiceMessage();
188
+ await th.user(TEST_USER_ID).sendVoiceMessage({
189
+ content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
190
+ });
139
191
  // Give time for question cleanup to propagate
140
192
  await new Promise((r) => {
141
193
  setTimeout(r, 1_000);
@@ -155,21 +207,54 @@ describe('queue advanced: voice message during pending question', () => {
155
207
  afterMessageIncludes: 'I want option Alpha please',
156
208
  afterAuthorId: ctx.discord.botUserId,
157
209
  });
210
+ const sessionId = await getThreadSession(thread.id);
211
+ expect(sessionId).toBeTruthy();
212
+ const sessionMessages = await waitForSessionMessages({
213
+ projectDirectory: ctx.directories.projectDirectory,
214
+ sessionId: sessionId,
215
+ timeoutMs: 8_000,
216
+ predicate: (messages) => {
217
+ const timeline = getSessionRoleTextTimeline(messages);
218
+ return timeline.some((entry) => {
219
+ return entry.text.includes('I want option Alpha please');
220
+ });
221
+ },
222
+ });
223
+ const sessionTimeline = getSessionRoleTextTimeline(sessionMessages);
224
+ const sessionSummary = getSessionMessageSummary(sessionMessages);
225
+ const latestUserText = sessionTimeline
226
+ .filter((entry) => {
227
+ return entry.role === 'user';
228
+ })
229
+ .at(-1)?.text;
230
+ const assistantTexts = sessionTimeline.flatMap((entry) => {
231
+ if (entry.role === 'assistant') {
232
+ return [entry.text];
233
+ }
234
+ return [];
235
+ });
236
+ expect(latestUserText).toContain('I want option Alpha please');
237
+ expect(latestUserText).not.toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
238
+ expect(assistantTexts).toContain('ok');
239
+ expect(sessionSummary.some((message) => {
240
+ return message.role === 'user'
241
+ && message.parts.some((part) => {
242
+ return part.type === 'text' && part.text.includes('I want option Alpha please');
243
+ });
244
+ })).toBe(true);
245
+ expect(sessionSummary.some((message) => {
246
+ return message.role === 'assistant'
247
+ && message.parts.some((part) => {
248
+ return part.type === 'text' && part.text === 'ok';
249
+ });
250
+ })).toBe(true);
158
251
  const timeline = await th.text({ showInteractions: true });
159
- expect(timeline).toMatchInlineSnapshot(`
160
- "--- from: user (queue-question-tester)
161
- QUESTION_TEXT_ANSWER_MARKER
162
- --- from: assistant (TestBot)
163
- **Pick one**
164
- Which option do you prefer?
165
- --- from: user (queue-question-tester)
166
- [attachment: voice-message.ogg]
167
- --- from: assistant (TestBot)
168
- 🎤 Transcribing voice message...
169
- 📝 **Transcribed message:** I want option Alpha please
170
- ⬥ ok
171
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
172
- `);
252
+ expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER');
253
+ expect(timeline).toContain('Which option do you prefer?');
254
+ expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL');
255
+ expect(timeline).toContain('🎤 Transcribing voice message...');
256
+ expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please');
257
+ expect(timeline).toContain('⬥ ok');
173
258
  // Voice content must be present as a real transcribed message, not lost
174
259
  expect(timeline).toContain('I want option Alpha please');
175
260
  }, 20_000);
package/dist/sentry.js CHANGED
@@ -1,123 +1,16 @@
1
- // Sentry error tracking initialization and notifyError helper.
2
- // Uses @sentry/node for the Node.js runtime (bot process, plugin process, worker threads).
3
- // Must be initialized early in both the bot process (cli.ts) and plugin process
4
- // (kimaki-opencode-plugin.ts). The plugin process receives the DSN via KIMAKI_SENTRY_DSN env var.
5
- import * as Sentry from '@sentry/node';
6
- import * as errore from 'errore';
7
- import { createRequire } from 'node:module';
8
- import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js';
9
- // DSN placeholder — replace with your Sentry project DSN.
10
- // Users can also set KIMAKI_SENTRY_DSN env var.
11
- const HARDCODED_DSN = 'https://3b87e21ac01cb9c66225719ea65111d2@o4510952031715328.ingest.us.sentry.io/4510952088469504';
12
- function readKimakiVersion() {
13
- try {
14
- const require = createRequire(import.meta.url);
15
- const pkg = require('../package.json');
16
- const version = pkg.version;
17
- if (!version) {
18
- return 'unknown';
19
- }
20
- return version;
21
- }
22
- catch {
23
- return 'unknown';
24
- }
25
- }
26
- const kimakiVersion = readKimakiVersion();
27
- const kimakiRelease = `kimaki@${kimakiVersion}`;
28
- let initialized = false;
1
+ // Sentry stubs. @sentry/node was removed these are no-op placeholders
2
+ // so the 20+ files importing notifyError/initSentry don't need changing.
3
+ // If Sentry is re-enabled in the future, replace these stubs with real calls.
29
4
  /**
30
- * Initialize Sentry. Call once at process startup.
31
- * No-op if DSN is empty or --no-sentry was passed.
5
+ * Initialize Sentry. Currently a no-op.
32
6
  */
33
- export function initSentry({ dsn } = {}) {
34
- if (process.env.KIMAKI_SENTRY_DISABLED === '1') {
35
- return;
36
- }
37
- const resolvedDsn = dsn || process.env.KIMAKI_SENTRY_DSN || HARDCODED_DSN;
38
- if (!resolvedDsn || initialized) {
39
- return;
40
- }
41
- Sentry.init({
42
- dsn: resolvedDsn,
43
- release: kimakiRelease,
44
- integrations: [],
45
- tracesSampleRate: 0,
46
- sendDefaultPii: false,
47
- profilesSampleRate: 0,
48
- beforeSend(event, hint) {
49
- // Skip in development — too noisy, errors appear in terminal
50
- if (process.env.NODE_ENV === 'development') {
51
- return null;
52
- }
53
- // Skip abort errors — walks the cause chain so wrapped aborts are caught
54
- if (errore.isAbortError(hint.originalException)) {
55
- return null;
56
- }
57
- try {
58
- const sanitizedEvent = sanitizeUnknownValue(event, {
59
- redactPaths: false,
60
- });
61
- if (sanitizedEvent && typeof sanitizedEvent === 'object') {
62
- return sanitizedEvent;
63
- }
64
- }
65
- catch {
66
- return event;
67
- }
68
- return event;
69
- },
70
- });
71
- Sentry.setTag('kimaki_version', kimakiVersion);
72
- initialized = true;
73
- }
7
+ export function initSentry(_opts) { }
74
8
  /**
75
- * Report an unexpected error to Sentry.
9
+ * Report an unexpected error. Currently a no-op.
76
10
  * Safe to call even if Sentry is not initialized.
77
11
  * Fire-and-forget only: use `void notifyError(error, msg)` and never await it.
78
- * This helper must never throw.
79
- * Use this at terminal error handlers — the "last catch" in a chain
80
- * where the error would otherwise be invisible.
81
12
  */
82
- export function notifyError(error, msg) {
83
- try {
84
- if (!initialized) {
85
- return;
86
- }
87
- // TODO re enable sentry?
88
- if (!process.env.KIMAKI_SENTRY)
89
- return;
90
- const safeMsg = (() => {
91
- if (!msg) {
92
- return undefined;
93
- }
94
- try {
95
- return sanitizeSensitiveText(msg, { redactPaths: false });
96
- }
97
- catch {
98
- return msg;
99
- }
100
- })();
101
- const safeError = (() => {
102
- try {
103
- return sanitizeUnknownValue(error, { redactPaths: false });
104
- }
105
- catch {
106
- return error;
107
- }
108
- })();
109
- Sentry.captureException(error, {
110
- tags: { kimaki_version: kimakiVersion },
111
- extra: { msg: safeMsg, kimakiVersion, error: safeError },
112
- });
113
- void Sentry.flush(1000).catch(() => {
114
- return;
115
- });
116
- }
117
- catch {
118
- return;
119
- }
120
- }
13
+ export function notifyError(_error, _msg) { }
121
14
  /**
122
15
  * User-readable error class. Messages from AppError instances
123
16
  * are forwarded to the user as-is; regular Error messages may be obfuscated.
@@ -5,7 +5,7 @@
5
5
  import { getOpencodeEventSessionId } from './opencode-session-event-log.js';
6
6
  function getTaskChildSessionId({ part, }) {
7
7
  // Event-shape reference:
8
- // - discord/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl
8
+ // - cli/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl
9
9
  // - In real task events, state.metadata.sessionId appears on running/completed
10
10
  // tool updates and is the canonical child-session identifier.
11
11
  // We intentionally do not parse state.output because it is user-facing text
@@ -15,6 +15,7 @@ import { store } from '../store.js';
15
15
  export function initialThreadState() {
16
16
  return {
17
17
  sessionId: undefined,
18
+ sessionUsername: undefined,
18
19
  queueItems: [],
19
20
  listenerController: undefined,
20
21
  sentPartIds: new Set(),
@@ -60,6 +61,14 @@ export function removeThread(threadId) {
60
61
  export function setSessionId(threadId, sessionId) {
61
62
  updateThread(threadId, (t) => ({ ...t, sessionId }));
62
63
  }
64
+ export function setSessionUsername(threadId, username) {
65
+ updateThread(threadId, (t) => {
66
+ if (t.sessionUsername) {
67
+ return t;
68
+ }
69
+ return { ...t, sessionUsername: username };
70
+ });
71
+ }
63
72
  export function enqueueItem(threadId, item) {
64
73
  updateThread(threadId, (t) => ({
65
74
  ...t,