@schuttdev/gigai 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,275 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ execCommandSafe
4
+ } from "./chunk-O45SW2HC.js";
5
+ import {
6
+ editBuiltin,
7
+ globBuiltin,
8
+ grepBuiltin,
9
+ listDirSafe,
10
+ readBuiltin,
11
+ readFileSafe,
12
+ searchFilesSafe,
13
+ writeBuiltin
14
+ } from "./chunk-FW3JH5IG.js";
15
+ import {
16
+ ErrorCode,
17
+ GigaiConfigSchema,
18
+ GigaiError,
19
+ decrypt,
20
+ encrypt,
21
+ generateEncryptionKey
22
+ } from "./chunk-P53UVHTF.js";
2
23
 
3
24
  // ../server/dist/index.mjs
4
25
  import { parseArgs } from "util";
26
+ import { resolve as resolve7 } from "path";
5
27
  import Fastify from "fastify";
6
28
  import cors from "@fastify/cors";
7
29
  import rateLimit from "@fastify/rate-limit";
8
30
  import multipart from "@fastify/multipart";
9
-
10
- // ../shared/dist/index.mjs
11
- import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
12
- import { z } from "zod";
13
- var ALGORITHM = "aes-256-gcm";
14
- var IV_LENGTH = 12;
15
- var TAG_LENGTH = 16;
16
- function encrypt(payload, key) {
17
- const keyBuffer = Buffer.from(key, "hex");
18
- if (keyBuffer.length !== 32) {
19
- throw new Error("Encryption key must be 32 bytes (64 hex chars)");
20
- }
21
- const iv = randomBytes(IV_LENGTH);
22
- const cipher = createCipheriv(ALGORITHM, keyBuffer, iv, {
23
- authTagLength: TAG_LENGTH
24
- });
25
- const plaintext = JSON.stringify(payload);
26
- const encrypted = Buffer.concat([
27
- cipher.update(plaintext, "utf8"),
28
- cipher.final()
29
- ]);
30
- const tag = cipher.getAuthTag();
31
- return {
32
- iv: iv.toString("base64"),
33
- ciphertext: encrypted.toString("base64"),
34
- tag: tag.toString("base64")
35
- };
36
- }
37
- function decrypt(encrypted, key) {
38
- const keyBuffer = Buffer.from(key, "hex");
39
- if (keyBuffer.length !== 32) {
40
- throw new Error("Encryption key must be 32 bytes (64 hex chars)");
41
- }
42
- const iv = Buffer.from(encrypted.iv, "base64");
43
- const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
44
- const tag = Buffer.from(encrypted.tag, "base64");
45
- const decipher = createDecipheriv(ALGORITHM, keyBuffer, iv, {
46
- authTagLength: TAG_LENGTH
47
- });
48
- decipher.setAuthTag(tag);
49
- const decrypted = Buffer.concat([
50
- decipher.update(ciphertext),
51
- decipher.final()
52
- ]);
53
- return JSON.parse(decrypted.toString("utf8"));
54
- }
55
- function generateEncryptionKey() {
56
- return randomBytes(32).toString("hex");
57
- }
58
- var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
59
- ErrorCode2["PAIRING_EXPIRED"] = "PAIRING_EXPIRED";
60
- ErrorCode2["PAIRING_INVALID"] = "PAIRING_INVALID";
61
- ErrorCode2["PAIRING_USED"] = "PAIRING_USED";
62
- ErrorCode2["TOKEN_INVALID"] = "TOKEN_INVALID";
63
- ErrorCode2["TOKEN_DECRYPT_FAILED"] = "TOKEN_DECRYPT_FAILED";
64
- ErrorCode2["ORG_MISMATCH"] = "ORG_MISMATCH";
65
- ErrorCode2["SESSION_EXPIRED"] = "SESSION_EXPIRED";
66
- ErrorCode2["SESSION_INVALID"] = "SESSION_INVALID";
67
- ErrorCode2["AUTH_REQUIRED"] = "AUTH_REQUIRED";
68
- ErrorCode2["TOOL_NOT_FOUND"] = "TOOL_NOT_FOUND";
69
- ErrorCode2["EXEC_TIMEOUT"] = "EXEC_TIMEOUT";
70
- ErrorCode2["EXEC_FAILED"] = "EXEC_FAILED";
71
- ErrorCode2["HTTPS_REQUIRED"] = "HTTPS_REQUIRED";
72
- ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
73
- ErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
74
- ErrorCode2["INTERNAL_ERROR"] = "INTERNAL_ERROR";
75
- ErrorCode2["MCP_ERROR"] = "MCP_ERROR";
76
- ErrorCode2["MCP_NOT_CONNECTED"] = "MCP_NOT_CONNECTED";
77
- ErrorCode2["TRANSFER_NOT_FOUND"] = "TRANSFER_NOT_FOUND";
78
- ErrorCode2["TRANSFER_EXPIRED"] = "TRANSFER_EXPIRED";
79
- ErrorCode2["PATH_NOT_ALLOWED"] = "PATH_NOT_ALLOWED";
80
- ErrorCode2["COMMAND_NOT_ALLOWED"] = "COMMAND_NOT_ALLOWED";
81
- return ErrorCode2;
82
- })(ErrorCode || {});
83
- var STATUS_CODES = {
84
- [
85
- "PAIRING_EXPIRED"
86
- /* PAIRING_EXPIRED */
87
- ]: 410,
88
- [
89
- "PAIRING_INVALID"
90
- /* PAIRING_INVALID */
91
- ]: 400,
92
- [
93
- "PAIRING_USED"
94
- /* PAIRING_USED */
95
- ]: 409,
96
- [
97
- "TOKEN_INVALID"
98
- /* TOKEN_INVALID */
99
- ]: 401,
100
- [
101
- "TOKEN_DECRYPT_FAILED"
102
- /* TOKEN_DECRYPT_FAILED */
103
- ]: 401,
104
- [
105
- "ORG_MISMATCH"
106
- /* ORG_MISMATCH */
107
- ]: 403,
108
- [
109
- "SESSION_EXPIRED"
110
- /* SESSION_EXPIRED */
111
- ]: 401,
112
- [
113
- "SESSION_INVALID"
114
- /* SESSION_INVALID */
115
- ]: 401,
116
- [
117
- "AUTH_REQUIRED"
118
- /* AUTH_REQUIRED */
119
- ]: 401,
120
- [
121
- "TOOL_NOT_FOUND"
122
- /* TOOL_NOT_FOUND */
123
- ]: 404,
124
- [
125
- "EXEC_TIMEOUT"
126
- /* EXEC_TIMEOUT */
127
- ]: 408,
128
- [
129
- "EXEC_FAILED"
130
- /* EXEC_FAILED */
131
- ]: 500,
132
- [
133
- "HTTPS_REQUIRED"
134
- /* HTTPS_REQUIRED */
135
- ]: 403,
136
- [
137
- "RATE_LIMITED"
138
- /* RATE_LIMITED */
139
- ]: 429,
140
- [
141
- "VALIDATION_ERROR"
142
- /* VALIDATION_ERROR */
143
- ]: 400,
144
- [
145
- "INTERNAL_ERROR"
146
- /* INTERNAL_ERROR */
147
- ]: 500,
148
- [
149
- "MCP_ERROR"
150
- /* MCP_ERROR */
151
- ]: 502,
152
- [
153
- "MCP_NOT_CONNECTED"
154
- /* MCP_NOT_CONNECTED */
155
- ]: 503,
156
- [
157
- "TRANSFER_NOT_FOUND"
158
- /* TRANSFER_NOT_FOUND */
159
- ]: 404,
160
- [
161
- "TRANSFER_EXPIRED"
162
- /* TRANSFER_EXPIRED */
163
- ]: 410,
164
- [
165
- "PATH_NOT_ALLOWED"
166
- /* PATH_NOT_ALLOWED */
167
- ]: 403,
168
- [
169
- "COMMAND_NOT_ALLOWED"
170
- /* COMMAND_NOT_ALLOWED */
171
- ]: 403
172
- };
173
- var GigaiError = class extends Error {
174
- code;
175
- statusCode;
176
- details;
177
- constructor(code, message, details) {
178
- super(message);
179
- this.name = "GigaiError";
180
- this.code = code;
181
- this.statusCode = STATUS_CODES[code];
182
- this.details = details;
183
- }
184
- toJSON() {
185
- return {
186
- error: {
187
- code: this.code,
188
- message: this.message,
189
- ...this.details !== void 0 && { details: this.details }
190
- }
191
- };
192
- }
193
- };
194
- var TailscaleHttpsConfigSchema = z.object({
195
- provider: z.literal("tailscale"),
196
- funnelPort: z.number().optional()
197
- });
198
- var CloudflareHttpsConfigSchema = z.object({
199
- provider: z.literal("cloudflare"),
200
- tunnelName: z.string(),
201
- domain: z.string().optional()
202
- });
203
- var ManualHttpsConfigSchema = z.object({
204
- provider: z.literal("manual"),
205
- certPath: z.string(),
206
- keyPath: z.string()
207
- });
208
- var HttpsConfigSchema = z.discriminatedUnion("provider", [
209
- TailscaleHttpsConfigSchema,
210
- CloudflareHttpsConfigSchema,
211
- ManualHttpsConfigSchema
212
- ]);
213
- var CliToolConfigSchema = z.object({
214
- type: z.literal("cli"),
215
- name: z.string(),
216
- command: z.string(),
217
- args: z.array(z.string()).optional(),
218
- description: z.string(),
219
- timeout: z.number().optional(),
220
- cwd: z.string().optional(),
221
- env: z.record(z.string()).optional()
222
- });
223
- var McpToolConfigSchema = z.object({
224
- type: z.literal("mcp"),
225
- name: z.string(),
226
- command: z.string(),
227
- args: z.array(z.string()).optional(),
228
- description: z.string(),
229
- env: z.record(z.string()).optional()
230
- });
231
- var ScriptToolConfigSchema = z.object({
232
- type: z.literal("script"),
233
- name: z.string(),
234
- path: z.string(),
235
- description: z.string(),
236
- timeout: z.number().optional(),
237
- interpreter: z.string().optional()
238
- });
239
- var BuiltinToolConfigSchema = z.object({
240
- type: z.literal("builtin"),
241
- name: z.string(),
242
- builtin: z.enum(["filesystem", "shell"]),
243
- description: z.string(),
244
- config: z.record(z.unknown()).optional()
245
- });
246
- var ToolConfigSchema = z.discriminatedUnion("type", [
247
- CliToolConfigSchema,
248
- McpToolConfigSchema,
249
- ScriptToolConfigSchema,
250
- BuiltinToolConfigSchema
251
- ]);
252
- var AuthConfigSchema = z.object({
253
- encryptionKey: z.string().length(64),
254
- pairingTtlSeconds: z.number().default(300),
255
- sessionTtlSeconds: z.number().default(14400)
256
- });
257
- var ServerConfigSchema = z.object({
258
- port: z.number().default(7443),
259
- host: z.string().default("0.0.0.0"),
260
- https: HttpsConfigSchema.optional()
261
- });
262
- var GigaiConfigSchema = z.object({
263
- serverName: z.string().optional(),
264
- server: ServerConfigSchema,
265
- auth: AuthConfigSchema,
266
- tools: z.array(ToolConfigSchema).default([])
267
- });
268
-
269
- // ../server/dist/index.mjs
270
31
  import fp from "fastify-plugin";
271
32
  import { nanoid } from "nanoid";
272
- import { randomBytes as randomBytes2 } from "crypto";
33
+ import { randomBytes } from "crypto";
273
34
  import { hostname } from "os";
274
35
  import { nanoid as nanoid2 } from "nanoid";
275
36
  import fp2 from "fastify-plugin";
@@ -278,33 +39,37 @@ import { spawn } from "child_process";
278
39
  import fp4 from "fastify-plugin";
279
40
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
280
41
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
281
- import { readFileSync } from "fs";
42
+ import fp5 from "fastify-plugin";
43
+ import { readFile, writeFile } from "fs/promises";
282
44
  import { resolve } from "path";
283
- import { readFile as fsReadFile, readdir } from "fs/promises";
284
- import { resolve as resolve2, relative, join } from "path";
285
- import { realpath } from "fs/promises";
286
- import { spawn as spawn2 } from "child_process";
287
- import { writeFile, readFile, unlink, mkdir } from "fs/promises";
288
- import { join as join2 } from "path";
289
- import { tmpdir } from "os";
290
45
  import { nanoid as nanoid3 } from "nanoid";
291
- import { execFile, spawn as spawn3 } from "child_process";
46
+ import { dirname as dirname2 } from "path";
47
+ import { readFileSync } from "fs";
48
+ import { resolve as resolve2 } from "path";
49
+ import { platform, hostname as hostname2 } from "os";
50
+ import { platform as platform2 } from "os";
51
+ import { writeFile as writeFile2, readFile as readFile2, unlink, mkdir } from "fs/promises";
52
+ import { join } from "path";
53
+ import { tmpdir } from "os";
54
+ import { nanoid as nanoid4 } from "nanoid";
55
+ import { execFile, spawn as spawn2 } from "child_process";
292
56
  import { promisify } from "util";
293
- import { readFile as readFile2 } from "fs/promises";
57
+ import { readFile as readFile3 } from "fs/promises";
294
58
  import { resolve as resolve3 } from "path";
59
+ import { spawn as spawn3 } from "child_process";
295
60
  import { spawn as spawn4 } from "child_process";
296
- import { spawn as spawn5 } from "child_process";
297
61
  import { input, select, checkbox, confirm } from "@inquirer/prompts";
298
- import { writeFile as writeFile2 } from "fs/promises";
299
- import { resolve as resolve4 } from "path";
300
- import { execFile as execFile2, spawn as spawn6 } from "child_process";
62
+ import { readFile as readFile4, writeFile as writeFile3, readdir } from "fs/promises";
63
+ import { resolve as resolve4, join as join2 } from "path";
64
+ import { execFile as execFile2, spawn as spawn5 } from "child_process";
301
65
  import { promisify as promisify2 } from "util";
66
+ import { homedir, platform as platform3 } from "os";
302
67
  import { input as input2 } from "@inquirer/prompts";
303
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
68
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
304
69
  import { resolve as resolve5 } from "path";
305
- import { writeFile as writeFile4 } from "fs/promises";
70
+ import { writeFile as writeFile5 } from "fs/promises";
306
71
  import { resolve as resolve6, join as join3 } from "path";
307
- import { homedir, platform } from "os";
72
+ import { homedir as homedir2, platform as platform4 } from "os";
308
73
  import { execFile as execFile3 } from "child_process";
309
74
  import { promisify as promisify3 } from "util";
310
75
  var AuthStore = class {
@@ -440,7 +205,7 @@ function validateAndPair(store, code, orgUuid, encryptionKey, serverFingerprint)
440
205
  );
441
206
  }
442
207
  function registerAuthRoutes(server, store, config) {
443
- const serverFingerprint = randomBytes2(16).toString("hex");
208
+ const serverFingerprint = randomBytes(16).toString("hex");
444
209
  const serverName = config.serverName ?? hostname();
445
210
  server.post("/auth/pair", {
446
211
  config: {
@@ -618,7 +383,7 @@ function executeTool(entry, args, timeout) {
618
383
  `Cannot execute tool of type: ${entry.type}`
619
384
  );
620
385
  }
621
- return new Promise((resolve7, reject) => {
386
+ return new Promise((resolve8, reject) => {
622
387
  const start = Date.now();
623
388
  const stdoutChunks = [];
624
389
  const stderrChunks = [];
@@ -666,7 +431,7 @@ function executeTool(entry, args, timeout) {
666
431
  reject(new GigaiError(ErrorCode.EXEC_TIMEOUT, `Tool execution timed out after ${effectiveTimeout}ms`));
667
432
  return;
668
433
  }
669
- resolve7({
434
+ resolve8({
670
435
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
671
436
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
672
437
  exitCode: exitCode ?? 1,
@@ -817,11 +582,296 @@ var mcpPlugin = fp4(async (server, opts) => {
817
582
  await lifecycle.shutdown();
818
583
  });
819
584
  }, { name: "mcp" });
585
+ function parseCronField(field, min, max) {
586
+ const result = /* @__PURE__ */ new Set();
587
+ for (const part of field.split(",")) {
588
+ let [range, stepStr] = part.split("/");
589
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
590
+ if (range === "*") {
591
+ for (let i = min; i <= max; i += step) result.add(i);
592
+ } else if (range.includes("-")) {
593
+ const [lo, hi] = range.split("-").map(Number);
594
+ for (let i = lo; i <= hi; i += step) result.add(i);
595
+ } else {
596
+ result.add(parseInt(range, 10));
597
+ }
598
+ }
599
+ return [...result].sort((a, b) => a - b);
600
+ }
601
+ function parseCron(expression) {
602
+ const parts = expression.trim().split(/\s+/);
603
+ if (parts.length !== 5) {
604
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);
605
+ }
606
+ return {
607
+ minutes: parseCronField(parts[0], 0, 59),
608
+ hours: parseCronField(parts[1], 0, 23),
609
+ daysOfMonth: parseCronField(parts[2], 1, 31),
610
+ months: parseCronField(parts[3], 1, 12),
611
+ daysOfWeek: parseCronField(parts[4], 0, 6)
612
+ // 0 = Sunday
613
+ };
614
+ }
615
+ function matchesCron(date, cron) {
616
+ return cron.minutes.includes(date.getMinutes()) && cron.hours.includes(date.getHours()) && cron.daysOfMonth.includes(date.getDate()) && cron.months.includes(date.getMonth() + 1) && cron.daysOfWeek.includes(date.getDay());
617
+ }
618
+ function nextRunDate(expression, after = /* @__PURE__ */ new Date()) {
619
+ const cron = parseCron(expression);
620
+ const d = new Date(after);
621
+ d.setSeconds(0, 0);
622
+ d.setMinutes(d.getMinutes() + 1);
623
+ const limit = 4 * 366 * 24 * 60;
624
+ for (let i = 0; i < limit; i++) {
625
+ if (matchesCron(d, cron)) return d;
626
+ d.setMinutes(d.getMinutes() + 1);
627
+ }
628
+ throw new Error(`Unable to compute next run for expression: ${expression}`);
629
+ }
630
+ function parseAtExpression(input3) {
631
+ const now = /* @__PURE__ */ new Date();
632
+ let target;
633
+ const relMatch = input3.match(/^in\s+(\d+)\s+(minute|minutes|hour|hours)$/i);
634
+ if (relMatch) {
635
+ const n = parseInt(relMatch[1], 10);
636
+ const unit = relMatch[2].toLowerCase();
637
+ target = new Date(now);
638
+ if (unit.startsWith("minute")) {
639
+ target.setMinutes(target.getMinutes() + n);
640
+ } else {
641
+ target.setHours(target.getHours() + n);
642
+ }
643
+ }
644
+ if (!target) {
645
+ const absMatch = input3.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{1,2}):(\d{2})$/);
646
+ if (absMatch) {
647
+ const [, datePart, h, m] = absMatch;
648
+ target = /* @__PURE__ */ new Date(`${datePart}T${h.padStart(2, "0")}:${m}:00`);
649
+ }
650
+ }
651
+ if (!target) {
652
+ const timeMatch = input3.match(
653
+ /^(\d{1,2}):(\d{2})\s*(AM|PM)(?:\s+(tomorrow))?$/i
654
+ );
655
+ if (timeMatch) {
656
+ let hours = parseInt(timeMatch[1], 10);
657
+ const minutes = parseInt(timeMatch[2], 10);
658
+ const ampm = timeMatch[3].toUpperCase();
659
+ const isTomorrow = !!timeMatch[4];
660
+ if (ampm === "PM" && hours !== 12) hours += 12;
661
+ if (ampm === "AM" && hours === 12) hours = 0;
662
+ target = new Date(now);
663
+ target.setHours(hours, minutes, 0, 0);
664
+ if (isTomorrow) {
665
+ target.setDate(target.getDate() + 1);
666
+ } else if (target <= now) {
667
+ target.setDate(target.getDate() + 1);
668
+ }
669
+ }
670
+ }
671
+ if (!target) {
672
+ throw new Error(
673
+ `Cannot parse time expression: "${input3}". Supported formats: "9:00 AM", "9:00 AM tomorrow", "2024-03-08 14:30", "in 30 minutes", "in 2 hours"`
674
+ );
675
+ }
676
+ const min = target.getMinutes();
677
+ const hour = target.getHours();
678
+ const day = target.getDate();
679
+ const month = target.getMonth() + 1;
680
+ return `${min} ${hour} ${day} ${month} *`;
681
+ }
682
+ var CronScheduler = class {
683
+ jobs = [];
684
+ timer;
685
+ filePath;
686
+ executor;
687
+ log;
688
+ constructor(configDir, executor, log) {
689
+ this.filePath = resolve(configDir, "gigai.crons.json");
690
+ this.executor = executor;
691
+ this.log = log;
692
+ }
693
+ // --- Persistence ---
694
+ async load() {
695
+ try {
696
+ const raw = await readFile(this.filePath, "utf8");
697
+ const data = JSON.parse(raw);
698
+ this.jobs = data.jobs ?? [];
699
+ } catch {
700
+ this.jobs = [];
701
+ }
702
+ }
703
+ async save() {
704
+ const data = { jobs: this.jobs };
705
+ await writeFile(this.filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
706
+ }
707
+ // --- Job CRUD ---
708
+ async addJob(opts) {
709
+ parseCron(opts.schedule);
710
+ const job = {
711
+ id: nanoid3(12),
712
+ schedule: opts.schedule,
713
+ tool: opts.tool,
714
+ args: opts.args,
715
+ description: opts.description,
716
+ createdAt: Date.now(),
717
+ nextRun: nextRunDate(opts.schedule).getTime(),
718
+ enabled: true,
719
+ oneShot: opts.oneShot
720
+ };
721
+ this.jobs.push(job);
722
+ await this.save();
723
+ return job;
724
+ }
725
+ async removeJob(id) {
726
+ const before = this.jobs.length;
727
+ this.jobs = this.jobs.filter((j) => j.id !== id);
728
+ if (this.jobs.length === before) return false;
729
+ await this.save();
730
+ return true;
731
+ }
732
+ async toggleJob(id) {
733
+ const job = this.jobs.find((j) => j.id === id);
734
+ if (!job) return void 0;
735
+ job.enabled = !job.enabled;
736
+ if (job.enabled) {
737
+ job.nextRun = nextRunDate(job.schedule).getTime();
738
+ }
739
+ await this.save();
740
+ return job;
741
+ }
742
+ listJobs() {
743
+ return [...this.jobs];
744
+ }
745
+ // --- Tick / execution ---
746
+ start() {
747
+ this.log.info("Cron scheduler started (30s interval)");
748
+ this.timer = setInterval(() => void this.tick(), 3e4);
749
+ void this.tick();
750
+ }
751
+ stop() {
752
+ if (this.timer) {
753
+ clearInterval(this.timer);
754
+ this.timer = void 0;
755
+ }
756
+ this.log.info("Cron scheduler stopped");
757
+ }
758
+ async tick() {
759
+ const now = /* @__PURE__ */ new Date();
760
+ let dirty = false;
761
+ for (const job of this.jobs) {
762
+ if (!job.enabled) continue;
763
+ const cron = parseCron(job.schedule);
764
+ if (!matchesCron(now, cron)) continue;
765
+ if (job.lastRun) {
766
+ const lastRunDate = new Date(job.lastRun);
767
+ if (lastRunDate.getFullYear() === now.getFullYear() && lastRunDate.getMonth() === now.getMonth() && lastRunDate.getDate() === now.getDate() && lastRunDate.getHours() === now.getHours() && lastRunDate.getMinutes() === now.getMinutes()) {
768
+ continue;
769
+ }
770
+ }
771
+ this.log.info(`Cron executing job ${job.id}: ${job.tool} ${job.args.join(" ")}`);
772
+ try {
773
+ await this.executor(job.tool, job.args);
774
+ this.log.info(`Cron job ${job.id} completed successfully`);
775
+ } catch (e) {
776
+ this.log.error(`Cron job ${job.id} failed: ${e.message}`);
777
+ }
778
+ job.lastRun = Date.now();
779
+ if (job.oneShot) {
780
+ job.enabled = false;
781
+ this.log.info(`Cron job ${job.id} (one-shot) disabled after execution`);
782
+ } else {
783
+ job.nextRun = nextRunDate(job.schedule).getTime();
784
+ }
785
+ dirty = true;
786
+ }
787
+ if (dirty) {
788
+ await this.save();
789
+ }
790
+ }
791
+ };
792
+ var cronPlugin = fp5(async (server, opts) => {
793
+ const configDir = dirname2(opts.configPath);
794
+ const executor = async (tool, args) => {
795
+ const entry = server.registry.get(tool);
796
+ if (entry.type === "builtin") {
797
+ const { execCommandSafe: execCommandSafe2 } = await import("./shell-B35UFUCJ-LJUZUNM6.js");
798
+ const {
799
+ readFileSafe: readFileSafe2,
800
+ listDirSafe: listDirSafe2,
801
+ searchFilesSafe: searchFilesSafe2,
802
+ readBuiltin: readBuiltin2,
803
+ writeBuiltin: writeBuiltin2,
804
+ editBuiltin: editBuiltin2,
805
+ globBuiltin: globBuiltin2,
806
+ grepBuiltin: grepBuiltin2
807
+ } = await import("./filesystem-JSSD3C2D-FJH6SPOF.js");
808
+ const builtinConfig = entry.config.config ?? {};
809
+ switch (entry.config.builtin) {
810
+ case "filesystem": {
811
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
812
+ const sub = args[0];
813
+ const target = args[1] ?? ".";
814
+ if (sub === "read") await readFileSafe2(target, allowedPaths);
815
+ else if (sub === "list") await listDirSafe2(target, allowedPaths);
816
+ else if (sub === "search") await searchFilesSafe2(target, args[2] ?? ".*", allowedPaths);
817
+ break;
818
+ }
819
+ case "shell":
820
+ case "bash": {
821
+ const allowlist = builtinConfig.allowlist ?? [];
822
+ const allowSudo = builtinConfig.allowSudo ?? false;
823
+ const cmd = args[0];
824
+ if (cmd) await execCommandSafe2(cmd, args.slice(1), { allowlist, allowSudo });
825
+ break;
826
+ }
827
+ case "read": {
828
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
829
+ await readBuiltin2(args, allowedPaths);
830
+ break;
831
+ }
832
+ case "write": {
833
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
834
+ await writeBuiltin2(args, allowedPaths);
835
+ break;
836
+ }
837
+ case "edit": {
838
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
839
+ await editBuiltin2(args, allowedPaths);
840
+ break;
841
+ }
842
+ case "glob": {
843
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
844
+ await globBuiltin2(args, allowedPaths);
845
+ break;
846
+ }
847
+ case "grep": {
848
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
849
+ await grepBuiltin2(args, allowedPaths);
850
+ break;
851
+ }
852
+ }
853
+ return;
854
+ }
855
+ if (entry.type === "mcp") {
856
+ const client = server.mcpPool.getClient(tool);
857
+ await client.callTool(args[0] ?? tool, {});
858
+ return;
859
+ }
860
+ await server.executor.execute(entry, args);
861
+ };
862
+ const scheduler = new CronScheduler(configDir, executor, server.log);
863
+ await scheduler.load();
864
+ scheduler.start();
865
+ server.decorate("scheduler", scheduler);
866
+ server.addHook("onClose", async () => {
867
+ scheduler.stop();
868
+ });
869
+ }, { name: "cron", dependencies: ["registry", "executor"] });
820
870
  var startTime = Date.now();
821
871
  var startupVersion = "0.0.0";
822
872
  try {
823
873
  const pkg = JSON.parse(
824
- readFileSync(resolve(import.meta.dirname ?? ".", "../package.json"), "utf8")
874
+ readFileSync(resolve2(import.meta.dirname ?? ".", "../package.json"), "utf8")
825
875
  );
826
876
  startupVersion = pkg.version;
827
877
  } catch {
@@ -833,13 +883,35 @@ async function healthRoutes(server) {
833
883
  return {
834
884
  status: "ok",
835
885
  version: startupVersion,
836
- uptime: Date.now() - startTime
886
+ uptime: Date.now() - startTime,
887
+ platform: platform(),
888
+ hostname: hostname2()
837
889
  };
838
890
  });
839
891
  }
840
892
  async function toolRoutes(server) {
841
893
  server.get("/tools", async () => {
842
- return { tools: server.registry.list() };
894
+ return { tools: server.registry.list(), platform: platform2() };
895
+ });
896
+ server.get("/tools/search", async (request) => {
897
+ const query = request.query.q?.toLowerCase().trim();
898
+ if (!query) {
899
+ return { tools: server.registry.list() };
900
+ }
901
+ const all = server.registry.list();
902
+ const keywords = query.split(/\s+/);
903
+ const scored = all.map((tool) => {
904
+ const text = `${tool.name} ${tool.description}`.toLowerCase();
905
+ let score = 0;
906
+ for (const kw of keywords) {
907
+ if (tool.name.toLowerCase() === kw) score += 10;
908
+ else if (tool.name.toLowerCase().includes(kw)) score += 5;
909
+ if (tool.description.toLowerCase().includes(kw)) score += 2;
910
+ }
911
+ return { tool, score };
912
+ });
913
+ const matches = scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, 10).map((s) => s.tool);
914
+ return { tools: matches };
843
915
  });
844
916
  server.get("/tools/:name", async (request) => {
845
917
  const { name } = request.params;
@@ -864,130 +936,6 @@ async function toolRoutes(server) {
864
936
  return { tools: mcpTools };
865
937
  });
866
938
  }
867
- async function validatePath(targetPath, allowedPaths) {
868
- const resolved = resolve2(targetPath);
869
- let real;
870
- try {
871
- real = await realpath(resolved);
872
- } catch {
873
- real = resolved;
874
- }
875
- const isAllowed = allowedPaths.some((allowed) => {
876
- const resolvedAllowed = resolve2(allowed);
877
- const allowedPrefix = resolvedAllowed.endsWith("/") ? resolvedAllowed : resolvedAllowed + "/";
878
- return real === resolvedAllowed || real.startsWith(allowedPrefix) || resolved === resolvedAllowed || resolved.startsWith(allowedPrefix);
879
- });
880
- if (!isAllowed) {
881
- throw new GigaiError(
882
- ErrorCode.PATH_NOT_ALLOWED,
883
- `Path not within allowed directories: ${targetPath}`
884
- );
885
- }
886
- return resolved;
887
- }
888
- async function readFileSafe(path, allowedPaths) {
889
- const safePath = await validatePath(path, allowedPaths);
890
- return fsReadFile(safePath, "utf8");
891
- }
892
- async function listDirSafe(path, allowedPaths) {
893
- const safePath = await validatePath(path, allowedPaths);
894
- const entries = await readdir(safePath, { withFileTypes: true });
895
- return entries.map((e) => ({
896
- name: e.name,
897
- type: e.isDirectory() ? "directory" : "file"
898
- }));
899
- }
900
- async function searchFilesSafe(path, pattern, allowedPaths) {
901
- const safePath = await validatePath(path, allowedPaths);
902
- const results = [];
903
- let regex;
904
- try {
905
- regex = new RegExp(pattern, "i");
906
- } catch {
907
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Invalid search pattern: ${pattern}`);
908
- }
909
- async function walk(dir) {
910
- const entries = await readdir(dir, { withFileTypes: true });
911
- for (const entry of entries) {
912
- const fullPath = join(dir, entry.name);
913
- if (regex.test(entry.name)) {
914
- results.push(relative(safePath, fullPath));
915
- }
916
- if (entry.isDirectory()) {
917
- await walk(fullPath);
918
- }
919
- }
920
- }
921
- await walk(safePath);
922
- return results;
923
- }
924
- var SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
925
- "sh",
926
- "bash",
927
- "zsh",
928
- "fish",
929
- "csh",
930
- "tcsh",
931
- "dash",
932
- "ksh",
933
- "env",
934
- "xargs",
935
- "nohup",
936
- "strace",
937
- "ltrace"
938
- ]);
939
- var MAX_OUTPUT_SIZE2 = 10 * 1024 * 1024;
940
- async function execCommandSafe(command, args, config) {
941
- if (!config.allowlist.includes(command)) {
942
- throw new GigaiError(
943
- ErrorCode.COMMAND_NOT_ALLOWED,
944
- `Command not in allowlist: ${command}. Allowed: ${config.allowlist.join(", ")}`
945
- );
946
- }
947
- if (command === "sudo" && !config.allowSudo) {
948
- throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, "sudo is not allowed");
949
- }
950
- if (SHELL_INTERPRETERS.has(command)) {
951
- throw new GigaiError(
952
- ErrorCode.COMMAND_NOT_ALLOWED,
953
- `Shell interpreter not allowed: ${command}`
954
- );
955
- }
956
- for (const arg of args) {
957
- if (arg.includes("\0")) {
958
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Null byte in argument");
959
- }
960
- }
961
- return new Promise((resolve7, reject) => {
962
- const child = spawn2(command, args, {
963
- shell: false,
964
- stdio: ["ignore", "pipe", "pipe"]
965
- });
966
- const stdoutChunks = [];
967
- const stderrChunks = [];
968
- let totalSize = 0;
969
- child.stdout.on("data", (chunk) => {
970
- totalSize += chunk.length;
971
- if (totalSize <= MAX_OUTPUT_SIZE2) stdoutChunks.push(chunk);
972
- else child.kill("SIGTERM");
973
- });
974
- child.stderr.on("data", (chunk) => {
975
- totalSize += chunk.length;
976
- if (totalSize <= MAX_OUTPUT_SIZE2) stderrChunks.push(chunk);
977
- else child.kill("SIGTERM");
978
- });
979
- child.on("error", (err) => {
980
- reject(new GigaiError(ErrorCode.EXEC_FAILED, `Failed to spawn ${command}: ${err.message}`));
981
- });
982
- child.on("close", (exitCode) => {
983
- resolve7({
984
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
985
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
986
- exitCode: exitCode ?? 1
987
- });
988
- });
989
- });
990
- }
991
939
  async function execRoutes(server) {
992
940
  server.post("/exec", {
993
941
  config: {
@@ -1047,6 +995,7 @@ async function execRoutes(server) {
1047
995
  async function handleBuiltin(config, args) {
1048
996
  const builtinConfig = config.config ?? {};
1049
997
  switch (config.builtin) {
998
+ // Legacy combined filesystem tool
1050
999
  case "filesystem": {
1051
1000
  const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1052
1001
  const subcommand = args[0];
@@ -1062,6 +1011,7 @@ async function handleBuiltin(config, args) {
1062
1011
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Unknown filesystem subcommand: ${subcommand}. Use: read, list, search`);
1063
1012
  }
1064
1013
  }
1014
+ // Legacy shell tool
1065
1015
  case "shell": {
1066
1016
  const allowlist = builtinConfig.allowlist ?? [];
1067
1017
  const allowSudo = builtinConfig.allowSudo ?? false;
@@ -1072,12 +1022,43 @@ async function handleBuiltin(config, args) {
1072
1022
  const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1073
1023
  return { ...result, durationMs: 0 };
1074
1024
  }
1025
+ // --- New builtins ---
1026
+ case "read": {
1027
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1028
+ return { ...await readBuiltin(args, allowedPaths), durationMs: 0 };
1029
+ }
1030
+ case "write": {
1031
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1032
+ return { ...await writeBuiltin(args, allowedPaths), durationMs: 0 };
1033
+ }
1034
+ case "edit": {
1035
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1036
+ return { ...await editBuiltin(args, allowedPaths), durationMs: 0 };
1037
+ }
1038
+ case "glob": {
1039
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1040
+ return { ...await globBuiltin(args, allowedPaths), durationMs: 0 };
1041
+ }
1042
+ case "grep": {
1043
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1044
+ return { ...await grepBuiltin(args, allowedPaths), durationMs: 0 };
1045
+ }
1046
+ case "bash": {
1047
+ const allowlist = builtinConfig.allowlist ?? [];
1048
+ const allowSudo = builtinConfig.allowSudo ?? false;
1049
+ const command = args[0];
1050
+ if (!command) {
1051
+ throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No command specified");
1052
+ }
1053
+ const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1054
+ return { ...result, durationMs: 0 };
1055
+ }
1075
1056
  default:
1076
1057
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Unknown builtin: ${config.builtin}`);
1077
1058
  }
1078
1059
  }
1079
1060
  var transfers = /* @__PURE__ */ new Map();
1080
- var TRANSFER_DIR = join2(tmpdir(), "gigai-transfers");
1061
+ var TRANSFER_DIR = join(tmpdir(), "gigai-transfers");
1081
1062
  var TRANSFER_TTL = 60 * 60 * 1e3;
1082
1063
  setInterval(async () => {
1083
1064
  const now = Date.now();
@@ -1098,10 +1079,10 @@ async function transferRoutes(server) {
1098
1079
  if (!data) {
1099
1080
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No file uploaded");
1100
1081
  }
1101
- const id = nanoid3(16);
1082
+ const id = nanoid4(16);
1102
1083
  const buffer = await data.toBuffer();
1103
- const filePath = join2(TRANSFER_DIR, id);
1104
- await writeFile(filePath, buffer);
1084
+ const filePath = join(TRANSFER_DIR, id);
1085
+ await writeFile2(filePath, buffer);
1105
1086
  const entry = {
1106
1087
  id,
1107
1088
  path: filePath,
@@ -1125,7 +1106,7 @@ async function transferRoutes(server) {
1125
1106
  transfers.delete(id);
1126
1107
  throw new GigaiError(ErrorCode.TRANSFER_EXPIRED, "Transfer expired");
1127
1108
  }
1128
- const content = await readFile(entry.path);
1109
+ const content = await readFile2(entry.path);
1129
1110
  reply.type(entry.mimeType).send(content);
1130
1111
  });
1131
1112
  }
@@ -1160,7 +1141,7 @@ async function adminRoutes(server) {
1160
1141
  args.push("--dev");
1161
1142
  }
1162
1143
  await server.close();
1163
- const child = spawn3("gigai", args, {
1144
+ const child = spawn2("gigai", args, {
1164
1145
  detached: true,
1165
1146
  stdio: "ignore",
1166
1147
  cwd: process.cwd()
@@ -1171,8 +1152,55 @@ async function adminRoutes(server) {
1171
1152
  return { updated: true, restarting: true };
1172
1153
  });
1173
1154
  }
1155
+ async function cronRoutes(server) {
1156
+ server.get("/cron", async () => {
1157
+ return { jobs: server.scheduler.listJobs() };
1158
+ });
1159
+ server.post("/cron", {
1160
+ schema: {
1161
+ body: {
1162
+ type: "object",
1163
+ required: ["schedule", "tool", "args"],
1164
+ properties: {
1165
+ schedule: { type: "string" },
1166
+ tool: { type: "string" },
1167
+ args: { type: "array", items: { type: "string" } },
1168
+ description: { type: "string" },
1169
+ oneShot: { type: "boolean" }
1170
+ }
1171
+ }
1172
+ }
1173
+ }, async (request) => {
1174
+ let { schedule, tool, args, description, oneShot } = request.body;
1175
+ if (schedule.startsWith("@at ")) {
1176
+ const atExpr = schedule.slice(4);
1177
+ schedule = parseAtExpression(atExpr);
1178
+ oneShot = true;
1179
+ }
1180
+ if (!server.registry.has(tool)) {
1181
+ throw new GigaiError(ErrorCode.TOOL_NOT_FOUND, `Tool not found: ${tool}`);
1182
+ }
1183
+ const job = await server.scheduler.addJob({ schedule, tool, args, description, oneShot });
1184
+ return { job };
1185
+ });
1186
+ server.delete("/cron/:id", async (request, reply) => {
1187
+ const removed = await server.scheduler.removeJob(request.params.id);
1188
+ if (!removed) {
1189
+ throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Cron job not found: ${request.params.id}`);
1190
+ }
1191
+ reply.status(204);
1192
+ return;
1193
+ });
1194
+ server.post("/cron/:id/toggle", async (request) => {
1195
+ const job = await server.scheduler.toggleJob(request.params.id);
1196
+ if (!job) {
1197
+ throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Cron job not found: ${request.params.id}`);
1198
+ }
1199
+ return { job };
1200
+ });
1201
+ }
1174
1202
  async function createServer(opts) {
1175
- const { config, dev = false } = opts;
1203
+ const { config, configPath, dev = false } = opts;
1176
1204
  const server = Fastify({
1177
1205
  logger: {
1178
1206
  level: dev ? "debug" : "info"
@@ -1196,11 +1224,17 @@ async function createServer(opts) {
1196
1224
  await server.register(registryPlugin, { config });
1197
1225
  await server.register(executorPlugin);
1198
1226
  await server.register(mcpPlugin, { config });
1227
+ if (configPath) {
1228
+ await server.register(cronPlugin, { configPath });
1229
+ }
1199
1230
  await server.register(healthRoutes);
1200
1231
  await server.register(toolRoutes);
1201
1232
  await server.register(execRoutes);
1202
1233
  await server.register(transferRoutes);
1203
1234
  await server.register(adminRoutes);
1235
+ if (configPath) {
1236
+ await server.register(cronRoutes);
1237
+ }
1204
1238
  server.setErrorHandler((error, _request, reply) => {
1205
1239
  if (error instanceof GigaiError) {
1206
1240
  reply.status(error.statusCode).send(error.toJSON());
@@ -1222,18 +1256,18 @@ async function createServer(opts) {
1222
1256
  var DEFAULT_CONFIG_PATH = "gigai.config.json";
1223
1257
  async function loadConfig(path) {
1224
1258
  const configPath = resolve3(path ?? DEFAULT_CONFIG_PATH);
1225
- const raw = await readFile2(configPath, "utf8");
1259
+ const raw = await readFile3(configPath, "utf8");
1226
1260
  const json = JSON.parse(raw);
1227
1261
  return GigaiConfigSchema.parse(json);
1228
1262
  }
1229
1263
  function runCommand(command, args) {
1230
- return new Promise((resolve7, reject) => {
1231
- const child = spawn4(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1264
+ return new Promise((resolve8, reject) => {
1265
+ const child = spawn3(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1232
1266
  const chunks = [];
1233
1267
  child.stdout.on("data", (chunk) => chunks.push(chunk));
1234
1268
  child.on("error", reject);
1235
1269
  child.on("close", (exitCode) => {
1236
- resolve7({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
1270
+ resolve8({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
1237
1271
  });
1238
1272
  });
1239
1273
  }
@@ -1269,7 +1303,7 @@ async function disableFunnel(port) {
1269
1303
  await runCommand("tailscale", ["funnel", "--bg", "off", `${port}`]);
1270
1304
  }
1271
1305
  function runTunnel(tunnelName, localPort) {
1272
- const child = spawn5("cloudflared", [
1306
+ const child = spawn4("cloudflared", [
1273
1307
  "tunnel",
1274
1308
  "--url",
1275
1309
  `http://localhost:${localPort}`,
@@ -1330,6 +1364,67 @@ async function ensureTailscaleFunnel(port) {
1330
1364
  console.log(` Tailscale Funnel active: https://${dnsName}`);
1331
1365
  return `https://${dnsName}`;
1332
1366
  }
1367
+ async function detectClaudeDesktopConfig() {
1368
+ try {
1369
+ const os = platform3();
1370
+ if (os === "darwin") {
1371
+ const configPath = join2(
1372
+ homedir(),
1373
+ "Library",
1374
+ "Application Support",
1375
+ "Claude",
1376
+ "claude_desktop_config.json"
1377
+ );
1378
+ const contents = await readFile4(configPath, "utf-8");
1379
+ if (contents) return configPath;
1380
+ }
1381
+ if (os === "linux") {
1382
+ try {
1383
+ const procVersion = await readFile4("/proc/version", "utf-8");
1384
+ const isWsl = /microsoft|wsl/i.test(procVersion);
1385
+ if (!isWsl) return null;
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ try {
1390
+ const usersDir = "/mnt/c/Users";
1391
+ const entries = await readdir(usersDir, { withFileTypes: true });
1392
+ for (const entry of entries) {
1393
+ if (!entry.isDirectory()) continue;
1394
+ if (entry.name === "Public" || entry.name === "Default" || entry.name === "Default User") continue;
1395
+ const configPath = join2(
1396
+ usersDir,
1397
+ entry.name,
1398
+ "AppData",
1399
+ "Roaming",
1400
+ "Claude",
1401
+ "claude_desktop_config.json"
1402
+ );
1403
+ try {
1404
+ const contents = await readFile4(configPath, "utf-8");
1405
+ if (contents) return configPath;
1406
+ } catch {
1407
+ }
1408
+ }
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ return null;
1413
+ } catch {
1414
+ return null;
1415
+ }
1416
+ }
1417
+ async function scanMcpServers(configPath) {
1418
+ try {
1419
+ const raw = await readFile4(configPath, "utf-8");
1420
+ const config = JSON.parse(raw);
1421
+ if (!config.mcpServers || typeof config.mcpServers !== "object") return null;
1422
+ if (Object.keys(config.mcpServers).length === 0) return null;
1423
+ return config.mcpServers;
1424
+ } catch {
1425
+ return null;
1426
+ }
1427
+ }
1333
1428
  async function runInit() {
1334
1429
  console.log("\n gigai server setup\n");
1335
1430
  const httpsProvider = await select({
@@ -1431,6 +1526,54 @@ async function runInit() {
1431
1526
  config: { allowlist, allowSudo }
1432
1527
  });
1433
1528
  }
1529
+ const configFilePath = await detectClaudeDesktopConfig();
1530
+ if (configFilePath) {
1531
+ const mcpServers = await scanMcpServers(configFilePath);
1532
+ if (mcpServers) {
1533
+ const serverNames = Object.keys(mcpServers);
1534
+ console.log(`
1535
+ Found ${serverNames.length} MCP server(s) in Claude Desktop config.`);
1536
+ const selectedMcp = await checkbox({
1537
+ message: "Import MCP servers:",
1538
+ choices: serverNames.map((name) => ({
1539
+ name: `${name} (${mcpServers[name].command}${mcpServers[name].args ? " " + mcpServers[name].args.join(" ") : ""})`,
1540
+ value: name,
1541
+ checked: true
1542
+ }))
1543
+ });
1544
+ if (selectedMcp.length > 0) {
1545
+ for (const name of selectedMcp) {
1546
+ const entry = mcpServers[name];
1547
+ const tool = {
1548
+ type: "mcp",
1549
+ name,
1550
+ command: entry.command,
1551
+ ...entry.args && { args: entry.args },
1552
+ description: `MCP server: ${name}`,
1553
+ ...entry.env && { env: entry.env }
1554
+ };
1555
+ tools.push(tool);
1556
+ }
1557
+ console.log(` Imported ${selectedMcp.length} MCP server${selectedMcp.length === 1 ? "" : "s"}: ${selectedMcp.join(", ")}`);
1558
+ }
1559
+ }
1560
+ }
1561
+ if (platform3() === "darwin") {
1562
+ const enableIMessage = await confirm({
1563
+ message: "Enable iMessage? (lets Claude send and read iMessages)",
1564
+ default: false
1565
+ });
1566
+ if (enableIMessage) {
1567
+ tools.push({
1568
+ type: "mcp",
1569
+ name: "imessage",
1570
+ command: "npx",
1571
+ args: ["-y", "@foxychat-mcp/apple-imessages"],
1572
+ description: "Send and read iMessages"
1573
+ });
1574
+ console.log(" iMessage requires Full Disk Access for your terminal. Grant it in System Settings > Privacy & Security > Full Disk Access.");
1575
+ }
1576
+ }
1434
1577
  let serverName;
1435
1578
  if (httpsProvider === "tailscale") {
1436
1579
  const dnsName = await getTailscaleDnsName();
@@ -1463,7 +1606,7 @@ async function runInit() {
1463
1606
  tools
1464
1607
  };
1465
1608
  const configPath = resolve4("gigai.config.json");
1466
- await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
1609
+ await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
1467
1610
  console.log(`
1468
1611
  Config written to: ${configPath}`);
1469
1612
  let serverUrl;
@@ -1487,7 +1630,7 @@ async function runInit() {
1487
1630
  console.log("\n Starting server...");
1488
1631
  const serverArgs = ["start", "--config", configPath];
1489
1632
  if (!httpsConfig) serverArgs.push("--dev");
1490
- const child = spawn6("gigai", serverArgs, {
1633
+ const child = spawn5("gigai", serverArgs, {
1491
1634
  detached: true,
1492
1635
  stdio: "ignore",
1493
1636
  cwd: resolve4(".")
@@ -1541,12 +1684,12 @@ async function runInit() {
1541
1684
  }
1542
1685
  async function loadConfigFile(path) {
1543
1686
  const configPath = resolve5(path ?? "gigai.config.json");
1544
- const raw = await readFile3(configPath, "utf8");
1687
+ const raw = await readFile5(configPath, "utf8");
1545
1688
  const config = GigaiConfigSchema.parse(JSON.parse(raw));
1546
1689
  return { config, path: configPath };
1547
1690
  }
1548
1691
  async function saveConfig(config, path) {
1549
- await writeFile3(path, JSON.stringify(config, null, 2) + "\n");
1692
+ await writeFile4(path, JSON.stringify(config, null, 2) + "\n");
1550
1693
  }
1551
1694
  async function wrapCli() {
1552
1695
  const { config, path } = await loadConfigFile();
@@ -1616,7 +1759,7 @@ async function wrapScript() {
1616
1759
  }
1617
1760
  async function wrapImport(configFilePath) {
1618
1761
  const { config, path } = await loadConfigFile();
1619
- const raw = await readFile3(resolve5(configFilePath), "utf8");
1762
+ const raw = await readFile5(resolve5(configFilePath), "utf8");
1620
1763
  const desktopConfig = JSON.parse(raw);
1621
1764
  const mcpServers = desktopConfig.mcpServers ?? {};
1622
1765
  for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
@@ -1636,6 +1779,46 @@ async function wrapImport(configFilePath) {
1636
1779
  console.log(`
1637
1780
  Imported ${Object.keys(mcpServers).length} MCP servers.`);
1638
1781
  }
1782
+ async function mcpAdd(name, command, args, env) {
1783
+ const { config, path } = await loadConfigFile();
1784
+ const existing = config.tools.find((t) => t.name === name);
1785
+ if (existing) {
1786
+ console.warn(`Warning: a tool named "${name}" already exists \u2014 overwriting.`);
1787
+ config.tools = config.tools.filter((t) => t.name !== name);
1788
+ }
1789
+ const tool = {
1790
+ type: "mcp",
1791
+ name,
1792
+ command,
1793
+ description: `MCP server: ${name}`,
1794
+ ...args.length > 0 && { args },
1795
+ ...env && Object.keys(env).length > 0 && { env }
1796
+ };
1797
+ config.tools.push(tool);
1798
+ await saveConfig(config, path);
1799
+ console.log(`Added MCP server: ${name}`);
1800
+ }
1801
+ async function mcpList() {
1802
+ const { config } = await loadConfigFile();
1803
+ const mcpTools = config.tools.filter((t) => t.type === "mcp");
1804
+ if (mcpTools.length === 0) {
1805
+ console.log("No MCP servers configured.");
1806
+ return;
1807
+ }
1808
+ console.log(`
1809
+ MCP servers (${mcpTools.length}):
1810
+ `);
1811
+ for (const t of mcpTools) {
1812
+ const tool = t;
1813
+ const cmdLine = [tool.command, ...tool.args ?? []].join(" ");
1814
+ console.log(` ${tool.name}`);
1815
+ console.log(` command: ${cmdLine}`);
1816
+ if (tool.env && Object.keys(tool.env).length > 0) {
1817
+ console.log(` env: ${Object.keys(tool.env).join(", ")}`);
1818
+ }
1819
+ console.log();
1820
+ }
1821
+ }
1639
1822
  async function unwrapTool(name) {
1640
1823
  const { config, path } = await loadConfigFile();
1641
1824
  const idx = config.tools.findIndex((t) => t.name === name);
@@ -1705,11 +1888,11 @@ function getLaunchdPlist(configPath) {
1705
1888
  <key>KeepAlive</key>
1706
1889
  <true/>
1707
1890
  <key>StandardOutPath</key>
1708
- <string>${join3(homedir(), ".gigai", "server.log")}</string>
1891
+ <string>${join3(homedir2(), ".gigai", "server.log")}</string>
1709
1892
  <key>StandardErrorPath</key>
1710
- <string>${join3(homedir(), ".gigai", "server.log")}</string>
1893
+ <string>${join3(homedir2(), ".gigai", "server.log")}</string>
1711
1894
  <key>WorkingDirectory</key>
1712
- <string>${homedir()}</string>
1895
+ <string>${homedir2()}</string>
1713
1896
  </dict>
1714
1897
  </plist>
1715
1898
  `;
@@ -1725,7 +1908,7 @@ Type=simple
1725
1908
  ExecStart=${bin} start --config ${configPath}
1726
1909
  Restart=always
1727
1910
  RestartSec=5
1728
- WorkingDirectory=${homedir()}
1911
+ WorkingDirectory=${homedir2()}
1729
1912
 
1730
1913
  [Install]
1731
1914
  WantedBy=default.target
@@ -1733,10 +1916,10 @@ WantedBy=default.target
1733
1916
  }
1734
1917
  async function installDaemon(configPath) {
1735
1918
  const config = resolve6(configPath ?? "gigai.config.json");
1736
- const os = platform();
1919
+ const os = platform4();
1737
1920
  if (os === "darwin") {
1738
- const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1739
- await writeFile4(plistPath, getLaunchdPlist(config));
1921
+ const plistPath = join3(homedir2(), "Library", "LaunchAgents", "com.gigai.server.plist");
1922
+ await writeFile5(plistPath, getLaunchdPlist(config));
1740
1923
  console.log(` Wrote launchd plist: ${plistPath}`);
1741
1924
  try {
1742
1925
  await execFileAsync3("launchctl", ["load", plistPath]);
@@ -1747,11 +1930,11 @@ async function installDaemon(configPath) {
1747
1930
  console.log(` Logs: ~/.gigai/server.log`);
1748
1931
  console.log(` Stop: launchctl unload ${plistPath}`);
1749
1932
  } else if (os === "linux") {
1750
- const unitDir = join3(homedir(), ".config", "systemd", "user");
1933
+ const unitDir = join3(homedir2(), ".config", "systemd", "user");
1751
1934
  const unitPath = join3(unitDir, "gigai.service");
1752
1935
  const { mkdir: mkdir2 } = await import("fs/promises");
1753
1936
  await mkdir2(unitDir, { recursive: true });
1754
- await writeFile4(unitPath, getSystemdUnit(config));
1937
+ await writeFile5(unitPath, getSystemdUnit(config));
1755
1938
  console.log(` Wrote systemd unit: ${unitPath}`);
1756
1939
  try {
1757
1940
  await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
@@ -1769,9 +1952,9 @@ async function installDaemon(configPath) {
1769
1952
  }
1770
1953
  }
1771
1954
  async function uninstallDaemon() {
1772
- const os = platform();
1955
+ const os = platform4();
1773
1956
  if (os === "darwin") {
1774
- const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1957
+ const plistPath = join3(homedir2(), "Library", "LaunchAgents", "com.gigai.server.plist");
1775
1958
  try {
1776
1959
  await execFileAsync3("launchctl", ["unload", plistPath]);
1777
1960
  } catch {
@@ -1788,7 +1971,7 @@ async function uninstallDaemon() {
1788
1971
  await execFileAsync3("systemctl", ["--user", "disable", "--now", "gigai"]);
1789
1972
  } catch {
1790
1973
  }
1791
- const unitPath = join3(homedir(), ".config", "systemd", "user", "gigai.service");
1974
+ const unitPath = join3(homedir2(), ".config", "systemd", "user", "gigai.service");
1792
1975
  const { unlink: unlink2 } = await import("fs/promises");
1793
1976
  try {
1794
1977
  await unlink2(unitPath);
@@ -1828,8 +2011,10 @@ async function startServer() {
1828
2011
  },
1829
2012
  strict: false
1830
2013
  });
1831
- const config = await loadConfig(values.config);
1832
- const server = await createServer({ config, dev: values.dev });
2014
+ const configFile = values.config;
2015
+ const config = await loadConfig(configFile);
2016
+ const configPath = resolve7(configFile ?? "gigai.config.json");
2017
+ const server = await createServer({ config, configPath, dev: values.dev });
1833
2018
  const port = config.server.port;
1834
2019
  const host = config.server.host;
1835
2020
  await server.listen({ port, host });
@@ -1873,10 +2058,14 @@ async function startServer() {
1873
2058
  process.on("SIGINT", shutdown);
1874
2059
  }
1875
2060
  export {
2061
+ CronScheduler,
1876
2062
  createServer,
1877
2063
  generateServerPairingCode,
1878
2064
  installDaemon,
1879
2065
  loadConfig,
2066
+ mcpAdd,
2067
+ mcpList,
2068
+ parseAtExpression,
1880
2069
  runInit,
1881
2070
  startServer,
1882
2071
  stopServer,