mcoda 0.1.76 → 0.1.79

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
@@ -4,6 +4,10 @@
4
4
  - Initial npm packaging scaffold for the mcoda CLI.
5
5
  - Added bundled mswarm consent terms plus guided `mcoda setup`/postinstall consent bootstrap for installed CLI packages.
6
6
 
7
+ ## 0.1.78
8
+ - Added owner-local GPU job commands: `mcoda gpu list`, `mcoda gpu ops`, and GPU-aware `mcoda job artifact upload|run|status|logs|events|artifacts|cancel|retry`.
9
+ - Added README and usage docs for the generic GPU job connection flags and environment fallbacks.
10
+
7
11
  ## 0.1.76
8
12
  - Added top-level help aliases: `mcoda help`, `mcoda --help`, `mcoda -h`, and `mcoda -H`.
9
13
  - Release v0.1.76.
package/README.md CHANGED
@@ -34,6 +34,8 @@ mcoda docs pdr generate --workspace-root . --project WEB --rfp-path docs/rfp/web
34
34
  - Execution: `mcoda add-tests`, `mcoda work-on-tasks`, `mcoda code-review`, `mcoda qa-tasks`
35
35
  - Backlog: `mcoda backlog`, `mcoda task`
36
36
  - Jobs/telemetry: `mcoda jobs`, `mcoda tokens`, `mcoda telemetry`
37
+ - Owner-local GPU jobs: `mcoda gpu list`, `mcoda gpu ops`, `mcoda job artifact upload|run|status|logs|events|artifacts|cancel|retry`
38
+ - Self-hosted agents: `mcoda self-hosted agent list`, `mcoda self-hosted agent details`, `mcoda self-hosted agent sync`
37
39
  - Agents: `mcoda test-agent`, `mcoda agent-run`
38
40
  - Updates: `mcoda update --check`
39
41
 
@@ -45,9 +47,22 @@ If that sufficiency pass errors, create-tasks continues (fail-open) and records
45
47
  Environment variables are optional overrides for workspace settings:
46
48
  - `MCODA_DOCDEX_URL` to point at a docdex server.
47
49
  - `MCODA_API_BASE_URL` or `MCODA_JOBS_API_URL` for job APIs.
50
+ - `MCODA_MSWARM_NODE_BASE_URL`, `MCODA_MSWARM_NODE_ID`, and `MCODA_MSWARM_NODE_SIGNING_SECRET` for owner-local generic GPU job commands.
48
51
  - `MCODA_TELEMETRY` set to `off` to disable telemetry.
49
52
  - `MCODA_STREAM_IO=1` to emit agent I/O lines to stderr.
50
53
 
54
+ ## Self-hosted mswarm agents
55
+
56
+ `mcoda self-hosted agent list` reads direct self-hosted agents and opt-in
57
+ load-balanced aliases from mswarm when the configured API key allows it. Direct
58
+ entries stay pinned to one server. Load-balanced aliases are saved as auto
59
+ routes and let mswarm choose an eligible upgraded node for the requested model
60
+ or capability.
61
+
62
+ Use direct slugs for rollback or pinned-server workloads. Use auto aliases only
63
+ when the product should allow mswarm to route around busy, drained, stale, or
64
+ incompatible nodes.
65
+
51
66
  ## Programmatic usage
52
67
  ```ts
53
68
  import { McodaEntrypoint } from "mcoda";
@@ -1 +1 @@
1
- {"version":3,"file":"McodaEntrypoint.d.ts","sourceRoot":"","sources":["../../src/bin/McodaEntrypoint.ts"],"names":[],"mappings":";AAuDA,qBAAa,eAAe;WACb,GAAG,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;CAoPxE"}
1
+ {"version":3,"file":"McodaEntrypoint.d.ts","sourceRoot":"","sources":["../../src/bin/McodaEntrypoint.ts"],"names":[],"mappings":";AAyDA,qBAAa,eAAe;WACb,GAAG,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC;CAwPxE"}
@@ -6,6 +6,7 @@ import { AgentsCommands } from '../commands/agents/AgentsCommands.js';
6
6
  import { CloudCommands } from '../commands/cloud/CloudCommands.js';
7
7
  import { SelfHostedCommands } from '../commands/self-hosted/SelfHostedCommands.js';
8
8
  import { WorkersCommands } from '../commands/workers/WorkersCommands.js';
9
+ import { GpuCommands } from '../commands/gpu/GpuCommands.js';
9
10
  import { ConfigCommands } from '../commands/config/ConfigCommands.js';
10
11
  import { ConsentCommands } from '../commands/consent/ConsentCommands.js';
11
12
  import { GatewayAgentCommand } from '../commands/agents/GatewayAgentCommand.js';
@@ -35,7 +36,7 @@ import { AgentRunCommand } from '../commands/agents/AgentRunCommand.js';
35
36
  import { SetWorkspaceCommand } from '../commands/workspace/SetWorkspaceCommand.js';
36
37
  import { ProjectGuidanceCommand } from '../commands/workspace/ProjectGuidanceCommand.js';
37
38
  import { MswarmConfigStore } from '@mcoda/core';
38
- const TOP_LEVEL_USAGE = 'Usage: mcoda <agent|cloud|cloud-agent|self-hosted|self-hosted-agent|workers|worker|config|consent|setup|gateway-agent|test-agent|agent-run|routing|docs|openapi|job|jobs|tokens|telemetry|create-tasks|migrate-tasks|refine-tasks|task-sufficiency-audit|sds-preflight|order-tasks|tasks|add-tests|work-on-tasks|gateway-trio|code-review|qa-tasks|backlog|task|task-detail|estimate|update|set-workspace|project-guidance|pdr|sds> [...args]\n' +
39
+ const TOP_LEVEL_USAGE = 'Usage: mcoda <agent|cloud|cloud-agent|self-hosted|self-hosted-agent|workers|worker|gpu|config|consent|setup|gateway-agent|test-agent|agent-run|routing|docs|openapi|job|jobs|tokens|telemetry|create-tasks|migrate-tasks|refine-tasks|task-sufficiency-audit|sds-preflight|order-tasks|tasks|add-tests|work-on-tasks|gateway-trio|code-review|qa-tasks|backlog|task|task-detail|estimate|update|set-workspace|project-guidance|pdr|sds> [...args]\n' +
39
40
  'Setup: use `mcoda setup` after installation (or accept the postinstall prompt) to complete the mandatory mswarm telemetry consent flow.\n' +
40
41
  'Config: use `mcoda config set mswarm-api-key <KEY>` to persist an encrypted mswarm API key in the resolved global mcoda config file.\n' +
41
42
  'Consent: use `mcoda consent accept` before other commands if you need to complete consent outside the guided setup flow.\n' +
@@ -43,10 +44,11 @@ const TOP_LEVEL_USAGE = 'Usage: mcoda <agent|cloud|cloud-agent|self-hosted|self-
43
44
  'Cloud agents: use `mcoda cloud agent list|details|sync` to discover and materialize mswarm-managed remote agents.\n' +
44
45
  'Self-hosted agents: use `mcoda self-hosted agent list|details|sync` to discover and materialize owner-hosted mswarm agents.\n' +
45
46
  'Workers: use `mcoda workers list|details|sync|run` to discover, materialize, and invoke mswarm Workers.\n' +
47
+ 'GPU jobs: use `mcoda gpu list` and `mcoda job artifact upload|run|status|logs|events|artifacts|cancel` for owner-local generic GPU jobs.\n' +
46
48
  'Expose this machine: install `@mcoda/mswarm`, then run `mswarm install <KEY>`.\n' +
47
49
  'Aliases: `tasks order-by-deps` forwards to `order-tasks` (dependency-aware ordering), `task`/`task-detail` show a single task.\n' +
48
50
  'Help: use `mcoda help`, `mcoda --help`, `mcoda -h`, or `mcoda -H` for this overview.\n' +
49
- 'Job commands (mcoda job --help for details): list|status|watch|logs|inspect|resume|cancel|tokens\n' +
51
+ 'Job commands (mcoda job --help for details): list|status|watch|logs|inspect|resume|cancel|tokens plus GPU job artifact|run|events|artifacts with node options\n' +
50
52
  'Jobs API required for job commands (set MCODA_API_BASE_URL/MCODA_JOBS_API_URL or workspace api.baseUrl). status/watch/logs exit non-zero on failed/cancelled jobs per SDS.';
51
53
  const TOP_LEVEL_HELP_COMMANDS = new Set(['--help', '-h', '-H', 'help']);
52
54
  export class McodaEntrypoint {
@@ -135,6 +137,10 @@ export class McodaEntrypoint {
135
137
  await WorkersCommands.run(rest);
136
138
  return;
137
139
  }
140
+ if (command === 'gpu') {
141
+ await GpuCommands.run(rest);
142
+ return;
143
+ }
138
144
  if (command === 'config') {
139
145
  await ConfigCommands.run(rest);
140
146
  return;
@@ -0,0 +1,8 @@
1
+ export declare class GpuCommands {
2
+ static run(argv: string[]): Promise<void>;
3
+ }
4
+ export declare class GpuJobCommands {
5
+ static shouldHandle(argv: string[]): boolean;
6
+ static run(argv: string[]): Promise<void>;
7
+ }
8
+ //# sourceMappingURL=GpuCommands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GpuCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/gpu/GpuCommands.ts"],"names":[],"mappings":"AAoSA,qBAAa,WAAW;WACT,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAiChD;AAED,qBAAa,cAAc;IACzB,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO;WAQ/B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CA8FhD"}
@@ -0,0 +1,382 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { basename } from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+ import YAML from "yaml";
5
+ import { MswarmApi } from "@mcoda/core";
6
+ const GPU_USAGE = `
7
+ Usage: mcoda gpu <list|ops> [options]
8
+
9
+ Commands:
10
+ gpu list List owner-local GPU/job capability projection
11
+ gpu ops Show owner-local generic job queue, usage, quota, and audit summary
12
+
13
+ Connection options:
14
+ --node-base-url <URL> Owner-local node URL (or MCODA_MSWARM_NODE_BASE_URL)
15
+ --node-id <ID> Node id for signing capability requests
16
+ --signing-secret <KEY> Owner-local generic job signing secret
17
+ --token <TOKEN> Pre-signed capability or ops token
18
+ --timeout-ms <N> Request timeout
19
+ --audit-limit <N> Audit rows for gpu ops (default 50, max 250)
20
+ --audit-offset <N> Audit row offset for gpu ops
21
+ --json Emit JSON
22
+ `.trim();
23
+ const JOB_USAGE = `
24
+ Usage: mcoda job <artifact|run|status|logs|events|artifacts|cancel|retry> [options]
25
+
26
+ GPU job commands:
27
+ job artifact upload <FILE>
28
+ --job-id <ID> --request-id <ID> --node-id <ID> --job-type <TYPE>
29
+ [--artifact-path <PATH>] [--artifact-name <NAME>] [--content-type <TYPE>]
30
+ job run --job-file <FILE> [--wait] [--json]
31
+ job run --type <TYPE> --payload-file <FILE> [--wait] [--json]
32
+ job status <JOB_ID> [--job-file <FILE> | --node-id ... --request-id ... --job-type ...]
33
+ job logs <JOB_ID>
34
+ job events <JOB_ID>
35
+ job artifacts <JOB_ID>
36
+ job cancel <JOB_ID>
37
+ job retry <JOB_ID>
38
+
39
+ Connection options:
40
+ --node-base-url <URL> Owner-local node URL (or MCODA_MSWARM_NODE_BASE_URL)
41
+ --node-id <ID> Node id for signing requests
42
+ --signing-secret <KEY> Owner-local generic job signing secret
43
+ --token <TOKEN> Pre-signed generic job token
44
+ --timeout-ms <N> Request timeout
45
+ --json Emit JSON
46
+ `.trim();
47
+ const GPU_JOB_SUBCOMMANDS = new Set(["artifact", "run", "events", "artifacts"]);
48
+ const SHARED_JOB_SUBCOMMANDS = new Set(["status", "logs", "cancel", "retry"]);
49
+ const GPU_JOB_FLAGS = new Set([
50
+ "--gpu",
51
+ "--node-base-url",
52
+ "--node-id",
53
+ "--signing-secret",
54
+ "--token",
55
+ "--job-file",
56
+ "--payload-file",
57
+ "--job-type",
58
+ "--type",
59
+ "--schema-version",
60
+ "--request-id"
61
+ ]);
62
+ const TERMINAL_STATES = new Set(["succeeded", "failed", "cancelled", "expired", "blocked"]);
63
+ const parseArgs = (argv) => {
64
+ const flags = {};
65
+ const positionals = [];
66
+ for (let index = 0; index < argv.length; index += 1) {
67
+ const arg = argv[index];
68
+ if (arg.startsWith("--")) {
69
+ const key = arg.replace(/^--/, "");
70
+ const next = argv[index + 1];
71
+ if (next && !next.startsWith("--")) {
72
+ const current = flags[key];
73
+ if (current === undefined)
74
+ flags[key] = next;
75
+ else if (Array.isArray(current))
76
+ flags[key] = [...current, next];
77
+ else if (typeof current === "string")
78
+ flags[key] = [current, next];
79
+ else
80
+ flags[key] = [next];
81
+ index += 1;
82
+ }
83
+ else {
84
+ flags[key] = true;
85
+ }
86
+ continue;
87
+ }
88
+ positionals.push(arg);
89
+ }
90
+ return { flags, positionals };
91
+ };
92
+ const resolveString = (value) => {
93
+ if (value === undefined || typeof value === "boolean")
94
+ return undefined;
95
+ return Array.isArray(value) ? value[value.length - 1] : value;
96
+ };
97
+ const resolvePositiveInt = (value, label) => {
98
+ const raw = resolveString(value);
99
+ if (raw === undefined)
100
+ return undefined;
101
+ const parsed = Number.parseInt(raw, 10);
102
+ if (!Number.isFinite(parsed) || parsed <= 0) {
103
+ throw new Error(`Invalid ${label}; expected a positive integer`);
104
+ }
105
+ return parsed;
106
+ };
107
+ const resolveNonNegativeInt = (value, label) => {
108
+ const raw = resolveString(value);
109
+ if (raw === undefined)
110
+ return undefined;
111
+ const parsed = Number.parseInt(raw, 10);
112
+ if (!Number.isFinite(parsed) || parsed < 0) {
113
+ throw new Error(`Invalid ${label}; expected a non-negative integer`);
114
+ }
115
+ return parsed;
116
+ };
117
+ const nodeBaseUrl = (parsed) => resolveString(parsed.flags["node-base-url"]) || process.env.MCODA_MSWARM_NODE_BASE_URL;
118
+ const nodeId = (parsed) => resolveString(parsed.flags["node-id"]) || process.env.MCODA_MSWARM_NODE_ID || process.env.MSWARM_SELF_HOSTED_NODE_ID;
119
+ const signingSecret = (parsed) => resolveString(parsed.flags["signing-secret"]) ||
120
+ process.env.MCODA_MSWARM_NODE_SIGNING_SECRET ||
121
+ process.env.MSWARM_SELF_HOSTED_INVOCATION_SIGNING_SECRET;
122
+ const baseAuth = (parsed) => ({
123
+ nodeBaseUrl: nodeBaseUrl(parsed),
124
+ token: resolveString(parsed.flags.token),
125
+ signingSecret: signingSecret(parsed),
126
+ tokenTtlSeconds: resolvePositiveInt(parsed.flags["token-ttl-seconds"], "--token-ttl-seconds")
127
+ });
128
+ const createApi = async (parsed) => MswarmApi.create({
129
+ baseUrl: nodeBaseUrl(parsed),
130
+ apiKey: resolveString(parsed.flags["api-key"]),
131
+ timeoutMs: resolvePositiveInt(parsed.flags["timeout-ms"], "--timeout-ms")
132
+ });
133
+ const readStructuredFile = async (file) => {
134
+ const raw = await readFile(file, "utf8");
135
+ if (file.endsWith(".yaml") || file.endsWith(".yml")) {
136
+ return YAML.parse(raw);
137
+ }
138
+ return JSON.parse(raw);
139
+ };
140
+ const asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
141
+ ? value
142
+ : {};
143
+ const readJobEnvelope = async (parsed) => {
144
+ const file = resolveString(parsed.flags["job-file"]) || resolveString(parsed.flags["payload-file"]);
145
+ if (!file) {
146
+ throw new Error("GPU job run requires --job-file or --payload-file");
147
+ }
148
+ const payload = asRecord(await readStructuredFile(file));
149
+ if (typeof payload.job_id === "string" && typeof payload.request_id === "string" && typeof payload.node_id === "string") {
150
+ return payload;
151
+ }
152
+ const schemaVersion = resolveString(parsed.flags["schema-version"]) ||
153
+ (typeof payload.schema_version === "string" ? payload.schema_version : "2026-06-14");
154
+ const jobType = resolveString(parsed.flags.type) ||
155
+ resolveString(parsed.flags["job-type"]) ||
156
+ (typeof payload.job_type === "string" ? payload.job_type : undefined);
157
+ if (!jobType) {
158
+ throw new Error("--job-file/--payload-file must contain job_type or use --type/--job-type");
159
+ }
160
+ return {
161
+ job_id: resolveString(parsed.flags["job-id"]) || `job-${randomUUID()}`,
162
+ request_id: resolveString(parsed.flags["request-id"]) || `req-${randomUUID()}`,
163
+ node_id: requireText(nodeId(parsed), "--node-id"),
164
+ job: {
165
+ ...payload,
166
+ schema_version: schemaVersion,
167
+ job_type: jobType
168
+ }
169
+ };
170
+ };
171
+ const referenceFromArgs = async (parsed, explicitJobId) => {
172
+ const jobFile = resolveString(parsed.flags["job-file"]) || resolveString(parsed.flags["payload-file"]);
173
+ if (jobFile) {
174
+ const job = await readJobEnvelope(parsed);
175
+ return {
176
+ ...baseAuth(parsed),
177
+ jobId: explicitJobId || job.job_id,
178
+ nodeId: job.node_id,
179
+ requestId: job.request_id,
180
+ schemaVersion: job.job.schema_version,
181
+ jobType: job.job.job_type
182
+ };
183
+ }
184
+ return {
185
+ ...baseAuth(parsed),
186
+ jobId: requireText(explicitJobId || resolveString(parsed.flags["job-id"]), "JOB_ID"),
187
+ nodeId: nodeId(parsed),
188
+ requestId: resolveString(parsed.flags["request-id"]),
189
+ schemaVersion: resolveString(parsed.flags["schema-version"]) || "2026-06-14",
190
+ jobType: resolveString(parsed.flags["job-type"]) || resolveString(parsed.flags.type)
191
+ };
192
+ };
193
+ function requireText(value, label) {
194
+ if (!value || !value.trim()) {
195
+ throw new Error(`${label} is required`);
196
+ }
197
+ return value.trim();
198
+ }
199
+ const printJsonOrValue = (value, json) => {
200
+ // eslint-disable-next-line no-console
201
+ console.log(json ? JSON.stringify(value, null, 2) : formatValue(value));
202
+ };
203
+ const formatValue = (value) => {
204
+ if (typeof value === "string")
205
+ return value;
206
+ return JSON.stringify(value, null, 2);
207
+ };
208
+ const printSnapshot = (snapshot, json) => {
209
+ if (json) {
210
+ printJsonOrValue(snapshot, true);
211
+ return;
212
+ }
213
+ const job = snapshot.job;
214
+ // eslint-disable-next-line no-console
215
+ console.log(`${job.job_id} ${job.state}`);
216
+ if (job.backpressure?.message) {
217
+ // eslint-disable-next-line no-console
218
+ console.log(job.backpressure.message);
219
+ }
220
+ if (job.result?.error?.message) {
221
+ // eslint-disable-next-line no-console
222
+ console.log(job.result.error.message);
223
+ }
224
+ };
225
+ const formatOpsSummary = (summary) => {
226
+ const lines = [
227
+ `${summary.node.node_id} ${summary.node.generic_jobs_enabled ? "generic-jobs-enabled" : "generic-jobs-disabled"}`,
228
+ `queue active=${summary.queue.active_jobs} queued=${summary.queue.queued_jobs} terminal=${summary.queue.terminal_jobs}`,
229
+ `quota available=${summary.quota.available_slots}/${summary.quota.max_concurrent_jobs} production_enforced=${summary.quota.production_enforced}`,
230
+ `usage total=${summary.usage.total_jobs} succeeded=${summary.usage.succeeded_jobs} failed=${summary.usage.failed_jobs} cancelled=${summary.usage.cancelled_jobs} blocked=${summary.usage.blocked_jobs} gpu_seconds=${summary.usage.gpu_seconds}`,
231
+ ];
232
+ if (summary.queue.jobs.length) {
233
+ lines.push("jobs:");
234
+ for (const job of summary.queue.jobs.slice(0, 10)) {
235
+ lines.push(` ${job.job_id} ${job.state} ${job.job_type} tenant=${job.tenant_id}`);
236
+ }
237
+ }
238
+ if (summary.audit.events.length) {
239
+ lines.push(`audit offset=${summary.audit.offset} limit=${summary.audit.limit} total=${summary.audit.total}:`);
240
+ for (const event of summary.audit.events.slice(0, 10)) {
241
+ lines.push(` ${event.timestamp} ${event.action} ${event.job_id}`);
242
+ }
243
+ }
244
+ return lines.join("\n");
245
+ };
246
+ const waitForJob = async (api, reference, intervalMs) => {
247
+ let snapshot = await api.getGenericJob(reference);
248
+ while (!TERMINAL_STATES.has(snapshot.job.state)) {
249
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
250
+ snapshot = await api.getGenericJob(reference);
251
+ }
252
+ return snapshot;
253
+ };
254
+ export class GpuCommands {
255
+ static async run(argv) {
256
+ const [subcommand, ...rest] = argv;
257
+ if (!subcommand || argv.includes("--help") || argv.includes("-h")) {
258
+ // eslint-disable-next-line no-console
259
+ console.log(GPU_USAGE);
260
+ return;
261
+ }
262
+ if (subcommand !== "list" && subcommand !== "ops") {
263
+ throw new Error(`Unknown gpu subcommand: ${subcommand}`);
264
+ }
265
+ const parsed = parseArgs(rest);
266
+ const api = await createApi(parsed);
267
+ try {
268
+ if (subcommand === "list") {
269
+ const capabilities = await api.listGpuCapabilities({
270
+ ...baseAuth(parsed),
271
+ nodeId: nodeId(parsed)
272
+ });
273
+ printJsonOrValue(capabilities, Boolean(parsed.flags.json));
274
+ return;
275
+ }
276
+ const ops = await api.getGenericJobOps({
277
+ ...baseAuth(parsed),
278
+ nodeId: nodeId(parsed),
279
+ auditLimit: resolvePositiveInt(parsed.flags["audit-limit"], "--audit-limit"),
280
+ auditOffset: resolveNonNegativeInt(parsed.flags["audit-offset"], "--audit-offset")
281
+ });
282
+ // eslint-disable-next-line no-console
283
+ console.log(parsed.flags.json ? JSON.stringify(ops, null, 2) : formatOpsSummary(ops));
284
+ }
285
+ finally {
286
+ await api.close();
287
+ }
288
+ }
289
+ }
290
+ export class GpuJobCommands {
291
+ static shouldHandle(argv) {
292
+ const [subcommand] = argv;
293
+ if (!subcommand)
294
+ return false;
295
+ if (GPU_JOB_SUBCOMMANDS.has(subcommand))
296
+ return true;
297
+ if (!SHARED_JOB_SUBCOMMANDS.has(subcommand))
298
+ return false;
299
+ return argv.some((arg) => GPU_JOB_FLAGS.has(arg) || arg.startsWith("--node-base-url="));
300
+ }
301
+ static async run(argv) {
302
+ const [subcommand, ...rest] = argv;
303
+ if (!subcommand || argv.includes("--help") || argv.includes("-h")) {
304
+ // eslint-disable-next-line no-console
305
+ console.log(JOB_USAGE);
306
+ return;
307
+ }
308
+ const parsed = parseArgs(rest);
309
+ const api = await createApi(parsed);
310
+ try {
311
+ switch (subcommand) {
312
+ case "artifact": {
313
+ const action = parsed.positionals[0];
314
+ const file = parsed.positionals[1];
315
+ if (action !== "upload" || !file) {
316
+ throw new Error("Usage: mcoda job artifact upload <FILE> [options]");
317
+ }
318
+ const content = await readFile(file);
319
+ const artifactPath = resolveString(parsed.flags["artifact-path"]) || basename(file);
320
+ const result = await api.uploadGenericJobArtifact({
321
+ ...(await referenceFromArgs(parsed, resolveString(parsed.flags["job-id"]))),
322
+ name: resolveString(parsed.flags["artifact-name"]) || basename(file),
323
+ path: artifactPath,
324
+ contentBase64: content.toString("base64"),
325
+ contentType: resolveString(parsed.flags["content-type"]),
326
+ sha256: createHash("sha256").update(content).digest("hex"),
327
+ sizeBytes: content.length
328
+ });
329
+ printJsonOrValue(result, Boolean(parsed.flags.json));
330
+ break;
331
+ }
332
+ case "run": {
333
+ const job = await readJobEnvelope(parsed);
334
+ const snapshot = await api.runGenericJob(job, baseAuth(parsed));
335
+ const finalSnapshot = parsed.flags.wait
336
+ ? await waitForJob(api, await referenceFromArgs(parsed, job.job_id), resolvePositiveInt(parsed.flags["interval-ms"], "--interval-ms") || 1000)
337
+ : snapshot;
338
+ printSnapshot(finalSnapshot, Boolean(parsed.flags.json));
339
+ break;
340
+ }
341
+ case "status": {
342
+ const snapshot = await api.getGenericJob(await referenceFromArgs(parsed, parsed.positionals[0]));
343
+ printSnapshot(snapshot, Boolean(parsed.flags.json));
344
+ break;
345
+ }
346
+ case "logs": {
347
+ const result = await api.getGenericJobLogs(await referenceFromArgs(parsed, parsed.positionals[0]));
348
+ if (parsed.flags.json)
349
+ printJsonOrValue(result, true);
350
+ else
351
+ printJsonOrValue(result.logs.map((log) => log.message).join("\n"), false);
352
+ break;
353
+ }
354
+ case "events": {
355
+ const result = await api.getGenericJobEvents(await referenceFromArgs(parsed, parsed.positionals[0]));
356
+ printJsonOrValue(result, Boolean(parsed.flags.json));
357
+ break;
358
+ }
359
+ case "artifacts": {
360
+ const result = await api.getGenericJobArtifacts(await referenceFromArgs(parsed, parsed.positionals[0]));
361
+ printJsonOrValue(result, Boolean(parsed.flags.json));
362
+ break;
363
+ }
364
+ case "cancel": {
365
+ const result = await api.cancelGenericJob(await referenceFromArgs(parsed, parsed.positionals[0]));
366
+ printSnapshot(result, Boolean(parsed.flags.json));
367
+ break;
368
+ }
369
+ case "retry": {
370
+ const result = await api.retryGenericJob(await referenceFromArgs(parsed, parsed.positionals[0]));
371
+ printSnapshot(result, Boolean(parsed.flags.json));
372
+ break;
373
+ }
374
+ default:
375
+ throw new Error(`Unknown GPU job subcommand: ${subcommand}`);
376
+ }
377
+ }
378
+ finally {
379
+ await api.close();
380
+ }
381
+ }
382
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"JobsCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/jobs/JobsCommands.ts"],"names":[],"mappings":"AACA,OAAO,EAAsE,aAAa,EAA0C,MAAM,aAAa,CAAC;AAGxJ,KAAK,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEvG,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAuCD,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,KAAG,aAyH7C,CAAC;AA8BF,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,SAiBhF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,SAsB/E,CAAC;AAaF,eAAO,MAAM,eAAe,GAAI,MAAM,aAAa,EAAE,KAAG,IA8CvD,CAAC;AAwJF,qBAAa,YAAY;WACV,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CA6OhD"}
1
+ {"version":3,"file":"JobsCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/jobs/JobsCommands.ts"],"names":[],"mappings":"AACA,OAAO,EAAsE,aAAa,EAA0C,MAAM,aAAa,CAAC;AAIxJ,KAAK,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEvG,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAuCD,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,KAAG,aAyH7C,CAAC;AA8BF,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,SAiBhF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,GAAG,SAsB/E,CAAC;AAaF,eAAO,MAAM,eAAe,GAAI,MAAM,aAAa,EAAE,KAAG,IA8CvD,CAAC;AAwJF,qBAAa,YAAY;WACV,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAiPhD"}
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { JobInsightsService, JobResumeService, JobService, TelemetryService, WorkspaceResolver } from "@mcoda/core";
3
+ import { GpuJobCommands } from "../gpu/GpuCommands.js";
3
4
  const usage = `mcoda job <list|status|watch|logs|inspect|resume|cancel|tokens> ...
4
5
 
5
6
  Commands:
@@ -401,6 +402,10 @@ const handleTokens = async (parsed, workspace) => {
401
402
  };
402
403
  export class JobsCommands {
403
404
  static async run(argv) {
405
+ if (GpuJobCommands.shouldHandle(argv)) {
406
+ await GpuJobCommands.run(argv);
407
+ return;
408
+ }
404
409
  const parsed = parseJobArgs(argv);
405
410
  const workspace = await WorkspaceResolver.resolveWorkspace({
406
411
  cwd: process.cwd(),
@@ -1 +1 @@
1
- {"version":3,"file":"SelfHostedCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/self-hosted/SelfHostedCommands.ts"],"names":[],"mappings":"AA6NA,qBAAa,kBAAkB;WAChB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CA8FhD"}
1
+ {"version":3,"file":"SelfHostedCommands.d.ts","sourceRoot":"","sources":["../../../src/commands/self-hosted/SelfHostedCommands.ts"],"names":[],"mappings":"AAyOA,qBAAa,kBAAkB;WAChB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAkGhD"}
@@ -7,6 +7,7 @@ Subcommands:
7
7
  --provider <NAME> Filter by provider (mcoda|ollama)
8
8
  --limit <N> Limit returned agents
9
9
  --include-unreachable Include unreachable agents in the catalog result
10
+ --include-load-balanced Include auto-routed load-balanced self-hosted aliases
10
11
  --max-cost-per-1m-token <N>
11
12
  Exclude agents above the given cost_per_million
12
13
  --sorted-by-catalog-rating
@@ -14,9 +15,11 @@ Subcommands:
14
15
  --min-context <N> Require at least this context window
15
16
  --min-reasoning <N> Require at least this reasoning rating
16
17
  agent details <SLUG> Show a single mswarm self-hosted agent (supports --json)
18
+ --include-load-balanced Allow details for auto-routed aliases
17
19
  agent sync Sync self-hosted agents into the local mcoda registry
18
20
  --provider <NAME> Filter by provider before syncing
19
21
  --include-unreachable Sync unreachable agents too
22
+ --include-load-balanced Sync auto-routed load-balanced aliases too
20
23
  --limit <N> Limit synced agents
21
24
  --prune Remove previously synced self-hosted agents missing from the current catalog result
22
25
  --agent-slug-prefix <P> Override the local managed-agent slug prefix
@@ -104,6 +107,7 @@ const resolveNonNegativeNumber = (value, label) => {
104
107
  const formatNumber = (value) => value === undefined || Number.isNaN(value) ? "-" : String(value);
105
108
  const formatCapabilities = (capabilities) => capabilities && capabilities.length > 0 ? capabilities.join(",") : "-";
106
109
  const formatBoolean = (value) => value === undefined ? "-" : value ? "yes" : "no";
110
+ const agentRoutingMode = (agent) => agent.load_balanced ? "auto" : "direct";
107
111
  const pad = (value, width) => value.padEnd(width, " ");
108
112
  const renderTable = (headers, rows) => {
109
113
  const widths = headers.map((header, columnIndex) => Math.max(header.length, ...rows.map((row) => row[columnIndex]?.length ?? 0)));
@@ -122,6 +126,7 @@ const printAgentList = (agents) => {
122
126
  }
123
127
  const headers = [
124
128
  "REMOTE SLUG",
129
+ "ROUTE",
125
130
  "PROVIDER",
126
131
  "ADAPTER",
127
132
  "MODEL",
@@ -136,6 +141,7 @@ const printAgentList = (agents) => {
136
141
  ];
137
142
  const rows = agents.map((agent) => [
138
143
  agent.remote_slug ?? agent.slug,
144
+ agentRoutingMode(agent),
139
145
  agent.provider,
140
146
  agent.adapter ?? "-",
141
147
  agent.default_model,
@@ -155,6 +161,8 @@ const printAgentDetails = (agent) => {
155
161
  const entries = [
156
162
  ["Slug", agent.slug],
157
163
  ["Remote slug", agent.remote_slug ?? "-"],
164
+ ["Route", agentRoutingMode(agent)],
165
+ ["Load-balanced group", agent.load_balanced_group_id ?? "-"],
158
166
  ["Provider", agent.provider],
159
167
  ["Adapter", agent.adapter ?? "-"],
160
168
  ["Source agent", agent.source_agent_slug ?? "-"],
@@ -170,6 +178,7 @@ const printAgentDetails = (agent) => {
170
178
  ["Supports tools", formatBoolean(agent.supports_tools)],
171
179
  ["Supports reasoning", formatBoolean(agent.supports_reasoning)],
172
180
  ["Health", agent.health_status ?? "-"],
181
+ ["Members", formatNumber(agent.member_count)],
173
182
  ["Capabilities", formatCapabilities(agent.capabilities)],
174
183
  ];
175
184
  const labelWidth = Math.max(...entries.map(([label]) => label.length));
@@ -187,11 +196,12 @@ const printSyncSummary = (summary) => {
187
196
  record.remoteSlug,
188
197
  record.localSlug,
189
198
  record.action,
199
+ record.routingMode ?? "-",
190
200
  record.provider,
191
201
  record.defaultModel,
192
202
  ]);
193
203
  // eslint-disable-next-line no-console
194
- console.log(renderTable(["REMOTE SLUG", "LOCAL SLUG", "ACTION", "PROVIDER", "MODEL"], rows));
204
+ console.log(renderTable(["REMOTE SLUG", "LOCAL SLUG", "ACTION", "ROUTE", "PROVIDER", "MODEL"], rows));
195
205
  };
196
206
  export class SelfHostedCommands {
197
207
  static async run(argv) {
@@ -227,6 +237,7 @@ export class SelfHostedCommands {
227
237
  provider: resolveString(parsed.flags.provider),
228
238
  limit: resolvePositiveInt(parsed.flags.limit, "--limit"),
229
239
  includeUnreachable: Boolean(parsed.flags["include-unreachable"]),
240
+ includeLoadBalanced: Boolean(parsed.flags["include-load-balanced"]),
230
241
  maxCostPerMillion: resolveNonNegativeNumber(parsed.flags["max-cost-per-1m-token"], "--max-cost-per-1m-token"),
231
242
  minContextWindow: resolvePositiveInt(parsed.flags["min-context"], "--min-context"),
232
243
  minReasoningRating: resolveNonNegativeNumber(parsed.flags["min-reasoning"], "--min-reasoning"),
@@ -246,7 +257,9 @@ export class SelfHostedCommands {
246
257
  if (!slug) {
247
258
  throw new Error("Usage: mcoda self-hosted agent details <SLUG> [--json]");
248
259
  }
249
- const agent = await api.getSelfHostedAgent(slug);
260
+ const agent = await api.getSelfHostedAgent(slug, {
261
+ includeLoadBalanced: Boolean(parsed.flags["include-load-balanced"]),
262
+ });
250
263
  if (parsed.flags.json) {
251
264
  // eslint-disable-next-line no-console
252
265
  console.log(JSON.stringify(agent, null, 2));
@@ -261,6 +274,7 @@ export class SelfHostedCommands {
261
274
  provider: resolveString(parsed.flags.provider),
262
275
  limit: resolvePositiveInt(parsed.flags.limit, "--limit"),
263
276
  includeUnreachable: Boolean(parsed.flags["include-unreachable"]),
277
+ includeLoadBalanced: Boolean(parsed.flags["include-load-balanced"]),
264
278
  pruneMissing: Boolean(parsed.flags.prune),
265
279
  });
266
280
  if (parsed.flags.json) {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './bin/McodaEntrypoint.js';
3
3
  export * from './commands/cloud/CloudCommands.js';
4
4
  export * from './commands/self-hosted/SelfHostedCommands.js';
5
5
  export * from './commands/workers/WorkersCommands.js';
6
+ export * from './commands/gpu/GpuCommands.js';
6
7
  export * from './commands/config/ConfigCommands.js';
7
8
  export * from './commands/consent/ConsentCommands.js';
8
9
  export * from './commands/docs/DocsCommands.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,0BAA0B,CAAC;AACzC,cAAc,mCAAmC,CAAC;AAClD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,uCAAuC,CAAC;AACtD,cAAc,wCAAwC,CAAC;AACvD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,oDAAoD,CAAC;AACnE,cAAc,4CAA4C,CAAC;AAC3D,cAAc,uCAAuC,CAAC;AACtD,cAAc,uCAAuC,CAAC;AACtD,cAAc,wCAAwC,CAAC;AACvD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,uCAAuC,CAAC;AACtD,cAAc,gDAAgD,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,0BAA0B,CAAC;AACzC,cAAc,mCAAmC,CAAC;AAClD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,uCAAuC,CAAC;AACtD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,uCAAuC,CAAC;AACtD,cAAc,wCAAwC,CAAC;AACvD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,oDAAoD,CAAC;AACnE,cAAc,4CAA4C,CAAC;AAC3D,cAAc,uCAAuC,CAAC;AACtD,cAAc,uCAAuC,CAAC;AACtD,cAAc,wCAAwC,CAAC;AACvD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,uCAAuC,CAAC;AACtD,cAAc,gDAAgD,CAAC"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from './bin/McodaEntrypoint.js';
3
3
  export * from './commands/cloud/CloudCommands.js';
4
4
  export * from './commands/self-hosted/SelfHostedCommands.js';
5
5
  export * from './commands/workers/WorkersCommands.js';
6
+ export * from './commands/gpu/GpuCommands.js';
6
7
  export * from './commands/config/ConfigCommands.js';
7
8
  export * from './commands/consent/ConsentCommands.js';
8
9
  export * from './commands/docs/DocsCommands.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcoda",
3
- "version": "0.1.76",
3
+ "version": "0.1.79",
4
4
  "description": "Local-first CLI for planning, documentation, and execution workflows with agent assistance.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,12 +47,12 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "yaml": "^2.4.2",
50
- "@mcoda/shared": "0.1.76",
51
- "@mcoda/core": "0.1.76"
50
+ "@mcoda/core": "0.1.79",
51
+ "@mcoda/shared": "0.1.79"
52
52
  },
53
53
  "devDependencies": {
54
- "@mcoda/db": "0.1.76",
55
- "@mcoda/integrations": "0.1.76"
54
+ "@mcoda/db": "0.1.79",
55
+ "@mcoda/integrations": "0.1.79"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsc -p tsconfig.json",