@gajae-gateway/cli 0.1.8 → 0.2.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/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 +6 -32
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1195 -580
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +21 -11
- 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 +178 -142
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +258 -90
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +170 -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,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { copyFile, lstat, readFile, stat } from "node:fs/promises";
|
|
2
|
+
|
|
4
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
+
|
|
5
5
|
import type {
|
|
6
6
|
MonitorEventRecord,
|
|
7
7
|
MonitorRecord,
|
|
8
8
|
MonitorScheduleProjection,
|
|
9
9
|
MonitorSpec,
|
|
10
10
|
OpsCycleResult,
|
|
11
|
+
OpsDeliveriesParams,
|
|
12
|
+
OpsDeliveriesResult,
|
|
13
|
+
OpsSloParams,
|
|
14
|
+
OpsSloResult,
|
|
15
|
+
WorkJobsParams,
|
|
11
16
|
WorkJobsResult,
|
|
12
17
|
WorkRetireResult,
|
|
13
18
|
WorkRunResult,
|
|
@@ -16,8 +21,11 @@ import type {
|
|
|
16
21
|
WorkStatusResult,
|
|
17
22
|
WorkSteerResult,
|
|
18
23
|
} from "@gajae-gateway/protocol";
|
|
19
|
-
import { LOOPBACK_ORIGIN, originKey } from "@gajae-gateway/protocol";
|
|
24
|
+
import { LOOPBACK_ORIGIN, originKey, WORK_JOBS_MAX_LIMIT } from "@gajae-gateway/protocol";
|
|
25
|
+
|
|
20
26
|
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
27
|
+
import pkg from "../package.json";
|
|
28
|
+
|
|
21
29
|
import {
|
|
22
30
|
columnNames,
|
|
23
31
|
type ListOptions,
|
|
@@ -41,7 +49,13 @@ import {
|
|
|
41
49
|
import { type InstallServicesOptions, installServices, type ServicePlatform, serviceUsage } from "./services";
|
|
42
50
|
import { type RunSetupOptions, runSetup } from "./setup";
|
|
43
51
|
import { parseUpdateArgs, renderUpdate, runUpdate, type UpdateDeps } from "./update";
|
|
44
|
-
|
|
52
|
+
|
|
53
|
+
async function connectGatewaySocket(
|
|
54
|
+
path: string,
|
|
55
|
+
options: { readonly requestTimeoutMs?: number } = {},
|
|
56
|
+
): Promise<GajaewayClient> {
|
|
57
|
+
return GajaewayClient.connectSocketWithBackoff(path, { ...options, deadlineMs: 30_000 });
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
export function socketPath(home = process.env.GAJAEWAY_HOME): string {
|
|
47
61
|
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
@@ -70,7 +84,7 @@ export const COMMANDS = [
|
|
|
70
84
|
] as const;
|
|
71
85
|
|
|
72
86
|
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
|
|
87
|
+
"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 ... (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
88
|
|
|
75
89
|
/** Usage errors exit 2, as `gajaeway-gateway` does; 1 stays a runtime failure. */
|
|
76
90
|
export const USAGE_EXIT_CODE = 2;
|
|
@@ -97,7 +111,7 @@ function gatewayHome(): string {
|
|
|
97
111
|
export interface MainOptions {
|
|
98
112
|
readonly services?: Pick<InstallServicesOptions, "loginPathRunner" | "writeFile">;
|
|
99
113
|
/** Test seams for `setup`: the platform fetch and the prompter. */
|
|
100
|
-
readonly setup?: Pick<RunSetupOptions, "fetch" | "prompter" | "env">;
|
|
114
|
+
readonly setup?: Pick<RunSetupOptions, "fetch" | "prompter" | "env" | "binDir" | "runBrokerAgent">;
|
|
101
115
|
/** Test seams for `ops restart-stack`; the real path spawns the service manager. */
|
|
102
116
|
readonly restartStack?: {
|
|
103
117
|
readonly launch?: Omit<LaunchRestartOptions, "home">;
|
|
@@ -307,6 +321,10 @@ export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
|
307
321
|
lines.push(
|
|
308
322
|
`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
323
|
);
|
|
324
|
+
if (cycle.memoryGitIndexLock) {
|
|
325
|
+
const { path, ageMs } = cycle.memoryGitIndexLock;
|
|
326
|
+
lines.push(`memory_git_index_lock: ${path} age=${Math.round(ageMs)}ms`);
|
|
327
|
+
}
|
|
310
328
|
if (cycle.agentDisk) {
|
|
311
329
|
const { path, freeBytes, totalBytes } = cycle.agentDisk;
|
|
312
330
|
const gib = (bytes: number) => (bytes / 1024 ** 3).toFixed(1);
|
|
@@ -316,6 +334,19 @@ export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
|
316
334
|
: `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`,
|
|
317
335
|
);
|
|
318
336
|
}
|
|
337
|
+
if (cycle.brokerAgent !== null) {
|
|
338
|
+
lines.push(`broker_agent: ${cycle.brokerAgent.status}`);
|
|
339
|
+
for (const entry of cycle.brokerAgent.entries)
|
|
340
|
+
if (entry.state !== "in_sync") lines.push(`broker_agent ${entry.name}: kind=${entry.kind} state=${entry.state}`);
|
|
341
|
+
}
|
|
342
|
+
const sloState = cycle.slo.ok
|
|
343
|
+
? "ok"
|
|
344
|
+
: cycle.slo.days.some((day) => day.status === "failed")
|
|
345
|
+
? "failed"
|
|
346
|
+
: cycle.slo.days.some((day) => day.status === "insufficient")
|
|
347
|
+
? "insufficient"
|
|
348
|
+
: "pending";
|
|
349
|
+
lines.push(`slo: seven-day=${sloState}`);
|
|
319
350
|
// Always rendered: an empty subsystem must be distinguishable from an absent one.
|
|
320
351
|
lines.push(
|
|
321
352
|
`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`,
|
|
@@ -346,106 +377,110 @@ export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
|
346
377
|
export function cycleExitCode(cycle: OpsCycleResult): number {
|
|
347
378
|
return cycle.gates.length > 0 ? 1 : 0;
|
|
348
379
|
}
|
|
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)})`,
|
|
380
|
+
export function renderSlo(result: OpsSloResult): string[] {
|
|
381
|
+
const message = result.ok
|
|
382
|
+
? `SLO ok: ${result.days.length} consecutive UTC days`
|
|
383
|
+
: result.days.some((day) => day.status === "failed")
|
|
384
|
+
? "SLO failed: counted failure events or journal overflow"
|
|
385
|
+
: result.days.some((day) => day.status === "insufficient")
|
|
386
|
+
? "SLO insufficient: coverage or durable evidence is incomplete"
|
|
387
|
+
: "SLO pending: an update window is still running; rerun later";
|
|
388
|
+
const lines = [message];
|
|
389
|
+
for (const day of result.days)
|
|
390
|
+
lines.push(
|
|
391
|
+
`${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
392
|
);
|
|
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)})`,
|
|
393
|
+
for (const window of result.windows)
|
|
394
|
+
lines.push(
|
|
395
|
+
`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
396
|
);
|
|
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})`);
|
|
397
|
+
return lines;
|
|
388
398
|
}
|
|
389
399
|
|
|
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.`);
|
|
400
|
+
export function sloExitCode(result: OpsSloResult): number {
|
|
401
|
+
return result.ok ? 0 : 1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const OPS_USAGE =
|
|
405
|
+
"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]";
|
|
406
|
+
function parseOpsSloArgs(args: readonly string[]): { params: OpsSloParams; json: boolean } {
|
|
407
|
+
let days = 7;
|
|
408
|
+
let json = false;
|
|
409
|
+
const seen = new Set<string>();
|
|
410
|
+
for (let index = 0; index < args.length; index++) {
|
|
411
|
+
const flag = args[index];
|
|
412
|
+
if (flag === "--json") {
|
|
413
|
+
if (json) throw new Error(OPS_USAGE);
|
|
414
|
+
json = true;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (flag !== "--days" || seen.has(flag)) throw new Error(OPS_USAGE);
|
|
418
|
+
seen.add(flag);
|
|
419
|
+
const value = args[++index];
|
|
420
|
+
if (!value || !/^\d+$/.test(value)) throw new Error(OPS_USAGE);
|
|
421
|
+
days = Number(value);
|
|
422
|
+
if (!Number.isSafeInteger(days) || days < 1 || days > 3_650) throw new Error(OPS_USAGE);
|
|
414
423
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
424
|
+
return { params: { days }, json };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function parseOpsDeliveriesArgs(args: readonly string[]): { params: OpsDeliveriesParams; json: boolean } {
|
|
428
|
+
const params: { since?: string; state?: string; limit?: number } = {};
|
|
429
|
+
const seen = new Set<string>();
|
|
430
|
+
let json = false;
|
|
431
|
+
for (let index = 0; index < args.length; index++) {
|
|
432
|
+
const flag = args[index];
|
|
433
|
+
if (flag === "--json") {
|
|
434
|
+
if (json) throw new Error(OPS_USAGE);
|
|
435
|
+
json = true;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (flag !== "--since" && flag !== "--state" && flag !== "--limit") throw new Error(OPS_USAGE);
|
|
439
|
+
if (seen.has(flag)) throw new Error(OPS_USAGE);
|
|
440
|
+
seen.add(flag);
|
|
441
|
+
const value = args[++index];
|
|
442
|
+
if (!value || value.startsWith("--")) throw new Error(OPS_USAGE);
|
|
443
|
+
if (flag === "--since") {
|
|
444
|
+
if (!Number.isFinite(Date.parse(value))) throw new Error(OPS_USAGE);
|
|
445
|
+
params.since = value;
|
|
446
|
+
} else if (flag === "--state") params.state = value;
|
|
447
|
+
else {
|
|
448
|
+
if (!/^\d+$/.test(value)) throw new Error(OPS_USAGE);
|
|
449
|
+
const limit = Number(value);
|
|
450
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) throw new Error(OPS_USAGE);
|
|
451
|
+
params.limit = limit;
|
|
452
|
+
}
|
|
421
453
|
}
|
|
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;
|
|
454
|
+
return { params, json };
|
|
429
455
|
}
|
|
430
456
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
457
|
+
/** `work jobs [--limit N] [--offset N]`, bounded like the gateway so a bad value is a usage error. */
|
|
458
|
+
function parseWorkJobsArgs(args: readonly string[], usage: string): WorkJobsParams {
|
|
459
|
+
const params: { limit?: number; offset?: number } = {};
|
|
460
|
+
for (let index = 0; index < args.length; index++) {
|
|
461
|
+
const flag = args[index];
|
|
462
|
+
const value = args[++index];
|
|
463
|
+
if ((flag !== "--limit" && flag !== "--offset") || value === undefined || !/^\d+$/.test(value))
|
|
464
|
+
throw new Error(usage);
|
|
465
|
+
const key = flag === "--limit" ? "limit" : "offset";
|
|
466
|
+
if (params[key] !== undefined) throw new Error(usage);
|
|
467
|
+
const number = Number(value);
|
|
468
|
+
if (!Number.isSafeInteger(number) || (key === "limit" && (number < 1 || number > WORK_JOBS_MAX_LIMIT)))
|
|
469
|
+
throw new Error(usage);
|
|
470
|
+
params[key] = number;
|
|
440
471
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
472
|
+
return params;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function renderDeliveries(result: OpsDeliveriesResult): string[] {
|
|
476
|
+
if (result.rows.length === 0) return ["deliveries: none"];
|
|
477
|
+
return [
|
|
478
|
+
`deliveries: ${result.rows.length}`,
|
|
479
|
+
...result.rows.map(
|
|
480
|
+
(row) =>
|
|
481
|
+
`${row.createdAt} ${row.state} delivery=${row.deliveryId} turn=${row.turnId} origin=${row.originKey} conversation=${row.conversationId} text=${JSON.stringify(row.textPrefix)}`,
|
|
482
|
+
),
|
|
483
|
+
];
|
|
449
484
|
}
|
|
450
485
|
|
|
451
486
|
const CHAT_TURN_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -453,7 +488,7 @@ const CHAT_TURN_TIMEOUT_MS = 30 * 60_000;
|
|
|
453
488
|
async function chat(socket: string): Promise<void> {
|
|
454
489
|
let client: GajaewayClient;
|
|
455
490
|
try {
|
|
456
|
-
client = await
|
|
491
|
+
client = await connectGatewaySocket(socket);
|
|
457
492
|
} catch {
|
|
458
493
|
console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
|
|
459
494
|
process.exitCode = 1;
|
|
@@ -510,6 +545,10 @@ async function chat(socket: string): Promise<void> {
|
|
|
510
545
|
|
|
511
546
|
export async function main(args = process.argv.slice(2), options: MainOptions = {}): Promise<void> {
|
|
512
547
|
const parsed = parseArgs(args);
|
|
548
|
+
if (parsed.command === "--version" && parsed.rest.length === 0) {
|
|
549
|
+
console.log(pkg.version);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
513
552
|
const usage = usageFor(parsed.command);
|
|
514
553
|
if (usage !== undefined) {
|
|
515
554
|
console.error(usage);
|
|
@@ -518,7 +557,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
518
557
|
try {
|
|
519
558
|
switch (parsed.command) {
|
|
520
559
|
case "status": {
|
|
521
|
-
const client = await
|
|
560
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
522
561
|
try {
|
|
523
562
|
console.log(JSON.stringify(await client.status()));
|
|
524
563
|
} finally {
|
|
@@ -527,7 +566,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
527
566
|
break;
|
|
528
567
|
}
|
|
529
568
|
case "shutdown": {
|
|
530
|
-
const client = await
|
|
569
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
531
570
|
try {
|
|
532
571
|
await client.shutdown();
|
|
533
572
|
} finally {
|
|
@@ -548,7 +587,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
548
587
|
case "sessions": {
|
|
549
588
|
const listOptions =
|
|
550
589
|
parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
|
|
551
|
-
const client = await
|
|
590
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
552
591
|
try {
|
|
553
592
|
const result = await client.request<{
|
|
554
593
|
sessions: Array<{
|
|
@@ -602,32 +641,14 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
602
641
|
break;
|
|
603
642
|
}
|
|
604
643
|
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
|
-
}
|
|
644
|
+
const [command, path] = parsed.rest;
|
|
645
|
+
if (
|
|
646
|
+
command === undefined ||
|
|
647
|
+
!["backup", "deliveries", "redeliver", "integrity", "slo", "cycle", "restart-stack"].includes(command)
|
|
648
|
+
)
|
|
649
|
+
throw new Error(`unknown ops command: ${command ?? "(missing)"}\n${OPS_USAGE}`);
|
|
650
|
+
const deliveries = command === "deliveries" ? parseOpsDeliveriesArgs(parsed.rest.slice(1)) : undefined;
|
|
651
|
+
const slo = command === "slo" ? parseOpsSloArgs(parsed.rest.slice(1)) : undefined;
|
|
631
652
|
if (command === "restart-stack") {
|
|
632
653
|
// Deliberately socket-free: the reason to run this is a gateway that
|
|
633
654
|
// has to come back, so it must not need the gateway to answer first.
|
|
@@ -647,9 +668,10 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
647
668
|
} else if (flag === "--status" && parsed.rest.length === 2) {
|
|
648
669
|
const receipt = await readRestartReceipt(home);
|
|
649
670
|
if (receipt === undefined) throw new Error("no restart-stack receipt");
|
|
650
|
-
|
|
671
|
+
const state = await effectiveRestartState(receipt, home);
|
|
672
|
+
for (const line of renderRestartReceipt(receipt, state)) console.log(line);
|
|
651
673
|
// Only a completed, verified sequence exits 0; queued or running is not yet success.
|
|
652
|
-
if (
|
|
674
|
+
if (state !== "ok") process.exitCode = 1;
|
|
653
675
|
} else if (parsed.rest.length === 1) {
|
|
654
676
|
const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
|
|
655
677
|
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
@@ -669,10 +691,15 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
669
691
|
redeliverParams = { since: parsed.rest[2] };
|
|
670
692
|
else throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
|
|
671
693
|
}
|
|
672
|
-
const client = await
|
|
694
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
673
695
|
try {
|
|
674
696
|
if (command === "backup" && path) console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
675
|
-
else if (
|
|
697
|
+
else if (deliveries) {
|
|
698
|
+
const result = await client.opsDeliveries(deliveries.params);
|
|
699
|
+
if (deliveries.json) console.log(JSON.stringify(result));
|
|
700
|
+
else for (const line of renderDeliveries(result)) console.log(line);
|
|
701
|
+
} else if (redeliverParams)
|
|
702
|
+
console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
|
|
676
703
|
else if (command === "integrity") console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
677
704
|
else if (command === "cycle") {
|
|
678
705
|
const cycle = await client.opsCycle();
|
|
@@ -682,10 +709,12 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
682
709
|
// Fail-closed: a gated/degraded cycle exits non-zero even though the
|
|
683
710
|
// request itself succeeded, so scripts can never read it as healthy.
|
|
684
711
|
process.exitCode = cycleExitCode(cycle);
|
|
685
|
-
} else
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
);
|
|
712
|
+
} else if (slo) {
|
|
713
|
+
const result = await client.opsSlo(slo.params);
|
|
714
|
+
if (slo.json) console.log(JSON.stringify(result));
|
|
715
|
+
else for (const line of renderSlo(result)) console.log(line);
|
|
716
|
+
process.exitCode = sloExitCode(result);
|
|
717
|
+
} else throw new Error(OPS_USAGE);
|
|
689
718
|
} finally {
|
|
690
719
|
await client.close();
|
|
691
720
|
}
|
|
@@ -698,7 +727,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
698
727
|
// plain audit would read as a repair attempt that reproduced the failure.
|
|
699
728
|
if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
|
|
700
729
|
throw new Error(`${usage} (unknown argument: ${parsed.rest[1]})`);
|
|
701
|
-
const client = await
|
|
730
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
702
731
|
try {
|
|
703
732
|
if (parsed.rest[0] === "audit") {
|
|
704
733
|
const result = await client.request<{ ok: boolean; issues: unknown[] }>("memory.audit");
|
|
@@ -721,7 +750,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
721
750
|
const listOptions = command === "list" ? parseListOptions(args, MONITOR_COLUMNS) : undefined;
|
|
722
751
|
const update = command === "update" ? parseMonitorUpdateArgs(args) : undefined;
|
|
723
752
|
const monitorTest = command === "test" ? parseMonitorTestArgs(args) : undefined;
|
|
724
|
-
const client = await
|
|
753
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
725
754
|
try {
|
|
726
755
|
if (command === "add" && args[0] === "--json" && args[1])
|
|
727
756
|
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args[1]))));
|
|
@@ -780,7 +809,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
780
809
|
(command === "status" ? args.length !== 1 : !text || args.slice(1).some((arg) => arg.startsWith("--")))
|
|
781
810
|
)
|
|
782
811
|
throw new Error(usage);
|
|
783
|
-
const client = await
|
|
812
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
784
813
|
try {
|
|
785
814
|
if (command === "status") {
|
|
786
815
|
console.log(JSON.stringify(await client.request<WorkStatusResult>("work.status", { name })));
|
|
@@ -798,6 +827,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
798
827
|
let force = false;
|
|
799
828
|
let allDead = false;
|
|
800
829
|
let name: string | undefined;
|
|
830
|
+
let jobsPage: WorkJobsParams | undefined;
|
|
801
831
|
if (command === "retire") {
|
|
802
832
|
for (let i = 0; i < args.length; i++) {
|
|
803
833
|
const arg = args[i];
|
|
@@ -815,9 +845,9 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
815
845
|
if (!allDead && !name) throw new Error(usage);
|
|
816
846
|
if (allDead && name) throw new Error(usage);
|
|
817
847
|
} else if (args.length !== 0) {
|
|
818
|
-
|
|
848
|
+
jobsPage = parseWorkJobsArgs(args, usage);
|
|
819
849
|
}
|
|
820
|
-
const client = await
|
|
850
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
821
851
|
try {
|
|
822
852
|
if (command === "retire") {
|
|
823
853
|
const result = await client.request<WorkRetireResult>("work.retire", {
|
|
@@ -836,7 +866,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
836
866
|
);
|
|
837
867
|
}
|
|
838
868
|
} else {
|
|
839
|
-
const result = await client.request<WorkJobsResult>("work.jobs");
|
|
869
|
+
const result = await client.request<WorkJobsResult>("work.jobs", jobsPage);
|
|
840
870
|
for (const job of result.jobs) {
|
|
841
871
|
// lane_key is the stored `work-<name>`; print the name work.retire accepts.
|
|
842
872
|
const name = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
@@ -861,6 +891,12 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
861
891
|
}`,
|
|
862
892
|
);
|
|
863
893
|
}
|
|
894
|
+
const shownEnd = result.offset + result.jobs.length;
|
|
895
|
+
if (result.jobs.length === 0) console.error(`jobs: none at offset ${result.offset} of ${result.total}`);
|
|
896
|
+
else if (result.offset > 0 || shownEnd < result.total)
|
|
897
|
+
console.error(
|
|
898
|
+
`jobs ${result.offset + 1}-${shownEnd} of ${result.total}${shownEnd < result.total ? ` (next: --offset ${shownEnd})` : ""}`,
|
|
899
|
+
);
|
|
864
900
|
}
|
|
865
901
|
} finally {
|
|
866
902
|
await client.close();
|
|
@@ -893,7 +929,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
893
929
|
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text) throw new Error(usage);
|
|
894
930
|
// This is a caller response-wait budget, never an attempt deadline.
|
|
895
931
|
// Timeout/disconnect leaves the attempt observable via work.status.
|
|
896
|
-
const client = await
|
|
932
|
+
const client = await connectGatewaySocket(parsed.socket, {
|
|
897
933
|
requestTimeoutMs: command === "run" ? 3_600_000 : 120_000,
|
|
898
934
|
});
|
|
899
935
|
try {
|
package/src/migrate/hermes.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
cronMonitor,
|
|
6
6
|
deliveryOrigin,
|
|
7
7
|
emitChannels,
|
|
8
|
-
emitImportedAxis,
|
|
9
8
|
emitMonitors,
|
|
10
9
|
emptyChannelPlan,
|
|
11
10
|
emptyExtract,
|
|
@@ -107,7 +106,6 @@ export async function readHermes(tree: SourceTree, home: string): Promise<Source
|
|
|
107
106
|
content: entriesToMarkdown("Long-term memory (imported from Hermes)", memoryText),
|
|
108
107
|
mtime: info?.mtime,
|
|
109
108
|
});
|
|
110
|
-
emitImportedAxis(extract);
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
const envPath = join(root, ".env");
|
package/src/migrate.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import { acquireLease, type Lease, LeaseHeldError } from "@gajae-gateway/home";
|
|
3
5
|
import { detectHermes, readHermes } from "./migrate/hermes";
|
|
4
6
|
import { detectOpenClaw, readOpenClaw } from "./migrate/openclaw";
|
|
5
7
|
import {
|
|
@@ -23,9 +25,26 @@ export interface MigrateOptions {
|
|
|
23
25
|
* reads its configuration, and writes the gajae-way-compatible output.
|
|
24
26
|
*/
|
|
25
27
|
export async function migrate(options: MigrateOptions): Promise<void> {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
const targetHome = expandHome(options.target ?? process.env.GAJAEWAY_HOME ?? `${process.env.HOME ?? "~"}/.gajaeway`);
|
|
29
|
+
if (options.dryRun) return migrateUnlocked(options, targetHome);
|
|
30
|
+
|
|
31
|
+
let lease: Lease;
|
|
32
|
+
try {
|
|
33
|
+
lease = await acquireLease(targetHome, "memory");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof LeaseHeldError)
|
|
36
|
+
throw new Error(`${error.message}; stop the gateway first before importing memory data`, { cause: error });
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
await migrateUnlocked(options, targetHome);
|
|
41
|
+
} finally {
|
|
42
|
+
await lease.release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
28
45
|
|
|
46
|
+
async function migrateUnlocked(options: MigrateOptions, targetHome: string): Promise<void> {
|
|
47
|
+
const sourceRoot = expandHome(options.source ?? ".");
|
|
29
48
|
// Detect source type
|
|
30
49
|
let sourceType: MigrationSource | undefined;
|
|
31
50
|
if (await detectHermes(sourceRoot)) {
|
|
@@ -109,7 +128,20 @@ export async function migrate(options: MigrateOptions): Promise<void> {
|
|
|
109
128
|
for (const file of extract.files) {
|
|
110
129
|
const filePath = join(targetHome, file.target);
|
|
111
130
|
await mkdir(join(filePath, ".."), { recursive: true });
|
|
112
|
-
|
|
131
|
+
if (file.secret) {
|
|
132
|
+
// Atomic 0600 write for secrets
|
|
133
|
+
const temporary = `${filePath}.${randomUUID()}.tmp`;
|
|
134
|
+
try {
|
|
135
|
+
await writeFile(temporary, file.content, { mode: 0o600 });
|
|
136
|
+
await chmod(temporary, 0o600);
|
|
137
|
+
await rename(temporary, filePath);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await rm(temporary, { force: true });
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
await writeFile(filePath, file.content);
|
|
144
|
+
}
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
// Write merged JSON files
|