kimaki 0.4.76 → 0.4.78

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 (162) hide show
  1. package/dist/adapter-rest-boundary.test.js +34 -0
  2. package/dist/agent-model.e2e.test.js +2 -20
  3. package/dist/cli.js +50 -13
  4. package/dist/commands/channel-ref.js +16 -0
  5. package/dist/commands/diff.js +20 -85
  6. package/dist/commands/merge-worktree.js +5 -17
  7. package/dist/commands/new-worktree.js +5 -9
  8. package/dist/commands/permissions.js +77 -11
  9. package/dist/commands/resume.js +5 -9
  10. package/dist/commands/screenshare.js +295 -0
  11. package/dist/commands/session.js +6 -17
  12. package/dist/critique-utils.js +95 -0
  13. package/dist/diff-patch-plugin.js +314 -0
  14. package/dist/discord-bot.js +19 -14
  15. package/dist/discord-js-import-boundary.test.js +62 -0
  16. package/dist/discord-utils.js +44 -0
  17. package/dist/event-stream-real-capture.e2e.test.js +2 -20
  18. package/dist/gateway-proxy.e2e.test.js +2 -5
  19. package/dist/generated/cloudflare/browser.js +17 -0
  20. package/dist/generated/cloudflare/client.js +34 -0
  21. package/dist/generated/cloudflare/commonInputTypes.js +10 -0
  22. package/dist/generated/cloudflare/enums.js +48 -0
  23. package/dist/generated/cloudflare/internal/class.js +47 -0
  24. package/dist/generated/cloudflare/internal/prismaNamespace.js +252 -0
  25. package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +222 -0
  26. package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +135 -0
  27. package/dist/generated/cloudflare/models/bot_api_keys.js +1 -0
  28. package/dist/generated/cloudflare/models/bot_tokens.js +1 -0
  29. package/dist/generated/cloudflare/models/channel_agents.js +1 -0
  30. package/dist/generated/cloudflare/models/channel_directories.js +1 -0
  31. package/dist/generated/cloudflare/models/channel_mention_mode.js +1 -0
  32. package/dist/generated/cloudflare/models/channel_models.js +1 -0
  33. package/dist/generated/cloudflare/models/channel_verbosity.js +1 -0
  34. package/dist/generated/cloudflare/models/channel_worktrees.js +1 -0
  35. package/dist/generated/cloudflare/models/forum_sync_configs.js +1 -0
  36. package/dist/generated/cloudflare/models/global_models.js +1 -0
  37. package/dist/generated/cloudflare/models/ipc_requests.js +1 -0
  38. package/dist/generated/cloudflare/models/part_messages.js +1 -0
  39. package/dist/generated/cloudflare/models/scheduled_tasks.js +1 -0
  40. package/dist/generated/cloudflare/models/session_agents.js +1 -0
  41. package/dist/generated/cloudflare/models/session_events.js +1 -0
  42. package/dist/generated/cloudflare/models/session_models.js +1 -0
  43. package/dist/generated/cloudflare/models/session_start_sources.js +1 -0
  44. package/dist/generated/cloudflare/models/thread_sessions.js +1 -0
  45. package/dist/generated/cloudflare/models/thread_worktrees.js +1 -0
  46. package/dist/generated/cloudflare/models.js +1 -0
  47. package/dist/generated/node/browser.js +17 -0
  48. package/dist/generated/node/client.js +37 -0
  49. package/dist/generated/node/commonInputTypes.js +10 -0
  50. package/dist/generated/node/enums.js +48 -0
  51. package/dist/generated/node/internal/class.js +49 -0
  52. package/dist/generated/node/internal/prismaNamespace.js +252 -0
  53. package/dist/generated/node/internal/prismaNamespaceBrowser.js +222 -0
  54. package/dist/generated/node/models/bot_api_keys.js +1 -0
  55. package/dist/generated/node/models/bot_tokens.js +1 -0
  56. package/dist/generated/node/models/channel_agents.js +1 -0
  57. package/dist/generated/node/models/channel_directories.js +1 -0
  58. package/dist/generated/node/models/channel_mention_mode.js +1 -0
  59. package/dist/generated/node/models/channel_models.js +1 -0
  60. package/dist/generated/node/models/channel_verbosity.js +1 -0
  61. package/dist/generated/node/models/channel_worktrees.js +1 -0
  62. package/dist/generated/node/models/forum_sync_configs.js +1 -0
  63. package/dist/generated/node/models/global_models.js +1 -0
  64. package/dist/generated/node/models/ipc_requests.js +1 -0
  65. package/dist/generated/node/models/part_messages.js +1 -0
  66. package/dist/generated/node/models/scheduled_tasks.js +1 -0
  67. package/dist/generated/node/models/session_agents.js +1 -0
  68. package/dist/generated/node/models/session_events.js +1 -0
  69. package/dist/generated/node/models/session_models.js +1 -0
  70. package/dist/generated/node/models/session_start_sources.js +1 -0
  71. package/dist/generated/node/models/thread_sessions.js +1 -0
  72. package/dist/generated/node/models/thread_worktrees.js +1 -0
  73. package/dist/generated/node/models.js +1 -0
  74. package/dist/interaction-handler.js +10 -0
  75. package/dist/kimaki-digital-twin.e2e.test.js +2 -20
  76. package/dist/message-flags-boundary.test.js +54 -0
  77. package/dist/message-formatting.js +3 -62
  78. package/dist/onboarding-tutorial-plugin.js +1 -1
  79. package/dist/opencode-command.js +129 -0
  80. package/dist/opencode-command.test.js +48 -0
  81. package/dist/opencode-interrupt-plugin.js +19 -1
  82. package/dist/opencode-interrupt-plugin.test.js +0 -5
  83. package/dist/opencode-plugin-loading.e2e.test.js +9 -20
  84. package/dist/opencode-plugin.js +4 -4
  85. package/dist/opencode.js +150 -27
  86. package/dist/patch-text-parser.js +97 -0
  87. package/dist/platform/components-v2.js +20 -0
  88. package/dist/platform/discord-adapter.js +1440 -0
  89. package/dist/platform/discord-routes.js +31 -0
  90. package/dist/platform/message-flags.js +8 -0
  91. package/dist/platform/platform-value.js +41 -0
  92. package/dist/platform/slack-adapter.js +872 -0
  93. package/dist/platform/slack-markdown.js +169 -0
  94. package/dist/platform/types.js +4 -0
  95. package/dist/queue-advanced-e2e-setup.js +265 -0
  96. package/dist/queue-advanced-footer.e2e.test.js +173 -0
  97. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  98. package/dist/queue-advanced-permissions-typing.e2e.test.js +73 -1
  99. package/dist/runtime-lifecycle.e2e.test.js +2 -20
  100. package/dist/session-handler/event-stream-state.js +5 -0
  101. package/dist/session-handler/event-stream-state.test.js +6 -2
  102. package/dist/session-handler/thread-session-runtime.js +32 -2
  103. package/dist/system-message.js +26 -23
  104. package/dist/test-utils.js +16 -0
  105. package/dist/thread-message-queue.e2e.test.js +2 -20
  106. package/dist/utils.js +3 -1
  107. package/dist/voice-message.e2e.test.js +2 -20
  108. package/dist/voice.js +122 -9
  109. package/dist/voice.test.js +17 -2
  110. package/dist/websockify.js +69 -0
  111. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  112. package/package.json +4 -2
  113. package/skills/critique/SKILL.md +17 -0
  114. package/skills/egaki/SKILL.md +35 -0
  115. package/skills/event-sourcing-state/SKILL.md +252 -0
  116. package/skills/goke/SKILL.md +1 -0
  117. package/skills/npm-package/SKILL.md +21 -2
  118. package/skills/playwriter/SKILL.md +1 -1
  119. package/skills/x-articles/SKILL.md +554 -0
  120. package/src/agent-model.e2e.test.ts +4 -19
  121. package/src/cli.ts +60 -13
  122. package/src/commands/diff.ts +25 -99
  123. package/src/commands/merge-worktree.ts +5 -21
  124. package/src/commands/new-worktree.ts +5 -11
  125. package/src/commands/permissions.ts +100 -15
  126. package/src/commands/resume.ts +5 -12
  127. package/src/commands/screenshare.ts +354 -0
  128. package/src/commands/session.ts +6 -23
  129. package/src/critique-utils.ts +139 -0
  130. package/src/discord-bot.ts +20 -15
  131. package/src/discord-utils.ts +53 -0
  132. package/src/event-stream-real-capture.e2e.test.ts +4 -20
  133. package/src/gateway-proxy.e2e.test.ts +2 -5
  134. package/src/interaction-handler.ts +15 -0
  135. package/src/kimaki-digital-twin.e2e.test.ts +2 -21
  136. package/src/message-formatting.ts +3 -68
  137. package/src/onboarding-tutorial-plugin.ts +1 -1
  138. package/src/opencode-command.test.ts +70 -0
  139. package/src/opencode-command.ts +188 -0
  140. package/src/opencode-interrupt-plugin.test.ts +0 -5
  141. package/src/opencode-interrupt-plugin.ts +34 -1
  142. package/src/opencode-plugin-loading.e2e.test.ts +25 -35
  143. package/src/opencode-plugin.ts +5 -4
  144. package/src/opencode.ts +199 -32
  145. package/src/patch-text-parser.ts +107 -0
  146. package/src/queue-advanced-e2e-setup.ts +273 -0
  147. package/src/queue-advanced-footer.e2e.test.ts +211 -0
  148. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  149. package/src/queue-advanced-permissions-typing.e2e.test.ts +92 -0
  150. package/src/runtime-lifecycle.e2e.test.ts +4 -19
  151. package/src/session-handler/event-stream-state.test.ts +6 -2
  152. package/src/session-handler/event-stream-state.ts +5 -0
  153. package/src/session-handler/thread-session-runtime.ts +45 -2
  154. package/src/system-message.ts +26 -23
  155. package/src/test-utils.ts +17 -0
  156. package/src/thread-message-queue.e2e.test.ts +2 -20
  157. package/src/utils.ts +3 -1
  158. package/src/voice-message.e2e.test.ts +3 -20
  159. package/src/voice.test.ts +26 -2
  160. package/src/voice.ts +147 -9
  161. package/src/websockify.ts +101 -0
  162. package/src/worktree-lifecycle.e2e.test.ts +391 -0
@@ -0,0 +1,222 @@
1
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
+ /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
4
+ // @ts-nocheck
5
+ /*
6
+ * WARNING: This is an internal file that is subject to change!
7
+ *
8
+ * 🛑 Under no circumstances should you import this file directly! 🛑
9
+ *
10
+ * All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
11
+ * While this enables partial backward compatibility, it is not part of the stable public API.
12
+ *
13
+ * If you are looking for your Models, Enums, and Input Types, please import them from the respective
14
+ * model files in the `model` directory!
15
+ */
16
+ import * as runtime from "@prisma/client/runtime/index-browser";
17
+ export const Decimal = runtime.Decimal;
18
+ export const NullTypes = {
19
+ DbNull: runtime.NullTypes.DbNull,
20
+ JsonNull: runtime.NullTypes.JsonNull,
21
+ AnyNull: runtime.NullTypes.AnyNull,
22
+ };
23
+ /**
24
+ * Helper for filtering JSON entries that have `null` on the database (empty on the db)
25
+ *
26
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
27
+ */
28
+ export const DbNull = runtime.DbNull;
29
+ /**
30
+ * Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
31
+ *
32
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
33
+ */
34
+ export const JsonNull = runtime.JsonNull;
35
+ /**
36
+ * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
37
+ *
38
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
39
+ */
40
+ export const AnyNull = runtime.AnyNull;
41
+ export const ModelName = {
42
+ thread_sessions: 'thread_sessions',
43
+ session_events: 'session_events',
44
+ part_messages: 'part_messages',
45
+ bot_tokens: 'bot_tokens',
46
+ channel_directories: 'channel_directories',
47
+ bot_api_keys: 'bot_api_keys',
48
+ thread_worktrees: 'thread_worktrees',
49
+ channel_models: 'channel_models',
50
+ session_models: 'session_models',
51
+ channel_agents: 'channel_agents',
52
+ session_agents: 'session_agents',
53
+ channel_worktrees: 'channel_worktrees',
54
+ channel_verbosity: 'channel_verbosity',
55
+ channel_mention_mode: 'channel_mention_mode',
56
+ global_models: 'global_models',
57
+ scheduled_tasks: 'scheduled_tasks',
58
+ session_start_sources: 'session_start_sources',
59
+ forum_sync_configs: 'forum_sync_configs',
60
+ ipc_requests: 'ipc_requests'
61
+ };
62
+ /*
63
+ * Enums
64
+ */
65
+ export const TransactionIsolationLevel = runtime.makeStrictEnum({
66
+ Serializable: 'Serializable'
67
+ });
68
+ export const Thread_sessionsScalarFieldEnum = {
69
+ thread_id: 'thread_id',
70
+ session_id: 'session_id',
71
+ created_at: 'created_at'
72
+ };
73
+ export const Session_eventsScalarFieldEnum = {
74
+ id: 'id',
75
+ session_id: 'session_id',
76
+ thread_id: 'thread_id',
77
+ timestamp: 'timestamp',
78
+ event_index: 'event_index',
79
+ event_json: 'event_json'
80
+ };
81
+ export const Part_messagesScalarFieldEnum = {
82
+ part_id: 'part_id',
83
+ message_id: 'message_id',
84
+ thread_id: 'thread_id',
85
+ created_at: 'created_at'
86
+ };
87
+ export const Bot_tokensScalarFieldEnum = {
88
+ app_id: 'app_id',
89
+ token: 'token',
90
+ bot_mode: 'bot_mode',
91
+ client_id: 'client_id',
92
+ client_secret: 'client_secret',
93
+ proxy_url: 'proxy_url',
94
+ created_at: 'created_at',
95
+ last_used_at: 'last_used_at'
96
+ };
97
+ export const Channel_directoriesScalarFieldEnum = {
98
+ channel_id: 'channel_id',
99
+ directory: 'directory',
100
+ channel_type: 'channel_type',
101
+ created_at: 'created_at'
102
+ };
103
+ export const Bot_api_keysScalarFieldEnum = {
104
+ app_id: 'app_id',
105
+ gemini_api_key: 'gemini_api_key',
106
+ openai_api_key: 'openai_api_key',
107
+ xai_api_key: 'xai_api_key',
108
+ created_at: 'created_at'
109
+ };
110
+ export const Thread_worktreesScalarFieldEnum = {
111
+ thread_id: 'thread_id',
112
+ worktree_name: 'worktree_name',
113
+ worktree_directory: 'worktree_directory',
114
+ project_directory: 'project_directory',
115
+ status: 'status',
116
+ error_message: 'error_message',
117
+ created_at: 'created_at'
118
+ };
119
+ export const Channel_modelsScalarFieldEnum = {
120
+ channel_id: 'channel_id',
121
+ model_id: 'model_id',
122
+ variant: 'variant',
123
+ created_at: 'created_at',
124
+ updated_at: 'updated_at'
125
+ };
126
+ export const Session_modelsScalarFieldEnum = {
127
+ session_id: 'session_id',
128
+ model_id: 'model_id',
129
+ variant: 'variant',
130
+ created_at: 'created_at'
131
+ };
132
+ export const Channel_agentsScalarFieldEnum = {
133
+ channel_id: 'channel_id',
134
+ agent_name: 'agent_name',
135
+ created_at: 'created_at',
136
+ updated_at: 'updated_at'
137
+ };
138
+ export const Session_agentsScalarFieldEnum = {
139
+ session_id: 'session_id',
140
+ agent_name: 'agent_name',
141
+ created_at: 'created_at'
142
+ };
143
+ export const Channel_worktreesScalarFieldEnum = {
144
+ channel_id: 'channel_id',
145
+ enabled: 'enabled',
146
+ created_at: 'created_at',
147
+ updated_at: 'updated_at'
148
+ };
149
+ export const Channel_verbosityScalarFieldEnum = {
150
+ channel_id: 'channel_id',
151
+ verbosity: 'verbosity',
152
+ updated_at: 'updated_at'
153
+ };
154
+ export const Channel_mention_modeScalarFieldEnum = {
155
+ channel_id: 'channel_id',
156
+ enabled: 'enabled',
157
+ created_at: 'created_at',
158
+ updated_at: 'updated_at'
159
+ };
160
+ export const Global_modelsScalarFieldEnum = {
161
+ app_id: 'app_id',
162
+ model_id: 'model_id',
163
+ variant: 'variant',
164
+ created_at: 'created_at',
165
+ updated_at: 'updated_at'
166
+ };
167
+ export const Scheduled_tasksScalarFieldEnum = {
168
+ id: 'id',
169
+ status: 'status',
170
+ schedule_kind: 'schedule_kind',
171
+ run_at: 'run_at',
172
+ cron_expr: 'cron_expr',
173
+ timezone: 'timezone',
174
+ next_run_at: 'next_run_at',
175
+ running_started_at: 'running_started_at',
176
+ last_run_at: 'last_run_at',
177
+ last_error: 'last_error',
178
+ attempts: 'attempts',
179
+ payload_json: 'payload_json',
180
+ prompt_preview: 'prompt_preview',
181
+ channel_id: 'channel_id',
182
+ thread_id: 'thread_id',
183
+ session_id: 'session_id',
184
+ project_directory: 'project_directory',
185
+ created_at: 'created_at',
186
+ updated_at: 'updated_at'
187
+ };
188
+ export const Session_start_sourcesScalarFieldEnum = {
189
+ session_id: 'session_id',
190
+ schedule_kind: 'schedule_kind',
191
+ scheduled_task_id: 'scheduled_task_id',
192
+ created_at: 'created_at',
193
+ updated_at: 'updated_at'
194
+ };
195
+ export const Forum_sync_configsScalarFieldEnum = {
196
+ id: 'id',
197
+ app_id: 'app_id',
198
+ forum_channel_id: 'forum_channel_id',
199
+ output_dir: 'output_dir',
200
+ direction: 'direction',
201
+ created_at: 'created_at',
202
+ updated_at: 'updated_at'
203
+ };
204
+ export const Ipc_requestsScalarFieldEnum = {
205
+ id: 'id',
206
+ type: 'type',
207
+ session_id: 'session_id',
208
+ thread_id: 'thread_id',
209
+ payload: 'payload',
210
+ response: 'response',
211
+ status: 'status',
212
+ created_at: 'created_at',
213
+ updated_at: 'updated_at'
214
+ };
215
+ export const SortOrder = {
216
+ asc: 'asc',
217
+ desc: 'desc'
218
+ };
219
+ export const NullsOrder = {
220
+ first: 'first',
221
+ last: 'last'
222
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -37,6 +37,7 @@ import { handleContextUsageCommand } from './commands/context-usage.js';
37
37
  import { handleSessionIdCommand } from './commands/session-id.js';
38
38
  import { handleUpgradeAndRestartCommand } from './commands/upgrade.js';
39
39
  import { handleMcpCommand, handleMcpSelectMenu } from './commands/mcp.js';
40
+ import { handleScreenshareCommand, handleScreenshareStopCommand, } from './commands/screenshare.js';
40
41
  import { handleModelVariantSelectMenu } from './commands/model.js';
41
42
  import { handleModelVariantCommand, handleVariantQuickSelectMenu, handleVariantScopeSelectMenu, } from './commands/model-variant.js';
42
43
  import { hasKimakiBotPermission } from './discord-utils.js';
@@ -211,6 +212,15 @@ export function registerInteractionHandler({ discordClient, appId, }) {
211
212
  case 'mcp':
212
213
  await handleMcpCommand({ command: interaction, appId });
213
214
  return;
215
+ case 'screenshare':
216
+ await handleScreenshareCommand({ command: interaction, appId });
217
+ return;
218
+ case 'screenshare-stop':
219
+ await handleScreenshareStopCommand({
220
+ command: interaction,
221
+ appId,
222
+ });
223
+ return;
214
224
  }
215
225
  // Handle quick agent commands (ending with -agent suffix, but not the base /agent command)
216
226
  if (interaction.commandName.endsWith('-agent') &&
@@ -1,7 +1,6 @@
1
1
  // End-to-end test using discord-digital-twin + real Kimaki bot runtime.
2
2
  // Verifies onboarding channel creation, message -> thread creation, and assistant reply.
3
3
  import fs from 'node:fs';
4
- import net from 'node:net';
5
4
  import path from 'node:path';
6
5
  import { expect, test } from 'vitest';
7
6
  import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
@@ -11,7 +10,7 @@ import { setDataDir } from './config.js';
11
10
  import { startDiscordBot } from './discord-bot.js';
12
11
  import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, } from './database.js';
13
12
  import { startHranaServer, stopHranaServer } from './hrana-server.js';
14
- import { cleanupTestSessions } from './test-utils.js';
13
+ import { cleanupTestSessions, chooseLockPort } from './test-utils.js';
15
14
  import { stopOpencodeServer } from './opencode.js';
16
15
  const geminiApiKey = process.env['GEMINI_API_KEY'] ||
17
16
  process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
@@ -32,23 +31,6 @@ function createRunDirectories() {
32
31
  providerCacheDbPath,
33
32
  };
34
33
  }
35
- function chooseLockPort() {
36
- return new Promise((resolve, reject) => {
37
- const server = net.createServer();
38
- server.listen(0, () => {
39
- const address = server.address();
40
- if (!address || typeof address === 'string') {
41
- server.close();
42
- reject(new Error('Failed to resolve lock port'));
43
- return;
44
- }
45
- const port = address.port;
46
- server.close(() => {
47
- resolve(port);
48
- });
49
- });
50
- });
51
- }
52
34
  function createDiscordJsClient({ restUrl }) {
53
35
  return new Client({
54
36
  intents: [
@@ -72,7 +54,7 @@ function createDiscordJsClient({ restUrl }) {
72
54
  e2eTest('onboarding then message creates thread and assistant reply via digital twin', async () => {
73
55
  const testStartTime = Date.now();
74
56
  const directories = createRunDirectories();
75
- const lockPort = await chooseLockPort();
57
+ const lockPort = chooseLockPort({ key: 'kimaki-digital-twin-e2e' });
76
58
  process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
77
59
  setDataDir(directories.dataDir);
78
60
  const proxy = new CachedOpencodeProviderProxy({
@@ -0,0 +1,54 @@
1
+ // Guardrail test for message-flag boundary during adapter migration.
2
+ // Runtime code outside adapter/voice/forum-sync must use platform constants,
3
+ // not discord.js MessageFlags enums directly.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { describe, expect, test } from 'vitest';
7
+ const SRC_DIR = path.resolve(import.meta.dirname);
8
+ function collectTsFiles(dir) {
9
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
10
+ return entries.flatMap((entry) => {
11
+ const fullPath = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ return collectTsFiles(fullPath);
14
+ }
15
+ if (!entry.name.endsWith('.ts')) {
16
+ return [];
17
+ }
18
+ return [fullPath];
19
+ });
20
+ }
21
+ function toWorkspaceRelative(filePath) {
22
+ return path.relative(path.resolve(import.meta.dirname, '..'), filePath);
23
+ }
24
+ function isAllowedBoundaryFile(relativePath) {
25
+ if (relativePath === 'src/platform/discord-adapter.ts') {
26
+ return true;
27
+ }
28
+ if (relativePath === 'src/voice-handler.ts') {
29
+ return true;
30
+ }
31
+ if (relativePath.startsWith('src/forum-sync/')) {
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ describe('message flags boundary', () => {
37
+ test('does not use discord.js MessageFlags outside allowed modules', () => {
38
+ const violations = collectTsFiles(SRC_DIR)
39
+ .map((filePath) => {
40
+ return {
41
+ relativePath: toWorkspaceRelative(filePath),
42
+ content: fs.readFileSync(filePath, 'utf8'),
43
+ };
44
+ })
45
+ .filter(({ relativePath }) => {
46
+ return !isAllowedBoundaryFile(relativePath);
47
+ })
48
+ .filter(({ content }) => {
49
+ return /from\s+['"]discord\.js['"][\s\S]*\bMessageFlags\b|\bMessageFlags\./.test(content);
50
+ })
51
+ .map(({ relativePath }) => relativePath);
52
+ expect(violations).toMatchInlineSnapshot(`[]`);
53
+ });
54
+ });
@@ -5,6 +5,7 @@ import * as errore from 'errore';
5
5
  import { createLogger, LogPrefix } from './logger.js';
6
6
  import { FetchError } from './errors.js';
7
7
  import { processImage } from './image-utils.js';
8
+ import { parsePatchFileCounts } from './patch-text-parser.js';
8
9
  const logger = createLogger(LogPrefix.FORMATTING);
9
10
  /**
10
11
  * Resolves Discord mentions in message content to human-readable names.
@@ -36,67 +37,7 @@ export function resolveMentions(message) {
36
37
  function escapeInlineMarkdown(text) {
37
38
  return text.replace(/([*_~|`\\])/g, '\\$1');
38
39
  }
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
- }
40
+ // parsePatchCounts → imported from patch-text-parser.ts as parsePatchFileCounts
100
41
  /**
101
42
  * Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
102
43
  */
@@ -220,7 +161,7 @@ export function getToolSummaryText(part) {
220
161
  if (!patchText) {
221
162
  return '';
222
163
  }
223
- const patchCounts = parsePatchCounts(patchText);
164
+ const patchCounts = parsePatchFileCounts(patchText);
224
165
  return [...patchCounts.entries()]
225
166
  .map(([filePath, { additions, deletions }]) => {
226
167
  const fileName = filePath.split('/').pop() || '';
@@ -42,7 +42,7 @@ const onboardingTutorialPlugin = async () => {
42
42
  return;
43
43
  }
44
44
  output.parts.push({
45
- id: crypto.randomUUID(),
45
+ id: `prt_${crypto.randomUUID()}`,
46
46
  sessionID,
47
47
  messageID: firstText.messageID,
48
48
  type: 'text',
@@ -0,0 +1,129 @@
1
+ // Shared OpenCode and Kimaki command resolution helpers.
2
+ // Normalizes `which`/`where` output across platforms, builds safe spawn
3
+ // arguments for Windows npm `.cmd` shims without relying on `shell: true`,
4
+ // and creates a stable `kimaki` shim for OpenCode child processes.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ const WINDOWS_CMD_SHIM_REGEX = /\.(cmd|bat)$/i;
8
+ function quotePosixShellSegment(value) {
9
+ return `'${value.replaceAll("'", `'\\''`)}'`;
10
+ }
11
+ export function splitCommandLookupOutput(output) {
12
+ return output
13
+ .split(/\r?\n/g)
14
+ .map((line) => {
15
+ return line.trim();
16
+ })
17
+ .filter((line) => {
18
+ return line.length > 0;
19
+ });
20
+ }
21
+ export function selectResolvedCommand({ output, isWindows, }) {
22
+ const lines = splitCommandLookupOutput(output);
23
+ if (lines.length === 0) {
24
+ return null;
25
+ }
26
+ if (!isWindows) {
27
+ return lines[0] || null;
28
+ }
29
+ const cmdShim = lines.find((line) => {
30
+ return WINDOWS_CMD_SHIM_REGEX.test(line);
31
+ });
32
+ return cmdShim || lines[0] || null;
33
+ }
34
+ function quoteWindowsCommandSegment(value) {
35
+ if (!/[\s"]/u.test(value)) {
36
+ return value;
37
+ }
38
+ return `"${value.replaceAll('"', '\\"')}"`;
39
+ }
40
+ export function getSpawnCommandAndArgs({ resolvedCommand, baseArgs, platform, }) {
41
+ const effectivePlatform = platform || process.platform;
42
+ if (effectivePlatform !== 'win32') {
43
+ return { command: resolvedCommand, args: baseArgs };
44
+ }
45
+ if (!WINDOWS_CMD_SHIM_REGEX.test(resolvedCommand)) {
46
+ return { command: resolvedCommand, args: baseArgs };
47
+ }
48
+ return {
49
+ command: 'cmd.exe',
50
+ args: [
51
+ '/d',
52
+ '/s',
53
+ '/c',
54
+ quoteWindowsCommandSegment(resolvedCommand),
55
+ ...baseArgs.map((arg) => {
56
+ return quoteWindowsCommandSegment(arg);
57
+ }),
58
+ ],
59
+ // Let cmd.exe receive the command line exactly as constructed above.
60
+ // Without this, Node re-quotes the executable segment and npm shim paths
61
+ // like `C:\Program Files\nodejs\opencode.cmd` break again.
62
+ windowsVerbatimArguments: true,
63
+ };
64
+ }
65
+ export function ensureKimakiCommandShim({ dataDir, execPath, execArgv, entryScript, platform, }) {
66
+ const effectivePlatform = platform || process.platform;
67
+ const shimDirectory = path.join(dataDir, 'bin');
68
+ try {
69
+ fs.mkdirSync(shimDirectory, { recursive: true });
70
+ const launcherArgs = [...execArgv, entryScript];
71
+ if (effectivePlatform === 'win32') {
72
+ const shimPath = path.join(shimDirectory, 'kimaki.cmd');
73
+ const shimContent = [
74
+ '@echo off',
75
+ [execPath, ...launcherArgs].map((segment) => {
76
+ return `"${segment.replaceAll('"', '""')}"`;
77
+ }).join(' ') + ' %*',
78
+ '',
79
+ ].join('\r\n');
80
+ writeShimIfNeeded({
81
+ shimPath,
82
+ shimContent,
83
+ });
84
+ return shimDirectory;
85
+ }
86
+ const shimPath = path.join(shimDirectory, 'kimaki');
87
+ const shimContent = [
88
+ '#!/bin/sh',
89
+ `exec ${[execPath, ...launcherArgs].map((segment) => {
90
+ return quotePosixShellSegment(segment);
91
+ }).join(' ')} "$@"`,
92
+ '',
93
+ ].join('\n');
94
+ writeShimIfNeeded({
95
+ shimPath,
96
+ shimContent,
97
+ mode: 0o755,
98
+ });
99
+ return shimDirectory;
100
+ }
101
+ catch (cause) {
102
+ return new Error('Failed to create kimaki command shim', { cause });
103
+ }
104
+ }
105
+ export function prependPathEntry({ entry, existingPath, }) {
106
+ const pathEntries = (existingPath || '').split(path.delimiter).filter((segment) => {
107
+ return segment.length > 0;
108
+ });
109
+ if (pathEntries.includes(entry)) {
110
+ return existingPath || entry;
111
+ }
112
+ return [entry, ...pathEntries].join(path.delimiter);
113
+ }
114
+ export function getPathEnvKey(env) {
115
+ return Object.keys(env).find((key) => {
116
+ return key.toLowerCase() === 'path';
117
+ }) || 'PATH';
118
+ }
119
+ function writeShimIfNeeded({ shimPath, shimContent, mode, }) {
120
+ const existingContent = fs.existsSync(shimPath)
121
+ ? fs.readFileSync(shimPath, 'utf8')
122
+ : null;
123
+ if (existingContent !== shimContent) {
124
+ fs.writeFileSync(shimPath, shimContent, 'utf8');
125
+ }
126
+ if (mode !== undefined) {
127
+ fs.chmodSync(shimPath, mode);
128
+ }
129
+ }