@nkmc/agent-fs 0.1.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.
Files changed (42) hide show
  1. package/dist/chunk-7LIZT7L3.js +966 -0
  2. package/dist/index.cjs +1278 -0
  3. package/dist/index.d.cts +96 -0
  4. package/dist/index.d.ts +96 -0
  5. package/dist/index.js +419 -0
  6. package/dist/rpc-D1IHpjF_.d.cts +330 -0
  7. package/dist/rpc-D1IHpjF_.d.ts +330 -0
  8. package/dist/testing.cjs +842 -0
  9. package/dist/testing.d.cts +29 -0
  10. package/dist/testing.d.ts +29 -0
  11. package/dist/testing.js +10 -0
  12. package/package.json +25 -0
  13. package/src/agent-fs.ts +151 -0
  14. package/src/backends/http.ts +835 -0
  15. package/src/backends/memory.ts +183 -0
  16. package/src/backends/rpc.ts +456 -0
  17. package/src/index.ts +36 -0
  18. package/src/mount.ts +84 -0
  19. package/src/parser.ts +162 -0
  20. package/src/server.ts +158 -0
  21. package/src/testing.ts +3 -0
  22. package/src/types.ts +52 -0
  23. package/test/agent-fs.test.ts +325 -0
  24. package/test/http-204.test.ts +102 -0
  25. package/test/http-auth-prefix.test.ts +79 -0
  26. package/test/http-cloudflare.test.ts +533 -0
  27. package/test/http-form-encoding.test.ts +119 -0
  28. package/test/http-github.test.ts +580 -0
  29. package/test/http-listkey.test.ts +128 -0
  30. package/test/http-oauth2.test.ts +174 -0
  31. package/test/http-pagination.test.ts +200 -0
  32. package/test/http-param-styles.test.ts +98 -0
  33. package/test/http-passthrough.test.ts +282 -0
  34. package/test/http-retry.test.ts +132 -0
  35. package/test/http.test.ts +360 -0
  36. package/test/memory.test.ts +120 -0
  37. package/test/mount.test.ts +94 -0
  38. package/test/parser.test.ts +100 -0
  39. package/test/rpc-crud.test.ts +627 -0
  40. package/test/rpc-evm.test.ts +390 -0
  41. package/tsconfig.json +8 -0
  42. package/tsup.config.ts +8 -0
@@ -0,0 +1,96 @@
1
+ import { F as FsResult, A as AgentContext, M as Mount, a as FsError, b as FsBackend, c as FsCommand } from './rpc-D1IHpjF_.cjs';
2
+ export { d as AccessRole, e as FsOp, H as HttpAuth, f as HttpBackend, g as HttpBackendConfig, h as HttpEndpoint, i as HttpResource, J as JsonRpcTransport, j as JsonRpcTransportConfig, P as PaginationConfig, R as RpcBackend, k as RpcBackendConfig, l as RpcCallContext, m as RpcError, n as RpcMethod, o as RpcResource, p as RpcTransport } from './rpc-D1IHpjF_.cjs';
3
+ import * as node_http from 'node:http';
4
+ import { IncomingMessage, ServerResponse } from 'node:http';
5
+
6
+ /**
7
+ * Parse a raw command string into a structured FsCommand.
8
+ *
9
+ * Accepted formats:
10
+ * ls /path
11
+ * cat /path
12
+ * write /path '{"key":"value"}'
13
+ * write /path {"key":"value"}
14
+ * rm /path
15
+ * grep pattern /path
16
+ */
17
+ declare function parseCommand(input: string): FsResult;
18
+
19
+ interface ResolvedMount {
20
+ mount: Mount;
21
+ /** The path relative to the mount point (e.g. "/users/42.json") */
22
+ relativePath: string;
23
+ }
24
+ declare class MountResolver {
25
+ private mounts;
26
+ /** Optional async callback invoked when resolve() finds no matching mount.
27
+ * Should return true if a new mount was added. */
28
+ onMiss?: (path: string, agent?: AgentContext) => Promise<boolean>;
29
+ /** Register a mount point */
30
+ add(mount: Mount): void;
31
+ /** Resolve a virtual path to a mount and its relative path */
32
+ resolve(virtualPath: string): ResolvedMount | null;
33
+ /** Async resolve: tries sync resolve first, then calls onMiss if set. */
34
+ resolveAsync(virtualPath: string, agent?: AgentContext): Promise<ResolvedMount | null>;
35
+ /** Check if a role has permission for an operation on a mount */
36
+ checkPermission(mount: Mount, op: "read" | "write", roles: string[]): FsError | null;
37
+ /** List all registered mount paths (used by "ls /") */
38
+ listMounts(): string[];
39
+ /** Get the backend for a mount path */
40
+ getBackend(mountPath: string): FsBackend | undefined;
41
+ }
42
+
43
+ interface AgentFsOptions {
44
+ mounts: Mount[];
45
+ /** Called when a path doesn't match any static mount. Return true if you added a new mount. */
46
+ onMiss?: (path: string, addMount: (mount: Mount) => void, agent?: AgentContext) => Promise<boolean>;
47
+ /** Called on "ls /" to include dynamically-known domains. */
48
+ listDomains?: () => Promise<string[]>;
49
+ /** Called on "grep /" to search across registered services. */
50
+ searchDomains?: (query: string) => Promise<unknown[]>;
51
+ /** Called on "grep pattern /domain/" to search endpoint metadata within a single domain. */
52
+ searchEndpoints?: (domain: string, query: string) => Promise<unknown[]>;
53
+ }
54
+ /**
55
+ * The core engine: takes a raw command string, parses it,
56
+ * routes to the correct backend, and returns a result.
57
+ */
58
+ declare class AgentFs {
59
+ private resolver;
60
+ private _listDomains?;
61
+ private _searchDomains?;
62
+ private _searchEndpoints?;
63
+ constructor(options: AgentFsOptions);
64
+ /** Execute a raw command string like "ls /db/users/" */
65
+ execute(input: string, roles?: string[], agent?: AgentContext): Promise<FsResult>;
66
+ /** Execute a pre-parsed FsCommand */
67
+ executeCommand(cmd: FsCommand, roles?: string[], agent?: AgentContext): Promise<FsResult>;
68
+ }
69
+
70
+ interface ServerOptions {
71
+ agentFs: AgentFs;
72
+ port?: number;
73
+ }
74
+ /**
75
+ * HTTP server that exposes the AgentFs as a REST API.
76
+ *
77
+ * Route mapping:
78
+ * GET /fs/* → ls (if path ends with /) or cat
79
+ * GET /fs/*?q=xxx → grep
80
+ * POST /fs/* → write
81
+ * PUT /fs/* → write
82
+ * DELETE /fs/* → rm
83
+ *
84
+ * Also accepts raw command strings via:
85
+ * POST /execute body: { command: "ls /db/users/" }
86
+ */
87
+ declare function createAgentFsServer(options: ServerOptions): {
88
+ server: node_http.Server<typeof IncomingMessage, typeof ServerResponse>;
89
+ listen: () => Promise<void>;
90
+ close: () => Promise<void>;
91
+ port: number;
92
+ };
93
+
94
+ declare const VERSION = "0.1.0";
95
+
96
+ export { AgentContext, AgentFs, type AgentFsOptions, FsBackend, FsCommand, FsError, FsResult, Mount, MountResolver, type ResolvedMount, type ServerOptions, VERSION, createAgentFsServer, parseCommand };
@@ -0,0 +1,96 @@
1
+ import { F as FsResult, A as AgentContext, M as Mount, a as FsError, b as FsBackend, c as FsCommand } from './rpc-D1IHpjF_.js';
2
+ export { d as AccessRole, e as FsOp, H as HttpAuth, f as HttpBackend, g as HttpBackendConfig, h as HttpEndpoint, i as HttpResource, J as JsonRpcTransport, j as JsonRpcTransportConfig, P as PaginationConfig, R as RpcBackend, k as RpcBackendConfig, l as RpcCallContext, m as RpcError, n as RpcMethod, o as RpcResource, p as RpcTransport } from './rpc-D1IHpjF_.js';
3
+ import * as node_http from 'node:http';
4
+ import { IncomingMessage, ServerResponse } from 'node:http';
5
+
6
+ /**
7
+ * Parse a raw command string into a structured FsCommand.
8
+ *
9
+ * Accepted formats:
10
+ * ls /path
11
+ * cat /path
12
+ * write /path '{"key":"value"}'
13
+ * write /path {"key":"value"}
14
+ * rm /path
15
+ * grep pattern /path
16
+ */
17
+ declare function parseCommand(input: string): FsResult;
18
+
19
+ interface ResolvedMount {
20
+ mount: Mount;
21
+ /** The path relative to the mount point (e.g. "/users/42.json") */
22
+ relativePath: string;
23
+ }
24
+ declare class MountResolver {
25
+ private mounts;
26
+ /** Optional async callback invoked when resolve() finds no matching mount.
27
+ * Should return true if a new mount was added. */
28
+ onMiss?: (path: string, agent?: AgentContext) => Promise<boolean>;
29
+ /** Register a mount point */
30
+ add(mount: Mount): void;
31
+ /** Resolve a virtual path to a mount and its relative path */
32
+ resolve(virtualPath: string): ResolvedMount | null;
33
+ /** Async resolve: tries sync resolve first, then calls onMiss if set. */
34
+ resolveAsync(virtualPath: string, agent?: AgentContext): Promise<ResolvedMount | null>;
35
+ /** Check if a role has permission for an operation on a mount */
36
+ checkPermission(mount: Mount, op: "read" | "write", roles: string[]): FsError | null;
37
+ /** List all registered mount paths (used by "ls /") */
38
+ listMounts(): string[];
39
+ /** Get the backend for a mount path */
40
+ getBackend(mountPath: string): FsBackend | undefined;
41
+ }
42
+
43
+ interface AgentFsOptions {
44
+ mounts: Mount[];
45
+ /** Called when a path doesn't match any static mount. Return true if you added a new mount. */
46
+ onMiss?: (path: string, addMount: (mount: Mount) => void, agent?: AgentContext) => Promise<boolean>;
47
+ /** Called on "ls /" to include dynamically-known domains. */
48
+ listDomains?: () => Promise<string[]>;
49
+ /** Called on "grep /" to search across registered services. */
50
+ searchDomains?: (query: string) => Promise<unknown[]>;
51
+ /** Called on "grep pattern /domain/" to search endpoint metadata within a single domain. */
52
+ searchEndpoints?: (domain: string, query: string) => Promise<unknown[]>;
53
+ }
54
+ /**
55
+ * The core engine: takes a raw command string, parses it,
56
+ * routes to the correct backend, and returns a result.
57
+ */
58
+ declare class AgentFs {
59
+ private resolver;
60
+ private _listDomains?;
61
+ private _searchDomains?;
62
+ private _searchEndpoints?;
63
+ constructor(options: AgentFsOptions);
64
+ /** Execute a raw command string like "ls /db/users/" */
65
+ execute(input: string, roles?: string[], agent?: AgentContext): Promise<FsResult>;
66
+ /** Execute a pre-parsed FsCommand */
67
+ executeCommand(cmd: FsCommand, roles?: string[], agent?: AgentContext): Promise<FsResult>;
68
+ }
69
+
70
+ interface ServerOptions {
71
+ agentFs: AgentFs;
72
+ port?: number;
73
+ }
74
+ /**
75
+ * HTTP server that exposes the AgentFs as a REST API.
76
+ *
77
+ * Route mapping:
78
+ * GET /fs/* → ls (if path ends with /) or cat
79
+ * GET /fs/*?q=xxx → grep
80
+ * POST /fs/* → write
81
+ * PUT /fs/* → write
82
+ * DELETE /fs/* → rm
83
+ *
84
+ * Also accepts raw command strings via:
85
+ * POST /execute body: { command: "ls /db/users/" }
86
+ */
87
+ declare function createAgentFsServer(options: ServerOptions): {
88
+ server: node_http.Server<typeof IncomingMessage, typeof ServerResponse>;
89
+ listen: () => Promise<void>;
90
+ close: () => Promise<void>;
91
+ port: number;
92
+ };
93
+
94
+ declare const VERSION = "0.1.0";
95
+
96
+ export { AgentContext, AgentFs, type AgentFsOptions, FsBackend, FsCommand, FsError, FsResult, Mount, MountResolver, type ResolvedMount, type ServerOptions, VERSION, createAgentFsServer, parseCommand };
package/dist/index.js ADDED
@@ -0,0 +1,419 @@
1
+ import {
2
+ HttpBackend,
3
+ JsonRpcTransport,
4
+ NotFoundError,
5
+ RpcBackend,
6
+ RpcError
7
+ } from "./chunk-7LIZT7L3.js";
8
+
9
+ // src/parser.ts
10
+ var VALID_OPS = /* @__PURE__ */ new Set(["ls", "cat", "write", "rm", "grep"]);
11
+ function parseCommand(input) {
12
+ const trimmed = input.trim();
13
+ const normalized = trimmed.startsWith("nk ") ? trimmed.slice(3).trim() : trimmed;
14
+ const spaceIdx = normalized.indexOf(" ");
15
+ if (spaceIdx === -1) {
16
+ return {
17
+ ok: false,
18
+ error: {
19
+ code: "PARSE_ERROR",
20
+ message: `Missing path: "${input}"`
21
+ }
22
+ };
23
+ }
24
+ const op = normalized.slice(0, spaceIdx);
25
+ if (!VALID_OPS.has(op)) {
26
+ return {
27
+ ok: false,
28
+ error: {
29
+ code: "PARSE_ERROR",
30
+ message: `Unknown operation: "${op}". Valid: ls, cat, write, rm, grep`
31
+ }
32
+ };
33
+ }
34
+ const rest = normalized.slice(spaceIdx + 1).trim();
35
+ if (op === "grep") {
36
+ return parseGrep(rest, input);
37
+ }
38
+ if (op === "write") {
39
+ return parseWrite(rest, input);
40
+ }
41
+ const path = normalizePath(rest);
42
+ if (!path) {
43
+ return {
44
+ ok: false,
45
+ error: { code: "PARSE_ERROR", message: `Invalid path: "${rest}"` }
46
+ };
47
+ }
48
+ return { ok: true, data: { op, path } };
49
+ }
50
+ function parseGrep(rest, raw) {
51
+ let pattern;
52
+ let pathPart;
53
+ if (rest.startsWith('"') || rest.startsWith("'")) {
54
+ const quote = rest[0];
55
+ const endQuote = rest.indexOf(quote, 1);
56
+ if (endQuote === -1) {
57
+ return {
58
+ ok: false,
59
+ error: { code: "PARSE_ERROR", message: `Unterminated quote in: "${raw}"` }
60
+ };
61
+ }
62
+ pattern = rest.slice(1, endQuote);
63
+ pathPart = rest.slice(endQuote + 1).trim();
64
+ } else {
65
+ const spaceIdx = rest.indexOf(" ");
66
+ if (spaceIdx === -1) {
67
+ return {
68
+ ok: false,
69
+ error: { code: "PARSE_ERROR", message: `grep requires pattern and path: "${raw}"` }
70
+ };
71
+ }
72
+ pattern = rest.slice(0, spaceIdx);
73
+ pathPart = rest.slice(spaceIdx + 1).trim();
74
+ }
75
+ const path = normalizePath(pathPart);
76
+ if (!path) {
77
+ return {
78
+ ok: false,
79
+ error: { code: "PARSE_ERROR", message: `Invalid path: "${pathPart}"` }
80
+ };
81
+ }
82
+ return { ok: true, data: { op: "grep", path, pattern } };
83
+ }
84
+ function parseWrite(rest, raw) {
85
+ const pathMatch = rest.match(/^(\/\S+)\s+(.+)$/s);
86
+ if (!pathMatch) {
87
+ return {
88
+ ok: false,
89
+ error: { code: "PARSE_ERROR", message: `write requires path and data: "${raw}"` }
90
+ };
91
+ }
92
+ const path = normalizePath(pathMatch[1]);
93
+ if (!path) {
94
+ return {
95
+ ok: false,
96
+ error: { code: "PARSE_ERROR", message: `Invalid path: "${pathMatch[1]}"` }
97
+ };
98
+ }
99
+ let dataStr = pathMatch[2].trim();
100
+ if (dataStr.startsWith("'") && dataStr.endsWith("'") || dataStr.startsWith('"') && dataStr.endsWith('"')) {
101
+ dataStr = dataStr.slice(1, -1);
102
+ }
103
+ let data;
104
+ try {
105
+ data = JSON.parse(dataStr);
106
+ } catch {
107
+ return {
108
+ ok: false,
109
+ error: { code: "PARSE_ERROR", message: `Invalid JSON data: ${dataStr}` }
110
+ };
111
+ }
112
+ return { ok: true, data: { op: "write", path, data } };
113
+ }
114
+ function normalizePath(raw) {
115
+ if (!raw.startsWith("/")) return null;
116
+ if (raw.includes("..")) return null;
117
+ const cleaned = raw.replace(/\/+/g, "/");
118
+ return cleaned;
119
+ }
120
+
121
+ // src/mount.ts
122
+ var MountResolver = class {
123
+ mounts = [];
124
+ /** Optional async callback invoked when resolve() finds no matching mount.
125
+ * Should return true if a new mount was added. */
126
+ onMiss;
127
+ /** Register a mount point */
128
+ add(mount) {
129
+ const normalized = mount.path.replace(/\/+$/, "") || "/";
130
+ this.mounts.push({ ...mount, path: normalized });
131
+ this.mounts.sort((a, b) => b.path.length - a.path.length);
132
+ }
133
+ /** Resolve a virtual path to a mount and its relative path */
134
+ resolve(virtualPath) {
135
+ for (const mount of this.mounts) {
136
+ if (virtualPath === mount.path || virtualPath.startsWith(mount.path + "/")) {
137
+ const relativePath = virtualPath.slice(mount.path.length) || "/";
138
+ return { mount, relativePath };
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ /** Async resolve: tries sync resolve first, then calls onMiss if set. */
144
+ async resolveAsync(virtualPath, agent) {
145
+ const result = this.resolve(virtualPath);
146
+ if (result) return result;
147
+ if (this.onMiss) {
148
+ const added = await this.onMiss(virtualPath, agent);
149
+ if (added) {
150
+ return this.resolve(virtualPath);
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ /** Check if a role has permission for an operation on a mount */
156
+ checkPermission(mount, op, roles) {
157
+ const allowed = mount.permissions?.[op];
158
+ if (!allowed) return null;
159
+ const hasRole = roles.some((r) => allowed.includes(r));
160
+ if (!hasRole) {
161
+ return {
162
+ code: "PERMISSION_DENIED",
163
+ message: `Requires one of [${allowed.join(", ")}] for ${op} on ${mount.path}`
164
+ };
165
+ }
166
+ return null;
167
+ }
168
+ /** List all registered mount paths (used by "ls /") */
169
+ listMounts() {
170
+ return this.mounts.map((m) => m.path);
171
+ }
172
+ /** Get the backend for a mount path */
173
+ getBackend(mountPath) {
174
+ const mount = this.mounts.find((m) => m.path === mountPath);
175
+ return mount?.backend;
176
+ }
177
+ };
178
+
179
+ // src/agent-fs.ts
180
+ var AgentFs = class {
181
+ resolver;
182
+ _listDomains;
183
+ _searchDomains;
184
+ _searchEndpoints;
185
+ constructor(options) {
186
+ this.resolver = new MountResolver();
187
+ for (const mount of options.mounts) {
188
+ this.resolver.add(mount);
189
+ }
190
+ if (options.onMiss) {
191
+ const addMount = (mount) => this.resolver.add(mount);
192
+ this.resolver.onMiss = (path, agent) => options.onMiss(path, addMount, agent);
193
+ }
194
+ this._listDomains = options.listDomains;
195
+ this._searchDomains = options.searchDomains;
196
+ this._searchEndpoints = options.searchEndpoints;
197
+ }
198
+ /** Execute a raw command string like "ls /db/users/" */
199
+ async execute(input, roles = ["agent"], agent) {
200
+ const parsed = parseCommand(input);
201
+ if (!parsed.ok) return parsed;
202
+ const cmd = parsed.data;
203
+ return this.executeCommand(cmd, roles, agent);
204
+ }
205
+ /** Execute a pre-parsed FsCommand */
206
+ async executeCommand(cmd, roles = ["agent"], agent) {
207
+ if (cmd.path === "/" && cmd.op === "ls") {
208
+ const staticEntries = this.resolver.listMounts().map((p) => p.slice(1) + "/");
209
+ if (this._listDomains) {
210
+ const dynamicDomains = await this._listDomains();
211
+ const dynamicEntries = dynamicDomains.map((d) => d + "/");
212
+ const merged = [.../* @__PURE__ */ new Set([...staticEntries, ...dynamicEntries])];
213
+ return { ok: true, data: merged };
214
+ }
215
+ return { ok: true, data: staticEntries };
216
+ }
217
+ if (cmd.path === "/" && cmd.op === "grep" && this._searchDomains) {
218
+ const results = await this._searchDomains(cmd.pattern);
219
+ return { ok: true, data: results };
220
+ }
221
+ if (cmd.op === "grep" && this._searchEndpoints) {
222
+ const segments = cmd.path.split("/").filter(Boolean);
223
+ if (segments.length === 1) {
224
+ const domain = segments[0];
225
+ const results = await this._searchEndpoints(domain, cmd.pattern);
226
+ return { ok: true, data: results };
227
+ }
228
+ }
229
+ const resolved = await this.resolver.resolveAsync(cmd.path, agent);
230
+ if (!resolved) {
231
+ return {
232
+ ok: false,
233
+ error: { code: "NO_MOUNT", message: `No mount for path: ${cmd.path}` }
234
+ };
235
+ }
236
+ const permType = cmd.op === "write" || cmd.op === "rm" ? "write" : "read";
237
+ const permError = this.resolver.checkPermission(
238
+ resolved.mount,
239
+ permType,
240
+ roles
241
+ );
242
+ if (permError) {
243
+ return { ok: false, error: permError };
244
+ }
245
+ const backend = resolved.mount.backend;
246
+ const relPath = resolved.relativePath;
247
+ try {
248
+ switch (cmd.op) {
249
+ case "ls": {
250
+ const entries = await backend.list(relPath);
251
+ return { ok: true, data: entries };
252
+ }
253
+ case "cat": {
254
+ const data = await backend.read(relPath);
255
+ return { ok: true, data };
256
+ }
257
+ case "write": {
258
+ const result = await backend.write(relPath, cmd.data);
259
+ return { ok: true, data: result };
260
+ }
261
+ case "rm": {
262
+ await backend.remove(relPath);
263
+ return { ok: true, data: { deleted: cmd.path } };
264
+ }
265
+ case "grep": {
266
+ const results = await backend.search(relPath, cmd.pattern);
267
+ return { ok: true, data: results };
268
+ }
269
+ }
270
+ } catch (err) {
271
+ if (err instanceof NotFoundError) {
272
+ return {
273
+ ok: false,
274
+ error: { code: "NOT_FOUND", message: err.message }
275
+ };
276
+ }
277
+ return {
278
+ ok: false,
279
+ error: {
280
+ code: "BACKEND_ERROR",
281
+ message: err instanceof Error ? err.message : String(err)
282
+ }
283
+ };
284
+ }
285
+ }
286
+ };
287
+
288
+ // src/server.ts
289
+ import { createServer } from "http";
290
+ function createAgentFsServer(options) {
291
+ const { agentFs, port = 3071 } = options;
292
+ const server = createServer(async (req, res) => {
293
+ try {
294
+ res.setHeader("Access-Control-Allow-Origin", "*");
295
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
296
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
297
+ if (req.method === "OPTIONS") {
298
+ res.writeHead(204);
299
+ res.end();
300
+ return;
301
+ }
302
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
303
+ if (url.pathname === "/execute" && req.method === "POST") {
304
+ const body = await readBody(req);
305
+ const { command, roles } = body;
306
+ if (!command || typeof command !== "string") {
307
+ sendJson(res, 400, { error: "Missing 'command' field" });
308
+ return;
309
+ }
310
+ const result = await agentFs.execute(command, roles);
311
+ const status = result.ok ? 200 : errorToStatus(result.error.code);
312
+ sendJson(res, status, result);
313
+ return;
314
+ }
315
+ if (url.pathname.startsWith("/fs")) {
316
+ const virtualPath = url.pathname.slice(3) || "/";
317
+ const query = url.searchParams.get("q");
318
+ let op;
319
+ let data;
320
+ let pattern;
321
+ switch (req.method) {
322
+ case "GET":
323
+ if (query) {
324
+ op = "grep";
325
+ pattern = query;
326
+ } else if (virtualPath.endsWith("/")) {
327
+ op = "ls";
328
+ } else {
329
+ op = "cat";
330
+ }
331
+ break;
332
+ case "POST":
333
+ case "PUT":
334
+ op = "write";
335
+ data = await readBody(req);
336
+ break;
337
+ case "DELETE":
338
+ op = "rm";
339
+ break;
340
+ default:
341
+ sendJson(res, 405, { error: "Method not allowed" });
342
+ return;
343
+ }
344
+ const result = await agentFs.executeCommand(
345
+ { op, path: virtualPath, data, pattern }
346
+ );
347
+ const status = result.ok ? 200 : errorToStatus(result.error.code);
348
+ sendJson(res, status, result);
349
+ return;
350
+ }
351
+ sendJson(res, 404, { error: "Not found. Use /fs/* or /execute" });
352
+ } catch (err) {
353
+ sendJson(res, 500, {
354
+ error: err instanceof Error ? err.message : "Internal server error"
355
+ });
356
+ }
357
+ });
358
+ return {
359
+ server,
360
+ listen: () => new Promise((resolve) => {
361
+ server.listen(port, () => resolve());
362
+ }),
363
+ close: () => new Promise((resolve, reject) => {
364
+ server.close((err) => err ? reject(err) : resolve());
365
+ }),
366
+ port
367
+ };
368
+ }
369
+ async function readBody(req) {
370
+ return new Promise((resolve, reject) => {
371
+ const chunks = [];
372
+ req.on("data", (chunk) => chunks.push(chunk));
373
+ req.on("end", () => {
374
+ const raw = Buffer.concat(chunks).toString("utf-8");
375
+ if (!raw) {
376
+ resolve({});
377
+ return;
378
+ }
379
+ try {
380
+ resolve(JSON.parse(raw));
381
+ } catch {
382
+ reject(new Error("Invalid JSON body"));
383
+ }
384
+ });
385
+ req.on("error", reject);
386
+ });
387
+ }
388
+ function sendJson(res, status, data) {
389
+ res.writeHead(status, { "Content-Type": "application/json" });
390
+ res.end(JSON.stringify(data));
391
+ }
392
+ function errorToStatus(code) {
393
+ switch (code) {
394
+ case "PARSE_ERROR":
395
+ case "INVALID_PATH":
396
+ return 400;
397
+ case "PERMISSION_DENIED":
398
+ return 403;
399
+ case "NOT_FOUND":
400
+ case "NO_MOUNT":
401
+ return 404;
402
+ default:
403
+ return 500;
404
+ }
405
+ }
406
+
407
+ // src/index.ts
408
+ var VERSION = "0.1.0";
409
+ export {
410
+ AgentFs,
411
+ HttpBackend,
412
+ JsonRpcTransport,
413
+ MountResolver,
414
+ RpcBackend,
415
+ RpcError,
416
+ VERSION,
417
+ createAgentFsServer,
418
+ parseCommand
419
+ };