@love-moon/conductor-cli 0.15.1 → 0.16.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.
@@ -0,0 +1,608 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * conductor settings — the user preferences behind the web settings page.
5
+ *
6
+ * Subcommands (all under /api/user-preferences unless noted):
7
+ * global-backends get GET /global-ai-backends
8
+ * global-backends set (--json-body J | --from-file F)
9
+ * PUT /global-ai-backends { backends: [{ host, backend }] }
10
+ * global-backends add <host> <backend> GET then PUT (read-modify-write)
11
+ * global-backends remove <host> <backend> GET then PUT (read-modify-write)
12
+ *
13
+ * catchphrases list GET /catchphrases
14
+ * catchphrases add <text...> POST /catchphrases { text }
15
+ * catchphrases update <id> <text...> PATCH /catchphrases/:id { text }
16
+ * catchphrases delete <id> DELETE /catchphrases/:id
17
+ * catchphrases reorder <id...> PUT /catchphrases/reorder { ids } (full ordered set)
18
+ * catchphrases touch <id> POST /catchphrases/:id/touch
19
+ *
20
+ * daily-report get GET /daily-report
21
+ * daily-report set [--enabled B] [--delivery-channels in_app,feishu] [--timezone TZ]
22
+ * PATCH /daily-report { enabled?, deliveryChannels? }
23
+ * The server takes the schedule timezone from the X-Client-Timezone
24
+ * header (the browser's zone on the web); the CLI sends --timezone or
25
+ * this machine's zone.
26
+ *
27
+ * task-list get GET /task-list
28
+ * task-list set (--running-only B | --json-body J | --from-file F)
29
+ * PATCH /task-list { tasksRunningOnly }
30
+ *
31
+ * task-card-groups get|set GET / PATCH /task-card-groups { scope, groups }
32
+ * project-card-groups get|set GET / PATCH /project-card-groups { scope, groups }
33
+ * `set` takes --json-body/--from-file: either the full { scope, groups }
34
+ * object, or a bare groups array combined with --scope (default
35
+ * "projects:all", the scope the web UI writes).
36
+ *
37
+ * reports list [--limit N] GET /api/daily-reports?list=1&limit=N
38
+ * reports show [--date YYYY-MM-DD] [--timezone TZ]
39
+ * GET /api/daily-reports?date=&timezone=
40
+ * reports generate [--date YYYY-MM-DD] [--timezone TZ]
41
+ * POST /api/daily-reports { reportDate, timezone }
42
+ * (`daily-reports` is an alias of `reports`; the generated daily reports
43
+ * live beside the daily-report *setting* so both are under `settings`.)
44
+ *
45
+ * Global flags: --json (raw response), --dry-run (write verbs print the request
46
+ * instead of sending it), --config-file.
47
+ */
48
+
49
+ import fs from "node:fs";
50
+ import path from "node:path";
51
+ import process from "node:process";
52
+ import { fileURLToPath } from "node:url";
53
+
54
+ import yargs from "yargs/yargs";
55
+ import { hideBin } from "yargs/helpers";
56
+
57
+ import { EXIT, exitCodeForError, pad, printJson, printPretty, reportError } from "../src/entity-helpers.js";
58
+ import { apiPath, argsError, buildHttp, formatTable, parseJsonOption, sendOrPreview } from "../src/backend-http.js";
59
+
60
+ const isMainModule = (() => {
61
+ const currentFile = fileURLToPath(import.meta.url);
62
+ const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
63
+ return entryFile === currentFile;
64
+ })();
65
+
66
+ const PREFS = (...segments) => apiPath("user-preferences", ...segments);
67
+ const DEFAULT_CARD_GROUPS_SCOPE = "projects:all";
68
+ const DELIVERY_CHANNELS = ["in_app", "feishu"];
69
+
70
+ // ---------------------------------------------------------------- helpers
71
+
72
+ function readJsonBody(argv, { required = true } = {}) {
73
+ if (argv.jsonBody !== undefined && argv.fromFile !== undefined) {
74
+ throw argsError("Provide only one of --json-body and --from-file");
75
+ }
76
+ if (argv.jsonBody !== undefined) return parseJsonOption(argv.jsonBody, "--json-body");
77
+ if (argv.fromFile !== undefined) {
78
+ let text;
79
+ try {
80
+ text = fs.readFileSync(path.resolve(String(argv.fromFile)), "utf8");
81
+ } catch (error) {
82
+ throw argsError(`Cannot read --from-file: ${error.message}`);
83
+ }
84
+ return parseJsonOption(text, "--from-file");
85
+ }
86
+ if (required) throw argsError("Pass the new value with --json-body '<json>' or --from-file FILE");
87
+ return undefined;
88
+ }
89
+
90
+ const jsonBodyOptions = (cmd) => cmd
91
+ .option("json-body", { type: "string", describe: "Request body as JSON" })
92
+ .option("from-file", { type: "string", describe: "Read the JSON request body from a file" });
93
+
94
+ function localTimezone() {
95
+ try {
96
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ function joinText(value) {
103
+ const text = [].concat(value ?? []).map(String).join(" ").trim();
104
+ if (!text) throw argsError("Catchphrase text must not be empty");
105
+ return text;
106
+ }
107
+
108
+ function oneLine(text) {
109
+ return String(text ?? "").replace(/\s+/g, " ").trim();
110
+ }
111
+
112
+ // ---------------------------------------------------------------- global backends
113
+
114
+ function readBackends(payload) {
115
+ return Array.isArray(payload?.backends) ? payload.backends : [];
116
+ }
117
+
118
+ function printBackends(argv, deps, payload) {
119
+ if (argv.json) {
120
+ printJson(deps.stdout, payload);
121
+ return;
122
+ }
123
+ const backends = readBackends(payload);
124
+ if (backends.length === 0) {
125
+ printPretty(deps.stdout, "(no global AI backends)");
126
+ return;
127
+ }
128
+ printPretty(deps.stdout, `${pad("HOST", 32)} BACKEND`);
129
+ for (const entry of backends) {
130
+ printPretty(deps.stdout, `${pad(entry.host, 32)} ${entry.backend}`);
131
+ }
132
+ }
133
+
134
+ async function putBackends(http, argv, deps, backends, message) {
135
+ const { dryRun, data } = await sendOrPreview(
136
+ http, argv, deps, "PUT", PREFS("global-ai-backends"), { backends },
137
+ );
138
+ if (dryRun) return EXIT.OK;
139
+ if (!argv.json && message) printPretty(deps.stdout, message);
140
+ printBackends(argv, deps, data);
141
+ return EXIT.OK;
142
+ }
143
+
144
+ async function handleBackendsGet(argv, deps) {
145
+ const http = await buildHttp(deps);
146
+ printBackends(argv, deps, await http.get(PREFS("global-ai-backends")));
147
+ return EXIT.OK;
148
+ }
149
+
150
+ async function handleBackendsSet(argv, deps) {
151
+ const body = readJsonBody(argv);
152
+ const backends = Array.isArray(body) ? body : body?.backends;
153
+ if (!Array.isArray(backends)) {
154
+ throw argsError("Body must be { \"backends\": [{ \"host\", \"backend\" }] } or a bare array");
155
+ }
156
+ const http = await buildHttp(deps);
157
+ return putBackends(http, argv, deps, backends, `Saved ${backends.length} global AI backend(s)`);
158
+ }
159
+
160
+ async function handleBackendsAdd(argv, deps) {
161
+ const http = await buildHttp(deps);
162
+ // Same normalization the server stores entries with (user-preferences.ts).
163
+ const host = String(argv.host).trim();
164
+ const backend = String(argv.backend).trim().toLowerCase();
165
+ const current = readBackends(await http.get(PREFS("global-ai-backends")));
166
+ if (current.some((entry) => entry.host === host && entry.backend === backend)) {
167
+ if (argv.json) printJson(deps.stdout, { backends: current });
168
+ else printPretty(deps.stdout, `${host} / ${backend} is already a global AI backend`);
169
+ return EXIT.OK;
170
+ }
171
+ return putBackends(http, argv, deps, [...current, { host, backend }], `Added ${host} / ${backend}`);
172
+ }
173
+
174
+ async function handleBackendsRemove(argv, deps) {
175
+ const http = await buildHttp(deps);
176
+ // Same normalization the server stores entries with (user-preferences.ts).
177
+ const host = String(argv.host).trim();
178
+ const backend = String(argv.backend).trim().toLowerCase();
179
+ const current = readBackends(await http.get(PREFS("global-ai-backends")));
180
+ const next = current.filter((entry) => !(entry.host === host && entry.backend === backend));
181
+ if (next.length === current.length) {
182
+ const err = new Error(`${host} / ${backend} is not a global AI backend`);
183
+ err.statusCode = 404;
184
+ throw err;
185
+ }
186
+ return putBackends(http, argv, deps, next, `Removed ${host} / ${backend}`);
187
+ }
188
+
189
+ // ---------------------------------------------------------------- catchphrases
190
+
191
+ function printCatchphrases(argv, deps, payload, message) {
192
+ if (argv.json) {
193
+ printJson(deps.stdout, payload);
194
+ return;
195
+ }
196
+ if (message) {
197
+ printPretty(deps.stdout, message);
198
+ return;
199
+ }
200
+ const list = Array.isArray(payload?.catchphrases) ? payload.catchphrases : [];
201
+ if (list.length === 0) {
202
+ printPretty(deps.stdout, "(no catchphrases)");
203
+ return;
204
+ }
205
+ for (const line of formatTable(["ID", "TEXT"], list.map((item) => [item.id, oneLine(item.text)]))) {
206
+ printPretty(deps.stdout, line);
207
+ }
208
+ }
209
+
210
+ async function handleCatchphrasesList(argv, deps) {
211
+ const http = await buildHttp(deps);
212
+ printCatchphrases(argv, deps, await http.get(PREFS("catchphrases")));
213
+ return EXIT.OK;
214
+ }
215
+
216
+ async function handleCatchphrasesAdd(argv, deps) {
217
+ const text = joinText(argv.text);
218
+ const http = await buildHttp(deps);
219
+ const { dryRun, data } = await sendOrPreview(http, argv, deps, "POST", PREFS("catchphrases"), { text });
220
+ if (dryRun) return EXIT.OK;
221
+ const list = Array.isArray(data?.catchphrases) ? data.catchphrases : [];
222
+ const created = [...list].reverse().find((item) => item.text === text);
223
+ printCatchphrases(argv, deps, data, `Added catchphrase${created ? ` ${created.id}` : ""}`);
224
+ return EXIT.OK;
225
+ }
226
+
227
+ async function handleCatchphrasesUpdate(argv, deps) {
228
+ const text = joinText(argv.text);
229
+ const http = await buildHttp(deps);
230
+ const { dryRun, data } = await sendOrPreview(
231
+ http, argv, deps, "PATCH", PREFS("catchphrases", argv.id), { text },
232
+ );
233
+ if (dryRun) return EXIT.OK;
234
+ printCatchphrases(argv, deps, data, `Updated catchphrase ${argv.id}`);
235
+ return EXIT.OK;
236
+ }
237
+
238
+ async function handleCatchphrasesDelete(argv, deps) {
239
+ const http = await buildHttp(deps);
240
+ const { dryRun, data } = await sendOrPreview(
241
+ http, argv, deps, "DELETE", PREFS("catchphrases", argv.id), undefined,
242
+ );
243
+ if (dryRun) return EXIT.OK;
244
+ printCatchphrases(argv, deps, data, `Deleted catchphrase ${argv.id}`);
245
+ return EXIT.OK;
246
+ }
247
+
248
+ async function handleCatchphrasesReorder(argv, deps) {
249
+ const ids = [].concat(argv.ids ?? []).map(String).filter(Boolean);
250
+ if (ids.length === 0) throw argsError("Pass the catchphrase ids in their new order");
251
+ const http = await buildHttp(deps);
252
+ const { dryRun, data } = await sendOrPreview(
253
+ http, argv, deps, "PUT", PREFS("catchphrases", "reorder"), { ids },
254
+ { note: "ids must list every catchphrase; the server rejects a partial set with 409" },
255
+ );
256
+ if (dryRun) return EXIT.OK;
257
+ printCatchphrases(argv, deps, data, `Reordered ${ids.length} catchphrase(s)`);
258
+ return EXIT.OK;
259
+ }
260
+
261
+ async function handleCatchphrasesTouch(argv, deps) {
262
+ const http = await buildHttp(deps);
263
+ const { dryRun, data } = await sendOrPreview(
264
+ http, argv, deps, "POST", PREFS("catchphrases", argv.id, "touch"), undefined,
265
+ );
266
+ if (dryRun) return EXIT.OK;
267
+ printCatchphrases(argv, deps, data, `Marked catchphrase ${argv.id} as used`);
268
+ return EXIT.OK;
269
+ }
270
+
271
+ // ---------------------------------------------------------------- daily report setting
272
+
273
+ function printDailyReportSetting(argv, deps, setting, message) {
274
+ if (argv.json) {
275
+ printJson(deps.stdout, setting);
276
+ return;
277
+ }
278
+ if (message) printPretty(deps.stdout, message);
279
+ const s = setting ?? {};
280
+ const channels = s.deliveryChannels ?? s.delivery_channels ?? [];
281
+ const rows = [
282
+ ["enabled", s.enabled],
283
+ ["timezone", s.timezone],
284
+ ["send time", s.sendTimeLocal ?? s.send_time_local],
285
+ ["channels", Array.isArray(channels) ? channels.join(", ") : channels],
286
+ ["next run", s.nextRunAt ?? s.next_run_at],
287
+ ["last run", s.lastRunAt ?? s.last_run_at],
288
+ ["last sent for", s.lastSentForDate ?? s.last_sent_for_date],
289
+ ["last error", s.lastError ?? s.last_error],
290
+ ];
291
+ for (const [label, value] of rows) {
292
+ if (value === undefined || value === null || value === "") continue;
293
+ printPretty(deps.stdout, `${pad(`${label}:`, 15)}${value}`);
294
+ }
295
+ }
296
+
297
+ async function handleDailyReportGet(argv, deps) {
298
+ const http = await buildHttp(deps);
299
+ printDailyReportSetting(argv, deps, await http.get(PREFS("daily-report")));
300
+ return EXIT.OK;
301
+ }
302
+
303
+ function parseChannels(value) {
304
+ const channels = String(value).split(",").map((entry) => entry.trim()).filter(Boolean);
305
+ const bad = channels.filter((entry) => !DELIVERY_CHANNELS.includes(entry));
306
+ if (bad.length > 0) {
307
+ throw argsError(`Unknown delivery channel(s): ${bad.join(", ")} (valid: ${DELIVERY_CHANNELS.join(", ")})`);
308
+ }
309
+ return channels;
310
+ }
311
+
312
+ async function handleDailyReportSet(argv, deps) {
313
+ const body = {
314
+ ...(argv.enabled !== undefined ? { enabled: Boolean(argv.enabled) } : {}),
315
+ ...(argv.deliveryChannels !== undefined ? { deliveryChannels: parseChannels(argv.deliveryChannels) } : {}),
316
+ };
317
+ if (Object.keys(body).length === 0) {
318
+ throw argsError("Nothing to update: pass --enabled and/or --delivery-channels");
319
+ }
320
+ const timezone = argv.timezone ? String(argv.timezone) : localTimezone();
321
+ const http = await buildHttp(deps);
322
+ const { dryRun, data } = await sendOrPreview(
323
+ http, argv, deps, "PATCH", PREFS("daily-report"), body,
324
+ {
325
+ ...(timezone ? { headers: { "X-Client-Timezone": timezone } } : {}),
326
+ ...(timezone && body.enabled !== false ? { note: `schedule timezone sent as X-Client-Timezone: ${timezone}` } : {}),
327
+ },
328
+ );
329
+ if (dryRun) return EXIT.OK;
330
+ printDailyReportSetting(argv, deps, data, "Updated daily report setting");
331
+ return EXIT.OK;
332
+ }
333
+
334
+ // ---------------------------------------------------------------- task list
335
+
336
+ function printTaskList(argv, deps, prefs, message) {
337
+ if (argv.json) {
338
+ printJson(deps.stdout, prefs);
339
+ return;
340
+ }
341
+ if (message) printPretty(deps.stdout, message);
342
+ printPretty(deps.stdout, `tasksRunningOnly: ${prefs?.tasksRunningOnly ?? prefs?.tasks_running_only ?? false}`);
343
+ }
344
+
345
+ async function handleTaskListGet(argv, deps) {
346
+ const http = await buildHttp(deps);
347
+ printTaskList(argv, deps, await http.get(PREFS("task-list")));
348
+ return EXIT.OK;
349
+ }
350
+
351
+ async function handleTaskListSet(argv, deps) {
352
+ let body;
353
+ if (argv.runningOnly !== undefined) {
354
+ if (argv.jsonBody !== undefined || argv.fromFile !== undefined) {
355
+ throw argsError("Provide either --running-only or a JSON body, not both");
356
+ }
357
+ body = { tasksRunningOnly: Boolean(argv.runningOnly) };
358
+ } else {
359
+ body = readJsonBody(argv);
360
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
361
+ throw argsError("Body must be an object like { \"tasksRunningOnly\": true }");
362
+ }
363
+ }
364
+ const http = await buildHttp(deps);
365
+ const { dryRun, data } = await sendOrPreview(http, argv, deps, "PATCH", PREFS("task-list"), body);
366
+ if (dryRun) return EXIT.OK;
367
+ printTaskList(argv, deps, data, "Updated task list preferences");
368
+ return EXIT.OK;
369
+ }
370
+
371
+ // ---------------------------------------------------------------- card groups
372
+
373
+ function makeCardGroupHandlers(resource, memberKey, noun) {
374
+ const route = PREFS(resource);
375
+
376
+ function printSnapshot(argv, deps, snapshot, message) {
377
+ if (argv.json) {
378
+ printJson(deps.stdout, snapshot);
379
+ return;
380
+ }
381
+ if (message) printPretty(deps.stdout, message);
382
+ const scopes = snapshot?.scopes && typeof snapshot.scopes === "object" ? snapshot.scopes : {};
383
+ printPretty(deps.stdout, `revision: ${snapshot?.revision ?? 0}`);
384
+ const entries = Object.entries(scopes);
385
+ if (entries.length === 0) {
386
+ printPretty(deps.stdout, "(no card groups)");
387
+ return;
388
+ }
389
+ for (const [scope, groups] of entries) {
390
+ printPretty(deps.stdout, `scope ${scope}:`);
391
+ const list = Array.isArray(groups) ? groups : [];
392
+ if (list.length === 0) printPretty(deps.stdout, " (empty)");
393
+ for (const group of list) {
394
+ const members = Array.isArray(group?.[memberKey]) ? group[memberKey] : [];
395
+ printPretty(deps.stdout, ` ${pad(group?.id ?? "", 26)} ${members.length} ${noun}s: ${members.join(", ")}`);
396
+ }
397
+ }
398
+ }
399
+
400
+ return {
401
+ async get(argv, deps) {
402
+ const http = await buildHttp(deps);
403
+ printSnapshot(argv, deps, await http.get(route));
404
+ return EXIT.OK;
405
+ },
406
+ async set(argv, deps) {
407
+ const parsed = readJsonBody(argv);
408
+ let body;
409
+ if (Array.isArray(parsed)) {
410
+ body = { scope: String(argv.scope ?? DEFAULT_CARD_GROUPS_SCOPE), groups: parsed };
411
+ } else if (parsed && typeof parsed === "object") {
412
+ body = { ...parsed };
413
+ if (argv.scope !== undefined) body.scope = String(argv.scope);
414
+ if (body.scope === undefined) body.scope = DEFAULT_CARD_GROUPS_SCOPE;
415
+ } else {
416
+ throw argsError(`Body must be { "scope", "groups": [...] } or a bare groups array`);
417
+ }
418
+ if (!Array.isArray(body.groups)) throw argsError("groups must be an array");
419
+ const http = await buildHttp(deps);
420
+ const { dryRun, data } = await sendOrPreview(http, argv, deps, "PATCH", route, body);
421
+ if (dryRun) return EXIT.OK;
422
+ printSnapshot(argv, deps, data, `Saved ${body.groups.length} ${noun} card group(s) in ${body.scope}`);
423
+ return EXIT.OK;
424
+ },
425
+ };
426
+ }
427
+
428
+ const taskCardGroups = makeCardGroupHandlers("task-card-groups", "taskIds", "task");
429
+ const projectCardGroups = makeCardGroupHandlers("project-card-groups", "projectIds", "project");
430
+
431
+ // ---------------------------------------------------------------- daily reports
432
+
433
+ function printReport(argv, deps, report, message) {
434
+ if (argv.json) {
435
+ printJson(deps.stdout, report);
436
+ return;
437
+ }
438
+ if (message) printPretty(deps.stdout, message);
439
+ const r = report ?? {};
440
+ printPretty(deps.stdout, `date: ${r.reportDate ?? r.report_date ?? ""}`);
441
+ printPretty(deps.stdout, `timezone: ${r.timezone ?? ""}`);
442
+ printPretty(deps.stdout, `status: ${r.status ?? ""}${r.persisted === false ? " (not saved)" : ""}`);
443
+ const sentAt = r.sentAt ?? r.sent_at;
444
+ if (sentAt) printPretty(deps.stdout, `sent: ${sentAt}`);
445
+ const lastError = r.lastError ?? r.last_error;
446
+ if (lastError) printPretty(deps.stdout, `error: ${lastError}`);
447
+ const summary = r.summaryMarkdown ?? r.summary_markdown;
448
+ if (summary) {
449
+ printPretty(deps.stdout, "");
450
+ printPretty(deps.stdout, String(summary).trimEnd());
451
+ }
452
+ }
453
+
454
+ function checkDate(value) {
455
+ if (value === undefined) return undefined;
456
+ const text = String(value);
457
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) throw argsError("--date must be YYYY-MM-DD");
458
+ return text;
459
+ }
460
+
461
+ async function handleReportsList(argv, deps) {
462
+ const http = await buildHttp(deps);
463
+ const result = await http.get(apiPath("daily-reports"), {
464
+ query: { list: 1, ...(argv.limit !== undefined ? { limit: argv.limit } : {}) },
465
+ });
466
+ if (argv.json) {
467
+ printJson(deps.stdout, result);
468
+ return EXIT.OK;
469
+ }
470
+ const reports = Array.isArray(result?.reports) ? result.reports : [];
471
+ if (reports.length === 0) {
472
+ printPretty(deps.stdout, "(no daily reports)");
473
+ return EXIT.OK;
474
+ }
475
+ printPretty(deps.stdout, `${pad("DATE", 11)} ${pad("STATUS", 11)} ${pad("TIMEZONE", 20)} SENT`);
476
+ for (const r of reports) {
477
+ printPretty(
478
+ deps.stdout,
479
+ `${pad(r.reportDate ?? r.report_date ?? "", 11)} ${pad(r.status ?? "", 11)} ${pad(r.timezone ?? "", 20)} ${r.sentAt ?? r.sent_at ?? "-"}`,
480
+ );
481
+ }
482
+ return EXIT.OK;
483
+ }
484
+
485
+ async function handleReportsShow(argv, deps) {
486
+ const date = checkDate(argv.date);
487
+ const http = await buildHttp(deps);
488
+ const report = await http.get(apiPath("daily-reports"), {
489
+ query: { date, timezone: argv.timezone },
490
+ });
491
+ printReport(argv, deps, report);
492
+ return EXIT.OK;
493
+ }
494
+
495
+ async function handleReportsGenerate(argv, deps) {
496
+ const date = checkDate(argv.date);
497
+ const body = {
498
+ ...(date ? { reportDate: date } : {}),
499
+ ...(argv.timezone ? { timezone: String(argv.timezone) } : {}),
500
+ };
501
+ const http = await buildHttp(deps);
502
+ const { dryRun, data } = await sendOrPreview(http, argv, deps, "POST", apiPath("daily-reports"), body);
503
+ if (dryRun) return EXIT.OK;
504
+ printReport(argv, deps, data, "Generated daily report");
505
+ return EXIT.OK;
506
+ }
507
+
508
+ // ---------------------------------------------------------------- CLI
509
+
510
+ export async function main(argvInput = hideBin(process.argv), deps = {}) {
511
+ const stdout = deps.stdout || process.stdout;
512
+ const stderr = deps.stderr || process.stderr;
513
+ const env = deps.env || process.env;
514
+ const cwd = deps.cwd || process.cwd();
515
+ const consoleErr = { error: (msg) => stderr.write(`${msg}\n`) };
516
+ const handlerDeps = { ...deps, stdout, stderr, env, cwd };
517
+ const run = (handler) => async (argv) => {
518
+ exitCode = await handler(argv, { ...handlerDeps, configFile: argv.configFile });
519
+ };
520
+ const noop = () => {};
521
+ const idArg = (cmd) => cmd.positional("id", { type: "string", demandOption: true });
522
+ const hostBackendArgs = (cmd) => cmd
523
+ .positional("host", { type: "string", demandOption: true, describe: "Daemon host name" })
524
+ .positional("backend", { type: "string", demandOption: true, describe: "AI backend (e.g. claude, codex)" });
525
+ const scopeOption = (cmd) => jsonBodyOptions(cmd)
526
+ .option("scope", { type: "string", describe: `Scope for a bare groups array (default ${DEFAULT_CARD_GROUPS_SCOPE})` });
527
+ const reportTzOption = (cmd) => cmd
528
+ .option("date", { type: "string", describe: "Report date YYYY-MM-DD (default: today)" })
529
+ .option("timezone", { type: "string", describe: "IANA timezone (default: your daily report setting)" });
530
+
531
+ let exitCode = EXIT.OK;
532
+ try {
533
+ await yargs(argvInput)
534
+ .scriptName("conductor settings")
535
+ .strict()
536
+ .help()
537
+ .option("json", { type: "boolean", default: false })
538
+ .option("dry-run", { type: "boolean", default: false })
539
+ .option("config-file", { type: "string", describe: "Path to Conductor config file" })
540
+ .command("global-backends", "Daemon x AI backends any project may run tasks on", (y) => y
541
+ .command("get", "Show global AI backends", noop, run(handleBackendsGet))
542
+ .command("set", "Replace the whole list", jsonBodyOptions, run(handleBackendsSet))
543
+ .command("add <host> <backend>", "Add one host/backend pair", hostBackendArgs, run(handleBackendsAdd))
544
+ .command("remove <host> <backend>", "Remove one host/backend pair", hostBackendArgs, run(handleBackendsRemove))
545
+ .demandCommand(1))
546
+ .command("catchphrases", "Saved chat composer phrases", (y) => y
547
+ .command("list", "List catchphrases", noop, run(handleCatchphrasesList))
548
+ .command("add <text..>", "Add a catchphrase",
549
+ (cmd) => cmd.positional("text", { type: "string", array: true }), run(handleCatchphrasesAdd))
550
+ .command("update <id> <text..>", "Change a catchphrase's text",
551
+ (cmd) => idArg(cmd).positional("text", { type: "string", array: true }), run(handleCatchphrasesUpdate))
552
+ .command("delete <id>", "Delete a catchphrase", idArg, run(handleCatchphrasesDelete))
553
+ .command("reorder <ids..>", "Set the order (list every id)",
554
+ (cmd) => cmd.positional("ids", { type: "string", array: true }), run(handleCatchphrasesReorder))
555
+ .command("touch <id>", "Mark a catchphrase as just used", idArg, run(handleCatchphrasesTouch))
556
+ .demandCommand(1))
557
+ .command("daily-report", "Daily report schedule setting", (y) => y
558
+ .command("get", "Show the daily report setting", noop, run(handleDailyReportGet))
559
+ .command("set", "Update the daily report setting", (cmd) => cmd
560
+ .option("enabled", { type: "boolean", describe: "Turn the daily report on or off" })
561
+ .option("delivery-channels", { type: "string", describe: `Comma-separated: ${DELIVERY_CHANNELS.join(",")}` })
562
+ .option("timezone", { type: "string", describe: "Schedule timezone (default: this machine's)" }),
563
+ run(handleDailyReportSet))
564
+ .demandCommand(1))
565
+ .command(["reports", "daily-reports"], "Generated daily reports", (y) => y
566
+ .command("list", "List recent daily reports",
567
+ (cmd) => cmd.option("limit", { type: "number", describe: "Max reports (server default 14, max 60)" }),
568
+ run(handleReportsList))
569
+ .command("show", "Show one day's report", reportTzOption, run(handleReportsShow))
570
+ .command("generate", "Generate (and save) a day's report", reportTzOption, run(handleReportsGenerate))
571
+ .demandCommand(1))
572
+ .command("task-list", "Task list view preferences", (y) => y
573
+ .command("get", "Show task list preferences", noop, run(handleTaskListGet))
574
+ .command("set", "Update task list preferences", (cmd) => jsonBodyOptions(cmd)
575
+ .option("running-only", { type: "boolean", describe: "Show only running tasks" }),
576
+ run(handleTaskListSet))
577
+ .demandCommand(1))
578
+ .command("task-card-groups", "Grouped task cards", (y) => y
579
+ .command("get", "Show task card groups", noop, run(taskCardGroups.get))
580
+ .command("set", "Replace the task card groups of a scope", scopeOption, run(taskCardGroups.set))
581
+ .demandCommand(1))
582
+ .command("project-card-groups", "Grouped project cards", (y) => y
583
+ .command("get", "Show project card groups", noop, run(projectCardGroups.get))
584
+ .command("set", "Replace the project card groups of a scope", scopeOption, run(projectCardGroups.set))
585
+ .demandCommand(1))
586
+ .demandCommand(1)
587
+ .fail((msg, err) => {
588
+ if (err) {
589
+ throw err;
590
+ }
591
+ stderr.write(`${msg}\n`);
592
+ exitCode = EXIT.ARGS;
593
+ })
594
+ .parseAsync();
595
+ } catch (err) {
596
+ exitCode = reportError(consoleErr, err);
597
+ }
598
+ return exitCode;
599
+ }
600
+
601
+ if (isMainModule) {
602
+ main().then((code) => {
603
+ if (code !== 0) process.exit(code);
604
+ }).catch((err) => {
605
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
606
+ process.exit(exitCodeForError(err));
607
+ });
608
+ }