@plurnk/plurnk-service 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SPEC.md +125 -74
- package/bin/plurnk-service.ts +132 -0
- package/dist/Paths.d.ts +8 -0
- package/dist/Paths.d.ts.map +1 -0
- package/dist/Paths.js +47 -0
- package/dist/Paths.js.map +1 -0
- package/dist/content/index.d.ts +9 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +10 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/line-marker.d.ts +26 -0
- package/dist/content/line-marker.d.ts.map +1 -0
- package/dist/content/line-marker.js +323 -0
- package/dist/content/line-marker.js.map +1 -0
- package/dist/content/matcher.d.ts +15 -0
- package/dist/content/matcher.d.ts.map +1 -0
- package/dist/content/matcher.js +112 -0
- package/dist/content/matcher.js.map +1 -0
- package/dist/content/mimetype-binary.d.ts +9 -0
- package/dist/content/mimetype-binary.d.ts.map +1 -0
- package/dist/content/mimetype-binary.js +86 -0
- package/dist/content/mimetype-binary.js.map +1 -0
- package/dist/content/path-mimetype.d.ts +6 -0
- package/dist/content/path-mimetype.d.ts.map +1 -0
- package/dist/content/path-mimetype.js +49 -0
- package/dist/content/path-mimetype.js.map +1 -0
- package/dist/content/read-resolve.d.ts +20 -0
- package/dist/content/read-resolve.d.ts.map +1 -0
- package/dist/content/read-resolve.js +60 -0
- package/dist/content/read-resolve.js.map +1 -0
- package/dist/core/ChannelWrite.d.ts +33 -30
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +43 -41
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/Engine.d.ts +14 -10
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +272 -94
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/EnvFlags.d.ts +6 -3
- package/dist/core/EnvFlags.d.ts.map +1 -1
- package/dist/core/EnvFlags.js +62 -60
- package/dist/core/EnvFlags.js.map +1 -1
- package/dist/core/PluginLoader.d.ts +6 -3
- package/dist/core/PluginLoader.d.ts.map +1 -1
- package/dist/core/PluginLoader.js +77 -73
- package/dist/core/PluginLoader.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +4 -2
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +23 -22
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +1 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +3 -3
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/git-membership.d.ts +8 -0
- package/dist/core/git-membership.d.ts.map +1 -0
- package/dist/core/git-membership.js +125 -0
- package/dist/core/git-membership.js.map +1 -0
- package/dist/core/packet-wire.d.ts +47 -6
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +376 -312
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/resolveForLoop.d.ts +14 -0
- package/dist/core/resolveForLoop.d.ts.map +1 -0
- package/dist/core/resolveForLoop.js +34 -0
- package/dist/core/resolveForLoop.js.map +1 -0
- package/dist/core/results.d.ts +40 -0
- package/dist/core/results.d.ts.map +1 -0
- package/dist/core/results.js +52 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/scheme-types.d.ts +6 -3
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js +4 -4
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +17 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +1 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -43
- package/dist/index.js.map +1 -1
- package/dist/schemes/Exec.d.ts +1 -0
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +101 -94
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/File.d.ts +0 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +32 -66
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Known.d.ts.map +1 -1
- package/dist/schemes/Known.js +13 -13
- package/dist/schemes/Known.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +8 -52
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Plurnk.d.ts.map +1 -1
- package/dist/schemes/Plurnk.js +13 -13
- package/dist/schemes/Plurnk.js.map +1 -1
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +13 -13
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Unknown.d.ts.map +1 -1
- package/dist/schemes/Unknown.js +13 -13
- package/dist/schemes/Unknown.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +5 -3
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +55 -50
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-find.d.ts +10 -3
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +99 -77
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts +6 -0
- package/dist/schemes/_entry-manifest.d.ts.map +1 -0
- package/dist/schemes/_entry-manifest.js +45 -0
- package/dist/schemes/_entry-manifest.js.map +1 -0
- package/dist/schemes/_entry-ops.d.ts +7 -4
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +198 -316
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-send.d.ts +4 -1
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +57 -55
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/server/ClientConnection.js +3 -3
- package/dist/server/ClientConnection.js.map +1 -1
- package/dist/server/Daemon.d.ts +5 -5
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +221 -176
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/clientTurn.d.ts +4 -1
- package/dist/server/clientTurn.d.ts.map +1 -1
- package/dist/server/clientTurn.js +19 -17
- package/dist/server/clientTurn.js.map +1 -1
- package/dist/server/dsl.d.ts +19 -16
- package/dist/server/dsl.d.ts.map +1 -1
- package/dist/server/dsl.js +127 -105
- package/dist/server/dsl.js.map +1 -1
- package/dist/server/envelope.d.ts +22 -19
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +116 -102
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/logEntry.d.ts +4 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +41 -39
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/methods/_dispatchAsClient.d.ts +3 -1
- package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
- package/dist/server/methods/_dispatchAsClient.js +31 -29
- package/dist/server/methods/_dispatchAsClient.js.map +1 -1
- package/dist/server/methods/discover.d.ts +3 -1
- package/dist/server/methods/discover.d.ts.map +1 -1
- package/dist/server/methods/discover.js +8 -6
- package/dist/server/methods/discover.js.map +1 -1
- package/dist/server/methods/entry_read.d.ts +4 -1
- package/dist/server/methods/entry_read.d.ts.map +1 -1
- package/dist/server/methods/entry_read.js +54 -52
- package/dist/server/methods/entry_read.js.map +1 -1
- package/dist/server/methods/log_read.d.ts +4 -1
- package/dist/server/methods/log_read.d.ts.map +1 -1
- package/dist/server/methods/log_read.js +38 -36
- package/dist/server/methods/log_read.js.map +1 -1
- package/dist/server/methods/loop_cancel.d.ts +3 -1
- package/dist/server/methods/loop_cancel.d.ts.map +1 -1
- package/dist/server/methods/loop_cancel.js +19 -17
- package/dist/server/methods/loop_cancel.js.map +1 -1
- package/dist/server/methods/loop_resolve.d.ts +3 -1
- package/dist/server/methods/loop_resolve.d.ts.map +1 -1
- package/dist/server/methods/loop_resolve.js +42 -40
- package/dist/server/methods/loop_resolve.js.map +1 -1
- package/dist/server/methods/loop_run.d.ts +3 -1
- package/dist/server/methods/loop_run.d.ts.map +1 -1
- package/dist/server/methods/loop_run.js +104 -102
- package/dist/server/methods/loop_run.js.map +1 -1
- package/dist/server/methods/op_copy.d.ts +3 -1
- package/dist/server/methods/op_copy.d.ts.map +1 -1
- package/dist/server/methods/op_copy.js +25 -23
- package/dist/server/methods/op_copy.js.map +1 -1
- package/dist/server/methods/op_dispatch.d.ts +3 -1
- package/dist/server/methods/op_dispatch.d.ts.map +1 -1
- package/dist/server/methods/op_dispatch.js +18 -16
- package/dist/server/methods/op_dispatch.js.map +1 -1
- package/dist/server/methods/op_edit.d.ts +3 -1
- package/dist/server/methods/op_edit.d.ts.map +1 -1
- package/dist/server/methods/op_edit.js +23 -21
- package/dist/server/methods/op_edit.js.map +1 -1
- package/dist/server/methods/op_exec.d.ts +3 -1
- package/dist/server/methods/op_exec.d.ts.map +1 -1
- package/dist/server/methods/op_exec.js +20 -18
- package/dist/server/methods/op_exec.js.map +1 -1
- package/dist/server/methods/op_find.d.ts +3 -1
- package/dist/server/methods/op_find.d.ts.map +1 -1
- package/dist/server/methods/op_find.js +23 -21
- package/dist/server/methods/op_find.js.map +1 -1
- package/dist/server/methods/op_hide.d.ts +3 -1
- package/dist/server/methods/op_hide.d.ts.map +1 -1
- package/dist/server/methods/op_hide.js +23 -21
- package/dist/server/methods/op_hide.js.map +1 -1
- package/dist/server/methods/op_move.d.ts +3 -1
- package/dist/server/methods/op_move.d.ts.map +1 -1
- package/dist/server/methods/op_move.js +23 -21
- package/dist/server/methods/op_move.js.map +1 -1
- package/dist/server/methods/op_parse.d.ts +3 -1
- package/dist/server/methods/op_parse.d.ts.map +1 -1
- package/dist/server/methods/op_parse.js +24 -22
- package/dist/server/methods/op_parse.js.map +1 -1
- package/dist/server/methods/op_read.d.ts +3 -1
- package/dist/server/methods/op_read.d.ts.map +1 -1
- package/dist/server/methods/op_read.js +23 -21
- package/dist/server/methods/op_read.js.map +1 -1
- package/dist/server/methods/op_send.d.ts +3 -1
- package/dist/server/methods/op_send.d.ts.map +1 -1
- package/dist/server/methods/op_send.js +22 -20
- package/dist/server/methods/op_send.js.map +1 -1
- package/dist/server/methods/op_show.d.ts +3 -1
- package/dist/server/methods/op_show.d.ts.map +1 -1
- package/dist/server/methods/op_show.js +23 -21
- package/dist/server/methods/op_show.js.map +1 -1
- package/dist/server/methods/ping.d.ts +3 -1
- package/dist/server/methods/ping.d.ts.map +1 -1
- package/dist/server/methods/ping.js +8 -6
- package/dist/server/methods/ping.js.map +1 -1
- package/dist/server/methods/providers_list.d.ts +3 -1
- package/dist/server/methods/providers_list.d.ts.map +1 -1
- package/dist/server/methods/providers_list.js +19 -17
- package/dist/server/methods/providers_list.js.map +1 -1
- package/dist/server/methods/session_attach.d.ts +3 -1
- package/dist/server/methods/session_attach.d.ts.map +1 -1
- package/dist/server/methods/session_attach.js +43 -41
- package/dist/server/methods/session_attach.js.map +1 -1
- package/dist/server/methods/session_create.d.ts +3 -1
- package/dist/server/methods/session_create.d.ts.map +1 -1
- package/dist/server/methods/session_create.js +51 -49
- package/dist/server/methods/session_create.js.map +1 -1
- package/dist/server/methods/session_list.d.ts +3 -1
- package/dist/server/methods/session_list.d.ts.map +1 -1
- package/dist/server/methods/session_list.js +9 -7
- package/dist/server/methods/session_list.js.map +1 -1
- package/dist/server/methods/session_runs.d.ts +3 -1
- package/dist/server/methods/session_runs.d.ts.map +1 -1
- package/dist/server/methods/session_runs.js +19 -17
- package/dist/server/methods/session_runs.js.map +1 -1
- package/dist/server/methods/session_set_persona.d.ts +3 -1
- package/dist/server/methods/session_set_persona.d.ts.map +1 -1
- package/dist/server/methods/session_set_persona.js +28 -26
- package/dist/server/methods/session_set_persona.js.map +1 -1
- package/dist/server/methods/session_set_root.d.ts +3 -1
- package/dist/server/methods/session_set_root.d.ts.map +1 -1
- package/dist/server/methods/session_set_root.js +31 -29
- package/dist/server/methods/session_set_root.js.map +1 -1
- package/dist/server/yolo.d.ts +3 -1
- package/dist/server/yolo.d.ts.map +1 -1
- package/dist/server/yolo.js +15 -13
- package/dist/server/yolo.js.map +1 -1
- package/package.json +77 -32
- package/bin/plurnk-service.js +0 -112
- /package/migrations/{001_schema.sql → 0000-00-00.01_schema.sql} +0 -0
package/SPEC.md
CHANGED
|
@@ -90,6 +90,27 @@ Three independent axes on entries and channels. Confusion across them is a recur
|
|
|
90
90
|
|
|
91
91
|
## §1 Architecture
|
|
92
92
|
|
|
93
|
+
### §1.1 Ecosystem
|
|
94
|
+
|
|
95
|
+
The plurnk project is a modular monorepo-of-repos in the `@plurnk/*` npm namespace. Each repo has one published package and one agent who owns it; cross-repo coordination happens through issues, not shared code. This service sits in the middle of that ecosystem and is its **runtime substrate** — the daemon other repos plug into.
|
|
96
|
+
|
|
97
|
+
Dependency direction (from root to leaf):
|
|
98
|
+
|
|
99
|
+
- **`plurnk-grammar`** — root. Owns the JSON-Schema contracts (Packet, TelemetryEvent, AST shapes), the ANTLR parser that turns model output into `PlurnkStatement[]`, and `PlurnkParseError` with its `toTelemetryEvent()` helper. Nothing in the ecosystem can speak the DSL without it; everything else pins it exactly.
|
|
100
|
+
- **Framework siblings** consume grammar and define their own author-facing contracts:
|
|
101
|
+
- `plurnk-providers` — Provider/Alias types, `parseAliasesFromEnv`, `resolveActiveAlias`, `Mock`, `ProviderUsage` (currency-aware, includes `reasoning`). Vendor-specific implementations are children: `plurnk-providers-openai`, `-google`, `-ollama`, `-openrouter`, `-cloudflare`, `-xai`.
|
|
102
|
+
- `plurnk-mimetypes` — handler base classes, discovery, fitting algorithm, matcher dispatch. Handler children are per-mimetype: `plurnk-mimetypes-text-{python,typescript,markdown,html,csv,plain}`, `plurnk-mimetypes-application-{json,yaml,toml,pdf}`, …
|
|
103
|
+
- `plurnk-schemes` — scheme-author types (`SchemeManifest`, `WriterTier`, `LoopFlags`), result-shape contracts (`EntryResult` / `ProposalResult` / `PassthroughResult`), slicing primitives, matcher helpers, `schemeError(...)` constructor. Future scheme children: `plurnk-schemes-http`, `plurnk-schemes-git`, …
|
|
104
|
+
- `plurnk-execs` — `BaseExecutor`, `SubprocessExecutor`, runtime resolver, discovery. Children declare runtimes: `plurnk-execs-sh`, future `plurnk-execs-search`, `plurnk-execs-node`, …
|
|
105
|
+
- **`plurnk-service`** (this repo) — consumes all of the above. Implements the engine, dispatches ops through scheme handlers, hosts the in-tree set of schemes (`plurnk`, `log`, `exec`, `known`, `unknown`, `skill`, `file`), discovers installed mimetype handlers + provider vendors + executor siblings at boot, hosts the daemon (`bin/plurnk-service.js` over WebSocket + JSON-RPC), and projects packets to the wire per `Packet.json`. Most of the substantive runtime work lives here.
|
|
106
|
+
- **`plurnk`** (client) — terminal UI consuming the daemon's RPC surface. Renders `telemetry/event` notifications, subscribes to log/stream/proposal events. No engine logic of its own.
|
|
107
|
+
|
|
108
|
+
The grammar is the contract. The frameworks consume the contract and add author-facing surfaces. The service consumes the frameworks and runs the engine. The client consumes the service and renders to humans. Each tier is its own published package; each tier's evolution happens in its own repo.
|
|
109
|
+
|
|
110
|
+
**This service's central role:** sole consumer of every author-facing framework contract (one set of integrations across the ecosystem), sole producer of the engine's runtime behavior (one canonical implementation of dispatch, log, packet wire), and sole orchestrator of cross-scheme operations (COPY/MOVE flow through engine-mediated `readEntry` / `writeEntry` / `deleteEntry`, never scheme-to-scheme). Most cross-repo coordination flows through us — we file the consumer-need issues at upstream repos, adopt their decisions, document the surface in SPEC.
|
|
111
|
+
|
|
112
|
+
### §1.2 In-process architecture
|
|
113
|
+
|
|
93
114
|
Engine library + admin CLI + daemon. Four plug points:
|
|
94
115
|
|
|
95
116
|
- **Providers** (§2) — LLM transports. Engine sends a turn's messages, receives raw content + usage; engine parses the content into `PlurnkStatement[]`.
|
|
@@ -113,23 +134,23 @@ Author-facing contract: [plurnk-providers#1](https://github.com/plurnk/plurnk-pr
|
|
|
113
134
|
|
|
114
135
|
Three entry points:
|
|
115
136
|
|
|
116
|
-
- `provider.generate({messages, signal})` — once per turn; returns `{ assistant: { content, reasoning, usage, finishReason, model }, assistantRaw }`. **Engine parses `assistant.content`** into `PlurnkStatement[]` via `@plurnk/plurnk-grammar`.
|
|
117
|
-
- `provider.countTokens(text)` — synchronous, called at write-time (§14.2) and render-time. Non-negative integer.
|
|
118
|
-
- `provider.costFor(usage)` — once per completed turn; pico-USD. Engine writes to `turns.usage_cost_pico`; triggers cascade to `runs.cost_pico` / `sessions.cost_pico`.
|
|
137
|
+
- `provider.generate({messages, signal})` — once per turn; returns `{ assistant: { content, reasoning, usage, finishReason, model }, assistantRaw }`. **Engine parses `assistant.content`** into `PlurnkStatement[]` via `@plurnk/plurnk-grammar`. {§2.1-generate}
|
|
138
|
+
- `provider.countTokens(text)` — synchronous, called at write-time (§14.2) and render-time. Non-negative integer. {§2.1-counttokens}
|
|
139
|
+
- `provider.costFor(usage)` — once per completed turn; pico-USD. Engine writes to `turns.usage_cost_pico`; triggers cascade to `runs.cost_pico` / `sessions.cost_pico`. {§2.1-costfor}
|
|
119
140
|
|
|
120
|
-
Plus immutable identity: `provider.contextSize` (token total, or `null` → "no budget info") and `provider.model
|
|
141
|
+
Plus immutable identity: `provider.contextSize` (token total, or `null` → "no budget info"), read by the budget {§2.1-identity}; and `provider.model` — the instance identity the deferred model-switch recompute compares (§14.2-hot-switch-recompute), exposed but not yet consumed here.
|
|
121
142
|
|
|
122
143
|
### §2.2 Engine → provider guarantees
|
|
123
144
|
|
|
124
145
|
- `messages` is a complete prompt (`system_definition`, `persona`, `index`, `log`, `prompt`, `telemetry`, `system_requirements` pre-assembled). Provider does not reorder.
|
|
125
|
-
- `signal` is wired to the run's AbortController.
|
|
126
|
-
- `generate` is single-call per turn. No parallel calls on the same instance.
|
|
127
|
-
- `assistantRaw` is opaque to the engine (forensics-only).
|
|
146
|
+
- `signal` is wired to the run's AbortController. {§2.2-signal-wired}
|
|
147
|
+
- `generate` is single-call per turn. No parallel calls on the same instance. {§2.2-single-call}
|
|
148
|
+
- `assistantRaw` is opaque to the engine (forensics-only). {§2.2-assistantraw-opaque}
|
|
128
149
|
- `countTokens` is cheap by contract; engine calls frequently.
|
|
129
150
|
|
|
130
151
|
### §2.3 Provider instantiation
|
|
131
152
|
|
|
132
|
-
Model alias parsing (`parseAliasesFromEnv` / `resolveActiveAlias`) lives in [`@plurnk/plurnk-providers`](https://github.com/plurnk/plurnk-providers). Dynamic provider instantiation (`instantiateProvider` / `loadActiveProvider`) lives in `src/core/ProviderInstantiate.ts` here — `import()` resolves package specifiers relative to the calling module, so the dynamic-import path stays in the consumer where the `@plurnk/plurnk-providers-<vendor>` packages are installed.
|
|
153
|
+
Model alias parsing (`parseAliasesFromEnv` / `resolveActiveAlias`) lives in [`@plurnk/plurnk-providers`](https://github.com/plurnk/plurnk-providers). {§2.3-alias-resolution} Dynamic provider instantiation (`instantiateProvider` / `loadActiveProvider`) lives in `src/core/ProviderInstantiate.ts` here — `import()` resolves package specifiers relative to the calling module, so the dynamic-import path stays in the consumer where the `@plurnk/plurnk-providers-<vendor>` packages are installed.
|
|
133
154
|
|
|
134
155
|
```
|
|
135
156
|
PLURNK_MODEL_gemma=openai/macher.gguf
|
|
@@ -139,9 +160,9 @@ PLURNK_MODEL=gemma
|
|
|
139
160
|
|
|
140
161
|
First path segment = provider plugin; rest = provider's own model id.
|
|
141
162
|
|
|
142
|
-
### §2.4
|
|
163
|
+
### §2.4 Mock provider (sibling fixture)
|
|
143
164
|
|
|
144
|
-
`Mock` (exported from `@plurnk/plurnk-providers`) — intg fixture + reference implementation. `{ contextSize, responses }` constructor; `generate` shifts from the queue. `MockResponse.assistant.ops?: PlurnkStatement[]` is a pre-parsed escape hatch the engine consumes directly when present; production providers don't expose this.
|
|
165
|
+
`Mock` (exported from `@plurnk/plurnk-providers`) — intg fixture + reference implementation. `{ contextSize, responses }` constructor; `generate` shifts from the queue. `MockResponse.assistant.ops?: PlurnkStatement[]` is a pre-parsed escape hatch the engine consumes directly when present; production providers don't expose this. {§2.4-mock-fixture}
|
|
145
166
|
|
|
146
167
|
---
|
|
147
168
|
|
|
@@ -151,15 +172,15 @@ Author-facing contract: [plurnk-schemes#1](https://github.com/plurnk/plurnk-sche
|
|
|
151
172
|
|
|
152
173
|
### §3.1 Manifest
|
|
153
174
|
|
|
154
|
-
Per author contract. Each scheme declares a `static manifest: SchemeManifest` with `name`, `channels`, `defaultChannel`, `category`, `scope`, `writableBy`, `volatile`, `modelVisible`, optional `flags`. Identity match enforced at plugin load: `manifest.name` must equal `package.json#plurnk.name`.
|
|
175
|
+
Per author contract. Each scheme declares a `static manifest: SchemeManifest` with `name`, `channels`, `defaultChannel`, `category`, `scope`, `writableBy`, `volatile`, `modelVisible`, optional `flags`. {§3.1-manifest} Identity match enforced at plugin load: `manifest.name` must equal `package.json#plurnk.name`.
|
|
155
176
|
|
|
156
177
|
### §3.2 CRUD primitives
|
|
157
178
|
|
|
158
|
-
Per author contract (`readEntry` / `writeEntry` / `deleteEntry`). Engine drives cross-scheme COPY/MOVE/SEND[410] through these. Each method is one SQL transaction; engine owns the outer transaction for orchestrations.
|
|
179
|
+
Per author contract (`readEntry` / `writeEntry` / `deleteEntry`). Engine drives cross-scheme COPY/MOVE/SEND[410] through these — the orchestration and its 404/409/415 semantics are anchored under §6.4/§6.5. Each method is one SQL transaction; engine owns the outer transaction for orchestrations.
|
|
159
180
|
|
|
160
181
|
### §3.3 Op methods
|
|
161
182
|
|
|
162
|
-
Per author contract (`edit`/`read`/`show`/`hide`/`find`/`send`/`exec?`). Engine dispatches by `PlurnkStatement.op`. COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives.
|
|
183
|
+
Per author contract (`edit`/`read`/`show`/`hide`/`find`/`send`/`exec?`). Engine dispatches by `PlurnkStatement.op`. {§3.3-op-dispatch} COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives (§6.4/§6.5).
|
|
163
184
|
|
|
164
185
|
### §3.4 Cross-scheme orchestration
|
|
165
186
|
|
|
@@ -179,17 +200,17 @@ move(source_path, dest_path, signal_tags, ctx):
|
|
|
179
200
|
src_scheme.deleteEntry(source_pathname, ctx)
|
|
180
201
|
```
|
|
181
202
|
|
|
182
|
-
Same- and cross-scheme operations share the orchestrator. Same-scheme COPY is not a special case.
|
|
203
|
+
Same- and cross-scheme operations share the orchestrator. Same-scheme COPY is not a special case. Orchestration behavior — 404/409/415, `move` = `copy` + `deleteEntry` — is anchored under §6.4/§6.5.
|
|
183
204
|
|
|
184
205
|
### §3.5 SEND dispatch (status-code-as-verb)
|
|
185
206
|
|
|
186
207
|
Directed SEND (non-null path) routes to scheme's `send`. Status = intent:
|
|
187
208
|
|
|
188
209
|
- `SEND[200](path)` — write body into resource (WS message, exec stdin).
|
|
189
|
-
- `SEND[410](path)` — delete the resource. {§3.5-410-deletes-resource}
|
|
190
|
-
- `SEND[410](path#fragment)` — delete the named channel only. {§3.5-410-fragment-channel-delete}
|
|
191
210
|
- `SEND[499](path)` — cancel active subscription (§7).
|
|
192
211
|
|
|
212
|
+
`SEND[410](path[#fragment])` also deletes the target entry/channel — an implemented side-effect, NOT taught to the model and with no live/demo surface. The model-facing delete idiom is MOVE to `/dev/null` (§6.5).
|
|
213
|
+
|
|
193
214
|
Other status codes return 501 from entry-bearing schemes by default. {§3.5-entry-schemes-501-on-non-410}
|
|
194
215
|
|
|
195
216
|
Null-path SEND is broadcast (§6.7), engine-handled.
|
|
@@ -219,11 +240,11 @@ Engine → scheme guarantees:
|
|
|
219
240
|
|
|
220
241
|
- `ctx` is fresh per call. No mutation across calls.
|
|
221
242
|
- `ctx.writer` reflects the actual writer at this dispatch.
|
|
222
|
-
- `manifest.writableBy` checked BEFORE invocation; engine returns 403 directly on exclusion.
|
|
223
|
-
- `ctx.signal` is wired to the run's AbortController.
|
|
224
|
-
- Scheme exceptions become the action-entry's outcome (status 500); summary surfaces in next turn's `packet.user.telemetry.errors[]` (§15.1).
|
|
243
|
+
- `manifest.writableBy` checked BEFORE invocation; engine returns 403 directly on exclusion. {§3.6-writableby-403}
|
|
244
|
+
- `ctx.signal` is wired to the run's AbortController (§2.2-signal-wired).
|
|
245
|
+
- Scheme exceptions become the action-entry's outcome (status 500); summary surfaces in next turn's `packet.user.telemetry.errors[]` (§15.1). {§3.6-exception-500}
|
|
225
246
|
|
|
226
|
-
**Tokenization participation.** Schemes route writes through the shared `_entry-crud.ts` write helper (in plurnk-service today; migrates to plurnk-schemes). Helper populates `entry_channels.tokens` at write time via `ctx.provider.countTokens
|
|
247
|
+
**Tokenization participation.** Schemes route writes through the shared `_entry-crud.ts` write helper (in plurnk-service today; migrates to plurnk-schemes). Helper populates `entry_channels.tokens` at write time via `ctx.provider.countTokens` (§14.2-tokens-stored-at-write). Raw DB writes bypass tokenization — out of API scope.
|
|
227
248
|
|
|
228
249
|
---
|
|
229
250
|
|
|
@@ -392,16 +413,17 @@ Per-op semantics. AST shapes from `@plurnk/plurnk-grammar`'s `PlurnkStatement`.
|
|
|
392
413
|
AST: `{ op: "EDIT", target, body: string | null, signal: tags | null, lineMarker? }`.
|
|
393
414
|
|
|
394
415
|
- Resolves target channel from fragment (§5.5); unknown channel → 400; undeclared in manifest → engine crash (§5.3).
|
|
395
|
-
- Writes body; `body: null` clears.
|
|
396
|
-
- Sets `indexed=1` for written channel in current run.
|
|
397
|
-
- Returns `{ status: 201, entryId }` for new entries; `{ status: 200, entryId }` for updates.
|
|
398
|
-
-
|
|
416
|
+
- Writes body; `body: null` clears. {§6.1-null-clears}
|
|
417
|
+
- Sets `indexed=1` for written channel in current run. {§6.1-indexed}
|
|
418
|
+
- Returns `{ status: 201, entryId }` for new entries; `{ status: 200, entryId }` for content updates. {§6.1-status-201-200}
|
|
419
|
+
- A write that changes nothing — identical content and no new tag — returns `{ status: 304, entryId }`, mirroring SHOW/HIDE's no-op (§6.3). {§6.1-noop-304}
|
|
420
|
+
- Tags from `signal[]` apply additively via `entry_tags` (scheme may vary). {§6.1-tags-additive}
|
|
399
421
|
|
|
400
422
|
### §6.2 READ
|
|
401
423
|
|
|
402
424
|
AST: `{ op: "READ", target, body: MatcherBody | null, signal: tags | null, lineMarker? }`.
|
|
403
425
|
|
|
404
|
-
- Returns channel content + mimetype, or 404.
|
|
426
|
+
- Returns channel content + mimetype {§6.2-read-content}, or 404 {§6.2-read-404}.
|
|
405
427
|
- `lineMarker` slices per §16.3.
|
|
406
428
|
- `body` matcher dispatches through `Mimetypes.query` per §16.1 (all four dialects wired).
|
|
407
429
|
|
|
@@ -410,7 +432,7 @@ AST: `{ op: "READ", target, body: MatcherBody | null, signal: tags | null, lineM
|
|
|
410
432
|
AST: `{ op: "SHOW"|"HIDE", target, body: MatcherBody | null, signal: tags | null, lineMarker? }`.
|
|
411
433
|
|
|
412
434
|
- Flips `visibility.indexed` for the targeted channel(s) per §5.5 rules.
|
|
413
|
-
- Returns 200 on transition, 304 on no-op, 404 if entry absent.
|
|
435
|
+
- Returns 200 on transition {§6.3-flip-200}, 304 on no-op {§6.3-noop-304}, 404 if entry absent {§6.3-absent-404}.
|
|
414
436
|
|
|
415
437
|
### §6.4 COPY (engine-orchestrated)
|
|
416
438
|
|
|
@@ -419,7 +441,7 @@ AST: `{ op: "COPY", target (source), body (destination), signal: tags | null, li
|
|
|
419
441
|
Engine orchestrates over CRUD primitives (§3.2, §3.4):
|
|
420
442
|
|
|
421
443
|
1. `src_scheme.readEntry` → 404 if missing. {§6.4-missing-source-404}
|
|
422
|
-
2. `dst_scheme.readEntry` →
|
|
444
|
+
2. `dst_scheme.readEntry` → conflict verdict, deferred until the written content is known (step 5): exists with identical content + tags → 304 (no-op, mirrors EDIT §6.1) {§6.4-noop-304}; exists with different content → 409 (no overwrite) {§6.4-conflict-409}; absent → proceed.
|
|
423
445
|
3. Mimetype compat — channels' mimetypes must be accepted by `dst_scheme.manifest.channels`. Mismatch → 415.
|
|
424
446
|
4. Tags: `signal` non-null replaces source tags {§6.4-signal-replaces-source-tags}; null/empty carries source tags {§6.4-no-signal-carries-source-tags}.
|
|
425
447
|
5. `dst_scheme.writeEntry({channels, tags})`.
|
|
@@ -432,7 +454,7 @@ Returns 201 on success. Same- and cross-scheme COPY share the orchestrator. {§6
|
|
|
432
454
|
AST: `{ op: "MOVE", target (source), body: dest | null, signal: tags | null, lineMarker? }`.
|
|
433
455
|
|
|
434
456
|
- **Relocation** (`body` non-null): COPY (§6.4) + `src_scheme.deleteEntry` in one transaction. 201 on success. {§6.5-relocation-deletes-source} Cross-scheme same as same-scheme. {§6.5-cross-scheme-move}
|
|
435
|
-
- **Deletion** (`body: null`): `src_scheme.deleteEntry` directly. {§6.5-null-body-deletes} 200 on success, 404 if absent. {§6.5-missing-source-404}
|
|
457
|
+
- **Deletion** (`body: null`, or `body` = `/dev/null`): `src_scheme.deleteEntry` directly. {§6.5-null-body-deletes} 200 on success, 404 if absent. {§6.5-missing-source-404} `/dev/null` is the model-facing delete idiom the grammar teaches; a null body is its programmatic equivalent. {§6.5-dev-null-deletes}
|
|
436
458
|
|
|
437
459
|
Log history preserved — `log_entries` stores path tuple as text, not FK to `entries.id`.
|
|
438
460
|
|
|
@@ -441,7 +463,7 @@ Log history preserved — `log_entries` stores path tuple as text, not FK to `en
|
|
|
441
463
|
AST: `{ op: "FIND", target (scope), body: MatcherBody | null (predicate), signal: tags | null, lineMarker? }`.
|
|
442
464
|
|
|
443
465
|
- Filters entries within scope (scheme + pathname prefix). {§6.6-scope-prefix-filter}
|
|
444
|
-
- `body` matcher operates on
|
|
466
|
+
- `body` matcher operates on entry content (glob/regex/jsonpath/xpath), per grammar plurnk.md §"Body matcher dispatch"; the path-glob lives in the (target), not the body. {§6.6-glob-filter-on-content}
|
|
445
467
|
- `signal` is a tag filter; entries match if they have ALL listed tags. {§6.6-tag-filter-and-semantics}
|
|
446
468
|
- Session + scheme scoped — no cross-session/cross-scheme leakage. {§6.6-scoped-isolation}
|
|
447
469
|
- Returns `{ status: 200, results: string }` (newline-separated matching paths, `text/plain`).
|
|
@@ -610,7 +632,7 @@ Plugin discovery (§9) registers whatever's in `node_modules/@plurnk/*`.
|
|
|
610
632
|
- Render budget per channel — `PLURNK_ENTRY_SIZE_DEFAULT_TOKENS` (§12); tokenization per §14.2.
|
|
611
633
|
- Backpressure caps — none (§7.8).
|
|
612
634
|
- Stream cancel — `SEND[499]` (§7.7).
|
|
613
|
-
- Delete — `SEND[410]` (§3.5
|
|
635
|
+
- Delete — MOVE to `/dev/null` (§6.5); `SEND[410]` also deletes as a side-effect (§3.5).
|
|
614
636
|
- Per-loop flags — `loops.flags` JSON column; `yolo` end-to-end today, others scheduled.
|
|
615
637
|
- Default-channel wire rendering — §5.5.
|
|
616
638
|
|
|
@@ -690,7 +712,7 @@ registry.register("loop.run", {
|
|
|
690
712
|
- `description`: one-liner surfaced by `discover`.
|
|
691
713
|
- `params`: `"type — meaning"` per param; `?` suffix = optional. Self-documenting, not enforced.
|
|
692
714
|
- `requiresInit`: rejects until a session is attached.
|
|
693
|
-
- `longRunning`: exempt from `PLURNK_RPC_TIMEOUT`.
|
|
715
|
+
- `longRunning`: exempt from `PLURNK_RPC_TIMEOUT`. {§13.3-register}
|
|
694
716
|
|
|
695
717
|
### §13.4 Discovery
|
|
696
718
|
|
|
@@ -716,7 +738,7 @@ registry.register("loop.run", {
|
|
|
716
738
|
}
|
|
717
739
|
```
|
|
718
740
|
|
|
719
|
-
`capabilities` lists registered plug-ins by `(kind, name)`. Cold clients call `discover` first. No hardcoded method names or capability lists in any client.
|
|
741
|
+
`capabilities` lists registered plug-ins by `(kind, name)`. Cold clients call `discover` first. No hardcoded method names or capability lists in any client. {§13.4-discover}
|
|
720
742
|
|
|
721
743
|
### §13.5 Core method set
|
|
722
744
|
|
|
@@ -733,18 +755,18 @@ registry.register("loop.run", {
|
|
|
733
755
|
|------------------------|---------------------|-------------------|-------|
|
|
734
756
|
| `session.create` | `name?: string`, `projectRoot?: string`, `persona?: string` | `{ id, name, projectRoot, persona }` | Creates new session; auto-name if unprovided. Optional `projectRoot` pins the workspace (null/omitted = headless). Optional `persona` sets the session-level persona override. |
|
|
735
757
|
| `session.list` | none | `{ sessions: Session[] }` | Lists all sessions. |
|
|
736
|
-
| `session.attach` | `id: number`, `runId?: number`, `runName?: string`, `persona?: string` | `{ id, name, runId, runName }` | Binds this connection to an existing session. Optional `runId` resumes that specific run (must belong to the session). Optional `runName` reuses-or-creates by name within the session. Both omitted → new auto-named run. Optional `persona` sets run-level persona only when a NEW run is created. |
|
|
758
|
+
| `session.attach` | `id: number`, `runId?: number`, `runName?: string`, `persona?: string` | `{ id, name, runId, runName }` | Binds this connection to an existing session. Optional `runId` resumes that specific run (must belong to the session). Optional `runName` reuses-or-creates by name within the session. Both omitted → new auto-named run. Optional `persona` sets run-level persona only when a NEW run is created. {§13.5-session-attach} |
|
|
737
759
|
| `session.runs` | `id?: number` | `{ runs: Run[] }` | Lists runs in a session (defaults to attached session); most-recent first. |
|
|
738
760
|
| `session.set_root` | `projectRoot: string \| null` | `{ projectRoot }` | Update the workspace pointer on the attached session. Null reverts to headless. |
|
|
739
761
|
| `session.set_persona` | `persona: string \| null` | `{ persona }` | Update the session-level persona. Null clears the override (falls through to PLURNK_PERSONA file). |
|
|
740
762
|
|
|
741
|
-
**Auto-envelope.** Clients calling a `requiresInit: true` method without first attaching get auto-created session → run → client loop. Records persist normally; auto-created ≠ auto-deleted. Cleanup is a future `session.delete` / `session.archive` endpoint.
|
|
763
|
+
**Auto-envelope.** Clients calling a `requiresInit: true` method without first attaching get auto-created session → run → client loop. Records persist normally; auto-created ≠ auto-deleted. Cleanup is a future `session.delete` / `session.archive` endpoint. {§13.5-auto-envelope}
|
|
742
764
|
|
|
743
765
|
**Loops (model-driven)**
|
|
744
766
|
|
|
745
767
|
| Method | Params | Result | Notes |
|
|
746
768
|
|-------------------|-------------------------------------|------------------------|-------|
|
|
747
|
-
| `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags`, `persona?: string` | `{ loopId, turnIds, finalStatus, hitMaxTurns, reason }` | Model-driven loop. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (currently `{yolo?: boolean}`; more arrive as wired — see §0.5). Optional `persona` sets the loop-level persona (highest precedence in the cascade). Streams `log/entry` and `loop/proposal` notifications during. `longRunning: true`. |
|
|
769
|
+
| `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags`, `persona?: string` | `{ loopId, turnIds, finalStatus, hitMaxTurns, reason }` | Model-driven loop. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (currently `{yolo?: boolean}`; more arrive as wired — see §0.5). Optional `persona` sets the loop-level persona (highest precedence in the cascade). Streams `log/entry` and `loop/proposal` notifications during. `longRunning: true`. {§13.5-loop-run} |
|
|
748
770
|
| `loop.resolve` | `logEntryId: number`, `decision: "accept" \| "reject" \| "cancel"`, `body?: string`, `outcome?: string` | `{ status, logEntryId }` | Resolve a pending proposal (status=202 log entry). Engine.dispatch unpauses on resolution. |
|
|
749
771
|
| `providers.list` | none | `{ aliases: ProviderAlias[] }` | Lists configured `PLURNK_MODEL_<alias>` entries with `{alias, provider, model, active}`. Clients use to populate model-selection UI. |
|
|
750
772
|
|
|
@@ -752,12 +774,12 @@ registry.register("loop.run", {
|
|
|
752
774
|
|
|
753
775
|
| Method | Params | Result | Notes |
|
|
754
776
|
|---------------|-------------------------------------|------------------------|-------|
|
|
755
|
-
| `entry.read` | `target: string` | `{ status, entry }` | Read the full entry shape (channels + tags + metadata) at the given URI. |
|
|
756
|
-
| `log.read` | `loopId?: number`, … | `{ entries: LogEntry[] }` | Read recent log entries from the attached session, optionally filtered by loop. |
|
|
777
|
+
| `entry.read` | `target: string` | `{ status, entry }` | Read the full entry shape (channels + tags + metadata) at the given URI. {§13.5-entry-read} |
|
|
778
|
+
| `log.read` | `loopId?: number`, … | `{ entries: LogEntry[] }` | Read recent log entries from the attached session, optionally filtered by loop. {§13.5-log-read} |
|
|
757
779
|
|
|
758
780
|
**DSL operations (client-driven, mirror grammar)**
|
|
759
781
|
|
|
760
|
-
Per the **Speak in DSL, not plumbing** rule (AGENTS.md): `op.*` methods construct DSL statements internally and dispatch through `Engine.dispatch`. Param shapes are ergonomic (semantic names, not HEREDOC slots); semantics are the DSL's.
|
|
782
|
+
Per the **Speak in DSL, not plumbing** rule (AGENTS.md): `op.*` methods construct DSL statements internally and dispatch through `Engine.dispatch`. {§13.5-op-mirror} Param shapes are ergonomic (semantic names, not HEREDOC slots); semantics are the DSL's.
|
|
761
783
|
|
|
762
784
|
Each `op.*` call creates a turn in the connection's client loop (§13.7), dispatches, fires `log/entry`, returns the dispatch result.
|
|
763
785
|
|
|
@@ -787,7 +809,7 @@ Server-initiated events on the same WebSocket.
|
|
|
787
809
|
|
|
788
810
|
| Notification | Params | When fired |
|
|
789
811
|
|--------------------|-------------------------------------|------------|
|
|
790
|
-
| `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. |
|
|
812
|
+
| `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. {§13.6-log-entry-notify} |
|
|
791
813
|
| `loop/terminated` | `{ loopId, finalStatus, hitMaxTurns }` | Loop reaches terminal status. |
|
|
792
814
|
| `loop/proposal` | `{ logEntryId, sessionId, runId, loopId, turnId, op, target, body, attrs, flags }` | Dispatch pauses on status=202. Carries `flags` so server-YOLO clients can suppress review UI. Client responds with `loop.resolve` (or `PLURNK_PROPOSAL_TIMEOUT_MS` fires). |
|
|
793
815
|
| `session/created` | `{ id, name, projectRoot, persona }` | Any client creates a session. |
|
|
@@ -853,7 +875,7 @@ Plurnk-specific (`-32000` to `-32099`):
|
|
|
853
875
|
| -32006 | Mimetype unavailable |
|
|
854
876
|
| -32007 | Timeout |
|
|
855
877
|
|
|
856
|
-
Error responses MAY include `data: {…}` with structured context (404'd path, timed-out method, etc.).
|
|
878
|
+
Error responses MAY include `data: {…}` with structured context (404'd path, timed-out method, etc.). {§13.8-error-codes}
|
|
857
879
|
|
|
858
880
|
### §13.9 Versioning
|
|
859
881
|
|
|
@@ -875,43 +897,73 @@ Each entry: question, answer, rationale, migration path.
|
|
|
875
897
|
|
|
876
898
|
**Migration path.** If a plugin needs to inject a packet section, grow a single `packet.augment` hook called after `#buildIndex`; plugins return system/user augmentation objects merged into the packet. Additive — engine-direct base stays.
|
|
877
899
|
|
|
878
|
-
### §14.2 Tokenomics: real provider tokens,
|
|
900
|
+
### §14.2 Tokenomics: real provider tokens, render-weight budget, per-scheme balance
|
|
901
|
+
|
|
902
|
+
**Question.** How does plurnk track token costs accurately enough to ground the model's SHOW/HIDE/compose decisions? Accuracy is the whole game — a budget that smells wrong is one the model stops trusting and curating against.
|
|
903
|
+
|
|
904
|
+
**Two measures, never conflated:**
|
|
879
905
|
|
|
880
|
-
**
|
|
906
|
+
- **render-weight** — the tokens the model actually processes this turn (rendered previews + meta + fences). The budget is about this.
|
|
907
|
+
- **content-depth** — an entry's full content size (`entry_channels.tokens`). The manifest's `tokens` is this.
|
|
881
908
|
|
|
882
|
-
**
|
|
909
|
+
**Built.**
|
|
883
910
|
|
|
884
|
-
|
|
911
|
+
- **Provider tokens, stored at write.** `provider.countTokens` is the source of truth; `entry_channels.tokens` (via `_entry-crud`) and `log_entries.tokens` (via `Engine.#writeLog`) are populated at write as a write-time snapshot. A `ceil(len/DIVISOR)` fallback (the divisor tripwire) applies only when no provider tokenizer is wired. {§14.2-tokens-stored-at-write}
|
|
912
|
+
- **Render-weight budget.** The budget headline — `ceiling`, `tokenUsage`, `tokensFree` — is measured from the *assembled packet* (placeholders substituted after measuring), so it reflects what the model actually receives. A `SUM` of stored content-depth would mis-price previews; render-weight is the accurate measure. {§14.2-render-weight-budget}
|
|
913
|
+
- **Per-scheme balance.** A markdown table groups the model's context by scheme — `indexed`/`archived` counts and render-weight `tokens` — anchored `repo, known, unknown, log`, tail sorted by tokens. The model sees at a glance what's eating its window. {§14.2-per-scheme-balance}
|
|
914
|
+
- **Context-window percent.** The headline carries usage as a percent of the ceiling — `usage Y (P%)` — a fullness gauge beside the absolutes. Reads the ceiling already in hand; no extra provider call. {§14.2-context-percent}
|
|
915
|
+
- **Depth re-counted at render.** The manifest re-tokenizes each entry's `tokens` through the live provider at build — never the write-time snapshot — so a model change between loops can't stale the catalog. Every token figure in the packet is render-fresh, manifest and budget alike; nothing trusts a cross-loop cached total. {§14.2-depth-render-fresh}
|
|
916
|
+
- **Over-budget is honest.** When usage exceeds the ceiling, `free` floors at 0 and the percent passes 100 — the readout shows the overshoot rather than a negative free, so the model knows it's over and curates down. {§14.2-over-budget-floor}
|
|
885
917
|
|
|
886
|
-
|
|
887
|
-
- **Stored at write time.** `entry_channels.tokens` populated by `_entry-crud.ts` on INSERT/UPDATE. `log_entries.tokens` populated by `Engine.#writeLog`. Write helper IS the contract sister modules see.
|
|
888
|
-
- **Render-time SUM.** `Engine.#buildIndex` adds per-scheme `SUM(tokens)`. Only wrapper strings re-tokenize each render — bounded.
|
|
889
|
-
- **Hot model switch is a feature.** Session model change walks `entry_channels` + `log_entries` and recomputes against new tokenizer. One-time cost at switch boundary.
|
|
890
|
-
- **Budget table self-reference via placeholder substitution.** Render with `{{tokenUsage}}`/`{{tokensFree}}`, measure, substitute. ±1–2 token drift accepted.
|
|
891
|
-
- **Telemetry shape.** Per-scheme breakdown table in `packet.user.telemetry.budget` with `tokenCeiling`/`tokenUsage`/`tokensFree` headline; markdown table groups by scheme with indexed-count, archived-count, tokens.
|
|
918
|
+
**Rejected / obviated.**
|
|
892
919
|
|
|
893
|
-
**
|
|
920
|
+
- **Hot model-switch recompute** — *obviated* by render-fresh depth (above). There's no cross-loop cache to recompute: the manifest re-tokenizes at build, the budget always did. A model change between loops can't stale a number nothing caches.
|
|
921
|
+
- **Reasoning-token surfacing** — *rejected* for the model-facing budget: reasoning is *output*, not window-context, and the model can't HIDE it. The thinking-vs-output distinction is cost-forensics (the usage breakdown is stored on every packet), not a curation signal.
|
|
894
922
|
|
|
895
|
-
**
|
|
923
|
+
**Rationale.** Rummy used chars/DIVISOR + compute-at-SELECT only because its sync-only SQL couldn't call a tokenizer. plurnk has real `countTokens`: store content tokens once at write (the depth), measure the small rendered output for the budget (the weight). Approximation can't ground curation — the model only curates against numbers it trusts.
|
|
924
|
+
|
|
925
|
+
**Migration path.** None on cost — SQLite, JS, and a local tokenizer are negligible against the model's token budget, the only thing worth economizing. The fallback divisor is a correctness tripwire (no provider tokenizer wired), not a performance retreat. Schema unchanged.
|
|
896
926
|
|
|
897
927
|
### §14.3 Workspace identity, membership, disk co-location
|
|
898
928
|
|
|
899
929
|
**Question.** How does plurnk represent the project a session works on? Where does file membership come from? Does writing an entry imply writing to disk?
|
|
900
930
|
|
|
901
|
-
**
|
|
931
|
+
**The boundary is the client's.** The client owns the model's filesystem access in both directions: reads are membership-gated (a file is invisible to the model unless it is a member), and writes are proposals the client accepts or rejects (`yolo` auto-accepts). Writing an entry never implies writing to disk — entries are canonical in the store; disk only moves when the client accepts a side-effecting proposal, and only where `project_root` is set (null = headless, client owns materialization).
|
|
932
|
+
|
|
933
|
+
**Built — git-substrate membership.** {§14.3-git-membership}
|
|
934
|
+
|
|
935
|
+
- **Identity on the session.** No `projects` table; `sessions.project_root TEXT` (nullable = headless). `entries.scope` unchanged (`∈ {'agent','session'}`). Workspace = session; no users/auth/multi-tenant.
|
|
936
|
+
- **git-ls-files membership.** git present → tracked files (`git ls-files`) are members with no explicit `add` — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership).
|
|
937
|
+
- **EMI eager + relevance-bounded.** Materializes only the run's indexed members (`visibility.indexed=1`) at prompt-composition, re-reading disk so a divergent member reflects current content.
|
|
938
|
+
- **Disk-read-first edits.** Disk writes route through `File.edit`, which reads disk before diffing — an untouched member's baseline is its real content, never empty. Silent overwrite is structurally prevented.
|
|
902
939
|
|
|
903
|
-
|
|
904
|
-
- **D2. No new `entries.scope` value.** Stays `∈ {'agent', 'session'}`.
|
|
905
|
-
- **D3. Disk co-location optional.** `sessions.project_root` set → disk side-effects on accept; null → canonical entries land, no disk write (client owns materialization).
|
|
906
|
-
- **D4. Membership without git: no fs-walk.** git present → ls-files + constraint overlay. git absent → `effect='add'` constraints only.
|
|
907
|
-
- **D5. EMI is eager + relevance-bounded.** Fires at prompt-composition time; checks only entries the current run will include (`visibility.indexed=1`). Divergent → re-read disk + emit synthetic log entry.
|
|
908
|
-
- **D6. Edit-on-untouched-file: disk-read-first.** EDIT targeting a member with no existing entry reads disk first to populate canonical baseline, then diffs. Prevents silent overwrite.
|
|
909
|
-
- **D7. Constraint pattern matching via `node:path.matchesGlob`.** No dep. Helper wrapper for swap-out optionality.
|
|
940
|
+
**Deferred — promised, not yet built.** Each carries an anchor with a deliberately-red test so the deferral is tracked, never silently covered.
|
|
910
941
|
|
|
911
|
-
**
|
|
942
|
+
- **Constraint overlay — the client supersede.** {§14.3-constraint-overlay} A `session_constraints` table layering *add* (members git misses), *ignore* (drop ones it tracks), and *read-only* (member for read, writes rejected) over the git substrate; glob matching via `node:path.matchesGlob`. git-absent, these `effect='add'` constraints are the *sole* membership source — so this overlay is not merely the override knob, it is the entire membership mechanism without git.
|
|
943
|
+
- **EMI divergence signal.** {§14.3-emi-divergence-signal} When EMI re-reads a member whose disk content changed out-of-band, emit a synthetic log entry so the model sees the change (the re-read is built; only the signal is deferred).
|
|
944
|
+
|
|
945
|
+
**Rationale.** No users/tenants. Session is the right scope unit. git+constraints membership keeps the model out of fs-walk territory. EMI's eager-relevance-bounded firing prevents stale previews without per-turn full-repo cost.
|
|
912
946
|
|
|
913
947
|
**Migration path.** Tenancy / cross-session shared workspaces require a `workspaces` table joining sessions to workspace id, with constraints lifted off `session_constraints`. Disk co-location semantics unchanged.
|
|
914
948
|
|
|
949
|
+
### §14.4 Budget enforcement: the grinder
|
|
950
|
+
|
|
951
|
+
**Question.** §14.2 surfaces the budget honestly and the model curates against `tokensFree` — almost always enough. Three states defeat self-regulation, none of them the model's doing: a jumbo prompt, a jumbo repo (the index overflows before the model acts), an unexpectedly large read. What enforces the ceiling when the signal isn't enough?
|
|
952
|
+
|
|
953
|
+
**Decision — a pre-LLM grinder, fired only on actual overflow.** In `Engine.runTurn`, after the packet is assembled (`#buildRequestPacket`) and before `provider.generate`, the assembled render-weight (§14.2) is measured against the ceiling. At or under → the packet ships untouched; the grinder never trims speculatively or "helpfully." {§14.4-overflow-only} On overflow it reclaims window in two passes, re-measuring between:
|
|
954
|
+
|
|
955
|
+
- **Prior-turn rollback.** The immediately-prior turn's log entries — the latest emissions, the ones that pushed the packet over — are hidden (`indexed=0`, the same flag the model's own HIDE uses); the prior turn fit by induction, so reverting it usually lands back under. Hidden, not deleted: rows and bodies persist and are re-SHOWable, so log *history* is preserved while the render shrinks. {§14.4-layer1-rollback}
|
|
956
|
+
- **Index collapse.** If clearing replays isn't enough, every catalog entry except `plurnk://manifest.json` is hidden (`indexed=0`); the manifest stays as the lifeline the model reads to pull anything back by path. The index is engine-built scaffolding — a derived, previews-only view regenerated each turn — never the model's context, so collapsing it removes no capability. {§14.4-layer2-index-collapse}
|
|
957
|
+
- **Hard stop.** If the packet still overflows with only the manifest left, the loop abandons at 499 (`engine_loop_cancel`) — the path `maxTurns` and the strike threshold already use. No third pass. {§14.4-hard-413-abort}
|
|
958
|
+
|
|
959
|
+
**Strike coupling.** A grinder fire bumps the engine's `turnErrors` — the same internal counter cycle detection feeds — so an overflow counts toward the strike streak that ends a runaway loop at 499. This is the pressure that keeps self-curation the path of least resistance. {§14.4-strike-coupling} **Turn 0/1 is exempt:** the first turn's overflow precedes any model action — it's the environment, not the model — so it never strikes. {§14.4-soft-turn-0-1}
|
|
960
|
+
|
|
961
|
+
**What the model sees.** A `budget_overflow` telemetry event (§15.1), in the model's own terms: which of its entries left the window, by scheme. No mechanism vocabulary — no "layer," no "grinder," no "reclaim" — and no advice. The engine reports *what happened to the model's world*; the per-scheme budget table (§14.2) is the diagnostic surface, and the model — which can see what changed in its repo, its reads, its turn — diagnoses the cause the engine can't attribute. {§14.4-event-model-terms} Per the gamification policy (§15.1), the *strike* the overflow triggers stays engine-internal; the model sees the hidden entries, never the accounting.
|
|
962
|
+
|
|
963
|
+
**Rationale.** The model owns curation (§14.2); the grinder is the exceptional backstop. It only *hides* — reversibly — the prior turn's render, then the index scaffolding; nothing is deleted, so the model can SHOW any of it back and log history stays intact. Rummy's §1316 spec described clearing log *bodies*, but its code instead hid the prior turn whole — because body-clearing is destructive (it deletes the read result) and bespoke. The code was the lesson; plurnk follows it (and the §1316 index-collapse pass that doc never caught up to).
|
|
964
|
+
|
|
965
|
+
**Migration path.** None on mechanism. Speculative or non-overflow trimming is a different feature, deliberately excluded — the grinder fires only in response to actual overflow.
|
|
966
|
+
|
|
915
967
|
---
|
|
916
968
|
|
|
917
969
|
## §15 Packet shape
|
|
@@ -941,6 +993,8 @@ type Packet = {
|
|
|
941
993
|
|
|
942
994
|
**Prompt as a first-class entry.** Each loop's prompt is written on loop start as a system-origin `EDIT` against `plurnk://prompt/<loop_id>` (indexable, body channel, text/markdown). At render time the current loop's entry is foisted out of `packet.system.index` into `packet.user.prompt` — no duplicate rendering. Previous loops' prompts remain in the index, addressable for READ/HIDE. {§15-current-prompt-foist}
|
|
943
995
|
|
|
996
|
+
**The entry catalog.** `plurnk://manifest.json` is a real session entry the model READs to discover what's available — rewritten every turn as a live view of the full entry set. Built in the schemes layer (`_entry-manifest`) and materialized like any entry (the engine only orchestrates the per-turn write — the same pattern as git membership), so it's indexed, READable, and queryable. Body is `application/json`: a flat array, one item per entry across all schemes (hidden ones included — the model sees what it could pull in), each `{ path, shown, channels: { <name>: { mimetype, tokens, lines } } }`. `shown` is whether the entry is in this turn's index — `[?(@.shown==false)]` gives the hidden inventory to SHOW; `tokens` is the provider's write-time count (budget depth), `lines` the content extent from `Mimetypes.process().totalLines`. The engine counts neither. It does not list itself. {§15-manifest-catalog}
|
|
997
|
+
|
|
944
998
|
### §15.1 user.telemetry — model-facing runtime telemetry
|
|
945
999
|
|
|
946
1000
|
Slot for telemetry the model MUST react to immediately. Rendered at the bottom of the user section. Errors are transient — appear on the turn AFTER the failure, clear once seen. `packet.system.log[]` is the durable audit; `telemetry.errors[]` is the **alert**.
|
|
@@ -965,6 +1019,7 @@ Slot for telemetry the model MUST react to immediately. Rendered at the bottom o
|
|
|
965
1019
|
| `parse_error` | Grammar parser failed mid-statement | `source: "grammar"`, `kind`, `message`, `position` (content-offset), `snippet` (model's offending line, N:\t-prefixed), `parserSource` (`lexer`/`parser`/`visitor`) |
|
|
966
1020
|
| `action_failure` | Log entry with `status_rx ≥ 400` from previous turn | `kind`, `coordinate` (`<L>/<T>/<S>`), `op`, `status`, `target` (URI or null). May carry scheme-emitted `error` (a terse fact, not guidance). |
|
|
967
1021
|
| `max_commands_exceeded` | Single emission exceeded `PLURNK_MAX_COMMANDS` cap; overflow ops dropped without dispatch | `source: "engine:rail"`, `kind`, `emitted`, `dropped` |
|
|
1022
|
+
| `budget_overflow` | Assembled packet exceeded the budget ceiling; entries moved out of the window to fit | `source: "engine:rail"`, `kind`, `hidden` (per-scheme `[{scheme, count}]` — entries removed from the window) |
|
|
968
1023
|
|
|
969
1024
|
Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeeping are all engine-internal — they drive abandonment silently per the gamification policy above. Action-bound failures (handler returned 4xx/5xx or threw) mirror as `action_failure` kind on the next packet. Full detail queryable via `log://`. {§15.1-no-error-scheme}
|
|
970
1025
|
|
|
@@ -1052,12 +1107,12 @@ Body: JSON array of `{line, matched, matching?}`. Empty → 204. Mimetype = `app
|
|
|
1052
1107
|
|
|
1053
1108
|
### §16.4 Structural EDIT on JSON
|
|
1054
1109
|
|
|
1055
|
-
When effective mimetype is `application/json`, EDIT dispatches through `applyJsonItemEdit`. Body shape rule (parse-then-discriminate):
|
|
1110
|
+
When effective mimetype is `application/json`, EDIT dispatches through `applyJsonItemEdit`. {§16.4-structural-json-edit} Body shape rule (parse-then-discriminate):
|
|
1056
1111
|
|
|
1057
1112
|
- Body parses as JSON array → items to splice
|
|
1058
1113
|
- Body parses as non-array JSON → single item to splice
|
|
1059
1114
|
- Empty body → delete the selection
|
|
1060
|
-
- Body fails JSON parse → 400 (path-extension declares intent; honor strictly)
|
|
1115
|
+
- Body fails JSON parse → 400 (path-extension declares intent; honor strictly) {§16.4-json-parse-fail-400}
|
|
1061
1116
|
|
|
1062
1117
|
**Array source marker × body:**
|
|
1063
1118
|
|
|
@@ -1088,14 +1143,14 @@ When effective mimetype is `application/json`, EDIT dispatches through `applyJso
|
|
|
1088
1143
|
- `known://config.yaml` → `application/yaml`
|
|
1089
1144
|
- `known://users` (no suffix) → `text/markdown` (Known manifest default)
|
|
1090
1145
|
|
|
1091
|
-
Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype is stored in `entry_channels.mimetype` on write and drives `<L>` and matcher dispatch on read.
|
|
1146
|
+
Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype is stored in `entry_channels.mimetype` on write and drives `<L>` and matcher dispatch on read. {§16.5-extension-mimetype}
|
|
1092
1147
|
|
|
1093
1148
|
### §16.6 Render rule (mimetype-driven)
|
|
1094
1149
|
|
|
1095
1150
|
`packet-wire` log render branches on `isLineNavigableMimetype`:
|
|
1096
1151
|
|
|
1097
|
-
- **Line-navigable** (text/markdown, text/plain, csv, source code, yaml, toml) → `N:\t` line-number prefix per line
|
|
1098
|
-
- **Tree-navigable** (application/json, application/xml, text/html, +json/+xml suffixes) → verbatim body (no `N:\t` — outer line numbers would collide with structural navigation like jsonpath/xpath)
|
|
1152
|
+
- **Line-navigable** (text/markdown, text/plain, csv, source code, yaml, toml) → `N:\t` line-number prefix per line {§16.6-line-navigable-prefix}
|
|
1153
|
+
- **Tree-navigable** (application/json, application/xml, text/html, +json/+xml suffixes) → verbatim body (no `N:\t` — outer line numbers would collide with structural navigation like jsonpath/xpath) {§16.6-tree-navigable-verbatim}
|
|
1099
1154
|
|
|
1100
1155
|
The `N:\t` prefix is presentation/reference per plurnk.md ("not part of the source"); stripped before any matcher operation on the log entry.
|
|
1101
1156
|
|
|
@@ -1103,16 +1158,12 @@ The `N:\t` prefix is presentation/reference per plurnk.md ("not part of the sour
|
|
|
1103
1158
|
|
|
1104
1159
|
Auto-derived text mimetypes anywhere in plurnk-service normalize to `text/markdown`:
|
|
1105
1160
|
|
|
1106
|
-
- `<L>` slice on line-navigable source → `text/markdown`
|
|
1161
|
+
- `<L>` slice on line-navigable source → `text/markdown` {§16.7-text-markdown-normalize}
|
|
1107
1162
|
- File scheme extension fallback → `text/markdown`
|
|
1108
1163
|
- `Mimetypes.detect()` returning `text/plain` → normalized via `normalizeAutoTextMimetype`
|
|
1109
1164
|
|
|
1110
1165
|
`text/plain` survives only where a scheme explicitly declares it (exec stdout/stderr — subprocess byte-streams aren't markdown). The model never auto-encounters `text/plain` from defaults.
|
|
1111
1166
|
|
|
1112
|
-
### §16.8 EDIT response surfaces unified diff
|
|
1113
|
-
|
|
1114
|
-
Every EDIT@200/201 carries `diff` (unified diff format) on the result. Both `_entry-ops.editSessionEntry` and `File.edit` produce it. `packet-wire` log render emits the diff under the entry's fence so the model sees what changed without a follow-up READ — wrong-marker mistakes (e.g., `<1>` when `<-1>` was meant) become visible on the next turn. Omitted on no-op edits (content unchanged). {§16.8-edit-diff}
|
|
1115
|
-
|
|
1116
1167
|
### §16.9 Op-level invariants and resolved ambiguities
|
|
1117
1168
|
|
|
1118
1169
|
Carried from the contract walk; durable.
|
|
@@ -1122,9 +1173,9 @@ Carried from the contract walk; durable.
|
|
|
1122
1173
|
- **EDIT `<L>` on non-existent entry** → body becomes content; `<L>` is positional-only on existing content.
|
|
1123
1174
|
- **COPY `<L>`** → source range, symmetric with READ `<L>`.
|
|
1124
1175
|
- **READ rx** prefixes each line with `N:\t` per §16.6. `sliceLinesRaw` (used by COPY) returns the lines without prefix.
|
|
1125
|
-
- **FIND body matcher** applies to
|
|
1126
|
-
- **SHOW/HIDE** has a single-entry path (exact pathname, no slots) and a multi-entry path (any of body/signal/`<L>` present). Multi-entry path treats target as pathname glob scope, applies body matcher to
|
|
1127
|
-
- **SEND[410]** with `#fragment
|
|
1176
|
+
- **FIND body matcher** applies to entry content (all dialects), per-candidate via `Matcher.matchAgainstContent` → `Mimetypes.query` (status 200 = content hit → entry selected). Scope + tags select candidates in SQL; the path-glob is the (target).
|
|
1177
|
+
- **SHOW/HIDE** has a single-entry path (exact pathname, no slots) and a multi-entry path (any of body/signal/`<L>` present). Multi-entry path treats target as pathname glob scope, applies the body matcher to entry content (shared with FIND via `matchPathnames`), filters by `signal` tags, paginates with `<L>`, and flips visibility on the resulting set.
|
|
1178
|
+
- **SEND[410]** deletes as a side-effect (not the model idiom; §6.5): with `#fragment`, that channel only; without, the whole entry. **SEND[499]** is owned by the streaming scheme that holds the subscription.
|
|
1128
1179
|
- **File scheme** reads disk content with mimetype detected via `Mimetypes.detect({ path })` (plumbed through `PlurnkSchemeContext.mimetypes`). Binary mimetypes → 415 on READ and EDIT.
|
|
1129
1180
|
|
|
1130
1181
|
### §16.10 Directed-SEND status code policy
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import SqlRite from "@possumtech/sqlrite";
|
|
8
|
+
import type { Db } from "../src/core/Db.ts";
|
|
9
|
+
import Daemon from "../src/server/Daemon.ts";
|
|
10
|
+
import EnvFlags from "../src/core/EnvFlags.ts";
|
|
11
|
+
import ProviderInstantiate from "../src/core/ProviderInstantiate.ts";
|
|
12
|
+
import { resolveActiveAlias } from "@plurnk/plurnk-providers";
|
|
13
|
+
|
|
14
|
+
export default class Cli {
|
|
15
|
+
static #projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
|
|
17
|
+
static #die(code: number, message: string): never {
|
|
18
|
+
process.stderr.write(`${message}\n`);
|
|
19
|
+
process.exit(code);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static #loadEnv(path: string, required: boolean): void {
|
|
23
|
+
if (existsSync(path)) {
|
|
24
|
+
try { process.loadEnvFile(path); }
|
|
25
|
+
catch (cause) { Cli.#die(64, `failed to load ${path}: ${cause instanceof Error ? cause.message : String(cause)}`); }
|
|
26
|
+
} else if (required) {
|
|
27
|
+
Cli.#die(64, `--config: ${path} does not exist`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The .env cascade always populates these from .env.example, so absence is
|
|
32
|
+
// a broken config, not a runtime branch — fail hard rather than `?? ""`.
|
|
33
|
+
static #requireEnv(name: string): string {
|
|
34
|
+
const value = process.env[name];
|
|
35
|
+
if (value === undefined || value.length === 0) Cli.#die(78, `missing required env ${name} (declare it in .env.example)`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async #openDb(dbPath: string): Promise<Db> {
|
|
40
|
+
const db = await SqlRite.open({
|
|
41
|
+
path: dbPath,
|
|
42
|
+
dir: [resolve(Cli.#projectRoot, "migrations"), resolve(Cli.#projectRoot, "src")],
|
|
43
|
+
});
|
|
44
|
+
return db as unknown as Db;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static async #migrate(): Promise<void> {
|
|
48
|
+
const dbPath = Cli.#requireEnv("PLURNK_DB_PATH");
|
|
49
|
+
const db = await Cli.#openDb(dbPath);
|
|
50
|
+
try { process.stdout.write(`migrated: ${dbPath}\n`); }
|
|
51
|
+
finally { await db.close(); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static async #start(): Promise<void> {
|
|
55
|
+
const dbPath = Cli.#requireEnv("PLURNK_DB_PATH");
|
|
56
|
+
const host = Cli.#requireEnv("PLURNK_HOST");
|
|
57
|
+
const port = Number(Cli.#requireEnv("PLURNK_PORT"));
|
|
58
|
+
|
|
59
|
+
const db = await Cli.#openDb(dbPath);
|
|
60
|
+
const alias = resolveActiveAlias();
|
|
61
|
+
const provider = alias === null ? null : await ProviderInstantiate.loadActiveProvider();
|
|
62
|
+
const daemon = new Daemon({ db, provider });
|
|
63
|
+
const addr = await daemon.start({ host, port });
|
|
64
|
+
const aliasStr = alias === null ? "no model" : `${alias.alias}=${alias.provider}/${alias.model}`;
|
|
65
|
+
process.stdout.write(`plurnk-service ws://${addr.host}:${addr.port} db=${dbPath} ${aliasStr}\n`);
|
|
66
|
+
|
|
67
|
+
const shutdown = async (): Promise<void> => { await daemon.stop(); await db.close(); process.exit(0); };
|
|
68
|
+
process.on("SIGINT", shutdown);
|
|
69
|
+
process.on("SIGTERM", shutdown);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static async main(): Promise<void> {
|
|
73
|
+
// Env cascade: .env.example (shipped defaults) < .env (project) <
|
|
74
|
+
// .env.<config> (--config) < shell. process.loadEnvFile is set-if-unset,
|
|
75
|
+
// so loading in low→high precedence order yields the right effective env
|
|
76
|
+
// (highest precedence loads FIRST — first write wins).
|
|
77
|
+
const configFlagIndex = process.argv.findIndex((a) => a === "--config" || a.startsWith("--config="));
|
|
78
|
+
const configFile = ((): string | null => {
|
|
79
|
+
if (configFlagIndex === -1) return null;
|
|
80
|
+
const arg = process.argv[configFlagIndex];
|
|
81
|
+
if (arg.includes("=")) return arg.slice(arg.indexOf("=") + 1);
|
|
82
|
+
return process.argv[configFlagIndex + 1] ?? null;
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
if (configFile !== null) Cli.#loadEnv(configFile, true);
|
|
86
|
+
Cli.#loadEnv(".env", false);
|
|
87
|
+
Cli.#loadEnv(resolve(Cli.#projectRoot, ".env.example"), false);
|
|
88
|
+
|
|
89
|
+
const flagDescriptors = await EnvFlags.parseEnvExample(resolve(Cli.#projectRoot, ".env.example"));
|
|
90
|
+
const flagOptions: Record<string, { type: "string" }> = {};
|
|
91
|
+
for (const f of flagDescriptors) {
|
|
92
|
+
flagOptions[f.flagName.replace(/^--/, "")] = { type: "string" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const usage = `usage: plurnk-service [options] [migrate]
|
|
96
|
+
|
|
97
|
+
${EnvFlags.formatFlagsHelp(flagDescriptors)}
|
|
98
|
+
|
|
99
|
+
--config=<path> layer additional env from <path>
|
|
100
|
+
-h, --help show this help
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const { positionals, values } = parseArgs({
|
|
104
|
+
allowPositionals: true,
|
|
105
|
+
strict: false,
|
|
106
|
+
options: { help: { type: "boolean", short: "h" }, config: { type: "string" }, ...flagOptions },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
for (const f of flagDescriptors) {
|
|
110
|
+
const key = f.flagName.replace(/^--/, "");
|
|
111
|
+
const v = values[key];
|
|
112
|
+
if (typeof v === "string") process.env[f.envName] = v;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (values.help) { process.stdout.write(usage); process.exit(0); }
|
|
116
|
+
|
|
117
|
+
const dispatch: Record<string, () => Promise<void>> = { migrate: Cli.#migrate, start: Cli.#start };
|
|
118
|
+
const subcommand = typeof positionals[0] === "string" ? positionals[0] : "start";
|
|
119
|
+
const handler = dispatch[subcommand];
|
|
120
|
+
if (handler === undefined) Cli.#die(64, `unknown subcommand: ${subcommand}\n\n${usage}`);
|
|
121
|
+
if (positionals.length > 1) Cli.#die(64, `unexpected arguments: ${positionals.slice(1).join(" ")}`);
|
|
122
|
+
|
|
123
|
+
try { await handler(); }
|
|
124
|
+
catch (cause) {
|
|
125
|
+
process.stderr.write(`${subcommand}: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
126
|
+
if (cause instanceof Error && cause.cause) process.stderr.write(` cause: ${cause.cause instanceof Error ? cause.cause.message : String(cause.cause)}\n`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await Cli.main();
|
package/dist/Paths.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Paths.d.ts","sourceRoot":"","sources":["../src/Paths.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,OAAO,OAAO,KAAK;;IAItB,MAAM,CAAC,UAAU,SAA8C;IAC/D,MAAM,CAAC,kBAAkB,SAA6C;IAKtE,MAAM,CAAC,cAAc,SAAkC;IAIvD,MAAM,CAAC,mBAAmB,SAAuC;CAuBpE"}
|