@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Worktree management command: /new-worktree
|
|
2
|
+
// Uses OpenCode SDK v2 to create worktrees with kimaki- prefix
|
|
3
|
+
// Creates thread immediately, then worktree in background so user can type
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ChannelType,
|
|
7
|
+
REST,
|
|
8
|
+
type TextChannel,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
type Message,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import type { CommandContext } from './types.js'
|
|
14
|
+
import {
|
|
15
|
+
createPendingWorktree,
|
|
16
|
+
setWorktreeReady,
|
|
17
|
+
setWorktreeError,
|
|
18
|
+
getChannelDirectory,
|
|
19
|
+
getThreadWorktree,
|
|
20
|
+
} from '../database.js'
|
|
21
|
+
import { SILENT_MESSAGE_FLAGS, reactToThread } from '../discord-utils.js'
|
|
22
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
23
|
+
import { notifyError } from '../sentry.js'
|
|
24
|
+
import {
|
|
25
|
+
createWorktreeWithSubmodules,
|
|
26
|
+
captureGitDiff,
|
|
27
|
+
execAsync,
|
|
28
|
+
type CapturedDiff,
|
|
29
|
+
} from '../worktree-utils.js'
|
|
30
|
+
import { WORKTREE_PREFIX } from './merge-worktree.js'
|
|
31
|
+
import * as errore from 'errore'
|
|
32
|
+
|
|
33
|
+
const logger = createLogger(LogPrefix.WORKTREE)
|
|
34
|
+
|
|
35
|
+
class WorktreeError extends Error {
|
|
36
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
37
|
+
super(message, options)
|
|
38
|
+
this.name = 'WorktreeError'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/kimaki- prefix.
|
|
44
|
+
* "My Feature" → "opencode/kimaki-my-feature"
|
|
45
|
+
* Returns empty string if no valid name can be extracted.
|
|
46
|
+
*/
|
|
47
|
+
export function formatWorktreeName(name: string): string {
|
|
48
|
+
const formatted = name
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/\s+/g, '-')
|
|
52
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
53
|
+
|
|
54
|
+
if (!formatted) {
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
return `opencode/kimaki-${formatted}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Derive worktree name from thread name.
|
|
62
|
+
* Handles existing "⬦ worktree: opencode/kimaki-name" format or uses thread name directly.
|
|
63
|
+
*/
|
|
64
|
+
function deriveWorktreeNameFromThread(threadName: string): string {
|
|
65
|
+
// Handle existing "⬦ worktree: opencode/kimaki-name" format
|
|
66
|
+
const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i)
|
|
67
|
+
const extractedName = worktreeMatch?.[1]?.trim()
|
|
68
|
+
if (extractedName) {
|
|
69
|
+
// If already has opencode/kimaki- prefix, return as is
|
|
70
|
+
if (extractedName.startsWith('opencode/kimaki-')) {
|
|
71
|
+
return extractedName
|
|
72
|
+
}
|
|
73
|
+
return formatWorktreeName(extractedName)
|
|
74
|
+
}
|
|
75
|
+
// Use thread name directly
|
|
76
|
+
return formatWorktreeName(threadName)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get project directory from database.
|
|
81
|
+
*/
|
|
82
|
+
async function getProjectDirectoryFromChannel(
|
|
83
|
+
channel: TextChannel,
|
|
84
|
+
appId: string,
|
|
85
|
+
): Promise<string | WorktreeError> {
|
|
86
|
+
const channelConfig = await getChannelDirectory(channel.id)
|
|
87
|
+
|
|
88
|
+
if (!channelConfig) {
|
|
89
|
+
return new WorktreeError(
|
|
90
|
+
'This channel is not configured with a project directory',
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
95
|
+
return new WorktreeError('This channel is not configured for this bot')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(channelConfig.directory)) {
|
|
99
|
+
return new WorktreeError(
|
|
100
|
+
`Directory does not exist: ${channelConfig.directory}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return channelConfig.directory
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create worktree in background and update starter message when done.
|
|
109
|
+
* If diff is provided, it's applied during worktree creation (before submodule init).
|
|
110
|
+
*/
|
|
111
|
+
async function createWorktreeInBackground({
|
|
112
|
+
thread,
|
|
113
|
+
starterMessage,
|
|
114
|
+
worktreeName,
|
|
115
|
+
projectDirectory,
|
|
116
|
+
diff,
|
|
117
|
+
rest,
|
|
118
|
+
}: {
|
|
119
|
+
thread: ThreadChannel
|
|
120
|
+
starterMessage: Message
|
|
121
|
+
worktreeName: string
|
|
122
|
+
projectDirectory: string
|
|
123
|
+
diff?: CapturedDiff | null
|
|
124
|
+
rest: REST
|
|
125
|
+
}): Promise<void> {
|
|
126
|
+
// Create worktree using git, apply diff, then init submodules
|
|
127
|
+
logger.log(
|
|
128
|
+
`Creating worktree "${worktreeName}" for project ${projectDirectory}`,
|
|
129
|
+
)
|
|
130
|
+
const worktreeResult = await createWorktreeWithSubmodules({
|
|
131
|
+
directory: projectDirectory,
|
|
132
|
+
name: worktreeName,
|
|
133
|
+
diff,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (worktreeResult instanceof Error) {
|
|
137
|
+
const errorMsg = worktreeResult.message
|
|
138
|
+
logger.error('[NEW-WORKTREE] Error:', worktreeResult)
|
|
139
|
+
await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
|
|
140
|
+
await starterMessage.edit(
|
|
141
|
+
`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`,
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Success - update database and edit starter message
|
|
147
|
+
await setWorktreeReady({
|
|
148
|
+
threadId: thread.id,
|
|
149
|
+
worktreeDirectory: worktreeResult.directory,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// React with tree emoji to mark as worktree thread
|
|
153
|
+
await reactToThread({
|
|
154
|
+
rest,
|
|
155
|
+
threadId: thread.id,
|
|
156
|
+
channelId: thread.parentId || undefined,
|
|
157
|
+
emoji: '🌳',
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const diffStatus = diff
|
|
161
|
+
? worktreeResult.diffApplied
|
|
162
|
+
? '\n✅ Changes applied'
|
|
163
|
+
: '\n⚠️ Failed to apply changes'
|
|
164
|
+
: ''
|
|
165
|
+
await starterMessage.edit(
|
|
166
|
+
`🌳 **Worktree: ${worktreeName}**\n` +
|
|
167
|
+
`📁 \`${worktreeResult.directory}\`\n` +
|
|
168
|
+
`🌿 Branch: \`${worktreeResult.branch}\`` +
|
|
169
|
+
diffStatus,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function findExistingWorktreePath({
|
|
174
|
+
projectDirectory,
|
|
175
|
+
worktreeName,
|
|
176
|
+
}: {
|
|
177
|
+
projectDirectory: string
|
|
178
|
+
worktreeName: string
|
|
179
|
+
}): Promise<string | undefined | Error> {
|
|
180
|
+
const listResult = await errore.tryAsync({
|
|
181
|
+
try: () =>
|
|
182
|
+
execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
|
|
183
|
+
catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
|
|
184
|
+
})
|
|
185
|
+
if (errore.isError(listResult)) {
|
|
186
|
+
return listResult
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const lines = listResult.stdout.split('\n')
|
|
190
|
+
let currentPath = ''
|
|
191
|
+
const branchRef = `refs/heads/${worktreeName}`
|
|
192
|
+
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
if (line.startsWith('worktree ')) {
|
|
195
|
+
currentPath = line.slice('worktree '.length)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
line.startsWith('branch ') &&
|
|
200
|
+
line.slice('branch '.length) === branchRef
|
|
201
|
+
) {
|
|
202
|
+
return currentPath || undefined
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function handleNewWorktreeCommand({
|
|
210
|
+
command,
|
|
211
|
+
appId,
|
|
212
|
+
}: CommandContext): Promise<void> {
|
|
213
|
+
await command.deferReply({ ephemeral: false })
|
|
214
|
+
|
|
215
|
+
const channel = command.channel
|
|
216
|
+
if (!channel) {
|
|
217
|
+
await command.editReply('Cannot determine channel')
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const isThread =
|
|
222
|
+
channel.type === ChannelType.PublicThread ||
|
|
223
|
+
channel.type === ChannelType.PrivateThread
|
|
224
|
+
|
|
225
|
+
// Handle command in existing thread - attach worktree to this thread
|
|
226
|
+
if (isThread) {
|
|
227
|
+
await handleWorktreeInThread({
|
|
228
|
+
command,
|
|
229
|
+
appId,
|
|
230
|
+
thread: channel as ThreadChannel,
|
|
231
|
+
})
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Handle command in text channel - create new thread with worktree (existing behavior)
|
|
236
|
+
if (channel.type !== ChannelType.GuildText) {
|
|
237
|
+
await command.editReply(
|
|
238
|
+
'This command can only be used in text channels or threads',
|
|
239
|
+
)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const rawName = command.options.getString('name')
|
|
244
|
+
if (!rawName) {
|
|
245
|
+
await command.editReply(
|
|
246
|
+
'Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`',
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const worktreeName = formatWorktreeName(rawName)
|
|
252
|
+
if (!worktreeName) {
|
|
253
|
+
await command.editReply(
|
|
254
|
+
'Invalid worktree name. Please use letters, numbers, and spaces.',
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const textChannel = channel as TextChannel
|
|
260
|
+
|
|
261
|
+
const projectDirectory = await getProjectDirectoryFromChannel(
|
|
262
|
+
textChannel,
|
|
263
|
+
appId,
|
|
264
|
+
)
|
|
265
|
+
if (errore.isError(projectDirectory)) {
|
|
266
|
+
await command.editReply(projectDirectory.message)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const existingWorktree = await findExistingWorktreePath({
|
|
271
|
+
projectDirectory,
|
|
272
|
+
worktreeName,
|
|
273
|
+
})
|
|
274
|
+
if (errore.isError(existingWorktree)) {
|
|
275
|
+
await command.editReply(existingWorktree.message)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
if (existingWorktree) {
|
|
279
|
+
await command.editReply(
|
|
280
|
+
`Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``,
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create thread immediately so user can start typing
|
|
286
|
+
const result = await errore.tryAsync({
|
|
287
|
+
try: async () => {
|
|
288
|
+
const starterMessage = await textChannel.send({
|
|
289
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`,
|
|
290
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const thread = await starterMessage.startThread({
|
|
294
|
+
name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
|
|
295
|
+
autoArchiveDuration: 1440,
|
|
296
|
+
reason: 'Worktree session',
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Add user to thread so it appears in their sidebar
|
|
300
|
+
await thread.members.add(command.user.id)
|
|
301
|
+
|
|
302
|
+
return { thread, starterMessage }
|
|
303
|
+
},
|
|
304
|
+
catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (errore.isError(result)) {
|
|
308
|
+
logger.error('[NEW-WORKTREE] Error:', result.cause)
|
|
309
|
+
await command.editReply(result.message)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { thread, starterMessage } = result
|
|
314
|
+
|
|
315
|
+
// Store pending worktree in database
|
|
316
|
+
await createPendingWorktree({
|
|
317
|
+
threadId: thread.id,
|
|
318
|
+
worktreeName,
|
|
319
|
+
projectDirectory,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await command.editReply(`Creating worktree in ${thread.toString()}`)
|
|
323
|
+
|
|
324
|
+
// Create worktree in background (don't await)
|
|
325
|
+
createWorktreeInBackground({
|
|
326
|
+
thread,
|
|
327
|
+
starterMessage,
|
|
328
|
+
worktreeName,
|
|
329
|
+
projectDirectory,
|
|
330
|
+
rest: command.client.rest,
|
|
331
|
+
}).catch((e) => {
|
|
332
|
+
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
333
|
+
void notifyError(e, 'Background worktree creation failed')
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Handle /new-worktree when called inside an existing thread.
|
|
339
|
+
* Attaches a worktree to the current thread, using thread name if no name provided.
|
|
340
|
+
*/
|
|
341
|
+
async function handleWorktreeInThread({
|
|
342
|
+
command,
|
|
343
|
+
appId,
|
|
344
|
+
thread,
|
|
345
|
+
}: CommandContext & { thread: ThreadChannel }): Promise<void> {
|
|
346
|
+
// Error if thread already has a worktree
|
|
347
|
+
if (await getThreadWorktree(thread.id)) {
|
|
348
|
+
await command.editReply('This thread already has a worktree attached.')
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Get worktree name from parameter or derive from thread name
|
|
353
|
+
const rawName = command.options.getString('name')
|
|
354
|
+
const worktreeName = rawName
|
|
355
|
+
? formatWorktreeName(rawName)
|
|
356
|
+
: deriveWorktreeNameFromThread(thread.name)
|
|
357
|
+
|
|
358
|
+
if (!worktreeName) {
|
|
359
|
+
await command.editReply(
|
|
360
|
+
'Invalid worktree name. Please provide a name or rename the thread.',
|
|
361
|
+
)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Get parent channel for project directory
|
|
366
|
+
const parent = thread.parent
|
|
367
|
+
if (!parent || parent.type !== ChannelType.GuildText) {
|
|
368
|
+
await command.editReply('Cannot determine parent channel')
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const projectDirectory = await getProjectDirectoryFromChannel(
|
|
373
|
+
parent as TextChannel,
|
|
374
|
+
appId,
|
|
375
|
+
)
|
|
376
|
+
if (errore.isError(projectDirectory)) {
|
|
377
|
+
await command.editReply(projectDirectory.message)
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const existingWorktreePath = await findExistingWorktreePath({
|
|
382
|
+
projectDirectory,
|
|
383
|
+
worktreeName,
|
|
384
|
+
})
|
|
385
|
+
if (errore.isError(existingWorktreePath)) {
|
|
386
|
+
await command.editReply(existingWorktreePath.message)
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if (existingWorktreePath) {
|
|
390
|
+
await command.editReply(
|
|
391
|
+
`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``,
|
|
392
|
+
)
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Capture git diff from project directory before creating worktree.
|
|
397
|
+
// This allows transferring uncommitted changes to the new worktree.
|
|
398
|
+
const diff = await captureGitDiff(projectDirectory)
|
|
399
|
+
const hasDiff = diff && (diff.staged || diff.unstaged)
|
|
400
|
+
|
|
401
|
+
// Store pending worktree in database for this existing thread
|
|
402
|
+
await createPendingWorktree({
|
|
403
|
+
threadId: thread.id,
|
|
404
|
+
worktreeName,
|
|
405
|
+
projectDirectory,
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Send status message in thread
|
|
409
|
+
const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : ''
|
|
410
|
+
const statusMessage = await thread.send({
|
|
411
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
|
|
412
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
await command.editReply(
|
|
416
|
+
`Creating worktree \`${worktreeName}\` for this thread...`,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
// Create worktree in background, passing diff to apply after creation
|
|
420
|
+
createWorktreeInBackground({
|
|
421
|
+
thread,
|
|
422
|
+
starterMessage: statusMessage,
|
|
423
|
+
worktreeName,
|
|
424
|
+
projectDirectory,
|
|
425
|
+
diff,
|
|
426
|
+
rest: command.client.rest,
|
|
427
|
+
}).catch((e) => {
|
|
428
|
+
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
429
|
+
void notifyError(e, 'Background worktree creation failed (in-thread)')
|
|
430
|
+
})
|
|
431
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Utility to condense MEMORY.md into a line-numbered table of contents.
|
|
2
|
+
// Separated from opencode-plugin.ts because OpenCode's plugin loader calls
|
|
3
|
+
// every exported function in the module as a plugin initializer — exporting
|
|
4
|
+
// this utility from the plugin entry file caused it to be invoked with a
|
|
5
|
+
// PluginInput object instead of a string, crashing inside marked's Lexer.
|
|
6
|
+
|
|
7
|
+
import { Lexer } from 'marked'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Condense MEMORY.md into a line-numbered table of contents.
|
|
11
|
+
* Parses markdown AST with marked's Lexer, emits each heading prefixed by
|
|
12
|
+
* its source line number, and collapses non-heading content to `...`.
|
|
13
|
+
* The agent can then use Read with offset/limit to read specific sections.
|
|
14
|
+
*/
|
|
15
|
+
export function condenseMemoryMd(content: string): string {
|
|
16
|
+
const tokens = new Lexer().lex(content)
|
|
17
|
+
const lines: string[] = []
|
|
18
|
+
let charOffset = 0
|
|
19
|
+
let lastWasEllipsis = false
|
|
20
|
+
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
// Compute 1-based line number from character offset
|
|
23
|
+
const lineNumber = content.slice(0, charOffset).split('\n').length
|
|
24
|
+
if (token.type === 'heading') {
|
|
25
|
+
const prefix = '#'.repeat(token.depth)
|
|
26
|
+
lines.push(`${lineNumber}: ${prefix} ${token.text}`)
|
|
27
|
+
lastWasEllipsis = false
|
|
28
|
+
} else if (!lastWasEllipsis) {
|
|
29
|
+
lines.push('...')
|
|
30
|
+
lastWasEllipsis = true
|
|
31
|
+
}
|
|
32
|
+
charOffset += token.raw.length
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return lines.join('\n')
|
|
36
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Runtime configuration for Kimaki bot.
|
|
2
|
+
// Stores data directory path and provides accessors for other modules.
|
|
3
|
+
// Must be initialized before database or other path-dependent modules are used.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_DATA_DIR = path.join(os.homedir(), '.kimaki')
|
|
10
|
+
|
|
11
|
+
let dataDir: string | null = null
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the data directory path.
|
|
15
|
+
* Falls back to ~/.kimaki if not explicitly set.
|
|
16
|
+
* Under vitest (KIMAKI_VITEST env var), auto-creates an isolated temp dir so
|
|
17
|
+
* tests never touch the real ~/.kimaki/ database. Tests that need a specific
|
|
18
|
+
* dir can still call setDataDir() before any DB access to override this.
|
|
19
|
+
*/
|
|
20
|
+
export function getDataDir(): string {
|
|
21
|
+
if (!dataDir) {
|
|
22
|
+
if (process.env.KIMAKI_VITEST) {
|
|
23
|
+
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-test-'))
|
|
24
|
+
} else {
|
|
25
|
+
dataDir = DEFAULT_DATA_DIR
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return dataDir
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set the data directory path.
|
|
33
|
+
* Creates the directory if it doesn't exist.
|
|
34
|
+
* Must be called before any database or path-dependent operations.
|
|
35
|
+
*/
|
|
36
|
+
export function setDataDir(dir: string): void {
|
|
37
|
+
const resolvedDir = path.resolve(dir)
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
40
|
+
fs.mkdirSync(resolvedDir, { recursive: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
dataDir = resolvedDir
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the projects directory path (for /create-new-project command).
|
|
48
|
+
* Returns <dataDir>/projects
|
|
49
|
+
*/
|
|
50
|
+
export function getProjectsDir(): string {
|
|
51
|
+
return path.join(getDataDir(), 'projects')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Default verbosity for channels that haven't set a per-channel override.
|
|
55
|
+
// Set via --verbosity CLI flag at startup.
|
|
56
|
+
import type { VerbosityLevel } from './database.js'
|
|
57
|
+
|
|
58
|
+
let defaultVerbosity: VerbosityLevel = 'text-and-essential-tools'
|
|
59
|
+
|
|
60
|
+
export function getDefaultVerbosity(): VerbosityLevel {
|
|
61
|
+
return defaultVerbosity
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function setDefaultVerbosity(level: VerbosityLevel): void {
|
|
65
|
+
defaultVerbosity = level
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Default mention mode for channels that haven't set a per-channel override.
|
|
69
|
+
// Set via --mention-mode CLI flag at startup.
|
|
70
|
+
let defaultMentionMode = false
|
|
71
|
+
|
|
72
|
+
export function getDefaultMentionMode(): boolean {
|
|
73
|
+
return defaultMentionMode
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function setDefaultMentionMode(enabled: boolean): void {
|
|
77
|
+
defaultMentionMode = enabled
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Whether critique (diff upload to critique.work) is enabled in system prompts.
|
|
81
|
+
// Enabled by default, disabled via --no-critique CLI flag.
|
|
82
|
+
let critiqueEnabled = true
|
|
83
|
+
|
|
84
|
+
export function getCritiqueEnabled(): boolean {
|
|
85
|
+
return critiqueEnabled
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setCritiqueEnabled(enabled: boolean): void {
|
|
89
|
+
critiqueEnabled = enabled
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Whether to forward OpenCode server stdout/stderr to kimaki.log.
|
|
93
|
+
// Disabled by default, enabled via --verbose-opencode-server CLI flag.
|
|
94
|
+
let verboseOpencodeServer = false
|
|
95
|
+
|
|
96
|
+
export function getVerboseOpencodeServer(): boolean {
|
|
97
|
+
return verboseOpencodeServer
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function setVerboseOpencodeServer(enabled: boolean): void {
|
|
101
|
+
verboseOpencodeServer = enabled
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Registered user commands, populated by registerCommands() in cli.ts.
|
|
105
|
+
// Stored here (not cli.ts) to avoid circular imports since commands/ modules need this.
|
|
106
|
+
// discordName is the sanitized Discord slash command name (without -cmd suffix),
|
|
107
|
+
// name is the original OpenCode command name (may contain :, /, etc).
|
|
108
|
+
export type RegisteredUserCommand = {
|
|
109
|
+
name: string
|
|
110
|
+
discordName: string
|
|
111
|
+
description: string
|
|
112
|
+
}
|
|
113
|
+
export const registeredUserCommands: RegisteredUserCommand[] = []
|
|
114
|
+
|
|
115
|
+
const DEFAULT_LOCK_PORT = 29988
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Derive a lock port from the data directory path.
|
|
119
|
+
* If KIMAKI_LOCK_PORT is set to a valid TCP port, it takes precedence.
|
|
120
|
+
* Returns 29988 for the default ~/.kimaki directory (backwards compatible).
|
|
121
|
+
* For custom data dirs, uses a hash to generate a port in the range 30000-39999.
|
|
122
|
+
*/
|
|
123
|
+
export function getLockPort(): number {
|
|
124
|
+
const envPortRaw = process.env['KIMAKI_LOCK_PORT']
|
|
125
|
+
if (envPortRaw) {
|
|
126
|
+
const envPort = Number.parseInt(envPortRaw, 10)
|
|
127
|
+
if (Number.isInteger(envPort) && envPort >= 1 && envPort <= 65535) {
|
|
128
|
+
return envPort
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const dir = getDataDir()
|
|
133
|
+
|
|
134
|
+
// Use original port for default data dir (backwards compatible)
|
|
135
|
+
if (dir === DEFAULT_DATA_DIR) {
|
|
136
|
+
return DEFAULT_LOCK_PORT
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Hash-based port for custom data dirs
|
|
140
|
+
let hash = 0
|
|
141
|
+
for (let i = 0; i < dir.length; i++) {
|
|
142
|
+
const char = dir.charCodeAt(i)
|
|
143
|
+
hash = (hash << 5) - hash + char
|
|
144
|
+
hash = hash & hash // Convert to 32bit integer
|
|
145
|
+
}
|
|
146
|
+
// Map to port range 30000-39999
|
|
147
|
+
return 30000 + (Math.abs(hash) % 10000)
|
|
148
|
+
}
|