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.
- package/README.md +56 -9
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +6 -6
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +10 -4
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +126 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +24 -12
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +47 -14
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +30 -12
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +7 -4
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +20 -7
- package/registry/blocks/org-chart/README.md +14 -9
- package/registry/blocks/page-builder/README.md +14 -7
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +26 -21
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +25 -10
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +34 -22
- package/src/cli.mjs +33 -6
- package/src/install.mjs +285 -26
- package/src/registry.mjs +109 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +1100 -89
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/run.mjs +1514 -45
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/layout.jsx +307 -0
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +170 -51
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +15 -1
- package/templates/next/files/lib/data.js +119 -29
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/lib/hooks.js +71 -2
- package/templates/next/files/lib/platform.js +93 -4
- package/templates/next/files/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +18 -3
- 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.
|
|
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(() => "");
|