@otto-assistant/bridge 0.4.97 → 0.4.101
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 +7 -1
- package/dist/anthropic-auth-plugin.js +227 -176
- package/dist/cli-send-thread.e2e.test.js +4 -7
- package/dist/cli.js +2 -2
- package/dist/commands/login.js +6 -4
- package/dist/commands/screenshare.js +1 -1
- package/dist/commands/screenshare.test.js +2 -2
- package/dist/commands/vscode.js +269 -0
- package/dist/context-awareness-plugin.js +8 -38
- package/dist/db.js +1 -0
- package/dist/discord-command-registration.js +5 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +2 -2
- package/dist/interaction-handler.js +4 -0
- package/dist/kimaki-opencode-plugin.js +3 -1
- package/dist/memory-overview-plugin.js +126 -0
- package/dist/system-message.js +23 -22
- package/dist/system-message.test.js +23 -22
- package/dist/system-prompt-drift-plugin.js +41 -11
- package/dist/utils.js +1 -1
- package/package.json +1 -1
- package/src/agent-model.e2e.test.ts +8 -1
- package/src/anthropic-auth-plugin.ts +574 -451
- package/src/cli-send-thread.e2e.test.ts +6 -7
- package/src/cli.ts +2 -2
- package/src/commands/login.ts +6 -4
- package/src/commands/screenshare.test.ts +2 -2
- package/src/commands/screenshare.ts +1 -1
- package/src/commands/vscode.ts +342 -0
- package/src/context-awareness-plugin.ts +11 -42
- package/src/db.ts +1 -0
- package/src/discord-command-registration.ts +7 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +2 -2
- package/src/interaction-handler.ts +5 -0
- package/src/kimaki-opencode-plugin.ts +3 -1
- package/src/memory-overview-plugin.ts +161 -0
- package/src/system-message.test.ts +23 -22
- package/src/system-message.ts +23 -22
- package/src/system-prompt-drift-plugin.ts +48 -12
- package/src/utils.ts +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// OpenCode plugin that snapshots the MEMORY.md heading overview once per
|
|
2
|
+
// session and injects that frozen snapshot on the first real user message.
|
|
3
|
+
// The snapshot is cached by session ID so later MEMORY.md edits do not change
|
|
4
|
+
// the prompt for the same session and do not invalidate OpenCode's cache.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import type { Plugin } from '@opencode-ai/plugin'
|
|
10
|
+
import * as errore from 'errore'
|
|
11
|
+
import {
|
|
12
|
+
createPluginLogger,
|
|
13
|
+
formatPluginErrorWithStack,
|
|
14
|
+
setPluginLogFilePath,
|
|
15
|
+
} from './plugin-logger.js'
|
|
16
|
+
import { condenseMemoryMd } from './condense-memory.js'
|
|
17
|
+
import { initSentry, notifyError } from './sentry.js'
|
|
18
|
+
|
|
19
|
+
const logger = createPluginLogger('OPENCODE')
|
|
20
|
+
|
|
21
|
+
type SessionState = {
|
|
22
|
+
hasFrozenOverview: boolean
|
|
23
|
+
frozenOverviewText: string | null
|
|
24
|
+
injected: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createSessionState(): SessionState {
|
|
28
|
+
return {
|
|
29
|
+
hasFrozenOverview: false,
|
|
30
|
+
frozenOverviewText: null,
|
|
31
|
+
injected: false,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildMemoryOverviewReminder({ condensed }: { condensed: string }): string {
|
|
36
|
+
return `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function freezeMemoryOverview({
|
|
40
|
+
directory,
|
|
41
|
+
state,
|
|
42
|
+
}: {
|
|
43
|
+
directory: string
|
|
44
|
+
state: SessionState
|
|
45
|
+
}): Promise<string | null> {
|
|
46
|
+
if (state.hasFrozenOverview) {
|
|
47
|
+
return state.frozenOverviewText
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const memoryPath = path.join(directory, 'MEMORY.md')
|
|
51
|
+
const memoryContentResult = await fs.promises.readFile(memoryPath, 'utf-8').catch(() => {
|
|
52
|
+
return null
|
|
53
|
+
})
|
|
54
|
+
if (!memoryContentResult) {
|
|
55
|
+
state.hasFrozenOverview = true
|
|
56
|
+
state.frozenOverviewText = null
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const condensed = condenseMemoryMd(memoryContentResult)
|
|
61
|
+
state.hasFrozenOverview = true
|
|
62
|
+
state.frozenOverviewText = buildMemoryOverviewReminder({ condensed })
|
|
63
|
+
return state.frozenOverviewText
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const memoryOverviewPlugin: Plugin = async ({ directory }) => {
|
|
67
|
+
initSentry()
|
|
68
|
+
|
|
69
|
+
const dataDir = process.env.KIMAKI_DATA_DIR
|
|
70
|
+
if (dataDir) {
|
|
71
|
+
setPluginLogFilePath(dataDir)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sessions = new Map<string, SessionState>()
|
|
75
|
+
|
|
76
|
+
function getOrCreateSessionState({ sessionID }: { sessionID: string }): SessionState {
|
|
77
|
+
const existing = sessions.get(sessionID)
|
|
78
|
+
if (existing) {
|
|
79
|
+
return existing
|
|
80
|
+
}
|
|
81
|
+
const state = createSessionState()
|
|
82
|
+
sessions.set(sessionID, state)
|
|
83
|
+
return state
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
'chat.message': async (input, output) => {
|
|
88
|
+
const result = await errore.tryAsync({
|
|
89
|
+
try: async () => {
|
|
90
|
+
const state = getOrCreateSessionState({ sessionID: input.sessionID })
|
|
91
|
+
if (state.injected) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const firstPart = output.parts.find((part) => {
|
|
96
|
+
if (part.type !== 'text') {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
return part.synthetic !== true
|
|
100
|
+
})
|
|
101
|
+
if (!firstPart || firstPart.type !== 'text' || firstPart.text.trim().length === 0) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const overviewText = await freezeMemoryOverview({ directory, state })
|
|
106
|
+
state.injected = true
|
|
107
|
+
if (!overviewText) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
output.parts.push({
|
|
112
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
113
|
+
sessionID: input.sessionID,
|
|
114
|
+
messageID: firstPart.messageID,
|
|
115
|
+
type: 'text' as const,
|
|
116
|
+
text: overviewText,
|
|
117
|
+
synthetic: true,
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
catch: (error) => {
|
|
121
|
+
return new Error('memory overview chat.message hook failed', {
|
|
122
|
+
cause: error,
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
if (!(result instanceof Error)) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
logger.warn(
|
|
130
|
+
`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`,
|
|
131
|
+
)
|
|
132
|
+
void notifyError(result, 'memory overview plugin chat.message hook failed')
|
|
133
|
+
},
|
|
134
|
+
event: async ({ event }) => {
|
|
135
|
+
const result = await errore.tryAsync({
|
|
136
|
+
try: async () => {
|
|
137
|
+
if (event.type !== 'session.deleted') {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const id = event.properties?.info?.id
|
|
141
|
+
if (!id) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
sessions.delete(id)
|
|
145
|
+
},
|
|
146
|
+
catch: (error) => {
|
|
147
|
+
return new Error('memory overview event hook failed', {
|
|
148
|
+
cause: error,
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
if (!(result instanceof Error)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`)
|
|
156
|
+
void notifyError(result, 'memory overview plugin event hook failed')
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { memoryOverviewPlugin }
|
|
@@ -23,7 +23,7 @@ describe('system-message', () => {
|
|
|
23
23
|
}).replace(/`[^`]*\/kimaki\.log`/, '`<data-dir>/kimaki.log`'),
|
|
24
24
|
).toMatchInlineSnapshot(`
|
|
25
25
|
"
|
|
26
|
-
The user is reading your messages from inside Discord, via kimaki.
|
|
26
|
+
The user is reading your messages from inside Discord, via kimaki.dev
|
|
27
27
|
|
|
28
28
|
## bash tool
|
|
29
29
|
|
|
@@ -92,39 +92,40 @@ describe('system-message', () => {
|
|
|
92
92
|
|
|
93
93
|
To start a new thread/session in this channel pro-grammatically, run:
|
|
94
94
|
|
|
95
|
-
kimaki send --channel chan_123 --prompt "your prompt here" --user "Tommy"
|
|
95
|
+
kimaki send --channel chan_123 --prompt "your prompt here" --agent <current_agent> --user "Tommy"
|
|
96
96
|
|
|
97
97
|
You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
|
|
98
|
+
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.
|
|
98
99
|
|
|
99
100
|
IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
|
|
100
101
|
|
|
101
102
|
To send a prompt to an existing thread instead of creating a new one:
|
|
102
103
|
|
|
103
|
-
kimaki send --thread <thread_id> --prompt "follow-up prompt"
|
|
104
|
+
kimaki send --thread <thread_id> --prompt "follow-up prompt" --agent <current_agent>
|
|
104
105
|
|
|
105
106
|
Use this when you already have the Discord thread ID.
|
|
106
107
|
|
|
107
108
|
To send to the thread associated with a known session:
|
|
108
109
|
|
|
109
|
-
kimaki send --session <session_id> --prompt "follow-up prompt"
|
|
110
|
+
kimaki send --session <session_id> --prompt "follow-up prompt" --agent <current_agent>
|
|
110
111
|
|
|
111
112
|
Use this when you have the OpenCode session ID.
|
|
112
113
|
|
|
113
114
|
Use --notify-only to create a notification thread without starting an AI session:
|
|
114
115
|
|
|
115
|
-
kimaki send --channel chan_123 --prompt "User cancelled subscription" --notify-only --user "Tommy"
|
|
116
|
+
kimaki send --channel chan_123 --prompt "User cancelled subscription" --notify-only --agent <current_agent> --user "Tommy"
|
|
116
117
|
|
|
117
118
|
Use --user to add a specific Discord user to the new thread:
|
|
118
119
|
|
|
119
|
-
kimaki send --channel chan_123 --prompt "Review the latest CI failure" --user "Tommy"
|
|
120
|
+
kimaki send --channel chan_123 --prompt "Review the latest CI failure" --agent <current_agent> --user "Tommy"
|
|
120
121
|
|
|
121
122
|
Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
|
|
122
123
|
|
|
123
|
-
kimaki send --channel chan_123 --prompt "Add dark mode support" --worktree dark-mode --user "Tommy"
|
|
124
|
+
kimaki send --channel chan_123 --prompt "Add dark mode support" --worktree dark-mode --agent <current_agent> --user "Tommy"
|
|
124
125
|
|
|
125
126
|
Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
|
|
126
127
|
|
|
127
|
-
kimaki send --channel chan_123 --prompt "Continue work on feature" --cwd /path/to/existing-worktree --user "Tommy"
|
|
128
|
+
kimaki send --channel chan_123 --prompt "Continue work on feature" --cwd /path/to/existing-worktree --agent <current_agent> --user "Tommy"
|
|
128
129
|
|
|
129
130
|
Important:
|
|
130
131
|
- NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
|
|
@@ -146,8 +147,8 @@ describe('system-message', () => {
|
|
|
146
147
|
|
|
147
148
|
You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
|
|
148
149
|
|
|
149
|
-
kimaki send --thread <thread_id> --prompt "/review fix the auth module"
|
|
150
|
-
kimaki send --channel chan_123 --prompt "/build-cmd update dependencies" --user "Tommy"
|
|
150
|
+
kimaki send --thread <thread_id> --prompt "/review fix the auth module" --agent <current_agent>
|
|
151
|
+
kimaki send --channel chan_123 --prompt "/build-cmd update dependencies" --agent <current_agent> --user "Tommy"
|
|
151
152
|
|
|
152
153
|
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\`).
|
|
153
154
|
|
|
@@ -157,14 +158,14 @@ describe('system-message', () => {
|
|
|
157
158
|
|
|
158
159
|
You can also switch agents via \`kimaki send\`:
|
|
159
160
|
|
|
160
|
-
kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
|
|
161
|
+
kimaki send --thread <thread_id> --prompt "/<agentname>-agent" --agent <current_agent>
|
|
161
162
|
|
|
162
163
|
## scheduled sends and task management
|
|
163
164
|
|
|
164
165
|
Use \`--send-at\` to schedule a one-time or recurring task:
|
|
165
166
|
|
|
166
|
-
kimaki send --channel chan_123 --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --user "Tommy"
|
|
167
|
-
kimaki send --channel chan_123 --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --user "Tommy"
|
|
167
|
+
kimaki send --channel chan_123 --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --agent <current_agent> --user "Tommy"
|
|
168
|
+
kimaki send --channel chan_123 --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --agent <current_agent> --user "Tommy"
|
|
168
169
|
|
|
169
170
|
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).
|
|
170
171
|
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.
|
|
@@ -198,13 +199,13 @@ describe('system-message', () => {
|
|
|
198
199
|
|
|
199
200
|
Use case patterns:
|
|
200
201
|
- Reminder flows: create deadline reminders in this channel with one-time \`--send-at\`; mention only if action is required.
|
|
201
|
-
- Proactive reminders: when you encounter time-sensitive information during your work (e.g. creating an API key that expires in 90 days, a certificate with an expiration date, a trial period ending, a deadline mentioned in code comments), proactively schedule a \`--notify-only\` reminder before the expiration so the user gets notified in time. For example, if you generate an API key expiring on 2026-06-01, schedule a reminder a few days before: \`kimaki send --channel chan_123 --prompt "Reminder: <@USER_ID> the API key created on 2026-03-01 expires on 2026-06-01. Renew it before it breaks production." --send-at "2026-05-28T09:00:00Z" --notify-only
|
|
202
|
+
- Proactive reminders: when you encounter time-sensitive information during your work (e.g. creating an API key that expires in 90 days, a certificate with an expiration date, a trial period ending, a deadline mentioned in code comments), proactively schedule a \`--notify-only\` reminder before the expiration so the user gets notified in time. For example, if you generate an API key expiring on 2026-06-01, schedule a reminder a few days before: \`kimaki send --channel chan_123 --prompt "Reminder: <@USER_ID> the API key created on 2026-03-01 expires on 2026-06-01. Renew it before it breaks production." --send-at "2026-05-28T09:00:00Z" --notify-only --agent <current_agent>\`. Always tell the user you scheduled the reminder so they know.
|
|
202
203
|
- Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention @username only when failures require review".
|
|
203
204
|
- Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
|
|
204
205
|
- 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.
|
|
205
206
|
- 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:
|
|
206
207
|
|
|
207
|
-
kimaki send --session ses_123 --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only
|
|
208
|
+
kimaki send --session ses_123 --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only --agent <current_agent>
|
|
208
209
|
|
|
209
210
|
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.
|
|
210
211
|
|
|
@@ -219,7 +220,7 @@ describe('system-message', () => {
|
|
|
219
220
|
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:
|
|
220
221
|
|
|
221
222
|
\`\`\`bash
|
|
222
|
-
kimaki send --channel chan_123 --prompt "your task description" --worktree worktree-name --user "Tommy"
|
|
223
|
+
kimaki send --channel chan_123 --prompt "your task description" --worktree worktree-name --agent <current_agent> --user "Tommy"
|
|
223
224
|
\`\`\`
|
|
224
225
|
|
|
225
226
|
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.
|
|
@@ -235,7 +236,7 @@ describe('system-message', () => {
|
|
|
235
236
|
Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
|
|
236
237
|
|
|
237
238
|
\`\`\`bash
|
|
238
|
-
kimaki send --channel chan_123 --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --user "Tommy"
|
|
239
|
+
kimaki send --channel chan_123 --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --agent <current_agent> --user "Tommy"
|
|
239
240
|
\`\`\`
|
|
240
241
|
|
|
241
242
|
The path must be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses the worktree as its working directory. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing one.
|
|
@@ -249,7 +250,7 @@ describe('system-message', () => {
|
|
|
249
250
|
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:
|
|
250
251
|
|
|
251
252
|
\`\`\`bash
|
|
252
|
-
kimaki send --channel chan_123 --prompt "Continuing from previous session: <summary of current task and state>" --user "Tommy"
|
|
253
|
+
kimaki send --channel chan_123 --prompt "Continuing from previous session: <summary of current task and state>" --agent <current_agent> --user "Tommy"
|
|
253
254
|
\`\`\`
|
|
254
255
|
|
|
255
256
|
The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
|
|
@@ -307,10 +308,10 @@ describe('system-message', () => {
|
|
|
307
308
|
|
|
308
309
|
\`\`\`bash
|
|
309
310
|
# Send to a specific channel
|
|
310
|
-
kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2"
|
|
311
|
+
kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2" --agent <current_agent>
|
|
311
312
|
|
|
312
313
|
# Or use --project to resolve from directory
|
|
313
|
-
kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0"
|
|
314
|
+
kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0" --agent <current_agent>
|
|
314
315
|
\`\`\`
|
|
315
316
|
|
|
316
317
|
When sending prompts to other projects, always ask the agent to plan first, never build upfront. The prompt should start with "Plan how to ..." so the user can review before greenlighting implementation.
|
|
@@ -333,10 +334,10 @@ describe('system-message', () => {
|
|
|
333
334
|
|
|
334
335
|
\`\`\`bash
|
|
335
336
|
# Start a session and wait for it to finish
|
|
336
|
-
kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait
|
|
337
|
+
kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait --agent <current_agent>
|
|
337
338
|
|
|
338
339
|
# Send to an existing thread and wait
|
|
339
|
-
kimaki send --thread <thread_id> --prompt "Run the tests" --wait
|
|
340
|
+
kimaki send --thread <thread_id> --prompt "Run the tests" --wait --agent <current_agent>
|
|
340
341
|
\`\`\`
|
|
341
342
|
|
|
342
343
|
The command exits with the session markdown on stdout once the model finishes responding.
|
package/src/system-message.ts
CHANGED
|
@@ -363,7 +363,7 @@ export function getOpencodeSystemMessage({
|
|
|
363
363
|
.join('\n')}`
|
|
364
364
|
: ''
|
|
365
365
|
return `
|
|
366
|
-
The user is reading your messages from inside Discord, via kimaki.
|
|
366
|
+
The user is reading your messages from inside Discord, via kimaki.dev
|
|
367
367
|
|
|
368
368
|
## bash tool
|
|
369
369
|
|
|
@@ -431,39 +431,40 @@ ${
|
|
|
431
431
|
|
|
432
432
|
To start a new thread/session in this channel pro-grammatically, run:
|
|
433
433
|
|
|
434
|
-
kimaki send --channel ${channelId} --prompt "your prompt here"
|
|
434
|
+
kimaki send --channel ${channelId} --prompt "your prompt here" --agent <current_agent>${userArg}
|
|
435
435
|
|
|
436
436
|
You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
|
|
437
|
+
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.
|
|
437
438
|
|
|
438
439
|
IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
|
|
439
440
|
|
|
440
441
|
To send a prompt to an existing thread instead of creating a new one:
|
|
441
442
|
|
|
442
|
-
kimaki send --thread <thread_id> --prompt "follow-up prompt"
|
|
443
|
+
kimaki send --thread <thread_id> --prompt "follow-up prompt" --agent <current_agent>
|
|
443
444
|
|
|
444
445
|
Use this when you already have the Discord thread ID.
|
|
445
446
|
|
|
446
447
|
To send to the thread associated with a known session:
|
|
447
448
|
|
|
448
|
-
kimaki send --session <session_id> --prompt "follow-up prompt"
|
|
449
|
+
kimaki send --session <session_id> --prompt "follow-up prompt" --agent <current_agent>
|
|
449
450
|
|
|
450
451
|
Use this when you have the OpenCode session ID.
|
|
451
452
|
|
|
452
453
|
Use --notify-only to create a notification thread without starting an AI session:
|
|
453
454
|
|
|
454
|
-
kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
|
|
455
|
+
kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only --agent <current_agent>${userArg}
|
|
455
456
|
|
|
456
457
|
Use --user to add a specific Discord user to the new thread:
|
|
457
458
|
|
|
458
|
-
kimaki send --channel ${channelId} --prompt "Review the latest CI failure"
|
|
459
|
+
kimaki send --channel ${channelId} --prompt "Review the latest CI failure" --agent <current_agent>${userArg}
|
|
459
460
|
|
|
460
461
|
Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
|
|
461
462
|
|
|
462
|
-
kimaki send --channel ${channelId} --prompt "Add dark mode support" --worktree dark-mode
|
|
463
|
+
kimaki send --channel ${channelId} --prompt "Add dark mode support" --worktree dark-mode --agent <current_agent>${userArg}
|
|
463
464
|
|
|
464
465
|
Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
|
|
465
466
|
|
|
466
|
-
kimaki send --channel ${channelId} --prompt "Continue work on feature" --cwd /path/to/existing-worktree
|
|
467
|
+
kimaki send --channel ${channelId} --prompt "Continue work on feature" --cwd /path/to/existing-worktree --agent <current_agent>${userArg}
|
|
467
468
|
|
|
468
469
|
Important:
|
|
469
470
|
- NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
|
|
@@ -481,8 +482,8 @@ ${availableAgentsContext}
|
|
|
481
482
|
|
|
482
483
|
You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
|
|
483
484
|
|
|
484
|
-
kimaki send --thread <thread_id> --prompt "/review fix the auth module"
|
|
485
|
-
kimaki send --channel ${channelId} --prompt "/build-cmd update dependencies"
|
|
485
|
+
kimaki send --thread <thread_id> --prompt "/review fix the auth module" --agent <current_agent>
|
|
486
|
+
kimaki send --channel ${channelId} --prompt "/build-cmd update dependencies" --agent <current_agent>${userArg}
|
|
486
487
|
|
|
487
488
|
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\`).
|
|
488
489
|
|
|
@@ -492,14 +493,14 @@ The user can switch the active agent mid-session using the Discord slash command
|
|
|
492
493
|
|
|
493
494
|
You can also switch agents via \`kimaki send\`:
|
|
494
495
|
|
|
495
|
-
kimaki send --thread <thread_id> --prompt "/<agentname>-agent"
|
|
496
|
+
kimaki send --thread <thread_id> --prompt "/<agentname>-agent" --agent <current_agent>
|
|
496
497
|
|
|
497
498
|
## scheduled sends and task management
|
|
498
499
|
|
|
499
500
|
Use \`--send-at\` to schedule a one-time or recurring task:
|
|
500
501
|
|
|
501
|
-
kimaki send --channel ${channelId} --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z"
|
|
502
|
-
kimaki send --channel ${channelId} --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1"
|
|
502
|
+
kimaki send --channel ${channelId} --prompt "Reminder: review open PRs" --send-at "2026-03-01T09:00:00Z" --agent <current_agent>${userArg}
|
|
503
|
+
kimaki send --channel ${channelId} --prompt "Run weekly test suite and summarize failures" --send-at "0 9 * * 1" --agent <current_agent>${userArg}
|
|
503
504
|
|
|
504
505
|
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).
|
|
505
506
|
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.
|
|
@@ -533,13 +534,13 @@ kimaki task delete <id>
|
|
|
533
534
|
|
|
534
535
|
Use case patterns:
|
|
535
536
|
- Reminder flows: create deadline reminders in this channel with one-time \`--send-at\`; mention only if action is required.
|
|
536
|
-
- Proactive reminders: when you encounter time-sensitive information during your work (e.g. creating an API key that expires in 90 days, a certificate with an expiration date, a trial period ending, a deadline mentioned in code comments), proactively schedule a \`--notify-only\` reminder before the expiration so the user gets notified in time. For example, if you generate an API key expiring on 2026-06-01, schedule a reminder a few days before: \`kimaki send --channel ${channelId} --prompt "Reminder: <@USER_ID> the API key created on 2026-03-01 expires on 2026-06-01. Renew it before it breaks production." --send-at "2026-05-28T09:00:00Z" --notify-only
|
|
537
|
+
- Proactive reminders: when you encounter time-sensitive information during your work (e.g. creating an API key that expires in 90 days, a certificate with an expiration date, a trial period ending, a deadline mentioned in code comments), proactively schedule a \`--notify-only\` reminder before the expiration so the user gets notified in time. For example, if you generate an API key expiring on 2026-06-01, schedule a reminder a few days before: \`kimaki send --channel ${channelId} --prompt "Reminder: <@USER_ID> the API key created on 2026-03-01 expires on 2026-06-01. Renew it before it breaks production." --send-at "2026-05-28T09:00:00Z" --notify-only --agent <current_agent>\`. Always tell the user you scheduled the reminder so they know.
|
|
537
538
|
- Weekly QA: schedule "run full test suite, inspect failures, post summary, and mention @username only when failures require review".
|
|
538
539
|
- Weekly benchmark automation: schedule a benchmark prompt that runs model evals, writes JSON outputs in the repo, commits results, and mentions only for regressions.
|
|
539
540
|
- 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.
|
|
540
541
|
- 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:
|
|
541
542
|
|
|
542
|
-
kimaki send --session ${sessionId} --prompt "Reminder: <@USER_ID> you asked to be reminded about this thread." --send-at "<future_UTC_time>" --notify-only
|
|
543
|
+
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>
|
|
543
544
|
|
|
544
545
|
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.
|
|
545
546
|
|
|
@@ -554,7 +555,7 @@ ONLY create worktrees when the user explicitly asks for one. Never proactively u
|
|
|
554
555
|
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:
|
|
555
556
|
|
|
556
557
|
\`\`\`bash
|
|
557
|
-
kimaki send --channel ${channelId} --prompt "your task description" --worktree worktree-name
|
|
558
|
+
kimaki send --channel ${channelId} --prompt "your task description" --worktree worktree-name --agent <current_agent>${userArg}
|
|
558
559
|
\`\`\`
|
|
559
560
|
|
|
560
561
|
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.
|
|
@@ -570,7 +571,7 @@ Critical recursion guard:
|
|
|
570
571
|
Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
|
|
571
572
|
|
|
572
573
|
\`\`\`bash
|
|
573
|
-
kimaki send --channel ${channelId} --prompt "Continue work on feature X" --cwd /path/to/existing-worktree
|
|
574
|
+
kimaki send --channel ${channelId} --prompt "Continue work on feature X" --cwd /path/to/existing-worktree --agent <current_agent>${userArg}
|
|
574
575
|
\`\`\`
|
|
575
576
|
|
|
576
577
|
The path must be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses the worktree as its working directory. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing one.
|
|
@@ -584,7 +585,7 @@ This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
|
584
585
|
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:
|
|
585
586
|
|
|
586
587
|
\`\`\`bash
|
|
587
|
-
kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>"
|
|
588
|
+
kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>" --agent <current_agent>${userArg}
|
|
588
589
|
\`\`\`
|
|
589
590
|
|
|
590
591
|
The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
|
|
@@ -642,10 +643,10 @@ To send a task to another project:
|
|
|
642
643
|
|
|
643
644
|
\`\`\`bash
|
|
644
645
|
# Send to a specific channel
|
|
645
|
-
kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2"
|
|
646
|
+
kimaki send --channel <channel_id> --prompt "Plan how to update the API client to v2" --agent <current_agent>
|
|
646
647
|
|
|
647
648
|
# Or use --project to resolve from directory
|
|
648
|
-
kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0"
|
|
649
|
+
kimaki send --project /path/to/other-repo --prompt "Plan how to bump version to 1.2.0" --agent <current_agent>
|
|
649
650
|
\`\`\`
|
|
650
651
|
|
|
651
652
|
When sending prompts to other projects, always ask the agent to plan first, never build upfront. The prompt should start with "Plan how to ..." so the user can review before greenlighting implementation.
|
|
@@ -668,10 +669,10 @@ If your Bash tool timeout triggers anyway, fall back to reading the session outp
|
|
|
668
669
|
|
|
669
670
|
\`\`\`bash
|
|
670
671
|
# Start a session and wait for it to finish
|
|
671
|
-
kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait
|
|
672
|
+
kimaki send --channel <channel_id> --prompt "Fix the auth bug" --wait --agent <current_agent>
|
|
672
673
|
|
|
673
674
|
# Send to an existing thread and wait
|
|
674
|
-
kimaki send --thread <thread_id> --prompt "Run the tests" --wait
|
|
675
|
+
kimaki send --thread <thread_id> --prompt "Run the tests" --wait --agent <current_agent>
|
|
675
676
|
\`\`\`
|
|
676
677
|
|
|
677
678
|
The command exits with the session markdown on stdout once the model finishes responding.
|
|
@@ -33,6 +33,7 @@ type SessionState = {
|
|
|
33
33
|
comparedTurn: number
|
|
34
34
|
previousTurnContext: TurnContext | undefined
|
|
35
35
|
currentTurnContext: TurnContext | undefined
|
|
36
|
+
pendingCompareTimeout: ReturnType<typeof setTimeout> | undefined
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
type SystemPromptDiff = {
|
|
@@ -146,7 +147,6 @@ function writeSystemPromptDiffFile({
|
|
|
146
147
|
additions: number
|
|
147
148
|
deletions: number
|
|
148
149
|
filePath: string
|
|
149
|
-
latestPromptPath: string
|
|
150
150
|
} {
|
|
151
151
|
const diff = buildPatch({
|
|
152
152
|
beforeText: beforePrompt,
|
|
@@ -157,7 +157,7 @@ function writeSystemPromptDiffFile({
|
|
|
157
157
|
const timestamp = new Date().toISOString().replaceAll(':', '-')
|
|
158
158
|
const sessionDir = path.join(getSystemPromptDiffDir({ dataDir }), sessionId)
|
|
159
159
|
const filePath = path.join(sessionDir, `${timestamp}.diff`)
|
|
160
|
-
|
|
160
|
+
|
|
161
161
|
const fileContent = [
|
|
162
162
|
`Session: ${sessionId}`,
|
|
163
163
|
`Created: ${new Date().toISOString()}`,
|
|
@@ -176,7 +176,6 @@ function writeSystemPromptDiffFile({
|
|
|
176
176
|
additions: diff.additions,
|
|
177
177
|
deletions: diff.deletions,
|
|
178
178
|
filePath,
|
|
179
|
-
latestPromptPath,
|
|
180
179
|
}
|
|
181
180
|
},
|
|
182
181
|
catch: (error) => {
|
|
@@ -204,6 +203,7 @@ function getOrCreateSessionState({
|
|
|
204
203
|
comparedTurn: 0,
|
|
205
204
|
previousTurnContext: undefined,
|
|
206
205
|
currentTurnContext: undefined,
|
|
206
|
+
pendingCompareTimeout: undefined,
|
|
207
207
|
}
|
|
208
208
|
sessions.set(sessionId, state)
|
|
209
209
|
return state
|
|
@@ -278,8 +278,7 @@ async function handleSystemTransform({
|
|
|
278
278
|
sessionId,
|
|
279
279
|
message:
|
|
280
280
|
`system prompt changed since the previous message (+${diffFileResult.additions} / -${diffFileResult.deletions}). ` +
|
|
281
|
-
`Diff: \`${abbreviatePath(diffFileResult.filePath)}\`. `
|
|
282
|
-
`Latest prompt: \`${abbreviatePath(diffFileResult.latestPromptPath)}\``,
|
|
281
|
+
`Diff: \`${abbreviatePath(diffFileResult.filePath)}\`. `
|
|
283
282
|
}),
|
|
284
283
|
},
|
|
285
284
|
})
|
|
@@ -315,13 +314,46 @@ const systemPromptDriftPlugin: Plugin = async ({ client, directory }) => {
|
|
|
315
314
|
'experimental.chat.system.transform': async (input, output) => {
|
|
316
315
|
const result = await errore.tryAsync({
|
|
317
316
|
try: async () => {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
317
|
+
const sessionId = input.sessionID
|
|
318
|
+
if (!sessionId) {
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
const state = getOrCreateSessionState({ sessions, sessionId })
|
|
322
|
+
if (state.pendingCompareTimeout) {
|
|
323
|
+
clearTimeout(state.pendingCompareTimeout)
|
|
324
|
+
}
|
|
325
|
+
// Delay one tick so other system-transform hooks can finish mutating
|
|
326
|
+
// output.system before we snapshot it for drift detection.
|
|
327
|
+
state.pendingCompareTimeout = setTimeout(() => {
|
|
328
|
+
state.pendingCompareTimeout = undefined
|
|
329
|
+
void errore.tryAsync({
|
|
330
|
+
try: async () => {
|
|
331
|
+
await handleSystemTransform({
|
|
332
|
+
input,
|
|
333
|
+
output,
|
|
334
|
+
sessions,
|
|
335
|
+
dataDir,
|
|
336
|
+
client,
|
|
337
|
+
})
|
|
338
|
+
},
|
|
339
|
+
catch: (error) => {
|
|
340
|
+
return new Error('system prompt drift transform hook failed', {
|
|
341
|
+
cause: error,
|
|
342
|
+
})
|
|
343
|
+
},
|
|
344
|
+
}).then((delayedResult) => {
|
|
345
|
+
if (!(delayedResult instanceof Error)) {
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
logger.warn(
|
|
349
|
+
`[system-prompt-drift-plugin] ${formatPluginErrorWithStack(delayedResult)}`,
|
|
350
|
+
)
|
|
351
|
+
void notifyError(
|
|
352
|
+
delayedResult,
|
|
353
|
+
'system prompt drift plugin transform hook failed',
|
|
354
|
+
)
|
|
355
|
+
})
|
|
356
|
+
}, 0)
|
|
325
357
|
},
|
|
326
358
|
catch: (error) => {
|
|
327
359
|
return new Error('system prompt drift transform hook failed', {
|
|
@@ -346,6 +378,10 @@ const systemPromptDriftPlugin: Plugin = async ({ client, directory }) => {
|
|
|
346
378
|
if (!deletedSessionId) {
|
|
347
379
|
return
|
|
348
380
|
}
|
|
381
|
+
const state = sessions.get(deletedSessionId)
|
|
382
|
+
if (state?.pendingCompareTimeout) {
|
|
383
|
+
clearTimeout(state.pendingCompareTimeout)
|
|
384
|
+
}
|
|
349
385
|
sessions.delete(deletedSessionId)
|
|
350
386
|
},
|
|
351
387
|
catch: (error) => {
|
package/src/utils.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function generateBotInstallUrl({
|
|
|
82
82
|
|
|
83
83
|
export const KIMAKI_GATEWAY_APP_ID =
|
|
84
84
|
process.env.KIMAKI_GATEWAY_APP_ID || '1477605701202481173'
|
|
85
|
-
export const KIMAKI_WEBSITE_URL = process.env.KIMAKI_WEBSITE_URL || 'https://kimaki.
|
|
85
|
+
export const KIMAKI_WEBSITE_URL = process.env.KIMAKI_WEBSITE_URL || 'https://kimaki.dev'
|
|
86
86
|
|
|
87
87
|
export function generateDiscordInstallUrlForBot({
|
|
88
88
|
appId,
|