@schuttdev/gigai 0.2.9 → 0.3.1
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-DTBR4X7U.js} +608 -695
- 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(".")
|
|
@@ -1843,28 +1682,56 @@ async function runInit() {
|
|
|
1843
1682
|
console.log(` Run 'gigai pair' to generate a new one.
|
|
1844
1683
|
`);
|
|
1845
1684
|
}
|
|
1685
|
+
function splitCommand(input3) {
|
|
1686
|
+
const tokens = [];
|
|
1687
|
+
let current = "";
|
|
1688
|
+
let inQuote = null;
|
|
1689
|
+
for (const ch of input3.trim()) {
|
|
1690
|
+
if (inQuote) {
|
|
1691
|
+
if (ch === inQuote) {
|
|
1692
|
+
inQuote = null;
|
|
1693
|
+
} else {
|
|
1694
|
+
current += ch;
|
|
1695
|
+
}
|
|
1696
|
+
} else if (ch === '"' || ch === "'") {
|
|
1697
|
+
inQuote = ch;
|
|
1698
|
+
} else if (ch === " " || ch === " ") {
|
|
1699
|
+
if (current) {
|
|
1700
|
+
tokens.push(current);
|
|
1701
|
+
current = "";
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
current += ch;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (current) tokens.push(current);
|
|
1708
|
+
return { command: tokens[0] ?? input3.trim(), args: tokens.slice(1) };
|
|
1709
|
+
}
|
|
1846
1710
|
async function loadConfigFile(path) {
|
|
1847
1711
|
const configPath = resolve5(path ?? "gigai.config.json");
|
|
1848
|
-
const raw = await
|
|
1712
|
+
const raw = await readFile5(configPath, "utf8");
|
|
1849
1713
|
const config = GigaiConfigSchema.parse(JSON.parse(raw));
|
|
1850
1714
|
return { config, path: configPath };
|
|
1851
1715
|
}
|
|
1852
1716
|
async function saveConfig(config, path) {
|
|
1853
|
-
await
|
|
1717
|
+
await writeFile4(path, JSON.stringify(config, null, 2) + "\n");
|
|
1854
1718
|
}
|
|
1855
1719
|
async function wrapCli() {
|
|
1856
1720
|
const { config, path } = await loadConfigFile();
|
|
1857
1721
|
const name = await input2({ message: "Tool name:", required: true });
|
|
1858
|
-
const
|
|
1722
|
+
const commandInput = await input2({
|
|
1723
|
+
message: "Command (as you'd run it in your terminal):",
|
|
1724
|
+
required: true
|
|
1725
|
+
});
|
|
1859
1726
|
const description = await input2({ message: "Description:", required: true });
|
|
1860
|
-
const argsStr = await input2({ message: "Default args (space-separated, optional):" });
|
|
1861
1727
|
const timeoutStr = await input2({ message: "Timeout in ms (optional):", default: "30000" });
|
|
1728
|
+
const { command, args } = splitCommand(commandInput);
|
|
1862
1729
|
const tool = {
|
|
1863
1730
|
type: "cli",
|
|
1864
1731
|
name,
|
|
1865
1732
|
command,
|
|
1866
1733
|
description,
|
|
1867
|
-
...
|
|
1734
|
+
...args.length > 0 && { args },
|
|
1868
1735
|
timeout: parseInt(timeoutStr, 10)
|
|
1869
1736
|
};
|
|
1870
1737
|
config.tools.push(tool);
|
|
@@ -1920,7 +1787,7 @@ async function wrapScript() {
|
|
|
1920
1787
|
}
|
|
1921
1788
|
async function wrapImport(configFilePath) {
|
|
1922
1789
|
const { config, path } = await loadConfigFile();
|
|
1923
|
-
const raw = await
|
|
1790
|
+
const raw = await readFile5(resolve5(configFilePath), "utf8");
|
|
1924
1791
|
const desktopConfig = JSON.parse(raw);
|
|
1925
1792
|
const mcpServers = desktopConfig.mcpServers ?? {};
|
|
1926
1793
|
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
@@ -1940,6 +1807,46 @@ async function wrapImport(configFilePath) {
|
|
|
1940
1807
|
console.log(`
|
|
1941
1808
|
Imported ${Object.keys(mcpServers).length} MCP servers.`);
|
|
1942
1809
|
}
|
|
1810
|
+
async function mcpAdd(name, command, args, env) {
|
|
1811
|
+
const { config, path } = await loadConfigFile();
|
|
1812
|
+
const existing = config.tools.find((t) => t.name === name);
|
|
1813
|
+
if (existing) {
|
|
1814
|
+
console.warn(`Warning: a tool named "${name}" already exists \u2014 overwriting.`);
|
|
1815
|
+
config.tools = config.tools.filter((t) => t.name !== name);
|
|
1816
|
+
}
|
|
1817
|
+
const tool = {
|
|
1818
|
+
type: "mcp",
|
|
1819
|
+
name,
|
|
1820
|
+
command,
|
|
1821
|
+
description: `MCP server: ${name}`,
|
|
1822
|
+
...args.length > 0 && { args },
|
|
1823
|
+
...env && Object.keys(env).length > 0 && { env }
|
|
1824
|
+
};
|
|
1825
|
+
config.tools.push(tool);
|
|
1826
|
+
await saveConfig(config, path);
|
|
1827
|
+
console.log(`Added MCP server: ${name}`);
|
|
1828
|
+
}
|
|
1829
|
+
async function mcpList() {
|
|
1830
|
+
const { config } = await loadConfigFile();
|
|
1831
|
+
const mcpTools = config.tools.filter((t) => t.type === "mcp");
|
|
1832
|
+
if (mcpTools.length === 0) {
|
|
1833
|
+
console.log("No MCP servers configured.");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
console.log(`
|
|
1837
|
+
MCP servers (${mcpTools.length}):
|
|
1838
|
+
`);
|
|
1839
|
+
for (const t of mcpTools) {
|
|
1840
|
+
const tool = t;
|
|
1841
|
+
const cmdLine = [tool.command, ...tool.args ?? []].join(" ");
|
|
1842
|
+
console.log(` ${tool.name}`);
|
|
1843
|
+
console.log(` command: ${cmdLine}`);
|
|
1844
|
+
if (tool.env && Object.keys(tool.env).length > 0) {
|
|
1845
|
+
console.log(` env: ${Object.keys(tool.env).join(", ")}`);
|
|
1846
|
+
}
|
|
1847
|
+
console.log();
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1943
1850
|
async function unwrapTool(name) {
|
|
1944
1851
|
const { config, path } = await loadConfigFile();
|
|
1945
1852
|
const idx = config.tools.findIndex((t) => t.name === name);
|
|
@@ -2009,11 +1916,11 @@ function getLaunchdPlist(configPath) {
|
|
|
2009
1916
|
<key>KeepAlive</key>
|
|
2010
1917
|
<true/>
|
|
2011
1918
|
<key>StandardOutPath</key>
|
|
2012
|
-
<string>${join3(
|
|
1919
|
+
<string>${join3(homedir2(), ".gigai", "server.log")}</string>
|
|
2013
1920
|
<key>StandardErrorPath</key>
|
|
2014
|
-
<string>${join3(
|
|
1921
|
+
<string>${join3(homedir2(), ".gigai", "server.log")}</string>
|
|
2015
1922
|
<key>WorkingDirectory</key>
|
|
2016
|
-
<string>${
|
|
1923
|
+
<string>${homedir2()}</string>
|
|
2017
1924
|
</dict>
|
|
2018
1925
|
</plist>
|
|
2019
1926
|
`;
|
|
@@ -2029,7 +1936,7 @@ Type=simple
|
|
|
2029
1936
|
ExecStart=${bin} start --config ${configPath}
|
|
2030
1937
|
Restart=always
|
|
2031
1938
|
RestartSec=5
|
|
2032
|
-
WorkingDirectory=${
|
|
1939
|
+
WorkingDirectory=${homedir2()}
|
|
2033
1940
|
|
|
2034
1941
|
[Install]
|
|
2035
1942
|
WantedBy=default.target
|
|
@@ -2037,10 +1944,10 @@ WantedBy=default.target
|
|
|
2037
1944
|
}
|
|
2038
1945
|
async function installDaemon(configPath) {
|
|
2039
1946
|
const config = resolve6(configPath ?? "gigai.config.json");
|
|
2040
|
-
const os =
|
|
1947
|
+
const os = platform4();
|
|
2041
1948
|
if (os === "darwin") {
|
|
2042
|
-
const plistPath = join3(
|
|
2043
|
-
await
|
|
1949
|
+
const plistPath = join3(homedir2(), "Library", "LaunchAgents", "com.gigai.server.plist");
|
|
1950
|
+
await writeFile5(plistPath, getLaunchdPlist(config));
|
|
2044
1951
|
console.log(` Wrote launchd plist: ${plistPath}`);
|
|
2045
1952
|
try {
|
|
2046
1953
|
await execFileAsync3("launchctl", ["load", plistPath]);
|
|
@@ -2051,11 +1958,11 @@ async function installDaemon(configPath) {
|
|
|
2051
1958
|
console.log(` Logs: ~/.gigai/server.log`);
|
|
2052
1959
|
console.log(` Stop: launchctl unload ${plistPath}`);
|
|
2053
1960
|
} else if (os === "linux") {
|
|
2054
|
-
const unitDir = join3(
|
|
1961
|
+
const unitDir = join3(homedir2(), ".config", "systemd", "user");
|
|
2055
1962
|
const unitPath = join3(unitDir, "gigai.service");
|
|
2056
1963
|
const { mkdir: mkdir2 } = await import("fs/promises");
|
|
2057
1964
|
await mkdir2(unitDir, { recursive: true });
|
|
2058
|
-
await
|
|
1965
|
+
await writeFile5(unitPath, getSystemdUnit(config));
|
|
2059
1966
|
console.log(` Wrote systemd unit: ${unitPath}`);
|
|
2060
1967
|
try {
|
|
2061
1968
|
await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -2073,9 +1980,9 @@ async function installDaemon(configPath) {
|
|
|
2073
1980
|
}
|
|
2074
1981
|
}
|
|
2075
1982
|
async function uninstallDaemon() {
|
|
2076
|
-
const os =
|
|
1983
|
+
const os = platform4();
|
|
2077
1984
|
if (os === "darwin") {
|
|
2078
|
-
const plistPath = join3(
|
|
1985
|
+
const plistPath = join3(homedir2(), "Library", "LaunchAgents", "com.gigai.server.plist");
|
|
2079
1986
|
try {
|
|
2080
1987
|
await execFileAsync3("launchctl", ["unload", plistPath]);
|
|
2081
1988
|
} catch {
|
|
@@ -2092,7 +1999,7 @@ async function uninstallDaemon() {
|
|
|
2092
1999
|
await execFileAsync3("systemctl", ["--user", "disable", "--now", "gigai"]);
|
|
2093
2000
|
} catch {
|
|
2094
2001
|
}
|
|
2095
|
-
const unitPath = join3(
|
|
2002
|
+
const unitPath = join3(homedir2(), ".config", "systemd", "user", "gigai.service");
|
|
2096
2003
|
const { unlink: unlink2 } = await import("fs/promises");
|
|
2097
2004
|
try {
|
|
2098
2005
|
await unlink2(unitPath);
|
|
@@ -2132,8 +2039,10 @@ async function startServer() {
|
|
|
2132
2039
|
},
|
|
2133
2040
|
strict: false
|
|
2134
2041
|
});
|
|
2135
|
-
const
|
|
2136
|
-
const
|
|
2042
|
+
const configFile = values.config;
|
|
2043
|
+
const config = await loadConfig(configFile);
|
|
2044
|
+
const configPath = resolve7(configFile ?? "gigai.config.json");
|
|
2045
|
+
const server = await createServer({ config, configPath, dev: values.dev });
|
|
2137
2046
|
const port = config.server.port;
|
|
2138
2047
|
const host = config.server.host;
|
|
2139
2048
|
await server.listen({ port, host });
|
|
@@ -2177,10 +2086,14 @@ async function startServer() {
|
|
|
2177
2086
|
process.on("SIGINT", shutdown);
|
|
2178
2087
|
}
|
|
2179
2088
|
export {
|
|
2089
|
+
CronScheduler,
|
|
2180
2090
|
createServer,
|
|
2181
2091
|
generateServerPairingCode,
|
|
2182
2092
|
installDaemon,
|
|
2183
2093
|
loadConfig,
|
|
2094
|
+
mcpAdd,
|
|
2095
|
+
mcpList,
|
|
2096
|
+
parseAtExpression,
|
|
2184
2097
|
runInit,
|
|
2185
2098
|
startServer,
|
|
2186
2099
|
stopServer,
|