kimaki 0.20.1 → 0.22.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 (64) hide show
  1. package/dist/agent-model.e2e.test.js +6 -4
  2. package/dist/cli-commands/project.js +148 -24
  3. package/dist/cli-commands/send.js +48 -3
  4. package/dist/cli-runner.js +86 -7
  5. package/dist/cli-runner.test.js +28 -1
  6. package/dist/commands/btw.js +7 -3
  7. package/dist/commands/new-worktree.js +14 -2
  8. package/dist/commands/session.js +66 -13
  9. package/dist/commands/tasks.js +104 -19
  10. package/dist/commands/user-command.js +3 -2
  11. package/dist/database.js +14 -0
  12. package/dist/db.js +1 -0
  13. package/dist/discord-bot.js +65 -9
  14. package/dist/interaction-handler.js +6 -15
  15. package/dist/message-preprocessing.js +9 -2
  16. package/dist/queue-advanced-e2e-setup.js +22 -0
  17. package/dist/queue-delete-message.e2e.test.js +75 -0
  18. package/dist/schema.js +3 -0
  19. package/dist/session-handler/thread-runtime-state.js +9 -0
  20. package/dist/session-handler/thread-session-runtime.js +49 -1
  21. package/dist/system-message.js +50 -63
  22. package/dist/system-message.test.js +65 -61
  23. package/dist/task-runner.js +18 -3
  24. package/dist/task-schedule.js +4 -0
  25. package/dist/thread-message-queue.e2e.test.js +4 -4
  26. package/dist/voice-message.e2e.test.js +1 -1
  27. package/dist/worktree-lifecycle.e2e.test.js +70 -1
  28. package/dist/worktrees.js +63 -0
  29. package/package.json +11 -4
  30. package/skills/egaki/SKILL.md +26 -80
  31. package/skills/goke/SKILL.md +27 -20
  32. package/skills/holocron/SKILL.md +72 -8
  33. package/skills/new-skill/SKILL.md +87 -0
  34. package/skills/playwriter/SKILL.md +4 -4
  35. package/skills/spiceflow/SKILL.md +2 -0
  36. package/src/agent-model.e2e.test.ts +10 -4
  37. package/src/cli-commands/project.ts +183 -26
  38. package/src/cli-commands/send.ts +64 -5
  39. package/src/cli-runner.test.ts +39 -1
  40. package/src/cli-runner.ts +115 -6
  41. package/src/commands/btw.ts +9 -2
  42. package/src/commands/new-worktree.ts +14 -1
  43. package/src/commands/session.ts +79 -17
  44. package/src/commands/tasks.ts +119 -19
  45. package/src/commands/user-command.ts +3 -2
  46. package/src/database.ts +24 -0
  47. package/src/db.ts +1 -0
  48. package/src/discord-bot.ts +80 -8
  49. package/src/interaction-handler.ts +7 -19
  50. package/src/message-preprocessing.ts +11 -2
  51. package/src/queue-advanced-e2e-setup.ts +23 -0
  52. package/src/queue-delete-message.e2e.test.ts +102 -0
  53. package/src/schema.sql +1 -0
  54. package/src/schema.ts +3 -0
  55. package/src/session-handler/thread-runtime-state.ts +19 -0
  56. package/src/session-handler/thread-session-runtime.ts +75 -0
  57. package/src/system-message.test.ts +73 -61
  58. package/src/system-message.ts +63 -62
  59. package/src/task-runner.ts +34 -3
  60. package/src/task-schedule.ts +6 -0
  61. package/src/thread-message-queue.e2e.test.ts +4 -4
  62. package/src/voice-message.e2e.test.ts +1 -1
  63. package/src/worktree-lifecycle.e2e.test.ts +88 -1
  64. package/src/worktrees.ts +74 -0
@@ -0,0 +1,75 @@
1
+ // E2e tests for deleting queued Discord messages.
2
+ // Covers the MessageDelete path that removes pending local queue items before drain.
3
+ import { describe, test, expect } from 'vitest';
4
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
+ import { waitForBotMessageContaining, waitForFooterMessage, waitForThreadState, } from './test-utils.js';
6
+ const TEXT_CHANNEL_ID = '200000000000001071';
7
+ const e2eTest = describe;
8
+ e2eTest('queue delete message', () => {
9
+ const ctx = setupQueueAdvancedSuite({
10
+ channelId: TEXT_CHANNEL_ID,
11
+ channelName: 'queue-delete-message-e2e',
12
+ dirName: 'queue-delete-message-e2e',
13
+ username: 'queue-delete-tester',
14
+ });
15
+ test('deleting a queued Discord message removes it from queue', async () => {
16
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
17
+ content: 'SLOW_BUSY_MARKER Reply with exactly: delete-queue-setup',
18
+ });
19
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
20
+ timeout: 4_000,
21
+ predicate: (t) => {
22
+ return t.name === 'SLOW_BUSY_MARKER Reply with exactly: delete-queue-setup';
23
+ },
24
+ });
25
+ const th = ctx.discord.thread(thread.id);
26
+ await th.waitForBotReply({ timeout: 4_000 });
27
+ const queuedMsg = await th.user(TEST_USER_ID).sendMessage({
28
+ content: 'Reply with exactly: deleted-queue. queue',
29
+ });
30
+ await waitForThreadState({
31
+ threadId: thread.id,
32
+ predicate: (state) => {
33
+ return state.queueItems.some((item) => {
34
+ return item.sourceMessageId === queuedMsg.id;
35
+ });
36
+ },
37
+ timeout: 4_000,
38
+ description: 'queue has item to delete',
39
+ });
40
+ await th.user(TEST_USER_ID).deleteMessage({ messageId: queuedMsg.id });
41
+ await waitForThreadState({
42
+ threadId: thread.id,
43
+ predicate: (state) => {
44
+ return !state.queueItems.some((item) => {
45
+ return item.sourceMessageId === queuedMsg.id;
46
+ });
47
+ },
48
+ timeout: 4_000,
49
+ description: 'queued item removed after Discord delete',
50
+ });
51
+ await waitForBotMessageContaining({
52
+ discord: ctx.discord,
53
+ threadId: thread.id,
54
+ text: 'removed message from queue',
55
+ timeout: 4_000,
56
+ });
57
+ await waitForFooterMessage({
58
+ discord: ctx.discord,
59
+ threadId: thread.id,
60
+ timeout: 8_000,
61
+ });
62
+ expect(await th.text()).toMatchInlineSnapshot(`
63
+ "--- from: user (queue-delete-tester)
64
+ SLOW_BUSY_MARKER Reply with exactly: delete-queue-setup
65
+ --- from: assistant (TestBot)
66
+ *using deterministic-provider/deterministic-v2*
67
+ Queued at position 1. Edit or delete your message to update the queue
68
+ ⬦ **queue-delete-tester** removed message from queue
69
+ ⬥ slow-busy-reply
70
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
71
+ `);
72
+ const finalText = await th.text();
73
+ expect(finalText).not.toContain('» **queue-delete-tester:** Reply with exactly: deleted-queue');
74
+ }, 12_000);
75
+ });
package/dist/schema.js CHANGED
@@ -23,6 +23,9 @@ export const thread_sessions = sqliteCore.sqliteTable('thread_sessions', {
23
23
  session_id: sqliteCore.text('session_id').notNull(),
24
24
  source: sqliteCore.text('source', { enum: ['kimaki', 'external_poll'] }).notNull().default('kimaki'),
25
25
  last_synced_name: sqliteCore.text('last_synced_name'),
26
+ // Parent OpenCode session that spawned this thread via kimaki send --parent-session.
27
+ // Survives bot restarts so child multi-turn system prompts keep the parent ID.
28
+ parent_session_id: sqliteCore.text('parent_session_id'),
26
29
  created_at: datetime('created_at').default(orm.sql `CURRENT_TIMESTAMP`),
27
30
  });
28
31
  export const session_events = sqliteCore.sqliteTable('session_events', {
@@ -17,6 +17,7 @@ export function initialThreadState() {
17
17
  sessionId: undefined,
18
18
  sessionUsername: undefined,
19
19
  sessionUserId: undefined,
20
+ parentSessionId: undefined,
20
21
  queueItems: [],
21
22
  sentPartIds: new Set(),
22
23
  };
@@ -77,6 +78,14 @@ export function setSessionUserId(threadId, userId) {
77
78
  return { ...t, sessionUserId: userId };
78
79
  });
79
80
  }
81
+ export function setParentSessionId(threadId, parentSessionId) {
82
+ updateThread(threadId, (t) => {
83
+ if (t.parentSessionId) {
84
+ return t;
85
+ }
86
+ return { ...t, parentSessionId };
87
+ });
88
+ }
80
89
  export function enqueueItem(threadId, item) {
81
90
  updateThread(threadId, (t) => ({
82
91
  ...t,
@@ -17,7 +17,7 @@ import { registerEventListener, unregisterEventListener, } from './global-event-
17
17
  import { createLogger, LogPrefix } from '../logger.js';
18
18
  import { sendThreadMessage, SILENT_MESSAGE_FLAGS, NOTIFY_MESSAGE_FLAGS, } from '../discord-utils.js';
19
19
  import { formatPart } from '../message-formatting.js';
20
- import { getChannelVerbosity, getPartMessageIds, getDb, setPartMessage, getThreadSession, setThreadSession, getThreadWorktreeOrWorkspace, setSessionAgent, clearSessionModel, getVariantCascade, setSessionStartSource, appendSessionEventsSinceLastTimestamp, getSessionEventSnapshot, } from '../database.js';
20
+ import { getChannelVerbosity, getPartMessageIds, getDb, setPartMessage, getThreadSession, setThreadSession, getThreadParentSessionId, setThreadParentSessionId, getThreadWorktreeOrWorkspace, setSessionAgent, clearSessionModel, getVariantCascade, setSessionStartSource, appendSessionEventsSinceLastTimestamp, getSessionEventSnapshot, } from '../database.js';
21
21
  import * as orm from 'drizzle-orm';
22
22
  import * as schema from '../schema.js';
23
23
  import { showPermissionButtons, addPermissionRequestToContext, arePatternsCoveredBy, pendingPermissionContexts, } from '../commands/permissions.js';
@@ -2274,6 +2274,7 @@ export class ThreadSessionRuntime {
2274
2274
  agents: availableAgents,
2275
2275
  username: this.state?.sessionUsername || input.username,
2276
2276
  userId: this.state?.sessionUserId || input.userId,
2277
+ parentSessionId: this.state?.parentSessionId || input.parentSessionId,
2277
2278
  }),
2278
2279
  ...(resolvedAgent ? { agent: resolvedAgent } : {}),
2279
2280
  ...(modelField ? { model: modelField } : {}),
@@ -2321,6 +2322,7 @@ export class ThreadSessionRuntime {
2321
2322
  permissions: input.permissions,
2322
2323
  permissionRules: input.permissionRules,
2323
2324
  injectionGuardPatterns: input.injectionGuardPatterns,
2325
+ parentSessionId: input.parentSessionId,
2324
2326
  sourceMessageId: input.sourceMessageId,
2325
2327
  sourceThreadId: input.sourceThreadId,
2326
2328
  repliedMessage: input.repliedMessage,
@@ -2363,6 +2365,9 @@ export class ThreadSessionRuntime {
2363
2365
  async enqueueIncoming(input) {
2364
2366
  threadState.setSessionUsername(this.threadId, input.username);
2365
2367
  threadState.setSessionUserId(this.threadId, input.userId);
2368
+ await this.ensureParentSessionId({
2369
+ parentSessionId: input.parentSessionId,
2370
+ });
2366
2371
  // When a preprocessor is provided, we must resolve it inside
2367
2372
  // dispatchAction before we know the final mode for routing.
2368
2373
  if (input.preprocess) {
@@ -2383,6 +2388,33 @@ export class ThreadSessionRuntime {
2383
2388
  }
2384
2389
  return this.submitViaOpencodeQueue(input);
2385
2390
  }
2391
+ /**
2392
+ * Resolve parent session ID for child system prompts.
2393
+ * Prefer in-memory state, then SQLite, then the ingress marker.
2394
+ * Persist once so multi-turn child sessions keep the parent after restart.
2395
+ */
2396
+ async ensureParentSessionId({ parentSessionId, }) {
2397
+ if (this.state?.parentSessionId) {
2398
+ return;
2399
+ }
2400
+ const storedParentSessionId = await getThreadParentSessionId(this.threadId);
2401
+ if (storedParentSessionId) {
2402
+ threadState.setParentSessionId(this.threadId, storedParentSessionId);
2403
+ return;
2404
+ }
2405
+ if (!parentSessionId) {
2406
+ return;
2407
+ }
2408
+ threadState.setParentSessionId(this.threadId, parentSessionId);
2409
+ // Row may not exist yet on first ingress before ensureSession creates it.
2410
+ // Best-effort write; ensureSession path also persists after setThreadSession.
2411
+ await setThreadParentSessionId({
2412
+ threadId: this.threadId,
2413
+ parentSessionId,
2414
+ }).catch((error) => {
2415
+ logger.warn(`[PARENT SESSION] Failed to persist parent session for thread ${this.threadId}: ${error instanceof Error ? error.message : String(error)}`);
2416
+ });
2417
+ }
2386
2418
  /**
2387
2419
  * Serialize the preprocess callback via a lightweight promise chain, then
2388
2420
  * route the resolved input through the normal enqueue paths.
@@ -2586,6 +2618,10 @@ export class ThreadSessionRuntime {
2586
2618
  return { found: true, removed: true };
2587
2619
  return { found: true, removed: false };
2588
2620
  }
2621
+ /** Remove a queued message identified by its Discord source message ID. */
2622
+ removeQueuedMessage(sourceMessageId) {
2623
+ return threadState.updateQueueItemBySourceMessageId(this.threadId, sourceMessageId, () => null);
2624
+ }
2589
2625
  // ── Queue Drain ─────────────────────────────────────────────
2590
2626
  /**
2591
2627
  * Check if we can dispatch the next queued message. If so, dequeue and
@@ -2964,6 +3000,7 @@ export class ThreadSessionRuntime {
2964
3000
  agents: earlyAvailableAgents,
2965
3001
  username: this.state?.sessionUsername || input.username,
2966
3002
  userId: this.state?.sessionUserId || input.userId,
3003
+ parentSessionId: this.state?.parentSessionId || input.parentSessionId,
2967
3004
  }),
2968
3005
  model: earlyModelParam,
2969
3006
  agent: earlyAgentPreference,
@@ -3103,6 +3140,17 @@ export class ThreadSessionRuntime {
3103
3140
  // Store session in DB and thread state
3104
3141
  await setThreadSession(this.thread.id, session.id);
3105
3142
  threadState.setSessionId(this.threadId, session.id);
3143
+ // Parent may have been set on ingress before the thread_sessions row
3144
+ // existed; write it now that the row is guaranteed.
3145
+ const parentSessionId = this.state?.parentSessionId;
3146
+ if (parentSessionId) {
3147
+ await setThreadParentSessionId({
3148
+ threadId: this.thread.id,
3149
+ parentSessionId,
3150
+ }).catch((error) => {
3151
+ logger.warn(`[PARENT SESSION] Failed to persist parent session for thread ${this.threadId}: ${error instanceof Error ? error.message : String(error)}`);
3152
+ });
3153
+ }
3106
3154
  await this.hydrateSessionEventsFromDatabase({ sessionId: session.id });
3107
3155
  // Store session start source for scheduled tasks
3108
3156
  if (createdNewSession && sessionStartScheduleKind) {
@@ -78,45 +78,6 @@ No code is stored permanently, diffs are ephemeral. The tool and website are ful
78
78
  If the user asks about critique or expresses concern about their code being uploaded,
79
79
  reassure them: their data is safe, URLs are unique and not indexed, and they can disable
80
80
  this feature by restarting kimaki with the \`--no-critique\` flag.
81
-
82
- ### reviewing diffs with AI
83
-
84
- \`bunx critique review --web\` generates an AI-powered review of a diff and uploads it as a shareable URL.
85
- It spawns a separate opencode session that analyzes the diff, groups related changes, and produces
86
- a structured review with explanations, diagrams, and suggestions. This is useful when the user
87
- asks you to explain or review a diff — the output is much richer than a plain diff URL.
88
-
89
- **WARNING: This command is very slow (up to 20 minutes for large diffs).** Only run it when the
90
- user explicitly asks for a code review or diff explanation. Always warn the user it will take
91
- a while before running it. Set Bash tool timeout to at least 25 minutes (\`timeout: 1_500_000\`).
92
-
93
- Always pass \`--agent opencode\` and \`--session ${sessionId}\` so the reviewer has context about
94
- why the changes were made. If you know other session IDs that produced the diff (e.g. from
95
- \`kimaki session list\` or from the thread history), pass them too with additional \`--session\` flags.
96
-
97
- Examples:
98
-
99
- \`\`\`bash
100
- # Review working tree changes
101
- bunx critique review --web --agent opencode --session ${sessionId}
102
-
103
- # Review staged changes
104
- bunx critique review --staged --web --agent opencode --session ${sessionId}
105
-
106
- # Review a specific commit
107
- bunx critique review --commit HEAD --web --agent opencode --session ${sessionId}
108
-
109
- # Review branch changes compared to main
110
- bunx critique review main...HEAD --web --agent opencode --session ${sessionId}
111
-
112
- # Review with multiple session contexts (current + the session that made the changes)
113
- bunx critique review --commit abc1234 --web --agent opencode --session ${sessionId} --session ses_other_session_id
114
-
115
- # Review only specific files
116
- bunx critique review --web --agent opencode --session ${sessionId} --filter "src/**/*.ts"
117
- \`\`\`
118
-
119
- The command prints a preview URL when done — share that URL with the user.
120
81
  `;
121
82
  }
122
83
  const KIMAKI_TUNNEL_INSTRUCTIONS = `
@@ -262,8 +223,12 @@ ${escapePromptText(repliedMessage.text)}
262
223
  // model concatenates message parts.
263
224
  return `${sections.join('\n\n')}\n`;
264
225
  }
265
- export function getOpencodeSystemMessage({ sessionId, channelId, guildId, threadId, channelTopic, agents, userId, }) {
226
+ export function getOpencodeSystemMessage({ sessionId, channelId, guildId, threadId, channelTopic, agents, userId, parentSessionId, }) {
266
227
  const userArg = ` --user '${userId || '<discord-user-id>'}'`;
228
+ const parentSessionArg = ` --parent-session ${sessionId}`;
229
+ // Prefer thread ID for cross-machine compatibility; fall back to session ID.
230
+ const archiveTarget = threadId ? `${threadId} (or --session ${sessionId})` : `--session ${sessionId}`;
231
+ const sendToSelfTarget = threadId ? `--thread ${threadId} (or --session ${sessionId})` : `--session ${sessionId}`;
267
232
  const topicContext = channelTopic?.trim()
268
233
  ? `\n\n<channel-topic>\n${channelTopic.trim()}\n</channel-topic>`
269
234
  : '';
@@ -274,6 +239,11 @@ export function getOpencodeSystemMessage({ sessionId, channelId, guildId, thread
274
239
  })
275
240
  .join('\n')}`
276
241
  : '';
242
+ // Opt-in only. Empty by default so /btw, task subagents, and normal sessions
243
+ // keep the same system prompt prefix as their parent for cache hits.
244
+ const parentSessionContext = parentSessionId
245
+ ? `\nYour parent OpenCode session ID is: ${parentSessionId}\nYou can send a message back to the parent session with:\nkimaki send --session ${parentSessionId} --prompt 'your update here' --agent <current_agent>\nDo NOT message the parent session unless the user explicitly asks you to.`
246
+ : '';
277
247
  return `
278
248
  The user is reading your messages from inside Discord, via kimaki.dev
279
249
 
@@ -296,7 +266,7 @@ interface BashToolInput {
296
266
  \`description\` is shown to the user in Discord as a summary of the bash call.
297
267
  \`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
298
268
 
299
- Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}
269
+ Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}${parentSessionContext}
300
270
 
301
271
  Per-turn Discord metadata like the current user and current agent is delivered in synthetic user message parts.
302
272
 
@@ -353,7 +323,7 @@ To ask the user to upload files from their device, use the \`kimaki_file_upload\
353
323
 
354
324
  To archive the current Discord thread (hide it from sidebar) and stop the session, run:
355
325
 
356
- kimaki session archive --session ${sessionId}
326
+ kimaki session archive ${archiveTarget}
357
327
 
358
328
  Only do this when the user explicitly asks to close or archive the thread, and only after your final message.
359
329
 
@@ -382,9 +352,10 @@ ${channelId
382
352
 
383
353
  To start a new thread/session in this channel pro-grammatically, run:
384
354
 
385
- kimaki send --channel ${channelId} --prompt 'your prompt here' --agent <current_agent>${userArg}
355
+ kimaki send --channel ${channelId} --prompt 'your prompt here' --agent <current_agent>${parentSessionArg}${userArg}
386
356
 
387
357
  You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
358
+ ALWAYS pass \`--parent-session ${sessionId}\` (your current session ID) when starting a new session from this one. The child system message will include the parent session ID so it can message back only if the user asks.
388
359
  Prefer passing the current agent with \`--agent <current_agent>\` so spawned or scheduled sessions keep the same agent unless you are intentionally switching. Replace \`<current_agent>\` with the value from the per-turn \`Current agent\` reminder.
389
360
  When writing \`kimaki send\` shell commands, use single quotes around \`--prompt\`, \`--user\`, \`--send-at\`, and other literal arguments so backticks inside prompts are not interpreted by the shell. Prefer \`--user '<discord-user-id>'\` over \`--user 'name'\` because name lookup depends on optional Server Members Intent.
390
361
 
@@ -398,13 +369,13 @@ To send a prompt to an existing thread instead of creating a new one:
398
369
 
399
370
  kimaki send --thread <thread_id> --prompt 'follow-up prompt' --agent <current_agent>
400
371
 
401
- Use this when you already have the Discord thread ID.
372
+ Use this when you already have the Discord thread ID. Prefer \`--thread\` over \`--session\` because thread IDs work across machines while session IDs only resolve on the machine that created the session.
402
373
 
403
- To send to the thread associated with a known session:
374
+ To send to the thread associated with a known session (same machine only):
404
375
 
405
376
  kimaki send --session <session_id> --prompt 'follow-up prompt' --agent <current_agent>
406
377
 
407
- Use this when you have the OpenCode session ID.
378
+ Use this when you only have the OpenCode session ID and the session was created on this machine.
408
379
 
409
380
  Use --notify-only to create a notification thread without starting an AI session:
410
381
 
@@ -412,26 +383,32 @@ kimaki send --channel ${channelId} --prompt 'User cancelled subscription' --noti
412
383
 
413
384
  Use --user with a Discord user ID or raw mention to add a specific Discord user to the new thread:
414
385
 
415
- kimaki send --channel ${channelId} --prompt 'Review the latest CI failure' --agent <current_agent>${userArg}
386
+ kimaki send --channel ${channelId} --prompt 'Review the latest CI failure' --agent <current_agent>${parentSessionArg}${userArg}
416
387
 
417
388
  Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
418
389
 
419
- kimaki send --channel ${channelId} --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent>${userArg}
390
+ kimaki send --channel ${channelId} --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent>${parentSessionArg}${userArg}
420
391
 
421
392
  Use --cwd to start a session in an existing project subfolder or git worktree directory:
422
393
 
423
- kimaki send --channel ${channelId} --prompt 'Run the restricted task' --cwd /path/to/project/restricted-task --agent <current_agent>${userArg}
394
+ kimaki send --channel ${channelId} --prompt 'Run the restricted task' --cwd /path/to/project/restricted-task --agent <current_agent>${parentSessionArg}${userArg}
424
395
 
425
396
  Important:
397
+ - ALWAYS pass \`--parent-session ${sessionId}\` when spawning a new session from this one so the child knows who started it.
426
398
  - NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
427
399
  - Use \`--cwd\` to reuse an existing project subfolder or worktree directory. Use \`--worktree\` to create a new worktree.
428
400
  - The prompt passed to \`--worktree\` is the task for the new thread running inside that worktree.
429
401
  - Do NOT tell that prompt to "create a new worktree" again, or it can create recursive worktree threads.
430
402
  - Ask the new session to operate on its current checkout only (e.g. "validate current worktree", "run checks in this repo").
431
403
 
404
+ Use --file to attach local files (images, text files, PDFs) to the message:
405
+
406
+ kimaki send --channel ${channelId} --prompt 'Review this screenshot' --file /path/to/screenshot.png --agent <current_agent>${parentSessionArg}${userArg}
407
+ kimaki send --thread <thread_id> --prompt 'Here is the error log' --file ./error.log --file ./stack-trace.txt --agent <current_agent>
408
+
432
409
  Use --agent to specify which agent to use for the session:
433
410
 
434
- kimaki send --channel ${channelId} --prompt 'Plan the refactor of the auth module' --agent plan${userArg}
411
+ kimaki send --channel ${channelId} --prompt 'Plan the refactor of the auth module' --agent plan${parentSessionArg}${userArg}
435
412
  ${availableAgentsContext}
436
413
 
437
414
  ## running opencode commands via kimaki send
@@ -439,7 +416,7 @@ ${availableAgentsContext}
439
416
  You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
440
417
 
441
418
  kimaki send --thread <thread_id> --prompt '/review fix the auth module' --agent <current_agent>
442
- kimaki send --channel ${channelId} --prompt '/build-cmd update dependencies' --agent <current_agent>${userArg}
419
+ kimaki send --channel ${channelId} --prompt '/build-cmd update dependencies' --agent <current_agent>${parentSessionArg}${userArg}
443
420
 
444
421
  The command name must match a registered opencode command. If the command is not recognized, the prompt is sent as plain text to the model. This works for both new threads (\`--channel\`) and existing threads (\`--thread\`/\`--session\`).
445
422
 
@@ -455,8 +432,8 @@ kimaki send --thread <thread_id> --prompt '/<agentname>-agent' --agent <current_
455
432
 
456
433
  Use \`--send-at\` to schedule a one-time or recurring task:
457
434
 
458
- kimaki send --channel ${channelId} --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent>${userArg}
459
- kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${userArg}
435
+ kimaki send --channel ${channelId} --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent>${parentSessionArg}${userArg}
436
+ kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}${userArg}
460
437
 
461
438
  ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
462
439
  When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
@@ -465,6 +442,7 @@ When the user specifies a time without a timezone, ask them to confirm their tim
465
442
  - \`--notify-only\` to create a reminder thread without auto-starting a session
466
443
  - \`--worktree\` to create the scheduled thread as a worktree session (only if the user explicitly asks for a worktree)
467
444
  - \`--agent\` and \`--model\` to control scheduled session behavior
445
+ - \`--parent-session\` to pass this session as parent of the scheduled child
468
446
  - \`--user\` to add a specific user to the scheduled thread
469
447
 
470
448
  \`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
@@ -480,7 +458,7 @@ Notification strategy for scheduled tasks:
480
458
  - Without \`--user\`, there is no guaranteed direct user mention path; task output should mention users only when relevant.
481
459
  - With \`--user\`, the user is added to the thread and may receive more frequent thread-level notifications.
482
460
  - If a scheduled task completes with no actionable result and no user-visible change, prefer archiving the session after the final message so Discord does not keep a no-op thread highlighted.
483
- - Example no-op cleanup command: \`kimaki session archive --session ${sessionId}\`
461
+ - Example no-op cleanup command: \`kimaki session archive ${archiveTarget}\`
484
462
 
485
463
  Manage scheduled tasks with:
486
464
 
@@ -496,10 +474,10 @@ Use case patterns:
496
474
  - Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention the user via Discord ID only when failures require review".
497
475
  - Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
498
476
  - Recurring maintenance: use cron \`--send-at\` for repetitive tasks like rotating secrets, checking dependency updates, running security audits, or cleaning up stale branches. Example: \`--send-at "0 9 1 * *"\` to run on the 1st of every month.
499
- - Quiet no-op checks: if a recurring task checks something and finds nothing to report, let it post a brief final summary and then archive the session with \`kimaki session archive --session ${sessionId}\`. Example: a scheduled email triage run that finds no new emails should archive itself so it does not add noise to Discord.
477
+ - Quiet no-op checks: if a recurring task checks something and finds nothing to report, let it post a brief final summary and then archive the session with \`kimaki session archive ${archiveTarget}\`. Example: a scheduled email triage run that finds no new emails should archive itself so it does not add noise to Discord.
500
478
  - Thread reminders: when the user says "remind me about this in 2 hours" (or any duration), use \`--send-at\` with \`--thread\` to resurface the current thread. Compute the future UTC time and send a mention so Discord shows a notification:
501
479
 
502
- kimaki send --session ${sessionId} --prompt 'Reminder: <@USER_ID> you asked to be reminded about this thread.' --send-at '<future_UTC_time>' --notify-only --agent <current_agent>
480
+ kimaki send ${sendToSelfTarget} --prompt 'Reminder: <@USER_ID> you asked to be reminded about this thread.' --send-at '<future_UTC_time>' --notify-only --agent <current_agent>
503
481
 
504
482
  Replace \`<future_UTC_time>\` with the computed UTC ISO timestamp. The \`--notify-only\` flag creates just a notification message without starting a new AI session. The \`<@userId>\` mention ensures the user gets a Discord notification.
505
483
 
@@ -514,7 +492,7 @@ ONLY create worktrees when the user explicitly asks for one. Never proactively u
514
492
  When the user asks to "create a worktree" or "make a worktree", they mean you should use the kimaki CLI to create it. Do NOT use raw \`git worktree add\` commands. Instead use:
515
493
 
516
494
  \`\`\`bash
517
- kimaki send --channel ${channelId} --prompt 'your task description' --worktree worktree-name --agent <current_agent>${userArg}
495
+ kimaki send --channel ${channelId} --prompt 'your task description' --worktree worktree-name --agent <current_agent>${parentSessionArg}${userArg}
518
496
  \`\`\`
519
497
 
520
498
  This creates a new Discord thread with an isolated git worktree and starts a session in it. The worktree name should be kebab-case and descriptive of the task.
@@ -530,7 +508,7 @@ Critical recursion guard:
530
508
  Use \`--cwd\` to start a session in an existing project subfolder or git worktree directory instead of the project root:
531
509
 
532
510
  \`\`\`bash
533
- kimaki send --channel ${channelId} --prompt 'Run restricted task X' --cwd /path/to/project/restricted-task --agent <current_agent>${userArg}
511
+ kimaki send --channel ${channelId} --prompt 'Run restricted task X' --cwd /path/to/project/restricted-task --agent <current_agent>${parentSessionArg}${userArg}
534
512
  \`\`\`
535
513
 
536
514
  The path must be inside the project or be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses that path as its working directory, so subfolder \`opencode.json\` config can apply. Passing the project root itself is allowed and behaves like the default. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing directory.
@@ -544,7 +522,7 @@ This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
544
522
  When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
545
523
 
546
524
  \`\`\`bash
547
- kimaki send --channel ${channelId} --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent>${userArg}
525
+ kimaki send --channel ${channelId} --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent>${parentSessionArg}${userArg}
548
526
  \`\`\`
549
527
 
550
528
  The command automatically handles long prompts (over 2000 chars) by sending them as file attachments. With \`--notify-only\`, long prompts are split into multiple messages instead so the content is directly visible.
@@ -586,7 +564,7 @@ Then use grep/read tools on the file to find what you need.
586
564
 
587
565
  When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
588
566
 
589
- When the user uses \`#project-name\` syntax, they usually mean a Kimaki project channel. Use \`kimaki project list --json\` to resolve the \`channel_name\` to its repo working directory. Try the lookup yourself before acting, for example filter by \`channel_name\` with jq: \`kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name") | .directory'\`.
567
+ When the user uses \`#project-name\` syntax, they usually mean a Kimaki project channel. Use \`kimaki project list --json\` to resolve the \`channel_name\` to its repo working directory. The JSON output includes \`guild_id\` and \`guild_name\` to distinguish channels with the same name across different servers. When duplicates exist, prefer filtering by \`guild_id\` (stable) over \`guild_name\` (mutable): \`kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name" and .guild_id == "123456") | .channel_id'\`.
590
568
 
591
569
  When the user uses \`#Some Thread Title\` with spaces, they mean a **thread title**, not a project channel. Find the session by searching across projects, then read the session markdown:
592
570
 
@@ -601,16 +579,25 @@ kimaki session read <sessionId> > ./tmp/session.md 2>/dev/null
601
579
  If you don't know which project the thread belongs to, try each project from \`kimaki project list --json\`.
602
580
 
603
581
  \`\`\`bash
604
- # List all registered projects with their channel IDs
582
+ # List all registered projects with their channel IDs and guild names
605
583
  kimaki project list
606
- kimaki project list --json # machine-readable output
607
- kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name") | .directory'
584
+ kimaki project list --json # machine-readable output with guild_id, guild_name, is_local
585
+
586
+ # Include projects from other machines (scans Kimaki category in Discord)
587
+ kimaki project list --all
588
+ kimaki project list --all --json # remote projects have is_local: false and directory: null
589
+
590
+ # Resolve by channel name (prefer adding guild_name filter if duplicates exist)
591
+ kimaki project list --json | jq -r '.[] | select(.channel_name == "project-name") | .channel_id + " " + .guild_name + " " + .directory'
608
592
 
609
593
  # Create a new project in ~/.kimaki/projects/<name> (folder + git init + Discord channel)
610
594
  kimaki project create my-new-app
611
595
 
612
596
  # Add an existing directory as a project
613
597
  kimaki project add /path/to/repo
598
+
599
+ # Remove a stale or duplicate channel mapping (local DB only, does not delete Discord channel)
600
+ kimaki project remove <channel_id>
614
601
  \`\`\`
615
602
 
616
603
  To send a task to another project: