@dcasegr/skein 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.
- package/README.md +522 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +773 -0
- package/dist/host/router.js +2389 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Root's admin messages (#124, #142, #143): building them is a library — installing an app, removing it,
|
|
2
|
+
// a route, a read route, an address-book entry, the host row, a directory into `main`, a grant — each a
|
|
3
|
+
// few messages from root to the kernel's admin boxes (docs/MESSAGES.md "The dispatch table and the
|
|
4
|
+
// kernel's operations": objects, head, dispatch, peers, grant) and the app's own box (start/stop). The
|
|
5
|
+
// management page builds the same messages with the same code (src/host/plan.ts) and sends them with the
|
|
6
|
+
// browser's wallet. The client `skein` (admin-cli.ts) signs them with the operator's key and delivers them
|
|
7
|
+
// (target.ts): over the host's control socket on the host machine, else on one BRC-104 session.
|
|
8
|
+
//
|
|
9
|
+
// A message is a box and a body (a record value: what the kernel's operation takes). `messageJson` is its
|
|
10
|
+
// BRC-33 `/sendMessage` JSON form (what `--dry-run` prints):
|
|
11
|
+
//
|
|
12
|
+
// {"message": {"recipient": "<the instance's identity key, hex>", "messageBox": "<box>", "body": <the body, DAG-JSON>}}
|
|
13
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
14
|
+
import * as dagJson from "@ipld/dag-json";
|
|
15
|
+
import {} from "./bundle.js";
|
|
16
|
+
import { handlerCid, hashDir, recordBundles } from "./client.js";
|
|
17
|
+
import { dispatchOrigin, fold } from "../runtime/dispatch.js";
|
|
18
|
+
import { MAIN } from "../runtime/heads.js";
|
|
19
|
+
import { DEFAULT_ONLY, onlyIgnore } from "../host/deploy.js";
|
|
20
|
+
import { appHead, describe, mergeConfig, planInstall, planRootRoute, planUninstall, sendInstall, sendUninstall } from "../host/plan.js";
|
|
21
|
+
import { lookup, readBlob } from "../runtime/tree.js";
|
|
22
|
+
const isKey = (s) => /^0[23][0-9a-f]{64}$/.test(s);
|
|
23
|
+
const keyBytes = (hex) => Uint8Array.from(Buffer.from(hex, "hex"));
|
|
24
|
+
/** A message as the `/sendMessage` JSON body. */
|
|
25
|
+
export function messageJson(recipient, m) {
|
|
26
|
+
return new TextDecoder().decode(dagJson.encode({ message: { recipient, messageBox: m.box, body: m.body } }));
|
|
27
|
+
}
|
|
28
|
+
/** Messages as `send(box, bytes)` calls make them (plan.ts sendInstall, sendUninstall), collected. */
|
|
29
|
+
async function collect(run) {
|
|
30
|
+
const out = [];
|
|
31
|
+
await run(async (box, body) => { out.push({ box, body: dagCbor.decode(body) }); });
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Install (or upgrade) the app `tree` into the instance `view` reads (docs/APPS.md §3): objects (≤ 1 MiB
|
|
36
|
+
* bundles), the head `<app>/app`, a dispatch message per row that changes, the start. `config` is merged
|
|
37
|
+
* over the manifest's (one level deep per program). `modules`: where a `bin/<x>.cid` module the instance
|
|
38
|
+
* lacks is found (this repo's wasm/ directory, boot.ts wasmDirObjects).
|
|
39
|
+
*/
|
|
40
|
+
export async function planInstallApp(tree, view, o = {}) {
|
|
41
|
+
if (o.config)
|
|
42
|
+
tree.checked.manifest.config = mergeConfig(tree.checked.manifest.config, o.config);
|
|
43
|
+
const plan = await planInstall(tree, view, { modules: o.modules ?? { get: async () => undefined } });
|
|
44
|
+
const messages = await collect((send) => sendInstall(plan, send));
|
|
45
|
+
return { prompt: describe(plan), recipient: view.identity, messages, app: plan.app, version: plan.version, recordCid: plan.recordCid, record: plan.record };
|
|
46
|
+
}
|
|
47
|
+
/** Uninstall: the app's `stop`, then every route the table holds for it removed; its heads are left. */
|
|
48
|
+
export async function planUninstallApp(name, view) {
|
|
49
|
+
const p = await planUninstall(name, view);
|
|
50
|
+
const messages = await collect((send) => sendUninstall(p, send));
|
|
51
|
+
const prompt = [`uninstall ${name} ${p.record.version}${p.stop ? ` · stop ${JSON.stringify(p.stop)}` : ""} · dispatch remove ×${p.rows.length} · head ${name}/app left`, ...p.rows.map((r) => ` dispatch remove ${r.row.transport} ${r.row.address}${r.row.prefix ? " prefix" : ""}`)];
|
|
52
|
+
return { prompt, recipient: view.identity, messages };
|
|
53
|
+
}
|
|
54
|
+
/** The fields of a route that are not a handler's settings. */
|
|
55
|
+
const ROW_FIELDS = ["transport", "address", "prefix", "filters", "sender", "program", "fn", "app", "optional"];
|
|
56
|
+
/**
|
|
57
|
+
* Root's own route (#125, #143; `skein routes`): one `dispatch` add or remove of a route with no `app` —
|
|
58
|
+
* an app's upgrade or uninstall leaves it. A mailbox or event route at a box, an http route at a path
|
|
59
|
+
* (with `filters` and a handler's `fn`, or filters only: a read route — the site at `/`: filters
|
|
60
|
+
* ["site.get"], settings {root: "www"}), a libp2p route at a topic. No sender (#143). A remove names the
|
|
61
|
+
* key (transport, address, prefix) only.
|
|
62
|
+
*/
|
|
63
|
+
export async function planRoute(view, a, programs = {}) {
|
|
64
|
+
const transport = a.transport ?? (a.address.startsWith("/") ? "http" : "mailbox");
|
|
65
|
+
if (!["mailbox", "event", "http", "libp2p"].includes(transport))
|
|
66
|
+
throw new Error(`--transport ${transport}: mailbox, event, http or libp2p`);
|
|
67
|
+
if (transport === "http" && (!a.address.startsWith("/") || /[\\\0?#]/.test(a.address) || a.address.split("/").some((x) => x === "." || x === "..")))
|
|
68
|
+
throw new Error(`http address ${JSON.stringify(a.address)}: a path (from /, no ".", "..", query or fragment)`);
|
|
69
|
+
if (transport !== "http" && a.prefix)
|
|
70
|
+
throw new Error("--prefix: an http route's");
|
|
71
|
+
const settings = a.settings ?? {};
|
|
72
|
+
const bad = Object.keys(settings).filter((k) => ROW_FIELDS.includes(k));
|
|
73
|
+
if (bad.length)
|
|
74
|
+
throw new Error(`--settings: ${bad.join(", ")} ${bad.length > 1 ? "are" : "is"} the route's own, not a setting`);
|
|
75
|
+
let row = { transport, address: a.address, ...(a.prefix ? { prefix: true } : {}) };
|
|
76
|
+
if (a.op === "add") {
|
|
77
|
+
if (a.handler === undefined) {
|
|
78
|
+
if (!(transport === "http" || (transport === "libp2p" && a.address.startsWith("/"))) || !a.filters?.length)
|
|
79
|
+
throw new Error("a route names its handler; only an http or libp2p protocol (/…) read route (--filters, no handler) has none");
|
|
80
|
+
if (a.fn)
|
|
81
|
+
throw new Error("--fn: a read route has no handler");
|
|
82
|
+
}
|
|
83
|
+
else if (transport === "http" || transport === "libp2p") {
|
|
84
|
+
if (!a.fn)
|
|
85
|
+
throw new Error(`an ${transport} route names its handler's function (--fn)`);
|
|
86
|
+
}
|
|
87
|
+
row = { ...settings, ...row, ...(a.filters?.length ? { filters: a.filters } : {}), ...(a.handler !== undefined ? { program: handlerCid(a.handler, programs), ...(a.fn ? { fn: a.fn } : {}) } : {}) };
|
|
88
|
+
}
|
|
89
|
+
const p = await planRootRoute(view, a.op, row);
|
|
90
|
+
return { prompt: p.prompt, recipient: view.identity, messages: p.rows.map((r) => ({ box: "dispatch", body: { op: r.op, row: r.row } })) };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The claim (#89, #127, #143): one message in box `claim` to an image (its claim route takes anyone) —
|
|
94
|
+
* its sender (the wallet that sends it) is granted root; the body carries only the claimant's
|
|
95
|
+
* mailbox entry, if any.
|
|
96
|
+
*/
|
|
97
|
+
export function planClaim(recipient, o = {}) {
|
|
98
|
+
const body = { ...(o.messagebox ? { messagebox: o.messagebox } : {}), ...(o.handle ? { handle: o.handle, ...(o.domain ? { domain: o.domain } : {}) } : {}) };
|
|
99
|
+
return { prompt: [`claim ${recipient}: root to the key that sends this${o.messagebox ? ` · messagebox ${o.messagebox}` : ""}${o.handle ? ` (@${o.handle}${o.domain ? `@${o.domain}` : ""})` : ""}`], recipient, messages: [{ box: "claim", body }] };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The host row (#130): root's choice of a host and its rates — one `dispatch` message adding (or
|
|
103
|
+
* removing) the kernel route {mailbox, billing, host: <the host's key>, kernel, tick, x, rates} (#143:
|
|
104
|
+
* the host's key a setting, no sender). The first such route is the host's; the kernel bills by it,
|
|
105
|
+
* the host ticks it. Removing it ends billing.
|
|
106
|
+
*/
|
|
107
|
+
export function planHost(recipient, op, t) {
|
|
108
|
+
if (!isKey(t.key))
|
|
109
|
+
throw new Error(`${t.key}: not the host's key (hex)`);
|
|
110
|
+
if (op === "add" && (!Number.isSafeInteger(t.x) || t.x < 1))
|
|
111
|
+
throw new Error("x: the sats the skein prepays at a time (a whole number ≥ 1)");
|
|
112
|
+
const rates = Object.fromEntries(Object.entries(t.rates ?? {}).map(([k, v]) => {
|
|
113
|
+
if (!Number.isSafeInteger(v) || v < 0)
|
|
114
|
+
throw new Error(`rates.${k}: a whole number of sats`);
|
|
115
|
+
return [k, v];
|
|
116
|
+
}));
|
|
117
|
+
const row = { transport: "mailbox", address: "billing", host: keyBytes(t.key), program: "kernel", fn: "tick", ...(op === "add" ? { x: t.x, ...(Object.keys(rates).length ? { rates } : {}) } : {}) };
|
|
118
|
+
const shown = Object.entries(rates).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
119
|
+
return {
|
|
120
|
+
prompt: [op === "add" ? `host ${t.key}: bills this skein — x ${t.x} sats a block${shown ? `, rates ${shown}` : ""} (#130: the skein pays it from its own wallet, ahead)` : `host ${t.key}: removed (nothing is billed)`],
|
|
121
|
+
recipient, messages: [{ box: "dispatch", body: { op, row } }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Address-book changes (#40, #70): one message to `peers` each. */
|
|
125
|
+
export function planPeers(recipient, changes) {
|
|
126
|
+
const messages = [];
|
|
127
|
+
const prompt = [];
|
|
128
|
+
for (const c of changes) {
|
|
129
|
+
if (!isKey(c.key))
|
|
130
|
+
throw new Error(`${c.key}: not an identity key (hex)`);
|
|
131
|
+
if (c.op === "remove") {
|
|
132
|
+
messages.push({ box: "peers", body: { op: "remove", key: keyBytes(c.key) } });
|
|
133
|
+
prompt.push(`peers remove ${c.key}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
messages.push({ box: "peers", body: { op: "add", key: keyBytes(c.key), transport: c.transport, address: c.address, ...(c.handle ? { handle: c.handle } : {}), ...(c.domain ? { domain: c.domain } : {}) } });
|
|
137
|
+
prompt.push(`peers add ${c.key} → ${c.transport} ${c.address}${c.handle ? ` (@${c.handle}${c.domain ? `@${c.domain}` : ""})` : ""}`);
|
|
138
|
+
}
|
|
139
|
+
return { prompt, recipient, messages };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A directory into the instance's `main` (#23): its git objects in ≤ 1 MiB bundles to `objects`, the root
|
|
143
|
+
* on the last (the kernel sets `main` when there is none), then `head {name: "main", tree}` when `main` is
|
|
144
|
+
* another tree. `only` filters the directory (deploy.ts onlyIgnore; default DEFAULT_ONLY; [] for all of it).
|
|
145
|
+
* Records the view's store has are not sent. With no view (`recipient` alone) everything is sent, and the head.
|
|
146
|
+
*/
|
|
147
|
+
export async function planDeploy(dir, target, o = {}) {
|
|
148
|
+
const only = o.only ?? DEFAULT_ONLY;
|
|
149
|
+
const { root, records } = await hashDir(dir, only.length ? { ignore: onlyIgnore(dir, only) } : {});
|
|
150
|
+
const view = "store" in target ? target : undefined;
|
|
151
|
+
const { records: n, bundles } = await recordBundles(root, records, view ? { skip: (c) => view.store.has(c) } : {});
|
|
152
|
+
const main = view?.heads.find((h) => h.name === MAIN)?.root;
|
|
153
|
+
const head = !view || (main !== undefined && !main.equals(root));
|
|
154
|
+
const messages = bundles.map((b) => ({ box: "objects", body: dagCbor.decode(b) }));
|
|
155
|
+
if (head)
|
|
156
|
+
messages.push({ box: "head", body: { name: MAIN, tree: root } });
|
|
157
|
+
const prompt = [`deploy ${dir} → main ${root}${main?.equals(root) ? " (main is this tree already)" : ""}`, ` messages objects ×${bundles.length} (${n} records)${head ? " · head main" : ""}`];
|
|
158
|
+
return { prompt, recipient: target.identity, messages, root };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The instance as the install plan reads it (plan.ts InstanceView), from its explorer: heads, the genesis
|
|
162
|
+
* (identity, programs), the address book, the route table, and the store by CID —
|
|
163
|
+
* what the management page's view reads (shruggr/skein-site www/app.js `Skein.view`).
|
|
164
|
+
*/
|
|
165
|
+
export async function explorerView(read) {
|
|
166
|
+
const records = new Map();
|
|
167
|
+
const record = async (cid) => {
|
|
168
|
+
const k = cid.toString();
|
|
169
|
+
if (!records.has(k))
|
|
170
|
+
records.set(k, await read(`/record/${k}`));
|
|
171
|
+
return records.get(k);
|
|
172
|
+
};
|
|
173
|
+
const top = await read("");
|
|
174
|
+
if (!top)
|
|
175
|
+
throw new Error("the explorer answered nothing (is it a skein?)");
|
|
176
|
+
const heads = Object.entries(top.heads ?? {}).map(([name, root]) => ({ name, root }));
|
|
177
|
+
const head = (name) => heads.find((x) => x.name === name)?.root;
|
|
178
|
+
const first = (await read("/log?before=1&limit=1"))?.entries?.[0]?.record;
|
|
179
|
+
const genesis = (first?.genesis ? await record(first.genesis) : {});
|
|
180
|
+
const hex = (k) => (k instanceof Uint8Array ? Buffer.from(k).toString("hex") : typeof k === "string" ? k : "");
|
|
181
|
+
const book = [];
|
|
182
|
+
if (head("peers")) {
|
|
183
|
+
for (const e of (await record(head("peers")))?.peers ?? []) {
|
|
184
|
+
const p = await record(e.peer);
|
|
185
|
+
book.push({ key: hex(p.key ?? e.key), transport: p.transport ?? "mailbox", address: p.address ?? "", ...(p.handle ? { handle: p.handle } : {}), ...(p.domain ? { domain: p.domain } : {}), ...(p.source ? { source: p.source } : {}) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const chain = await read(`/thread/${dispatchOrigin()}`);
|
|
189
|
+
const dispatch = fold((chain?.updates ?? []).map((u) => u.record).filter((u) => u && u.row));
|
|
190
|
+
const store = {
|
|
191
|
+
get: async (cid) => { const v = await record(cid); if (v === undefined)
|
|
192
|
+
throw new Error(`not found: ${cid}`); return v; },
|
|
193
|
+
has: async (cid) => (await record(cid)) !== undefined,
|
|
194
|
+
bytes: async (cid) => { const v = await record(cid); if (!(v instanceof Uint8Array))
|
|
195
|
+
throw new Error(`not found: ${cid}`); return v; },
|
|
196
|
+
putBlock: async () => { },
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
store, heads, dispatch, addressBook: book,
|
|
200
|
+
identity: hex(genesis.identity), programs: genesis.programs ?? {},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A grant (#142, #143; `skein-host grant`): the kernel's `grant` operation — `role` (root by default,
|
|
205
|
+
* or an app's `<app>.<role>`) to `key`, or with `remove` from it. One message to box `grant`, sent by
|
|
206
|
+
* root. Nothing is sent when the grants already say so.
|
|
207
|
+
*/
|
|
208
|
+
export async function planGrant(view, key, o = {}) {
|
|
209
|
+
if (!isKey(key))
|
|
210
|
+
throw new Error(`${key}: not an identity key (hex)`);
|
|
211
|
+
const role = o.role ?? "root";
|
|
212
|
+
if (role !== "root" && !/^[a-z0-9][a-z0-9._-]*\.[a-z0-9][a-z0-9_-]*$/.test(role))
|
|
213
|
+
throw new Error(`${role}: a role is root or <app>.<role> (user is any principal: never granted)`);
|
|
214
|
+
const op = o.remove ? "remove" : "add";
|
|
215
|
+
const root = view.heads.find((h) => h.name === "grants")?.root;
|
|
216
|
+
const g = root ? await view.store.get(root).catch(() => undefined) : undefined;
|
|
217
|
+
const holds = (g?.roles?.[role] ?? []).some((k) => (k instanceof Uint8Array ? Buffer.from(k).toString("hex") : String(k)) === key);
|
|
218
|
+
if (holds === (op === "add"))
|
|
219
|
+
return { prompt: [`grant ${op} ${role} ${key}: the grants say so already`], recipient: view.identity, messages: [] };
|
|
220
|
+
return { prompt: [`grant ${op} ${role} ${key}`], recipient: view.identity, messages: [{ box: "grant", body: { op, role, principal: keyBytes(key) } }] };
|
|
221
|
+
}
|
|
222
|
+
/** The catalog in the instance's own site tree (its app `site`: www/catalog.json), and the names in it. */
|
|
223
|
+
export async function catalogOf(view) {
|
|
224
|
+
const head = view.heads.find((h) => h.name === appHead("site"))?.root;
|
|
225
|
+
const rec = head ? await view.store.get(head).catch(() => undefined) : undefined;
|
|
226
|
+
if (rec?.kind !== "app")
|
|
227
|
+
throw new Error("the instance has no site app (site/app): no catalog to name an app from; give <url>#<commit>");
|
|
228
|
+
const leaf = await lookup(view.store, rec.tree, "www/catalog.json");
|
|
229
|
+
if (!leaf)
|
|
230
|
+
throw new Error(`the site app ${rec.version} has no www/catalog.json`);
|
|
231
|
+
const c = JSON.parse(new TextDecoder().decode(await readBlob(view.store, leaf.cid)));
|
|
232
|
+
return (c.apps ?? []).filter((e) => e && typeof e.name === "string" && typeof e.url === "string" && /^[0-9a-f]{40}$/i.test(e.hash ?? ""));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The kernel's gate (#143, kernel-zig/src/grants.zig), read from the instance's view: the roles that
|
|
236
|
+
* gate a route's handler (its app's `roles` for its fn; a kernel admin box's is root) and whether
|
|
237
|
+
* `key` passes them. Root passes anything; `user` passes any key; no gating roles: open.
|
|
238
|
+
*/
|
|
239
|
+
export async function gatePasses(view, row, key) {
|
|
240
|
+
const root = view.heads.find((h) => h.name === "grants")?.root;
|
|
241
|
+
const g = root ? await view.store.get(root).catch(() => undefined) : undefined;
|
|
242
|
+
const holds = (role) => (g?.roles?.[role] ?? []).some((k) => (k instanceof Uint8Array ? Buffer.from(k).toString("hex") : String(k)) === key);
|
|
243
|
+
const r = row;
|
|
244
|
+
let roles = [];
|
|
245
|
+
if (r.op)
|
|
246
|
+
roles = ["root"];
|
|
247
|
+
else if (r.app && r.fn) {
|
|
248
|
+
const { appRecordIn } = await import("../host/plan.js");
|
|
249
|
+
const rec = await appRecordIn(view, r.app);
|
|
250
|
+
const map = rec?.record?.roles ?? {};
|
|
251
|
+
roles = Object.entries(map).filter(([, fns]) => fns.includes(r.fn)).map(([role]) => role === "root" || role === "user" ? role : `${r.app}.${role}`);
|
|
252
|
+
}
|
|
253
|
+
if (!roles.length || holds("root") || roles.includes("user") || roles.some((x) => x !== "root" && holds(x)))
|
|
254
|
+
return { pass: true, roles };
|
|
255
|
+
return { pass: false, roles };
|
|
256
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CID } from "multiformats/cid";
|
|
2
|
+
export declare const BUNDLE_LIMIT: number;
|
|
3
|
+
export interface Rec {
|
|
4
|
+
cid: CID;
|
|
5
|
+
bytes: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export declare function chunk(records: Iterable<Rec>, limit?: number, root?: CID): Generator<Uint8Array>;
|
|
8
|
+
export declare function encodeBundle(records: Rec[], root?: CID): Uint8Array;
|
|
9
|
+
export declare function decodeBundle(bytes: Uint8Array): Rec[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Records into ≤1 MiB dag-cbor bundles: { records: [{ cid, bytes }], root? }; `root`
|
|
2
|
+
// (the imported tree) rides on the last bundle only. A record
|
|
3
|
+
// that alone exceeds the limit travels alone in an oversized bundle (the
|
|
4
|
+
// messagebox takes up to its body limit; 1sat serve: 30 MB of JSON).
|
|
5
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
6
|
+
export const BUNDLE_LIMIT = 1 << 20;
|
|
7
|
+
// dag-cbor overhead per record: map header, two keys, a tag-42 CID (<=~45 bytes) and a byte-string header.
|
|
8
|
+
const PER_RECORD = 64;
|
|
9
|
+
const HEADER = 16;
|
|
10
|
+
export function* chunk(records, limit = BUNDLE_LIMIT, root) {
|
|
11
|
+
let batch = [];
|
|
12
|
+
let size = HEADER;
|
|
13
|
+
for (const r of records) {
|
|
14
|
+
const n = r.bytes.length + PER_RECORD;
|
|
15
|
+
if (batch.length && size + n > limit) {
|
|
16
|
+
yield encodeBundle(batch);
|
|
17
|
+
batch = [];
|
|
18
|
+
size = HEADER;
|
|
19
|
+
}
|
|
20
|
+
batch.push(r);
|
|
21
|
+
size += n;
|
|
22
|
+
}
|
|
23
|
+
if (batch.length)
|
|
24
|
+
yield encodeBundle(batch, root);
|
|
25
|
+
}
|
|
26
|
+
export function encodeBundle(records, root) {
|
|
27
|
+
return dagCbor.encode({ records: records.map((r) => ({ cid: r.cid, bytes: r.bytes })), ...(root && { root }) });
|
|
28
|
+
}
|
|
29
|
+
export function decodeBundle(bytes) {
|
|
30
|
+
const v = dagCbor.decode(bytes);
|
|
31
|
+
if (!v || !Array.isArray(v.records))
|
|
32
|
+
throw new Error("not a bundle");
|
|
33
|
+
return v.records;
|
|
34
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type Command = {
|
|
2
|
+
cmd: "whoami";
|
|
3
|
+
} | {
|
|
4
|
+
cmd: "import";
|
|
5
|
+
dir: string;
|
|
6
|
+
} | {
|
|
7
|
+
cmd: "run";
|
|
8
|
+
tree?: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
env?: Record<string, string>;
|
|
11
|
+
line: string;
|
|
12
|
+
} | {
|
|
13
|
+
cmd: "head";
|
|
14
|
+
name: string;
|
|
15
|
+
tree: string;
|
|
16
|
+
} | {
|
|
17
|
+
cmd: "inbox";
|
|
18
|
+
wait: boolean;
|
|
19
|
+
timeout: number;
|
|
20
|
+
ack: boolean;
|
|
21
|
+
json: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
cmd: "chat";
|
|
24
|
+
text: string;
|
|
25
|
+
tree?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
fresh: boolean;
|
|
28
|
+
wait: boolean;
|
|
29
|
+
timeout: number;
|
|
30
|
+
} | {
|
|
31
|
+
cmd: "talk";
|
|
32
|
+
tree?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
fresh: boolean;
|
|
35
|
+
timeout: number;
|
|
36
|
+
} | {
|
|
37
|
+
cmd: "help";
|
|
38
|
+
};
|
|
39
|
+
export declare const USAGE = "usage:\n skein whoami\n skein import <dir>\n skein run [--tree <cid>] [--cwd <path>] [--env K=V]... -- '<cmd>'\n skein head <name> <tree-cid>\n skein inbox [--wait] [--timeout <seconds>] [--no-ack] [--json]\n skein chat \"<text>\" [--tree <cid>] [--model <m>] [--new] [--wait] [--timeout <seconds>]\n skein talk [--tree <cid>] [--model <m>] [--new] [--timeout <seconds>]\n skein install|uninstall|routes|grant|peers|host|claim|deploy \u2026 the operator's admin messages, signed with its key\n and sent (skein install --help)";
|
|
40
|
+
export declare const DEFAULT_TIMEOUT = 120;
|
|
41
|
+
export declare function parseCli(argv: string[]): Command;
|
|
42
|
+
/** A `chat` reply as text: the text line, the page (markdown, as is), then tree/thread. */
|
|
43
|
+
export declare function formatReply(body: Record<string, unknown> | undefined): string;
|
|
44
|
+
export declare function main(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// skein — David's client. Signs as David through his wallet-api; raw BRC-33 on
|
|
2
|
+
// a BRC-104 session (#40): sends to the instance's front door
|
|
3
|
+
// (SKEIN_INSTANCE_URL, default $SKEIN_HOST_URL/@<handle>), reads David's
|
|
4
|
+
// mailbox instance (SKEIN_MAILBOX_URL, ~/.skein/mailbox.url, default
|
|
5
|
+
// $SKEIN_HOST_URL/@david). See scripts/host/README.md for the processes it expects.
|
|
6
|
+
//
|
|
7
|
+
// skein whoami
|
|
8
|
+
// skein import <dir>
|
|
9
|
+
// skein run [--tree <cid>] [--cwd <path>] [--env K=V]... -- '<cmd>'
|
|
10
|
+
// skein head <name> <tree-cid>
|
|
11
|
+
// skein inbox [--wait] [--timeout <s>] [--no-ack] [--json]
|
|
12
|
+
// skein chat "<text>" [--tree <cid>] [--model <m>] [--new] [--wait] [--timeout <s>]
|
|
13
|
+
// skein talk [--tree <cid>] [--model <m>] [--new] [--timeout <s>]
|
|
14
|
+
// skein install|uninstall|routes|grant|peers|host|claim|deploy … the operator's admin messages,
|
|
15
|
+
// signed here with the operator's key and sent (#142: src/client/admin-cli.ts)
|
|
16
|
+
import { parseArgs } from "node:util";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
import { loadConfig } from "./config.js";
|
|
19
|
+
import { BOX, SkeinClient } from "./client.js";
|
|
20
|
+
import { parseReplLine, parseReply, short } from "./conversation.js";
|
|
21
|
+
export const USAGE = `usage:
|
|
22
|
+
skein whoami
|
|
23
|
+
skein import <dir>
|
|
24
|
+
skein run [--tree <cid>] [--cwd <path>] [--env K=V]... -- '<cmd>'
|
|
25
|
+
skein head <name> <tree-cid>
|
|
26
|
+
skein inbox [--wait] [--timeout <seconds>] [--no-ack] [--json]
|
|
27
|
+
skein chat "<text>" [--tree <cid>] [--model <m>] [--new] [--wait] [--timeout <seconds>]
|
|
28
|
+
skein talk [--tree <cid>] [--model <m>] [--new] [--timeout <seconds>]
|
|
29
|
+
skein install|uninstall|routes|grant|peers|host|claim|deploy … the operator's admin messages, signed with its key
|
|
30
|
+
and sent (skein install --help)`;
|
|
31
|
+
export const DEFAULT_TIMEOUT = 120;
|
|
32
|
+
function timeoutOf(cmd, v) {
|
|
33
|
+
const t = v === undefined ? DEFAULT_TIMEOUT : Number(v);
|
|
34
|
+
if (!Number.isFinite(t) || t <= 0)
|
|
35
|
+
throw new Error(`${cmd}: bad --timeout ${v}`);
|
|
36
|
+
return t;
|
|
37
|
+
}
|
|
38
|
+
const convOptions = { tree: { type: "string" }, model: { type: "string" }, new: { type: "boolean" }, timeout: { type: "string" } };
|
|
39
|
+
export function parseCli(argv) {
|
|
40
|
+
const [sub, ...rest] = argv;
|
|
41
|
+
switch (sub) {
|
|
42
|
+
case undefined:
|
|
43
|
+
case "help":
|
|
44
|
+
case "-h":
|
|
45
|
+
case "--help":
|
|
46
|
+
return { cmd: "help" };
|
|
47
|
+
case "whoami":
|
|
48
|
+
parseArgs({ args: rest, options: {}, allowPositionals: false });
|
|
49
|
+
return { cmd: "whoami" };
|
|
50
|
+
case "import": {
|
|
51
|
+
const { positionals } = parseArgs({ args: rest, options: {}, allowPositionals: true });
|
|
52
|
+
if (positionals.length !== 1)
|
|
53
|
+
throw new Error("import: expected exactly one <dir>");
|
|
54
|
+
return { cmd: "import", dir: positionals[0] };
|
|
55
|
+
}
|
|
56
|
+
case "run": {
|
|
57
|
+
const dd = rest.indexOf("--");
|
|
58
|
+
if (dd < 0)
|
|
59
|
+
throw new Error("run: the command goes after `--`");
|
|
60
|
+
const line = rest.slice(dd + 1).join(" ");
|
|
61
|
+
if (!line.trim())
|
|
62
|
+
throw new Error("run: empty command");
|
|
63
|
+
const { values, positionals } = parseArgs({
|
|
64
|
+
args: rest.slice(0, dd),
|
|
65
|
+
options: { tree: { type: "string" }, cwd: { type: "string" }, env: { type: "string", multiple: true } },
|
|
66
|
+
allowPositionals: true,
|
|
67
|
+
});
|
|
68
|
+
if (positionals.length)
|
|
69
|
+
throw new Error(`run: unexpected ${positionals.join(" ")} (the command goes after \`--\`)`);
|
|
70
|
+
let env;
|
|
71
|
+
for (const kv of values.env ?? []) {
|
|
72
|
+
const i = kv.indexOf("=");
|
|
73
|
+
if (i <= 0)
|
|
74
|
+
throw new Error(`run: --env wants K=V, got ${kv}`);
|
|
75
|
+
(env ??= {})[kv.slice(0, i)] = kv.slice(i + 1);
|
|
76
|
+
}
|
|
77
|
+
return { cmd: "run", line, ...(values.tree !== undefined && { tree: values.tree }), ...(values.cwd !== undefined && { cwd: values.cwd }), ...(env && { env }) };
|
|
78
|
+
}
|
|
79
|
+
case "head": {
|
|
80
|
+
const { positionals } = parseArgs({ args: rest, options: {}, allowPositionals: true });
|
|
81
|
+
if (positionals.length !== 2)
|
|
82
|
+
throw new Error("head: expected <name> <tree-cid>");
|
|
83
|
+
return { cmd: "head", name: positionals[0], tree: positionals[1] };
|
|
84
|
+
}
|
|
85
|
+
case "inbox": {
|
|
86
|
+
const { values } = parseArgs({
|
|
87
|
+
args: rest,
|
|
88
|
+
options: { wait: { type: "boolean" }, timeout: { type: "string" }, "no-ack": { type: "boolean" }, json: { type: "boolean" } },
|
|
89
|
+
allowPositionals: false,
|
|
90
|
+
});
|
|
91
|
+
return { cmd: "inbox", wait: !!values.wait, timeout: timeoutOf("inbox", values.timeout), ack: !values["no-ack"], json: !!values.json };
|
|
92
|
+
}
|
|
93
|
+
case "chat": {
|
|
94
|
+
const { values, positionals } = parseArgs({ args: rest, options: { ...convOptions, wait: { type: "boolean" } }, allowPositionals: true });
|
|
95
|
+
const text = positionals.join(" ");
|
|
96
|
+
if (!text.trim())
|
|
97
|
+
throw new Error('chat: expected "<text>"');
|
|
98
|
+
return {
|
|
99
|
+
cmd: "chat", text, fresh: !!values.new, wait: !!values.wait, timeout: timeoutOf("chat", values.timeout),
|
|
100
|
+
...(values.tree !== undefined && { tree: values.tree }), ...(values.model !== undefined && { model: values.model }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case "talk": {
|
|
104
|
+
const { values } = parseArgs({ args: rest, options: convOptions, allowPositionals: false });
|
|
105
|
+
return {
|
|
106
|
+
cmd: "talk", fresh: !!values.new, timeout: timeoutOf("talk", values.timeout),
|
|
107
|
+
...(values.tree !== undefined && { tree: values.tree }), ...(values.model !== undefined && { model: values.model }),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
throw new Error(`unknown command: ${sub}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------- printing
|
|
115
|
+
function text(v) {
|
|
116
|
+
if (v instanceof Uint8Array)
|
|
117
|
+
return new TextDecoder().decode(v);
|
|
118
|
+
return v === undefined || v === null ? "" : String(v);
|
|
119
|
+
}
|
|
120
|
+
/** A `chat` reply as text: the text line, the page (markdown, as is), then tree/thread. */
|
|
121
|
+
export function formatReply(body) {
|
|
122
|
+
const s = parseReply(body);
|
|
123
|
+
const out = [s.text];
|
|
124
|
+
if (s.page)
|
|
125
|
+
out.push("", s.page.replace(/\s+$/, ""), "");
|
|
126
|
+
const ids = [s.tree && `tree ${short(s.tree.toString())}`, s.thread && `thread ${short(s.thread.toString())}`].filter(Boolean);
|
|
127
|
+
if (ids.length)
|
|
128
|
+
out.push(` [${ids.join(" ")}]`);
|
|
129
|
+
return out.join("\n");
|
|
130
|
+
}
|
|
131
|
+
function show(r, json) {
|
|
132
|
+
if (json) {
|
|
133
|
+
console.log(JSON.stringify(r, (_k, v) => (v instanceof Uint8Array ? text(v) : v)));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const b = r.body ?? {};
|
|
137
|
+
console.log(`— ${r.box} ${r.created ?? "?"} from ${r.sender.slice(0, 16)}… ${r.verified ? "verified" : "UNVERIFIED"} msg ${r.messageId.slice(0, 12)}`);
|
|
138
|
+
if (r.error) {
|
|
139
|
+
console.log(` error: ${r.error}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (r.box === BOX.chat && b.replyTo !== undefined) {
|
|
143
|
+
try {
|
|
144
|
+
console.log(formatReply(b));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
console.log(` malformed reply: ${e.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (r.box === BOX.chat && typeof b.text === "string") {
|
|
152
|
+
console.log(` (new conversation) ${b.text}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (b.replyTo !== undefined)
|
|
156
|
+
console.log(` replyTo: ${text(b.replyTo)}`);
|
|
157
|
+
if (b.exitCode !== undefined)
|
|
158
|
+
console.log(` exitCode: ${text(b.exitCode)}`);
|
|
159
|
+
if (b.tree !== undefined)
|
|
160
|
+
console.log(` tree: ${text(b.tree)}`);
|
|
161
|
+
for (const k of ["stdout", "stderr"]) {
|
|
162
|
+
const s = text(b[k]);
|
|
163
|
+
if (s)
|
|
164
|
+
console.log(` ${k}:\n${s.replace(/^/gm, " ").replace(/\s+$/, "")}`);
|
|
165
|
+
}
|
|
166
|
+
const other = Object.keys(b).filter((k) => !["replyTo", "exitCode", "tree", "stdout", "stderr"].includes(k));
|
|
167
|
+
for (const k of other)
|
|
168
|
+
console.log(` ${k}: ${JSON.stringify(b[k], (_k, v) => (v instanceof Uint8Array ? text(v) : v))}`);
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------- main
|
|
171
|
+
export async function main(argv) {
|
|
172
|
+
// #142: the operator's admin messages, signed in this process with its key (host.env configures where it is).
|
|
173
|
+
const { ADMIN_COMMANDS, ADMIN_USAGE, adminMain } = await import("./admin-cli.js");
|
|
174
|
+
if (ADMIN_COMMANDS.includes(argv[0] ?? "")) {
|
|
175
|
+
const { withHostEnv } = await import("../host/hostenv.js");
|
|
176
|
+
const env = { vars: withHostEnv(process.env), out: (l) => process.stdout.write(`${l}\n`), err: (l) => process.stderr.write(`${l}\n`) };
|
|
177
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
178
|
+
env.out(ADMIN_USAGE);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
return adminMain(argv[0], argv.slice(1), env);
|
|
182
|
+
}
|
|
183
|
+
const c = parseCli(argv);
|
|
184
|
+
if (c.cmd === "help") {
|
|
185
|
+
console.log(USAGE);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const cfg = loadConfig();
|
|
189
|
+
const client = new SkeinClient(cfg);
|
|
190
|
+
switch (c.cmd) {
|
|
191
|
+
case "whoami": {
|
|
192
|
+
console.log(`owner (you): ${await client.identityKey()} via ${cfg.walletUrl} as ${cfg.originator}`);
|
|
193
|
+
console.log(`instance: ${cfg.instance.identityKey} ${cfg.instance.handle}@${cfg.instance.domain}`);
|
|
194
|
+
console.log(`instance url: ${cfg.instanceUrl}`);
|
|
195
|
+
console.log(`mailbox: ${cfg.mailboxUrl}`);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
case "import": {
|
|
199
|
+
const r = await client.importDir(c.dir, (i, n, bytes) => process.stderr.write(`bundle ${i + 1}/${n} (${bytes} bytes)\n`));
|
|
200
|
+
process.stderr.write(`${r.records} objects in ${r.bundles.length} message(s) to box objects\n`);
|
|
201
|
+
console.log(r.root.toString());
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
case "run": {
|
|
205
|
+
const s = await client.run({ tree: c.tree, cmd: c.line, cwd: c.cwd, env: c.env });
|
|
206
|
+
console.log(s.cid);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
case "head": {
|
|
210
|
+
const s = await client.head(c.name, c.tree);
|
|
211
|
+
console.log(s.cid);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
case "inbox": {
|
|
215
|
+
if (!c.wait) {
|
|
216
|
+
for (const r of await client.inbox({ ack: c.ack }))
|
|
217
|
+
show(r, c.json);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
const want = [];
|
|
221
|
+
const chat = client.lastSent(BOX.chat), run = client.lastSent(BOX.run);
|
|
222
|
+
if (chat)
|
|
223
|
+
want.push({ box: BOX.chat, replyTo: chat.cid });
|
|
224
|
+
if (run)
|
|
225
|
+
want.push({ box: BOX.results, replyTo: run.cid });
|
|
226
|
+
if (!want.length)
|
|
227
|
+
throw new Error("inbox --wait: no chat or run sent from this machine yet (~/.skein/client/sent.jsonl)");
|
|
228
|
+
return wait(client, want, c.timeout, (r) => show(r, c.json), c.ack);
|
|
229
|
+
}
|
|
230
|
+
case "chat": {
|
|
231
|
+
const s = await client.chat(c);
|
|
232
|
+
if (!c.wait) {
|
|
233
|
+
console.log(s.cid);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
process.stderr.write(`sent ${short(s.cid)}${s.replyTo ? ` (reply to ${short(s.replyTo)})` : " (new conversation)"}\n`);
|
|
237
|
+
return wait(client, [{ box: BOX.chat, replyTo: s.cid }], c.timeout, (r) => show(r, false));
|
|
238
|
+
}
|
|
239
|
+
case "talk":
|
|
240
|
+
return talk(client, c);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function wait(client, want, timeout, onResult, ack = true) {
|
|
244
|
+
const hit = await client.waitFor(want, { timeoutMs: timeout * 1000, ack, onResult });
|
|
245
|
+
if (hit)
|
|
246
|
+
return 0;
|
|
247
|
+
console.error(`no answer to ${want.map((w) => `${w.box}←${short(w.replyTo)}`).join(" or ")} within ${timeout}s`);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
/** The REPL: each line is a chat continuing the last reply; waits for the reply to it. */
|
|
251
|
+
async function talk(client, c) {
|
|
252
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
253
|
+
let fresh = c.fresh, tree = c.tree;
|
|
254
|
+
console.log("talk: /new, /tree <cid>, /quit");
|
|
255
|
+
try {
|
|
256
|
+
for (;;) {
|
|
257
|
+
let line;
|
|
258
|
+
try {
|
|
259
|
+
line = await rl.question("> ");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return 0;
|
|
263
|
+
} // stdin closed
|
|
264
|
+
const cmd = parseReplLine(line);
|
|
265
|
+
switch (cmd.kind) {
|
|
266
|
+
case "empty": continue;
|
|
267
|
+
case "quit": return 0;
|
|
268
|
+
case "error":
|
|
269
|
+
console.log(cmd.message);
|
|
270
|
+
continue;
|
|
271
|
+
case "new":
|
|
272
|
+
fresh = true;
|
|
273
|
+
tree = undefined;
|
|
274
|
+
console.log("(new conversation)");
|
|
275
|
+
continue;
|
|
276
|
+
case "tree":
|
|
277
|
+
tree = cmd.cid;
|
|
278
|
+
console.log(`(tree ${short(cmd.cid)} for the next message)`);
|
|
279
|
+
continue;
|
|
280
|
+
case "chat": {
|
|
281
|
+
const s = await client.chat({ text: cmd.text, fresh, tree, model: c.model });
|
|
282
|
+
fresh = false;
|
|
283
|
+
tree = undefined; // after this, the reply's tree carries the conversation
|
|
284
|
+
const hit = await client.waitFor([{ box: BOX.chat, replyTo: s.cid }], {
|
|
285
|
+
timeoutMs: c.timeout * 1000,
|
|
286
|
+
onResult: (r) => { if (r.box === BOX.chat && String(r.body?.replyTo) === s.cid && !r.error)
|
|
287
|
+
console.log(formatReply(r.body));
|
|
288
|
+
else
|
|
289
|
+
show(r, false); },
|
|
290
|
+
});
|
|
291
|
+
if (!hit)
|
|
292
|
+
console.log(`(no answer within ${c.timeout}s; \`skein inbox\` picks it up later)`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
rl.close();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (import.meta.main) {
|
|
302
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
|
|
303
|
+
console.error(`skein: ${e.message}`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
306
|
+
}
|