makersclaw 0.6.0 → 0.8.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 (152) hide show
  1. package/README.md +56 -9
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +6 -6
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +10 -4
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +126 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +3 -3
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +24 -12
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chart/cartesian.jsx +1 -1
  67. package/registry/blocks/chart/core.jsx +33 -23
  68. package/registry/blocks/chart/pie.jsx +1 -1
  69. package/registry/blocks/chat/README.md +47 -14
  70. package/registry/blocks/chat/demo.jsx +2 -2
  71. package/registry/blocks/code-editor/README.md +4 -0
  72. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  73. package/registry/blocks/comment-box/README.md +20 -13
  74. package/registry/blocks/contact-section/README.md +7 -6
  75. package/registry/blocks/csv-import/README.md +34 -12
  76. package/registry/blocks/data-table/README.md +41 -8
  77. package/registry/blocks/diagram/diagram.jsx +1 -1
  78. package/registry/blocks/doc-editor/README.md +18 -6
  79. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  80. package/registry/blocks/doc-page/README.md +16 -6
  81. package/registry/blocks/file-upload/README.md +7 -1
  82. package/registry/blocks/flow-canvas/README.md +30 -12
  83. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  84. package/registry/blocks/footer/README.md +7 -4
  85. package/registry/blocks/gantt/README.md +18 -12
  86. package/registry/blocks/job-panel/README.md +1 -1
  87. package/registry/blocks/json-editor/README.md +16 -4
  88. package/registry/blocks/map/README.md +19 -3
  89. package/registry/blocks/map/map-view.jsx +1 -1
  90. package/registry/blocks/map/maplibre.js +1 -1
  91. package/registry/blocks/mind-map/README.md +20 -7
  92. package/registry/blocks/org-chart/README.md +14 -9
  93. package/registry/blocks/page-builder/README.md +14 -7
  94. package/registry/blocks/page-builder/demo.jsx +3 -3
  95. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  96. package/registry/blocks/page-builder/page-view.jsx +1 -1
  97. package/registry/blocks/page-builder/page.js +1 -1
  98. package/registry/blocks/pdf-viewer/README.md +26 -21
  99. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  100. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  101. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  102. package/registry/blocks/product-detail/README.md +13 -7
  103. package/registry/blocks/product-detail/demo.jsx +1 -1
  104. package/registry/blocks/product-grid/README.md +12 -10
  105. package/registry/blocks/record-form/README.md +26 -9
  106. package/registry/blocks/record-notes/README.md +42 -30
  107. package/registry/blocks/search-dialog/README.md +38 -7
  108. package/registry/blocks/settings-list/README.md +29 -17
  109. package/registry/blocks/setup-checklist/README.md +12 -14
  110. package/registry/blocks/share-dialog/README.md +1 -1
  111. package/registry/blocks/sidebar/README.md +23 -13
  112. package/registry/blocks/signature-pad/README.md +15 -4
  113. package/registry/blocks/sketch-canvas/README.md +25 -10
  114. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  115. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  116. package/registry/blocks/spreadsheet/README.md +17 -8
  117. package/registry/blocks/stat-cards/README.md +17 -8
  118. package/registry/blocks/task-list/README.md +14 -11
  119. package/registry/blocks/task-model/README.md +3 -3
  120. package/registry/blocks/transaction-list/README.md +15 -7
  121. package/registry/blocks/whiteboard/README.md +34 -22
  122. package/src/cli.mjs +33 -6
  123. package/src/install.mjs +285 -26
  124. package/src/registry.mjs +109 -1
  125. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  126. package/templates/next/browser/app/layout.jsx +94 -0
  127. package/templates/next/browser/app/page.jsx +60 -0
  128. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  129. package/templates/next/browser/lib/actions.js +16 -0
  130. package/templates/next/browser/lib/data.js +43 -0
  131. package/templates/next/files/README.md +1100 -89
  132. package/templates/next/files/app/frame.js +128 -4
  133. package/templates/next/files/app/shell.jsx +54 -37
  134. package/templates/next/files/check/run.mjs +1514 -45
  135. package/templates/next/files/components/ui/index.js +1 -0
  136. package/templates/next/files/components/ui/item.jsx +2 -2
  137. package/templates/next/files/components/ui/layout.jsx +307 -0
  138. package/templates/next/files/components/ui/notice.jsx +1 -1
  139. package/templates/next/files/components/ui/separator.jsx +1 -1
  140. package/templates/next/files/lib/actions.js +170 -51
  141. package/templates/next/files/lib/brain-server.js +96 -0
  142. package/templates/next/files/lib/brain.js +413 -0
  143. package/templates/next/files/lib/claw.js +15 -1
  144. package/templates/next/files/lib/data.js +119 -29
  145. package/templates/next/files/lib/db.js +97 -17
  146. package/templates/next/files/lib/door.js +680 -0
  147. package/templates/next/files/lib/hooks.js +71 -2
  148. package/templates/next/files/lib/platform.js +93 -4
  149. package/templates/next/files/next.config.mjs +36 -2
  150. package/templates/next/files/surfaces.js +3 -2
  151. package/templates/next/template.json +18 -3
  152. package/templates/next/files/schema.sql +0 -22
@@ -0,0 +1,96 @@
1
+ import { headers } from "next/headers";
2
+ import { clawAnswer } from "./claw";
3
+ import { brainRead } from "./brain";
4
+
5
+ // The company brain's reads, from a SERVER component, in a server app.
6
+ // The same nine reads as lib/brain.js, with the same arguments and
7
+ // results (claw docs/brain/CONTRACTS.md §10); no writes. A write from the
8
+ // server would be app code acting, never the member's press, so writes go
9
+ // from the browser through lib/brain.js even here.
10
+ //
11
+ // The browser's door grant is its cookie, which never reaches this
12
+ // server. Here the grant is the bearer the product proxy stamps on each
13
+ // member's request as `x-claw-door` (5 minutes, bound to this app and
14
+ // that member), read with headers(), and the door is CLAW_DOOR_URL (the
15
+ // app's public origin and path plus /_claw/data). No header (a render
16
+ // outside a member's request, or a machine whose proxy does not stamp it
17
+ // yet) or no CLAW_DOOR_URL: each read answers { ok: false, unavailable:
18
+ // true, code: "no_door" } and asks nothing. Read in the browser then.
19
+ //
20
+ // This file pulls in next/headers, so a "use client" file cannot import
21
+ // it (the build says so), as with lib/data.js.
22
+
23
+ const NO_DOOR = "no door grant on this request: read the brain in the browser (lib/brain.js)";
24
+
25
+ /** The bearer the proxy stamped on this request, or null. */
26
+ async function doorGrant() {
27
+ try {
28
+ return (await headers()).get("x-claw-door") || null;
29
+ } catch {
30
+ return null; // outside a request there are no headers at all
31
+ }
32
+ }
33
+
34
+ /** Send one read to the door with the bearer: a claw()-shaped answer. */
35
+ async function viaDoorGrant(path, signal) {
36
+ const grant = await doorGrant();
37
+ const door = String(process.env.CLAW_DOOR_URL ?? "").replace(/\/+$/, "");
38
+ if (!grant || !door) return { ok: false, status: 0, error: NO_DOOR, code: "no_door", unavailable: true };
39
+ let res;
40
+ try {
41
+ res = await fetch(`${door}/${path}`, {
42
+ cache: "no-store",
43
+ headers: { accept: "application/json", authorization: `Bearer ${grant}` },
44
+ signal,
45
+ });
46
+ } catch (error) {
47
+ if (error?.name === "AbortError") return { ok: false, status: 0, error: "aborted", aborted: true };
48
+ return { ok: false, status: 0, error: "not reachable", unavailable: true };
49
+ }
50
+ return clawAnswer(res);
51
+ }
52
+
53
+ /** → { ok, brain, kinds, error }, as lib/brain.js kinds(). */
54
+ export function kinds(args) {
55
+ return brainRead("kinds", args, viaDoorGrant);
56
+ }
57
+
58
+ /** → { ok, records, next, error }, as lib/brain.js listRecords(). */
59
+ export function listRecords(args) {
60
+ return brainRead("listRecords", args, viaDoorGrant);
61
+ }
62
+
63
+ /** → { ok, record, links, error }, as lib/brain.js getRecord(). */
64
+ export function getRecord(args) {
65
+ return brainRead("getRecord", args, viaDoorGrant);
66
+ }
67
+
68
+ /** → { ok, count, by, error }, as lib/brain.js countRecords(). */
69
+ export function countRecords(args) {
70
+ return brainRead("countRecords", args, viaDoorGrant);
71
+ }
72
+
73
+ /** → { ok, meaning, results, error }, as lib/brain.js searchBrain(). */
74
+ export function searchBrain(args) {
75
+ return brainRead("searchBrain", args, viaDoorGrant);
76
+ }
77
+
78
+ /** → { ok, changes, next, error }, as lib/brain.js recordHistory(). */
79
+ export function recordHistory(args) {
80
+ return brainRead("recordHistory", args, viaDoorGrant);
81
+ }
82
+
83
+ /** → { ok, changes, next, error }, as lib/brain.js brainChanges(). */
84
+ export function brainChanges(args) {
85
+ return brainRead("brainChanges", args, viaDoorGrant);
86
+ }
87
+
88
+ /** → { ok, lists, error }, as lib/brain.js brainLists(). */
89
+ export function brainLists(args) {
90
+ return brainRead("brainLists", args, viaDoorGrant);
91
+ }
92
+
93
+ /** → { ok, list, records, next, error }, as lib/brain.js listMembers(). */
94
+ export function listMembers(args) {
95
+ return brainRead("listMembers", args, viaDoorGrant);
96
+ }
@@ -0,0 +1,413 @@
1
+ import { claw } from "./claw";
2
+ import { dataChanged, inTurn, useDoorRead } from "./door";
3
+ import { surfaceOfPath } from "./platform";
4
+
5
+ // The company brain, through the data door.
6
+ //
7
+ // The brain is the workspace's shared records (contacts, companies,
8
+ // deals, whatever kinds the workspace keeps), each in an area: Everyone,
9
+ // Finance… An app never reaches it with SQL: its database login holds
10
+ // none of it. It reads and writes through _claw/data, which the Makersclaw
11
+ // gateway answers itself under this app's path, before anything reaches
12
+ // the app, so it works while the app's machine sleeps (claw
13
+ // docs/brain/CONTRACTS.md §8 and §10). The door hands the app only the
14
+ // kinds its makersclaw.json declares, and only what the member may read.
15
+ //
16
+ // BROWSER, in both modes: call these from "use client" files, in a server
17
+ // app too. The grant cookie is who is asking, and it makes a write a
18
+ // person's press: confirmed, able to overwrite a confirmed value, erase or
19
+ // merge. A write from the app's server would be app code acting:
20
+ // unconfirmed, skipped over a confirmed value, never an erase. So there is
21
+ // no Server Action for the brain. A server component reads with
22
+ // lib/brain-server.js: the same reads, over the proxy's door grant.
23
+ //
24
+ // Every function resolves to a plain object and never throws, like
25
+ // lib/data.js:
26
+ // - a read: the door's JSON as it came, plus `error: null`;
27
+ // - a write: the door's JSON as it came;
28
+ // - a refusal: { ok: false, error, code }. A read keeps its empty values
29
+ // (records: [], next: null…), so a surface renders it like no rows;
30
+ // - a 404 that names no code, or no network: { ok: false, error,
31
+ // unavailable: true }. A claw with no door yet: show a quiet "not
32
+ // connected yet", not an error.
33
+ // Arguments are camelCase (ifVersion); the door's keys are not (if_version).
34
+ //
35
+ // The door writes the activity line for every write it takes, so nothing
36
+ // here calls logActivity. Writes go one at a time, in the order called,
37
+ // in the same turn as lib/door.js's; one that answers ok calls
38
+ // dataChanged(), so every hook on the page reads again: these four
39
+ // (useRecords, useRecord, useRecordCounts, useBrainSearch) and the own
40
+ // tables' (lib/door.js). A server app may also router.refresh().
41
+
42
+ /** A value the door's path needs was not given. */
43
+ class Missing extends Error {}
44
+
45
+ /** One path segment: an id or a kind, encoded, so "a/b" stays one. */
46
+ function segment(value, what) {
47
+ if (value === undefined || value === null || value === "") throw new Missing(`needs ${what}`);
48
+ return encodeURIComponent(String(value));
49
+ }
50
+
51
+ /** A list of names as the door reads it, "a,b"; a string is taken as it is. */
52
+ function names(value) {
53
+ if (value === undefined || value === null) return undefined;
54
+ return Array.isArray(value) ? value.map(String).join(",") : String(value);
55
+ }
56
+
57
+ /** A filter as the door reads it: JSON in the query. A string is taken
58
+ * as JSON already. */
59
+ function filter(where) {
60
+ if (where === undefined || where === null) return undefined;
61
+ return typeof where === "string" ? where : JSON.stringify(where);
62
+ }
63
+
64
+ /** "?a=1&b=2", leaving out what was not given. */
65
+ function query(params) {
66
+ const q = new URLSearchParams();
67
+ for (const [key, value] of Object.entries(params)) {
68
+ if (value === undefined || value === null || value === "") continue;
69
+ q.set(key, String(value));
70
+ }
71
+ const text = q.toString();
72
+ return text ? `?${text}` : "";
73
+ }
74
+
75
+ /** The argument off, for a flag the door turns off with "0". */
76
+ const off = (flag) => (flag === false ? 0 : undefined);
77
+
78
+ /**
79
+ * The door's read routes: each turns its arguments into the path under
80
+ * _claw/data and names what an empty answer holds. A path of null asks
81
+ * nothing (a blank search). lib/brain-server.js reads the same paths.
82
+ */
83
+ export const BRAIN_READS = {
84
+ kinds: {
85
+ empty: { kinds: [] },
86
+ path: () => "kinds",
87
+ },
88
+ listRecords: {
89
+ empty: { records: [], next: null },
90
+ path: ({ kind, where, order, limit, cursor, fields, prefix, state }) =>
91
+ `kinds/${segment(kind, "a kind")}/records` +
92
+ query({ where: filter(where), order, limit, cursor, fields: names(fields), prefix, state: off(state) }),
93
+ },
94
+ getRecord: {
95
+ empty: { record: null, links: null },
96
+ path: ({ id, links = false, state }) => `records/${segment(id, "an id")}` + query({ links: links ? 1 : undefined, state: off(state) }),
97
+ },
98
+ countRecords: {
99
+ empty: { count: 0, by: null },
100
+ path: ({ kind, by, where }) => `kinds/${segment(kind, "a kind")}/counts` + query({ by, where: filter(where) }),
101
+ },
102
+ searchBrain: {
103
+ empty: { results: [], meaning: false },
104
+ path: ({ q, kinds, limit, meaning }) =>
105
+ String(q ?? "").trim() ? "search" + query({ q, kinds: names(kinds), limit, meaning: off(meaning) }) : null,
106
+ },
107
+ recordHistory: {
108
+ empty: { changes: [], next: null },
109
+ path: ({ id, limit, cursor }) => `records/${segment(id, "an id")}/history` + query({ limit, cursor }),
110
+ },
111
+ brainChanges: {
112
+ empty: { changes: [], next: null },
113
+ path: ({ since, kinds }) => "changes" + query({ since, kinds: names(kinds) }),
114
+ },
115
+ brainLists: {
116
+ empty: { lists: [] },
117
+ path: ({ kind }) => "lists" + query({ kind }),
118
+ },
119
+ listMembers: {
120
+ empty: { list: null, records: [], next: null },
121
+ path: ({ list, limit, cursor }) => `lists/${segment(list, "a list")}` + query({ limit, cursor }),
122
+ },
123
+ };
124
+
125
+ /** What went wrong before anything was sent, said as the door would. */
126
+ function refusedHere(name, error, empty = {}) {
127
+ const words = error instanceof Missing ? `${name} ${error.message}` : `${name}: ${error?.message ?? error}`;
128
+ return { ...empty, ok: false, error: words, code: "bad_request" };
129
+ }
130
+
131
+ /** A claw()-shaped answer → the result: the door's JSON as it came (a
132
+ * read's with `error: null` and its empty values under it), or the
133
+ * failure in the same keys. */
134
+ function shaped(answer, empty, read) {
135
+ const data = answer?.data;
136
+ const json = data && typeof data === "object" && !Array.isArray(data);
137
+ if (answer?.ok && json && data.ok !== false) return read ? { ...empty, ...data, error: null } : data;
138
+ const out = { ...empty, ok: false };
139
+ if (answer?.ok) {
140
+ // A 2xx that is not the door's success: its refusal, or not its JSON.
141
+ out.error = json ? String(data.error || "the door refused") : "the door answered something that is not its JSON";
142
+ if (json && typeof data.code === "string" && data.code) out.code = data.code;
143
+ return out;
144
+ }
145
+ out.error = String(answer?.error || "the door refused");
146
+ if (answer?.code) out.code = answer.code;
147
+ if (answer?.unavailable) out.unavailable = true;
148
+ if (answer?.aborted) out.aborted = true;
149
+ return out;
150
+ }
151
+
152
+ /**
153
+ * Run one of BRAIN_READS: its path from the arguments, `send(path,
154
+ * signal)` for a claw()-shaped answer, then the result. Never throws.
155
+ * The browser's reads send through claw(); lib/brain-server.js passes a
156
+ * `send` of its own.
157
+ */
158
+ export async function brainRead(name, args, send) {
159
+ const { empty, path } = BRAIN_READS[name];
160
+ let sub;
161
+ try {
162
+ sub = path(args ?? {});
163
+ } catch (error) {
164
+ return refusedHere(name, error, empty);
165
+ }
166
+ if (sub === null) return { ok: true, ...empty, error: null };
167
+ let answer;
168
+ try {
169
+ answer = await send(sub, args?.signal);
170
+ } catch (error) {
171
+ answer = { ok: false, error: String(error?.message ?? error) };
172
+ }
173
+ return shaped(answer, empty, true);
174
+ }
175
+
176
+ const viaCookie = (path, signal) => claw(`data/${path}`, { signal });
177
+
178
+ /** The surface the page is on, for the door's activity line. */
179
+ function pageSurface() {
180
+ return typeof location === "undefined" ? undefined : surfaceOfPath(location.pathname);
181
+ }
182
+
183
+ /** POST one write route, in its turn (lib/door.js inTurn). `build(args)`
184
+ * → { path, body }, or { done } when there is nothing to send. A write
185
+ * the door takes calls dataChanged(). Never throws. */
186
+ async function post(name, args, build) {
187
+ let built;
188
+ try {
189
+ built = build(args ?? {});
190
+ if (built.done) return built.done;
191
+ // claw() would read a body that cannot be JSON as "not reachable".
192
+ JSON.stringify(built.body);
193
+ } catch (error) {
194
+ return refusedHere(name, error);
195
+ }
196
+ return inTurn(async () => {
197
+ let answer;
198
+ try {
199
+ answer = await claw(`data/${built.path}`, { method: "POST", body: built.body });
200
+ } catch (error) {
201
+ answer = { ok: false, error: String(error?.message ?? error) };
202
+ }
203
+ const out = shaped(answer, {}, false);
204
+ if (out.ok !== false) dataChanged();
205
+ return out;
206
+ });
207
+ }
208
+
209
+ /** One write as the door reads it. `surface` defaults to the page's
210
+ * surface key (the <key> of /s/<key>, as logActivity's lines name it);
211
+ * null sends none. */
212
+ function writeBody(write) {
213
+ const { kind, id, fields, area, from, ifVersion, owner, surface, detail } = write ?? {};
214
+ return {
215
+ kind,
216
+ id,
217
+ fields,
218
+ area,
219
+ from,
220
+ if_version: ifVersion,
221
+ owner,
222
+ surface: surface === undefined ? pageSurface() : surface,
223
+ detail,
224
+ };
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Reads. Each takes an optional `signal` to abort it.
229
+
230
+ /** GET _claw/data/kinds: the kinds this app declared, with the words,
231
+ * fields and counts the member may see.
232
+ * → { ok, brain, kinds: [{ kind, label, plural, fields, key, filters,
233
+ * label_field, reads, writes, count, unconfirmed }], error }. `brain` is
234
+ * false while the workspace has no brain yet (every read is empty). */
235
+ export function kinds(args) {
236
+ return brainRead("kinds", args, viaCookie);
237
+ }
238
+
239
+ /** GET _claw/data/kinds/<kind>/records: one kind's records, newest first.
240
+ * `where` an object (below), `order` "-updated_at" (default) |
241
+ * "updated_at" | "created_at" | "-created_at" | "label" | "-label" |
242
+ * "<field>" | "-<field>", `limit` 50 (at most 200), `cursor` the last
243
+ * answer's `next`, `fields` ["name", "email"] only those, `prefix` the
244
+ * label starts with (pickers), `state: false` leaves each field's state
245
+ * out. → { ok, records, next, error } */
246
+ export function listRecords(args) {
247
+ return brainRead("listRecords", args, viaCookie);
248
+ }
249
+
250
+ /** GET _claw/data/records/<id>: one record; `links: true` adds its links
251
+ * both ways, to records the member may read.
252
+ * → { ok, record, links: { out, in } | null, error } */
253
+ export function getRecord(args) {
254
+ return brainRead("getRecord", args, viaCookie);
255
+ }
256
+
257
+ /** GET _claw/data/kinds/<kind>/counts: how many records match `where`;
258
+ * `by` a field counts per value ("null" for none).
259
+ * → { ok, count, by: { [value]: n } | null, error } */
260
+ export function countRecords(args) {
261
+ return brainRead("countRecords", args, viaCookie);
262
+ }
263
+
264
+ /** GET _claw/data/search: records by words and meaning, across `kinds`
265
+ * (default: every kind the app reads), `limit` at most 50. `meaning:
266
+ * false` is keywords only: nothing is embedded or charged (a picker that
267
+ * searches as the member types). A blank `q` asks nothing.
268
+ * → { ok, meaning, results: [{ record: { id, kind, label, area,
269
+ * updated_at }, score, matched, snippet }], error } */
270
+ export function searchBrain(args) {
271
+ return brainRead("searchBrain", args, viaCookie);
272
+ }
273
+
274
+ /** GET _claw/data/records/<id>/history: who changed what, newest first
275
+ * (`limit` at most 100). → { ok, changes: [{ id, at, op, fields, before,
276
+ * after, by: { member, app, via } }], next, error } */
277
+ export function recordHistory(args) {
278
+ return brainRead("recordHistory", args, viaCookie);
279
+ }
280
+
281
+ /** GET _claw/data/changes: what changed in the member's areas and the
282
+ * app's kinds since `since` (the last answer's `next`; none: from the
283
+ * start). → { ok, changes: [{ id, at, op, kind, record_id }], next, error } */
284
+ export function brainChanges(args) {
285
+ return brainRead("brainChanges", args, viaCookie);
286
+ }
287
+
288
+ /** GET _claw/data/lists: the lists of records, of one `kind` or all.
289
+ * → { ok, lists: [{ id, name, kind, area, count }], error } */
290
+ export function brainLists(args) {
291
+ return brainRead("brainLists", args, viaCookie);
292
+ }
293
+
294
+ /** GET _claw/data/lists/<list>: a list and its records, in order.
295
+ * → { ok, list, records, next, error } */
296
+ export function listMembers(args) {
297
+ return brainRead("listMembers", args, viaCookie);
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Writes. The member's press, through the cookie; the door logs each.
302
+
303
+ /** POST _claw/data/write: make or change one record. Without `id` the
304
+ * door finds the record by the kind's key, or makes one. `fields` {
305
+ * name: value | null } (null clears), `area` a new record's area (or a
306
+ * move), `ifVersion` refuses with `changed` when someone saved since,
307
+ * `owner` { kind: "member" | "group", id } | null, `surface` (default the
308
+ * page's) and `detail` (one sentence) for the activity line.
309
+ * → { ok, id, created, changed, version, skipped } | { ok: false, error, code } */
310
+ export function writeRecord(args) {
311
+ return post("writeRecord", args, (write) => ({ path: "write", body: writeBody(write) }));
312
+ }
313
+
314
+ /** POST _claw/data/write with { writes }: up to 100 writes, one
315
+ * transaction, all or none. → { ok, results: [one writeRecord answer
316
+ * each] } | { ok: false, error, code } */
317
+ export function writeRecords(args) {
318
+ return post("writeRecords", args, ({ writes }) => {
319
+ if (!Array.isArray(writes)) throw new Missing("needs a list of writes");
320
+ if (!writes.length) return { done: { ok: true, results: [] } };
321
+ return { path: "write", body: { writes: writes.map(writeBody) } };
322
+ });
323
+ }
324
+
325
+ /** POST _claw/data/records/<id>/confirm: the member says these `fields`
326
+ * are right (none named: every unconfirmed one).
327
+ * → { ok, confirmed, version } */
328
+ export function confirmFields(args) {
329
+ return post("confirmFields", args, ({ id, fields }) => ({ path: `records/${segment(id, "an id")}/confirm`, body: { fields } }));
330
+ }
331
+
332
+ /** POST _claw/data/records/<id>/erase: gone for every app, with no undo.
333
+ * A person's press only. → { ok, erased } */
334
+ export function eraseRecord(args) {
335
+ return post("eraseRecord", args, ({ id, reason }) => ({ path: `records/${segment(id, "an id")}/erase`, body: { reason } }));
336
+ }
337
+
338
+ /** POST _claw/data/records/<keep>/merge: `drop` folds into `keep` (its
339
+ * empty fields filled, its links and lists moved) and goes.
340
+ * → { ok, id, merged, filled, version } */
341
+ export function mergeRecords(args) {
342
+ return post("mergeRecords", args, ({ keep, drop }) => ({ path: `records/${segment(keep, "the id to keep")}/merge`, body: { drop } }));
343
+ }
344
+
345
+ /** POST _claw/data/links { op: "add" }: link two records (`type` default
346
+ * "related"). → { ok, created } */
347
+ export function linkRecords(args) {
348
+ return post("linkRecords", args, ({ from, to, type }) => ({ path: "links", body: { op: "add", from, to, type } }));
349
+ }
350
+
351
+ /** POST _claw/data/links { op: "remove" }: unlink them (no `type`: every
352
+ * type). → { ok, removed } */
353
+ export function unlinkRecords(args) {
354
+ return post("unlinkRecords", args, ({ from, to, type }) => ({ path: "links", body: { op: "remove", from, to, type } }));
355
+ }
356
+
357
+ /** POST _claw/data/lists: a new list of one kind's records.
358
+ * → { ok, id } */
359
+ export function createList(args) {
360
+ return post("createList", args, ({ name, kind, area }) => ({ path: "lists", body: { name, kind, area } }));
361
+ }
362
+
363
+ /** POST _claw/data/lists/<list>/members: `add` and `remove` record ids
364
+ * (at most 500), `position` where the added go. → { ok, added, removed } */
365
+ export function changeList(args) {
366
+ return post("changeList", args, ({ list, add, remove, position }) => ({
367
+ path: `lists/${segment(list, "a list")}/members`,
368
+ body: { add, remove, position },
369
+ }));
370
+ }
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // Hooks, for "use client" components: a read on mount, again on every
374
+ // dataChanged() (a write of this page's that answered ok, or a browser
375
+ // app's auto-refresh), with lib/door.js's rules: `loading` until the first
376
+ // answer for these arguments, a re-read keeps the last values until the
377
+ // new ones land, each answer a new array, a read no longer needed
378
+ // aborted. Each returns the read's values with `loading` and `reload`.
379
+
380
+ /** A brain read, for a hook: the browser's, through the cookie. */
381
+ const hookRead = (name) => (args) => brainRead(name, args, viaCookie);
382
+
383
+ /** A read's empty values, fresh: a hook hands out a new array each time. */
384
+ const emptyOf = (name) => () => JSON.parse(JSON.stringify(BRAIN_READS[name].empty));
385
+
386
+ /** listRecords as a hook. → { records, next, error, loading, reload } */
387
+ export function useRecords(args) {
388
+ return useDoorRead(hookRead("listRecords"), args, { empty: emptyOf("listRecords") });
389
+ }
390
+
391
+ /** getRecord as a hook; no id asks nothing and answers record: null.
392
+ * → { record, links, error, loading, reload } */
393
+ export function useRecord(args) {
394
+ return useDoorRead(hookRead("getRecord"), args, {
395
+ empty: emptyOf("getRecord"),
396
+ idle: (a) => a.id === undefined || a.id === null || a.id === "",
397
+ });
398
+ }
399
+
400
+ /** countRecords as a hook. → { count, by, error, loading, reload } */
401
+ export function useRecordCounts(args) {
402
+ return useDoorRead(hookRead("countRecords"), args, { empty: emptyOf("countRecords") });
403
+ }
404
+
405
+ /** searchBrain as a hook; a blank `q` asks nothing. For a picker that
406
+ * searches as the member types, pass `meaning: false`: each keystroke
407
+ * aborts the read before it. → { results, meaning, error, loading, reload } */
408
+ export function useBrainSearch(args) {
409
+ return useDoorRead(hookRead("searchBrain"), args, {
410
+ empty: emptyOf("searchBrain"),
411
+ idle: (a) => !String(a.q ?? "").trim(),
412
+ });
413
+ }
@@ -2,6 +2,8 @@
2
2
  // product's path, before it proxies anything to the app
3
3
  // (docs/ARTIFACT-CONTRACTS.md §5). It is reachable from the BROWSER only —
4
4
  // the grant cookie is the identity — so everything here is client-side.
5
+ // One exception: the data door (lib/brain-server.js) also answers the
6
+ // app's server, with a bearer, and clawAnswer reads its answers there.
5
7
  //
6
8
  // Every call resolves; nothing throws. A 404 that names no refusal, or a
7
9
  // network error, means the gateway has no such route yet, and comes back
@@ -16,7 +18,10 @@
16
18
  // lib/platform.js wraps them as ask / requests / stopRequest and
17
19
  // lib/hooks.js as useJob and useRequests. `chat` and `cancel` are the
18
20
  // 0.3.x door to the same work, streamed into the app (startJob /
19
- // cancelJob): kept for a claw that has no `ask` yet. What an app still
21
+ // cancelJob): kept for a claw that has no `ask` yet. `llm` (0.8.0) is one
22
+ // shot of model work answered in the page, not asking an agent: lib/
23
+ // platform.js wraps it as llm and lib/hooks.js as useLlm, and the check
24
+ // refuses a screen that names its path. What an app still
20
25
  // never calls is monitoring: the OS draws Agent, Runs and Activity as
21
26
  // tabs on its window. The rest of the gateway's routes (§5) stay
22
27
  // reachable through claw() / clawStream() for a surface that has a real
@@ -199,6 +204,15 @@ export async function claw(path, { method = "GET", body, signal } = {}) {
199
204
  if (error?.name === "AbortError") return { ok: false, status: 0, error: "aborted", aborted: true };
200
205
  return { ok: false, status: 0, error: "not reachable", unavailable: true };
201
206
  }
207
+ return clawAnswer(res);
208
+ }
209
+
210
+ /** A gateway's response read the way claw() reads it: a 404 with or
211
+ * without a code (notFound), a refusal's words and code, or the JSON.
212
+ * lib/brain-server.js reaches the data door from the server, with a
213
+ * bearer instead of the cookie, and reads its answers through this, so
214
+ * a 404 means the same thing on both sides. */
215
+ export async function clawAnswer(res) {
202
216
  if (res.status === 404) return notFound(res);
203
217
  if (!res.ok) return { ok: false, status: res.status, ...(await readFailure(res)), unavailable: false };
204
218
  const text = await res.text().catch(() => "");