@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,390 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { createServer, type Server } from "node:http";
3
+ import {
4
+ RpcBackend,
5
+ JsonRpcTransport,
6
+ RpcError,
7
+ type RpcResource,
8
+ } from "../src/backends/rpc.js";
9
+
10
+ // --- Hex utilities ---
11
+
12
+ function toHex(value: string | number): string {
13
+ const n = typeof value === "string" ? parseInt(value, 10) : value;
14
+ return "0x" + n.toString(16);
15
+ }
16
+
17
+ function fromHex(hex: string): number {
18
+ return parseInt(hex, 16);
19
+ }
20
+
21
+ // --- Mock JSON-RPC server ---
22
+
23
+ const mockBlocks: Record<string, object> = {};
24
+ for (let i = 0; i <= 16; i++) {
25
+ mockBlocks[toHex(i)] = {
26
+ number: toHex(i),
27
+ hash: `0x${"ab".repeat(16)}${i.toString(16).padStart(4, "0")}`,
28
+ transactions: [],
29
+ };
30
+ }
31
+
32
+ function handleRpcMethod(
33
+ method: string,
34
+ params: unknown[],
35
+ ): { result?: unknown; error?: { code: number; message: string } } {
36
+ switch (method) {
37
+ case "eth_blockNumber":
38
+ return { result: "0x10" }; // 16
39
+
40
+ case "eth_getBlockByNumber": {
41
+ const blockNum = params[0] as string;
42
+ const block = mockBlocks[blockNum];
43
+ return { result: block ?? null };
44
+ }
45
+
46
+ case "eth_getTransactionReceipt": {
47
+ const txHash = params[0] as string;
48
+ if (txHash === "0x0000") return { result: null };
49
+ return {
50
+ result: {
51
+ transactionHash: txHash,
52
+ blockNumber: "0x10",
53
+ status: "0x1",
54
+ gasUsed: "0x5208",
55
+ },
56
+ };
57
+ }
58
+
59
+ case "eth_getBalance":
60
+ return { result: "0xde0b6b3a7640000" }; // 1 ETH in wei
61
+
62
+ case "eth_getCode": {
63
+ const addr = params[0] as string;
64
+ if (addr === "0xeoa") return { result: "0x" };
65
+ return { result: "0x6060604052" };
66
+ }
67
+
68
+ case "eth_chainId":
69
+ return { result: "0x1" };
70
+
71
+ default:
72
+ return { error: { code: -32601, message: "Method not found" } };
73
+ }
74
+ }
75
+
76
+ let server: Server;
77
+ let baseUrl: string;
78
+
79
+ function createMockRpcServer(): Promise<{ server: Server; baseUrl: string }> {
80
+ return new Promise((resolve) => {
81
+ const srv = createServer((req, res) => {
82
+ res.setHeader("Content-Type", "application/json");
83
+
84
+ const chunks: Buffer[] = [];
85
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
86
+ req.on("end", () => {
87
+ const raw = Buffer.concat(chunks).toString();
88
+ const body = JSON.parse(raw);
89
+
90
+ // Batch request
91
+ if (Array.isArray(body)) {
92
+ const results = body.map((req: { id: number; method: string; params: unknown[] }) => {
93
+ const { result, error } = handleRpcMethod(req.method, req.params ?? []);
94
+ if (error) {
95
+ return { jsonrpc: "2.0", id: req.id, error };
96
+ }
97
+ return { jsonrpc: "2.0", id: req.id, result };
98
+ });
99
+ res.writeHead(200);
100
+ res.end(JSON.stringify(results));
101
+ return;
102
+ }
103
+
104
+ // Single request
105
+ const { result, error } = handleRpcMethod(body.method, body.params ?? []);
106
+ if (error) {
107
+ res.writeHead(200);
108
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id, error }));
109
+ return;
110
+ }
111
+ res.writeHead(200);
112
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: body.id, result }));
113
+ });
114
+ });
115
+
116
+ srv.listen(0, () => {
117
+ const addr = srv.address();
118
+ const port = typeof addr === "object" && addr ? addr.port : 0;
119
+ resolve({ server: srv, baseUrl: `http://localhost:${port}` });
120
+ });
121
+ });
122
+ }
123
+
124
+ // --- EVM Resource configuration ---
125
+
126
+ function createEvmResources(): RpcResource[] {
127
+ return [
128
+ {
129
+ name: "blocks",
130
+ idField: "number",
131
+ methods: {
132
+ read: {
133
+ method: "eth_getBlockByNumber",
134
+ params: (ctx) => [toHex(ctx.id!), false],
135
+ },
136
+ list: {
137
+ method: "eth_blockNumber",
138
+ params: () => [],
139
+ },
140
+ },
141
+ transform: {
142
+ list: (result) => {
143
+ const latest = fromHex(result as string);
144
+ return Array.from({ length: 10 }, (_, i) => `${latest - i}.json`);
145
+ },
146
+ read: (data) => ({
147
+ ...(data as object),
148
+ _number: fromHex((data as { number: string }).number),
149
+ }),
150
+ },
151
+ },
152
+ {
153
+ name: "txs",
154
+ idField: "hash",
155
+ methods: {
156
+ read: {
157
+ method: "eth_getTransactionReceipt",
158
+ params: (ctx) => [ctx.id!],
159
+ },
160
+ },
161
+ },
162
+ {
163
+ name: "balances",
164
+ methods: {
165
+ read: {
166
+ method: "eth_getBalance",
167
+ params: (ctx) => [ctx.id!, "latest"],
168
+ },
169
+ },
170
+ transform: {
171
+ read: (data) => ({
172
+ balance: data,
173
+ balanceWei: fromHex(data as string),
174
+ balanceEth: fromHex(data as string) / 1e18,
175
+ }),
176
+ },
177
+ },
178
+ {
179
+ name: "code",
180
+ methods: {
181
+ read: {
182
+ method: "eth_getCode",
183
+ params: (ctx) => [ctx.id!, "latest"],
184
+ },
185
+ },
186
+ transform: {
187
+ read: (data) => ({
188
+ code: data,
189
+ isContract: (data as string) !== "0x",
190
+ }),
191
+ },
192
+ },
193
+ {
194
+ name: "chain",
195
+ methods: {
196
+ read: {
197
+ method: "eth_chainId",
198
+ params: () => [],
199
+ },
200
+ },
201
+ transform: {
202
+ read: (data) => ({
203
+ chainId: data,
204
+ chainIdDecimal: fromHex(data as string),
205
+ }),
206
+ },
207
+ },
208
+ ];
209
+ }
210
+
211
+ // --- Tests ---
212
+
213
+ describe("RpcBackend (EVM)", () => {
214
+ let backend: RpcBackend;
215
+ let transport: JsonRpcTransport;
216
+
217
+ beforeAll(async () => {
218
+ const mock = await createMockRpcServer();
219
+ server = mock.server;
220
+ baseUrl = mock.baseUrl;
221
+
222
+ transport = new JsonRpcTransport({ url: baseUrl });
223
+ backend = new RpcBackend({
224
+ transport,
225
+ resources: createEvmResources(),
226
+ });
227
+ });
228
+
229
+ afterAll(
230
+ () =>
231
+ new Promise<void>((resolve) => {
232
+ server.close(() => resolve());
233
+ }),
234
+ );
235
+
236
+ // 1. Root listing
237
+ it("should list all resources at root", async () => {
238
+ const result = await backend.list("/");
239
+ expect(result).toContain("blocks/");
240
+ expect(result).toContain("txs/");
241
+ expect(result).toContain("balances/");
242
+ expect(result).toContain("code/");
243
+ expect(result).toContain("chain/");
244
+ });
245
+
246
+ // 2. Blocks list
247
+ it("should list recent blocks", async () => {
248
+ const result = await backend.list("/blocks/");
249
+ expect(result).toHaveLength(10);
250
+ expect(result[0]).toBe("16.json");
251
+ expect(result[9]).toBe("7.json");
252
+ });
253
+
254
+ // 3. Block read
255
+ it("should read a block by number", async () => {
256
+ const result = (await backend.read("/blocks/16.json")) as {
257
+ number: string;
258
+ _number: number;
259
+ };
260
+ expect(result._number).toBe(16);
261
+ expect(result.number).toBe("0x10");
262
+ });
263
+
264
+ // 4. Block not found
265
+ it("should throw NotFoundError for non-existent block", async () => {
266
+ await expect(backend.read("/blocks/999.json")).rejects.toThrow("Not found");
267
+ });
268
+
269
+ // 5. Transaction read
270
+ it("should read a transaction receipt", async () => {
271
+ const result = (await backend.read("/txs/0xabc.json")) as {
272
+ transactionHash: string;
273
+ status: string;
274
+ };
275
+ expect(result.transactionHash).toBe("0xabc");
276
+ expect(result.status).toBe("0x1");
277
+ });
278
+
279
+ // 6. Transaction not found
280
+ it("should throw NotFoundError for non-existent transaction", async () => {
281
+ await expect(backend.read("/txs/0x0000.json")).rejects.toThrow("Not found");
282
+ });
283
+
284
+ // 7. No list method
285
+ it("should return empty array for resources without list method", async () => {
286
+ const result = await backend.list("/txs/");
287
+ expect(result).toEqual([]);
288
+ });
289
+
290
+ // 8. Balance read
291
+ it("should read balance with ETH conversion", async () => {
292
+ const result = (await backend.read("/balances/0xdead.json")) as {
293
+ balanceEth: number;
294
+ };
295
+ expect(result.balanceEth).toBe(1);
296
+ });
297
+
298
+ // 9. Contract code
299
+ it("should detect contract code", async () => {
300
+ const result = (await backend.read("/code/0xcontract.json")) as {
301
+ isContract: boolean;
302
+ };
303
+ expect(result.isContract).toBe(true);
304
+ });
305
+
306
+ // 10. EOA detection
307
+ it("should detect EOA (no code)", async () => {
308
+ const result = (await backend.read("/code/0xeoa.json")) as {
309
+ isContract: boolean;
310
+ };
311
+ expect(result.isContract).toBe(false);
312
+ });
313
+
314
+ // 11. Chain info
315
+ it("should read chain info", async () => {
316
+ const result = (await backend.read("/chain/info.json")) as {
317
+ chainIdDecimal: number;
318
+ };
319
+ expect(result.chainIdDecimal).toBe(1);
320
+ });
321
+
322
+ // 12. Unknown resource
323
+ it("should throw NotFoundError for unknown resource", async () => {
324
+ await expect(backend.read("/unknown/1.json")).rejects.toThrow("Not found");
325
+ });
326
+
327
+ // 13. RPC error
328
+ it("should throw RpcError for unknown RPC method", async () => {
329
+ await expect(
330
+ transport.call("eth_nonExistent", []),
331
+ ).rejects.toThrow(RpcError);
332
+ });
333
+
334
+ // 14. Search fallback
335
+ it("should search blocks via client-side filter", async () => {
336
+ // eth_blockNumber returns "0x10" which contains "0x1"
337
+ const results = await backend.search("/blocks/", "0x1");
338
+ expect(results.length).toBeGreaterThanOrEqual(1);
339
+ });
340
+
341
+ // 15. AgentFs integration
342
+ describe("integration with AgentFs", async () => {
343
+ const { AgentFs } = await import("../src/agent-fs.js");
344
+
345
+ it("should work as a mount in AgentFs", async () => {
346
+ const agentFs = new AgentFs({
347
+ mounts: [{ path: "/eth", backend }],
348
+ });
349
+
350
+ const lsRoot = await agentFs.execute("ls /");
351
+ expect(lsRoot.ok).toBe(true);
352
+ if (lsRoot.ok) {
353
+ expect(lsRoot.data).toContain("eth/");
354
+ }
355
+
356
+ const lsEth = await agentFs.execute("ls /eth/");
357
+ expect(lsEth.ok).toBe(true);
358
+ if (lsEth.ok) {
359
+ expect(lsEth.data).toContain("blocks/");
360
+ }
361
+
362
+ const catBlock = await agentFs.execute("cat /eth/blocks/16.json");
363
+ expect(catBlock.ok).toBe(true);
364
+ if (catBlock.ok) {
365
+ expect((catBlock.data as { _number: number })._number).toBe(16);
366
+ }
367
+
368
+ const catBalance = await agentFs.execute(
369
+ "cat /eth/balances/0xdead.json",
370
+ );
371
+ expect(catBalance.ok).toBe(true);
372
+ if (catBalance.ok) {
373
+ expect((catBalance.data as { balanceEth: number }).balanceEth).toBe(1);
374
+ }
375
+ });
376
+ });
377
+
378
+ // Batch transport test
379
+ describe("JsonRpcTransport batch", () => {
380
+ it("should handle batch requests", async () => {
381
+ const results = await transport.batch!([
382
+ { method: "eth_chainId", params: [] },
383
+ { method: "eth_blockNumber", params: [] },
384
+ ]);
385
+ expect(results).toHaveLength(2);
386
+ expect(results[0]).toBe("0x1");
387
+ expect(results[1]).toBe("0x10");
388
+ });
389
+ });
390
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts", "src/testing.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ clean: true,
8
+ });