@quo-systems/dock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +27 -0
  4. package/beings/avatar.ts +84 -0
  5. package/beings/desk.ts +46 -0
  6. package/beings/index.ts +5 -0
  7. package/beings/quo-dock.md +698 -0
  8. package/beings/setup.ts +23 -0
  9. package/beings/side.ts +51 -0
  10. package/beings/user.ts +78 -0
  11. package/cli/client.ts +65 -0
  12. package/cli/daemon.ts +582 -0
  13. package/cli/quo.ts +126 -0
  14. package/dist/beings/avatar.d.ts +32 -0
  15. package/dist/beings/avatar.js +82 -0
  16. package/dist/beings/desk.d.ts +42 -0
  17. package/dist/beings/desk.js +43 -0
  18. package/dist/beings/index.d.ts +3 -0
  19. package/dist/beings/index.js +5 -0
  20. package/dist/beings/setup.d.ts +2 -0
  21. package/dist/beings/setup.js +16 -0
  22. package/dist/beings/side.d.ts +25 -0
  23. package/dist/beings/side.js +37 -0
  24. package/dist/beings/user.d.ts +108 -0
  25. package/dist/beings/user.js +74 -0
  26. package/dist/cli/client.d.ts +14 -0
  27. package/dist/cli/client.js +51 -0
  28. package/dist/cli/daemon.d.ts +57 -0
  29. package/dist/cli/daemon.js +581 -0
  30. package/dist/cli/quo.d.ts +2 -0
  31. package/dist/cli/quo.js +121 -0
  32. package/dist/harbor/browser.d.ts +13 -0
  33. package/dist/harbor/browser.js +50 -0
  34. package/dist/harbor/disk.d.ts +19 -0
  35. package/dist/harbor/disk.js +126 -0
  36. package/dist/harbor/edge/edge.d.ts +26 -0
  37. package/dist/harbor/edge/edge.js +123 -0
  38. package/dist/harbor/edge/exercise.d.ts +7 -0
  39. package/dist/harbor/edge/exercise.js +179 -0
  40. package/dist/harbor/edge/storage.d.ts +20 -0
  41. package/dist/harbor/edge/storage.js +81 -0
  42. package/dist/harbor/edge/worker.d.ts +14 -0
  43. package/dist/harbor/edge/worker.js +47 -0
  44. package/dist/harbor/files.d.ts +14 -0
  45. package/dist/harbor/files.js +83 -0
  46. package/dist/harbor/idb.d.ts +15 -0
  47. package/dist/harbor/idb.js +85 -0
  48. package/dist/human/dom.d.ts +2 -0
  49. package/dist/human/dom.js +15 -0
  50. package/dist/human/html.d.ts +32 -0
  51. package/dist/human/html.js +136 -0
  52. package/dist/human/screen.d.ts +11 -0
  53. package/dist/human/screen.js +66 -0
  54. package/dist/human/tab.d.ts +7 -0
  55. package/dist/human/tab.js +68 -0
  56. package/dist/mcp/agent.d.ts +10 -0
  57. package/dist/mcp/agent.js +78 -0
  58. package/dist/mcp/http.d.ts +27 -0
  59. package/dist/mcp/http.js +96 -0
  60. package/dist/mcp/oauth.d.ts +98 -0
  61. package/dist/mcp/oauth.js +241 -0
  62. package/dist/mcp/pilot.d.ts +6 -0
  63. package/dist/mcp/pilot.js +68 -0
  64. package/dist/mcp/runner.d.ts +45 -0
  65. package/dist/mcp/runner.js +94 -0
  66. package/dist/mcp/server.d.ts +9 -0
  67. package/dist/mcp/server.js +63 -0
  68. package/dist/mcp/web/exchange.d.ts +23 -0
  69. package/dist/mcp/web/exchange.js +136 -0
  70. package/harbor/browser.ts +63 -0
  71. package/harbor/disk.ts +120 -0
  72. package/harbor/edge/edge.ts +147 -0
  73. package/harbor/edge/exercise.ts +187 -0
  74. package/harbor/edge/platform.d.ts +28 -0
  75. package/harbor/edge/storage.ts +82 -0
  76. package/harbor/edge/worker.ts +47 -0
  77. package/harbor/edge/wrangler.toml +21 -0
  78. package/harbor/files.ts +89 -0
  79. package/harbor/idb.ts +90 -0
  80. package/harbor/quo-harbor.md +403 -0
  81. package/human/dom.ts +21 -0
  82. package/human/html.ts +164 -0
  83. package/human/quo-human.md +86 -0
  84. package/human/screen.ts +76 -0
  85. package/human/tab.ts +84 -0
  86. package/mcp/agent.ts +94 -0
  87. package/mcp/http.ts +96 -0
  88. package/mcp/oauth.ts +260 -0
  89. package/mcp/pilot.ts +73 -0
  90. package/mcp/quo-mcp.md +262 -0
  91. package/mcp/runner.ts +131 -0
  92. package/mcp/server.ts +67 -0
  93. package/mcp/web/exchange.ts +146 -0
  94. package/package.json +89 -0
package/cli/quo.ts ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // `quo`: the one command of the estate. Thin on purpose: each subcommand
4
+ // picks a folder, reaches the daemon or the disk, and hands off. Logic that
5
+ // appears here was missing from a being or a side, and moves there.
6
+ //
7
+ // quo init [--dir D] [--ward W] mint a seed, boot a ward over a disk store, write both
8
+ // quo serve [--dir D] [--http PORT] [--dial URL] the daemon: harbor from disk, the two sockets, the HTTP door on loopback, a socket held to URL
9
+ // quo census [--dir D] [--ward W] [--via S] the empty ask: pk and every being
10
+ // quo boot [--dir D] [--ward W] [--via S] <json> { key, class, public? }
11
+ // quo invite [--dir D] [--ward W] [--via S] <json> { being, id }
12
+ // quo knock [--dir D] [--ward W] [--via S] <json> { being | { boot, key }, id, invitation, method?, args?, wanted? }
13
+ // quo remove [--dir D] [--ward W] [--via S] <json> { being, id }: a relation out of a being; on the ward pk, an owner
14
+ // quo side [--dir D] [--ward W] --as NAME an avatar over stdio for a local MCP client; NAME is the client identity
15
+ // quo run [--dir D] [--ward W] --as NAME --url URL --model NAME [--turns N] a human at a terminal talking to a model through an avatar: a line in, the model's final text out
16
+ // quo pilot [--dir D] [--ward W] [--via S] the owner pilot over stdio: census, boot, invite, knock, remove
17
+ // quo reach [--dir D] <pk> <url> a hint for the harbor's directory: that ward pk lives at that quo. route
18
+ //
19
+ // D defaults to $QUO_DIR, then ~/.quo. W defaults to main. init takes
20
+ // --user NAME for the user being, defaulting to the device's user. run
21
+ // takes the model's base URL as --url or $QUO_MODEL_URL, its name as
22
+ // --model or $QUO_MODEL, a bearer from $QUO_MODEL_KEY, read here and never
23
+ // kept, and --turns as the ceiling of model calls in one turn. S is a
24
+ // standing the user being holds at another ward, one the root there invited
25
+ // as an owner: with --via, the owner asks go to that ward's door, sealed,
26
+ // and the answers are its. Without it they are the root's, on the pointer.
27
+ import { homedir, userInfo } from 'node:os';
28
+ import { join } from 'node:path';
29
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
30
+ import { DiskHarbor } from '../harbor/disk.ts';
31
+ import { serve } from './daemon.ts';
32
+ import { ask, side, reach, type Run } from './client.ts';
33
+ import { pilotSide } from '../mcp/pilot.ts';
34
+
35
+ const OWNER_ASKS = new Set(['boot', 'invite', 'knock', 'remove']);
36
+
37
+ function parse(argv: string[]) {
38
+ const flags: Record<string, string> = {};
39
+ const rest: string[] = [];
40
+ for (let i = 0; i < argv.length; i++) {
41
+ const a = argv[i] ?? '';
42
+ if (a.startsWith('--')) flags[a.slice(2)] = argv[++i] ?? '';
43
+ else rest.push(a);
44
+ }
45
+ const [cmd, json, more] = rest;
46
+ return { cmd, json, more, flags, dir: flags.dir ?? process.env.QUO_DIR ?? join(homedir(), '.quo'), ward: flags.ward ?? 'main', via: flags.via };
47
+ }
48
+
49
+ const print = (v: unknown) => console.log(JSON.stringify(v, null, 2));
50
+
51
+ async function main(argv: string[]): Promise<number> {
52
+ const { cmd, json, more, flags, dir, ward, via } = parse(argv);
53
+ if (cmd === 'init') {
54
+ print(await DiskHarbor.init(dir, ward, flags.user ?? userInfo().username));
55
+ return 0;
56
+ }
57
+ if (cmd === 'side' || cmd === 'run') {
58
+ if (!flags.as) {
59
+ console.error(`quo ${cmd} needs --as NAME`);
60
+ return 2;
61
+ }
62
+ let run: Run | undefined;
63
+ if (cmd === 'run') {
64
+ const url = flags.url ?? process.env.QUO_MODEL_URL;
65
+ const model = flags.model ?? process.env.QUO_MODEL;
66
+ if (!url || !model) {
67
+ console.error('quo run needs --url URL and --model NAME, or QUO_MODEL_URL and QUO_MODEL');
68
+ return 2;
69
+ }
70
+ const key = process.env.QUO_MODEL_KEY;
71
+ const turns = flags.turns !== undefined ? Number(flags.turns) : undefined;
72
+ if (turns !== undefined && !(Number.isInteger(turns) && turns > 0)) {
73
+ console.error('--turns is a positive integer');
74
+ return 2;
75
+ }
76
+ run = { url, model, ...(key ? { key } : {}), ...(turns !== undefined ? { turns } : {}) };
77
+ }
78
+ await side(dir, flags.as, ward, run); // stdio to the daemon, until one end closes
79
+ return -1;
80
+ }
81
+ if (cmd === 'pilot') {
82
+ await ask(dir, undefined, {}, ward); // no daemon is a clear word now, not at the first tool call
83
+ const serving = await pilotSide(dir, ward, new StdioServerTransport(), (line) => console.error(`pilot ${line}`), via);
84
+ process.stdin.once('end', () => void serving.close().then(() => process.exit(0)));
85
+ return -1;
86
+ }
87
+ if (cmd === 'serve') {
88
+ const port = flags.http ?? process.env.QUO_HTTP;
89
+ const s = await serve(dir, { ...(port !== undefined ? { http: { port: Number(port) } } : {}), ...(flags.dial ? { dial: [flags.dial] } : {}) });
90
+ print({ dir: s.harbor.dir, sock: s.sock, side: s.side, http: s.http ? `http://${s.http.host}:${s.http.port}` : null, dial: s.quo.dialers.map((d) => d.url), wards: Object.fromEntries([...s.harbor.wards].map(([n, h]) => [n, h.pk])) });
91
+ const stop = () => void s.close().then(() => process.exit(0));
92
+ process.once('SIGINT', stop);
93
+ process.once('SIGTERM', stop);
94
+ return -1; // stays up
95
+ }
96
+ if (cmd === 'reach') {
97
+ if (!json || !more) {
98
+ console.error('quo reach needs <pk> <url>');
99
+ return 2;
100
+ }
101
+ const out = await reach(dir, json, more);
102
+ print('error' in out ? { error: out.error } : out.result);
103
+ return 'error' in out ? 1 : 0;
104
+ }
105
+ if (cmd === 'census' || (cmd !== undefined && OWNER_ASKS.has(cmd))) {
106
+ const args = json ? (JSON.parse(json) as Record<string, unknown>) : {};
107
+ const out = await ask(dir, cmd === 'census' ? undefined : cmd, args, ward, via);
108
+ print('error' in out ? { error: out.error } : out.result);
109
+ // an error object the ward answered is an ordinary answer, and still a
110
+ // non-zero exit, so a script can tell a boot that happened from one that did not.
111
+ const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
112
+ return failed ? 1 : 0;
113
+ }
114
+ console.error('usage: quo init|serve|census|boot|invite|knock|remove|side|run|pilot|reach [--dir D] [--ward W] [--via S] [--as NAME] [--url URL --model NAME] [--dial URL] [json]');
115
+ return 2;
116
+ }
117
+
118
+ main(process.argv.slice(2)).then(
119
+ (code) => {
120
+ if (code >= 0) process.exit(code);
121
+ },
122
+ (e: unknown) => {
123
+ console.error(e instanceof Error ? e.message : String(e));
124
+ process.exit(1);
125
+ },
126
+ );
@@ -0,0 +1,32 @@
1
+ import { Being } from '@quo-systems/quo';
2
+ import type { Answer, Blueprint, Invitation, JsonObject, OccupantRecord, Wanted } from '@quo-systems/quo';
3
+ export declare const USER = "user";
4
+ export declare const PUSHER = "user:push";
5
+ export declare class Avatar extends Being {
6
+ static cells: {
7
+ inbox: JsonObject[];
8
+ };
9
+ static asks: {
10
+ notify: {
11
+ description: string;
12
+ input: {
13
+ type: string;
14
+ };
15
+ for: (occ: OccupantRecord | undefined) => boolean;
16
+ };
17
+ };
18
+ readonly ears: Set<(object: JsonObject) => void>;
19
+ notify(args: JsonObject): {
20
+ notified: boolean;
21
+ };
22
+ enter(desk: Invitation, proof: JsonObject): Promise<Blueprint | {
23
+ error: string;
24
+ }>;
25
+ join(invitation: Invitation): Promise<Blueprint | {
26
+ error: string;
27
+ }>;
28
+ tools(): Promise<Blueprint | {
29
+ error: string;
30
+ }>;
31
+ call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
32
+ }
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The avatar. Someone outside Quo's voice: a human at a screen, a model at
3
+ // an MCP client, a model in an API loop. One class for every side. She holds
4
+ // exactly one standing, `user`, and everything her side asks goes through
5
+ // it; the user being reaches her only to push, and she hands the push to
6
+ // every side she has. A side is in-process with her, holds the object, and
7
+ // calls the three methods below. Nothing of a side is in her cells.
8
+ import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
9
+ // Her one standing, and the id under which the user being pushes to her.
10
+ // Two ids for one far being, because standings and occupants share one
11
+ // namespace and a record holds one or the other.
12
+ export const USER = 'user';
13
+ export const PUSHER = 'user:push';
14
+ export class Avatar extends Being {
15
+ static cells = { inbox: [] };
16
+ static asks = {
17
+ notify: { description: 'a push from the user being', input: { type: 'object' }, for: (occ) => occ?.id === PUSHER },
18
+ };
19
+ // Her sides' ears. One identity is one avatar for life, and two sessions
20
+ // of that identity are two sides on her at once, so a push goes to every
21
+ // ear present; each side adds its own on open and removes it on close.
22
+ // Dropped if there is none.
23
+ ears = new Set();
24
+ // ---- what the user being may ask her
25
+ notify(args) {
26
+ this.cells.inbox.push(args);
27
+ for (const ear of this.ears)
28
+ ear(args);
29
+ return { notified: true };
30
+ }
31
+ // ---- what her side calls. Not asks: nobody reaches these through a door.
32
+ // The credential exchange's last step. `desk` is the desk's public
33
+ // invitation, `proof` is what the route produced. She knocks the desk as a
34
+ // stranger, gets an invitation the user being minted, and joins with it. A
35
+ // knock on a public invitation births nothing, so she holds no standing at
36
+ // the desk afterwards. If she already holds `user`, this is a reconnect and
37
+ // nothing is minted.
38
+ async enter(desk, proof) {
39
+ if (this.standings[USER])
40
+ return this.tools();
41
+ const inv = await this.knock(desk, 'device', { proof });
42
+ if (isSilence(inv))
43
+ return { error: 'silence' };
44
+ if (isWord(inv))
45
+ return { error: wordOf(inv) };
46
+ if (!inv || typeof inv !== 'object' || Array.isArray(inv) || 'error' in inv)
47
+ return inv ?? { error: 'no invitation' };
48
+ return this.join(inv);
49
+ }
50
+ // Knock with an invitation, hand the user being one back so she can push,
51
+ // and take her as `user`. The only moment a standing is born here.
52
+ async join(invitation) {
53
+ if (this.standings[USER])
54
+ return this.tools();
55
+ const mine = await this.invite(PUSHER);
56
+ if (mine === null)
57
+ return { error: 'user is taken' };
58
+ const out = await this.knock(invitation, 'hello', { invitation: mine });
59
+ if (isSilence(out) || isWord(out)) {
60
+ this.occupants.remove(PUSHER);
61
+ return { error: isSilence(out) ? 'silence' : wordOf(out) };
62
+ }
63
+ await this.take(USER, invitation);
64
+ return this.tools();
65
+ }
66
+ // Her describe, as the user being shows it to her. This is the tool list.
67
+ async tools() {
68
+ const st = this.standings[USER];
69
+ if (!st)
70
+ return { error: 'not joined' };
71
+ const bp = await st.ask();
72
+ if (isSilence(bp) || isWord(bp))
73
+ return { error: isSilence(bp) ? 'silence' : wordOf(bp) };
74
+ return bp;
75
+ }
76
+ // One call from the side, one ask on the standing. The three words pass
77
+ // through untouched; the side renders them.
78
+ call(name, args = {}, wanted) {
79
+ const st = this.standings[USER];
80
+ return st ? st.ask(name, args, wanted) : Promise.resolve({ error: 'not joined' });
81
+ }
82
+ }
@@ -0,0 +1,42 @@
1
+ import { Being } from '@quo-systems/quo';
2
+ import type { Asker, Json, JsonObject } from '@quo-systems/quo';
3
+ export type Proof = {
4
+ kind: string;
5
+ [more: string]: Json;
6
+ };
7
+ export type Verified = {
8
+ user: string;
9
+ client: string;
10
+ wake?: boolean;
11
+ };
12
+ export type Verifier = (proof: Proof) => Promise<Verified | null> | Verified | null;
13
+ export declare class Desk extends Being {
14
+ static verifiers: Record<string, Verifier>;
15
+ static asks: {
16
+ hello: {
17
+ description: string;
18
+ input: {
19
+ type: string;
20
+ };
21
+ };
22
+ device: {
23
+ description: string;
24
+ input: {
25
+ type: string;
26
+ properties: {
27
+ proof: {
28
+ type: string;
29
+ };
30
+ };
31
+ required: string[];
32
+ };
33
+ };
34
+ };
35
+ hello(_args: JsonObject, asker: Asker): {
36
+ desk: boolean;
37
+ asker: string | null;
38
+ kinds: string[];
39
+ users: string[];
40
+ };
41
+ device(args: JsonObject): Promise<Json>;
42
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The front desk. The one public being of a world, and public for the first
3
+ // hello only: everything after is behind an invitation. It holds a standing
4
+ // to every user being, placed by the root, and trades a proof for a device
5
+ // invitation that the user being mints herself. The desk never mints one.
6
+ //
7
+ // A proof is what a route produced after authenticating by its own means. A
8
+ // verifier turns a proof of one kind into who it is for: which user, which
9
+ // client identity. Verifiers are the device's, set on the class before boot
10
+ // the way an envoy reads a secret from the device: never through cells.
11
+ import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
12
+ export class Desk extends Being {
13
+ // proof kind -> verifier. The route that makes a proof kind registers the
14
+ // verifier for it; the desk knows no kind by itself.
15
+ static verifiers = {};
16
+ static asks = {
17
+ hello: { description: 'how to get in', input: { type: 'object' } },
18
+ device: { description: 'trade a proof for a device invitation', input: { type: 'object', properties: { proof: { type: 'object' } }, required: ['proof'] } },
19
+ };
20
+ hello(_args, asker) {
21
+ return { desk: true, asker: asker.id ?? null, kinds: Object.keys(Desk.verifiers), users: Object.keys(this.cells.standings).filter((k) => k.startsWith('user:')).map((k) => k.slice(5)) };
22
+ }
23
+ async device(args) {
24
+ const proof = args.proof;
25
+ if (!proof || typeof proof !== 'object' || Array.isArray(proof) || typeof proof.kind !== 'string')
26
+ return { error: 'proof has a kind' };
27
+ const verify = Object.hasOwn(Desk.verifiers, proof.kind) ? Desk.verifiers[proof.kind] : undefined;
28
+ if (typeof verify !== 'function')
29
+ return { error: 'no such proof kind' };
30
+ const who = await verify(proof);
31
+ if (!who)
32
+ return { error: 'refused' };
33
+ const user = this.standings[`user:${who.user}`];
34
+ if (!user)
35
+ return { error: 'no such user' };
36
+ const inv = await user.ask('device', who.wake === true ? { client: who.client, wake: true } : { client: who.client });
37
+ if (isSilence(inv))
38
+ return { error: 'silence' };
39
+ if (isWord(inv))
40
+ return { error: wordOf(inv) };
41
+ return inv;
42
+ }
43
+ }
@@ -0,0 +1,3 @@
1
+ export { User, DESK } from './user.ts';
2
+ export { Desk, type Proof, type Verified, type Verifier } from './desk.ts';
3
+ export { Avatar, USER, PUSHER } from './avatar.ts';
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The beings every side shares. See quo-estate.md, the trunk.
3
+ export { User, DESK } from './user.js';
4
+ export { Desk } from './desk.js';
5
+ export { Avatar, USER, PUSHER } from './avatar.js';
@@ -0,0 +1,2 @@
1
+ import type { Hosted } from '@quo-systems/quo/harbor';
2
+ export declare function setup(hosted: Hosted, user: string): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { DESK } from './user.js';
2
+ export async function setup(hosted, user) {
3
+ if (!/^[\w.-]+$/.test(user) || user === 'desk')
4
+ throw new Error(`a user is a word, and not desk: ${user}`);
5
+ const boot = async (args) => {
6
+ const out = (await hosted.ask('boot', args));
7
+ if (out.error)
8
+ throw new Error(`init: ${out.error}`);
9
+ };
10
+ await boot({ key: 'desk', class: 'Desk', public: true });
11
+ await boot({ key: user, class: 'User' });
12
+ const inv = (await hosted.ask('invite', { being: user, id: DESK }));
13
+ const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello' }));
14
+ if (placed.taken !== `user:${user}`)
15
+ throw new Error('init: the desk could not reach the user being');
16
+ }
@@ -0,0 +1,25 @@
1
+ import type { Answer, Json, JsonObject, WordName } from '@quo-systems/quo';
2
+ import type { Avatar } from './avatar.ts';
3
+ export type Serving = {
4
+ close(): Promise<void>;
5
+ };
6
+ export type Side = (avatar: Avatar) => Promise<Serving>;
7
+ export type Word = {
8
+ word: 'object';
9
+ value: Json;
10
+ } | {
11
+ word: 'error';
12
+ value: JsonObject;
13
+ } | {
14
+ word: 'silence';
15
+ } | {
16
+ word: 'unreached';
17
+ } | {
18
+ word: 'quo';
19
+ name: WordName;
20
+ };
21
+ export declare function word(answer: Answer): Word;
22
+ export declare const SILENCE_TEXT = "silence: the work may have happened, and no answer came back";
23
+ export declare const UNREACHED_TEXT = "unreached: nothing was delivered; asking again is safe";
24
+ export declare const WORD_TEXT: Record<WordName, string>;
25
+ export declare const wordText: (name: WordName) => string;
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The side: the one interface an avatar speaks through to someone outside
3
+ // Quo. A side takes the avatar object, in-process, and renders three things
4
+ // its own way: her describe as what that someone reads, a call as an ask on
5
+ // her one standing, and a push as a notification. Sides in this repository:
6
+ // a screen, an MCP server, a runner, an event-driven agent. None knows
7
+ // another exists, and none of this file names a platform.
8
+ import { isSilence, isUnreached, isWord, wordOf } from '@quo-systems/quo';
9
+ export function word(answer) {
10
+ if (isSilence(answer))
11
+ return { word: 'silence' };
12
+ if (isUnreached(answer))
13
+ return { word: 'unreached' };
14
+ if (isWord(answer))
15
+ return { word: 'quo', name: wordOf(answer) };
16
+ if (answer !== null && typeof answer === 'object' && !Array.isArray(answer) && 'error' in answer)
17
+ return { word: 'error', value: answer };
18
+ return { word: 'object', value: answer };
19
+ }
20
+ // What a side says about the two words that carry nothing, so that whoever
21
+ // reads them can decide: silence promises nothing, unreached promises that
22
+ // nothing was delivered.
23
+ export const SILENCE_TEXT = 'silence: the work may have happened, and no answer came back';
24
+ export const UNREACHED_TEXT = 'unreached: nothing was delivered; asking again is safe';
25
+ // And the ward's other words, each with what a side says of it.
26
+ export const WORD_TEXT = {
27
+ unreached: UNREACHED_TEXT,
28
+ late: 'late: the wait ran out; the work may have happened',
29
+ invitation: 'invitation: that is not an invitation',
30
+ dropped: 'dropped: the standing is gone',
31
+ removed: 'removed: the far being removed you',
32
+ absent: 'absent: the far being did not come back this run',
33
+ unannounced: 'unannounced: the knock announced no key of its own',
34
+ repeated: 'repeated: that number was already honoured',
35
+ threw: 'threw: the far being threw',
36
+ };
37
+ export const wordText = (name) => WORD_TEXT[name];
@@ -0,0 +1,108 @@
1
+ import { Being } from '@quo-systems/quo';
2
+ import type { Asker, JsonObject, OccupantRecord } from '@quo-systems/quo';
3
+ export declare const DESK = "desk";
4
+ declare const isDesk: (occ: OccupantRecord | undefined) => boolean;
5
+ declare const isDevice: (occ: OccupantRecord | undefined) => boolean;
6
+ declare const mayWake: (occ: OccupantRecord | undefined) => boolean;
7
+ export declare class User extends Being {
8
+ static cells: {
9
+ name: string;
10
+ reports: JsonObject[];
11
+ };
12
+ static asks: {
13
+ hello: {
14
+ description: string;
15
+ input: {
16
+ type: string;
17
+ properties: {
18
+ invitation: {
19
+ type: string;
20
+ };
21
+ };
22
+ };
23
+ };
24
+ whoami: {
25
+ description: string;
26
+ input: {
27
+ type: string;
28
+ };
29
+ for: typeof isDevice;
30
+ };
31
+ device: {
32
+ description: string;
33
+ input: {
34
+ type: string;
35
+ properties: {
36
+ client: {
37
+ type: string;
38
+ };
39
+ wake: {
40
+ type: string;
41
+ };
42
+ };
43
+ required: string[];
44
+ };
45
+ for: typeof isDesk;
46
+ };
47
+ push: {
48
+ description: string;
49
+ input: {
50
+ type: string;
51
+ properties: {
52
+ client: {
53
+ type: string;
54
+ };
55
+ object: {
56
+ type: string;
57
+ };
58
+ };
59
+ required: string[];
60
+ };
61
+ for: typeof mayWake;
62
+ };
63
+ chores: {
64
+ description: string;
65
+ input: {
66
+ type: string;
67
+ };
68
+ for: (occ: OccupantRecord | undefined) => boolean;
69
+ };
70
+ report: {
71
+ description: string;
72
+ input: {
73
+ type: string;
74
+ properties: {
75
+ event: {
76
+ type: string;
77
+ };
78
+ result: {};
79
+ };
80
+ required: string[];
81
+ };
82
+ for: typeof isDevice;
83
+ };
84
+ };
85
+ hello(args: JsonObject, asker: Asker): Promise<{
86
+ welcome: string | null;
87
+ name: import("@quo-systems/quo").Json;
88
+ }>;
89
+ whoami(_args: JsonObject, asker: Asker): {
90
+ id: string | null;
91
+ client: string | null;
92
+ };
93
+ device(args: JsonObject): Promise<JsonObject>;
94
+ push(args: JsonObject): Promise<{
95
+ error: string;
96
+ pushed?: undefined;
97
+ } | {
98
+ error?: undefined;
99
+ pushed: import("@quo-systems/quo").Json;
100
+ }>;
101
+ chores(): {
102
+ chores: string[];
103
+ };
104
+ report(args: JsonObject, asker: Asker): {
105
+ reported: boolean;
106
+ };
107
+ }
108
+ export {};
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The user being. One per human, running wherever the human's world runs.
3
+ // She holds the human's standings, and everyone who acts for the human is
4
+ // her occupant, seeing what her gate shows. Two of her occupants are
5
+ // special only by the id the root chose for them: `desk`, the front desk,
6
+ // who may ask her to invite a device; and each device, whose id is the
7
+ // client identity the desk established.
8
+ import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
9
+ export const DESK = 'desk';
10
+ const isDesk = (occ) => occ?.id === DESK;
11
+ const isDevice = (occ) => occ !== undefined && occ.id !== DESK;
12
+ const client = (occ) => (typeof occ?.notes.client === 'string' ? occ.notes.client : null);
13
+ // A device the human allowed to wake her other devices: the note says so.
14
+ const mayWake = (occ) => isDesk(occ) || occ?.notes.wake === true;
15
+ export class User extends Being {
16
+ static cells = { name: '', reports: [] };
17
+ static asks = {
18
+ hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
19
+ whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
20
+ device: { description: 'mint an invitation for a device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' } }, required: ['client'] }, for: isDesk },
21
+ push: { description: 'push an object to a device: wake it with an event', input: { type: 'object', properties: { client: { type: 'string' }, object: { type: 'object' } }, required: ['client', 'object'] }, for: mayWake },
22
+ chores: { description: 'what the agent may run', input: { type: 'object' }, for: (occ) => client(occ) === 'agent' },
23
+ report: { description: 'what a run of yours found', input: { type: 'object', properties: { event: { type: 'object' }, result: {} }, required: ['event', 'result'] }, for: isDevice },
24
+ };
25
+ // Anyone may say hello. A device that hands her an invitation in the args
26
+ // is taken as a standing under its own id, so she can push to it later.
27
+ async hello(args, asker) {
28
+ if (asker.id !== undefined && args.invitation && typeof args.invitation === 'object' && !Array.isArray(args.invitation)) {
29
+ const inv = args.invitation;
30
+ const back = await this.knock(inv);
31
+ if (!isSilence(back) && !isWord(back))
32
+ await this.take(`to:${asker.id}`, inv);
33
+ }
34
+ return { welcome: asker.id ?? null, name: this.cells.name };
35
+ }
36
+ whoami(_args, asker) {
37
+ return { id: asker.id ?? null, client: client(this.occupant(asker)) };
38
+ }
39
+ // The front desk asks; she mints. The client identity goes into the
40
+ // occupant's notes, and that is what every gate reads. `wake` is the
41
+ // human's word at the exchange that this device may wake her others.
42
+ async device(args) {
43
+ const c = typeof args.client === 'string' ? args.client : null;
44
+ if (c === null)
45
+ return { error: 'client is a string' };
46
+ const inv = await this.invite(c);
47
+ if (inv === null)
48
+ return { error: 'id taken' };
49
+ const rec = this.cells.occupants[c];
50
+ if (rec) {
51
+ rec.notes.client = c;
52
+ if (args.wake === true)
53
+ rec.notes.wake = true;
54
+ }
55
+ return inv;
56
+ }
57
+ async push(args) {
58
+ const c = typeof args.client === 'string' ? args.client : null;
59
+ const st = c === null ? undefined : this.standings[`to:${c}`];
60
+ if (!st)
61
+ return { error: 'no such device, or it gave no way back' };
62
+ const out = await st.ask('notify', args.object ?? {});
63
+ return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
64
+ }
65
+ chores() {
66
+ return { chores: ['census', 'report'] };
67
+ }
68
+ // A device that ran something for her says what it found. Kept, so that
69
+ // whoever renders her can show it; the ask itself is the callback.
70
+ report(args, asker) {
71
+ this.cells.reports.push({ from: asker.id ?? null, event: args.event ?? null, result: args.result ?? null });
72
+ return { reported: true };
73
+ }
74
+ }
@@ -0,0 +1,14 @@
1
+ export type Answer = {
2
+ result: unknown;
3
+ } | {
4
+ error: string;
5
+ };
6
+ export declare function ask(dir: string, method: string | undefined, args?: Record<string, unknown>, ward?: string, via?: string): Promise<Answer>;
7
+ export declare function reach(dir: string, pk: string, url: string): Promise<Answer>;
8
+ export type Run = {
9
+ url: string;
10
+ model: string;
11
+ key?: string;
12
+ turns?: number;
13
+ };
14
+ export declare function side(dir: string, as: string, ward?: string, run?: Run): Promise<void>;