@gajae-gateway/cli 0.1.8 → 0.2.1
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/dist/discord-setup.d.ts +41 -0
- package/dist/discord-setup.d.ts.map +1 -0
- package/dist/gjc-install.d.ts +13 -0
- package/dist/gjc-install.d.ts.map +1 -0
- package/dist/main.d.ts +17 -30
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1318 -582
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +27 -13
- package/dist/restart-stack.d.ts.map +1 -1
- package/dist/services.d.ts +9 -6
- package/dist/services.d.ts.map +1 -1
- package/dist/setup.d.ts +14 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/up.d.ts +20 -0
- package/dist/up.d.ts.map +1 -0
- package/dist/update.d.ts +10 -6
- package/dist/update.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/discord-setup.ts +103 -0
- package/src/gjc-install.ts +148 -0
- package/src/main.ts +296 -151
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +309 -92
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +175 -98
- package/dist/upgrade.d.ts +0 -40
- package/dist/upgrade.d.ts.map +0 -1
- package/src/upgrade.ts +0 -205
package/src/main.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
|
|
5
5
|
import type {
|
|
6
|
+
MonitorEventOperatorResult,
|
|
6
7
|
MonitorEventRecord,
|
|
7
8
|
MonitorRecord,
|
|
8
9
|
MonitorScheduleProjection,
|
|
9
10
|
MonitorSpec,
|
|
10
11
|
OpsCycleResult,
|
|
12
|
+
OpsDeliveriesParams,
|
|
13
|
+
OpsDeliveriesResult,
|
|
14
|
+
OpsSloParams,
|
|
15
|
+
OpsSloResult,
|
|
16
|
+
WorkJobsParams,
|
|
11
17
|
WorkJobsResult,
|
|
12
18
|
WorkRetireResult,
|
|
13
19
|
WorkRunResult,
|
|
@@ -16,8 +22,11 @@ import type {
|
|
|
16
22
|
WorkStatusResult,
|
|
17
23
|
WorkSteerResult,
|
|
18
24
|
} from "@gajae-gateway/protocol";
|
|
19
|
-
import { LOOPBACK_ORIGIN, originKey } from "@gajae-gateway/protocol";
|
|
25
|
+
import { LOOPBACK_ORIGIN, originKey, WORK_JOBS_MAX_LIMIT } from "@gajae-gateway/protocol";
|
|
26
|
+
|
|
20
27
|
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
28
|
+
import pkg from "../package.json";
|
|
29
|
+
|
|
21
30
|
import {
|
|
22
31
|
columnNames,
|
|
23
32
|
type ListOptions,
|
|
@@ -41,7 +50,13 @@ import {
|
|
|
41
50
|
import { type InstallServicesOptions, installServices, type ServicePlatform, serviceUsage } from "./services";
|
|
42
51
|
import { type RunSetupOptions, runSetup } from "./setup";
|
|
43
52
|
import { parseUpdateArgs, renderUpdate, runUpdate, type UpdateDeps } from "./update";
|
|
44
|
-
|
|
53
|
+
|
|
54
|
+
async function connectGatewaySocket(
|
|
55
|
+
path: string,
|
|
56
|
+
options: { readonly requestTimeoutMs?: number } = {},
|
|
57
|
+
): Promise<GajaewayClient> {
|
|
58
|
+
return GajaewayClient.connectSocketWithBackoff(path, { ...options, deadlineMs: 30_000 });
|
|
59
|
+
}
|
|
45
60
|
|
|
46
61
|
export function socketPath(home = process.env.GAJAEWAY_HOME): string {
|
|
47
62
|
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
@@ -70,7 +85,7 @@ export const COMMANDS = [
|
|
|
70
85
|
] as const;
|
|
71
86
|
|
|
72
87
|
export const CLI_USAGE =
|
|
73
|
-
"usage: gajaeway [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ... (test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire [--force] <name>|work retire --all-dead|work jobs|ops backup <path>|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops
|
|
88
|
+
"usage: gajaeway [--version] [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ... (settle <eventId>|retry <eventId>|test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire [--force] <name>|work retire --all-dead|work jobs [--limit N] [--offset N]|ops backup <path>|ops deliveries [--since ISO] [--state S] [--limit N] [--json]|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops restart-stack [--status]|services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux]|migrate [--source PATH] [--target PATH] [--dry-run]|update [--check] [--force] [--bin-dir DIR] [--no-restart]|setup [--from-env] [--adapters discord,slack,telegram] [--owner ID] [--discord-app-id ID]|setup --status|setup --broker-agent [--from DIR] (work run waits for a response; caller timeout does not end the attempt); cron timezone is an IANA zone and defaults to the gateway host's local timezone";
|
|
74
89
|
|
|
75
90
|
/** Usage errors exit 2, as `gajaeway-gateway` does; 1 stays a runtime failure. */
|
|
76
91
|
export const USAGE_EXIT_CODE = 2;
|
|
@@ -97,7 +112,7 @@ function gatewayHome(): string {
|
|
|
97
112
|
export interface MainOptions {
|
|
98
113
|
readonly services?: Pick<InstallServicesOptions, "loginPathRunner" | "writeFile">;
|
|
99
114
|
/** Test seams for `setup`: the platform fetch and the prompter. */
|
|
100
|
-
readonly setup?: Pick<RunSetupOptions, "fetch" | "prompter" | "env">;
|
|
115
|
+
readonly setup?: Pick<RunSetupOptions, "fetch" | "prompter" | "env" | "binDir" | "runBrokerAgent">;
|
|
101
116
|
/** Test seams for `ops restart-stack`; the real path spawns the service manager. */
|
|
102
117
|
readonly restartStack?: {
|
|
103
118
|
readonly launch?: Omit<LaunchRestartOptions, "home">;
|
|
@@ -211,6 +226,13 @@ interface ParsedMonitorTestArgs {
|
|
|
211
226
|
|
|
212
227
|
const MONITOR_TEST_USAGE = "usage: gajaeway monitors test <id> [--type T] [--payload J] [--wait[=SECONDS]]";
|
|
213
228
|
const MONITOR_WAIT_ERROR = `${MONITOR_TEST_USAGE}\n--wait expects a finite non-negative integer up to 86400`;
|
|
229
|
+
const MONITOR_OPERATOR_USAGE = "usage: gajaeway monitors settle <eventId>|retry <eventId>";
|
|
230
|
+
|
|
231
|
+
function parseMonitorEventOperatorArgs(args: readonly string[]): string {
|
|
232
|
+
const eventId = args[0];
|
|
233
|
+
if (args.length !== 1 || !eventId?.trim() || eventId.startsWith("--")) throw new Error(MONITOR_OPERATOR_USAGE);
|
|
234
|
+
return eventId;
|
|
235
|
+
}
|
|
214
236
|
|
|
215
237
|
function parseMonitorTestArgs(args: readonly string[]): ParsedMonitorTestArgs {
|
|
216
238
|
const [monitorId, ...flags] = args;
|
|
@@ -251,7 +273,7 @@ function parseMonitorTestArgs(args: readonly string[]): ParsedMonitorTestArgs {
|
|
|
251
273
|
};
|
|
252
274
|
}
|
|
253
275
|
|
|
254
|
-
const TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry"]);
|
|
276
|
+
const TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry", "skipped"]);
|
|
255
277
|
|
|
256
278
|
function waitForMonitorStage(client: GajaewayClient, eventId: string, timeoutSeconds: number): Promise<string> {
|
|
257
279
|
return new Promise((resolve) => {
|
|
@@ -307,6 +329,10 @@ export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
|
307
329
|
lines.push(
|
|
308
330
|
`memory: queued=${cycle.memoryIntents.queued} written=${cycle.memoryIntents.written} committed=${cycle.memoryIntents.committed} receipted=${cycle.memoryIntents.receipted} quarantined=${cycle.memoryIntents.quarantined}${cycle.memoryClosing ? " (closing)" : ""}`,
|
|
309
331
|
);
|
|
332
|
+
if (cycle.memoryGitIndexLock) {
|
|
333
|
+
const { path, ageMs } = cycle.memoryGitIndexLock;
|
|
334
|
+
lines.push(`memory_git_index_lock: ${path} age=${Math.round(ageMs)}ms`);
|
|
335
|
+
}
|
|
310
336
|
if (cycle.agentDisk) {
|
|
311
337
|
const { path, freeBytes, totalBytes } = cycle.agentDisk;
|
|
312
338
|
const gib = (bytes: number) => (bytes / 1024 ** 3).toFixed(1);
|
|
@@ -316,6 +342,19 @@ export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
|
316
342
|
: `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`,
|
|
317
343
|
);
|
|
318
344
|
}
|
|
345
|
+
if (cycle.brokerAgent !== null) {
|
|
346
|
+
lines.push(`broker_agent: ${cycle.brokerAgent.status}`);
|
|
347
|
+
for (const entry of cycle.brokerAgent.entries)
|
|
348
|
+
if (entry.state !== "in_sync") lines.push(`broker_agent ${entry.name}: kind=${entry.kind} state=${entry.state}`);
|
|
349
|
+
}
|
|
350
|
+
const sloState = cycle.slo.ok
|
|
351
|
+
? "ok"
|
|
352
|
+
: cycle.slo.days.some((day) => day.status === "failed")
|
|
353
|
+
? "failed"
|
|
354
|
+
: cycle.slo.days.some((day) => day.status === "insufficient")
|
|
355
|
+
? "insufficient"
|
|
356
|
+
: "pending";
|
|
357
|
+
lines.push(`slo: seven-day=${sloState}`);
|
|
319
358
|
// Always rendered: an empty subsystem must be distinguishable from an absent one.
|
|
320
359
|
lines.push(
|
|
321
360
|
`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`,
|
|
@@ -346,106 +385,110 @@ export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
|
346
385
|
export function cycleExitCode(cycle: OpsCycleResult): number {
|
|
347
386
|
return cycle.gates.length > 0 ? 1 : 0;
|
|
348
387
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* Opening a WAL-mode backup read-only may leave empty `-wal`/`-shm` sidecars
|
|
362
|
-
* next to the backup; `ops backup` writes delete-mode files, which gain none.
|
|
363
|
-
*/
|
|
364
|
-
export function verifyBackupIntegrity(backupPath: string): void {
|
|
365
|
-
let database: Database;
|
|
366
|
-
try {
|
|
367
|
-
database = new Database(backupPath, { readonly: true });
|
|
368
|
-
} catch (error) {
|
|
369
|
-
throw new Error(
|
|
370
|
-
`Backup is not readable: ${backupPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
388
|
+
export function renderSlo(result: OpsSloResult): string[] {
|
|
389
|
+
const message = result.ok
|
|
390
|
+
? `SLO ok: ${result.days.length} consecutive UTC days`
|
|
391
|
+
: result.days.some((day) => day.status === "failed")
|
|
392
|
+
? "SLO failed: counted failure events or journal overflow"
|
|
393
|
+
: result.days.some((day) => day.status === "insufficient")
|
|
394
|
+
? "SLO insufficient: coverage or durable evidence is incomplete"
|
|
395
|
+
: "SLO pending: an update window is still running; rerun later";
|
|
396
|
+
const lines = [message];
|
|
397
|
+
for (const day of result.days)
|
|
398
|
+
lines.push(
|
|
399
|
+
`${day.date} status=${day.status} coverage=${day.complete ? "complete" : "incomplete"} alive_ms=${day.aliveMs} max_gap_ms=${day.maxUncoveredGapMs} broker_unavailable=${day.broker_unavailable} persona_bind_failed=${day.persona_bind_failed} excluded=${day.excluded}`,
|
|
371
400
|
);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
pageCount = database.query<{ page_count: number }, []>("PRAGMA page_count").get()?.page_count ?? 0;
|
|
377
|
-
detail =
|
|
378
|
-
database.query<{ integrity_check: string }, []>("PRAGMA integrity_check").get()?.integrity_check ?? "unknown";
|
|
379
|
-
} catch (error) {
|
|
380
|
-
throw new Error(
|
|
381
|
-
`Backup is not a readable SQLite database: ${backupPath} (${error instanceof Error ? error.message : String(error)})`,
|
|
401
|
+
for (const window of result.windows)
|
|
402
|
+
lines.push(
|
|
403
|
+
`window id=${window.id} trigger=${window.trigger} state=${window.state} requested=${window.requestedAt} finished=${window.finishedAt ?? "-"} verified=${window.verified} eligible=${window.eligibleStart ?? "-"}..${window.eligibleEnd ?? "-"} over_cap_ms=${window.overCapMs}`,
|
|
382
404
|
);
|
|
383
|
-
|
|
384
|
-
database.close();
|
|
385
|
-
}
|
|
386
|
-
if (pageCount === 0) throw new Error(`Backup is an empty SQLite database: ${backupPath}`);
|
|
387
|
-
if (detail !== "ok") throw new Error(`Backup failed SQLite integrity_check: ${backupPath} (${detail})`);
|
|
405
|
+
return lines;
|
|
388
406
|
}
|
|
389
407
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
);
|
|
413
|
-
if (code !== "ENOENT" || entryExists) throw new Error(`Cannot read ${configPath}; refusing restore.`);
|
|
408
|
+
export function sloExitCode(result: OpsSloResult): number {
|
|
409
|
+
return result.ok ? 0 : 1;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const OPS_USAGE =
|
|
413
|
+
"usage: gajaeway ops backup <path>|deliveries [--since ISO] [--state S] [--limit N] [--json]|redeliver <deliveryId>|redeliver --since <iso>|slo [--days N] [--json]|cycle [--json]|integrity|restart-stack [--status]";
|
|
414
|
+
function parseOpsSloArgs(args: readonly string[]): { params: OpsSloParams; json: boolean } {
|
|
415
|
+
let days = 7;
|
|
416
|
+
let json = false;
|
|
417
|
+
const seen = new Set<string>();
|
|
418
|
+
for (let index = 0; index < args.length; index++) {
|
|
419
|
+
const flag = args[index];
|
|
420
|
+
if (flag === "--json") {
|
|
421
|
+
if (json) throw new Error(OPS_USAGE);
|
|
422
|
+
json = true;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (flag !== "--days" || seen.has(flag)) throw new Error(OPS_USAGE);
|
|
426
|
+
seen.add(flag);
|
|
427
|
+
const value = args[++index];
|
|
428
|
+
if (!value || !/^\d+$/.test(value)) throw new Error(OPS_USAGE);
|
|
429
|
+
days = Number(value);
|
|
430
|
+
if (!Number.isSafeInteger(days) || days < 1 || days > 3_650) throw new Error(OPS_USAGE);
|
|
414
431
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
432
|
+
return { params: { days }, json };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function parseOpsDeliveriesArgs(args: readonly string[]): { params: OpsDeliveriesParams; json: boolean } {
|
|
436
|
+
const params: { since?: string; state?: string; limit?: number } = {};
|
|
437
|
+
const seen = new Set<string>();
|
|
438
|
+
let json = false;
|
|
439
|
+
for (let index = 0; index < args.length; index++) {
|
|
440
|
+
const flag = args[index];
|
|
441
|
+
if (flag === "--json") {
|
|
442
|
+
if (json) throw new Error(OPS_USAGE);
|
|
443
|
+
json = true;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (flag !== "--since" && flag !== "--state" && flag !== "--limit") throw new Error(OPS_USAGE);
|
|
447
|
+
if (seen.has(flag)) throw new Error(OPS_USAGE);
|
|
448
|
+
seen.add(flag);
|
|
449
|
+
const value = args[++index];
|
|
450
|
+
if (!value || value.startsWith("--")) throw new Error(OPS_USAGE);
|
|
451
|
+
if (flag === "--since") {
|
|
452
|
+
if (!Number.isFinite(Date.parse(value))) throw new Error(OPS_USAGE);
|
|
453
|
+
params.since = value;
|
|
454
|
+
} else if (flag === "--state") params.state = value;
|
|
455
|
+
else {
|
|
456
|
+
if (!/^\d+$/.test(value)) throw new Error(OPS_USAGE);
|
|
457
|
+
const limit = Number(value);
|
|
458
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) throw new Error(OPS_USAGE);
|
|
459
|
+
params.limit = limit;
|
|
460
|
+
}
|
|
421
461
|
}
|
|
422
|
-
|
|
423
|
-
throw new Error(`Invalid configuration ${configPath}: config must be an object; refusing restore.`);
|
|
424
|
-
const dbPath = (parsed as { dbPath?: unknown }).dbPath;
|
|
425
|
-
if (dbPath === undefined) return join(home, "gateway.db");
|
|
426
|
-
if (typeof dbPath !== "string" || dbPath.length === 0)
|
|
427
|
-
throw new Error(`Invalid configuration ${configPath}: dbPath must be a non-empty string; refusing restore.`);
|
|
428
|
-
return dbPath;
|
|
462
|
+
return { params, json };
|
|
429
463
|
}
|
|
430
464
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
465
|
+
/** `work jobs [--limit N] [--offset N]`, bounded like the gateway so a bad value is a usage error. */
|
|
466
|
+
function parseWorkJobsArgs(args: readonly string[], usage: string): WorkJobsParams {
|
|
467
|
+
const params: { limit?: number; offset?: number } = {};
|
|
468
|
+
for (let index = 0; index < args.length; index++) {
|
|
469
|
+
const flag = args[index];
|
|
470
|
+
const value = args[++index];
|
|
471
|
+
if ((flag !== "--limit" && flag !== "--offset") || value === undefined || !/^\d+$/.test(value))
|
|
472
|
+
throw new Error(usage);
|
|
473
|
+
const key = flag === "--limit" ? "limit" : "offset";
|
|
474
|
+
if (params[key] !== undefined) throw new Error(usage);
|
|
475
|
+
const number = Number(value);
|
|
476
|
+
if (!Number.isSafeInteger(number) || (key === "limit" && (number < 1 || number > WORK_JOBS_MAX_LIMIT)))
|
|
477
|
+
throw new Error(usage);
|
|
478
|
+
params[key] = number;
|
|
440
479
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
480
|
+
return params;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function renderDeliveries(result: OpsDeliveriesResult): string[] {
|
|
484
|
+
if (result.rows.length === 0) return ["deliveries: none"];
|
|
485
|
+
return [
|
|
486
|
+
`deliveries: ${result.rows.length}`,
|
|
487
|
+
...result.rows.map(
|
|
488
|
+
(row) =>
|
|
489
|
+
`${row.createdAt} ${row.state} delivery=${row.deliveryId} turn=${row.turnId} origin=${row.originKey} conversation=${row.conversationId} text=${JSON.stringify(row.textPrefix)}`,
|
|
490
|
+
),
|
|
491
|
+
];
|
|
449
492
|
}
|
|
450
493
|
|
|
451
494
|
const CHAT_TURN_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -453,7 +496,7 @@ const CHAT_TURN_TIMEOUT_MS = 30 * 60_000;
|
|
|
453
496
|
async function chat(socket: string): Promise<void> {
|
|
454
497
|
let client: GajaewayClient;
|
|
455
498
|
try {
|
|
456
|
-
client = await
|
|
499
|
+
client = await connectGatewaySocket(socket);
|
|
457
500
|
} catch {
|
|
458
501
|
console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
|
|
459
502
|
process.exitCode = 1;
|
|
@@ -508,8 +551,55 @@ async function chat(socket: string): Promise<void> {
|
|
|
508
551
|
}
|
|
509
552
|
}
|
|
510
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Poll for memory operation completion. Memory operations (audit, autolink) are
|
|
556
|
+
* long-running and return an opRef immediately; this function polls for the result.
|
|
557
|
+
* If the CLI wait expires before completion, report 'still running' instead of failure.
|
|
558
|
+
*/
|
|
559
|
+
export async function pollMemoryOp(
|
|
560
|
+
client: Pick<GajaewayClient, "request">,
|
|
561
|
+
opRef: string,
|
|
562
|
+
maxWaitMs = 300_000, // 5 min default CLI timeout
|
|
563
|
+
pollIntervalMs = 1_000,
|
|
564
|
+
): Promise<{ status: "completed" | "failed" | "still_running"; elapsedMs: number; result?: unknown; error?: string }> {
|
|
565
|
+
const startTime = Date.now();
|
|
566
|
+
let operationElapsedMs = 0;
|
|
567
|
+
|
|
568
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
569
|
+
const status = await client.request<{
|
|
570
|
+
status: "pending" | "completed" | "failed";
|
|
571
|
+
elapsedMs: number;
|
|
572
|
+
result?: unknown;
|
|
573
|
+
error?: string;
|
|
574
|
+
}>("memory.status", { opRef });
|
|
575
|
+
operationElapsedMs = status.elapsedMs;
|
|
576
|
+
|
|
577
|
+
if (status.status === "completed") {
|
|
578
|
+
return { status: "completed", elapsedMs: status.elapsedMs, result: status.result };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (status.status === "failed") {
|
|
582
|
+
return { status: "failed", elapsedMs: status.elapsedMs, error: status.error };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Wait before polling again
|
|
586
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// CLI wait expired, but operation may still be running on the server
|
|
590
|
+
const waitedMs = Date.now() - startTime;
|
|
591
|
+
console.error(
|
|
592
|
+
`memory operation still running (${Math.round(operationElapsedMs / 1000)}s operation elapsed; CLI wait ended after ${Math.round(waitedMs / 1000)}s).\nCheck status: gajaeway memory status ${opRef}`,
|
|
593
|
+
);
|
|
594
|
+
return { status: "still_running", elapsedMs: operationElapsedMs };
|
|
595
|
+
}
|
|
596
|
+
|
|
511
597
|
export async function main(args = process.argv.slice(2), options: MainOptions = {}): Promise<void> {
|
|
512
598
|
const parsed = parseArgs(args);
|
|
599
|
+
if (parsed.command === "--version" && parsed.rest.length === 0) {
|
|
600
|
+
console.log(pkg.version);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
513
603
|
const usage = usageFor(parsed.command);
|
|
514
604
|
if (usage !== undefined) {
|
|
515
605
|
console.error(usage);
|
|
@@ -518,7 +608,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
518
608
|
try {
|
|
519
609
|
switch (parsed.command) {
|
|
520
610
|
case "status": {
|
|
521
|
-
const client = await
|
|
611
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
522
612
|
try {
|
|
523
613
|
console.log(JSON.stringify(await client.status()));
|
|
524
614
|
} finally {
|
|
@@ -527,7 +617,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
527
617
|
break;
|
|
528
618
|
}
|
|
529
619
|
case "shutdown": {
|
|
530
|
-
const client = await
|
|
620
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
531
621
|
try {
|
|
532
622
|
await client.shutdown();
|
|
533
623
|
} finally {
|
|
@@ -548,7 +638,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
548
638
|
case "sessions": {
|
|
549
639
|
const listOptions =
|
|
550
640
|
parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
|
|
551
|
-
const client = await
|
|
641
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
552
642
|
try {
|
|
553
643
|
const result = await client.request<{
|
|
554
644
|
sessions: Array<{
|
|
@@ -602,32 +692,14 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
602
692
|
break;
|
|
603
693
|
}
|
|
604
694
|
case "ops": {
|
|
605
|
-
const [command, path
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
throw new Error("usage: gajaeway ops upgrade [--gjc X.Y.Z]");
|
|
614
|
-
}
|
|
615
|
-
const result = await performUpgrade({ home, gjcVersion });
|
|
616
|
-
console.log(result.detail);
|
|
617
|
-
if (result.status !== "ok") {
|
|
618
|
-
process.exitCode = 1;
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
// After upgrade, trigger restart-stack
|
|
622
|
-
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
623
|
-
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
624
|
-
console.log("read the outcome with: gajaeway ops restart-stack --status");
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
if (command === "restore" && path) {
|
|
628
|
-
await restoreDatabase(parsed.socket, path);
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
695
|
+
const [command, path] = parsed.rest;
|
|
696
|
+
if (
|
|
697
|
+
command === undefined ||
|
|
698
|
+
!["backup", "deliveries", "redeliver", "integrity", "slo", "cycle", "restart-stack"].includes(command)
|
|
699
|
+
)
|
|
700
|
+
throw new Error(`unknown ops command: ${command ?? "(missing)"}\n${OPS_USAGE}`);
|
|
701
|
+
const deliveries = command === "deliveries" ? parseOpsDeliveriesArgs(parsed.rest.slice(1)) : undefined;
|
|
702
|
+
const slo = command === "slo" ? parseOpsSloArgs(parsed.rest.slice(1)) : undefined;
|
|
631
703
|
if (command === "restart-stack") {
|
|
632
704
|
// Deliberately socket-free: the reason to run this is a gateway that
|
|
633
705
|
// has to come back, so it must not need the gateway to answer first.
|
|
@@ -647,9 +719,10 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
647
719
|
} else if (flag === "--status" && parsed.rest.length === 2) {
|
|
648
720
|
const receipt = await readRestartReceipt(home);
|
|
649
721
|
if (receipt === undefined) throw new Error("no restart-stack receipt");
|
|
650
|
-
|
|
722
|
+
const state = await effectiveRestartState(receipt, home);
|
|
723
|
+
for (const line of renderRestartReceipt(receipt, state)) console.log(line);
|
|
651
724
|
// Only a completed, verified sequence exits 0; queued or running is not yet success.
|
|
652
|
-
if (
|
|
725
|
+
if (state !== "ok") process.exitCode = 1;
|
|
653
726
|
} else if (parsed.rest.length === 1) {
|
|
654
727
|
const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
|
|
655
728
|
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
@@ -669,10 +742,15 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
669
742
|
redeliverParams = { since: parsed.rest[2] };
|
|
670
743
|
else throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
|
|
671
744
|
}
|
|
672
|
-
const client = await
|
|
745
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
673
746
|
try {
|
|
674
747
|
if (command === "backup" && path) console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
675
|
-
else if (
|
|
748
|
+
else if (deliveries) {
|
|
749
|
+
const result = await client.opsDeliveries(deliveries.params);
|
|
750
|
+
if (deliveries.json) console.log(JSON.stringify(result));
|
|
751
|
+
else for (const line of renderDeliveries(result)) console.log(line);
|
|
752
|
+
} else if (redeliverParams)
|
|
753
|
+
console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
|
|
676
754
|
else if (command === "integrity") console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
677
755
|
else if (command === "cycle") {
|
|
678
756
|
const cycle = await client.opsCycle();
|
|
@@ -682,34 +760,76 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
682
760
|
// Fail-closed: a gated/degraded cycle exits non-zero even though the
|
|
683
761
|
// request itself succeeded, so scripts can never read it as healthy.
|
|
684
762
|
process.exitCode = cycleExitCode(cycle);
|
|
685
|
-
} else
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
);
|
|
763
|
+
} else if (slo) {
|
|
764
|
+
const result = await client.opsSlo(slo.params);
|
|
765
|
+
if (slo.json) console.log(JSON.stringify(result));
|
|
766
|
+
else for (const line of renderSlo(result)) console.log(line);
|
|
767
|
+
process.exitCode = sloExitCode(result);
|
|
768
|
+
} else throw new Error(OPS_USAGE);
|
|
689
769
|
} finally {
|
|
690
770
|
await client.close();
|
|
691
771
|
}
|
|
692
772
|
break;
|
|
693
773
|
}
|
|
694
774
|
case "memory": {
|
|
695
|
-
const usage = "usage: gajaeway memory audit|
|
|
775
|
+
const usage = "usage: gajaeway memory audit|autolink|search <query>|status <opRef>";
|
|
696
776
|
// An unrecognised argument is refused before the socket is opened rather
|
|
697
777
|
// than ignored: a run of `memory audit --fix` that silently degraded to a
|
|
698
778
|
// plain audit would read as a repair attempt that reproduced the failure.
|
|
699
779
|
if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
|
|
700
780
|
throw new Error(`${usage} (unknown argument: ${parsed.rest[1]})`);
|
|
701
|
-
|
|
781
|
+
if (parsed.rest[0] === "status" && parsed.rest.length < 2) throw new Error(`${usage} (status requires opRef)`);
|
|
782
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
702
783
|
try {
|
|
703
784
|
if (parsed.rest[0] === "audit") {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
785
|
+
// Request audit operation; get opRef and poll for completion
|
|
786
|
+
const initResp = await client.request<{ opRef: string }>("memory.audit");
|
|
787
|
+
const poll = await pollMemoryOp(client, initResp.opRef);
|
|
788
|
+
if (poll.status === "failed") {
|
|
789
|
+
console.error(`audit failed after ${poll.elapsedMs}ms: ${poll.error}`);
|
|
790
|
+
process.exitCode = 1;
|
|
791
|
+
} else if (poll.status === "still_running") {
|
|
792
|
+
// Operation is still running; exit with distinct code so it's not reported as failure
|
|
793
|
+
process.exitCode = 75;
|
|
794
|
+
} else {
|
|
795
|
+
const auditResult = poll.result as { ok?: boolean; issues?: unknown[] };
|
|
796
|
+
console.log(JSON.stringify(auditResult?.issues ?? []));
|
|
797
|
+
console.error(`audit completed in ${poll.elapsedMs}ms`);
|
|
798
|
+
if (!auditResult?.ok) process.exitCode = 1;
|
|
799
|
+
}
|
|
707
800
|
} else if (parsed.rest[0] === "autolink") {
|
|
708
|
-
|
|
801
|
+
// Request autolink operation; get opRef and poll for completion
|
|
802
|
+
const initResp = await client.request<{ opRef: string }>("memory.autolink");
|
|
803
|
+
const poll = await pollMemoryOp(client, initResp.opRef);
|
|
804
|
+
if (poll.status === "failed") {
|
|
805
|
+
console.error(`autolink failed after ${poll.elapsedMs}ms: ${poll.error}`);
|
|
806
|
+
process.exitCode = 1;
|
|
807
|
+
} else if (poll.status === "still_running") {
|
|
808
|
+
// Operation is still running; exit with distinct code so it's not reported as failure
|
|
809
|
+
process.exitCode = 75;
|
|
810
|
+
} else {
|
|
811
|
+
console.log(JSON.stringify(poll.result));
|
|
812
|
+
}
|
|
709
813
|
} else if (parsed.rest[0] === "search" && parsed.rest.slice(1).join(" ")) {
|
|
710
814
|
console.log(
|
|
711
815
|
JSON.stringify(await client.request("memory.search", { query: parsed.rest.slice(1).join(" ") })),
|
|
712
816
|
);
|
|
817
|
+
} else if (parsed.rest[0] === "status" && parsed.rest[1]) {
|
|
818
|
+
const opRef = parsed.rest[1];
|
|
819
|
+
const result = await client.request<{
|
|
820
|
+
status: "pending" | "completed" | "failed";
|
|
821
|
+
elapsedMs: number;
|
|
822
|
+
result?: unknown;
|
|
823
|
+
error?: string;
|
|
824
|
+
}>("memory.status", { opRef });
|
|
825
|
+
if (result.status === "failed") {
|
|
826
|
+
console.error(`operation ${opRef} failed after ${result.elapsedMs}ms: ${result.error}`);
|
|
827
|
+
process.exitCode = 1;
|
|
828
|
+
} else if (result.status === "completed") {
|
|
829
|
+
console.log(JSON.stringify(result));
|
|
830
|
+
} else {
|
|
831
|
+
console.log(JSON.stringify({ status: result.status, elapsedMs: result.elapsedMs }));
|
|
832
|
+
}
|
|
713
833
|
} else throw new Error(usage);
|
|
714
834
|
} finally {
|
|
715
835
|
await client.close();
|
|
@@ -721,7 +841,9 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
721
841
|
const listOptions = command === "list" ? parseListOptions(args, MONITOR_COLUMNS) : undefined;
|
|
722
842
|
const update = command === "update" ? parseMonitorUpdateArgs(args) : undefined;
|
|
723
843
|
const monitorTest = command === "test" ? parseMonitorTestArgs(args) : undefined;
|
|
724
|
-
const
|
|
844
|
+
const operatorEventId =
|
|
845
|
+
command === "settle" || command === "retry" ? parseMonitorEventOperatorArgs(args) : undefined;
|
|
846
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
725
847
|
try {
|
|
726
848
|
if (command === "add" && args[0] === "--json" && args[1])
|
|
727
849
|
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args[1]))));
|
|
@@ -758,9 +880,15 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
758
880
|
const stage = await waitForMonitorStage(client, result.eventId, monitorTest.waitSeconds);
|
|
759
881
|
console.log(JSON.stringify({ eventId: result.eventId, stage }));
|
|
760
882
|
}
|
|
883
|
+
} else if ((command === "settle" || command === "retry") && operatorEventId !== undefined) {
|
|
884
|
+
const result = await client.request<MonitorEventOperatorResult>(`monitor.${command}`, {
|
|
885
|
+
eventId: operatorEventId,
|
|
886
|
+
});
|
|
887
|
+
console.log(JSON.stringify(result));
|
|
888
|
+
if (!result.done) process.exitCode = 1;
|
|
761
889
|
} else
|
|
762
890
|
throw new Error(
|
|
763
|
-
`usage: gajaeway monitors add --json '<MonitorSpec json>'|update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|test <id> [--type T] [--payload J] [--wait[=SECONDS]]`,
|
|
891
|
+
`usage: gajaeway monitors add --json '<MonitorSpec json>'|update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|settle <eventId>|retry <eventId>|test <id> [--type T] [--payload J] [--wait[=SECONDS]]`,
|
|
764
892
|
);
|
|
765
893
|
} finally {
|
|
766
894
|
await client.close();
|
|
@@ -780,7 +908,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
780
908
|
(command === "status" ? args.length !== 1 : !text || args.slice(1).some((arg) => arg.startsWith("--")))
|
|
781
909
|
)
|
|
782
910
|
throw new Error(usage);
|
|
783
|
-
const client = await
|
|
911
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
784
912
|
try {
|
|
785
913
|
if (command === "status") {
|
|
786
914
|
console.log(JSON.stringify(await client.request<WorkStatusResult>("work.status", { name })));
|
|
@@ -798,6 +926,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
798
926
|
let force = false;
|
|
799
927
|
let allDead = false;
|
|
800
928
|
let name: string | undefined;
|
|
929
|
+
let jobsPage: WorkJobsParams | undefined;
|
|
801
930
|
if (command === "retire") {
|
|
802
931
|
for (let i = 0; i < args.length; i++) {
|
|
803
932
|
const arg = args[i];
|
|
@@ -815,9 +944,9 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
815
944
|
if (!allDead && !name) throw new Error(usage);
|
|
816
945
|
if (allDead && name) throw new Error(usage);
|
|
817
946
|
} else if (args.length !== 0) {
|
|
818
|
-
|
|
947
|
+
jobsPage = parseWorkJobsArgs(args, usage);
|
|
819
948
|
}
|
|
820
|
-
const client = await
|
|
949
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
821
950
|
try {
|
|
822
951
|
if (command === "retire") {
|
|
823
952
|
const result = await client.request<WorkRetireResult>("work.retire", {
|
|
@@ -836,7 +965,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
836
965
|
);
|
|
837
966
|
}
|
|
838
967
|
} else {
|
|
839
|
-
const result = await client.request<WorkJobsResult>("work.jobs");
|
|
968
|
+
const result = await client.request<WorkJobsResult>("work.jobs", jobsPage);
|
|
840
969
|
for (const job of result.jobs) {
|
|
841
970
|
// lane_key is the stored `work-<name>`; print the name work.retire accepts.
|
|
842
971
|
const name = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
@@ -861,6 +990,12 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
861
990
|
}`,
|
|
862
991
|
);
|
|
863
992
|
}
|
|
993
|
+
const shownEnd = result.offset + result.jobs.length;
|
|
994
|
+
if (result.jobs.length === 0) console.error(`jobs: none at offset ${result.offset} of ${result.total}`);
|
|
995
|
+
else if (result.offset > 0 || shownEnd < result.total)
|
|
996
|
+
console.error(
|
|
997
|
+
`jobs ${result.offset + 1}-${shownEnd} of ${result.total}${shownEnd < result.total ? ` (next: --offset ${shownEnd})` : ""}`,
|
|
998
|
+
);
|
|
864
999
|
}
|
|
865
1000
|
} finally {
|
|
866
1001
|
await client.close();
|
|
@@ -871,16 +1006,24 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
871
1006
|
const name = args[0];
|
|
872
1007
|
let cwd: string | undefined;
|
|
873
1008
|
let resume = false;
|
|
1009
|
+
let repo: string | undefined;
|
|
1010
|
+
let base: string | undefined;
|
|
874
1011
|
let model: string | { preset: string } | undefined;
|
|
875
1012
|
const textParts: string[] = [];
|
|
876
1013
|
for (let i = 1; i < args.length; i++) {
|
|
877
1014
|
const arg = args[i];
|
|
878
|
-
if (arg === "--cwd" || arg === "--model" || arg === "--preset") {
|
|
1015
|
+
if (arg === "--cwd" || arg === "--repo" || arg === "--base" || arg === "--model" || arg === "--preset") {
|
|
879
1016
|
const value = args[++i];
|
|
880
1017
|
if (!value?.trim() || value.startsWith("--")) throw new Error(usage);
|
|
881
1018
|
if (arg === "--cwd") {
|
|
882
1019
|
if (cwd !== undefined || !isAbsolute(value)) throw new Error(usage);
|
|
883
1020
|
cwd = value;
|
|
1021
|
+
} else if (arg === "--repo") {
|
|
1022
|
+
if (repo !== undefined) throw new Error(usage);
|
|
1023
|
+
repo = value;
|
|
1024
|
+
} else if (arg === "--base") {
|
|
1025
|
+
if (base !== undefined) throw new Error(usage);
|
|
1026
|
+
base = value;
|
|
884
1027
|
} else {
|
|
885
1028
|
if (model !== undefined) throw new Error(`${usage}\n--model and --preset are mutually exclusive`);
|
|
886
1029
|
model = arg === "--preset" ? { preset: value } : value;
|
|
@@ -893,7 +1036,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
893
1036
|
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text) throw new Error(usage);
|
|
894
1037
|
// This is a caller response-wait budget, never an attempt deadline.
|
|
895
1038
|
// Timeout/disconnect leaves the attempt observable via work.status.
|
|
896
|
-
const client = await
|
|
1039
|
+
const client = await connectGatewaySocket(parsed.socket, {
|
|
897
1040
|
requestTimeoutMs: command === "run" ? 3_600_000 : 120_000,
|
|
898
1041
|
});
|
|
899
1042
|
try {
|
|
@@ -902,6 +1045,8 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
902
1045
|
name,
|
|
903
1046
|
text,
|
|
904
1047
|
...(cwd ? { cwd } : {}),
|
|
1048
|
+
...(repo ? { repo } : {}),
|
|
1049
|
+
...(base ? { base } : {}),
|
|
905
1050
|
...(resume ? { resume: true } : {}),
|
|
906
1051
|
...(model === undefined ? {} : { model }),
|
|
907
1052
|
...(callerSessionId ? { callerSessionId } : {}),
|