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