@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.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +27 -0
- package/beings/avatar.ts +84 -0
- package/beings/desk.ts +46 -0
- package/beings/index.ts +5 -0
- package/beings/quo-dock.md +698 -0
- package/beings/setup.ts +23 -0
- package/beings/side.ts +51 -0
- package/beings/user.ts +78 -0
- package/cli/client.ts +65 -0
- package/cli/daemon.ts +582 -0
- package/cli/quo.ts +126 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +82 -0
- package/dist/beings/desk.d.ts +42 -0
- package/dist/beings/desk.js +43 -0
- package/dist/beings/index.d.ts +3 -0
- package/dist/beings/index.js +5 -0
- package/dist/beings/setup.d.ts +2 -0
- package/dist/beings/setup.js +16 -0
- package/dist/beings/side.d.ts +25 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +108 -0
- package/dist/beings/user.js +74 -0
- package/dist/cli/client.d.ts +14 -0
- package/dist/cli/client.js +51 -0
- package/dist/cli/daemon.d.ts +57 -0
- package/dist/cli/daemon.js +581 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +121 -0
- package/dist/harbor/browser.d.ts +13 -0
- package/dist/harbor/browser.js +50 -0
- package/dist/harbor/disk.d.ts +19 -0
- package/dist/harbor/disk.js +126 -0
- package/dist/harbor/edge/edge.d.ts +26 -0
- package/dist/harbor/edge/edge.js +123 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +179 -0
- package/dist/harbor/edge/storage.d.ts +20 -0
- package/dist/harbor/edge/storage.js +81 -0
- package/dist/harbor/edge/worker.d.ts +14 -0
- package/dist/harbor/edge/worker.js +47 -0
- package/dist/harbor/files.d.ts +14 -0
- package/dist/harbor/files.js +83 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +85 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/html.d.ts +32 -0
- package/dist/human/html.js +136 -0
- package/dist/human/screen.d.ts +11 -0
- package/dist/human/screen.js +66 -0
- package/dist/human/tab.d.ts +7 -0
- package/dist/human/tab.js +68 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +96 -0
- package/dist/mcp/oauth.d.ts +98 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/pilot.d.ts +6 -0
- package/dist/mcp/pilot.js +68 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +94 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +63 -0
- package/dist/mcp/web/exchange.d.ts +23 -0
- package/dist/mcp/web/exchange.js +136 -0
- package/harbor/browser.ts +63 -0
- package/harbor/disk.ts +120 -0
- package/harbor/edge/edge.ts +147 -0
- package/harbor/edge/exercise.ts +187 -0
- package/harbor/edge/platform.d.ts +28 -0
- package/harbor/edge/storage.ts +82 -0
- package/harbor/edge/worker.ts +47 -0
- package/harbor/edge/wrangler.toml +21 -0
- package/harbor/files.ts +89 -0
- package/harbor/idb.ts +90 -0
- package/harbor/quo-harbor.md +403 -0
- package/human/dom.ts +21 -0
- package/human/html.ts +164 -0
- package/human/quo-human.md +86 -0
- package/human/screen.ts +76 -0
- package/human/tab.ts +84 -0
- package/mcp/agent.ts +94 -0
- package/mcp/http.ts +96 -0
- package/mcp/oauth.ts +260 -0
- package/mcp/pilot.ts +73 -0
- package/mcp/quo-mcp.md +262 -0
- package/mcp/runner.ts +131 -0
- package/mcp/server.ts +67 -0
- package/mcp/web/exchange.ts +146 -0
- package/package.json +89 -0
package/beings/setup.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The root's setup of a fresh ward, as every terrain does it: the front desk
|
|
3
|
+
// booted public, one user being, and the desk holding a standing to her
|
|
4
|
+
// under `user:<name>`, placed by the root's knock. She names it `desk`, and
|
|
5
|
+
// that id is the only thing that makes it the desk. A disk harbor does this
|
|
6
|
+
// at `quo init`, the edge at its root route's init; the memory harbor's
|
|
7
|
+
// test world does the same by hand.
|
|
8
|
+
import type { Invitation } from '@quo-systems/quo';
|
|
9
|
+
import type { Hosted } from '@quo-systems/quo/harbor';
|
|
10
|
+
import { DESK } from './user.ts';
|
|
11
|
+
|
|
12
|
+
export async function setup(hosted: Hosted, user: string): Promise<void> {
|
|
13
|
+
if (!/^[\w.-]+$/.test(user) || user === 'desk') throw new Error(`a user is a word, and not desk: ${user}`);
|
|
14
|
+
const boot = async (args: Record<string, unknown>) => {
|
|
15
|
+
const out = (await hosted.ask('boot', args)) as { booted?: string; error?: string };
|
|
16
|
+
if (out.error) throw new Error(`init: ${out.error}`);
|
|
17
|
+
};
|
|
18
|
+
await boot({ key: 'desk', class: 'Desk', public: true });
|
|
19
|
+
await boot({ key: user, class: 'User' });
|
|
20
|
+
const inv = (await hosted.ask('invite', { being: user, id: DESK })) as Invitation;
|
|
21
|
+
const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello' })) as { taken?: string };
|
|
22
|
+
if (placed.taken !== `user:${user}`) throw new Error('init: the desk could not reach the user being');
|
|
23
|
+
}
|
package/beings/side.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import type { Answer, Json, JsonObject, WordName } from '@quo-systems/quo';
|
|
10
|
+
import type { Avatar } from './avatar.ts';
|
|
11
|
+
|
|
12
|
+
// A side, running: it holds the avatar until closed.
|
|
13
|
+
export type Serving = { close(): Promise<void> };
|
|
14
|
+
export type Side = (avatar: Avatar) => Promise<Serving>;
|
|
15
|
+
|
|
16
|
+
// The three words for "no object", and the object itself, as every side must
|
|
17
|
+
// tell them apart before rendering. An error object is an ordinary answer
|
|
18
|
+
// that the side may mark; silence and unreached are not objects at all.
|
|
19
|
+
export type Word =
|
|
20
|
+
| { word: 'object'; value: Json }
|
|
21
|
+
| { word: 'error'; value: JsonObject }
|
|
22
|
+
| { word: 'silence' }
|
|
23
|
+
| { word: 'unreached' }
|
|
24
|
+
| { word: 'quo'; name: WordName }; // the ward's word, named: why no object came
|
|
25
|
+
|
|
26
|
+
export function word(answer: Answer): Word {
|
|
27
|
+
if (isSilence(answer)) return { word: 'silence' };
|
|
28
|
+
if (isUnreached(answer)) return { word: 'unreached' };
|
|
29
|
+
if (isWord(answer)) return { word: 'quo', name: wordOf(answer) };
|
|
30
|
+
if (answer !== null && typeof answer === 'object' && !Array.isArray(answer) && 'error' in answer) return { word: 'error', value: answer };
|
|
31
|
+
return { word: 'object', value: answer };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// What a side says about the two words that carry nothing, so that whoever
|
|
35
|
+
// reads them can decide: silence promises nothing, unreached promises that
|
|
36
|
+
// nothing was delivered.
|
|
37
|
+
export const SILENCE_TEXT = 'silence: the work may have happened, and no answer came back';
|
|
38
|
+
export const UNREACHED_TEXT = 'unreached: nothing was delivered; asking again is safe';
|
|
39
|
+
// And the ward's other words, each with what a side says of it.
|
|
40
|
+
export const WORD_TEXT: Record<WordName, string> = {
|
|
41
|
+
unreached: UNREACHED_TEXT,
|
|
42
|
+
late: 'late: the wait ran out; the work may have happened',
|
|
43
|
+
invitation: 'invitation: that is not an invitation',
|
|
44
|
+
dropped: 'dropped: the standing is gone',
|
|
45
|
+
removed: 'removed: the far being removed you',
|
|
46
|
+
absent: 'absent: the far being did not come back this run',
|
|
47
|
+
unannounced: 'unannounced: the knock announced no key of its own',
|
|
48
|
+
repeated: 'repeated: that number was already honoured',
|
|
49
|
+
threw: 'threw: the far being threw',
|
|
50
|
+
};
|
|
51
|
+
export const wordText = (name: WordName): string => WORD_TEXT[name];
|
package/beings/user.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
import type { Asker, Invitation, JsonObject, OccupantRecord } from '@quo-systems/quo';
|
|
10
|
+
|
|
11
|
+
export const DESK = 'desk';
|
|
12
|
+
const isDesk = (occ: OccupantRecord | undefined) => occ?.id === DESK;
|
|
13
|
+
const isDevice = (occ: OccupantRecord | undefined) => occ !== undefined && occ.id !== DESK;
|
|
14
|
+
const client = (occ: OccupantRecord | undefined) => (typeof occ?.notes.client === 'string' ? occ.notes.client : null);
|
|
15
|
+
// A device the human allowed to wake her other devices: the note says so.
|
|
16
|
+
const mayWake = (occ: OccupantRecord | undefined) => isDesk(occ) || occ?.notes.wake === true;
|
|
17
|
+
|
|
18
|
+
export class User extends Being {
|
|
19
|
+
static override cells = { name: '', reports: [] as JsonObject[] };
|
|
20
|
+
static override asks = {
|
|
21
|
+
hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
|
|
22
|
+
whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
|
|
23
|
+
device: { description: 'mint an invitation for a device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' } }, required: ['client'] }, for: isDesk },
|
|
24
|
+
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 },
|
|
25
|
+
chores: { description: 'what the agent may run', input: { type: 'object' }, for: (occ: OccupantRecord | undefined) => client(occ) === 'agent' },
|
|
26
|
+
report: { description: 'what a run of yours found', input: { type: 'object', properties: { event: { type: 'object' }, result: {} }, required: ['event', 'result'] }, for: isDevice },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Anyone may say hello. A device that hands her an invitation in the args
|
|
30
|
+
// is taken as a standing under its own id, so she can push to it later.
|
|
31
|
+
async hello(args: JsonObject, asker: Asker) {
|
|
32
|
+
if (asker.id !== undefined && args.invitation && typeof args.invitation === 'object' && !Array.isArray(args.invitation)) {
|
|
33
|
+
const inv = args.invitation as Invitation;
|
|
34
|
+
const back = await this.knock(inv);
|
|
35
|
+
if (!isSilence(back) && !isWord(back)) await this.take(`to:${asker.id}`, inv);
|
|
36
|
+
}
|
|
37
|
+
return { welcome: asker.id ?? null, name: this.cells.name };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
whoami(_args: JsonObject, asker: Asker) {
|
|
41
|
+
return { id: asker.id ?? null, client: client(this.occupant(asker)) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The front desk asks; she mints. The client identity goes into the
|
|
45
|
+
// occupant's notes, and that is what every gate reads. `wake` is the
|
|
46
|
+
// human's word at the exchange that this device may wake her others.
|
|
47
|
+
async device(args: JsonObject) {
|
|
48
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
49
|
+
if (c === null) return { error: 'client is a string' };
|
|
50
|
+
const inv = await this.invite(c);
|
|
51
|
+
if (inv === null) return { error: 'id taken' };
|
|
52
|
+
const rec = this.cells.occupants[c];
|
|
53
|
+
if (rec) {
|
|
54
|
+
rec.notes.client = c;
|
|
55
|
+
if (args.wake === true) rec.notes.wake = true;
|
|
56
|
+
}
|
|
57
|
+
return inv as unknown as JsonObject;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async push(args: JsonObject) {
|
|
61
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
62
|
+
const st = c === null ? undefined : this.standings[`to:${c}`];
|
|
63
|
+
if (!st) return { error: 'no such device, or it gave no way back' };
|
|
64
|
+
const out = await st.ask('notify', (args.object as JsonObject) ?? {});
|
|
65
|
+
return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
chores() {
|
|
69
|
+
return { chores: ['census', 'report'] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A device that ran something for her says what it found. Kept, so that
|
|
73
|
+
// whoever renders her can show it; the ask itself is the callback.
|
|
74
|
+
report(args: JsonObject, asker: Asker) {
|
|
75
|
+
(this.cells.reports as JsonObject[]).push({ from: asker.id ?? null, event: args.event ?? null, result: args.result ?? null });
|
|
76
|
+
return { reported: true };
|
|
77
|
+
}
|
|
78
|
+
}
|
package/cli/client.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A client of the daemon's socket. One ask, one line, one reply. This is
|
|
3
|
+
// what every subcommand but init and serve is: connect, ask, print, exit.
|
|
4
|
+
import { connect } from 'node:net';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { sockPath, sidePath } from './daemon.ts';
|
|
7
|
+
|
|
8
|
+
export type Answer = { result: unknown } | { error: string };
|
|
9
|
+
|
|
10
|
+
// `via` names a standing the ward's user being holds at another ward: the
|
|
11
|
+
// ask is then an owner's ask at that ward's door, sealed, and the answer is
|
|
12
|
+
// the far ward's. Without it the ask is the root's, on the pointer.
|
|
13
|
+
export function ask(dir: string, method: string | undefined, args: Record<string, unknown> = {}, ward = 'main', via?: string): Promise<Answer> {
|
|
14
|
+
return line(dir, via === undefined ? { ward, method, args } : { ward, method, args, via });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// `quo reach`: a hint for the harbor's directory, which is the harbor's and
|
|
18
|
+
// not a ward's, so it crosses the root socket as its own line.
|
|
19
|
+
export function reach(dir: string, pk: string, url: string): Promise<Answer> {
|
|
20
|
+
return line(dir, { reach: { pk, url } });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function line(dir: string, body: Record<string, unknown>): Promise<Answer> {
|
|
24
|
+
return new Promise((ok, no) => {
|
|
25
|
+
const c = connect(sockPath(dir));
|
|
26
|
+
const id = Math.random().toString(36).slice(2);
|
|
27
|
+
c.once('error', (e: NodeJS.ErrnoException) => no(e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? new Error(`no daemon at ${dir}: run \`quo serve\``) : e));
|
|
28
|
+
c.once('connect', () => {
|
|
29
|
+
c.write(JSON.stringify({ id, ...body }) + '\n');
|
|
30
|
+
createInterface({ input: c }).once('line', (line) => {
|
|
31
|
+
c.end();
|
|
32
|
+
const r = JSON.parse(line) as { id: string; result?: unknown; error?: string };
|
|
33
|
+
ok(r.error !== undefined ? { error: r.error } : { result: r.result });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `quo side`: a pipe. Hello to the daemon's side socket, then stdin to the
|
|
40
|
+
// socket and the socket to stdout, line for line, until either end closes.
|
|
41
|
+
// The MCP server runs in the daemon, beside the avatar; this process knows
|
|
42
|
+
// nothing of MCP but its newlines.
|
|
43
|
+
//
|
|
44
|
+
// `quo run` is the same pipe with a model named in the hello: the daemon
|
|
45
|
+
// runs the runner beside the avatar, each line typed is a turn, and each
|
|
46
|
+
// final text comes back as a line.
|
|
47
|
+
export type Run = { url: string; model: string; key?: string; turns?: number };
|
|
48
|
+
export function side(dir: string, as: string, ward = 'main', run?: Run): Promise<void> {
|
|
49
|
+
return new Promise((ok, no) => {
|
|
50
|
+
const c = connect(sidePath(dir));
|
|
51
|
+
c.once('error', (e: NodeJS.ErrnoException) => no(e.code === 'ENOENT' || e.code === 'ECONNREFUSED' ? new Error(`no daemon at ${dir}: run \`quo serve\``) : e));
|
|
52
|
+
c.once('connect', () => {
|
|
53
|
+
c.write(JSON.stringify(run ? { as, ward, run } : { as, ward }) + '\n');
|
|
54
|
+
const lines = createInterface({ input: c });
|
|
55
|
+
lines.once('line', (hello) => {
|
|
56
|
+
const h = JSON.parse(hello) as { ok?: string; error?: string };
|
|
57
|
+
if (h.error !== undefined) return no(new Error(h.error));
|
|
58
|
+
lines.on('line', (line) => process.stdout.write(line + '\n'));
|
|
59
|
+
createInterface({ input: process.stdin }).on('line', (line) => c.write(line + '\n'));
|
|
60
|
+
process.stdin.once('end', () => c.end());
|
|
61
|
+
});
|
|
62
|
+
c.once('close', () => ok());
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|