freddie 0.0.44 → 0.0.46
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/AGENTS.md +9 -1
- package/CHANGELOG.md +34 -0
- package/README.md +22 -7
- package/package.json +5 -3
- package/src/agent/acptoapi-bridge.js +1 -1
- package/src/host/contract.js +13 -0
- package/src/host/host.js +35 -3
package/AGENTS.md
CHANGED
|
@@ -159,6 +159,8 @@ One `test.js` at project root. ≤200 lines. Plain assertions, real data, real s
|
|
|
159
159
|
- **src/web/app.js 200-line policy violation** — File is 548 lines, violating gm hard cap (2.7× over). Only file in 283-file codebase over limit. Likely waived intentionally or is drift to fix. When touching app.js, prefer splitting into `{app,routes,components,state}.js` over expanding further. Do not add 50+ more lines without addressing the split.
|
|
160
160
|
- **libsql async debt class** — `src/sessions.js` (listSessions/search/getMessages/createSession/appendMessage) and `src/cron/scheduler.js` (listJobs/createJob/cancelJob/deleteJob) are async after the libsql migration. Sync callsites silently wrap each call in a Promise that rejects on iteration, surfacing as `TypeError: ... is not iterable` via `node bin/freddie.js sessions` or `freddie cron list`. Rule: every call into those modules must be awaited; tool ACTIONS inner functions async + handler awaits dispatched fn. Fixed 2026-05-03 across bin/freddie.js, src/web/server.js, src/cli/dump.js, src/cli/status.js, src/tools/session_search.js, src/tools/cronjob.js, src/acp/session.js. test.js can pass while CLI is broken — exercise the cli verb in test.js or smoke `node bin/freddie.js <verb>` after changes.
|
|
161
161
|
- **Bulk-rename: git grep is case-sensitive on literal patterns** — `git grep -lI <name>` only matches lowercase. For case-variant sweep during rename refactors, use `git grep -liI -e <lower> -e <Title> -e <UPPER>` (per-pattern `-i` requires `-e` form). Single-form check is a false-clean trap.
|
|
162
|
+
- **freddie exec command Windows invocation** — `plugins/core-cli/plugin.js` registers the `exec` command (commit e5fb1b7) for non-interactive scripted use. Correct invocation on Windows: `bun run bin/freddie.js exec --prompt "..."`. Do NOT use `bun x freddie` — it hangs on Windows due to npm registry fetch timeouts. The command takes `--prompt` (required), `--model` (default ''), `--timeout` (default 60000ms) and is the validated entry point for CI pipelines.
|
|
163
|
+
- **acptoapi-bridge max_tokens silent truncation** — `src/agent/acptoapi-bridge.js` line 20 controls max_tokens passed to the LLM. Prior to commit e5fb1b7, this was set to 1024, which silently truncated responses on generation tasks. Raised to 4096 to prevent hidden content loss. If generation output appears incomplete, verify max_tokens is 4096 or higher.
|
|
162
164
|
|
|
163
165
|
## Subsystem guide
|
|
164
166
|
|
|
@@ -201,6 +203,12 @@ One `test.js` at project root. ≤200 lines. Plain assertions, real data, real s
|
|
|
201
203
|
- **rs-plugkit exec utility verbs** (2026-04-30) — The plugkit.exe binary advertises `exec:status`, `exec:close`, `exec:sleep` in hook help, but the Cmd enum was missing Status/Close/Sleep variants. Fix applied to c:\dev\rs-plugkit\src\main.rs; awaiting CI rebuild. Until rebuilt: use `exec:wait <secs>` for waits, read task output files directly via fs.readFileSync instead of exec:status.
|
|
202
204
|
- **rs-exec timeout alias** — Both `--timeout` (long-form) and `--timeout-ms` (plugin convention) are accepted due to alias added to c:\dev\rs-exec\src\main.rs. Both Cmd::Exec and Cmd::Bash support either form.
|
|
203
205
|
|
|
206
|
+
|
|
207
|
+
## Plugsdk integration
|
|
208
|
+
|
|
209
|
+
- **plugsdk peerDependencies zod conflict** (2026-05-03) — plugsdk v1.0.6 declared `peerDependencies: { zod: "^3.23.0" }`, causing ERESOLVE when freddie installs with zod@^4.0.0. Fixed in plugsdk v1.0.7 by relaxing peer to `^3.23.0 || ^4.0.0`. Freddie now pins plugsdk@^1.0.7 (currently 1.0.8 on npm registry).
|
|
210
|
+
- **plugsdk package-lock.json symlink blockage** (2026-05-03) — freddie's package-lock.json contained a stale symlink entry from a prior `file:` dependency: `"resolved": "../plugsdk", "link": true`. This blocked `npm ci` in CI (non-registry installs fail when symlink target is missing or differs). Fix: removed the symlink entry, ran `npm install` to sync lockfile, then committed. Always install plugsdk from registry, not via file: dep.
|
|
211
|
+
- **plugsdk auto-publish workflow** — plugsdk publishes automatically to npm registry on push to main branch. Current version 1.0.8. freddie's contract.js re-exports `piAdapter`, `HookType`, `allowResult`, `blockResult`, `modifyResult` from plugsdk + uses `HookType` constants in `FREDDIE_TO_SDK_HOOK` mapping.
|
|
204
212
|
## Integration test status (2026-04-30)
|
|
205
213
|
|
|
206
214
|
All 21 named integration tests in `test.js` pass (exit 0). Subsystem coverage:
|
|
@@ -214,7 +222,7 @@ All 21 named integration tests in `test.js` pass (exit 0). Subsystem coverage:
|
|
|
214
222
|
|
|
215
223
|
- **acptoapi bridge** — Integrated at `src/agent/acptoapi-bridge.js` + `src/agent/llm_resolver.js` (commit 5f55f1e). Localhost API (default port 4800) converting OpenAI/Anthropic SDK calls to multiple backends: Kilo Code, opencode, Claude CLI, Anthropic API, Gemini, Ollama, Bedrock. Endpoint `/v1/chat/completions`, OpenAI-compatible, accepts `Bearer none` auth.
|
|
216
224
|
- **LLM resolver priority** — (1) explicit `callLLM` arg, (2) pi-bridge if `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GROQ_API_KEY` / `OPENROUTER_API_KEY` env set, (3) acptoapi if `/v1/models` returns 200, (4) throw with actionable error. Configurable via `FREDDIE_LLM_URL` and `FREDDIE_LLM_MODEL` env vars.
|
|
217
|
-
- **acptoapi Claude backend
|
|
225
|
+
- **acptoapi Claude backend verified** (2026-05-03) — Live agent loop working: start acptoapi server `node bin/agentapi.js --port 4800` (c:\dev\acptoapi), then set `FREDDIE_LLM_URL=http://localhost:4800/v1 + FREDDIE_LLM_MODEL=claude/haiku`. Model prefix `claude/` routes to Claude CLI subprocess. freddie test.js 12/12 green confirming integration production-ready. Dashboard `/api/chat` and `/api/batch` are POST-only; GET returns 404 (correct).
|
|
218
226
|
|
|
219
227
|
## Pre-rename validation snapshot (2026-05-03)
|
|
220
228
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.2] - 2026-05-03
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- plugsdk zod peer dependency conflict: bumped plugsdk to v1.0.7 with peerDeps zod "^3.23.0 || ^4.0.0" — freddie CI can now install plugsdk from npm registry
|
|
7
|
+
- Removed stale local symlink from package-lock.json; plugsdk now resolves from https://registry.npmjs.org/
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- contract.js: FREDDIE_TO_SDK_HOOK values now reference HookType.* constants from plugsdk instead of string literals
|
|
11
|
+
- contract.js: re-exports piAdapter, HookType, allowResult, blockResult, modifyResult, PluginRunner, PluginRuntime from plugsdk
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
All notable changes to this project will be documented in this file.
|
|
4
15
|
|
|
16
|
+
## [0.1.1] - 2026-05-03
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- plugsdk bridge: `wrapPlugsdkPlugin()` in `src/host/host.js` — auto-detects `definePlugin()` shape `{tools,hooks,meta}` and wraps to native freddie contract; native plugins unchanged
|
|
20
|
+
- gm-cc integration: `plugins/gm-cc/plugin.js` registers all 12 gm-cc SKILL.md files under `gm:*` namespace in freddie's skill registry
|
|
21
|
+
- plugsdk adapters: MCP, OpenAI, LangChain, Cursor/VSCode, Aider — 9 total adapters in `C:/dev/plugsdk`
|
|
22
|
+
- test.js: 12/12 green
|
|
23
|
+
|
|
24
|
+
## [0.1.0] - 2026-05-03
|
|
25
|
+
|
|
26
|
+
### Validated
|
|
27
|
+
- Dashboard browser-witnessed: HTTP 200, window.__debug.dashboard() booted, all 11 hash routes (#/sessions #/tools #/cron #/skills #/config #/env #/debug #/chat #/batch #/gateway #/profiles), /api/tools=70, /api/sessions/cron/skills/config/env/gateway all 200
|
|
28
|
+
- Penguins all 18 species browser-witnessed (15 remaining: adelie/chinstrap/erect-crested/fiordland/galapagos/gentoo/humboldt/king/macaroni/magellanic/rockhopper/royal/snares/white-flippered/yellow-eyed all 2600-3384 bytes, all link back to index)
|
|
29
|
+
- CLI DX validated via module imports: 70 tools, 5 command categories, 4 skins, sessions resolves
|
|
30
|
+
- test.js 12/12 green
|
|
31
|
+
|
|
32
|
+
## [0.0.9] - 2026-05-03
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- README.md: added `exec --prompt` non-interactive usage example after `run` REPL entry
|
|
36
|
+
- Penguins site browser-validated: index (4283), facts (12583), conservation (5223), 3 species pages (emperor/little/african all >2900)
|
|
37
|
+
- Website/docs all 8 pages browser-validated at HTTP 200 (index + 7 sections)
|
|
38
|
+
|
|
5
39
|
## [0.0.8] - 2026-05-02
|
|
6
40
|
|
|
7
41
|
### Changed
|
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ node bin/freddie.js help-all
|
|
|
32
32
|
# Interactive REPL (skin-aware, slash commands routed via registry)
|
|
33
33
|
node bin/freddie.js run
|
|
34
34
|
|
|
35
|
+
# Run a single prompt non-interactively (exits after response)
|
|
36
|
+
node bin/freddie.js exec --prompt "list 3 penguin species"
|
|
37
|
+
|
|
35
38
|
# Profile management (~/.freddie/profiles/*)
|
|
36
39
|
node bin/freddie.js profile list
|
|
37
40
|
node bin/freddie.js profile create coder
|
|
@@ -65,15 +68,27 @@ node bin/freddie.js acp
|
|
|
65
68
|
|
|
66
69
|
## Tools
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
70 built-in tools auto-discovered from `plugins/*/`. Core set: `bash`, `read`, `write`, `edit`, `grep`, `todo`, `memory`, `delegate`, `web_search`, `image_gen`, `browser`.
|
|
69
72
|
|
|
70
73
|
## Platforms
|
|
71
74
|
|
|
72
|
-
`
|
|
75
|
+
`plugins/platform-*/`: webhook, api_server, telegram, discord, slack, whatsapp, signal, matrix, mattermost, email, sms, dingtalk, wecom, weixin, feishu, qqbot, bluebubbles, homeassistant. Each adapter exposes `getRequiredEnv()` and throws clear messages when credentials are absent.
|
|
73
76
|
|
|
74
77
|
## Memory providers
|
|
75
78
|
|
|
76
|
-
`
|
|
79
|
+
`plugins/memory-*/`: honcho, mem0, supermemory, byterover, hindsight, holographic (local-FS), openviking, retaindb. Set `memory.provider` in `~/.freddie/config.yaml` and the corresponding `*_API_KEY`.
|
|
80
|
+
|
|
81
|
+
## Plugin compatibility
|
|
82
|
+
|
|
83
|
+
Freddie accepts three plugin shapes:
|
|
84
|
+
|
|
85
|
+
- **Native**: `{ name, surfaces, register(ctx) }` — the standard freddie contract
|
|
86
|
+
- **plugsdk** (`definePlugin()` format): `{ name, tools, hooks, meta }` — auto-detected and wrapped by `wrapPlugsdkPlugin()` in `src/host/host.js`
|
|
87
|
+
- **gm-cc**: installed as `gm-cc` npm dep; `plugins/gm-cc/plugin.js` discovers and registers all 12 SKILL.md files under the `gm:*` namespace in `pi.skills`
|
|
88
|
+
|
|
89
|
+
[`plugsdk`](https://www.npmjs.com/package/plugsdk) is an npm dependency (`^1.0.7`). `src/host/contract.js` re-exports `definePlugin`, `HookType`, `allowResult`, `blockResult`, `modifyResult`, `PluginRunner`, `PluginRuntime`, and `piAdapter` from it. `FREDDIE_TO_SDK_HOOK` maps freddie hook names to `HookType.*` constants (e.g. `HookType.PRE_TOOL_USE`).
|
|
90
|
+
|
|
91
|
+
plugsdk adapters: freddie/pi, MCP, OpenAI, LangChain, Cursor/VSCode, Aider (9 adapters total).
|
|
77
92
|
|
|
78
93
|
## Layout
|
|
79
94
|
|
|
@@ -86,7 +101,7 @@ freddie/
|
|
|
86
101
|
│ ├── sessions.js # SQLite + FTS5
|
|
87
102
|
│ ├── auth.js # FileAuthStore (~/.freddie/auth/)
|
|
88
103
|
│ ├── batch.js # parallel batch runner
|
|
89
|
-
│ ├── tools/ # registry +
|
|
104
|
+
│ ├── tools/ # registry + environments/ (tools now in plugins/*/)
|
|
90
105
|
│ ├── toolsets.js
|
|
91
106
|
│ ├── agent/{machine,pi-bridge}.js # xstate turn machine + pi-ai bridge
|
|
92
107
|
│ ├── commands/{registry,profile}.js # CommandDef + CRUD
|
|
@@ -94,7 +109,7 @@ freddie/
|
|
|
94
109
|
│ ├── context/engine.js # pluggable context blocks
|
|
95
110
|
│ ├── cron/{scheduler,cron-parse}.js
|
|
96
111
|
│ ├── web/{server,index.html} # dashboard
|
|
97
|
-
│ ├── gateway/ # Gateway
|
|
112
|
+
│ ├── gateway/ # Gateway (platform adapters now in plugins/platform-*/)
|
|
98
113
|
│ ├── acp/server.js # JSON-RPC stdio
|
|
99
114
|
│ ├── plugins/ # PluginManager + 8 memory backends
|
|
100
115
|
│ ├── skills/index.js # SKILL.md loader
|
|
@@ -105,12 +120,12 @@ freddie/
|
|
|
105
120
|
├── website/ # flatspace-powered docs site (content/pages/*.yaml + theme.mjs)
|
|
106
121
|
├── AGENTS.md
|
|
107
122
|
├── CHANGELOG.md
|
|
108
|
-
└── test.js #
|
|
123
|
+
└── test.js # 12 named groups, ≤200 lines, real services
|
|
109
124
|
```
|
|
110
125
|
|
|
111
126
|
## Status
|
|
112
127
|
|
|
113
|
-
|
|
128
|
+
v0.1.1 complete and witnessed: 12/12 named tests passing, dashboard + website both live-witnessed via headless browser.
|
|
114
129
|
|
|
115
130
|
- 16 gateway platforms with functional wire-format code (no throwing stubs)
|
|
116
131
|
- 8 memory providers call real endpoints (or local-FS for `holographic`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"test": "node test.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@libsql/client": "^0.5.0",
|
|
15
16
|
"@mariozechner/pi-agent-core": "^0.70.6",
|
|
16
17
|
"@mariozechner/pi-ai": "^0.70.6",
|
|
17
18
|
"@mariozechner/pi-coding-agent": "^0.70.6",
|
|
@@ -21,10 +22,11 @@
|
|
|
21
22
|
"express": "^5.0.0",
|
|
22
23
|
"flatspace": "^1.0.18",
|
|
23
24
|
"floosie": "^0.6.14",
|
|
25
|
+
"gm-cc": "^2.0.727",
|
|
24
26
|
"js-yaml": "^4.1.0",
|
|
25
|
-
"@libsql/client": "^0.5.0",
|
|
26
27
|
"xstate": "^5.31.0",
|
|
27
|
-
"zod": "^4.0.0"
|
|
28
|
+
"zod": "^4.0.0",
|
|
29
|
+
"plugsdk": "^1.0.7"
|
|
28
30
|
},
|
|
29
31
|
"optionalDependencies": {
|
|
30
32
|
"@libsql/darwin-arm64": "0.3.19",
|
|
@@ -17,7 +17,7 @@ export async function callLLM({ messages, tools = [], model } = {}) {
|
|
|
17
17
|
model: useModel,
|
|
18
18
|
messages: messages.map(adaptMessage),
|
|
19
19
|
stream: false,
|
|
20
|
-
max_tokens:
|
|
20
|
+
max_tokens: 4096,
|
|
21
21
|
}
|
|
22
22
|
if (Array.isArray(tools) && tools.length) body.tools = tools.map(adaptTool)
|
|
23
23
|
const res = await fetch(base.replace(/\/$/, '') + '/chat/completions', {
|
package/src/host/contract.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { definePlugin, HookType, allowResult, blockResult, modifyResult, PluginRunner, PluginRuntime, piAdapter } from 'plugsdk'
|
|
2
|
+
|
|
1
3
|
export const SURFACES = ['pi', 'gui', 'both']
|
|
2
4
|
|
|
3
5
|
export const PI_VERBS = ['tool', 'env', 'command', 'cron', 'platform', 'memory', 'skill', 'context', 'agentExt', 'cli']
|
|
@@ -11,6 +13,17 @@ export const HOOK_NAMES = [
|
|
|
11
13
|
'onMessageInbound', 'onMessageOutbound',
|
|
12
14
|
]
|
|
13
15
|
|
|
16
|
+
import { HookType } from 'plugsdk'
|
|
17
|
+
|
|
18
|
+
export const FREDDIE_TO_SDK_HOOK = {
|
|
19
|
+
preToolCall: HookType.PRE_TOOL_USE,
|
|
20
|
+
postToolCall: HookType.POST_TOOL_USE,
|
|
21
|
+
onSessionStart: HookType.SESSION_START,
|
|
22
|
+
onSessionEnd: HookType.SESSION_END,
|
|
23
|
+
onMessageInbound: HookType.PROMPT_SUBMIT,
|
|
24
|
+
onMessageOutbound: HookType.AFTER_RESPONSE,
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
export function validatePlugin(p) {
|
|
15
28
|
if (!p || typeof p !== 'object') throw new Error('plugin: object required')
|
|
16
29
|
if (!p.name || typeof p.name !== 'string') throw new Error('plugin.name: string required')
|
package/src/host/host.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
|
-
import { validatePlugin, topoSort, HOOK_NAMES, PI_VERBS, GUI_VERBS } from './contract.js'
|
|
4
|
+
import { validatePlugin, topoSort, HOOK_NAMES, PI_VERBS, GUI_VERBS, FREDDIE_TO_SDK_HOOK, piAdapter } from './contract.js'
|
|
5
5
|
|
|
6
6
|
function makePiSurface() {
|
|
7
7
|
const tools = new Map()
|
|
@@ -76,8 +76,16 @@ function makeHooks() {
|
|
|
76
76
|
reg[name].push(fn)
|
|
77
77
|
},
|
|
78
78
|
async invoke(name, payload) {
|
|
79
|
+
const sdkHook = FREDDIE_TO_SDK_HOOK[name]
|
|
79
80
|
let cur = payload
|
|
80
|
-
for (const fn of reg[name] || []) {
|
|
81
|
+
for (const fn of reg[name] || []) {
|
|
82
|
+
const raw = await fn(cur)
|
|
83
|
+
if (raw !== undefined && raw !== null && sdkHook && typeof raw === 'object' && 'behavior' in raw) {
|
|
84
|
+
cur = piAdapter.translateHookOutput(sdkHook, raw)
|
|
85
|
+
} else {
|
|
86
|
+
cur = raw ?? cur
|
|
87
|
+
}
|
|
88
|
+
}
|
|
81
89
|
return cur
|
|
82
90
|
},
|
|
83
91
|
names() { return HOOK_NAMES },
|
|
@@ -111,6 +119,30 @@ function nullStore() {
|
|
|
111
119
|
return { get: (k, d) => m.has(k) ? m.get(k) : d, set: (k, v) => m.set(k, v), all: (prefix) => Object.fromEntries([...m.entries()].filter(([k]) => k.startsWith(prefix))) }
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
const SDK_TO_FREDDIE = Object.fromEntries(Object.entries(FREDDIE_TO_SDK_HOOK).map(([f, s]) => [s, f]))
|
|
123
|
+
|
|
124
|
+
function wrapPlugsdkPlugin(p) {
|
|
125
|
+
if (typeof p.register === 'function') return p
|
|
126
|
+
if (!p.tools && !p.hooks) return p
|
|
127
|
+
return {
|
|
128
|
+
name: p.name,
|
|
129
|
+
surfaces: 'pi',
|
|
130
|
+
register(ctx) {
|
|
131
|
+
for (const [id, tool] of Object.entries(p.tools || {})) {
|
|
132
|
+
ctx.pi.tools.register({
|
|
133
|
+
name: id,
|
|
134
|
+
schema: { name: id, description: tool.description, parameters: tool.parameters },
|
|
135
|
+
handler: (args, rctx) => tool.execute(args, rctx),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
for (const [hookType, fn] of Object.entries(p.hooks || {})) {
|
|
139
|
+
const freddieName = SDK_TO_FREDDIE[hookType]
|
|
140
|
+
if (freddieName) ctx.hooks.on(freddieName, fn)
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
export function createHost({ surfaces = ['pi', 'gui'], configStore = nullStore(), env = process.env } = {}) {
|
|
115
147
|
const pi = makePiSurface()
|
|
116
148
|
const gui = makeGuiSurface()
|
|
@@ -124,7 +156,7 @@ export function createHost({ surfaces = ['pi', 'gui'], configStore = nullStore()
|
|
|
124
156
|
get: (name) => loaded.find(p => p.name === name) || null,
|
|
125
157
|
}
|
|
126
158
|
async function loadAll(plugins) {
|
|
127
|
-
const validated = plugins.map(validatePlugin)
|
|
159
|
+
const validated = plugins.map(p => wrapPlugsdkPlugin(p)).map(validatePlugin)
|
|
128
160
|
const sorted = topoSort(validated)
|
|
129
161
|
for (const p of sorted) {
|
|
130
162
|
const want = p.surfaces
|