@love-moon/conductor-cli 0.2.42 → 0.3.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,285 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * conductor task — entity-oriented task management.
5
+ *
6
+ * Subcommands:
7
+ * list [--project ...] [--issue <id>] [--status ...]
8
+ * show <id>
9
+ * send <id> [<message>] [--stdin] [--from-file FILE] [--metadata-json '{...}']
10
+ * messages <id> [--limit N] [--before <msg-id>]
11
+ *
12
+ * Global flags supported on every write subcommand:
13
+ * --json, --dry-run, --project, --config-file
14
+ */
15
+
16
+ import path from "node:path";
17
+ import process from "node:process";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ import yargs from "yargs/yargs";
21
+ import { hideBin } from "yargs/helpers";
22
+
23
+ import {
24
+ EXIT,
25
+ buildApis,
26
+ buildAuditMetadata,
27
+ emitDryRun,
28
+ exitCodeForError,
29
+ makeDryRunPayload,
30
+ pad,
31
+ printJson,
32
+ printPretty,
33
+ readMessageInput,
34
+ reportError,
35
+ resolveProject,
36
+ } from "../src/entity-helpers.js";
37
+
38
+ const isMainModule = (() => {
39
+ const currentFile = fileURLToPath(import.meta.url);
40
+ const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
41
+ return entryFile === currentFile;
42
+ })();
43
+
44
+ function buildBaseUrl(config) {
45
+ const raw = (config?.backendUrl || "").replace(/\/+$/, "");
46
+ return raw || "http://localhost";
47
+ }
48
+
49
+ function taskAsObject(task) {
50
+ if (!task) return null;
51
+ if (typeof task.asObject === "function") return task.asObject();
52
+ return {
53
+ id: task.id,
54
+ projectId: task.projectId,
55
+ issueId: task.issueId ?? null,
56
+ title: task.title,
57
+ status: task.status,
58
+ backendType: task.backendType,
59
+ sessionId: task.sessionId,
60
+ createdAt: task.createdAt,
61
+ updatedAt: task.updatedAt,
62
+ };
63
+ }
64
+
65
+ function parseStatusList(value) {
66
+ if (!value) return undefined;
67
+ return String(value)
68
+ .split(",")
69
+ .map((entry) => entry.trim())
70
+ .filter(Boolean);
71
+ }
72
+
73
+ function parseMetadataJson(value) {
74
+ if (!value) return undefined;
75
+ try {
76
+ const parsed = JSON.parse(String(value));
77
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
78
+ throw new Error("must be a JSON object");
79
+ }
80
+ return parsed;
81
+ } catch (err) {
82
+ const e = new Error(`Invalid --metadata-json: ${err instanceof Error ? err.message : String(err)}`);
83
+ e.code = "ARGS";
84
+ throw e;
85
+ }
86
+ }
87
+
88
+ async function handleList(argv, deps) {
89
+ const apis = await buildApis(deps);
90
+ const project = await resolveProject(apis, { env: deps.env, cwd: deps.cwd, project: argv.project });
91
+ const list = await apis.tasks.listTasks({
92
+ projectId: project.id,
93
+ issueId: argv.issue ? String(argv.issue) : undefined,
94
+ status: parseStatusList(argv.status),
95
+ });
96
+ const objects = (Array.isArray(list) ? list : []).map(taskAsObject);
97
+ if (argv.json) {
98
+ printJson(deps.stdout, objects);
99
+ return EXIT.OK;
100
+ }
101
+ if (objects.length === 0) {
102
+ printPretty(deps.stdout, "(no tasks)");
103
+ return EXIT.OK;
104
+ }
105
+ printPretty(deps.stdout, `${pad("ID", 24)} ${pad("STATUS", 12)} TITLE`);
106
+ for (const task of objects) {
107
+ printPretty(
108
+ deps.stdout,
109
+ `${pad(task.id, 24)} ${pad(task.status, 12)} ${task.title ?? ""}`,
110
+ );
111
+ }
112
+ return EXIT.OK;
113
+ }
114
+
115
+ async function handleShow(argv, deps) {
116
+ const apis = await buildApis(deps);
117
+ const task = await apis.tasks.getTask(argv.id);
118
+ if (!task) {
119
+ const err = new Error(`Task not found: ${argv.id}`);
120
+ err.statusCode = 404;
121
+ throw err;
122
+ }
123
+ const obj = taskAsObject(task);
124
+ if (argv.json) {
125
+ printJson(deps.stdout, obj);
126
+ return EXIT.OK;
127
+ }
128
+ for (const [key, value] of Object.entries(obj)) {
129
+ if (value === undefined || value === null) continue;
130
+ if (typeof value === "object") {
131
+ printPretty(deps.stdout, `${key}: ${JSON.stringify(value)}`);
132
+ } else {
133
+ printPretty(deps.stdout, `${key}: ${value}`);
134
+ }
135
+ }
136
+ return EXIT.OK;
137
+ }
138
+
139
+ async function handleSend(argv, deps) {
140
+ const apis = await buildApis(deps);
141
+ const content = readMessageInput({
142
+ positional: argv.message,
143
+ fromFile: argv.fromFile,
144
+ useStdin: Boolean(argv.stdin),
145
+ stdin: deps.stdin,
146
+ });
147
+ const extraMetadata = parseMetadataJson(argv.metadataJson);
148
+ // `buildAuditMetadata(env, extra)` namespaces `audit.*` so that user-supplied
149
+ // `--metadata-json '{"actor":"system"}'` cannot spoof CLI audit fields
150
+ // (review H1). The CLI's `actor: "cli"` always wins inside `audit`.
151
+ const metadata = buildAuditMetadata(deps.env, extraMetadata || {});
152
+ const body = {
153
+ role: "user",
154
+ content,
155
+ metadata,
156
+ };
157
+ if (argv.dryRun) {
158
+ emitDryRun(
159
+ deps.stdout,
160
+ argv.json,
161
+ makeDryRunPayload(
162
+ "POST",
163
+ `${buildBaseUrl(apis.config)}/api/tasks/${encodeURIComponent(argv.id)}/messages`,
164
+ body,
165
+ ),
166
+ );
167
+ return EXIT.OK;
168
+ }
169
+ // SDK signature is `sendTaskMessage(taskId, content, options?)`. Earlier we
170
+ // were passing the body object as the second arg, which caused the SDK's
171
+ // `typeof content === 'string'` guard to throw at runtime (review B1).
172
+ const result = await apis.tasks.sendTaskMessage(argv.id, content, {
173
+ role: body.role,
174
+ metadata: body.metadata,
175
+ });
176
+ if (argv.json) {
177
+ printJson(deps.stdout, result ?? { sent: true });
178
+ return EXIT.OK;
179
+ }
180
+ const id = result?.id ? `${result.id} ` : "";
181
+ printPretty(deps.stdout, `Sent message ${id}to task ${argv.id}`);
182
+ return EXIT.OK;
183
+ }
184
+
185
+ async function handleMessages(argv, deps) {
186
+ const apis = await buildApis(deps);
187
+ const list = await apis.tasks.listTaskMessages(argv.id, {
188
+ limit: argv.limit ? Number(argv.limit) : undefined,
189
+ before: argv.before ? String(argv.before) : undefined,
190
+ });
191
+ if (argv.json) {
192
+ printJson(deps.stdout, Array.isArray(list) ? list : []);
193
+ return EXIT.OK;
194
+ }
195
+ for (const msg of Array.isArray(list) ? list : []) {
196
+ const role = msg.role || "msg";
197
+ const content = (msg.content || "").toString();
198
+ printPretty(deps.stdout, `[${role}] ${content}`);
199
+ }
200
+ return EXIT.OK;
201
+ }
202
+
203
+ export async function main(argvInput = hideBin(process.argv), deps = {}) {
204
+ const stdout = deps.stdout || process.stdout;
205
+ const stderr = deps.stderr || process.stderr;
206
+ const env = deps.env || process.env;
207
+ const cwd = deps.cwd || process.cwd();
208
+ const consoleErr = { error: (msg) => stderr.write(`${msg}\n`) };
209
+ const handlerDeps = { ...deps, stdout, stderr, env, cwd };
210
+
211
+ let exitCode = EXIT.OK;
212
+ try {
213
+ await yargs(argvInput)
214
+ .scriptName("conductor task")
215
+ .strict()
216
+ .help()
217
+ .option("json", { type: "boolean", default: false })
218
+ .option("dry-run", { type: "boolean", default: false })
219
+ .option("project", { type: "string", describe: "Project id or name override" })
220
+ .option("config-file", { type: "string", describe: "Path to Conductor config file" })
221
+ .command(
222
+ "list",
223
+ "List tasks in a project",
224
+ (cmd) => cmd
225
+ .option("issue", { type: "string", describe: "Filter by linked issue id" })
226
+ .option("status", { type: "string", describe: "Comma-separated status filter" }),
227
+ async (argv) => {
228
+ exitCode = await handleList(argv, { ...handlerDeps, configFile: argv.configFile });
229
+ },
230
+ )
231
+ .command(
232
+ "show <id>",
233
+ "Show one task's detail",
234
+ (cmd) => cmd.positional("id", { type: "string", demandOption: true }),
235
+ async (argv) => {
236
+ exitCode = await handleShow(argv, { ...handlerDeps, configFile: argv.configFile });
237
+ },
238
+ )
239
+ .command(
240
+ "send <id> [message]",
241
+ "Send a user message into a running task",
242
+ (cmd) => cmd
243
+ .positional("id", { type: "string", demandOption: true })
244
+ .positional("message", { type: "string" })
245
+ .option("stdin", { type: "boolean", default: false })
246
+ .option("from-file", { type: "string" })
247
+ .option("metadata-json", { type: "string", describe: "Extra JSON metadata to merge into the message" }),
248
+ async (argv) => {
249
+ exitCode = await handleSend(argv, { ...handlerDeps, configFile: argv.configFile });
250
+ },
251
+ )
252
+ .command(
253
+ "messages <id>",
254
+ "Pull a slice of task messages and exit (no --follow in this RFC)",
255
+ (cmd) => cmd
256
+ .positional("id", { type: "string", demandOption: true })
257
+ .option("limit", { type: "number" })
258
+ .option("before", { type: "string", describe: "Cursor: message id to paginate before" }),
259
+ async (argv) => {
260
+ exitCode = await handleMessages(argv, { ...handlerDeps, configFile: argv.configFile });
261
+ },
262
+ )
263
+ .demandCommand(1)
264
+ .fail((msg, err) => {
265
+ if (err) {
266
+ throw err;
267
+ }
268
+ stderr.write(`${msg}\n`);
269
+ exitCode = EXIT.ARGS;
270
+ })
271
+ .parseAsync();
272
+ } catch (err) {
273
+ exitCode = reportError(consoleErr, err);
274
+ }
275
+ return exitCode;
276
+ }
277
+
278
+ if (isMainModule) {
279
+ main().then((code) => {
280
+ if (code !== 0) process.exit(code);
281
+ }).catch((err) => {
282
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
283
+ process.exit(exitCodeForError(err));
284
+ });
285
+ }
package/bin/conductor.js CHANGED
@@ -12,6 +12,9 @@
12
12
  * send-file - Upload a local file into a task session
13
13
  * channel - Connect user-owned chat channel providers
14
14
  * serve-ai - Start an OpenAI-compatible local AI server
15
+ * project - Manage Conductor projects (list/show/create/...)
16
+ * issue - Manage issues (list/show/create/update/start/done)
17
+ * task - Manage tasks (list/show/send/messages)
15
18
  */
16
19
 
17
20
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -35,7 +38,19 @@ export function runConductorCli(args = argv, deps = {}) {
35
38
  const processArgv = deps.processArgv || process.argv;
36
39
  const fsExistsSync = deps.existsSync || fs.existsSync;
37
40
  const checkForUpdates = deps.maybeCheckForUpdates || maybeCheckForUpdates;
38
- const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file", "channel", "serve-ai"];
41
+ const validSubcommands = [
42
+ "fire",
43
+ "daemon",
44
+ "config",
45
+ "update",
46
+ "diagnose",
47
+ "send-file",
48
+ "channel",
49
+ "serve-ai",
50
+ "project",
51
+ "issue",
52
+ "task",
53
+ ];
39
54
 
40
55
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
41
56
  showHelp(consoleImpl);
@@ -117,6 +132,9 @@ Subcommands:
117
132
  send-file Upload a local file into a task session
118
133
  channel Connect user-owned chat channel providers
119
134
  serve-ai Start an OpenAI-compatible local AI server
135
+ project Manage Conductor projects (list/show/create/...)
136
+ issue Manage issues (list/show/create/update/start/done)
137
+ task Manage tasks (list/show/send/messages)
120
138
 
121
139
  Options:
122
140
  -h, --help Show this help message
@@ -132,6 +150,9 @@ Examples:
132
150
  conductor serve-ai --port 8787
133
151
  conductor config
134
152
  conductor update
153
+ conductor project list
154
+ conductor issue create --title "Refactor module" --priority P2
155
+ conductor task send <task-id> "please add a unit test"
135
156
 
136
157
  For subcommand-specific help:
137
158
  conductor fire --help
@@ -142,6 +163,9 @@ For subcommand-specific help:
142
163
  conductor send-file --help
143
164
  conductor channel --help
144
165
  conductor serve-ai --help
166
+ conductor project --help
167
+ conductor issue --help
168
+ conductor task --help
145
169
 
146
170
  Version: ${pkgJson.version}
147
171
  `);
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.42",
4
- "gitCommitId": "f79f36f",
3
+ "version": "0.3.0",
4
+ "gitCommitId": "b9f33cd",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/lovemoon-ai/conductor.git"
8
+ },
5
9
  "type": "module",
6
10
  "bin": {
7
11
  "conductor": "bin/conductor.js"
@@ -11,7 +15,8 @@
11
15
  "src"
12
16
  ],
13
17
  "publishConfig": {
14
- "access": "public"
18
+ "access": "public",
19
+ "provenance": true
15
20
  },
16
21
  "scripts": {
17
22
  "test": "node --test test/*.test.js"
@@ -19,7 +24,7 @@
19
24
  "dependencies": {
20
25
  "@love-moon/ai-manager": "0.2.42",
21
26
  "@love-moon/ai-sdk": "0.2.42",
22
- "@love-moon/conductor-sdk": "0.2.42",
27
+ "@love-moon/conductor-sdk": "0.3.0",
23
28
  "@github/copilot-sdk": "^0.2.2",
24
29
  "chrome-launcher": "^1.2.1",
25
30
  "chrome-remote-interface": "^0.33.0",
@@ -78,7 +78,23 @@ export function createAiManagerHandlers(opts = {}) {
78
78
  }
79
79
 
80
80
  async function listAccounts() {
81
- return { accounts: await manager.listCodexAccounts() };
81
+ const accounts = await manager.listCodexAccounts();
82
+ // Enrich each account with its on-disk cached quota so the web UI can
83
+ // restore inactive-account snapshots across page refreshes. The cache is
84
+ // read without any network call, so missing/empty caches just leave the
85
+ // field undefined and cost effectively nothing. We swallow per-account
86
+ // errors because one broken auth.json shouldn't fail the whole list.
87
+ const enriched = await Promise.all(
88
+ accounts.map(async (acct) => {
89
+ try {
90
+ const cachedQuota = await manager.readCachedCodexQuota(acct.path);
91
+ return cachedQuota ? { ...acct, cachedQuota } : acct;
92
+ } catch {
93
+ return acct;
94
+ }
95
+ }),
96
+ );
97
+ return { accounts: enriched };
82
98
  }
83
99
 
84
100
  async function switchAccount(args = {}) {