@abaigram/abai-stub 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/audit.d.ts +102 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +196 -0
- package/dist/audit.js.map +1 -0
- package/dist/cells/fit.d.ts +6 -0
- package/dist/cells/fit.d.ts.map +1 -0
- package/dist/cells/fit.js +44 -0
- package/dist/cells/fit.js.map +1 -0
- package/dist/content.d.ts +17 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +71 -0
- package/dist/content.js.map +1 -0
- package/dist/endpoint.d.ts +90 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +804 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/entry.d.ts +36 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +409 -0
- package/dist/entry.js.map +1 -0
- package/dist/env.d.ts +38 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +69 -0
- package/dist/env.js.map +1 -0
- package/dist/harness-binding.d.ts +235 -0
- package/dist/harness-binding.d.ts.map +1 -0
- package/dist/harness-binding.js +618 -0
- package/dist/harness-binding.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/index.d.ts +3 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +3 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest.d.ts +177 -0
- package/dist/manifest/manifest.d.ts.map +1 -0
- package/dist/manifest/manifest.js +452 -0
- package/dist/manifest/manifest.js.map +1 -0
- package/dist/manifest/render.d.ts +6 -0
- package/dist/manifest/render.d.ts.map +1 -0
- package/dist/manifest/render.js +39 -0
- package/dist/manifest/render.js.map +1 -0
- package/dist/null-harness.d.ts +32 -0
- package/dist/null-harness.d.ts.map +1 -0
- package/dist/null-harness.js +162 -0
- package/dist/null-harness.js.map +1 -0
- package/dist/program.d.ts +255 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +898 -0
- package/dist/program.js.map +1 -0
- package/dist/region.d.ts +72 -0
- package/dist/region.d.ts.map +1 -0
- package/dist/region.js +93 -0
- package/dist/region.js.map +1 -0
- package/dist/registry.d.ts +60 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +105 -0
- package/dist/registry.js.map +1 -0
- package/dist/wire/descriptor.d.ts +38 -0
- package/dist/wire/descriptor.d.ts.map +1 -0
- package/dist/wire/descriptor.js +144 -0
- package/dist/wire/descriptor.js.map +1 -0
- package/dist/wire/framing.d.ts +11 -0
- package/dist/wire/framing.d.ts.map +1 -0
- package/dist/wire/framing.js +42 -0
- package/dist/wire/framing.js.map +1 -0
- package/dist/wire/index.d.ts +4 -0
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/wire/index.js +4 -0
- package/dist/wire/index.js.map +1 -0
- package/dist/wire/protocol.d.ts +257 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +17 -0
- package/dist/wire/protocol.js.map +1 -0
- package/package.json +36 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joon Hwa Jung
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @abaigram/abai-stub
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The library a TypeScript program includes to become an abaigram, with
|
|
4
|
+
`npm install @abaigram/abai-stub @abaigram/abai-embedded-harness`: the
|
|
5
|
+
program side of the contract, `spec/abaigrammar.md` in the
|
|
6
|
+
[abai repository](https://github.com/jjbits/abai). Declarations in code become the
|
|
7
|
+
manifest; cells, events, capabilities, decisions, turns and safepoints are
|
|
8
|
+
the program's end of the five channels; `<prog> abai manifest | doctor`
|
|
9
|
+
is the stub's part of the standard entry.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createAbaigram } from "@abaigram/abai-stub";
|
|
13
|
+
|
|
14
|
+
const app = createAbaigram({ name: "counter", modes: ["turns", "sidecar"], cells: {...}, ... });
|
|
15
|
+
if ((await app.entry(process.argv.slice(2))) >= 0) process.exit();
|
|
16
|
+
const depth = app.cell<number>("queue.depth"); // obtain handles once; names are checked here
|
|
17
|
+
const failed = app.event("job.failed");
|
|
18
|
+
depth.set(3); // one store
|
|
19
|
+
await app.decide("on_backlog", { depth: 3 }); // the default, until a harness attaches
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
In the repository, `implementations/typescript/abai-stub/examples/counter/` is
|
|
23
|
+
the smallest abaigram and the test subject.
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type DecisionDecl } from "./manifest/manifest.ts";
|
|
2
|
+
export type AuditAct = "capability" | "knob" | "decision" | "turn" | "lifecycle" | "counters" | "message";
|
|
3
|
+
export type AuditOutcome = "ok" | "error" | "forbidden" | "agent" | "default" | "stub" | "failed";
|
|
4
|
+
export interface AuditRecord {
|
|
5
|
+
ts: string;
|
|
6
|
+
pid: number;
|
|
7
|
+
/** The connection that acted (`k1`, `k2`, ...); absent for counters. */
|
|
8
|
+
conn?: string;
|
|
9
|
+
act: AuditAct;
|
|
10
|
+
/** The capability, cell, decision or counter name. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The request id or hook id. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** The parameters (capped, see `capValue`). */
|
|
15
|
+
params?: unknown;
|
|
16
|
+
/** A knob's landed value, or a decision answer's summary. */
|
|
17
|
+
value?: unknown;
|
|
18
|
+
outcome: AuditOutcome;
|
|
19
|
+
/** Why a default or stub answer applied, or what a counter counts. */
|
|
20
|
+
cause?: string;
|
|
21
|
+
/** What the harness stated for an act whose policy is `confirm`: approved, auto, or denied. */
|
|
22
|
+
consent?: string;
|
|
23
|
+
/** The agent's stated reason. */
|
|
24
|
+
reason?: unknown;
|
|
25
|
+
error?: unknown;
|
|
26
|
+
/** Milliseconds from dispatch to settle. */
|
|
27
|
+
ms?: number;
|
|
28
|
+
/** Counters only. */
|
|
29
|
+
count?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One message sent to or received from a member of the run's network
|
|
33
|
+
* (section 10), as the harness hands it to the stub for the audit log. The
|
|
34
|
+
* stub writes it as `act: message` with the counterpart's name as `name`,
|
|
35
|
+
* the envelope id as `id`, the body capped as `value`.
|
|
36
|
+
*/
|
|
37
|
+
export interface MessageRecord {
|
|
38
|
+
direction: "sent" | "received";
|
|
39
|
+
/** The counterpart's name as the network's authority issued it, else its address. */
|
|
40
|
+
peer: string;
|
|
41
|
+
addr: string;
|
|
42
|
+
network: string;
|
|
43
|
+
/** The envelope id, when the payload carried one. */
|
|
44
|
+
id?: string;
|
|
45
|
+
thread?: string;
|
|
46
|
+
re?: string;
|
|
47
|
+
/** `msg` is an aiap message; `raw` a plain payload with no envelope. */
|
|
48
|
+
kind: "msg" | "raw";
|
|
49
|
+
body: string;
|
|
50
|
+
outcome: "ok" | "error";
|
|
51
|
+
/** The agent's stated reason, for a send. */
|
|
52
|
+
reason?: string;
|
|
53
|
+
error?: string;
|
|
54
|
+
/** Milliseconds a send took. */
|
|
55
|
+
ms?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Whether a value is a `MessageRecord`: what the sidecar endpoint checks before it writes what a harness sent. */
|
|
58
|
+
export declare function isMessageRecord(value: unknown): value is MessageRecord;
|
|
59
|
+
/** Bytes a string or the JSON of a value may take in a record before it is cut. */
|
|
60
|
+
export declare const AUDIT_VALUE_CAP = 512;
|
|
61
|
+
/** Bytes a name, an id or an address from the network may take in a message record. */
|
|
62
|
+
export declare const AUDIT_FIELD_CAP = 256;
|
|
63
|
+
/** A string the harness or the network supplied, cut to `maxBytes` for a record. */
|
|
64
|
+
export declare function capField(value: string, maxBytes?: number): string;
|
|
65
|
+
/** Bytes `tail` reads from the end of the file. */
|
|
66
|
+
export declare const AUDIT_TAIL_BYTES: number;
|
|
67
|
+
/**
|
|
68
|
+
* Cap a value for a record: a value whose JSON exceeds the cap becomes
|
|
69
|
+
* `{truncated: true, bytes, head}` with the JSON's first 512 characters, so
|
|
70
|
+
* the record stays valid JSON. A value that cannot be serialized becomes a
|
|
71
|
+
* marker string.
|
|
72
|
+
*/
|
|
73
|
+
export declare function capValue(value: unknown): unknown;
|
|
74
|
+
/**
|
|
75
|
+
* What an audit record keeps of a decision answer: never the transcript.
|
|
76
|
+
* An options decision keeps the chosen option; any other answer keeps
|
|
77
|
+
* whether it changed something (a non-empty object).
|
|
78
|
+
*/
|
|
79
|
+
export declare function summarizeAnswer(decision: DecisionDecl | undefined, answer: unknown): unknown;
|
|
80
|
+
export interface AuditTail {
|
|
81
|
+
records: unknown[];
|
|
82
|
+
/** More records exist than were returned. */
|
|
83
|
+
truncated: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare class AuditLog {
|
|
86
|
+
readonly path: string;
|
|
87
|
+
private readonly onFailure;
|
|
88
|
+
private fd;
|
|
89
|
+
private _failed;
|
|
90
|
+
private closed;
|
|
91
|
+
constructor(path: string, onFailure?: (error: string) => void);
|
|
92
|
+
/** The error that latched the log off, if any. */
|
|
93
|
+
get failed(): string | undefined;
|
|
94
|
+
/** Append one record. Never throws; a no-op once the log failed or was closed. True when the line was written. */
|
|
95
|
+
append(record: Omit<AuditRecord, "ts" | "pid">): boolean;
|
|
96
|
+
private fail;
|
|
97
|
+
/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on("exit")`. */
|
|
98
|
+
close(): void;
|
|
99
|
+
/** The last `limit` records, read from the last megabyte of the file. Never throws. */
|
|
100
|
+
tail(limit: number): AuditTail;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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","sourcesContent":["// The audit log (abaigrammar section 10).\n//\n// One JSON line per act of control: a mutate capability call, a knob write\n// by any path, a decision's outcome, and the counters of defaulted decisions\n// flushed at attach, close and exit. Written synchronously, one line per\n// act: acts are rare and a partial line on a crash is worse than a syscall.\n// Nothing here throws into the agent: the first failure latches the log off,\n// reports once through `onFailure`, and every later append is a no-op.\n// `tail` reads the log back for the harness.\nimport { closeSync, fstatSync, mkdirSync, openSync, readSync, writeSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { cutUtf8, type DecisionDecl } from \"./manifest/manifest.ts\";\n\nexport type AuditAct = \"capability\" | \"knob\" | \"decision\" | \"turn\" | \"lifecycle\" | \"counters\" | \"message\";\nexport type AuditOutcome = \"ok\" | \"error\" | \"forbidden\" | \"agent\" | \"default\" | \"stub\" | \"failed\";\n\nexport interface AuditRecord {\n\tts: string;\n\tpid: number;\n\t/** The connection that acted (`k1`, `k2`, ...); absent for counters. */\n\tconn?: string;\n\tact: AuditAct;\n\t/** The capability, cell, decision or counter name. */\n\tname: string;\n\t/** The request id or hook id. */\n\tid?: string;\n\t/** The parameters (capped, see `capValue`). */\n\tparams?: unknown;\n\t/** A knob's landed value, or a decision answer's summary. */\n\tvalue?: unknown;\n\toutcome: AuditOutcome;\n\t/** Why a default or stub answer applied, or what a counter counts. */\n\tcause?: string;\n\t/** What the harness stated for an act whose policy is `confirm`: approved, auto, or denied. */\n\tconsent?: string;\n\t/** The agent's stated reason. */\n\treason?: unknown;\n\terror?: unknown;\n\t/** Milliseconds from dispatch to settle. */\n\tms?: number;\n\t/** Counters only. */\n\tcount?: number;\n}\n\n/**\n * One message sent to or received from a member of the run's network\n * (section 10), as the harness hands it to the stub for the audit log. The\n * stub writes it as `act: message` with the counterpart's name as `name`,\n * the envelope id as `id`, the body capped as `value`.\n */\nexport interface MessageRecord {\n\tdirection: \"sent\" | \"received\";\n\t/** The counterpart's name as the network's authority issued it, else its address. */\n\tpeer: string;\n\taddr: string;\n\tnetwork: string;\n\t/** The envelope id, when the payload carried one. */\n\tid?: string;\n\tthread?: string;\n\tre?: string;\n\t/** `msg` is an aiap message; `raw` a plain payload with no envelope. */\n\tkind: \"msg\" | \"raw\";\n\tbody: string;\n\toutcome: \"ok\" | \"error\";\n\t/** The agent's stated reason, for a send. */\n\treason?: string;\n\terror?: string;\n\t/** Milliseconds a send took. */\n\tms?: number;\n}\n\nfunction optionalString(value: unknown): boolean {\n\treturn value === undefined || typeof value === \"string\";\n}\n\n/** Whether a value is a `MessageRecord`: what the sidecar endpoint checks before it writes what a harness sent. */\nexport function isMessageRecord(value: unknown): value is MessageRecord {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst r = value as Record<string, unknown>;\n\treturn (\n\t\t(r.direction === \"sent\" || r.direction === \"received\") &&\n\t\ttypeof r.peer === \"string\" &&\n\t\ttypeof r.addr === \"string\" &&\n\t\ttypeof r.network === \"string\" &&\n\t\t(r.kind === \"msg\" || r.kind === \"raw\") &&\n\t\ttypeof r.body === \"string\" &&\n\t\t(r.outcome === \"ok\" || r.outcome === \"error\") &&\n\t\toptionalString(r.id) &&\n\t\toptionalString(r.thread) &&\n\t\toptionalString(r.re) &&\n\t\toptionalString(r.reason) &&\n\t\toptionalString(r.error) &&\n\t\t(r.ms === undefined || typeof r.ms === \"number\")\n\t);\n}\n\n/** Bytes a string or the JSON of a value may take in a record before it is cut. */\nexport const AUDIT_VALUE_CAP = 512;\n/** Bytes a name, an id or an address from the network may take in a message record. */\nexport const AUDIT_FIELD_CAP = 256;\n\n/** A string the harness or the network supplied, cut to `maxBytes` for a record. */\nexport function capField(value: string, maxBytes: number = AUDIT_FIELD_CAP): string {\n\treturn cutUtf8(value, maxBytes);\n}\n/** Bytes `tail` reads from the end of the file. */\nexport const AUDIT_TAIL_BYTES = 1024 * 1024;\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Cap a value for a record: a value whose JSON exceeds the cap becomes\n * `{truncated: true, bytes, head}` with the JSON's first 512 characters, so\n * the record stays valid JSON. A value that cannot be serialized becomes a\n * marker string.\n */\nexport function capValue(value: unknown): unknown {\n\tif (value === undefined) return undefined;\n\tlet json: string;\n\ttry {\n\t\tjson = JSON.stringify(value) ?? \"null\";\n\t} catch (error) {\n\t\treturn `<unserializable: ${errorMessage(error)}>`;\n\t}\n\tconst bytes = Buffer.byteLength(json, \"utf8\");\n\tif (bytes <= AUDIT_VALUE_CAP) return value;\n\treturn { truncated: true, bytes, head: cutUtf8(json, AUDIT_VALUE_CAP) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * What an audit record keeps of a decision answer: never the transcript.\n * An options decision keeps the chosen option; any other answer keeps\n * whether it changed something (a non-empty object).\n */\nexport function summarizeAnswer(decision: DecisionDecl | undefined, answer: unknown): unknown {\n\tif (answer === undefined) return undefined;\n\tif (decision?.options && typeof answer === \"string\") return { option: answer };\n\treturn { changed: Object.keys(isRecord(answer) ? answer : {}).length > 0 };\n}\n\nexport interface AuditTail {\n\trecords: unknown[];\n\t/** More records exist than were returned. */\n\ttruncated: boolean;\n}\n\nexport class AuditLog {\n\treadonly path: string;\n\tprivate readonly onFailure: ((error: string) => void) | undefined;\n\tprivate fd: number | undefined;\n\tprivate _failed: string | undefined;\n\tprivate closed = false;\n\n\tconstructor(path: string, onFailure?: (error: string) => void) {\n\t\tthis.path = path;\n\t\tthis.onFailure = onFailure;\n\t}\n\n\t/** The error that latched the log off, if any. */\n\tget failed(): string | undefined {\n\t\treturn this._failed;\n\t}\n\n\t/** Append one record. Never throws; a no-op once the log failed or was closed. True when the line was written. */\n\tappend(record: Omit<AuditRecord, \"ts\" | \"pid\">): boolean {\n\t\tif (this._failed !== undefined || this.closed) return false;\n\t\ttry {\n\t\t\tif (this.fd === undefined) {\n\t\t\t\tmkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });\n\t\t\t\tthis.fd = openSync(this.path, \"a\", 0o600);\n\t\t\t}\n\t\t\tconst full: AuditRecord = { ts: new Date().toISOString(), pid: process.pid, ...record };\n\t\t\twriteSync(this.fd, `${JSON.stringify(full)}\\n`);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.fail(errorMessage(error));\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate fail(error: string): void {\n\t\tthis._failed = error;\n\t\tthis.close();\n\t\ttry {\n\t\t\tthis.onFailure?.(error);\n\t\t} catch {\n\t\t\t// a failure callback must not fail the caller\n\t\t}\n\t}\n\n\t/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on(\"exit\")`. */\n\tclose(): void {\n\t\tthis.closed = true;\n\t\tif (this.fd === undefined) return;\n\t\tconst fd = this.fd;\n\t\tthis.fd = undefined;\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\t/** The last `limit` records, read from the last megabyte of the file. Never throws. */\n\ttail(limit: number): AuditTail {\n\t\tlet fd: number | undefined;\n\t\ttry {\n\t\t\tfd = openSync(this.path, \"r\");\n\t\t\tconst size = fstatSync(fd).size;\n\t\t\tconst start = Math.max(0, size - AUDIT_TAIL_BYTES);\n\t\t\tconst buffer = Buffer.alloc(size - start);\n\t\t\tlet read = 0;\n\t\t\twhile (read < buffer.length) {\n\t\t\t\tconst n = readSync(fd, buffer, read, buffer.length - read, start + read);\n\t\t\t\tif (n === 0) break;\n\t\t\t\tread += n;\n\t\t\t}\n\t\t\tconst lines = buffer.toString(\"utf8\", 0, read).split(\"\\n\");\n\t\t\t// the window may start mid-line: drop the first line unless the byte before it ends a line\n\t\t\tif (start > 0) {\n\t\t\t\tconst before = Buffer.alloc(1);\n\t\t\t\tconst n = readSync(fd, before, 0, 1, start - 1);\n\t\t\t\tif (n !== 1 || before[0] !== 0x0a) lines.shift();\n\t\t\t}\n\t\t\tconst records: unknown[] = [];\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line === \"\") continue;\n\t\t\t\ttry {\n\t\t\t\t\trecords.push(JSON.parse(line));\n\t\t\t\t} catch {\n\t\t\t\t\trecords.push({ raw: line });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst truncated = start > 0 || records.length > limit;\n\t\t\treturn { records: records.slice(-limit), truncated };\n\t\t} catch (error) {\n\t\t\t// no log yet, or a path that can never hold one (the failure is in `failed`)\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"ENOENT\" || code === \"ENOTDIR\") return { records: [], truncated: false };\n\t\t\treturn { records: [{ error: errorMessage(error) }], truncated: false };\n\t\t} finally {\n\t\t\tif (fd !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The audit log (abaigrammar section 10).
|
|
2
|
+
//
|
|
3
|
+
// One JSON line per act of control: a mutate capability call, a knob write
|
|
4
|
+
// by any path, a decision's outcome, and the counters of defaulted decisions
|
|
5
|
+
// flushed at attach, close and exit. Written synchronously, one line per
|
|
6
|
+
// act: acts are rare and a partial line on a crash is worse than a syscall.
|
|
7
|
+
// Nothing here throws into the agent: the first failure latches the log off,
|
|
8
|
+
// reports once through `onFailure`, and every later append is a no-op.
|
|
9
|
+
// `tail` reads the log back for the harness.
|
|
10
|
+
import { closeSync, fstatSync, mkdirSync, openSync, readSync, writeSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { cutUtf8 } from "./manifest/manifest.js";
|
|
13
|
+
function optionalString(value) {
|
|
14
|
+
return value === undefined || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
/** Whether a value is a `MessageRecord`: what the sidecar endpoint checks before it writes what a harness sent. */
|
|
17
|
+
export function isMessageRecord(value) {
|
|
18
|
+
if (typeof value !== "object" || value === null)
|
|
19
|
+
return false;
|
|
20
|
+
const r = value;
|
|
21
|
+
return ((r.direction === "sent" || r.direction === "received") &&
|
|
22
|
+
typeof r.peer === "string" &&
|
|
23
|
+
typeof r.addr === "string" &&
|
|
24
|
+
typeof r.network === "string" &&
|
|
25
|
+
(r.kind === "msg" || r.kind === "raw") &&
|
|
26
|
+
typeof r.body === "string" &&
|
|
27
|
+
(r.outcome === "ok" || r.outcome === "error") &&
|
|
28
|
+
optionalString(r.id) &&
|
|
29
|
+
optionalString(r.thread) &&
|
|
30
|
+
optionalString(r.re) &&
|
|
31
|
+
optionalString(r.reason) &&
|
|
32
|
+
optionalString(r.error) &&
|
|
33
|
+
(r.ms === undefined || typeof r.ms === "number"));
|
|
34
|
+
}
|
|
35
|
+
/** Bytes a string or the JSON of a value may take in a record before it is cut. */
|
|
36
|
+
export const AUDIT_VALUE_CAP = 512;
|
|
37
|
+
/** Bytes a name, an id or an address from the network may take in a message record. */
|
|
38
|
+
export const AUDIT_FIELD_CAP = 256;
|
|
39
|
+
/** A string the harness or the network supplied, cut to `maxBytes` for a record. */
|
|
40
|
+
export function capField(value, maxBytes = AUDIT_FIELD_CAP) {
|
|
41
|
+
return cutUtf8(value, maxBytes);
|
|
42
|
+
}
|
|
43
|
+
/** Bytes `tail` reads from the end of the file. */
|
|
44
|
+
export const AUDIT_TAIL_BYTES = 1024 * 1024;
|
|
45
|
+
function errorMessage(error) {
|
|
46
|
+
return error instanceof Error ? error.message : String(error);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Cap a value for a record: a value whose JSON exceeds the cap becomes
|
|
50
|
+
* `{truncated: true, bytes, head}` with the JSON's first 512 characters, so
|
|
51
|
+
* the record stays valid JSON. A value that cannot be serialized becomes a
|
|
52
|
+
* marker string.
|
|
53
|
+
*/
|
|
54
|
+
export function capValue(value) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
let json;
|
|
58
|
+
try {
|
|
59
|
+
json = JSON.stringify(value) ?? "null";
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return `<unserializable: ${errorMessage(error)}>`;
|
|
63
|
+
}
|
|
64
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
65
|
+
if (bytes <= AUDIT_VALUE_CAP)
|
|
66
|
+
return value;
|
|
67
|
+
return { truncated: true, bytes, head: cutUtf8(json, AUDIT_VALUE_CAP) };
|
|
68
|
+
}
|
|
69
|
+
function isRecord(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What an audit record keeps of a decision answer: never the transcript.
|
|
74
|
+
* An options decision keeps the chosen option; any other answer keeps
|
|
75
|
+
* whether it changed something (a non-empty object).
|
|
76
|
+
*/
|
|
77
|
+
export function summarizeAnswer(decision, answer) {
|
|
78
|
+
if (answer === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (decision?.options && typeof answer === "string")
|
|
81
|
+
return { option: answer };
|
|
82
|
+
return { changed: Object.keys(isRecord(answer) ? answer : {}).length > 0 };
|
|
83
|
+
}
|
|
84
|
+
export class AuditLog {
|
|
85
|
+
path;
|
|
86
|
+
onFailure;
|
|
87
|
+
fd;
|
|
88
|
+
_failed;
|
|
89
|
+
closed = false;
|
|
90
|
+
constructor(path, onFailure) {
|
|
91
|
+
this.path = path;
|
|
92
|
+
this.onFailure = onFailure;
|
|
93
|
+
}
|
|
94
|
+
/** The error that latched the log off, if any. */
|
|
95
|
+
get failed() {
|
|
96
|
+
return this._failed;
|
|
97
|
+
}
|
|
98
|
+
/** Append one record. Never throws; a no-op once the log failed or was closed. True when the line was written. */
|
|
99
|
+
append(record) {
|
|
100
|
+
if (this._failed !== undefined || this.closed)
|
|
101
|
+
return false;
|
|
102
|
+
try {
|
|
103
|
+
if (this.fd === undefined) {
|
|
104
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
105
|
+
this.fd = openSync(this.path, "a", 0o600);
|
|
106
|
+
}
|
|
107
|
+
const full = { ts: new Date().toISOString(), pid: process.pid, ...record };
|
|
108
|
+
writeSync(this.fd, `${JSON.stringify(full)}\n`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.fail(errorMessage(error));
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
fail(error) {
|
|
117
|
+
this._failed = error;
|
|
118
|
+
this.close();
|
|
119
|
+
try {
|
|
120
|
+
this.onFailure?.(error);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// a failure callback must not fail the caller
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on("exit")`. */
|
|
127
|
+
close() {
|
|
128
|
+
this.closed = true;
|
|
129
|
+
if (this.fd === undefined)
|
|
130
|
+
return;
|
|
131
|
+
const fd = this.fd;
|
|
132
|
+
this.fd = undefined;
|
|
133
|
+
try {
|
|
134
|
+
closeSync(fd);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** The last `limit` records, read from the last megabyte of the file. Never throws. */
|
|
141
|
+
tail(limit) {
|
|
142
|
+
let fd;
|
|
143
|
+
try {
|
|
144
|
+
fd = openSync(this.path, "r");
|
|
145
|
+
const size = fstatSync(fd).size;
|
|
146
|
+
const start = Math.max(0, size - AUDIT_TAIL_BYTES);
|
|
147
|
+
const buffer = Buffer.alloc(size - start);
|
|
148
|
+
let read = 0;
|
|
149
|
+
while (read < buffer.length) {
|
|
150
|
+
const n = readSync(fd, buffer, read, buffer.length - read, start + read);
|
|
151
|
+
if (n === 0)
|
|
152
|
+
break;
|
|
153
|
+
read += n;
|
|
154
|
+
}
|
|
155
|
+
const lines = buffer.toString("utf8", 0, read).split("\n");
|
|
156
|
+
// the window may start mid-line: drop the first line unless the byte before it ends a line
|
|
157
|
+
if (start > 0) {
|
|
158
|
+
const before = Buffer.alloc(1);
|
|
159
|
+
const n = readSync(fd, before, 0, 1, start - 1);
|
|
160
|
+
if (n !== 1 || before[0] !== 0x0a)
|
|
161
|
+
lines.shift();
|
|
162
|
+
}
|
|
163
|
+
const records = [];
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
if (line === "")
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
records.push(JSON.parse(line));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
records.push({ raw: line });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const truncated = start > 0 || records.length > limit;
|
|
175
|
+
return { records: records.slice(-limit), truncated };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// no log yet, or a path that can never hold one (the failure is in `failed`)
|
|
179
|
+
const code = error.code;
|
|
180
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
181
|
+
return { records: [], truncated: false };
|
|
182
|
+
return { records: [{ error: errorMessage(error) }], truncated: false };
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (fd !== undefined) {
|
|
186
|
+
try {
|
|
187
|
+
closeSync(fd);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// ignore
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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","sourcesContent":["// The audit log (abaigrammar section 10).\n//\n// One JSON line per act of control: a mutate capability call, a knob write\n// by any path, a decision's outcome, and the counters of defaulted decisions\n// flushed at attach, close and exit. Written synchronously, one line per\n// act: acts are rare and a partial line on a crash is worse than a syscall.\n// Nothing here throws into the agent: the first failure latches the log off,\n// reports once through `onFailure`, and every later append is a no-op.\n// `tail` reads the log back for the harness.\nimport { closeSync, fstatSync, mkdirSync, openSync, readSync, writeSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { cutUtf8, type DecisionDecl } from \"./manifest/manifest.ts\";\n\nexport type AuditAct = \"capability\" | \"knob\" | \"decision\" | \"turn\" | \"lifecycle\" | \"counters\" | \"message\";\nexport type AuditOutcome = \"ok\" | \"error\" | \"forbidden\" | \"agent\" | \"default\" | \"stub\" | \"failed\";\n\nexport interface AuditRecord {\n\tts: string;\n\tpid: number;\n\t/** The connection that acted (`k1`, `k2`, ...); absent for counters. */\n\tconn?: string;\n\tact: AuditAct;\n\t/** The capability, cell, decision or counter name. */\n\tname: string;\n\t/** The request id or hook id. */\n\tid?: string;\n\t/** The parameters (capped, see `capValue`). */\n\tparams?: unknown;\n\t/** A knob's landed value, or a decision answer's summary. */\n\tvalue?: unknown;\n\toutcome: AuditOutcome;\n\t/** Why a default or stub answer applied, or what a counter counts. */\n\tcause?: string;\n\t/** What the harness stated for an act whose policy is `confirm`: approved, auto, or denied. */\n\tconsent?: string;\n\t/** The agent's stated reason. */\n\treason?: unknown;\n\terror?: unknown;\n\t/** Milliseconds from dispatch to settle. */\n\tms?: number;\n\t/** Counters only. */\n\tcount?: number;\n}\n\n/**\n * One message sent to or received from a member of the run's network\n * (section 10), as the harness hands it to the stub for the audit log. The\n * stub writes it as `act: message` with the counterpart's name as `name`,\n * the envelope id as `id`, the body capped as `value`.\n */\nexport interface MessageRecord {\n\tdirection: \"sent\" | \"received\";\n\t/** The counterpart's name as the network's authority issued it, else its address. */\n\tpeer: string;\n\taddr: string;\n\tnetwork: string;\n\t/** The envelope id, when the payload carried one. */\n\tid?: string;\n\tthread?: string;\n\tre?: string;\n\t/** `msg` is an aiap message; `raw` a plain payload with no envelope. */\n\tkind: \"msg\" | \"raw\";\n\tbody: string;\n\toutcome: \"ok\" | \"error\";\n\t/** The agent's stated reason, for a send. */\n\treason?: string;\n\terror?: string;\n\t/** Milliseconds a send took. */\n\tms?: number;\n}\n\nfunction optionalString(value: unknown): boolean {\n\treturn value === undefined || typeof value === \"string\";\n}\n\n/** Whether a value is a `MessageRecord`: what the sidecar endpoint checks before it writes what a harness sent. */\nexport function isMessageRecord(value: unknown): value is MessageRecord {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst r = value as Record<string, unknown>;\n\treturn (\n\t\t(r.direction === \"sent\" || r.direction === \"received\") &&\n\t\ttypeof r.peer === \"string\" &&\n\t\ttypeof r.addr === \"string\" &&\n\t\ttypeof r.network === \"string\" &&\n\t\t(r.kind === \"msg\" || r.kind === \"raw\") &&\n\t\ttypeof r.body === \"string\" &&\n\t\t(r.outcome === \"ok\" || r.outcome === \"error\") &&\n\t\toptionalString(r.id) &&\n\t\toptionalString(r.thread) &&\n\t\toptionalString(r.re) &&\n\t\toptionalString(r.reason) &&\n\t\toptionalString(r.error) &&\n\t\t(r.ms === undefined || typeof r.ms === \"number\")\n\t);\n}\n\n/** Bytes a string or the JSON of a value may take in a record before it is cut. */\nexport const AUDIT_VALUE_CAP = 512;\n/** Bytes a name, an id or an address from the network may take in a message record. */\nexport const AUDIT_FIELD_CAP = 256;\n\n/** A string the harness or the network supplied, cut to `maxBytes` for a record. */\nexport function capField(value: string, maxBytes: number = AUDIT_FIELD_CAP): string {\n\treturn cutUtf8(value, maxBytes);\n}\n/** Bytes `tail` reads from the end of the file. */\nexport const AUDIT_TAIL_BYTES = 1024 * 1024;\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Cap a value for a record: a value whose JSON exceeds the cap becomes\n * `{truncated: true, bytes, head}` with the JSON's first 512 characters, so\n * the record stays valid JSON. A value that cannot be serialized becomes a\n * marker string.\n */\nexport function capValue(value: unknown): unknown {\n\tif (value === undefined) return undefined;\n\tlet json: string;\n\ttry {\n\t\tjson = JSON.stringify(value) ?? \"null\";\n\t} catch (error) {\n\t\treturn `<unserializable: ${errorMessage(error)}>`;\n\t}\n\tconst bytes = Buffer.byteLength(json, \"utf8\");\n\tif (bytes <= AUDIT_VALUE_CAP) return value;\n\treturn { truncated: true, bytes, head: cutUtf8(json, AUDIT_VALUE_CAP) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * What an audit record keeps of a decision answer: never the transcript.\n * An options decision keeps the chosen option; any other answer keeps\n * whether it changed something (a non-empty object).\n */\nexport function summarizeAnswer(decision: DecisionDecl | undefined, answer: unknown): unknown {\n\tif (answer === undefined) return undefined;\n\tif (decision?.options && typeof answer === \"string\") return { option: answer };\n\treturn { changed: Object.keys(isRecord(answer) ? answer : {}).length > 0 };\n}\n\nexport interface AuditTail {\n\trecords: unknown[];\n\t/** More records exist than were returned. */\n\ttruncated: boolean;\n}\n\nexport class AuditLog {\n\treadonly path: string;\n\tprivate readonly onFailure: ((error: string) => void) | undefined;\n\tprivate fd: number | undefined;\n\tprivate _failed: string | undefined;\n\tprivate closed = false;\n\n\tconstructor(path: string, onFailure?: (error: string) => void) {\n\t\tthis.path = path;\n\t\tthis.onFailure = onFailure;\n\t}\n\n\t/** The error that latched the log off, if any. */\n\tget failed(): string | undefined {\n\t\treturn this._failed;\n\t}\n\n\t/** Append one record. Never throws; a no-op once the log failed or was closed. True when the line was written. */\n\tappend(record: Omit<AuditRecord, \"ts\" | \"pid\">): boolean {\n\t\tif (this._failed !== undefined || this.closed) return false;\n\t\ttry {\n\t\t\tif (this.fd === undefined) {\n\t\t\t\tmkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });\n\t\t\t\tthis.fd = openSync(this.path, \"a\", 0o600);\n\t\t\t}\n\t\t\tconst full: AuditRecord = { ts: new Date().toISOString(), pid: process.pid, ...record };\n\t\t\twriteSync(this.fd, `${JSON.stringify(full)}\\n`);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.fail(errorMessage(error));\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate fail(error: string): void {\n\t\tthis._failed = error;\n\t\tthis.close();\n\t\ttry {\n\t\t\tthis.onFailure?.(error);\n\t\t} catch {\n\t\t\t// a failure callback must not fail the caller\n\t\t}\n\t}\n\n\t/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on(\"exit\")`. */\n\tclose(): void {\n\t\tthis.closed = true;\n\t\tif (this.fd === undefined) return;\n\t\tconst fd = this.fd;\n\t\tthis.fd = undefined;\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\t/** The last `limit` records, read from the last megabyte of the file. Never throws. */\n\ttail(limit: number): AuditTail {\n\t\tlet fd: number | undefined;\n\t\ttry {\n\t\t\tfd = openSync(this.path, \"r\");\n\t\t\tconst size = fstatSync(fd).size;\n\t\t\tconst start = Math.max(0, size - AUDIT_TAIL_BYTES);\n\t\t\tconst buffer = Buffer.alloc(size - start);\n\t\t\tlet read = 0;\n\t\t\twhile (read < buffer.length) {\n\t\t\t\tconst n = readSync(fd, buffer, read, buffer.length - read, start + read);\n\t\t\t\tif (n === 0) break;\n\t\t\t\tread += n;\n\t\t\t}\n\t\t\tconst lines = buffer.toString(\"utf8\", 0, read).split(\"\\n\");\n\t\t\t// the window may start mid-line: drop the first line unless the byte before it ends a line\n\t\t\tif (start > 0) {\n\t\t\t\tconst before = Buffer.alloc(1);\n\t\t\t\tconst n = readSync(fd, before, 0, 1, start - 1);\n\t\t\t\tif (n !== 1 || before[0] !== 0x0a) lines.shift();\n\t\t\t}\n\t\t\tconst records: unknown[] = [];\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line === \"\") continue;\n\t\t\t\ttry {\n\t\t\t\t\trecords.push(JSON.parse(line));\n\t\t\t\t} catch {\n\t\t\t\t\trecords.push({ raw: line });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst truncated = start > 0 || records.length > limit;\n\t\t\treturn { records: records.slice(-limit), truncated };\n\t\t} catch (error) {\n\t\t\t// no log yet, or a path that can never hold one (the failure is in `failed`)\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"ENOENT\" || code === \"ENOTDIR\") return { records: [], truncated: false };\n\t\t\treturn { records: [{ error: errorMessage(error) }], truncated: false };\n\t\t} finally {\n\t\t\tif (fd !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CellDecl } from "../manifest/manifest.ts";
|
|
2
|
+
/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */
|
|
3
|
+
export declare function fitValue(value: unknown, decl: CellDecl): unknown;
|
|
4
|
+
/** Whether a value can be written to a knob: the declared type, enum and range. */
|
|
5
|
+
export declare function validateKnobValue(decl: CellDecl, value: unknown): string | undefined;
|
|
6
|
+
//# sourceMappingURL=fit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fit.d.ts","sourceRoot":"","sources":["../../src/cells/fit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,QAAQ,EAAiE,MAAM,yBAAyB,CAAC;AAOvH,mGAAmG;AACnG,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAsBhE;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQpF","sourcesContent":["// Fitting cell values to their declared sizes and validating knob writes\n// (abaigrammar section 5.2).\nimport { type CellDecl, cutUtf8, encodedBytes, type FieldSchema, fieldOf, matchesType } from \"../manifest/manifest.ts\";\n\n/** Cut a string to at most `max` bytes without splitting a code point (one encode, one decode). */\nfunction cutString(value: string, max: number): string {\n\treturn cutUtf8(value, max);\n}\n\n/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */\nexport function fitValue(value: unknown, decl: CellDecl): unknown {\n\tif (decl.type === \"string\" && typeof value === \"string\") {\n\t\treturn cutString(value, decl.max ?? 256);\n\t}\n\tif (decl.type === \"object\" && typeof value === \"object\" && value !== null) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst bytes = encodedBytes(value);\n\t\tif (bytes > max) return { truncated: true, bytes };\n\t\treturn JSON.parse(JSON.stringify(value));\n\t}\n\tif (decl.type === \"list\" && Array.isArray(value)) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst items = decl.items === undefined ? undefined : fieldOf(decl.items);\n\t\tconst kept: unknown[] = [];\n\t\tfor (const item of value) {\n\t\t\tconst fitted = items?.type === \"string\" && typeof item === \"string\" ? cutString(item, items.max ?? 256) : item;\n\t\t\tif (encodedBytes([...kept, fitted]) > max) break;\n\t\t\tkept.push(fitted);\n\t\t}\n\t\treturn kept;\n\t}\n\treturn value;\n}\n\n/** Whether a value can be written to a knob: the declared type, enum and range. */\nexport function validateKnobValue(decl: CellDecl, value: unknown): string | undefined {\n\tif (!matchesType(value, decl as unknown as FieldSchema)) {\n\t\treturn decl.type === \"enum\" ? `value must be one of: ${decl.enum?.join(\", \")}` : `value must be a ${decl.type}`;\n\t}\n\tif (decl.range && typeof value === \"number\" && (value < decl.range[0] || value > decl.range[1])) {\n\t\treturn `value must be within ${decl.range[0]} and ${decl.range[1]}`;\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Fitting cell values to their declared sizes and validating knob writes
|
|
2
|
+
// (abaigrammar section 5.2).
|
|
3
|
+
import { cutUtf8, encodedBytes, fieldOf, matchesType } from "../manifest/manifest.js";
|
|
4
|
+
/** Cut a string to at most `max` bytes without splitting a code point (one encode, one decode). */
|
|
5
|
+
function cutString(value, max) {
|
|
6
|
+
return cutUtf8(value, max);
|
|
7
|
+
}
|
|
8
|
+
/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */
|
|
9
|
+
export function fitValue(value, decl) {
|
|
10
|
+
if (decl.type === "string" && typeof value === "string") {
|
|
11
|
+
return cutString(value, decl.max ?? 256);
|
|
12
|
+
}
|
|
13
|
+
if (decl.type === "object" && typeof value === "object" && value !== null) {
|
|
14
|
+
const max = decl.max ?? 1024;
|
|
15
|
+
const bytes = encodedBytes(value);
|
|
16
|
+
if (bytes > max)
|
|
17
|
+
return { truncated: true, bytes };
|
|
18
|
+
return JSON.parse(JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
if (decl.type === "list" && Array.isArray(value)) {
|
|
21
|
+
const max = decl.max ?? 1024;
|
|
22
|
+
const items = decl.items === undefined ? undefined : fieldOf(decl.items);
|
|
23
|
+
const kept = [];
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
const fitted = items?.type === "string" && typeof item === "string" ? cutString(item, items.max ?? 256) : item;
|
|
26
|
+
if (encodedBytes([...kept, fitted]) > max)
|
|
27
|
+
break;
|
|
28
|
+
kept.push(fitted);
|
|
29
|
+
}
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
/** Whether a value can be written to a knob: the declared type, enum and range. */
|
|
35
|
+
export function validateKnobValue(decl, value) {
|
|
36
|
+
if (!matchesType(value, decl)) {
|
|
37
|
+
return decl.type === "enum" ? `value must be one of: ${decl.enum?.join(", ")}` : `value must be a ${decl.type}`;
|
|
38
|
+
}
|
|
39
|
+
if (decl.range && typeof value === "number" && (value < decl.range[0] || value > decl.range[1])) {
|
|
40
|
+
return `value must be within ${decl.range[0]} and ${decl.range[1]}`;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=fit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fit.js","sourceRoot":"","sources":["../../src/cells/fit.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,6BAA6B;AAC7B,OAAO,EAAiB,OAAO,EAAE,YAAY,EAAoB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEvH,mGAAmG;AACnG,SAAS,SAAS,CAAC,KAAa,EAAE,GAAW,EAAU;IACtD,OAAO,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAAA,CAC3B;AAED,mGAAmG;AACnG,MAAM,UAAU,QAAQ,CAAC,KAAc,EAAE,IAAc,EAAW;IACjE,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACzD,OAAO,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC3E,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC;QAC7B,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,KAAK,GAAG,GAAG;YAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzE,MAAM,IAAI,GAAc,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG,KAAK,EAAE,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC/G,IAAI,YAAY,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,GAAG,GAAG;gBAAE,MAAM;YACjD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,IAAc,EAAE,KAAc,EAAsB;IACrF,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAA8B,CAAC,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,yBAAyB,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,mBAAmB,IAAI,CAAC,IAAI,EAAE,CAAC;IACjH,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjG,OAAO,wBAAwB,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["// Fitting cell values to their declared sizes and validating knob writes\n// (abaigrammar section 5.2).\nimport { type CellDecl, cutUtf8, encodedBytes, type FieldSchema, fieldOf, matchesType } from \"../manifest/manifest.ts\";\n\n/** Cut a string to at most `max` bytes without splitting a code point (one encode, one decode). */\nfunction cutString(value: string, max: number): string {\n\treturn cutUtf8(value, max);\n}\n\n/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */\nexport function fitValue(value: unknown, decl: CellDecl): unknown {\n\tif (decl.type === \"string\" && typeof value === \"string\") {\n\t\treturn cutString(value, decl.max ?? 256);\n\t}\n\tif (decl.type === \"object\" && typeof value === \"object\" && value !== null) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst bytes = encodedBytes(value);\n\t\tif (bytes > max) return { truncated: true, bytes };\n\t\treturn JSON.parse(JSON.stringify(value));\n\t}\n\tif (decl.type === \"list\" && Array.isArray(value)) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst items = decl.items === undefined ? undefined : fieldOf(decl.items);\n\t\tconst kept: unknown[] = [];\n\t\tfor (const item of value) {\n\t\t\tconst fitted = items?.type === \"string\" && typeof item === \"string\" ? cutString(item, items.max ?? 256) : item;\n\t\t\tif (encodedBytes([...kept, fitted]) > max) break;\n\t\t\tkept.push(fitted);\n\t\t}\n\t\treturn kept;\n\t}\n\treturn value;\n}\n\n/** Whether a value can be written to a knob: the declared type, enum and range. */\nexport function validateKnobValue(decl: CellDecl, value: unknown): string | undefined {\n\tif (!matchesType(value, decl as unknown as FieldSchema)) {\n\t\treturn decl.type === \"enum\" ? `value must be one of: ${decl.enum?.join(\", \")}` : `value must be a ${decl.type}`;\n\t}\n\tif (decl.range && typeof value === \"number\" && (value < decl.range[0] || value > decl.range[1])) {\n\t\treturn `value must be within ${decl.range[0]} and ${decl.range[1]}`;\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Manifest } from "./manifest/manifest.ts";
|
|
2
|
+
export declare const MANIFEST_FILE = "abaigram.yaml";
|
|
3
|
+
export interface ContentSlots {
|
|
4
|
+
readonly root: string;
|
|
5
|
+
/** The identity text; throws when the slot is missing or empty. */
|
|
6
|
+
identity(): string;
|
|
7
|
+
install(): string | undefined;
|
|
8
|
+
use(): string | undefined;
|
|
9
|
+
/** The content for a named turn or decision, from the `turns` directory. */
|
|
10
|
+
turn(name: string): string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Whether the manifest file at `path` names this program. */
|
|
13
|
+
export declare function isManifestOf(path: string, name: string): boolean;
|
|
14
|
+
/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */
|
|
15
|
+
export declare function findRoot(name: string, dir?: string): string | undefined;
|
|
16
|
+
export declare function createContent(manifest: Manifest, resolveRoot: () => string): ContentSlots;
|
|
17
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,IAAI,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,IAAI,MAAM,GAAG,SAAS,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOhE;AAED,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,SAAS,CAQtF;AAOD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,MAAM,GAAG,YAAY,CA8BzF","sourcesContent":["// The content slots (abaigrammar section 8): the abaigrammer's prompts, read\n// from files under the program's root. The root is where abaigram.yaml lives:\n// an explicit option, else the nearest ancestor of the cwd holding this\n// program's manifest, else the cwd. Reads are lazy, so a program that never\n// attaches never touches the files.\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { Manifest } from \"./manifest/manifest.ts\";\nimport { parseManifestYaml } from \"./manifest/render.ts\";\n\nexport const MANIFEST_FILE = \"abaigram.yaml\";\n\nexport interface ContentSlots {\n\treadonly root: string;\n\t/** The identity text; throws when the slot is missing or empty. */\n\tidentity(): string;\n\tinstall(): string | undefined;\n\tuse(): string | undefined;\n\t/** The content for a named turn or decision, from the `turns` directory. */\n\tturn(name: string): string | undefined;\n}\n\n/** Whether the manifest file at `path` names this program. */\nexport function isManifestOf(path: string, name: string): boolean {\n\ttry {\n\t\tconst parsed = parseManifestYaml(readFileSync(path, \"utf8\"));\n\t\treturn typeof parsed === \"object\" && parsed !== null && (parsed as { name?: unknown }).name === name;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */\nexport function findRoot(name: string, dir: string = process.cwd()): string | undefined {\n\tlet current = resolve(dir);\n\twhile (true) {\n\t\tif (isManifestOf(join(current, MANIFEST_FILE), name)) return current;\n\t\tconst parent = dirname(current);\n\t\tif (parent === current) return undefined;\n\t\tcurrent = parent;\n\t}\n}\n\nfunction readOptional(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\treturn readFileSync(path, \"utf8\");\n}\n\nexport function createContent(manifest: Manifest, resolveRoot: () => string): ContentSlots {\n\tlet found: string | undefined;\n\tconst root = (): string => {\n\t\tif (found === undefined) found = resolveRoot();\n\t\treturn found;\n\t};\n\tconst slot = (relative: string): string => resolve(root(), relative);\n\treturn {\n\t\tget root() {\n\t\t\treturn root();\n\t\t},\n\t\tidentity() {\n\t\t\tconst path = slot(manifest.content.identity);\n\t\t\tconst text = readOptional(path);\n\t\t\tif (text === undefined) throw new Error(`the identity content is missing: ${path}`);\n\t\t\tif (text.trim() === \"\") throw new Error(`the identity content is empty: ${path}`);\n\t\t\treturn text;\n\t\t},\n\t\t// the undeclared slots default to the abai/ directory (section 8); a missing file is simply no content\n\t\tinstall() {\n\t\t\treturn readOptional(slot(manifest.content.install ?? \"abai/install.md\"));\n\t\t},\n\t\tuse() {\n\t\t\treturn readOptional(slot(manifest.content.use ?? \"abai/use.md\"));\n\t\t},\n\t\tturn(name) {\n\t\t\tconst dir = manifest.content.turns ?? \"abai/turns\";\n\t\t\treturn readOptional(slot(join(dir, `${name}.md`)));\n\t\t},\n\t};\n}\n"]}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The content slots (abaigrammar section 8): the abaigrammer's prompts, read
|
|
2
|
+
// from files under the program's root. The root is where abaigram.yaml lives:
|
|
3
|
+
// an explicit option, else the nearest ancestor of the cwd holding this
|
|
4
|
+
// program's manifest, else the cwd. Reads are lazy, so a program that never
|
|
5
|
+
// attaches never touches the files.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { parseManifestYaml } from "./manifest/render.js";
|
|
9
|
+
export const MANIFEST_FILE = "abaigram.yaml";
|
|
10
|
+
/** Whether the manifest file at `path` names this program. */
|
|
11
|
+
export function isManifestOf(path, name) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = parseManifestYaml(readFileSync(path, "utf8"));
|
|
14
|
+
return typeof parsed === "object" && parsed !== null && parsed.name === name;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */
|
|
21
|
+
export function findRoot(name, dir = process.cwd()) {
|
|
22
|
+
let current = resolve(dir);
|
|
23
|
+
while (true) {
|
|
24
|
+
if (isManifestOf(join(current, MANIFEST_FILE), name))
|
|
25
|
+
return current;
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current)
|
|
28
|
+
return undefined;
|
|
29
|
+
current = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readOptional(path) {
|
|
33
|
+
if (!existsSync(path))
|
|
34
|
+
return undefined;
|
|
35
|
+
return readFileSync(path, "utf8");
|
|
36
|
+
}
|
|
37
|
+
export function createContent(manifest, resolveRoot) {
|
|
38
|
+
let found;
|
|
39
|
+
const root = () => {
|
|
40
|
+
if (found === undefined)
|
|
41
|
+
found = resolveRoot();
|
|
42
|
+
return found;
|
|
43
|
+
};
|
|
44
|
+
const slot = (relative) => resolve(root(), relative);
|
|
45
|
+
return {
|
|
46
|
+
get root() {
|
|
47
|
+
return root();
|
|
48
|
+
},
|
|
49
|
+
identity() {
|
|
50
|
+
const path = slot(manifest.content.identity);
|
|
51
|
+
const text = readOptional(path);
|
|
52
|
+
if (text === undefined)
|
|
53
|
+
throw new Error(`the identity content is missing: ${path}`);
|
|
54
|
+
if (text.trim() === "")
|
|
55
|
+
throw new Error(`the identity content is empty: ${path}`);
|
|
56
|
+
return text;
|
|
57
|
+
},
|
|
58
|
+
// the undeclared slots default to the abai/ directory (section 8); a missing file is simply no content
|
|
59
|
+
install() {
|
|
60
|
+
return readOptional(slot(manifest.content.install ?? "abai/install.md"));
|
|
61
|
+
},
|
|
62
|
+
use() {
|
|
63
|
+
return readOptional(slot(manifest.content.use ?? "abai/use.md"));
|
|
64
|
+
},
|
|
65
|
+
turn(name) {
|
|
66
|
+
const dir = manifest.content.turns ?? "abai/turns";
|
|
67
|
+
return readOptional(slot(join(dir, `${name}.md`)));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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","sourcesContent":["// The content slots (abaigrammar section 8): the abaigrammer's prompts, read\n// from files under the program's root. The root is where abaigram.yaml lives:\n// an explicit option, else the nearest ancestor of the cwd holding this\n// program's manifest, else the cwd. Reads are lazy, so a program that never\n// attaches never touches the files.\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { Manifest } from \"./manifest/manifest.ts\";\nimport { parseManifestYaml } from \"./manifest/render.ts\";\n\nexport const MANIFEST_FILE = \"abaigram.yaml\";\n\nexport interface ContentSlots {\n\treadonly root: string;\n\t/** The identity text; throws when the slot is missing or empty. */\n\tidentity(): string;\n\tinstall(): string | undefined;\n\tuse(): string | undefined;\n\t/** The content for a named turn or decision, from the `turns` directory. */\n\tturn(name: string): string | undefined;\n}\n\n/** Whether the manifest file at `path` names this program. */\nexport function isManifestOf(path: string, name: string): boolean {\n\ttry {\n\t\tconst parsed = parseManifestYaml(readFileSync(path, \"utf8\"));\n\t\treturn typeof parsed === \"object\" && parsed !== null && (parsed as { name?: unknown }).name === name;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */\nexport function findRoot(name: string, dir: string = process.cwd()): string | undefined {\n\tlet current = resolve(dir);\n\twhile (true) {\n\t\tif (isManifestOf(join(current, MANIFEST_FILE), name)) return current;\n\t\tconst parent = dirname(current);\n\t\tif (parent === current) return undefined;\n\t\tcurrent = parent;\n\t}\n}\n\nfunction readOptional(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\treturn readFileSync(path, \"utf8\");\n}\n\nexport function createContent(manifest: Manifest, resolveRoot: () => string): ContentSlots {\n\tlet found: string | undefined;\n\tconst root = (): string => {\n\t\tif (found === undefined) found = resolveRoot();\n\t\treturn found;\n\t};\n\tconst slot = (relative: string): string => resolve(root(), relative);\n\treturn {\n\t\tget root() {\n\t\t\treturn root();\n\t\t},\n\t\tidentity() {\n\t\t\tconst path = slot(manifest.content.identity);\n\t\t\tconst text = readOptional(path);\n\t\t\tif (text === undefined) throw new Error(`the identity content is missing: ${path}`);\n\t\t\tif (text.trim() === \"\") throw new Error(`the identity content is empty: ${path}`);\n\t\t\treturn text;\n\t\t},\n\t\t// the undeclared slots default to the abai/ directory (section 8); a missing file is simply no content\n\t\tinstall() {\n\t\t\treturn readOptional(slot(manifest.content.install ?? \"abai/install.md\"));\n\t\t},\n\t\tuse() {\n\t\t\treturn readOptional(slot(manifest.content.use ?? \"abai/use.md\"));\n\t\t},\n\t\tturn(name) {\n\t\t\tconst dir = manifest.content.turns ?? \"abai/turns\";\n\t\t\treturn readOptional(slot(join(dir, `${name}.md`)));\n\t\t},\n\t};\n}\n"]}
|