prae-cli 0.9.2 → 0.10.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/lib/bin.js +2 -6
- package/lib/{checkpoint-C_q_Rpa5.js → checkpoint-BqJ8xwky.js} +1 -1
- package/lib/cli-BcAJlB9B.js +8 -0
- package/lib/{cli-B_B6-bUZ.js → cli-DPldZ2ND.js} +259 -526
- package/lib/docs/pilot-claude-code.md +191 -0
- package/lib/{evaluate-BqLxhYiP.js → evaluate-DhoGQw0Z.js} +23 -1
- package/lib/frameworks-BgZHGKiH.js +118 -0
- package/lib/hook-Gll2spTP.js +695 -0
- package/lib/index.js +6 -4
- package/lib/init-DRd-IRR6.js +266 -0
- package/lib/{instructions-ya0LVnZy.js → instructions-DihPiiC2.js} +2 -1
- package/lib/{mcp-BCXbpqu4.js → mcp-D521EoP6.js} +2 -2
- package/lib/pack-h5n43S0F.js +448 -0
- package/lib/playground.js +4 -0
- package/lib/{receipt-check-DQK_gVPc.js → receipt-check-CUuEjGHL.js} +1 -1
- package/lib/{src-LA7NdLKb.js → src-Cy-Acrgj.js} +104 -25
- package/lib/{src-CwBEFhww.js → src-DMoYsdfo.js} +1 -1
- package/lib/src-wG36kT6K.js +3 -0
- package/lib/{test-run-CCDgJ0Dp.js → test-run-C8dHjIE2.js} +1 -1
- package/lib/views-CUcixte0.js +1396 -0
- package/package.json +4 -4
- package/lib/src-7X6dO7gC.js +0 -3
- package/lib/views-D4UEfbzt.js +0 -1505
- /package/lib/{ocsf-DhVGbBUw.js → ocsf-DF9cMiQJ.js} +0 -0
- /package/lib/{src-CBGZVvt7.js → src-BlmsTGdY.js} +0 -0
- /package/lib/{src-B25SHWcz.js → src-BuaOiL2v.js} +0 -0
- /package/lib/{src-d4c_n6wd.js → src-CDYyZyeU.js} +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Pilot: PRAE on one Claude Code laptop
|
|
2
|
+
|
|
3
|
+
PRAE runs as a Claude Code `PreToolUse` hook. Before every tool call (built-in
|
|
4
|
+
tools such as `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`, `WebFetch`, and
|
|
5
|
+
every MCP tool `mcp__<server>__<tool>`), Claude Code runs `prae hook
|
|
6
|
+
claude-code`. The hook judges the call against a policy pack with the same
|
|
7
|
+
kernel the other PRAE harnesses use, and appends the verdict to a signed,
|
|
8
|
+
hash-chained ledger on this machine.
|
|
9
|
+
|
|
10
|
+
It starts in **shadow mode**. Shadow mode records every verdict and blocks
|
|
11
|
+
nothing. When the record looks right, one command switches it to **enforce**.
|
|
12
|
+
|
|
13
|
+
There is no cloud account, no open port, no network call and no service to run.
|
|
14
|
+
|
|
15
|
+
**Verified on Claude Code 2.1.233**, from a fresh clone, by following this
|
|
16
|
+
guide in a temporary project:
|
|
17
|
+
|
|
18
|
+
- **Shadow.** One headless session made 10 tool calls: `Read`, `Write`, `Edit`,
|
|
19
|
+
`Bash`, `Glob`, `Grep`, `WebFetch` and `mcp__prae__read_file`. All 10 ran,
|
|
20
|
+
and each one was sealed with its verdict. The `.env` read was sealed as a
|
|
21
|
+
shadow deny (`BASE-005`), and `rm -rf` as a shadow hold (`BASE-002`).
|
|
22
|
+
- **Enforce.** Claude Code refused these: the `.env` read, the same read
|
|
23
|
+
through the MCP tool, a fetch to an unlisted host, edits to the pack and
|
|
24
|
+
to the hook settings, a shell command truncating the pack, and a read of
|
|
25
|
+
the signing key. `rm -rf` and a write outside the workspace were held, and
|
|
26
|
+
`claude -p` refused them. Nothing protected changed on disk.
|
|
27
|
+
- **Record.** `prae verify` passed the chain, with 25 of 25 signatures.
|
|
28
|
+
|
|
29
|
+
## 1. Install (one command)
|
|
30
|
+
|
|
31
|
+
You need Node 22.19 or later and pnpm 10 (`corepack enable`). Until the
|
|
32
|
+
release that carries this hook is on npm (`prae-cli` 0.9.2 does not have it),
|
|
33
|
+
run it from a checkout of this repository, built once:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/taltara/prae-harness && cd prae-harness && pnpm install && pnpm --filter prae-cli build
|
|
37
|
+
PRAE="node $PWD/packages/cli/lib/bin.js" # used below; after the npm release this is just `prae`
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then preview the change, and apply it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$PRAE init claude-code --scope user # prints the settings diff, writes nothing
|
|
44
|
+
$PRAE init claude-code --scope user --apply # installs, shadow mode
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You must name a target. `--scope user` is `~/.claude/settings.json`, which
|
|
48
|
+
covers every project. `--scope project` is `./.claude/settings.json`.
|
|
49
|
+
`--settings <file>` is any file you name, for example
|
|
50
|
+
`.claude/settings.local.json`. Nothing is written without `--apply`. The file
|
|
51
|
+
is written back as 2-space JSON, the format Claude Code itself writes, so a
|
|
52
|
+
hand-formatted file shows layout changes in the diff. Its values do not change.
|
|
53
|
+
|
|
54
|
+
`init` adds one entry and leaves every other setting and hook as it was:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "timeout": 10,
|
|
58
|
+
"command": "'/opt/homebrew/bin/node' '…/packages/cli/lib/bin.js' hook claude-code --shadow --pack '~/.prae/claude-code/prae-baseline.prae.json' --ledger '~/.prae/claude-code/ledger.jsonl' --key '~/.prae/claude-code/prae-signing.pem' --settings '~/.claude/settings.json'" }] }]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The real file holds absolute paths. The command names the node binary and
|
|
62
|
+
PRAE's own script by absolute path, so it does not depend on `PATH`. If you
|
|
63
|
+
move the checkout, run `init` again. Claude Code reloads settings on its own,
|
|
64
|
+
so new tool calls go through PRAE without a restart.
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
|
|
68
|
+
- `--pack <file>`: use your own pack. The default copies the built-in baseline
|
|
69
|
+
(8 rules: download-and-execute, `rm -rf`, privilege escalation, block devices,
|
|
70
|
+
credential files, writes outside the workspace, force-push, publishes).
|
|
71
|
+
`prae check <file>` summarises a pack.
|
|
72
|
+
- `--ledger <file>`: write the ledger somewhere else.
|
|
73
|
+
- `--dir <dir>`: keep PRAE's state somewhere other than `~/.prae/claude-code/`.
|
|
74
|
+
|
|
75
|
+
## 2. What it records, and where
|
|
76
|
+
|
|
77
|
+
Everything lives in `~/.prae/claude-code/`, which is created with mode 0700:
|
|
78
|
+
|
|
79
|
+
| File | What it is |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `ledger.jsonl` | One row for each tool call. Every Claude Code session on the machine appends to this one file, under a lock, so parallel tool calls and parallel sessions stay one chain. |
|
|
82
|
+
| `prae-signing.pem` | The Ed25519 key that signs each row. It is generated once with mode 0600. |
|
|
83
|
+
| `prae-signing.pub.pem` | The public half. Give it to whoever verifies the record. |
|
|
84
|
+
| `prae-baseline.prae.json` | The pack in force, unless you passed `--pack`. |
|
|
85
|
+
|
|
86
|
+
Each row holds the time, the Claude Code `session_id`, the `tool_use_id` (as
|
|
87
|
+
`correlation`), the tool, its capability (`fs:read`, `fs:write`,
|
|
88
|
+
`proc:spawn`, `net:fetch`), the subject, the verdict, the rule and its reason,
|
|
89
|
+
the pack's name, version and sha256, and, in shadow mode, `shadow: true`. The
|
|
90
|
+
subject is the path or URL only. A shell call is stored as its program word
|
|
91
|
+
and a hash, never the command line. File contents and tool output are never
|
|
92
|
+
stored.
|
|
93
|
+
|
|
94
|
+
Read the record:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
P=~/.prae/claude-code
|
|
98
|
+
$PRAE verify $P/ledger.jsonl --key $P/prae-signing.pub.pem
|
|
99
|
+
$PRAE report $P/ledger.jsonl --pack $P/prae-baseline.prae.json --out report.html
|
|
100
|
+
$PRAE console $P/ledger.jsonl --pack $P/prae-baseline.prae.json --out console/
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
In the report and the console, a shadow verdict is marked `· SHADOW`. It is
|
|
104
|
+
never counted as vetoed or held.
|
|
105
|
+
|
|
106
|
+
## 3. From shadow to enforce
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
$PRAE init claude-code --scope user --enforce --apply
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This replaces PRAE's entry. It does not add a second one. In enforce mode:
|
|
113
|
+
|
|
114
|
+
| Verdict | What Claude Code does |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| allow | The hook prints nothing. Claude Code's own permission flow decides, as before. PRAE never answers `allow`, because that would skip your permission prompt. |
|
|
117
|
+
| deny | `permissionDecision: "deny"`. The call does not run, and Claude reads the reason, for example `PRAE BASE-005: … (ledger row 12)`. |
|
|
118
|
+
| ask | `permissionDecision: "ask"`. You get a permission prompt. In a headless `claude -p` run nobody can answer it, so Claude Code refuses the call (verified on 2.1.233). |
|
|
119
|
+
| gate error | The pack does not parse, the ledger cannot be written, the key is unreadable, the payload is broken, or the ledger lock is held for more than 3 seconds. The hook refuses the call (exit 2) and records the error in the ledger when it can. |
|
|
120
|
+
|
|
121
|
+
In shadow mode a gate error lets the call run. It shows a `systemMessage`
|
|
122
|
+
warning and records a `PRAE-ERROR` row marked `shadow`.
|
|
123
|
+
|
|
124
|
+
## 4. Uninstall
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
$PRAE init claude-code --scope user --remove --apply
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This removes PRAE's hook entry and nothing else. The ledger and the key stay
|
|
131
|
+
in place, because they are the evidence of what ran. Delete
|
|
132
|
+
`~/.prae/claude-code/` yourself when you no longer need them.
|
|
133
|
+
|
|
134
|
+
## 5. Privacy
|
|
135
|
+
|
|
136
|
+
Nothing leaves the machine. The hook reads the payload Claude Code passes on
|
|
137
|
+
stdin, reads the pack, appends to a local file, and exits. It opens no port
|
|
138
|
+
and makes no network request. A `WebFetch` is judged by its URL and is never
|
|
139
|
+
fetched by PRAE. For credential checks the gate reads the file the agent is
|
|
140
|
+
about to read, in its own process, and only the finding (type, line,
|
|
141
|
+
confidence) reaches the ledger.
|
|
142
|
+
|
|
143
|
+
## 6. Limits, stated plainly
|
|
144
|
+
|
|
145
|
+
- **A hook that cannot start does not block, on current Claude Code.** In
|
|
146
|
+
enforce mode, PRAE refuses whenever it runs and fails: a bad pack, an
|
|
147
|
+
unwritable ledger, an exception, its own 3 s lock deadline, or broken
|
|
148
|
+
input. Before Claude Code 2.1.295, though, a hook that cannot start at all
|
|
149
|
+
(node or the checkout moved) or that hits Claude Code's 10 s timeout lets
|
|
150
|
+
the call through. This was checked on 2.1.233. Enforce writes
|
|
151
|
+
`onFailure: "block"`, which Claude Code 2.1.295 and later honour, and
|
|
152
|
+
which 2.1.233 ignores.
|
|
153
|
+
- **The shell is judged by its command line, not by its effect.** The baseline
|
|
154
|
+
stops `Read` of `.env`, but `cat .env` in `Bash` is a different call, and
|
|
155
|
+
no path rule sees it. Write shell rules (`command`) for the programs you care
|
|
156
|
+
about. PRAE does not yet screen tool output in Claude Code. A `PostToolUse`
|
|
157
|
+
hook can only replace output in each tool's exact output shape, which this
|
|
158
|
+
release does not take on.
|
|
159
|
+
- **PRAE's own files are protected only against plain attempts.** Writes to the
|
|
160
|
+
pack, the ledger and the hook settings are refused (`PRAE-SELF`), and so are
|
|
161
|
+
reads of the signing key. Shell commands that name the pack, the settings or
|
|
162
|
+
the key are refused too, even read-only ones such as `wc -c <pack>`. But the agent runs as you. A shell can spell a path
|
|
163
|
+
in ways no string check sees, so a determined agent with `Bash` can still
|
|
164
|
+
edit them. An edited ledger breaks its chain and its signatures. A wholesale
|
|
165
|
+
rewrite by someone who holds the key needs an external checkpoint
|
|
166
|
+
(`prae checkpoint`) to catch.
|
|
167
|
+
- **What the hook never sees.** Files you attach with `@` in a prompt are read
|
|
168
|
+
without a tool call, so no hook fires. Managed settings can switch user hooks
|
|
169
|
+
off (`allowManagedHooksOnly`, `disableAllHooks`).
|
|
170
|
+
- **Coverage depends on knowing what a tool does.** Built-in tools map to
|
|
171
|
+
capabilities by name. MCP tools map by their tool name: the reference
|
|
172
|
+
`filesystem` and `fetch` servers' tools, and `prae mcp`'s own, are
|
|
173
|
+
recognised. Any other MCP tool is recorded and matched only by rules that
|
|
174
|
+
name the tool. `WebSearch` has a query, not a URL, so host rules do not
|
|
175
|
+
apply to it. A path that only appears inside an array (for example
|
|
176
|
+
`read_multiple_files`) is not path-checked.
|
|
177
|
+
- **What happens after a hold is not recorded.** The row says the call was
|
|
178
|
+
held (`ask`). Whether you then approved it in the prompt is not written
|
|
179
|
+
back, because a `PreToolUse` hook gets no outcome. The report lists every
|
|
180
|
+
enforced hold under "awaiting human".
|
|
181
|
+
- **The agent's own account is not evidence.** In the verification runs the
|
|
182
|
+
model twice reported a refused call as having run. The ledger, Claude
|
|
183
|
+
Code's `permission_denials` and the file system agreed with each other, and
|
|
184
|
+
they are what to read.
|
|
185
|
+
- **Budgets cost a ledger read.** A pack with a `count` rule makes each call
|
|
186
|
+
count this session's earlier rows in the ledger. Packs without one never
|
|
187
|
+
read the ledger.
|
|
188
|
+
- **Latency.** Each tool call starts one node process. Measured on an M-series
|
|
189
|
+
Mac, the built `lib/bin.js` took 30 ms at p50 and 34 ms at p95. Run from
|
|
190
|
+
`src/bin.ts` with Node's type stripping, it took 60 ms at p50 and 64 ms at
|
|
191
|
+
p95. A bare `node -e 0` takes 20 ms on the same machine.
|
|
@@ -272,6 +272,28 @@ function withinAny(path, globs) {
|
|
|
272
272
|
return globs.some((glob) => within(path, glob));
|
|
273
273
|
}
|
|
274
274
|
/**
|
|
275
|
+
* An absolute path under `cwd`, rewritten as the workspace-relative path a
|
|
276
|
+
* pack's `./` globs speak about; anything else comes back as written.
|
|
277
|
+
*
|
|
278
|
+
* A model is free to name a workspace file by its absolute path — the first
|
|
279
|
+
* real agent run through the gate did exactly that for its first three
|
|
280
|
+
* reads — and a scope rule written as `outside: ["./src/**"]` must decide
|
|
281
|
+
* that file by where it is, not by how it was spelled. The anchoring in
|
|
282
|
+
* {@link within} is right to keep `/etc/hosts` out of `./**`; this is the
|
|
283
|
+
* other half, applied by an adapter that knows the session's directory.
|
|
284
|
+
* A path that climbs out of `cwd` with `..` stays absolute, so it stays
|
|
285
|
+
* outside.
|
|
286
|
+
*/
|
|
287
|
+
function relativeTo(path, cwd) {
|
|
288
|
+
const target = normalize(path);
|
|
289
|
+
const base = normalize(cwd);
|
|
290
|
+
if (!target.absolute || !base.absolute || target.escaped) return path;
|
|
291
|
+
if (base.segments.length > target.segments.length) return path;
|
|
292
|
+
if (!base.segments.every((s, i) => s === target.segments[i])) return path;
|
|
293
|
+
const rest = target.segments.slice(base.segments.length);
|
|
294
|
+
return rest.length === 0 ? "." : `./${rest.join("/")}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
275
297
|
* The host of a URL, or undefined when it is not a URL we can read.
|
|
276
298
|
*
|
|
277
299
|
* Unparseable input returns undefined, and callers treat that as "no host" —
|
|
@@ -496,4 +518,4 @@ function evaluate(pack, facts) {
|
|
|
496
518
|
}
|
|
497
519
|
|
|
498
520
|
//#endregion
|
|
499
|
-
export {
|
|
521
|
+
export { withinAny as a, relativeTo as i, PATH_ARGS as n, segments as o, hostOf as r, evaluate as t };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//#region ../kernel/src/frameworks.ts
|
|
2
|
+
const FRAMEWORKS = {
|
|
3
|
+
"owasp-llm-top-10-2026": {
|
|
4
|
+
name: "OWASP Top 10 for LLM Applications — 2026",
|
|
5
|
+
short: "OWASP LLM 2026",
|
|
6
|
+
controls: {
|
|
7
|
+
LLM01: {
|
|
8
|
+
name: "Prompt Injection",
|
|
9
|
+
summary: "Instructions in a prompt or in retrieved content alter what the model does."
|
|
10
|
+
},
|
|
11
|
+
LLM02: {
|
|
12
|
+
name: "Sensitive Information Disclosure",
|
|
13
|
+
summary: "Credentials, personal data or proprietary content reach the model or its output."
|
|
14
|
+
},
|
|
15
|
+
LLM03: {
|
|
16
|
+
name: "Excessive Agency",
|
|
17
|
+
summary: "The model can take actions, or holds permissions, beyond what its task needs."
|
|
18
|
+
},
|
|
19
|
+
LLM04: {
|
|
20
|
+
name: "Supply Chain",
|
|
21
|
+
summary: "A compromised model, dataset, plugin or dependency enters the application."
|
|
22
|
+
},
|
|
23
|
+
LLM05: {
|
|
24
|
+
name: "Data and Model Poisoning",
|
|
25
|
+
summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
|
|
26
|
+
},
|
|
27
|
+
LLM06: {
|
|
28
|
+
name: "Unbounded Consumption",
|
|
29
|
+
summary: "The application permits resource use without a limit, exhausting budget or service."
|
|
30
|
+
},
|
|
31
|
+
LLM07: {
|
|
32
|
+
name: "Misinformation",
|
|
33
|
+
summary: "The model produces false or misleading content that is acted on as if true."
|
|
34
|
+
},
|
|
35
|
+
LLM08: {
|
|
36
|
+
name: "Hidden Context Exposure",
|
|
37
|
+
summary: "System prompts, hidden instructions or internal context are revealed."
|
|
38
|
+
},
|
|
39
|
+
LLM09: {
|
|
40
|
+
name: "Vector and Embedding Weaknesses",
|
|
41
|
+
summary: "Retrieval stores are manipulated or leak across tenants and permissions."
|
|
42
|
+
},
|
|
43
|
+
LLM10: {
|
|
44
|
+
name: "Improper Output Handling",
|
|
45
|
+
summary: "Model output is passed to downstream systems without validation."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"owasp-agentic-top-10-2026": {
|
|
50
|
+
name: "OWASP Top 10 for Agentic Applications — 2026",
|
|
51
|
+
short: "OWASP Agentic 2026",
|
|
52
|
+
controls: {
|
|
53
|
+
ASI01: {
|
|
54
|
+
name: "Agent Goal Hijack",
|
|
55
|
+
summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
|
|
56
|
+
},
|
|
57
|
+
ASI02: {
|
|
58
|
+
name: "Tool Misuse and Exploitation",
|
|
59
|
+
summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
|
|
60
|
+
},
|
|
61
|
+
ASI03: {
|
|
62
|
+
name: "Identity and Privilege Abuse",
|
|
63
|
+
summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
|
|
64
|
+
},
|
|
65
|
+
ASI04: {
|
|
66
|
+
name: "Agentic Supply Chain",
|
|
67
|
+
summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
|
|
68
|
+
},
|
|
69
|
+
ASI05: {
|
|
70
|
+
name: "Unexpected Code Execution",
|
|
71
|
+
summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
|
|
72
|
+
},
|
|
73
|
+
ASI06: {
|
|
74
|
+
name: "Memory and Context Poisoning",
|
|
75
|
+
summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
|
|
76
|
+
},
|
|
77
|
+
ASI07: {
|
|
78
|
+
name: "Insecure Inter-Agent Communication",
|
|
79
|
+
summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
|
|
80
|
+
},
|
|
81
|
+
ASI08: {
|
|
82
|
+
name: "Cascading Failures",
|
|
83
|
+
summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
|
|
84
|
+
},
|
|
85
|
+
ASI09: {
|
|
86
|
+
name: "Human-Agent Trust Exploitation",
|
|
87
|
+
summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
|
|
88
|
+
},
|
|
89
|
+
ASI10: {
|
|
90
|
+
name: "Rogue Agents",
|
|
91
|
+
summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Every framework control a rule cites, resolved to names.
|
|
98
|
+
*
|
|
99
|
+
* A pack that parsed cites only ids the registry knows, so the lookups here
|
|
100
|
+
* cannot miss; the fallbacks exist for a `Rule` built by hand in code.
|
|
101
|
+
*/
|
|
102
|
+
function cite(rule) {
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const [framework, controls] of Object.entries(rule.frameworks ?? {})) {
|
|
105
|
+
const known = FRAMEWORKS[framework];
|
|
106
|
+
for (const control of controls) out.push({
|
|
107
|
+
framework,
|
|
108
|
+
frameworkName: known?.name ?? framework,
|
|
109
|
+
frameworkShort: known?.short ?? framework,
|
|
110
|
+
control,
|
|
111
|
+
controlName: known?.controls[control]?.name ?? control
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { cite as n, FRAMEWORKS as t };
|