kimaki 0.26.0 → 0.27.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 +25 -122
- package/dist/anthropic-account-identity.js +1 -0
- package/dist/anthropic-auth-plugin.js +1 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/send.js +24 -0
- package/dist/cli-commands/session.js +183 -8
- package/dist/cli-commands/task.js +13 -1
- package/dist/cli-parsing.test.js +59 -3
- package/dist/cli-runner.js +28 -4
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/agent.js +6 -8
- package/dist/commands/ask-question.js +21 -32
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +36 -10
- package/dist/commands/model-variant.js +7 -1
- package/dist/commands/model.js +14 -8
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/database.js +124 -3
- package/dist/db.js +22 -0
- package/dist/db.test.js +167 -1
- package/dist/discord-bot.js +34 -32
- package/dist/discord-command-registration.js +9 -1
- package/dist/discord-utils.js +13 -5
- package/dist/discord-utils.test.js +17 -1
- package/dist/errors.js +5 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- 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 +5 -5
- package/dist/git-worktree-core.js +182 -17
- package/dist/ipc-tools-plugin.js +63 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +7 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +10 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +18 -7
- package/dist/message-formatting.test.js +56 -9
- 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.test.js +60 -0
- package/dist/opencode.js +53 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +1 -1
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +40 -246
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
- package/dist/queue-advanced-question.e2e.test.js +31 -26
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
- package/dist/queue-delete-message.e2e.test.js +1 -1
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +112 -2
- package/dist/queue-question-select-drain.e2e.test.js +30 -128
- package/dist/runtime-lifecycle.e2e.test.js +10 -37
- package/dist/schema.js +34 -0
- package/dist/session-handler/event-stream-state.js +267 -50
- package/dist/session-handler/event-stream-state.test.js +415 -8
- package/dist/session-handler/model-utils.js +159 -5
- package/dist/session-handler/model-utils.test.js +345 -0
- package/dist/session-handler/thread-session-runtime.js +252 -182
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +7 -39
- package/dist/sleep.e2e.test.js +491 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +59 -6
- package/dist/system-message.test.js +76 -6
- package/dist/task-runner.js +94 -4
- package/dist/task-schedule.js +56 -0
- package/dist/task-schedule.test.js +71 -1
- package/dist/thread-message-queue.e2e.test.js +10 -169
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-message.e2e.test.js +10 -10
- package/dist/voice.test.js +55 -29
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +29 -10
- 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/errore/SKILL.md +18 -0
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +37 -0
- 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 +25 -151
- package/src/anthropic-account-identity.ts +1 -0
- package/src/anthropic-auth-plugin.ts +1 -0
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/send.ts +25 -0
- package/src/cli-commands/session.ts +203 -9
- package/src/cli-commands/task.ts +13 -1
- package/src/cli-parsing.test.ts +69 -3
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +28 -4
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/agent.ts +6 -6
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +21 -32
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +43 -12
- package/src/commands/model-variant.ts +8 -1
- package/src/commands/model.ts +18 -8
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/database.ts +195 -3
- package/src/db.test.ts +188 -0
- package/src/db.ts +26 -0
- package/src/discord-bot.ts +35 -29
- package/src/discord-command-registration.ts +15 -1
- package/src/discord-utils.test.ts +19 -0
- package/src/discord-utils.ts +21 -6
- package/src/errors.ts +5 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- 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 +5 -5
- package/src/git-worktree-core.ts +247 -20
- package/src/ipc-tools-plugin.ts +75 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +7 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +13 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +86 -12
- package/src/message-formatting.ts +19 -7
- 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 +81 -0
- package/src/opencode.ts +71 -11
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +1 -1
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +47 -294
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
- package/src/queue-advanced-question.e2e.test.ts +35 -29
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
- package/src/queue-delete-message.e2e.test.ts +1 -1
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +123 -2
- package/src/queue-question-select-drain.e2e.test.ts +32 -157
- package/src/runtime-lifecycle.e2e.test.ts +10 -41
- package/src/schema.sql +14 -1
- package/src/schema.ts +36 -0
- package/src/session-handler/event-stream-state.test.ts +437 -7
- package/src/session-handler/event-stream-state.ts +389 -53
- package/src/session-handler/model-utils.test.ts +415 -0
- package/src/session-handler/model-utils.ts +249 -5
- package/src/session-handler/thread-session-runtime.ts +300 -215
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +9 -48
- package/src/sleep.e2e.test.ts +562 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +21 -0
- package/src/system-message.test.ts +86 -6
- package/src/system-message.ts +66 -7
- package/src/task-runner.ts +136 -2
- package/src/task-schedule.test.ts +76 -0
- package/src/task-schedule.ts +95 -0
- package/src/thread-message-queue.e2e.test.ts +10 -202
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-attachment.ts +29 -1
- package/src/voice-message.e2e.test.ts +10 -10
- package/src/voice.test.ts +63 -29
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +29 -9
- 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
|
@@ -402,7 +402,7 @@ describe('agent model resolution', () => {
|
|
|
402
402
|
--- from: assistant (TestBot)
|
|
403
403
|
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
404
404
|
⬥ ok
|
|
405
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
405
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
406
406
|
`);
|
|
407
407
|
expect(footerMessage).toBeDefined();
|
|
408
408
|
if (!footerMessage) {
|
|
@@ -446,7 +446,7 @@ describe('agent model resolution', () => {
|
|
|
446
446
|
--- from: assistant (TestBot)
|
|
447
447
|
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
448
448
|
⬥ system-context-ok
|
|
449
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
449
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
450
450
|
`);
|
|
451
451
|
}, 15_000);
|
|
452
452
|
test('session.command path includes kimaki system prompt on first message', async () => {
|
|
@@ -552,7 +552,7 @@ describe('agent model resolution', () => {
|
|
|
552
552
|
--- from: assistant (TestBot)
|
|
553
553
|
*using deterministic-provider/channel-model-v2*
|
|
554
554
|
⬥ ok
|
|
555
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
555
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2* <@200000000000000920>"
|
|
556
556
|
`);
|
|
557
557
|
expect(footerMessage).toBeDefined();
|
|
558
558
|
if (!footerMessage) {
|
|
@@ -565,40 +565,30 @@ describe('agent model resolution', () => {
|
|
|
565
565
|
expect(footerMessage.content).toContain(CHANNEL_MODEL);
|
|
566
566
|
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
567
567
|
}, 15_000);
|
|
568
|
-
test('channel model
|
|
569
|
-
// Clear channel agent so model resolution falls through to channel model
|
|
568
|
+
test('unsupported channel model variant falls back to the selected model', async () => {
|
|
570
569
|
const db = await getDb();
|
|
571
570
|
await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
|
|
572
|
-
// Set channel model with a variant (thinking level)
|
|
573
|
-
// The deterministic provider doesn't support thinking, so the variant
|
|
574
|
-
// is resolved but silently dropped (no matching thinking values).
|
|
575
|
-
// This test verifies the variant cascade code path runs without crashing
|
|
576
|
-
// and the correct model still appears in the footer.
|
|
577
571
|
await setChannelModel({
|
|
578
572
|
channelId: TEXT_CHANNEL_ID,
|
|
579
573
|
modelId: `${PROVIDER_NAME}/${CHANNEL_MODEL}`,
|
|
580
574
|
variant: 'high',
|
|
581
575
|
});
|
|
576
|
+
const prompt = 'Reply with exactly: variant-check';
|
|
582
577
|
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
583
|
-
content:
|
|
578
|
+
content: prompt,
|
|
584
579
|
});
|
|
585
580
|
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
586
581
|
timeout: 4_000,
|
|
587
|
-
predicate: (
|
|
588
|
-
return t.name === 'Reply with exactly: variant-check';
|
|
589
|
-
},
|
|
582
|
+
predicate: (candidate) => candidate.name === prompt,
|
|
590
583
|
});
|
|
591
|
-
await
|
|
584
|
+
const messages = await waitForFooterMessage({
|
|
592
585
|
discord,
|
|
593
586
|
threadId: thread.id,
|
|
594
|
-
userId: TEST_USER_ID,
|
|
595
|
-
text: '*project',
|
|
596
587
|
timeout: 4_000,
|
|
597
588
|
});
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
message.content.startsWith('*'));
|
|
589
|
+
const footer = messages.find((message) => {
|
|
590
|
+
return message.author.id === discord.botUserId
|
|
591
|
+
&& message.content.startsWith('*');
|
|
602
592
|
});
|
|
603
593
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
604
594
|
"--- from: user (agent-model-tester)
|
|
@@ -606,18 +596,10 @@ describe('agent model resolution', () => {
|
|
|
606
596
|
--- from: assistant (TestBot)
|
|
607
597
|
*using deterministic-provider/channel-model-v2*
|
|
608
598
|
⬥ ok
|
|
609
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
599
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2* <@200000000000000920>"
|
|
610
600
|
`);
|
|
611
|
-
expect(
|
|
612
|
-
|
|
613
|
-
throw new Error(`Expected footer message but none found. Bot messages: ${messages
|
|
614
|
-
.filter((m) => m.author.id === discord.botUserId)
|
|
615
|
-
.map((m) => m.content.slice(0, 150))
|
|
616
|
-
.join(' | ')}`);
|
|
617
|
-
}
|
|
618
|
-
// Footer should still contain the channel model (variant doesn't crash)
|
|
619
|
-
expect(footerMessage.content).toContain(CHANNEL_MODEL);
|
|
620
|
-
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
601
|
+
expect(footer?.content).toContain(CHANNEL_MODEL);
|
|
602
|
+
expect(footer?.content).not.toContain(DEFAULT_MODEL);
|
|
621
603
|
}, 15_000);
|
|
622
604
|
test('/btw fork keeps source session model when channel model differs', async () => {
|
|
623
605
|
const db = await getDb();
|
|
@@ -681,7 +663,7 @@ describe('agent model resolution', () => {
|
|
|
681
663
|
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
682
664
|
Reply with exactly: btw-model-check
|
|
683
665
|
⬥ ok
|
|
684
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2*"
|
|
666
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2* <@200000000000000920>"
|
|
685
667
|
`);
|
|
686
668
|
expect(forkedSessionModel).toMatchInlineSnapshot(`
|
|
687
669
|
{
|
|
@@ -746,12 +728,12 @@ describe('agent model resolution', () => {
|
|
|
746
728
|
--- from: assistant (TestBot)
|
|
747
729
|
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
748
730
|
⬥ ok
|
|
749
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
731
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
750
732
|
--- from: user (agent-model-tester)
|
|
751
733
|
Reply with exactly: second-thread-msg
|
|
752
734
|
--- from: assistant (TestBot)
|
|
753
735
|
⬥ ok
|
|
754
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
736
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>"
|
|
755
737
|
`);
|
|
756
738
|
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
757
739
|
const secondFooter = [...secondMessages]
|
|
@@ -765,85 +747,6 @@ describe('agent model resolution', () => {
|
|
|
765
747
|
expect(secondFooter.content).toContain(AGENT_MODEL);
|
|
766
748
|
expect(secondFooter.content).not.toContain(PLAN_AGENT_MODEL);
|
|
767
749
|
}, 20_000);
|
|
768
|
-
test('thread created with no agent keeps default model after channel agent is set', async () => {
|
|
769
|
-
// Clear any channel agent — thread starts with default (no agent)
|
|
770
|
-
const db = await getDb();
|
|
771
|
-
await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
|
|
772
|
-
// Also clear channel model so we get the pure default
|
|
773
|
-
await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
|
|
774
|
-
// 1. Send a message to create a thread (no channel agent set)
|
|
775
|
-
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
776
|
-
content: 'Reply with exactly: default-thread-msg',
|
|
777
|
-
});
|
|
778
|
-
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
779
|
-
timeout: 4_000,
|
|
780
|
-
predicate: (t) => {
|
|
781
|
-
return t.name === 'Reply with exactly: default-thread-msg';
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
// Wait for footer — should show the default model
|
|
785
|
-
await waitForFooterMessage({
|
|
786
|
-
discord,
|
|
787
|
-
threadId: thread.id,
|
|
788
|
-
timeout: 4_000,
|
|
789
|
-
afterMessageIncludes: 'ok',
|
|
790
|
-
afterAuthorId: discord.botUserId,
|
|
791
|
-
});
|
|
792
|
-
const firstMessages = await discord.thread(thread.id).getMessages();
|
|
793
|
-
const firstFooter = firstMessages.find((m) => {
|
|
794
|
-
return (m.author.id === discord.botUserId && m.content.startsWith('*'));
|
|
795
|
-
});
|
|
796
|
-
expect(firstFooter).toBeDefined();
|
|
797
|
-
// First run uses the default model (no agent set)
|
|
798
|
-
expect(firstFooter.content).toContain(DEFAULT_MODEL);
|
|
799
|
-
expect(firstFooter.content).not.toContain(AGENT_MODEL);
|
|
800
|
-
// 2. Set channel agent to test-agent via /test-agent-agent in the CHANNEL
|
|
801
|
-
const { id: interactionId } = await discord
|
|
802
|
-
.channel(TEXT_CHANNEL_ID)
|
|
803
|
-
.user(TEST_USER_ID)
|
|
804
|
-
.runSlashCommand({ name: 'test-agent-agent' });
|
|
805
|
-
await discord
|
|
806
|
-
.channel(TEXT_CHANNEL_ID)
|
|
807
|
-
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
808
|
-
// 3. Send a second message in the EXISTING thread
|
|
809
|
-
await discord
|
|
810
|
-
.thread(thread.id)
|
|
811
|
-
.user(TEST_USER_ID)
|
|
812
|
-
.sendMessage({
|
|
813
|
-
content: 'Reply with exactly: default-second-msg',
|
|
814
|
-
});
|
|
815
|
-
await waitForFooterMessage({
|
|
816
|
-
discord,
|
|
817
|
-
threadId: thread.id,
|
|
818
|
-
timeout: 4_000,
|
|
819
|
-
afterMessageIncludes: 'default-second-msg',
|
|
820
|
-
afterAuthorId: TEST_USER_ID,
|
|
821
|
-
});
|
|
822
|
-
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
823
|
-
"--- from: user (agent-model-tester)
|
|
824
|
-
Reply with exactly: default-thread-msg
|
|
825
|
-
--- from: assistant (TestBot)
|
|
826
|
-
*using deterministic-provider/deterministic-v2*
|
|
827
|
-
⬥ ok
|
|
828
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
829
|
-
--- from: user (agent-model-tester)
|
|
830
|
-
Reply with exactly: default-second-msg
|
|
831
|
-
--- from: assistant (TestBot)
|
|
832
|
-
⬥ ok
|
|
833
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
834
|
-
`);
|
|
835
|
-
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
836
|
-
const secondFooter = [...secondMessages]
|
|
837
|
-
.reverse()
|
|
838
|
-
.find((m) => {
|
|
839
|
-
return (m.author.id === discord.botUserId && m.content.startsWith('*'));
|
|
840
|
-
});
|
|
841
|
-
expect(secondFooter).toBeDefined();
|
|
842
|
-
// The existing thread should still use the DEFAULT model,
|
|
843
|
-
// NOT the test-agent's model (AGENT_MODEL)
|
|
844
|
-
expect(secondFooter.content).toContain(DEFAULT_MODEL);
|
|
845
|
-
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
846
|
-
}, 20_000);
|
|
847
750
|
test('/plan-agent with prompt starts a session with the plan agent', async () => {
|
|
848
751
|
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
849
752
|
const prompt = 'Reply with exactly: inline-plan-agent-msg';
|
|
@@ -879,7 +782,7 @@ describe('agent model resolution', () => {
|
|
|
879
782
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
880
783
|
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
881
784
|
⬥ ok
|
|
882
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
785
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
883
786
|
`);
|
|
884
787
|
}, 20_000);
|
|
885
788
|
test('/plan-agent with prompt in an existing thread changes the session agent', async () => {
|
|
@@ -924,10 +827,10 @@ describe('agent model resolution', () => {
|
|
|
924
827
|
--- from: assistant (TestBot)
|
|
925
828
|
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
926
829
|
⬥ ok
|
|
927
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
830
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
928
831
|
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
929
832
|
⬥ ok
|
|
930
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
833
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
931
834
|
`);
|
|
932
835
|
}, 20_000);
|
|
933
836
|
test('/plan-agent inside a thread switches the model for that thread', async () => {
|
|
@@ -978,7 +881,7 @@ describe('agent model resolution', () => {
|
|
|
978
881
|
--- from: assistant (TestBot)
|
|
979
882
|
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
980
883
|
⬥ ok
|
|
981
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
884
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
982
885
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
983
886
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
984
887
|
The agent will change on the next message.
|
|
@@ -986,7 +889,7 @@ describe('agent model resolution', () => {
|
|
|
986
889
|
Reply with exactly: after-switch-msg
|
|
987
890
|
--- from: assistant (TestBot)
|
|
988
891
|
⬥ ok
|
|
989
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
892
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>"
|
|
990
893
|
`);
|
|
991
894
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
992
895
|
.reverse()
|
|
@@ -1035,7 +938,7 @@ describe('agent model resolution', () => {
|
|
|
1035
938
|
--- from: assistant (TestBot)
|
|
1036
939
|
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1037
940
|
⬥ ok
|
|
1038
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***
|
|
941
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan*** <@200000000000000920>
|
|
1039
942
|
Using **plan** agent for this session
|
|
1040
943
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1041
944
|
The agent will change on the next message."
|
|
@@ -1077,7 +980,7 @@ describe('agent model resolution', () => {
|
|
|
1077
980
|
--- from: assistant (TestBot)
|
|
1078
981
|
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1079
982
|
⬥ ok
|
|
1080
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
983
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent*** <@200000000000000920>
|
|
1081
984
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1082
985
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1083
986
|
The agent will change on the next message."
|
|
@@ -1119,7 +1022,7 @@ describe('agent model resolution', () => {
|
|
|
1119
1022
|
--- from: assistant (TestBot)
|
|
1120
1023
|
*using deterministic-provider/channel-model-v2*
|
|
1121
1024
|
⬥ ok
|
|
1122
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*
|
|
1025
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2* <@200000000000000920>
|
|
1123
1026
|
Switched to **plain** agent for this session
|
|
1124
1027
|
Model: *deterministic-provider/channel-model-v2* (channel override)
|
|
1125
1028
|
This model comes from a channel override. Use /model and press Clear override if you want this agent's model.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
|
|
1
2
|
// Helpers for extracting and normalizing Anthropic OAuth account identity.
|
|
2
3
|
const identityHintKeys = new Set(['user', 'profile', 'account', 'viewer']);
|
|
3
4
|
const idKeys = ['user_id', 'userId', 'account_id', 'accountId', 'id', 'sub'];
|
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) {
|
|
@@ -29,6 +29,7 @@ import { execAsync, resolveSessionWorkingDirectory, isGitRepositoryRoot } from '
|
|
|
29
29
|
import { upgrade, getCurrentVersion } from '../upgrade.js';
|
|
30
30
|
import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
|
|
31
31
|
import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
|
|
32
|
+
import { validateCliModelOption } from '../session-handler/model-utils.js';
|
|
32
33
|
const cliLogger = createLogger(LogPrefix.CLI);
|
|
33
34
|
const cli = goke();
|
|
34
35
|
cli
|
|
@@ -89,6 +90,13 @@ cli
|
|
|
89
90
|
cliLogger.error('Prompt is required. Use --prompt <prompt>');
|
|
90
91
|
process.exit(EXIT_NO_RESTART);
|
|
91
92
|
}
|
|
93
|
+
const earlyModelCheck = await validateCliModelOption({
|
|
94
|
+
model: options.model,
|
|
95
|
+
});
|
|
96
|
+
if (earlyModelCheck instanceof Error) {
|
|
97
|
+
cliLogger.error(earlyModelCheck.message);
|
|
98
|
+
process.exit(EXIT_NO_RESTART);
|
|
99
|
+
}
|
|
92
100
|
const filePaths = options.file?.length
|
|
93
101
|
? options.file.map((f) => path.resolve(f))
|
|
94
102
|
: undefined;
|
|
@@ -335,6 +343,14 @@ cli
|
|
|
335
343
|
// We only require it for features that genuinely need a local directory
|
|
336
344
|
// (scheduled tasks and --wait).
|
|
337
345
|
const channelConfig = await getChannelDirectory(threadData.parent_id);
|
|
346
|
+
const threadModelCheck = await validateCliModelOption({
|
|
347
|
+
model: options.model,
|
|
348
|
+
directory: channelConfig?.directory,
|
|
349
|
+
});
|
|
350
|
+
if (threadModelCheck instanceof Error) {
|
|
351
|
+
cliLogger.error(threadModelCheck.message);
|
|
352
|
+
process.exit(EXIT_NO_RESTART);
|
|
353
|
+
}
|
|
338
354
|
// Guard early: fail before sending the message if a feature that
|
|
339
355
|
// needs local project directory mapping is requested.
|
|
340
356
|
if (!channelConfig && (parsedSchedule || options.wait)) {
|
|
@@ -446,6 +462,14 @@ cli
|
|
|
446
462
|
// (--send-at, --wait, --cwd).
|
|
447
463
|
const channelConfig = await getChannelDirectory(channelData.id);
|
|
448
464
|
const projectDirectory = channelConfig?.directory;
|
|
465
|
+
const channelModelCheck = await validateCliModelOption({
|
|
466
|
+
model: options.model,
|
|
467
|
+
directory: projectDirectory,
|
|
468
|
+
});
|
|
469
|
+
if (channelModelCheck instanceof Error) {
|
|
470
|
+
cliLogger.error(channelModelCheck.message);
|
|
471
|
+
process.exit(EXIT_NO_RESTART);
|
|
472
|
+
}
|
|
449
473
|
// Features that require a local project directory mapping
|
|
450
474
|
const needsProjectDirectory = Boolean(parsedSchedule || options.wait || options.cwd);
|
|
451
475
|
if (!channelConfig && needsProjectDirectory) {
|