@schuttdev/gigai 0.1.0-beta.8 → 0.2.2

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.
@@ -1,11 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- AuthStore
4
- } from "./chunk-OWDYY3IG.js";
5
- import {
6
- generatePairingCode,
7
- validateAndPair
8
- } from "./chunk-FN4LCKUA.js";
9
2
  import {
10
3
  ErrorCode,
11
4
  GigaiConfigSchema,
@@ -13,7 +6,7 @@ import {
13
6
  decrypt,
14
7
  encrypt,
15
8
  generateEncryptionKey
16
- } from "./chunk-4XUWD3DZ.js";
9
+ } from "./chunk-7C3UYEKE.js";
17
10
 
18
11
  // ../server/dist/index.mjs
19
12
  import { parseArgs } from "util";
@@ -22,7 +15,10 @@ import cors from "@fastify/cors";
22
15
  import rateLimit from "@fastify/rate-limit";
23
16
  import multipart from "@fastify/multipart";
24
17
  import fp from "fastify-plugin";
18
+ import { nanoid } from "nanoid";
25
19
  import { randomBytes } from "crypto";
20
+ import { hostname } from "os";
21
+ import { nanoid as nanoid2 } from "nanoid";
26
22
  import fp2 from "fastify-plugin";
27
23
  import fp3 from "fastify-plugin";
28
24
  import { spawn } from "child_process";
@@ -38,18 +34,91 @@ import { spawn as spawn2 } from "child_process";
38
34
  import { writeFile, readFile as readFile2, unlink, mkdir } from "fs/promises";
39
35
  import { join as join2 } from "path";
40
36
  import { tmpdir } from "os";
41
- import { nanoid } from "nanoid";
37
+ import { nanoid as nanoid3 } from "nanoid";
38
+ import { execFile, spawn as spawn3 } from "child_process";
39
+ import { promisify } from "util";
42
40
  import { readFile as readFile3 } from "fs/promises";
43
41
  import { resolve as resolve3 } from "path";
42
+ import { spawn as spawn4 } from "child_process";
43
+ import { spawn as spawn5 } from "child_process";
44
44
  import { input, select, checkbox, confirm } from "@inquirer/prompts";
45
- import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
46
- import { resolve as resolve4, join as join3 } from "path";
47
- import { execFile } from "child_process";
48
- import { promisify } from "util";
49
- import { randomBytes as randomBytes2 } from "crypto";
45
+ import { writeFile as writeFile2 } from "fs/promises";
46
+ import { resolve as resolve4 } from "path";
47
+ import { execFile as execFile2, spawn as spawn6 } from "child_process";
48
+ import { promisify as promisify2 } from "util";
50
49
  import { input as input2 } from "@inquirer/prompts";
51
50
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
52
51
  import { resolve as resolve5 } from "path";
52
+ import { writeFile as writeFile4 } from "fs/promises";
53
+ import { resolve as resolve6, join as join3 } from "path";
54
+ import { homedir, platform } from "os";
55
+ import { execFile as execFile3 } from "child_process";
56
+ import { promisify as promisify3 } from "util";
57
+ var AuthStore = class {
58
+ pairingCodes = /* @__PURE__ */ new Map();
59
+ sessions = /* @__PURE__ */ new Map();
60
+ cleanupInterval;
61
+ constructor() {
62
+ this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
63
+ }
64
+ // Pairing codes
65
+ addPairingCode(code, ttlSeconds) {
66
+ const entry = {
67
+ code,
68
+ expiresAt: Date.now() + ttlSeconds * 1e3,
69
+ used: false
70
+ };
71
+ this.pairingCodes.set(code, entry);
72
+ return entry;
73
+ }
74
+ getPairingCode(code) {
75
+ return this.pairingCodes.get(code);
76
+ }
77
+ markPairingCodeUsed(code) {
78
+ const entry = this.pairingCodes.get(code);
79
+ if (entry) {
80
+ entry.used = true;
81
+ }
82
+ }
83
+ // Sessions
84
+ createSession(orgUuid, ttlSeconds) {
85
+ const session = {
86
+ id: nanoid(32),
87
+ orgUuid,
88
+ token: nanoid(48),
89
+ expiresAt: Date.now() + ttlSeconds * 1e3,
90
+ lastActivity: Date.now()
91
+ };
92
+ this.sessions.set(session.token, session);
93
+ return session;
94
+ }
95
+ getSession(token) {
96
+ const session = this.sessions.get(token);
97
+ if (session) {
98
+ session.lastActivity = Date.now();
99
+ }
100
+ return session;
101
+ }
102
+ // Cleanup
103
+ cleanup() {
104
+ const now = Date.now();
105
+ for (const [key, code] of this.pairingCodes) {
106
+ if (code.expiresAt < now) {
107
+ this.pairingCodes.delete(key);
108
+ }
109
+ }
110
+ for (const [key, session] of this.sessions) {
111
+ if (session.expiresAt < now) {
112
+ this.sessions.delete(key);
113
+ }
114
+ }
115
+ }
116
+ destroy() {
117
+ clearInterval(this.cleanupInterval);
118
+ this.pairingCodes.clear();
119
+ this.sessions.clear();
120
+ }
121
+ };
53
122
  function connectWithToken(store, encryptedToken, orgUuid, encryptionKey, sessionTtlSeconds) {
54
123
  let payload;
55
124
  try {
@@ -63,7 +132,7 @@ function connectWithToken(store, encryptedToken, orgUuid, encryptionKey, session
63
132
  } catch {
64
133
  throw new GigaiError(ErrorCode.TOKEN_DECRYPT_FAILED, "Failed to decrypt token");
65
134
  }
66
- if (decrypted.orgUuid !== "*" && decrypted.orgUuid !== orgUuid) {
135
+ if (decrypted.orgUuid !== orgUuid) {
67
136
  throw new GigaiError(ErrorCode.ORG_MISMATCH, "Organization UUID mismatch");
68
137
  }
69
138
  return store.createSession(orgUuid, sessionTtlSeconds);
@@ -89,8 +158,37 @@ function createAuthMiddleware(store) {
89
158
  request.session = session;
90
159
  };
91
160
  }
161
+ var PAIRING_CODE_LENGTH = 8;
162
+ var PAIRING_CODE_CHARS = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ";
163
+ function generatePairingCode(store, ttlSeconds) {
164
+ let code = "";
165
+ const bytes = nanoid2(PAIRING_CODE_LENGTH);
166
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
167
+ code += PAIRING_CODE_CHARS[bytes.charCodeAt(i) % PAIRING_CODE_CHARS.length];
168
+ }
169
+ store.addPairingCode(code, ttlSeconds);
170
+ return code;
171
+ }
172
+ function validateAndPair(store, code, orgUuid, encryptionKey, serverFingerprint) {
173
+ const entry = store.getPairingCode(code.toUpperCase());
174
+ if (!entry) {
175
+ throw new GigaiError(ErrorCode.PAIRING_INVALID, "Invalid pairing code");
176
+ }
177
+ if (entry.used) {
178
+ throw new GigaiError(ErrorCode.PAIRING_USED, "Pairing code already used");
179
+ }
180
+ if (entry.expiresAt < Date.now()) {
181
+ throw new GigaiError(ErrorCode.PAIRING_EXPIRED, "Pairing code expired");
182
+ }
183
+ store.markPairingCodeUsed(code.toUpperCase());
184
+ return encrypt(
185
+ { orgUuid, serverFingerprint, createdAt: Date.now() },
186
+ encryptionKey
187
+ );
188
+ }
92
189
  function registerAuthRoutes(server, store, config) {
93
190
  const serverFingerprint = randomBytes(16).toString("hex");
191
+ const serverName = config.serverName ?? hostname();
94
192
  server.post("/auth/pair", {
95
193
  config: {
96
194
  rateLimit: { max: 5, timeWindow: "1 hour" }
@@ -114,7 +212,7 @@ function registerAuthRoutes(server, store, config) {
114
212
  config.auth.encryptionKey,
115
213
  serverFingerprint
116
214
  );
117
- return { encryptedToken: JSON.stringify(encryptedToken) };
215
+ return { encryptedToken: JSON.stringify(encryptedToken), serverName };
118
216
  });
119
217
  server.post("/auth/connect", {
120
218
  config: {
@@ -267,7 +365,7 @@ function executeTool(entry, args, timeout) {
267
365
  `Cannot execute tool of type: ${entry.type}`
268
366
  );
269
367
  }
270
- return new Promise((resolve6, reject) => {
368
+ return new Promise((resolve7, reject) => {
271
369
  const start = Date.now();
272
370
  const stdoutChunks = [];
273
371
  const stderrChunks = [];
@@ -315,7 +413,7 @@ function executeTool(entry, args, timeout) {
315
413
  reject(new GigaiError(ErrorCode.EXEC_TIMEOUT, `Tool execution timed out after ${effectiveTimeout}ms`));
316
414
  return;
317
415
  }
318
- resolve6({
416
+ resolve7({
319
417
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
320
418
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
321
419
  exitCode: exitCode ?? 1,
@@ -607,7 +705,7 @@ async function execCommandSafe(command, args, config) {
607
705
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Null byte in argument");
608
706
  }
609
707
  }
610
- return new Promise((resolve6, reject) => {
708
+ return new Promise((resolve7, reject) => {
611
709
  const child = spawn2(command, args, {
612
710
  shell: false,
613
711
  stdio: ["ignore", "pipe", "pipe"]
@@ -629,7 +727,7 @@ async function execCommandSafe(command, args, config) {
629
727
  reject(new GigaiError(ErrorCode.EXEC_FAILED, `Failed to spawn ${command}: ${err.message}`));
630
728
  });
631
729
  child.on("close", (exitCode) => {
632
- resolve6({
730
+ resolve7({
633
731
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
634
732
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
635
733
  exitCode: exitCode ?? 1
@@ -747,7 +845,7 @@ async function transferRoutes(server) {
747
845
  if (!data) {
748
846
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No file uploaded");
749
847
  }
750
- const id = nanoid(16);
848
+ const id = nanoid3(16);
751
849
  const buffer = await data.toBuffer();
752
850
  const filePath = join2(TRANSFER_DIR, id);
753
851
  await writeFile(filePath, buffer);
@@ -778,6 +876,48 @@ async function transferRoutes(server) {
778
876
  reply.type(entry.mimeType).send(content);
779
877
  });
780
878
  }
879
+ var execFileAsync = promisify(execFile);
880
+ async function adminRoutes(server) {
881
+ server.post("/admin/update", async (_request, reply) => {
882
+ try {
883
+ const { stdout, stderr } = await execFileAsync(
884
+ "npm",
885
+ ["install", "-g", "@schuttdev/gigai@latest"],
886
+ { timeout: 12e4 }
887
+ );
888
+ server.log.info(`Update output: ${stdout}`);
889
+ if (stderr) server.log.warn(`Update stderr: ${stderr}`);
890
+ } catch (e) {
891
+ server.log.error(`Update failed: ${e.message}`);
892
+ reply.status(500);
893
+ return { updated: false, error: e.message };
894
+ }
895
+ setTimeout(async () => {
896
+ server.log.info("Restarting server after update...");
897
+ const args = ["server", "start"];
898
+ const configIdx = process.argv.indexOf("--config");
899
+ if (configIdx !== -1 && process.argv[configIdx + 1]) {
900
+ args.push("--config", process.argv[configIdx + 1]);
901
+ }
902
+ const shortIdx = process.argv.indexOf("-c");
903
+ if (shortIdx !== -1 && process.argv[shortIdx + 1]) {
904
+ args.push("--config", process.argv[shortIdx + 1]);
905
+ }
906
+ if (process.argv.includes("--dev")) {
907
+ args.push("--dev");
908
+ }
909
+ await server.close();
910
+ const child = spawn3("gigai", args, {
911
+ detached: true,
912
+ stdio: "ignore",
913
+ cwd: process.cwd()
914
+ });
915
+ child.unref();
916
+ process.exit(0);
917
+ }, 500);
918
+ return { updated: true, restarting: true };
919
+ });
920
+ }
781
921
  async function createServer(opts) {
782
922
  const { config, dev = false } = opts;
783
923
  const server = Fastify({
@@ -787,7 +927,9 @@ async function createServer(opts) {
787
927
  trustProxy: !dev
788
928
  // Only trust proxy headers in production (behind HTTPS reverse proxy)
789
929
  });
790
- if (!dev) {
930
+ const httpsProvider = config.server.https?.provider;
931
+ const behindTunnel = httpsProvider === "tailscale" || httpsProvider === "cloudflare";
932
+ if (!dev && !behindTunnel) {
791
933
  server.addHook("onRequest", async (request, _reply) => {
792
934
  if (request.protocol !== "https") {
793
935
  throw new GigaiError(ErrorCode.HTTPS_REQUIRED, "HTTPS is required");
@@ -805,6 +947,7 @@ async function createServer(opts) {
805
947
  await server.register(toolRoutes);
806
948
  await server.register(execRoutes);
807
949
  await server.register(transferRoutes);
950
+ await server.register(adminRoutes);
808
951
  server.setErrorHandler((error, _request, reply) => {
809
952
  if (error instanceof GigaiError) {
810
953
  reply.status(error.statusCode).send(error.toJSON());
@@ -830,10 +973,67 @@ async function loadConfig(path) {
830
973
  const json = JSON.parse(raw);
831
974
  return GigaiConfigSchema.parse(json);
832
975
  }
833
- var execFileAsync = promisify(execFile);
976
+ function runCommand(command, args) {
977
+ return new Promise((resolve7, reject) => {
978
+ const child = spawn4(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
979
+ const chunks = [];
980
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
981
+ child.on("error", reject);
982
+ child.on("close", (exitCode) => {
983
+ resolve7({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
984
+ });
985
+ });
986
+ }
987
+ async function getTailscaleStatus() {
988
+ try {
989
+ const { stdout, exitCode } = await runCommand("tailscale", ["status", "--json"]);
990
+ if (exitCode !== 0) return { online: false };
991
+ const status = JSON.parse(stdout);
992
+ return {
993
+ online: status.BackendState === "Running",
994
+ hostname: status.Self?.DNSName?.replace(/\.$/, "")
995
+ };
996
+ } catch {
997
+ return { online: false };
998
+ }
999
+ }
1000
+ async function enableFunnel(port) {
1001
+ const status = await getTailscaleStatus();
1002
+ if (!status.online || !status.hostname) {
1003
+ throw new Error("Tailscale is not running or not connected");
1004
+ }
1005
+ const { exitCode, stdout } = await runCommand("tailscale", [
1006
+ "funnel",
1007
+ "--bg",
1008
+ `${port}`
1009
+ ]);
1010
+ if (exitCode !== 0) {
1011
+ throw new Error(`Failed to enable Tailscale Funnel: ${stdout}`);
1012
+ }
1013
+ return `https://${status.hostname}`;
1014
+ }
1015
+ async function disableFunnel(port) {
1016
+ await runCommand("tailscale", ["funnel", "--bg", "off", `${port}`]);
1017
+ }
1018
+ function runTunnel(tunnelName, localPort) {
1019
+ const child = spawn5("cloudflared", [
1020
+ "tunnel",
1021
+ "--url",
1022
+ `http://localhost:${localPort}`,
1023
+ "run",
1024
+ tunnelName
1025
+ ], {
1026
+ shell: false,
1027
+ stdio: "ignore",
1028
+ detached: true
1029
+ });
1030
+ child.unref();
1031
+ return child;
1032
+ }
1033
+ var execFileAsync2 = promisify2(execFile2);
834
1034
  async function getTailscaleDnsName() {
835
1035
  try {
836
- const { stdout } = await execFileAsync("tailscale", ["status", "--json"]);
1036
+ const { stdout } = await execFileAsync2("tailscale", ["status", "--json"]);
837
1037
  const data = JSON.parse(stdout);
838
1038
  const dnsName = data?.Self?.DNSName;
839
1039
  if (dnsName) return dnsName.replace(/\.$/, "");
@@ -842,6 +1042,41 @@ async function getTailscaleDnsName() {
842
1042
  return null;
843
1043
  }
844
1044
  }
1045
+ async function ensureTailscaleFunnel(port) {
1046
+ const dnsName = await getTailscaleDnsName();
1047
+ if (!dnsName) {
1048
+ throw new Error("Tailscale is not running or not connected. Install/start Tailscale first.");
1049
+ }
1050
+ console.log(" Enabling Tailscale Funnel...");
1051
+ try {
1052
+ const { stdout, stderr } = await execFileAsync2("tailscale", ["funnel", "--bg", `${port}`]);
1053
+ const output = stdout + stderr;
1054
+ if (output.includes("Funnel is not enabled")) {
1055
+ const urlMatch = output.match(/(https:\/\/login\.tailscale\.com\/\S+)/);
1056
+ const enableUrl = urlMatch?.[1] ?? "https://login.tailscale.com/admin/machines";
1057
+ console.log(`
1058
+ Funnel is not enabled on your tailnet.`);
1059
+ console.log(` Enable it here: ${enableUrl}
1060
+ `);
1061
+ await confirm({ message: "I've enabled Funnel in my Tailscale admin. Continue?", default: true });
1062
+ const retry = await execFileAsync2("tailscale", ["funnel", "--bg", `${port}`]);
1063
+ if ((retry.stdout + retry.stderr).includes("Funnel is not enabled")) {
1064
+ throw new Error("Funnel is still not enabled. Please enable it in your Tailscale admin and try again.");
1065
+ }
1066
+ }
1067
+ } catch (e) {
1068
+ if (e.message.includes("Funnel is still not enabled")) throw e;
1069
+ }
1070
+ try {
1071
+ const { stdout } = await execFileAsync2("tailscale", ["funnel", "status"]);
1072
+ if (stdout.includes("No serve config")) {
1073
+ throw new Error("Funnel setup failed. Run 'tailscale funnel --bg " + port + "' manually to debug.");
1074
+ }
1075
+ } catch {
1076
+ }
1077
+ console.log(` Tailscale Funnel active: https://${dnsName}`);
1078
+ return `https://${dnsName}`;
1079
+ }
845
1080
  async function runInit() {
846
1081
  console.log("\n gigai server setup\n");
847
1082
  const httpsProvider = await select({
@@ -849,7 +1084,6 @@ async function runInit() {
849
1084
  choices: [
850
1085
  { name: "Tailscale Funnel (recommended)", value: "tailscale" },
851
1086
  { name: "Cloudflare Tunnel", value: "cloudflare" },
852
- { name: "Let's Encrypt", value: "letsencrypt" },
853
1087
  { name: "Manual (provide certs)", value: "manual" },
854
1088
  { name: "None (dev mode only)", value: "none" }
855
1089
  ]
@@ -861,7 +1095,6 @@ async function runInit() {
861
1095
  provider: "tailscale",
862
1096
  funnelPort: 7443
863
1097
  };
864
- console.log(" Will use Tailscale Funnel for HTTPS.");
865
1098
  break;
866
1099
  case "cloudflare": {
867
1100
  const tunnelName = await input({
@@ -878,22 +1111,6 @@ async function runInit() {
878
1111
  };
879
1112
  break;
880
1113
  }
881
- case "letsencrypt": {
882
- const domain = await input({
883
- message: "Domain name:",
884
- required: true
885
- });
886
- const email = await input({
887
- message: "Email for Let's Encrypt:",
888
- required: true
889
- });
890
- httpsConfig = {
891
- provider: "letsencrypt",
892
- domain,
893
- email
894
- };
895
- break;
896
- }
897
1114
  case "manual": {
898
1115
  const certPath = await input({
899
1116
  message: "Path to TLS certificate:",
@@ -961,8 +1178,25 @@ async function runInit() {
961
1178
  config: { allowlist, allowSudo }
962
1179
  });
963
1180
  }
1181
+ let serverName;
1182
+ if (httpsProvider === "tailscale") {
1183
+ const dnsName = await getTailscaleDnsName();
1184
+ if (dnsName) {
1185
+ serverName = dnsName.split(".")[0];
1186
+ }
1187
+ } else if (httpsProvider === "cloudflare") {
1188
+ serverName = await input({
1189
+ message: "Server name (identifies this machine):",
1190
+ required: true
1191
+ });
1192
+ }
1193
+ if (!serverName) {
1194
+ const { hostname: osHostname } = await import("os");
1195
+ serverName = osHostname();
1196
+ }
964
1197
  const encryptionKey = generateEncryptionKey();
965
1198
  const config = {
1199
+ serverName,
966
1200
  server: {
967
1201
  port,
968
1202
  host: "0.0.0.0",
@@ -979,86 +1213,78 @@ async function runInit() {
979
1213
  await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
980
1214
  console.log(`
981
1215
  Config written to: ${configPath}`);
982
- const skillDir = resolve4("gigai-skill");
983
- await mkdir2(skillDir, { recursive: true });
984
- let serverUrl = "<YOUR_SERVER_URL>";
1216
+ let serverUrl;
985
1217
  if (httpsProvider === "tailscale") {
986
- const dnsName = await getTailscaleDnsName();
987
- if (dnsName) {
988
- serverUrl = `https://${dnsName}:${port}`;
989
- console.log(` Detected Tailscale URL: ${serverUrl}`);
1218
+ try {
1219
+ serverUrl = await ensureTailscaleFunnel(port);
1220
+ } catch (e) {
1221
+ console.error(` ${e.message}`);
1222
+ console.log(" You can enable Funnel later and run 'gigai server start' manually.\n");
990
1223
  }
1224
+ } else if (httpsProvider === "cloudflare" && httpsConfig && "domain" in httpsConfig && httpsConfig.domain) {
1225
+ serverUrl = `https://${httpsConfig.domain}`;
1226
+ console.log(` Cloudflare URL: ${serverUrl}`);
1227
+ }
1228
+ if (!serverUrl) {
1229
+ serverUrl = await input({
1230
+ message: "Server URL (how clients will reach this server):",
1231
+ required: true
1232
+ });
1233
+ }
1234
+ console.log("\n Starting server...");
1235
+ const serverArgs = ["server", "start", "--config", configPath];
1236
+ if (!httpsConfig) serverArgs.push("--dev");
1237
+ const child = spawn6("gigai", serverArgs, {
1238
+ detached: true,
1239
+ stdio: "ignore",
1240
+ cwd: resolve4(".")
1241
+ });
1242
+ child.unref();
1243
+ await new Promise((r) => setTimeout(r, 1500));
1244
+ try {
1245
+ const res = await fetch(`http://localhost:${port}/health`);
1246
+ if (res.ok) {
1247
+ console.log(` Server running on port ${port} (PID ${child.pid})`);
1248
+ }
1249
+ } catch {
1250
+ console.log(` Server starting in background (PID ${child.pid})`);
1251
+ }
1252
+ let code;
1253
+ const maxRetries = 5;
1254
+ for (let i = 0; i < maxRetries; i++) {
1255
+ try {
1256
+ const res = await fetch(`http://localhost:${port}/auth/pair/generate`);
1257
+ if (res.ok) {
1258
+ const data = await res.json();
1259
+ code = data.code;
1260
+ break;
1261
+ }
1262
+ } catch {
1263
+ await new Promise((r) => setTimeout(r, 1e3));
1264
+ }
1265
+ }
1266
+ if (!code) {
1267
+ console.log("\n Server is starting but not ready yet.");
1268
+ console.log(" Run 'gigai server pair' once it's up to get a pairing code.\n");
1269
+ return;
991
1270
  }
992
- const skillMd = `---
993
- name: gigai
994
- description: Access tools on the user's machine via the gigai CLI
995
- ---
996
-
997
- # gigai Skill
998
-
999
- This skill gives you access to tools running on the user's machine via the gigai CLI.
1000
-
1001
- ## Quick Start
1002
-
1003
- 1. Connect to the server:
1004
- \`\`\`
1005
- gigai connect
1006
- \`\`\`
1007
-
1008
- 2. List available tools:
1009
- \`\`\`
1010
- gigai list
1011
- \`\`\`
1012
-
1013
- 3. Get help on a specific tool:
1014
- \`\`\`
1015
- gigai help <tool-name>
1016
- \`\`\`
1017
-
1018
- 4. Use a tool directly:
1019
- \`\`\`
1020
- gigai <tool-name> [args...]
1021
- \`\`\`
1022
-
1023
- ## Commands
1024
-
1025
- ### Connection
1026
- - \`gigai connect\` \u2014 Establish/renew a session with the server
1027
- - \`gigai status\` \u2014 Check connection status
1028
- - \`gigai pair <code> <server-url>\` \u2014 Pair with a new server
1029
-
1030
- ### Tool Usage
1031
- - \`gigai list\` \u2014 List all available tools
1032
- - \`gigai help <name>\` \u2014 Show detailed help for a tool
1033
- - \`gigai <name> [args...]\` \u2014 Execute a tool by name
1034
-
1035
- ### File Transfer
1036
- - \`gigai upload <file>\` \u2014 Upload a file to the server
1037
- - \`gigai download <id> <dest>\` \u2014 Download a file from the server
1038
-
1039
- ## Notes
1040
-
1041
- - The connection is authenticated and encrypted end-to-end
1042
- - Tools are scoped to what the user has explicitly configured
1043
- - Sessions auto-renew; if you get auth errors, run \`gigai connect\`
1044
- - All tool execution happens on the user's machine, not in this container
1045
- `;
1046
- await writeFile2(join3(skillDir, "SKILL.md"), skillMd);
1047
- const tokenPayload = {
1048
- orgUuid: "*",
1049
- serverFingerprint: randomBytes2(16).toString("hex"),
1050
- createdAt: Date.now()
1051
- };
1052
- const encryptedToken = encrypt(tokenPayload, encryptionKey);
1053
- const skillConfig = {
1054
- server: serverUrl,
1055
- token: JSON.stringify(encryptedToken)
1056
- };
1057
- await writeFile2(join3(skillDir, "config.json"), JSON.stringify(skillConfig, null, 2) + "\n", { mode: 384 });
1058
- console.log(` Skill template written to: ${skillDir}/`);
1059
1271
  console.log(`
1060
- Start the server with: gigai server start${httpsConfig ? "" : " --dev"}`);
1061
- console.log(` Then upload gigai-skill/ contents to your Claude Project.`);
1272
+ Paste this into Claude to pair:
1273
+ `);
1274
+ 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`);
1275
+ console.log(` Install the gigai CLI and pair with my server:
1276
+ `);
1277
+ console.log(` \`\`\`bash`);
1278
+ console.log(` npm install -g @schuttdev/gigai`);
1279
+ console.log(` gigai pair ${code} ${serverUrl}`);
1280
+ console.log(` \`\`\`
1281
+ `);
1282
+ console.log(` Then show me the skill file output so I can save it.`);
1283
+ 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`);
1284
+ console.log(`
1285
+ Pairing code expires in ${config.auth.pairingTtlSeconds / 60} minutes.`);
1286
+ console.log(` Run 'gigai server pair' to generate a new one.
1287
+ `);
1062
1288
  }
1063
1289
  async function loadConfigFile(path) {
1064
1290
  const configPath = resolve5(path ?? "gigai.config.json");
@@ -1171,17 +1397,165 @@ async function unwrapTool(name) {
1171
1397
  }
1172
1398
  async function generateServerPairingCode(configPath) {
1173
1399
  const { config } = await loadConfigFile(configPath);
1174
- const { AuthStore: AuthStore2 } = await import("./store-Y4V3TOYJ-GKOB6ANA.js");
1175
- const { generatePairingCode: generatePairingCode2 } = await import("./pairing-IGMDVOIZ-RA7GNFU7.js");
1176
- const store = new AuthStore2();
1177
- const code = generatePairingCode2(store, config.auth.pairingTtlSeconds);
1178
- console.log(`
1179
- Pairing code: ${code}`);
1180
- console.log(`Expires in ${config.auth.pairingTtlSeconds / 60} minutes.`);
1181
- console.log(`
1182
- Note: The server must be running for the client to use this code.`);
1183
- console.log(`For a live pairing code, use the server's /auth/pair/generate endpoint.`);
1184
- store.destroy();
1400
+ const port = config.server.port;
1401
+ try {
1402
+ const res = await fetch(`http://localhost:${port}/auth/pair/generate`);
1403
+ if (!res.ok) {
1404
+ const body = await res.text();
1405
+ throw new Error(`Server returned ${res.status}: ${body}`);
1406
+ }
1407
+ const data = await res.json();
1408
+ console.log(`
1409
+ Pairing code: ${data.code}`);
1410
+ console.log(`Expires in ${data.expiresIn / 60} minutes.`);
1411
+ } catch (e) {
1412
+ if (e.message.includes("fetch failed") || e.message.includes("ECONNREFUSED")) {
1413
+ console.error("Server is not running. Start it with: gigai server start");
1414
+ } else {
1415
+ console.error(`Error: ${e.message}`);
1416
+ }
1417
+ process.exitCode = 1;
1418
+ }
1419
+ }
1420
+ var execFileAsync3 = promisify3(execFile3);
1421
+ function getGigaiBin() {
1422
+ return process.argv[1] ?? "gigai";
1423
+ }
1424
+ function getLaunchdPlist(configPath) {
1425
+ const bin = getGigaiBin();
1426
+ return `<?xml version="1.0" encoding="UTF-8"?>
1427
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1428
+ <plist version="1.0">
1429
+ <dict>
1430
+ <key>Label</key>
1431
+ <string>com.gigai.server</string>
1432
+ <key>ProgramArguments</key>
1433
+ <array>
1434
+ <string>${bin}</string>
1435
+ <string>server</string>
1436
+ <string>start</string>
1437
+ <string>--config</string>
1438
+ <string>${configPath}</string>
1439
+ </array>
1440
+ <key>RunAtLoad</key>
1441
+ <true/>
1442
+ <key>KeepAlive</key>
1443
+ <true/>
1444
+ <key>StandardOutPath</key>
1445
+ <string>${join3(homedir(), ".gigai", "server.log")}</string>
1446
+ <key>StandardErrorPath</key>
1447
+ <string>${join3(homedir(), ".gigai", "server.log")}</string>
1448
+ <key>WorkingDirectory</key>
1449
+ <string>${homedir()}</string>
1450
+ </dict>
1451
+ </plist>
1452
+ `;
1453
+ }
1454
+ function getSystemdUnit(configPath) {
1455
+ const bin = getGigaiBin();
1456
+ return `[Unit]
1457
+ Description=gigai server
1458
+ After=network.target
1459
+
1460
+ [Service]
1461
+ Type=simple
1462
+ ExecStart=${bin} server start --config ${configPath}
1463
+ Restart=always
1464
+ RestartSec=5
1465
+ WorkingDirectory=${homedir()}
1466
+
1467
+ [Install]
1468
+ WantedBy=default.target
1469
+ `;
1470
+ }
1471
+ async function installDaemon(configPath) {
1472
+ const config = resolve6(configPath ?? "gigai.config.json");
1473
+ const os = platform();
1474
+ if (os === "darwin") {
1475
+ const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1476
+ await writeFile4(plistPath, getLaunchdPlist(config));
1477
+ console.log(` Wrote launchd plist: ${plistPath}`);
1478
+ try {
1479
+ await execFileAsync3("launchctl", ["load", plistPath]);
1480
+ console.log(" Service loaded and started.");
1481
+ } catch {
1482
+ console.log(` Load it with: launchctl load ${plistPath}`);
1483
+ }
1484
+ console.log(` Logs: ~/.gigai/server.log`);
1485
+ console.log(` Stop: launchctl unload ${plistPath}`);
1486
+ } else if (os === "linux") {
1487
+ const unitDir = join3(homedir(), ".config", "systemd", "user");
1488
+ const unitPath = join3(unitDir, "gigai.service");
1489
+ const { mkdir: mkdir2 } = await import("fs/promises");
1490
+ await mkdir2(unitDir, { recursive: true });
1491
+ await writeFile4(unitPath, getSystemdUnit(config));
1492
+ console.log(` Wrote systemd unit: ${unitPath}`);
1493
+ try {
1494
+ await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
1495
+ await execFileAsync3("systemctl", ["--user", "enable", "--now", "gigai"]);
1496
+ console.log(" Service enabled and started.");
1497
+ } catch {
1498
+ console.log(" Enable it with: systemctl --user enable --now gigai");
1499
+ }
1500
+ console.log(` Logs: journalctl --user -u gigai -f`);
1501
+ console.log(` Stop: systemctl --user stop gigai`);
1502
+ console.log(` Remove: systemctl --user disable gigai`);
1503
+ } else {
1504
+ console.log(" Persistent daemon not supported on this platform.");
1505
+ console.log(" Run 'gigai server start' manually.");
1506
+ }
1507
+ }
1508
+ async function uninstallDaemon() {
1509
+ const os = platform();
1510
+ if (os === "darwin") {
1511
+ const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1512
+ try {
1513
+ await execFileAsync3("launchctl", ["unload", plistPath]);
1514
+ } catch {
1515
+ }
1516
+ const { unlink: unlink2 } = await import("fs/promises");
1517
+ try {
1518
+ await unlink2(plistPath);
1519
+ console.log(" Service removed.");
1520
+ } catch {
1521
+ console.log(" No service found.");
1522
+ }
1523
+ } else if (os === "linux") {
1524
+ try {
1525
+ await execFileAsync3("systemctl", ["--user", "disable", "--now", "gigai"]);
1526
+ } catch {
1527
+ }
1528
+ const unitPath = join3(homedir(), ".config", "systemd", "user", "gigai.service");
1529
+ const { unlink: unlink2 } = await import("fs/promises");
1530
+ try {
1531
+ await unlink2(unitPath);
1532
+ await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
1533
+ console.log(" Service removed.");
1534
+ } catch {
1535
+ console.log(" No service found.");
1536
+ }
1537
+ }
1538
+ }
1539
+ async function stopServer() {
1540
+ const { execFileSync } = await import("child_process");
1541
+ let pids = [];
1542
+ try {
1543
+ const out = execFileSync("pgrep", ["-f", "gigai server start"], { encoding: "utf8" });
1544
+ pids = out.trim().split("\n").map(Number).filter((pid) => pid && pid !== process.pid);
1545
+ } catch {
1546
+ }
1547
+ if (pids.length === 0) {
1548
+ console.log("No running gigai server found.");
1549
+ return;
1550
+ }
1551
+ for (const pid of pids) {
1552
+ try {
1553
+ process.kill(pid, "SIGTERM");
1554
+ console.log(`Stopped gigai server (PID ${pid})`);
1555
+ } catch (e) {
1556
+ console.error(`Failed to stop PID ${pid}: ${e.message}`);
1557
+ }
1558
+ }
1185
1559
  }
1186
1560
  async function startServer() {
1187
1561
  const { values } = parseArgs({
@@ -1197,8 +1571,38 @@ async function startServer() {
1197
1571
  const host = config.server.host;
1198
1572
  await server.listen({ port, host });
1199
1573
  server.log.info(`gigai server listening on ${host}:${port}`);
1574
+ let cfTunnel;
1575
+ const httpsProvider = config.server.https?.provider;
1576
+ if (httpsProvider === "tailscale") {
1577
+ try {
1578
+ const funnelUrl = await enableFunnel(port);
1579
+ server.log.info(`Tailscale Funnel enabled: ${funnelUrl}`);
1580
+ } catch (e) {
1581
+ server.log.error(`Failed to enable Tailscale Funnel: ${e.message}`);
1582
+ }
1583
+ } else if (httpsProvider === "cloudflare") {
1584
+ try {
1585
+ const tunnelName = config.server.https.tunnelName;
1586
+ cfTunnel = runTunnel(tunnelName, port);
1587
+ server.log.info(`Cloudflare Tunnel started: ${tunnelName}`);
1588
+ } catch (e) {
1589
+ server.log.error(`Failed to start Cloudflare Tunnel: ${e.message}`);
1590
+ }
1591
+ }
1200
1592
  const shutdown = async () => {
1201
1593
  server.log.info("Shutting down...");
1594
+ if (httpsProvider === "tailscale") {
1595
+ try {
1596
+ await disableFunnel(port);
1597
+ } catch {
1598
+ }
1599
+ }
1600
+ if (cfTunnel) {
1601
+ try {
1602
+ cfTunnel.kill();
1603
+ } catch {
1604
+ }
1605
+ }
1202
1606
  await server.close();
1203
1607
  process.exit(0);
1204
1608
  };
@@ -1208,9 +1612,12 @@ async function startServer() {
1208
1612
  export {
1209
1613
  createServer,
1210
1614
  generateServerPairingCode,
1615
+ installDaemon,
1211
1616
  loadConfig,
1212
1617
  runInit,
1213
1618
  startServer,
1619
+ stopServer,
1620
+ uninstallDaemon,
1214
1621
  unwrapTool,
1215
1622
  wrapCli,
1216
1623
  wrapImport,