@fungi.computer/truffle 0.3.1 → 0.5.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 (41) hide show
  1. package/dist/local-board-agents.d.ts +46 -0
  2. package/dist/local-board-agents.js +138 -0
  3. package/dist/local-board-shipping.d.ts +129 -0
  4. package/dist/local-board-shipping.js +325 -0
  5. package/dist/local-extensions.d.ts +2 -0
  6. package/dist/local-extensions.js +121 -36
  7. package/dist/local-login.js +33 -19
  8. package/dist/local-upgrade.d.ts +63 -0
  9. package/dist/local-upgrade.js +358 -0
  10. package/dist/private-local-agent-control.d.ts +8 -1
  11. package/dist/private-local-agent-control.js +16 -2
  12. package/dist/private-local-auth-rpc.d.ts +5 -0
  13. package/dist/private-local-auth-rpc.js +46 -4
  14. package/dist/private-local-client-version.js +3 -1
  15. package/dist/private-local-delegation.d.ts +18 -0
  16. package/dist/private-local-delegation.js +82 -21
  17. package/dist/private-local-exec-review.js +45 -8
  18. package/dist/private-local-extension-archive-notice.d.ts +8 -0
  19. package/dist/private-local-extension-archive-notice.js +108 -0
  20. package/dist/private-local-extension-records.d.ts +25 -0
  21. package/dist/private-local-extension-records.js +51 -0
  22. package/dist/private-local-process-identity.d.ts +14 -0
  23. package/dist/private-local-process-identity.js +51 -0
  24. package/dist/private-local-runtime-host.d.ts +7 -1
  25. package/dist/private-local-runtime-host.js +88 -31
  26. package/dist/private-local-service-contract.d.ts +1 -0
  27. package/dist/private-local-service-discovery.js +2 -3
  28. package/dist/private-local-service-owner.js +80 -9
  29. package/dist/private-local-service-termination.js +12 -11
  30. package/dist/private-local-service.js +19 -1
  31. package/dist/private-local-session-discovery.d.ts +16 -4
  32. package/dist/private-local-session-discovery.js +68 -4
  33. package/dist/private-mcp-form-contract.d.ts +13 -0
  34. package/dist/private-mcp-form-contract.js +6 -1
  35. package/dist/private-mcp-form-session.d.ts +26 -0
  36. package/dist/private-mcp-form-session.js +8 -5
  37. package/dist/private-provider-rejection.d.ts +1 -1
  38. package/dist/private-provider-rejection.js +28 -8
  39. package/dist/private-sqlite-owner.d.ts +7 -1
  40. package/dist/private-sqlite-owner.js +14 -2
  41. package/package.json +32 -16
@@ -1,15 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, constants, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
2
+ import { access, constants, mkdir, mkdtemp, open, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
3
3
  import { join, resolve, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { Mycelium } from "@fungi.computer/mycelium";
6
6
  import { defineExtension, ExtensionId, } from "@fungi.computer/shiitake/extensions";
7
7
  import * as Predicate from "effect/Predicate";
8
8
  import * as Schema from "effect/Schema";
9
- import lockfile from "proper-lockfile";
10
9
  import writeFileAtomic from "write-file-atomic";
11
10
  import { z } from "zod";
12
11
  import { describeExtensionSkills, } from "./local-skill-source.js";
12
+ import { activationsSchema, capabilities, } from "./private-local-extension-records.js";
13
+ import { acquireExtensionOwnerLock, extensionArchiveNoticesAreDeferred, extensionArchiveNameSchema, takePendingExtensionArchiveNoticesUnderOwnerLock, writeExtensionArchiveNotice, } from "./private-local-extension-archive-notice.js";
13
14
  /**
14
15
  * Installed Local extensions. One owner holds the package bytes under
15
16
  * `~/.fungi/extensions`, and the activations its typed operations write. A
@@ -23,19 +24,6 @@ import { describeExtensionSkills, } from "./local-skill-source.js";
23
24
  * them. This covers Local installed extensions only; Hub-hosted extensions
24
25
  * are not covered.
25
26
  */
26
- const capabilities = [
27
- "state",
28
- "attention",
29
- "settled",
30
- "send",
31
- "timers",
32
- "commands",
33
- "projection",
34
- "run_tools",
35
- "files",
36
- "prompt",
37
- "compaction_handoff",
38
- ];
39
27
  const decodeExtensionId = Schema.decodeUnknownSync(ExtensionId);
40
28
  const identifier = z.string().regex(/^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$/u);
41
29
  const revisionPattern = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/u;
@@ -49,6 +37,11 @@ const manifestSchema = z.strictObject({
49
37
  id: identifier.max(64),
50
38
  name: z.string().trim().min(1).max(64),
51
39
  description: z.string().max(200).default(""),
40
+ /** A shell key that opens this program beside the focused pane. */
41
+ binding: z
42
+ .string()
43
+ .regex(/^<leader>[a-z]$/u)
44
+ .optional(),
52
45
  /** Package-relative executable and its arguments. */
53
46
  command: z
54
47
  .array(z.string().min(1).max(1_024))
@@ -72,6 +65,7 @@ const terminalRecord = z.strictObject({
72
65
  id: z.string(),
73
66
  name: z.string(),
74
67
  description: z.string(),
68
+ binding: z.string().optional(),
75
69
  command: z.array(z.string()),
76
70
  });
77
71
  const revisionRecord = z.strictObject({
@@ -93,14 +87,13 @@ const installedSchema = z.strictObject({
93
87
  revisions: z.record(z.string(), revisionRecord),
94
88
  })),
95
89
  });
96
- const activationsSchema = z.strictObject({
97
- projects: z.record(z.string(), z.record(z.string(), z.string())),
98
- agents: z.record(z.string(), z.record(z.string(), z.strictObject({
99
- revision: z.string(),
100
- capabilities: z.array(z.enum(capabilities)),
101
- /** The opt-in to the revision's skills folder; separate from tools. */
102
- skills: z.boolean(),
103
- }))),
90
+ const extensionNameNoticeSchema = z.object({
91
+ extensions: z.record(z.string(), z.object({
92
+ current: z.string(),
93
+ revisions: z.record(z.string(), z.object({
94
+ name: z.unknown().optional(),
95
+ })),
96
+ })),
104
97
  });
105
98
  const lockSchema = z.looseObject({
106
99
  packages: z.record(z.string(), z.array(z.unknown()).min(1)),
@@ -169,20 +162,108 @@ export function localExtensions(options) {
169
162
  throw new LocalExtensionError("install", `${path} is from an earlier extension format; ${remedy}`, { cause: parsed.error });
170
163
  return parsed.data;
171
164
  }
172
- const readInstalled = () => readJson(installedPath, installedSchema, { extensions: {} }, "delete it and run `fungi extension add` again");
165
+ function extensionNamesForNotice(text) {
166
+ let value;
167
+ try {
168
+ value = JSON.parse(text);
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ const parsed = extensionNameNoticeSchema.safeParse(value);
174
+ if (!parsed.success)
175
+ return [];
176
+ return [
177
+ ...new Set(Object.entries(parsed.data.extensions).flatMap(([id, extension]) => {
178
+ if (!Object.hasOwn(extension.revisions, extension.current))
179
+ return [];
180
+ const revision = extension.revisions[extension.current];
181
+ if (revision === undefined)
182
+ return [];
183
+ const revisionName = extensionArchiveNameSchema.safeParse(revision.name);
184
+ const idName = extensionArchiveNameSchema.safeParse(id);
185
+ const name = revisionName.success ? revisionName : idName;
186
+ return name.success ? [name.data] : [];
187
+ })),
188
+ ];
189
+ }
190
+ async function archiveInstalled(text) {
191
+ const names = extensionNamesForNotice(text);
192
+ const archiveRoot = join(resolve(options.home), ".fungi", "archive");
193
+ await mkdir(archiveRoot, { recursive: true, mode: 0o700 });
194
+ for (let suffix = 1; suffix <= 9; suffix += 1) {
195
+ const slug = "unversioned";
196
+ const directory = join(archiveRoot, slug, "extensions");
197
+ const filename = suffix === 1 ? "installed.json" : `installed-${suffix.toString()}.json`;
198
+ const destination = join(directory, filename);
199
+ await mkdir(directory, { recursive: true, mode: 0o700 });
200
+ const reservation = await open(destination, "wx", 0o600).catch((cause) => {
201
+ if (cause instanceof Error &&
202
+ "code" in cause &&
203
+ cause.code === "EEXIST")
204
+ return undefined;
205
+ throw cause;
206
+ });
207
+ if (reservation === undefined)
208
+ continue;
209
+ try {
210
+ await rename(installedPath, destination);
211
+ }
212
+ catch (cause) {
213
+ if (absent(cause))
214
+ return { extensions: {} };
215
+ throw new LocalExtensionError("install", `Could not archive ${installedPath} at ${destination}`, { cause });
216
+ }
217
+ finally {
218
+ await reservation.close();
219
+ }
220
+ await writeExtensionArchiveNotice(options.home, slug, filename, names);
221
+ if (!extensionArchiveNoticesAreDeferred(options.home)) {
222
+ const pending = await takePendingExtensionArchiveNoticesUnderOwnerLock(options.home);
223
+ for (const message of pending)
224
+ process.stderr.write(`${message}\n`);
225
+ }
226
+ return { extensions: {} };
227
+ }
228
+ throw new LocalExtensionError("install", `Could not archive ${installedPath}: every unversioned archive destination already exists`);
229
+ }
230
+ async function readInstalled(underOwnerLock = false) {
231
+ let text;
232
+ try {
233
+ text = await readFile(installedPath, "utf8");
234
+ }
235
+ catch (cause) {
236
+ if (absent(cause))
237
+ return { extensions: {} };
238
+ throw cause;
239
+ }
240
+ let value;
241
+ try {
242
+ value = JSON.parse(text);
243
+ }
244
+ catch {
245
+ value = undefined;
246
+ }
247
+ const parsed = installedSchema.safeParse(value);
248
+ if (parsed.success)
249
+ return parsed.data;
250
+ if (!underOwnerLock) {
251
+ const release = await acquireExtensionOwnerLock(root);
252
+ try {
253
+ return await readInstalled(true);
254
+ }
255
+ finally {
256
+ await release();
257
+ }
258
+ }
259
+ return archiveInstalled(text);
260
+ }
173
261
  const readActivations = () => readJson(activationsPath, activationsSchema, { projects: {}, agents: {} }, "delete it, then re-enable your extensions");
174
262
  /** Serialize owner writes across CLI and service processes. */
175
263
  async function update(change) {
176
- await mkdir(root, { recursive: true, mode: 0o700 });
177
- const release = await lockfile.lock(root, {
178
- realpath: false,
179
- lockfilePath: join(root, ".owner.lock"),
180
- stale: 30_000,
181
- update: 10_000,
182
- retries: { retries: 20, factor: 1.2, minTimeout: 25, maxTimeout: 100 },
183
- });
264
+ const release = await acquireExtensionOwnerLock(root);
184
265
  try {
185
- const installed = await readInstalled();
266
+ const installed = await readInstalled(true);
186
267
  const activations = await readActivations();
187
268
  const result = change(installed, activations);
188
269
  const options = { encoding: "utf8", fsync: true, mode: 0o600 };
@@ -539,8 +620,8 @@ export function localExtensions(options) {
539
620
  if (record.client !== null)
540
621
  clients.push(record.client);
541
622
  const runtime = await runtimeDirectory(project, id);
542
- for (const terminal of record.terminals)
543
- terminals.push({
623
+ for (const terminal of record.terminals) {
624
+ const launcher = {
544
625
  extensionId: id,
545
626
  terminalId: terminal.id,
546
627
  name: terminal.name,
@@ -555,7 +636,11 @@ export function localExtensions(options) {
555
636
  FUNGI_PROJECT_ROOT: project,
556
637
  FUNGI_EXTENSION_RUNTIME_DIR: runtime,
557
638
  },
558
- });
639
+ };
640
+ terminals.push(terminal.binding === undefined
641
+ ? launcher
642
+ : { ...launcher, binding: terminal.binding });
643
+ }
559
644
  }
560
645
  return { clients, terminals, diagnostics };
561
646
  },
@@ -1,3 +1,4 @@
1
+ import { interactionTitleLimit } from "@fungi.computer/whistle/wire";
1
2
  /** Display order for popular model providers; labels remain owned by Pi. */
2
3
  export const localPopularProviderIds = Object.freeze([
3
4
  "fungi",
@@ -54,13 +55,32 @@ function notice(event) {
54
55
  return { type: "progress", message: event.message };
55
56
  }
56
57
  }
58
+ /**
59
+ * Pi's prompt text is provider wording, not a modal title: Whistle titles are
60
+ * at most `interactionTitleLimit` characters, and Pi 1.3's Anthropic paste
61
+ * prompt is longer. A message that fits stays the title; a longer one keeps
62
+ * every word as the summary under Fungi's fixed title for that prompt kind,
63
+ * so a provider sentence can never make the dialog refuse to open.
64
+ */
65
+ function modalText(message, fallbackTitle, hint) {
66
+ const fits = message.trim() !== "" && message.length <= interactionTitleLimit;
67
+ const lines = [
68
+ ...(fits ? [] : [message]),
69
+ ...(hint === undefined ? [] : [hint]),
70
+ ];
71
+ const title = fits ? message : fallbackTitle;
72
+ return lines.length === 0
73
+ ? { message: title }
74
+ : { message: title, summary: lines.join(" ") };
75
+ }
76
+ const placeholderOf = (placeholder) => placeholder === undefined ? {} : { placeholder };
57
77
  async function ask(interaction, prompt) {
58
78
  const options = prompt.signal === undefined ? {} : { signal: prompt.signal };
59
79
  switch (prompt.type) {
60
80
  case "select":
61
81
  return interaction.prompt({
62
82
  type: "choice",
63
- message: prompt.message,
83
+ ...modalText(prompt.message, "Choose a sign-in option"),
64
84
  options: prompt.options.map((option) => option.description === undefined
65
85
  ? { value: option.id, label: option.label }
66
86
  : {
@@ -69,30 +89,24 @@ async function ask(interaction, prompt) {
69
89
  description: option.description,
70
90
  }),
71
91
  }, options);
72
- case "manual_code": {
92
+ case "manual_code":
73
93
  // Generic across every provider that falls back to a pasted code or
74
94
  // redirect URL (OpenAI, Anthropic): the provider's own
75
95
  // message explains what to paste; this adds the SSH hint once, here,
76
96
  // instead of baking it into one provider's registry label.
77
- const message = `${prompt.message} If you're connecting over SSH, paste the URL here instead of opening a browser.`;
78
- return interaction.prompt(prompt.placeholder === undefined
79
- ? { type: "text", message }
80
- : { type: "text", message, placeholder: prompt.placeholder }, options);
81
- }
97
+ return interaction.prompt({
98
+ type: "text",
99
+ ...modalText(prompt.message, "Paste the sign-in code or redirect URL", "Connecting over SSH? Paste the URL here instead of opening a browser."),
100
+ ...placeholderOf(prompt.placeholder),
101
+ }, options);
82
102
  case "secret":
83
103
  case "text":
84
- return interaction.prompt(prompt.placeholder === undefined
85
- ? {
86
- type: "text",
87
- message: prompt.message,
88
- secret: prompt.type === "secret",
89
- }
90
- : {
91
- type: "text",
92
- message: prompt.message,
93
- placeholder: prompt.placeholder,
94
- secret: prompt.type === "secret",
95
- }, options);
104
+ return interaction.prompt({
105
+ type: "text",
106
+ ...modalText(prompt.message, prompt.type === "secret" ? "Enter your key" : "Enter a value"),
107
+ ...placeholderOf(prompt.placeholder),
108
+ secret: prompt.type === "secret",
109
+ }, options);
96
110
  }
97
111
  }
98
112
  const methodNotOffered = "The provider did not offer that sign-in method";
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import type { PrivateLocalServiceStopResult } from "./private-local-service-contract.js";
3
+ declare const versionMarkerSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<1>;
5
+ release: z.ZodString;
6
+ spore: z.ZodString;
7
+ build: z.ZodUnion<readonly [z.ZodLiteral<"source">, z.ZodString]>;
8
+ }, z.core.$strict>;
9
+ export type PriorInstall = Readonly<{
10
+ kind: "recorded";
11
+ release: string;
12
+ spore: string;
13
+ }> | Readonly<{
14
+ kind: "protocol";
15
+ protocol: number;
16
+ build: string;
17
+ }> | Readonly<{
18
+ kind: "unrecorded";
19
+ }>;
20
+ /** Version marker is a label, not permission to accept an older store schema. */
21
+ export declare function readPriorInstall(home: string): Promise<PriorInstall>;
22
+ /** Only the completed upgrade pass calls this writer; never write it early. */
23
+ export declare function writeLocalVersionMarker(home: string, marker: Omit<z.input<typeof versionMarkerSchema>, "version">): Promise<void>;
24
+ export type LocalUpgradeOptions = Readonly<{
25
+ home?: string;
26
+ directory?: string;
27
+ signal?: AbortSignal;
28
+ /** The package's generated, changelog-checked asset beside its CLI. */
29
+ newsPath: string;
30
+ spore: string;
31
+ /** Tests may supply the same bounded stop contract as the real manager. */
32
+ stopService?: (expectedServiceId?: string) => Promise<PrivateLocalServiceStopResult>;
33
+ /** Override only for an isolated proof of the installed-client identity. */
34
+ installedClientBuildId?: () => Promise<string | undefined>;
35
+ }>;
36
+ /** Invalid news lines are optional; a missing release label is not safe to mark current. */
37
+ export declare function readLocalUpgradeNews(path: string): Promise<{
38
+ release: string;
39
+ lines: string[];
40
+ }>;
41
+ export type ArchivedArtifact = Readonly<{
42
+ kind: "agent-store" | "extension-activations";
43
+ from: string;
44
+ to: string;
45
+ }>;
46
+ export type LocalUpgradeReport = Readonly<{
47
+ from: PriorInstall;
48
+ to: string;
49
+ service: "stopped" | "killed" | "absent";
50
+ archive: string | undefined;
51
+ archived: readonly ArchivedArtifact[];
52
+ whatsNew: readonly string[];
53
+ }>;
54
+ export type LocalUpgradeOutcome = Readonly<{
55
+ kind: "current";
56
+ }> | Readonly<{
57
+ kind: "upgraded";
58
+ report: LocalUpgradeReport;
59
+ }>;
60
+ /** Upgrade → service startup → extensions owner: this lock is always first. */
61
+ export declare function upgradeLocalHome(options: LocalUpgradeOptions): Promise<LocalUpgradeOutcome>;
62
+ export declare function formatLocalUpgradeNotice(report: LocalUpgradeReport): readonly string[];
63
+ export {};