@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/main.js CHANGED
@@ -2,11 +2,63 @@
2
2
  // @bun
3
3
 
4
4
  // src/main.ts
5
- import { Database } from "bun:sqlite";
6
- import { copyFile as copyFile2, lstat as lstat2, readFile as readFile8, stat as stat3 } from "fs/promises";
7
- import { isAbsolute, join as join10 } from "path";
8
- import { LOOPBACK_ORIGIN, originKey as originKey2 } from "@gajae-gateway/protocol";
5
+ import { isAbsolute } from "path";
6
+ import { LOOPBACK_ORIGIN, originKey as originKey2, WORK_JOBS_MAX_LIMIT } from "@gajae-gateway/protocol";
9
7
  import { GajaewayClient } from "@gajae-gateway/sdk";
8
+ // package.json
9
+ var package_default = {
10
+ name: "@gajae-gateway/cli",
11
+ version: "0.2.1",
12
+ type: "module",
13
+ description: "Owner CLI for the gajaeway gateway: status, sessions, chat, monitors, and worker lanes over the gateway socket. Requires the Bun runtime (uses Bun.file/Bun.spawn).",
14
+ license: "AGPL-3.0-only",
15
+ author: "Yeachan Heo",
16
+ repository: {
17
+ type: "git",
18
+ url: "git+https://github.com/Yeachan-Heo/gajae-way.git",
19
+ directory: "packages/cli"
20
+ },
21
+ homepage: "https://github.com/Yeachan-Heo/gajae-way/tree/main/packages/cli#readme",
22
+ bugs: "https://github.com/Yeachan-Heo/gajae-way/issues",
23
+ keywords: [
24
+ "gajaeway",
25
+ "cli",
26
+ "gateway"
27
+ ],
28
+ files: [
29
+ "dist",
30
+ "src"
31
+ ],
32
+ main: "./dist/main.js",
33
+ types: "./src/main.ts",
34
+ exports: {
35
+ ".": {
36
+ bun: "./src/main.ts",
37
+ types: "./src/main.ts",
38
+ default: "./dist/main.js"
39
+ }
40
+ },
41
+ bin: {
42
+ gajaeway: "./dist/main.js"
43
+ },
44
+ publishConfig: {
45
+ access: "public"
46
+ },
47
+ scripts: {
48
+ build: "rm -rf dist && bun build src/main.ts --outdir dist --target bun --format esm --external @gajae-gateway/protocol --external @gajae-gateway/sdk && tsc -p tsconfig.build.json",
49
+ prepublishOnly: "bun run build"
50
+ },
51
+ dependencies: {
52
+ "@gajae-gateway/sdk": "workspace:*",
53
+ "@gajae-gateway/protocol": "workspace:*"
54
+ },
55
+ engines: {
56
+ bun: ">=1.4.0"
57
+ },
58
+ devDependencies: {
59
+ "@gajae-gateway/home": "workspace:*"
60
+ }
61
+ };
10
62
 
11
63
  // src/list.ts
12
64
  import { originKey } from "@gajae-gateway/protocol";
@@ -145,11 +197,203 @@ function renderList(columns, rows, options, envelope) {
145
197
  }
146
198
 
147
199
  // src/migrate.ts
148
- import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
149
- import { join as join4 } from "path";
200
+ import { randomUUID } from "crypto";
201
+ import { chmod, mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile } from "fs/promises";
202
+ import { join as join5 } from "path";
203
+
204
+ // ../home/src/layout.ts
205
+ import { join } from "path";
206
+ function homeLayout(home) {
207
+ const leasesDir = join(home, "leases");
208
+ const brokerDir = join(home, "broker");
209
+ const brokerProvisionDir = join(brokerDir, "provision");
210
+ const gjcPrefix = join(home, "gjc");
211
+ const gjcCurrent = join(gjcPrefix, "current");
212
+ return {
213
+ home,
214
+ leasesDir,
215
+ lease: (name) => join(leasesDir, name),
216
+ brokerAgentDir: join(brokerDir, "agent"),
217
+ brokerProvisionDir,
218
+ brokerProvisionManifest: join(brokerProvisionDir, "manifest.json"),
219
+ backupsDir: join(home, "backups"),
220
+ gjcPrefix,
221
+ gjcCurrent,
222
+ gjcExecutable: (version) => join(version === undefined ? gjcCurrent : join(gjcPrefix, version), "bin", "gjc"),
223
+ socket: join(home, "gateway.sock"),
224
+ db: join(home, "gateway.db")
225
+ };
226
+ }
227
+ // ../home/src/lease.ts
228
+ import { dlopen, FFIType, read } from "bun:ffi";
229
+ import { constants } from "fs";
230
+ import { mkdir, open, readFile } from "fs/promises";
231
+ var LOCK_EX = 2;
232
+ var LOCK_NB = 4;
233
+ var F_GETFD = 1;
234
+ var FD_CLOEXEC = 1;
235
+ var MAX_OWNER_BYTES = 4096;
236
+ var PLATFORMS = {
237
+ darwin: { library: "/usr/lib/libSystem.B.dylib", errnoSymbol: "__error", oCloexec: 16777216, ewouldblock: 35 },
238
+ linux: { library: "libc.so.6", errnoSymbol: "__errno_location", oCloexec: 524288, ewouldblock: 11 }
239
+ };
240
+ var libc;
241
+ var heldHandles = new Set;
242
+ function loadLibc(name) {
243
+ if (libc)
244
+ return libc;
245
+ const platform = PLATFORMS[process.platform];
246
+ if (!platform)
247
+ throw new LeaseError("lease_unsupported", name, "dlopen", process.platform);
248
+ const symbols = {
249
+ flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
250
+ fcntl: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
251
+ [platform.errnoSymbol]: { args: [], returns: FFIType.ptr }
252
+ };
253
+ const library = dlopen(platform.library, symbols);
254
+ const errnoLocation = library.symbols[platform.errnoSymbol];
255
+ libc = {
256
+ platform,
257
+ flock: (fd, operation) => library.symbols.flock(fd, operation),
258
+ getfd: (fd) => library.symbols.fcntl(fd, F_GETFD),
259
+ errno: () => read.i32(errnoLocation(), 0)
260
+ };
261
+ return libc;
262
+ }
263
+
264
+ class LeaseHeldError extends Error {
265
+ name;
266
+ owner;
267
+ code = "lease_held";
268
+ constructor(name, owner) {
269
+ super(`lease_held name=${name}${owner ? ` owner_pid=${owner.pid} since=${owner.startedAt}` : ""}`);
270
+ this.name = name;
271
+ this.owner = owner;
272
+ }
273
+ }
150
274
 
275
+ class LeaseError extends Error {
276
+ code;
277
+ name;
278
+ syscall;
279
+ errno;
280
+ constructor(code, name, syscall, errno) {
281
+ super(`${code} name=${name} syscall=${syscall} errno=${errno}`);
282
+ this.code = code;
283
+ this.name = name;
284
+ this.syscall = syscall;
285
+ this.errno = errno;
286
+ }
287
+ }
288
+ async function openLeaseFile(path, name, lib, create) {
289
+ const flags = constants.O_RDWR | constants.O_NOFOLLOW | lib.platform.oCloexec | (create ? constants.O_CREAT : 0);
290
+ let handle;
291
+ try {
292
+ handle = await open(path, flags, 384);
293
+ } catch (error) {
294
+ const code = error.code ?? String(error);
295
+ throw new LeaseError("lease_failed", name, "open", code);
296
+ }
297
+ try {
298
+ const info = await handle.stat();
299
+ if (!info.isFile() || info.nlink !== 1)
300
+ throw new LeaseError("lease_failed", name, "fstat", "not_regular_file");
301
+ const fdFlags = lib.getfd(handle.fd);
302
+ if (fdFlags < 0 || (fdFlags & FD_CLOEXEC) === 0)
303
+ throw new LeaseError("lease_failed", name, "fcntl", fdFlags < 0 ? lib.errno() : "cloexec_missing");
304
+ return handle;
305
+ } catch (error) {
306
+ await handle.close();
307
+ throw error;
308
+ }
309
+ }
310
+ function tryLock(handle, name, lib) {
311
+ if (lib.flock(handle.fd, LOCK_EX | LOCK_NB) === 0)
312
+ return true;
313
+ const errno = lib.errno();
314
+ if (errno === lib.platform.ewouldblock)
315
+ return false;
316
+ throw new LeaseError("lease_failed", name, "flock", errno);
317
+ }
318
+ function parseOwner(raw) {
319
+ if (!raw.trim() || raw.length > MAX_OWNER_BYTES)
320
+ return;
321
+ try {
322
+ const value = JSON.parse(raw);
323
+ if (Number.isSafeInteger(value.pid) && typeof value.token === "string" && typeof value.startedAt === "string")
324
+ return { pid: value.pid, token: value.token, startedAt: value.startedAt };
325
+ } catch {}
326
+ return;
327
+ }
328
+ async function readLeaseOwner(home, name) {
329
+ try {
330
+ return parseOwner(await readFile(homeLayout(home).lease(name), "utf8"));
331
+ } catch {
332
+ return;
333
+ }
334
+ }
335
+ async function leaseHeld(home, name) {
336
+ const lib = loadLibc(name);
337
+ let handle;
338
+ try {
339
+ handle = await openLeaseFile(homeLayout(home).lease(name), name, lib, false);
340
+ } catch (error) {
341
+ if (error instanceof LeaseError && error.syscall === "open" && error.errno === "ENOENT")
342
+ return false;
343
+ throw error;
344
+ }
345
+ try {
346
+ return !tryLock(handle, name, lib);
347
+ } finally {
348
+ await handle.close();
349
+ }
350
+ }
351
+ async function acquireLease(home, name) {
352
+ const lib = loadLibc(name);
353
+ const layout = homeLayout(home);
354
+ try {
355
+ await mkdir(layout.leasesDir, { recursive: true, mode: 448 });
356
+ } catch (error) {
357
+ throw new LeaseError("lease_failed", name, "mkdir", error.code ?? String(error));
358
+ }
359
+ const path = layout.lease(name);
360
+ const handle = await openLeaseFile(path, name, lib, true);
361
+ try {
362
+ if (!tryLock(handle, name, lib))
363
+ throw new LeaseHeldError(name, await readLeaseOwner(home, name));
364
+ const owner = { pid: process.pid, token: crypto.randomUUID(), startedAt: new Date().toISOString() };
365
+ const bytes = Buffer.from(`${JSON.stringify(owner)}
366
+ `);
367
+ try {
368
+ await handle.truncate(0);
369
+ let offset = 0;
370
+ while (offset < bytes.length) {
371
+ const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, offset);
372
+ if (bytesWritten === 0)
373
+ throw new Error("short write");
374
+ offset += bytesWritten;
375
+ }
376
+ await handle.sync();
377
+ } catch (error) {
378
+ throw new LeaseError("lease_failed", name, "write", error.code ?? String(error));
379
+ }
380
+ heldHandles.add(handle);
381
+ let released = false;
382
+ const release = async () => {
383
+ if (released)
384
+ return;
385
+ released = true;
386
+ heldHandles.delete(handle);
387
+ await handle.close();
388
+ };
389
+ return { name, token: owner.token, path, release, [Symbol.asyncDispose]: release };
390
+ } catch (error) {
391
+ await handle.close();
392
+ throw error;
393
+ }
394
+ }
151
395
  // src/migrate/hermes.ts
152
- import { join as join2 } from "path";
396
+ import { join as join3 } from "path";
153
397
 
154
398
  // src/migrate/plan.ts
155
399
  import { eventTypeOrigin, validateOriginRef } from "@gajae-gateway/protocol";
@@ -385,9 +629,9 @@ function emitImportedAxis(extract) {
385
629
  }
386
630
 
387
631
  // src/migrate/source-fs.ts
388
- import { lstat, readdir, readFile } from "fs/promises";
632
+ import { lstat, readdir, readFile as readFile2 } from "fs/promises";
389
633
  import { homedir } from "os";
390
- import { join, relative } from "path";
634
+ import { join as join2, relative } from "path";
391
635
 
392
636
  class SourceTree {
393
637
  root;
@@ -419,14 +663,14 @@ class SourceTree {
419
663
  if (!info)
420
664
  return;
421
665
  this.consume(path);
422
- return readFile(path, "utf8");
666
+ return readFile2(path, "utf8");
423
667
  }
424
668
  async bytes(path) {
425
669
  const info = await regularFile(path);
426
670
  if (!info)
427
671
  return;
428
672
  this.consume(path);
429
- return new Uint8Array(await readFile(path));
673
+ return new Uint8Array(await readFile2(path));
430
674
  }
431
675
  }
432
676
  async function regularFile(path) {
@@ -454,16 +698,16 @@ async function entries(path) {
454
698
  async function walkFiles(dir, prefix = "") {
455
699
  const out = [];
456
700
  for (const name of await entries(dir)) {
457
- const full = join(dir, name);
701
+ const full = join2(dir, name);
458
702
  const info = await lstat(full).catch(() => {
459
703
  return;
460
704
  });
461
705
  if (!info)
462
706
  continue;
463
707
  if (info.isDirectory())
464
- out.push(...await walkFiles(full, join(prefix, name)));
708
+ out.push(...await walkFiles(full, join2(prefix, name)));
465
709
  else if (info.isFile())
466
- out.push(join(prefix, name));
710
+ out.push(join2(prefix, name));
467
711
  }
468
712
  return out;
469
713
  }
@@ -487,7 +731,7 @@ function parseDotenv(text) {
487
731
  return values;
488
732
  }
489
733
  function expandHome(path) {
490
- return path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
734
+ return path === "~" ? homedir() : path.startsWith("~/") ? join2(homedir(), path.slice(2)) : path;
491
735
  }
492
736
  function isRecord(value) {
493
737
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -502,7 +746,7 @@ var HERMES_ENTRY_DELIMITER = `
502
746
  \xA7
503
747
  `;
504
748
  async function detectHermes(root) {
505
- return await regularFile(join2(root, "config.yaml")) !== undefined || await isDirectory(join2(root, "memories")) || await regularFile(join2(root, "cron", "jobs.json")) !== undefined;
749
+ return await regularFile(join3(root, "config.yaml")) !== undefined || await isDirectory(join3(root, "memories")) || await regularFile(join3(root, "cron", "jobs.json")) !== undefined;
506
750
  }
507
751
  var TOKEN_KEYS = {
508
752
  DISCORD_BOT_TOKEN: "discord",
@@ -520,7 +764,7 @@ async function readHermes(tree, home) {
520
764
  const root = tree.root;
521
765
  const extract = emptyExtract("hermes", root);
522
766
  for (const name of ["SOUL.md", "AGENTS.md"]) {
523
- const path = join2(root, name);
767
+ const path = join3(root, name);
524
768
  const text = await tree.text(path);
525
769
  if (text === undefined)
526
770
  continue;
@@ -534,8 +778,8 @@ async function readHermes(tree, home) {
534
778
  mtime: info?.mtime
535
779
  });
536
780
  }
537
- const memoryDir = join2(root, "memories");
538
- const userPath = join2(memoryDir, "USER.md");
781
+ const memoryDir = join3(root, "memories");
782
+ const userPath = join3(memoryDir, "USER.md");
539
783
  const userText = await tree.text(userPath);
540
784
  if (userText !== undefined) {
541
785
  const info = await regularFile(userPath);
@@ -548,7 +792,7 @@ async function readHermes(tree, home) {
548
792
  mtime: info?.mtime
549
793
  });
550
794
  }
551
- const memoryPath = join2(memoryDir, "MEMORY.md");
795
+ const memoryPath = join3(memoryDir, "MEMORY.md");
552
796
  const memoryText = await tree.text(memoryPath);
553
797
  if (memoryText !== undefined) {
554
798
  const info = await regularFile(memoryPath);
@@ -560,13 +804,12 @@ async function readHermes(tree, home) {
560
804
  content: entriesToMarkdown("Long-term memory (imported from Hermes)", memoryText),
561
805
  mtime: info?.mtime
562
806
  });
563
- emitImportedAxis(extract);
564
807
  }
565
- const envPath = join2(root, ".env");
808
+ const envPath = join3(root, ".env");
566
809
  const envText = await tree.text(envPath);
567
810
  const env = envText === undefined ? new Map : parseDotenv(envText);
568
811
  const consumedEnv = new Set;
569
- const configPath = join2(root, "config.yaml");
812
+ const configPath = join3(root, "config.yaml");
570
813
  const configText = await tree.text(configPath);
571
814
  let config = {};
572
815
  if (configText !== undefined) {
@@ -693,7 +936,7 @@ function readHermesChannelPolicy(config, env, channels, consumedConfig, consumed
693
936
  }
694
937
  }
695
938
  async function readHermesCron(tree, env, consumedEnv, extract) {
696
- const path = join2(tree.root, "cron", "jobs.json");
939
+ const path = join3(tree.root, "cron", "jobs.json");
697
940
  const text = await tree.text(path);
698
941
  if (text === undefined)
699
942
  return;
@@ -793,11 +1036,11 @@ function hermesTarget(job, env, consumedEnv, extract, source) {
793
1036
  return resolved[0];
794
1037
  }
795
1038
  async function readHermesSkills(tree, extract) {
796
- const skillsDir = join2(tree.root, "skills");
1039
+ const skillsDir = join3(tree.root, "skills");
797
1040
  if (!await isDirectory(skillsDir))
798
1041
  return;
799
1042
  tree.consume(skillsDir);
800
- const bundledText = await tree.text(join2(skillsDir, ".bundled_manifest"));
1043
+ const bundledText = await tree.text(join3(skillsDir, ".bundled_manifest"));
801
1044
  const bundled = new Set((bundledText ?? "").split(/\r?\n/).map((line) => line.split(":")[0]?.trim() ?? "").filter(Boolean));
802
1045
  const files = (await walkFiles(skillsDir)).filter((file) => !file.split("/").some((part) => part.startsWith(".")));
803
1046
  const skillRoots = files.filter((file) => file.endsWith("/SKILL.md") || file === "SKILL.md").map((file) => file.slice(0, -"/SKILL.md".length));
@@ -806,7 +1049,7 @@ async function readHermesSkills(tree, extract) {
806
1049
  const name = skillRoot.split("/").at(-1) ?? skillRoot;
807
1050
  if (bundled.has(name))
808
1051
  continue;
809
- await copySkill(tree, join2(skillsDir, skillRoot), name, extract, seen);
1052
+ await copySkill(tree, join3(skillsDir, skillRoot), name, extract, seen);
810
1053
  }
811
1054
  if (bundled.size)
812
1055
  extract.warnings.push(`${bundled.size} Hermes-bundled skill(s) skipped; they ship with Hermes, not with your data`);
@@ -824,7 +1067,7 @@ async function copySkill(tree, dir, name, extract, seen) {
824
1067
  }
825
1068
  seen.add(name);
826
1069
  for (const file of await walkFiles(dir)) {
827
- const path = join2(dir, file);
1070
+ const path = join3(dir, file);
828
1071
  const content = await tree.bytes(path);
829
1072
  if (content === undefined)
830
1073
  continue;
@@ -839,17 +1082,17 @@ async function copySkill(tree, dir, name, extract, seen) {
839
1082
  }
840
1083
 
841
1084
  // src/migrate/openclaw.ts
842
- import { join as join3 } from "path";
1085
+ import { join as join4 } from "path";
843
1086
  var OPENCLAW_CONFIG = "config.json";
844
1087
  async function detectOpenClaw(root) {
845
- const configPath = join3(root, OPENCLAW_CONFIG);
1088
+ const configPath = join4(root, OPENCLAW_CONFIG);
846
1089
  const stat = await regularFile(configPath);
847
1090
  return stat !== undefined;
848
1091
  }
849
1092
  async function readOpenClaw(tree, _home) {
850
1093
  const extract = emptyExtract("openclaw", tree.root);
851
- const configPath = join3(tree.root, OPENCLAW_CONFIG);
852
- const envPath = join3(tree.root, ".env");
1094
+ const configPath = join4(tree.root, OPENCLAW_CONFIG);
1095
+ const envPath = join4(tree.root, ".env");
853
1096
  let config = {};
854
1097
  const configText = await tree.text(configPath);
855
1098
  if (configText) {
@@ -999,25 +1242,25 @@ function buildOpenClawMonitor(name, spec) {
999
1242
  });
1000
1243
  }
1001
1244
  async function readOpenClawSkills(tree, skillsConfig, extract) {
1002
- const skillsDir = join3(tree.root, "skills");
1245
+ const skillsDir = join4(tree.root, "skills");
1003
1246
  const hasSkillsDir = await isDirectory(skillsDir);
1004
1247
  for (const [skillName] of Object.entries(skillsConfig)) {
1005
1248
  if (!hasSkillsDir)
1006
1249
  continue;
1007
- const skillPath = join3(skillsDir, `${skillName}.md`);
1250
+ const skillPath = join4(skillsDir, `${skillName}.md`);
1008
1251
  const skillContent = await tree.text(skillPath);
1009
1252
  if (skillContent) {
1010
1253
  extract.files.push({
1011
1254
  category: "skills",
1012
1255
  sources: [skillPath],
1013
- target: join3(".gjc", "skills", `${slug(skillName)}.md`),
1256
+ target: join4(".gjc", "skills", `${slug(skillName)}.md`),
1014
1257
  content: skillContent
1015
1258
  });
1016
1259
  }
1017
1260
  }
1018
1261
  }
1019
1262
  async function readOpenClawMemory(tree, _memoryConfig, extract) {
1020
- const memoryDir = join3(tree.root, "memory");
1263
+ const memoryDir = join4(tree.root, "memory");
1021
1264
  const hasMemoryDir = await isDirectory(memoryDir);
1022
1265
  if (!hasMemoryDir) {
1023
1266
  return;
@@ -1025,13 +1268,13 @@ async function readOpenClawMemory(tree, _memoryConfig, extract) {
1025
1268
  const files = await walkFiles(memoryDir);
1026
1269
  for (const file of files) {
1027
1270
  if (file.endsWith(".md")) {
1028
- const fullPath = join3(memoryDir, file);
1271
+ const fullPath = join4(memoryDir, file);
1029
1272
  const content = await tree.text(fullPath);
1030
1273
  if (content) {
1031
1274
  extract.files.push({
1032
1275
  category: "memory",
1033
1276
  sources: [fullPath],
1034
- target: join3("memory", "imported.md"),
1277
+ target: join4("memory", "imported.md"),
1035
1278
  content
1036
1279
  });
1037
1280
  }
@@ -1041,8 +1284,25 @@ async function readOpenClawMemory(tree, _memoryConfig, extract) {
1041
1284
 
1042
1285
  // src/migrate.ts
1043
1286
  async function migrate(options) {
1287
+ const targetHome = expandHome(options.target ?? process.env.GAJAEWAY_HOME ?? `${process.env.HOME ?? "~"}/.gajaeway`);
1288
+ if (options.dryRun)
1289
+ return migrateUnlocked(options, targetHome);
1290
+ let lease;
1291
+ try {
1292
+ lease = await acquireLease(targetHome, "memory");
1293
+ } catch (error) {
1294
+ if (error instanceof LeaseHeldError)
1295
+ throw new Error(`${error.message}; stop the gateway first before importing memory data`, { cause: error });
1296
+ throw error;
1297
+ }
1298
+ try {
1299
+ await migrateUnlocked(options, targetHome);
1300
+ } finally {
1301
+ await lease.release();
1302
+ }
1303
+ }
1304
+ async function migrateUnlocked(options, targetHome) {
1044
1305
  const sourceRoot = expandHome(options.source ?? ".");
1045
- const targetHome = expandHome(options.target ?? `${process.env.HOME ?? "~"}/.gajaeway`);
1046
1306
  let sourceType;
1047
1307
  if (await detectHermes(sourceRoot)) {
1048
1308
  sourceType = "hermes";
@@ -1099,22 +1359,34 @@ Migration summary:`);
1099
1359
  }
1100
1360
  return;
1101
1361
  }
1102
- await mkdir(targetHome, { recursive: true });
1362
+ await mkdir2(targetHome, { recursive: true });
1103
1363
  const channels = emptyChannelPlan();
1104
1364
  emitChannels(extract, channels, targetHome, sourceType);
1105
1365
  emitMonitors(extract);
1106
1366
  emitImportedAxis(extract);
1107
1367
  for (const file of extract.files) {
1108
- const filePath = join4(targetHome, file.target);
1109
- await mkdir(join4(filePath, ".."), { recursive: true });
1110
- await writeFile(filePath, file.content);
1368
+ const filePath = join5(targetHome, file.target);
1369
+ await mkdir2(join5(filePath, ".."), { recursive: true });
1370
+ if (file.secret) {
1371
+ const temporary = `${filePath}.${randomUUID()}.tmp`;
1372
+ try {
1373
+ await writeFile(temporary, file.content, { mode: 384 });
1374
+ await chmod(temporary, 384);
1375
+ await rename(temporary, filePath);
1376
+ } catch (error) {
1377
+ await rm(temporary, { force: true });
1378
+ throw error;
1379
+ }
1380
+ } else {
1381
+ await writeFile(filePath, file.content);
1382
+ }
1111
1383
  }
1112
1384
  for (const merge of extract.merges) {
1113
- const filePath = join4(targetHome, merge.target);
1114
- await mkdir(join4(filePath, ".."), { recursive: true });
1385
+ const filePath = join5(targetHome, merge.target);
1386
+ await mkdir2(join5(filePath, ".."), { recursive: true });
1115
1387
  let existing;
1116
1388
  try {
1117
- const text = await readFile2(filePath, "utf-8");
1389
+ const text = await readFile3(filePath, "utf-8");
1118
1390
  existing = JSON.parse(text);
1119
1391
  } catch {}
1120
1392
  const merged = merge.merge(existing);
@@ -1145,21 +1417,29 @@ function parseMigrateArgs(args) {
1145
1417
  }
1146
1418
 
1147
1419
  // src/restart-stack.ts
1148
- import { randomUUID } from "crypto";
1149
- import { access, mkdir as mkdir3, readFile as readFile4, rename, stat, writeFile as writeFile3 } from "fs/promises";
1420
+ import { randomUUID as randomUUID2 } from "crypto";
1421
+ import { access, mkdir as mkdir4, readFile as readFile5, rename as rename2, stat, writeFile as writeFile3 } from "fs/promises";
1150
1422
  import { homedir as homedir3 } from "os";
1151
- import { join as join6 } from "path";
1423
+ import { dirname, join as join7 } from "path";
1152
1424
 
1153
1425
  // src/services.ts
1154
- import { chmod, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1426
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
1155
1427
  import { homedir as homedir2 } from "os";
1156
- import { join as join5 } from "path";
1428
+ import { join as join6 } from "path";
1157
1429
  import { parseRuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol";
1158
1430
  var CONFIG_SCHEMA_VERSION = 1;
1159
1431
  var FALLBACK_PATH_DIRS = ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
1160
1432
  var LOGIN_PATH_COMMAND = 'printf %s "$PATH"';
1433
+ var BROKER_UNIT = "gajaeway-broker.service";
1161
1434
  var GATEWAY_UNIT = "gajaeway-gateway.service";
1162
1435
  var SERVICE_SPECS = [
1436
+ {
1437
+ id: "broker",
1438
+ label: "dev.gajaeway.broker",
1439
+ binary: "gajaeway-gateway",
1440
+ args: ["broker"],
1441
+ dependsOnGateway: false
1442
+ },
1163
1443
  {
1164
1444
  id: "gateway",
1165
1445
  label: "dev.gajaeway.gateway",
@@ -1181,16 +1461,23 @@ var SERVICE_SPECS = [
1181
1461
  args: [],
1182
1462
  dependsOnGateway: true
1183
1463
  },
1464
+ {
1465
+ id: "adapter-telegram",
1466
+ label: "dev.gajaeway.adapter-telegram",
1467
+ binary: "gajaeway-telegram",
1468
+ args: [],
1469
+ dependsOnGateway: true
1470
+ },
1184
1471
  { id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"], dependsOnGateway: true }
1185
1472
  ];
1186
1473
  function serviceSpecs() {
1187
1474
  return SERVICE_SPECS;
1188
1475
  }
1189
1476
  function homeForEnvironment(env) {
1190
- return env.GAJAEWAY_HOME || join5(env.HOME || homedir2(), ".gajaeway");
1477
+ return env.GAJAEWAY_HOME || join6(env.HOME || homedir2(), ".gajaeway");
1191
1478
  }
1192
1479
  function expandHome2(value, home) {
1193
- return value.startsWith("~/") ? join5(home, value.slice(2)) : value;
1480
+ return value.startsWith("~/") ? join6(home, value.slice(2)) : value;
1194
1481
  }
1195
1482
  function normalizedEntries(entries2, home) {
1196
1483
  const seen = new Set;
@@ -1209,9 +1496,9 @@ function normalizedEntries(entries2, home) {
1209
1496
  function mergePathEntries(binDir, userHome, loginPath) {
1210
1497
  return normalizedEntries([
1211
1498
  expandHome2(binDir, userHome),
1212
- join5(userHome, "bin"),
1213
- join5(userHome, ".local", "bin"),
1214
- join5(userHome, ".bun", "bin"),
1499
+ join6(userHome, "bin"),
1500
+ join6(userHome, ".local", "bin"),
1501
+ join6(userHome, ".bun", "bin"),
1215
1502
  ...loginPath.replace(/(?:\r\n|\n)+$/, "").split(":"),
1216
1503
  ...FALLBACK_PATH_DIRS
1217
1504
  ], userHome);
@@ -1254,7 +1541,7 @@ function plistString(value) {
1254
1541
  return `<string>${escapeXml(value)}</string>`;
1255
1542
  }
1256
1543
  function renderLaunchAgent(spec, binDir, home, path) {
1257
- const program = join5(binDir, spec.binary);
1544
+ const program = join6(binDir, spec.binary);
1258
1545
  const argumentsXml = [program, ...spec.args].map(plistString).join("");
1259
1546
  return `<?xml version="1.0" encoding="UTF-8"?>
1260
1547
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -1268,8 +1555,8 @@ function renderLaunchAgent(spec, binDir, home, path) {
1268
1555
  </dict>
1269
1556
  <key>RunAtLoad</key><true/>
1270
1557
  <key>KeepAlive</key><true/>
1271
- <key>StandardOutPath</key>${plistString(join5(home, `${spec.id}.stdout.log`))}
1272
- <key>StandardErrorPath</key>${plistString(join5(home, `${spec.id}.stderr.log`))}
1558
+ <key>StandardOutPath</key>${plistString(join6(home, `${spec.id}.stdout.log`))}
1559
+ <key>StandardErrorPath</key>${plistString(join6(home, `${spec.id}.stderr.log`))}
1273
1560
  </dict></plist>
1274
1561
  `;
1275
1562
  }
@@ -1279,21 +1566,30 @@ function unitEnvironment(name, value) {
1279
1566
  function systemdUnitName(spec) {
1280
1567
  return `gajaeway-${spec.id}.service`;
1281
1568
  }
1569
+ function unitPath(path) {
1570
+ if (!path.startsWith("/") || /[\n\r\0]/.test(path))
1571
+ throw new Error(`unit path must be absolute and single-line: ${path}`);
1572
+ return path.replaceAll("%", "%%");
1573
+ }
1574
+ function unitEnvironmentFile(path) {
1575
+ return `EnvironmentFile=-${unitPath(path)}`;
1576
+ }
1282
1577
  function renderSystemdUnit(spec, binDir, home, path) {
1283
- const execStart = [join5(binDir, spec.binary), ...spec.args].join(" ");
1578
+ const execStart = [join6(binDir, spec.binary), ...spec.args].join(" ");
1284
1579
  const unit = [
1285
1580
  "[Unit]",
1286
1581
  `Description=gajaeway ${spec.id}`,
1287
- ...spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
1582
+ ...spec.id === "broker" ? [`ConditionPathExists=${unitPath(homeLayout(home).brokerProvisionManifest)}`] : spec.id === "gateway" ? [`Wants=${BROKER_UNIT}`, `After=${BROKER_UNIT}`] : spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
1288
1583
  "",
1289
1584
  "[Service]",
1290
1585
  `ExecStart=${execStart}`,
1291
1586
  `WorkingDirectory=${binDir}`,
1292
1587
  unitEnvironment("GAJAEWAY_HOME", home),
1293
1588
  unitEnvironment("PATH", path),
1589
+ ...spec.id === "broker" ? [unitEnvironmentFile(join6(home, "broker.env"))] : [],
1294
1590
  "Restart=always",
1295
- "RestartSec=2",
1296
- ...spec.dependsOnGateway ? [] : ["KillMode=mixed", "TimeoutStopSec=30s"],
1591
+ spec.id === "broker" ? "RestartSec=2s" : "RestartSec=2",
1592
+ ...spec.id === "broker" ? ["KillMode=control-group", "TimeoutStopSec=30s", "SuccessExitStatus=143"] : spec.id === "gateway" ? ["KillMode=mixed", "TimeoutStopSec=30s"] : [],
1297
1593
  "",
1298
1594
  "[Install]",
1299
1595
  `WantedBy=${spec.dependsOnGateway ? GATEWAY_UNIT : "default.target"}`,
@@ -1303,18 +1599,21 @@ function renderSystemdUnit(spec, binDir, home, path) {
1303
1599
  `);
1304
1600
  }
1305
1601
  function systemdUnitDir(env, userHome) {
1306
- const base = env.XDG_CONFIG_HOME || join5(userHome, ".config");
1307
- return join5(base, "systemd", "user");
1602
+ const base = env.XDG_CONFIG_HOME || join6(userHome, ".config");
1603
+ return join6(base, "systemd", "user");
1308
1604
  }
1309
1605
  function launchAgentPlistPath(spec, launchAgentsDir) {
1310
- return join5(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`);
1606
+ return join6(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`);
1311
1607
  }
1312
1608
  function systemdUnitPath(spec, unitDir) {
1313
- return join5(unitDir, systemdUnitName(spec));
1609
+ return join6(unitDir, systemdUnitName(spec));
1314
1610
  }
1315
1611
  function restartStackCommands(platform, uid) {
1316
1612
  if (platform === "linux")
1317
- return [["systemctl", "--user", "restart", GATEWAY_UNIT]];
1613
+ return [
1614
+ ["systemctl", "--user", "restart", BROKER_UNIT],
1615
+ ["systemctl", "--user", "restart", GATEWAY_UNIT]
1616
+ ];
1318
1617
  if (platform === "darwin") {
1319
1618
  const target = uid ?? process.getuid?.() ?? 0;
1320
1619
  return restartOrder().map((spec) => ["launchctl", "kickstart", "-k", `gui/${target}/${spec.label}`]);
@@ -1338,10 +1637,10 @@ function currentPlatform() {
1338
1637
  throw new Error(`unsupported service platform: ${process.platform}`);
1339
1638
  }
1340
1639
  async function readRuntime(home) {
1341
- const configPath = join5(home, "config.json");
1640
+ const configPath = join6(home, "config.json");
1342
1641
  let raw;
1343
1642
  try {
1344
- raw = await readFile3(configPath, "utf8");
1643
+ raw = await readFile4(configPath, "utf8");
1345
1644
  } catch (error) {
1346
1645
  throw new Error(`could not read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
1347
1646
  }
@@ -1371,7 +1670,7 @@ async function installServices(options) {
1371
1670
  const userHome = env.HOME || homedir2();
1372
1671
  const home = homeForEnvironment(env);
1373
1672
  const binDir = expandHome2(options.binDir, userHome);
1374
- const targetDir = platform === "darwin" ? expandHome2(options.launchAgentsDir ?? join5(userHome, "Library", "LaunchAgents"), userHome) : expandHome2(options.unitDir ?? systemdUnitDir(env, userHome), userHome);
1673
+ const targetDir = platform === "darwin" ? expandHome2(options.launchAgentsDir ?? join6(userHome, "Library", "LaunchAgents"), userHome) : expandHome2(options.unitDir ?? systemdUnitDir(env, userHome), userHome);
1375
1674
  const runtime = await readRuntime(home);
1376
1675
  const path = await resolvePath({
1377
1676
  binDir,
@@ -1380,18 +1679,36 @@ async function installServices(options) {
1380
1679
  env,
1381
1680
  loginPathRunner: options.loginPathRunner
1382
1681
  });
1383
- await mkdir2(targetDir, { recursive: true, mode: 448 });
1384
- const definitions = SERVICE_SPECS.map((spec) => platform === "darwin" ? {
1385
- path: join5(targetDir, `dev.gajaeway.${spec.id}.plist`),
1682
+ await mkdir3(targetDir, { recursive: true, mode: 448 });
1683
+ const specs = [];
1684
+ for (const spec of SERVICE_SPECS) {
1685
+ if (options.configuredAdaptersOnly && spec.id.startsWith("adapter-")) {
1686
+ const adapter = spec.id.slice("adapter-".length);
1687
+ if (!await Bun.file(join6(home, `adapter-${adapter}.json`)).exists())
1688
+ continue;
1689
+ }
1690
+ specs.push(spec);
1691
+ }
1692
+ const definitions = specs.map((spec) => platform === "darwin" ? {
1693
+ path: join6(targetDir, `dev.gajaeway.${spec.id}.plist`),
1386
1694
  contents: renderLaunchAgent(spec, binDir, home, path)
1387
- } : { path: join5(targetDir, systemdUnitName(spec)), contents: renderSystemdUnit(spec, binDir, home, path) });
1695
+ } : { path: join6(targetDir, systemdUnitName(spec)), contents: renderSystemdUnit(spec, binDir, home, path) });
1388
1696
  const write = options.writeFile;
1389
1697
  for (const definition of definitions) {
1698
+ if (options.compareAndWrite) {
1699
+ try {
1700
+ if (await readFile4(definition.path, "utf8") === definition.contents)
1701
+ continue;
1702
+ } catch (error) {
1703
+ if (error.code !== "ENOENT")
1704
+ throw error;
1705
+ }
1706
+ }
1390
1707
  if (write)
1391
1708
  await write(definition.path, definition.contents);
1392
1709
  else {
1393
1710
  await writeFile2(definition.path, definition.contents, { encoding: "utf8", mode: 384 });
1394
- await chmod(definition.path, 384);
1711
+ await chmod2(definition.path, 384);
1395
1712
  }
1396
1713
  }
1397
1714
  return definitions.map((definition) => definition.path);
@@ -1405,12 +1722,12 @@ var RECEIPT_FILE = "restart-stack.json";
1405
1722
  var VERIFY_TIMEOUT_MS = 90000;
1406
1723
  var POLL_MS = 1000;
1407
1724
  function restartReceiptPath(home) {
1408
- return join6(home, RECEIPT_FILE);
1725
+ return join7(home, RECEIPT_FILE);
1409
1726
  }
1410
1727
  async function readRestartReceipt(home) {
1411
1728
  let raw;
1412
1729
  try {
1413
- raw = await readFile4(restartReceiptPath(home), "utf8");
1730
+ raw = await readFile5(restartReceiptPath(home), "utf8");
1414
1731
  } catch (error) {
1415
1732
  if (error.code === "ENOENT")
1416
1733
  return;
@@ -1419,23 +1736,24 @@ async function readRestartReceipt(home) {
1419
1736
  return JSON.parse(raw);
1420
1737
  }
1421
1738
  async function writeReceipt(home, receipt) {
1422
- await mkdir3(home, { recursive: true });
1739
+ await mkdir4(home, { recursive: true });
1423
1740
  const path = restartReceiptPath(home);
1424
1741
  const temporary = `${path}.${process.pid}.tmp`;
1425
1742
  await writeFile3(temporary, `${JSON.stringify(receipt, null, 2)}
1426
1743
  `, { mode: 384 });
1427
- await rename(temporary, path);
1744
+ await rename2(temporary, path);
1428
1745
  }
1429
- function newReceipt(id, platform, uid, now) {
1746
+ function newReceipt(id, platform, uid, now, trigger) {
1430
1747
  const commands = restartStackCommands(platform, uid);
1431
1748
  return {
1432
1749
  id,
1750
+ trigger,
1433
1751
  state: "queued",
1434
1752
  platform,
1435
1753
  requestedAt: new Date(now).toISOString(),
1436
1754
  steps: restartOrder().map((spec, index) => ({
1437
1755
  label: spec.label,
1438
- ...platform === "darwin" ? { command: commands[index] } : index === 0 ? { command: commands[0] } : {},
1756
+ ...platform === "darwin" ? { command: commands[index] } : spec.id === "broker" ? { command: commands[0] } : spec.id === "gateway" ? { command: commands[1] } : {},
1439
1757
  result: "pending"
1440
1758
  }))
1441
1759
  };
@@ -1454,33 +1772,17 @@ async function capture(command) {
1454
1772
  const output = await new Response(child.stdout).text();
1455
1773
  return await child.exited === 0 ? output : undefined;
1456
1774
  }
1457
- function defaultServiceProbe(platform) {
1775
+ function defaultServiceProbe(platform, uid = process.getuid?.() ?? 0) {
1458
1776
  return async (label) => {
1459
1777
  const spec = serviceSpecs().find((candidate) => candidate.label === label);
1460
1778
  if (spec === undefined)
1461
1779
  throw new Error(`unknown service label: ${label}`);
1462
- let pid;
1463
- if (platform === "darwin") {
1464
- const listed = await capture(["launchctl", "list", label]);
1465
- const match = listed?.match(/"PID"\s*=\s*(\d+);/);
1466
- pid = match ? Number(match[1]) : undefined;
1467
- } else {
1468
- const shown = await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
1469
- pid = shown === undefined ? undefined : Number(shown.trim());
1470
- }
1471
- if (pid === undefined || !Number.isInteger(pid) || pid <= 0)
1472
- return;
1473
- const lstart = await capture(["ps", "-o", "lstart=", "-p", String(pid)]);
1474
- const args = await capture(["ps", "-ww", "-o", "args=", "-p", String(pid)]);
1475
- if (lstart === undefined || args === undefined)
1780
+ const shown = platform === "darwin" ? await capture(["launchctl", "print", `gui/${uid}/${label}`]) : await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
1781
+ const rawPid = platform === "darwin" ? shown?.match(/^\s*pid = (\d+);?\s*$/m)?.[1] : shown?.trim();
1782
+ const pid = rawPid === undefined ? undefined : Number(rawPid);
1783
+ if (pid === undefined || !Number.isSafeInteger(pid) || pid <= 0)
1476
1784
  return;
1477
- const startedAt = Date.parse(lstart.trim());
1478
- if (!Number.isFinite(startedAt))
1479
- return;
1480
- const suffix = spec.args.length > 0 ? ` ${spec.args.join(" ")}` : "";
1481
- const line = args.trim();
1482
- const binary = suffix && line.endsWith(suffix) ? line.slice(0, -suffix.length) : line;
1483
- return { pid, startedAt, binary };
1785
+ return { pid };
1484
1786
  };
1485
1787
  }
1486
1788
  async function defaultBinaryModifiedAt(path) {
@@ -1493,7 +1795,7 @@ function defaultIsServiceInstalled(options) {
1493
1795
  return false;
1494
1796
  if (options.platform === "darwin") {
1495
1797
  const userHome2 = options.env.HOME || homedir3();
1496
- const agentsDir = options.launchAgentsDir || join6(userHome2, "Library", "LaunchAgents");
1798
+ const agentsDir = options.launchAgentsDir || join7(userHome2, "Library", "LaunchAgents");
1497
1799
  const plistPath = launchAgentPlistPath(spec, agentsDir);
1498
1800
  try {
1499
1801
  await access(plistPath);
@@ -1504,23 +1806,61 @@ function defaultIsServiceInstalled(options) {
1504
1806
  }
1505
1807
  const userHome = options.env.HOME || homedir3();
1506
1808
  const unitDirPath = options.unitDir || systemdUnitDir(options.env, userHome);
1507
- const unitPath = systemdUnitPath(spec, unitDirPath);
1809
+ const unitPath2 = systemdUnitPath(spec, unitDirPath);
1508
1810
  try {
1509
- await access(unitPath);
1811
+ await access(unitPath2);
1510
1812
  return true;
1511
1813
  } catch {
1512
1814
  return false;
1513
1815
  }
1514
1816
  };
1515
1817
  }
1516
- function psStartBound(ms) {
1517
- return Math.floor(ms / 1000) * 1000 - PS_START_SKEW_MS;
1818
+ var SERVICE_LEASES = {
1819
+ broker: "broker",
1820
+ gateway: "gateway",
1821
+ "adapter-discord": "adapter-discord",
1822
+ "adapter-slack": "adapter-slack",
1823
+ "adapter-telegram": "adapter-telegram"
1824
+ };
1825
+ function serviceStopCommand(platform, uid) {
1826
+ if (platform === "linux")
1827
+ return ["systemctl", "--user", "stop", BROKER_UNIT];
1828
+ const target = uid ?? process.getuid?.() ?? 0;
1829
+ return ["launchctl", "bootout", `gui/${target}/dev.gajaeway.broker`];
1830
+ }
1831
+ function serviceBootstrapCommand(options, platform, label) {
1832
+ if (platform === "linux")
1833
+ return ["systemctl", "--user", "start", BROKER_UNIT];
1834
+ const target = options.uid ?? process.getuid?.() ?? 0;
1835
+ const userHome = options.env?.HOME || homedir3();
1836
+ const agentsDir = options.launchAgentsDir ?? join7(userHome, "Library", "LaunchAgents");
1837
+ const spec = serviceSpecs().find((candidate) => candidate.label === label);
1838
+ if (spec === undefined)
1839
+ throw new Error(`unknown service label: ${label}`);
1840
+ return ["launchctl", "bootstrap", `gui/${target}`, launchAgentPlistPath(spec, agentsDir)];
1518
1841
  }
1519
- var PS_START_SKEW_MS = 1000;
1520
1842
  async function runRestartStack(options) {
1843
+ let lease;
1844
+ try {
1845
+ lease = await acquireLease(options.home, "update");
1846
+ console.info(`lease_acquired name=update pid=${process.pid}`);
1847
+ } catch (error) {
1848
+ if (error instanceof LeaseHeldError)
1849
+ console.error(error.message);
1850
+ else
1851
+ console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
1852
+ throw error;
1853
+ }
1854
+ try {
1855
+ return await runRestartStackWithLease(options);
1856
+ } finally {
1857
+ await lease.release();
1858
+ }
1859
+ }
1860
+ async function runRestartStackWithLease(options) {
1521
1861
  const platform = options.platform ?? currentPlatform();
1522
1862
  const runner = options.runner ?? defaultCommandRunner;
1523
- const probe = options.probe ?? defaultServiceProbe(platform);
1863
+ const probe = options.probe ?? defaultServiceProbe(platform, options.uid);
1524
1864
  const modifiedAt = options.binaryModifiedAt ?? defaultBinaryModifiedAt;
1525
1865
  const isInstalled = options.isServiceInstalled ?? defaultIsServiceInstalled({
1526
1866
  platform,
@@ -1532,8 +1872,16 @@ async function runRestartStack(options) {
1532
1872
  const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
1533
1873
  const timeoutMs = options.verifyTimeoutMs ?? VERIFY_TIMEOUT_MS;
1534
1874
  const pollMs = options.pollMs ?? POLL_MS;
1875
+ const binDir = options.binDir ?? process.env.GAJAEWAY_BIN_DIR ?? dirname(process.execPath);
1876
+ const originalPids = new Map;
1877
+ for (const spec of restartOrder()) {
1878
+ if (SERVICE_LEASES[spec.id] === undefined && await isInstalled(spec.label))
1879
+ originalPids.set(spec.label, (await probe(spec.label))?.pid);
1880
+ }
1535
1881
  const existing = await readRestartReceipt(options.home);
1536
- const receipt = existing?.id === options.id && existing.state === "queued" ? existing : newReceipt(options.id, platform, options.uid, now());
1882
+ if (existing?.id === options.id && existing.state !== "queued")
1883
+ throw new Error(`restart-stack ${options.id}: receipt is ${existing.state}; refusing to start worker`);
1884
+ const receipt = existing?.id === options.id ? existing : newReceipt(options.id, platform, options.uid, now(), options.trigger ?? "manual");
1537
1885
  receipt.state = "running";
1538
1886
  receipt.supervisorPid = process.pid;
1539
1887
  await writeReceipt(options.home, receipt);
@@ -1544,6 +1892,12 @@ async function runRestartStack(options) {
1544
1892
  continue;
1545
1893
  }
1546
1894
  try {
1895
+ if (step.label === "dev.gajaeway.broker" && !await Bun.file(homeLayout(options.home).brokerProvisionManifest).exists()) {
1896
+ step.result = "skipped";
1897
+ step.detail = "broker agent is not provisioned";
1898
+ await writeReceipt(options.home, receipt);
1899
+ continue;
1900
+ }
1547
1901
  const installed = await isInstalled(step.label);
1548
1902
  if (!installed) {
1549
1903
  step.result = "skipped";
@@ -1551,17 +1905,68 @@ async function runRestartStack(options) {
1551
1905
  await writeReceipt(options.home, receipt);
1552
1906
  continue;
1553
1907
  }
1554
- if (step.command !== undefined) {
1555
- issuedAt = now();
1556
- const status = await runner(step.command);
1557
- step.exitStatus = status;
1558
- if (status !== 0) {
1908
+ if (step.label === "dev.gajaeway.broker" && receipt.trigger === "update") {
1909
+ const stopCommand = serviceStopCommand(platform, options.uid);
1910
+ const stopStatus = await runner(stopCommand);
1911
+ if (stopStatus !== 0 && !(platform === "darwin" && await probe(step.label) === undefined))
1912
+ throw new Error(`${stopCommand.join(" ")} exited with status ${stopStatus}`);
1913
+ const provisionCommand = [
1914
+ join7(binDir, "gajaeway-gateway"),
1915
+ "broker-agent",
1916
+ "provision",
1917
+ "--mode",
1918
+ "update",
1919
+ "--lease-wait-ms",
1920
+ "35000"
1921
+ ];
1922
+ const provisionStatus = await runner(provisionCommand);
1923
+ step.agentSync = {
1924
+ command: provisionCommand,
1925
+ exitStatus: provisionStatus,
1926
+ ...provisionStatus === 1 || provisionStatus === 3 ? { error: `broker-agent provision exited with status ${provisionStatus}` } : provisionStatus === 4 ? { error: "broker_agent_unrecoverable" } : {}
1927
+ };
1928
+ if (provisionStatus === 4) {
1929
+ delete step.command;
1930
+ step.exitStatus = 4;
1559
1931
  step.result = "failed";
1560
- step.detail = `${step.command.join(" ")} exited with status ${status}`;
1932
+ step.detail = "broker left stopped: broker_agent_unrecoverable; run gajaeway setup --broker-agent";
1933
+ } else if (provisionStatus === 0 || provisionStatus === 1 || provisionStatus === 3) {
1934
+ const startCommand = serviceBootstrapCommand(options, platform, step.label);
1935
+ step.command = startCommand;
1936
+ issuedAt = now();
1937
+ const startStatus = await runner(startCommand);
1938
+ step.exitStatus = startStatus;
1939
+ if (startStatus !== 0) {
1940
+ step.result = "failed";
1941
+ step.detail = `${startCommand.join(" ")} exited with status ${startStatus}`;
1942
+ } else
1943
+ await verifyStep(step, issuedAt);
1944
+ } else {
1945
+ delete step.command;
1946
+ step.exitStatus = provisionStatus;
1947
+ step.result = "failed";
1948
+ step.detail = `broker-agent provision exited with unexpected status ${provisionStatus}`;
1561
1949
  }
1950
+ } else {
1951
+ if (step.command !== undefined) {
1952
+ issuedAt = now();
1953
+ let command = step.command;
1954
+ let status = await runner(command);
1955
+ if (status !== 0 && receipt.trigger === "update" && platform === "darwin" && await probe(step.label) === undefined) {
1956
+ command = serviceBootstrapCommand(options, platform, step.label);
1957
+ step.command = command;
1958
+ issuedAt = now();
1959
+ status = await runner(command);
1960
+ }
1961
+ step.exitStatus = status;
1962
+ if (status !== 0) {
1963
+ step.result = "failed";
1964
+ step.detail = `${command.join(" ")} exited with status ${status}`;
1965
+ }
1966
+ }
1967
+ if (step.result === "pending")
1968
+ await verifyStep(step, issuedAt);
1562
1969
  }
1563
- if (step.result === "pending")
1564
- await verifyStep(step, issuedAt);
1565
1970
  } catch (error) {
1566
1971
  step.result = "failed";
1567
1972
  step.detail = error instanceof Error ? error.message : String(error);
@@ -1577,29 +1982,53 @@ async function runRestartStack(options) {
1577
1982
  await writeReceipt(options.home, receipt);
1578
1983
  return receipt;
1579
1984
  async function verifyStep(step, since) {
1985
+ const spec = serviceSpecs().find((candidate) => candidate.label === step.label);
1986
+ if (spec === undefined)
1987
+ throw new Error(`unknown service label: ${step.label}`);
1988
+ const leaseName = SERVICE_LEASES[spec.id];
1989
+ const binary = join7(binDir, spec.binary);
1990
+ const binaryModifiedAt = leaseName === undefined ? undefined : await modifiedAt(binary);
1580
1991
  const deadline = now() + timeoutMs;
1581
1992
  for (;; ) {
1582
1993
  const running = await probe(step.label);
1583
- let binaryModifiedAt = 0;
1584
- if (running !== undefined) {
1585
- binaryModifiedAt = await modifiedAt(running.binary);
1994
+ const owner = leaseName === undefined ? undefined : await readLeaseOwner(options.home, leaseName);
1995
+ if (running !== undefined)
1586
1996
  step.pid = running.pid;
1587
- step.processStartedAt = new Date(running.startedAt).toISOString();
1588
- step.binary = running.binary;
1997
+ if (leaseName !== undefined) {
1998
+ step.binary = binary;
1589
1999
  step.binaryModifiedAt = new Date(binaryModifiedAt).toISOString();
1590
- if (running.startedAt >= psStartBound(Math.max(binaryModifiedAt, since))) {
2000
+ if (owner !== undefined)
2001
+ step.processStartedAt = owner.startedAt;
2002
+ const startedAt = owner === undefined ? Number.NaN : Date.parse(owner.startedAt);
2003
+ if (running !== undefined && owner !== undefined && running.pid === owner.pid && Number.isFinite(startedAt) && startedAt >= Math.max(since, binaryModifiedAt)) {
1591
2004
  step.result = "ok";
1592
2005
  delete step.detail;
1593
2006
  return;
1594
2007
  }
2008
+ } else if (running !== undefined && running.pid !== originalPids.get(step.label)) {
2009
+ step.result = "ok";
2010
+ delete step.detail;
2011
+ return;
1595
2012
  }
1596
2013
  if (now() >= deadline) {
1597
2014
  if (running === undefined) {
1598
2015
  step.result = "failed";
1599
2016
  step.detail = "no running process after restart";
2017
+ } else if (leaseName === undefined) {
2018
+ step.result = "stale";
2019
+ step.detail = "MainPID did not change after restart";
2020
+ } else if (owner === undefined) {
2021
+ step.result = "stale";
2022
+ step.detail = "no lease owner after restart";
2023
+ } else if (running.pid !== owner.pid) {
2024
+ step.result = "stale";
2025
+ step.detail = "lease owner PID does not match service MainPID";
2026
+ } else if (Date.parse(owner.startedAt) < binaryModifiedAt) {
2027
+ step.result = "stale";
2028
+ step.detail = "process started before the deployed binary was modified";
1600
2029
  } else {
1601
2030
  step.result = "stale";
1602
- step.detail = running.startedAt < binaryModifiedAt ? "process started before the deployed binary was modified" : "process was not restarted by this sequence";
2031
+ step.detail = "lease owner predates this restart sequence";
1603
2032
  }
1604
2033
  return;
1605
2034
  }
@@ -1607,38 +2036,80 @@ async function runRestartStack(options) {
1607
2036
  }
1608
2037
  }
1609
2038
  }
2039
+ var SYSTEMD_RUN_TIMEOUT_MS = 30000;
1610
2040
  function cliCommand() {
1611
2041
  return Bun.main.startsWith("/$bunfs/") ? [process.execPath] : [process.execPath, Bun.main];
1612
2042
  }
1613
- function alive(pid) {
1614
- if (pid === undefined)
1615
- return false;
1616
- try {
1617
- process.kill(pid, 0);
1618
- return true;
1619
- } catch {
1620
- return false;
1621
- }
1622
- }
1623
2043
  async function launchRestartStack(options) {
1624
2044
  const platform = options.platform ?? currentPlatform();
1625
- const existing = await readRestartReceipt(options.home);
1626
- if (existing?.state === "running" && alive(existing.supervisorPid))
1627
- throw new Error(`restart-stack ${existing.id} is still running (supervisor pid ${existing.supervisorPid})`);
1628
- const receipt = newReceipt(randomUUID(), platform, options.uid, Date.now());
2045
+ if (await leaseHeld(options.home, "update"))
2046
+ throw new LeaseHeldError("update", await readLeaseOwner(options.home, "update"));
2047
+ const receipt = newReceipt(randomUUID2(), platform, options.uid, Date.now(), options.trigger ?? "manual");
1629
2048
  await writeReceipt(options.home, receipt);
1630
2049
  const worker = [...options.worker ?? [...cliCommand(), "ops", "restart-stack"], "--run", receipt.id];
1631
- const child = Bun.spawn(supervisorArgv(platform, worker, options.home, receipt.id), {
2050
+ const argv = supervisorArgv(platform, worker, options.home, receipt.id, options.binDir);
2051
+ const spawnOptions = {
1632
2052
  detached: true,
1633
2053
  stdin: "ignore",
1634
2054
  stdout: "ignore",
1635
2055
  stderr: "ignore",
1636
- env: { ...options.env ?? process.env, GAJAEWAY_HOME: options.home }
1637
- });
2056
+ env: {
2057
+ ...options.env ?? process.env,
2058
+ GAJAEWAY_HOME: options.home,
2059
+ ...options.binDir === undefined ? {} : { GAJAEWAY_BIN_DIR: options.binDir }
2060
+ }
2061
+ };
2062
+ let child;
2063
+ try {
2064
+ child = platform === "linux" ? (options.systemdRun ?? ((command) => Bun.spawn([...command], spawnOptions)))(argv) : Bun.spawn(argv, spawnOptions);
2065
+ } catch (error) {
2066
+ const detail = platform === "linux" ? `supervisor systemd-run could not be started (${error instanceof Error ? error.message : String(error)}); without a service manager here, restart through the host's own supervisor` : `supervisor ${argv[0]} could not be started (${error instanceof Error ? error.message : String(error)})`;
2067
+ await failLaunch(options.home, receipt, detail);
2068
+ throw new Error(`restart-stack ${receipt.id}: failed: ${detail}`);
2069
+ }
2070
+ if (platform === "linux") {
2071
+ const outcome = await waitForLauncher(child, options.systemdRunTimeoutMs ?? SYSTEMD_RUN_TIMEOUT_MS);
2072
+ if (outcome.kind === "timed_out") {
2073
+ const detail = `supervisor systemd-run timed out after ${options.systemdRunTimeoutMs ?? SYSTEMD_RUN_TIMEOUT_MS}ms; handoff outcome is indeterminate`;
2074
+ await failLaunch(options.home, receipt, detail);
2075
+ throw new Error(`restart-stack ${receipt.id}: failed: ${detail}`);
2076
+ }
2077
+ if (outcome.exitStatus !== 0) {
2078
+ const detail = `supervisor systemd-run exited with status ${outcome.exitStatus}`;
2079
+ await failLaunch(options.home, receipt, detail);
2080
+ throw new Error(`restart-stack ${receipt.id}: failed: ${detail}`);
2081
+ }
2082
+ }
1638
2083
  child.unref();
1639
2084
  return { receipt, supervisorPid: child.pid };
1640
2085
  }
1641
- function supervisorArgv(platform, worker, home, id) {
2086
+ async function failLaunch(home, receipt, detail) {
2087
+ receipt.state = "failed";
2088
+ receipt.finishedAt = new Date().toISOString();
2089
+ receipt.detail = detail;
2090
+ for (const step of receipt.steps)
2091
+ step.result = "skipped";
2092
+ await writeReceipt(home, receipt);
2093
+ }
2094
+ async function waitForLauncher(child, timeoutMs) {
2095
+ let timer;
2096
+ const outcome = await Promise.race([
2097
+ child.exited.then((exitStatus) => ({ kind: "exited", exitStatus })),
2098
+ new Promise((resolve) => {
2099
+ timer = setTimeout(() => resolve({ kind: "timed_out" }), timeoutMs);
2100
+ })
2101
+ ]);
2102
+ if (timer !== undefined)
2103
+ clearTimeout(timer);
2104
+ if (outcome.kind === "timed_out") {
2105
+ child.kill("SIGKILL");
2106
+ child.exited.catch(() => {
2107
+ return;
2108
+ });
2109
+ }
2110
+ return outcome;
2111
+ }
2112
+ function supervisorArgv(platform, worker, home, id, binDir) {
1642
2113
  if (platform === "darwin")
1643
2114
  return [...worker];
1644
2115
  return [
@@ -1646,28 +2117,32 @@ function supervisorArgv(platform, worker, home, id) {
1646
2117
  "--user",
1647
2118
  "--collect",
1648
2119
  "--quiet",
2120
+ "--service-type=exec",
1649
2121
  `--unit=gajaeway-restart-stack-${id}`,
1650
2122
  `--setenv=GAJAEWAY_HOME=${home}`,
2123
+ ...binDir === undefined ? [] : [`--setenv=GAJAEWAY_BIN_DIR=${binDir}`],
1651
2124
  "--",
1652
2125
  ...worker
1653
2126
  ];
1654
2127
  }
1655
- function effectiveRestartState(receipt) {
1656
- if (receipt.state === "running" && !alive(receipt.supervisorPid))
2128
+ async function effectiveRestartState(receipt, home) {
2129
+ if (receipt.state === "running" && !await leaseHeld(home, "update"))
1657
2130
  return "interrupted";
1658
2131
  return receipt.state;
1659
2132
  }
1660
- function renderRestartReceipt(receipt) {
2133
+ function renderRestartReceipt(receipt, state = receipt.state) {
1661
2134
  const lines = [
1662
- `restart-stack ${receipt.id}: ${effectiveRestartState(receipt)}`,
2135
+ `restart-stack ${receipt.id}: ${state}`,
1663
2136
  `requestedAt: ${receipt.requestedAt}`,
1664
- `finishedAt: ${receipt.finishedAt ?? "-"}`
2137
+ `finishedAt: ${receipt.finishedAt ?? "-"}`,
2138
+ ...receipt.detail === undefined ? [] : [`detail: ${receipt.detail}`]
1665
2139
  ];
1666
2140
  for (const step of receipt.steps) {
1667
2141
  const facts = [
1668
2142
  step.pid === undefined ? undefined : `pid=${step.pid}`,
1669
2143
  step.processStartedAt === undefined ? undefined : `started=${step.processStartedAt}`,
1670
2144
  step.binaryModifiedAt === undefined ? undefined : `binary=${step.binaryModifiedAt}`,
2145
+ step.agentSync?.error === undefined ? undefined : `agentSync.error=${step.agentSync.error}`,
1671
2146
  step.detail
1672
2147
  ].filter((fact) => fact !== undefined);
1673
2148
  lines.push(`${step.label}: ${step.result}${facts.length > 0 ? ` ${facts.join(" ")}` : ""}`);
@@ -1676,9 +2151,74 @@ function renderRestartReceipt(receipt) {
1676
2151
  }
1677
2152
 
1678
2153
  // src/setup.ts
1679
- import { randomUUID as randomUUID2 } from "crypto";
1680
- import { chmod as chmod2, mkdir as mkdir4, readFile as readFile5, rename as rename2, rm, writeFile as writeFile4 } from "fs/promises";
1681
- import { join as join7 } from "path";
2154
+ import { randomUUID as randomUUID3 } from "crypto";
2155
+ import { chmod as chmod3, mkdir as mkdir5, readFile as readFile6, realpath, rename as rename3, rm as rm2, writeFile as writeFile4 } from "fs/promises";
2156
+ import { basename, dirname as dirname2, join as join8 } from "path";
2157
+
2158
+ // src/discord-setup.ts
2159
+ var DEVELOPER_PORTAL_URL = "https://discord.com/developers/applications";
2160
+ var DISCORD_BOT_SCOPES = ["bot", "applications.commands"];
2161
+ var DISCORD_BOT_PERMISSIONS = 2048 | 16384 | 33554432 | 65536 | 131072 | 8192 | 64;
2162
+ function printDiscordSetupGuide(log) {
2163
+ log("");
2164
+ log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2165
+ log(" Getting your Discord bot token");
2166
+ log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2167
+ log("");
2168
+ log("1. Open the Discord Developer Portal:");
2169
+ log(` ${DEVELOPER_PORTAL_URL}`);
2170
+ log("");
2171
+ log("2. Click 'New Application' and choose a name (e.g., 'My gajae-way agent')");
2172
+ log("");
2173
+ log("3. In the left menu, click 'Bot' \u2192 'Add Bot'");
2174
+ log("");
2175
+ log("4. Under 'TOKEN', click 'Reset Token' \u2192 'Yes, do it!'");
2176
+ log("");
2177
+ log("5. Click 'Copy' to copy your bot token (it starts with 'MTI...')");
2178
+ log("");
2179
+ log("6. (Important) Scroll down to 'GATEWAY INTENTS' and enable 'MESSAGE CONTENT INTENT'");
2180
+ log(" This allows the bot to read message text, not just usernames.");
2181
+ log("");
2182
+ log("7. Click 'Save Changes'");
2183
+ log("");
2184
+ log("8. Come back here and paste your token below");
2185
+ log("");
2186
+ log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2187
+ log("");
2188
+ }
2189
+ function generateDiscordInviteUrl(appId) {
2190
+ const scopeParam = DISCORD_BOT_SCOPES.join("+");
2191
+ const permissionsParam = String(DISCORD_BOT_PERMISSIONS);
2192
+ return `https://discord.com/oauth2/authorize?client_id=${appId}&scope=${scopeParam}&permissions=${permissionsParam}`;
2193
+ }
2194
+ function printDiscordBotReady(log, appId, botName) {
2195
+ const inviteUrl = generateDiscordInviteUrl(appId);
2196
+ log("");
2197
+ log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2198
+ log(" \u2713 Discord bot token validated");
2199
+ log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2200
+ log("");
2201
+ log(`Application ID: ${appId}`);
2202
+ if (botName) {
2203
+ log(`Bot name: ${botName}`);
2204
+ }
2205
+ log("");
2206
+ log("To invite your bot to your Discord server, click this link:");
2207
+ log("");
2208
+ log(` ${inviteUrl}`);
2209
+ log("");
2210
+ log("Then:");
2211
+ log(" 1. Choose the server where you want to add the bot");
2212
+ log(" 2. Click 'Authorize'");
2213
+ log(" 3. Complete the CAPTCHA");
2214
+ log("");
2215
+ log("Once authorized, send the bot a DM and start chatting!");
2216
+ log("");
2217
+ log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2218
+ log("");
2219
+ }
2220
+
2221
+ // src/setup.ts
1682
2222
  var SETUP_PLATFORMS = ["discord", "slack", "telegram"];
1683
2223
  var SETUP_ENV = {
1684
2224
  discordToken: "GAJAEWAY_DISCORD_TOKEN",
@@ -1688,7 +2228,7 @@ var SETUP_ENV = {
1688
2228
  telegramToken: "GAJAEWAY_TELEGRAM_TOKEN",
1689
2229
  ownerIds: "GAJAEWAY_OWNER_IDS"
1690
2230
  };
1691
- var SETUP_USAGE = "usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status";
2231
+ var SETUP_USAGE = "usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status | setup --broker-agent [--from DIR]";
1692
2232
  var SECRET_FILES2 = {
1693
2233
  discord: "discord-token",
1694
2234
  slackBot: "slack-bot-token",
@@ -1713,6 +2253,8 @@ function redact(text, secrets) {
1713
2253
  function parseSetupArgs(args) {
1714
2254
  let fromEnv = false;
1715
2255
  let status = false;
2256
+ let brokerAgent = false;
2257
+ let from;
1716
2258
  let adapters;
1717
2259
  const owners = [];
1718
2260
  let discordAppId;
@@ -1722,7 +2264,15 @@ function parseSetupArgs(args) {
1722
2264
  fromEnv = true;
1723
2265
  else if (flag === "--status")
1724
2266
  status = true;
1725
- else if (flag === "--adapters" || flag === "--owner" || flag === "--discord-app-id") {
2267
+ else if (flag === "--broker-agent")
2268
+ brokerAgent = true;
2269
+ else if (flag === "--from") {
2270
+ const value = args[++i];
2271
+ if (value === undefined || value.trim() === "" || value.startsWith("--"))
2272
+ throw new SetupError(`${flag} expects a value
2273
+ ${SETUP_USAGE}`);
2274
+ from = value;
2275
+ } else if (flag === "--adapters" || flag === "--owner" || flag === "--discord-app-id") {
1726
2276
  const value = args[++i];
1727
2277
  if (value === undefined || value.trim() === "" || value.startsWith("--"))
1728
2278
  throw new SetupError(`${flag} expects a value
@@ -1739,15 +2289,22 @@ ${SETUP_USAGE}`);
1739
2289
  throw new SetupError(`unknown option: ${flag.split("=")[0]}
1740
2290
  ${SETUP_USAGE}`);
1741
2291
  }
1742
- if (status && (fromEnv || adapters || owners.length > 0 || discordAppId))
2292
+ if (status && (fromEnv || brokerAgent || from !== undefined || adapters || owners.length > 0 || discordAppId))
1743
2293
  throw new SetupError(`--status takes no other options
2294
+ ${SETUP_USAGE}`);
2295
+ if (brokerAgent && (fromEnv || adapters || owners.length > 0 || discordAppId))
2296
+ throw new SetupError(`--broker-agent is standalone and cannot be combined with adapter setup options
2297
+ ${SETUP_USAGE}`);
2298
+ if (from !== undefined && !brokerAgent)
2299
+ throw new SetupError(`--from requires --broker-agent
1744
2300
  ${SETUP_USAGE}`);
1745
2301
  return {
1746
- mode: status ? "status" : "setup",
2302
+ mode: status ? "status" : brokerAgent ? "broker-agent" : "setup",
1747
2303
  fromEnv,
1748
2304
  ...adapters ? { adapters } : {},
1749
2305
  owners,
1750
- ...discordAppId ? { discordAppId } : {}
2306
+ ...discordAppId ? { discordAppId } : {},
2307
+ ...from === undefined ? {} : { from }
1751
2308
  };
1752
2309
  }
1753
2310
  function splitList(value) {
@@ -1845,7 +2402,8 @@ async function validateDiscord(credentials, fetcher) {
1845
2402
  const ownerId = !hasTeam && typeof owner?.id === "string" ? owner.id : undefined;
1846
2403
  return {
1847
2404
  platform: "discord",
1848
- summary: `bot ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
2405
+ summary: `bot named ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
2406
+ appId,
1849
2407
  ...ownerId ? { ownerId } : {}
1850
2408
  };
1851
2409
  }
@@ -1899,19 +2457,19 @@ async function validateAll(tokens, fetcher) {
1899
2457
  return identities;
1900
2458
  }
1901
2459
  async function writePrivateFile(path, contents) {
1902
- const temporary = `${path}.${randomUUID2()}.tmp`;
2460
+ const temporary = `${path}.${randomUUID3()}.tmp`;
1903
2461
  try {
1904
2462
  await writeFile4(temporary, contents, { flag: "wx", mode: 384 });
1905
- await chmod2(temporary, 384);
1906
- await rename2(temporary, path);
2463
+ await chmod3(temporary, 384);
2464
+ await rename3(temporary, path);
1907
2465
  } finally {
1908
- await rm(temporary, { force: true });
2466
+ await rm2(temporary, { force: true });
1909
2467
  }
1910
2468
  }
1911
2469
  async function readJsonObject(path) {
1912
2470
  let text;
1913
2471
  try {
1914
- text = await readFile5(path, "utf8");
2472
+ text = await readFile6(path, "utf8");
1915
2473
  } catch (error) {
1916
2474
  if (error.code === "ENOENT")
1917
2475
  return;
@@ -1929,21 +2487,21 @@ async function readJsonObject(path) {
1929
2487
  }
1930
2488
  async function applySetup(options) {
1931
2489
  const { home, tokens } = options;
1932
- const secretsDir = join7(home, "secrets");
1933
- const configPath = join7(home, "config.json");
1934
- const adapterPath = (platform) => join7(home, `adapter-${platform}.json`);
2490
+ const secretsDir = join8(home, "secrets");
2491
+ const configPath = join8(home, "config.json");
2492
+ const adapterPath = (platform) => join8(home, `adapter-${platform}.json`);
1935
2493
  const existingConfig = await readJsonObject(configPath);
1936
2494
  const existingAdapters = new Map;
1937
2495
  for (const platform of SETUP_PLATFORMS)
1938
2496
  if (tokens[platform])
1939
2497
  existingAdapters.set(platform, await readJsonObject(adapterPath(platform)));
1940
- await mkdir4(home, { recursive: true, mode: 448 });
1941
- await chmod2(home, 448);
1942
- await mkdir4(secretsDir, { recursive: true, mode: 448 });
1943
- await chmod2(secretsDir, 448);
2498
+ await mkdir5(home, { recursive: true, mode: 448 });
2499
+ await chmod3(home, 448);
2500
+ await mkdir5(secretsDir, { recursive: true, mode: 448 });
2501
+ await chmod3(secretsDir, 448);
1944
2502
  const written = [];
1945
2503
  const secret = async (name, value) => {
1946
- const path = join7(secretsDir, name);
2504
+ const path = join8(secretsDir, name);
1947
2505
  await writePrivateFile(path, `${value}
1948
2506
  `);
1949
2507
  written.push(path);
@@ -1990,7 +2548,7 @@ async function applySetup(options) {
1990
2548
  async function enabledAdapters(home) {
1991
2549
  const enabled = [];
1992
2550
  for (const platform of SETUP_PLATFORMS) {
1993
- if (await Bun.file(join7(home, `adapter-${platform}.json`)).exists())
2551
+ if (await Bun.file(join8(home, `adapter-${platform}.json`)).exists())
1994
2552
  enabled.push(platform);
1995
2553
  }
1996
2554
  return enabled;
@@ -2011,12 +2569,14 @@ async function askRequiredSecret(prompter, question) {
2011
2569
  return answer;
2012
2570
  }
2013
2571
  }
2014
- async function tokensFromPrompts(prompter, parsed) {
2572
+ async function tokensFromPrompts(prompter, parsed, log) {
2015
2573
  const wanted = parsed.adapters ?? [];
2016
2574
  const enable = async (platform, label) => parsed.adapters ? wanted.includes(platform) : await askYes(prompter, `Enable ${label}?`);
2017
2575
  const tokens = {};
2018
2576
  if (await enable("discord", "Discord")) {
2019
- const token = await askRequiredSecret(prompter, "Discord bot token (input hidden): ");
2577
+ const logger = log ?? (() => {});
2578
+ printDiscordSetupGuide(logger);
2579
+ const token = await askRequiredSecret(prompter, "Paste your Discord bot token here (input hidden): ");
2020
2580
  const appId = parsed.discordAppId ?? (await prompter.ask("Discord application id (optional): ")).trim();
2021
2581
  tokens.discord = { token, ...appId ? { appId } : {} };
2022
2582
  }
@@ -2085,6 +2645,28 @@ function terminalPrompter() {
2085
2645
  close: () => stdin.pause()
2086
2646
  };
2087
2647
  }
2648
+ async function defaultBrokerAgentRunner(command) {
2649
+ const child = Bun.spawn([...command], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
2650
+ const [stdout, stderr, exitCode] = await Promise.all([
2651
+ new Response(child.stdout).text(),
2652
+ new Response(child.stderr).text(),
2653
+ child.exited
2654
+ ]);
2655
+ return { exitCode, stdout, stderr };
2656
+ }
2657
+ async function setupBinDir(options) {
2658
+ if (options.binDir !== undefined)
2659
+ return options.binDir;
2660
+ const env = options.env ?? process.env;
2661
+ if (env.GAJAEWAY_BIN_DIR)
2662
+ return env.GAJAEWAY_BIN_DIR;
2663
+ const executable = await realpath(process.execPath).catch(() => {
2664
+ return;
2665
+ });
2666
+ if (executable !== undefined && basename(executable).startsWith("gajaeway"))
2667
+ return dirname2(executable);
2668
+ throw new SetupError("setup --broker-agent requires the installed gajaeway binary or an explicit bin directory");
2669
+ }
2088
2670
  async function runSetup(args, options) {
2089
2671
  const log = options.log ?? ((line) => console.log(line));
2090
2672
  const parsed = parseSetupArgs(args);
@@ -2092,6 +2674,26 @@ async function runSetup(args, options) {
2092
2674
  log((await enabledAdapters(options.home)).join(","));
2093
2675
  return;
2094
2676
  }
2677
+ if (parsed.mode === "broker-agent") {
2678
+ const command = [
2679
+ join8(await setupBinDir(options), "gajaeway-gateway"),
2680
+ "broker-agent",
2681
+ "provision",
2682
+ "--mode",
2683
+ "force",
2684
+ ...parsed.from === undefined ? [] : ["--from", parsed.from]
2685
+ ];
2686
+ const result = await (options.runBrokerAgent ?? defaultBrokerAgentRunner)(command);
2687
+ if (result.stdout.length > 0)
2688
+ process.stdout.write(result.stdout);
2689
+ if (result.stderr.length > 0)
2690
+ process.stderr.write(result.stderr);
2691
+ if (result.exitCode === 1 && /lease_held name=broker/.test(`${result.stdout}
2692
+ ${result.stderr}`))
2693
+ log("broker lease is held; stop it with: systemctl --user stop gajaeway-broker");
2694
+ process.exitCode = result.exitCode;
2695
+ return;
2696
+ }
2095
2697
  let tokens;
2096
2698
  let owners;
2097
2699
  if (parsed.fromEnv) {
@@ -2100,7 +2702,7 @@ async function runSetup(args, options) {
2100
2702
  } else {
2101
2703
  const prompter = (options.prompter ?? terminalPrompter)();
2102
2704
  try {
2103
- ({ tokens, owners } = await tokensFromPrompts(prompter, parsed));
2705
+ ({ tokens, owners } = await tokensFromPrompts(prompter, parsed, log));
2104
2706
  } finally {
2105
2707
  prompter.close();
2106
2708
  }
@@ -2110,6 +2712,11 @@ async function runSetup(args, options) {
2110
2712
  const identities = await validateAll(tokens, options.fetch ?? ((input, init) => fetch(input, init)));
2111
2713
  for (const identity of identities)
2112
2714
  log(`${identity.platform}: token valid \u2014 ${identity.summary}`);
2715
+ const discordIdentity = identities.find((i) => i.platform === "discord");
2716
+ if (discordIdentity && tokens.discord && discordIdentity.appId) {
2717
+ const botName = (discordIdentity.summary.match(/named (\S+)/) ?? [])[1];
2718
+ printDiscordBotReady(log, discordIdentity.appId, botName);
2719
+ }
2113
2720
  for (const identity of identities)
2114
2721
  if (identity.ownerId && owners.length === 0) {
2115
2722
  owners.push(identity.ownerId);
@@ -2129,77 +2736,152 @@ async function runSetup(args, options) {
2129
2736
 
2130
2737
  // src/update.ts
2131
2738
  import {
2132
- chmod as chmod3,
2739
+ chmod as chmod5,
2133
2740
  copyFile,
2134
- mkdir as mkdir5,
2741
+ mkdir as mkdir7,
2742
+ mkdtemp as mkdtemp2,
2743
+ readdir as readdir3,
2744
+ readFile as readFile7,
2745
+ realpath as realpath2,
2746
+ rename as rename5,
2747
+ rm as rm4,
2748
+ stat as stat2,
2749
+ writeFile as writeFile6
2750
+ } from "fs/promises";
2751
+ import { tmpdir } from "os";
2752
+ import { basename as basename2, dirname as dirname3, join as join10 } from "path";
2753
+
2754
+ // src/gjc-install.ts
2755
+ import { randomUUID as randomUUID4 } from "crypto";
2756
+ import {
2757
+ access as access2,
2758
+ chmod as chmod4,
2759
+ lstat as lstat2,
2760
+ mkdir as mkdir6,
2135
2761
  mkdtemp,
2136
- open,
2137
2762
  readdir as readdir2,
2138
- readFile as readFile6,
2139
- realpath,
2140
- rename as rename3,
2141
- rm as rm2,
2142
- stat as stat2,
2763
+ readlink as readlink2,
2764
+ rename as rename4,
2765
+ rm as rm3,
2766
+ symlink,
2143
2767
  writeFile as writeFile5
2144
2768
  } from "fs/promises";
2145
- import { tmpdir } from "os";
2146
- import { basename, dirname, join as join8 } from "path";
2147
- // package.json
2148
- var package_default = {
2149
- name: "@gajae-gateway/cli",
2150
- version: "0.1.8",
2151
- type: "module",
2152
- description: "Owner CLI for the gajaeway gateway: status, sessions, chat, monitors, and worker lanes over the gateway socket. Requires the Bun runtime (uses Bun.file/Bun.spawn).",
2153
- license: "AGPL-3.0-only",
2154
- author: "Yeachan Heo",
2155
- repository: {
2156
- type: "git",
2157
- url: "git+https://github.com/Yeachan-Heo/gajae-way.git",
2158
- directory: "packages/cli"
2159
- },
2160
- homepage: "https://github.com/Yeachan-Heo/gajae-way/tree/main/packages/cli#readme",
2161
- bugs: "https://github.com/Yeachan-Heo/gajae-way/issues",
2162
- keywords: [
2163
- "gajaeway",
2164
- "cli",
2165
- "gateway"
2166
- ],
2167
- files: [
2168
- "dist",
2169
- "src"
2170
- ],
2171
- main: "./dist/main.js",
2172
- types: "./src/main.ts",
2173
- exports: {
2174
- ".": {
2175
- bun: "./src/main.ts",
2176
- types: "./src/main.ts",
2177
- default: "./dist/main.js"
2769
+ import { join as join9 } from "path";
2770
+ var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
2771
+ async function defaultRunner(command, cwd) {
2772
+ const child = Bun.spawn([...command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
2773
+ const [stdout, stderr, exitCode] = await Promise.all([
2774
+ new Response(child.stdout).text(),
2775
+ new Response(child.stderr).text(),
2776
+ child.exited
2777
+ ]);
2778
+ return { exitCode, stdout, stderr };
2779
+ }
2780
+ function assertSuccessful(command, result) {
2781
+ if (result.exitCode !== 0) {
2782
+ const detail = result.stderr.trim();
2783
+ throw new Error(`${command.join(" ")} exited with status ${result.exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
2784
+ }
2785
+ }
2786
+ function versionFromOutput(output) {
2787
+ return output.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/)?.[0];
2788
+ }
2789
+ async function verifyGjc(path, cwd, version, runner) {
2790
+ await access2(path);
2791
+ const command = [path, "--version"];
2792
+ const result = await runner(command, cwd);
2793
+ assertSuccessful(command, result);
2794
+ const reported = versionFromOutput(`${result.stdout}
2795
+ ${result.stderr}`);
2796
+ if (reported !== version)
2797
+ throw new Error(`gjc version verification failed: expected ${version}, got ${reported ?? "unknown"}`);
2798
+ }
2799
+ async function currentVersion(prefix, current) {
2800
+ try {
2801
+ const target = await readlink2(current);
2802
+ if (target.includes("/") || !VERSION_PATTERN.test(target))
2803
+ return;
2804
+ const targetPath = join9(prefix, target);
2805
+ if (!(await lstat2(targetPath)).isDirectory())
2806
+ return;
2807
+ return target;
2808
+ } catch {
2809
+ return;
2810
+ }
2811
+ }
2812
+ async function pruneVersions(prefix, keep) {
2813
+ const retained = new Set(keep);
2814
+ const entries2 = await readdir2(prefix, { withFileTypes: true });
2815
+ const versions = entries2.filter((entry) => entry.isDirectory() && VERSION_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => Bun.semver.order(right, left));
2816
+ for (const version of versions) {
2817
+ if (retained.size >= 2)
2818
+ break;
2819
+ retained.add(version);
2820
+ }
2821
+ for (const version of versions)
2822
+ if (!retained.has(version))
2823
+ await rm3(join9(prefix, version), { recursive: true, force: true });
2824
+ }
2825
+ async function installPinnedGjc(options) {
2826
+ const { home, version } = options;
2827
+ if (!VERSION_PATTERN.test(version))
2828
+ throw new Error(`invalid pinned gjc version: ${version}`);
2829
+ const layout = homeLayout(home);
2830
+ const runner = options.runner ?? defaultRunner;
2831
+ await mkdir6(layout.gjcPrefix, { recursive: true, mode: 448 });
2832
+ await chmod4(layout.gjcPrefix, 448);
2833
+ const versionDir = join9(layout.gjcPrefix, version);
2834
+ const previousVersion = await currentVersion(layout.gjcPrefix, layout.gjcCurrent);
2835
+ let staging;
2836
+ try {
2837
+ let installed = false;
2838
+ try {
2839
+ const info = await lstat2(versionDir);
2840
+ if (!info.isDirectory())
2841
+ throw new Error(`gjc version path is not a directory: ${versionDir}`);
2842
+ installed = true;
2843
+ } catch (error) {
2844
+ if (error.code !== "ENOENT")
2845
+ throw error;
2178
2846
  }
2179
- },
2180
- bin: {
2181
- gajaeway: "./dist/main.js"
2182
- },
2183
- publishConfig: {
2184
- access: "public"
2185
- },
2186
- scripts: {
2187
- build: "rm -rf dist && bun build src/main.ts --outdir dist --target bun --format esm --external @gajae-gateway/protocol --external @gajae-gateway/sdk && tsc -p tsconfig.build.json",
2188
- prepublishOnly: "bun run build"
2189
- },
2190
- dependencies: {
2191
- "@gajae-gateway/sdk": "workspace:*",
2192
- "@gajae-gateway/protocol": "workspace:*"
2193
- },
2194
- engines: {
2195
- bun: ">=1.4.0"
2847
+ if (!installed) {
2848
+ staging = await mkdtemp(join9(layout.gjcPrefix, ".staging-"));
2849
+ await chmod4(staging, 448);
2850
+ await writeFile5(join9(staging, "package.json"), `${JSON.stringify({ private: true, dependencies: { "gajae-code": version } }, null, 2)}
2851
+ `, { mode: 384 });
2852
+ const installCommand = ["bun", "install", "--production"];
2853
+ assertSuccessful(installCommand, await runner(installCommand, staging));
2854
+ const installedBin = join9(staging, "node_modules", ".bin", "gjc");
2855
+ await access2(installedBin);
2856
+ const binDir = join9(staging, "bin");
2857
+ await mkdir6(binDir, { mode: 448 });
2858
+ await symlink("../node_modules/.bin/gjc", join9(binDir, "gjc"), "file");
2859
+ await verifyGjc(join9(binDir, "gjc"), staging, version, runner);
2860
+ await rename4(staging, versionDir);
2861
+ staging = undefined;
2862
+ } else {
2863
+ await verifyGjc(join9(versionDir, "bin", "gjc"), versionDir, version, runner);
2864
+ }
2865
+ await pruneVersions(layout.gjcPrefix, [version, ...previousVersion === undefined ? [] : [previousVersion]]);
2866
+ const temporaryLink = join9(layout.gjcPrefix, `.current-${randomUUID4()}`);
2867
+ try {
2868
+ await symlink(version, temporaryLink, "dir");
2869
+ await rename4(temporaryLink, layout.gjcCurrent);
2870
+ } finally {
2871
+ await rm3(temporaryLink, { force: true });
2872
+ }
2873
+ return layout.gjcExecutable();
2874
+ } finally {
2875
+ if (staging !== undefined)
2876
+ await rm3(staging, { recursive: true, force: true });
2196
2877
  }
2197
- };
2878
+ }
2198
2879
 
2199
2880
  // src/update.ts
2200
2881
  var UPDATE_USAGE = "usage: gajaeway update [--check] [--force] [--bin-dir DIR] [--no-restart]";
2201
2882
  var DEFAULT_RELEASE_REPO = "Yeachan-Heo/gajae-way";
2202
2883
  var RELEASE_REPO_OVERRIDE_ENV = "GAJAEWAY_UPDATE_REPO";
2884
+ var PIN_MANIFEST = "package.json";
2203
2885
  function releaseRepo(env = process.env) {
2204
2886
  const override = env[RELEASE_REPO_OVERRIDE_ENV]?.trim();
2205
2887
  return override ? override : declaredReleaseRepo();
@@ -2262,13 +2944,12 @@ var CORE_BINARIES = ["gajaeway", "gajaeway-gateway"];
2262
2944
  var MIN_BINARY_BYTES = 1048576;
2263
2945
  var MAX_ASSET_BYTES = 512 * 1024 * 1024;
2264
2946
  var RELEASE_MARKER = ".gajaeway-release";
2265
- var LOCK_FILE = ".gajaeway-update.lock";
2266
2947
  function releaseMarkerPath(binDir) {
2267
- return join8(binDir, RELEASE_MARKER);
2948
+ return join10(binDir, RELEASE_MARKER);
2268
2949
  }
2269
2950
  async function readInstalledRelease(binDir) {
2270
2951
  try {
2271
- const version = releaseVersion(await readFile6(releaseMarkerPath(binDir), "utf8"));
2952
+ const version = releaseVersion(await readFile7(releaseMarkerPath(binDir), "utf8"));
2272
2953
  return version;
2273
2954
  } catch {
2274
2955
  return;
@@ -2277,12 +2958,12 @@ async function readInstalledRelease(binDir) {
2277
2958
  async function resolveBinDir(flag, execPath = process.execPath) {
2278
2959
  if (flag !== undefined)
2279
2960
  return flag;
2280
- const real = await realpath(execPath).catch(() => {
2961
+ const real = await realpath2(execPath).catch(() => {
2281
2962
  return;
2282
2963
  });
2283
2964
  if (real === undefined)
2284
2965
  return;
2285
- return basename(real).startsWith("gajaeway") ? dirname(real) : undefined;
2966
+ return basename2(real).startsWith("gajaeway") ? dirname3(real) : undefined;
2286
2967
  }
2287
2968
  function renderUpdate(result) {
2288
2969
  const lines = [];
@@ -2302,8 +2983,71 @@ function renderUpdate(result) {
2302
2983
  lines.push(`restart: queued ${result.restartQueued} \u2014 check with: gajaeway ops restart-stack --status`);
2303
2984
  else if (result.restartSkipped)
2304
2985
  lines.push("restart: skipped (--no-restart)");
2986
+ if (result.brokerNotEnabled)
2987
+ lines.push("broker unit installed, not enabled: run gajaeway setup --broker-agent");
2305
2988
  return lines;
2306
2989
  }
2990
+ async function readPinnedGjcVersion(path) {
2991
+ let parsed;
2992
+ try {
2993
+ parsed = JSON.parse(await readFile7(path, "utf8"));
2994
+ } catch (error) {
2995
+ throw new Error(`could not read pinned gjc version from ${path}: ${error instanceof Error ? error.message : String(error)}`);
2996
+ }
2997
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2998
+ throw new Error(`invalid release package metadata at ${path}`);
2999
+ const gjc = parsed.gjc;
3000
+ if (typeof gjc !== "object" || gjc === null || Array.isArray(gjc))
3001
+ throw new Error(`release package metadata at ${path} has no gjc.version`);
3002
+ const version = gjc.version;
3003
+ if (typeof version !== "string" || version.trim() === "")
3004
+ throw new Error(`release package metadata at ${path} has no gjc.version`);
3005
+ return version;
3006
+ }
3007
+ async function defaultUpdateCommandRunner(command) {
3008
+ const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
3009
+ const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
3010
+ if (exitCode !== 0) {
3011
+ const detail = stderr.trim();
3012
+ throw new Error(`${command.join(" ")} exited with status ${exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
3013
+ }
3014
+ return exitCode;
3015
+ }
3016
+ async function installUpdatedServices(options) {
3017
+ const { home, binDir, platform, noRestart, deps } = options;
3018
+ const brokerProvisioned = await exists(homeLayout(home).brokerProvisionManifest);
3019
+ const env = { ...deps.env ?? process.env, GAJAEWAY_HOME: home };
3020
+ await installServices({
3021
+ binDir,
3022
+ platform,
3023
+ env,
3024
+ configuredAdaptersOnly: true,
3025
+ compareAndWrite: true,
3026
+ ...deps.serviceOptions
3027
+ });
3028
+ if (platform === "linux") {
3029
+ const run = deps.serviceRunner ?? defaultUpdateCommandRunner;
3030
+ const execute = async (command) => {
3031
+ const exitCode = await run(command);
3032
+ if (exitCode !== 0)
3033
+ throw new Error(`${command.join(" ")} exited with status ${exitCode}`);
3034
+ };
3035
+ await execute(["systemctl", "--user", "daemon-reload"]);
3036
+ if (!noRestart) {
3037
+ const enableUnits = [];
3038
+ for (const spec of serviceSpecs()) {
3039
+ if (spec.id === "broker" && !brokerProvisioned)
3040
+ continue;
3041
+ if (spec.id.startsWith("adapter-") && !await Bun.file(join10(home, `adapter-${spec.id.slice(8)}.json`)).exists())
3042
+ continue;
3043
+ enableUnits.push(systemdUnitName(spec));
3044
+ }
3045
+ if (enableUnits.length > 0)
3046
+ await execute(["systemctl", "--user", "enable", ...enableUnits]);
3047
+ }
3048
+ }
3049
+ return !brokerProvisioned;
3050
+ }
2307
3051
  async function runUpdate(options) {
2308
3052
  const deps = options.deps ?? {};
2309
3053
  const platform = deps.platform ?? process.platform;
@@ -2326,46 +3070,67 @@ async function runUpdate(options) {
2326
3070
  return { current, latest, tag: release.tag, repo, checkedOnly: true, upToDate: false, installed: [] };
2327
3071
  if (binDir === undefined)
2328
3072
  throw new Error("run the update from a compiled gajaeway binary, or pass --bin-dir DIR");
2329
- const unlock = await acquireUpdateLock(binDir);
3073
+ let lease;
2330
3074
  try {
2331
- const staged = await stageRelease(asset, deps);
2332
- const installedNames = await knownBinariesIn(binDir);
2333
- const toInstall = binariesToInstall(staged.names, installedNames);
2334
- const missingCore = CORE_BINARIES.filter((name) => !toInstall.includes(name));
2335
- if (missingCore.length > 0)
2336
- throw new Error(`release tarball is missing core binaries: ${missingCore.join(", ")}`);
2337
- await assertStagedBinariesUsable(staged, toInstall);
2338
- const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
2339
- const backupDir = join8(binDir, `backup-${current}-${stamp}Z`);
2340
- await mkdir5(backupDir, { recursive: false });
2341
- for (const name of toInstall) {
2342
- const target = join8(binDir, name);
2343
- if (await exists(target))
2344
- await copyFile(target, join8(backupDir, name));
2345
- }
2346
- for (const name of toInstall) {
2347
- const stagedPath = join8(staged.dir, name);
2348
- const tempPath = join8(binDir, `.update-${name}`);
2349
- await copyFile(stagedPath, tempPath);
2350
- await chmod3(tempPath, 493);
2351
- await rename3(tempPath, join8(binDir, name));
3075
+ lease = await acquireLease(options.home, "update");
3076
+ console.info(`lease_acquired name=update pid=${process.pid}`);
3077
+ } catch (error) {
3078
+ if (error instanceof LeaseHeldError) {
3079
+ console.error(error.message);
3080
+ throw new Error(`another gajaeway update is running (${error.message})`, { cause: error });
2352
3081
  }
2353
- await writeFile5(releaseMarkerPath(binDir), `${latest}
3082
+ console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
3083
+ throw error;
3084
+ }
3085
+ const installed = await (async () => {
3086
+ let staged;
3087
+ try {
3088
+ staged = await stageRelease(asset, deps);
3089
+ const installedNames = await knownBinariesIn(binDir);
3090
+ const toInstall = binariesToInstall(staged.names, installedNames);
3091
+ const missingCore = CORE_BINARIES.filter((name) => !toInstall.includes(name));
3092
+ if (missingCore.length > 0)
3093
+ throw new Error(`release tarball is missing core binaries: ${missingCore.join(", ")}`);
3094
+ await assertStagedBinariesUsable(staged, toInstall);
3095
+ const gjcVersion = await readPinnedGjcVersion(join10(staged.dir, PIN_MANIFEST));
3096
+ await installPinnedGjc({
3097
+ home: options.home,
3098
+ version: gjcVersion,
3099
+ ...deps.gjcInstallRunner === undefined ? {} : { runner: deps.gjcInstallRunner }
3100
+ });
3101
+ const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
3102
+ const backupDir = join10(binDir, `backup-${current}-${stamp}Z`);
3103
+ await mkdir7(backupDir, { recursive: false });
3104
+ const replaced = [...toInstall, PIN_MANIFEST];
3105
+ for (const name of replaced) {
3106
+ const target = join10(binDir, name);
3107
+ if (await exists(target))
3108
+ await copyFile(target, join10(backupDir, name));
3109
+ }
3110
+ for (const name of replaced) {
3111
+ const stagedPath = join10(staged.dir, name);
3112
+ const tempPath = join10(binDir, `.update-${name}`);
3113
+ await copyFile(stagedPath, tempPath);
3114
+ await chmod5(tempPath, name === PIN_MANIFEST ? 420 : 493);
3115
+ await rename5(tempPath, join10(binDir, name));
3116
+ }
3117
+ await writeFile6(releaseMarkerPath(binDir), `${latest}
2354
3118
  `, "utf8");
2355
- await rm2(staged.dir, { recursive: true, force: true });
2356
- if (options.args.noRestart)
2357
- return {
2358
- current,
2359
- latest,
2360
- tag: release.tag,
2361
- repo,
2362
- checkedOnly: false,
2363
- upToDate: false,
2364
- installed: toInstall,
2365
- backupDir,
2366
- restartSkipped: true
2367
- };
2368
- const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home);
3119
+ const brokerNotEnabled = await installUpdatedServices({
3120
+ home: options.home,
3121
+ binDir,
3122
+ platform,
3123
+ noRestart: options.args.noRestart,
3124
+ deps
3125
+ });
3126
+ return { toInstall, backupDir, brokerNotEnabled };
3127
+ } finally {
3128
+ if (staged !== undefined)
3129
+ await rm4(staged.dir, { recursive: true, force: true });
3130
+ await lease.release();
3131
+ }
3132
+ })();
3133
+ if (options.args.noRestart)
2369
3134
  return {
2370
3135
  current,
2371
3136
  latest,
@@ -2373,26 +3138,37 @@ async function runUpdate(options) {
2373
3138
  repo,
2374
3139
  checkedOnly: false,
2375
3140
  upToDate: false,
2376
- installed: toInstall,
2377
- backupDir,
2378
- restartQueued: receipt.receiptId
3141
+ installed: installed.toInstall,
3142
+ backupDir: installed.backupDir,
3143
+ brokerNotEnabled: installed.brokerNotEnabled,
3144
+ restartSkipped: true
2379
3145
  };
2380
- } finally {
2381
- await unlock();
2382
- }
3146
+ const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home, binDir, "update");
3147
+ return {
3148
+ current,
3149
+ latest,
3150
+ tag: release.tag,
3151
+ repo,
3152
+ checkedOnly: false,
3153
+ upToDate: false,
3154
+ installed: installed.toInstall,
3155
+ backupDir: installed.backupDir,
3156
+ brokerNotEnabled: installed.brokerNotEnabled,
3157
+ restartQueued: receipt.receiptId
3158
+ };
2383
3159
  }
2384
3160
  function binariesToInstall(stagedNames, installedNames) {
2385
3161
  const installed = new Set(installedNames);
2386
3162
  return KNOWN_BINARIES.filter((name) => stagedNames.includes(name) && (installed.has(name) || CORE_BINARIES.includes(name)));
2387
3163
  }
2388
3164
  async function knownBinariesIn(dir) {
2389
- const entries2 = await readdir2(dir).catch(() => []);
3165
+ const entries2 = await readdir3(dir).catch(() => []);
2390
3166
  const names = new Set(entries2);
2391
3167
  return KNOWN_BINARIES.filter((name) => names.has(name));
2392
3168
  }
2393
3169
  async function assertStagedBinariesUsable(staged, toInstall) {
2394
3170
  for (const name of toInstall) {
2395
- const info = await stat2(join8(staged.dir, name));
3171
+ const info = await stat2(join10(staged.dir, name));
2396
3172
  if (!info.isFile())
2397
3173
  throw new Error(`staged ${name} is not a regular file`);
2398
3174
  if (info.size < MIN_BINARY_BYTES)
@@ -2408,10 +3184,10 @@ async function exists(path) {
2408
3184
  }
2409
3185
  }
2410
3186
  async function stageRelease(asset, deps) {
2411
- const dir = await mkdtemp(join8(tmpdir(), "gajaeway-update-"));
3187
+ const dir = await mkdtemp2(join10(tmpdir(), "gajaeway-update-"));
2412
3188
  const tarball = await (deps.downloadAsset ?? defaultDownloadAsset)(asset);
2413
3189
  await (deps.extractTarball ?? defaultExtractTarball)(tarball, dir);
2414
- const names = (await readdir2(dir)).filter((name) => KNOWN_BINARIES.includes(name));
3190
+ const names = (await readdir3(dir)).filter((name) => KNOWN_BINARIES.includes(name));
2415
3191
  return { dir, names };
2416
3192
  }
2417
3193
  async function defaultFetchLatestRelease(repo) {
@@ -2437,8 +3213,8 @@ async function defaultDownloadAsset(asset) {
2437
3213
  return bytes;
2438
3214
  }
2439
3215
  async function defaultExtractTarball(tarball, intoDir) {
2440
- const tarballPath = join8(intoDir, "release.tar.gz");
2441
- await writeFile5(tarballPath, tarball);
3216
+ const tarballPath = join10(intoDir, "release.tar.gz");
3217
+ await writeFile6(tarballPath, tarball);
2442
3218
  const process2 = Bun.spawn(["tar", "-xzf", tarballPath, "-C", intoDir], {
2443
3219
  stdout: "ignore",
2444
3220
  stderr: "pipe"
@@ -2446,186 +3222,19 @@ async function defaultExtractTarball(tarball, intoDir) {
2446
3222
  const exitCode = await process2.exited;
2447
3223
  if (exitCode !== 0) {
2448
3224
  const detail = await new Response(process2.stderr).text();
2449
- throw new Error(`extracting ${basename(tarballPath)} failed (tar exit ${exitCode}): ${detail.trim()}`);
3225
+ throw new Error(`extracting ${basename2(tarballPath)} failed (tar exit ${exitCode}): ${detail.trim()}`);
2450
3226
  }
2451
- await rm2(tarballPath, { force: true });
3227
+ await rm4(tarballPath, { force: true });
2452
3228
  }
2453
- async function defaultQueueRestart(home) {
2454
- const { receipt, supervisorPid } = await launchRestartStack({ home });
3229
+ async function defaultQueueRestart(home, binDir, trigger) {
3230
+ const { receipt, supervisorPid } = await launchRestartStack({ home, binDir, trigger });
2455
3231
  return { receiptId: receipt.id, supervisorPid };
2456
3232
  }
2457
- async function acquireUpdateLock(binDir) {
2458
- const lockPath = join8(binDir, LOCK_FILE);
2459
- for (let attempt = 0;attempt < 2; attempt++) {
2460
- try {
2461
- const handle = await open(lockPath, "wx");
2462
- try {
2463
- await handle.write(`${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}
2464
- `);
2465
- } finally {
2466
- await handle.close();
2467
- }
2468
- return async () => {
2469
- await rm2(lockPath, { force: true });
2470
- };
2471
- } catch (error) {
2472
- if (!isFileExistsError(error))
2473
- throw error;
2474
- const holder = await readLockHolder(lockPath);
2475
- if (holder !== undefined && isProcessAlive(holder)) {
2476
- throw new Error(`another gajaeway update is running (pid ${holder}); refusing to run concurrently`);
2477
- }
2478
- await rm2(lockPath, { force: true });
2479
- }
2480
- }
2481
- throw new Error(`could not acquire the update lock at ${lockPath}`);
2482
- }
2483
- function isFileExistsError(error) {
2484
- return error !== null && typeof error === "object" && error.code === "EEXIST";
2485
- }
2486
- async function readLockHolder(lockPath) {
2487
- try {
2488
- const parsed = JSON.parse(await readFile6(lockPath, "utf8"));
2489
- return typeof parsed.pid === "number" ? parsed.pid : undefined;
2490
- } catch {
2491
- return;
2492
- }
2493
- }
2494
- function isProcessAlive(pid) {
2495
- try {
2496
- process.kill(pid, 0);
2497
- return true;
2498
- } catch (error) {
2499
- return error.code === "EPERM";
2500
- }
2501
- }
2502
-
2503
- // src/upgrade.ts
2504
- import { readFile as readFile7 } from "fs/promises";
2505
- import { dirname as dirname2, join as join9 } from "path";
2506
- async function readGatewayPinnedGjcVersion(home) {
2507
- const candidates = [
2508
- join9(home, "..", "..", "packages", "gateway", "package.json"),
2509
- join9(dirname2(process.execPath), "package.json")
2510
- ];
2511
- for (const path of candidates) {
2512
- try {
2513
- const version = JSON.parse(await readFile7(path, "utf-8")).gjc?.version;
2514
- if (typeof version === "string" && version.length > 0)
2515
- return version;
2516
- } catch {}
2517
- }
2518
- throw new Error(`Failed to read pinned GJC version from any of ${candidates.join(", ")}`);
2519
- }
2520
- async function checkLatestCiStatus(repo) {
2521
- try {
2522
- const branchProc = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
2523
- stdout: "pipe",
2524
- stderr: "pipe"
2525
- });
2526
- if (branchProc.exitCode !== 0) {
2527
- throw new Error("Could not determine current git branch");
2528
- }
2529
- const branch = branchProc.stdout?.toString().trim();
2530
- if (!branch) {
2531
- throw new Error("Empty branch name");
2532
- }
2533
- const runProc = Bun.spawnSync(["gh", "run", "list", "-R", repo, "--branch", branch, "--limit", "1", "--json", "conclusion,status"], {
2534
- stdout: "pipe",
2535
- stderr: "pipe"
2536
- });
2537
- if (runProc.exitCode !== 0) {
2538
- throw new Error(`gh run list failed: ${runProc.stderr?.toString()}`);
2539
- }
2540
- const output = runProc.stdout?.toString() || "[]";
2541
- const runs = JSON.parse(output);
2542
- if (!Array.isArray(runs) || runs.length === 0) {
2543
- throw new Error("No CI runs found for this branch");
2544
- }
2545
- const latestRun = runs[0];
2546
- if (!latestRun.conclusion || !latestRun.status) {
2547
- throw new Error(`Latest CI run status is incomplete: ${JSON.stringify(latestRun)}`);
2548
- }
2549
- if (latestRun.status === "in_progress") {
2550
- throw new Error("CI is currently running; wait for completion");
2551
- }
2552
- return latestRun.conclusion === "success";
2553
- } catch (error) {
2554
- throw new Error(`Failed to check CI status: ${error instanceof Error ? error.message : String(error)}`);
2555
- }
2556
- }
2557
- async function performUpgrade(options) {
2558
- const readVersion = options.readPinnedVersion || readGatewayPinnedGjcVersion;
2559
- const targetVersion = options.gjcVersion ?? await readVersion(options.home);
2560
- try {
2561
- const checkCi = options.checkCiStatus || checkLatestCiStatus;
2562
- let ciPassed = false;
2563
- try {
2564
- ciPassed = await checkCi("Yeachan-Heo/gajae-way");
2565
- } catch (ciError) {
2566
- const ciDetail = ciError instanceof Error ? ciError.message : String(ciError);
2567
- return { status: "failed", detail: `CI status check failed: ${ciDetail}` };
2568
- }
2569
- if (!ciPassed) {
2570
- return {
2571
- status: "failed",
2572
- detail: `Cannot upgrade: latest CI run did not pass. Fix failures and try again.`
2573
- };
2574
- }
2575
- const gjcProc = Bun.spawnSync(["gjc", "--version"], {
2576
- stdout: "pipe",
2577
- stderr: "pipe"
2578
- });
2579
- if (gjcProc.exitCode !== 0) {
2580
- return { status: "failed", detail: "Failed to get current gjc version" };
2581
- }
2582
- const currentVersionMatch = ((gjcProc.stdout?.toString() || "") + (gjcProc.stderr?.toString() || "")).match(/(?:gjc\/)?(\d+)\.(\d+)\.(\d+)/);
2583
- if (!currentVersionMatch) {
2584
- return { status: "failed", detail: "Could not parse current gjc version" };
2585
- }
2586
- const currentVersion = currentVersionMatch.slice(1, 4).join(".");
2587
- if (currentVersion === targetVersion) {
2588
- return { status: "ok", detail: `Already running gjc ${targetVersion}` };
2589
- }
2590
- const installProc = Bun.spawnSync(["bun", "add", "-g", `gajae-code@${targetVersion}`], {
2591
- stdout: "pipe",
2592
- stderr: "pipe",
2593
- cwd: join9(options.home, "workspace")
2594
- });
2595
- if (installProc.exitCode !== 0) {
2596
- return {
2597
- status: "failed",
2598
- detail: `Failed to install gjc ${targetVersion}: ${installProc.stderr?.toString()}`
2599
- };
2600
- }
2601
- const verifyProc = Bun.spawnSync(["gjc", "--version"], {
2602
- stdout: "pipe",
2603
- stderr: "pipe"
2604
- });
2605
- if (verifyProc.exitCode !== 0) {
2606
- return { status: "failed", detail: "Failed to verify gjc installation" };
2607
- }
2608
- const verifiedVersionMatch = ((verifyProc.stdout?.toString() || "") + (verifyProc.stderr?.toString() || "")).match(/(?:gjc\/)?(\d+)\.(\d+)\.(\d+)/);
2609
- if (!verifiedVersionMatch) {
2610
- return { status: "failed", detail: "Could not verify installed gjc version" };
2611
- }
2612
- const verifiedVersion = verifiedVersionMatch.slice(1, 4).join(".");
2613
- if (verifiedVersion !== targetVersion) {
2614
- return {
2615
- status: "failed",
2616
- detail: `Verification failed: expected ${targetVersion}, got ${verifiedVersion}`
2617
- };
2618
- }
2619
- return { status: "ok", detail: `Successfully upgraded gjc from ${currentVersion} to ${targetVersion}` };
2620
- } catch (error) {
2621
- return {
2622
- status: "failed",
2623
- detail: `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`
2624
- };
2625
- }
2626
- }
2627
3233
 
2628
3234
  // src/main.ts
3235
+ async function connectGatewaySocket(path, options = {}) {
3236
+ return GajaewayClient.connectSocketWithBackoff(path, { ...options, deadlineMs: 30000 });
3237
+ }
2629
3238
  function socketPath(home = process.env.GAJAEWAY_HOME) {
2630
3239
  return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
2631
3240
  }
@@ -2644,7 +3253,7 @@ var COMMANDS = [
2644
3253
  "update",
2645
3254
  "setup"
2646
3255
  ];
2647
- var CLI_USAGE = "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 restore <backupPath>|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 (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";
3256
+ var CLI_USAGE = "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";
2648
3257
  var USAGE_EXIT_CODE = 2;
2649
3258
  function usageFor(command) {
2650
3259
  return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
@@ -2753,6 +3362,13 @@ function parseMonitorUpdateArgs(args) {
2753
3362
  var MONITOR_TEST_USAGE = "usage: gajaeway monitors test <id> [--type T] [--payload J] [--wait[=SECONDS]]";
2754
3363
  var MONITOR_WAIT_ERROR = `${MONITOR_TEST_USAGE}
2755
3364
  --wait expects a finite non-negative integer up to 86400`;
3365
+ var MONITOR_OPERATOR_USAGE = "usage: gajaeway monitors settle <eventId>|retry <eventId>";
3366
+ function parseMonitorEventOperatorArgs(args) {
3367
+ const eventId = args[0];
3368
+ if (args.length !== 1 || !eventId?.trim() || eventId.startsWith("--"))
3369
+ throw new Error(MONITOR_OPERATOR_USAGE);
3370
+ return eventId;
3371
+ }
2756
3372
  function parseMonitorTestArgs(args) {
2757
3373
  const [monitorId, ...flags] = args;
2758
3374
  if (!monitorId || monitorId.startsWith("--"))
@@ -2803,7 +3419,7 @@ unknown argument: ${flag}`);
2803
3419
  ...waitSeconds === undefined ? {} : { waitSeconds }
2804
3420
  };
2805
3421
  }
2806
- var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry"]);
3422
+ var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry", "skipped"]);
2807
3423
  function waitForMonitorStage(client, eventId, timeoutSeconds) {
2808
3424
  return new Promise((resolve) => {
2809
3425
  let stage = "admitted";
@@ -2848,11 +3464,23 @@ function renderCycle(cycle) {
2848
3464
  lines.push(`context: unread=${cycle.contextDiff.unread} expired=${cycle.contextDiff.expired} truncated=${cycle.contextDiff.truncated} omitted_oldest=${cycle.contextDiff.omittedOldestAt ?? "-"} omitted_newest=${cycle.contextDiff.omittedNewestAt ?? "-"}`);
2849
3465
  lines.push(`deliveries: pending=${cycle.deliveries.pending} inflight=${cycle.deliveries.inflight} confirmed=${cycle.deliveries.confirmed} failed_ambiguous=${cycle.deliveries.failedAmbiguous} expired=${cycle.deliveries.expired}`);
2850
3466
  lines.push(`memory: queued=${cycle.memoryIntents.queued} written=${cycle.memoryIntents.written} committed=${cycle.memoryIntents.committed} receipted=${cycle.memoryIntents.receipted} quarantined=${cycle.memoryIntents.quarantined}${cycle.memoryClosing ? " (closing)" : ""}`);
3467
+ if (cycle.memoryGitIndexLock) {
3468
+ const { path, ageMs } = cycle.memoryGitIndexLock;
3469
+ lines.push(`memory_git_index_lock: ${path} age=${Math.round(ageMs)}ms`);
3470
+ }
2851
3471
  if (cycle.agentDisk) {
2852
3472
  const { path, freeBytes, totalBytes } = cycle.agentDisk;
2853
3473
  const gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
2854
3474
  lines.push(freeBytes === null || totalBytes === null ? `agent_disk: ${path} unobservable` : `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`);
2855
3475
  }
3476
+ if (cycle.brokerAgent !== null) {
3477
+ lines.push(`broker_agent: ${cycle.brokerAgent.status}`);
3478
+ for (const entry of cycle.brokerAgent.entries)
3479
+ if (entry.state !== "in_sync")
3480
+ lines.push(`broker_agent ${entry.name}: kind=${entry.kind} state=${entry.state}`);
3481
+ }
3482
+ const sloState = cycle.slo.ok ? "ok" : cycle.slo.days.some((day) => day.status === "failed") ? "failed" : cycle.slo.days.some((day) => day.status === "insufficient") ? "insufficient" : "pending";
3483
+ lines.push(`slo: seven-day=${sloState}`);
2856
3484
  lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
2857
3485
  if (cycle.monitorAuthoringLost.length > 0)
2858
3486
  lines.push(`monitor authoring lost: ${cycle.monitorAuthoringLost.map((m) => `${m.eventType}=${m.consecutive} (last ${m.lastFiredAt})`).join(" ")}`);
@@ -2870,82 +3498,110 @@ function renderCycle(cycle) {
2870
3498
  function cycleExitCode(cycle) {
2871
3499
  return cycle.gates.length > 0 ? 1 : 0;
2872
3500
  }
2873
- function verifyBackupIntegrity(backupPath) {
2874
- let database;
2875
- try {
2876
- database = new Database(backupPath, { readonly: true });
2877
- } catch (error) {
2878
- throw new Error(`Backup is not readable: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
2879
- }
2880
- let detail;
2881
- let pageCount;
2882
- try {
2883
- pageCount = database.query("PRAGMA page_count").get()?.page_count ?? 0;
2884
- detail = database.query("PRAGMA integrity_check").get()?.integrity_check ?? "unknown";
2885
- } catch (error) {
2886
- throw new Error(`Backup is not a readable SQLite database: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
2887
- } finally {
2888
- database.close();
3501
+ function renderSlo(result) {
3502
+ const message = result.ok ? `SLO ok: ${result.days.length} consecutive UTC days` : result.days.some((day) => day.status === "failed") ? "SLO failed: counted failure events or journal overflow" : result.days.some((day) => day.status === "insufficient") ? "SLO insufficient: coverage or durable evidence is incomplete" : "SLO pending: an update window is still running; rerun later";
3503
+ const lines = [message];
3504
+ for (const day of result.days)
3505
+ lines.push(`${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}`);
3506
+ for (const window of result.windows)
3507
+ lines.push(`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}`);
3508
+ return lines;
3509
+ }
3510
+ function sloExitCode(result) {
3511
+ return result.ok ? 0 : 1;
3512
+ }
3513
+ var OPS_USAGE = "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]";
3514
+ function parseOpsSloArgs(args) {
3515
+ let days = 7;
3516
+ let json = false;
3517
+ const seen = new Set;
3518
+ for (let index = 0;index < args.length; index++) {
3519
+ const flag = args[index];
3520
+ if (flag === "--json") {
3521
+ if (json)
3522
+ throw new Error(OPS_USAGE);
3523
+ json = true;
3524
+ continue;
3525
+ }
3526
+ if (flag !== "--days" || seen.has(flag))
3527
+ throw new Error(OPS_USAGE);
3528
+ seen.add(flag);
3529
+ const value = args[++index];
3530
+ if (!value || !/^\d+$/.test(value))
3531
+ throw new Error(OPS_USAGE);
3532
+ days = Number(value);
3533
+ if (!Number.isSafeInteger(days) || days < 1 || days > 3650)
3534
+ throw new Error(OPS_USAGE);
2889
3535
  }
2890
- if (pageCount === 0)
2891
- throw new Error(`Backup is an empty SQLite database: ${backupPath}`);
2892
- if (detail !== "ok")
2893
- throw new Error(`Backup failed SQLite integrity_check: ${backupPath} (${detail})`);
3536
+ return { params: { days }, json };
2894
3537
  }
2895
- async function restoreTargetPath(home = gatewayHome()) {
2896
- const configPath = join10(home, "config.json");
2897
- let raw;
2898
- try {
2899
- raw = await readFile8(configPath, "utf8");
2900
- } catch (error) {
2901
- const code = error.code;
2902
- const entryExists = await lstat2(configPath).then(() => true, () => false);
2903
- if (code !== "ENOENT" || entryExists)
2904
- throw new Error(`Cannot read ${configPath}; refusing restore.`);
3538
+ function parseOpsDeliveriesArgs(args) {
3539
+ const params = {};
3540
+ const seen = new Set;
3541
+ let json = false;
3542
+ for (let index = 0;index < args.length; index++) {
3543
+ const flag = args[index];
3544
+ if (flag === "--json") {
3545
+ if (json)
3546
+ throw new Error(OPS_USAGE);
3547
+ json = true;
3548
+ continue;
3549
+ }
3550
+ if (flag !== "--since" && flag !== "--state" && flag !== "--limit")
3551
+ throw new Error(OPS_USAGE);
3552
+ if (seen.has(flag))
3553
+ throw new Error(OPS_USAGE);
3554
+ seen.add(flag);
3555
+ const value = args[++index];
3556
+ if (!value || value.startsWith("--"))
3557
+ throw new Error(OPS_USAGE);
3558
+ if (flag === "--since") {
3559
+ if (!Number.isFinite(Date.parse(value)))
3560
+ throw new Error(OPS_USAGE);
3561
+ params.since = value;
3562
+ } else if (flag === "--state")
3563
+ params.state = value;
3564
+ else {
3565
+ if (!/^\d+$/.test(value))
3566
+ throw new Error(OPS_USAGE);
3567
+ const limit = Number(value);
3568
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
3569
+ throw new Error(OPS_USAGE);
3570
+ params.limit = limit;
3571
+ }
2905
3572
  }
2906
- if (raw === undefined)
2907
- return join10(home, "gateway.db");
2908
- let parsed;
2909
- try {
2910
- parsed = JSON.parse(raw);
2911
- } catch {
2912
- throw new Error(`Cannot parse ${configPath}; refusing restore.`);
3573
+ return { params, json };
3574
+ }
3575
+ function parseWorkJobsArgs(args, usage) {
3576
+ const params = {};
3577
+ for (let index = 0;index < args.length; index++) {
3578
+ const flag = args[index];
3579
+ const value = args[++index];
3580
+ if (flag !== "--limit" && flag !== "--offset" || value === undefined || !/^\d+$/.test(value))
3581
+ throw new Error(usage);
3582
+ const key = flag === "--limit" ? "limit" : "offset";
3583
+ if (params[key] !== undefined)
3584
+ throw new Error(usage);
3585
+ const number = Number(value);
3586
+ if (!Number.isSafeInteger(number) || key === "limit" && (number < 1 || number > WORK_JOBS_MAX_LIMIT))
3587
+ throw new Error(usage);
3588
+ params[key] = number;
2913
3589
  }
2914
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2915
- throw new Error(`Invalid configuration ${configPath}: config must be an object; refusing restore.`);
2916
- const dbPath = parsed.dbPath;
2917
- if (dbPath === undefined)
2918
- return join10(home, "gateway.db");
2919
- if (typeof dbPath !== "string" || dbPath.length === 0)
2920
- throw new Error(`Invalid configuration ${configPath}: dbPath must be a non-empty string; refusing restore.`);
2921
- return dbPath;
2922
- }
2923
- async function restoreDatabase(socket, backupPath) {
2924
- if (!isAbsolute(backupPath))
2925
- throw new Error("ops restore requires an absolute backup path");
2926
- try {
2927
- await stat3(socket);
2928
- throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
2929
- } catch (error) {
2930
- if (error instanceof Error && error.message.startsWith("Refusing restore:"))
2931
- throw error;
2932
- if (error.code !== "ENOENT")
2933
- throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
2934
- }
2935
- verifyBackupIntegrity(backupPath);
2936
- const databasePath = await restoreTargetPath();
2937
- const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
2938
- await copyFile2(databasePath, preservedPath);
2939
- await copyFile2(backupPath, databasePath);
2940
- console.log(`Validated SQLite backup: ${backupPath}`);
2941
- console.log(`Copied current database to: ${preservedPath}`);
2942
- console.log(`Restored backup to: ${databasePath}`);
3590
+ return params;
3591
+ }
3592
+ function renderDeliveries(result) {
3593
+ if (result.rows.length === 0)
3594
+ return ["deliveries: none"];
3595
+ return [
3596
+ `deliveries: ${result.rows.length}`,
3597
+ ...result.rows.map((row) => `${row.createdAt} ${row.state} delivery=${row.deliveryId} turn=${row.turnId} origin=${row.originKey} conversation=${row.conversationId} text=${JSON.stringify(row.textPrefix)}`)
3598
+ ];
2943
3599
  }
2944
3600
  var CHAT_TURN_TIMEOUT_MS = 30 * 60000;
2945
3601
  async function chat(socket) {
2946
3602
  let client;
2947
3603
  try {
2948
- client = await GajaewayClient.connectSocket(socket);
3604
+ client = await connectGatewaySocket(socket);
2949
3605
  } catch {
2950
3606
  console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
2951
3607
  process.exitCode = 1;
@@ -3000,8 +3656,31 @@ async function chat(socket) {
3000
3656
  await client.close();
3001
3657
  }
3002
3658
  }
3659
+ async function pollMemoryOp(client, opRef, maxWaitMs = 300000, pollIntervalMs = 1000) {
3660
+ const startTime = Date.now();
3661
+ let operationElapsedMs = 0;
3662
+ while (Date.now() - startTime < maxWaitMs) {
3663
+ const status = await client.request("memory.status", { opRef });
3664
+ operationElapsedMs = status.elapsedMs;
3665
+ if (status.status === "completed") {
3666
+ return { status: "completed", elapsedMs: status.elapsedMs, result: status.result };
3667
+ }
3668
+ if (status.status === "failed") {
3669
+ return { status: "failed", elapsedMs: status.elapsedMs, error: status.error };
3670
+ }
3671
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
3672
+ }
3673
+ const waitedMs = Date.now() - startTime;
3674
+ console.error(`memory operation still running (${Math.round(operationElapsedMs / 1000)}s operation elapsed; CLI wait ended after ${Math.round(waitedMs / 1000)}s).
3675
+ Check status: gajaeway memory status ${opRef}`);
3676
+ return { status: "still_running", elapsedMs: operationElapsedMs };
3677
+ }
3003
3678
  async function main(args = process.argv.slice(2), options = {}) {
3004
3679
  const parsed = parseArgs(args);
3680
+ if (parsed.command === "--version" && parsed.rest.length === 0) {
3681
+ console.log(package_default.version);
3682
+ return;
3683
+ }
3005
3684
  const usage = usageFor(parsed.command);
3006
3685
  if (usage !== undefined) {
3007
3686
  console.error(usage);
@@ -3010,7 +3689,7 @@ async function main(args = process.argv.slice(2), options = {}) {
3010
3689
  try {
3011
3690
  switch (parsed.command) {
3012
3691
  case "status": {
3013
- const client = await GajaewayClient.connectSocket(parsed.socket);
3692
+ const client = await connectGatewaySocket(parsed.socket);
3014
3693
  try {
3015
3694
  console.log(JSON.stringify(await client.status()));
3016
3695
  } finally {
@@ -3019,7 +3698,7 @@ async function main(args = process.argv.slice(2), options = {}) {
3019
3698
  break;
3020
3699
  }
3021
3700
  case "shutdown": {
3022
- const client = await GajaewayClient.connectSocket(parsed.socket);
3701
+ const client = await connectGatewaySocket(parsed.socket);
3023
3702
  try {
3024
3703
  await client.shutdown();
3025
3704
  } finally {
@@ -3038,7 +3717,7 @@ async function main(args = process.argv.slice(2), options = {}) {
3038
3717
  break;
3039
3718
  case "sessions": {
3040
3719
  const listOptions = parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
3041
- const client = await GajaewayClient.connectSocket(parsed.socket);
3720
+ const client = await connectGatewaySocket(parsed.socket);
3042
3721
  try {
3043
3722
  const result = await client.request("session.list");
3044
3723
  const [command, selector] = parsed.rest;
@@ -3073,30 +3752,12 @@ async function main(args = process.argv.slice(2), options = {}) {
3073
3752
  break;
3074
3753
  }
3075
3754
  case "ops": {
3076
- const [command, path, ...extraArgs] = parsed.rest;
3077
- if (command === "upgrade") {
3078
- const home = gatewayHome();
3079
- let gjcVersion;
3080
- if (path === "--gjc" && extraArgs[0]) {
3081
- gjcVersion = extraArgs[0];
3082
- } else if (path && path !== "--gjc") {
3083
- throw new Error("usage: gajaeway ops upgrade [--gjc X.Y.Z]");
3084
- }
3085
- const result = await performUpgrade({ home, gjcVersion });
3086
- console.log(result.detail);
3087
- if (result.status !== "ok") {
3088
- process.exitCode = 1;
3089
- break;
3090
- }
3091
- const { receipt, supervisorPid } = await launchRestartStack({ home });
3092
- console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
3093
- console.log("read the outcome with: gajaeway ops restart-stack --status");
3094
- break;
3095
- }
3096
- if (command === "restore" && path) {
3097
- await restoreDatabase(parsed.socket, path);
3098
- break;
3099
- }
3755
+ const [command, path] = parsed.rest;
3756
+ if (command === undefined || !["backup", "deliveries", "redeliver", "integrity", "slo", "cycle", "restart-stack"].includes(command))
3757
+ throw new Error(`unknown ops command: ${command ?? "(missing)"}
3758
+ ${OPS_USAGE}`);
3759
+ const deliveries = command === "deliveries" ? parseOpsDeliveriesArgs(parsed.rest.slice(1)) : undefined;
3760
+ const slo = command === "slo" ? parseOpsSloArgs(parsed.rest.slice(1)) : undefined;
3100
3761
  if (command === "restart-stack") {
3101
3762
  const usage2 = "usage: gajaeway ops restart-stack [--status]";
3102
3763
  const home = gatewayHome();
@@ -3115,9 +3776,10 @@ async function main(args = process.argv.slice(2), options = {}) {
3115
3776
  const receipt = await readRestartReceipt(home);
3116
3777
  if (receipt === undefined)
3117
3778
  throw new Error("no restart-stack receipt");
3118
- for (const line of renderRestartReceipt(receipt))
3779
+ const state = await effectiveRestartState(receipt, home);
3780
+ for (const line of renderRestartReceipt(receipt, state))
3119
3781
  console.log(line);
3120
- if (effectiveRestartState(receipt) !== "ok")
3782
+ if (state !== "ok")
3121
3783
  process.exitCode = 1;
3122
3784
  } else if (parsed.rest.length === 1) {
3123
3785
  const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
@@ -3136,11 +3798,18 @@ async function main(args = process.argv.slice(2), options = {}) {
3136
3798
  else
3137
3799
  throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
3138
3800
  }
3139
- const client = await GajaewayClient.connectSocket(parsed.socket);
3801
+ const client = await connectGatewaySocket(parsed.socket);
3140
3802
  try {
3141
3803
  if (command === "backup" && path)
3142
3804
  console.log(JSON.stringify(await client.request("ops.backup", { path })));
3143
- else if (redeliverParams)
3805
+ else if (deliveries) {
3806
+ const result = await client.opsDeliveries(deliveries.params);
3807
+ if (deliveries.json)
3808
+ console.log(JSON.stringify(result));
3809
+ else
3810
+ for (const line of renderDeliveries(result))
3811
+ console.log(line);
3812
+ } else if (redeliverParams)
3144
3813
  console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
3145
3814
  else if (command === "integrity")
3146
3815
  console.log(JSON.stringify(await client.request("ops.integrity")));
@@ -3154,28 +3823,68 @@ async function main(args = process.argv.slice(2), options = {}) {
3154
3823
  for (const line of renderCycle(cycle))
3155
3824
  console.log(line);
3156
3825
  process.exitCode = cycleExitCode(cycle);
3826
+ } else if (slo) {
3827
+ const result = await client.opsSlo(slo.params);
3828
+ if (slo.json)
3829
+ console.log(JSON.stringify(result));
3830
+ else
3831
+ for (const line of renderSlo(result))
3832
+ console.log(line);
3833
+ process.exitCode = sloExitCode(result);
3157
3834
  } else
3158
- throw new Error("usage: gajaeway ops backup <path>|redeliver <deliveryId>|redeliver --since <iso>|cycle [--json]|integrity|restore <backupPath>|upgrade [--gjc X.Y.Z]|restart-stack [--status]");
3835
+ throw new Error(OPS_USAGE);
3159
3836
  } finally {
3160
3837
  await client.close();
3161
3838
  }
3162
3839
  break;
3163
3840
  }
3164
3841
  case "memory": {
3165
- const usage2 = "usage: gajaeway memory audit|memory autolink|memory search <query>";
3842
+ const usage2 = "usage: gajaeway memory audit|autolink|search <query>|status <opRef>";
3166
3843
  if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
3167
3844
  throw new Error(`${usage2} (unknown argument: ${parsed.rest[1]})`);
3168
- const client = await GajaewayClient.connectSocket(parsed.socket);
3845
+ if (parsed.rest[0] === "status" && parsed.rest.length < 2)
3846
+ throw new Error(`${usage2} (status requires opRef)`);
3847
+ const client = await connectGatewaySocket(parsed.socket);
3169
3848
  try {
3170
3849
  if (parsed.rest[0] === "audit") {
3171
- const result = await client.request("memory.audit");
3172
- console.log(JSON.stringify(result.issues));
3173
- if (!result.ok)
3850
+ const initResp = await client.request("memory.audit");
3851
+ const poll = await pollMemoryOp(client, initResp.opRef);
3852
+ if (poll.status === "failed") {
3853
+ console.error(`audit failed after ${poll.elapsedMs}ms: ${poll.error}`);
3174
3854
  process.exitCode = 1;
3855
+ } else if (poll.status === "still_running") {
3856
+ process.exitCode = 75;
3857
+ } else {
3858
+ const auditResult = poll.result;
3859
+ console.log(JSON.stringify(auditResult?.issues ?? []));
3860
+ console.error(`audit completed in ${poll.elapsedMs}ms`);
3861
+ if (!auditResult?.ok)
3862
+ process.exitCode = 1;
3863
+ }
3175
3864
  } else if (parsed.rest[0] === "autolink") {
3176
- console.log(JSON.stringify(await client.request("memory.autolink")));
3865
+ const initResp = await client.request("memory.autolink");
3866
+ const poll = await pollMemoryOp(client, initResp.opRef);
3867
+ if (poll.status === "failed") {
3868
+ console.error(`autolink failed after ${poll.elapsedMs}ms: ${poll.error}`);
3869
+ process.exitCode = 1;
3870
+ } else if (poll.status === "still_running") {
3871
+ process.exitCode = 75;
3872
+ } else {
3873
+ console.log(JSON.stringify(poll.result));
3874
+ }
3177
3875
  } else if (parsed.rest[0] === "search" && parsed.rest.slice(1).join(" ")) {
3178
3876
  console.log(JSON.stringify(await client.request("memory.search", { query: parsed.rest.slice(1).join(" ") })));
3877
+ } else if (parsed.rest[0] === "status" && parsed.rest[1]) {
3878
+ const opRef = parsed.rest[1];
3879
+ const result = await client.request("memory.status", { opRef });
3880
+ if (result.status === "failed") {
3881
+ console.error(`operation ${opRef} failed after ${result.elapsedMs}ms: ${result.error}`);
3882
+ process.exitCode = 1;
3883
+ } else if (result.status === "completed") {
3884
+ console.log(JSON.stringify(result));
3885
+ } else {
3886
+ console.log(JSON.stringify({ status: result.status, elapsedMs: result.elapsedMs }));
3887
+ }
3179
3888
  } else
3180
3889
  throw new Error(usage2);
3181
3890
  } finally {
@@ -3188,7 +3897,8 @@ async function main(args = process.argv.slice(2), options = {}) {
3188
3897
  const listOptions = command === "list" ? parseListOptions(args2, MONITOR_COLUMNS) : undefined;
3189
3898
  const update = command === "update" ? parseMonitorUpdateArgs(args2) : undefined;
3190
3899
  const monitorTest = command === "test" ? parseMonitorTestArgs(args2) : undefined;
3191
- const client = await GajaewayClient.connectSocket(parsed.socket);
3900
+ const operatorEventId = command === "settle" || command === "retry" ? parseMonitorEventOperatorArgs(args2) : undefined;
3901
+ const client = await connectGatewaySocket(parsed.socket);
3192
3902
  try {
3193
3903
  if (command === "add" && args2[0] === "--json" && args2[1])
3194
3904
  console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
@@ -3223,8 +3933,15 @@ async function main(args = process.argv.slice(2), options = {}) {
3223
3933
  const stage = await waitForMonitorStage(client, result.eventId, monitorTest.waitSeconds);
3224
3934
  console.log(JSON.stringify({ eventId: result.eventId, stage }));
3225
3935
  }
3936
+ } else if ((command === "settle" || command === "retry") && operatorEventId !== undefined) {
3937
+ const result = await client.request(`monitor.${command}`, {
3938
+ eventId: operatorEventId
3939
+ });
3940
+ console.log(JSON.stringify(result));
3941
+ if (!result.done)
3942
+ process.exitCode = 1;
3226
3943
  } else
3227
- throw new Error(`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]]`);
3944
+ throw new Error(`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]]`);
3228
3945
  } finally {
3229
3946
  await client.close();
3230
3947
  }
@@ -3238,7 +3955,7 @@ async function main(args = process.argv.slice(2), options = {}) {
3238
3955
  const text2 = args2.slice(1).join(" ").trim();
3239
3956
  if (!name2 || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name2) || (command === "status" ? args2.length !== 1 : !text2 || args2.slice(1).some((arg) => arg.startsWith("--"))))
3240
3957
  throw new Error(usage2);
3241
- const client2 = await GajaewayClient.connectSocket(parsed.socket);
3958
+ const client2 = await connectGatewaySocket(parsed.socket);
3242
3959
  try {
3243
3960
  if (command === "status") {
3244
3961
  console.log(JSON.stringify(await client2.request("work.status", { name: name2 })));
@@ -3257,6 +3974,7 @@ async function main(args = process.argv.slice(2), options = {}) {
3257
3974
  let force = false;
3258
3975
  let allDead = false;
3259
3976
  let name2;
3977
+ let jobsPage;
3260
3978
  if (command === "retire") {
3261
3979
  for (let i = 0;i < args2.length; i++) {
3262
3980
  const arg = args2[i];
@@ -3280,9 +3998,9 @@ async function main(args = process.argv.slice(2), options = {}) {
3280
3998
  if (allDead && name2)
3281
3999
  throw new Error(usage2);
3282
4000
  } else if (args2.length !== 0) {
3283
- throw new Error(usage2);
4001
+ jobsPage = parseWorkJobsArgs(args2, usage2);
3284
4002
  }
3285
- const client2 = await GajaewayClient.connectSocket(parsed.socket);
4003
+ const client2 = await connectGatewaySocket(parsed.socket);
3286
4004
  try {
3287
4005
  if (command === "retire") {
3288
4006
  const result = await client2.request("work.retire", {
@@ -3297,13 +4015,18 @@ async function main(args = process.argv.slice(2), options = {}) {
3297
4015
  console.log(result.retired ? `retired: ${result.sessionKey} session=${result.sessionId} closed=${result.closed}${forced}` : `not retired: ${result.reason}`);
3298
4016
  }
3299
4017
  } else {
3300
- const result = await client2.request("work.jobs");
4018
+ const result = await client2.request("work.jobs", jobsPage);
3301
4019
  for (const job of result.jobs) {
3302
4020
  const name3 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
3303
4021
  const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
3304
4022
  const head = job.last_commit ? `${job.last_commit.sha.slice(0, 7)}@${job.last_commit.committed_at} ${JSON.stringify(job.last_commit.subject)}` : "-";
3305
4023
  console.log(`${name3} ${state} session=${job.session_id || "-"} accepted=${job.accepted_at || "-"} op=${job.attempt?.op_ref || "-"} last=${job.last_activity_at || "-"} head=${head} ${job.worktree_path}${job.reports && (job.reports.pending !== 0 || job.reports.claimed !== 0 || job.reports.held !== 0 || job.reports.undeliverable !== 0) ? ` reports=p:${job.reports.pending} c:${job.reports.claimed} h:${job.reports.held} u:${job.reports.undeliverable}` : ""}`);
3306
4024
  }
4025
+ const shownEnd = result.offset + result.jobs.length;
4026
+ if (result.jobs.length === 0)
4027
+ console.error(`jobs: none at offset ${result.offset} of ${result.total}`);
4028
+ else if (result.offset > 0 || shownEnd < result.total)
4029
+ console.error(`jobs ${result.offset + 1}-${shownEnd} of ${result.total}${shownEnd < result.total ? ` (next: --offset ${shownEnd})` : ""}`);
3307
4030
  }
3308
4031
  } finally {
3309
4032
  await client2.close();
@@ -3315,11 +4038,13 @@ async function main(args = process.argv.slice(2), options = {}) {
3315
4038
  const name = args2[0];
3316
4039
  let cwd;
3317
4040
  let resume = false;
4041
+ let repo;
4042
+ let base;
3318
4043
  let model;
3319
4044
  const textParts = [];
3320
4045
  for (let i = 1;i < args2.length; i++) {
3321
4046
  const arg = args2[i];
3322
- if (arg === "--cwd" || arg === "--model" || arg === "--preset") {
4047
+ if (arg === "--cwd" || arg === "--repo" || arg === "--base" || arg === "--model" || arg === "--preset") {
3323
4048
  const value = args2[++i];
3324
4049
  if (!value?.trim() || value.startsWith("--"))
3325
4050
  throw new Error(usage2);
@@ -3327,6 +4052,14 @@ async function main(args = process.argv.slice(2), options = {}) {
3327
4052
  if (cwd !== undefined || !isAbsolute(value))
3328
4053
  throw new Error(usage2);
3329
4054
  cwd = value;
4055
+ } else if (arg === "--repo") {
4056
+ if (repo !== undefined)
4057
+ throw new Error(usage2);
4058
+ repo = value;
4059
+ } else if (arg === "--base") {
4060
+ if (base !== undefined)
4061
+ throw new Error(usage2);
4062
+ base = value;
3330
4063
  } else {
3331
4064
  if (model !== undefined)
3332
4065
  throw new Error(`${usage2}
@@ -3343,7 +4076,7 @@ async function main(args = process.argv.slice(2), options = {}) {
3343
4076
  const text = textParts.join(" ").trim();
3344
4077
  if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text)
3345
4078
  throw new Error(usage2);
3346
- const client = await GajaewayClient.connectSocket(parsed.socket, {
4079
+ const client = await connectGatewaySocket(parsed.socket, {
3347
4080
  requestTimeoutMs: command === "run" ? 3600000 : 120000
3348
4081
  });
3349
4082
  try {
@@ -3352,6 +4085,8 @@ async function main(args = process.argv.slice(2), options = {}) {
3352
4085
  name,
3353
4086
  text,
3354
4087
  ...cwd ? { cwd } : {},
4088
+ ...repo ? { repo } : {},
4089
+ ...base ? { base } : {},
3355
4090
  ...resume ? { resume: true } : {},
3356
4091
  ...model === undefined ? {} : { model },
3357
4092
  ...callerSessionId ? { callerSessionId } : {}
@@ -3430,10 +4165,11 @@ export {
3430
4165
  main,
3431
4166
  parseArgs,
3432
4167
  parseServicesArgs,
4168
+ pollMemoryOp,
3433
4169
  renderCycle,
3434
- restoreDatabase,
3435
- restoreTargetPath,
4170
+ renderDeliveries,
4171
+ renderSlo,
4172
+ sloExitCode,
3436
4173
  socketPath,
3437
- usageFor,
3438
- verifyBackupIntegrity
4174
+ usageFor
3439
4175
  };