kimaki 0.26.0 → 0.28.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 +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -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/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- 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/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- 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-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- 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 +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- 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 +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -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 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- 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 +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- 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/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- 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/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -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 +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- 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-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- 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 +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- 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 +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- 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 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- 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 +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
|
|
1
2
|
/**
|
|
2
3
|
* Anthropic OAuth authentication plugin for OpenCode.
|
|
3
4
|
*
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
import { appendToastSessionMarker } from "./plugin-logger.js";
|
|
26
27
|
import { createPluginClient } from "./plugin-opencode-client.js";
|
|
27
28
|
import { isPermanentOAuthRefreshFailure, loadAccountStore, rememberAnthropicOAuth, removeAccountByAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
|
|
28
|
-
import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
29
|
+
import { applyClaudeCodeRequestIdentity, extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
29
30
|
// PKCE (Proof Key for Code Exchange) using Web Crypto API.
|
|
30
31
|
// Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
|
|
31
32
|
function base64urlEncode(bytes) {
|
|
@@ -62,7 +63,6 @@ const CALLBACK_PATH = "/callback";
|
|
|
62
63
|
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
63
64
|
const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
64
65
|
const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
65
|
-
const CLAUDE_CODE_VERSION = "2.1.75";
|
|
66
66
|
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
67
67
|
const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
|
|
68
68
|
// Subagent prompts don't contain OPENCODE_IDENTITY; opencode appends this
|
|
@@ -242,13 +242,10 @@ async function fetchAnthropicAccountIdentity(accessToken) {
|
|
|
242
242
|
for (const url of urls) {
|
|
243
243
|
const responseText = await requestText(url, {
|
|
244
244
|
method: "GET",
|
|
245
|
-
headers: {
|
|
246
|
-
Accept: "application/json",
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
`claude-cli/${CLAUDE_CODE_VERSION}`,
|
|
250
|
-
"x-app": "cli",
|
|
251
|
-
},
|
|
245
|
+
headers: Object.fromEntries(applyClaudeCodeRequestIdentity({
|
|
246
|
+
headers: new Headers({ Accept: "application/json" }),
|
|
247
|
+
accessToken,
|
|
248
|
+
}).entries()),
|
|
252
249
|
}).catch(() => {
|
|
253
250
|
return undefined;
|
|
254
251
|
});
|
|
@@ -876,10 +873,10 @@ const AnthropicAuthPlugin = async ({ serverUrl, directory }) => {
|
|
|
876
873
|
requestHeaders.set("accept", "application/json");
|
|
877
874
|
requestHeaders.set("anthropic-beta", mergeBetas(requestHeaders.get("anthropic-beta"), betas));
|
|
878
875
|
requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
876
|
+
applyClaudeCodeRequestIdentity({
|
|
877
|
+
headers: requestHeaders,
|
|
878
|
+
accessToken: auth.access,
|
|
879
|
+
});
|
|
883
880
|
requestHeaders.delete("x-api-key");
|
|
884
881
|
return fetch(input, {
|
|
885
882
|
...(init ?? {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Tests Anthropic system prompt rewriting so project instructions survive OpenCode prompt layout changes.
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { applyClaudeCodeRequestIdentity } from './anthropic-account-identity.js';
|
|
3
4
|
import { replacer } from './anthropic-auth-plugin.js';
|
|
4
5
|
async function transformSystem(systemText) {
|
|
5
6
|
const plugin = await replacer({});
|
|
@@ -12,6 +13,22 @@ async function transformSystem(systemText) {
|
|
|
12
13
|
}, output);
|
|
13
14
|
return output.system.join('\n');
|
|
14
15
|
}
|
|
16
|
+
describe('Anthropic OAuth request identity', () => {
|
|
17
|
+
test('applies the current external Claude Code client headers', () => {
|
|
18
|
+
const headers = applyClaudeCodeRequestIdentity({
|
|
19
|
+
headers: new Headers({ Accept: 'application/json' }),
|
|
20
|
+
accessToken: 'access-token',
|
|
21
|
+
});
|
|
22
|
+
expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`
|
|
23
|
+
{
|
|
24
|
+
"accept": "application/json",
|
|
25
|
+
"authorization": "Bearer access-token",
|
|
26
|
+
"user-agent": "claude-cli/2.1.257 (external, cli)",
|
|
27
|
+
"x-app": "cli",
|
|
28
|
+
}
|
|
29
|
+
`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
15
32
|
describe('Anthropic system prompt rewriting', () => {
|
|
16
33
|
test('preserves instructions when OpenCode places them before skills', async () => {
|
|
17
34
|
const transformed = await transformSystem(`You are OpenCode, the best coding agent on the planet.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Adds description and hasSideEffect to the built-in bash tool schema.
|
|
2
|
+
// Uses tool.definition so OpenCode keeps the real bash execute.
|
|
3
|
+
// Assign jsonSchema only. Leave parameters as the Effect Schema used to decode
|
|
4
|
+
// args before execute. Extra fields stay in the model-facing schema and in
|
|
5
|
+
// Discord input; they never reach the shell.
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const bashParameters = z.object({
|
|
8
|
+
command: z.string().describe('The command to execute'),
|
|
9
|
+
description: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe('Short 5-10 word summary shown in Discord when the command is longer than 50 characters'),
|
|
12
|
+
hasSideEffect: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.describe('True if the command writes files, modifies state, installs packages, or triggers external effects'),
|
|
15
|
+
workdir: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Working directory. Defaults to the current directory.'),
|
|
19
|
+
timeout: z.number().int().optional().describe('Optional timeout in milliseconds'),
|
|
20
|
+
});
|
|
21
|
+
export function extendBashToolDefinition(output) {
|
|
22
|
+
output.jsonSchema = z.toJSONSchema(bashParameters);
|
|
23
|
+
}
|
|
24
|
+
export const bashToolSchemaPlugin = async () => {
|
|
25
|
+
return {
|
|
26
|
+
'tool.definition': async (input, output) => {
|
|
27
|
+
if (input.toolID !== 'bash')
|
|
28
|
+
return;
|
|
29
|
+
extendBashToolDefinition(output);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { extendBashToolDefinition, } 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
|
+
});
|
package/dist/bin.js
CHANGED
|
@@ -32,14 +32,57 @@ else {
|
|
|
32
32
|
console.error('no subcommand detected. kimaki will automatically restart on crash');
|
|
33
33
|
console.error();
|
|
34
34
|
const EXIT_NO_RESTART = 64;
|
|
35
|
+
// Keep in sync with EXIT_TEMPFAIL in cli-runner.ts. Network-down login
|
|
36
|
+
// failures use this so a long outage cannot trip the crash-loop detector.
|
|
37
|
+
const EXIT_TEMPFAIL = 75;
|
|
35
38
|
const MAX_RAPID_RESTARTS = 5;
|
|
36
39
|
const RAPID_RESTART_WINDOW_MS = 60_000;
|
|
37
40
|
const RESTART_DELAY_MS = 2_000;
|
|
41
|
+
const CHILD_EXIT_DEADLINE_MS = 15_000;
|
|
38
42
|
const restartTimestamps = [];
|
|
43
|
+
let tempFailAttempts = 0;
|
|
39
44
|
let child = null;
|
|
40
45
|
// Track when we forwarded a termination signal so we don't restart after graceful shutdown
|
|
41
46
|
let shutdownRequested = false;
|
|
47
|
+
let forceKillTimer = null;
|
|
48
|
+
let scheduledRestart = null;
|
|
49
|
+
function clearForceKillTimer() {
|
|
50
|
+
if (!forceKillTimer)
|
|
51
|
+
return;
|
|
52
|
+
clearTimeout(forceKillTimer);
|
|
53
|
+
forceKillTimer = null;
|
|
54
|
+
}
|
|
55
|
+
function clearScheduledRestart() {
|
|
56
|
+
if (!scheduledRestart)
|
|
57
|
+
return;
|
|
58
|
+
clearTimeout(scheduledRestart);
|
|
59
|
+
scheduledRestart = null;
|
|
60
|
+
}
|
|
61
|
+
function killChild(signal) {
|
|
62
|
+
const target = child;
|
|
63
|
+
if (!target) {
|
|
64
|
+
if (shutdownRequested) {
|
|
65
|
+
clearScheduledRestart();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const sent = target.kill(signal);
|
|
71
|
+
if (!sent || forceKillTimer)
|
|
72
|
+
return;
|
|
73
|
+
forceKillTimer = setTimeout(() => {
|
|
74
|
+
forceKillTimer = null;
|
|
75
|
+
if (child !== target)
|
|
76
|
+
return;
|
|
77
|
+
console.error(`[kimaki] Child did not exit within ${CHILD_EXIT_DEADLINE_MS / 1000}s, force-killing it`);
|
|
78
|
+
target.kill('SIGKILL');
|
|
79
|
+
}, CHILD_EXIT_DEADLINE_MS);
|
|
80
|
+
forceKillTimer.unref();
|
|
81
|
+
}
|
|
42
82
|
function start() {
|
|
83
|
+
if (shutdownRequested)
|
|
84
|
+
return;
|
|
85
|
+
scheduledRestart = null;
|
|
43
86
|
if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
|
|
44
87
|
fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true });
|
|
45
88
|
}
|
|
@@ -48,32 +91,42 @@ else {
|
|
|
48
91
|
`--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
|
|
49
92
|
];
|
|
50
93
|
const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)];
|
|
51
|
-
|
|
94
|
+
const currentChild = spawn(process.argv[0], args, {
|
|
52
95
|
stdio: 'inherit',
|
|
53
96
|
env: { ...process.env, __KIMAKI_CHILD: '1' },
|
|
54
97
|
});
|
|
55
|
-
child
|
|
98
|
+
child = currentChild;
|
|
99
|
+
currentChild.on('exit', (code, signal) => {
|
|
100
|
+
if (child === currentChild)
|
|
101
|
+
child = null;
|
|
102
|
+
clearForceKillTimer();
|
|
56
103
|
if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
|
|
57
104
|
process.exit(code ?? 0);
|
|
58
105
|
return;
|
|
59
106
|
}
|
|
60
107
|
const now = Date.now();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
restartTimestamps
|
|
64
|
-
restartTimestamps.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
108
|
+
const isTempFail = code === EXIT_TEMPFAIL;
|
|
109
|
+
if (!isTempFail) {
|
|
110
|
+
restartTimestamps.push(now);
|
|
111
|
+
while (restartTimestamps.length > 0 &&
|
|
112
|
+
restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
|
|
113
|
+
restartTimestamps.shift();
|
|
114
|
+
}
|
|
115
|
+
if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
|
|
116
|
+
console.error(`[kimaki] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
70
120
|
}
|
|
71
121
|
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
|
72
122
|
// Progressive backoff: 2s, 4s, 8s, 16s, capped at 30s.
|
|
73
123
|
// Prevents hammering DNS/gateway during sustained network outages.
|
|
74
|
-
const
|
|
124
|
+
const backoffStep = isTempFail
|
|
125
|
+
? Math.min(tempFailAttempts++, 4)
|
|
126
|
+
: restartTimestamps.length - 1;
|
|
127
|
+
const delay = Math.min(RESTART_DELAY_MS * 2 ** backoffStep, 30_000);
|
|
75
128
|
console.error(`[kimaki] Process exited with ${reason}, restarting in ${(delay / 1000).toFixed(0)}s...`);
|
|
76
|
-
setTimeout(start, delay);
|
|
129
|
+
scheduledRestart = setTimeout(start, delay);
|
|
77
130
|
});
|
|
78
131
|
}
|
|
79
132
|
// Forward signals to child so graceful shutdown and heap snapshots work.
|
|
@@ -81,13 +134,14 @@ else {
|
|
|
81
134
|
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
82
135
|
process.on(sig, () => {
|
|
83
136
|
shutdownRequested = true;
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
for (const sig of ['SIGUSR1', 'SIGUSR2']) {
|
|
88
|
-
process.on(sig, () => {
|
|
89
|
-
child?.kill(sig);
|
|
137
|
+
killChild(sig);
|
|
90
138
|
});
|
|
91
139
|
}
|
|
140
|
+
process.on('SIGUSR1', () => {
|
|
141
|
+
child?.kill('SIGUSR1');
|
|
142
|
+
});
|
|
143
|
+
process.on('SIGUSR2', () => {
|
|
144
|
+
killChild('SIGUSR2');
|
|
145
|
+
});
|
|
92
146
|
start();
|
|
93
147
|
}
|
|
@@ -60,13 +60,21 @@ cli
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
cli
|
|
63
|
-
.command('worktree
|
|
63
|
+
.command('merge-worktree', 'Merge the current worktree into a target branch')
|
|
64
64
|
.option('-d, --directory <path>', 'Worktree directory (defaults to cwd)')
|
|
65
65
|
.option('-m, --main-repo <path>', 'Main repository directory (auto-detected from worktree)')
|
|
66
66
|
.option('-n, --name <name>', 'Worktree/branch name (auto-detected from branch)')
|
|
67
|
+
.option('--strategy [strategy]', z.enum(['rebase', 'squash'])
|
|
68
|
+
.default('rebase')
|
|
69
|
+
.describe('Keep commits or squash them into one'))
|
|
70
|
+
.option('--target-branch [branch]', z.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('Branch to merge into (defaults to origin/HEAD or main)'))
|
|
73
|
+
.option('--thread [threadId]', z.string().optional().describe('Discord thread to mark as merged'))
|
|
74
|
+
.option('--thread-name [name]', z.string().optional().describe('Discord thread name after merging'))
|
|
67
75
|
.action(async (options) => {
|
|
68
76
|
try {
|
|
69
|
-
const { mergeWorktree } = await import('../worktrees.js');
|
|
77
|
+
const { formatMergeWorktreeError, mergeWorktree } = await import('../worktrees.js');
|
|
70
78
|
const worktreeDir = path.resolve(options.directory || '.');
|
|
71
79
|
// Auto-detect main repo: find the main worktree's toplevel.
|
|
72
80
|
// For linked worktrees, --git-common-dir points to the shared .git,
|
|
@@ -77,14 +85,20 @@ cli
|
|
|
77
85
|
if (!mainRepoDir) {
|
|
78
86
|
try {
|
|
79
87
|
// `git worktree list --porcelain` first line is always the main worktree
|
|
80
|
-
const { stdout } = await execAsync(
|
|
88
|
+
const { stdout } = await execAsync({
|
|
89
|
+
command: 'git',
|
|
90
|
+
args: ['-C', worktreeDir, 'worktree', 'list', '--porcelain'],
|
|
91
|
+
});
|
|
81
92
|
const firstLine = stdout.split('\n')[0] || '';
|
|
82
93
|
// Format: "worktree /path/to/main"
|
|
83
94
|
mainRepoDir = firstLine.replace(/^worktree\s+/, '').trim();
|
|
84
95
|
}
|
|
85
96
|
catch {
|
|
86
97
|
// Fallback: derive from git common dir
|
|
87
|
-
const { stdout: commonDir } = await execAsync(
|
|
98
|
+
const { stdout: commonDir } = await execAsync({
|
|
99
|
+
command: 'git',
|
|
100
|
+
args: ['-C', worktreeDir, 'rev-parse', '--git-common-dir'],
|
|
101
|
+
});
|
|
88
102
|
const resolved = path.isAbsolute(commonDir.trim())
|
|
89
103
|
? commonDir.trim()
|
|
90
104
|
: path.resolve(worktreeDir, commonDir.trim());
|
|
@@ -95,7 +109,10 @@ cli
|
|
|
95
109
|
let worktreeName = options.name;
|
|
96
110
|
if (!worktreeName) {
|
|
97
111
|
try {
|
|
98
|
-
const { stdout } = await execAsync(
|
|
112
|
+
const { stdout } = await execAsync({
|
|
113
|
+
command: 'git',
|
|
114
|
+
args: ['-C', worktreeDir, 'symbolic-ref', '--short', 'HEAD'],
|
|
115
|
+
});
|
|
99
116
|
worktreeName = stdout.trim();
|
|
100
117
|
}
|
|
101
118
|
catch {
|
|
@@ -110,18 +127,40 @@ cli
|
|
|
110
127
|
worktreeDir,
|
|
111
128
|
mainRepoDir,
|
|
112
129
|
worktreeName,
|
|
130
|
+
strategy: options.strategy,
|
|
131
|
+
targetBranch: options.targetBranch,
|
|
113
132
|
onProgress: (msg) => {
|
|
114
133
|
cliLogger.log(msg);
|
|
115
134
|
},
|
|
116
135
|
});
|
|
117
136
|
if (result instanceof Error) {
|
|
118
|
-
cliLogger.error(
|
|
137
|
+
cliLogger.error(formatMergeWorktreeError(result, { maxLength: 8000 }));
|
|
119
138
|
if (result instanceof RebaseConflictError) {
|
|
120
139
|
cliLogger.log('Resolve the rebase conflicts, then run this command again.');
|
|
121
140
|
}
|
|
122
141
|
process.exit(1);
|
|
123
142
|
}
|
|
124
|
-
|
|
143
|
+
const action = options.strategy === 'squash' ? 'Squashed' : 'Merged';
|
|
144
|
+
cliLogger.log(`${action} ${result.branchName} into ${result.defaultBranch} @ ${result.shortSha} (${result.commitCount} source commit${result.commitCount === 1 ? '' : 's'})`);
|
|
145
|
+
if (options.thread && options.threadName) {
|
|
146
|
+
const credentials = await resolveBotCredentials().catch((e) => {
|
|
147
|
+
return new Error('Failed to resolve Discord credentials', { cause: e });
|
|
148
|
+
});
|
|
149
|
+
if (credentials instanceof Error) {
|
|
150
|
+
cliLogger.warn(`Could not update the thread title: ${credentials.message}`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const rest = createDiscordRest(credentials.token);
|
|
154
|
+
const renameResult = await rest
|
|
155
|
+
.patch(Routes.channel(options.thread), {
|
|
156
|
+
body: { name: options.threadName },
|
|
157
|
+
})
|
|
158
|
+
.catch((e) => new Error('Discord rejected the thread rename', { cause: e }));
|
|
159
|
+
if (renameResult instanceof Error) {
|
|
160
|
+
cliLogger.warn(`Could not update the thread title: ${renameResult.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
125
164
|
process.exit(0);
|
|
126
165
|
}
|
|
127
166
|
catch (error) {
|
|
@@ -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 } 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,
|
|
@@ -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;
|
|
@@ -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';
|
|
@@ -29,6 +30,7 @@ import { execAsync, resolveSessionWorkingDirectory, isGitRepositoryRoot } from '
|
|
|
29
30
|
import { upgrade, getCurrentVersion } from '../upgrade.js';
|
|
30
31
|
import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
|
|
31
32
|
import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
|
|
33
|
+
import { validateCliModelOption } from '../session-handler/model-utils.js';
|
|
32
34
|
const cliLogger = createLogger(LogPrefix.CLI);
|
|
33
35
|
const cli = goke();
|
|
34
36
|
cli
|
|
@@ -89,6 +91,13 @@ cli
|
|
|
89
91
|
cliLogger.error('Prompt is required. Use --prompt <prompt>');
|
|
90
92
|
process.exit(EXIT_NO_RESTART);
|
|
91
93
|
}
|
|
94
|
+
const earlyModelCheck = await validateCliModelOption({
|
|
95
|
+
model: options.model,
|
|
96
|
+
});
|
|
97
|
+
if (earlyModelCheck instanceof Error) {
|
|
98
|
+
cliLogger.error(earlyModelCheck.message);
|
|
99
|
+
process.exit(EXIT_NO_RESTART);
|
|
100
|
+
}
|
|
92
101
|
const filePaths = options.file?.length
|
|
93
102
|
? options.file.map((f) => path.resolve(f))
|
|
94
103
|
: undefined;
|
|
@@ -335,6 +344,14 @@ cli
|
|
|
335
344
|
// We only require it for features that genuinely need a local directory
|
|
336
345
|
// (scheduled tasks and --wait).
|
|
337
346
|
const channelConfig = await getChannelDirectory(threadData.parent_id);
|
|
347
|
+
const threadModelCheck = await validateCliModelOption({
|
|
348
|
+
model: options.model,
|
|
349
|
+
directory: channelConfig?.directory,
|
|
350
|
+
});
|
|
351
|
+
if (threadModelCheck instanceof Error) {
|
|
352
|
+
cliLogger.error(threadModelCheck.message);
|
|
353
|
+
process.exit(EXIT_NO_RESTART);
|
|
354
|
+
}
|
|
338
355
|
// Guard early: fail before sending the message if a feature that
|
|
339
356
|
// needs local project directory mapping is requested.
|
|
340
357
|
if (!channelConfig && (parsedSchedule || options.wait)) {
|
|
@@ -378,6 +395,10 @@ cli
|
|
|
378
395
|
}
|
|
379
396
|
const threadPromptMarker = {
|
|
380
397
|
start: true,
|
|
398
|
+
...(threadTargetUser && {
|
|
399
|
+
userId: threadTargetUser.id,
|
|
400
|
+
...(threadTargetUser.username && { username: threadTargetUser.username }),
|
|
401
|
+
}),
|
|
381
402
|
...(options.agent && { agent: options.agent }),
|
|
382
403
|
...(options.model && { model: options.model }),
|
|
383
404
|
...(options.permission?.length ? { permissions: options.permission } : {}),
|
|
@@ -393,7 +414,7 @@ cli
|
|
|
393
414
|
// Prefix the prompt so it's clear who sent it (matches /queue format).
|
|
394
415
|
// Use a newline between prefix and prompt so leading /command
|
|
395
416
|
// detection can find the command on its own line.
|
|
396
|
-
const prefixedPrompt =
|
|
417
|
+
const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`;
|
|
397
418
|
if (threadTargetUser) {
|
|
398
419
|
cliLogger.log(`Adding user ${threadTargetUser.username || threadTargetUser.id} to thread...`);
|
|
399
420
|
const addMemberResult = await ensureThreadMember({
|
|
@@ -446,6 +467,14 @@ cli
|
|
|
446
467
|
// (--send-at, --wait, --cwd).
|
|
447
468
|
const channelConfig = await getChannelDirectory(channelData.id);
|
|
448
469
|
const projectDirectory = channelConfig?.directory;
|
|
470
|
+
const channelModelCheck = await validateCliModelOption({
|
|
471
|
+
model: options.model,
|
|
472
|
+
directory: projectDirectory,
|
|
473
|
+
});
|
|
474
|
+
if (channelModelCheck instanceof Error) {
|
|
475
|
+
cliLogger.error(channelModelCheck.message);
|
|
476
|
+
process.exit(EXIT_NO_RESTART);
|
|
477
|
+
}
|
|
449
478
|
// Features that require a local project directory mapping
|
|
450
479
|
const needsProjectDirectory = Boolean(parsedSchedule || options.wait || options.cwd);
|
|
451
480
|
if (!channelConfig && needsProjectDirectory) {
|