kimaki 0.4.46 → 0.4.48
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/cli.js +69 -21
- package/dist/commands/abort.js +4 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +60 -30
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +23 -10
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +5 -3
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +6 -3
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +2 -2
- package/dist/commands/verbosity.js +5 -5
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/config.js +7 -0
- package/dist/database.js +10 -7
- package/dist/discord-bot.js +30 -12
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +91 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +19 -25
- package/dist/session-handler.js +89 -29
- package/dist/system-message.js +11 -9
- package/dist/tools.js +3 -2
- package/dist/utils.js +1 -0
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +3 -3
- package/src/cli.ts +108 -21
- package/src/commands/abort.ts +4 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +87 -36
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +47 -10
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +6 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +6 -3
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +5 -5
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/config.ts +14 -0
- package/src/database.ts +13 -7
- package/src/discord-bot.ts +45 -12
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +100 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +19 -26
- package/src/session-handler.ts +102 -29
- package/src/system-message.ts +11 -9
- package/src/tools.ts +3 -2
- package/src/utils.ts +1 -0
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
package/dist/system-message.js
CHANGED
|
@@ -111,19 +111,21 @@ headings are discouraged anyway. instead try to use bold text for titles which r
|
|
|
111
111
|
|
|
112
112
|
you can create diagrams wrapping them in code blocks.
|
|
113
113
|
|
|
114
|
-
##
|
|
114
|
+
## proactivity
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Be proactive. When the user asks you to do something, do it. Do NOT stop to ask for confirmation.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Only ask questions when the request is genuinely ambiguous with multiple valid approaches, or the action is destructive and irreversible.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
- After showing a plan: offer "Start implementing?" with Yes/No options
|
|
122
|
-
- After completing edits: offer "Commit changes?" with Yes/No options
|
|
123
|
-
- After debugging: offer "How to proceed?" with options like "Apply fix", "Investigate further", "Try different approach"
|
|
120
|
+
## ending conversations with options
|
|
124
121
|
|
|
125
|
-
|
|
122
|
+
After **completing** a task, use the question tool to offer follow-up options. The question tool must be called last, after all text parts.
|
|
126
123
|
|
|
127
|
-
|
|
124
|
+
IMPORTANT: Do NOT use the question tool to ask permission before doing work. Do the work first, then offer follow-ups.
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
- After completing edits: offer "Commit changes?" or "Run tests?"
|
|
128
|
+
- After debugging: offer "Apply fix", "Investigate further", "Try different approach"
|
|
129
|
+
- After a genuinely ambiguous request where you cannot infer intent: offer the different approaches
|
|
128
130
|
`;
|
|
129
131
|
}
|
package/dist/tools.js
CHANGED
|
@@ -6,9 +6,9 @@ import { z } from 'zod';
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import net from 'node:net';
|
|
8
8
|
import { createOpencodeClient, } from '@opencode-ai/sdk';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
9
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
10
10
|
import * as errore from 'errore';
|
|
11
|
-
const toolsLogger = createLogger(
|
|
11
|
+
const toolsLogger = createLogger(LogPrefix.TOOLS);
|
|
12
12
|
import { ShareMarkdown } from './markdown.js';
|
|
13
13
|
import { formatDistanceToNow } from './utils.js';
|
|
14
14
|
import pc from 'picocolors';
|
|
@@ -289,6 +289,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
289
289
|
}),
|
|
290
290
|
execute: async ({ sessionId }) => {
|
|
291
291
|
try {
|
|
292
|
+
toolsLogger.log(`[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`);
|
|
292
293
|
const result = await getClient().session.abort({
|
|
293
294
|
path: { id: sessionId },
|
|
294
295
|
});
|
package/dist/utils.js
CHANGED
|
@@ -50,6 +50,7 @@ export function isAbortError(error, signal) {
|
|
|
50
50
|
error.name === 'Aborterror' ||
|
|
51
51
|
error.name === 'aborterror' ||
|
|
52
52
|
error.name.toLowerCase() === 'aborterror' ||
|
|
53
|
+
error.name === 'MessageAbortedError' ||
|
|
53
54
|
error.message?.includes('aborted') ||
|
|
54
55
|
(signal?.aborted ?? false))) ||
|
|
55
56
|
(error instanceof DOMException && error.name === 'AbortError'));
|
package/dist/voice-handler.js
CHANGED
|
@@ -17,8 +17,8 @@ import { getDatabase } from './database.js';
|
|
|
17
17
|
import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
|
|
18
18
|
import { transcribeAudio } from './voice.js';
|
|
19
19
|
import { FetchError } from './errors.js';
|
|
20
|
-
import { createLogger } from './logger.js';
|
|
21
|
-
const voiceLogger = createLogger(
|
|
20
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
21
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
22
22
|
export const voiceConnections = new Map();
|
|
23
23
|
export function convertToMono16k(buffer) {
|
|
24
24
|
const inputSampleRate = 48000;
|
package/dist/voice.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// Uses errore for type-safe error handling.
|
|
5
5
|
import { GoogleGenAI, Type } from '@google/genai';
|
|
6
6
|
import * as errore from 'errore';
|
|
7
|
-
import { createLogger } from './logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
import { ripGrep } from 'ripgrep-js';
|
|
10
10
|
import { ApiKeyMissingError, InvalidAudioFormatError, TranscriptionError, EmptyTranscriptionError, NoResponseContentError, NoToolResponseError, GrepSearchError, GlobSearchError, } from './errors.js';
|
|
11
|
-
const voiceLogger = createLogger(
|
|
11
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
12
12
|
function runGrep({ pattern, directory }) {
|
|
13
13
|
return errore.tryAsync({
|
|
14
14
|
try: async () => {
|
package/dist/worktree-utils.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Worktree utility functions.
|
|
2
2
|
// Wrapper for OpenCode worktree creation that also initializes git submodules.
|
|
3
|
-
|
|
3
|
+
// Also handles capturing and applying git diffs when creating worktrees from threads.
|
|
4
|
+
import { exec, spawn } from 'node:child_process';
|
|
4
5
|
import { promisify } from 'node:util';
|
|
5
|
-
import { createLogger } from './logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
7
|
export const execAsync = promisify(exec);
|
|
7
|
-
const logger = createLogger(
|
|
8
|
+
const logger = createLogger(LogPrefix.WORKTREE);
|
|
8
9
|
/**
|
|
9
10
|
* Create a worktree using OpenCode SDK and initialize git submodules.
|
|
10
11
|
* This wrapper ensures submodules are properly set up in new worktrees.
|
|
12
|
+
*
|
|
13
|
+
* If diff is provided, it's applied BEFORE submodule update to ensure
|
|
14
|
+
* any submodule pointer changes in the diff are respected.
|
|
11
15
|
*/
|
|
12
|
-
export async function createWorktreeWithSubmodules({ clientV2, directory, name, }) {
|
|
16
|
+
export async function createWorktreeWithSubmodules({ clientV2, directory, name, diff, }) {
|
|
13
17
|
// 1. Create worktree via OpenCode SDK
|
|
14
18
|
const response = await clientV2.worktree.create({
|
|
15
19
|
directory,
|
|
@@ -22,7 +26,17 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
|
|
|
22
26
|
return new Error('No worktree data returned from SDK');
|
|
23
27
|
}
|
|
24
28
|
const worktreeDir = response.data.directory;
|
|
25
|
-
|
|
29
|
+
let diffApplied = false;
|
|
30
|
+
// 2. Apply diff BEFORE submodule update (if provided)
|
|
31
|
+
// This ensures any submodule pointer changes in the diff are applied first,
|
|
32
|
+
// so submodule update checks out the correct commits.
|
|
33
|
+
if (diff) {
|
|
34
|
+
logger.log(`Applying diff to ${worktreeDir} before submodule init`);
|
|
35
|
+
diffApplied = await applyGitDiff(worktreeDir, diff);
|
|
36
|
+
}
|
|
37
|
+
// 3. Init submodules in new worktree (don't block on failure)
|
|
38
|
+
// Uses --init to initialize, --recursive for nested submodules.
|
|
39
|
+
// Submodules will be checked out at the commit specified by the (possibly updated) index.
|
|
26
40
|
try {
|
|
27
41
|
logger.log(`Initializing submodules in ${worktreeDir}`);
|
|
28
42
|
await execAsync('git submodule update --init --recursive', {
|
|
@@ -34,7 +48,7 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
|
|
|
34
48
|
// Log but don't fail - submodules might not exist
|
|
35
49
|
logger.warn(`Failed to init submodules in ${worktreeDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
36
50
|
}
|
|
37
|
-
//
|
|
51
|
+
// 4. Install dependencies using ni (detects package manager from lockfile)
|
|
38
52
|
try {
|
|
39
53
|
logger.log(`Installing dependencies in ${worktreeDir}`);
|
|
40
54
|
await execAsync('npx -y ni', {
|
|
@@ -46,5 +60,75 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
|
|
|
46
60
|
// Log but don't fail - might not be a JS project or might fail for various reasons
|
|
47
61
|
logger.warn(`Failed to install dependencies in ${worktreeDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
48
62
|
}
|
|
49
|
-
return response.data;
|
|
63
|
+
return { ...response.data, diffApplied };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Capture git diff from a directory (both staged and unstaged changes).
|
|
67
|
+
* Returns null if no changes or on error.
|
|
68
|
+
*/
|
|
69
|
+
export async function captureGitDiff(directory) {
|
|
70
|
+
try {
|
|
71
|
+
// Capture unstaged changes
|
|
72
|
+
const unstagedResult = await execAsync('git diff', { cwd: directory });
|
|
73
|
+
const unstaged = unstagedResult.stdout.trim();
|
|
74
|
+
// Capture staged changes
|
|
75
|
+
const stagedResult = await execAsync('git diff --staged', { cwd: directory });
|
|
76
|
+
const staged = stagedResult.stdout.trim();
|
|
77
|
+
if (!unstaged && !staged) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return { unstaged, staged };
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
logger.warn(`Failed to capture git diff from ${directory}: ${e instanceof Error ? e.message : String(e)}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Run a git command with stdin input.
|
|
89
|
+
* Uses spawn to pipe the diff content to git apply.
|
|
90
|
+
*/
|
|
91
|
+
function runGitWithStdin(args, cwd, input) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const child = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
94
|
+
let stderr = '';
|
|
95
|
+
child.stderr?.on('data', (data) => {
|
|
96
|
+
stderr += data.toString();
|
|
97
|
+
});
|
|
98
|
+
child.on('close', (code) => {
|
|
99
|
+
if (code === 0) {
|
|
100
|
+
resolve();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
reject(new Error(stderr || `git ${args.join(' ')} failed with code ${code}`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
child.on('error', reject);
|
|
107
|
+
child.stdin?.write(input);
|
|
108
|
+
child.stdin?.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Apply a captured git diff to a directory.
|
|
113
|
+
* Applies staged changes first, then unstaged.
|
|
114
|
+
*/
|
|
115
|
+
export async function applyGitDiff(directory, diff) {
|
|
116
|
+
try {
|
|
117
|
+
// Apply staged changes first (and stage them)
|
|
118
|
+
if (diff.staged) {
|
|
119
|
+
logger.log(`Applying staged diff to ${directory}`);
|
|
120
|
+
await runGitWithStdin(['apply', '--index'], directory, diff.staged);
|
|
121
|
+
}
|
|
122
|
+
// Apply unstaged changes (don't stage them)
|
|
123
|
+
if (diff.unstaged) {
|
|
124
|
+
logger.log(`Applying unstaged diff to ${directory}`);
|
|
125
|
+
await runGitWithStdin(['apply'], directory, diff.unstaged);
|
|
126
|
+
}
|
|
127
|
+
logger.log(`Successfully applied diff to ${directory}`);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
logger.warn(`Failed to apply git diff to ${directory}: ${e instanceof Error ? e.message : String(e)}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
50
134
|
}
|
package/dist/xml.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Parses XML-like tags from strings (e.g., channel topics) to extract
|
|
3
3
|
// Kimaki configuration like directory paths and app IDs.
|
|
4
4
|
import { DomHandler, Parser, ElementType } from 'htmlparser2';
|
|
5
|
-
import { createLogger } from './logger.js';
|
|
6
|
-
const xmlLogger = createLogger(
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
6
|
+
const xmlLogger = createLogger(LogPrefix.XML);
|
|
7
7
|
export function extractTagsArrays({ xml, tags, }) {
|
|
8
8
|
const result = {
|
|
9
9
|
others: [],
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.48",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"tsx": "^4.20.5"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@clack/prompts": "^0.
|
|
23
|
+
"@clack/prompts": "^1.0.0",
|
|
24
24
|
"@discordjs/voice": "^0.19.0",
|
|
25
25
|
"@google/genai": "^1.34.0",
|
|
26
26
|
"@opencode-ai/sdk": "^1.1.31",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"string-dedent": "^3.0.2",
|
|
42
42
|
"undici": "^7.16.0",
|
|
43
43
|
"zod": "^4.2.1",
|
|
44
|
-
"errore": "^0.
|
|
44
|
+
"errore": "^0.10.0"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
47
47
|
"@discordjs/opus": "^0.10.0",
|
package/src/cli.ts
CHANGED
|
@@ -43,14 +43,34 @@ import path from 'node:path'
|
|
|
43
43
|
import fs from 'node:fs'
|
|
44
44
|
import * as errore from 'errore'
|
|
45
45
|
|
|
46
|
-
import { createLogger } from './logger.js'
|
|
46
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
47
47
|
import { uploadFilesToDiscord } from './discord-utils.js'
|
|
48
48
|
import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
49
49
|
import http from 'node:http'
|
|
50
|
-
import { setDataDir, getDataDir, getLockPort } from './config.js'
|
|
50
|
+
import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity } from './config.js'
|
|
51
51
|
import { sanitizeAgentName } from './commands/agent.js'
|
|
52
52
|
|
|
53
|
-
const cliLogger = createLogger(
|
|
53
|
+
const cliLogger = createLogger(LogPrefix.CLI)
|
|
54
|
+
|
|
55
|
+
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
56
|
+
// Not detached, so it dies automatically with the parent process.
|
|
57
|
+
function startCaffeinate() {
|
|
58
|
+
if (process.platform !== 'darwin') {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const proc = spawn('caffeinate', ['-i'], {
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
detached: false,
|
|
65
|
+
})
|
|
66
|
+
proc.on('error', (err) => {
|
|
67
|
+
cliLogger.warn('Failed to start caffeinate:', err.message)
|
|
68
|
+
})
|
|
69
|
+
cliLogger.log('Started caffeinate to prevent system sleep')
|
|
70
|
+
} catch (err) {
|
|
71
|
+
cliLogger.warn('Failed to spawn caffeinate:', err instanceof Error ? err.message : String(err))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
54
74
|
const cli = cac('kimaki')
|
|
55
75
|
|
|
56
76
|
process.title = 'kimaki'
|
|
@@ -125,7 +145,11 @@ async function checkSingleInstance(): Promise<void> {
|
|
|
125
145
|
setTimeout(resolve, 500)
|
|
126
146
|
})
|
|
127
147
|
}
|
|
128
|
-
} catch {
|
|
148
|
+
} catch (error) {
|
|
149
|
+
cliLogger.debug(
|
|
150
|
+
'Lock port check failed:',
|
|
151
|
+
error instanceof Error ? error.message : String(error),
|
|
152
|
+
)
|
|
129
153
|
cliLogger.debug('No other kimaki instance detected on lock port')
|
|
130
154
|
}
|
|
131
155
|
}
|
|
@@ -301,6 +325,10 @@ async function registerCommands({
|
|
|
301
325
|
.setName('abort')
|
|
302
326
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
303
327
|
.toJSON(),
|
|
328
|
+
new SlashCommandBuilder()
|
|
329
|
+
.setName('compact')
|
|
330
|
+
.setDescription('Compact the session context by summarizing conversation history')
|
|
331
|
+
.toJSON(),
|
|
304
332
|
new SlashCommandBuilder()
|
|
305
333
|
.setName('stop')
|
|
306
334
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
@@ -527,11 +555,23 @@ async function backgroundInit({
|
|
|
527
555
|
getClient()
|
|
528
556
|
.command.list({ query: { directory: currentDir } })
|
|
529
557
|
.then((r) => r.data || [])
|
|
530
|
-
.catch(() =>
|
|
558
|
+
.catch((error) => {
|
|
559
|
+
cliLogger.warn(
|
|
560
|
+
'Failed to load user commands during background init:',
|
|
561
|
+
error instanceof Error ? error.message : String(error),
|
|
562
|
+
)
|
|
563
|
+
return []
|
|
564
|
+
}),
|
|
531
565
|
getClient()
|
|
532
566
|
.app.agents({ query: { directory: currentDir } })
|
|
533
567
|
.then((r) => r.data || [])
|
|
534
|
-
.catch(() =>
|
|
568
|
+
.catch((error) => {
|
|
569
|
+
cliLogger.warn(
|
|
570
|
+
'Failed to load agents during background init:',
|
|
571
|
+
error instanceof Error ? error.message : String(error),
|
|
572
|
+
)
|
|
573
|
+
return []
|
|
574
|
+
}),
|
|
535
575
|
])
|
|
536
576
|
|
|
537
577
|
await registerCommands({ token, appId, userCommands, agents })
|
|
@@ -545,6 +585,8 @@ async function backgroundInit({
|
|
|
545
585
|
}
|
|
546
586
|
|
|
547
587
|
async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
588
|
+
startCaffeinate()
|
|
589
|
+
|
|
548
590
|
const forceSetup = Boolean(restart)
|
|
549
591
|
|
|
550
592
|
intro('🤖 Discord Bot Setup')
|
|
@@ -587,7 +629,11 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
587
629
|
try {
|
|
588
630
|
fs.accessSync(p, fs.constants.F_OK)
|
|
589
631
|
return true
|
|
590
|
-
} catch {
|
|
632
|
+
} catch (error) {
|
|
633
|
+
cliLogger.debug(
|
|
634
|
+
`OpenCode path not found at ${p}:`,
|
|
635
|
+
error instanceof Error ? error.message : String(error),
|
|
636
|
+
)
|
|
591
637
|
return false
|
|
592
638
|
}
|
|
593
639
|
})
|
|
@@ -888,11 +934,23 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
888
934
|
getClient()
|
|
889
935
|
.command.list({ query: { directory: currentDir } })
|
|
890
936
|
.then((r) => r.data || [])
|
|
891
|
-
.catch(() =>
|
|
937
|
+
.catch((error) => {
|
|
938
|
+
cliLogger.warn(
|
|
939
|
+
'Failed to load user commands during setup:',
|
|
940
|
+
error instanceof Error ? error.message : String(error),
|
|
941
|
+
)
|
|
942
|
+
return []
|
|
943
|
+
}),
|
|
892
944
|
getClient()
|
|
893
945
|
.app.agents({ query: { directory: currentDir } })
|
|
894
946
|
.then((r) => r.data || [])
|
|
895
|
-
.catch(() =>
|
|
947
|
+
.catch((error) => {
|
|
948
|
+
cliLogger.warn(
|
|
949
|
+
'Failed to load agents during setup:',
|
|
950
|
+
error instanceof Error ? error.message : String(error),
|
|
951
|
+
)
|
|
952
|
+
return []
|
|
953
|
+
}),
|
|
896
954
|
])
|
|
897
955
|
|
|
898
956
|
s.stop(`Found ${projects.length} OpenCode project(s)`)
|
|
@@ -1040,6 +1098,7 @@ cli
|
|
|
1040
1098
|
.option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
|
|
1041
1099
|
.option('--install-url', 'Print the bot install URL and exit')
|
|
1042
1100
|
.option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
|
|
1101
|
+
.option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text or text-only)')
|
|
1043
1102
|
.action(
|
|
1044
1103
|
async (options: {
|
|
1045
1104
|
restart?: boolean
|
|
@@ -1047,6 +1106,7 @@ cli
|
|
|
1047
1106
|
dataDir?: string
|
|
1048
1107
|
installUrl?: boolean
|
|
1049
1108
|
useWorktrees?: boolean
|
|
1109
|
+
verbosity?: string
|
|
1050
1110
|
}) => {
|
|
1051
1111
|
try {
|
|
1052
1112
|
// Set data directory early, before any database access
|
|
@@ -1055,6 +1115,15 @@ cli
|
|
|
1055
1115
|
cliLogger.log(`Using data directory: ${getDataDir()}`)
|
|
1056
1116
|
}
|
|
1057
1117
|
|
|
1118
|
+
if (options.verbosity) {
|
|
1119
|
+
if (options.verbosity !== 'tools-and-text' && options.verbosity !== 'text-only') {
|
|
1120
|
+
cliLogger.error(`Invalid verbosity level: ${options.verbosity}. Use "tools-and-text" or "text-only".`)
|
|
1121
|
+
process.exit(EXIT_NO_RESTART)
|
|
1122
|
+
}
|
|
1123
|
+
setDefaultVerbosity(options.verbosity)
|
|
1124
|
+
cliLogger.log(`Default verbosity: ${options.verbosity}`)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1058
1127
|
if (options.installUrl) {
|
|
1059
1128
|
const db = getDatabase()
|
|
1060
1129
|
const existingBot = db
|
|
@@ -1214,8 +1283,11 @@ cli
|
|
|
1214
1283
|
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
1215
1284
|
.get() as { app_id: string } | undefined
|
|
1216
1285
|
appId = botRow?.app_id
|
|
1217
|
-
} catch {
|
|
1218
|
-
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
cliLogger.debug(
|
|
1288
|
+
'Database lookup failed while resolving app ID:',
|
|
1289
|
+
error instanceof Error ? error.message : String(error),
|
|
1290
|
+
)
|
|
1219
1291
|
}
|
|
1220
1292
|
}
|
|
1221
1293
|
} else {
|
|
@@ -1330,8 +1402,11 @@ cli
|
|
|
1330
1402
|
if (ch && 'guild' in ch && ch.guild) {
|
|
1331
1403
|
return ch.guild
|
|
1332
1404
|
}
|
|
1333
|
-
} catch {
|
|
1334
|
-
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
cliLogger.debug(
|
|
1407
|
+
'Failed to fetch existing channel while selecting guild:',
|
|
1408
|
+
error instanceof Error ? error.message : String(error),
|
|
1409
|
+
)
|
|
1335
1410
|
}
|
|
1336
1411
|
}
|
|
1337
1412
|
// Fall back to first guild the bot is in
|
|
@@ -1571,8 +1646,11 @@ cli
|
|
|
1571
1646
|
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
1572
1647
|
.get() as { app_id: string } | undefined
|
|
1573
1648
|
appId = botRow?.app_id
|
|
1574
|
-
} catch {
|
|
1575
|
-
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
cliLogger.debug(
|
|
1651
|
+
'Database lookup failed while resolving app ID:',
|
|
1652
|
+
error instanceof Error ? error.message : String(error),
|
|
1653
|
+
)
|
|
1576
1654
|
}
|
|
1577
1655
|
}
|
|
1578
1656
|
} else {
|
|
@@ -1651,8 +1729,11 @@ cli
|
|
|
1651
1729
|
} else {
|
|
1652
1730
|
throw new Error('Channel has no guild')
|
|
1653
1731
|
}
|
|
1654
|
-
} catch {
|
|
1655
|
-
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
cliLogger.debug(
|
|
1734
|
+
'Failed to fetch existing channel while selecting guild:',
|
|
1735
|
+
error instanceof Error ? error.message : String(error),
|
|
1736
|
+
)
|
|
1656
1737
|
const firstGuild = client.guilds.cache.first()
|
|
1657
1738
|
if (!firstGuild) {
|
|
1658
1739
|
s.stop('No guild found')
|
|
@@ -1696,12 +1777,18 @@ cli
|
|
|
1696
1777
|
client.destroy()
|
|
1697
1778
|
process.exit(0)
|
|
1698
1779
|
}
|
|
1699
|
-
} catch {
|
|
1700
|
-
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
cliLogger.debug(
|
|
1782
|
+
`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`,
|
|
1783
|
+
error instanceof Error ? error.message : String(error),
|
|
1784
|
+
)
|
|
1701
1785
|
}
|
|
1702
1786
|
}
|
|
1703
|
-
} catch {
|
|
1704
|
-
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
cliLogger.debug(
|
|
1789
|
+
'Database lookup failed while checking existing channels:',
|
|
1790
|
+
error instanceof Error ? error.message : String(error),
|
|
1791
|
+
)
|
|
1705
1792
|
}
|
|
1706
1793
|
|
|
1707
1794
|
s.message(`Creating channels in ${guild.name}...`)
|
package/src/commands/abort.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { getDatabase } from '../database.js'
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
8
|
import { abortControllers } from '../session-handler.js'
|
|
9
|
-
import { createLogger } from '../logger.js'
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
10
10
|
import * as errore from 'errore'
|
|
11
11
|
|
|
12
|
-
const logger = createLogger(
|
|
12
|
+
const logger = createLogger(LogPrefix.ABORT)
|
|
13
13
|
|
|
14
14
|
export async function handleAbortCommand({ command }: CommandContext): Promise<void> {
|
|
15
15
|
const channel = command.channel
|
|
@@ -67,6 +67,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
|
|
|
67
67
|
|
|
68
68
|
const existingController = abortControllers.get(sessionId)
|
|
69
69
|
if (existingController) {
|
|
70
|
+
logger.log(`[ABORT] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - user ran /abort command`)
|
|
70
71
|
existingController.abort(new Error('User requested abort'))
|
|
71
72
|
abortControllers.delete(sessionId)
|
|
72
73
|
}
|
|
@@ -82,6 +83,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
try {
|
|
86
|
+
logger.log(`[ABORT-API] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - sending API abort from /abort command`)
|
|
85
87
|
await getClient().session.abort({
|
|
86
88
|
path: { id: sessionId },
|
|
87
89
|
})
|
|
@@ -6,11 +6,11 @@ import type { CommandContext, AutocompleteContext } from './types.js'
|
|
|
6
6
|
import { getDatabase } from '../database.js'
|
|
7
7
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
8
8
|
import { createProjectChannels } from '../channel-management.js'
|
|
9
|
-
import { createLogger } from '../logger.js'
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
10
10
|
import { abbreviatePath } from '../utils.js'
|
|
11
11
|
import * as errore from 'errore'
|
|
12
12
|
|
|
13
|
-
const logger = createLogger(
|
|
13
|
+
const logger = createLogger(LogPrefix.ADD_PROJECT)
|
|
14
14
|
|
|
15
15
|
export async function handleAddProjectCommand({ command, appId }: CommandContext): Promise<void> {
|
|
16
16
|
await command.deferReply({ ephemeral: false })
|
package/src/commands/agent.ts
CHANGED
|
@@ -14,10 +14,10 @@ import crypto from 'node:crypto'
|
|
|
14
14
|
import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js'
|
|
15
15
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
16
16
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
17
|
-
import { createLogger } from '../logger.js'
|
|
17
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
18
18
|
import * as errore from 'errore'
|
|
19
19
|
|
|
20
|
-
const agentLogger = createLogger(
|
|
20
|
+
const agentLogger = createLogger(LogPrefix.AGENT)
|
|
21
21
|
|
|
22
22
|
const pendingAgentContexts = new Map<
|
|
23
23
|
string,
|
|
@@ -257,7 +257,7 @@ export async function handleAgentSelectMenu(
|
|
|
257
257
|
})
|
|
258
258
|
} else {
|
|
259
259
|
await interaction.editReply({
|
|
260
|
-
content: `Agent preference set for this channel: **${selectedAgent}**\
|
|
260
|
+
content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
|
|
261
261
|
components: [],
|
|
262
262
|
})
|
|
263
263
|
}
|
|
@@ -331,7 +331,7 @@ export async function handleQuickAgentCommand({
|
|
|
331
331
|
})
|
|
332
332
|
} else {
|
|
333
333
|
await command.editReply({
|
|
334
|
-
content: `Switched to **${matchingAgent.name}** agent for this channel\
|
|
334
|
+
content: `Switched to **${matchingAgent.name}** agent for this channel\nAll new sessions will use this agent.`,
|
|
335
335
|
})
|
|
336
336
|
}
|
|
337
337
|
} catch (error) {
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
import crypto from 'node:crypto'
|
|
12
12
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
13
13
|
import { getOpencodeClientV2 } from '../opencode.js'
|
|
14
|
-
import { createLogger } from '../logger.js'
|
|
14
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
15
15
|
|
|
16
|
-
const logger = createLogger(
|
|
16
|
+
const logger = createLogger(LogPrefix.ASK_QUESTION)
|
|
17
17
|
|
|
18
18
|
// Schema matching the question tool input
|
|
19
19
|
export type AskUserQuestionInput = {
|
|
@@ -268,9 +268,9 @@ export function parseAskUserQuestionTool(part: {
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* Cancel a pending question for a thread (e.g., when user sends a new message).
|
|
271
|
-
* Sends
|
|
271
|
+
* Sends the user's message as the answer to OpenCode so the model sees their actual response.
|
|
272
272
|
*/
|
|
273
|
-
export async function cancelPendingQuestion(threadId: string): Promise<boolean> {
|
|
273
|
+
export async function cancelPendingQuestion(threadId: string, userMessage?: string): Promise<boolean> {
|
|
274
274
|
// Find pending question for this thread
|
|
275
275
|
let contextHash: string | undefined
|
|
276
276
|
let context: PendingQuestionContext | undefined
|
|
@@ -292,9 +292,10 @@ export async function cancelPendingQuestion(threadId: string): Promise<boolean>
|
|
|
292
292
|
throw new Error('OpenCode server not found for directory')
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
//
|
|
295
|
+
// Use user's message as answer if provided, otherwise mark as "Other"
|
|
296
|
+
const customAnswer = userMessage || 'Other'
|
|
296
297
|
const answers = context.questions.map((_, i) => {
|
|
297
|
-
return context.answers[i] || [
|
|
298
|
+
return context.answers[i] || [customAnswer]
|
|
298
299
|
})
|
|
299
300
|
|
|
300
301
|
await clientV2.question.reply({
|
|
@@ -302,9 +303,9 @@ export async function cancelPendingQuestion(threadId: string): Promise<boolean>
|
|
|
302
303
|
answers,
|
|
303
304
|
})
|
|
304
305
|
|
|
305
|
-
logger.log(`
|
|
306
|
+
logger.log(`Answered question ${context.requestId} with user message`)
|
|
306
307
|
} catch (error) {
|
|
307
|
-
logger.error('Failed to
|
|
308
|
+
logger.error('Failed to answer question:', error)
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
// Clean up regardless of whether the API call succeeded
|