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
package/src/ipc-tools-plugin.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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.
|
|
10
11
|
|
|
11
12
|
import type { Plugin } from '@opencode-ai/plugin'
|
|
12
|
-
import type { ToolContext } from '@opencode-ai/plugin/tool'
|
|
13
|
+
import type { ToolContext, ToolResult } from '@opencode-ai/plugin/tool'
|
|
13
14
|
import dedent from 'string-dedent'
|
|
14
15
|
import { z } from 'zod'
|
|
15
16
|
import { setDataDir } from './config.js'
|
|
@@ -33,7 +34,7 @@ function tool<Args extends z.ZodRawShape>(input: {
|
|
|
33
34
|
execute(
|
|
34
35
|
args: z.infer<z.ZodObject<Args>>,
|
|
35
36
|
context: ToolContext,
|
|
36
|
-
): Promise<
|
|
37
|
+
): Promise<ToolResult>
|
|
37
38
|
}) {
|
|
38
39
|
return input
|
|
39
40
|
}
|
|
@@ -222,6 +223,75 @@ const ipcToolsPlugin: any = async () => {
|
|
|
222
223
|
return 'Action button request timed out'
|
|
223
224
|
},
|
|
224
225
|
}),
|
|
226
|
+
kimaki_sleep: tool({
|
|
227
|
+
description: dedent`
|
|
228
|
+
Sleep this session until a future time, then wake with a new message.
|
|
229
|
+
Use this to wait hours or days for CI, a deploy, a date, or any later event.
|
|
230
|
+
The sleep is stored in SQLite and survives bot restarts.
|
|
231
|
+
|
|
232
|
+
Pass either duration (2h, 30m, 1d) or until (UTC ISO ending with Z).
|
|
233
|
+
Do not pass both. You MUST call kimaki_sleep LAST, after ALL text.
|
|
234
|
+
Do not call more tools after it. A new user message cancels the sleep.
|
|
235
|
+
`,
|
|
236
|
+
args: {
|
|
237
|
+
duration: z
|
|
238
|
+
.string()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe(
|
|
241
|
+
'Relative wait. Examples: 30s, 2h, 1d. Do not pass with until.',
|
|
242
|
+
),
|
|
243
|
+
until: z
|
|
244
|
+
.string()
|
|
245
|
+
.optional()
|
|
246
|
+
.describe(
|
|
247
|
+
'UTC ISO date ending with Z. Example: 2026-08-20T09:00:00Z.',
|
|
248
|
+
),
|
|
249
|
+
reason: z
|
|
250
|
+
.string()
|
|
251
|
+
.optional()
|
|
252
|
+
.describe('Why the session is sleeping. Shown when it wakes.'),
|
|
253
|
+
},
|
|
254
|
+
async execute({ duration, until, reason }, context) {
|
|
255
|
+
const { parseSleepWakeAt, formatSessionSleepWakeAt } = await import(
|
|
256
|
+
'./task-schedule.js'
|
|
257
|
+
)
|
|
258
|
+
const wakeAt = parseSleepWakeAt({
|
|
259
|
+
duration,
|
|
260
|
+
until,
|
|
261
|
+
now: new Date(),
|
|
262
|
+
})
|
|
263
|
+
if (wakeAt instanceof Error) {
|
|
264
|
+
return wakeAt.message
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { getThreadIdBySessionId, upsertSessionSleep } =
|
|
268
|
+
await loadDatabaseModule()
|
|
269
|
+
// Only a check that a thread owns this session: subagents have none.
|
|
270
|
+
// The wake resolves the thread again at delivery time, so nothing
|
|
271
|
+
// here can go stale if /resume rebinds the session later.
|
|
272
|
+
const threadId = await getThreadIdBySessionId(context.sessionID)
|
|
273
|
+
if (!threadId) {
|
|
274
|
+
return 'sleep is only available in the main session'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await upsertSessionSleep({
|
|
278
|
+
sessionId: context.sessionID,
|
|
279
|
+
wakeAt,
|
|
280
|
+
reason,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// The title carries the resolved absolute time so Discord shows the
|
|
284
|
+
// real wake instant instead of a relative input like "2h".
|
|
285
|
+
const untilLabel = formatSessionSleepWakeAt(wakeAt)
|
|
286
|
+
const reasonText = reason?.trim()
|
|
287
|
+
? ` Reason: ${reason.trim()}.`
|
|
288
|
+
: ''
|
|
289
|
+
return {
|
|
290
|
+
title: `until ${untilLabel}`,
|
|
291
|
+
output: `Sleeping until ${untilLabel}.${reasonText} Stop now. You will be woken with a new message.`,
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
225
295
|
},
|
|
226
296
|
}
|
|
227
297
|
}
|
|
@@ -1,117 +1,208 @@
|
|
|
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
|
|
|
6
8
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
7
9
|
import fs from 'node:fs'
|
|
8
10
|
import path from 'node:path'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import { createRequire } from 'node:module'
|
|
12
|
+
import { pathToFileURL } from 'node:url'
|
|
13
|
+
import { afterAll, beforeAll, expect, test } from 'vitest'
|
|
14
|
+
import { PROVIDER_IDS, type ConfigFile } from '@subrouter/cli'
|
|
11
15
|
import { resolveOpencodeCommand } from './opencode.js'
|
|
12
16
|
import { getSpawnCommandAndArgs } from './opencode-command.js'
|
|
13
17
|
import { chooseLockPort } from './test-utils.js'
|
|
14
18
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
const require = createRequire(import.meta.url)
|
|
20
|
+
|
|
21
|
+
const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' })
|
|
22
|
+
const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e')
|
|
23
|
+
const subrouterHome = path.join(projectDir, 'subrouter-home')
|
|
24
|
+
const stderrLines: string[] = []
|
|
25
|
+
|
|
26
|
+
let serverProcess: ChildProcess | undefined
|
|
27
|
+
|
|
28
|
+
async function api<T>(routePath: string): Promise<T> {
|
|
29
|
+
const url = new URL(routePath, `http://127.0.0.1:${port}`)
|
|
30
|
+
url.searchParams.set('directory', projectDir)
|
|
31
|
+
const response = await fetch(url, { headers: { 'x-opencode-directory': projectDir } })
|
|
32
|
+
if (!response.ok) throw new Error(`${routePath} returned ${response.status}`)
|
|
33
|
+
return (await response.json()) as T
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function waitForHealth() {
|
|
37
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
38
|
+
const ok = await fetch(`http://127.0.0.1:${port}/api/health`)
|
|
39
|
+
.then((r) => r.status < 500)
|
|
40
|
+
.catch(() => false)
|
|
41
|
+
if (ok) return true
|
|
33
42
|
await new Promise((resolve) => {
|
|
34
|
-
setTimeout(resolve,
|
|
43
|
+
setTimeout(resolve, 500)
|
|
35
44
|
})
|
|
36
45
|
}
|
|
37
46
|
return false
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
|
|
54
|
-
XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
|
|
55
|
-
XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
fs.mkdirSync(isolatedOpencodeRoot, { recursive: true })
|
|
59
|
-
Object.values(xdgDirectories).forEach((directory) => {
|
|
60
|
-
fs.mkdirSync(directory, { recursive: true })
|
|
61
|
-
})
|
|
49
|
+
beforeAll(async () => {
|
|
50
|
+
fs.rmSync(projectDir, { recursive: true, force: true })
|
|
51
|
+
const opencodeRoot = path.join(projectDir, 'opencode-test-home')
|
|
52
|
+
const xdgDirectories = {
|
|
53
|
+
OPENCODE_CONFIG_DIR: path.join(opencodeRoot, '.opencode-kimaki'),
|
|
54
|
+
XDG_CONFIG_HOME: path.join(opencodeRoot, '.config'),
|
|
55
|
+
XDG_DATA_HOME: path.join(opencodeRoot, '.local', 'share'),
|
|
56
|
+
XDG_CACHE_HOME: path.join(opencodeRoot, '.cache'),
|
|
57
|
+
XDG_STATE_HOME: path.join(opencodeRoot, '.local', 'state'),
|
|
58
|
+
}
|
|
59
|
+
;[...Object.values(xdgDirectories), subrouterHome].forEach((directory) => {
|
|
60
|
+
fs.mkdirSync(directory, { recursive: true })
|
|
61
|
+
})
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} = getSpawnCommandAndArgs({
|
|
68
|
-
resolvedCommand: resolveOpencodeCommand(),
|
|
69
|
-
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
70
|
-
})
|
|
63
|
+
const { command, args, windowsVerbatimArguments } = getSpawnCommandAndArgs({
|
|
64
|
+
resolvedCommand: resolveOpencodeCommand(),
|
|
65
|
+
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
66
|
+
})
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
|
|
68
|
+
serverProcess = spawn(command, args, {
|
|
69
|
+
stdio: 'pipe',
|
|
70
|
+
cwd: projectDir,
|
|
71
|
+
windowsVerbatimArguments,
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
75
|
+
$schema: 'https://opencode.ai/config.json',
|
|
76
|
+
lsp: false,
|
|
77
|
+
formatter: false,
|
|
78
|
+
plugin: [
|
|
79
|
+
new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href,
|
|
80
|
+
pathToFileURL(require.resolve('@subrouter/opencode')).href,
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
OPENCODE_TEST_HOME: opencodeRoot,
|
|
84
|
+
SUBROUTER_HOME: subrouterHome,
|
|
85
|
+
SUBROUTER_MANUAL_OAUTH: '1',
|
|
86
|
+
...xdgDirectories,
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
91
|
+
stderrLines.push(...data.toString().split('\n').filter(Boolean))
|
|
92
|
+
})
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
expect(await waitForHealth()).toBe(true)
|
|
95
|
+
}, 120_000)
|
|
96
|
+
|
|
97
|
+
afterAll(() => {
|
|
98
|
+
serverProcess?.kill('SIGTERM')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('server loads both plugins without errors', () => {
|
|
102
|
+
const pluginErrorPatterns = [
|
|
103
|
+
/plugin.*error/i,
|
|
104
|
+
/failed to load plugin/i,
|
|
105
|
+
/cannot find module/i,
|
|
106
|
+
/ERR_MODULE_NOT_FOUND/i,
|
|
107
|
+
/plugin.*failed/i,
|
|
108
|
+
/plugin.*crash/i,
|
|
109
|
+
]
|
|
110
|
+
const errorLines = stderrLines.filter((line) => {
|
|
111
|
+
return pluginErrorPatterns.some((pattern) => {
|
|
112
|
+
return pattern.test(line)
|
|
91
113
|
})
|
|
114
|
+
})
|
|
115
|
+
expect(errorLines).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('subrouter is a connected provider so /model accepts subrouter/default', async () => {
|
|
119
|
+
const providers = await api<{
|
|
120
|
+
all: Array<{
|
|
121
|
+
id: string
|
|
122
|
+
name: string
|
|
123
|
+
models: Record<
|
|
124
|
+
string,
|
|
125
|
+
{
|
|
126
|
+
name: string
|
|
127
|
+
capabilities: {
|
|
128
|
+
attachment: boolean
|
|
129
|
+
input: { image: boolean; pdf: boolean }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
}>
|
|
134
|
+
connected: string[]
|
|
135
|
+
}>('/provider')
|
|
136
|
+
|
|
137
|
+
expect(providers.connected).toContain('subrouter')
|
|
138
|
+
const subrouter = providers.all.find((entry) => {
|
|
139
|
+
return entry.id === 'subrouter'
|
|
140
|
+
})
|
|
141
|
+
expect(subrouter?.name).toBe('subrouter.org')
|
|
142
|
+
expect(subrouter?.models.default).toMatchObject({
|
|
143
|
+
capabilities: {
|
|
144
|
+
attachment: false,
|
|
145
|
+
input: { image: false, pdf: false },
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('login offers subrouter and asks which subscription to add first', async () => {
|
|
151
|
+
const methods = await api<
|
|
152
|
+
Record<
|
|
153
|
+
string,
|
|
154
|
+
Array<{
|
|
155
|
+
type: string
|
|
156
|
+
label: string
|
|
157
|
+
prompts?: Array<{ type: string; key: string; options?: Array<{ value: string }> }>
|
|
158
|
+
}>
|
|
159
|
+
>
|
|
160
|
+
>('/provider/auth')
|
|
161
|
+
|
|
162
|
+
expect(methods.subrouter).toHaveLength(1)
|
|
163
|
+
expect(methods.subrouter?.[0]).toMatchObject({ type: 'oauth', label: 'Add a subscription' })
|
|
164
|
+
|
|
165
|
+
// Derived from subrouter, not hardcoded: it adds providers over time and a
|
|
166
|
+
// literal list here just breaks on the next one.
|
|
167
|
+
const prompt = methods.subrouter?.[0]?.prompts?.[0]
|
|
168
|
+
expect(prompt).toMatchObject({ type: 'select', key: 'provider' })
|
|
169
|
+
expect(prompt?.options?.map((option) => option.value)).toEqual([...PROVIDER_IDS])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('authorize and callback add an opencode-go subscription', async () => {
|
|
173
|
+
const url = new URL('/provider/subrouter/oauth/authorize', `http://127.0.0.1:${port}`)
|
|
174
|
+
url.searchParams.set('directory', projectDir)
|
|
175
|
+
const response = await fetch(url, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json', 'x-opencode-directory': projectDir },
|
|
178
|
+
body: JSON.stringify({ method: 0, inputs: { provider: 'opencode-go' } }),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
expect(response.ok).toBe(true)
|
|
182
|
+
const authorization = (await response.json()) as { url: string; method: string; instructions: string }
|
|
183
|
+
expect(authorization.method).toBe('code')
|
|
184
|
+
expect(authorization.url).toBe('https://opencode.ai/auth')
|
|
185
|
+
expect(authorization.instructions).toContain('API key')
|
|
186
|
+
|
|
187
|
+
const callbackUrl = new URL(
|
|
188
|
+
'/provider/subrouter/oauth/callback',
|
|
189
|
+
`http://127.0.0.1:${port}`,
|
|
190
|
+
)
|
|
191
|
+
callbackUrl.searchParams.set('directory', projectDir)
|
|
192
|
+
const callbackResponse = await fetch(callbackUrl, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
'x-opencode-directory': projectDir,
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify({ method: 0, code: 'zen-key-1' }),
|
|
199
|
+
})
|
|
92
200
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
/cannot find module/i,
|
|
102
|
-
/ERR_MODULE_NOT_FOUND/i,
|
|
103
|
-
/plugin.*failed/i,
|
|
104
|
-
/plugin.*crash/i,
|
|
105
|
-
]
|
|
106
|
-
const errorLines = stderrLines.filter((line) => {
|
|
107
|
-
return pluginErrorPatterns.some((pattern) => {
|
|
108
|
-
return pattern.test(line)
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
expect(errorLines).toEqual([])
|
|
112
|
-
} finally {
|
|
113
|
-
serverProcess.kill('SIGTERM')
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
60_000,
|
|
117
|
-
)
|
|
201
|
+
expect(callbackResponse.ok).toBe(true)
|
|
202
|
+
const config = JSON.parse(
|
|
203
|
+
fs.readFileSync(path.join(subrouterHome, 'config.json'), 'utf8'),
|
|
204
|
+
) as ConfigFile
|
|
205
|
+
expect(config.providers['opencode-go']?.accounts).toMatchObject([
|
|
206
|
+
{ type: 'api', key: 'zen-key-1' },
|
|
207
|
+
])
|
|
208
|
+
})
|
|
@@ -4,23 +4,27 @@
|
|
|
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
|
|
|
14
14
|
export { ipcToolsPlugin } from './ipc-tools-plugin.js'
|
|
15
15
|
export { contextAwarenessPlugin } from './context-awareness-plugin.js'
|
|
16
16
|
export { memoryOverviewPlugin } from './memory-overview-plugin.js'
|
|
17
17
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js'
|
|
18
|
+
// LEGACY per-provider account rotation. Superseded by @subrouter/opencode
|
|
19
|
+
// (registered separately in opencode.ts), which also fails over across
|
|
20
|
+
// providers. These stay because they own `anthropic/*` auth, which opencode
|
|
21
|
+
// itself does not provide. See anthropic-auth-plugin.ts for the full note.
|
|
18
22
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js'
|
|
19
23
|
export { openaiRotationPlugin } from './openai-auth-plugin.js'
|
|
20
24
|
export { xaiRotationPlugin } from './xai-auth-plugin.js'
|
|
21
25
|
export { imageOptimizerPlugin } from './image-optimizer-plugin.js'
|
|
22
|
-
export { subagentRateLimitPlugin } from './subagent-rate-limit-plugin.js'
|
|
23
26
|
export { cacheDriftPlugin } from './cache-drift-plugin.js'
|
|
24
27
|
export { kittyGraphicsPlugin } from 'kitty-graphics-agent'
|
|
25
28
|
export { injectionGuardInternal as injectionGuard } from 'opencode-injection-guard'
|
|
26
29
|
export { kimakiWorkspaceAdaptorPlugin } from './kimaki-workspace-adaptor.js'
|
|
30
|
+
export { fileEditTrackerPlugin } from './file-edit-log.js'
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
import type { Plugin, WorkspaceAdapter, WorkspaceInfo } from '@opencode-ai/plugin'
|
|
10
10
|
import crypto from 'node:crypto'
|
|
11
11
|
import path from 'node:path'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createWorktreeCore,
|
|
14
|
+
KIMAKI_WORKTREE_ADAPTER_TYPE,
|
|
15
|
+
removeWorktreeFromOwnRepository,
|
|
16
|
+
} from './git-worktree-core.js'
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Compute the on-disk directory for a managed worktree.
|
|
@@ -39,15 +43,41 @@ function computeWorktreeDirectory({
|
|
|
39
43
|
return path.join(dataDir, 'worktrees', projectHash, withoutPrefix)
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
function
|
|
46
|
+
function getWorktreeIdentity(info: WorkspaceInfo) {
|
|
47
|
+
if (!info.extra || typeof info.extra !== 'object') {
|
|
48
|
+
return new Error('Kimaki worktree identity is missing')
|
|
49
|
+
}
|
|
50
|
+
const identity: { projectDirectory?: string; baseCommit?: string } = {}
|
|
51
|
+
Object.assign(identity, info.extra)
|
|
52
|
+
if (
|
|
53
|
+
typeof identity.projectDirectory !== 'string' ||
|
|
54
|
+
!path.isAbsolute(identity.projectDirectory)
|
|
55
|
+
) {
|
|
56
|
+
return new Error('Kimaki worktree project directory must be absolute')
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
typeof identity.baseCommit !== 'string' ||
|
|
60
|
+
!/^[0-9a-f]{40}$/i.test(identity.baseCommit)
|
|
61
|
+
) {
|
|
62
|
+
return new Error('Kimaki worktree base commit must be a full commit SHA')
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
projectDirectory: identity.projectDirectory,
|
|
66
|
+
baseCommit: identity.baseCommit,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createKimakiWorktreeAdaptor(): WorkspaceAdapter {
|
|
43
71
|
return {
|
|
44
72
|
name: 'Kimaki Worktree',
|
|
45
73
|
description: 'Create a git worktree managed by Kimaki',
|
|
46
74
|
|
|
47
75
|
configure(info: WorkspaceInfo): WorkspaceInfo {
|
|
76
|
+
const identity = getWorktreeIdentity(info)
|
|
77
|
+
if (identity instanceof Error) throw identity
|
|
48
78
|
const branchName = info.branch || info.name
|
|
49
79
|
const directory = computeWorktreeDirectory({
|
|
50
|
-
projectDirectory,
|
|
80
|
+
projectDirectory: identity.projectDirectory,
|
|
51
81
|
branchName,
|
|
52
82
|
})
|
|
53
83
|
if (directory instanceof Error) {
|
|
@@ -65,12 +95,13 @@ function createKimakiWorktreeAdaptor(projectDirectory: string): WorkspaceAdapter
|
|
|
65
95
|
if (!info.directory) {
|
|
66
96
|
throw new Error('Workspace directory not set — configure() likely failed')
|
|
67
97
|
}
|
|
68
|
-
const
|
|
98
|
+
const identity = getWorktreeIdentity(info)
|
|
99
|
+
if (identity instanceof Error) throw identity
|
|
69
100
|
const result = await createWorktreeCore({
|
|
70
|
-
projectDirectory,
|
|
101
|
+
projectDirectory: identity.projectDirectory,
|
|
71
102
|
targetDirectory: info.directory,
|
|
72
103
|
branchName: info.branch || info.name,
|
|
73
|
-
|
|
104
|
+
baseCommit: identity.baseCommit,
|
|
74
105
|
// Silent log — plugin must not write to stdout/stderr
|
|
75
106
|
})
|
|
76
107
|
if (result instanceof Error) {
|
|
@@ -80,8 +111,7 @@ function createKimakiWorktreeAdaptor(projectDirectory: string): WorkspaceAdapter
|
|
|
80
111
|
|
|
81
112
|
async remove(info: WorkspaceInfo): Promise<void> {
|
|
82
113
|
if (!info.directory) return
|
|
83
|
-
const result = await
|
|
84
|
-
projectDirectory,
|
|
114
|
+
const result = await removeWorktreeFromOwnRepository({
|
|
85
115
|
worktreeDirectory: info.directory,
|
|
86
116
|
branchName: info.branch || '',
|
|
87
117
|
})
|
|
@@ -104,12 +134,11 @@ function createKimakiWorktreeAdaptor(projectDirectory: string): WorkspaceAdapter
|
|
|
104
134
|
* Called by OpenCode's plugin loader.
|
|
105
135
|
*/
|
|
106
136
|
export const kimakiWorkspaceAdaptorPlugin: Plugin = async ({
|
|
107
|
-
directory,
|
|
108
137
|
experimental_workspace,
|
|
109
138
|
}) => {
|
|
110
139
|
experimental_workspace.register(
|
|
111
|
-
|
|
112
|
-
createKimakiWorktreeAdaptor(
|
|
140
|
+
KIMAKI_WORKTREE_ADAPTER_TYPE,
|
|
141
|
+
createKimakiWorktreeAdaptor(),
|
|
113
142
|
)
|
|
114
143
|
return {}
|
|
115
144
|
}
|
package/src/logger.ts
CHANGED
|
@@ -113,10 +113,19 @@ function formatArg(arg: LogArg): string {
|
|
|
113
113
|
|
|
114
114
|
export function formatErrorWithStack<T>(error: T): string {
|
|
115
115
|
if (error instanceof Error) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
116
|
+
const seen = new Set<Error>()
|
|
117
|
+
const parts: string[] = []
|
|
118
|
+
let current: unknown = error
|
|
119
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
120
|
+
seen.add(current)
|
|
121
|
+
const formatted = sanitizeSensitiveText(
|
|
122
|
+
current.stack ?? `${current.name}: ${current.message}`,
|
|
123
|
+
{ redactPaths: false },
|
|
124
|
+
)
|
|
125
|
+
parts.push(parts.length === 0 ? formatted : `Caused by: ${formatted}`)
|
|
126
|
+
current = current.cause
|
|
127
|
+
}
|
|
128
|
+
return parts.join('\n')
|
|
120
129
|
}
|
|
121
130
|
if (typeof error === 'string') {
|
|
122
131
|
return sanitizeSensitiveText(error, { redactPaths: false })
|
package/src/markdown.test.ts
CHANGED
|
@@ -133,6 +133,10 @@ beforeAll(async () => {
|
|
|
133
133
|
await client.session.promptAsync({
|
|
134
134
|
sessionID,
|
|
135
135
|
directory: directories.projectDirectory,
|
|
136
|
+
model: {
|
|
137
|
+
providerID: 'deterministic-provider',
|
|
138
|
+
modelID: 'deterministic-v2',
|
|
139
|
+
},
|
|
136
140
|
parts: [{ type: 'text', text: 'hello markdown test' }],
|
|
137
141
|
})
|
|
138
142
|
|
|
@@ -173,6 +177,10 @@ beforeAll(async () => {
|
|
|
173
177
|
await client.session.promptAsync({
|
|
174
178
|
sessionID: toolSessionID,
|
|
175
179
|
directory: directories.projectDirectory,
|
|
180
|
+
model: {
|
|
181
|
+
providerID: 'deterministic-provider',
|
|
182
|
+
modelID: 'deterministic-v2',
|
|
183
|
+
},
|
|
176
184
|
parts: [{ type: 'text', text: 'use a tool please' }],
|
|
177
185
|
})
|
|
178
186
|
|
|
@@ -243,7 +251,9 @@ test('generate markdown with system info', async () => {
|
|
|
243
251
|
expect(markdown).toContain('## Conversation')
|
|
244
252
|
expect(markdown).toContain('### 👤 User')
|
|
245
253
|
expect(markdown).toContain('hello markdown test')
|
|
246
|
-
expect(markdown).toContain(
|
|
254
|
+
expect(markdown).toContain(
|
|
255
|
+
'### 🤖 Assistant (deterministic-provider/deterministic-v2)',
|
|
256
|
+
)
|
|
247
257
|
expect(markdown).toContain('Hello! This is a deterministic markdown test response.')
|
|
248
258
|
expect(markdown).toContain('**Started using deterministic-provider/deterministic-v2**')
|
|
249
259
|
|
|
@@ -264,7 +274,7 @@ test('generate markdown with system info', async () => {
|
|
|
264
274
|
hello markdown test
|
|
265
275
|
|
|
266
276
|
|
|
267
|
-
### 🤖 Assistant (deterministic-v2)
|
|
277
|
+
### 🤖 Assistant (deterministic-provider/deterministic-v2)
|
|
268
278
|
|
|
269
279
|
**Started using deterministic-provider/deterministic-v2**
|
|
270
280
|
|
|
@@ -301,7 +311,7 @@ test('generate markdown without system info', async () => {
|
|
|
301
311
|
hello markdown test
|
|
302
312
|
|
|
303
313
|
|
|
304
|
-
### 🤖 Assistant (deterministic-v2)
|
|
314
|
+
### 🤖 Assistant (deterministic-provider/deterministic-v2)
|
|
305
315
|
|
|
306
316
|
**Started using deterministic-provider/deterministic-v2**
|
|
307
317
|
|
package/src/markdown.ts
CHANGED
|
@@ -129,7 +129,10 @@ export class ShareMarkdown {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
} else if (message.role === 'assistant') {
|
|
132
|
-
|
|
132
|
+
const modelId =
|
|
133
|
+
[message.providerID, message.modelID].filter(Boolean).join('/') ||
|
|
134
|
+
'unknown model'
|
|
135
|
+
lines.push(`### 🤖 Assistant (${modelId})`)
|
|
133
136
|
lines.push('')
|
|
134
137
|
|
|
135
138
|
// Filter and process parts
|