@fungi.computer/truffle 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -2
  3. package/computerd-loopback.cjs +16 -0
  4. package/dist/auth-cli.d.ts +36 -0
  5. package/dist/auth-cli.js +351 -0
  6. package/dist/fungi-account-command.d.ts +18 -0
  7. package/dist/fungi-account-command.js +92 -0
  8. package/dist/index.d.ts +19 -0
  9. package/dist/index.js +17 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
  11. package/dist/internal/sandbox-metafile.json +6726 -0
  12. package/dist/linear-auth.d.ts +4 -0
  13. package/dist/linear-auth.js +72 -0
  14. package/dist/local-extensions.d.ts +97 -0
  15. package/dist/local-extensions.js +484 -0
  16. package/dist/local-login.d.ts +55 -0
  17. package/dist/local-login.js +167 -0
  18. package/dist/local-models.d.ts +14 -0
  19. package/dist/local-models.js +77 -0
  20. package/dist/local-skill-source.d.ts +6 -0
  21. package/dist/local-skill-source.js +357 -0
  22. package/dist/log-event.d.ts +2 -0
  23. package/dist/log-event.js +9 -0
  24. package/dist/private-bun-shiitake-host.d.ts +11 -0
  25. package/dist/private-bun-shiitake-host.js +66 -0
  26. package/dist/private-file-credential-store.d.ts +33 -0
  27. package/dist/private-file-credential-store.js +283 -0
  28. package/dist/private-files-patch.d.ts +3 -0
  29. package/dist/private-files-patch.js +86 -0
  30. package/dist/private-fungi-account.d.ts +235 -0
  31. package/dist/private-fungi-account.js +1149 -0
  32. package/dist/private-fungi-auth-error.d.ts +5 -0
  33. package/dist/private-fungi-auth-error.js +7 -0
  34. package/dist/private-fungi-mcp-connection.d.ts +22 -0
  35. package/dist/private-fungi-mcp-connection.js +143 -0
  36. package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
  37. package/dist/private-fungi-mcp-endpoint.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +14 -0
  39. package/dist/private-fungi-mcp-selection.js +36 -0
  40. package/dist/private-fungi-model-api.d.ts +6 -0
  41. package/dist/private-fungi-model-api.js +125 -0
  42. package/dist/private-fungi-model-catalog.d.ts +6 -0
  43. package/dist/private-fungi-model-catalog.js +78 -0
  44. package/dist/private-fungi-model-provider.d.ts +6 -0
  45. package/dist/private-fungi-model-provider.js +46 -0
  46. package/dist/private-fungi-pending-logouts.d.ts +34 -0
  47. package/dist/private-fungi-pending-logouts.js +136 -0
  48. package/dist/private-invocation-computer.d.ts +37 -0
  49. package/dist/private-invocation-computer.js +459 -0
  50. package/dist/private-invocation-files.d.ts +15 -0
  51. package/dist/private-invocation-files.js +449 -0
  52. package/dist/private-local-acp-stdio.d.ts +17 -0
  53. package/dist/private-local-acp-stdio.js +142 -0
  54. package/dist/private-local-auth-rpc.d.ts +69 -0
  55. package/dist/private-local-auth-rpc.js +273 -0
  56. package/dist/private-local-computer-attach.d.ts +49 -0
  57. package/dist/private-local-computer-attach.js +216 -0
  58. package/dist/private-local-computer-backend.d.ts +282 -0
  59. package/dist/private-local-computer-backend.js +402 -0
  60. package/dist/private-local-computer-processes.d.ts +199 -0
  61. package/dist/private-local-computer-processes.js +589 -0
  62. package/dist/private-local-default-model.d.ts +11 -0
  63. package/dist/private-local-default-model.js +45 -0
  64. package/dist/private-local-delegation.d.ts +29 -0
  65. package/dist/private-local-delegation.js +78 -0
  66. package/dist/private-local-effective-model.d.ts +19 -0
  67. package/dist/private-local-effective-model.js +14 -0
  68. package/dist/private-local-exec-launch.d.ts +41 -0
  69. package/dist/private-local-exec-launch.js +51 -0
  70. package/dist/private-local-exec-path.d.ts +6 -0
  71. package/dist/private-local-exec-path.js +40 -0
  72. package/dist/private-local-git-exclude.d.ts +2 -0
  73. package/dist/private-local-git-exclude.js +33 -0
  74. package/dist/private-local-http-dispatch.d.ts +5 -0
  75. package/dist/private-local-http-dispatch.js +14 -0
  76. package/dist/private-local-mail.d.ts +11 -0
  77. package/dist/private-local-mail.js +69 -0
  78. package/dist/private-local-model-settings.d.ts +5 -0
  79. package/dist/private-local-model-settings.js +31 -0
  80. package/dist/private-local-runtime-host.d.ts +141 -0
  81. package/dist/private-local-runtime-host.js +988 -0
  82. package/dist/private-local-service-contract.d.ts +90 -0
  83. package/dist/private-local-service-contract.js +1 -0
  84. package/dist/private-local-service-discovery.d.ts +101 -0
  85. package/dist/private-local-service-discovery.js +221 -0
  86. package/dist/private-local-service-ids.d.ts +3 -0
  87. package/dist/private-local-service-ids.js +8 -0
  88. package/dist/private-local-service-listener.d.ts +15 -0
  89. package/dist/private-local-service-listener.js +211 -0
  90. package/dist/private-local-service-owner.d.ts +1567 -0
  91. package/dist/private-local-service-owner.js +371 -0
  92. package/dist/private-local-service-process.d.ts +1 -0
  93. package/dist/private-local-service-process.js +6 -0
  94. package/dist/private-local-service.d.ts +17 -0
  95. package/dist/private-local-service.js +330 -0
  96. package/dist/private-local-session-discovery.d.ts +19 -0
  97. package/dist/private-local-session-discovery.js +157 -0
  98. package/dist/private-local-shiitake-host.d.ts +44 -0
  99. package/dist/private-local-shiitake-host.js +143 -0
  100. package/dist/private-local-subagents.d.ts +27 -0
  101. package/dist/private-local-subagents.js +77 -0
  102. package/dist/private-local-worker-server.d.ts +15 -0
  103. package/dist/private-local-worker-server.js +136 -0
  104. package/dist/private-sqlite-owner.d.ts +7 -0
  105. package/dist/private-sqlite-owner.js +103 -0
  106. package/dist/private-team-computers.d.ts +38 -0
  107. package/dist/private-team-computers.js +101 -0
  108. package/dist/private-workerd-sandbox.d.ts +3 -0
  109. package/dist/private-workerd-sandbox.js +347 -0
  110. package/dist/team-apps.d.ts +24 -0
  111. package/dist/team-apps.js +123 -0
  112. package/dist/tracker-git-credential.d.ts +15 -0
  113. package/dist/tracker-git-credential.js +87 -0
  114. package/native/bin/build.json +4037 -0
  115. package/native/bin/fungi-apply-patch +0 -0
  116. package/package.json +148 -3
@@ -0,0 +1,449 @@
1
+ import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, matchesGlob, normalize, relative, resolve, sep, } from "node:path";
3
+ import { FilesEditError } from "@fungi.computer/knapsack";
4
+ import { RE2JS } from "re2js";
5
+ import { createPrivateFilesPatch } from "./private-files-patch.js";
6
+ import writeFileAtomic from "write-file-atomic";
7
+ const maximumFileBytes = 128 * 1_024;
8
+ const maximumScanBytes = maximumFileBytes;
9
+ const maximumScanCandidates = 256;
10
+ const utf8 = new TextEncoder();
11
+ /** Safe Files path failures surfaced across the workerd binding boundary. */
12
+ export class FilesPathError extends Error {
13
+ reason;
14
+ code;
15
+ constructor(reason, requested) {
16
+ const description = {
17
+ "outside-root": "Files path resolves outside the invocation root",
18
+ "not-found": "Files path does not exist",
19
+ "not-file": "Files path is not a file",
20
+ }[reason];
21
+ super(`${description}: ${requested}`);
22
+ this.reason = reason;
23
+ this.name = "FilesPathError";
24
+ // Optional instruction and skill reads retain Node's missing-file signal.
25
+ this.code = reason === "not-found" ? "ENOENT" : undefined;
26
+ }
27
+ }
28
+ function missingPath(cause) {
29
+ return cause instanceof Error && "code" in cause && cause.code === "ENOENT";
30
+ }
31
+ async function existingRealpath(candidate, requested) {
32
+ try {
33
+ return await realpath(candidate);
34
+ }
35
+ catch (cause) {
36
+ if (missingPath(cause))
37
+ throw new FilesPathError("not-found", requested);
38
+ throw cause;
39
+ }
40
+ }
41
+ /** One maximum result page and one maximum read page bound hidden scan work. */
42
+ class ScanBudget {
43
+ bytes = 0;
44
+ candidates = 0;
45
+ exhausted = false;
46
+ admitCandidate() {
47
+ if (this.candidates === maximumScanCandidates) {
48
+ this.exhausted = true;
49
+ return false;
50
+ }
51
+ this.candidates += 1;
52
+ return true;
53
+ }
54
+ admitBytes(bytes) {
55
+ if (bytes > maximumScanBytes - this.bytes) {
56
+ this.exhausted = true;
57
+ return false;
58
+ }
59
+ this.bytes += bytes;
60
+ return true;
61
+ }
62
+ }
63
+ function checkCancellation(context) {
64
+ context.signal.throwIfAborted();
65
+ }
66
+ // Grep skips unsupported text; filesystem failures are reported separately.
67
+ class FilesTextError extends Error {
68
+ }
69
+ function decodeUtf8(data, path, moreBytesRemain = false) {
70
+ try {
71
+ const content = new TextDecoder("utf-8", {
72
+ fatal: true,
73
+ ignoreBOM: true,
74
+ }).decode(data, { stream: moreBytesRemain });
75
+ const bytes = utf8.encode(content).byteLength;
76
+ if (bytes === 0 && data.byteLength > 0) {
77
+ throw new Error(`Files read cannot advance a UTF-8 page: ${path}`);
78
+ }
79
+ return { content, bytes };
80
+ }
81
+ catch (cause) {
82
+ throw new FilesTextError(`Files path is not valid UTF-8: ${path}`, {
83
+ cause,
84
+ });
85
+ }
86
+ }
87
+ function entryType(metadata, path) {
88
+ if (metadata.isSymbolicLink())
89
+ return "symlink";
90
+ if (metadata.isFile())
91
+ return "file";
92
+ if (metadata.isDirectory())
93
+ return "directory";
94
+ throw new Error(`Files path has an unsupported filesystem type: ${path}`);
95
+ }
96
+ function outputPath(parent, child) {
97
+ if (isAbsolute(parent))
98
+ return resolve(parent, child);
99
+ return parent === "." ? child : normalize(join(parent, child));
100
+ }
101
+ function lineMatcher(pattern, regex, ignoreCase) {
102
+ if (regex) {
103
+ let compiled;
104
+ try {
105
+ compiled = RE2JS.compile(pattern, ignoreCase ? RE2JS.CASE_INSENSITIVE : 0);
106
+ }
107
+ catch (cause) {
108
+ throw new Error("Files grep pattern is not supported by RE2", { cause });
109
+ }
110
+ return (line) => compiled.test(line);
111
+ }
112
+ if (!ignoreCase)
113
+ return (line) => line.includes(pattern);
114
+ const foldedPattern = pattern.toLowerCase();
115
+ return (line) => line.toLowerCase().includes(foldedPattern);
116
+ }
117
+ function appendMatchingLines(matches, path, content, matchesLine, limit, context) {
118
+ const lines = content.split(/\r\n|\n|\r/u);
119
+ if (content.endsWith("\n") || content.endsWith("\r"))
120
+ lines.pop();
121
+ for (const [index, line] of lines.entries()) {
122
+ checkCancellation(context);
123
+ if (!matchesLine(line))
124
+ continue;
125
+ if (matches.length === limit)
126
+ return true;
127
+ matches.push({ path, line: index + 1, text: line });
128
+ }
129
+ return false;
130
+ }
131
+ async function openRegularFile(path, context) {
132
+ checkCancellation(context);
133
+ let handle;
134
+ try {
135
+ handle = await open(path.resolved, "r");
136
+ }
137
+ catch (cause) {
138
+ if (missingPath(cause))
139
+ throw new FilesPathError("not-found", path.requested);
140
+ throw cause;
141
+ }
142
+ try {
143
+ checkCancellation(context);
144
+ const metadata = await handle.stat();
145
+ if (!metadata.isFile())
146
+ throw new FilesPathError("not-file", path.requested);
147
+ return { handle, size: metadata.size };
148
+ }
149
+ catch (cause) {
150
+ await handle.close();
151
+ throw cause;
152
+ }
153
+ }
154
+ /**
155
+ * One canonical invocation root and every pathname decision made beneath it.
156
+ *
157
+ * This is same-user containment, not an OS sandbox against pathname races.
158
+ */
159
+ class InvocationRoot {
160
+ canonicalPath;
161
+ static async capture(path) {
162
+ return new InvocationRoot(await realpath(resolve(path)));
163
+ }
164
+ constructor(canonicalPath) {
165
+ this.canonicalPath = canonicalPath;
166
+ }
167
+ async existing(requested, context) {
168
+ checkCancellation(context);
169
+ const candidate = isAbsolute(requested)
170
+ ? resolve(requested)
171
+ : resolve(this.canonicalPath, requested);
172
+ this.assertContained(candidate, requested);
173
+ const resolvedPath = await existingRealpath(candidate, requested);
174
+ this.assertContained(resolvedPath, requested);
175
+ return { requested, resolved: resolvedPath };
176
+ }
177
+ async walked(candidate, requested, context) {
178
+ checkCancellation(context);
179
+ const resolvedPath = await existingRealpath(candidate, requested);
180
+ this.assertContained(resolvedPath, requested);
181
+ return resolvedPath;
182
+ }
183
+ assertContained(candidate, requested) {
184
+ const fromRoot = relative(this.canonicalPath, candidate);
185
+ if (fromRoot === "" ||
186
+ (!fromRoot.startsWith(`..${sep}`) &&
187
+ fromRoot !== ".." &&
188
+ !isAbsolute(fromRoot))) {
189
+ return;
190
+ }
191
+ throw new FilesPathError("outside-root", requested);
192
+ }
193
+ }
194
+ async function readPage(path, offset, limit, context) {
195
+ const file = await openRegularFile(path, context);
196
+ try {
197
+ if (offset > file.size)
198
+ throw new Error(`Files read offset exceeds file size: ${path.requested}`);
199
+ if (offset === file.size) {
200
+ return {
201
+ path: path.requested,
202
+ content: "",
203
+ offset,
204
+ nextOffset: null,
205
+ truncated: false,
206
+ };
207
+ }
208
+ const requestedBytes = Math.min(limit, file.size - offset);
209
+ const buffer = Buffer.alloc(requestedBytes);
210
+ let bytesRead = 0;
211
+ while (bytesRead < requestedBytes) {
212
+ checkCancellation(context);
213
+ const next = await file.handle.read(buffer, bytesRead, requestedBytes - bytesRead, offset + bytesRead);
214
+ if (next.bytesRead === 0)
215
+ throw new Error(`Files path changed during read: ${path.requested}`);
216
+ bytesRead += next.bytesRead;
217
+ }
218
+ const page = decodeUtf8(buffer, path.requested, offset + bytesRead < file.size);
219
+ const nextOffset = offset + page.bytes;
220
+ return {
221
+ path: path.requested,
222
+ content: page.content,
223
+ offset,
224
+ nextOffset: nextOffset < file.size ? nextOffset : null,
225
+ truncated: nextOffset < file.size,
226
+ };
227
+ }
228
+ finally {
229
+ await file.handle.close();
230
+ }
231
+ }
232
+ async function readBoundedText(path, context) {
233
+ const file = await openRegularFile(path, context);
234
+ try {
235
+ if (file.size > maximumFileBytes) {
236
+ throw new FilesTextError(`Files path exceeds the ${String(maximumFileBytes)}-byte bound: ${path.requested}`);
237
+ }
238
+ const buffer = Buffer.alloc(maximumFileBytes + 1);
239
+ let bytesRead = 0;
240
+ while (bytesRead < buffer.byteLength) {
241
+ checkCancellation(context);
242
+ const next = await file.handle.read(buffer, bytesRead, buffer.byteLength - bytesRead, bytesRead);
243
+ if (next.bytesRead === 0)
244
+ break;
245
+ bytesRead += next.bytesRead;
246
+ }
247
+ if (bytesRead > maximumFileBytes) {
248
+ throw new FilesTextError(`Files path exceeds the ${String(maximumFileBytes)}-byte bound: ${path.requested}`);
249
+ }
250
+ return decodeUtf8(buffer.subarray(0, bytesRead), path.requested).content;
251
+ }
252
+ finally {
253
+ await file.handle.close();
254
+ }
255
+ }
256
+ async function readGrepCandidate(path, context) {
257
+ try {
258
+ return await readBoundedText(path, context);
259
+ }
260
+ catch (cause) {
261
+ checkCancellation(context);
262
+ if (cause instanceof FilesTextError)
263
+ return undefined;
264
+ console.error("Local file search candidate unavailable", cause);
265
+ return undefined;
266
+ }
267
+ }
268
+ async function* walkFiles(root, base, budget, context) {
269
+ async function* visit(directory, relativeParent, ancestors) {
270
+ if (ancestors.has(directory))
271
+ return;
272
+ const childAncestors = new Set(ancestors).add(directory);
273
+ checkCancellation(context);
274
+ const handle = await opendir(directory);
275
+ for await (const entry of handle) {
276
+ checkCancellation(context);
277
+ if (!budget.admitCandidate())
278
+ return;
279
+ const relativePath = relativeParent === "" ? entry.name : join(relativeParent, entry.name);
280
+ const candidate = join(directory, entry.name);
281
+ const resolvedPath = await root.walked(candidate, outputPath(base.requested, relativePath), context);
282
+ checkCancellation(context);
283
+ const metadata = await stat(resolvedPath);
284
+ const walkedEntry = {
285
+ relativePath,
286
+ resolvedPath,
287
+ isDirectory: metadata.isDirectory(),
288
+ isFile: metadata.isFile(),
289
+ size: metadata.size,
290
+ };
291
+ yield walkedEntry;
292
+ if (walkedEntry.isDirectory) {
293
+ yield* visit(resolvedPath, relativePath, childAncestors);
294
+ if (budget.exhausted)
295
+ return;
296
+ }
297
+ }
298
+ }
299
+ yield* visit(base.resolved, "", new Set());
300
+ }
301
+ async function grepFile(target, input, matchesLine, context) {
302
+ const matches = [];
303
+ const content = await readBoundedText(target, context);
304
+ return {
305
+ path: input.path,
306
+ matches,
307
+ truncated: appendMatchingLines(matches, input.path, content, matchesLine, input.limit, context),
308
+ };
309
+ }
310
+ async function grepDirectory(root, target, input, matchesLine, context) {
311
+ const matches = [];
312
+ const budget = new ScanBudget();
313
+ let incomplete = false;
314
+ for await (const entry of walkFiles(root, target, budget, context)) {
315
+ if (entry.isDirectory)
316
+ continue;
317
+ if (!entry.isFile) {
318
+ incomplete = true;
319
+ continue;
320
+ }
321
+ const logicalPath = outputPath(input.path, entry.relativePath);
322
+ if (entry.size > maximumFileBytes) {
323
+ incomplete = true;
324
+ continue;
325
+ }
326
+ if (!budget.admitBytes(entry.size))
327
+ break;
328
+ const content = await readGrepCandidate({ requested: logicalPath, resolved: entry.resolvedPath }, context);
329
+ if (content === undefined) {
330
+ incomplete = true;
331
+ continue;
332
+ }
333
+ if (appendMatchingLines(matches, logicalPath, content, matchesLine, input.limit, context)) {
334
+ return { path: input.path, matches, truncated: true };
335
+ }
336
+ }
337
+ return {
338
+ path: input.path,
339
+ matches,
340
+ truncated: incomplete || budget.exhausted,
341
+ };
342
+ }
343
+ async function grepPath(root, input, context) {
344
+ const target = await root.existing(input.path, context);
345
+ checkCancellation(context);
346
+ const targetMetadata = await stat(target.resolved);
347
+ const matchesLine = lineMatcher(input.pattern, input.regex, input.ignoreCase);
348
+ if (targetMetadata.isFile())
349
+ return await grepFile(target, input, matchesLine, context);
350
+ if (targetMetadata.isDirectory())
351
+ return await grepDirectory(root, target, input, matchesLine, context);
352
+ throw new Error(`Files path is not a file or directory: ${input.path}`);
353
+ }
354
+ function createWriteQueue() {
355
+ let tail = Promise.resolve();
356
+ return async (operation) => {
357
+ const previous = tail;
358
+ const gate = Promise.withResolvers();
359
+ tail = previous.then(() => gate.promise);
360
+ await previous;
361
+ try {
362
+ return await operation();
363
+ }
364
+ finally {
365
+ gate.resolve(undefined);
366
+ }
367
+ };
368
+ }
369
+ /**
370
+ * Construct the private local Files port for one captured invocation root.
371
+ *
372
+ * Resolved paths stay under the captured canonical root, including admitted
373
+ * in-root symlinks. This is trusted same-user containment, not protection from
374
+ * a same-user process racing pathname resolution.
375
+ */
376
+ export async function createPrivateInvocationFilesPort(path) {
377
+ const root = await InvocationRoot.capture(path);
378
+ const write = createWriteQueue();
379
+ const port = {
380
+ async read(input, context) {
381
+ return await readPage(await root.existing(input.path, context), input.offset, input.limit, context);
382
+ },
383
+ async list(input, context) {
384
+ const directory = await root.existing(input.path, context);
385
+ checkCancellation(context);
386
+ const handle = await opendir(directory.resolved);
387
+ const entries = [];
388
+ for await (const entry of handle) {
389
+ checkCancellation(context);
390
+ if (entries.length === input.limit) {
391
+ return { path: input.path, entries, truncated: true };
392
+ }
393
+ const metadata = await lstat(join(directory.resolved, entry.name));
394
+ const path = outputPath(input.path, entry.name);
395
+ entries.push({
396
+ path,
397
+ name: entry.name,
398
+ type: entryType(metadata, path),
399
+ size: metadata.size,
400
+ modifiedAt: metadata.mtimeMs,
401
+ });
402
+ }
403
+ return { path: input.path, entries, truncated: false };
404
+ },
405
+ async glob(input, context) {
406
+ const directory = await root.existing(input.path, context);
407
+ const matches = [];
408
+ const budget = new ScanBudget();
409
+ for await (const entry of walkFiles(root, directory, budget, context)) {
410
+ checkCancellation(context);
411
+ if (!matchesGlob(entry.relativePath, input.pattern))
412
+ continue;
413
+ if (matches.length === input.limit) {
414
+ return { path: input.path, matches, truncated: true };
415
+ }
416
+ matches.push(outputPath(input.path, entry.relativePath));
417
+ }
418
+ return { path: input.path, matches, truncated: budget.exhausted };
419
+ },
420
+ async grep(input, context) {
421
+ return await grepPath(root, input, context);
422
+ },
423
+ async replace(input, context) {
424
+ return await write(async () => {
425
+ checkCancellation(context);
426
+ const path = await root.existing(input.path, context);
427
+ const current = await readBoundedText(path, context);
428
+ if (current !== input.expectedContent)
429
+ throw new FilesEditError("conflict", input.path, "File changed since it was read; read it again before editing");
430
+ const bytes = utf8.encode(input.content).byteLength;
431
+ if (bytes > maximumFileBytes) {
432
+ throw new FilesEditError("file-too-large", input.path, `Result exceeds ${String(maximumFileBytes)} bytes; use files.patch for a small change`);
433
+ }
434
+ checkCancellation(context);
435
+ await writeFileAtomic(path.resolved, input.content, {
436
+ encoding: "utf8",
437
+ });
438
+ return { path: input.path, bytes };
439
+ });
440
+ },
441
+ };
442
+ const patch = await createPrivateFilesPatch(root.canonicalPath, port.read);
443
+ if (patch === undefined)
444
+ return Object.freeze(port);
445
+ return Object.freeze({
446
+ ...port,
447
+ patch: (input, context) => write(() => patch(input, context)),
448
+ });
449
+ }
@@ -0,0 +1,17 @@
1
+ import * as acp from "@agentclientprotocol/sdk";
2
+ import type { ShiitakeEventSourceConstructor } from "@fungi.computer/shiitake/client";
3
+ import { type PrivateLocalServiceManager } from "./private-local-service.js";
4
+ export type LocalAcpStdioOptions = Readonly<{
5
+ serviceDirectory?: string;
6
+ invocationRoot?: string;
7
+ databasePath?: string;
8
+ eventSource?: ShiitakeEventSourceConstructor;
9
+ manager?: PrivateLocalServiceManager;
10
+ input?: ReadableStream<Uint8Array>;
11
+ output?: WritableStream<Uint8Array>;
12
+ name?: string;
13
+ }>;
14
+ /** Build the ACP agent half over the persistent Local service's Session client. */
15
+ export declare function createPrivateLocalAcpAgent(options?: LocalAcpStdioOptions): acp.AgentApp;
16
+ /** Serve one ACP stable-v1 stdio connection over the persistent Local service. */
17
+ export declare function runPrivateLocalAcpStdio(options?: LocalAcpStdioOptions): Promise<void>;
@@ -0,0 +1,142 @@
1
+ import * as acp from "@agentclientprotocol/sdk";
2
+ import * as Schema from "effect/Schema";
3
+ import { createShiitakeAcpAgent, } from "@fungi.computer/graft";
4
+ import { createPrivateLocalServiceManager, } from "./private-local-service.js";
5
+ import { createPrivateLocalSessionRequestId } from "./private-local-runtime-host.js";
6
+ function stdinByteStream() {
7
+ const input = process.stdin[Symbol.asyncIterator]();
8
+ const encoder = new TextEncoder();
9
+ const chunkSchema = Schema.Union([Schema.Uint8Array, Schema.String]);
10
+ return new ReadableStream({
11
+ async pull(controller) {
12
+ try {
13
+ const next = await input.next();
14
+ if (next.done) {
15
+ controller.close();
16
+ }
17
+ else {
18
+ const chunk = Schema.decodeUnknownSync(chunkSchema)(next.value);
19
+ controller.enqueue(Schema.is(Schema.Uint8Array)(chunk) ? chunk : encoder.encode(chunk));
20
+ }
21
+ }
22
+ catch (cause) {
23
+ controller.error(cause);
24
+ }
25
+ },
26
+ async cancel() {
27
+ await input.return?.();
28
+ },
29
+ });
30
+ }
31
+ function stdoutByteStream() {
32
+ return new WritableStream({
33
+ write(chunk) {
34
+ return new Promise((resolve, reject) => {
35
+ process.stdout.write(chunk, (error) => {
36
+ if (error)
37
+ reject(error);
38
+ else
39
+ resolve();
40
+ });
41
+ });
42
+ },
43
+ close() {
44
+ process.stdout.end();
45
+ },
46
+ abort(reason) {
47
+ process.stdout.destroy(reason instanceof Error
48
+ ? reason
49
+ : new Error("ACP stdout was aborted", { cause: reason }));
50
+ },
51
+ });
52
+ }
53
+ function assertLocalInputs(additionalDirectories, mcpServers) {
54
+ if (additionalDirectories.length > 0)
55
+ throw new Error("Local ACP does not support ACP additionalDirectories yet");
56
+ if (mcpServers.length > 0)
57
+ throw new Error("Local ACP does not support ACP MCP servers yet");
58
+ }
59
+ function binding(connection) {
60
+ return {
61
+ session: connection.session,
62
+ sessionId: connection.sessionId,
63
+ close: connection.close,
64
+ };
65
+ }
66
+ function localDirectory(manager, databasePath, invocationRoot) {
67
+ return {
68
+ async list(input) {
69
+ if (input.cursor !== undefined)
70
+ throw acp.RequestError.invalidParams(undefined, "Local discovery has no next page");
71
+ const cwd = input.cwd ?? invocationRoot;
72
+ const discovery = await manager.discover(cwd, { signal: input.signal });
73
+ if (discovery.problems.length > 0)
74
+ throw new Error("Local Session discovery contains unreadable stores");
75
+ return {
76
+ sessions: discovery.sessions.map((session) => ({
77
+ sessionId: session.sessionId,
78
+ cwd,
79
+ title: session.title,
80
+ updatedAt: new Date(session.updatedAt).toISOString(),
81
+ })),
82
+ };
83
+ },
84
+ async create(input) {
85
+ assertLocalInputs(input.additionalDirectories, input.mcpServers);
86
+ const selected = await manager.connect({
87
+ invocationRoot: input.cwd,
88
+ databasePath,
89
+ }, { signal: input.signal });
90
+ try {
91
+ const created = await manager.createSession({
92
+ invocationRoot: input.cwd,
93
+ databasePath: selected.databasePath,
94
+ requestId: createPrivateLocalSessionRequestId(),
95
+ }, { signal: input.signal });
96
+ return binding(created);
97
+ }
98
+ finally {
99
+ await selected.close();
100
+ }
101
+ },
102
+ async load(input) {
103
+ assertLocalInputs(input.additionalDirectories, input.mcpServers);
104
+ const connected = await manager.connect({
105
+ invocationRoot: input.cwd,
106
+ databasePath,
107
+ sessionId: input.sessionId,
108
+ }, { signal: input.signal });
109
+ return binding(connected);
110
+ },
111
+ };
112
+ }
113
+ /** Build the ACP agent half over the persistent Local service's Session client. */
114
+ export function createPrivateLocalAcpAgent(options = {}) {
115
+ const manager = options.manager ??
116
+ createPrivateLocalServiceManager(localManagerOptions(options));
117
+ return createShiitakeAcpAgent({
118
+ directory: localDirectory(manager, options.databasePath, options.invocationRoot ?? process.cwd()),
119
+ terminalAuth: {
120
+ id: "fungi-login",
121
+ name: "Sign in to Fungi",
122
+ args: ["login"],
123
+ },
124
+ name: options.name ?? "Local Shiitake",
125
+ });
126
+ }
127
+ function localManagerOptions(options) {
128
+ const result = {};
129
+ if (options.serviceDirectory !== undefined)
130
+ result.directory = options.serviceDirectory;
131
+ if (options.eventSource !== undefined)
132
+ result.eventSource = options.eventSource;
133
+ return result;
134
+ }
135
+ /** Serve one ACP stable-v1 stdio connection over the persistent Local service. */
136
+ export async function runPrivateLocalAcpStdio(options = {}) {
137
+ const app = createPrivateLocalAcpAgent(options);
138
+ const input = options.input ?? stdinByteStream();
139
+ const output = options.output ?? stdoutByteStream();
140
+ const connection = app.connect(acp.ndJsonStream(output, input));
141
+ await connection.closed;
142
+ }
@@ -0,0 +1,69 @@
1
+ import type { AuthInteraction, AuthEvent } from "@earendil-works/pi-ai";
2
+ import { RpcTarget, type RpcStub } from "@fungi.computer/capnweb";
3
+ import { z } from "zod";
4
+ import { runLocalAuthCli, type LocalAuthResult } from "./auth-cli.js";
5
+ declare const requestSchema: z.ZodObject<{
6
+ command: z.ZodEnum<{
7
+ status: "status";
8
+ login: "login";
9
+ logout: "logout";
10
+ }>;
11
+ provider: z.ZodOptional<z.ZodString>;
12
+ authType: z.ZodOptional<z.ZodEnum<{
13
+ api_key: "api_key";
14
+ oauth: "oauth";
15
+ }>>;
16
+ method: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$strip>;
18
+ export type PrivateLocalAuthRequest = z.infer<typeof requestSchema>;
19
+ declare const promptSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
20
+ type: z.ZodLiteral<"text">;
21
+ message: z.ZodString;
22
+ placeholder: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strip>, z.ZodObject<{
24
+ type: z.ZodLiteral<"secret">;
25
+ message: z.ZodString;
26
+ placeholder: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>, z.ZodObject<{
28
+ type: z.ZodLiteral<"manual_code">;
29
+ message: z.ZodString;
30
+ placeholder: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>, z.ZodObject<{
32
+ type: z.ZodLiteral<"select">;
33
+ message: z.ZodString;
34
+ options: z.ZodArray<z.ZodObject<{
35
+ id: z.ZodString;
36
+ label: z.ZodString;
37
+ description: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strip>>;
39
+ }, z.core.$strip>], "type">;
40
+ export type AuthPromptRequest = z.infer<typeof promptSchema>;
41
+ declare class AuthPromptTarget extends RpcTarget {
42
+ #private;
43
+ constructor(prompt: AuthPromptRequest, interaction: AuthInteraction);
44
+ result(): Promise<string>;
45
+ cancel(): void;
46
+ [Symbol.dispose](): void;
47
+ }
48
+ declare class AuthPresenterTarget extends RpcTarget {
49
+ #private;
50
+ constructor(interaction: AuthInteraction);
51
+ openPrompt(request: AuthPromptRequest): AuthPromptTarget;
52
+ notify(event: AuthEvent): void;
53
+ }
54
+ /** The auth-only RPC door shared by its producer and remote client. */
55
+ export type PrivateLocalAuthEndpoint = RpcTarget & {
56
+ cancel(): void;
57
+ run(input: PrivateLocalAuthRequest, presenter: RpcStub<AuthPresenterTarget>): Promise<LocalAuthResult>;
58
+ };
59
+ /** One transient connection runs one operation through the existing Pi owner. */
60
+ export declare class PrivateLocalAuthTarget extends RpcTarget implements PrivateLocalAuthEndpoint {
61
+ #private;
62
+ constructor(run?: typeof runLocalAuthCli);
63
+ cancel(): void;
64
+ [Symbol.dispose](): void;
65
+ run(input: PrivateLocalAuthRequest, presenter: RpcStub<AuthPresenterTarget>): Promise<LocalAuthResult>;
66
+ }
67
+ /** Takes ownership of a dedicated auth RPC connection, never the Session client. */
68
+ export declare function runPrivateLocalAuthRemote(remote: RpcStub<PrivateLocalAuthEndpoint>, request: PrivateLocalAuthRequest, interaction: AuthInteraction): Promise<LocalAuthResult>;
69
+ export type { AuthPresenterTarget, AuthPromptTarget };