kimaki 0.25.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 +157 -122
- package/dist/analytics.test.js +30 -1
- 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 +37 -3
- package/dist/cli-commands/session.js +183 -8
- package/dist/cli-commands/task.js +39 -8
- package/dist/cli-parsing.test.js +59 -3
- package/dist/cli-runner.js +94 -74
- package/dist/cli-runner.test.js +37 -1
- 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 +93 -82
- 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 +32 -8
- package/dist/commands/multioauth.js +170 -1
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/tasks.js +6 -0
- package/dist/commands/unset-model.js +5 -20
- 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 +189 -3
- package/dist/db.js +22 -0
- package/dist/db.test.js +167 -1
- package/dist/discord-bot.js +41 -33
- 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 +67 -6
- 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 +10 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +95 -5
- package/dist/message-formatting.test.js +207 -2
- package/dist/oauth-rotation-shared.js +17 -1
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +21 -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 +53 -0
- package/dist/session-handler/event-stream-state.js +364 -0
- package/dist/session-handler/event-stream-state.test.js +844 -1
- 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 +288 -165
- 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 +76 -9
- package/dist/system-message.test.js +103 -9
- package/dist/task-runner.js +235 -14
- package/dist/task-schedule.js +68 -0
- package/dist/task-schedule.test.js +219 -2
- 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 +238 -0
- package/dist/xai-auth-state.js +234 -0
- package/dist/xai-auth-state.test.js +183 -0
- package/package.json +9 -7
- 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 +20 -3
- package/skills/spiceflow/SKILL.md +63 -4
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +194 -152
- package/src/analytics.test.ts +30 -1
- package/src/analytics.ts +1 -0
- 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 +47 -3
- package/src/cli-commands/session.ts +203 -9
- package/src/cli-commands/task.ts +45 -20
- package/src/cli-parsing.test.ts +69 -3
- package/src/cli-runner.test.ts +65 -1
- package/src/cli-runner.ts +116 -93
- 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 +126 -90
- 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 +43 -8
- package/src/commands/multioauth.ts +196 -2
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/tasks.ts +6 -0
- package/src/commands/unset-model.ts +5 -21
- 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 +319 -3
- package/src/db.test.ts +188 -0
- package/src/db.ts +26 -0
- package/src/discord-bot.ts +42 -30
- 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 +79 -8
- 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 +13 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +271 -2
- package/src/message-formatting.ts +110 -5
- package/src/oauth-rotation-shared.ts +16 -1
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +20 -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 +29 -1
- package/src/schema.ts +56 -0
- package/src/session-handler/event-stream-state.test.ts +920 -0
- package/src/session-handler/event-stream-state.ts +546 -0
- package/src/session-handler/model-utils.test.ts +415 -0
- package/src/session-handler/model-utils.ts +250 -5
- package/src/session-handler/thread-session-runtime.ts +346 -200
- 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 +114 -9
- package/src/system-message.ts +85 -10
- package/src/task-runner.ts +304 -13
- package/src/task-schedule.test.ts +248 -2
- package/src/task-schedule.ts +118 -2
- 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 +303 -0
- package/src/xai-auth-state.test.ts +224 -0
- package/src/xai-auth-state.ts +294 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -173
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -218
|
@@ -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
|
|
@@ -52,6 +53,8 @@ cli
|
|
|
52
53
|
.option('-f, --file <path>', z.array(z.string()).describe('Local file to attach (repeatable). Images, text files, PDFs, etc. ' +
|
|
53
54
|
'Examples: --file screenshot.png --file report.pdf'))
|
|
54
55
|
.option('--send-at <schedule>', 'Schedule send for future (UTC ISO date/time ending in Z, or cron expression)')
|
|
56
|
+
.option('--pre-run <command>', 'Run a shell command in the project before starting a scheduled task')
|
|
57
|
+
.option('--allow-concurrency', 'Allow concurrent sessions from the same scheduled task')
|
|
55
58
|
.option('--thread <threadId>', 'Post prompt to an existing thread')
|
|
56
59
|
.option('--session <sessionId>', 'Post prompt to thread mapped to an existing session')
|
|
57
60
|
.option('--parent-session <sessionId>', 'Parent OpenCode session ID for newly created child sessions')
|
|
@@ -67,6 +70,10 @@ cli
|
|
|
67
70
|
const { project: projectPath } = options;
|
|
68
71
|
const sendAt = options.sendAt;
|
|
69
72
|
const existingThreadMode = Boolean(threadId || sessionId);
|
|
73
|
+
if ((options.preRun || options.allowConcurrency) && !sendAt) {
|
|
74
|
+
cliLogger.error('--pre-run and --allow-concurrency require --send-at');
|
|
75
|
+
process.exit(EXIT_NO_RESTART);
|
|
76
|
+
}
|
|
70
77
|
if (threadId && sessionId) {
|
|
71
78
|
cliLogger.error('Use either --thread or --session, not both');
|
|
72
79
|
process.exit(EXIT_NO_RESTART);
|
|
@@ -83,6 +90,13 @@ cli
|
|
|
83
90
|
cliLogger.error('Prompt is required. Use --prompt <prompt>');
|
|
84
91
|
process.exit(EXIT_NO_RESTART);
|
|
85
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
|
+
}
|
|
86
100
|
const filePaths = options.file?.length
|
|
87
101
|
? options.file.map((f) => path.resolve(f))
|
|
88
102
|
: undefined;
|
|
@@ -329,6 +343,14 @@ cli
|
|
|
329
343
|
// We only require it for features that genuinely need a local directory
|
|
330
344
|
// (scheduled tasks and --wait).
|
|
331
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
|
+
}
|
|
332
354
|
// Guard early: fail before sending the message if a feature that
|
|
333
355
|
// needs local project directory mapping is requested.
|
|
334
356
|
if (!channelConfig && (parsedSchedule || options.wait)) {
|
|
@@ -348,6 +370,8 @@ cli
|
|
|
348
370
|
permissions: options.permission?.length ? options.permission : null,
|
|
349
371
|
injectionGuardPatterns: options.injectionGuard?.length ? options.injectionGuard : null,
|
|
350
372
|
parentSessionId: options.parentSession || null,
|
|
373
|
+
preRunCommand: options.preRun || null,
|
|
374
|
+
allowConcurrency: Boolean(options.allowConcurrency),
|
|
351
375
|
};
|
|
352
376
|
const taskId = await createScheduledTask({
|
|
353
377
|
scheduleKind: parsedSchedule.scheduleKind,
|
|
@@ -387,7 +411,7 @@ cli
|
|
|
387
411
|
// detection can find the command on its own line.
|
|
388
412
|
const prefixedPrompt = `» **kimaki-cli:**\n${prompt}`;
|
|
389
413
|
if (threadTargetUser) {
|
|
390
|
-
cliLogger.log(`Adding user ${threadTargetUser.username} to thread...`);
|
|
414
|
+
cliLogger.log(`Adding user ${threadTargetUser.username || threadTargetUser.id} to thread...`);
|
|
391
415
|
const addMemberResult = await ensureThreadMember({
|
|
392
416
|
rest,
|
|
393
417
|
threadId: targetThreadId,
|
|
@@ -438,6 +462,14 @@ cli
|
|
|
438
462
|
// (--send-at, --wait, --cwd).
|
|
439
463
|
const channelConfig = await getChannelDirectory(channelData.id);
|
|
440
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
|
+
}
|
|
441
473
|
// Features that require a local project directory mapping
|
|
442
474
|
const needsProjectDirectory = Boolean(parsedSchedule || options.wait || options.cwd);
|
|
443
475
|
if (!channelConfig && needsProjectDirectory) {
|
|
@@ -509,6 +541,8 @@ cli
|
|
|
509
541
|
permissions: options.permission?.length ? options.permission : null,
|
|
510
542
|
injectionGuardPatterns: options.injectionGuard?.length ? options.injectionGuard : null,
|
|
511
543
|
parentSessionId: options.parentSession || null,
|
|
544
|
+
preRunCommand: options.preRun || null,
|
|
545
|
+
allowConcurrency: Boolean(options.allowConcurrency),
|
|
512
546
|
};
|
|
513
547
|
const taskId = await createScheduledTask({
|
|
514
548
|
scheduleKind: parsedSchedule.scheduleKind,
|
|
@@ -535,8 +569,8 @@ cli
|
|
|
535
569
|
...(worktreeName && { worktree: worktreeName }),
|
|
536
570
|
...(resolvedCwd && { cwd: resolvedCwd }),
|
|
537
571
|
...(resolvedUser && {
|
|
538
|
-
username: resolvedUser.username,
|
|
539
572
|
userId: resolvedUser.id,
|
|
573
|
+
...(resolvedUser.username && { username: resolvedUser.username }),
|
|
540
574
|
}),
|
|
541
575
|
...(options.agent && { agent: options.agent }),
|
|
542
576
|
...(options.model && { model: options.model }),
|
|
@@ -574,7 +608,7 @@ cli
|
|
|
574
608
|
cliLogger.log('Thread created!');
|
|
575
609
|
// Add user to thread if specified
|
|
576
610
|
if (resolvedUser) {
|
|
577
|
-
cliLogger.log(`Adding user ${resolvedUser.username} to thread...`);
|
|
611
|
+
cliLogger.log(`Adding user ${resolvedUser.username || resolvedUser.id} to thread...`);
|
|
578
612
|
await rest.put(Routes.threadMembers(threadData.id, resolvedUser.id));
|
|
579
613
|
}
|
|
580
614
|
const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Session inspection and archival terminal commands.
|
|
2
2
|
import { goke } from 'goke';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import dedent from 'string-dedent';
|
|
4
5
|
import { note } from '@clack/prompts';
|
|
5
6
|
import YAML from 'yaml';
|
|
6
7
|
import * as errore from 'errore';
|
|
@@ -15,10 +16,13 @@ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessi
|
|
|
15
16
|
import { ShareMarkdown } from '../markdown.js';
|
|
16
17
|
import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
|
|
17
18
|
import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
|
|
19
|
+
import { formatTimeAgo } from '../commands/worktrees.js';
|
|
20
|
+
import { editorsForFile, loadFileEditEvents } from '../file-edit-log.js';
|
|
18
21
|
import { WORKTREE_PREFIX } from '../commands/merge-worktree.js';
|
|
19
22
|
import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js';
|
|
20
23
|
import { createDiscordRest } from '../discord-urls.js';
|
|
21
24
|
import { archiveThread, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
|
|
25
|
+
import { OpenCodeSdkError } from '../errors.js';
|
|
22
26
|
import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js';
|
|
23
27
|
import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
|
|
24
28
|
import { upgrade, getCurrentVersion } from '../upgrade.js';
|
|
@@ -53,6 +57,8 @@ async function resolveSessionDirectoryFromDatabase({ sessionId, }) {
|
|
|
53
57
|
cli
|
|
54
58
|
.command('session list', 'List all OpenCode sessions, marking which were started via Kimaki')
|
|
55
59
|
.option('--project <path>', 'Project directory to list sessions for (defaults to cwd)')
|
|
60
|
+
.option('--active', 'Only list active sessions; exits 1 when none remain')
|
|
61
|
+
.option('--exclude <sessionId>', 'Exclude one session ID from the results')
|
|
56
62
|
.option('--json', 'Output as JSON')
|
|
57
63
|
.action(async (options) => {
|
|
58
64
|
try {
|
|
@@ -66,9 +72,32 @@ cli
|
|
|
66
72
|
}
|
|
67
73
|
const sessionsResponse = await getClient().session.list();
|
|
68
74
|
const sessions = sessionsResponse.data || [];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
const statuses = await (async () => {
|
|
76
|
+
if (!options.active)
|
|
77
|
+
return null;
|
|
78
|
+
const response = await getClient().session.status({
|
|
79
|
+
directory: projectDirectory,
|
|
80
|
+
});
|
|
81
|
+
if (response.error) {
|
|
82
|
+
cliLogger.error('Failed to list active sessions');
|
|
83
|
+
process.exit(EXIT_NO_RESTART);
|
|
84
|
+
}
|
|
85
|
+
return response.data || {};
|
|
86
|
+
})();
|
|
87
|
+
const selectedSessions = sessions.filter((session) => {
|
|
88
|
+
if (session.id === options.exclude)
|
|
89
|
+
return false;
|
|
90
|
+
if (!options.active)
|
|
91
|
+
return true;
|
|
92
|
+
const status = statuses?.[session.id];
|
|
93
|
+
return Boolean(status && status.type !== 'idle');
|
|
94
|
+
});
|
|
95
|
+
if (selectedSessions.length === 0) {
|
|
96
|
+
if (options.json)
|
|
97
|
+
console.log('[]');
|
|
98
|
+
else
|
|
99
|
+
cliLogger.log(options.active ? 'No active sessions found' : 'No sessions found');
|
|
100
|
+
process.exit(options.active ? 1 : 0);
|
|
72
101
|
}
|
|
73
102
|
// Look up which sessions were started via kimaki (have a thread mapping)
|
|
74
103
|
const db = await getDb();
|
|
@@ -78,7 +107,7 @@ cli
|
|
|
78
107
|
const sessionToThread = new Map(threadSessions
|
|
79
108
|
.filter((row) => row.session_id !== '')
|
|
80
109
|
.map((row) => [row.session_id, row.thread_id]));
|
|
81
|
-
const sessionStartSources = await getSessionStartSourcesBySessionIds(
|
|
110
|
+
const sessionStartSources = await getSessionStartSourcesBySessionIds(selectedSessions.map((session) => session.id));
|
|
82
111
|
const scheduleModeLabel = ({ scheduleKind, }) => {
|
|
83
112
|
if (scheduleKind === 'at') {
|
|
84
113
|
return 'delay';
|
|
@@ -86,7 +115,7 @@ cli
|
|
|
86
115
|
return 'cron';
|
|
87
116
|
};
|
|
88
117
|
if (options.json) {
|
|
89
|
-
const output =
|
|
118
|
+
const output = selectedSessions.map((session) => {
|
|
90
119
|
const startSource = sessionStartSources.get(session.id);
|
|
91
120
|
const startedBy = startSource
|
|
92
121
|
? `scheduled-${scheduleModeLabel({ scheduleKind: startSource.schedule_kind })}`
|
|
@@ -100,12 +129,13 @@ cli
|
|
|
100
129
|
threadId: sessionToThread.get(session.id) || null,
|
|
101
130
|
startedBy,
|
|
102
131
|
scheduledTaskId: startSource?.scheduled_task_id || null,
|
|
132
|
+
status: options.active ? statuses?.[session.id]?.type || 'busy' : undefined,
|
|
103
133
|
};
|
|
104
134
|
});
|
|
105
135
|
console.log(JSON.stringify(output, null, 2));
|
|
106
136
|
process.exit(0);
|
|
107
137
|
}
|
|
108
|
-
for (const session of
|
|
138
|
+
for (const session of selectedSessions) {
|
|
109
139
|
const threadId = sessionToThread.get(session.id);
|
|
110
140
|
const startSource = sessionStartSources.get(session.id);
|
|
111
141
|
const source = threadId ? '(kimaki)' : '(opencode)';
|
|
@@ -114,7 +144,10 @@ cli
|
|
|
114
144
|
: '';
|
|
115
145
|
const updatedAt = new Date(session.time.updated).toISOString();
|
|
116
146
|
const threadInfo = threadId ? ` | thread: ${threadId}` : '';
|
|
117
|
-
|
|
147
|
+
const statusInfo = options.active
|
|
148
|
+
? ` | status: ${statuses?.[session.id]?.type || 'busy'}`
|
|
149
|
+
: '';
|
|
150
|
+
console.log(`${session.id} | ${session.title || 'Untitled Session'} | ${session.directory} | ${updatedAt} | ${source}${threadInfo}${startedBy}${statusInfo}`);
|
|
118
151
|
}
|
|
119
152
|
process.exit(0);
|
|
120
153
|
}
|
|
@@ -123,6 +156,79 @@ cli
|
|
|
123
156
|
process.exit(EXIT_NO_RESTART);
|
|
124
157
|
}
|
|
125
158
|
});
|
|
159
|
+
cli
|
|
160
|
+
.command('session editors <file>', dedent `
|
|
161
|
+
List sessions that last edited a file, newest first.
|
|
162
|
+
|
|
163
|
+
Use this before a commit in another session so the \`Session:\` line
|
|
164
|
+
uses the session that actually edited the file.
|
|
165
|
+
`)
|
|
166
|
+
.option('--json', 'Output as JSON')
|
|
167
|
+
.option('--limit <n>', z.number().default(20).describe('Max sessions to show'))
|
|
168
|
+
.example('kimaki session editors src/cli.ts')
|
|
169
|
+
.example('kimaki session editors src/cli.ts --json')
|
|
170
|
+
.action(async (file, options, { console, process }) => {
|
|
171
|
+
try {
|
|
172
|
+
const cwd = process.cwd;
|
|
173
|
+
const loaded = loadFileEditEvents({ dataDir: getDataDir() });
|
|
174
|
+
if (loaded instanceof Error) {
|
|
175
|
+
console.error(loaded.message);
|
|
176
|
+
process.exit(EXIT_NO_RESTART);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const editors = editorsForFile({
|
|
180
|
+
events: loaded,
|
|
181
|
+
filePath: file,
|
|
182
|
+
cwd,
|
|
183
|
+
}).slice(0, options.limit);
|
|
184
|
+
if (editors.length === 0) {
|
|
185
|
+
console.error(`No recorded editors for ${path.resolve(cwd, file)}`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const titles = new Map();
|
|
190
|
+
try {
|
|
191
|
+
await initDatabase();
|
|
192
|
+
const db = await getDb();
|
|
193
|
+
const sessionRows = await db.query.thread_sessions.findMany({
|
|
194
|
+
columns: { session_id: true, last_synced_name: true },
|
|
195
|
+
where: { session_id: { in: editors.map((editor) => editor.sessionId) } },
|
|
196
|
+
orderBy: { updated_at: 'desc' },
|
|
197
|
+
});
|
|
198
|
+
for (const row of sessionRows) {
|
|
199
|
+
if (!titles.has(row.session_id) && row.last_synced_name) {
|
|
200
|
+
titles.set(row.session_id, row.last_synced_name);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
206
|
+
}
|
|
207
|
+
const rows = editors.map((editor) => {
|
|
208
|
+
const title = titles.get(editor.sessionId) || '-';
|
|
209
|
+
const editedAt = new Date(editor.at);
|
|
210
|
+
return {
|
|
211
|
+
sessionId: editor.sessionId,
|
|
212
|
+
title,
|
|
213
|
+
editedAt: editedAt.toISOString(),
|
|
214
|
+
ago: formatTimeAgo(editedAt),
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
if (options.json) {
|
|
218
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
219
|
+
process.exit(0);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
console.log(`${row.sessionId} | ${row.title} | ${row.ago}`);
|
|
224
|
+
}
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error(error instanceof Error ? error.stack : String(error));
|
|
229
|
+
process.exit(EXIT_NO_RESTART);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
126
232
|
cli
|
|
127
233
|
.command('session read <sessionId>', 'Read a session conversation as markdown (pipe to file to grep)')
|
|
128
234
|
.option('--project <path>', 'Project directory (defaults to cwd)')
|
|
@@ -430,7 +536,7 @@ cli
|
|
|
430
536
|
process.exit(0);
|
|
431
537
|
});
|
|
432
538
|
cli
|
|
433
|
-
.command('session archive [threadId]', 'Archive a Discord thread
|
|
539
|
+
.command('session archive [threadId]', 'Archive a Discord thread without stopping its mapped OpenCode session')
|
|
434
540
|
.option('--session <sessionId>', 'Resolve thread from an OpenCode session ID')
|
|
435
541
|
.action(async (threadIdArg, options) => {
|
|
436
542
|
try {
|
|
@@ -544,6 +650,75 @@ cli
|
|
|
544
650
|
process.exit(EXIT_NO_RESTART);
|
|
545
651
|
}
|
|
546
652
|
});
|
|
653
|
+
cli
|
|
654
|
+
.command('session title <title>', 'Update the OpenCode session title. Discord thread name follows automatically.')
|
|
655
|
+
.option('--session <sessionId>', 'OpenCode session ID')
|
|
656
|
+
.option('--thread <threadId>', 'Discord thread ID')
|
|
657
|
+
.example("kimaki session title 'Fix queue draining' --session ses_xxx")
|
|
658
|
+
.action(async (title, options) => {
|
|
659
|
+
try {
|
|
660
|
+
await initDatabase();
|
|
661
|
+
const trimmedTitle = title.trim();
|
|
662
|
+
if (!trimmedTitle) {
|
|
663
|
+
cliLogger.error('Title must not be empty');
|
|
664
|
+
process.exit(EXIT_NO_RESTART);
|
|
665
|
+
}
|
|
666
|
+
if (options.session && options.thread) {
|
|
667
|
+
cliLogger.error('Use either --session or --thread, not both');
|
|
668
|
+
process.exit(EXIT_NO_RESTART);
|
|
669
|
+
}
|
|
670
|
+
if (!options.session && !options.thread) {
|
|
671
|
+
cliLogger.error('Provide --session <sessionId> or --thread <threadId>');
|
|
672
|
+
process.exit(EXIT_NO_RESTART);
|
|
673
|
+
}
|
|
674
|
+
const sessionId = await (async () => {
|
|
675
|
+
if (options.session)
|
|
676
|
+
return options.session;
|
|
677
|
+
const threadId = options.thread;
|
|
678
|
+
if (!threadId)
|
|
679
|
+
return null;
|
|
680
|
+
return getThreadSession(threadId);
|
|
681
|
+
})();
|
|
682
|
+
if (!sessionId) {
|
|
683
|
+
cliLogger.error(options.thread
|
|
684
|
+
? `No OpenCode session found for thread: ${options.thread}`
|
|
685
|
+
: 'Provide --session <sessionId> or --thread <threadId>');
|
|
686
|
+
process.exit(EXIT_NO_RESTART);
|
|
687
|
+
}
|
|
688
|
+
const directory = await resolveSessionDirectoryFromDatabase({
|
|
689
|
+
sessionId,
|
|
690
|
+
});
|
|
691
|
+
if (directory instanceof Error) {
|
|
692
|
+
cliLogger.error(directory.message);
|
|
693
|
+
process.exit(EXIT_NO_RESTART);
|
|
694
|
+
}
|
|
695
|
+
const serverResult = await initializeOpencodeForDirectory(directory);
|
|
696
|
+
if (serverResult instanceof Error) {
|
|
697
|
+
cliLogger.error(`Failed to initialize OpenCode: ${serverResult.message}`);
|
|
698
|
+
process.exit(EXIT_NO_RESTART);
|
|
699
|
+
}
|
|
700
|
+
const updateResult = await serverResult()
|
|
701
|
+
.session.update({
|
|
702
|
+
sessionID: sessionId,
|
|
703
|
+
title: trimmedTitle,
|
|
704
|
+
})
|
|
705
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'session.update', cause: e }));
|
|
706
|
+
if (updateResult instanceof Error) {
|
|
707
|
+
cliLogger.error(updateResult.message);
|
|
708
|
+
process.exit(EXIT_NO_RESTART);
|
|
709
|
+
}
|
|
710
|
+
if (updateResult.error) {
|
|
711
|
+
cliLogger.error('OpenCode rejected the session title update');
|
|
712
|
+
process.exit(EXIT_NO_RESTART);
|
|
713
|
+
}
|
|
714
|
+
note(`Updated OpenCode title: ${trimmedTitle}`, 'Title updated');
|
|
715
|
+
process.exit(0);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
719
|
+
process.exit(EXIT_NO_RESTART);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
547
722
|
cli
|
|
548
723
|
.command('session discord-url <sessionId>', 'Print the Discord thread URL for a session')
|
|
549
724
|
.option('--json', 'Output as JSON')
|
|
@@ -24,6 +24,7 @@ import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
|
|
|
24
24
|
import { upgrade, getCurrentVersion } from '../upgrade.js';
|
|
25
25
|
import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
|
|
26
26
|
import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, getDiscordUserIdFromUserOption, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
|
|
27
|
+
import { validateCliModelOption } from '../session-handler/model-utils.js';
|
|
27
28
|
const cliLogger = createLogger(LogPrefix.CLI);
|
|
28
29
|
const cli = goke();
|
|
29
30
|
cli
|
|
@@ -40,7 +41,7 @@ cli
|
|
|
40
41
|
cliLogger.log('No scheduled tasks found');
|
|
41
42
|
process.exit(0);
|
|
42
43
|
}
|
|
43
|
-
console.log('id | status | message | channelId | userId | projectName | folderName | agent | model | timeRemaining | firesAt | cron');
|
|
44
|
+
console.log('id | status | message | channelId | userId | projectName | folderName | agent | model | preRun | allowConcurrency | timeRemaining | firesAt | cron');
|
|
44
45
|
tasks.forEach((task) => {
|
|
45
46
|
// Surfacing userId makes it obvious which tasks will never show up in
|
|
46
47
|
// the user's Discord sidebar (no thread member is ever added for them).
|
|
@@ -49,6 +50,10 @@ cli
|
|
|
49
50
|
const userId = payload instanceof Error ? '?' : payload.userId || '-';
|
|
50
51
|
const agent = payload instanceof Error ? '?' : payload.agent || '-';
|
|
51
52
|
const model = payload instanceof Error ? '?' : payload.model || '-';
|
|
53
|
+
const preRun = payload instanceof Error ? '?' : payload.preRunCommand || '-';
|
|
54
|
+
const allowConcurrency = payload instanceof Error
|
|
55
|
+
? '?'
|
|
56
|
+
: String(payload.allowConcurrency);
|
|
52
57
|
const projectDirectory = task.project_directory || '';
|
|
53
58
|
const projectName = projectDirectory
|
|
54
59
|
? path.basename(projectDirectory)
|
|
@@ -60,7 +65,7 @@ cli
|
|
|
60
65
|
? task.run_at.toISOString()
|
|
61
66
|
: '-';
|
|
62
67
|
const cronValue = task.schedule_kind === 'cron' ? task.cron_expr || '-' : '-';
|
|
63
|
-
console.log(`${task.id} | ${task.status} | ${task.prompt_preview} | ${task.channel_id || '-'} | ${userId} | ${projectName} | ${folderName} | ${agent} | ${model} | ${formatRelativeTime(task.next_run_at)} | ${firesAt} | ${cronValue}`);
|
|
68
|
+
console.log(`${task.id} | ${task.status} | ${task.prompt_preview} | ${task.channel_id || '-'} | ${userId} | ${projectName} | ${folderName} | ${agent} | ${model} | ${preRun} | ${allowConcurrency} | ${formatRelativeTime(task.next_run_at)} | ${firesAt} | ${cronValue}`);
|
|
64
69
|
});
|
|
65
70
|
process.exit(0);
|
|
66
71
|
}
|
|
@@ -98,7 +103,7 @@ cli
|
|
|
98
103
|
async function resolveTaskUser({ user, channelId, }) {
|
|
99
104
|
const directUserId = getDiscordUserIdFromUserOption(user);
|
|
100
105
|
if (directUserId) {
|
|
101
|
-
return { id: directUserId
|
|
106
|
+
return { id: directUserId };
|
|
102
107
|
}
|
|
103
108
|
if (!channelId) {
|
|
104
109
|
return new Error(`Cannot look up username "${user}": task has no channel to resolve a guild from. Pass a Discord user ID instead.`);
|
|
@@ -128,18 +133,24 @@ cli
|
|
|
128
133
|
.option('--send-at <sendAt>', 'New schedule (UTC ISO date or cron expression)')
|
|
129
134
|
.option('--agent <agent>', 'Agent for the scheduled session (empty string clears)')
|
|
130
135
|
.option('--model <model>', 'Model for the scheduled session, format provider/model (empty string clears)')
|
|
136
|
+
.option('--pre-run <command>', 'New pre-run command (empty string clears)')
|
|
137
|
+
.option('--allow-concurrency <enabled>', z.enum(['true', 'false']).describe('Allow concurrent sessions: true or false'))
|
|
131
138
|
.option('-u, --user <user>', 'Discord user ID, mention, or username added to the task thread')
|
|
132
139
|
.action(async (id, options) => {
|
|
133
140
|
try {
|
|
134
141
|
const trimmedPrompt = options.prompt === undefined ? undefined : options.prompt.trim();
|
|
135
142
|
const hasAgent = options.agent !== undefined;
|
|
136
143
|
const hasModel = options.model !== undefined;
|
|
144
|
+
const hasPreRun = options.preRun !== undefined;
|
|
145
|
+
const hasAllowConcurrency = options.allowConcurrency !== undefined;
|
|
137
146
|
if (!trimmedPrompt &&
|
|
138
147
|
!options.sendAt &&
|
|
139
148
|
!options.user &&
|
|
140
149
|
!hasAgent &&
|
|
141
|
-
!hasModel
|
|
142
|
-
|
|
150
|
+
!hasModel &&
|
|
151
|
+
!hasPreRun &&
|
|
152
|
+
!hasAllowConcurrency) {
|
|
153
|
+
cliLogger.error('Provide at least --prompt, --send-at, --user, --agent, --model, --pre-run or --allow-concurrency');
|
|
143
154
|
process.exit(EXIT_NO_RESTART);
|
|
144
155
|
}
|
|
145
156
|
if (trimmedPrompt !== undefined && trimmedPrompt.length === 0) {
|
|
@@ -181,15 +192,30 @@ cli
|
|
|
181
192
|
process.exit(EXIT_NO_RESTART);
|
|
182
193
|
}
|
|
183
194
|
const newPrompt = trimmedPrompt ?? existingPayload.prompt;
|
|
195
|
+
const nextModel = hasModel ? options.model.trim() || null : existingPayload.model;
|
|
196
|
+
if (nextModel) {
|
|
197
|
+
const modelCheck = await validateCliModelOption({
|
|
198
|
+
model: nextModel,
|
|
199
|
+
directory: task.project_directory ?? undefined,
|
|
200
|
+
});
|
|
201
|
+
if (modelCheck instanceof Error) {
|
|
202
|
+
cliLogger.error(modelCheck.message);
|
|
203
|
+
process.exit(EXIT_NO_RESTART);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
184
206
|
// Match send --model/--agent: empty string clears the override (null).
|
|
185
207
|
const updatedPayload = {
|
|
186
208
|
...existingPayload,
|
|
187
209
|
prompt: newPrompt,
|
|
188
210
|
...(resolvedUser
|
|
189
|
-
? { userId: resolvedUser.id, username: resolvedUser.username }
|
|
211
|
+
? { userId: resolvedUser.id, username: resolvedUser.username || null }
|
|
190
212
|
: {}),
|
|
191
213
|
...(hasAgent ? { agent: options.agent.trim() || null } : {}),
|
|
192
|
-
...(hasModel ? { model:
|
|
214
|
+
...(hasModel ? { model: nextModel } : {}),
|
|
215
|
+
...(hasPreRun ? { preRunCommand: options.preRun.trim() || null } : {}),
|
|
216
|
+
...(hasAllowConcurrency
|
|
217
|
+
? { allowConcurrency: options.allowConcurrency === 'true' }
|
|
218
|
+
: {}),
|
|
193
219
|
};
|
|
194
220
|
const updateData = {
|
|
195
221
|
taskId,
|
|
@@ -220,7 +246,7 @@ cli
|
|
|
220
246
|
}
|
|
221
247
|
const parts = [`Updated task ${taskId}`];
|
|
222
248
|
if (resolvedUser) {
|
|
223
|
-
parts.push(`user ${resolvedUser.username} will be added to the thread`);
|
|
249
|
+
parts.push(`user ${resolvedUser.username || resolvedUser.id} will be added to the thread`);
|
|
224
250
|
}
|
|
225
251
|
if (hasAgent) {
|
|
226
252
|
parts.push(`agent=${updatedPayload.agent || '-'}`);
|
|
@@ -228,6 +254,11 @@ cli
|
|
|
228
254
|
if (hasModel) {
|
|
229
255
|
parts.push(`model=${updatedPayload.model || '-'}`);
|
|
230
256
|
}
|
|
257
|
+
if (hasPreRun)
|
|
258
|
+
parts.push(`preRun=${updatedPayload.preRunCommand || '-'}`);
|
|
259
|
+
if (hasAllowConcurrency) {
|
|
260
|
+
parts.push(`allowConcurrency=${updatedPayload.allowConcurrency}`);
|
|
261
|
+
}
|
|
231
262
|
cliLogger.log(parts.join(' | '));
|
|
232
263
|
process.exit(0);
|
|
233
264
|
}
|