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.
- package/dist/agent-model.e2e.test.js +6 -4
- package/dist/cli-commands/project.js +148 -24
- package/dist/cli-commands/send.js +48 -3
- package/dist/cli-runner.js +86 -7
- package/dist/cli-runner.test.js +28 -1
- package/dist/commands/btw.js +7 -3
- package/dist/commands/new-worktree.js +14 -2
- package/dist/commands/session.js +66 -13
- package/dist/commands/tasks.js +104 -19
- package/dist/commands/user-command.js +3 -2
- package/dist/database.js +14 -0
- package/dist/db.js +1 -0
- package/dist/discord-bot.js +65 -9
- package/dist/interaction-handler.js +6 -15
- package/dist/message-preprocessing.js +9 -2
- package/dist/queue-advanced-e2e-setup.js +22 -0
- package/dist/queue-delete-message.e2e.test.js +75 -0
- package/dist/schema.js +3 -0
- package/dist/session-handler/thread-runtime-state.js +9 -0
- package/dist/session-handler/thread-session-runtime.js +49 -1
- package/dist/system-message.js +50 -63
- package/dist/system-message.test.js +65 -61
- package/dist/task-runner.js +18 -3
- package/dist/task-schedule.js +4 -0
- package/dist/thread-message-queue.e2e.test.js +4 -4
- package/dist/voice-message.e2e.test.js +1 -1
- package/dist/worktree-lifecycle.e2e.test.js +70 -1
- package/dist/worktrees.js +63 -0
- package/package.json +11 -4
- package/skills/egaki/SKILL.md +26 -80
- package/skills/goke/SKILL.md +27 -20
- package/skills/holocron/SKILL.md +72 -8
- package/skills/new-skill/SKILL.md +87 -0
- package/skills/playwriter/SKILL.md +4 -4
- package/skills/spiceflow/SKILL.md +2 -0
- package/src/agent-model.e2e.test.ts +10 -4
- package/src/cli-commands/project.ts +183 -26
- package/src/cli-commands/send.ts +64 -5
- package/src/cli-runner.test.ts +39 -1
- package/src/cli-runner.ts +115 -6
- package/src/commands/btw.ts +9 -2
- package/src/commands/new-worktree.ts +14 -1
- package/src/commands/session.ts +79 -17
- package/src/commands/tasks.ts +119 -19
- package/src/commands/user-command.ts +3 -2
- package/src/database.ts +24 -0
- package/src/db.ts +1 -0
- package/src/discord-bot.ts +80 -8
- package/src/interaction-handler.ts +7 -19
- package/src/message-preprocessing.ts +11 -2
- package/src/queue-advanced-e2e-setup.ts +23 -0
- package/src/queue-delete-message.e2e.test.ts +102 -0
- package/src/schema.sql +1 -0
- package/src/schema.ts +3 -0
- package/src/session-handler/thread-runtime-state.ts +19 -0
- package/src/session-handler/thread-session-runtime.ts +75 -0
- package/src/system-message.test.ts +73 -61
- package/src/system-message.ts +63 -62
- package/src/task-runner.ts +34 -3
- package/src/task-schedule.ts +6 -0
- package/src/thread-message-queue.e2e.test.ts +4 -4
- package/src/voice-message.e2e.test.ts +1 -1
- package/src/worktree-lifecycle.e2e.test.ts +88 -1
- 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) {
|
package/dist/system-message.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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:
|