kimaki 0.26.0 → 0.27.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 +25 -122
- package/dist/anthropic-account-identity.js +1 -0
- package/dist/anthropic-auth-plugin.js +1 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/send.js +24 -0
- package/dist/cli-commands/session.js +183 -8
- package/dist/cli-commands/task.js +13 -1
- package/dist/cli-parsing.test.js +59 -3
- package/dist/cli-runner.js +28 -4
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/agent.js +6 -8
- package/dist/commands/ask-question.js +21 -32
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +36 -10
- package/dist/commands/model-variant.js +7 -1
- package/dist/commands/model.js +14 -8
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/database.js +124 -3
- package/dist/db.js +22 -0
- package/dist/db.test.js +167 -1
- package/dist/discord-bot.js +34 -32
- package/dist/discord-command-registration.js +9 -1
- package/dist/discord-utils.js +13 -5
- package/dist/discord-utils.test.js +17 -1
- package/dist/errors.js +5 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- 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 +5 -5
- package/dist/git-worktree-core.js +182 -17
- package/dist/ipc-tools-plugin.js +63 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +7 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +10 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +18 -7
- package/dist/message-formatting.test.js +56 -9
- 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.test.js +60 -0
- package/dist/opencode.js +53 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +1 -1
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +40 -246
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
- package/dist/queue-advanced-question.e2e.test.js +31 -26
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
- package/dist/queue-delete-message.e2e.test.js +1 -1
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +112 -2
- package/dist/queue-question-select-drain.e2e.test.js +30 -128
- package/dist/runtime-lifecycle.e2e.test.js +10 -37
- package/dist/schema.js +34 -0
- package/dist/session-handler/event-stream-state.js +267 -50
- package/dist/session-handler/event-stream-state.test.js +415 -8
- package/dist/session-handler/model-utils.js +159 -5
- package/dist/session-handler/model-utils.test.js +345 -0
- package/dist/session-handler/thread-session-runtime.js +252 -182
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +7 -39
- package/dist/sleep.e2e.test.js +491 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +59 -6
- package/dist/system-message.test.js +76 -6
- package/dist/task-runner.js +94 -4
- package/dist/task-schedule.js +56 -0
- package/dist/task-schedule.test.js +71 -1
- package/dist/thread-message-queue.e2e.test.js +10 -169
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-message.e2e.test.js +10 -10
- package/dist/voice.test.js +55 -29
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +29 -10
- 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/errore/SKILL.md +18 -0
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +37 -0
- 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 +25 -151
- package/src/anthropic-account-identity.ts +1 -0
- package/src/anthropic-auth-plugin.ts +1 -0
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/send.ts +25 -0
- package/src/cli-commands/session.ts +203 -9
- package/src/cli-commands/task.ts +13 -1
- package/src/cli-parsing.test.ts +69 -3
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +28 -4
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/agent.ts +6 -6
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +21 -32
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +43 -12
- package/src/commands/model-variant.ts +8 -1
- package/src/commands/model.ts +18 -8
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/database.ts +195 -3
- package/src/db.test.ts +188 -0
- package/src/db.ts +26 -0
- package/src/discord-bot.ts +35 -29
- package/src/discord-command-registration.ts +15 -1
- package/src/discord-utils.test.ts +19 -0
- package/src/discord-utils.ts +21 -6
- package/src/errors.ts +5 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- 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 +5 -5
- package/src/git-worktree-core.ts +247 -20
- package/src/ipc-tools-plugin.ts +75 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +7 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +13 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +86 -12
- package/src/message-formatting.ts +19 -7
- 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 +81 -0
- package/src/opencode.ts +71 -11
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +1 -1
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +47 -294
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
- package/src/queue-advanced-question.e2e.test.ts +35 -29
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
- package/src/queue-delete-message.e2e.test.ts +1 -1
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +123 -2
- package/src/queue-question-select-drain.e2e.test.ts +32 -157
- package/src/runtime-lifecycle.e2e.test.ts +10 -41
- package/src/schema.sql +14 -1
- package/src/schema.ts +36 -0
- package/src/session-handler/event-stream-state.test.ts +437 -7
- package/src/session-handler/event-stream-state.ts +389 -53
- package/src/session-handler/model-utils.test.ts +415 -0
- package/src/session-handler/model-utils.ts +249 -5
- package/src/session-handler/thread-session-runtime.ts +300 -215
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +9 -48
- package/src/sleep.e2e.test.ts +562 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +21 -0
- package/src/system-message.test.ts +86 -6
- package/src/system-message.ts +66 -7
- package/src/task-runner.ts +136 -2
- package/src/task-schedule.test.ts +76 -0
- package/src/task-schedule.ts +95 -0
- package/src/thread-message-queue.e2e.test.ts +10 -202
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-attachment.ts +29 -1
- package/src/voice-message.e2e.test.ts +10 -10
- package/src/voice.test.ts +63 -29
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +29 -9
- 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
|
@@ -325,7 +325,7 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
325
325
|
--- from: assistant (TestBot)
|
|
326
326
|
*using deterministic-provider/deterministic-v2*
|
|
327
327
|
⬥ gateway-proxy-reply
|
|
328
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
328
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@900000000000000001>"
|
|
329
329
|
`);
|
|
330
330
|
expect(reply).toBeDefined();
|
|
331
331
|
expect(reply.content.trim().length).toBeGreaterThan(0);
|
|
@@ -352,12 +352,12 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
352
352
|
--- from: assistant (TestBot)
|
|
353
353
|
*using deterministic-provider/deterministic-v2*
|
|
354
354
|
⬥ gateway-proxy-reply
|
|
355
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
355
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@900000000000000001>
|
|
356
356
|
--- from: user (proxy-tester)
|
|
357
357
|
follow up through proxy
|
|
358
358
|
--- from: assistant (TestBot)
|
|
359
359
|
⬥ gateway-proxy-reply
|
|
360
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
360
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@900000000000000001>"
|
|
361
361
|
`);
|
|
362
362
|
expect(reply).toBeDefined();
|
|
363
363
|
expect(reply.content.trim().length).toBeGreaterThan(0);
|
|
@@ -384,12 +384,12 @@ describeIf('gateway-proxy e2e', () => {
|
|
|
384
384
|
--- from: assistant (TestBot)
|
|
385
385
|
*using deterministic-provider/deterministic-v2*
|
|
386
386
|
⬥ gateway-proxy-reply
|
|
387
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
387
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@900000000000000001>
|
|
388
388
|
--- from: user (proxy-tester)
|
|
389
389
|
follow up through proxy
|
|
390
390
|
--- from: assistant (TestBot)
|
|
391
391
|
⬥ gateway-proxy-reply
|
|
392
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
392
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@900000000000000001>
|
|
393
393
|
--- from: user (proxy-tester)
|
|
394
394
|
!echo proxy-shell-test
|
|
395
395
|
--- from: assistant (TestBot)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Plugin-safe
|
|
1
|
+
// Plugin-safe Git worktree creation, identity validation, and removal primitives.
|
|
2
2
|
// This module must NOT import config.ts, logger.ts, or any module that
|
|
3
3
|
// transitively pulls them in. It is used by both:
|
|
4
4
|
// - worktrees.ts (bot process — wraps with kimaki logger + config)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// All logging goes through an optional `log` callback so callers control output.
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import * as errore from 'errore';
|
|
10
11
|
import { execAsync } from './exec-async.js';
|
|
11
12
|
const SUBMODULE_INIT_TIMEOUT_MS = 20 * 60_000;
|
|
12
13
|
const INSTALL_TIMEOUT_MS = 60_000;
|
|
@@ -17,6 +18,12 @@ const LOCKFILE_TO_INSTALL_COMMAND = [
|
|
|
17
18
|
['yarn.lock', 'yarn install'],
|
|
18
19
|
['package-lock.json', 'npm install'],
|
|
19
20
|
];
|
|
21
|
+
export const KIMAKI_WORKTREE_ADAPTER_TYPE = 'kimaki-worktree';
|
|
22
|
+
export class WorktreeIdentityError extends errore.createTaggedError({
|
|
23
|
+
name: 'WorktreeIdentityError',
|
|
24
|
+
message: 'Created worktree does not match the requested checkout: $reason',
|
|
25
|
+
}) {
|
|
26
|
+
}
|
|
20
27
|
const silentLog = {
|
|
21
28
|
info() { },
|
|
22
29
|
warn() { },
|
|
@@ -114,7 +121,7 @@ async function hasSubmoduleGitMetadata(directory) {
|
|
|
114
121
|
* Remove broken submodule stubs created by `git worktree add`.
|
|
115
122
|
* git worktree creates .git files pointing to incomplete gitdirs (missing HEAD).
|
|
116
123
|
*/
|
|
117
|
-
async function removeBrokenSubmoduleStubs(directory, log) {
|
|
124
|
+
async function removeBrokenSubmoduleStubs({ directory, expectedCommonGitDirectory, log, }) {
|
|
118
125
|
const submodulePaths = await getSubmodulePaths(directory, log);
|
|
119
126
|
for (const subPath of submodulePaths) {
|
|
120
127
|
const fullPath = path.join(directory, subPath);
|
|
@@ -130,7 +137,9 @@ async function removeBrokenSubmoduleStubs(directory, log) {
|
|
|
130
137
|
const gitdir = path.resolve(fullPath, match[1].trim());
|
|
131
138
|
const headFile = path.join(gitdir, 'HEAD');
|
|
132
139
|
const headExists = await fs.promises.access(headFile).then(() => true).catch(() => false);
|
|
133
|
-
|
|
140
|
+
const relativeGitdir = path.relative(expectedCommonGitDirectory, gitdir);
|
|
141
|
+
const pointsOutsideExpectedRepository = relativeGitdir === '..' || relativeGitdir.startsWith(`..${path.sep}`);
|
|
142
|
+
if (!headExists || pointsOutsideExpectedRepository) {
|
|
134
143
|
log.info(`Removing broken submodule stub: ${subPath}`);
|
|
135
144
|
await fs.promises.rm(fullPath, { recursive: true, force: true });
|
|
136
145
|
}
|
|
@@ -186,17 +195,27 @@ async function initializeSubmodulesWithLocalReferences({ sourceDirectory, worktr
|
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
}
|
|
189
|
-
async function validateSubmodulePointers(directory, log) {
|
|
190
|
-
const
|
|
198
|
+
async function validateSubmodulePointers({ directory, expectedCommonGitDirectory, log, }) {
|
|
199
|
+
const submoduleConfigs = await readSubmoduleConfigs(directory);
|
|
200
|
+
if (submoduleConfigs instanceof Error) {
|
|
201
|
+
log.warn(submoduleConfigs.message);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const submodulePaths = submoduleConfigs.map(({ path: submodulePath }) => {
|
|
205
|
+
return submodulePath;
|
|
206
|
+
});
|
|
191
207
|
if (submodulePaths.length === 0)
|
|
192
208
|
return;
|
|
193
209
|
const issues = [];
|
|
210
|
+
const foreignGitdirs = [];
|
|
194
211
|
await Promise.all(submodulePaths.map(async (subPath) => {
|
|
195
212
|
const gitFile = path.join(directory, subPath, '.git');
|
|
196
213
|
try {
|
|
197
214
|
const stat = await fs.promises.stat(gitFile);
|
|
198
|
-
if (!stat.isFile())
|
|
215
|
+
if (!stat.isFile()) {
|
|
216
|
+
issues.push(`${subPath}: .git is not a file`);
|
|
199
217
|
return;
|
|
218
|
+
}
|
|
200
219
|
const content = await fs.promises.readFile(gitFile, 'utf-8');
|
|
201
220
|
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
202
221
|
const gitdir = match?.[1]?.trim();
|
|
@@ -205,18 +224,26 @@ async function validateSubmodulePointers(directory, log) {
|
|
|
205
224
|
return;
|
|
206
225
|
}
|
|
207
226
|
const resolvedGitdir = path.resolve(path.join(directory, subPath), gitdir);
|
|
227
|
+
const relativeGitdir = path.relative(expectedCommonGitDirectory, resolvedGitdir);
|
|
228
|
+
if (relativeGitdir === '..' || relativeGitdir.startsWith(`..${path.sep}`)) {
|
|
229
|
+
foreignGitdirs.push(`${subPath}: ${resolvedGitdir}`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
208
232
|
const headExists = await fs.promises.access(path.join(resolvedGitdir, 'HEAD')).then(() => true).catch(() => false);
|
|
209
233
|
if (!headExists) {
|
|
210
234
|
issues.push(`${subPath}: gitdir missing HEAD (${resolvedGitdir})`);
|
|
235
|
+
return;
|
|
211
236
|
}
|
|
212
237
|
}
|
|
213
|
-
catch {
|
|
214
|
-
|
|
238
|
+
catch (e) {
|
|
239
|
+
issues.push(`${subPath}: failed to validate .git (${e instanceof Error ? e.message : String(e)})`);
|
|
215
240
|
}
|
|
216
241
|
}));
|
|
217
|
-
if (
|
|
218
|
-
|
|
242
|
+
if (foreignGitdirs.length > 0) {
|
|
243
|
+
return new Error(`Submodule gitdir belongs to another repository: ${foreignGitdirs.join('; ')}`);
|
|
219
244
|
}
|
|
245
|
+
if (issues.length > 0)
|
|
246
|
+
log.warn(`Submodule validation issues: ${issues.join('; ')}`);
|
|
220
247
|
}
|
|
221
248
|
function detectInstallCommand(directory) {
|
|
222
249
|
for (const [lockfile, command] of LOCKFILE_TO_INSTALL_COMMAND) {
|
|
@@ -239,18 +266,107 @@ async function runDependencyInstall(directory, log) {
|
|
|
239
266
|
return new Error(`Install failed: ${formatCommandError(e)}`, { cause: e });
|
|
240
267
|
}
|
|
241
268
|
}
|
|
269
|
+
async function canonicalizePath(value) {
|
|
270
|
+
return await fs.promises.realpath(value).catch(() => path.resolve(value));
|
|
271
|
+
}
|
|
272
|
+
export async function resolveGitCommit({ directory, ref, }) {
|
|
273
|
+
const result = await execAsync(`git rev-parse --verify ${JSON.stringify(`${ref}^{commit}`)}`, {
|
|
274
|
+
cwd: directory,
|
|
275
|
+
timeout: 10_000,
|
|
276
|
+
}).catch((e) => new Error(`Failed to resolve Git commit ${ref}`, { cause: e }));
|
|
277
|
+
if (result instanceof Error)
|
|
278
|
+
return result;
|
|
279
|
+
const commit = result.stdout.trim();
|
|
280
|
+
if (!/^[0-9a-f]{40}$/i.test(commit)) {
|
|
281
|
+
return new Error(`Git returned an invalid commit for ${ref}: ${commit}`);
|
|
282
|
+
}
|
|
283
|
+
return commit;
|
|
284
|
+
}
|
|
285
|
+
export async function resolveGitCommonDirectory({ directory, }) {
|
|
286
|
+
const result = await execAsync('git rev-parse --git-common-dir', {
|
|
287
|
+
cwd: directory,
|
|
288
|
+
timeout: 10_000,
|
|
289
|
+
}).catch((e) => new Error(`Failed to resolve Git common directory for ${directory}`, { cause: e }));
|
|
290
|
+
if (result instanceof Error)
|
|
291
|
+
return result;
|
|
292
|
+
const commonDirectory = path.isAbsolute(result.stdout.trim())
|
|
293
|
+
? result.stdout.trim()
|
|
294
|
+
: path.resolve(directory, result.stdout.trim());
|
|
295
|
+
return canonicalizePath(commonDirectory);
|
|
296
|
+
}
|
|
297
|
+
async function resolveGitDirectory({ directory, }) {
|
|
298
|
+
const result = await execAsync('git rev-parse --absolute-git-dir', {
|
|
299
|
+
cwd: directory,
|
|
300
|
+
timeout: 10_000,
|
|
301
|
+
}).catch((e) => new Error(`Failed to resolve Git directory for ${directory}`, { cause: e }));
|
|
302
|
+
if (result instanceof Error)
|
|
303
|
+
return result;
|
|
304
|
+
return canonicalizePath(result.stdout.trim());
|
|
305
|
+
}
|
|
306
|
+
export async function validateWorktreeIdentity({ projectDirectory, worktreeDirectory, baseCommit, }) {
|
|
307
|
+
const [sourceCommonGitDirectory, commonGitDirectory, gitDirectory, headCommit] = await Promise.all([
|
|
308
|
+
resolveGitCommonDirectory({ directory: projectDirectory }),
|
|
309
|
+
resolveGitCommonDirectory({ directory: worktreeDirectory }),
|
|
310
|
+
resolveGitDirectory({ directory: worktreeDirectory }),
|
|
311
|
+
resolveGitCommit({ directory: worktreeDirectory, ref: 'HEAD' }),
|
|
312
|
+
]);
|
|
313
|
+
if (sourceCommonGitDirectory instanceof Error) {
|
|
314
|
+
return new WorktreeIdentityError({
|
|
315
|
+
reason: sourceCommonGitDirectory.message,
|
|
316
|
+
cause: sourceCommonGitDirectory,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (commonGitDirectory instanceof Error) {
|
|
320
|
+
return new WorktreeIdentityError({
|
|
321
|
+
reason: commonGitDirectory.message,
|
|
322
|
+
cause: commonGitDirectory,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (headCommit instanceof Error) {
|
|
326
|
+
return new WorktreeIdentityError({
|
|
327
|
+
reason: headCommit.message,
|
|
328
|
+
cause: headCommit,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (gitDirectory instanceof Error) {
|
|
332
|
+
return new WorktreeIdentityError({
|
|
333
|
+
reason: gitDirectory.message,
|
|
334
|
+
cause: gitDirectory,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (commonGitDirectory !== sourceCommonGitDirectory) {
|
|
338
|
+
return new WorktreeIdentityError({
|
|
339
|
+
reason: `expected common directory ${sourceCommonGitDirectory}, received ${commonGitDirectory}`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (gitDirectory === commonGitDirectory) {
|
|
343
|
+
return new WorktreeIdentityError({
|
|
344
|
+
reason: `${worktreeDirectory} is not a linked worktree`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (headCommit !== baseCommit) {
|
|
348
|
+
return new WorktreeIdentityError({
|
|
349
|
+
reason: `expected HEAD ${baseCommit}, received ${headCommit}`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return { commonGitDirectory, headCommit };
|
|
353
|
+
}
|
|
242
354
|
/**
|
|
243
355
|
* Create a git worktree with full submodule initialization and dependency install.
|
|
244
356
|
* Plugin-safe: no config.ts or logger.ts imports. Logging is done via the `log`
|
|
245
357
|
* callback (defaults to silent no-op for plugin use).
|
|
246
358
|
*/
|
|
247
|
-
export async function createWorktreeCore({ projectDirectory, targetDirectory, branchName,
|
|
359
|
+
export async function createWorktreeCore({ projectDirectory, targetDirectory, branchName, baseCommit, onProgress, log = silentLog, }) {
|
|
248
360
|
if (fs.existsSync(targetDirectory)) {
|
|
249
361
|
return new Error(`Worktree directory already exists: ${targetDirectory}`);
|
|
250
362
|
}
|
|
363
|
+
const commonGitDirectory = await resolveGitCommonDirectory({
|
|
364
|
+
directory: projectDirectory,
|
|
365
|
+
});
|
|
366
|
+
if (commonGitDirectory instanceof Error)
|
|
367
|
+
return commonGitDirectory;
|
|
251
368
|
await fs.promises.mkdir(path.dirname(targetDirectory), { recursive: true });
|
|
252
|
-
const
|
|
253
|
-
const createCmd = `git worktree add ${JSON.stringify(targetDirectory)} -B ${JSON.stringify(branchName)} ${JSON.stringify(targetRef)}`;
|
|
369
|
+
const createCmd = `git worktree add ${JSON.stringify(targetDirectory)} -B ${JSON.stringify(branchName)} ${JSON.stringify(baseCommit)}`;
|
|
254
370
|
const createResult = await execAsync(createCmd, {
|
|
255
371
|
cwd: projectDirectory,
|
|
256
372
|
timeout: SUBMODULE_INIT_TIMEOUT_MS,
|
|
@@ -258,7 +374,11 @@ export async function createWorktreeCore({ projectDirectory, targetDirectory, br
|
|
|
258
374
|
if (createResult instanceof Error)
|
|
259
375
|
return createResult;
|
|
260
376
|
// Remove broken submodule stubs before init
|
|
261
|
-
await removeBrokenSubmoduleStubs(
|
|
377
|
+
await removeBrokenSubmoduleStubs({
|
|
378
|
+
directory: targetDirectory,
|
|
379
|
+
expectedCommonGitDirectory: commonGitDirectory,
|
|
380
|
+
log,
|
|
381
|
+
});
|
|
262
382
|
// Init submodules with local --reference directories
|
|
263
383
|
log.info(`Initializing submodules in ${targetDirectory}`);
|
|
264
384
|
const submoduleResult = await initializeSubmodulesWithLocalReferences({
|
|
@@ -272,8 +392,25 @@ export async function createWorktreeCore({ projectDirectory, targetDirectory, br
|
|
|
272
392
|
else {
|
|
273
393
|
log.info(`Submodules initialized in ${targetDirectory}`);
|
|
274
394
|
}
|
|
275
|
-
//
|
|
276
|
-
await validateSubmodulePointers(
|
|
395
|
+
// Invalid submodule pointers can route later Git commands into another clone.
|
|
396
|
+
const submoduleValidation = await validateSubmodulePointers({
|
|
397
|
+
directory: targetDirectory,
|
|
398
|
+
expectedCommonGitDirectory: commonGitDirectory,
|
|
399
|
+
log,
|
|
400
|
+
});
|
|
401
|
+
if (submoduleValidation instanceof Error) {
|
|
402
|
+
const cleanupResult = await removeWorktreeCore({
|
|
403
|
+
projectDirectory,
|
|
404
|
+
worktreeDirectory: targetDirectory,
|
|
405
|
+
branchName,
|
|
406
|
+
});
|
|
407
|
+
if (cleanupResult instanceof Error) {
|
|
408
|
+
return new Error(`${submoduleValidation.message}; cleanup failed: ${cleanupResult.message}`, {
|
|
409
|
+
cause: submoduleValidation,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return submoduleValidation;
|
|
413
|
+
}
|
|
277
414
|
// Dependency install (non-fatal)
|
|
278
415
|
onProgress?.('Installing dependencies...');
|
|
279
416
|
const installResult = await runDependencyInstall(targetDirectory, log);
|
|
@@ -291,6 +428,34 @@ export async function removeWorktreeCore({ projectDirectory, worktreeDirectory,
|
|
|
291
428
|
if (removeResult instanceof Error)
|
|
292
429
|
return removeResult;
|
|
293
430
|
if (branchName) {
|
|
294
|
-
await execAsync(`git branch
|
|
431
|
+
const listResult = await execAsync(`git branch --list --format=${JSON.stringify('%(refname:short)')} ${JSON.stringify(branchName)}`, { cwd: projectDirectory, timeout: 10_000 }).catch((e) => new Error(`git branch list failed: ${formatCommandError(e)}`, { cause: e }));
|
|
432
|
+
if (listResult instanceof Error)
|
|
433
|
+
return listResult;
|
|
434
|
+
if (listResult.stdout.trim() === branchName) {
|
|
435
|
+
const deleteResult = await execAsync(`git branch -D ${JSON.stringify(branchName)}`, { cwd: projectDirectory, timeout: 10_000 }).catch((e) => new Error(`git branch delete failed: ${formatCommandError(e)}`, { cause: e }));
|
|
436
|
+
if (deleteResult instanceof Error)
|
|
437
|
+
return deleteResult;
|
|
438
|
+
}
|
|
295
439
|
}
|
|
296
440
|
}
|
|
441
|
+
export async function removeWorktreeFromOwnRepository({ worktreeDirectory, branchName, }) {
|
|
442
|
+
if (!fs.existsSync(worktreeDirectory))
|
|
443
|
+
return;
|
|
444
|
+
const listResult = await execAsync('git worktree list --porcelain', {
|
|
445
|
+
cwd: worktreeDirectory,
|
|
446
|
+
timeout: 10_000,
|
|
447
|
+
}).catch((e) => new Error('Failed to locate the worktree repository', { cause: e }));
|
|
448
|
+
if (listResult instanceof Error)
|
|
449
|
+
return listResult;
|
|
450
|
+
const mainWorktreeLine = listResult.stdout
|
|
451
|
+
.split('\n')
|
|
452
|
+
.find((line) => line.startsWith('worktree '));
|
|
453
|
+
if (!mainWorktreeLine) {
|
|
454
|
+
return new Error('Git returned no main worktree during cleanup');
|
|
455
|
+
}
|
|
456
|
+
return removeWorktreeCore({
|
|
457
|
+
projectDirectory: mainWorktreeLine.slice('worktree '.length),
|
|
458
|
+
worktreeDirectory,
|
|
459
|
+
branchName,
|
|
460
|
+
});
|
|
461
|
+
}
|
package/dist/ipc-tools-plugin.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// OpenCode plugin that provides
|
|
1
|
+
// OpenCode plugin that provides Discord and session tools:
|
|
2
2
|
// - kimaki_file_upload: prompts the Discord user to upload files via native picker
|
|
3
3
|
// - kimaki_action_buttons: shows clickable action buttons in the Discord thread
|
|
4
|
+
// - kimaki_sleep: persist a wake time so the session can sleep hours or days
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
// runs inside the OpenCode server process, not the bot
|
|
6
|
+
// IPC tools talk to the bot process via SQLite rows. Sleep writes session_sleeps
|
|
7
|
+
// directly. The plugin runs inside the OpenCode server process, not the bot.
|
|
7
8
|
//
|
|
8
9
|
// Exported from kimaki-opencode-plugin.ts — each export is treated as a separate
|
|
9
10
|
// plugin by OpenCode's plugin loader.
|
|
@@ -180,6 +181,65 @@ const ipcToolsPlugin = async () => {
|
|
|
180
181
|
return 'Action button request timed out';
|
|
181
182
|
},
|
|
182
183
|
}),
|
|
184
|
+
kimaki_sleep: tool({
|
|
185
|
+
description: dedent `
|
|
186
|
+
Sleep this session until a future time, then wake with a new message.
|
|
187
|
+
Use this to wait hours or days for CI, a deploy, a date, or any later event.
|
|
188
|
+
The sleep is stored in SQLite and survives bot restarts.
|
|
189
|
+
|
|
190
|
+
Pass either duration (2h, 30m, 1d) or until (UTC ISO ending with Z).
|
|
191
|
+
Do not pass both. You MUST call kimaki_sleep LAST, after ALL text.
|
|
192
|
+
Do not call more tools after it. A new user message cancels the sleep.
|
|
193
|
+
`,
|
|
194
|
+
args: {
|
|
195
|
+
duration: z
|
|
196
|
+
.string()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe('Relative wait. Examples: 30s, 2h, 1d. Do not pass with until.'),
|
|
199
|
+
until: z
|
|
200
|
+
.string()
|
|
201
|
+
.optional()
|
|
202
|
+
.describe('UTC ISO date ending with Z. Example: 2026-08-20T09:00:00Z.'),
|
|
203
|
+
reason: z
|
|
204
|
+
.string()
|
|
205
|
+
.optional()
|
|
206
|
+
.describe('Why the session is sleeping. Shown when it wakes.'),
|
|
207
|
+
},
|
|
208
|
+
async execute({ duration, until, reason }, context) {
|
|
209
|
+
const { parseSleepWakeAt, formatSessionSleepWakeAt } = await import('./task-schedule.js');
|
|
210
|
+
const wakeAt = parseSleepWakeAt({
|
|
211
|
+
duration,
|
|
212
|
+
until,
|
|
213
|
+
now: new Date(),
|
|
214
|
+
});
|
|
215
|
+
if (wakeAt instanceof Error) {
|
|
216
|
+
return wakeAt.message;
|
|
217
|
+
}
|
|
218
|
+
const { getThreadIdBySessionId, upsertSessionSleep } = await loadDatabaseModule();
|
|
219
|
+
// Only a check that a thread owns this session: subagents have none.
|
|
220
|
+
// The wake resolves the thread again at delivery time, so nothing
|
|
221
|
+
// here can go stale if /resume rebinds the session later.
|
|
222
|
+
const threadId = await getThreadIdBySessionId(context.sessionID);
|
|
223
|
+
if (!threadId) {
|
|
224
|
+
return 'sleep is only available in the main session';
|
|
225
|
+
}
|
|
226
|
+
await upsertSessionSleep({
|
|
227
|
+
sessionId: context.sessionID,
|
|
228
|
+
wakeAt,
|
|
229
|
+
reason,
|
|
230
|
+
});
|
|
231
|
+
// The title carries the resolved absolute time so Discord shows the
|
|
232
|
+
// real wake instant instead of a relative input like "2h".
|
|
233
|
+
const untilLabel = formatSessionSleepWakeAt(wakeAt);
|
|
234
|
+
const reasonText = reason?.trim()
|
|
235
|
+
? ` Reason: ${reason.trim()}.`
|
|
236
|
+
: '';
|
|
237
|
+
return {
|
|
238
|
+
title: `until ${untilLabel}`,
|
|
239
|
+
output: `Sleeping until ${untilLabel}.${reasonText} Stop now. You will be woken with a new message.`,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
183
243
|
},
|
|
184
244
|
};
|
|
185
245
|
};
|
|
@@ -1,56 +1,64 @@
|
|
|
1
1
|
// E2e test for OpenCode plugin loading.
|
|
2
|
-
// Spawns `opencode serve`
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Spawns one `opencode serve` with the same plugin list kimaki writes (its own
|
|
3
|
+
// plugin plus @subrouter/opencode), then checks stderr for load errors and
|
|
4
|
+
// hits the real HTTP API. One server for both plugins, because that is how
|
|
5
|
+
// they actually run, and because booting two costs seconds per file.
|
|
6
|
+
// No Discord infrastructure and no real provider APIs.
|
|
5
7
|
import { spawn } from 'node:child_process';
|
|
6
8
|
import fs from 'node:fs';
|
|
7
9
|
import path from 'node:path';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { afterAll, beforeAll, expect, test } from 'vitest';
|
|
13
|
+
import { PROVIDER_IDS } from '@subrouter/cli';
|
|
10
14
|
import { resolveOpencodeCommand } from './opencode.js';
|
|
11
15
|
import { getSpawnCommandAndArgs } from './opencode-command.js';
|
|
12
16
|
import { chooseLockPort } from './test-utils.js';
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' });
|
|
19
|
+
const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e');
|
|
20
|
+
const subrouterHome = path.join(projectDir, 'subrouter-home');
|
|
21
|
+
const stderrLines = [];
|
|
22
|
+
let serverProcess;
|
|
23
|
+
async function api(routePath) {
|
|
24
|
+
const url = new URL(routePath, `http://127.0.0.1:${port}`);
|
|
25
|
+
url.searchParams.set('directory', projectDir);
|
|
26
|
+
const response = await fetch(url, { headers: { 'x-opencode-directory': projectDir } });
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error(`${routePath} returned ${response.status}`);
|
|
29
|
+
return (await response.json());
|
|
30
|
+
}
|
|
31
|
+
async function waitForHealth() {
|
|
32
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
33
|
+
const ok = await fetch(`http://127.0.0.1:${port}/api/health`)
|
|
34
|
+
.then((r) => r.status < 500)
|
|
35
|
+
.catch(() => false);
|
|
36
|
+
if (ok)
|
|
37
|
+
return true;
|
|
25
38
|
await new Promise((resolve) => {
|
|
26
|
-
setTimeout(resolve,
|
|
39
|
+
setTimeout(resolve, 500);
|
|
27
40
|
});
|
|
28
41
|
}
|
|
29
42
|
return false;
|
|
30
43
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' });
|
|
35
|
-
const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href;
|
|
36
|
-
const stderrLines = [];
|
|
37
|
-
const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home');
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
46
|
+
const opencodeRoot = path.join(projectDir, 'opencode-test-home');
|
|
38
47
|
const xdgDirectories = {
|
|
39
|
-
OPENCODE_CONFIG_DIR: path.join(
|
|
40
|
-
XDG_CONFIG_HOME: path.join(
|
|
41
|
-
XDG_DATA_HOME: path.join(
|
|
42
|
-
XDG_CACHE_HOME: path.join(
|
|
43
|
-
XDG_STATE_HOME: path.join(
|
|
48
|
+
OPENCODE_CONFIG_DIR: path.join(opencodeRoot, '.opencode-kimaki'),
|
|
49
|
+
XDG_CONFIG_HOME: path.join(opencodeRoot, '.config'),
|
|
50
|
+
XDG_DATA_HOME: path.join(opencodeRoot, '.local', 'share'),
|
|
51
|
+
XDG_CACHE_HOME: path.join(opencodeRoot, '.cache'),
|
|
52
|
+
XDG_STATE_HOME: path.join(opencodeRoot, '.local', 'state'),
|
|
44
53
|
};
|
|
45
|
-
|
|
46
|
-
Object.values(xdgDirectories).forEach((directory) => {
|
|
54
|
+
[...Object.values(xdgDirectories), subrouterHome].forEach((directory) => {
|
|
47
55
|
fs.mkdirSync(directory, { recursive: true });
|
|
48
56
|
});
|
|
49
|
-
const { command, args, windowsVerbatimArguments
|
|
57
|
+
const { command, args, windowsVerbatimArguments } = getSpawnCommandAndArgs({
|
|
50
58
|
resolvedCommand: resolveOpencodeCommand(),
|
|
51
59
|
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
52
60
|
});
|
|
53
|
-
|
|
61
|
+
serverProcess = spawn(command, args, {
|
|
54
62
|
stdio: 'pipe',
|
|
55
63
|
cwd: projectDir,
|
|
56
64
|
windowsVerbatimArguments,
|
|
@@ -60,35 +68,91 @@ test('opencode server loads plugin without errors', async () => {
|
|
|
60
68
|
$schema: 'https://opencode.ai/config.json',
|
|
61
69
|
lsp: false,
|
|
62
70
|
formatter: false,
|
|
63
|
-
plugin: [
|
|
71
|
+
plugin: [
|
|
72
|
+
new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href,
|
|
73
|
+
pathToFileURL(require.resolve('@subrouter/opencode')).href,
|
|
74
|
+
],
|
|
64
75
|
}),
|
|
65
|
-
OPENCODE_TEST_HOME:
|
|
76
|
+
OPENCODE_TEST_HOME: opencodeRoot,
|
|
77
|
+
SUBROUTER_HOME: subrouterHome,
|
|
78
|
+
SUBROUTER_MANUAL_OAUTH: '1',
|
|
66
79
|
...xdgDirectories,
|
|
67
80
|
},
|
|
68
81
|
});
|
|
69
82
|
serverProcess.stderr?.on('data', (data) => {
|
|
70
83
|
stderrLines.push(...data.toString().split('\n').filter(Boolean));
|
|
71
84
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
expect(await waitForHealth()).toBe(true);
|
|
86
|
+
}, 120_000);
|
|
87
|
+
afterAll(() => {
|
|
88
|
+
serverProcess?.kill('SIGTERM');
|
|
89
|
+
});
|
|
90
|
+
test('server loads both plugins without errors', () => {
|
|
91
|
+
const pluginErrorPatterns = [
|
|
92
|
+
/plugin.*error/i,
|
|
93
|
+
/failed to load plugin/i,
|
|
94
|
+
/cannot find module/i,
|
|
95
|
+
/ERR_MODULE_NOT_FOUND/i,
|
|
96
|
+
/plugin.*failed/i,
|
|
97
|
+
/plugin.*crash/i,
|
|
98
|
+
];
|
|
99
|
+
const errorLines = stderrLines.filter((line) => {
|
|
100
|
+
return pluginErrorPatterns.some((pattern) => {
|
|
101
|
+
return pattern.test(line);
|
|
88
102
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
});
|
|
104
|
+
expect(errorLines).toEqual([]);
|
|
105
|
+
});
|
|
106
|
+
test('subrouter is a connected provider so /model accepts subrouter/default', async () => {
|
|
107
|
+
const providers = await api('/provider');
|
|
108
|
+
expect(providers.connected).toContain('subrouter');
|
|
109
|
+
const subrouter = providers.all.find((entry) => {
|
|
110
|
+
return entry.id === 'subrouter';
|
|
111
|
+
});
|
|
112
|
+
expect(subrouter?.name).toBe('subrouter.org');
|
|
113
|
+
expect(subrouter?.models.default).toMatchObject({
|
|
114
|
+
capabilities: {
|
|
115
|
+
attachment: false,
|
|
116
|
+
input: { image: false, pdf: false },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
test('login offers subrouter and asks which subscription to add first', async () => {
|
|
121
|
+
const methods = await api('/provider/auth');
|
|
122
|
+
expect(methods.subrouter).toHaveLength(1);
|
|
123
|
+
expect(methods.subrouter?.[0]).toMatchObject({ type: 'oauth', label: 'Add a subscription' });
|
|
124
|
+
// Derived from subrouter, not hardcoded: it adds providers over time and a
|
|
125
|
+
// literal list here just breaks on the next one.
|
|
126
|
+
const prompt = methods.subrouter?.[0]?.prompts?.[0];
|
|
127
|
+
expect(prompt).toMatchObject({ type: 'select', key: 'provider' });
|
|
128
|
+
expect(prompt?.options?.map((option) => option.value)).toEqual([...PROVIDER_IDS]);
|
|
129
|
+
});
|
|
130
|
+
test('authorize and callback add an opencode-go subscription', async () => {
|
|
131
|
+
const url = new URL('/provider/subrouter/oauth/authorize', `http://127.0.0.1:${port}`);
|
|
132
|
+
url.searchParams.set('directory', projectDir);
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json', 'x-opencode-directory': projectDir },
|
|
136
|
+
body: JSON.stringify({ method: 0, inputs: { provider: 'opencode-go' } }),
|
|
137
|
+
});
|
|
138
|
+
expect(response.ok).toBe(true);
|
|
139
|
+
const authorization = (await response.json());
|
|
140
|
+
expect(authorization.method).toBe('code');
|
|
141
|
+
expect(authorization.url).toBe('https://opencode.ai/auth');
|
|
142
|
+
expect(authorization.instructions).toContain('API key');
|
|
143
|
+
const callbackUrl = new URL('/provider/subrouter/oauth/callback', `http://127.0.0.1:${port}`);
|
|
144
|
+
callbackUrl.searchParams.set('directory', projectDir);
|
|
145
|
+
const callbackResponse = await fetch(callbackUrl, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
'x-opencode-directory': projectDir,
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({ method: 0, code: 'zen-key-1' }),
|
|
152
|
+
});
|
|
153
|
+
expect(callbackResponse.ok).toBe(true);
|
|
154
|
+
const config = JSON.parse(fs.readFileSync(path.join(subrouterHome, 'config.json'), 'utf8'));
|
|
155
|
+
expect(config.providers['opencode-go']?.accounts).toMatchObject([
|
|
156
|
+
{ type: 'api', key: 'zen-key-1' },
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
@@ -4,22 +4,26 @@
|
|
|
4
4
|
// initializer functions. OpenCode calls every export as a plugin.
|
|
5
5
|
//
|
|
6
6
|
// Plugins are split into focused modules:
|
|
7
|
-
// - ipc-tools-plugin: file upload
|
|
7
|
+
// - ipc-tools-plugin: file upload, action buttons, and session sleep
|
|
8
8
|
// - context-awareness-plugin: branch, pwd, memory reminder, onboarding tutorial
|
|
9
9
|
// - memory-overview-plugin: frozen MEMORY.md heading overview per session
|
|
10
10
|
// - opencode-interrupt-plugin: interrupt queued messages at step boundaries
|
|
11
|
-
// - subagent-rate-limit-plugin: aborts only task subagents after rate limits
|
|
12
11
|
// - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
|
|
12
|
+
// - file-edit-log: record edit/write/apply_patch files per session
|
|
13
13
|
export { ipcToolsPlugin } from './ipc-tools-plugin.js';
|
|
14
14
|
export { contextAwarenessPlugin } from './context-awareness-plugin.js';
|
|
15
15
|
export { memoryOverviewPlugin } from './memory-overview-plugin.js';
|
|
16
16
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
|
|
17
|
+
// LEGACY per-provider account rotation. Superseded by @subrouter/opencode
|
|
18
|
+
// (registered separately in opencode.ts), which also fails over across
|
|
19
|
+
// providers. These stay because they own `anthropic/*` auth, which opencode
|
|
20
|
+
// itself does not provide. See anthropic-auth-plugin.ts for the full note.
|
|
17
21
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
|
|
18
22
|
export { openaiRotationPlugin } from './openai-auth-plugin.js';
|
|
19
23
|
export { xaiRotationPlugin } from './xai-auth-plugin.js';
|
|
20
24
|
export { imageOptimizerPlugin } from './image-optimizer-plugin.js';
|
|
21
|
-
export { subagentRateLimitPlugin } from './subagent-rate-limit-plugin.js';
|
|
22
25
|
export { cacheDriftPlugin } from './cache-drift-plugin.js';
|
|
23
26
|
export { kittyGraphicsPlugin } from 'kitty-graphics-agent';
|
|
24
27
|
export { injectionGuardInternal as injectionGuard } from 'opencode-injection-guard';
|
|
25
28
|
export { kimakiWorkspaceAdaptorPlugin } from './kimaki-workspace-adaptor.js';
|
|
29
|
+
export { fileEditTrackerPlugin } from './file-edit-log.js';
|