@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.
- package/dist/local-board-agents.d.ts +46 -0
- package/dist/local-board-agents.js +138 -0
- package/dist/local-board-shipping.d.ts +129 -0
- package/dist/local-board-shipping.js +325 -0
- package/dist/local-extensions.d.ts +2 -0
- package/dist/local-extensions.js +121 -36
- package/dist/local-login.js +33 -19
- package/dist/local-upgrade.d.ts +63 -0
- package/dist/local-upgrade.js +358 -0
- package/dist/private-local-agent-control.d.ts +8 -1
- package/dist/private-local-agent-control.js +16 -2
- package/dist/private-local-auth-rpc.d.ts +5 -0
- package/dist/private-local-auth-rpc.js +46 -4
- package/dist/private-local-client-version.js +3 -1
- package/dist/private-local-delegation.d.ts +18 -0
- package/dist/private-local-delegation.js +82 -21
- package/dist/private-local-exec-review.js +45 -8
- package/dist/private-local-extension-archive-notice.d.ts +8 -0
- package/dist/private-local-extension-archive-notice.js +108 -0
- package/dist/private-local-extension-records.d.ts +25 -0
- package/dist/private-local-extension-records.js +51 -0
- package/dist/private-local-process-identity.d.ts +14 -0
- package/dist/private-local-process-identity.js +51 -0
- package/dist/private-local-runtime-host.d.ts +7 -1
- package/dist/private-local-runtime-host.js +88 -31
- package/dist/private-local-service-contract.d.ts +1 -0
- package/dist/private-local-service-discovery.js +2 -3
- package/dist/private-local-service-owner.js +80 -9
- package/dist/private-local-service-termination.js +12 -11
- package/dist/private-local-service.js +19 -1
- package/dist/private-local-session-discovery.d.ts +16 -4
- package/dist/private-local-session-discovery.js +68 -4
- package/dist/private-mcp-form-contract.d.ts +13 -0
- package/dist/private-mcp-form-contract.js +6 -1
- package/dist/private-mcp-form-session.d.ts +26 -0
- package/dist/private-mcp-form-session.js +8 -5
- package/dist/private-provider-rejection.d.ts +1 -1
- package/dist/private-provider-rejection.js +28 -8
- package/dist/private-sqlite-owner.d.ts +7 -1
- package/dist/private-sqlite-owner.js +14 -2
- package/package.json +32 -16
package/dist/local-extensions.js
CHANGED
|
@@ -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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
},
|
package/dist/local-login.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 {};
|