@love-moon/conductor-cli 0.6.1 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 0.6.1
4
16
 
5
17
  ### Patch Changes
@@ -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, { env });
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);
@@ -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";
@@ -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
- this.copilotLog(`turn interrupted replyTo=${normalizedReplyTo || "latest"}`);
2547
- await this.reportRuntimeStatus(
2548
- {
2549
- phase: "interrupted",
2550
- reply_in_progress: false,
2551
- status_done_line: "Conversation interrupted",
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
- try {
2556
- await this.conductor.sendMessage(this.taskId, "Conversation interrupted", {
2557
- backend: this.backendName,
2558
- reply_to: normalizedReplyTo || undefined,
2559
- interrupted: true,
2560
- interruption_request_id: interruptInfo?.requestId || undefined,
2561
- reason: interruptInfo?.reason || undefined,
2562
- cli_args: this.cliArgs,
2563
- });
2564
- } catch (error) {
2565
- log(`Failed to send interrupt confirmation for ${this.taskId}: ${error?.message || error}`);
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 = "") {
@@ -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.6.1",
4
- "gitCommitId": "707fffe",
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.6.1",
27
- "@love-moon/conductor-sdk": "0.6.1",
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.6.1"
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 || process.env.CONDUCTOR_AGENT_TOKEN || fileConfig?.agentToken || "default-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,
@@ -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
  /**