@love-moon/conductor-cli 0.6.0 → 0.7.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/CHANGELOG.md +20 -0
- package/bin/conductor-channel.js +4 -1
- package/bin/conductor-config.js +1 -1
- package/bin/conductor-fire.js +28 -19
- package/bin/conductor-send-file.js +4 -2
- package/bin/conductor-task.js +320 -0
- package/bin/conductor.js +3 -2
- package/package.json +6 -5
- package/src/config-env.js +10 -0
- package/src/daemon.js +13 -7
- package/src/entity-helpers.js +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @love-moon/conductor-cli
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 689fc50: Add scheduled message management APIs and conductor task schedule list/create/delete commands.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [689fc50]
|
|
12
|
+
- @love-moon/conductor-sdk@0.7.0
|
|
13
|
+
- @love-moon/ai-sdk@0.7.0
|
|
14
|
+
|
|
15
|
+
## 0.6.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [650fc55]
|
|
20
|
+
- @love-moon/ai-sdk@0.6.1
|
|
21
|
+
- @love-moon/conductor-sdk@0.6.1
|
|
22
|
+
|
|
3
23
|
## 0.6.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/bin/conductor-channel.js
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import yargs from "yargs/yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
11
|
import { loadConfig } from "@love-moon/conductor-sdk";
|
|
12
|
+
import { envForExplicitConfigFile } from "../src/config-env.js";
|
|
12
13
|
|
|
13
14
|
const isMainModule = (() => {
|
|
14
15
|
const currentFile = fileURLToPath(import.meta.url);
|
|
@@ -56,7 +57,9 @@ export async function connectFeishuChannel(options = {}) {
|
|
|
56
57
|
|
|
57
58
|
const resolvedConfigPath = path.resolve(options.configFile || resolveDefaultConfigPath(env));
|
|
58
59
|
const rawYaml = readTextFile(resolvedConfigPath);
|
|
59
|
-
const config = loadConfig(resolvedConfigPath, {
|
|
60
|
+
const config = loadConfig(resolvedConfigPath, {
|
|
61
|
+
env: envForExplicitConfigFile(options.configFile, env),
|
|
62
|
+
});
|
|
60
63
|
const feishu = ensureFeishuChannelConfig(config);
|
|
61
64
|
|
|
62
65
|
const url = new URL("/api/channel/feishu/config", config.backendUrl);
|
package/bin/conductor-config.js
CHANGED
|
@@ -63,7 +63,7 @@ const DEFAULT_CLIs = {
|
|
|
63
63
|
const backendUrl =
|
|
64
64
|
process.env.CONDUCTOR_BACKEND_URL ||
|
|
65
65
|
process.env.BACKEND_URL ||
|
|
66
|
-
"https://conductor-ai.top";
|
|
66
|
+
"https://conductor.conductor-ai.top";
|
|
67
67
|
const defaultDaemonName = os.hostname() || "my-daemon";
|
|
68
68
|
const cliVersion = packageJson.version || "unknown";
|
|
69
69
|
const OPENCODE_INSTALL_URL = "https://opencode.ai/install";
|
package/bin/conductor-fire.js
CHANGED
|
@@ -2543,26 +2543,35 @@ export class BridgeRunner {
|
|
|
2543
2543
|
async handleInterruptedTurn(replyTo, interruptInfo) {
|
|
2544
2544
|
const normalizedReplyTo = this.normalizeReplyTarget(replyTo);
|
|
2545
2545
|
this.clearInterruptRetryForReplyTarget(normalizedReplyTo);
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
}
|
|
2553
|
-
normalizedReplyTo,
|
|
2546
|
+
// An insert is implemented as "interrupt the running turn so the just
|
|
2547
|
+
// inserted message runs next". In that case we deliberately suppress the
|
|
2548
|
+
// "Conversation interrupted" confirmation so the insertion feels seamless:
|
|
2549
|
+
// the inserted user message + its fresh reply are all the user should see.
|
|
2550
|
+
const isInsertInterrupt = interruptInfo?.reason === "user_insert";
|
|
2551
|
+
this.copilotLog(
|
|
2552
|
+
`turn interrupted replyTo=${normalizedReplyTo || "latest"}${isInsertInterrupt ? " (insert)" : ""}`,
|
|
2554
2553
|
);
|
|
2555
|
-
|
|
2556
|
-
await this.
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2554
|
+
if (!isInsertInterrupt) {
|
|
2555
|
+
await this.reportRuntimeStatus(
|
|
2556
|
+
{
|
|
2557
|
+
phase: "interrupted",
|
|
2558
|
+
reply_in_progress: false,
|
|
2559
|
+
status_done_line: "Conversation interrupted",
|
|
2560
|
+
},
|
|
2561
|
+
normalizedReplyTo,
|
|
2562
|
+
);
|
|
2563
|
+
try {
|
|
2564
|
+
await this.conductor.sendMessage(this.taskId, "Conversation interrupted", {
|
|
2565
|
+
backend: this.backendName,
|
|
2566
|
+
reply_to: normalizedReplyTo || undefined,
|
|
2567
|
+
interrupted: true,
|
|
2568
|
+
interruption_request_id: interruptInfo?.requestId || undefined,
|
|
2569
|
+
reason: interruptInfo?.reason || undefined,
|
|
2570
|
+
cli_args: this.cliArgs,
|
|
2571
|
+
});
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
log(`Failed to send interrupt confirmation for ${this.taskId}: ${error?.message || error}`);
|
|
2574
|
+
}
|
|
2566
2575
|
}
|
|
2567
2576
|
if (normalizedReplyTo) {
|
|
2568
2577
|
this.processedMessageIds.add(normalizedReplyTo);
|
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import yargs from "yargs/yargs";
|
|
11
11
|
import { hideBin } from "yargs/helpers";
|
|
12
12
|
import { ConductorConfig, loadConfig } from "@love-moon/conductor-sdk";
|
|
13
|
+
import { envForExplicitConfigFile } from "../src/config-env.js";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
15
16
|
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".conductor", "config.yaml");
|
|
@@ -110,8 +111,9 @@ export function detectTaskId(options = {}) {
|
|
|
110
111
|
|
|
111
112
|
function loadCliConfig(configFile, env = process.env) {
|
|
112
113
|
const configPath = configFile ? path.resolve(configFile) : DEFAULT_CONFIG_PATH;
|
|
114
|
+
const configEnv = envForExplicitConfigFile(configFile, env);
|
|
113
115
|
if (fs.existsSync(configPath)) {
|
|
114
|
-
return loadConfig(configPath, { env });
|
|
116
|
+
return loadConfig(configPath, { env: configEnv });
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
const agentToken = typeof env.CONDUCTOR_AGENT_TOKEN === "string" ? env.CONDUCTOR_AGENT_TOKEN.trim() : "";
|
|
@@ -123,7 +125,7 @@ function loadCliConfig(configFile, env = process.env) {
|
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
return loadConfig(configPath, { env });
|
|
128
|
+
return loadConfig(configPath, { env: configEnv });
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
export function guessMimeType(fileName, preferredMimeType = "") {
|
package/bin/conductor-task.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* list [--project ...] [--issue <id>] [--status ...]
|
|
8
8
|
* show <id>
|
|
9
9
|
* send <id> [<message>] [--stdin] [--from-file FILE] [--metadata-json '{...}']
|
|
10
|
+
* insert <id> [<message>] [--stdin] [--from-file FILE] [--target-reply-to <msg-id>]
|
|
10
11
|
* messages <id> [--limit N] [--before <msg-id>]
|
|
12
|
+
* schedule list <id>
|
|
13
|
+
* schedule create <id> [<message>] (--delay 10m | --at ISO | --every 1h)
|
|
14
|
+
* schedule delete <id> <schedule-id>
|
|
11
15
|
*
|
|
12
16
|
* Global flags supported on every write subcommand:
|
|
13
17
|
* --json, --dry-run, --project, --config-file
|
|
@@ -85,6 +89,133 @@ function parseMetadataJson(value) {
|
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
function parseDuration(value, label) {
|
|
93
|
+
const raw = String(value ?? "").trim().toLowerCase();
|
|
94
|
+
const match = raw.match(/^(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours)$/);
|
|
95
|
+
if (!match) {
|
|
96
|
+
const err = new Error(`${label} must look like 10m, 2h, 10 minutes, or 2 hours`);
|
|
97
|
+
err.code = "ARGS";
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const amount = Number.parseInt(match[1], 10);
|
|
101
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
102
|
+
const err = new Error(`${label} must be a positive duration`);
|
|
103
|
+
err.code = "ARGS";
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
amount,
|
|
108
|
+
unit: match[2].startsWith("h") ? "hour" : "minute",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseFutureIso(value, label) {
|
|
113
|
+
const raw = String(value ?? "").trim();
|
|
114
|
+
const date = new Date(raw);
|
|
115
|
+
if (!raw || !Number.isFinite(date.getTime())) {
|
|
116
|
+
const err = new Error(`${label} must be a valid date/time string`);
|
|
117
|
+
err.code = "ARGS";
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
return date.toISOString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parsePositiveIntegerOption(value, label) {
|
|
124
|
+
if (value === undefined || value === null) return undefined;
|
|
125
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
126
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
127
|
+
const err = new Error(`${label} must be a positive integer`);
|
|
128
|
+
err.code = "ARGS";
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseScheduleOptions(argv) {
|
|
135
|
+
const modes = [
|
|
136
|
+
argv.delay !== undefined ? "delay" : null,
|
|
137
|
+
argv.at !== undefined ? "at" : null,
|
|
138
|
+
argv.every !== undefined ? "every" : null,
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
if (modes.length !== 1) {
|
|
141
|
+
const err = new Error("Provide exactly one schedule mode: --delay, --at, or --every");
|
|
142
|
+
err.code = "ARGS";
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (argv.delay !== undefined) {
|
|
147
|
+
const parsed = parseDuration(argv.delay, "--delay");
|
|
148
|
+
return {
|
|
149
|
+
mode: "delay",
|
|
150
|
+
amount: parsed.amount,
|
|
151
|
+
unit: parsed.unit,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (argv.at !== undefined) {
|
|
156
|
+
return {
|
|
157
|
+
mode: "at",
|
|
158
|
+
sendAt: parseFutureIso(argv.at, "--at"),
|
|
159
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const parsed = parseDuration(argv.every, "--every");
|
|
164
|
+
const stop = {
|
|
165
|
+
stopWhenTaskNotRunning: !argv.keepWhenTaskStopped,
|
|
166
|
+
};
|
|
167
|
+
const maxRuns = parsePositiveIntegerOption(argv.maxRuns, "--max-runs");
|
|
168
|
+
const maxSkips = parsePositiveIntegerOption(argv.maxSkips, "--max-skips");
|
|
169
|
+
if (maxRuns !== undefined) {
|
|
170
|
+
stop.maxRuns = maxRuns;
|
|
171
|
+
}
|
|
172
|
+
if (maxSkips !== undefined) {
|
|
173
|
+
stop.maxSkips = maxSkips;
|
|
174
|
+
}
|
|
175
|
+
if (argv.stopAt !== undefined) {
|
|
176
|
+
stop.stopAt = parseFutureIso(argv.stopAt, "--stop-at");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
mode: "interval",
|
|
181
|
+
every: parsed.amount,
|
|
182
|
+
unit: parsed.unit,
|
|
183
|
+
condition: argv.ifIdle ? "ai_idle" : "none",
|
|
184
|
+
stop,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function scheduledMessageAsObject(schedule) {
|
|
189
|
+
if (!schedule) return null;
|
|
190
|
+
if (typeof schedule.asObject === "function") return schedule.asObject();
|
|
191
|
+
return {
|
|
192
|
+
id: schedule.id,
|
|
193
|
+
taskId: schedule.taskId ?? schedule.task_id,
|
|
194
|
+
sourceMessageId: schedule.sourceMessageId ?? schedule.source_message_id ?? null,
|
|
195
|
+
content: schedule.content,
|
|
196
|
+
kind: schedule.kind,
|
|
197
|
+
condition: schedule.condition,
|
|
198
|
+
status: schedule.status,
|
|
199
|
+
nextRunAt: schedule.nextRunAt ?? schedule.next_run_at,
|
|
200
|
+
runCount: schedule.runCount ?? schedule.run_count ?? 0,
|
|
201
|
+
skipCount: schedule.skipCount ?? schedule.skip_count ?? 0,
|
|
202
|
+
failureCount: schedule.failureCount ?? schedule.failure_count ?? 0,
|
|
203
|
+
maxRuns: schedule.maxRuns ?? schedule.max_runs ?? null,
|
|
204
|
+
maxSkips: schedule.maxSkips ?? schedule.max_skips ?? null,
|
|
205
|
+
stopAt: schedule.stopAt ?? schedule.stop_at ?? null,
|
|
206
|
+
lastRunAt: schedule.lastRunAt ?? schedule.last_run_at ?? null,
|
|
207
|
+
lastError: schedule.lastError ?? schedule.last_error ?? null,
|
|
208
|
+
createdAt: schedule.createdAt ?? schedule.created_at ?? null,
|
|
209
|
+
updatedAt: schedule.updatedAt ?? schedule.updated_at ?? null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatContentPreview(value) {
|
|
214
|
+
const text = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
215
|
+
if (text.length <= 60) return text;
|
|
216
|
+
return `${text.slice(0, 57)}...`;
|
|
217
|
+
}
|
|
218
|
+
|
|
88
219
|
async function handleList(argv, deps) {
|
|
89
220
|
const apis = await buildApis(deps);
|
|
90
221
|
const project = await resolveProject(apis, { env: deps.env, cwd: deps.cwd, project: argv.project });
|
|
@@ -182,6 +313,48 @@ async function handleSend(argv, deps) {
|
|
|
182
313
|
return EXIT.OK;
|
|
183
314
|
}
|
|
184
315
|
|
|
316
|
+
async function handleInsert(argv, deps) {
|
|
317
|
+
const apis = await buildApis(deps);
|
|
318
|
+
const content = readMessageInput({
|
|
319
|
+
positional: argv.message,
|
|
320
|
+
fromFile: argv.fromFile,
|
|
321
|
+
useStdin: Boolean(argv.stdin),
|
|
322
|
+
stdin: deps.stdin,
|
|
323
|
+
});
|
|
324
|
+
const extraMetadata = parseMetadataJson(argv.metadataJson);
|
|
325
|
+
const metadata = buildAuditMetadata(deps.env, extraMetadata || {});
|
|
326
|
+
const body = {
|
|
327
|
+
content,
|
|
328
|
+
metadata,
|
|
329
|
+
};
|
|
330
|
+
if (argv.targetReplyTo) {
|
|
331
|
+
body.target_reply_to = String(argv.targetReplyTo);
|
|
332
|
+
}
|
|
333
|
+
if (argv.dryRun) {
|
|
334
|
+
emitDryRun(
|
|
335
|
+
deps.stdout,
|
|
336
|
+
argv.json,
|
|
337
|
+
makeDryRunPayload(
|
|
338
|
+
"POST",
|
|
339
|
+
`${buildBaseUrl(apis.config)}/api/tasks/${encodeURIComponent(argv.id)}/insert`,
|
|
340
|
+
body,
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
return EXIT.OK;
|
|
344
|
+
}
|
|
345
|
+
const result = await apis.tasks.insertTaskMessage(argv.id, content, {
|
|
346
|
+
metadata: body.metadata,
|
|
347
|
+
targetReplyTo: argv.targetReplyTo ? String(argv.targetReplyTo) : undefined,
|
|
348
|
+
});
|
|
349
|
+
if (argv.json) {
|
|
350
|
+
printJson(deps.stdout, result ?? { inserted: true });
|
|
351
|
+
return EXIT.OK;
|
|
352
|
+
}
|
|
353
|
+
const id = result?.id ? `${result.id} ` : "";
|
|
354
|
+
printPretty(deps.stdout, `Inserted message ${id}into task ${argv.id}`);
|
|
355
|
+
return EXIT.OK;
|
|
356
|
+
}
|
|
357
|
+
|
|
185
358
|
async function handleMessages(argv, deps) {
|
|
186
359
|
const apis = await buildApis(deps);
|
|
187
360
|
const list = await apis.tasks.listTaskMessages(argv.id, {
|
|
@@ -200,6 +373,90 @@ async function handleMessages(argv, deps) {
|
|
|
200
373
|
return EXIT.OK;
|
|
201
374
|
}
|
|
202
375
|
|
|
376
|
+
async function handleScheduleList(argv, deps) {
|
|
377
|
+
const apis = await buildApis(deps);
|
|
378
|
+
const schedules = await apis.tasks.listScheduledMessages(argv.id);
|
|
379
|
+
const objects = (Array.isArray(schedules) ? schedules : []).map(scheduledMessageAsObject);
|
|
380
|
+
if (argv.json) {
|
|
381
|
+
printJson(deps.stdout, objects);
|
|
382
|
+
return EXIT.OK;
|
|
383
|
+
}
|
|
384
|
+
if (objects.length === 0) {
|
|
385
|
+
printPretty(deps.stdout, "(no scheduled messages)");
|
|
386
|
+
return EXIT.OK;
|
|
387
|
+
}
|
|
388
|
+
printPretty(deps.stdout, `${pad("ID", 24)} ${pad("STATUS", 10)} ${pad("NEXT RUN", 24)} CONTENT`);
|
|
389
|
+
for (const schedule of objects) {
|
|
390
|
+
printPretty(
|
|
391
|
+
deps.stdout,
|
|
392
|
+
`${pad(schedule.id, 24)} ${pad(schedule.status, 10)} ${pad(schedule.nextRunAt, 24)} ${formatContentPreview(schedule.content)}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return EXIT.OK;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function handleScheduleCreate(argv, deps) {
|
|
399
|
+
const apis = await buildApis(deps);
|
|
400
|
+
const content = readMessageInput({
|
|
401
|
+
positional: argv.message,
|
|
402
|
+
fromFile: argv.fromFile,
|
|
403
|
+
useStdin: Boolean(argv.stdin),
|
|
404
|
+
stdin: deps.stdin,
|
|
405
|
+
});
|
|
406
|
+
const schedule = parseScheduleOptions(argv);
|
|
407
|
+
const body = {
|
|
408
|
+
content,
|
|
409
|
+
sourceMessageId: argv.sourceMessageId ? String(argv.sourceMessageId) : null,
|
|
410
|
+
schedule,
|
|
411
|
+
};
|
|
412
|
+
if (argv.dryRun) {
|
|
413
|
+
emitDryRun(
|
|
414
|
+
deps.stdout,
|
|
415
|
+
argv.json,
|
|
416
|
+
makeDryRunPayload(
|
|
417
|
+
"POST",
|
|
418
|
+
`${buildBaseUrl(apis.config)}/api/tasks/${encodeURIComponent(argv.id)}/scheduled-messages`,
|
|
419
|
+
body,
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
return EXIT.OK;
|
|
423
|
+
}
|
|
424
|
+
const result = await apis.tasks.createScheduledMessage(argv.id, body);
|
|
425
|
+
const obj = scheduledMessageAsObject(result);
|
|
426
|
+
if (argv.json) {
|
|
427
|
+
printJson(deps.stdout, obj);
|
|
428
|
+
return EXIT.OK;
|
|
429
|
+
}
|
|
430
|
+
printPretty(deps.stdout, `Created scheduled message ${obj.id} for task ${argv.id}`);
|
|
431
|
+
if (obj.nextRunAt) {
|
|
432
|
+
printPretty(deps.stdout, `Next run: ${obj.nextRunAt}`);
|
|
433
|
+
}
|
|
434
|
+
return EXIT.OK;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function handleScheduleDelete(argv, deps) {
|
|
438
|
+
const apis = await buildApis(deps);
|
|
439
|
+
if (argv.dryRun) {
|
|
440
|
+
emitDryRun(
|
|
441
|
+
deps.stdout,
|
|
442
|
+
argv.json,
|
|
443
|
+
makeDryRunPayload(
|
|
444
|
+
"DELETE",
|
|
445
|
+
`${buildBaseUrl(apis.config)}/api/tasks/${encodeURIComponent(argv.id)}/scheduled-messages/${encodeURIComponent(argv.scheduleId)}`,
|
|
446
|
+
undefined,
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
return EXIT.OK;
|
|
450
|
+
}
|
|
451
|
+
await apis.tasks.deleteScheduledMessage(argv.id, argv.scheduleId);
|
|
452
|
+
if (argv.json) {
|
|
453
|
+
printJson(deps.stdout, { deleted: true, taskId: argv.id, scheduleId: argv.scheduleId });
|
|
454
|
+
return EXIT.OK;
|
|
455
|
+
}
|
|
456
|
+
printPretty(deps.stdout, `Deleted scheduled message ${argv.scheduleId} from task ${argv.id}`);
|
|
457
|
+
return EXIT.OK;
|
|
458
|
+
}
|
|
459
|
+
|
|
203
460
|
export async function main(argvInput = hideBin(process.argv), deps = {}) {
|
|
204
461
|
const stdout = deps.stdout || process.stdout;
|
|
205
462
|
const stderr = deps.stderr || process.stderr;
|
|
@@ -249,6 +506,20 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
|
|
|
249
506
|
exitCode = await handleSend(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
250
507
|
},
|
|
251
508
|
)
|
|
509
|
+
.command(
|
|
510
|
+
"insert <id> [message]",
|
|
511
|
+
"Insert a mid-turn message into a running task (interrupts the current turn so it runs next)",
|
|
512
|
+
(cmd) => cmd
|
|
513
|
+
.positional("id", { type: "string", demandOption: true })
|
|
514
|
+
.positional("message", { type: "string" })
|
|
515
|
+
.option("stdin", { type: "boolean", default: false })
|
|
516
|
+
.option("from-file", { type: "string" })
|
|
517
|
+
.option("target-reply-to", { type: "string", describe: "Reply target of the in-flight turn to interrupt (defaults to the latest user message)" })
|
|
518
|
+
.option("metadata-json", { type: "string", describe: "Extra JSON metadata to merge into the message" }),
|
|
519
|
+
async (argv) => {
|
|
520
|
+
exitCode = await handleInsert(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
521
|
+
},
|
|
522
|
+
)
|
|
252
523
|
.command(
|
|
253
524
|
"messages <id>",
|
|
254
525
|
"Pull a slice of task messages and exit (no --follow in this RFC)",
|
|
@@ -260,6 +531,55 @@ export async function main(argvInput = hideBin(process.argv), deps = {}) {
|
|
|
260
531
|
exitCode = await handleMessages(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
261
532
|
},
|
|
262
533
|
)
|
|
534
|
+
.command(
|
|
535
|
+
"schedule",
|
|
536
|
+
"Create, list, and delete scheduled messages for a task",
|
|
537
|
+
(cmd) => cmd
|
|
538
|
+
.command(
|
|
539
|
+
"list <id>",
|
|
540
|
+
"List scheduled messages for a task",
|
|
541
|
+
(sub) => sub.positional("id", { type: "string", demandOption: true }),
|
|
542
|
+
async (argv) => {
|
|
543
|
+
exitCode = await handleScheduleList(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
.command(
|
|
547
|
+
"create <id> [message]",
|
|
548
|
+
"Create a scheduled message for a task",
|
|
549
|
+
(sub) => sub
|
|
550
|
+
.positional("id", { type: "string", demandOption: true })
|
|
551
|
+
.positional("message", { type: "string" })
|
|
552
|
+
.option("stdin", { type: "boolean", default: false })
|
|
553
|
+
.option("from-file", { type: "string" })
|
|
554
|
+
.option("source-message-id", { type: "string", describe: "Original message id, when scheduling a copy" })
|
|
555
|
+
.option("delay", { type: "string", describe: "Send once after a duration, e.g. 10m or 2h" })
|
|
556
|
+
.option("at", { type: "string", describe: "Send once at an ISO/local date-time string" })
|
|
557
|
+
.option("every", { type: "string", describe: "Repeat every duration, e.g. 30m or 1h" })
|
|
558
|
+
.option("if-idle", { type: "boolean", default: false, describe: "For repeats, only send when the AI is idle" })
|
|
559
|
+
.option("max-runs", { type: "number", describe: "For repeats, stop after N sends" })
|
|
560
|
+
.option("max-skips", { type: "number", describe: "For repeats, stop after N skips" })
|
|
561
|
+
.option("stop-at", { type: "string", describe: "For repeats, stop after this date-time" })
|
|
562
|
+
.option("keep-when-task-stopped", {
|
|
563
|
+
type: "boolean",
|
|
564
|
+
default: false,
|
|
565
|
+
describe: "For repeats, skip instead of completing when the task is not running",
|
|
566
|
+
}),
|
|
567
|
+
async (argv) => {
|
|
568
|
+
exitCode = await handleScheduleCreate(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
569
|
+
},
|
|
570
|
+
)
|
|
571
|
+
.command(
|
|
572
|
+
"delete <id> <scheduleId>",
|
|
573
|
+
"Delete an active scheduled message",
|
|
574
|
+
(sub) => sub
|
|
575
|
+
.positional("id", { type: "string", demandOption: true })
|
|
576
|
+
.positional("scheduleId", { type: "string", demandOption: true }),
|
|
577
|
+
async (argv) => {
|
|
578
|
+
exitCode = await handleScheduleDelete(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
579
|
+
},
|
|
580
|
+
)
|
|
581
|
+
.demandCommand(1),
|
|
582
|
+
)
|
|
263
583
|
.demandCommand(1)
|
|
264
584
|
.fail((msg, err) => {
|
|
265
585
|
if (err) {
|
package/bin/conductor.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* serve-ai - Start an OpenAI-compatible local AI server
|
|
15
15
|
* project - Manage Conductor projects (list/show/create/...)
|
|
16
16
|
* issue - Manage issues (list/show/create/update/start/done)
|
|
17
|
-
* task - Manage tasks (list/show/send/messages)
|
|
17
|
+
* task - Manage tasks (list/show/send/messages/schedule)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -134,7 +134,7 @@ Subcommands:
|
|
|
134
134
|
serve-ai Start an OpenAI-compatible local AI server
|
|
135
135
|
project Manage Conductor projects (list/show/create/...)
|
|
136
136
|
issue Manage issues (list/show/create/update/start/done)
|
|
137
|
-
task Manage tasks (list/show/send/messages)
|
|
137
|
+
task Manage tasks (list/show/send/messages/schedule)
|
|
138
138
|
|
|
139
139
|
Options:
|
|
140
140
|
-h, --help Show this help message
|
|
@@ -153,6 +153,7 @@ Examples:
|
|
|
153
153
|
conductor project list
|
|
154
154
|
conductor issue create --title "Refactor module" --priority P2
|
|
155
155
|
conductor task send <task-id> "please add a unit test"
|
|
156
|
+
conductor task schedule create <task-id> "follow up" --delay 10m
|
|
156
157
|
|
|
157
158
|
For subcommand-specific help:
|
|
158
159
|
conductor fire --help
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"gitCommitId": "ed4e3ab",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
"provenance": true
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
+
"pretest": "pnpm --dir ../modules/ai-sdk build && pnpm --dir ../modules/conductor-sdk build",
|
|
23
24
|
"test": "node --test test/*.test.js"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@love-moon/ai-sdk": "0.
|
|
27
|
-
"@love-moon/conductor-sdk": "0.
|
|
27
|
+
"@love-moon/ai-sdk": "0.7.0",
|
|
28
|
+
"@love-moon/conductor-sdk": "0.7.0",
|
|
28
29
|
"@github/copilot-sdk": "^0.3.0",
|
|
29
30
|
"chrome-launcher": "^1.2.1",
|
|
30
31
|
"chrome-remote-interface": "^0.33.0",
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
},
|
|
38
39
|
"optionalDependencies": {
|
|
39
40
|
"@roamhq/wrtc": "^0.10.0",
|
|
40
|
-
"@love-moon/chat-web": "0.
|
|
41
|
+
"@love-moon/chat-web": "0.7.0"
|
|
41
42
|
},
|
|
42
43
|
"pnpm": {
|
|
43
44
|
"onlyBuiltDependencies": [
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function envForExplicitConfigFile(configFile, env = process.env) {
|
|
2
|
+
if (!configFile) return env;
|
|
3
|
+
|
|
4
|
+
const configEnv = { ...env };
|
|
5
|
+
delete configEnv.CONDUCTOR_AGENT_TOKEN;
|
|
6
|
+
delete configEnv.CONDUCTOR_BACKEND_URL;
|
|
7
|
+
delete configEnv.CONDUCTOR_WS_URL;
|
|
8
|
+
delete configEnv.CONDUCTOR_BACKEND_WS_URL;
|
|
9
|
+
return configEnv;
|
|
10
|
+
}
|
package/src/daemon.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ProjectContext,
|
|
17
17
|
} from "@love-moon/conductor-sdk";
|
|
18
18
|
import { DaemonLogCollector } from "./log-collector.js";
|
|
19
|
+
import { envForExplicitConfigFile } from "./config-env.js";
|
|
19
20
|
import { createAiManagerHandlers, handleAiManagerRequest } from "./ai-manager-handlers.js";
|
|
20
21
|
import {
|
|
21
22
|
CUSTOM_COMMANDS_CAPABILITY,
|
|
@@ -598,6 +599,7 @@ function normalizeTerminalEnv(value) {
|
|
|
598
599
|
}
|
|
599
600
|
|
|
600
601
|
const PTY_TASK_SCOPED_ENV_KEYS = [
|
|
602
|
+
"CONDUCTOR_CLI_COMMAND",
|
|
601
603
|
"CONDUCTOR_PROJECT_ID",
|
|
602
604
|
"CONDUCTOR_TASK_ID",
|
|
603
605
|
"CONDUCTOR_PTY_SESSION_ID",
|
|
@@ -667,8 +669,9 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
667
669
|
};
|
|
668
670
|
|
|
669
671
|
let fileConfig;
|
|
672
|
+
const configFileEnv = envForExplicitConfigFile(config.CONFIG_FILE, process.env);
|
|
670
673
|
try {
|
|
671
|
-
fileConfig = loadConfig(config.CONFIG_FILE);
|
|
674
|
+
fileConfig = loadConfig(config.CONFIG_FILE, { env: configFileEnv });
|
|
672
675
|
log(`Loaded config from ${config.CONFIG_FILE || "~/.conductor/config.yaml"}`);
|
|
673
676
|
} catch (err) {
|
|
674
677
|
if (!(err instanceof ConfigFileNotFound)) {
|
|
@@ -677,15 +680,15 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
677
680
|
}
|
|
678
681
|
|
|
679
682
|
const userConfig = getUserConfig(config.CONFIG_FILE);
|
|
683
|
+
const allowEnvConfigOverrides = !config.CONFIG_FILE;
|
|
680
684
|
const explicitWsUrl =
|
|
681
685
|
config.BACKEND_URL ||
|
|
682
|
-
process.env.CONDUCTOR_BACKEND_WS_URL ||
|
|
683
|
-
process.env.CONDUCTOR_WS_URL ||
|
|
686
|
+
(allowEnvConfigOverrides ? process.env.CONDUCTOR_BACKEND_WS_URL || process.env.CONDUCTOR_WS_URL : null) ||
|
|
684
687
|
null;
|
|
685
688
|
const derivedHttpFromWs = explicitWsUrl ? deriveBackendHttpFromWebsocket(explicitWsUrl) : null;
|
|
686
689
|
const BACKEND_HTTP =
|
|
687
690
|
config.BACKEND_HTTP ||
|
|
688
|
-
process.env.CONDUCTOR_BACKEND_URL ||
|
|
691
|
+
(allowEnvConfigOverrides ? process.env.CONDUCTOR_BACKEND_URL : null) ||
|
|
689
692
|
derivedHttpFromWs ||
|
|
690
693
|
fileConfig?.backendUrl ||
|
|
691
694
|
"http://localhost:6152";
|
|
@@ -693,7 +696,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
693
696
|
explicitWsUrl ||
|
|
694
697
|
deriveWebsocketUrlFromHttp(BACKEND_HTTP);
|
|
695
698
|
const AGENT_TOKEN =
|
|
696
|
-
config.AGENT_TOKEN ||
|
|
699
|
+
config.AGENT_TOKEN ||
|
|
700
|
+
(allowEnvConfigOverrides ? process.env.CONDUCTOR_AGENT_TOKEN : null) ||
|
|
701
|
+
fileConfig?.agentToken ||
|
|
702
|
+
"default-agent-token";
|
|
697
703
|
const configuredDaemonName =
|
|
698
704
|
(typeof userConfig.daemon_name === "string" && userConfig.daemon_name.trim()) ||
|
|
699
705
|
(typeof fileConfig?.daemonName === "string" && fileConfig.daemonName.trim()) ||
|
|
@@ -5264,7 +5270,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5264
5270
|
});
|
|
5265
5271
|
|
|
5266
5272
|
const env = {
|
|
5267
|
-
...process.env,
|
|
5273
|
+
...stripPtyTaskScopedEnv(process.env),
|
|
5268
5274
|
PWD: taskDir,
|
|
5269
5275
|
CONDUCTOR_PROJECT_ID: projectId,
|
|
5270
5276
|
CONDUCTOR_TASK_ID: taskId,
|
|
@@ -5907,7 +5913,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
5907
5913
|
}
|
|
5908
5914
|
|
|
5909
5915
|
const env = {
|
|
5910
|
-
...process.env,
|
|
5916
|
+
...stripPtyTaskScopedEnv(process.env),
|
|
5911
5917
|
PWD: taskDir,
|
|
5912
5918
|
CONDUCTOR_PROJECT_ID: normalizedProjectId,
|
|
5913
5919
|
CONDUCTOR_TASK_ID: normalizedTargetTaskId,
|
package/src/entity-helpers.js
CHANGED
|
@@ -20,6 +20,8 @@ import path from "node:path";
|
|
|
20
20
|
import process from "node:process";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
|
|
23
|
+
import { envForExplicitConfigFile } from "./config-env.js";
|
|
24
|
+
|
|
23
25
|
// RFC §4 exit codes
|
|
24
26
|
export const EXIT = {
|
|
25
27
|
OK: 0,
|
|
@@ -69,8 +71,9 @@ export async function loadConductorConfig(options = {}) {
|
|
|
69
71
|
const sdk = await loadSdk(options);
|
|
70
72
|
|
|
71
73
|
const configPath = resolveConfigPath(configFile, env);
|
|
74
|
+
const configEnv = envForExplicitConfigFile(configFile, env);
|
|
72
75
|
if (fs.existsSync(configPath)) {
|
|
73
|
-
return sdk.loadConfig(configPath, { env });
|
|
76
|
+
return sdk.loadConfig(configPath, { env: configEnv });
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
const agentToken = typeof env.CONDUCTOR_AGENT_TOKEN === "string" ? env.CONDUCTOR_AGENT_TOKEN.trim() : "";
|
|
@@ -83,7 +86,7 @@ export async function loadConductorConfig(options = {}) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// Let the SDK raise a typed not-found error.
|
|
86
|
-
return sdk.loadConfig(configPath, { env });
|
|
89
|
+
return sdk.loadConfig(configPath, { env: configEnv });
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|