@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.
- package/bin/conductor-fire.js +21 -3
- package/bin/conductor-issue.js +357 -0
- package/bin/conductor-project.js +436 -0
- package/bin/conductor-task.js +285 -0
- package/bin/conductor.js +25 -1
- package/package.json +9 -4
- package/src/ai-manager-handlers.js +17 -1
- package/src/daemon.js +795 -35
- package/src/entity-helpers.js +345 -0
- package/src/fire/resume.js +113 -870
- package/src/runtime-backends.js +48 -8
|
@@ -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 = [
|
|
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.
|
|
4
|
-
"gitCommitId": "
|
|
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.
|
|
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
|
-
|
|
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 = {}) {
|