kimaki 0.27.0 → 0.29.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 +247 -64
- package/dist/anthropic-account-identity.js +8 -1
- package/dist/anthropic-auth-plugin.js +9 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/bash-tool-schema-plugin.js +50 -0
- package/dist/bash-tool-schema-plugin.test.js +68 -0
- package/dist/channel-management.js +113 -51
- package/dist/cli-commands/misc.js +4 -1
- package/dist/cli-commands/project.js +17 -30
- package/dist/cli-commands/send.js +6 -1
- package/dist/cli-commands/session.js +223 -115
- package/dist/cli-commands/task.js +21 -14
- package/dist/cli-commands/thread.js +63 -0
- package/dist/cli-parsing.test.js +24 -1
- package/dist/cli-runner.js +16 -4
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +2 -0
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +158 -24
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/merge-worktree.js +13 -6
- package/dist/commands/model-variant.js +3 -2
- package/dist/commands/model.js +4 -3
- package/dist/commands/queue.js +82 -33
- package/dist/commands/queue.test.js +15 -0
- package/dist/commands/restart-opencode-server.js +7 -1
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session-id.js +5 -5
- package/dist/commands/session.js +78 -63
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +104 -0
- package/dist/db.js +45 -0
- package/dist/db.test.js +196 -1
- package/dist/default-channel-provisioning.test.js +149 -0
- package/dist/discord-bot.js +509 -476
- package/dist/discord-command-registration.js +17 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +84 -0
- package/dist/discord-utils.test.js +135 -1
- package/dist/errors.js +5 -0
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/guild-categories.test.js +225 -0
- package/dist/interaction-handler.js +421 -399
- package/dist/ipc-tools-plugin.js +12 -7
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
- package/dist/kimaki-opencode-plugin.js +4 -0
- package/dist/kimaki-opencode-plugin.test.js +28 -0
- package/dist/list-channel-threads.js +163 -0
- package/dist/list-channel-threads.test.js +80 -0
- package/dist/logger.js +1 -0
- package/dist/message-formatting.js +156 -44
- package/dist/message-formatting.test.js +562 -13
- package/dist/message-preprocessing.js +105 -1
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +49 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +29 -3
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +5 -5
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-footer.e2e.test.js +29 -32
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
- package/dist/queue-advanced-question.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +20 -20
- package/dist/queue-question-select-drain.e2e.test.js +7 -7
- package/dist/runtime-lifecycle.e2e.test.js +16 -19
- package/dist/schema.js +22 -1
- package/dist/session-handler/event-stream-state.js +176 -4
- package/dist/session-handler/event-stream-state.test.js +545 -1
- package/dist/session-handler/model-utils.js +16 -3
- package/dist/session-handler/model-utils.test.js +103 -3
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +6 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +642 -196
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-title-rename.test.js +30 -0
- package/dist/sleep.e2e.test.js +14 -11
- package/dist/system-message.js +100 -25
- package/dist/system-message.test.js +167 -23
- package/dist/task-id-plugin.js +16 -0
- package/dist/task-runner.js +2 -1
- package/dist/task-schedule.js +32 -2
- package/dist/task-schedule.test.js +79 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -54
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +41 -1
- package/dist/worktree-lifecycle.e2e.test.js +30 -30
- package/package.json +9 -9
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +56 -232
- package/skills/holocron/SKILL.md +117 -17
- package/skills/strada/SKILL.md +61 -232
- package/src/agent-model.e2e.test.ts +293 -66
- package/src/anthropic-account-identity.ts +19 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +11 -16
- package/src/bash-tool-schema-plugin.test.ts +84 -0
- package/src/bash-tool-schema-plugin.ts +77 -0
- package/src/channel-management.ts +144 -65
- package/src/cli-commands/misc.ts +4 -1
- package/src/cli-commands/project.ts +22 -38
- package/src/cli-commands/send.ts +6 -1
- package/src/cli-commands/session.ts +294 -139
- package/src/cli-commands/task.ts +22 -15
- package/src/cli-commands/thread.ts +86 -0
- package/src/cli-parsing.test.ts +34 -1
- package/src/cli-runner.ts +16 -3
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +2 -0
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +199 -23
- package/src/commands/ask-question.ts +2 -1
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/merge-worktree.ts +13 -6
- package/src/commands/model-variant.ts +3 -2
- package/src/commands/model.ts +4 -3
- package/src/commands/queue.test.ts +19 -0
- package/src/commands/queue.ts +109 -42
- package/src/commands/restart-opencode-server.ts +12 -1
- package/src/commands/resume.ts +4 -3
- package/src/commands/session-id.ts +5 -5
- package/src/commands/session.ts +94 -70
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +154 -0
- package/src/db.test.ts +217 -0
- package/src/db.ts +50 -0
- package/src/default-channel-provisioning.test.ts +156 -0
- package/src/discord-bot.ts +54 -9
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +30 -12
- package/src/discord-utils.test.ts +157 -0
- package/src/discord-utils.ts +131 -1
- package/src/errors.ts +7 -0
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/guild-categories.test.ts +260 -0
- package/src/interaction-handler.ts +43 -13
- package/src/ipc-tools-plugin.ts +16 -9
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
- package/src/kimaki-opencode-plugin.test.ts +34 -0
- package/src/kimaki-opencode-plugin.ts +4 -0
- package/src/list-channel-threads.test.ts +90 -0
- package/src/list-channel-threads.ts +252 -0
- package/src/logger.ts +1 -0
- package/src/message-formatting.test.ts +690 -13
- package/src/message-formatting.ts +218 -46
- package/src/message-preprocessing.ts +116 -1
- package/src/opencode-command.test.ts +59 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +44 -4
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +5 -5
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-footer.e2e.test.ts +30 -31
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
- package/src/queue-advanced-question.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +20 -20
- package/src/queue-question-select-drain.e2e.test.ts +7 -7
- package/src/runtime-lifecycle.e2e.test.ts +16 -18
- package/src/schema.sql +17 -1
- package/src/schema.ts +24 -1
- package/src/session-handler/event-stream-state.test.ts +579 -0
- package/src/session-handler/event-stream-state.ts +235 -4
- package/src/session-handler/model-utils.test.ts +125 -3
- package/src/session-handler/model-utils.ts +36 -4
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +8 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +797 -229
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-title-rename.test.ts +45 -0
- package/src/sleep.e2e.test.ts +14 -11
- package/src/store.ts +1 -0
- package/src/system-message.test.ts +191 -23
- package/src/system-message.ts +124 -23
- package/src/task-id-plugin.ts +17 -0
- package/src/task-runner.ts +2 -1
- package/src/task-schedule.test.ts +94 -0
- package/src/task-schedule.ts +47 -2
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -53
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +44 -0
- package/src/voice.ts +34 -6
- package/src/worktree-lifecycle.e2e.test.ts +30 -30
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { extendBashToolDefinition, injectKimakiSessionEnv, KIMAKI_SESSION_ID_ENV, resolveUploadToDiscordSessionId, } from './bash-tool-schema-plugin.js';
|
|
3
|
+
describe('extendBashToolDefinition', () => {
|
|
4
|
+
test('keeps parameters and assigns jsonSchema from bashParameters', () => {
|
|
5
|
+
const parameters = { keep: true };
|
|
6
|
+
const output = {
|
|
7
|
+
description: 'Execute a shell command',
|
|
8
|
+
parameters,
|
|
9
|
+
};
|
|
10
|
+
extendBashToolDefinition(output);
|
|
11
|
+
expect(output.parameters).toBe(parameters);
|
|
12
|
+
expect(output.jsonSchema).toMatchInlineSnapshot(`
|
|
13
|
+
{
|
|
14
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"properties": {
|
|
17
|
+
"command": {
|
|
18
|
+
"description": "The command to execute",
|
|
19
|
+
"type": "string",
|
|
20
|
+
},
|
|
21
|
+
"description": {
|
|
22
|
+
"description": "Short 5-10 word summary shown in Discord when the command is longer than 50 characters",
|
|
23
|
+
"type": "string",
|
|
24
|
+
},
|
|
25
|
+
"hasSideEffect": {
|
|
26
|
+
"description": "True if the command writes files, modifies state, installs packages, or triggers external effects",
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
},
|
|
29
|
+
"timeout": {
|
|
30
|
+
"description": "Optional timeout in milliseconds",
|
|
31
|
+
"maximum": 9007199254740991,
|
|
32
|
+
"minimum": -9007199254740991,
|
|
33
|
+
"type": "integer",
|
|
34
|
+
},
|
|
35
|
+
"workdir": {
|
|
36
|
+
"description": "Working directory. Defaults to the current directory.",
|
|
37
|
+
"type": "string",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
"required": [
|
|
41
|
+
"command",
|
|
42
|
+
"description",
|
|
43
|
+
"hasSideEffect",
|
|
44
|
+
],
|
|
45
|
+
"type": "object",
|
|
46
|
+
}
|
|
47
|
+
`);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('upload-to-discord session targeting', () => {
|
|
51
|
+
test('bash env uses the live OpenCode session, not a copied --session flag', () => {
|
|
52
|
+
const env = {};
|
|
53
|
+
injectKimakiSessionEnv({ sessionID: 'ses_child', env });
|
|
54
|
+
expect(env).toEqual({ [KIMAKI_SESSION_ID_ENV]: 'ses_child' });
|
|
55
|
+
expect(resolveUploadToDiscordSessionId({
|
|
56
|
+
flagSessionId: 'ses_parent',
|
|
57
|
+
envSessionId: env[KIMAKI_SESSION_ID_ENV],
|
|
58
|
+
})).toBe('ses_child');
|
|
59
|
+
});
|
|
60
|
+
test('falls back to --session when bash is not inside an OpenCode session', () => {
|
|
61
|
+
expect(resolveUploadToDiscordSessionId({
|
|
62
|
+
flagSessionId: 'ses_parent',
|
|
63
|
+
})).toBe('ses_parent');
|
|
64
|
+
});
|
|
65
|
+
test('returns undefined when neither live session nor flag is set', () => {
|
|
66
|
+
expect(resolveUploadToDiscordSessionId({})).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ChannelType, } from 'discord.js';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { getChannelDirectory, setChannelDirectory, findChannelsByDirectory, listTrackedTextChannels, } from './database.js';
|
|
7
|
+
import { getChannelDirectory, setChannelDirectory, findChannelsByDirectory, listTrackedTextChannels, getGuildCategories, setGuildCategoryId, } from './database.js';
|
|
8
8
|
import { getProjectsDir } from './config.js';
|
|
9
9
|
import { execAsync } from './worktrees.js';
|
|
10
10
|
import { createLogger, LogPrefix } from './logger.js';
|
|
@@ -39,41 +39,117 @@ async function trackProjectRegistered({ projectKind, source, }) {
|
|
|
39
39
|
trackEvent('project_registered', props);
|
|
40
40
|
}
|
|
41
41
|
const logger = createLogger(LogPrefix.CHANNEL);
|
|
42
|
-
|
|
43
|
-
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
|
|
42
|
+
function defaultCategoryName(kind, botName) {
|
|
44
43
|
const isKimakiBot = botName?.toLowerCase() === 'kimaki';
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if (kind === 'audio') {
|
|
45
|
+
return botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
|
|
46
|
+
}
|
|
47
|
+
return botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki';
|
|
48
|
+
}
|
|
49
|
+
function defaultKimakiChannelName({ botName, isGatewayMode, }) {
|
|
50
|
+
if (isGatewayMode || !botName)
|
|
51
|
+
return 'kimaki';
|
|
52
|
+
const sanitized = botName
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
55
|
+
.replace(/-+/g, '-')
|
|
56
|
+
.replace(/^-|-$/g, '');
|
|
57
|
+
if (!sanitized || sanitized === 'kimaki')
|
|
58
|
+
return 'kimaki';
|
|
59
|
+
return `kimaki-${sanitized}`.slice(0, 100);
|
|
60
|
+
}
|
|
61
|
+
const categoryEnsures = new Map();
|
|
62
|
+
function ensureCategorySerialized({ key, run, }) {
|
|
63
|
+
const existing = categoryEnsures.get(key);
|
|
64
|
+
if (existing)
|
|
65
|
+
return existing;
|
|
66
|
+
const promise = run().finally(() => {
|
|
67
|
+
if (categoryEnsures.get(key) === promise) {
|
|
68
|
+
categoryEnsures.delete(key);
|
|
49
69
|
}
|
|
50
|
-
return channel.name.toLowerCase() === categoryName.toLowerCase();
|
|
51
70
|
});
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
categoryEnsures.set(key, promise);
|
|
72
|
+
return promise;
|
|
73
|
+
}
|
|
74
|
+
function isUnknownDiscordChannel(error) {
|
|
75
|
+
const code = error instanceof Error ? Reflect.get(error, 'code') : undefined;
|
|
76
|
+
const status = error instanceof Error ? Reflect.get(error, 'status') : undefined;
|
|
77
|
+
return code === 10003 || status === 404;
|
|
78
|
+
}
|
|
79
|
+
function isCategoryChannel(channel) {
|
|
80
|
+
return channel?.type === ChannelType.GuildCategory;
|
|
81
|
+
}
|
|
82
|
+
async function fetchCategoryById(guild, categoryId) {
|
|
83
|
+
const cached = guild.channels.cache.get(categoryId);
|
|
84
|
+
if (isCategoryChannel(cached))
|
|
85
|
+
return cached;
|
|
86
|
+
try {
|
|
87
|
+
const fetched = await guild.channels.fetch(categoryId);
|
|
88
|
+
return isCategoryChannel(fetched) ? fetched : null;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (isUnknownDiscordChannel(error))
|
|
92
|
+
return null;
|
|
93
|
+
throw error;
|
|
54
94
|
}
|
|
55
|
-
return guild.channels.create({
|
|
56
|
-
name: categoryName,
|
|
57
|
-
type: ChannelType.GuildCategory,
|
|
58
|
-
});
|
|
59
95
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
96
|
+
async function adoptParentFromTrackedChannels({ guild, channelType, }) {
|
|
97
|
+
const mappings = await findChannelsByDirectory({ channelType });
|
|
98
|
+
const channels = await guild.channels.fetch();
|
|
99
|
+
for (const row of mappings) {
|
|
100
|
+
if (row.guild_id && row.guild_id !== guild.id)
|
|
101
|
+
continue;
|
|
102
|
+
const channel = channels.get(row.channel_id);
|
|
103
|
+
if (!channel?.parentId)
|
|
104
|
+
continue;
|
|
105
|
+
const parent = await fetchCategoryById(guild, channel.parentId);
|
|
106
|
+
if (parent)
|
|
107
|
+
return parent;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
async function resolveKimakiCategory({ guild, kind, botName, }) {
|
|
112
|
+
const stored = await getGuildCategories(guild.id);
|
|
113
|
+
const storedId = kind === 'audio' ? stored?.audio_category_id : stored?.category_id;
|
|
114
|
+
if (storedId) {
|
|
115
|
+
const existing = await fetchCategoryById(guild, storedId);
|
|
116
|
+
if (existing)
|
|
117
|
+
return existing;
|
|
118
|
+
}
|
|
119
|
+
const adopted = await adoptParentFromTrackedChannels({
|
|
120
|
+
guild,
|
|
121
|
+
channelType: kind === 'audio' ? 'voice' : 'text',
|
|
69
122
|
});
|
|
70
|
-
if (
|
|
71
|
-
|
|
123
|
+
if (adopted) {
|
|
124
|
+
await setGuildCategoryId({
|
|
125
|
+
guildId: guild.id,
|
|
126
|
+
kind,
|
|
127
|
+
categoryId: adopted.id,
|
|
128
|
+
});
|
|
129
|
+
return adopted;
|
|
72
130
|
}
|
|
73
|
-
|
|
74
|
-
name:
|
|
131
|
+
const created = await guild.channels.create({
|
|
132
|
+
name: defaultCategoryName(kind, botName),
|
|
75
133
|
type: ChannelType.GuildCategory,
|
|
76
134
|
});
|
|
135
|
+
await setGuildCategoryId({
|
|
136
|
+
guildId: guild.id,
|
|
137
|
+
kind,
|
|
138
|
+
categoryId: created.id,
|
|
139
|
+
});
|
|
140
|
+
return created;
|
|
141
|
+
}
|
|
142
|
+
export function ensureKimakiCategory(guild, botName) {
|
|
143
|
+
return ensureCategorySerialized({
|
|
144
|
+
key: `${guild.id}:text`,
|
|
145
|
+
run: () => resolveKimakiCategory({ guild, kind: 'text', botName }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export function ensureKimakiAudioCategory(guild, botName) {
|
|
149
|
+
return ensureCategorySerialized({
|
|
150
|
+
key: `${guild.id}:audio`,
|
|
151
|
+
run: () => resolveKimakiCategory({ guild, kind: 'audio', botName }),
|
|
152
|
+
});
|
|
77
153
|
}
|
|
78
154
|
export async function createProjectChannels({ guild, projectDirectory, botName, enableVoiceChannels = false, analyticsSource = 'cli', }) {
|
|
79
155
|
const baseName = path.basename(projectDirectory);
|
|
@@ -92,6 +168,7 @@ export async function createProjectChannels({ guild, projectDirectory, botName,
|
|
|
92
168
|
channelId: textChannel.id,
|
|
93
169
|
directory: projectDirectory,
|
|
94
170
|
channelType: 'text',
|
|
171
|
+
guildId: guild.id,
|
|
95
172
|
});
|
|
96
173
|
await trackProjectRegistered({
|
|
97
174
|
projectKind: 'user',
|
|
@@ -109,6 +186,7 @@ export async function createProjectChannels({ guild, projectDirectory, botName,
|
|
|
109
186
|
channelId: voiceChannel.id,
|
|
110
187
|
directory: projectDirectory,
|
|
111
188
|
channelType: 'voice',
|
|
189
|
+
guildId: guild.id,
|
|
112
190
|
});
|
|
113
191
|
voiceChannelId = voiceChannel.id;
|
|
114
192
|
}
|
|
@@ -158,8 +236,9 @@ const DEFAULT_CHANNEL_TOPIC = 'General channel for misc tasks with Kimaki. Not c
|
|
|
158
236
|
* Directory is ~/.kimaki/projects/kimaki, git-initialized with a .gitignore.
|
|
159
237
|
*
|
|
160
238
|
* Idempotency: checks the database for an existing channel mapped to the
|
|
161
|
-
* kimaki projects directory. Also scans
|
|
162
|
-
* as a fallback for channels created before
|
|
239
|
+
* kimaki projects directory. Also scans this machine's category for the
|
|
240
|
+
* exact default channel name as a fallback for channels created before
|
|
241
|
+
* DB mapping existed.
|
|
163
242
|
*/
|
|
164
243
|
export async function createDefaultKimakiChannel({ guild, botName, appId, isGatewayMode, }) {
|
|
165
244
|
const projectDirectory = getDefaultKimakiDirectory();
|
|
@@ -209,11 +288,9 @@ export async function createDefaultKimakiChannel({ guild, botName, appId, isGate
|
|
|
209
288
|
logger.log(`Default kimaki channel was previously provisioned for guild ${guild.name} (${guild.id}) as ${staleForThisGuild.channel_id}, but no longer exists. Skipping recreation.`);
|
|
210
289
|
return null;
|
|
211
290
|
}
|
|
212
|
-
// 2. Fallback: detect existing channel
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
// Do NOT adopt it — just skip channel creation entirely to avoid both
|
|
216
|
-
// instances fighting over the same channel.
|
|
291
|
+
// 2. Fallback: detect an existing default channel in THIS machine's group.
|
|
292
|
+
// A #kimaki channel in another machine's group is ignored.
|
|
293
|
+
const channelName = defaultKimakiChannelName({ botName, isGatewayMode });
|
|
217
294
|
const kimakiCategory = await ensureKimakiCategory(guild, botName);
|
|
218
295
|
const existingByName = guild.channels.cache.find((ch) => {
|
|
219
296
|
if (ch.type !== ChannelType.GuildText) {
|
|
@@ -222,10 +299,10 @@ export async function createDefaultKimakiChannel({ guild, botName, appId, isGate
|
|
|
222
299
|
if (ch.parentId !== kimakiCategory.id) {
|
|
223
300
|
return false;
|
|
224
301
|
}
|
|
225
|
-
return ch.name ===
|
|
302
|
+
return ch.name === channelName;
|
|
226
303
|
});
|
|
227
304
|
if (existingByName) {
|
|
228
|
-
logger.log(`Found existing default kimaki channel by name: ${existingByName.id}
|
|
305
|
+
logger.log(`Found existing default kimaki channel by name: ${existingByName.id}. Skipping recreation.`);
|
|
229
306
|
return null;
|
|
230
307
|
}
|
|
231
308
|
// Git init — gracefully skip if git is not installed
|
|
@@ -244,21 +321,6 @@ export async function createDefaultKimakiChannel({ guild, botName, appId, isGate
|
|
|
244
321
|
if (!fs.existsSync(gitignorePath)) {
|
|
245
322
|
fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE);
|
|
246
323
|
}
|
|
247
|
-
// Channel name: "kimaki-{botName}" for self-hosted, "kimaki" for gateway
|
|
248
|
-
const channelName = (() => {
|
|
249
|
-
if (isGatewayMode || !botName) {
|
|
250
|
-
return 'kimaki';
|
|
251
|
-
}
|
|
252
|
-
const sanitized = botName
|
|
253
|
-
.toLowerCase()
|
|
254
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
255
|
-
.replace(/-+/g, '-')
|
|
256
|
-
.replace(/^-|-$/g, '');
|
|
257
|
-
if (!sanitized || sanitized === 'kimaki') {
|
|
258
|
-
return 'kimaki';
|
|
259
|
-
}
|
|
260
|
-
return `kimaki-${sanitized}`.slice(0, 100);
|
|
261
|
-
})();
|
|
262
324
|
const textChannel = await guild.channels.create({
|
|
263
325
|
name: channelName,
|
|
264
326
|
type: ChannelType.GuildText,
|
|
@@ -24,6 +24,7 @@ import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
|
|
|
24
24
|
import { upgrade, getCurrentVersion } from '../upgrade.js';
|
|
25
25
|
import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
|
|
26
26
|
import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
|
|
27
|
+
import { resolveUploadToDiscordSessionId } from '../bash-tool-schema-plugin.js';
|
|
27
28
|
const cliLogger = createLogger(LogPrefix.CLI);
|
|
28
29
|
const cli = goke();
|
|
29
30
|
cli
|
|
@@ -31,7 +32,9 @@ cli
|
|
|
31
32
|
.option('-s, --session <sessionId>', 'OpenCode session ID')
|
|
32
33
|
.action(async (files, options) => {
|
|
33
34
|
try {
|
|
34
|
-
const
|
|
35
|
+
const sessionId = resolveUploadToDiscordSessionId({
|
|
36
|
+
flagSessionId: options.session,
|
|
37
|
+
});
|
|
35
38
|
if (!sessionId) {
|
|
36
39
|
cliLogger.error('Session ID is required. Use --session <sessionId>');
|
|
37
40
|
process.exit(EXIT_NO_RESTART);
|
|
@@ -12,7 +12,7 @@ import { spawn, execSync } from 'node:child_process';
|
|
|
12
12
|
import { createLogger, LogPrefix, initLogFile } from '../logger.js';
|
|
13
13
|
import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
|
|
14
14
|
import { getDefaultKimakiDirectory } from '../channel-management.js';
|
|
15
|
-
import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
|
|
15
|
+
import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, findRegisteredTextChannelForDirectory, formatProjectAlreadyRegisteredError, listGuildCategoryIds } from '../database.js';
|
|
16
16
|
import { ShareMarkdown } from '../markdown.js';
|
|
17
17
|
import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
|
|
18
18
|
import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
|
|
@@ -39,8 +39,15 @@ cli
|
|
|
39
39
|
cliLogger.error(`Directory does not exist: ${absolutePath}`);
|
|
40
40
|
process.exit(EXIT_NO_RESTART);
|
|
41
41
|
}
|
|
42
|
-
// Initialize database
|
|
43
42
|
await initDatabase();
|
|
43
|
+
const existingChannel = await findRegisteredTextChannelForDirectory(absolutePath);
|
|
44
|
+
if (existingChannel) {
|
|
45
|
+
cliLogger.error(formatProjectAlreadyRegisteredError({
|
|
46
|
+
channelId: existingChannel.channel_id,
|
|
47
|
+
directory: absolutePath,
|
|
48
|
+
}));
|
|
49
|
+
process.exit(EXIT_NO_RESTART);
|
|
50
|
+
}
|
|
44
51
|
const { token: botToken, appId } = await resolveBotCredentials({
|
|
45
52
|
appIdOverride: options.appId,
|
|
46
53
|
});
|
|
@@ -62,30 +69,6 @@ cli
|
|
|
62
69
|
});
|
|
63
70
|
cliLogger.log('Finding guild...');
|
|
64
71
|
const guild = await resolveGuildForProjectCommand({ client, guildIdOverride: options.guild });
|
|
65
|
-
// Check if channel already exists in this guild
|
|
66
|
-
cliLogger.log('Checking for existing channel...');
|
|
67
|
-
try {
|
|
68
|
-
const existingChannels = await findChannelsByDirectory({
|
|
69
|
-
directory: absolutePath,
|
|
70
|
-
channelType: 'text',
|
|
71
|
-
});
|
|
72
|
-
for (const existingChannel of existingChannels) {
|
|
73
|
-
try {
|
|
74
|
-
const ch = await client.channels.fetch(existingChannel.channel_id);
|
|
75
|
-
if (ch && !ch.isDMBased() && ch.guild.id === guild.id) {
|
|
76
|
-
void client.destroy();
|
|
77
|
-
cliLogger.error(`Channel already exists for this directory in ${guild.name}. Channel ID: ${existingChannel.channel_id}`);
|
|
78
|
-
process.exit(EXIT_NO_RESTART);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.stack : String(error));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.stack : String(error));
|
|
88
|
-
}
|
|
89
72
|
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
90
73
|
guild,
|
|
91
74
|
projectDirectory: absolutePath,
|
|
@@ -105,7 +88,7 @@ cli
|
|
|
105
88
|
cli
|
|
106
89
|
.command('project list', 'List all registered projects with their Discord channels')
|
|
107
90
|
.option('--json', 'Output as JSON')
|
|
108
|
-
.option('--all', 'Include remote projects from other machines (scans Kimaki
|
|
91
|
+
.option('--all', 'Include remote projects from other machines (scans Kimaki groups in Discord)')
|
|
109
92
|
.option('-g, --guild <guildId>', 'Discord guild/server ID to scan (used with --all when no local projects exist)')
|
|
110
93
|
.option('--prune', 'Remove stale entries whose Discord channel no longer exists')
|
|
111
94
|
.action(async (options) => {
|
|
@@ -131,7 +114,8 @@ cli
|
|
|
131
114
|
}
|
|
132
115
|
catch (error) {
|
|
133
116
|
// Only mark as deleted for Unknown Channel (10003) or 404,
|
|
134
|
-
// not transient errors like rate limits or 5xx
|
|
117
|
+
// not transient errors like rate limits or 5xx.
|
|
118
|
+
// Gateway-proxy must return Discord 404 for unknown channels, not 403.
|
|
135
119
|
const code = error instanceof Error ? Reflect.get(error, 'code') : undefined;
|
|
136
120
|
const status = error instanceof Error ? Reflect.get(error, 'status') : undefined;
|
|
137
121
|
const isUnknownChannel = code === 10003 || status === 404;
|
|
@@ -174,12 +158,15 @@ cli
|
|
|
174
158
|
process.exit(EXIT_NO_RESTART);
|
|
175
159
|
}
|
|
176
160
|
let guildScanFailures = 0;
|
|
161
|
+
const storedCategoryIds = new Set(await listGuildCategoryIds());
|
|
177
162
|
for (const guildId of uniqueGuildIds) {
|
|
178
163
|
try {
|
|
179
164
|
const guildChannels = (await rest.get(Routes.guildChannels(guildId)));
|
|
180
|
-
//
|
|
165
|
+
// Kimaki groups: name still starts with Kimaki, or this machine stored the id
|
|
166
|
+
// after a rename. Other machines' renamed groups cannot be found by name.
|
|
181
167
|
const kimakiCategoryIds = new Set(guildChannels
|
|
182
|
-
.filter((ch) => ch.type === 4 &&
|
|
168
|
+
.filter((ch) => ch.type === 4 &&
|
|
169
|
+
(/^kimaki(\s|$)/i.test(ch.name) || storedCategoryIds.has(ch.id)))
|
|
183
170
|
.map((ch) => ch.id));
|
|
184
171
|
// Find text channels (type 0) in Kimaki categories that are not in our local DB
|
|
185
172
|
for (const ch of guildChannels) {
|
|
@@ -21,6 +21,7 @@ import { ShareMarkdown } from '../markdown.js';
|
|
|
21
21
|
import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
|
|
22
22
|
import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
|
|
23
23
|
import { WORKTREE_PREFIX } from '../commands/merge-worktree.js';
|
|
24
|
+
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
24
25
|
import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js';
|
|
25
26
|
import { createDiscordRest } from '../discord-urls.js';
|
|
26
27
|
import { archiveThread, ensureThreadMember, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
|
|
@@ -394,6 +395,10 @@ cli
|
|
|
394
395
|
}
|
|
395
396
|
const threadPromptMarker = {
|
|
396
397
|
start: true,
|
|
398
|
+
...(threadTargetUser && {
|
|
399
|
+
userId: threadTargetUser.id,
|
|
400
|
+
...(threadTargetUser.username && { username: threadTargetUser.username }),
|
|
401
|
+
}),
|
|
397
402
|
...(options.agent && { agent: options.agent }),
|
|
398
403
|
...(options.model && { model: options.model }),
|
|
399
404
|
...(options.permission?.length ? { permissions: options.permission } : {}),
|
|
@@ -409,7 +414,7 @@ cli
|
|
|
409
414
|
// Prefix the prompt so it's clear who sent it (matches /queue format).
|
|
410
415
|
// Use a newline between prefix and prompt so leading /command
|
|
411
416
|
// detection can find the command on its own line.
|
|
412
|
-
const prefixedPrompt =
|
|
417
|
+
const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`;
|
|
413
418
|
if (threadTargetUser) {
|
|
414
419
|
cliLogger.log(`Adding user ${threadTargetUser.username || threadTargetUser.id} to thread...`);
|
|
415
420
|
const addMemberResult = await ensureThreadMember({
|