@quo-systems/dock 0.2.3 → 0.2.5

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 (54) hide show
  1. package/beings/carry.ts +7 -3
  2. package/beings/desk.ts +5 -1
  3. package/beings/look.ts +12 -3
  4. package/beings/quo-dock.md +22 -13
  5. package/beings/user.ts +20 -6
  6. package/dist/beings/carry.js +10 -4
  7. package/dist/beings/desk.d.ts +0 -1
  8. package/dist/beings/desk.js +5 -1
  9. package/dist/beings/look.d.ts +2 -0
  10. package/dist/beings/look.js +9 -0
  11. package/dist/beings/user.d.ts +24 -0
  12. package/dist/beings/user.js +20 -6
  13. package/dist/harbor/browser.d.ts +4 -4
  14. package/dist/harbor/browser.js +18 -9
  15. package/dist/harbor/capacitor.d.ts +19 -0
  16. package/dist/harbor/capacitor.js +151 -0
  17. package/dist/harbor/disk.js +8 -3
  18. package/dist/harbor/edge/edge.js +2 -1
  19. package/dist/harbor/edge/exercise.js +2 -1
  20. package/dist/harbor/edge/storage.d.ts +0 -3
  21. package/dist/harbor/edge/storage.js +6 -25
  22. package/dist/harbor/files.d.ts +2 -1
  23. package/dist/harbor/files.js +87 -22
  24. package/dist/harbor/seal.d.ts +3 -0
  25. package/dist/harbor/seal.js +25 -0
  26. package/dist/human/html.d.ts +1 -0
  27. package/dist/human/html.js +40 -19
  28. package/dist/human/local.d.ts +10 -0
  29. package/dist/human/local.js +10 -0
  30. package/dist/human/screen.js +32 -18
  31. package/dist/human/tab.d.ts +1 -0
  32. package/dist/human/tab.js +59 -3
  33. package/dist/human/web.d.ts +1 -0
  34. package/dist/human/web.js +34 -4
  35. package/dist/mcp/runner.js +5 -3
  36. package/dist/mcp/server.js +5 -3
  37. package/harbor/browser.ts +28 -17
  38. package/harbor/capacitor.ts +159 -0
  39. package/harbor/disk.ts +8 -3
  40. package/harbor/edge/edge.ts +2 -1
  41. package/harbor/edge/exercise.ts +2 -1
  42. package/harbor/edge/storage.ts +6 -25
  43. package/harbor/files.ts +79 -19
  44. package/harbor/quo-harbor.md +66 -14
  45. package/harbor/seal.ts +26 -0
  46. package/human/html.ts +39 -18
  47. package/human/local.ts +26 -0
  48. package/human/quo-human.md +70 -13
  49. package/human/screen.ts +27 -15
  50. package/human/tab.ts +50 -5
  51. package/human/web.ts +34 -5
  52. package/mcp/runner.ts +5 -3
  53. package/mcp/server.ts +5 -3
  54. package/package.json +14 -2
package/human/web.ts CHANGED
@@ -23,6 +23,8 @@
23
23
  import type { IncomingMessage, ServerResponse } from 'node:http';
24
24
  import { existsSync } from 'node:fs';
25
25
  import { fileURLToPath } from 'node:url';
26
+ import { join } from 'node:path';
27
+ import process from 'node:process';
26
28
  import { build } from 'esbuild';
27
29
  import type { DiskHarbor, Hosted } from '../harbor/disk.ts';
28
30
  import { door } from './door.ts';
@@ -33,16 +35,28 @@ export type WebOptions = {
33
35
  };
34
36
 
35
37
  // The paths under `/web` that are the exchange's, never a ward's name.
36
- const RESERVED_PATHS = new Set(['allow', 'tab.js']);
38
+ const RESERVED_PATHS = new Set(['allow', 'tab.js', 'beings.js']);
39
+
40
+ // The world's code for the tab: a module in the harbor folder, the twin of
41
+ // `classes/index.ts`, exporting by name the classes the tab boots into its
42
+ // local ward for a world of this harbor. Served bundled as `beings.js` on
43
+ // this origin, and one origin is one world's code.
44
+ export const TAB_CODE = 'tab/index.ts';
37
45
 
38
46
  export function webRoute(harbor: DiskHarbor, o: WebOptions): (req: IncomingMessage, res: ServerResponse, rest: string) => Promise<boolean> {
39
47
  const tabEntry = () => {
40
48
  const js = fileURLToPath(new URL('./tab.js', import.meta.url));
41
49
  return existsSync(js) ? js : fileURLToPath(new URL('./tab.ts', import.meta.url));
42
50
  };
51
+ const beingsEntry = join(harbor.dir, TAB_CODE);
52
+ const bundled = (entry: string) =>
53
+ // the entry may sit in a harbor folder with no node_modules of its own, as on a droplet whose
54
+ // estate folder is the working directory: what it imports is resolved from there too
55
+ build({ entryPoints: [entry], bundle: true, format: 'esm', platform: 'browser', target: 'es2023', write: false, nodePaths: [join(process.cwd(), 'node_modules')] }).then((out) => out.outputFiles[0]!.text);
43
56
  let bundle: Promise<string> | undefined;
44
- const built = () =>
45
- (bundle ??= build({ entryPoints: [tabEntry()], bundle: true, format: 'esm', platform: 'browser', target: 'es2023', write: false }).then((out) => out.outputFiles[0]!.text));
57
+ const built = () => (bundle ??= bundled(tabEntry()));
58
+ let beings: Promise<string> | undefined;
59
+ const builtBeings = () => (beings ??= bundled(beingsEntry));
46
60
  const quoOrigin = (() => {
47
61
  try {
48
62
  const u = new URL(o.at.quo);
@@ -71,7 +85,7 @@ export function webRoute(harbor: DiskHarbor, o: WebOptions): (req: IncomingMessa
71
85
  };
72
86
  const wards = () => Object.fromEntries([...harbor.wards].map(([n, h]) => [n, { pk: h.pk, public: publicOf(h) !== null }]));
73
87
  const tab = (ward?: string) => {
74
- const cfg = { quo: o.at.quo, web: o.at.web, wards: wards(), ...(ward ? { ward } : {}) };
88
+ const cfg = { quo: o.at.quo, web: o.at.web, wards: wards(), beings: existsSync(beingsEntry), ...(ward ? { ward } : {}) };
75
89
  return `<script id="quo" type="application/json">${JSON.stringify(cfg).replace(/</g, '\\u003c')}</script><script type="module" src="${o.at.web}/tab.js"></script>`;
76
90
  };
77
91
  return async (req, res, rest) => {
@@ -88,6 +102,13 @@ export function webRoute(harbor: DiskHarbor, o: WebOptions): (req: IncomingMessa
88
102
  res.end(await built());
89
103
  return true;
90
104
  }
105
+ // the world's code for the tab, when the harbor folder holds any; a harbor with none has no such path
106
+ if (rest === '/beings.js' && req.method === 'GET') {
107
+ if (!existsSync(beingsEntry)) return false;
108
+ res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' });
109
+ res.end(await builtBeings());
110
+ return true;
111
+ }
91
112
  const wardName = parts[0] ?? '';
92
113
  if (!wardName || RESERVED_PATHS.has(wardName)) return false;
93
114
  const hosted = harbor.wards.get(wardName);
@@ -104,18 +125,26 @@ const CSS = [
104
125
  ':root{color-scheme:light dark;--accent:#3b6ef5;--bg:transparent;--fg:inherit;--font:system-ui,sans-serif;--radius:6px;--ink:#1c1b22;--paper:#fbfaf7;--mute:#6b6a73}',
105
126
  '@media(prefers-color-scheme:dark){:root{--ink:#ecebe6;--paper:#141318;--mute:#9a99a2}}',
106
127
  'html{background:var(--paper);color:var(--ink)}',
107
- 'body{font:16px/1.5 system-ui,sans-serif;max-width:40rem;margin:2rem auto;padding:0 1rem}',
128
+ // one column that is a phone first and a desk after: the same page, more air
129
+ 'body{font:16px/1.5 system-ui,sans-serif;max-width:40rem;margin:1rem auto;padding:0 1rem}@media(min-width:40rem){body{margin:2rem auto}}',
130
+ 'input:not([type=checkbox]),select,textarea{width:100%;max-width:24rem;box-sizing:border-box}',
108
131
  'nav.worlds{display:flex;flex-wrap:wrap;gap:.5rem 1rem;font-size:.9rem;opacity:.8}nav.worlds a[aria-current]{font-weight:600}',
109
132
  'nav.relations{display:flex;flex-wrap:wrap;gap:.25rem;margin:.5rem 0}nav.relations button{background:transparent;color:inherit;border:1px solid color-mix(in srgb,currentColor 30%,transparent)}nav.relations button[aria-current]{border-color:var(--accent);font-weight:600}',
110
133
  'header,main{background:var(--bg);color:var(--fg);font-family:var(--font)}header{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem}header .notice{width:100%;margin:0}',
111
134
  'input,select,textarea,button{font:inherit;padding:.4rem;margin:.2rem;border-radius:var(--radius)}button{background:var(--accent);color:#fff;border:0;padding:.4rem .9rem}',
112
135
  'fieldset{border:1px solid color-mix(in srgb,currentColor 20%,transparent);border-radius:var(--radius);margin:.5rem 0}',
136
+ // a being of the world running in the tab: her own section under the far page
137
+ 'section.local{border-top:1px solid color-mix(in srgb,currentColor 15%,transparent);margin:1.5rem 0;padding-top:.5rem}section.local header:empty{display:none}',
113
138
  'section.standing{background:var(--bg);color:var(--fg);font-family:var(--font);border-left:4px solid var(--accent);border-radius:var(--radius);padding:.25rem 1rem;margin:1.5rem 0}section.standing h2{display:flex;align-items:center;gap:.5rem;font-size:1.1rem}img.logo{height:1.6rem}',
114
139
  'table{border-collapse:collapse}td,th{padding:.15rem .5rem;text-align:left}.answer{margin:.5rem 0 1rem;padding:.5rem;border-left:3px solid var(--accent)}.answer.error{border-color:#c33}.answer.silence,.answer.word,.answer.unreached{border-color:#c93}pre{padding:.75rem;overflow:auto}',
115
140
  // a page in the grammar: regions, roles, cards
116
141
  '.page .stack{display:flex;flex-direction:column;gap:.5rem}.page .row{display:flex;flex-wrap:wrap;gap:.5rem 1rem;align-items:baseline}',
117
142
  '.page .t-title{font-size:2rem;font-weight:600;letter-spacing:-.02em;margin:.5rem 0}.page .t-lead{font-size:1.25rem;margin:.25rem 0}.page .t-label{font-size:.8rem;letter-spacing:.06em;text-transform:uppercase;color:var(--mute);margin:0}.page .t-quiet{color:var(--mute);font-size:.9rem}.page .t-body{margin:.25rem 0}',
118
143
  '.page .picture{max-height:6rem}.cards{display:flex;flex-wrap:wrap;gap:.75rem}.card{border:1px solid color-mix(in srgb,currentColor 20%,transparent);border-radius:var(--radius);padding:.5rem .75rem}',
144
+ // a row with a picture is a line about someone or something: the picture small and the words beside it, centred
145
+ '.page .row:has(>.picture){align-items:center;flex-wrap:nowrap}.page .row>.picture{max-height:2.75rem;flex:none}.page .row>.stack{gap:0;min-width:0}.page .row>.stack>p{margin:0}',
146
+ // a far page inside a standing's section: her title is a section's, not the page's
147
+ 'section.standing .page{padding:.5rem 0}section.standing .page .t-title{font-size:1.3rem;margin:.25rem 0}',
119
148
  // the door page: one column, generous air, the mark, a word, a sentence
120
149
  'main.door{min-height:70vh;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;max-width:32rem;margin:0 auto;padding:3rem 0;font-family:ui-serif,Georgia,"Times New Roman",serif}',
121
150
  'main.door .picture{width:3.5rem;height:3.5rem;color:var(--ink);opacity:.9;margin-bottom:1.25rem}',
package/mcp/runner.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  import type { Blueprint, JsonObject } from '@quo-systems/quo';
14
14
  import type { Avatar } from '../beings/avatar.ts';
15
15
  import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT, type Serving } from '../beings/side.ts';
16
+ import { presentation } from '../beings/look.ts';
16
17
 
17
18
  // Where the model is. `key` is a bearer for endpoints that want one, read
18
19
  // from the device and never from cells. `turns` is the ceiling of model
@@ -32,11 +33,12 @@ type Completion = { choices: { message: { role: 'assistant'; content: string | n
32
33
  // Her describe, spoken as a tools array. Name, description and input are
33
34
  // verbatim, with the one narrowing an endpoint has asked for: an ask that
34
35
  // declares no properties is sent with an empty `properties`, because
35
- // LM Studio refuses a parameters schema without one. `look` and `page` are
36
- // presentation, for a screen, and are not functions.
36
+ // LM Studio refuses a parameters schema without one. Her look, her page and
37
+ // every carried page are presentation, for a screen, and are not functions.
37
38
  export function tools(bp: Blueprint): Tool[] {
39
+ const shown = presentation(bp);
38
40
  return bp.asks
39
- .filter((a) => a.name !== 'look' && a.name !== 'page')
41
+ .filter((a) => !shown.has(a.name))
40
42
  .map((a) => {
41
43
  const t: Tool = { type: 'function', function: { name: a.name, parameters: { properties: {}, ...a.input, type: 'object' } } };
42
44
  if (a.description !== undefined) t.function.description = a.description;
package/mcp/server.ts CHANGED
@@ -13,7 +13,7 @@ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
13
13
  import { ListToolsRequestSchema, CallToolRequestSchema, type CallToolResult, type Tool } from '@modelcontextprotocol/sdk/types.js';
14
14
  import type { Blueprint, JsonObject } from '@quo-systems/quo';
15
15
  import { word, wordText, SILENCE_TEXT, UNREACHED_TEXT, type Serving, type Subject } from '../beings/side.ts';
16
- import { hintFor, sanitise, type Look } from '../beings/look.ts';
16
+ import { hintFor, presentation, sanitise, type Look } from '../beings/look.ts';
17
17
  import { isSilence, isWord, digest } from '@quo-systems/quo';
18
18
  import type { Json } from '@quo-systems/quo';
19
19
 
@@ -28,10 +28,12 @@ export const DESCRIBE: Tool = { name: 'describe', description: 'the empty ask: h
28
28
  // Her describe, spoken as tools, the empty ask first. Name, description and
29
29
  // input are verbatim; an output schema crosses when she declared one. Her
30
30
  // look, when she has one, is the hints: a title, and the annotations a host
31
- // reads. The `look` ask itself is presentation, and is not a tool.
31
+ // reads. Her look, her page and every carried page are presentation, for a
32
+ // screen, and are not tools.
32
33
  export function tools(bp: Blueprint, look: Look = {}): Tool[] {
34
+ const shown = presentation(bp);
33
35
  const asks = bp.asks
34
- .filter((a) => a.name !== 'look' && a.name !== 'page') // presentation, for a screen, and not a tool
36
+ .filter((a) => !shown.has(a.name))
35
37
  .map((a) => {
36
38
  const t: Tool = { name: a.name, inputSchema: { ...a.input, type: 'object' } };
37
39
  if (a.description !== undefined) t.description = a.description;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/dock",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
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
5
  "keywords": [
6
6
  "quo",
@@ -31,6 +31,10 @@
31
31
  "types": "./dist/harbor/browser.d.ts",
32
32
  "default": "./dist/harbor/browser.js"
33
33
  },
34
+ "./harbor/capacitor": {
35
+ "types": "./dist/harbor/capacitor.d.ts",
36
+ "default": "./dist/harbor/capacitor.js"
37
+ },
34
38
  "./cli/daemon": {
35
39
  "types": "./dist/cli/daemon.d.ts",
36
40
  "default": "./dist/cli/daemon.js"
@@ -55,6 +59,10 @@
55
59
  "types": "./dist/human/html.d.ts",
56
60
  "default": "./dist/human/html.js"
57
61
  },
62
+ "./human/local": {
63
+ "types": "./dist/human/local.d.ts",
64
+ "default": "./dist/human/local.js"
65
+ },
58
66
  "./harbor/edge/worker": {
59
67
  "types": "./dist/harbor/edge/worker.d.ts",
60
68
  "default": "./dist/harbor/edge/worker.js"
@@ -67,8 +75,12 @@
67
75
  "prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
68
76
  },
69
77
  "dependencies": {
78
+ "@aparajita/capacitor-secure-storage": "^8.0.0",
79
+ "@capacitor/app": "^8.1.1",
80
+ "@capacitor/core": "^8.5.1",
81
+ "@capacitor/filesystem": "^8.1.3",
70
82
  "@modelcontextprotocol/sdk": "^1.30.0",
71
- "@quo-systems/quo": "^0.2.3",
83
+ "@quo-systems/quo": "^0.2.5",
72
84
  "esbuild": "^0.28.2",
73
85
  "ws": "^8.21.3"
74
86
  },