@gajae-gateway/cli 0.1.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/list.d.ts +55 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/main.d.ts +50 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +792 -0
- package/dist/services.d.ts +40 -0
- package/dist/services.d.ts.map +1 -0
- package/package.json +50 -0
- package/src/list.ts +187 -0
- package/src/main.ts +576 -0
- package/src/services.ts +224 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { copyFile, stat } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
MonitorRecord,
|
|
6
|
+
OpsCycleResult,
|
|
7
|
+
OriginRef,
|
|
8
|
+
WorkJobsResult,
|
|
9
|
+
WorkRetireResult,
|
|
10
|
+
WorkRunResult,
|
|
11
|
+
WorkStartParams,
|
|
12
|
+
WorkStartResult,
|
|
13
|
+
WorkStatusResult,
|
|
14
|
+
WorkSteerResult,
|
|
15
|
+
} from "@gajae-gateway/protocol";
|
|
16
|
+
import { LOOPBACK_ORIGIN, originKey, parseOriginKey } from "@gajae-gateway/protocol";
|
|
17
|
+
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
18
|
+
import {
|
|
19
|
+
columnNames,
|
|
20
|
+
type ListOptions,
|
|
21
|
+
MONITOR_COLUMNS,
|
|
22
|
+
parseListOptions,
|
|
23
|
+
renderList,
|
|
24
|
+
SESSION_COLUMNS,
|
|
25
|
+
type SessionListRow,
|
|
26
|
+
} from "./list";
|
|
27
|
+
import { type InstallServicesOptions, installServices, serviceUsage } from "./services";
|
|
28
|
+
|
|
29
|
+
export function socketPath(home = process.env.GAJAEWAY_HOME): string {
|
|
30
|
+
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every dispatchable top-level subcommand. An empty argv used to fall through
|
|
35
|
+
* the switch into the shared catch, which only *sets* an exit code — a probe
|
|
36
|
+
* with no arguments has to terminate before `main` opens a gateway socket,
|
|
37
|
+
* never after, so the list is checked up front.
|
|
38
|
+
*/
|
|
39
|
+
export const COMMANDS = [
|
|
40
|
+
"status",
|
|
41
|
+
"shutdown",
|
|
42
|
+
"chat",
|
|
43
|
+
"daemon",
|
|
44
|
+
"sessions",
|
|
45
|
+
"ops",
|
|
46
|
+
"memory",
|
|
47
|
+
"monitors",
|
|
48
|
+
"work",
|
|
49
|
+
"services",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
export const CLI_USAGE =
|
|
53
|
+
"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 ...|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 <name>|work jobs|ops backup <path>|ops cycle [--json]|ops integrity|ops restore <backupPath>|services install|repair --bin-dir DIR [--launch-agents-dir DIR] (work run waits for a response; caller timeout does not end the attempt)";
|
|
54
|
+
|
|
55
|
+
/** Usage errors exit 2, as `gajaeway-gateway` does; 1 stays a runtime failure. */
|
|
56
|
+
export const USAGE_EXIT_CODE = 2;
|
|
57
|
+
|
|
58
|
+
/** The usage text when `command` cannot be dispatched, undefined when it can. */
|
|
59
|
+
export function usageFor(command: string | undefined): string | undefined {
|
|
60
|
+
return command !== undefined && (COMMANDS as readonly string[]).includes(command) ? undefined : CLI_USAGE;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseArgs(args: string[]): { command?: string; rest: string[]; socket: string } {
|
|
64
|
+
let socket = socketPath();
|
|
65
|
+
const rest: string[] = [];
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
if (args[i] === "--socket") socket = args[++i] ?? socket;
|
|
68
|
+
else rest.push(args[i]);
|
|
69
|
+
}
|
|
70
|
+
return { command: rest[0], rest: rest.slice(1), socket };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function gatewayHome(): string {
|
|
74
|
+
return process.env.GAJAEWAY_HOME ?? `${process.env.HOME ?? "~"}/.gajaeway`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MainOptions {
|
|
78
|
+
readonly services?: Pick<InstallServicesOptions, "loginPathRunner" | "writeFile">;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type ServicesAction = "install" | "repair";
|
|
82
|
+
|
|
83
|
+
export interface ParsedServicesArgs {
|
|
84
|
+
readonly action: ServicesAction;
|
|
85
|
+
readonly binDir: string;
|
|
86
|
+
readonly launchAgentsDir?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseServicesArgs(args: readonly string[]): ParsedServicesArgs {
|
|
90
|
+
const action = args[0];
|
|
91
|
+
if (action !== "install" && action !== "repair") throw new Error(serviceUsage());
|
|
92
|
+
let binDir: string | undefined;
|
|
93
|
+
let launchAgentsDir: string | undefined;
|
|
94
|
+
for (let i = 1; i < args.length; i++) {
|
|
95
|
+
const flag = args[i];
|
|
96
|
+
if (flag === "--bin-dir" || flag === "--launch-agents-dir") {
|
|
97
|
+
const value = args[++i];
|
|
98
|
+
if (value === undefined || value.length === 0 || value.startsWith("--"))
|
|
99
|
+
throw new Error(`${flag} expects a non-empty DIR`);
|
|
100
|
+
if (flag === "--bin-dir") binDir = value;
|
|
101
|
+
else launchAgentsDir = value;
|
|
102
|
+
} else throw new Error(`unknown option: ${flag}`);
|
|
103
|
+
}
|
|
104
|
+
if (binDir === undefined) throw new Error("services requires --bin-dir DIR");
|
|
105
|
+
return { action, binDir, ...(launchAgentsDir === undefined ? {} : { launchAgentsDir }) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Operator runtime-cycle view (`gajaeway ops cycle`).
|
|
110
|
+
*
|
|
111
|
+
* Rendering contract:
|
|
112
|
+
* - The aggregate phase line always prints.
|
|
113
|
+
* - Any gate prints under `gates:` and forces exit code 1 — a gated cycle is
|
|
114
|
+
* never reported as healthy, so scripting cannot mistake it for idle.
|
|
115
|
+
* - `--json` emits the typed OpsCycleResult verbatim; the exit-code contract
|
|
116
|
+
* is identical.
|
|
117
|
+
*/
|
|
118
|
+
export function renderCycle(cycle: OpsCycleResult): string[] {
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
lines.push(`phase: ${cycle.phase}`);
|
|
121
|
+
if (cycle.gates.length > 0) lines.push(`gates: ${cycle.gates.join(", ")}`);
|
|
122
|
+
else lines.push("gates: none");
|
|
123
|
+
lines.push(`generatedAt: ${cycle.generatedAt}`);
|
|
124
|
+
lines.push(`instance: ${cycle.instanceId}`);
|
|
125
|
+
lines.push(`inbound: pending=${cycle.pendingInbound} inflight=${cycle.inFlightInbound}`);
|
|
126
|
+
lines.push(
|
|
127
|
+
`context: unread=${cycle.contextDiff.unread} expired=${cycle.contextDiff.expired} truncated=${cycle.contextDiff.truncated} omitted_oldest=${cycle.contextDiff.omittedOldestAt ?? "-"} omitted_newest=${cycle.contextDiff.omittedNewestAt ?? "-"}`,
|
|
128
|
+
);
|
|
129
|
+
lines.push(
|
|
130
|
+
`deliveries: pending=${cycle.deliveries.pending} inflight=${cycle.deliveries.inflight} confirmed=${cycle.deliveries.confirmed} failed_ambiguous=${cycle.deliveries.failedAmbiguous} expired=${cycle.deliveries.expired}`,
|
|
131
|
+
);
|
|
132
|
+
lines.push(
|
|
133
|
+
`memory: queued=${cycle.memoryIntents.queued} written=${cycle.memoryIntents.written} committed=${cycle.memoryIntents.committed} receipted=${cycle.memoryIntents.receipted} quarantined=${cycle.memoryIntents.quarantined}${cycle.memoryClosing ? " (closing)" : ""}`,
|
|
134
|
+
);
|
|
135
|
+
// Always rendered: an empty subsystem must be distinguishable from an absent one.
|
|
136
|
+
lines.push(
|
|
137
|
+
`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`,
|
|
138
|
+
);
|
|
139
|
+
if (cycle.sessions.length > 0) {
|
|
140
|
+
lines.push("sessions:");
|
|
141
|
+
lines.push("INDEX ORIGIN EPOCH SESSION PENDING UNSETTLED OLDEST");
|
|
142
|
+
for (const [index, session] of cycle.sessions.entries()) {
|
|
143
|
+
const sessionId = session.sessionId === "" ? "(rebinding)" : session.sessionId.slice(0, 11);
|
|
144
|
+
const oldest =
|
|
145
|
+
session.oldestUnsettledAgeMs === null ? "-" : `${Math.round(session.oldestUnsettledAgeMs / 1000)}s`;
|
|
146
|
+
lines.push(
|
|
147
|
+
`${String(index).padEnd(6)} ${session.originKey.padEnd(43)} ${String(session.epoch).padEnd(6)} ${sessionId.padEnd(12)} ${String(session.pendingInbound).padEnd(8)} ${String(session.unsettledDeliveries).padEnd(10)} ${oldest}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Exit-code contract for `gajaeway ops cycle`: 0 only when the projection is
|
|
156
|
+
* healthy. Extracted so the automation-facing contract is pinned by tests.
|
|
157
|
+
*/
|
|
158
|
+
export function cycleExitCode(cycle: OpsCycleResult): number {
|
|
159
|
+
return cycle.gates.length > 0 ? 1 : 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function restoreDatabase(socket: string, backupPath: string): Promise<void> {
|
|
163
|
+
if (!isAbsolute(backupPath)) throw new Error("ops restore requires an absolute backup path");
|
|
164
|
+
try {
|
|
165
|
+
await stat(socket);
|
|
166
|
+
throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof Error && error.message.startsWith("Refusing restore:")) throw error;
|
|
169
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
170
|
+
throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
|
|
171
|
+
}
|
|
172
|
+
let header: Uint8Array;
|
|
173
|
+
try {
|
|
174
|
+
header = new Uint8Array(await Bun.file(backupPath).slice(0, 16).arrayBuffer());
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(`Backup is not readable: ${backupPath}`);
|
|
177
|
+
}
|
|
178
|
+
if (new TextDecoder().decode(header) !== "SQLite format 3\u0000")
|
|
179
|
+
throw new Error(`Backup is not a SQLite database: ${backupPath}`);
|
|
180
|
+
const databasePath = join(gatewayHome(), "gateway.db");
|
|
181
|
+
const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
182
|
+
await copyFile(databasePath, preservedPath);
|
|
183
|
+
await copyFile(backupPath, databasePath);
|
|
184
|
+
console.log(`Validated SQLite backup: ${backupPath}`);
|
|
185
|
+
console.log(`Copied current database to: ${preservedPath}`);
|
|
186
|
+
console.log(`Restored backup to: ${databasePath}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function chat(socket: string): Promise<void> {
|
|
190
|
+
let client: GajaewayClient;
|
|
191
|
+
try {
|
|
192
|
+
client = await GajaewayClient.connectSocket(socket);
|
|
193
|
+
} catch {
|
|
194
|
+
console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const turnWaiters = new Map<string, () => void>();
|
|
199
|
+
client.onChatMessage((message) => {
|
|
200
|
+
console.log(message.text);
|
|
201
|
+
if (message.final) turnWaiters.get(message.turnId)?.();
|
|
202
|
+
});
|
|
203
|
+
const sendAndWait = async (text: string): Promise<void> => {
|
|
204
|
+
const { turnId } = await client.chatSend(LOOPBACK_ORIGIN, text);
|
|
205
|
+
if (turnId === null) {
|
|
206
|
+
console.error("(message was not engaged)");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await new Promise<void>((resolve) => {
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
turnWaiters.delete(turnId);
|
|
212
|
+
console.error("(turn timed out after 120s)");
|
|
213
|
+
resolve();
|
|
214
|
+
}, 120_000);
|
|
215
|
+
turnWaiters.set(turnId, () => {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
turnWaiters.delete(turnId);
|
|
218
|
+
resolve();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
const reader = Bun.stdin.stream().getReader();
|
|
223
|
+
const decoder = new TextDecoder();
|
|
224
|
+
let buffer = "";
|
|
225
|
+
process.stdout.write("> ");
|
|
226
|
+
try {
|
|
227
|
+
while (true) {
|
|
228
|
+
const { value, done } = await reader.read();
|
|
229
|
+
if (done) break;
|
|
230
|
+
buffer += decoder.decode(value, { stream: true });
|
|
231
|
+
const lines = buffer.split(/\r?\n/);
|
|
232
|
+
buffer = lines.pop() ?? "";
|
|
233
|
+
for (const line of lines) {
|
|
234
|
+
if (line === "/quit") return;
|
|
235
|
+
if (line.trim()) await sendAndWait(line);
|
|
236
|
+
process.stdout.write("> ");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} finally {
|
|
240
|
+
await client.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function main(args = process.argv.slice(2), options: MainOptions = {}): Promise<void> {
|
|
245
|
+
const parsed = parseArgs(args);
|
|
246
|
+
const usage = usageFor(parsed.command);
|
|
247
|
+
if (usage !== undefined) {
|
|
248
|
+
console.error(usage);
|
|
249
|
+
process.exit(USAGE_EXIT_CODE);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
switch (parsed.command) {
|
|
253
|
+
case "status": {
|
|
254
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
255
|
+
try {
|
|
256
|
+
console.log(JSON.stringify(await client.status()));
|
|
257
|
+
} finally {
|
|
258
|
+
await client.close();
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case "shutdown": {
|
|
263
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
264
|
+
try {
|
|
265
|
+
await client.shutdown();
|
|
266
|
+
} finally {
|
|
267
|
+
await client.close();
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "chat":
|
|
272
|
+
await chat(parsed.socket);
|
|
273
|
+
break;
|
|
274
|
+
case "daemon":
|
|
275
|
+
if (parsed.rest[0] === "run")
|
|
276
|
+
console.log("Launch the gateway out-of-band with: bun packages/gateway/src/main.ts daemon");
|
|
277
|
+
else throw new Error("usage: gajaeway daemon run");
|
|
278
|
+
break;
|
|
279
|
+
// Flag validation happens before the socket connect so a bad
|
|
280
|
+
// `--fields`/`--limit` fails fast without a running gateway.
|
|
281
|
+
case "sessions": {
|
|
282
|
+
const listOptions =
|
|
283
|
+
parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
|
|
284
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
285
|
+
try {
|
|
286
|
+
const result = await client.request<{
|
|
287
|
+
sessions: Array<{
|
|
288
|
+
origin: Parameters<typeof originKey>[0];
|
|
289
|
+
epoch: number;
|
|
290
|
+
createdAt: string;
|
|
291
|
+
lastActivityAt: string | null;
|
|
292
|
+
bootstrap: {
|
|
293
|
+
epoch: number;
|
|
294
|
+
pending: boolean;
|
|
295
|
+
appliedAt: string | null;
|
|
296
|
+
includedSections: string[];
|
|
297
|
+
byteCount: number;
|
|
298
|
+
truncated: boolean;
|
|
299
|
+
diagnostics: string[];
|
|
300
|
+
};
|
|
301
|
+
}>;
|
|
302
|
+
}>("session.list");
|
|
303
|
+
const [command, selector] = parsed.rest;
|
|
304
|
+
if (command === "list") {
|
|
305
|
+
const options = listOptions as ListOptions;
|
|
306
|
+
for (const line of renderList(SESSION_COLUMNS, result.sessions satisfies SessionListRow[], options, {
|
|
307
|
+
key: "sessions",
|
|
308
|
+
result,
|
|
309
|
+
}))
|
|
310
|
+
console.log(line);
|
|
311
|
+
} else if (command === "inspect" && selector) {
|
|
312
|
+
const index = Number(selector);
|
|
313
|
+
const session = Number.isInteger(index)
|
|
314
|
+
? result.sessions[index]
|
|
315
|
+
: result.sessions.find((candidate) => originKey(candidate.origin) === selector);
|
|
316
|
+
if (!session) throw new Error(`Unknown session: ${selector}`);
|
|
317
|
+
console.log(`origin: ${originKey(session.origin)}`);
|
|
318
|
+
console.log(`epoch: ${session.epoch}`);
|
|
319
|
+
console.log(`createdAt: ${session.createdAt}`);
|
|
320
|
+
console.log(`lastActivityAt: ${session.lastActivityAt ?? "-"}`);
|
|
321
|
+
console.log(`bootstrapEpoch: ${session.bootstrap.epoch}`);
|
|
322
|
+
console.log(`bootstrapPending: ${session.bootstrap.pending}`);
|
|
323
|
+
console.log(`bootstrapAppliedAt: ${session.bootstrap.appliedAt ?? "-"}`);
|
|
324
|
+
console.log(`bootstrapSections: ${session.bootstrap.includedSections.join(", ") || "-"}`);
|
|
325
|
+
console.log(`bootstrapBytes: ${session.bootstrap.byteCount}`);
|
|
326
|
+
console.log(`bootstrapTruncated: ${session.bootstrap.truncated}`);
|
|
327
|
+
console.log(`bootstrapDiagnostics: ${session.bootstrap.diagnostics.join(", ") || "-"}`);
|
|
328
|
+
} else
|
|
329
|
+
throw new Error(
|
|
330
|
+
`usage: gajaeway sessions list [--json] [--fields ${columnNames(SESSION_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <originKey-or-index>`,
|
|
331
|
+
);
|
|
332
|
+
} finally {
|
|
333
|
+
await client.close();
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "ops": {
|
|
338
|
+
const [command, path] = parsed.rest;
|
|
339
|
+
if (command === "restore" && path) {
|
|
340
|
+
await restoreDatabase(parsed.socket, path);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
344
|
+
try {
|
|
345
|
+
if (command === "backup" && path) console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
346
|
+
else if (command === "integrity") console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
347
|
+
else if (command === "cycle") {
|
|
348
|
+
const cycle = await client.opsCycle();
|
|
349
|
+
if (parsed.rest[1] === "--json") console.log(JSON.stringify(cycle));
|
|
350
|
+
else if (parsed.rest[1]) throw new Error("usage: gajaeway ops cycle [--json]");
|
|
351
|
+
else for (const line of renderCycle(cycle)) console.log(line);
|
|
352
|
+
// Fail-closed: a gated/degraded cycle exits non-zero even though the
|
|
353
|
+
// request itself succeeded, so scripts can never read it as healthy.
|
|
354
|
+
process.exitCode = cycleExitCode(cycle);
|
|
355
|
+
} else throw new Error("usage: gajaeway ops backup <path>|cycle [--json]|integrity|restore <backupPath>");
|
|
356
|
+
} finally {
|
|
357
|
+
await client.close();
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "memory": {
|
|
362
|
+
const usage = "usage: gajaeway memory audit|memory autolink|memory search <query>";
|
|
363
|
+
// An unrecognised argument is refused before the socket is opened rather
|
|
364
|
+
// than ignored: a run of `memory audit --fix` that silently degraded to a
|
|
365
|
+
// plain audit would read as a repair attempt that reproduced the failure.
|
|
366
|
+
if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
|
|
367
|
+
throw new Error(`${usage} (unknown argument: ${parsed.rest[1]})`);
|
|
368
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
369
|
+
try {
|
|
370
|
+
if (parsed.rest[0] === "audit") {
|
|
371
|
+
const result = await client.request<{ ok: boolean; issues: unknown[] }>("memory.audit");
|
|
372
|
+
console.log(JSON.stringify(result.issues));
|
|
373
|
+
if (!result.ok) process.exitCode = 1;
|
|
374
|
+
} else if (parsed.rest[0] === "autolink") {
|
|
375
|
+
console.log(JSON.stringify(await client.request("memory.autolink")));
|
|
376
|
+
} else if (parsed.rest[0] === "search" && parsed.rest.slice(1).join(" ")) {
|
|
377
|
+
console.log(
|
|
378
|
+
JSON.stringify(await client.request("memory.search", { query: parsed.rest.slice(1).join(" ") })),
|
|
379
|
+
);
|
|
380
|
+
} else throw new Error(usage);
|
|
381
|
+
} finally {
|
|
382
|
+
await client.close();
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case "monitors": {
|
|
387
|
+
const listOptions =
|
|
388
|
+
parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), MONITOR_COLUMNS) : undefined;
|
|
389
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
390
|
+
try {
|
|
391
|
+
const [command, ...args] = parsed.rest;
|
|
392
|
+
if (command === "add" && args[0] === "--json" && args[1])
|
|
393
|
+
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args[1]))));
|
|
394
|
+
else if (command === "list") {
|
|
395
|
+
const options = listOptions as ListOptions;
|
|
396
|
+
const result = await client.request<{ monitors: MonitorRecord[] }>("monitor.list");
|
|
397
|
+
for (const line of renderList(MONITOR_COLUMNS, result.monitors, options, {
|
|
398
|
+
key: "monitors",
|
|
399
|
+
result,
|
|
400
|
+
}))
|
|
401
|
+
console.log(line);
|
|
402
|
+
} else if (command === "inspect" && args[0])
|
|
403
|
+
console.log(JSON.stringify(await client.request("monitor.inspect", { monitorId: args[0] })));
|
|
404
|
+
else if (command === "remove" && args[0])
|
|
405
|
+
console.log(JSON.stringify(await client.request("monitor.remove", { monitorId: args[0] })));
|
|
406
|
+
else if (command === "test" && args[0]) {
|
|
407
|
+
let eventType: string | undefined;
|
|
408
|
+
let payload: unknown = {};
|
|
409
|
+
for (let i = 1; i < args.length; i++) {
|
|
410
|
+
if (args[i] === "--type") eventType = args[++i];
|
|
411
|
+
else if (args[i] === "--payload") payload = JSON.parse(args[++i] ?? "");
|
|
412
|
+
}
|
|
413
|
+
console.log(
|
|
414
|
+
JSON.stringify(
|
|
415
|
+
await client.request("monitor.test", {
|
|
416
|
+
monitorId: args[0],
|
|
417
|
+
...(eventType ? { eventType } : {}),
|
|
418
|
+
payload,
|
|
419
|
+
}),
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
} else
|
|
423
|
+
throw new Error(
|
|
424
|
+
`usage: gajaeway monitors add --json '<MonitorSpec json>'|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|test <id> [--type T] [--payload J]`,
|
|
425
|
+
);
|
|
426
|
+
} finally {
|
|
427
|
+
await client.close();
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "work": {
|
|
432
|
+
const [command, ...args] = parsed.rest;
|
|
433
|
+
const usage =
|
|
434
|
+
'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] "<task text>"|status <name>|steer <name> <text>|retire <name>|jobs';
|
|
435
|
+
if (command === "status" || command === "steer") {
|
|
436
|
+
const name = args[0];
|
|
437
|
+
const text = args.slice(1).join(" ").trim();
|
|
438
|
+
if (
|
|
439
|
+
!name ||
|
|
440
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) ||
|
|
441
|
+
(command === "status" ? args.length !== 1 : !text || args.slice(1).some((arg) => arg.startsWith("--")))
|
|
442
|
+
)
|
|
443
|
+
throw new Error(usage);
|
|
444
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
445
|
+
try {
|
|
446
|
+
if (command === "status") {
|
|
447
|
+
console.log(JSON.stringify(await client.request<WorkStatusResult>("work.status", { name })));
|
|
448
|
+
} else {
|
|
449
|
+
const result = await client.request<WorkSteerResult>("work.steer", { name, text });
|
|
450
|
+
console.log(result.steered ? `steered: ${result.clientRef}` : `not steered: ${result.reason}`);
|
|
451
|
+
if (!result.steered) process.exitCode = 1;
|
|
452
|
+
}
|
|
453
|
+
} finally {
|
|
454
|
+
await client.close();
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (command === "retire" || command === "jobs") {
|
|
459
|
+
if (command === "retire" ? args.length !== 1 || !args[0] || args[0].startsWith("--") : args.length !== 0)
|
|
460
|
+
throw new Error(usage);
|
|
461
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
462
|
+
try {
|
|
463
|
+
if (command === "retire") {
|
|
464
|
+
const result = await client.request<WorkRetireResult>("work.retire", { name: args[0] });
|
|
465
|
+
console.log(
|
|
466
|
+
result.retired
|
|
467
|
+
? `retired: ${result.sessionKey} session=${result.sessionId} closed=${result.closed}`
|
|
468
|
+
: `not retired: ${result.reason}`,
|
|
469
|
+
);
|
|
470
|
+
} else {
|
|
471
|
+
const result = await client.request<WorkJobsResult>("work.jobs");
|
|
472
|
+
for (const job of result.jobs) {
|
|
473
|
+
// lane_key is the stored `work-<name>`; print the name work.retire accepts.
|
|
474
|
+
const name = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
475
|
+
const state = job.quarantined
|
|
476
|
+
? `HELD: quarantined reason=${job.reason} historical_state=${job.state}`
|
|
477
|
+
: job.state;
|
|
478
|
+
console.log(
|
|
479
|
+
`${name} ${state} session=${job.session_id || "-"} last=${job.last_activity_at || "-"} ${job.worktree_path}`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
await client.close();
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
if (command !== "run" && command !== "start") throw new Error(usage);
|
|
489
|
+
const name = args[0];
|
|
490
|
+
let cwd: string | undefined;
|
|
491
|
+
let resume = false;
|
|
492
|
+
let model: string | { preset: string } | undefined;
|
|
493
|
+
let notify: OriginRef | undefined;
|
|
494
|
+
const textParts: string[] = [];
|
|
495
|
+
for (let i = 1; i < args.length; i++) {
|
|
496
|
+
const arg = args[i];
|
|
497
|
+
if (arg === "--cwd" || arg === "--model" || arg === "--preset" || arg === "--notify") {
|
|
498
|
+
const value = args[++i];
|
|
499
|
+
if (!value?.trim() || value.startsWith("--")) throw new Error(usage);
|
|
500
|
+
if (arg === "--cwd") {
|
|
501
|
+
if (cwd !== undefined || !isAbsolute(value)) throw new Error(usage);
|
|
502
|
+
cwd = value;
|
|
503
|
+
} else if (arg === "--notify") {
|
|
504
|
+
if (command !== "start" || notify !== undefined) throw new Error(usage);
|
|
505
|
+
try {
|
|
506
|
+
notify = parseOriginKey(value);
|
|
507
|
+
} catch {
|
|
508
|
+
throw new Error(`${usage}\ninvalid --notify originKey`);
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
if (model !== undefined) throw new Error(`${usage}\n--model and --preset are mutually exclusive`);
|
|
512
|
+
model = arg === "--preset" ? { preset: value } : value;
|
|
513
|
+
}
|
|
514
|
+
} else if (arg === "--resume") resume = true;
|
|
515
|
+
else if (arg?.startsWith("--")) throw new Error(usage);
|
|
516
|
+
else textParts.push(arg as string);
|
|
517
|
+
}
|
|
518
|
+
const text = textParts.join(" ").trim();
|
|
519
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text) throw new Error(usage);
|
|
520
|
+
// This is a caller response-wait budget, never an attempt deadline.
|
|
521
|
+
// Timeout/disconnect leaves the attempt observable via work.status.
|
|
522
|
+
const client = await GajaewayClient.connectSocket(parsed.socket, {
|
|
523
|
+
requestTimeoutMs: command === "run" ? 3_600_000 : 120_000,
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
const params: WorkStartParams = {
|
|
527
|
+
name,
|
|
528
|
+
text,
|
|
529
|
+
...(cwd ? { cwd } : {}),
|
|
530
|
+
...(resume ? { resume: true } : {}),
|
|
531
|
+
...(model === undefined ? {} : { model }),
|
|
532
|
+
...(notify === undefined ? {} : { notify }),
|
|
533
|
+
};
|
|
534
|
+
if (command === "start") {
|
|
535
|
+
const result = await client.request<WorkStartResult>("work.start", params);
|
|
536
|
+
if (result.started) {
|
|
537
|
+
console.log(
|
|
538
|
+
`started: ${result.sessionKey} session=${result.sessionId} job=${result.jobId} op=${result.opRef}`,
|
|
539
|
+
);
|
|
540
|
+
} else {
|
|
541
|
+
console.log(`HELD: ${result.reason}\njob: ${result.jobId} state: ${result.state}`);
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
const result = await client.request<WorkRunResult>("work.run", params);
|
|
546
|
+
if (result.held) {
|
|
547
|
+
console.log(`HELD: ${result.reason}\njob: ${result.jobId} state: ${result.state}`);
|
|
548
|
+
process.exitCode = 1;
|
|
549
|
+
} else console.log(result.text);
|
|
550
|
+
}
|
|
551
|
+
} finally {
|
|
552
|
+
await client.close();
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case "services": {
|
|
557
|
+
const service = parseServicesArgs(parsed.rest);
|
|
558
|
+
const written = await installServices({
|
|
559
|
+
binDir: service.binDir,
|
|
560
|
+
...(service.launchAgentsDir === undefined ? {} : { launchAgentsDir: service.launchAgentsDir }),
|
|
561
|
+
env: process.env,
|
|
562
|
+
...options.services,
|
|
563
|
+
});
|
|
564
|
+
console.log(`services ${service.action}: wrote ${written.length} LaunchAgent plists`);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
throw new Error(CLI_USAGE);
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
572
|
+
process.exitCode = 1;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (import.meta.main) await main();
|