kimaki 1.4.0 → 1.6.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/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +92 -0
- package/dist/agent-ui.e2e.test.js +27 -13
- package/dist/agent-ui.e2e.test.js.map +1 -1
- package/dist/agent-ui.js +2 -1
- package/dist/agent-ui.js.map +1 -1
- package/dist/analytics.e2e.test.js +1 -1
- package/dist/analytics.e2e.test.js.map +1 -1
- package/dist/autostart.d.ts +4 -0
- package/dist/autostart.js +55 -10
- package/dist/autostart.js.map +1 -1
- package/dist/bot.d.ts +10 -1
- package/dist/bot.js +14 -6
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +1 -1
- package/dist/builtin-skills.js +1 -1
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +3 -4
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.js +25 -14
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/discord.js +32 -6
- package/dist/cli/discord.js.map +1 -1
- package/dist/cli/schedule.js +23 -9
- package/dist/cli/schedule.js.map +1 -1
- package/dist/cli/session.js +37 -12
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +45 -10
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli-discord.e2e.test.js +13 -8
- package/dist/cli-discord.e2e.test.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +87 -20
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/cli-read.e2e.test.js +18 -9
- package/dist/cli-read.e2e.test.js.map +1 -1
- package/dist/cli-sessions.e2e.test.js +4 -3
- package/dist/cli-sessions.e2e.test.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/preference-commands.d.ts +4 -0
- package/dist/commands/preference-commands.js +38 -4
- package/dist/commands/preference-commands.js.map +1 -1
- package/dist/commands/session-commands.js +11 -8
- package/dist/commands/session-commands.js.map +1 -1
- package/dist/commands/thread-commands.js +94 -24
- package/dist/commands/thread-commands.js.map +1 -1
- package/dist/commands/worktree-commands.d.ts +4 -5
- package/dist/commands/worktree-commands.js +8 -7
- package/dist/commands/worktree-commands.js.map +1 -1
- package/dist/commands.e2e.test.js +25 -21
- package/dist/commands.e2e.test.js.map +1 -1
- package/dist/coordinator-agents.e2e.test.js +135 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.js +100 -0
- package/dist/coordinator-idle.e2e.test.js.map +1 -0
- package/dist/coordinator-queue.e2e.test.js +244 -0
- package/dist/coordinator-queue.e2e.test.js.map +1 -0
- package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
- package/dist/coordinator-warmup.e2e.test.js +131 -0
- package/dist/coordinator-warmup.e2e.test.js.map +1 -0
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.e2e.test.d.ts +1 -0
- package/dist/coordinator.e2e.test.js +505 -0
- package/dist/coordinator.e2e.test.js.map +1 -0
- package/dist/coordinator.js +164 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db.d.ts +75 -0
- package/dist/db.test.js +1 -0
- package/dist/db.test.js.map +1 -1
- package/dist/diagram.d.ts +11 -0
- package/dist/diagram.js +232 -0
- package/dist/diagram.js.map +1 -0
- package/dist/diagram.test.d.ts +1 -0
- package/dist/diagram.test.js +52 -0
- package/dist/diagram.test.js.map +1 -0
- package/dist/duration.js +13 -5
- package/dist/duration.js.map +1 -1
- package/dist/effects.d.ts +6 -1
- package/dist/effects.js +97 -26
- package/dist/effects.js.map +1 -1
- package/dist/event-loop.d.ts +1 -1
- package/dist/event-loop.js +73 -43
- package/dist/event-loop.js.map +1 -1
- package/dist/format-parts.d.ts +21 -4
- package/dist/format-parts.js +40 -23
- package/dist/format-parts.js.map +1 -1
- package/dist/gateway-replay.e2e.test.js +4 -4
- package/dist/harness.e2e.test.js +2 -2
- package/dist/harness.e2e.test.js.map +1 -1
- package/dist/hello-thread.e2e.test.js +56 -5
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +68 -13
- package/dist/ingress.js.map +1 -1
- package/dist/interaction-context.d.ts +4 -4
- package/dist/interaction-context.js +9 -7
- package/dist/interaction-context.js.map +1 -1
- package/dist/interrupt.e2e.test.js +8 -5
- package/dist/interrupt.e2e.test.js.map +1 -1
- package/dist/legacy.e2e.test.js +1 -1
- package/dist/lock-routes.d.ts +68 -16
- package/dist/lock-routes.js +68 -8
- package/dist/lock-routes.js.map +1 -1
- package/dist/lock-server.d.ts +3 -2
- package/dist/lock-server.js +23 -7
- package/dist/lock-server.js.map +1 -1
- package/dist/main.d.ts +4 -2
- package/dist/main.js +13 -3
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +4 -4
- package/dist/markdown/render-markdown.js +6 -5
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/onboarding-cli.e2e.test.js +7 -6
- package/dist/onboarding-cli.e2e.test.js.map +1 -1
- package/dist/onboarding.d.ts +3 -10
- package/dist/onboarding.e2e.test.js +8 -10
- package/dist/onboarding.e2e.test.js.map +1 -1
- package/dist/onboarding.js +14 -24
- package/dist/onboarding.js.map +1 -1
- package/dist/permissions.e2e.test.js +3 -3
- package/dist/plugin/coordinator.d.ts +4 -0
- package/dist/plugin/coordinator.js +389 -0
- package/dist/plugin/coordinator.js.map +1 -0
- package/dist/plugin/index.js +76 -17
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin.e2e.test.js +1 -1
- package/dist/preference-commands.e2e.test.js +103 -24
- package/dist/preference-commands.e2e.test.js.map +1 -1
- package/dist/prompt.d.ts +21 -8
- package/dist/prompt.js +64 -48
- package/dist/prompt.js.map +1 -1
- package/dist/questions.e2e.test.js +13 -13
- package/dist/queue-sources.e2e.test.d.ts +1 -0
- package/dist/queue-sources.e2e.test.js +168 -0
- package/dist/queue-sources.e2e.test.js.map +1 -0
- package/dist/queue.d.ts +9 -1
- package/dist/queue.e2e.test.js +48 -30
- package/dist/queue.e2e.test.js.map +1 -1
- package/dist/queue.js +97 -24
- package/dist/queue.js.map +1 -1
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +57 -0
- package/dist/queue.test.js.map +1 -0
- package/dist/remote-send.e2e.test.js +6 -6
- package/dist/remote-send.js +2 -1
- package/dist/remote-send.js.map +1 -1
- package/dist/renderer.e2e.test.js +7 -7
- package/dist/restart.e2e.test.js +2 -2
- package/dist/routines.e2e.test.js +2 -2
- package/dist/scheduler.d.ts +1 -1
- package/dist/scheduler.e2e.test.js +43 -20
- package/dist/scheduler.e2e.test.js.map +1 -1
- package/dist/scheduler.js +4 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/dist/schema-sql.js.map +1 -1
- package/dist/schema.d.ts +146 -0
- package/dist/schema.js +12 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-commands.e2e.test.js +36 -11
- package/dist/session-commands.e2e.test.js.map +1 -1
- package/dist/session-events.d.ts +19 -7
- package/dist/session-events.js +166 -22
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +88 -11
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +41 -18
- package/dist/sessions.js +174 -63
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +12 -6
- package/dist/slash-commands.js.map +1 -1
- package/dist/slash-commands.test.js +2 -0
- package/dist/slash-commands.test.js.map +1 -1
- package/dist/sleep.e2e.test.js +29 -14
- package/dist/sleep.e2e.test.js.map +1 -1
- package/dist/sleeps.d.ts +1 -0
- package/dist/sleeps.js +21 -3
- package/dist/sleeps.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/system-prompt.d.ts +3 -1
- package/dist/system-prompt.js +138 -64
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.js +15 -0
- package/dist/test/generate-log-plugin/index.js.map +1 -0
- package/dist/test/harness.d.ts +20 -1
- package/dist/test/harness.js +51 -10
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -7
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -3
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +112 -25
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +197 -24
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/voice-attachments.e2e.test.js +108 -18
- package/dist/voice-attachments.e2e.test.js.map +1 -1
- package/dist/voice-call.e2e.test.js +9 -9
- package/dist/voice-call.js +6 -6
- package/dist/voice-call.js.map +1 -1
- package/dist/voice-openai.e2e.test.js +59 -23
- package/dist/voice-openai.e2e.test.js.map +1 -1
- package/dist/voice.js +80 -24
- package/dist/voice.js.map +1 -1
- package/dist/working-directory.e2e.test.js +11 -11
- package/dist/working-directory.e2e.test.js.map +1 -1
- package/package.json +12 -6
- package/src/agent-ui.e2e.test.ts +27 -13
- package/src/agent-ui.ts +2 -1
- package/src/analytics.e2e.test.ts +1 -1
- package/src/autostart.ts +55 -10
- package/src/bot.ts +29 -5
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +3 -4
- package/src/builtin-skills.ts +1 -1
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +23 -14
- package/src/cli/discord.ts +30 -6
- package/src/cli/schedule.ts +23 -9
- package/src/cli/session.ts +35 -12
- package/src/cli/shared.ts +43 -8
- package/src/cli-discord.e2e.test.ts +13 -8
- package/src/cli-lock.e2e.test.ts +88 -20
- package/src/cli-read.e2e.test.ts +17 -8
- package/src/cli-sessions.e2e.test.ts +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/preference-commands.ts +38 -4
- package/src/commands/session-commands.ts +12 -7
- package/src/commands/thread-commands.ts +92 -24
- package/src/commands/worktree-commands.ts +10 -9
- package/src/commands.e2e.test.ts +25 -21
- package/src/coordinator-agents.e2e.test.ts +168 -0
- package/src/coordinator-idle.e2e.test.ts +122 -0
- package/src/coordinator-queue.e2e.test.ts +279 -0
- package/src/coordinator-warmup.e2e.test.ts +158 -0
- package/src/coordinator.e2e.test.ts +545 -0
- package/src/coordinator.ts +168 -0
- package/src/db.test.ts +1 -0
- package/src/diagram.test.ts +56 -0
- package/src/diagram.ts +243 -0
- package/src/duration.ts +13 -5
- package/src/effects.ts +99 -27
- package/src/event-loop.ts +71 -42
- package/src/format-parts.ts +53 -22
- package/src/gateway-replay.e2e.test.ts +4 -4
- package/src/harness.e2e.test.ts +2 -2
- package/src/hello-thread.e2e.test.ts +59 -5
- package/src/ingress.ts +62 -12
- package/src/interaction-context.ts +12 -11
- package/src/interrupt.e2e.test.ts +8 -5
- package/src/legacy.e2e.test.ts +1 -1
- package/src/lock-routes.ts +62 -8
- package/src/lock-server.ts +23 -7
- package/src/main.ts +22 -7
- package/src/markdown/markdown.test.ts +4 -4
- package/src/markdown/render-markdown.ts +6 -5
- package/src/onboarding-cli.e2e.test.ts +7 -6
- package/src/onboarding.e2e.test.ts +8 -10
- package/src/onboarding.ts +13 -29
- package/src/permissions.e2e.test.ts +3 -3
- package/src/plugin/coordinator.ts +401 -0
- package/src/plugin/index.ts +68 -15
- package/src/plugin.e2e.test.ts +1 -1
- package/src/preference-commands.e2e.test.ts +103 -23
- package/src/prompt.ts +87 -43
- package/src/questions.e2e.test.ts +13 -13
- package/src/queue-sources.e2e.test.ts +197 -0
- package/src/queue.e2e.test.ts +48 -30
- package/src/queue.test.ts +59 -0
- package/src/queue.ts +104 -24
- package/src/remote-send.e2e.test.ts +6 -6
- package/src/remote-send.ts +2 -1
- package/src/renderer.e2e.test.ts +7 -7
- package/src/restart.e2e.test.ts +2 -2
- package/src/routines.e2e.test.ts +2 -2
- package/src/scheduler.e2e.test.ts +45 -19
- package/src/scheduler.ts +4 -2
- package/src/schema-sql.ts +1 -1
- package/src/schema.ts +13 -0
- package/src/session-commands.e2e.test.ts +38 -11
- package/src/session-events.ts +160 -21
- package/src/session-ops.e2e.test.ts +90 -10
- package/src/sessions.ts +214 -64
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +12 -6
- package/src/sleep.e2e.test.ts +30 -13
- package/src/sleeps.ts +20 -3
- package/src/store.ts +5 -2
- package/src/system-prompt.ts +143 -63
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -14
- package/src/test/replay.ts +12 -6
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +207 -26
- package/src/thread-reducer.ts +138 -37
- package/src/voice-attachments.e2e.test.ts +110 -18
- package/src/voice-call.e2e.test.ts +9 -9
- package/src/voice-call.ts +6 -6
- package/src/voice-openai.e2e.test.ts +64 -24
- package/src/voice.ts +101 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/dist/message-tool.e2e.test.js +0 -39
- package/dist/message-tool.e2e.test.js.map +0 -1
- package/dist/plugin/read-media.d.ts +0 -23
- package/dist/plugin/read-media.js +0 -72
- package/dist/plugin/read-media.js.map +0 -1
- package/dist/plugin/session-id/index.js +0 -18
- package/dist/plugin/session-id/index.js.map +0 -1
- package/dist/probe-live.tmp.js +0 -82
- package/dist/probe-live.tmp.js.map +0 -1
- package/dist/tasks.e2e.test.js +0 -173
- package/dist/tasks.e2e.test.js.map +0 -1
- /package/dist/{message-tool.e2e.test.d.ts → coordinator-agents.e2e.test.d.ts} +0 -0
- /package/dist/{probe-live.tmp.d.ts → coordinator-idle.e2e.test.d.ts} +0 -0
- /package/dist/{tasks.e2e.test.d.ts → coordinator-queue.e2e.test.d.ts} +0 -0
- /package/dist/{plugin/session-id → test/generate-log-plugin}/index.d.ts +0 -0
package/src/cli/bot.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
|
|
|
10
10
|
import dedent from 'string-dedent'
|
|
11
11
|
import { wrapJsonSchema, type Goke } from 'goke'
|
|
12
12
|
|
|
13
|
-
import { distCliEntry, writeAutostartScript } from '../autostart.ts'
|
|
13
|
+
import { distCliEntry, removeAutostartScript, stableNodePath, writeAutostartScript } from '../autostart.ts'
|
|
14
14
|
import { parseDisabledPromptSections, parseDisabledSkills, PROMPT_SECTIONS } from '../builtin-skills.ts'
|
|
15
15
|
import { callBot, DEFAULT_LOCK_PORT, RESTART_EXIT_CODE, startLockServer } from '../lock-server.ts'
|
|
16
16
|
import { parseDuration } from '../duration.ts'
|
|
@@ -59,8 +59,10 @@ async function supervise(): Promise<never> {
|
|
|
59
59
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
60
60
|
process.on(signal, () => state.child?.kill('SIGTERM'))
|
|
61
61
|
}
|
|
62
|
+
// Resolved before any Node upgrade: `kimaki restart` must not spawn the deleted binary.
|
|
63
|
+
const node = stableNodePath()
|
|
62
64
|
while (true) {
|
|
63
|
-
const child = spawn(
|
|
65
|
+
const child = spawn(node, [...process.execArgv, ...process.argv.slice(1)], {
|
|
64
66
|
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
65
67
|
env: { ...process.env, KIMAKI_SUPERVISED: '1' },
|
|
66
68
|
})
|
|
@@ -93,6 +95,8 @@ export function registerStartCommand(cli: Goke) {
|
|
|
93
95
|
.option('--disable-skills <list>', 'Built-in Kimaki skills to turn off: "all" or a comma separated list (kimaki-debug,kimaki-tunnel)')
|
|
94
96
|
.option('--disable-prompt <section>', wrapJsonSchema<string[]>({ type: 'array', items: { type: 'string' }, description: `System prompt section to remove from new sessions (repeatable): ${PROMPT_SECTIONS.join(', ')}` }))
|
|
95
97
|
.option('--no-analytics', 'Disable anonymous usage analytics (same as KIMAKI_STRADA_ENABLED=0)')
|
|
98
|
+
.option('--coordinator [enabled]', wrapJsonSchema<boolean>({ type: 'boolean', default: true, description: 'Channel messages go to one long coordinator session that starts threads for the work. "false" turns it off (/toggle-coordinator sets one channel)' }))
|
|
99
|
+
.option('--no-autostart', 'Do not open a terminal with the bot when OpenCode starts (for bots run by launchd, systemd or pm2)')
|
|
96
100
|
.option('--if-not-running', 'Start only if no bot runs; never stop the running one (used by autostart)')
|
|
97
101
|
.action(async (options) => {
|
|
98
102
|
// The IPC check ignores a KIMAKI_SUPERVISED leaked into a shell by an older bot.
|
|
@@ -103,7 +107,7 @@ export function registerStartCommand(cli: Goke) {
|
|
|
103
107
|
// Bot code loads only here: the other subcommands start without it.
|
|
104
108
|
const [
|
|
105
109
|
{ startBot },
|
|
106
|
-
{ ensureOpencode,
|
|
110
|
+
{ ensureOpencode, runOnboarding, startCaffeinate },
|
|
107
111
|
{ emitEvent, gatewayCredentials, gatewayUrlsFromEnv, installUrlFor, readSavedCredentials, resolveCredentials, restApiUrl },
|
|
108
112
|
{ openDb },
|
|
109
113
|
{ createAnalytics },
|
|
@@ -201,14 +205,13 @@ export function registerStartCommand(cli: Goke) {
|
|
|
201
205
|
// The agent calls this same install. Prefer the built dist entry, so agent
|
|
202
206
|
// commands skip the tsx loader also when the bot runs from src.
|
|
203
207
|
const distCli = distCliEntry()
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
})
|
|
208
|
+
const node = stableNodePath()
|
|
209
|
+
const kimakiArgv = fs.existsSync(distCli)
|
|
210
|
+
? [node, distCli]
|
|
211
|
+
: [node, ...process.execArgv, process.argv[1] ?? 'kimaki']
|
|
209
212
|
|
|
210
213
|
const bot = await startBot({
|
|
211
|
-
|
|
214
|
+
kimakiArgv,
|
|
212
215
|
dataDir,
|
|
213
216
|
token: credentials.token,
|
|
214
217
|
appId: credentials.appId,
|
|
@@ -223,6 +226,7 @@ export function registerStartCommand(cli: Goke) {
|
|
|
223
226
|
allowAllUsers: Boolean(options.allowAllUsers),
|
|
224
227
|
disabledSkills,
|
|
225
228
|
disabledPrompt,
|
|
229
|
+
coordinator: options.coordinator,
|
|
226
230
|
})
|
|
227
231
|
if (bot instanceof Error) failStartup(bot)
|
|
228
232
|
// exit() keeps process.exitCode: the restart route sets RESTART_EXIT_CODE before its SIGTERM.
|
|
@@ -235,25 +239,30 @@ export function registerStartCommand(cli: Goke) {
|
|
|
235
239
|
process.once('disconnect', shutdown)
|
|
236
240
|
|
|
237
241
|
const gateway = credentials.mode === 'gateway'
|
|
238
|
-
const onboarded = await runOnboarding({ bot, dataDir, guildId: options.guild ?? install?.guildId, installUrl,
|
|
242
|
+
const onboarded = await runOnboarding({ bot, dataDir, guildId: options.guild ?? install?.guildId, installUrl, gateway, installerId: install?.installerId, machine })
|
|
239
243
|
// The bot keeps running; the next start retries onboarding.
|
|
240
244
|
if (onboarded instanceof Error) {
|
|
241
245
|
logger.error(`onboarding failed`, onboarded)
|
|
242
246
|
if (!process.stdin.isTTY) emitEvent({ type: 'error', message: `Onboarding failed: ${onboarded.message}. The bot is running; restart kimaki to retry.` })
|
|
243
247
|
return
|
|
244
248
|
}
|
|
245
|
-
if (onboarded) process.stderr.write(`Onboarding
|
|
249
|
+
if (onboarded) process.stderr.write(`Onboarding: https://discord.com/channels/${onboarded.guildId}/${onboarded.threadId}\n`)
|
|
246
250
|
const voiceChannels = await ensureVoiceChannels(bot, { machine })
|
|
247
251
|
if (voiceChannels instanceof Error) logger.warn('cannot create the Kimaki voice channel', voiceChannels)
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
252
|
+
// --no-autostart removes the script on every start, so the opt-out sticks while
|
|
253
|
+
// launchd or systemd passes the flag. Only the default port: the plugin checks
|
|
254
|
+
// that one. Runtime flags only; credentials, onboarding and --voice are saved in SQLite.
|
|
255
|
+
if (options.noAutostart) {
|
|
256
|
+
const removed = await removeAutostartScript({ dataDir })
|
|
257
|
+
if (removed instanceof Error) logger.warn('cannot remove the autostart script', removed)
|
|
258
|
+
} else if (lock.port === DEFAULT_LOCK_PORT) {
|
|
251
259
|
const autostart = await writeAutostartScript({ dataDir, args: [
|
|
252
260
|
...(options.worktrees ? ['--worktrees'] : []),
|
|
253
261
|
...(options.allowAllUsers ? ['--allow-all-users'] : []),
|
|
254
262
|
...(options.disableSkills ? ['--disable-skills', options.disableSkills] : []),
|
|
255
263
|
...disabledPrompt.flatMap((section) => ['--disable-prompt', section]),
|
|
256
264
|
...(options.noAnalytics ? ['--no-analytics'] : []),
|
|
265
|
+
...(options.coordinator ? [] : ['--coordinator', 'false']),
|
|
257
266
|
...(options.machineName ? ['--machine-name', options.machineName] : []),
|
|
258
267
|
] })
|
|
259
268
|
if (autostart instanceof Error) logger.warn('cannot write the autostart script', autostart)
|
package/src/cli/discord.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Discord-facing commands: agent UI (buttons, upload requests) through the
|
|
2
2
|
// running bot, plus thread/user lookups and uploads with the saved bot token.
|
|
3
3
|
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import os from 'node:os'
|
|
4
6
|
import path from 'node:path'
|
|
5
7
|
import { ChannelType } from 'discord-api-types/v10'
|
|
6
8
|
import { wrapJsonSchema, type Goke } from 'goke'
|
|
@@ -15,22 +17,23 @@ function agentShell() {
|
|
|
15
17
|
return { fromShell: Boolean(process.env['OPENCODE_SESSION_ID']), toolCall: process.env['KIMAKI_TOOL_CALL'] }
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
// The model reads this after `kimaki buttons
|
|
20
|
+
// The model reads this after `kimaki buttons`. Text planned before a tool call
|
|
21
|
+
// is sometimes never written (#256), so it asks for a missing answer or explanation.
|
|
19
22
|
const BUTTONS_SHOWN = dedent`
|
|
20
23
|
|
|
21
24
|
Buttons shown in Discord.
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
The user cannot see tool calls, command outputs, your reasoning, or subagent and task results. They only see your text.
|
|
27
|
+
If this turn has no text yet that answers the user and explains the buttons, write it now:
|
|
28
|
+
- your answer to what the user asked, if anything
|
|
26
29
|
- what was done in this session, concisely, including any findings from tool or task outputs that the choice depends on
|
|
27
30
|
- what each button does and its tradeoffs
|
|
28
|
-
|
|
31
|
+
Be concise: a few short lines in plain sentences, no walls of text. Do not only repeat the button labels. Then stop and wait for the click.
|
|
29
32
|
|
|
30
33
|
`
|
|
31
34
|
// buttons, upload-request
|
|
32
35
|
export function registerAgentUiCommands(cli: Goke) {
|
|
33
|
-
cli.command('buttons', 'Show 1-3 action buttons.
|
|
36
|
+
cli.command('buttons', 'Show 1-3 action buttons. Explain them in your text')
|
|
34
37
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
35
38
|
.option('-s, --session <id>', SESSION_HELP)
|
|
36
39
|
.option('-b, --button <spec>', wrapJsonSchema<string[]>({ type: 'array', items: { type: 'string' }, description: "Repeatable: Label[:white|blue|green|red]" }))
|
|
@@ -113,4 +116,25 @@ export function registerDiscordCommands(cli: Goke) {
|
|
|
113
116
|
// Waits for Discord: big files and a busy thread take longer than the 30s default.
|
|
114
117
|
await action({ route: 'upload', dataDir: options.dataDir, input, signal: AbortSignal.timeout(5 * 60_000) })
|
|
115
118
|
})
|
|
119
|
+
|
|
120
|
+
cli.command('diagram <input>', 'Render a Mermaid diagram (file path, or - for stdin) to a light themed PNG. Upload it with upload-to-discord')
|
|
121
|
+
.option('-o, --output <path>', 'PNG file to write (default: input path with .png, or a temp file for stdin)')
|
|
122
|
+
.option('--dark', 'Dark theme instead of light')
|
|
123
|
+
.action(async (input, options) => {
|
|
124
|
+
const source = input === '-' ? await readStdin() : await fs.promises.readFile(input, 'utf8').catch((error: Error) => error)
|
|
125
|
+
if (source instanceof Error) fail(source)
|
|
126
|
+
const { renderDiagram } = await import('../diagram.ts')
|
|
127
|
+
const png = renderDiagram(source, { theme: options.dark ? 'dark' : 'light' })
|
|
128
|
+
if (png instanceof Error) fail(png)
|
|
129
|
+
const output = path.resolve(options.output ?? (input === '-' ? path.join(os.tmpdir(), `kimaki-diagram-${Date.now()}.png`) : input.replace(/(\.\w+)?$/, '.png')))
|
|
130
|
+
const written = await fs.promises.writeFile(output, png).catch((error: Error) => error)
|
|
131
|
+
if (written instanceof Error) fail(written)
|
|
132
|
+
process.stdout.write(`${output}\n`)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function readStdin(): Promise<string> {
|
|
137
|
+
const chunks: Buffer[] = []
|
|
138
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk))
|
|
139
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
116
140
|
}
|
package/src/cli/schedule.ts
CHANGED
|
@@ -7,7 +7,10 @@ import { wrapJsonSchema, type Goke } from 'goke'
|
|
|
7
7
|
|
|
8
8
|
import { BotNotRunningError } from '../errors.ts'
|
|
9
9
|
import { callBot } from '../lock-server.ts'
|
|
10
|
-
import { action, DATA_DIR_HELP, dataDirOrDefault, discordApi, fail, openCliDb, parseTarget, printJson, readClient, SESSION_HELP, targetOrEnv,
|
|
10
|
+
import { action, DATA_DIR_HELP, dataDirOrDefault, discordApi, fail, openCliDb, parseTarget, printJson, readClient, SESSION_HELP, targetOrEnv, waitAndPrintSessions } from './shared.ts'
|
|
11
|
+
|
|
12
|
+
// Bare `--wait`: long enough that the shell timeout ends it first.
|
|
13
|
+
const DEFAULT_WAIT_SECONDS = 24 * 60 * 60
|
|
11
14
|
|
|
12
15
|
export function registerSendCommand(cli: Goke) {
|
|
13
16
|
cli.command('send', 'Start a session in a channel, or continue a thread')
|
|
@@ -16,7 +19,7 @@ export function registerSendCommand(cli: Goke) {
|
|
|
16
19
|
.option('-d, --project <path>', 'Project directory of the new thread')
|
|
17
20
|
.option('--thread <id>', 'Continue this thread (ID or URL)')
|
|
18
21
|
.option('-s, --session <id>', 'Continue this session: session ID, thread ID or thread URL')
|
|
19
|
-
.option('-p, --prompt <text>', 'Prompt; thread suffixes . queue and . btw are supported')
|
|
22
|
+
.option('-p, --prompt <text>', 'Prompt; thread suffixes . queue and . btw are supported').required()
|
|
20
23
|
.option('-f, --file <path>', wrapJsonSchema<string[]>({ type: 'array', items: { type: 'string' }, description: 'Attach a local file (repeatable)' }))
|
|
21
24
|
.option('-n, --name <text>', 'Thread name')
|
|
22
25
|
.option('--agent <name>', 'Agent ID')
|
|
@@ -28,12 +31,14 @@ export function registerSendCommand(cli: Goke) {
|
|
|
28
31
|
.option('--parent-session <id>', 'Record the parent session in session metadata')
|
|
29
32
|
.option('--permission <rule>', wrapJsonSchema<string[]>({ type: 'array', items: { type: 'string' }, description: 'Repeatable: tool[:pattern]:allow|deny|ask' }))
|
|
30
33
|
.option('--notify-only', 'Post a notification thread without a model turn')
|
|
31
|
-
.option('--wait', 'Wait until idle or input is needed, then print the session')
|
|
34
|
+
.option('--wait [seconds]', 'Wait until idle or input is needed (at most this many seconds, default 1 day), then print the session')
|
|
32
35
|
.option('--send-at <when>', 'Schedule: UTC ISO date ending in Z, or cron expression (UTC)')
|
|
33
36
|
.option('--pre-run <command>', 'Scheduled only: run first in the project. Exit 0 starts, stdout is appended')
|
|
34
37
|
.option('--allow-concurrency', 'Scheduled only: allow overlapping runs of this routine')
|
|
35
38
|
.action(async (options) => {
|
|
36
|
-
|
|
39
|
+
const wait = options.wait === undefined ? undefined : options.wait === '' ? DEFAULT_WAIT_SECONDS : Number(options.wait)
|
|
40
|
+
if (wait !== undefined && !(Number.isFinite(wait) && wait > 0 && wait <= DEFAULT_WAIT_SECONDS)) fail(new Error('--wait takes a positive number of seconds, at most 86400'))
|
|
41
|
+
if (wait !== undefined && options.sendAt) fail(new Error('--wait cannot be used with --send-at: the routine runs later'))
|
|
37
42
|
if (options.thread !== undefined && options.session !== undefined) fail(new Error('Use exactly one of --channel, --thread, --session, --project'))
|
|
38
43
|
const continued = options.thread ?? options.session
|
|
39
44
|
const target = continued === undefined ? {} : parseTarget(continued)
|
|
@@ -49,11 +54,14 @@ export function registerSendCommand(cli: Goke) {
|
|
|
49
54
|
dataDir: dataDirOrDefault(options.dataDir),
|
|
50
55
|
route: 'send',
|
|
51
56
|
signal: AbortSignal.timeout(25 * 60_000),
|
|
52
|
-
input: {
|
|
57
|
+
input: {
|
|
58
|
+
...input, wait, sendAt: options.sendAt, preRun: options.preRun, allowConcurrency: options.allowConcurrency,
|
|
59
|
+
senderSessionId: process.env['OPENCODE_SESSION_ID'] || undefined,
|
|
60
|
+
},
|
|
53
61
|
})
|
|
54
62
|
if (result instanceof BotNotRunningError) {
|
|
55
63
|
// No local bot (CI, another machine): the bot that owns the channel runs it (remote-send.ts).
|
|
56
|
-
if (options.sendAt ||
|
|
64
|
+
if (options.sendAt || wait !== undefined) fail(new Error('Kimaki bot is not running here. --send-at and --wait need a local bot.'))
|
|
57
65
|
const [{ sendInput }, { sendWithoutBot }] = await Promise.all([import('../lock-routes.ts'), import('../remote-send.ts')])
|
|
58
66
|
const parsed = sendInput.safeParse(input)
|
|
59
67
|
if (!parsed.success) fail(new Error(parsed.error.issues.map((issue) => issue.message).join('; ')))
|
|
@@ -65,10 +73,16 @@ export function registerSendCommand(cli: Goke) {
|
|
|
65
73
|
}
|
|
66
74
|
if (result instanceof Error) fail(result)
|
|
67
75
|
process.stdout.write(`${JSON.stringify(result.data)}\n`)
|
|
68
|
-
if (
|
|
76
|
+
if (wait === undefined) return
|
|
69
77
|
const data = result.data
|
|
70
78
|
if (!data || typeof data !== 'object' || !('sessionId' in data) || typeof data.sessionId !== 'string') fail(new Error('--wait requires an AI session, not --notify-only'))
|
|
71
|
-
await
|
|
79
|
+
await waitAndPrintSessions({
|
|
80
|
+
client: await readClient(),
|
|
81
|
+
sessionIds: [data.sessionId],
|
|
82
|
+
dataDir: options.dataDir,
|
|
83
|
+
signal: AbortSignal.timeout(wait * 1000),
|
|
84
|
+
timeout: `${wait}s`,
|
|
85
|
+
})
|
|
72
86
|
})
|
|
73
87
|
}
|
|
74
88
|
|
|
@@ -134,7 +148,7 @@ export function registerScheduleCommands(cli: Goke) {
|
|
|
134
148
|
}
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
cli.command('sleep', 'Wake this session later with a new message in the same thread. Run it last, after your text. To monitor slow events (email replies, PR reviews), wait 2h or more; never poll every few minutes')
|
|
151
|
+
cli.command('sleep', 'Wake this session later with a new message in the same thread. Run it last, after your text. To monitor slow events (email replies, PR reviews), wait 2h or more; never poll every few minutes. When blocked on a user action outside the chat (a sudo or permission dialog, a login), ask for it, assume they will do it, and sleep 5m, then check and continue on your own')
|
|
138
152
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
139
153
|
.option('--duration <duration>', 'Relative wait, e.g. 30m, 2h, 1d')
|
|
140
154
|
.option('--until <date>', 'UTC ISO date ending in Z')
|
package/src/cli/session.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
SESSION_HELP,
|
|
27
27
|
resolveTarget,
|
|
28
28
|
targetOrEnv,
|
|
29
|
-
|
|
29
|
+
waitAndPrintSessions,
|
|
30
30
|
} from './shared.ts'
|
|
31
31
|
|
|
32
32
|
const execFileAsync = promisify(execFile)
|
|
@@ -44,8 +44,11 @@ export function registerSessionQueryCommands(cli: Goke) {
|
|
|
44
44
|
const errorCode = options.active ? 64 : 1
|
|
45
45
|
const client = await readClient(errorCode)
|
|
46
46
|
const directory = options.all ? undefined : await projectDirectory({ project: options.project, channel: undefined, dataDir: options.dataDir })
|
|
47
|
+
// By project ID, not directory: sessions in linked worktrees of the same clone belong to the project too.
|
|
48
|
+
const location = directory ? await client.location.get({ location: { directory } }).catch((error: Error) => error) : undefined
|
|
49
|
+
if (location instanceof Error) fail(location, errorCode)
|
|
47
50
|
const [sessions, active, forms, permissions] = await Promise.all([
|
|
48
|
-
allSessions({ client,
|
|
51
|
+
allSessions({ client, project: location?.project.id }),
|
|
49
52
|
client.session.active().catch((error: Error) => error),
|
|
50
53
|
client.form.list().catch((error: Error) => error),
|
|
51
54
|
client.permission.request.list().catch((error: Error) => error),
|
|
@@ -82,15 +85,30 @@ export function registerSessionQueryCommands(cli: Goke) {
|
|
|
82
85
|
process.stdout.write(`${transcriptsDir({ dataDir })}: ${result.written} written, ${result.skipped} skipped, ${result.failed} failed\n`)
|
|
83
86
|
})
|
|
84
87
|
|
|
85
|
-
cli.command('session wait
|
|
88
|
+
cli.command('session wait [...ids]', 'Wait until every session is idle or waits for the user, then print each one and why it returned')
|
|
89
|
+
.example('kimaki session wait ses_abc 1495130000000000000')
|
|
90
|
+
.example(`kimaki session wait $(kimaki session list --all --active | awk '{print $1}')`)
|
|
86
91
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
87
|
-
.option('--timeout <duration>', 'Timeout, for example 30m or 2h')
|
|
88
|
-
.action(async (
|
|
89
|
-
const { sessionId } = await resolveTarget(id, options.dataDir)
|
|
92
|
+
.option('--timeout <duration>', 'Timeout, for example 30m or 2h; exit 1 when a session still runs')
|
|
93
|
+
.action(async (ids, options) => {
|
|
90
94
|
const timeout = options.timeout ? parseDuration(options.timeout) : undefined
|
|
91
95
|
if (timeout instanceof Error) fail(new Error('Use --timeout 30m, 2h, or another positive duration'))
|
|
96
|
+
// Empty when `$(kimaki session list --active)` found nothing.
|
|
97
|
+
if (ids.length === 0) {
|
|
98
|
+
process.stdout.write('No sessions to wait for\n')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const sessionIds: string[] = []
|
|
102
|
+
for (const id of ids) sessionIds.push((await resolveTarget(id, options.dataDir)).sessionId)
|
|
92
103
|
const client = await readClient()
|
|
93
|
-
|
|
104
|
+
const done = await waitAndPrintSessions({
|
|
105
|
+
client,
|
|
106
|
+
sessionIds: [...new Set(sessionIds)],
|
|
107
|
+
dataDir: options.dataDir,
|
|
108
|
+
signal: timeout === undefined ? undefined : AbortSignal.timeout(timeout),
|
|
109
|
+
timeout: options.timeout ?? '',
|
|
110
|
+
})
|
|
111
|
+
if (!done) process.exitCode = 1
|
|
94
112
|
})
|
|
95
113
|
|
|
96
114
|
cli.command('session editors <file>', 'List sessions that last edited a file, newest first')
|
|
@@ -147,7 +165,7 @@ export function registerSessionQueryCommands(cli: Goke) {
|
|
|
147
165
|
})
|
|
148
166
|
}
|
|
149
167
|
|
|
150
|
-
// abort, archive, title, queue, command, shell, btw, fork, resume: all through the bot.
|
|
168
|
+
// abort, archive, compact, title, queue, command, shell, btw, fork, resume: all through the bot.
|
|
151
169
|
export function registerSessionActionCommands(cli: Goke) {
|
|
152
170
|
cli.command('session abort [id]', 'Stop the running turn and clear its queue')
|
|
153
171
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
@@ -157,6 +175,10 @@ export function registerSessionActionCommands(cli: Goke) {
|
|
|
157
175
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
158
176
|
.action(async (id, options) => action({ route: 'session.archive', dataDir: options.dataDir, input: targetOrEnv(id) }))
|
|
159
177
|
|
|
178
|
+
cli.command('session compact [id]', 'Summarize the session history after its current turn (default: this session)')
|
|
179
|
+
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
180
|
+
.action(async (id, options) => action({ route: 'session.compact', dataDir: options.dataDir, input: targetOrEnv(id) }))
|
|
181
|
+
|
|
160
182
|
cli.command('session title <title>', 'Rename the session and its Discord thread')
|
|
161
183
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
162
184
|
.option('-s, --session <id>', SESSION_HELP)
|
|
@@ -224,10 +246,11 @@ export function registerSessionHistoryCommands(cli: Goke) {
|
|
|
224
246
|
await pipeline(fs.createReadStream(file), process.stdout)
|
|
225
247
|
})
|
|
226
248
|
|
|
227
|
-
cli.command('session read <id>', 'Print the
|
|
249
|
+
cli.command('session read <id>', 'Print the prompts and final answers of a session as markdown')
|
|
228
250
|
.option('--data-dir <path>', DATA_DIR_HELP)
|
|
229
|
-
.option('--
|
|
230
|
-
.option('--
|
|
251
|
+
.option('--full', 'Every message with tool calls')
|
|
252
|
+
.option('--thinking', 'Full output with reasoning')
|
|
253
|
+
.option('--verbose', 'Full output with tool inputs and outputs')
|
|
231
254
|
.option('--json', 'Print raw OpenCode messages')
|
|
232
255
|
.action(async (id, options) => {
|
|
233
256
|
// Subagent sessions are not in SQLite: resolveTarget keeps their ID.
|
|
@@ -238,7 +261,7 @@ export function registerSessionHistoryCommands(cli: Goke) {
|
|
|
238
261
|
if (messages instanceof Error) fail(messages)
|
|
239
262
|
return printJson(messages)
|
|
240
263
|
}
|
|
241
|
-
const markdown = await readSessionMarkdown({ client, sessionId, thinking: options.thinking, verbose: options.verbose })
|
|
264
|
+
const markdown = await readSessionMarkdown({ client, sessionId, full: options.full, thinking: options.thinking, verbose: options.verbose })
|
|
242
265
|
if (markdown instanceof Error) fail(markdown)
|
|
243
266
|
process.stdout.write(`${markdown}\n`)
|
|
244
267
|
})
|
package/src/cli/shared.ts
CHANGED
|
@@ -10,12 +10,12 @@ import path from 'node:path'
|
|
|
10
10
|
import type { OpenCodeClient } from '@opencode/client'
|
|
11
11
|
import { ChannelType } from 'discord-api-types/v10'
|
|
12
12
|
|
|
13
|
-
import { DbError } from '../errors.ts'
|
|
13
|
+
import { BotNotRunningError, DbError } from '../errors.ts'
|
|
14
14
|
import { canonicalPath } from '../file-edit-log.ts'
|
|
15
15
|
import type { LockRouteInput, LockRouteName } from '../lock-routes.ts'
|
|
16
16
|
import { callBot } from '../lock-server.ts'
|
|
17
17
|
import { resolveOpencode } from '../opencode-server.ts'
|
|
18
|
-
import { readSessionMarkdown, resolveSession,
|
|
18
|
+
import { readSessionMarkdown, resolveSession, waitForSessions, type WaitStatus } from '../session-events.ts'
|
|
19
19
|
|
|
20
20
|
export const DATA_DIR_HELP = 'Data directory (default: ~/.kimaki)'
|
|
21
21
|
export const SESSION_HELP = 'Session ID, Discord thread ID or thread URL (default: OPENCODE_SESSION_ID)'
|
|
@@ -90,12 +90,47 @@ export async function resolveTarget(value: string, dataDir: string | undefined):
|
|
|
90
90
|
fail(result)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
function statusText({ sessionId, status, buttons, timeout }: { sessionId: string; status: WaitStatus; buttons: string[]; timeout: string }): string {
|
|
94
|
+
// `kimaki buttons` ends the turn, so the run looks finished while the session waits for a click.
|
|
95
|
+
if (buttons.length > 0 && status !== 'running' && status !== 'question' && status !== 'permission') {
|
|
96
|
+
return `is paused: it showed buttons (${buttons.join(', ')}) and waits for the user to click one, so its work may not be done`
|
|
97
|
+
}
|
|
98
|
+
if (status === 'finished') return 'finished its run'
|
|
99
|
+
if (status === 'failed') return 'ended its run with an error'
|
|
100
|
+
if (status === 'interrupted') return 'was interrupted, by an abort or by a new user message that started another run'
|
|
101
|
+
if (status === 'idle') return 'was already idle: its last run ended before the wait started'
|
|
102
|
+
if (status === 'question') return 'is paused: it waits for the user to answer a question, so its work is not done'
|
|
103
|
+
if (status === 'permission') return 'is paused: it waits for the user to allow a permission, so its work is not done'
|
|
104
|
+
return `is still running after ${timeout}. Run kimaki session wait ${sessionId} to wait for the rest`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Waits until every session ended its run or paused for the user, then prints each one in
|
|
108
|
+
// order under a header that says why it returned. Returns false when `signal` ended the wait first.
|
|
109
|
+
export async function waitAndPrintSessions({ client, sessionIds, dataDir, signal, timeout }: {
|
|
110
|
+
client: OpenCodeClient
|
|
111
|
+
sessionIds: readonly string[]
|
|
112
|
+
dataDir: string | undefined
|
|
113
|
+
signal?: AbortSignal
|
|
114
|
+
timeout: string
|
|
115
|
+
}): Promise<boolean> {
|
|
116
|
+
const statuses = await waitForSessions({ client, sessionIds, signal })
|
|
117
|
+
if (statuses instanceof Error) fail(statuses)
|
|
118
|
+
// Buttons live in the bot's memory. Without a bot nothing can show them.
|
|
119
|
+
const buttons = await callBot({ dataDir: dataDirOrDefault(dataDir), route: 'session.buttons', input: { sessionIds: [...sessionIds] } })
|
|
120
|
+
if (buttons instanceof Error && !(buttons instanceof BotNotRunningError)) process.stderr.write(`cannot read pending buttons: ${buttons.message}\n`)
|
|
121
|
+
for (const sessionId of sessionIds) {
|
|
122
|
+
const status = statuses.get(sessionId) ?? 'running'
|
|
123
|
+
const text = statusText({ sessionId, status, buttons: buttons instanceof Error ? [] : buttons.data[sessionId] ?? [], timeout })
|
|
124
|
+
const header = `=== ${sessionId} ${text} ===`
|
|
125
|
+
if (status === 'running') {
|
|
126
|
+
process.stdout.write(`${header}\n\n`)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
const markdown = await readSessionMarkdown({ client, sessionId })
|
|
130
|
+
if (markdown instanceof Error) fail(markdown)
|
|
131
|
+
process.stdout.write(`${header}\n\n${markdown}\n\n`)
|
|
132
|
+
}
|
|
133
|
+
return [...statuses.values()].every((status) => status !== 'running')
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
// Discord REST with the bot token: KIMAKI_BOT_TOKEN (no kimaki.db needed, e.g. CI), else saved credentials.
|
|
@@ -40,7 +40,12 @@ test('independent Discord CLI lists threads and users, uploads a file, and reads
|
|
|
40
40
|
// Many files keep argument order, numbered, images and files mixed.
|
|
41
41
|
fs.mkdirSync(path.join(server.root, 'other'), { recursive: true })
|
|
42
42
|
for (const name of ['b.png', 'my shot.png', 'other/b.png']) fs.writeFileSync(path.join(server.root, name), 'png')
|
|
43
|
-
await cli(['upload-to-discord', ...['b.png', 'report.txt', 'my shot.png', 'other/b.png'].map((name) => path.join(server.root, name)), '--session', started.sessionId])
|
|
43
|
+
const uploaded = await cli(['upload-to-discord', ...['b.png', 'report.txt', 'my shot.png', 'other/b.png'].map((name) => path.join(server.root, name)), '--session', started.sessionId])
|
|
44
|
+
// Each file with its Discord URL, in argument order, so a coordinator can repost them (the twin uses data: URLs).
|
|
45
|
+
expect(uploaded.stdout).toMatchInlineSnapshot(`
|
|
46
|
+
"{"uploaded":[{"name":"b.png","url":"data:application/octet-stream;base64,cG5n"},{"name":"report.txt","url":"data:application/octet-stream;base64,cmVwb3J0"},{"name":"my shot.png","url":"data:application/octet-stream;base64,cG5n"},{"name":"b.png","url":"data:application/octet-stream;base64,cG5n"}]}
|
|
47
|
+
"
|
|
48
|
+
`)
|
|
44
49
|
// The agent learns that nothing was posted.
|
|
45
50
|
const missing = await cli(['upload-to-discord', path.join(server.root, 'missing.png'), '--session', started.sessionId]).catch((error: { stderr: string }) => error)
|
|
46
51
|
expect(missing.stderr.replace(server.root, '<root>')).toMatchInlineSnapshot(`
|
|
@@ -51,20 +56,20 @@ test('independent Discord CLI lists threads and users, uploads a file, and reads
|
|
|
51
56
|
const install = await cli(['bot', 'install-url'])
|
|
52
57
|
expect(await twin.discord.thread(started.threadId).text()).toMatchInlineSnapshot(`
|
|
53
58
|
"--- from: assistant (TestBot)
|
|
54
|
-
» **CLI:** Discord CLI flow
|
|
59
|
+
-# » **CLI:** Discord CLI flow
|
|
55
60
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
56
61
|
ok
|
|
57
|
-
-# *
|
|
58
|
-
[file:
|
|
62
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
63
|
+
[file: data:application/octet-stream;base64,cmVwb3J0]
|
|
59
64
|
[attachment: report.txt]
|
|
60
65
|
-# 1. \`b.png\`
|
|
61
|
-
[media:
|
|
66
|
+
[media: data:application/octet-stream;base64,cG5n]
|
|
62
67
|
-# 2. \`report.txt\`
|
|
63
|
-
[file:
|
|
68
|
+
[file: data:application/octet-stream;base64,cmVwb3J0]
|
|
64
69
|
-# 3. \`my shot.png\`
|
|
65
|
-
[media:
|
|
70
|
+
[media: data:application/octet-stream;base64,cG5n]
|
|
66
71
|
-# 4. \`b.png\`
|
|
67
|
-
[media:
|
|
72
|
+
[media: data:application/octet-stream;base64,cG5n]
|
|
68
73
|
[attachment: b.png]
|
|
69
74
|
[attachment: report.txt]
|
|
70
75
|
[attachment: my_shot.png]
|