@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/mcp/runner.ts ADDED
@@ -0,0 +1,131 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The model side as a runner: an MCP client written inline, for a model that
3
+ // speaks function calling and not MCP. The avatar is the one in server.ts and
4
+ // the mapping is the same envelope; what moved in-process is the host that
5
+ // drives the loop, because a completions API cannot. The runner holds one
6
+ // conversation for as long as it serves. Two things start a turn in it: a
7
+ // line from the human, and a push from the user being, which is the world
8
+ // asking. A turn posts the conversation with her describe as tools, turns
9
+ // each tool call into a named ask on her standing, hands the result back as
10
+ // a tool message, and ends when the model answers with no tool call, or at
11
+ // the ceiling. Nothing here names a vendor: the endpoint is a base URL and a
12
+ // model name, which is LM Studio, OpenRouter, or a scripted fake alike.
13
+ import type { Blueprint, JsonObject } from '@quo-systems/quo';
14
+ import type { Avatar } from '../beings/avatar.ts';
15
+ import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT, type Serving } from '../beings/side.ts';
16
+
17
+ // Where the model is. `key` is a bearer for endpoints that want one, read
18
+ // from the device and never from cells. `turns` is the ceiling of model
19
+ // calls in one turn of the conversation; ten by default.
20
+ export type Model = { url: string; model: string; key?: string; turns?: number };
21
+ export const TURNS = 10;
22
+
23
+ // The wire shapes, only as much as the loop reads and writes.
24
+ export type Tool = { type: 'function'; function: { name: string; description?: string; parameters: JsonObject } };
25
+ export type ToolCall = { id: string; type: 'function'; function: { name: string; arguments: string } };
26
+ export type Message =
27
+ | { role: 'system' | 'user'; content: string }
28
+ | { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] }
29
+ | { role: 'tool'; tool_call_id: string; content: string };
30
+ type Completion = { choices: { message: { role: 'assistant'; content: string | null; tool_calls?: ToolCall[] } }[]; error?: unknown };
31
+
32
+ // Her describe, spoken as a tools array. Name, description and input are
33
+ // verbatim, with the one narrowing an endpoint has asked for: an ask that
34
+ // declares no properties is sent with an empty `properties`, because
35
+ // LM Studio refuses a parameters schema without one.
36
+ export function tools(bp: Blueprint): Tool[] {
37
+ return bp.asks.map((a) => {
38
+ const t: Tool = { type: 'function', function: { name: a.name, parameters: { properties: {}, ...a.input, type: 'object' } } };
39
+ if (a.description !== undefined) t.function.description = a.description;
40
+ return t;
41
+ });
42
+ }
43
+
44
+ // One tool call, one ask, one tool message: the three words cross as the
45
+ // table in quo-mcp.md says, and a tool message is text, so the two words
46
+ // that carry nothing cross as the text the trunk gives them.
47
+ export async function result(avatar: Avatar, call: ToolCall): Promise<string> {
48
+ let args: JsonObject = {};
49
+ try {
50
+ const parsed: unknown = call.function.arguments ? JSON.parse(call.function.arguments) : {};
51
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) args = parsed as JsonObject;
52
+ } catch {
53
+ return JSON.stringify({ error: 'arguments are not JSON' });
54
+ }
55
+ const w = word(await avatar.call(call.function.name, args));
56
+ if (w.word === 'object' || w.word === 'error') return JSON.stringify(w.value);
57
+ return w.word === 'silence' ? SILENCE_TEXT : w.word === 'quo' ? wordText(w.name) : UNREACHED_TEXT;
58
+ }
59
+
60
+ export type Runner = Serving & {
61
+ // A line from the human; resolves with the model's final text, or null
62
+ // when the turn ended at the ceiling.
63
+ say(text: string): Promise<string | null>;
64
+ // The conversation so far, for whoever renders it.
65
+ readonly messages: readonly Message[];
66
+ };
67
+
68
+ // `hear` gets every final text the model says, whoever started the turn.
69
+ // `after` runs when an ask is done, as in the MCP server: a harbor that must
70
+ // write what the ward changed hooks it.
71
+ export function runnerSide(avatar: Avatar, model: Model, hear: (text: string) => void = () => {}, after: () => Promise<void> = async () => {}): Runner {
72
+ const messages: Message[] = [];
73
+ const ceiling = model.turns ?? TURNS;
74
+ let busy: Promise<unknown> = Promise.resolve();
75
+
76
+ async function complete(t: Tool[]): Promise<Completion['choices'][0]['message']> {
77
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
78
+ if (model.key !== undefined) headers.authorization = `Bearer ${model.key}`;
79
+ const res = await fetch(`${model.url.replace(/\/$/, '')}/chat/completions`, { method: 'POST', headers, body: JSON.stringify({ model: model.model, messages, tools: t }) });
80
+ const body = (await res.json()) as Completion;
81
+ const m = body.choices?.[0]?.message;
82
+ if (!res.ok || !m) throw new Error(`model: ${res.status} ${JSON.stringify(body.error ?? body)}`);
83
+ return m;
84
+ }
85
+
86
+ // One turn: from the last appended message to the model's final text.
87
+ async function turn(): Promise<string | null> {
88
+ const bp = await avatar.tools();
89
+ const t = 'asks' in bp ? tools(bp) : [];
90
+ for (let i = 0; i < ceiling; i++) {
91
+ const m = await complete(t);
92
+ const reply: Message = { role: 'assistant', content: m.content };
93
+ if (m.tool_calls?.length) reply.tool_calls = m.tool_calls;
94
+ messages.push(reply);
95
+ if (!m.tool_calls?.length) {
96
+ const text = m.content ?? '';
97
+ hear(text);
98
+ return text;
99
+ }
100
+ for (const call of m.tool_calls) {
101
+ messages.push({ role: 'tool', tool_call_id: call.id, content: await result(avatar, call) });
102
+ await after();
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ // Turns never overlap: one conversation, one train of thought.
109
+ function start(message: Message): Promise<string | null> {
110
+ const next = busy.then(async () => {
111
+ messages.push(message);
112
+ return turn();
113
+ });
114
+ busy = next.catch(() => {});
115
+ return next;
116
+ }
117
+
118
+ const ear = (object: JsonObject) => {
119
+ void start({ role: 'user', content: JSON.stringify(object) }).catch(() => {});
120
+ };
121
+ avatar.ears.add(ear);
122
+
123
+ return {
124
+ messages,
125
+ say: (text) => start({ role: 'user', content: text }),
126
+ close: async () => {
127
+ avatar.ears.delete(ear);
128
+ await busy;
129
+ },
130
+ };
131
+ }
package/mcp/server.ts ADDED
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The model side as an MCP server: one avatar, one transport. The mapping is
3
+ // total and needs nothing beyond this envelope. tools/list is her describe,
4
+ // tools/call is a named ask on her one standing, a push is a logging
5
+ // notification, and the three words for "no object" each cross as the table
6
+ // in quo-mcp.md says. The transport is whatever the caller connected: the
7
+ // SDK's in-memory pair in a test, stdio for a local client, HTTP on a route.
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
10
+ import { ListToolsRequestSchema, CallToolRequestSchema, type CallToolResult, type Tool } from '@modelcontextprotocol/sdk/types.js';
11
+ import type { Blueprint, JsonObject } from '@quo-systems/quo';
12
+ import type { Avatar } from '../beings/avatar.ts';
13
+ import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT, type Serving } from '../beings/side.ts';
14
+
15
+ export const NAME = 'quo';
16
+ export const VERSION = '0.0.0';
17
+
18
+ // Her describe, spoken as tools. Name, description and input are verbatim;
19
+ // an output schema crosses when she declared one.
20
+ export function tools(bp: Blueprint): Tool[] {
21
+ return bp.asks.map((a) => {
22
+ const t: Tool = { name: a.name, inputSchema: { ...a.input, type: 'object' } };
23
+ if (a.description !== undefined) t.description = a.description;
24
+ if (a.output !== undefined) t.outputSchema = { ...a.output, type: 'object' };
25
+ return t;
26
+ });
27
+ }
28
+
29
+ // `after` runs when a call is done: a harbor that must write what the ward
30
+ // changed hooks it, since a same-ward ask never crosses the harbor.
31
+ export async function mcpSide(avatar: Avatar, transport: Transport, after: () => Promise<void> = async () => {}): Promise<Serving> {
32
+ const server = new Server({ name: NAME, version: VERSION }, { capabilities: { tools: { listChanged: true }, logging: {} } });
33
+
34
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
35
+ const bp = await avatar.tools();
36
+ if ('error' in bp && !('asks' in bp)) return { tools: [] }; // not joined: nothing to show, and nothing to call
37
+ return { tools: tools(bp as Blueprint) };
38
+ });
39
+
40
+ server.setRequestHandler(CallToolRequestSchema, async (req): Promise<CallToolResult> => {
41
+ const w = word(await avatar.call(req.params.name, (req.params.arguments ?? {}) as JsonObject));
42
+ await after();
43
+ if (w.word === 'object') {
44
+ const structured = w.value !== null && typeof w.value === 'object' && !Array.isArray(w.value) ? { structuredContent: w.value } : {};
45
+ return { content: [{ type: 'text', text: JSON.stringify(w.value) }], ...structured };
46
+ }
47
+ if (w.word === 'error') return { content: [{ type: 'text', text: JSON.stringify(w.value) }], structuredContent: w.value, isError: true };
48
+ if (w.word === 'silence') return { content: [{ type: 'text', text: SILENCE_TEXT }], structuredContent: { error: 'silence' }, isError: true };
49
+ if (w.word === 'quo') return { content: [{ type: 'text', text: wordText(w.name) }], structuredContent: { error: w.name }, isError: true };
50
+ return { content: [{ type: 'text', text: UNREACHED_TEXT }], structuredContent: { error: 'unreached' }, isError: true };
51
+ });
52
+
53
+ // A push from the user being is a notification to the client. If the
54
+ // transport is gone, the push is dropped, as the side says it is.
55
+ const ear = (object: JsonObject) => {
56
+ void server.sendLoggingMessage({ level: 'info', logger: NAME, data: object }).catch(() => {});
57
+ };
58
+ avatar.ears.add(ear);
59
+
60
+ await server.connect(transport);
61
+ return {
62
+ close: async () => {
63
+ avatar.ears.delete(ear);
64
+ await server.close();
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The exchange pages on the web route. Two static pages, hand-written, and
3
+ // explicitly not the human side: they render no blueprint. They end the
4
+ // credential exchange the way the trunk says every route does: a proof is
5
+ // made, the front desk trades it for an invitation, the avatar knocks, and
6
+ // the OAuth request completes with the client identity the human chose.
7
+ //
8
+ // GET /login?request=ID the owner password, from QUO_OWNER_PASSWORD
9
+ // POST /login sets a short session cookie, goes to /allow
10
+ // GET /allow?request=ID who is asking, and the identity they will be
11
+ // POST /allow allow as that identity, or deny
12
+ //
13
+ // The password is the device's, read from the environment the way an envoy
14
+ // reads a secret, never through cells. With none set, the exchange is closed.
15
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
16
+ import type { IncomingMessage, ServerResponse } from 'node:http';
17
+ import type { OAuth } from '../oauth.ts';
18
+ import { readForm } from '../oauth.ts';
19
+
20
+ export const SESSION_TTL = 10 * 60 * 1000; // a login lives as long as a request: ten minutes
21
+ const COOKIE = 'quo_exchange';
22
+
23
+ export type Admit = (identity: string, wake: boolean) => Promise<{ error?: string }>;
24
+ export type Options = {
25
+ oauth: OAuth;
26
+ password: () => string | undefined; // QUO_OWNER_PASSWORD, read at every login
27
+ admit: Admit; // the daemon: boot or find the avatar for this identity, and enter her
28
+ user: string; // the user being's name, for the page
29
+ now?: () => number;
30
+ };
31
+
32
+ const esc = (s: string) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c] ?? c);
33
+ const word = (s: unknown) => (typeof s === 'string' && /^[\w.-]{1,40}$/.test(s) ? s : null);
34
+ // A client's name as an identity: lowercase words joined by dashes.
35
+ export const suggest = (name: string) =>
36
+ name
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, '-')
39
+ .replace(/^-|-$/g, '')
40
+ .slice(0, 40) || 'client';
41
+
42
+ export class Exchange {
43
+ readonly o: Options;
44
+ readonly secret = randomBytes(32); // sessions die with the daemon, and that is fine: they live ten minutes
45
+ constructor(o: Options) {
46
+ this.o = o;
47
+ }
48
+ now() {
49
+ return this.o.now?.() ?? Date.now();
50
+ }
51
+
52
+ // ---- the session cookie: an expiry, signed
53
+
54
+ mint(): string {
55
+ const exp = String(this.now() + SESSION_TTL);
56
+ return `${exp}.${createHmac('sha256', this.secret).update(exp).digest('base64url')}`;
57
+ }
58
+ valid(req: IncomingMessage): boolean {
59
+ const m = /(?:^|;\s*)quo_exchange=([^;]+)/.exec(req.headers.cookie ?? '');
60
+ const [exp, sig] = (m?.[1] ?? '').split('.');
61
+ if (!exp || !sig) return false;
62
+ const want = createHmac('sha256', this.secret).update(exp).digest('base64url');
63
+ return sig.length === want.length && timingSafeEqual(Buffer.from(sig), Buffer.from(want)) && Number(exp) > this.now();
64
+ }
65
+
66
+ async handle(req: IncomingMessage, res: ServerResponse, rest: string): Promise<boolean> {
67
+ const url = new URL(req.url ?? '/', 'http://localhost');
68
+ const page = (status: number, body: string, headers: Record<string, string> = {}) => {
69
+ res.writeHead(status, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', ...headers });
70
+ res.end(shell(body));
71
+ };
72
+ const go = (to: string, headers: Record<string, string> = {}) => {
73
+ res.writeHead(302, { location: to, 'cache-control': 'no-store', ...headers });
74
+ res.end();
75
+ };
76
+
77
+ if (rest === '/login' && req.method === 'GET') {
78
+ const request = url.searchParams.get('request') ?? '';
79
+ const p = this.o.oauth.pending(request);
80
+ if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone. Start again from the app that sent you here.</p>`), true;
81
+ if (this.o.password() === undefined) return page(503, `<h1>Closed</h1><p>This world takes no logins: no owner password is set.</p>`), true;
82
+ if (this.valid(req)) return go(`/allow?request=${encodeURIComponent(request)}`), true;
83
+ return page(200, loginForm(request, p.client.client_name)), true;
84
+ }
85
+ if (rest === '/login' && req.method === 'POST') {
86
+ const f = await readForm(req);
87
+ const request = f.get('request') ?? '';
88
+ const p = this.o.oauth.pending(request);
89
+ if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`), true;
90
+ const want = this.o.password();
91
+ const got = f.get('password') ?? '';
92
+ if (want === undefined) return page(503, `<h1>Closed</h1>`), true;
93
+ if (got.length !== want.length || !timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
94
+ await new Promise((ok) => setTimeout(ok, 300));
95
+ return page(401, loginForm(request, p.client.client_name, 'That is not the password.')), true;
96
+ }
97
+ return go(`/allow?request=${encodeURIComponent(request)}`, { 'set-cookie': `${COOKIE}=${this.mint()}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${SESSION_TTL / 1000}` }), true;
98
+ }
99
+ if (rest === '/allow' && req.method === 'GET') {
100
+ const request = url.searchParams.get('request') ?? '';
101
+ const p = this.o.oauth.pending(request);
102
+ if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`), true;
103
+ if (!this.valid(req)) return go(`/login?request=${encodeURIComponent(request)}`), true;
104
+ return page(200, allowForm(request, p.client.client_name, p.redirect_uri, suggest(p.client.client_name), this.o.user)), true;
105
+ }
106
+ if (rest === '/allow' && req.method === 'POST') {
107
+ const f = await readForm(req);
108
+ const request = f.get('request') ?? '';
109
+ const p = this.o.oauth.pending(request);
110
+ if (!p) return page(400, `<h1>Nothing to allow</h1><p>This request is gone.</p>`), true;
111
+ if (!this.valid(req)) return go(`/login?request=${encodeURIComponent(request)}`), true;
112
+ if (f.get('decision') !== 'allow') {
113
+ const out = await this.o.oauth.deny(request);
114
+ return 'redirect' in out ? go(out.redirect) : page(400, `<h1>Gone</h1>`), true;
115
+ }
116
+ const identity = word(f.get('identity'));
117
+ if (identity === null || identity === this.o.user || identity === 'desk') return page(400, allowForm(request, p.client.client_name, p.redirect_uri, suggest(p.client.client_name), this.o.user, 'An identity is one word, and not the user or the desk.')), true;
118
+ const admitted = await this.o.admit(identity, f.get('wake') === 'on');
119
+ if (admitted.error) return page(500, `<h1>Not admitted</h1><p>${esc(admitted.error)}</p>`), true;
120
+ const out = await this.o.oauth.complete(request, identity);
121
+ return 'redirect' in out ? go(out.redirect, { 'set-cookie': `${COOKIE}=; Path=/; Max-Age=0` }) : page(400, `<h1>Gone</h1>`), true;
122
+ }
123
+ return false;
124
+ }
125
+ }
126
+
127
+ const shell = (body: string) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>quo</title>
128
+ <style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.4rem}label{display:block;margin:1rem 0 .25rem}input{font:inherit;padding:.5rem;width:100%;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem;margin:1rem .5rem 0 0}.err{color:#b00}.who{background:#f4f4f4;padding:.75rem 1rem;border-radius:.5rem}code{word-break:break-all}</style>
129
+ </head><body>${body}</body></html>`;
130
+
131
+ const loginForm = (request: string, client: string, err = '') => `<h1>${esc(client)} wants in</h1>
132
+ <p>Log in as the owner of this world to decide.</p>
133
+ ${err ? `<p class="err">${esc(err)}</p>` : ''}
134
+ <form method="post" action="/login"><input type="hidden" name="request" value="${esc(request)}">
135
+ <label for="p">Owner password</label><input id="p" name="password" type="password" autocomplete="current-password" autofocus required>
136
+ <button type="submit">Log in</button></form>`;
137
+
138
+ const allowForm = (request: string, client: string, redirect: string, identity: string, user: string, err = '') => `<h1>Allow ${esc(client)}?</h1>
139
+ <div class="who"><p><strong>${esc(client)}</strong> asks to be an occupant of <strong>${esc(user)}</strong>.</p>
140
+ <p>It will see exactly what ${esc(user)} shows the identity below, and nothing else. You can remove it any time.</p>
141
+ <p>It returns to <code>${esc(redirect)}</code>.</p></div>
142
+ ${err ? `<p class="err">${esc(err)}</p>` : ''}
143
+ <form method="post" action="/allow"><input type="hidden" name="request" value="${esc(request)}">
144
+ <label for="i">Identity</label><input id="i" name="identity" value="${esc(identity)}" pattern="[\\w.-]{1,40}" required>
145
+ <label for="w"><input id="w" name="wake" type="checkbox" style="width:auto"> May wake your other devices: hand an agent an event through ${esc(user)}</label>
146
+ <button type="submit" name="decision" value="allow">Allow</button><button type="submit" name="decision" value="deny">Deny</button></form>`;
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@quo-systems/dock",
3
+ "version": "0.1.0",
4
+ "description": "The dock: what every estate on Quo needs and nobody writes twice. A daemon and the quo command, the front desk, the user being and the avatar, harbors on disk, in a tab and on the edge, the model sides and the screen.",
5
+ "keywords": [
6
+ "quo",
7
+ "estate",
8
+ "harbor",
9
+ "mcp",
10
+ "daemon"
11
+ ],
12
+ "author": "Razvan Gherghina",
13
+ "license": "Apache-2.0",
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=22.18"
17
+ },
18
+ "bin": {
19
+ "quo": "./dist/cli/quo.js"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/beings/index.d.ts",
24
+ "default": "./dist/beings/index.js"
25
+ },
26
+ "./harbor/disk": {
27
+ "types": "./dist/harbor/disk.d.ts",
28
+ "default": "./dist/harbor/disk.js"
29
+ },
30
+ "./harbor/browser": {
31
+ "types": "./dist/harbor/browser.d.ts",
32
+ "default": "./dist/harbor/browser.js"
33
+ },
34
+ "./cli/daemon": {
35
+ "types": "./dist/cli/daemon.d.ts",
36
+ "default": "./dist/cli/daemon.js"
37
+ },
38
+ "./mcp/server": {
39
+ "types": "./dist/mcp/server.d.ts",
40
+ "default": "./dist/mcp/server.js"
41
+ },
42
+ "./mcp/runner": {
43
+ "types": "./dist/mcp/runner.d.ts",
44
+ "default": "./dist/mcp/runner.js"
45
+ },
46
+ "./mcp/agent": {
47
+ "types": "./dist/mcp/agent.d.ts",
48
+ "default": "./dist/mcp/agent.js"
49
+ },
50
+ "./human/screen": {
51
+ "types": "./dist/human/screen.d.ts",
52
+ "default": "./dist/human/screen.js"
53
+ },
54
+ "./human/html": {
55
+ "types": "./dist/human/html.d.ts",
56
+ "default": "./dist/human/html.js"
57
+ }
58
+ },
59
+ "scripts": {
60
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
61
+ "test": "node --test \"test/*.test.ts\"",
62
+ "check:terrain": "node --test \"test/terrain/*.test.ts\"",
63
+ "prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
64
+ },
65
+ "dependencies": {
66
+ "@modelcontextprotocol/sdk": "^1.30.0",
67
+ "@quo-systems/quo": "^0.2.0",
68
+ "esbuild": "^0.28.2",
69
+ "ws": "^8.21.3"
70
+ },
71
+ "devDependencies": {
72
+ "@types/ws": "^8.18.1",
73
+ "wrangler": "^4.129.0"
74
+ },
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "files": [
79
+ "dist",
80
+ "beings",
81
+ "cli",
82
+ "harbor",
83
+ "mcp",
84
+ "human",
85
+ "README.md",
86
+ "LICENSE",
87
+ "NOTICE"
88
+ ]
89
+ }