@fungi.computer/agar 0.0.0-stage → 0.0.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.
- package/LICENSE +21 -0
- package/README.md +766 -2
- package/dist/agent-message-meta.d.ts +74 -0
- package/dist/agent-message-meta.js +63 -0
- package/dist/agents.d.ts +170 -0
- package/dist/agents.js +218 -0
- package/dist/app-build-contract.d.ts +43 -0
- package/dist/app-build-contract.js +24 -0
- package/dist/apps.d.ts +66 -0
- package/dist/apps.js +47 -0
- package/dist/backend-declaration.d.ts +6 -0
- package/dist/backend-declaration.js +65 -0
- package/dist/backend-definition.d.ts +88 -0
- package/dist/backend-definition.js +158 -0
- package/dist/backend-refusal.d.ts +10 -0
- package/dist/backend-refusal.js +10 -0
- package/dist/backend.d.ts +30 -0
- package/dist/backend.js +28 -0
- package/dist/capabilities.d.ts +30 -0
- package/dist/capabilities.js +50 -0
- package/dist/channels.d.ts +142 -0
- package/dist/channels.js +97 -0
- package/dist/commands.d.ts +33 -0
- package/dist/commands.js +46 -0
- package/dist/computer-contracts.d.ts +73 -0
- package/dist/computer-contracts.js +57 -0
- package/dist/computer.d.ts +294 -0
- package/dist/computer.js +319 -0
- package/dist/connect.d.ts +11 -0
- package/dist/connect.js +211 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +106 -0
- package/dist/failure.d.ts +2 -0
- package/dist/failure.js +9 -0
- package/dist/files.d.ts +25 -0
- package/dist/files.js +84 -0
- package/dist/game-servers.d.ts +530 -0
- package/dist/game-servers.js +244 -0
- package/dist/gills.d.ts +43 -0
- package/dist/gills.js +174 -0
- package/dist/git-commit.d.ts +143 -0
- package/dist/git-commit.js +251 -0
- package/dist/git-rpc.d.ts +310 -0
- package/dist/git-rpc.js +166 -0
- package/dist/git-steps.d.ts +579 -0
- package/dist/git-steps.js +804 -0
- package/dist/git-wire.d.ts +33 -0
- package/dist/git-wire.js +240 -0
- package/dist/git.d.ts +322 -0
- package/dist/git.js +98 -0
- package/dist/host.d.ts +97 -0
- package/dist/host.js +732 -0
- package/dist/inbox.d.ts +99 -0
- package/dist/inbox.js +184 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +15 -0
- package/dist/inference.d.ts +17 -0
- package/dist/inference.js +115 -0
- package/dist/internal/app-error.d.ts +8 -0
- package/dist/internal/app-error.js +36 -0
- package/dist/internal/app-session.d.ts +8 -0
- package/dist/internal/app-session.js +551 -0
- package/dist/internal/back-inputs.d.ts +22 -0
- package/dist/internal/back-inputs.js +93 -0
- package/dist/internal/browser-bridge.d.ts +30 -0
- package/dist/internal/browser-bridge.js +203 -0
- package/dist/internal/capabilities.d.ts +13 -0
- package/dist/internal/capabilities.js +138 -0
- package/dist/internal/commands.d.ts +9 -0
- package/dist/internal/commands.js +257 -0
- package/dist/internal/disconnect-host.d.ts +7 -0
- package/dist/internal/disconnect-host.js +19 -0
- package/dist/internal/git-edit-tree.d.ts +6 -0
- package/dist/internal/git-edit-tree.js +46 -0
- package/dist/internal/memory-fs.d.ts +39 -0
- package/dist/internal/memory-fs.js +154 -0
- package/dist/internal/protocol.d.ts +116 -0
- package/dist/internal/protocol.js +6 -0
- package/dist/internal/query-feed.d.ts +23 -0
- package/dist/internal/query-feed.js +111 -0
- package/dist/internal/result.d.ts +22 -0
- package/dist/internal/result.js +61 -0
- package/dist/internal/rpc-contract.d.ts +24 -0
- package/dist/internal/rpc-contract.js +78 -0
- package/dist/internal/schemas.d.ts +307 -0
- package/dist/internal/schemas.js +524 -0
- package/dist/internal/store.d.ts +8 -0
- package/dist/internal/store.js +30 -0
- package/dist/internal/terminal-gate.d.ts +17 -0
- package/dist/internal/terminal-gate.js +94 -0
- package/dist/lifecycle.d.ts +44 -0
- package/dist/lifecycle.js +1 -0
- package/dist/marketplace.d.ts +159 -0
- package/dist/marketplace.js +102 -0
- package/dist/media-json.d.ts +181 -0
- package/dist/media-json.js +204 -0
- package/dist/media-library.d.ts +11 -0
- package/dist/media-library.js +15 -0
- package/dist/media-request-id.d.ts +7 -0
- package/dist/media-request-id.js +16 -0
- package/dist/media.d.ts +354 -0
- package/dist/media.js +654 -0
- package/dist/method-refusal.d.ts +64 -0
- package/dist/method-refusal.js +48 -0
- package/dist/pending.d.ts +10 -0
- package/dist/pending.js +10 -0
- package/dist/query.d.ts +76 -0
- package/dist/query.js +195 -0
- package/dist/react-mount.d.ts +23 -0
- package/dist/react-mount.js +108 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.js +111 -0
- package/dist/read-copy.d.ts +103 -0
- package/dist/read-copy.js +195 -0
- package/dist/roles/files.d.ts +295 -0
- package/dist/roles/files.js +184 -0
- package/dist/root.d.ts +21 -0
- package/dist/root.js +1 -0
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +1 -0
- package/dist/status.d.ts +17 -0
- package/dist/status.js +50 -0
- package/dist/storage-wire.d.ts +167 -0
- package/dist/storage-wire.js +115 -0
- package/dist/storage.d.ts +72 -0
- package/dist/storage.js +143 -0
- package/dist/store.d.ts +11 -0
- package/dist/store.js +1 -0
- package/dist/test-kit/git.d.ts +31 -0
- package/dist/test-kit/git.js +174 -0
- package/dist/ui.d.ts +169 -0
- package/dist/ui.js +10 -0
- package/dist/window.d.ts +41 -0
- package/dist/window.js +15 -0
- package/dist/world.d.ts +16 -0
- package/dist/world.js +17 -0
- package/docs/api/README.md +28 -0
- package/docs/api/app-build-contract.md +113 -0
- package/docs/api/backend-declaration.md +62 -0
- package/docs/api/backend-definition.md +215 -0
- package/docs/api/backend.md +269 -0
- package/docs/api/errors.md +169 -0
- package/docs/api/files.md +91 -0
- package/docs/api/gills.md +164 -0
- package/docs/api/git-commit.md +279 -0
- package/docs/api/git-rpc.md +243 -0
- package/docs/api/git-steps.md +1010 -0
- package/docs/api/git-wire.md +197 -0
- package/docs/api/git.md +579 -0
- package/docs/api/index.md +2880 -0
- package/docs/api/media-json.md +507 -0
- package/docs/api/media-request-id.md +32 -0
- package/docs/api/modules.md +32 -0
- package/docs/api/query.md +253 -0
- package/docs/api/react.md +216 -0
- package/docs/api/read-copy.md +204 -0
- package/docs/api/rpc.md +128 -0
- package/docs/api/storage.md +620 -0
- package/package.json +197 -3
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as git from "isomorphic-git";
|
|
2
|
+
const DIR = "/repository";
|
|
3
|
+
/** Apply changes under one tree; null means the tree became empty. */
|
|
4
|
+
export async function editTree(fs, oid, changes) {
|
|
5
|
+
if (changes.length === 0)
|
|
6
|
+
return oid;
|
|
7
|
+
const entries = new Map(oid === null
|
|
8
|
+
? []
|
|
9
|
+
: (await git.readTree({ fs, dir: DIR, oid })).tree.map((entry) => [
|
|
10
|
+
entry.path,
|
|
11
|
+
entry,
|
|
12
|
+
]));
|
|
13
|
+
const nested = new Map();
|
|
14
|
+
for (const change of changes) {
|
|
15
|
+
const [name = "", ...rest] = change.parts;
|
|
16
|
+
if (rest.length > 0) {
|
|
17
|
+
nested.set(name, [
|
|
18
|
+
...(nested.get(name) ?? []),
|
|
19
|
+
{ ...change, parts: rest },
|
|
20
|
+
]);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (change.content === null) {
|
|
24
|
+
entries.delete(name);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const existing = entries.get(name);
|
|
28
|
+
entries.set(name, {
|
|
29
|
+
path: name,
|
|
30
|
+
type: "blob",
|
|
31
|
+
mode: change.mode ?? (existing?.type === "blob" ? existing.mode : "100644"),
|
|
32
|
+
oid: await git.writeBlob({ fs, dir: DIR, blob: change.content }),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
for (const [name, children] of nested) {
|
|
36
|
+
const existing = entries.get(name);
|
|
37
|
+
const sub = await editTree(fs, existing?.type === "tree" ? existing.oid : null, children);
|
|
38
|
+
if (sub === null)
|
|
39
|
+
entries.delete(name);
|
|
40
|
+
else
|
|
41
|
+
entries.set(name, { path: name, type: "tree", mode: "040000", oid: sub });
|
|
42
|
+
}
|
|
43
|
+
if (entries.size === 0)
|
|
44
|
+
return null;
|
|
45
|
+
return git.writeTree({ fs, dir: DIR, tree: [...entries.values()] });
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal in-memory filesystem with the promise API isomorphic-git uses,
|
|
3
|
+
* so a merge runs inside an App backend with no Node built-ins. One instance
|
|
4
|
+
* lives for one merge.
|
|
5
|
+
*/
|
|
6
|
+
/** File metadata in the subset of `fs.Stats` isomorphic-git reads. */
|
|
7
|
+
export type MemoryStats = Readonly<{
|
|
8
|
+
type: "file" | "dir";
|
|
9
|
+
mode: number;
|
|
10
|
+
size: number;
|
|
11
|
+
ino: number;
|
|
12
|
+
mtimeMs: number;
|
|
13
|
+
ctimeMs: number;
|
|
14
|
+
uid: number;
|
|
15
|
+
gid: number;
|
|
16
|
+
dev: number;
|
|
17
|
+
isFile(): boolean;
|
|
18
|
+
isDirectory(): boolean;
|
|
19
|
+
isSymbolicLink(): boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/** The `{ promises }` filesystem isomorphic-git accepts as `fs`. */
|
|
22
|
+
export type MemoryFs = Readonly<{
|
|
23
|
+
promises: Readonly<{
|
|
24
|
+
readFile(path: string, options?: {
|
|
25
|
+
encoding?: string;
|
|
26
|
+
} | string): Promise<Uint8Array | string>;
|
|
27
|
+
writeFile(path: string, data: Uint8Array | string): Promise<void>;
|
|
28
|
+
unlink(path: string): Promise<void>;
|
|
29
|
+
readdir(path: string): Promise<string[]>;
|
|
30
|
+
mkdir(path: string): Promise<void>;
|
|
31
|
+
rmdir(path: string): Promise<void>;
|
|
32
|
+
stat(path: string): Promise<MemoryStats>;
|
|
33
|
+
lstat(path: string): Promise<MemoryStats>;
|
|
34
|
+
readlink(path: string): Promise<string>;
|
|
35
|
+
symlink(target: string, path: string): Promise<void>;
|
|
36
|
+
chmod(path: string): Promise<void>;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function memoryFs(): MemoryFs;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal in-memory filesystem with the promise API isomorphic-git uses,
|
|
3
|
+
* so a merge runs inside an App backend with no Node built-ins. One instance
|
|
4
|
+
* lives for one merge.
|
|
5
|
+
*/
|
|
6
|
+
/** The error shape isomorphic-git checks (`code`). */
|
|
7
|
+
class FsError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, path) {
|
|
10
|
+
super(`${code}: ${path}`);
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function normalize(path) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
for (const part of path.split("/")) {
|
|
17
|
+
if (part === "" || part === ".")
|
|
18
|
+
continue;
|
|
19
|
+
if (part === "..")
|
|
20
|
+
parts.pop();
|
|
21
|
+
else
|
|
22
|
+
parts.push(part);
|
|
23
|
+
}
|
|
24
|
+
return `/${parts.join("/")}`;
|
|
25
|
+
}
|
|
26
|
+
function parentOf(path) {
|
|
27
|
+
const index = path.lastIndexOf("/");
|
|
28
|
+
return index <= 0 ? "/" : path.slice(0, index);
|
|
29
|
+
}
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
const decoder = new TextDecoder();
|
|
32
|
+
export function memoryFs() {
|
|
33
|
+
const entries = new Map([
|
|
34
|
+
["/", { kind: "dir", mtimeMs: Date.now() }],
|
|
35
|
+
]);
|
|
36
|
+
const inodes = new Map();
|
|
37
|
+
const inode = (path) => {
|
|
38
|
+
let value = inodes.get(path);
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
value = inodes.size + 1;
|
|
41
|
+
inodes.set(path, value);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
const stats = (path) => {
|
|
46
|
+
const entry = entries.get(path);
|
|
47
|
+
if (entry === undefined)
|
|
48
|
+
throw new FsError("ENOENT", path);
|
|
49
|
+
const file = entry.kind === "file";
|
|
50
|
+
return {
|
|
51
|
+
type: entry.kind,
|
|
52
|
+
mode: file ? 0o100644 : 0o40000,
|
|
53
|
+
size: file ? entry.data.length : 0,
|
|
54
|
+
ino: inode(path),
|
|
55
|
+
mtimeMs: entry.mtimeMs,
|
|
56
|
+
ctimeMs: entry.mtimeMs,
|
|
57
|
+
uid: 1,
|
|
58
|
+
gid: 1,
|
|
59
|
+
dev: 1,
|
|
60
|
+
isFile: () => file,
|
|
61
|
+
isDirectory: () => !file,
|
|
62
|
+
isSymbolicLink: () => false,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const requireDir = (path) => {
|
|
66
|
+
const entry = entries.get(path);
|
|
67
|
+
if (entry === undefined)
|
|
68
|
+
throw new FsError("ENOENT", path);
|
|
69
|
+
if (entry.kind !== "dir")
|
|
70
|
+
throw new FsError("ENOTDIR", path);
|
|
71
|
+
};
|
|
72
|
+
const sync = {
|
|
73
|
+
readFile: (raw, options) => {
|
|
74
|
+
const path = normalize(raw);
|
|
75
|
+
const entry = entries.get(path);
|
|
76
|
+
if (entry === undefined)
|
|
77
|
+
throw new FsError("ENOENT", path);
|
|
78
|
+
if (entry.kind !== "file")
|
|
79
|
+
throw new FsError("EISDIR", path);
|
|
80
|
+
const utf8 = options === "utf8" ||
|
|
81
|
+
(options instanceof Object && options.encoding === "utf8");
|
|
82
|
+
return utf8 ? decoder.decode(entry.data) : entry.data;
|
|
83
|
+
},
|
|
84
|
+
writeFile: (raw, data) => {
|
|
85
|
+
const path = normalize(raw);
|
|
86
|
+
requireDir(parentOf(path));
|
|
87
|
+
entries.set(path, {
|
|
88
|
+
kind: "file",
|
|
89
|
+
data: data instanceof Uint8Array
|
|
90
|
+
? new Uint8Array(data)
|
|
91
|
+
: encoder.encode(data),
|
|
92
|
+
mtimeMs: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
unlink: (raw) => {
|
|
96
|
+
const path = normalize(raw);
|
|
97
|
+
if (entries.get(path)?.kind !== "file")
|
|
98
|
+
throw new FsError("ENOENT", path);
|
|
99
|
+
entries.delete(path);
|
|
100
|
+
},
|
|
101
|
+
readdir: (raw) => {
|
|
102
|
+
const path = normalize(raw);
|
|
103
|
+
requireDir(path);
|
|
104
|
+
const prefix = path === "/" ? "/" : `${path}/`;
|
|
105
|
+
return [...entries.keys()]
|
|
106
|
+
.filter((key) => key !== path &&
|
|
107
|
+
key.startsWith(prefix) &&
|
|
108
|
+
!key.slice(prefix.length).includes("/"))
|
|
109
|
+
.map((key) => key.slice(prefix.length));
|
|
110
|
+
},
|
|
111
|
+
mkdir: (raw) => {
|
|
112
|
+
const path = normalize(raw);
|
|
113
|
+
if (entries.has(path))
|
|
114
|
+
throw new FsError("EEXIST", path);
|
|
115
|
+
requireDir(parentOf(path));
|
|
116
|
+
entries.set(path, { kind: "dir", mtimeMs: Date.now() });
|
|
117
|
+
},
|
|
118
|
+
rmdir: (raw) => {
|
|
119
|
+
const path = normalize(raw);
|
|
120
|
+
const prefix = `${path}/`;
|
|
121
|
+
if ([...entries.keys()].some((key) => key.startsWith(prefix)))
|
|
122
|
+
throw new FsError("ENOTEMPTY", path);
|
|
123
|
+
entries.delete(path);
|
|
124
|
+
},
|
|
125
|
+
stat: (raw) => {
|
|
126
|
+
return stats(normalize(raw));
|
|
127
|
+
},
|
|
128
|
+
lstat: (raw) => {
|
|
129
|
+
return stats(normalize(raw));
|
|
130
|
+
},
|
|
131
|
+
readlink: (raw) => {
|
|
132
|
+
throw new FsError("ENOENT", normalize(raw));
|
|
133
|
+
},
|
|
134
|
+
symlink: (_target, raw) => {
|
|
135
|
+
throw new FsError("EEXIST", normalize(raw));
|
|
136
|
+
},
|
|
137
|
+
chmod: () => undefined,
|
|
138
|
+
};
|
|
139
|
+
const lift = (run) => async (...args) => await run(...args);
|
|
140
|
+
const promises = {
|
|
141
|
+
readFile: lift(sync.readFile),
|
|
142
|
+
writeFile: lift(sync.writeFile),
|
|
143
|
+
unlink: lift(sync.unlink),
|
|
144
|
+
readdir: lift(sync.readdir),
|
|
145
|
+
mkdir: lift(sync.mkdir),
|
|
146
|
+
rmdir: lift(sync.rmdir),
|
|
147
|
+
stat: lift(sync.stat),
|
|
148
|
+
lstat: lift(sync.lstat),
|
|
149
|
+
readlink: lift(sync.readlink),
|
|
150
|
+
symlink: lift(sync.symlink),
|
|
151
|
+
chmod: lift(sync.chmod),
|
|
152
|
+
};
|
|
153
|
+
return { promises };
|
|
154
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { AppCommand } from "../commands.js";
|
|
2
|
+
import type { WhistleExecutionOrigin, WhistleJsonValue } from "@fungi.computer/whistle";
|
|
3
|
+
import type { AppContext, AppIdentity, AppLifecycle, FocusState } from "../lifecycle.js";
|
|
4
|
+
import type { AppBrowserAction, AppBrowserActionOutcome, AgentSessionSelection, AppPermissionOutcome, AppPermissionRequest, AppStatusAction, AppStatusItem, FileNavigationInput, ToastInput } from "../ui.js";
|
|
5
|
+
import type { APP_NAVIGATION_CONTRACT_VERSION } from "../window.js";
|
|
6
|
+
import type { AppErrorData } from "./app-error.js";
|
|
7
|
+
import type { RpcResult } from "./result.js";
|
|
8
|
+
import type { WhistleFailure } from "@fungi.computer/whistle";
|
|
9
|
+
/** The iframe handshake version both App and host must speak. */
|
|
10
|
+
export declare const APP_PROTOCOL_VERSION: 2;
|
|
11
|
+
/** Message type the App iframe posts to announce it is ready to connect. */
|
|
12
|
+
export declare const APP_READY_TYPE: "fungi.app.ready";
|
|
13
|
+
/** Message type the host posts to hand the App its RPC MessagePort. */
|
|
14
|
+
export declare const APP_CONNECT_TYPE: "fungi.app.connect";
|
|
15
|
+
export type AppClientRpc = {
|
|
16
|
+
disconnect(error?: AppErrorData): Promise<RpcResult<void>>;
|
|
17
|
+
requestClose(): Promise<RpcResult<boolean>>;
|
|
18
|
+
/** Ask the App to go back one step in its own history; `true` when it did. */
|
|
19
|
+
requestBack(): Promise<RpcResult<boolean>>;
|
|
20
|
+
/** The person used one of the App's status items; `true` when it acted. */
|
|
21
|
+
activateStatus(action: AppStatusAction): Promise<RpcResult<boolean>>;
|
|
22
|
+
updateContext(context: VersionedFact<AppContext>): Promise<RpcResult<void>>;
|
|
23
|
+
reconfigure(value: AppHostReconfigurationWire): Promise<RpcResult<void>>;
|
|
24
|
+
updateLifecycle(lifecycle: VersionedFact<AppLifecycle>): Promise<RpcResult<void>>;
|
|
25
|
+
updateReservedChords(chords: VersionedFact<readonly ReservedChord[]>): Promise<RpcResult<void>>;
|
|
26
|
+
updateTheme(theme: VersionedFact<string>): Promise<RpcResult<void>>;
|
|
27
|
+
};
|
|
28
|
+
/** The host's handshake message that hands the App its RPC MessagePort. */
|
|
29
|
+
export type AppConnectMessage = {
|
|
30
|
+
readonly epoch: string;
|
|
31
|
+
readonly protocolVersion: typeof APP_PROTOCOL_VERSION;
|
|
32
|
+
readonly type: typeof APP_CONNECT_TYPE;
|
|
33
|
+
};
|
|
34
|
+
export type AppHostRpc = {
|
|
35
|
+
requestComputerChoice(): Promise<RpcResult<void>>;
|
|
36
|
+
requestBrowserAction(input: AppBrowserAction): Promise<RpcResult<AppBrowserActionOutcome>>;
|
|
37
|
+
connect(request: ConnectRequestWire): Promise<RpcResult<HostBootstrapWire>>;
|
|
38
|
+
invokeReservedChord(id: string, repeat: boolean): Promise<RpcResult<void>>;
|
|
39
|
+
openNavigation(input: FileNavigationInput): Promise<RpcResult<void>>;
|
|
40
|
+
openTeamBilling(): Promise<RpcResult<void>>;
|
|
41
|
+
openDesktopSettings(): Promise<RpcResult<void>>;
|
|
42
|
+
requestAppPermissions(input: AppPermissionRequest): Promise<RpcResult<AppPermissionOutcome>>;
|
|
43
|
+
registerCommand(command: AppCommand, handler: CommandHandlerRpc): Promise<RpcResult<RegisteredCommandWire>>;
|
|
44
|
+
reportFocus(focus: FocusState): Promise<RpcResult<void>>;
|
|
45
|
+
reportNavigation(state: NavigationStateWire): Promise<RpcResult<void>>;
|
|
46
|
+
reportStatus(items: readonly AppStatusItem[]): Promise<RpcResult<void>>;
|
|
47
|
+
reportAgentSessionSelection(selection: AgentSessionSelection | null): Promise<RpcResult<void>>;
|
|
48
|
+
showToast(input: ToastInput): Promise<RpcResult<void>>;
|
|
49
|
+
unregisterCommand(commandId: string): Promise<RpcResult<void>>;
|
|
50
|
+
};
|
|
51
|
+
/** The versioned navigation contract an App reports to its host. */
|
|
52
|
+
export type NavigationStateWire = Readonly<{
|
|
53
|
+
contractVersion: typeof APP_NAVIGATION_CONTRACT_VERSION;
|
|
54
|
+
canGoBack: boolean;
|
|
55
|
+
title?: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** The App iframe's handshake message announcing it is ready to connect. */
|
|
58
|
+
export type AppReadyMessage = {
|
|
59
|
+
readonly protocolVersion: typeof APP_PROTOCOL_VERSION;
|
|
60
|
+
readonly type: typeof APP_READY_TYPE;
|
|
61
|
+
};
|
|
62
|
+
/** The handle is deliberately opaque: capability owners define its methods. */
|
|
63
|
+
export type CapabilityGrantWire = {
|
|
64
|
+
readonly handle: object;
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly version: number;
|
|
67
|
+
};
|
|
68
|
+
export type CommandHandlerRpc = {
|
|
69
|
+
cancel(requestId: string): Promise<RpcResult<void>>;
|
|
70
|
+
invoke(invocation: CommandInvocationWire): Promise<RpcResult<CommandInvocationResultWire>>;
|
|
71
|
+
};
|
|
72
|
+
/** Data-only command outcome crossing the App boundary. */
|
|
73
|
+
export type CommandInvocationResultWire = Readonly<{
|
|
74
|
+
readonly status: "handled";
|
|
75
|
+
readonly result?: WhistleJsonValue;
|
|
76
|
+
}> | Readonly<{
|
|
77
|
+
readonly status: "failed";
|
|
78
|
+
readonly error: WhistleFailure;
|
|
79
|
+
}>;
|
|
80
|
+
export type CommandInvocationWire = {
|
|
81
|
+
readonly arguments?: WhistleJsonValue;
|
|
82
|
+
readonly commandId: string;
|
|
83
|
+
readonly origin: WhistleExecutionOrigin;
|
|
84
|
+
readonly requestId: string;
|
|
85
|
+
};
|
|
86
|
+
export type ConnectRequestWire = {
|
|
87
|
+
readonly epoch: string;
|
|
88
|
+
readonly protocolVersion: typeof APP_PROTOCOL_VERSION;
|
|
89
|
+
};
|
|
90
|
+
export type HostBootstrapWire = {
|
|
91
|
+
readonly capabilities: readonly CapabilityGrantWire[];
|
|
92
|
+
readonly context: VersionedFact<AppContext>;
|
|
93
|
+
readonly identity: AppIdentity;
|
|
94
|
+
readonly lifecycle: VersionedFact<AppLifecycle>;
|
|
95
|
+
readonly reservedChords: VersionedFact<readonly ReservedChord[]>;
|
|
96
|
+
readonly theme: VersionedFact<string>;
|
|
97
|
+
};
|
|
98
|
+
export type AppHostReconfigurationWire = {
|
|
99
|
+
readonly capabilities: readonly CapabilityGrantWire[];
|
|
100
|
+
readonly context: VersionedFact<AppContext>;
|
|
101
|
+
};
|
|
102
|
+
export type RegisteredCommandWire = {
|
|
103
|
+
readonly commandId: string;
|
|
104
|
+
};
|
|
105
|
+
export type ReservedChord = {
|
|
106
|
+
readonly altKey: boolean;
|
|
107
|
+
readonly ctrlKey: boolean;
|
|
108
|
+
readonly id: string;
|
|
109
|
+
readonly key: string;
|
|
110
|
+
readonly metaKey: boolean;
|
|
111
|
+
readonly shiftKey: boolean;
|
|
112
|
+
};
|
|
113
|
+
export type VersionedFact<T> = {
|
|
114
|
+
readonly value: T;
|
|
115
|
+
readonly version: number;
|
|
116
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The iframe handshake version both App and host must speak. */
|
|
2
|
+
export const APP_PROTOCOL_VERSION = 2;
|
|
3
|
+
/** Message type the App iframe posts to announce it is ready to connect. */
|
|
4
|
+
export const APP_READY_TYPE = "fungi.app.ready";
|
|
5
|
+
/** Message type the host posts to hand the App its RPC MessagePort. */
|
|
6
|
+
export const APP_CONNECT_TYPE = "fungi.app.connect";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type QueryKey, type QueryMeta } from "@tanstack/react-query";
|
|
2
|
+
import { AppError } from "../errors.js";
|
|
3
|
+
import { type MemberInboxItem } from "../inbox.js";
|
|
4
|
+
import type { AppRoot } from "../root.js";
|
|
5
|
+
import type { Disposer } from "../store.js";
|
|
6
|
+
/** A feed can lose its connection without losing its last owner snapshot. */
|
|
7
|
+
export type FeedSnapshot<T> = Readonly<{
|
|
8
|
+
status: "live";
|
|
9
|
+
data: T;
|
|
10
|
+
}> | Readonly<{
|
|
11
|
+
status: "loading" | "error";
|
|
12
|
+
data?: T;
|
|
13
|
+
}>;
|
|
14
|
+
/** Start a live feed, calling `onChange` with each snapshot until
|
|
15
|
+
* `signal` aborts; the returned (or resolved) Disposer tears it down. */
|
|
16
|
+
export type FeedWatch<T> = (onChange: (snapshot: FeedSnapshot<T>) => void, signal: AbortSignal) => Disposer | Promise<Disposer>;
|
|
17
|
+
/** Query owns sharing and teardown; Effect supplies a bounded latest-snapshot queue. */
|
|
18
|
+
export declare function useFeedQuery<T>(queryKey: QueryKey, watch: FeedWatch<T>, options?: {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
meta?: QueryMeta;
|
|
21
|
+
}): import("@tanstack/react-query").UseQueryResult<FeedSnapshot<T>, AppError>;
|
|
22
|
+
export declare function watchInbox(app: Pick<AppRoot, "capabilities"> | null, onChange: (snapshot: FeedSnapshot<readonly MemberInboxItem[]>) => void, signal: AbortSignal): Promise<Disposer>;
|
|
23
|
+
export declare function queryError(cause: unknown): AppError;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { experimental_streamedQuery, useQuery, } from "@tanstack/react-query";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Queue from "effect/Queue";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import { EventSource } from "eventsource";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
import { openAppInbox } from "../agents.js";
|
|
8
|
+
import { AppError } from "../errors.js";
|
|
9
|
+
import { reportCatchFailure } from "../failure.js";
|
|
10
|
+
import { watchMemberInbox } from "../inbox.js";
|
|
11
|
+
function retain(previous, next) {
|
|
12
|
+
return next.status === "live"
|
|
13
|
+
? next
|
|
14
|
+
: { ...next, data: next.data ?? previous.data };
|
|
15
|
+
}
|
|
16
|
+
/** Query owns sharing and teardown; Effect supplies a bounded latest-snapshot queue. */
|
|
17
|
+
export function useFeedQuery(queryKey, watch, options = {}) {
|
|
18
|
+
return useQuery({
|
|
19
|
+
queryKey,
|
|
20
|
+
...options,
|
|
21
|
+
retry: false,
|
|
22
|
+
staleTime: Infinity,
|
|
23
|
+
refetchOnMount: "always",
|
|
24
|
+
queryFn: experimental_streamedQuery({
|
|
25
|
+
initialValue: { status: "loading" },
|
|
26
|
+
refetchMode: "append",
|
|
27
|
+
reducer: retain,
|
|
28
|
+
streamFn: ({ signal }) => Stream.toAsyncIterable(Stream.callback((queue) => Effect.gen(function* () {
|
|
29
|
+
let latest = { status: "loading" };
|
|
30
|
+
const end = () => {
|
|
31
|
+
Queue.endUnsafe(queue);
|
|
32
|
+
};
|
|
33
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
34
|
+
signal.addEventListener("abort", end, { once: true });
|
|
35
|
+
}), () => Effect.sync(() => {
|
|
36
|
+
signal.removeEventListener("abort", end);
|
|
37
|
+
}));
|
|
38
|
+
if (signal.aborted) {
|
|
39
|
+
end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
yield* Effect.acquireRelease(Effect.tryPromise({
|
|
43
|
+
try: () => Promise.resolve(watch((snapshot) => {
|
|
44
|
+
latest = retain(latest, snapshot);
|
|
45
|
+
Queue.offerUnsafe(queue, latest);
|
|
46
|
+
}, signal)),
|
|
47
|
+
catch: queryError,
|
|
48
|
+
}), (release) => Effect.sync(release));
|
|
49
|
+
}).pipe(Effect.catch((error) => Queue.fail(queue, error))), { bufferSize: 1, strategy: "sliding" })),
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function watchInbox(app, onChange, signal) {
|
|
54
|
+
if (!app)
|
|
55
|
+
throw new AppError("No App connection", {
|
|
56
|
+
code: "DISCONNECTED",
|
|
57
|
+
retryable: true,
|
|
58
|
+
});
|
|
59
|
+
const transport = await openAppInbox(app.capabilities);
|
|
60
|
+
const close = () => {
|
|
61
|
+
void transport.close().catch((cause) => {
|
|
62
|
+
reportCatchFailure("inbox.query-close", cause);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
if (signal.aborted) {
|
|
66
|
+
close();
|
|
67
|
+
return () => undefined;
|
|
68
|
+
}
|
|
69
|
+
const feed = watchMemberInbox({
|
|
70
|
+
baseUrl: transport.baseUrl,
|
|
71
|
+
fetch: transport.fetch,
|
|
72
|
+
openEvents: (url) => {
|
|
73
|
+
const source = new EventSource(url, {
|
|
74
|
+
fetch: (input, init) => transport.fetch(input, init),
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
addEventListener: (type, listener) => {
|
|
78
|
+
source.addEventListener(type, (event) => {
|
|
79
|
+
if (type === "open" || type === "error")
|
|
80
|
+
listener(new MessageEvent(type));
|
|
81
|
+
else if (event instanceof MessageEvent)
|
|
82
|
+
listener(new MessageEvent(type, {
|
|
83
|
+
data: z.string().parse(event.data),
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
close: () => {
|
|
88
|
+
source.close();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
onChange: (snapshot) => {
|
|
93
|
+
onChange(snapshot.status === "live"
|
|
94
|
+
? { status: "live", data: snapshot.items }
|
|
95
|
+
: { status: snapshot.status });
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return () => {
|
|
99
|
+
feed.close();
|
|
100
|
+
close();
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function queryError(cause) {
|
|
104
|
+
return cause instanceof AppError
|
|
105
|
+
? cause
|
|
106
|
+
: new AppError("App operation failed", {
|
|
107
|
+
cause,
|
|
108
|
+
code: "HOST_FAILURE",
|
|
109
|
+
retryable: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type * as Schema from "effect/Schema";
|
|
2
|
+
import { type AppErrorData } from "./app-error.js";
|
|
3
|
+
export type RpcResult<T> = {
|
|
4
|
+
readonly error: AppErrorData;
|
|
5
|
+
readonly ok: false;
|
|
6
|
+
} | {
|
|
7
|
+
readonly ok: true;
|
|
8
|
+
readonly value: T;
|
|
9
|
+
};
|
|
10
|
+
export declare const rpcSuccess: <T>(value: T) => RpcResult<T>;
|
|
11
|
+
export declare const rpcFailure: <T = never>(cause: unknown) => RpcResult<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Decodes one ordinary RPC response and releases its Cap'n Web result owner.
|
|
14
|
+
* The bootstrap response is intentionally not passed here: it owns the live
|
|
15
|
+
* capability handles retained by the published app root.
|
|
16
|
+
*/
|
|
17
|
+
export declare function callRpc<T>(operation: PromiseLike<unknown>, parse: ReturnType<typeof Schema.decodeUnknownSync<Schema.Decoder<RpcResult<T>>>>): Promise<T>;
|
|
18
|
+
/**
|
|
19
|
+
* Callers must first decode an unknown transport envelope with an
|
|
20
|
+
* operation-specific schema. This function only exposes that trusted result.
|
|
21
|
+
*/
|
|
22
|
+
export declare function unwrapRpcResult<T>(result: RpcResult<T>): T;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as Predicate from "effect/Predicate";
|
|
2
|
+
import { AppError } from "../errors.js";
|
|
3
|
+
import { appErrorData, appErrorFromData, } from "./app-error.js";
|
|
4
|
+
export const rpcSuccess = (value) => ({ ok: true, value });
|
|
5
|
+
export const rpcFailure = (cause) => ({
|
|
6
|
+
error: appErrorData(cause),
|
|
7
|
+
ok: false,
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Decodes one ordinary RPC response and releases its Cap'n Web result owner.
|
|
11
|
+
* The bootstrap response is intentionally not passed here: it owns the live
|
|
12
|
+
* capability handles retained by the published app root.
|
|
13
|
+
*/
|
|
14
|
+
export async function callRpc(operation, parse) {
|
|
15
|
+
let result;
|
|
16
|
+
const disposed = new Set();
|
|
17
|
+
try {
|
|
18
|
+
result = await operation;
|
|
19
|
+
return unwrapRpcResult(parse(result));
|
|
20
|
+
}
|
|
21
|
+
catch (cause) {
|
|
22
|
+
if (isRpcResultOwner(cause))
|
|
23
|
+
disposeRpcResult(cause, disposed);
|
|
24
|
+
if (cause instanceof AppError)
|
|
25
|
+
throw cause;
|
|
26
|
+
throw new AppError("The remote operation failed", {
|
|
27
|
+
cause,
|
|
28
|
+
code: "HOST_FAILURE",
|
|
29
|
+
retryable: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (result !== operation && isRpcResultOwner(result))
|
|
34
|
+
disposeRpcResult(result, disposed);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Callers must first decode an unknown transport envelope with an
|
|
39
|
+
* operation-specific schema. This function only exposes that trusted result.
|
|
40
|
+
*/
|
|
41
|
+
export function unwrapRpcResult(result) {
|
|
42
|
+
if (result.ok)
|
|
43
|
+
return result.value;
|
|
44
|
+
throw appErrorFromData(result.error);
|
|
45
|
+
}
|
|
46
|
+
function isRpcResultOwner(value) {
|
|
47
|
+
return (Predicate.isObject(value) &&
|
|
48
|
+
Symbol.dispose in value &&
|
|
49
|
+
Predicate.isFunction(value[Symbol.dispose]));
|
|
50
|
+
}
|
|
51
|
+
function disposeRpcResult(value, disposed) {
|
|
52
|
+
if (disposed.has(value))
|
|
53
|
+
return;
|
|
54
|
+
disposed.add(value);
|
|
55
|
+
try {
|
|
56
|
+
value[Symbol.dispose]();
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
console.warn("RPC result disposal failed", cause);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
/** Cap'n Web stubs are callable proxies, not records. Select only owner methods
|
|
3
|
+
* and bind their receiver before the maintained function schemas wrap them. */
|
|
4
|
+
export declare function rpcHandleSchema<S extends z.ZodRawShape>(methods: S): z.ZodPipe<z.ZodUnknown, z.ZodTransform<Awaited<z.core.$InferObjectOutput<{ -readonly [P in keyof S]: S[P]; }, {}> & {
|
|
5
|
+
/** Release the underlying RPC stub; call once the handle is done with. */
|
|
6
|
+
[Symbol.dispose](): void;
|
|
7
|
+
}>, unknown>>;
|
|
8
|
+
/** Copy an owned data result through its domain decoder, then release the RPC
|
|
9
|
+
* envelope. Live resources use their own handle schemas instead. */
|
|
10
|
+
export declare function rpcDataSchema<T>(result: z.ZodType<T>): z.ZodPipe<z.ZodUnknown, z.ZodTransform<Awaited<T>, unknown>>;
|
|
11
|
+
/** Wrap an RPC method's result schema into a Zod function schema: the
|
|
12
|
+
* decoded value resolves on success, and a malformed result is rethrown
|
|
13
|
+
* as a PROTOCOL AppError after releasing the raw RPC envelope. */
|
|
14
|
+
export declare function rpcMethod<T>(result: z.ZodType<T>): z.ZodFunction<z.core.$ZodFunctionArgs, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<Awaited<T>, unknown>>>>;
|
|
15
|
+
/** A method that decodes and releases an empty RPC result. */
|
|
16
|
+
export declare const rpcVoid: z.ZodFunction<z.core.$ZodFunctionArgs, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<void, unknown>>>>;
|
|
17
|
+
/** A per-request Fetch capability keeps Response bodies live until consumed. */
|
|
18
|
+
export declare const rpcFetchHandleSchema: z.ZodPipe<z.ZodUnknown, z.ZodTransform<{
|
|
19
|
+
fetch: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<Response, unknown>>>>;
|
|
20
|
+
close: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.ZodPromise<z.ZodPipe<z.ZodUnknown, z.ZodTransform<void, unknown>>>>;
|
|
21
|
+
} & {
|
|
22
|
+
/** Release the underlying RPC stub; call once the handle is done with. */
|
|
23
|
+
[Symbol.dispose](): void;
|
|
24
|
+
}, unknown>>;
|