@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.
- package/dist/chunk-FW3JH5IG.js +330 -0
- package/dist/chunk-O45SW2HC.js +79 -0
- package/dist/chunk-P53UVHTF.js +269 -0
- package/dist/{dist-YWMCMOTP.js → dist-H6URC2HQ.js} +577 -692
- package/dist/filesystem-JSSD3C2D-FJH6SPOF.js +24 -0
- package/dist/index.js +249 -2
- package/dist/shell-B35UFUCJ-LJUZUNM6.js +8 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
57
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
299
58
|
import { resolve as resolve3 } from "path";
|
|
300
|
-
import { spawn as
|
|
301
|
-
import { spawn as
|
|
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
|
|
304
|
-
import { resolve as resolve4 } from "path";
|
|
305
|
-
import { execFile as execFile2, spawn as
|
|
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
|
|
68
|
+
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
309
69
|
import { resolve as resolve5 } from "path";
|
|
310
|
-
import { writeFile as
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
1082
|
+
const id = nanoid4(16);
|
|
1406
1083
|
const buffer = await data.toBuffer();
|
|
1407
|
-
const filePath =
|
|
1408
|
-
await
|
|
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
|
|
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 =
|
|
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
|
|
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((
|
|
1535
|
-
const child =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1891
|
+
<string>${join3(homedir2(), ".gigai", "server.log")}</string>
|
|
2013
1892
|
<key>StandardErrorPath</key>
|
|
2014
|
-
<string>${join3(
|
|
1893
|
+
<string>${join3(homedir2(), ".gigai", "server.log")}</string>
|
|
2015
1894
|
<key>WorkingDirectory</key>
|
|
2016
|
-
<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=${
|
|
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 =
|
|
1919
|
+
const os = platform4();
|
|
2041
1920
|
if (os === "darwin") {
|
|
2042
|
-
const plistPath = join3(
|
|
2043
|
-
await
|
|
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(
|
|
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
|
|
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 =
|
|
1955
|
+
const os = platform4();
|
|
2077
1956
|
if (os === "darwin") {
|
|
2078
|
-
const plistPath = join3(
|
|
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(
|
|
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
|
|
2136
|
-
const
|
|
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,
|