@schuttdev/gigai 0.1.0-beta.13 → 0.1.0-beta.15

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.
@@ -45,11 +45,16 @@ import { spawn as spawn4 } from "child_process";
45
45
  import { input, select, checkbox, confirm } from "@inquirer/prompts";
46
46
  import { writeFile as writeFile2 } from "fs/promises";
47
47
  import { resolve as resolve4 } from "path";
48
- import { execFile } from "child_process";
48
+ import { execFile, spawn as spawn5 } from "child_process";
49
49
  import { promisify } from "util";
50
50
  import { input as input2 } from "@inquirer/prompts";
51
51
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
52
52
  import { resolve as resolve5 } from "path";
53
+ import { writeFile as writeFile4 } from "fs/promises";
54
+ import { resolve as resolve6, join as join3 } from "path";
55
+ import { homedir, platform } from "os";
56
+ import { execFile as execFile2 } from "child_process";
57
+ import { promisify as promisify2 } from "util";
53
58
  function connectWithToken(store, encryptedToken, orgUuid, encryptionKey, sessionTtlSeconds) {
54
59
  let payload;
55
60
  try {
@@ -267,7 +272,7 @@ function executeTool(entry, args, timeout) {
267
272
  `Cannot execute tool of type: ${entry.type}`
268
273
  );
269
274
  }
270
- return new Promise((resolve6, reject) => {
275
+ return new Promise((resolve7, reject) => {
271
276
  const start = Date.now();
272
277
  const stdoutChunks = [];
273
278
  const stderrChunks = [];
@@ -315,7 +320,7 @@ function executeTool(entry, args, timeout) {
315
320
  reject(new GigaiError(ErrorCode.EXEC_TIMEOUT, `Tool execution timed out after ${effectiveTimeout}ms`));
316
321
  return;
317
322
  }
318
- resolve6({
323
+ resolve7({
319
324
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
320
325
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
321
326
  exitCode: exitCode ?? 1,
@@ -607,7 +612,7 @@ async function execCommandSafe(command, args, config) {
607
612
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Null byte in argument");
608
613
  }
609
614
  }
610
- return new Promise((resolve6, reject) => {
615
+ return new Promise((resolve7, reject) => {
611
616
  const child = spawn2(command, args, {
612
617
  shell: false,
613
618
  stdio: ["ignore", "pipe", "pipe"]
@@ -629,7 +634,7 @@ async function execCommandSafe(command, args, config) {
629
634
  reject(new GigaiError(ErrorCode.EXEC_FAILED, `Failed to spawn ${command}: ${err.message}`));
630
635
  });
631
636
  child.on("close", (exitCode) => {
632
- resolve6({
637
+ resolve7({
633
638
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
634
639
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
635
640
  exitCode: exitCode ?? 1
@@ -787,7 +792,9 @@ async function createServer(opts) {
787
792
  trustProxy: !dev
788
793
  // Only trust proxy headers in production (behind HTTPS reverse proxy)
789
794
  });
790
- if (!dev) {
795
+ const httpsProvider = config.server.https?.provider;
796
+ const behindTunnel = httpsProvider === "tailscale" || httpsProvider === "cloudflare";
797
+ if (!dev && !behindTunnel) {
791
798
  server.addHook("onRequest", async (request, _reply) => {
792
799
  if (request.protocol !== "https") {
793
800
  throw new GigaiError(ErrorCode.HTTPS_REQUIRED, "HTTPS is required");
@@ -831,13 +838,13 @@ async function loadConfig(path) {
831
838
  return GigaiConfigSchema.parse(json);
832
839
  }
833
840
  function runCommand(command, args) {
834
- return new Promise((resolve6, reject) => {
841
+ return new Promise((resolve7, reject) => {
835
842
  const child = spawn3(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
836
843
  const chunks = [];
837
844
  child.stdout.on("data", (chunk) => chunks.push(chunk));
838
845
  child.on("error", reject);
839
846
  child.on("close", (exitCode) => {
840
- resolve6({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
847
+ resolve7({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
841
848
  });
842
849
  });
843
850
  }
@@ -867,7 +874,7 @@ async function enableFunnel(port) {
867
874
  if (exitCode !== 0) {
868
875
  throw new Error(`Failed to enable Tailscale Funnel: ${stdout}`);
869
876
  }
870
- return `https://${status.hostname}:${port}`;
877
+ return `https://${status.hostname}`;
871
878
  }
872
879
  async function disableFunnel(port) {
873
880
  await runCommand("tailscale", ["funnel", "--bg", "off", `${port}`]);
@@ -899,6 +906,41 @@ async function getTailscaleDnsName() {
899
906
  return null;
900
907
  }
901
908
  }
909
+ async function ensureTailscaleFunnel(port) {
910
+ const dnsName = await getTailscaleDnsName();
911
+ if (!dnsName) {
912
+ throw new Error("Tailscale is not running or not connected. Install/start Tailscale first.");
913
+ }
914
+ console.log(" Enabling Tailscale Funnel...");
915
+ try {
916
+ const { stdout, stderr } = await execFileAsync("tailscale", ["funnel", "--bg", `${port}`]);
917
+ const output = stdout + stderr;
918
+ if (output.includes("Funnel is not enabled")) {
919
+ const urlMatch = output.match(/(https:\/\/login\.tailscale\.com\/\S+)/);
920
+ const enableUrl = urlMatch?.[1] ?? "https://login.tailscale.com/admin/machines";
921
+ console.log(`
922
+ Funnel is not enabled on your tailnet.`);
923
+ console.log(` Enable it here: ${enableUrl}
924
+ `);
925
+ await confirm({ message: "I've enabled Funnel in my Tailscale admin. Continue?", default: true });
926
+ const retry = await execFileAsync("tailscale", ["funnel", "--bg", `${port}`]);
927
+ if ((retry.stdout + retry.stderr).includes("Funnel is not enabled")) {
928
+ throw new Error("Funnel is still not enabled. Please enable it in your Tailscale admin and try again.");
929
+ }
930
+ }
931
+ } catch (e) {
932
+ if (e.message.includes("Funnel is still not enabled")) throw e;
933
+ }
934
+ try {
935
+ const { stdout } = await execFileAsync("tailscale", ["funnel", "status"]);
936
+ if (stdout.includes("No serve config")) {
937
+ throw new Error("Funnel setup failed. Run 'tailscale funnel --bg " + port + "' manually to debug.");
938
+ }
939
+ } catch {
940
+ }
941
+ console.log(` Tailscale Funnel active: https://${dnsName}`);
942
+ return `https://${dnsName}`;
943
+ }
902
944
  async function runInit() {
903
945
  console.log("\n gigai server setup\n");
904
946
  const httpsProvider = await select({
@@ -917,7 +959,6 @@ async function runInit() {
917
959
  provider: "tailscale",
918
960
  funnelPort: 7443
919
961
  };
920
- console.log(" Will use Tailscale Funnel for HTTPS.");
921
962
  break;
922
963
  case "cloudflare": {
923
964
  const tunnelName = await input({
@@ -1021,10 +1062,11 @@ async function runInit() {
1021
1062
  Config written to: ${configPath}`);
1022
1063
  let serverUrl;
1023
1064
  if (httpsProvider === "tailscale") {
1024
- const dnsName = await getTailscaleDnsName();
1025
- if (dnsName) {
1026
- serverUrl = `https://${dnsName}:${port}`;
1027
- console.log(` Detected Tailscale URL: ${serverUrl}`);
1065
+ try {
1066
+ serverUrl = await ensureTailscaleFunnel(port);
1067
+ } catch (e) {
1068
+ console.error(` ${e.message}`);
1069
+ console.log(" You can enable Funnel later and run 'gigai server start' manually.\n");
1028
1070
  }
1029
1071
  } else if (httpsProvider === "cloudflare" && httpsConfig && "domain" in httpsConfig && httpsConfig.domain) {
1030
1072
  serverUrl = `https://${httpsConfig.domain}`;
@@ -1036,16 +1078,29 @@ async function runInit() {
1036
1078
  required: true
1037
1079
  });
1038
1080
  }
1081
+ console.log("\n Starting server...");
1082
+ const serverArgs = ["server", "start", "--config", configPath];
1083
+ if (!httpsConfig) serverArgs.push("--dev");
1084
+ const child = spawn5("gigai", serverArgs, {
1085
+ detached: true,
1086
+ stdio: "ignore",
1087
+ cwd: resolve4(".")
1088
+ });
1089
+ child.unref();
1090
+ await new Promise((r) => setTimeout(r, 1500));
1091
+ try {
1092
+ const res = await fetch(`http://localhost:${port}/health`);
1093
+ if (res.ok) {
1094
+ console.log(` Server running on port ${port} (PID ${child.pid})`);
1095
+ }
1096
+ } catch {
1097
+ console.log(` Server starting in background (PID ${child.pid})`);
1098
+ }
1039
1099
  const store = new AuthStore();
1040
1100
  const code = generatePairingCode(store, config.auth.pairingTtlSeconds);
1041
1101
  store.destroy();
1042
1102
  console.log(`
1043
- Pairing code: ${code}`);
1044
- console.log(` Expires in ${config.auth.pairingTtlSeconds / 60} minutes.
1045
- `);
1046
- console.log(` Start the server with: gigai server start${httpsConfig ? "" : " --dev"}
1047
- `);
1048
- console.log(` Then paste this into Claude to pair:
1103
+ Paste this into Claude to pair:
1049
1104
  `);
1050
1105
  console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1051
1106
  console.log(` Install the gigai CLI and pair with my server:
@@ -1057,6 +1112,10 @@ async function runInit() {
1057
1112
  `);
1058
1113
  console.log(` Then show me the skill file output so I can save it.`);
1059
1114
  console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
1115
+ console.log(`
1116
+ Pairing code expires in ${config.auth.pairingTtlSeconds / 60} minutes.`);
1117
+ console.log(` Run 'gigai server pair' to generate a new one.
1118
+ `);
1060
1119
  }
1061
1120
  async function loadConfigFile(path) {
1062
1121
  const configPath = resolve5(path ?? "gigai.config.json");
@@ -1181,6 +1240,125 @@ Note: The server must be running for the client to use this code.`);
1181
1240
  console.log(`For a live pairing code, use the server's /auth/pair/generate endpoint.`);
1182
1241
  store.destroy();
1183
1242
  }
1243
+ var execFileAsync2 = promisify2(execFile2);
1244
+ function getGigaiBin() {
1245
+ return process.argv[1] ?? "gigai";
1246
+ }
1247
+ function getLaunchdPlist(configPath) {
1248
+ const bin = getGigaiBin();
1249
+ return `<?xml version="1.0" encoding="UTF-8"?>
1250
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1251
+ <plist version="1.0">
1252
+ <dict>
1253
+ <key>Label</key>
1254
+ <string>com.gigai.server</string>
1255
+ <key>ProgramArguments</key>
1256
+ <array>
1257
+ <string>${bin}</string>
1258
+ <string>server</string>
1259
+ <string>start</string>
1260
+ <string>--config</string>
1261
+ <string>${configPath}</string>
1262
+ </array>
1263
+ <key>RunAtLoad</key>
1264
+ <true/>
1265
+ <key>KeepAlive</key>
1266
+ <true/>
1267
+ <key>StandardOutPath</key>
1268
+ <string>${join3(homedir(), ".gigai", "server.log")}</string>
1269
+ <key>StandardErrorPath</key>
1270
+ <string>${join3(homedir(), ".gigai", "server.log")}</string>
1271
+ <key>WorkingDirectory</key>
1272
+ <string>${homedir()}</string>
1273
+ </dict>
1274
+ </plist>
1275
+ `;
1276
+ }
1277
+ function getSystemdUnit(configPath) {
1278
+ const bin = getGigaiBin();
1279
+ return `[Unit]
1280
+ Description=gigai server
1281
+ After=network.target
1282
+
1283
+ [Service]
1284
+ Type=simple
1285
+ ExecStart=${bin} server start --config ${configPath}
1286
+ Restart=always
1287
+ RestartSec=5
1288
+ WorkingDirectory=${homedir()}
1289
+
1290
+ [Install]
1291
+ WantedBy=default.target
1292
+ `;
1293
+ }
1294
+ async function installDaemon(configPath) {
1295
+ const config = resolve6(configPath ?? "gigai.config.json");
1296
+ const os = platform();
1297
+ if (os === "darwin") {
1298
+ const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1299
+ await writeFile4(plistPath, getLaunchdPlist(config));
1300
+ console.log(` Wrote launchd plist: ${plistPath}`);
1301
+ try {
1302
+ await execFileAsync2("launchctl", ["load", plistPath]);
1303
+ console.log(" Service loaded and started.");
1304
+ } catch {
1305
+ console.log(` Load it with: launchctl load ${plistPath}`);
1306
+ }
1307
+ console.log(` Logs: ~/.gigai/server.log`);
1308
+ console.log(` Stop: launchctl unload ${plistPath}`);
1309
+ } else if (os === "linux") {
1310
+ const unitDir = join3(homedir(), ".config", "systemd", "user");
1311
+ const unitPath = join3(unitDir, "gigai.service");
1312
+ const { mkdir: mkdir2 } = await import("fs/promises");
1313
+ await mkdir2(unitDir, { recursive: true });
1314
+ await writeFile4(unitPath, getSystemdUnit(config));
1315
+ console.log(` Wrote systemd unit: ${unitPath}`);
1316
+ try {
1317
+ await execFileAsync2("systemctl", ["--user", "daemon-reload"]);
1318
+ await execFileAsync2("systemctl", ["--user", "enable", "--now", "gigai"]);
1319
+ console.log(" Service enabled and started.");
1320
+ } catch {
1321
+ console.log(" Enable it with: systemctl --user enable --now gigai");
1322
+ }
1323
+ console.log(` Logs: journalctl --user -u gigai -f`);
1324
+ console.log(` Stop: systemctl --user stop gigai`);
1325
+ console.log(` Remove: systemctl --user disable gigai`);
1326
+ } else {
1327
+ console.log(" Persistent daemon not supported on this platform.");
1328
+ console.log(" Run 'gigai server start' manually.");
1329
+ }
1330
+ }
1331
+ async function uninstallDaemon() {
1332
+ const os = platform();
1333
+ if (os === "darwin") {
1334
+ const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1335
+ try {
1336
+ await execFileAsync2("launchctl", ["unload", plistPath]);
1337
+ } catch {
1338
+ }
1339
+ const { unlink: unlink2 } = await import("fs/promises");
1340
+ try {
1341
+ await unlink2(plistPath);
1342
+ console.log(" Service removed.");
1343
+ } catch {
1344
+ console.log(" No service found.");
1345
+ }
1346
+ } else if (os === "linux") {
1347
+ try {
1348
+ await execFileAsync2("systemctl", ["--user", "disable", "--now", "gigai"]);
1349
+ } catch {
1350
+ }
1351
+ const unitPath = join3(homedir(), ".config", "systemd", "user", "gigai.service");
1352
+ const { unlink: unlink2 } = await import("fs/promises");
1353
+ try {
1354
+ await unlink2(unitPath);
1355
+ await execFileAsync2("systemctl", ["--user", "daemon-reload"]);
1356
+ console.log(" Service removed.");
1357
+ } catch {
1358
+ console.log(" No service found.");
1359
+ }
1360
+ }
1361
+ }
1184
1362
  async function startServer() {
1185
1363
  const { values } = parseArgs({
1186
1364
  options: {
@@ -1236,9 +1414,11 @@ async function startServer() {
1236
1414
  export {
1237
1415
  createServer,
1238
1416
  generateServerPairingCode,
1417
+ installDaemon,
1239
1418
  loadConfig,
1240
1419
  runInit,
1241
1420
  startServer,
1421
+ uninstallDaemon,
1242
1422
  unwrapTool,
1243
1423
  wrapCli,
1244
1424
  wrapImport,
package/dist/index.js CHANGED
@@ -538,7 +538,7 @@ function runCitty() {
538
538
  dev: { type: "boolean", description: "Development mode (no HTTPS)" }
539
539
  },
540
540
  async run({ args }) {
541
- const { startServer } = await import("./dist-SIS4UF2D.js");
541
+ const { startServer } = await import("./dist-GANOIL6Z.js");
542
542
  const extraArgs = [];
543
543
  if (args.config) extraArgs.push("--config", args.config);
544
544
  if (args.dev) extraArgs.push("--dev");
@@ -549,7 +549,7 @@ function runCitty() {
549
549
  init: defineCommand({
550
550
  meta: { name: "init", description: "Interactive setup wizard" },
551
551
  async run() {
552
- const { runInit } = await import("./dist-SIS4UF2D.js");
552
+ const { runInit } = await import("./dist-GANOIL6Z.js");
553
553
  await runInit();
554
554
  }
555
555
  }),
@@ -559,10 +559,27 @@ function runCitty() {
559
559
  config: { type: "string", alias: "c", description: "Config file path" }
560
560
  },
561
561
  async run({ args }) {
562
- const { generateServerPairingCode } = await import("./dist-SIS4UF2D.js");
562
+ const { generateServerPairingCode } = await import("./dist-GANOIL6Z.js");
563
563
  await generateServerPairingCode(args.config);
564
564
  }
565
565
  }),
566
+ install: defineCommand({
567
+ meta: { name: "install", description: "Install as persistent background service" },
568
+ args: {
569
+ config: { type: "string", alias: "c", description: "Config file path" }
570
+ },
571
+ async run({ args }) {
572
+ const { installDaemon } = await import("./dist-GANOIL6Z.js");
573
+ await installDaemon(args.config);
574
+ }
575
+ }),
576
+ uninstall: defineCommand({
577
+ meta: { name: "uninstall", description: "Remove background service" },
578
+ async run() {
579
+ const { uninstallDaemon } = await import("./dist-GANOIL6Z.js");
580
+ await uninstallDaemon();
581
+ }
582
+ }),
566
583
  status: defineCommand({
567
584
  meta: { name: "status", description: "Show server status" },
568
585
  async run() {
@@ -586,21 +603,21 @@ function runCitty() {
586
603
  cli: defineCommand({
587
604
  meta: { name: "cli", description: "Wrap a CLI command" },
588
605
  async run() {
589
- const { wrapCli } = await import("./dist-SIS4UF2D.js");
606
+ const { wrapCli } = await import("./dist-GANOIL6Z.js");
590
607
  await wrapCli();
591
608
  }
592
609
  }),
593
610
  mcp: defineCommand({
594
611
  meta: { name: "mcp", description: "Wrap an MCP server" },
595
612
  async run() {
596
- const { wrapMcp } = await import("./dist-SIS4UF2D.js");
613
+ const { wrapMcp } = await import("./dist-GANOIL6Z.js");
597
614
  await wrapMcp();
598
615
  }
599
616
  }),
600
617
  script: defineCommand({
601
618
  meta: { name: "script", description: "Wrap a script" },
602
619
  async run() {
603
- const { wrapScript } = await import("./dist-SIS4UF2D.js");
620
+ const { wrapScript } = await import("./dist-GANOIL6Z.js");
604
621
  await wrapScript();
605
622
  }
606
623
  }),
@@ -610,7 +627,7 @@ function runCitty() {
610
627
  path: { type: "positional", description: "Path to config file", required: true }
611
628
  },
612
629
  async run({ args }) {
613
- const { wrapImport } = await import("./dist-SIS4UF2D.js");
630
+ const { wrapImport } = await import("./dist-GANOIL6Z.js");
614
631
  await wrapImport(args.path);
615
632
  }
616
633
  })
@@ -622,7 +639,7 @@ function runCitty() {
622
639
  name: { type: "positional", description: "Tool name", required: true }
623
640
  },
624
641
  async run({ args }) {
625
- const { unwrapTool } = await import("./dist-SIS4UF2D.js");
642
+ const { unwrapTool } = await import("./dist-GANOIL6Z.js");
626
643
  await unwrapTool(args.name);
627
644
  }
628
645
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schuttdev/gigai",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gigai": "dist/index.js"