@schuttdev/gigai 0.2.9 → 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", "read", "write", "edit", "glob", "grep", "bash"]),
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,38 +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 {
284
- readFile as fsReadFile,
285
- writeFile as fsWriteFile,
286
- readdir
287
- } from "fs/promises";
288
- import { resolve as resolve2, relative, join } from "path";
289
- import { realpath } from "fs/promises";
290
- import { spawn as spawn2 } from "child_process";
291
- import { spawn as spawn3 } from "child_process";
292
- import { writeFile, readFile, unlink, mkdir } from "fs/promises";
293
- import { join as join2 } from "path";
294
- import { tmpdir } from "os";
295
45
  import { nanoid as nanoid3 } from "nanoid";
296
- import { execFile, spawn as spawn4 } 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";
297
56
  import { promisify } from "util";
298
- import { readFile as readFile2 } from "fs/promises";
57
+ import { readFile as readFile3 } from "fs/promises";
299
58
  import { resolve as resolve3 } from "path";
300
- import { spawn as spawn5 } from "child_process";
301
- import { spawn as spawn6 } from "child_process";
59
+ import { spawn as spawn3 } from "child_process";
60
+ import { spawn as spawn4 } from "child_process";
302
61
  import { input, select, checkbox, confirm } from "@inquirer/prompts";
303
- import { writeFile as writeFile2 } from "fs/promises";
304
- import { resolve as resolve4 } from "path";
305
- import { execFile as execFile2, spawn as spawn7 } 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";
306
65
  import { promisify as promisify2 } from "util";
66
+ import { homedir, platform as platform3 } from "os";
307
67
  import { input as input2 } from "@inquirer/prompts";
308
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
68
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
309
69
  import { resolve as resolve5 } from "path";
310
- import { writeFile as writeFile4 } from "fs/promises";
70
+ import { writeFile as writeFile5 } from "fs/promises";
311
71
  import { resolve as resolve6, join as join3 } from "path";
312
- import { homedir, platform } from "os";
72
+ import { homedir as homedir2, platform as platform4 } from "os";
313
73
  import { execFile as execFile3 } from "child_process";
314
74
  import { promisify as promisify3 } from "util";
315
75
  var AuthStore = class {
@@ -445,7 +205,7 @@ function validateAndPair(store, code, orgUuid, encryptionKey, serverFingerprint)
445
205
  );
446
206
  }
447
207
  function registerAuthRoutes(server, store, config) {
448
- const serverFingerprint = randomBytes2(16).toString("hex");
208
+ const serverFingerprint = randomBytes(16).toString("hex");
449
209
  const serverName = config.serverName ?? hostname();
450
210
  server.post("/auth/pair", {
451
211
  config: {
@@ -623,7 +383,7 @@ function executeTool(entry, args, timeout) {
623
383
  `Cannot execute tool of type: ${entry.type}`
624
384
  );
625
385
  }
626
- return new Promise((resolve7, reject) => {
386
+ return new Promise((resolve8, reject) => {
627
387
  const start = Date.now();
628
388
  const stdoutChunks = [];
629
389
  const stderrChunks = [];
@@ -671,7 +431,7 @@ function executeTool(entry, args, timeout) {
671
431
  reject(new GigaiError(ErrorCode.EXEC_TIMEOUT, `Tool execution timed out after ${effectiveTimeout}ms`));
672
432
  return;
673
433
  }
674
- resolve7({
434
+ resolve8({
675
435
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
676
436
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
677
437
  exitCode: exitCode ?? 1,
@@ -822,11 +582,296 @@ var mcpPlugin = fp4(async (server, opts) => {
822
582
  await lifecycle.shutdown();
823
583
  });
824
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"] });
825
870
  var startTime = Date.now();
826
871
  var startupVersion = "0.0.0";
827
872
  try {
828
873
  const pkg = JSON.parse(
829
- readFileSync(resolve(import.meta.dirname ?? ".", "../package.json"), "utf8")
874
+ readFileSync(resolve2(import.meta.dirname ?? ".", "../package.json"), "utf8")
830
875
  );
831
876
  startupVersion = pkg.version;
832
877
  } catch {
@@ -838,13 +883,15 @@ async function healthRoutes(server) {
838
883
  return {
839
884
  status: "ok",
840
885
  version: startupVersion,
841
- uptime: Date.now() - startTime
886
+ uptime: Date.now() - startTime,
887
+ platform: platform(),
888
+ hostname: hostname2()
842
889
  };
843
890
  });
844
891
  }
845
892
  async function toolRoutes(server) {
846
893
  server.get("/tools", async () => {
847
- return { tools: server.registry.list() };
894
+ return { tools: server.registry.list(), platform: platform2() };
848
895
  });
849
896
  server.get("/tools/search", async (request) => {
850
897
  const query = request.query.q?.toLowerCase().trim();
@@ -889,376 +936,6 @@ async function toolRoutes(server) {
889
936
  return { tools: mcpTools };
890
937
  });
891
938
  }
892
- var MAX_OUTPUT_SIZE2 = 10 * 1024 * 1024;
893
- var MAX_READ_SIZE = 2 * 1024 * 1024;
894
- async function validatePath(targetPath, allowedPaths) {
895
- const resolved = resolve2(targetPath);
896
- let real;
897
- try {
898
- real = await realpath(resolved);
899
- } catch {
900
- real = resolved;
901
- }
902
- const isAllowed = allowedPaths.some((allowed) => {
903
- const resolvedAllowed = resolve2(allowed);
904
- const allowedPrefix = resolvedAllowed.endsWith("/") ? resolvedAllowed : resolvedAllowed + "/";
905
- return real === resolvedAllowed || real.startsWith(allowedPrefix) || resolved === resolvedAllowed || resolved.startsWith(allowedPrefix);
906
- });
907
- if (!isAllowed) {
908
- throw new GigaiError(
909
- ErrorCode.PATH_NOT_ALLOWED,
910
- `Path not within allowed directories: ${targetPath}`
911
- );
912
- }
913
- return resolved;
914
- }
915
- async function readFileSafe(path, allowedPaths) {
916
- const safePath = await validatePath(path, allowedPaths);
917
- return fsReadFile(safePath, "utf8");
918
- }
919
- async function listDirSafe(path, allowedPaths) {
920
- const safePath = await validatePath(path, allowedPaths);
921
- const entries = await readdir(safePath, { withFileTypes: true });
922
- return entries.map((e) => ({
923
- name: e.name,
924
- type: e.isDirectory() ? "directory" : "file"
925
- }));
926
- }
927
- async function searchFilesSafe(path, pattern, allowedPaths) {
928
- const safePath = await validatePath(path, allowedPaths);
929
- const results = [];
930
- let regex;
931
- try {
932
- regex = new RegExp(pattern, "i");
933
- } catch {
934
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Invalid search pattern: ${pattern}`);
935
- }
936
- async function walk(dir) {
937
- const entries = await readdir(dir, { withFileTypes: true });
938
- for (const entry of entries) {
939
- const fullPath = join(dir, entry.name);
940
- if (regex.test(entry.name)) {
941
- results.push(relative(safePath, fullPath));
942
- }
943
- if (entry.isDirectory()) {
944
- await walk(fullPath);
945
- }
946
- }
947
- }
948
- await walk(safePath);
949
- return results;
950
- }
951
- async function readBuiltin(args, allowedPaths) {
952
- const filePath = args[0];
953
- if (!filePath) {
954
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: read <file> [offset] [limit]");
955
- }
956
- const safePath = await validatePath(filePath, allowedPaths);
957
- const content = await fsReadFile(safePath, "utf8");
958
- if (content.length > MAX_READ_SIZE) {
959
- throw new GigaiError(
960
- ErrorCode.VALIDATION_ERROR,
961
- `File too large (${(content.length / 1024 / 1024).toFixed(1)}MB). Max: ${MAX_READ_SIZE / 1024 / 1024}MB. Use offset/limit.`
962
- );
963
- }
964
- const offset = args[1] ? parseInt(args[1], 10) : 0;
965
- const limit = args[2] ? parseInt(args[2], 10) : 0;
966
- if (offset || limit) {
967
- const lines = content.split("\n");
968
- const start = Math.max(0, offset);
969
- const end = limit ? start + limit : lines.length;
970
- const sliced = lines.slice(start, end);
971
- return { stdout: sliced.join("\n"), stderr: "", exitCode: 0 };
972
- }
973
- return { stdout: content, stderr: "", exitCode: 0 };
974
- }
975
- async function writeBuiltin(args, allowedPaths) {
976
- const filePath = args[0];
977
- const content = args[1];
978
- if (!filePath || content === void 0) {
979
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: write <file> <content>");
980
- }
981
- const safePath = await validatePath(filePath, allowedPaths);
982
- const { mkdir: mkdir2 } = await import("fs/promises");
983
- const { dirname } = await import("path");
984
- await mkdir2(dirname(safePath), { recursive: true });
985
- await fsWriteFile(safePath, content, "utf8");
986
- return { stdout: `Written: ${safePath}`, stderr: "", exitCode: 0 };
987
- }
988
- async function editBuiltin(args, allowedPaths) {
989
- const filePath = args[0];
990
- const oldStr = args[1];
991
- const newStr = args[2];
992
- const replaceAll = args.includes("--all");
993
- if (!filePath || oldStr === void 0 || newStr === void 0) {
994
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: edit <file> <old_string> <new_string> [--all]");
995
- }
996
- const safePath = await validatePath(filePath, allowedPaths);
997
- const content = await fsReadFile(safePath, "utf8");
998
- if (!content.includes(oldStr)) {
999
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "old_string not found in file");
1000
- }
1001
- if (!replaceAll) {
1002
- const firstIdx = content.indexOf(oldStr);
1003
- const secondIdx = content.indexOf(oldStr, firstIdx + 1);
1004
- if (secondIdx !== -1) {
1005
- throw new GigaiError(
1006
- ErrorCode.VALIDATION_ERROR,
1007
- "old_string matches multiple locations. Use --all to replace all, or provide more context to make it unique."
1008
- );
1009
- }
1010
- }
1011
- const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
1012
- await fsWriteFile(safePath, updated, "utf8");
1013
- const count = replaceAll ? content.split(oldStr).length - 1 : 1;
1014
- return { stdout: `Replaced ${count} occurrence(s) in ${safePath}`, stderr: "", exitCode: 0 };
1015
- }
1016
- async function globBuiltin(args, allowedPaths) {
1017
- const pattern = args[0];
1018
- if (!pattern) {
1019
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: glob <pattern> [path]");
1020
- }
1021
- const searchPath = args[1] ?? ".";
1022
- const safePath = await validatePath(searchPath, allowedPaths);
1023
- const results = [];
1024
- const globRegex = globToRegex(pattern);
1025
- async function walk(dir) {
1026
- let entries;
1027
- try {
1028
- entries = await readdir(dir, { withFileTypes: true });
1029
- } catch {
1030
- return;
1031
- }
1032
- for (const entry of entries) {
1033
- const fullPath = join(dir, entry.name);
1034
- const relPath = relative(safePath, fullPath);
1035
- if (globRegex.test(relPath) || globRegex.test(entry.name)) {
1036
- results.push(relPath);
1037
- }
1038
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1039
- await walk(fullPath);
1040
- }
1041
- if (results.length >= 1e3) return;
1042
- }
1043
- }
1044
- await walk(safePath);
1045
- return { stdout: results.join("\n"), stderr: "", exitCode: 0 };
1046
- }
1047
- async function grepBuiltin(args, allowedPaths) {
1048
- if (args.length === 0) {
1049
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: grep <pattern> [path] [--glob <filter>] [-i] [-n] [-C <num>]");
1050
- }
1051
- const positional = [];
1052
- const flags = [];
1053
- let i = 0;
1054
- while (i < args.length) {
1055
- const arg = args[i];
1056
- if (arg === "--glob" && args[i + 1]) {
1057
- flags.push("--glob", args[i + 1]);
1058
- i += 2;
1059
- } else if (arg === "--type" && args[i + 1]) {
1060
- flags.push("--type", args[i + 1]);
1061
- i += 2;
1062
- } else if (arg === "-C" && args[i + 1]) {
1063
- flags.push("-C", args[i + 1]);
1064
- i += 2;
1065
- } else if (arg === "-i" || arg === "-n" || arg === "-l") {
1066
- flags.push(arg);
1067
- i++;
1068
- } else {
1069
- positional.push(arg);
1070
- i++;
1071
- }
1072
- }
1073
- const pattern = positional[0];
1074
- if (!pattern) {
1075
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No search pattern provided");
1076
- }
1077
- const searchPath = positional[1] ?? ".";
1078
- const safePath = await validatePath(searchPath, allowedPaths);
1079
- try {
1080
- return await spawnGrep("rg", [pattern, safePath, "-n", ...flags]);
1081
- } catch {
1082
- try {
1083
- return await spawnGrep("grep", ["-rn", ...flags, pattern, safePath]);
1084
- } catch {
1085
- return jsGrep(pattern, safePath);
1086
- }
1087
- }
1088
- }
1089
- function spawnGrep(cmd, args) {
1090
- return new Promise((resolve7, reject) => {
1091
- const child = spawn2(cmd, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1092
- const stdoutChunks = [];
1093
- const stderrChunks = [];
1094
- let totalSize = 0;
1095
- child.stdout.on("data", (chunk) => {
1096
- totalSize += chunk.length;
1097
- if (totalSize <= MAX_OUTPUT_SIZE2) stdoutChunks.push(chunk);
1098
- else child.kill("SIGTERM");
1099
- });
1100
- child.stderr.on("data", (chunk) => {
1101
- totalSize += chunk.length;
1102
- if (totalSize <= MAX_OUTPUT_SIZE2) stderrChunks.push(chunk);
1103
- });
1104
- child.on("error", () => reject(new Error(`${cmd} not available`)));
1105
- child.on("close", (exitCode) => {
1106
- resolve7({
1107
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
1108
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
1109
- exitCode: exitCode ?? 1
1110
- });
1111
- });
1112
- });
1113
- }
1114
- async function jsGrep(pattern, searchPath) {
1115
- let regex;
1116
- try {
1117
- regex = new RegExp(pattern);
1118
- } catch {
1119
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Invalid pattern: ${pattern}`);
1120
- }
1121
- const results = [];
1122
- async function walk(dir) {
1123
- let entries;
1124
- try {
1125
- entries = await readdir(dir, { withFileTypes: true });
1126
- } catch {
1127
- return;
1128
- }
1129
- for (const entry of entries) {
1130
- if (results.length >= 500) return;
1131
- const fullPath = join(dir, entry.name);
1132
- if (entry.isDirectory()) {
1133
- if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
1134
- await walk(fullPath);
1135
- }
1136
- } else {
1137
- try {
1138
- const content = await fsReadFile(fullPath, "utf8");
1139
- const lines = content.split("\n");
1140
- for (let i = 0; i < lines.length; i++) {
1141
- if (regex.test(lines[i])) {
1142
- results.push(`${relative(searchPath, fullPath)}:${i + 1}:${lines[i]}`);
1143
- if (results.length >= 500) return;
1144
- }
1145
- }
1146
- } catch {
1147
- }
1148
- }
1149
- }
1150
- }
1151
- await walk(searchPath);
1152
- return {
1153
- stdout: results.join("\n"),
1154
- stderr: results.length >= 500 ? "Results truncated at 500 matches" : "",
1155
- exitCode: results.length > 0 ? 0 : 1
1156
- };
1157
- }
1158
- function globToRegex(pattern) {
1159
- let regex = "";
1160
- let i = 0;
1161
- while (i < pattern.length) {
1162
- const c = pattern[i];
1163
- if (c === "*") {
1164
- if (pattern[i + 1] === "*") {
1165
- regex += ".*";
1166
- i += 2;
1167
- if (pattern[i] === "/") i++;
1168
- } else {
1169
- regex += "[^/]*";
1170
- i++;
1171
- }
1172
- } else if (c === "?") {
1173
- regex += "[^/]";
1174
- i++;
1175
- } else if (c === "{") {
1176
- const end = pattern.indexOf("}", i);
1177
- if (end !== -1) {
1178
- const options = pattern.slice(i + 1, end).split(",");
1179
- regex += `(?:${options.map(escapeRegex).join("|")})`;
1180
- i = end + 1;
1181
- } else {
1182
- regex += escapeRegex(c);
1183
- i++;
1184
- }
1185
- } else {
1186
- regex += escapeRegex(c);
1187
- i++;
1188
- }
1189
- }
1190
- return new RegExp(regex);
1191
- }
1192
- function escapeRegex(s) {
1193
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1194
- }
1195
- var SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
1196
- "sh",
1197
- "bash",
1198
- "zsh",
1199
- "fish",
1200
- "csh",
1201
- "tcsh",
1202
- "dash",
1203
- "ksh",
1204
- "env",
1205
- "xargs",
1206
- "nohup",
1207
- "strace",
1208
- "ltrace"
1209
- ]);
1210
- var MAX_OUTPUT_SIZE3 = 10 * 1024 * 1024;
1211
- async function execCommandSafe(command, args, config) {
1212
- if (!config.allowlist.includes(command)) {
1213
- throw new GigaiError(
1214
- ErrorCode.COMMAND_NOT_ALLOWED,
1215
- `Command not in allowlist: ${command}. Allowed: ${config.allowlist.join(", ")}`
1216
- );
1217
- }
1218
- if (command === "sudo" && !config.allowSudo) {
1219
- throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, "sudo is not allowed");
1220
- }
1221
- if (SHELL_INTERPRETERS.has(command)) {
1222
- throw new GigaiError(
1223
- ErrorCode.COMMAND_NOT_ALLOWED,
1224
- `Shell interpreter not allowed: ${command}`
1225
- );
1226
- }
1227
- for (const arg of args) {
1228
- if (arg.includes("\0")) {
1229
- throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Null byte in argument");
1230
- }
1231
- }
1232
- return new Promise((resolve7, reject) => {
1233
- const child = spawn3(command, args, {
1234
- shell: false,
1235
- stdio: ["ignore", "pipe", "pipe"]
1236
- });
1237
- const stdoutChunks = [];
1238
- const stderrChunks = [];
1239
- let totalSize = 0;
1240
- child.stdout.on("data", (chunk) => {
1241
- totalSize += chunk.length;
1242
- if (totalSize <= MAX_OUTPUT_SIZE3) stdoutChunks.push(chunk);
1243
- else child.kill("SIGTERM");
1244
- });
1245
- child.stderr.on("data", (chunk) => {
1246
- totalSize += chunk.length;
1247
- if (totalSize <= MAX_OUTPUT_SIZE3) stderrChunks.push(chunk);
1248
- else child.kill("SIGTERM");
1249
- });
1250
- child.on("error", (err) => {
1251
- reject(new GigaiError(ErrorCode.EXEC_FAILED, `Failed to spawn ${command}: ${err.message}`));
1252
- });
1253
- child.on("close", (exitCode) => {
1254
- resolve7({
1255
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
1256
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
1257
- exitCode: exitCode ?? 1
1258
- });
1259
- });
1260
- });
1261
- }
1262
939
  async function execRoutes(server) {
1263
940
  server.post("/exec", {
1264
941
  config: {
@@ -1381,7 +1058,7 @@ async function handleBuiltin(config, args) {
1381
1058
  }
1382
1059
  }
1383
1060
  var transfers = /* @__PURE__ */ new Map();
1384
- var TRANSFER_DIR = join2(tmpdir(), "gigai-transfers");
1061
+ var TRANSFER_DIR = join(tmpdir(), "gigai-transfers");
1385
1062
  var TRANSFER_TTL = 60 * 60 * 1e3;
1386
1063
  setInterval(async () => {
1387
1064
  const now = Date.now();
@@ -1402,10 +1079,10 @@ async function transferRoutes(server) {
1402
1079
  if (!data) {
1403
1080
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No file uploaded");
1404
1081
  }
1405
- const id = nanoid3(16);
1082
+ const id = nanoid4(16);
1406
1083
  const buffer = await data.toBuffer();
1407
- const filePath = join2(TRANSFER_DIR, id);
1408
- await writeFile(filePath, buffer);
1084
+ const filePath = join(TRANSFER_DIR, id);
1085
+ await writeFile2(filePath, buffer);
1409
1086
  const entry = {
1410
1087
  id,
1411
1088
  path: filePath,
@@ -1429,7 +1106,7 @@ async function transferRoutes(server) {
1429
1106
  transfers.delete(id);
1430
1107
  throw new GigaiError(ErrorCode.TRANSFER_EXPIRED, "Transfer expired");
1431
1108
  }
1432
- const content = await readFile(entry.path);
1109
+ const content = await readFile2(entry.path);
1433
1110
  reply.type(entry.mimeType).send(content);
1434
1111
  });
1435
1112
  }
@@ -1464,7 +1141,7 @@ async function adminRoutes(server) {
1464
1141
  args.push("--dev");
1465
1142
  }
1466
1143
  await server.close();
1467
- const child = spawn4("gigai", args, {
1144
+ const child = spawn2("gigai", args, {
1468
1145
  detached: true,
1469
1146
  stdio: "ignore",
1470
1147
  cwd: process.cwd()
@@ -1475,8 +1152,55 @@ async function adminRoutes(server) {
1475
1152
  return { updated: true, restarting: true };
1476
1153
  });
1477
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
+ }
1478
1202
  async function createServer(opts) {
1479
- const { config, dev = false } = opts;
1203
+ const { config, configPath, dev = false } = opts;
1480
1204
  const server = Fastify({
1481
1205
  logger: {
1482
1206
  level: dev ? "debug" : "info"
@@ -1500,11 +1224,17 @@ async function createServer(opts) {
1500
1224
  await server.register(registryPlugin, { config });
1501
1225
  await server.register(executorPlugin);
1502
1226
  await server.register(mcpPlugin, { config });
1227
+ if (configPath) {
1228
+ await server.register(cronPlugin, { configPath });
1229
+ }
1503
1230
  await server.register(healthRoutes);
1504
1231
  await server.register(toolRoutes);
1505
1232
  await server.register(execRoutes);
1506
1233
  await server.register(transferRoutes);
1507
1234
  await server.register(adminRoutes);
1235
+ if (configPath) {
1236
+ await server.register(cronRoutes);
1237
+ }
1508
1238
  server.setErrorHandler((error, _request, reply) => {
1509
1239
  if (error instanceof GigaiError) {
1510
1240
  reply.status(error.statusCode).send(error.toJSON());
@@ -1526,18 +1256,18 @@ async function createServer(opts) {
1526
1256
  var DEFAULT_CONFIG_PATH = "gigai.config.json";
1527
1257
  async function loadConfig(path) {
1528
1258
  const configPath = resolve3(path ?? DEFAULT_CONFIG_PATH);
1529
- const raw = await readFile2(configPath, "utf8");
1259
+ const raw = await readFile3(configPath, "utf8");
1530
1260
  const json = JSON.parse(raw);
1531
1261
  return GigaiConfigSchema.parse(json);
1532
1262
  }
1533
1263
  function runCommand(command, args) {
1534
- return new Promise((resolve7, reject) => {
1535
- const child = spawn5(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"] });
1536
1266
  const chunks = [];
1537
1267
  child.stdout.on("data", (chunk) => chunks.push(chunk));
1538
1268
  child.on("error", reject);
1539
1269
  child.on("close", (exitCode) => {
1540
- resolve7({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
1270
+ resolve8({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
1541
1271
  });
1542
1272
  });
1543
1273
  }
@@ -1573,7 +1303,7 @@ async function disableFunnel(port) {
1573
1303
  await runCommand("tailscale", ["funnel", "--bg", "off", `${port}`]);
1574
1304
  }
1575
1305
  function runTunnel(tunnelName, localPort) {
1576
- const child = spawn6("cloudflared", [
1306
+ const child = spawn4("cloudflared", [
1577
1307
  "tunnel",
1578
1308
  "--url",
1579
1309
  `http://localhost:${localPort}`,
@@ -1634,6 +1364,67 @@ async function ensureTailscaleFunnel(port) {
1634
1364
  console.log(` Tailscale Funnel active: https://${dnsName}`);
1635
1365
  return `https://${dnsName}`;
1636
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
+ }
1637
1428
  async function runInit() {
1638
1429
  console.log("\n gigai server setup\n");
1639
1430
  const httpsProvider = await select({
@@ -1735,6 +1526,54 @@ async function runInit() {
1735
1526
  config: { allowlist, allowSudo }
1736
1527
  });
1737
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
+ }
1738
1577
  let serverName;
1739
1578
  if (httpsProvider === "tailscale") {
1740
1579
  const dnsName = await getTailscaleDnsName();
@@ -1767,7 +1606,7 @@ async function runInit() {
1767
1606
  tools
1768
1607
  };
1769
1608
  const configPath = resolve4("gigai.config.json");
1770
- await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
1609
+ await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
1771
1610
  console.log(`
1772
1611
  Config written to: ${configPath}`);
1773
1612
  let serverUrl;
@@ -1791,7 +1630,7 @@ async function runInit() {
1791
1630
  console.log("\n Starting server...");
1792
1631
  const serverArgs = ["start", "--config", configPath];
1793
1632
  if (!httpsConfig) serverArgs.push("--dev");
1794
- const child = spawn7("gigai", serverArgs, {
1633
+ const child = spawn5("gigai", serverArgs, {
1795
1634
  detached: true,
1796
1635
  stdio: "ignore",
1797
1636
  cwd: resolve4(".")
@@ -1845,12 +1684,12 @@ async function runInit() {
1845
1684
  }
1846
1685
  async function loadConfigFile(path) {
1847
1686
  const configPath = resolve5(path ?? "gigai.config.json");
1848
- const raw = await readFile3(configPath, "utf8");
1687
+ const raw = await readFile5(configPath, "utf8");
1849
1688
  const config = GigaiConfigSchema.parse(JSON.parse(raw));
1850
1689
  return { config, path: configPath };
1851
1690
  }
1852
1691
  async function saveConfig(config, path) {
1853
- await writeFile3(path, JSON.stringify(config, null, 2) + "\n");
1692
+ await writeFile4(path, JSON.stringify(config, null, 2) + "\n");
1854
1693
  }
1855
1694
  async function wrapCli() {
1856
1695
  const { config, path } = await loadConfigFile();
@@ -1920,7 +1759,7 @@ async function wrapScript() {
1920
1759
  }
1921
1760
  async function wrapImport(configFilePath) {
1922
1761
  const { config, path } = await loadConfigFile();
1923
- const raw = await readFile3(resolve5(configFilePath), "utf8");
1762
+ const raw = await readFile5(resolve5(configFilePath), "utf8");
1924
1763
  const desktopConfig = JSON.parse(raw);
1925
1764
  const mcpServers = desktopConfig.mcpServers ?? {};
1926
1765
  for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
@@ -1940,6 +1779,46 @@ async function wrapImport(configFilePath) {
1940
1779
  console.log(`
1941
1780
  Imported ${Object.keys(mcpServers).length} MCP servers.`);
1942
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
+ }
1943
1822
  async function unwrapTool(name) {
1944
1823
  const { config, path } = await loadConfigFile();
1945
1824
  const idx = config.tools.findIndex((t) => t.name === name);
@@ -2009,11 +1888,11 @@ function getLaunchdPlist(configPath) {
2009
1888
  <key>KeepAlive</key>
2010
1889
  <true/>
2011
1890
  <key>StandardOutPath</key>
2012
- <string>${join3(homedir(), ".gigai", "server.log")}</string>
1891
+ <string>${join3(homedir2(), ".gigai", "server.log")}</string>
2013
1892
  <key>StandardErrorPath</key>
2014
- <string>${join3(homedir(), ".gigai", "server.log")}</string>
1893
+ <string>${join3(homedir2(), ".gigai", "server.log")}</string>
2015
1894
  <key>WorkingDirectory</key>
2016
- <string>${homedir()}</string>
1895
+ <string>${homedir2()}</string>
2017
1896
  </dict>
2018
1897
  </plist>
2019
1898
  `;
@@ -2029,7 +1908,7 @@ Type=simple
2029
1908
  ExecStart=${bin} start --config ${configPath}
2030
1909
  Restart=always
2031
1910
  RestartSec=5
2032
- WorkingDirectory=${homedir()}
1911
+ WorkingDirectory=${homedir2()}
2033
1912
 
2034
1913
  [Install]
2035
1914
  WantedBy=default.target
@@ -2037,10 +1916,10 @@ WantedBy=default.target
2037
1916
  }
2038
1917
  async function installDaemon(configPath) {
2039
1918
  const config = resolve6(configPath ?? "gigai.config.json");
2040
- const os = platform();
1919
+ const os = platform4();
2041
1920
  if (os === "darwin") {
2042
- const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
2043
- await writeFile4(plistPath, getLaunchdPlist(config));
1921
+ const plistPath = join3(homedir2(), "Library", "LaunchAgents", "com.gigai.server.plist");
1922
+ await writeFile5(plistPath, getLaunchdPlist(config));
2044
1923
  console.log(` Wrote launchd plist: ${plistPath}`);
2045
1924
  try {
2046
1925
  await execFileAsync3("launchctl", ["load", plistPath]);
@@ -2051,11 +1930,11 @@ async function installDaemon(configPath) {
2051
1930
  console.log(` Logs: ~/.gigai/server.log`);
2052
1931
  console.log(` Stop: launchctl unload ${plistPath}`);
2053
1932
  } else if (os === "linux") {
2054
- const unitDir = join3(homedir(), ".config", "systemd", "user");
1933
+ const unitDir = join3(homedir2(), ".config", "systemd", "user");
2055
1934
  const unitPath = join3(unitDir, "gigai.service");
2056
1935
  const { mkdir: mkdir2 } = await import("fs/promises");
2057
1936
  await mkdir2(unitDir, { recursive: true });
2058
- await writeFile4(unitPath, getSystemdUnit(config));
1937
+ await writeFile5(unitPath, getSystemdUnit(config));
2059
1938
  console.log(` Wrote systemd unit: ${unitPath}`);
2060
1939
  try {
2061
1940
  await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
@@ -2073,9 +1952,9 @@ async function installDaemon(configPath) {
2073
1952
  }
2074
1953
  }
2075
1954
  async function uninstallDaemon() {
2076
- const os = platform();
1955
+ const os = platform4();
2077
1956
  if (os === "darwin") {
2078
- const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
1957
+ const plistPath = join3(homedir2(), "Library", "LaunchAgents", "com.gigai.server.plist");
2079
1958
  try {
2080
1959
  await execFileAsync3("launchctl", ["unload", plistPath]);
2081
1960
  } catch {
@@ -2092,7 +1971,7 @@ async function uninstallDaemon() {
2092
1971
  await execFileAsync3("systemctl", ["--user", "disable", "--now", "gigai"]);
2093
1972
  } catch {
2094
1973
  }
2095
- const unitPath = join3(homedir(), ".config", "systemd", "user", "gigai.service");
1974
+ const unitPath = join3(homedir2(), ".config", "systemd", "user", "gigai.service");
2096
1975
  const { unlink: unlink2 } = await import("fs/promises");
2097
1976
  try {
2098
1977
  await unlink2(unitPath);
@@ -2132,8 +2011,10 @@ async function startServer() {
2132
2011
  },
2133
2012
  strict: false
2134
2013
  });
2135
- const config = await loadConfig(values.config);
2136
- 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 });
2137
2018
  const port = config.server.port;
2138
2019
  const host = config.server.host;
2139
2020
  await server.listen({ port, host });
@@ -2177,10 +2058,14 @@ async function startServer() {
2177
2058
  process.on("SIGINT", shutdown);
2178
2059
  }
2179
2060
  export {
2061
+ CronScheduler,
2180
2062
  createServer,
2181
2063
  generateServerPairingCode,
2182
2064
  installDaemon,
2183
2065
  loadConfig,
2066
+ mcpAdd,
2067
+ mcpList,
2068
+ parseAtExpression,
2184
2069
  runInit,
2185
2070
  startServer,
2186
2071
  stopServer,