@nervur-org/dock 0.3.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 (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
@@ -0,0 +1,35 @@
1
+ # The daemon on a droplet, as one user, forever. Install with:
2
+ # sudo cp droplet/quo.service /etc/systemd/system/quo.service
3
+ # sudo systemctl enable --now quo
4
+ # The unit runs as the `quo` user, whose home holds the estate folder and
5
+ # the harbor folder, so the seed, the lease and the sockets are that user's
6
+ # alone. The HTTP door is loopback only; Caddy fronts it. See Caddyfile
7
+ # beside this file.
8
+ [Unit]
9
+ Description=quo: one harbor on this device
10
+ After=network.target
11
+
12
+ [Service]
13
+ User=quo
14
+ Group=quo
15
+ Environment=QUO_DIR=/home/quo/.quo
16
+ Environment=QUO_HTTP=8787
17
+ # CLAUDE_CODE_OAUTH_TOKEN, when an agent folder runs `claude -p`: the agent
18
+ # reads its credential from the device, never from cells.
19
+ EnvironmentFile=-/etc/quo/env
20
+ WorkingDirectory=/home/quo/ESTATE
21
+ ExecStart=/usr/bin/node /home/quo/ESTATE/node_modules/@nervur-org/dock/dist/cli/quo.js serve
22
+ Restart=always
23
+ RestartSec=2
24
+ KillSignal=SIGTERM
25
+ TimeoutStopSec=10
26
+ NoNewPrivileges=true
27
+ ProtectSystem=strict
28
+ # Agents run in their folders and Claude Code keeps its own state in the
29
+ # user's home; both must be writable for a run to happen, and a harbor
30
+ # with no agent has neither.
31
+ ReadWritePaths=/home/quo/.quo -/home/quo/agents -/home/quo/.claude -/home/quo/.claude.json /tmp
32
+ PrivateTmp=false
33
+
34
+ [Install]
35
+ WantedBy=multi-user.target
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ The daemon on a Mac, as the user, forever: a launchd agent. It holds the
4
+ harbor at ~/.quo and the socket to every quo. route ~/.quo/dial.json
5
+ names, so a Mac behind NAT is reached through the droplet it dialed. The
6
+ daemon runs the dock's emitted `dist/cli/quo.js` from where the estate
7
+ installed it, never the source. Install, with node on the path:
8
+
9
+ sed "s#HOME#$HOME#g; s#NODE#$(which node)#g" mac/systems.quo.plist > ~/Library/LaunchAgents/systems.quo.plist
10
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/systems.quo.plist
11
+
12
+ Remove with `launchctl bootout gui/$(id -u)/systems.quo`; restart after a
13
+ wire change with `launchctl kickstart -k gui/$(id -u)/systems.quo`. The
14
+ log is ~/.quo/serve.log. No HTTP door: a Mac is a dialer, not a listener.
15
+ -->
16
+ <plist version="1.0">
17
+ <dict>
18
+ <key>Label</key>
19
+ <string>systems.quo</string>
20
+ <key>ProgramArguments</key>
21
+ <array>
22
+ <string>NODE</string>
23
+ <string>HOME/ESTATE/node_modules/@nervur-org/dock/dist/cli/quo.js</string>
24
+ <string>serve</string>
25
+ </array>
26
+ <key>EnvironmentVariables</key>
27
+ <dict>
28
+ <key>QUO_DIR</key>
29
+ <string>HOME/.quo</string>
30
+ </dict>
31
+ <key>WorkingDirectory</key>
32
+ <string>HOME/ESTATE</string>
33
+ <key>RunAtLoad</key>
34
+ <true/>
35
+ <key>KeepAlive</key>
36
+ <true/>
37
+ <key>ThrottleInterval</key>
38
+ <integer>2</integer>
39
+ <key>StandardOutPath</key>
40
+ <string>HOME/.quo/serve.log</string>
41
+ <key>StandardErrorPath</key>
42
+ <string>HOME/.quo/serve.log</string>
43
+ </dict>
44
+ </plist>
@@ -0,0 +1,5 @@
1
+ export declare function estate(dir: string, domain: string): Promise<{
2
+ dir: string;
3
+ name: string;
4
+ files: string[];
5
+ }>;
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // `quo estate DIR --domain D`: an estate folder, written once, of the shape
3
+ // every estate has. One folder per harbor: `droplet/`, the quo directory
4
+ // that device runs minus what it mints, its routes under the domain, no
5
+ // agents, no schedules, no classes of its own, and that device's unit and
6
+ // Caddyfile; and `mac/`, the launchd agent that runs the daemon on an
7
+ // operator's Mac as a dialer, with the dial list it holds. A package file
8
+ // that depends on this dock and nothing else, and one document to fill in.
9
+ // An estate needs nothing the dock does not give it, and this is the dock
10
+ // giving it.
11
+ import { existsSync } from 'node:fs';
12
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
13
+ import { basename, join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { DOMAIN, routesFor } from './daemon.js';
16
+ // This package's own version, read beside the emitted or the source tree.
17
+ async function version() {
18
+ for (const rel of ['../package.json', '../../package.json']) {
19
+ const at = fileURLToPath(new URL(rel, import.meta.url));
20
+ if (!existsSync(at))
21
+ continue;
22
+ const pkg = JSON.parse(await readFile(at, 'utf8'));
23
+ if (pkg.name === '@nervur-org/dock' && pkg.version)
24
+ return pkg.version;
25
+ }
26
+ return '0.1.0';
27
+ }
28
+ const template = (name) => readFile(fileURLToPath(new URL(`estate/${name}`, import.meta.url)), 'utf8');
29
+ export async function estate(dir, domain) {
30
+ if (!DOMAIN.test(domain))
31
+ throw new Error(`estate: ${domain} is not a domain`);
32
+ if (existsSync(join(dir, 'package.json')))
33
+ throw new Error(`estate: ${dir} already holds a package`);
34
+ const name = basename(dir);
35
+ const files = {
36
+ 'package.json': JSON.stringify({ name, private: true, description: `The ${name} estate: one owner's harbors, built on the dock and nothing else.`, type: 'module', dependencies: { '@nervur-org/dock': `^${await version()}` } }, null, 2) + '\n',
37
+ [`${name}.md`]: `# ${name}\n\nAn estate: all of one owner's harbors. One folder per harbor, each the quo\ndirectory that device runs minus what it mints, plus that device's unit.\n\`droplet/\` is the first: its routes under \`${domain}\`, its agents, its\nclasses, its systemd unit and its Caddyfile. This folder depends on the\ndock and nothing else.\n\n## The droplet\n\nWhere it is, how it is reached, and what was done to stand it up: yours\nto write.\n`,
38
+ 'droplet/routes.json': JSON.stringify(routesFor(domain)) + '\n',
39
+ 'droplet/stand.ts': `// SPDX-License-Identifier: Apache-2.0\n// What the root stands up on this harbor, once, after the inits and with no\n// daemon over the folder: the beings the estate boots for its own working\n// and the relations between them, placed on real invitations. Run as\n// \`node droplet/stand.ts <dir>\`; twice is once.\nimport { stand } from '@nervur-org/dock/stand';\n\nconsole.log(await stand(process.argv[2] ?? process.env.QUO_DIR ?? '', { boot: [], wire: [] }));\n`,
40
+ 'droplet/classes/index.ts': '// The classes this harbor holds beside the dock\'s built-in ones, exported\n// by name.\nexport {};\n',
41
+ 'droplet/quo.service': (await template('quo.service')).replaceAll('ESTATE', name),
42
+ 'droplet/Caddyfile': (await template('Caddyfile')).replaceAll('DOMAIN', domain),
43
+ 'mac/systems.quo.plist': (await template('systems.quo.plist')).replaceAll('ESTATE', name),
44
+ };
45
+ for (const [rel, text] of Object.entries(files)) {
46
+ await mkdir(join(dir, rel, '..'), { recursive: true });
47
+ await writeFile(join(dir, rel), text);
48
+ }
49
+ return { dir, name, files: Object.keys(files) };
50
+ }
@@ -0,0 +1,2 @@
1
+ import { type Folder } from '../human/web.ts';
2
+ export declare function diskFolder(dir: string): Folder;
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The harbor folder as the web route asks for it. A route reads no file, so
3
+ // what a world keeps beside its wards is answered here, where the daemon
4
+ // already has a disk: `design/` for the face it wears and `tab/` for its own
5
+ // code for the tab. Nothing is held between requests, because a folder is
6
+ // worn, built and changed while the daemon runs and a restart is not how a
7
+ // world puts on a face.
8
+ //
9
+ // A world wears a design only when it holds `tokens.css`: that file is the
10
+ // design, and a folder with fonts and no tokens wears nothing, so nothing of
11
+ // it is served.
12
+ import { access, readFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { TAB_CODE, TAB_SOURCE } from '../human/web.js';
15
+ const DESIGN = 'design';
16
+ export function diskFolder(dir) {
17
+ const there = (at) => access(at).then(() => true, () => false);
18
+ const tokens = join(dir, DESIGN, 'tokens.css');
19
+ return {
20
+ wearing: async () => ((await there(tokens)) ? { tokens: true, fonts: await there(join(dir, DESIGN, 'fonts.css')) } : { tokens: false, fonts: false }),
21
+ design: async (file) => ((await there(tokens)) ? readFile(join(dir, DESIGN, file)).then((b) => new Uint8Array(b), () => null) : null),
22
+ serving: async () => (await there(join(dir, TAB_CODE))) || (await there(join(dir, TAB_SOURCE))),
23
+ beings: () => readFile(join(dir, TAB_CODE), 'utf8').catch(() => null),
24
+ };
25
+ }
@@ -0,0 +1,2 @@
1
+ import { type Store } from '../mcp/oauth.ts';
2
+ export declare function danceOnDisk(dir: string): Store;
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The OAuth dance as the daemon keeps it: `<dir>/oauth.json`, the route's
3
+ // own file and nobody else's, written whole through a temporary name so a
4
+ // crash mid-write leaves the last good dance behind. A route reads no file,
5
+ // so this is where the disk is, beside the folder and the wards.
6
+ //
7
+ // The writes are queued because the dance moves on a request and requests
8
+ // overlap: two saves racing on one name is how a file ends up half of each.
9
+ import { readFile, writeFile, rename } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ import { emptyDance } from '../mcp/oauth.js';
12
+ export function danceOnDisk(dir) {
13
+ const file = join(dir, 'oauth.json');
14
+ let queue = Promise.resolve();
15
+ return {
16
+ load: async () => {
17
+ const read = await readFile(file, 'utf8').catch(() => null);
18
+ if (read === null)
19
+ return emptyDance();
20
+ try {
21
+ return { ...emptyDance(), ...JSON.parse(read) };
22
+ }
23
+ catch {
24
+ return emptyDance(); // a dance nobody can read is a dance nobody is in the middle of
25
+ }
26
+ },
27
+ save: (dance) => (queue = queue.then(async () => {
28
+ await writeFile(file + '.tmp', JSON.stringify(dance), { mode: 0o600 });
29
+ await rename(file + '.tmp', file);
30
+ })),
31
+ };
32
+ }
@@ -0,0 +1,5 @@
1
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2
+ import type { Serving } from '../beings/side.ts';
3
+ import { type Subject } from '../mcp/server.ts';
4
+ export declare function owner(dir: string, ward: string, via?: string): Subject;
5
+ export declare function pilotSide(dir: string, ward: string, transport: Transport, via?: string): Promise<Serving>;
@@ -0,0 +1,22 @@
1
+ import { ask } from './client.js';
2
+ import { mcpSide } from '../mcp/server.js';
3
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
4
+ // The owner hears objects: the socket's own failure, a silence and a word
5
+ // each come back as an error object named for what it was.
6
+ export function owner(dir, ward, via) {
7
+ const one = async (method, args) => {
8
+ const out = await ask(dir, method, args, ward, via);
9
+ if ('error' in out)
10
+ return { error: out.error };
11
+ const r = out.result;
12
+ if (isRecord(r) && r.silence === true)
13
+ return { error: 'silence' };
14
+ if (isRecord(r) && typeof r.word === 'string')
15
+ return { error: r.word };
16
+ return r;
17
+ };
18
+ return { tools: () => one(undefined, {}), call: (name, args) => one(name, args ?? {}), ears: new Set() };
19
+ }
20
+ export function pilotSide(dir, ward, transport, via) {
21
+ return mcpSide(owner(dir, ward, via), transport);
22
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,201 @@
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] [--user U] [--class C] [--default] [--show] [--domain DOM] mint a seed, boot a ward over a disk store, write both; C is the class of the ward's home being, User unless said; --default marks it the default ward; --show shows it at the web route; --domain writes the four routes under DOM
8
+ // quo default [--dir D] <ward> mark one ward the default, the one a caller means when naming none; the one ward hosted is the default unmarked
9
+ // quo show|hide [--dir D] <ward> show a ward at the web route, listed at its root and its public being's page painted for a stranger; hidden at init, and a hidden ward's page is the plain door
10
+ // quo domain [--dir D] <domain> write routes.json by the convention: mcp., web., quo. and api. under the domain, over https
11
+ // quo serve [--dir D] [--http PORT] the daemon: harbor from disk, the two sockets, the HTTP door on loopback, a socket held to every route the dialers faculty keeps
12
+ // quo census [--dir D] [--ward W] [--via S] the empty ask: pk and every being
13
+ // quo boot [--dir D] [--ward W] [--via S] <json> { key, class, public? }
14
+ // quo invite [--dir D] [--ward W] [--via S] <json> { being, id }; with routes.json naming web., the answer carries the link
15
+ // quo knock [--dir D] [--ward W] [--via S] <json> { being | { boot, key }, id, invitation, method?, args?, wanted? }
16
+ // quo remove [--dir D] [--ward W] [--via S] <json> { being, id }: a relation out of a being; on the ward pk, an owner
17
+ // quo unboot [--dir D] [--ward W] [--via S] <json> { being }: a being out of the ward, with every relation she holds
18
+ // quo ask [--dir D] [--ward W] <being> [method] [json] the root asking a being of its own ward, as the device
19
+ // quo side [--dir D] [--ward W] --as NAME an avatar over stdio for a local MCP client; NAME is the client identity
20
+ // 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
21
+ // quo pilot [--dir D] [--ward W] [--via S] the owner pilot over stdio: census, boot, invite, knock, remove
22
+ // quo reach [--dir D] <pk> <url> a hint for the harbor's directory: that ward pk lives at that quo. route
23
+ // quo estate <dir> --domain D an estate folder of the shape every estate has: a droplet's quo directory, unit and Caddyfile, a Mac's launchd agent, on this dock
24
+ //
25
+ // D defaults to $QUO_DIR, then ~/.quo. W defaults to the default ward, and
26
+ // init names a new ward main when none is given. init takes
27
+ // --user NAME for the user being, defaulting to the device's user. run
28
+ // takes the model's base URL as --url or $QUO_MODEL_URL, its name as
29
+ // --model or $QUO_MODEL, a bearer from $QUO_MODEL_KEY, read here and never
30
+ // kept, and --turns as the ceiling of model calls in one turn. S is a
31
+ // standing the user being holds at another ward, one the root there invited
32
+ // as an owner: with --via, the owner asks go to that ward's door, sealed,
33
+ // and the answers are its. Without it they are the root's, on the pointer.
34
+ import { homedir, userInfo } from 'node:os';
35
+ import { join } from 'node:path';
36
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
37
+ import { isInvitation } from '@nervur-org/nervur';
38
+ import { DiskHarbor } from '../harbor/disk.js';
39
+ import { serve, readRoutes, writeRoutes } from './daemon.js';
40
+ import { ask, askBeing, side, reach } from './client.js';
41
+ import { link } from '../beings/link.js';
42
+ import { pilotSide } from './pilot.js';
43
+ import { estate } from './estate.js';
44
+ const OWNER_ASKS = new Set(['boot', 'public', 'invite', 'knock', 'remove', 'unboot']);
45
+ function parse(argv) {
46
+ const flags = {};
47
+ const rest = [];
48
+ for (let i = 0; i < argv.length; i++) {
49
+ const a = argv[i] ?? '';
50
+ // a flag takes the word after it, unless that word is a flag or there is
51
+ // none: --default stands alone
52
+ if (a.startsWith('--'))
53
+ flags[a.slice(2)] = argv[i + 1] === undefined || argv[i + 1].startsWith('--') ? '' : argv[++i];
54
+ else
55
+ rest.push(a);
56
+ }
57
+ const [cmd, json, more, third] = rest;
58
+ return { cmd, json, more, third, flags, dir: flags.dir ?? process.env.QUO_DIR ?? join(homedir(), '.quo'), ward: flags.ward, via: flags.via };
59
+ }
60
+ const print = (v) => console.log(JSON.stringify(v, null, 2));
61
+ async function main(argv) {
62
+ const { cmd, json, more, third, flags, dir, via } = parse(argv);
63
+ if (cmd === 'init') {
64
+ const made = await DiskHarbor.init(dir, flags.ward, flags.user ?? userInfo().username, flags.class, 'default' in flags, 'show' in flags);
65
+ print(flags.domain ? { ...made, routes: await writeRoutes(dir, flags.domain) } : made);
66
+ return 0;
67
+ }
68
+ if (cmd === 'default') {
69
+ if (!json) {
70
+ console.error('quo default <ward>');
71
+ return 2;
72
+ }
73
+ await DiskHarbor.setDefault(dir, json);
74
+ print({ default: json });
75
+ return 0;
76
+ }
77
+ if (cmd === 'show' || cmd === 'hide') {
78
+ if (!json) {
79
+ console.error(`quo ${cmd} <ward>`);
80
+ return 2;
81
+ }
82
+ await DiskHarbor.setShown(dir, json, cmd === 'show');
83
+ print({ [cmd === 'show' ? 'shown' : 'hidden']: json });
84
+ return 0;
85
+ }
86
+ if (cmd === 'domain') {
87
+ if (!json) {
88
+ console.error('quo domain <domain>');
89
+ return 2;
90
+ }
91
+ print({ routes: await writeRoutes(dir, json) });
92
+ return 0;
93
+ }
94
+ // Every command that reaches a ward is on one: the one named, or the
95
+ // default, read from the folder the way the daemon reads it, and only once
96
+ // the command is held right.
97
+ const wardOf = async () => flags.ward ?? (await DiskHarbor.defaultOf(dir));
98
+ if (cmd === 'side' || cmd === 'run') {
99
+ if (!flags.as) {
100
+ console.error(`quo ${cmd} needs --as NAME`);
101
+ return 2;
102
+ }
103
+ let run;
104
+ if (cmd === 'run') {
105
+ const url = flags.url ?? process.env.QUO_MODEL_URL;
106
+ const model = flags.model ?? process.env.QUO_MODEL;
107
+ if (!url || !model) {
108
+ console.error('quo run needs --url URL and --model NAME, or QUO_MODEL_URL and QUO_MODEL');
109
+ return 2;
110
+ }
111
+ const key = process.env.QUO_MODEL_KEY;
112
+ const turns = flags.turns !== undefined ? Number(flags.turns) : undefined;
113
+ if (turns !== undefined && !(Number.isInteger(turns) && turns > 0)) {
114
+ console.error('--turns is a positive integer');
115
+ return 2;
116
+ }
117
+ run = { url, model, ...(key ? { key } : {}), ...(turns !== undefined ? { turns } : {}) };
118
+ }
119
+ await side(dir, flags.as, await wardOf(), run); // stdio to the daemon, until one end closes
120
+ return -1;
121
+ }
122
+ if (cmd === 'pilot') {
123
+ const ward = await wardOf();
124
+ await ask(dir, undefined, {}, ward); // no daemon is a clear word now, not at the first tool call
125
+ const serving = await pilotSide(dir, ward, new StdioServerTransport(), via);
126
+ process.stdin.once('end', () => void serving.close().then(() => process.exit(0)));
127
+ return -1;
128
+ }
129
+ if (cmd === 'serve') {
130
+ const port = flags.http ?? process.env.QUO_HTTP;
131
+ const s = await serve(dir, port !== undefined ? { http: { port: Number(port) } } : {});
132
+ 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])) });
133
+ const stop = () => void s.close().then(() => process.exit(0));
134
+ process.once('SIGINT', stop);
135
+ process.once('SIGTERM', stop);
136
+ return -1; // stays up
137
+ }
138
+ if (cmd === 'estate') {
139
+ if (!json || !flags.domain) {
140
+ console.error('quo estate needs <dir> --domain D');
141
+ return 2;
142
+ }
143
+ print(await estate(json, flags.domain));
144
+ return 0;
145
+ }
146
+ // The root reaching into a being of its own ward. Two positionals and the
147
+ // args as JSON, because a human writes this one by hand: `quo ask clock keep
148
+ // '{"id":"nightly","ask":"desk.hello","cron":"0 3 * * *"}'`. With no
149
+ // method it is that being's describe, as the device sees her.
150
+ if (cmd === 'ask') {
151
+ if (!json) {
152
+ console.error('quo ask needs <being> [method] [json]');
153
+ return 2;
154
+ }
155
+ const out = await askBeing(dir, json, more, third ? JSON.parse(third) : {}, await wardOf());
156
+ print('error' in out ? { error: out.error } : out.result);
157
+ const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
158
+ return failed ? 1 : 0;
159
+ }
160
+ if (cmd === 'reach') {
161
+ if (!json || !more) {
162
+ console.error('quo reach needs <pk> <url>');
163
+ return 2;
164
+ }
165
+ const out = await reach(dir, json, more);
166
+ print('error' in out ? { error: out.error } : out.result);
167
+ return 'error' in out ? 1 : 0;
168
+ }
169
+ if (cmd === 'census' || (cmd !== undefined && OWNER_ASKS.has(cmd))) {
170
+ const args = json ? JSON.parse(json) : {};
171
+ const ward = await wardOf();
172
+ let out = await ask(dir, cmd === 'census' ? undefined : cmd, args, ward, via);
173
+ // an invitation minted here is handed on as a link when the harbor's
174
+ // routes name a web. origin: the world's page with the invitation in
175
+ // the fragment, one string to send to the person it is for
176
+ const minted = cmd === 'invite' && !via && 'result' in out && isInvitation(out.result) ? out.result : null;
177
+ const routes = minted ? await readRoutes(dir) : null;
178
+ // the ward answers null to an invite on an id already held, standing or
179
+ // occupant, and null on a terminal is nothing to read: it is a refusal
180
+ // here, in words, and an exit code a script can tell apart
181
+ if (cmd === 'invite' && 'result' in out && out.result === null)
182
+ out = { result: { error: 'id taken' } };
183
+ if (minted && routes)
184
+ print({ ...minted, link: link(`${routes.web}/${encodeURIComponent(ward)}`, minted) });
185
+ else
186
+ print('error' in out ? { error: out.error } : out.result);
187
+ // an error object the ward answered is an ordinary answer, and still a
188
+ // non-zero exit, so a script can tell a boot that happened from one that did not.
189
+ const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
190
+ return failed ? 1 : 0;
191
+ }
192
+ console.error('usage: quo init|default|show|hide|domain|serve|census|boot|public|invite|knock|remove|unboot|ask|side|run|pilot|reach|estate [--dir D] [--ward W] [--via S] [--as NAME] [--url URL --model NAME] [json]');
193
+ return 2;
194
+ }
195
+ main(process.argv.slice(2)).then((code) => {
196
+ if (code >= 0)
197
+ process.exit(code);
198
+ }, (e) => {
199
+ console.error(e instanceof Error ? e.message : String(e));
200
+ process.exit(1);
201
+ });
@@ -0,0 +1,37 @@
1
+ import type { JsonObject } from '@nervur-org/nervur';
2
+ import type { Harbor } from '@nervur-org/nervur/harbor';
3
+ export type Boot = {
4
+ ward: string;
5
+ key: string;
6
+ class: string;
7
+ public?: boolean;
8
+ };
9
+ export type Wire = {
10
+ from: string;
11
+ to: string;
12
+ as?: string;
13
+ known?: string;
14
+ notes?: JsonObject;
15
+ };
16
+ export type Stand = {
17
+ boot?: Boot[];
18
+ wire?: Wire[];
19
+ };
20
+ type Wards = {
21
+ wards: Map<string, {
22
+ being(key: string): unknown;
23
+ ask(method: string, args: JsonObject): Promise<unknown>;
24
+ }>;
25
+ partitionOf(name: string): Record<string, unknown> | undefined;
26
+ };
27
+ export declare function standUp(harbor: Harbor | Wards, decl: Stand): Promise<{
28
+ booted: string[];
29
+ wired: string[];
30
+ already: string[];
31
+ }>;
32
+ export declare function stand(dir: string, decl: Stand): Promise<{
33
+ booted: string[];
34
+ wired: string[];
35
+ already: string[];
36
+ }>;
37
+ export {};
@@ -0,0 +1,110 @@
1
+ import { DiskHarbor } from '../harbor/disk.js';
2
+ const KEY = /^[\w.:-]+$/;
3
+ const RESERVED = new Set(['knock', 'take', 'remove']);
4
+ const fail = (why) => {
5
+ throw new Error(`stand: ${why}`);
6
+ };
7
+ // `<ward>.<key>`, split at the first dot: a ward's name is a plain word and a
8
+ // being's key is not, so the ambiguity only ever falls one way.
9
+ function at(v, what) {
10
+ const dot = v.indexOf('.');
11
+ if (dot < 1)
12
+ return fail(`${what}: "${v}" is not "<ward>.<being>"`);
13
+ return { ward: v.slice(0, dot), key: v.slice(dot + 1) };
14
+ }
15
+ // A name a wire would use, read off the partition the harbor already holds:
16
+ // whether a being holds it as a standing and whether she holds it as an
17
+ // occupant. Standings and occupants are one namespace, so a name held under
18
+ // the other kind is a collision and never this wire.
19
+ function holds(h, ward, key, id) {
20
+ const p = h.partitionOf(ward);
21
+ return { standing: Object.hasOwn(p?.bind?.[key]?.standings ?? {}, id), occupant: Object.hasOwn(p?.beings?.[key]?.occupants ?? {}, id) };
22
+ }
23
+ const ward = (h, name, why) => h.wards.get(name) ?? fail(`${why}: this device hosts no ward ${name}`);
24
+ // Stand a booted harbor up. Everything declared and absent is placed;
25
+ // everything declared and present is left exactly as it is.
26
+ //
27
+ // Idempotency is read off the mint and not off anybody's cells. A being is
28
+ // whatever answers, `Being` or a bare object with one method, so there is no
29
+ // shape here to reach into: what says a wire already stands is that the far
30
+ // being refuses to mint that occupant a second time. `invite` answers null
31
+ // for exactly one reason once the reader has held the id to a word the ward
32
+ // does not reserve, which is that the id is taken, and an id taken is this
33
+ // wire, placed the last time the script ran.
34
+ export async function standUp(harbor, decl) {
35
+ const h = harbor;
36
+ const booted = [];
37
+ for (const b of decl.boot ?? []) {
38
+ const w = ward(h, b.ward, `boot ${b.ward}.${b.key}`);
39
+ if (!w.being(b.key)) {
40
+ const out = (await w.ask('boot', { key: b.key, class: b.class }));
41
+ if (out.error)
42
+ fail(`boot ${b.ward}.${b.key} as ${b.class}: ${out.error}`);
43
+ booted.push(`${b.ward}.${b.key}`);
44
+ }
45
+ // the mark is one per ward and the script is what says who holds it:
46
+ // taken off whoever holds it, the desk init put there, and put on her,
47
+ // which is the same two asks whether or not she held it already
48
+ if (b.public) {
49
+ const off = (await w.ask('public', { key: null }));
50
+ if (off.error)
51
+ fail(`public ${b.ward}.${b.key}: ${off.error}`);
52
+ const out = (await w.ask('public', { key: b.key }));
53
+ if (out.error)
54
+ fail(`public ${b.ward}.${b.key}: ${out.error}`);
55
+ }
56
+ }
57
+ const wired = [];
58
+ const already = [];
59
+ for (const one of decl.wire ?? []) {
60
+ const from = at(one.from, 'from'), to = at(one.to, 'to');
61
+ const as = one.as ?? to.key, known = one.known ?? from.key;
62
+ if (!KEY.test(as) || !KEY.test(known))
63
+ fail(`a name is a word: ${one.from} -> ${one.to}`);
64
+ // The three the stance keeps for itself. A ward refuses them at invite
65
+ // and at take, and refusing them here is what leaves `invite` answering
66
+ // null for one reason only, which is the whole of how this stays
67
+ // idempotent without reading anybody's cells.
68
+ if (RESERVED.has(as) || RESERVED.has(known))
69
+ fail(`knock, take and remove are the ward's own words: ${one.from} -> ${one.to}`);
70
+ const near = ward(h, from.ward, `wire ${one.from}`), far = ward(h, to.ward, `wire ${one.to}`);
71
+ if (!near.being(from.key))
72
+ fail(`wire ${one.from}: no such being`);
73
+ if (!far.being(to.key))
74
+ fail(`wire ${one.to}: no such being`);
75
+ // A name held under the other kind is not this wire and never will be:
76
+ // said now, before a heir is spent on it, rather than read as a wire
77
+ // already placed or refused at the knock with the far side already bound.
78
+ const atFar = holds(h, to.ward, to.key, known);
79
+ const atNear = holds(h, from.ward, from.key, as);
80
+ if (atFar.standing)
81
+ fail(`wire ${one.from} -> ${one.to}: ${to.key} already holds a standing named ${known}, and a name is one of the two`);
82
+ if (atNear.occupant)
83
+ fail(`wire ${one.from} -> ${one.to}: ${from.key} already holds an occupant named ${as}, and a name is one of the two`);
84
+ const inv = (await far.ask('invite', { being: to.key, id: known, ...(one.notes ? { notes: one.notes } : {}) }));
85
+ if (inv === null) {
86
+ already.push(`${one.from} -> ${one.to}`);
87
+ continue;
88
+ }
89
+ if ('error' in inv)
90
+ fail(`wire ${one.from} -> ${one.to}: ${inv.error}`);
91
+ const took = (await near.ask('knock', { being: from.key, id: as, invitation: inv }));
92
+ if (took.taken !== as)
93
+ fail(`wire ${one.from} -> ${one.to}: ${took.error ?? `${from.key} would not take ${as}`}`);
94
+ wired.push(`${one.from} -> ${one.to}`);
95
+ }
96
+ return { booted, wired, already };
97
+ }
98
+ // What an estate's `stand.ts` calls: the harbor folder booted here, with no
99
+ // daemon over it, stood up and written back. The lease refuses a folder a
100
+ // daemon holds, so the recipe is stop, stand, start, like an init.
101
+ export async function stand(dir, decl) {
102
+ const h = new DiskHarbor(dir);
103
+ await h.boot();
104
+ try {
105
+ return await standUp(h, decl);
106
+ }
107
+ finally {
108
+ await h.close();
109
+ }
110
+ }