kimaki 1.0.0 → 1.2.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.
Files changed (108) hide show
  1. package/dist/autostart.js +15 -3
  2. package/dist/autostart.js.map +1 -1
  3. package/dist/bot.d.ts +7 -5
  4. package/dist/bot.js.map +1 -1
  5. package/dist/builtin-skills.d.ts +18 -0
  6. package/dist/builtin-skills.js +51 -0
  7. package/dist/builtin-skills.js.map +1 -0
  8. package/dist/builtin-skills.test.d.ts +1 -0
  9. package/dist/builtin-skills.test.js +28 -0
  10. package/dist/builtin-skills.test.js.map +1 -0
  11. package/dist/cli/bot.js +10 -0
  12. package/dist/cli/bot.js.map +1 -1
  13. package/dist/cli/discord.js +1 -1
  14. package/dist/cli/discord.js.map +1 -1
  15. package/dist/cli/session.js +17 -53
  16. package/dist/cli/session.js.map +1 -1
  17. package/dist/cli-discord.e2e.test.js +19 -1
  18. package/dist/cli-discord.e2e.test.js.map +1 -1
  19. package/dist/cli-lock.e2e.test.js +7 -6
  20. package/dist/cli-lock.e2e.test.js.map +1 -1
  21. package/dist/cli-read.e2e.test.js +8 -3
  22. package/dist/cli-read.e2e.test.js.map +1 -1
  23. package/dist/cli-sessions.e2e.test.js +2 -4
  24. package/dist/cli-sessions.e2e.test.js.map +1 -1
  25. package/dist/commands/login-commands.d.ts +1 -1
  26. package/dist/commands/preference-commands.d.ts +1 -1
  27. package/dist/commands/preference-commands.js +17 -3
  28. package/dist/commands/preference-commands.js.map +1 -1
  29. package/dist/commands/worktree-commands.d.ts +1 -1
  30. package/dist/effects.js +30 -6
  31. package/dist/effects.js.map +1 -1
  32. package/dist/errors.d.ts +1 -1
  33. package/dist/errors.js +1 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/event-loop.d.ts +2 -1
  36. package/dist/event-loop.js +5 -1
  37. package/dist/event-loop.js.map +1 -1
  38. package/dist/ingress.d.ts +1 -1
  39. package/dist/ingress.js +14 -11
  40. package/dist/ingress.js.map +1 -1
  41. package/dist/lock-routes.d.ts +12 -12
  42. package/dist/login.e2e.test.js +2 -2
  43. package/dist/main.d.ts +2 -0
  44. package/dist/main.js +223 -151
  45. package/dist/main.js.map +1 -1
  46. package/dist/opencode-server.js +15 -11
  47. package/dist/opencode-server.js.map +1 -1
  48. package/dist/opencode-server.test.js +1 -1
  49. package/dist/opencode-server.test.js.map +1 -1
  50. package/dist/plugin/index.js +19 -1
  51. package/dist/plugin/index.js.map +1 -1
  52. package/dist/plugin/read-media.d.ts +23 -0
  53. package/dist/plugin/read-media.js +72 -0
  54. package/dist/plugin/read-media.js.map +1 -0
  55. package/dist/plugin.e2e.test.js +6 -1
  56. package/dist/plugin.e2e.test.js.map +1 -1
  57. package/dist/prompt.d.ts +6 -6
  58. package/dist/prompt.js +10 -11
  59. package/dist/prompt.js.map +1 -1
  60. package/dist/scheduler.d.ts +1 -1
  61. package/dist/session-events.d.ts +29 -1
  62. package/dist/session-events.js +70 -0
  63. package/dist/session-events.js.map +1 -1
  64. package/dist/sessions.d.ts +10 -10
  65. package/dist/sessions.js +3 -2
  66. package/dist/sessions.js.map +1 -1
  67. package/dist/slash-commands.js +4 -3
  68. package/dist/slash-commands.js.map +1 -1
  69. package/dist/system-prompt.d.ts +13 -2
  70. package/dist/system-prompt.js +41 -64
  71. package/dist/system-prompt.js.map +1 -1
  72. package/dist/voice-attachments.e2e.test.js +6 -3
  73. package/dist/voice-attachments.e2e.test.js.map +1 -1
  74. package/dist/voice-call.js +1 -1
  75. package/dist/voice-call.js.map +1 -1
  76. package/package.json +7 -6
  77. package/skills/kimaki-debug/SKILL.md +44 -0
  78. package/skills/kimaki-tunnel/SKILL.md +34 -0
  79. package/src/autostart.ts +15 -3
  80. package/src/bot.ts +4 -0
  81. package/src/builtin-skills.test.ts +29 -0
  82. package/src/builtin-skills.ts +59 -0
  83. package/src/cli/bot.ts +9 -0
  84. package/src/cli/discord.ts +1 -1
  85. package/src/cli/session.ts +14 -47
  86. package/src/cli-discord.e2e.test.ts +18 -1
  87. package/src/cli-lock.e2e.test.ts +7 -6
  88. package/src/cli-read.e2e.test.ts +8 -3
  89. package/src/cli-sessions.e2e.test.ts +2 -4
  90. package/src/commands/preference-commands.ts +19 -6
  91. package/src/effects.ts +40 -6
  92. package/src/errors.ts +1 -1
  93. package/src/event-loop.ts +9 -0
  94. package/src/ingress.ts +16 -11
  95. package/src/login.e2e.test.ts +2 -2
  96. package/src/main.ts +12 -2
  97. package/src/opencode-server.test.ts +1 -1
  98. package/src/opencode-server.ts +12 -9
  99. package/src/plugin/index.ts +20 -1
  100. package/src/plugin.e2e.test.ts +6 -1
  101. package/src/prompt.ts +10 -10
  102. package/src/session-events.ts +83 -0
  103. package/src/sessions.ts +3 -2
  104. package/src/slash-commands.ts +4 -3
  105. package/src/system-prompt.ts +43 -64
  106. package/src/voice-attachments.e2e.test.ts +6 -3
  107. package/src/voice-call.ts +1 -1
  108. package/src/plugin/session-id/index.ts +0 -19
@@ -108,28 +108,7 @@ export function baseInstructions({
108
108
 
109
109
  ## debugging kimaki issues
110
110
 
111
- ALWAYS read https://kimaki.dev/docs/guides/report-bugs first before submitting any issue to Kimaki. That page is the source of truth for exporting session jsonl, sharing evidence in a gist, and filing bugs. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
112
- If there are internal kimaki issues (sessions not responding, bot errors, unexpected behavior), read the logs of the running bot and of the OpenCode service it talks to. Read them before guessing.
113
-
114
- **Kimaki bot log**: \`${dataDir}/kimaki.log\`, only the current bot run. The run before the last restart (including the crash that caused it) is in \`${dataDir}/kimaki.previous.log\`. Each line is \`<ISO time> <LEVEL> [<MODULE>] <message>\`, and errors include their cause chain. It logs incoming messages, run start/finish/failure per thread and session, Discord and OpenCode errors, routines and crashes.
115
-
116
- \`\`\`bash
117
- tail -n 200 ${dataDir}/kimaki.log
118
- grep -E ' (WARN|ERROR) ' ${dataDir}/kimaki.log | tail -n 50
119
- grep '<thread_id or ses_id>' ${dataDir}/kimaki.log
120
- \`\`\`
121
-
122
- **OpenCode service log**: the shared \`opencode serve --service\` daemon appends to \`~/.local/share/opencode/log/opencode.log\` (\`$XDG_DATA_HOME/opencode/log\`; \`opencode debug paths\` prints the folder). The file is never reset and can be very large: always use \`tail\` or \`grep\`, never read it whole. Lines are logfmt. \`run=<id>\` is one daemon process, so filter by the run of the last line. The daemon pid and URL are in \`~/.local/state/opencode/service.json\`.
123
-
124
- \`\`\`bash
125
- LOG=~/.local/share/opencode/log/opencode.log
126
- RUN=$(tail -n 1 $LOG | grep -o 'run=[a-z0-9]*')
127
- grep "$RUN " $LOG | grep -E '^timestamp=[^ ]+ level=(ERROR|WARN)' | tail -n 50
128
- grep "$RUN " $LOG | grep '<ses_id>' | tail -n 100
129
- \`\`\`
130
-
131
- \`kimaki session events <id>\` prints the recorded OpenCode events of a thread as JSONL.
132
- \`kimaki restart\` restarts the bot (only when the user asks). \`kimaki profile cpu --duration 30s\` and \`kimaki profile heap\` print the profile file path.
111
+ When sessions do not respond, the bot shows errors, or you need the Kimaki or OpenCode logs, load the \`kimaki-debug\` skill (logs, profiling, \`kimaki login\` for provider credentials) and read the logs before guessing. Do not restart the bot unless the user explicitly asks. ALWAYS read https://kimaki.dev/docs/guides/report-bugs before submitting any issue to Kimaki. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
133
112
 
134
113
  ## uploading files to discord
135
114
 
@@ -137,6 +116,10 @@ export function baseInstructions({
137
116
 
138
117
  kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
139
118
 
119
+ To send several images or files, pass them all to ONE command in the order you want them shown, not one command per file. Discord shows them in argument order with numbered labels (\`1. a.png\`, \`2. b.pdf\`), so you and the user can refer to them by number (for example "screenshot 2"). Name them in your text with the same numbers.
120
+
121
+ Files the user attaches arrive in an \`<attachments>\` block, numbered in the order the user attached them. When the user says "the second image", use that numbering.
122
+
140
123
  NEVER show images with markdown like \`![alt](/tmp/file.png)\` or \`![alt](file://...)\`. Discord does not render local markdown images. ALWAYS upload them with \`kimaki upload-to-discord\` so they appear as real Discord attachments. Do this for every screenshot, generated image, and visual step the user should see.
141
124
 
142
125
  ## generating audio from text
@@ -386,15 +369,25 @@ export function baseInstructions({
386
369
 
387
370
  Each row shows the session ID, status (\`busy\`, \`waiting\` for input, or \`idle\`), the title, and \`tokens: N\`. Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
388
371
 
389
- To search past sessions (supports plain text or /regex/flags). Defaults to this project and the last 14 days. Use \`--days 0\` for all time. Use \`--all\` to search every project:
372
+ To search past sessions, search the transcripts. Kimaki writes one markdown file per session to \`${dataDir}/transcripts/<session id>.md\` when a run ends (user messages, assistant text and short tool inputs; no tool outputs, no thinking). The first lines are \`# <title> (<session id>)\` and \`directory: <folder>\`; messages start with \`## user <time>\` or \`## assistant <time> (<agent>, <model>)\`. Use rg (or \`grep -r\` when rg is missing):
373
+
374
+ \`\`\`bash
375
+ rg -il 'auth timeout' ${dataDir}/transcripts # sessions that mention it; the file name is the session ID
376
+ rg -i -C3 'auth timeout' ${dataDir}/transcripts/<session id>.md # matches with context in one session
377
+ rg -i -m3 'rate limit' ${dataDir}/transcripts # first 3 matches per session
378
+ rg -i 'error\\s+42|panic' ${dataDir}/transcripts # regex
379
+ rg -i '^# .*billing' ${dataDir}/transcripts # titles
380
+ rg -l '^directory: /path/to/project' ${dataDir}/transcripts # sessions of one project
381
+ rg -il 'auth timeout' $(find ${dataDir}/transcripts -name '*.md' -mtime -3) # last 3 days
382
+ \`\`\`
383
+
384
+ Sessions that Kimaki did not run (OpenCode TUI) and old sessions are not there until you sync. The command skips current transcripts, so it is cheap to run again:
390
385
 
391
386
  \`\`\`bash
392
- kimaki session search "auth timeout"
393
- kimaki session search "auth timeout" --days 0
394
- kimaki session search "/error\\s+42/i"
395
- kimaki session search "rate limit" --project /path/to/project
396
- kimaki session search "/panic|crash/i" --channel <channel_id>
397
- kimaki session search "auth timeout" --all
387
+ kimaki session sync # sessions updated in the last 14 days
388
+ kimaki session sync --days 60
389
+ kimaki session sync --all # every session, any age (slow the first time)
390
+ kimaki session sync --rebuild # write all again, for example after the format changed
398
391
  \`\`\`
399
392
 
400
393
  To read a session as markdown, pipe to a file. Logs go to stderr:
@@ -469,10 +462,6 @@ export function baseInstructions({
469
462
 
470
463
  \`session list --active\` exits 0 while it finds active sessions, 1 when none remain, and 64 on errors. The loop stops only on 1, so an error never looks like "no active sessions". \`--active\` never lists the calling session (\`OPENCODE_SESSION_ID\`), so the loop does not wait for itself. Sessions can start again after the loop ends, so run the check again right before each commit. \`session wait\` returns once the model finishes responding, or when the session pauses to show the user a question (it does not finish on its own until answered).
471
464
 
472
- ## provider credentials
473
-
474
- Provider credentials belong to OpenCode. Use \`kimaki login PROVIDER\` to list login methods; \`--key\` connects an API key, \`--method\` starts OAuth, and \`--attempt\` checks or completes an attempt.
475
-
476
465
  ## submodules
477
466
 
478
467
  When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work. Otherwise critique diffs later will include the noisy submodule jump along with the real changes.
@@ -512,31 +501,7 @@ export function baseInstructions({
512
501
 
513
502
  ## running dev servers with tunnel access
514
503
 
515
- Localhost URLs are useless from Discord. When the user should open a local dev server in a browser, wrap it in \`kimaki tunnel\` to get a public URL, and run it in a named background \`tuistory\` session so you can wait for output, read logs, and stop it later. Name it descriptively (e.g. \`projectname-dev\`) so you can reuse it. Run \`bunx tuistory --help\` first. Invoke \`kimaki\` directly, not via \`npx\` or \`bunx\`.
516
-
517
- - Use random tunnel IDs by default. Pass \`-t <id>\` only for services that are safe to be publicly discoverable.
518
- - \`kimaki tunnel\` detects the local port from the child output. Pass \`--port\` only when the server prints no detectable localhost URL or port line.
519
- - \`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Wire the app to it so OAuth callbacks, webhook URLs, and absolute links use the public URL.
520
-
521
- \`\`\`bash
522
- # start in a named background session, wait for output, then read the tunnel URL
523
- bunx tuistory launch "kimaki tunnel -- pnpm dev" -s myapp-dev
524
- bunx tuistory -s myapp-dev wait "/ready|local|tunnel/i" --timeout 30000
525
- bunx tuistory read -s myapp-dev
526
-
527
- # pass the public URL to the app (better-auth, Next.js, Vite; node can read process.env.TRAFORO_URL)
528
- bunx tuistory launch "kimaki tunnel -- sh -c 'BETTER_AUTH_URL=$TRAFORO_URL exec pnpm dev'" -s myapp-dev
529
- bunx tuistory launch "kimaki tunnel -- sh -c 'APP_URL=$TRAFORO_URL exec pnpm dev'" -s myapp-dev
530
- bunx tuistory launch "kimaki tunnel -- sh -c 'VITE_BASE_URL=$TRAFORO_URL exec pnpm dev'" -s myapp-dev
531
-
532
- # custom tunnel ID (only for intentionally public-safe services)
533
- bunx tuistory launch "kimaki tunnel -t holocron -- pnpm dev" -s holocron-dev
534
-
535
- # list sessions; stop with Ctrl+C, then close
536
- bunx tuistory sessions
537
- bunx tuistory -s myapp-dev press ctrl c
538
- bunx tuistory -s myapp-dev close
539
- \`\`\`
504
+ Localhost URLs are useless from Discord. When the user should open a local dev server in a browser, load the \`kimaki-tunnel\` skill: it wraps the server in \`kimaki tunnel\` to get a public URL.
540
505
 
541
506
  ## markdown formatting
542
507
 
@@ -641,8 +606,19 @@ export function stripTurnContext(text: string): string {
641
606
  return text.replace(/\n\n(<local-files>\n[\s\S]*?<\/local-files>\n)?<discord-user [^\n]*\/>$/, '')
642
607
  }
643
608
 
644
- // Attachments the model cannot see inline: it reads them with tools.
645
- export function localFilesBlock(paths: readonly string[]): string {
609
+ const PromptAttachmentSchema = z.object({ name: z.string(), path: z.string(), inline: z.boolean() })
610
+ export type PromptAttachment = z.infer<typeof PromptAttachmentSchema>
611
+
612
+ // Every attachment of the message, numbered in Discord order, so the user and the
613
+ // model can refer to "image 2". Inline ones are already in the message.
614
+ export function attachmentsBlock(attachments: readonly PromptAttachment[]): string {
615
+ if (attachments.length === 0) return ''
616
+ const lines = attachments.map((file, index) => `${index + 1}. ${file.name}${file.inline ? ' (inline)' : ''}: ${file.path}`)
617
+ return `<attachments>\nFiles of this message in the order the user attached them, saved on disk. Read the ones not marked inline with tools.\n${lines.join('\n')}\n</attachments>\n`
618
+ }
619
+
620
+ // Prompts before numbered attachments: only the files the model could not see inline.
621
+ function localFilesBlock(paths: readonly string[]): string {
646
622
  if (paths.length === 0) return ''
647
623
  return `<local-files>\nAttachments saved on disk. OpenCode cannot show these inline; use tools to read them.\n${paths.join('\n')}\n</local-files>\n`
648
624
  }
@@ -654,7 +630,8 @@ const TurnContextSchema = z.object({
654
630
  messageId: z.string(),
655
631
  threadId: z.string(),
656
632
  threadName: z.string(),
657
- localFiles: z.array(z.string()),
633
+ attachments: z.array(PromptAttachmentSchema).optional(),
634
+ localFiles: z.array(z.string()).optional(),
658
635
  })
659
636
  export type TurnContext = z.infer<typeof TurnContextSchema>
660
637
 
@@ -663,8 +640,9 @@ export type TurnContext = z.infer<typeof TurnContextSchema>
663
640
  export function turnContext(metadata: Readonly<Record<string, unknown>> | undefined): string | null {
664
641
  const parsed = TurnContextSchema.safeParse(metadata?.['discord'])
665
642
  if (!parsed.success) return null
666
- const { username, userId, messageId, threadId, threadName, localFiles } = parsed.data
667
- return `${localFilesBlock(localFiles)}<discord-user name="${escapeAttribute(username)}" user-id="${userId}" message-id="${messageId}" thread-id="${threadId}" thread-name="${escapeAttribute(threadName)}" />`
643
+ const { username, userId, messageId, threadId, threadName, attachments, localFiles } = parsed.data
644
+ const files = attachments ? attachmentsBlock(attachments) : localFilesBlock(localFiles ?? [])
645
+ return `${files}<discord-user name="${escapeAttribute(username)}" user-id="${userId}" message-id="${messageId}" thread-id="${threadId}" thread-name="${escapeAttribute(threadName)}" />`
668
646
  }
669
647
 
670
648
  export type VoiceCallUser = { id: string; username: string }
@@ -913,7 +891,8 @@ function voiceCallReference({
913
891
  kimaki project list --json # projects with channel ID, folder and guild ID
914
892
  kimaki session list --project <folder> # threads of a project with status busy, waiting or idle
915
893
  kimaki session list --all --active # threads that run now, in every project
916
- kimaki session search 'text' --all --days 0 # find threads by title or content, all time
894
+ rg -il 'text' ${dataDir}/transcripts # find threads by title or content; file name = session ID
895
+ kimaki session sync --days 60 # write transcripts of older or non-Kimaki sessions first
917
896
  kimaki session read <thread ID or link> 2>/dev/null | tail -c 3000 # end of a thread transcript
918
897
  kimaki session wait <thread ID> # wait until a thread finishes, then print it
919
898
  kimaki session abort <thread ID> # stop the agent of a thread; the thread stays
@@ -39,8 +39,11 @@ const matchers: DeterministicMatcher[] = [
39
39
  {
40
40
  id: 'image',
41
41
  priority: 500,
42
- // The plugin adds <local-files> and <discord-user> to the model request only.
43
- when: { latestUserTextRegex: 'image-marker[\\s\\S]*<local-files>[\\s\\S]*archive\\.zip\\n</local-files>\\n<discord-user ', rawPromptIncludes: 'image/png' },
42
+ // The plugin adds the numbered <attachments> and <discord-user> to the model request only.
43
+ when: {
44
+ latestUserTextRegex: 'image-marker[\\s\\S]*<attachments>[\\s\\S]*1\\. dot\\.png \\(inline\\): [^\\n]*\\n2\\. notes\\.txt: [^\\n]*\\n3\\. archive\\.zip: [^\\n]*\\n</attachments>\\n<discord-user ',
45
+ rawPromptIncludes: 'image/png',
46
+ },
44
47
  then: { parts: textParts('I see an image') },
45
48
  },
46
49
  reply('voice-steer', 'voice steer ok'),
@@ -137,7 +140,7 @@ test('image and text attachments reach the model as files', async () => {
137
140
  // OpenCode drops binary files: the model gets their local paths from metadata, the stored text stays clean.
138
141
  expect(user?.type === 'user' && user.text).toBe('What is this image-marker')
139
142
  const discord = user?.type === 'user' ? user.metadata?.['discord'] : null
140
- expect(JSON.stringify(discord).replaceAll(dataDir, '<data>').replace(/\/\d+\//g, '/<message>/').replace(/\d{6,}/g, 'ID')).toMatchInlineSnapshot(`"{"userId":"ID","username":"tommy","messageId":"ID","threadId":"ID","threadName":"What is this image-marker","localFiles":["<data>/attachments/<message>/1-notes.txt","<data>/attachments/<message>/2-archive.zip"]}"`)
143
+ expect(JSON.stringify(discord).replaceAll(dataDir, '<data>').replace(/\/\d+\//g, '/<message>/').replace(/\d{6,}/g, 'ID')).toMatchInlineSnapshot(`"{"userId":"ID","username":"tommy","messageId":"ID","threadId":"ID","threadName":"What is this image-marker","attachments":[{"name":"dot.png","path":"<data>/attachments/<message>/0-dot.png","inline":true},{"name":"notes.txt","path":"<data>/attachments/<message>/1-notes.txt","inline":false},{"name":"archive.zip","path":"<data>/attachments/<message>/2-archive.zip","inline":false}]}"`)
141
144
  })
142
145
 
143
146
  test('voice in a channel starts a session with the transcription and the spoken agent', async () => {
package/src/voice-call.ts CHANGED
@@ -349,7 +349,7 @@ export function createVoiceCalls({ opencodeConfigDir }: { opencodeConfigDir: str
349
349
  const states = guild.voiceStates.cache.filter((state) => state.channelId === channelId && state.id !== bot.discord.user?.id)
350
350
  const users = new Map<string, Author>()
351
351
  for (const state of states.values()) {
352
- if (!(await canUseKimaki({ guild, userId: state.id }))) continue
352
+ if (!(await canUseKimaki(bot, { guild, userId: state.id }))) continue
353
353
  const user = await bot.discord.users.fetch(state.id).catch(() => null)
354
354
  users.set(state.id, { id: state.id, username: user?.username ?? state.id })
355
355
  }
@@ -1,19 +0,0 @@
1
- // Tells the model its own OpenCode session ID, in every session (TUI, Discord, subagents).
2
- //
3
- // The ID never changes in a session, so the system text is the same on every
4
- // request and the provider prompt cache keeps hitting.
5
- //
6
- // Standalone in plain OpenCode without the bot: list this directory in
7
- // opencode.json `plugins`. The bot then skips its shim, see installPluginShim
8
- // in opencode-server.ts. It imports nothing from Kimaki outside this folder.
9
-
10
- import { Plugin } from '@opencode/plugin'
11
-
12
- export default Plugin.define({
13
- id: 'kimaki-session-id',
14
- async setup(ctx) {
15
- await ctx.session.hook('context', (event) => {
16
- event.system.push({ type: 'text', text: `OpenCode session ID: ${event.sessionID}` })
17
- })
18
- },
19
- })