@schuttdev/gigai 0.2.8 → 0.2.9

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.
@@ -239,7 +239,7 @@ var ScriptToolConfigSchema = z.object({
239
239
  var BuiltinToolConfigSchema = z.object({
240
240
  type: z.literal("builtin"),
241
241
  name: z.string(),
242
- builtin: z.enum(["filesystem", "shell"]),
242
+ builtin: z.enum(["filesystem", "shell", "read", "write", "edit", "glob", "grep", "bash"]),
243
243
  description: z.string(),
244
244
  config: z.record(z.unknown()).optional()
245
245
  });
@@ -280,24 +280,29 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
280
280
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
281
281
  import { readFileSync } from "fs";
282
282
  import { resolve } from "path";
283
- import { readFile as fsReadFile, readdir } from "fs/promises";
283
+ import {
284
+ readFile as fsReadFile,
285
+ writeFile as fsWriteFile,
286
+ readdir
287
+ } from "fs/promises";
284
288
  import { resolve as resolve2, relative, join } from "path";
285
289
  import { realpath } from "fs/promises";
286
290
  import { spawn as spawn2 } from "child_process";
291
+ import { spawn as spawn3 } from "child_process";
287
292
  import { writeFile, readFile, unlink, mkdir } from "fs/promises";
288
293
  import { join as join2 } from "path";
289
294
  import { tmpdir } from "os";
290
295
  import { nanoid as nanoid3 } from "nanoid";
291
- import { execFile, spawn as spawn3 } from "child_process";
296
+ import { execFile, spawn as spawn4 } from "child_process";
292
297
  import { promisify } from "util";
293
298
  import { readFile as readFile2 } from "fs/promises";
294
299
  import { resolve as resolve3 } from "path";
295
- import { spawn as spawn4 } from "child_process";
296
300
  import { spawn as spawn5 } from "child_process";
301
+ import { spawn as spawn6 } from "child_process";
297
302
  import { input, select, checkbox, confirm } from "@inquirer/prompts";
298
303
  import { writeFile as writeFile2 } from "fs/promises";
299
304
  import { resolve as resolve4 } from "path";
300
- import { execFile as execFile2, spawn as spawn6 } from "child_process";
305
+ import { execFile as execFile2, spawn as spawn7 } from "child_process";
301
306
  import { promisify as promisify2 } from "util";
302
307
  import { input as input2 } from "@inquirer/prompts";
303
308
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
@@ -841,6 +846,26 @@ async function toolRoutes(server) {
841
846
  server.get("/tools", async () => {
842
847
  return { tools: server.registry.list() };
843
848
  });
849
+ server.get("/tools/search", async (request) => {
850
+ const query = request.query.q?.toLowerCase().trim();
851
+ if (!query) {
852
+ return { tools: server.registry.list() };
853
+ }
854
+ const all = server.registry.list();
855
+ const keywords = query.split(/\s+/);
856
+ const scored = all.map((tool) => {
857
+ const text = `${tool.name} ${tool.description}`.toLowerCase();
858
+ let score = 0;
859
+ for (const kw of keywords) {
860
+ if (tool.name.toLowerCase() === kw) score += 10;
861
+ else if (tool.name.toLowerCase().includes(kw)) score += 5;
862
+ if (tool.description.toLowerCase().includes(kw)) score += 2;
863
+ }
864
+ return { tool, score };
865
+ });
866
+ const matches = scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, 10).map((s) => s.tool);
867
+ return { tools: matches };
868
+ });
844
869
  server.get("/tools/:name", async (request) => {
845
870
  const { name } = request.params;
846
871
  const detail = server.registry.getDetail(name);
@@ -864,6 +889,8 @@ async function toolRoutes(server) {
864
889
  return { tools: mcpTools };
865
890
  });
866
891
  }
892
+ var MAX_OUTPUT_SIZE2 = 10 * 1024 * 1024;
893
+ var MAX_READ_SIZE = 2 * 1024 * 1024;
867
894
  async function validatePath(targetPath, allowedPaths) {
868
895
  const resolved = resolve2(targetPath);
869
896
  let real;
@@ -921,6 +948,250 @@ async function searchFilesSafe(path, pattern, allowedPaths) {
921
948
  await walk(safePath);
922
949
  return results;
923
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
+ }
924
1195
  var SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
925
1196
  "sh",
926
1197
  "bash",
@@ -936,7 +1207,7 @@ var SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
936
1207
  "strace",
937
1208
  "ltrace"
938
1209
  ]);
939
- var MAX_OUTPUT_SIZE2 = 10 * 1024 * 1024;
1210
+ var MAX_OUTPUT_SIZE3 = 10 * 1024 * 1024;
940
1211
  async function execCommandSafe(command, args, config) {
941
1212
  if (!config.allowlist.includes(command)) {
942
1213
  throw new GigaiError(
@@ -959,7 +1230,7 @@ async function execCommandSafe(command, args, config) {
959
1230
  }
960
1231
  }
961
1232
  return new Promise((resolve7, reject) => {
962
- const child = spawn2(command, args, {
1233
+ const child = spawn3(command, args, {
963
1234
  shell: false,
964
1235
  stdio: ["ignore", "pipe", "pipe"]
965
1236
  });
@@ -968,12 +1239,12 @@ async function execCommandSafe(command, args, config) {
968
1239
  let totalSize = 0;
969
1240
  child.stdout.on("data", (chunk) => {
970
1241
  totalSize += chunk.length;
971
- if (totalSize <= MAX_OUTPUT_SIZE2) stdoutChunks.push(chunk);
1242
+ if (totalSize <= MAX_OUTPUT_SIZE3) stdoutChunks.push(chunk);
972
1243
  else child.kill("SIGTERM");
973
1244
  });
974
1245
  child.stderr.on("data", (chunk) => {
975
1246
  totalSize += chunk.length;
976
- if (totalSize <= MAX_OUTPUT_SIZE2) stderrChunks.push(chunk);
1247
+ if (totalSize <= MAX_OUTPUT_SIZE3) stderrChunks.push(chunk);
977
1248
  else child.kill("SIGTERM");
978
1249
  });
979
1250
  child.on("error", (err) => {
@@ -1047,6 +1318,7 @@ async function execRoutes(server) {
1047
1318
  async function handleBuiltin(config, args) {
1048
1319
  const builtinConfig = config.config ?? {};
1049
1320
  switch (config.builtin) {
1321
+ // Legacy combined filesystem tool
1050
1322
  case "filesystem": {
1051
1323
  const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1052
1324
  const subcommand = args[0];
@@ -1062,6 +1334,7 @@ async function handleBuiltin(config, args) {
1062
1334
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Unknown filesystem subcommand: ${subcommand}. Use: read, list, search`);
1063
1335
  }
1064
1336
  }
1337
+ // Legacy shell tool
1065
1338
  case "shell": {
1066
1339
  const allowlist = builtinConfig.allowlist ?? [];
1067
1340
  const allowSudo = builtinConfig.allowSudo ?? false;
@@ -1072,6 +1345,37 @@ async function handleBuiltin(config, args) {
1072
1345
  const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1073
1346
  return { ...result, durationMs: 0 };
1074
1347
  }
1348
+ // --- New builtins ---
1349
+ case "read": {
1350
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1351
+ return { ...await readBuiltin(args, allowedPaths), durationMs: 0 };
1352
+ }
1353
+ case "write": {
1354
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1355
+ return { ...await writeBuiltin(args, allowedPaths), durationMs: 0 };
1356
+ }
1357
+ case "edit": {
1358
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1359
+ return { ...await editBuiltin(args, allowedPaths), durationMs: 0 };
1360
+ }
1361
+ case "glob": {
1362
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1363
+ return { ...await globBuiltin(args, allowedPaths), durationMs: 0 };
1364
+ }
1365
+ case "grep": {
1366
+ const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1367
+ return { ...await grepBuiltin(args, allowedPaths), durationMs: 0 };
1368
+ }
1369
+ case "bash": {
1370
+ const allowlist = builtinConfig.allowlist ?? [];
1371
+ const allowSudo = builtinConfig.allowSudo ?? false;
1372
+ const command = args[0];
1373
+ if (!command) {
1374
+ throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No command specified");
1375
+ }
1376
+ const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1377
+ return { ...result, durationMs: 0 };
1378
+ }
1075
1379
  default:
1076
1380
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Unknown builtin: ${config.builtin}`);
1077
1381
  }
@@ -1160,7 +1464,7 @@ async function adminRoutes(server) {
1160
1464
  args.push("--dev");
1161
1465
  }
1162
1466
  await server.close();
1163
- const child = spawn3("gigai", args, {
1467
+ const child = spawn4("gigai", args, {
1164
1468
  detached: true,
1165
1469
  stdio: "ignore",
1166
1470
  cwd: process.cwd()
@@ -1228,7 +1532,7 @@ async function loadConfig(path) {
1228
1532
  }
1229
1533
  function runCommand(command, args) {
1230
1534
  return new Promise((resolve7, reject) => {
1231
- const child = spawn4(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1535
+ const child = spawn5(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
1232
1536
  const chunks = [];
1233
1537
  child.stdout.on("data", (chunk) => chunks.push(chunk));
1234
1538
  child.on("error", reject);
@@ -1269,7 +1573,7 @@ async function disableFunnel(port) {
1269
1573
  await runCommand("tailscale", ["funnel", "--bg", "off", `${port}`]);
1270
1574
  }
1271
1575
  function runTunnel(tunnelName, localPort) {
1272
- const child = spawn5("cloudflared", [
1576
+ const child = spawn6("cloudflared", [
1273
1577
  "tunnel",
1274
1578
  "--url",
1275
1579
  `http://localhost:${localPort}`,
@@ -1487,7 +1791,7 @@ async function runInit() {
1487
1791
  console.log("\n Starting server...");
1488
1792
  const serverArgs = ["start", "--config", configPath];
1489
1793
  if (!httpsConfig) serverArgs.push("--dev");
1490
- const child = spawn6("gigai", serverArgs, {
1794
+ const child = spawn7("gigai", serverArgs, {
1491
1795
  detached: true,
1492
1796
  stdio: "ignore",
1493
1797
  cwd: resolve4(".")
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@
4
4
  import { defineCommand, runMain } from "citty";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = "0.2.8";
7
+ var VERSION = "0.2.9";
8
8
 
9
9
  // src/index.ts
10
10
  async function requireServer() {
11
11
  try {
12
- return await import("./dist-Z7XWWQBF.js");
12
+ return await import("./dist-YWMCMOTP.js");
13
13
  } catch {
14
14
  console.error("Server dependencies not installed.");
15
15
  console.error("Run: npm install -g @schuttdev/gigai");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schuttdev/gigai",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gigai": "dist/index.js"