acrylblends 0.7.6 → 0.7.8

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 (105) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +148 -24
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +1 -1
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/agents/index.md +18 -3
  7. package/docs/framework/getting-started/desktop.md +2 -2
  8. package/docs/framework/guides/client-modules.md +4 -3
  9. package/docs/framework/guides/data.md +72 -8
  10. package/docs/framework/helpers/streaming.md +12 -27
  11. package/docs/framework/upstream.json +2 -2
  12. package/lib/browser/read-sse.js +84 -0
  13. package/lib/index-BLTYsyc1.d.ts +93 -0
  14. package/lib/kv/db.d.ts +12 -0
  15. package/lib/kv/db.js +57 -0
  16. package/lib/kv/json.d.ts +11 -0
  17. package/lib/kv/json.js +224 -0
  18. package/lib/kv.d.ts +2 -0
  19. package/lib/kv.js +2 -0
  20. package/lib/node-server.d.ts +3 -3
  21. package/lib/node-server.js +13 -8
  22. package/lib/read-sse.d.ts +33 -0
  23. package/lib/read-sse.js +84 -0
  24. package/lib/src-DRFVEQQb.js +90 -0
  25. package/package.json +17 -1
  26. package/plugins/acryl-db/plugin.json +1 -1
  27. package/plugins/acryl-db/upstream.json +2 -2
  28. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  29. package/plugins/acryl-inspect/upstream.json +1 -1
  30. package/plugins/acryl-kv/LICENSE +47 -0
  31. package/plugins/acryl-kv/plugin.json +12 -0
  32. package/plugins/acryl-kv/src/check.ts +58 -0
  33. package/plugins/acryl-kv/src/copy.ts +39 -0
  34. package/plugins/acryl-kv/src/cordis.ts +13 -0
  35. package/plugins/acryl-kv/src/index.ts +14 -0
  36. package/plugins/acryl-kv/src/kv.ts +37 -0
  37. package/plugins/acryl-kv/upstream.json +18 -0
  38. package/plugins/acryl-kv-db/LICENSE +47 -0
  39. package/plugins/acryl-kv-db/plugin.json +13 -0
  40. package/plugins/acryl-kv-db/src/index.ts +37 -0
  41. package/plugins/acryl-kv-db/src/open.ts +80 -0
  42. package/plugins/acryl-kv-db/upstream.json +21 -0
  43. package/plugins/acryl-kv-json/LICENSE +47 -0
  44. package/plugins/acryl-kv-json/plugin.json +13 -0
  45. package/plugins/acryl-kv-json/src/index.ts +71 -0
  46. package/plugins/acryl-kv-json/src/open.ts +249 -0
  47. package/plugins/acryl-kv-json/upstream.json +22 -0
  48. package/plugins/acryl-node-server/src/client-modules.ts +10 -9
  49. package/plugins/acryl-node-server/src/index.ts +12 -1
  50. package/plugins/acryl-node-server/upstream.json +2 -2
  51. package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
  52. package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
  53. package/plugins/acryl-router/upstream.json +2 -2
  54. package/plugins/acryl-router-http/upstream.json +1 -1
  55. package/plugins/acryl-state/upstream.json +1 -1
  56. package/plugins/builder/index.ts +2 -2
  57. package/plugins/builder/plugin.json +1 -1
  58. package/plugins/desktop/main.ts +3 -3
  59. package/plugins/desktop/plugin.json +2 -2
  60. package/plugins/desktop/upstream.json +1 -1
  61. package/plugins/desktop-web/main.ts +2 -2
  62. package/plugins/desktop-web/plugin.json +2 -2
  63. package/plugins/desktop-web/upstream.json +1 -1
  64. package/plugins/extensions/index.ts +19 -1
  65. package/plugins/extensions/plugin.json +1 -1
  66. package/plugins/extensions/resolve.ts +1 -1
  67. package/plugins/manifest/lock.ts +7 -3
  68. package/plugins/manifest/upgrade-cli.ts +1 -1
  69. package/plugins/save/cli.ts +1 -1
  70. package/plugins/save/plugin.json +1 -1
  71. package/plugins/tsconfig.json +2 -0
  72. package/shared/AGENTS.core.md +10 -10
  73. package/shared/DEFAULTS.md +40 -0
  74. package/shared/SKILL.md +14 -0
  75. package/templates/agent/AGENTS.md +1 -1
  76. package/templates/agent/README.md +6 -6
  77. package/templates/agent/gitignore +1 -0
  78. package/templates/agent/package.json +8 -8
  79. package/templates/agent/src/app.ts +28 -15
  80. package/templates/agent/src/main.ts +2 -2
  81. package/templates/agent/src/routes.ts +5 -5
  82. package/templates/agent/test/agent.test.ts +1 -1
  83. package/templates/agent/test/app.test.ts +1 -1
  84. package/templates/agent/test/builder.test.ts +1 -1
  85. package/templates/agent/test/model.test.ts +1 -1
  86. package/templates/agent/tsconfig.json +48 -50
  87. package/templates/blank/AGENTS.md +1 -1
  88. package/templates/blank/README.md +2 -2
  89. package/templates/blank/gitignore +1 -0
  90. package/templates/blank/package.json +8 -8
  91. package/templates/blank/src/about.tsx +2 -2
  92. package/templates/blank/src/app.ts +4 -4
  93. package/templates/blank/src/main.ts +1 -1
  94. package/templates/blank/src/routes.ts +1 -1
  95. package/templates/blank/test/app.test.ts +1 -1
  96. package/templates/blank/tsconfig.json +1 -1
  97. package/templates/todo/README.md +1 -1
  98. package/templates/todo/gitignore +1 -0
  99. package/templates/todo/package.json +8 -8
  100. package/templates/todo/src/app.ts +5 -5
  101. package/templates/todo/src/main.ts +1 -1
  102. package/templates/todo/src/todo.ts +4 -4
  103. package/templates/todo/test/todo.test.ts +1 -1
  104. package/templates/todo/tsconfig.json +1 -1
  105. package/templates/todo/upstream.json +2 -2
@@ -0,0 +1,33 @@
1
+ //#region ../../plugins/acryl-router/src/helpers/read-sse.d.ts
2
+ /**
3
+ * `readSSE(response)`: the events of a `text/event-stream` answer from `fetch`, for the browser. `EventSource` reads GET streams only; a chat
4
+ * or any stream that needs a POST body, a header or an `AbortSignal` is read with `fetch`, and this is the loop every such page otherwise writes by
5
+ * hand. It follows the parsing rules of the HTML standard (lines end in LF, CRLF or CR; the `data` lines of one event join with LF; a line that
6
+ * starts with a colon is a comment; an event ends at a blank line; an event with no `data` line is not dispatched; the last event is dispatched
7
+ * only if its blank line arrived), and it is one module with no imports, so it also runs in a service worker and on a fetch runtime.
8
+ *
9
+ * ACRYL's own addition, not part of the upstream port.
10
+ */
11
+ /** One event of the stream. */
12
+ interface SSEEvent {
13
+ /** The `event:` name; `message` when the event has none. */
14
+ readonly event: string;
15
+ /** The `data:` lines of the event, joined with a line feed. */
16
+ readonly data: string;
17
+ /** The `id:` of the event, when it had one. */
18
+ readonly id?: string;
19
+ /** The `retry:` of the event in milliseconds, when it had one. */
20
+ readonly retry?: number;
21
+ }
22
+ interface ReadSSEOptions {
23
+ /** Stops the loop and cancels the response body when it aborts. (Abort the `fetch` itself with the signal you pass to it.) */
24
+ readonly signal?: AbortSignal;
25
+ }
26
+ /**
27
+ * Read the events of a `text/event-stream` response as they arrive. Leaving the loop early (`break`, a thrown error) cancels the body, so the
28
+ * request to the server ends.
29
+ * @throws when the response is not `ok` (the status is in the message; the body is cancelled, not parsed), has no body, or fails while it is read.
30
+ */
31
+ declare function readSSE(response: Response, options?: ReadSSEOptions): AsyncGenerator<SSEEvent, void, undefined>;
32
+ //#endregion
33
+ export { ReadSSEOptions, SSEEvent, readSSE };
@@ -0,0 +1,84 @@
1
+ //#region ../../plugins/acryl-router/src/helpers/read-sse.ts
2
+ /**
3
+ * Read the events of a `text/event-stream` response as they arrive. Leaving the loop early (`break`, a thrown error) cancels the body, so the
4
+ * request to the server ends.
5
+ * @throws when the response is not `ok` (the status is in the message; the body is cancelled, not parsed), has no body, or fails while it is read.
6
+ */
7
+ async function* readSSE(response, options = {}) {
8
+ if (!response.ok) {
9
+ await response.body?.cancel().catch(() => void 0);
10
+ throw new Error(`readSSE: the request failed with ${String(response.status)}${response.statusText === "" ? "" : ` ${response.statusText}`}`);
11
+ }
12
+ if (response.body === null) throw new Error("readSSE: the response has no body to read");
13
+ const { signal } = options;
14
+ if (signal?.aborted === true) {
15
+ await response.body.cancel().catch(() => void 0);
16
+ return;
17
+ }
18
+ const reader = response.body.getReader();
19
+ const onAbort = () => {
20
+ reader.cancel().catch(() => void 0);
21
+ };
22
+ signal?.addEventListener("abort", onAbort, { once: true });
23
+ const decoder = new TextDecoder("utf-8");
24
+ let buffer = "";
25
+ let skipLineFeed = false;
26
+ let name = "";
27
+ let data = [];
28
+ let id;
29
+ let retry;
30
+ /** Apply one line; returns the event when this line completes one. */
31
+ const apply = (line) => {
32
+ if (line === "") {
33
+ const event = data.length === 0 ? void 0 : {
34
+ event: name === "" ? "message" : name,
35
+ data: data.join("\n"),
36
+ ...id === void 0 ? {} : { id },
37
+ ...retry === void 0 ? {} : { retry }
38
+ };
39
+ name = "";
40
+ data = [];
41
+ id = void 0;
42
+ retry = void 0;
43
+ return event;
44
+ }
45
+ if (line.startsWith(":")) return void 0;
46
+ const colon = line.indexOf(":");
47
+ const field = colon === -1 ? line : line.slice(0, colon);
48
+ let value = colon === -1 ? "" : line.slice(colon + 1);
49
+ if (value.startsWith(" ")) value = value.slice(1);
50
+ if (field === "event") name = value;
51
+ else if (field === "data") data.push(value);
52
+ else if (field === "id") {
53
+ if (!value.includes("\0")) id = value;
54
+ } else if (field === "retry" && /^\d+$/u.test(value)) retry = Number(value);
55
+ };
56
+ try {
57
+ for (;;) {
58
+ const { done, value } = await reader.read();
59
+ if (done) break;
60
+ let text = decoder.decode(value, { stream: true });
61
+ if (skipLineFeed && text.startsWith("\n")) text = text.slice(1);
62
+ if (text !== "") skipLineFeed = false;
63
+ buffer += text;
64
+ let start = 0;
65
+ for (let index = 0; index < buffer.length; index += 1) {
66
+ const code = buffer.charCodeAt(index);
67
+ if (code !== 10 && code !== 13) continue;
68
+ const line = buffer.slice(start, index);
69
+ if (code === 13) if (index + 1 < buffer.length) {
70
+ if (buffer.charCodeAt(index + 1) === 10) index += 1;
71
+ } else skipLineFeed = true;
72
+ start = index + 1;
73
+ const event = apply(line);
74
+ if (event !== void 0) yield event;
75
+ }
76
+ buffer = buffer.slice(start);
77
+ }
78
+ } finally {
79
+ signal?.removeEventListener("abort", onAbort);
80
+ await reader.cancel().catch(() => void 0);
81
+ }
82
+ }
83
+ //#endregion
84
+ export { readSSE };
@@ -0,0 +1,90 @@
1
+ //#region ../../plugins/acryl-kv/src/check.ts
2
+ /**
3
+ * What every `kv` provider checks the same way, so a bad call fails alike on a file and on SQL: the key, the value, and the order of keys.
4
+ */
5
+ /** A key is a non-empty string. */
6
+ function checkKey(key) {
7
+ if (typeof key !== "string" || key === "") throw new TypeError(`a kv key is a non-empty string, got ${typeof key === "string" ? "an empty string" : typeof key}`);
8
+ }
9
+ /**
10
+ * The JSON text of a value. `undefined`, functions, symbols, bigints, circular structures and non-finite numbers (which JSON would turn
11
+ * into `null`) are refused with a `TypeError`; nothing is stored for them.
12
+ */
13
+ function encodeValue(value) {
14
+ const text = JSON.stringify(value, (_key, part) => {
15
+ if (typeof part === "number" && !Number.isFinite(part)) throw new TypeError(`a kv value is JSON; ${String(part)} is not`);
16
+ return part;
17
+ });
18
+ if (text === void 0) throw new TypeError(`a kv value is JSON; ${typeof value} is not`);
19
+ return text;
20
+ }
21
+ /** The value of stored JSON text: a new copy each time, so changing what `get` returned changes nothing stored. */
22
+ function decodeValue(text) {
23
+ return JSON.parse(text);
24
+ }
25
+ /** Order two keys by code point (the order of UTF-8 bytes, which SQLite's binary collation and Postgres' "C" collation give too). */
26
+ function compareKeys(a, b) {
27
+ if (a === b) return 0;
28
+ const rank = (unit) => unit >= 55296 ? unit >= 57344 ? unit - 2048 : unit + 8192 : unit;
29
+ const length = Math.min(a.length, b.length);
30
+ for (let index = 0; index < length; index += 1) {
31
+ const difference = rank(a.charCodeAt(index)) - rank(b.charCodeAt(index));
32
+ if (difference !== 0) return difference;
33
+ }
34
+ return a.length - b.length;
35
+ }
36
+ /** The number of code points in a string (what SQL's `substr` counts). */
37
+ function codePoints(text) {
38
+ let count = 0;
39
+ for (const _ of text) count += 1;
40
+ return count;
41
+ }
42
+ /**
43
+ * Call an `update` callback and check what it returns: the callback is synchronous, so a promise (an `async` function the types should have
44
+ * refused) is rejected with a message that says what to do, instead of being stored as `{}`.
45
+ */
46
+ function runChange(change, current) {
47
+ const next = change(current);
48
+ if (typeof next === "object" && next !== null && typeof next.then === "function") throw new TypeError("the callback of kv.update is synchronous and returned a promise: read what you need first (await kv.get), then update with a plain function");
49
+ return next;
50
+ }
51
+ //#endregion
52
+ //#region ../../plugins/acryl-kv/src/copy.ts
53
+ /**
54
+ * Copy every key of one store into another: the import when an app moves from a JSON file to SQL (the file opened with
55
+ * `acryl-kv-json` is the source, `kvOnDb` the target). The source is not changed.
56
+ * @throws when the target has a key the source has and `overwrite` is not set; nothing is written then.
57
+ */
58
+ async function copyKv(source, target, options = {}) {
59
+ const keys = await source.list(options.prefix);
60
+ const existing = new Set(await target.list(options.prefix));
61
+ const shared = keys.filter((key) => existing.has(key));
62
+ if (shared.length > 0 && options.overwrite !== true) {
63
+ const shown = shared.slice(0, 5).map((key) => JSON.stringify(key)).join(", ");
64
+ throw new Error(`copyKv: the target already has ${shared.length} of these keys (${shown}${shared.length > 5 ? ", ..." : ""}); pass overwrite to replace them`);
65
+ }
66
+ let copied = 0;
67
+ for (const key of keys) {
68
+ const value = await source.get(key);
69
+ if (value === void 0) continue;
70
+ await target.set(key, value);
71
+ copied += 1;
72
+ }
73
+ return {
74
+ copied,
75
+ overwritten: shared.length
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region ../../plugins/acryl-kv/src/index.ts
80
+ /**
81
+ * The `kv` port: named JSON documents for an app too small for SQL, whichever provider serves it (`acryl-kv-json` for one file,
82
+ * `acryl-kv-db` over the `db` port). Plain code opens a store with a provider's `openKv`; a Cordis plugin declares `inject: ['kv']`, imports
83
+ * `acryl-kv/cordis` for the `ctx.kv` type, and never imports a provider.
84
+ *
85
+ * This entry imports nothing from Cordis, at runtime or in its types.
86
+ */
87
+ /** The service name every `kv` provider registers. */
88
+ const KV_SERVICE = "kv";
89
+ //#endregion
90
+ export { compareKeys as a, runChange as c, codePoints as i, copyKv as n, decodeValue as o, checkKey as r, encodeValue as s, KV_SERVICE as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acrylblends",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "The Acryl web framework (import { Acryl } from \"acrylblends\") and the acrylblends command that scaffolds Cordis apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -402,6 +402,10 @@
402
402
  "types": "./lib/schema-validator.d.ts",
403
403
  "default": "./lib/schema-validator.js"
404
404
  },
405
+ "./read-sse": {
406
+ "types": "./lib/read-sse.d.ts",
407
+ "default": "./lib/read-sse.js"
408
+ },
405
409
  "./node-server": {
406
410
  "types": "./lib/node-server.d.ts",
407
411
  "default": "./lib/node-server.js"
@@ -426,6 +430,18 @@
426
430
  "types": "./lib/db/sqlite.d.ts",
427
431
  "default": "./lib/db/sqlite.js"
428
432
  },
433
+ "./kv": {
434
+ "types": "./lib/kv.d.ts",
435
+ "default": "./lib/kv.js"
436
+ },
437
+ "./kv/json": {
438
+ "types": "./lib/kv/json.d.ts",
439
+ "default": "./lib/kv/json.js"
440
+ },
441
+ "./kv/db": {
442
+ "types": "./lib/kv/db.d.ts",
443
+ "default": "./lib/kv/db.js"
444
+ },
429
445
  "./package.json": "./package.json",
430
446
  "./cli": {
431
447
  "types": "./dist/index.d.ts",
@@ -7,6 +7,6 @@
7
7
  "dependencies": {
8
8
  "@deepseek-ai/cordis": "4.0.4"
9
9
  },
10
- "mount": "it is the port, not a service: a plugin declares inject: ['db'] and imports its types from './plugins/acryl-db/src/index.ts' (or the ctx.db type from './plugins/acryl-db/src/cordis.ts'); add acryl-db-sqlite to provide it",
10
+ "mount": "it is the port, not a service: a plugin declares inject: ['db'] and imports its types from '../plugins/acryl-db/src/index.ts' (or the ctx.db type from '../plugins/acryl-db/src/cordis.ts'); add acryl-db-sqlite to provide it",
11
11
  "omit": ["src/conformance.ts", "**/*.test.*", "**/*.case.ts"]
12
12
  }
@@ -2,7 +2,7 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "plugins/acryl-db",
5
- "commit": "8e23e14336647943d4232047e3ea253e434535d3"
5
+ "commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
6
6
  },
7
7
  "vendored": [
8
8
  "src",
@@ -10,7 +10,7 @@
10
10
  "LICENSE"
11
11
  ],
12
12
  "rewrittenSpecifiers": 0,
13
- "digest": "sha256:51e5613457498bfab7c281e0c6443c4f7535401c16520ac44624dc8791a90fa6",
13
+ "digest": "sha256:656f5bb8468ea1d1f5cef2fc14c4c3d3c8cfa1d85a75cebbf6bc98f1b2d245a7",
14
14
  "testsNotRun": {},
15
15
  "notice": [
16
16
  "LICENSE"
@@ -2,7 +2,7 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "plugins/acryl-db-sqlite",
5
- "commit": "8e23e14336647943d4232047e3ea253e434535d3"
5
+ "commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
6
6
  },
7
7
  "vendored": [
8
8
  "src",
@@ -2,7 +2,7 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "plugins/acryl-inspect",
5
- "commit": "8e23e14336647943d4232047e3ea253e434535d3"
5
+ "commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
6
6
  },
7
7
  "vendored": [
8
8
  "src",
@@ -0,0 +1,47 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebBoxes s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The files under src/facade are derived from the SQLite facade of chord in pi (https://github.com/earendil-works/pi, packages/chord, commit d5b3de22d0e24a3e9c38a9410d3329b099717283), used under this license:
26
+
27
+ MIT License
28
+
29
+ Copyright (c) 2025 Mario Zechner
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ of this software and associated documentation files (the "Software"), to deal
33
+ in the Software without restriction, including without limitation the rights
34
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35
+ copies of the Software, and to permit persons to whom the Software is
36
+ furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in all
39
+ copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "acryl-kv",
3
+ "description": "The kv port: named JSON documents for an app too small for SQL, with atomic update; types, checks and copyKv (the import). A provider (acryl-kv-json, acryl-kv-db) serves it. Vendored from ACRYL.",
4
+ "requires": [],
5
+ "inject": [],
6
+ "provides": [],
7
+ "dependencies": {
8
+ "@deepseek-ai/cordis": "4.0.4"
9
+ },
10
+ "mount": "it is the port, not a service: a plugin declares inject: ['kv'] and imports the ctx.kv type from '../plugins/acryl-kv/src/cordis.ts'; add acryl-kv-json (a file) or acryl-kv-db (your SQL database) to provide it",
11
+ "omit": ["src/conformance.ts", "**/*.test.*", "**/*.spec.*", "**/*.case.ts"]
12
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * What every `kv` provider checks the same way, so a bad call fails alike on a file and on SQL: the key, the value, and the order of keys.
3
+ */
4
+
5
+ /** A key is a non-empty string. */
6
+ export function checkKey(key: unknown): asserts key is string {
7
+ if (typeof key !== 'string' || key === '') throw new TypeError(`a kv key is a non-empty string, got ${typeof key === 'string' ? 'an empty string' : typeof key}`)
8
+ }
9
+
10
+ /**
11
+ * The JSON text of a value. `undefined`, functions, symbols, bigints, circular structures and non-finite numbers (which JSON would turn
12
+ * into `null`) are refused with a `TypeError`; nothing is stored for them.
13
+ */
14
+ export function encodeValue(value: unknown): string {
15
+ const text = JSON.stringify(value, (_key, part: unknown) => {
16
+ if (typeof part === 'number' && !Number.isFinite(part)) throw new TypeError(`a kv value is JSON; ${String(part)} is not`)
17
+ return part
18
+ })
19
+ if (text === undefined) throw new TypeError(`a kv value is JSON; ${typeof value} is not`)
20
+ return text
21
+ }
22
+
23
+ /** The value of stored JSON text: a new copy each time, so changing what `get` returned changes nothing stored. */
24
+ export function decodeValue<T>(text: string): T {
25
+ return JSON.parse(text) as T
26
+ }
27
+
28
+ /** Order two keys by code point (the order of UTF-8 bytes, which SQLite's binary collation and Postgres' "C" collation give too). */
29
+ export function compareKeys(a: string, b: string): number {
30
+ if (a === b) return 0
31
+ // UTF-16 order puts astral characters (surrogates, D800-DFFF) before U+E000-U+FFFF; moving the surrogates above them gives code point order.
32
+ const rank = (unit: number): number => (unit >= 0xd800 ? (unit >= 0xe000 ? unit - 0x800 : unit + 0x2000) : unit)
33
+ const length = Math.min(a.length, b.length)
34
+ for (let index = 0; index < length; index += 1) {
35
+ const difference = rank(a.charCodeAt(index)) - rank(b.charCodeAt(index))
36
+ if (difference !== 0) return difference
37
+ }
38
+ return a.length - b.length
39
+ }
40
+
41
+ /** The number of code points in a string (what SQL's `substr` counts). */
42
+ export function codePoints(text: string): number {
43
+ let count = 0
44
+ for (const _ of text) count += 1
45
+ return count
46
+ }
47
+
48
+ /**
49
+ * Call an `update` callback and check what it returns: the callback is synchronous, so a promise (an `async` function the types should have
50
+ * refused) is rejected with a message that says what to do, instead of being stored as `{}`.
51
+ */
52
+ export function runChange<T>(change: (current: T | undefined) => T, current: T | undefined): T {
53
+ const next: unknown = change(current)
54
+ if (typeof next === 'object' && next !== null && typeof (next as { then?: unknown }).then === 'function') {
55
+ throw new TypeError('the callback of kv.update is synchronous and returned a promise: read what you need first (await kv.get), then update with a plain function')
56
+ }
57
+ return next as T
58
+ }
@@ -0,0 +1,39 @@
1
+ import type { Kv } from './kv.ts'
2
+
3
+ export interface CopyKvOptions {
4
+ /** Replace keys the target already has. Without it, a shared key stops the copy before anything is written. */
5
+ readonly overwrite?: boolean
6
+ /** Copy only the keys that start with this. */
7
+ readonly prefix?: string
8
+ }
9
+
10
+ export interface CopyKvResult {
11
+ /** Keys written to the target. */
12
+ readonly copied: number
13
+ /** Of those, keys the target already had. */
14
+ readonly overwritten: number
15
+ }
16
+
17
+ /**
18
+ * Copy every key of one store into another: the import when an app moves from a JSON file to SQL (the file opened with
19
+ * `acryl-kv-json` is the source, `kvOnDb` the target). The source is not changed.
20
+ * @throws when the target has a key the source has and `overwrite` is not set; nothing is written then.
21
+ */
22
+ export async function copyKv(source: Kv, target: Kv, options: CopyKvOptions = {}): Promise<CopyKvResult> {
23
+ const keys = await source.list(options.prefix)
24
+ const existing = new Set(await target.list(options.prefix))
25
+ const shared = keys.filter(key => existing.has(key))
26
+ if (shared.length > 0 && options.overwrite !== true) {
27
+ const shown = shared.slice(0, 5).map(key => JSON.stringify(key)).join(', ')
28
+ throw new Error(`copyKv: the target already has ${shared.length} of these keys (${shown}${shared.length > 5 ? ', ...' : ''}); pass overwrite to replace them`)
29
+ }
30
+ let copied = 0
31
+ for (const key of keys) {
32
+ const value = await source.get(key)
33
+ // A key deleted from the source while copying is not copied.
34
+ if (value === undefined) continue
35
+ await target.set(key, value)
36
+ copied += 1
37
+ }
38
+ return { copied, overwritten: shared.length }
39
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The `ctx.kv` type for Cordis plugins: `import type {} from 'acryl-kv/cordis'` beside `inject: ['kv']`. Kept out of the root entry, so
3
+ * plain code that uses the port never names Cordis.
4
+ */
5
+
6
+ import type {} from '@deepseek-ai/cordis'
7
+ import type { Kv } from './kv.ts'
8
+
9
+ declare module '@deepseek-ai/cordis' {
10
+ interface Context {
11
+ kv: Kv
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `kv` port: named JSON documents for an app too small for SQL, whichever provider serves it (`acryl-kv-json` for one file,
3
+ * `acryl-kv-db` over the `db` port). Plain code opens a store with a provider's `openKv`; a Cordis plugin declares `inject: ['kv']`, imports
4
+ * `acryl-kv/cordis` for the `ctx.kv` type, and never imports a provider.
5
+ *
6
+ * This entry imports nothing from Cordis, at runtime or in its types.
7
+ */
8
+
9
+ /** The service name every `kv` provider registers. */
10
+ export const KV_SERVICE = 'kv'
11
+
12
+ export type { JsonValue, Kv, KvOpened, NotPromise } from './kv.ts'
13
+ export { checkKey, codePoints, compareKeys, decodeValue, encodeValue, runChange } from './check.ts'
14
+ export { copyKv, type CopyKvOptions, type CopyKvResult } from './copy.ts'
@@ -0,0 +1,37 @@
1
+ /** `T`, except a promise: the callback of `update` is synchronous, and an async one is a type error. */
2
+ export type NotPromise<T> = T extends PromiseLike<unknown> ? never : T
3
+
4
+ /** Anything JSON can hold. */
5
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
6
+
7
+ /**
8
+ * Named JSON documents. Every call returns a promise, so a file, SQLite and Postgres answer alike.
9
+ *
10
+ * Keys are non-empty strings. Values are JSON: `undefined`, functions, symbols, bigints and non-finite numbers are refused with a
11
+ * `TypeError` before anything is written, and a value read back is a copy.
12
+ *
13
+ * There is no `close`: the code that opened a store closes it (see {@link KvOpened}), so a plugin cannot close the store another uses.
14
+ */
15
+ export interface Kv {
16
+ /** The value stored under the key, or `undefined`. */
17
+ get<T = JsonValue>(key: string): Promise<T | undefined>
18
+ /** Store a value under the key, replacing any. Resolves once it is durable. */
19
+ set<T = JsonValue>(key: string, value: T): Promise<void>
20
+ /**
21
+ * Replace the value under the key by `change(current)`, atomically: `current` is `undefined` when the key is missing, `change` is
22
+ * synchronous (an `async` function is a type error, and a returned promise is rejected at run time: read what you need first, then update), and no other write to the key happens between reading it and writing the result. Resolves the new value. A `change` that
23
+ * throws, or returns something that is not JSON, leaves the value as it was and rejects with its error.
24
+ */
25
+ update<T = JsonValue>(key: string, change: (current: T | undefined) => NotPromise<T>): Promise<T>
26
+ /** Remove the key. A missing key is not an error. */
27
+ delete(key: string): Promise<void>
28
+ /** The keys that start with the prefix (all of them without one), sorted by code point (the order of their UTF-8 bytes, not of UTF-16 units). */
29
+ list(prefix?: string): Promise<string[]>
30
+ }
31
+
32
+ /** An open store and the one function that closes it. */
33
+ export interface KvOpened {
34
+ readonly kv: Kv
35
+ /** Waits for the writes in flight, then releases the storage. Calling it again does nothing; calls on `kv` after it reject. */
36
+ close(): Promise<void>
37
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "source": {
3
+ "repo": "acryldev/acryl",
4
+ "path": "plugins/acryl-kv",
5
+ "commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
6
+ },
7
+ "vendored": [
8
+ "src",
9
+ "tests",
10
+ "LICENSE"
11
+ ],
12
+ "rewrittenSpecifiers": 0,
13
+ "digest": "sha256:dbf0d156d0f3d3b394e48cba87b015362f2720c96a3b198ae97ac5bea61c4ae4",
14
+ "testsNotRun": {},
15
+ "notice": [
16
+ "LICENSE"
17
+ ]
18
+ }
@@ -0,0 +1,47 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebBoxes s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The files under src/facade are derived from the SQLite facade of chord in pi (https://github.com/earendil-works/pi, packages/chord, commit d5b3de22d0e24a3e9c38a9410d3329b099717283), used under this license:
26
+
27
+ MIT License
28
+
29
+ Copyright (c) 2025 Mario Zechner
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining a copy
32
+ of this software and associated documentation files (the "Software"), to deal
33
+ in the Software without restriction, including without limitation the rights
34
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
35
+ copies of the Software, and to permit persons to whom the Software is
36
+ furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in all
39
+ copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
43
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
44
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
45
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
46
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
47
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "acryl-kv-db",
3
+ "description": "The kv provider over the db port: a kv table in the app's SQL database, so an app that grows into SQL keeps its data. Vendored from ACRYL.",
4
+ "requires": ["acryl-kv", "acryl-db-sqlite"],
5
+ "inject": ["db"],
6
+ "provides": ["kv"],
7
+ "dependencies": {
8
+ "@deepseek-ai/cordis": "4.0.4",
9
+ "@deepseek-ai/schemastery": "3.18.4"
10
+ },
11
+ "mount": "mount it after acryl-db-sqlite: root.plugin(kvDb, {}); it waits for db and provides the same kv as acryl-kv-json",
12
+ "omit": ["**/*.test.*", "**/*.spec.*", "**/*.case.ts"]
13
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The kv provider over the `db` port (spec 049 slice 3): a `kv` table in the app's SQL database, provided as `kv` by this Cordis plugin.
3
+ * It injects `db` and nothing else, so it stays PENDING until a `db` provider (`acryl-db-sqlite`, later Postgres) is active, and its
4
+ * consumers (`inject: ['kv']`) wait behind it. Replacing the row with `acryl-kv-json` does not touch them.
5
+ *
6
+ * It owns no connection, so unloading only withdraws `kv`.
7
+ */
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+ import z from '@deepseek-ai/schemastery'
11
+ import { KV_SERVICE } from '../../acryl-kv/src/index.ts'
12
+ import { kvOnDb } from './open.ts'
13
+
14
+ export { kvOnDb, type KvOnDbOptions } from './open.ts'
15
+
16
+ /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
17
+ export const name = 'acryl-kv-db'
18
+
19
+ /** The service this plugin needs: Cordis starts it only while a `db` provider is active. */
20
+ export const inject = ['db']
21
+
22
+ export interface Config {
23
+ table?: string
24
+ }
25
+
26
+ export const Config: z<Config> = z.object({
27
+ table: z.string().description('The table that holds the documents. Defaults to kv.'),
28
+ })
29
+
30
+ export async function apply(ctx: Context, rawConfig: Config = {}): Promise<void> {
31
+ const config = Config(rawConfig)
32
+ const db = ctx.get('db') as Parameters<typeof kvOnDb>[0]
33
+ const kv = kvOnDb(db, config.table === undefined ? {} : { table: config.table })
34
+ await ctx.effect(async function* () {
35
+ yield ctx.reflect.provide(KV_SERVICE, kv)
36
+ }, 'acryl-kv-db')
37
+ }