bonecode 1.2.2 → 1.3.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/README.md +20 -0
- package/dist/src/engine/agent/prompt/compaction.txt +9 -0
- package/dist/src/engine/agent/prompt/explore.txt +18 -0
- package/dist/src/engine/agent/prompt/scout.txt +36 -0
- package/dist/src/engine/agent/prompt/summary.txt +11 -0
- package/dist/src/engine/agent/prompt/title.txt +44 -0
- package/dist/src/engine/session/prompt/anthropic.txt +105 -0
- package/dist/src/engine/session/prompt/beast.txt +147 -0
- package/dist/src/engine/session/prompt/bonescript.txt +391 -0
- package/dist/src/engine/session/prompt/build-switch.txt +5 -0
- package/dist/src/engine/session/prompt/codex.txt +79 -0
- package/dist/src/engine/session/prompt/copilot-gpt-5.txt +143 -0
- package/dist/src/engine/session/prompt/default.txt +105 -0
- package/dist/src/engine/session/prompt/gemini.txt +155 -0
- package/dist/src/engine/session/prompt/gpt.txt +107 -0
- package/dist/src/engine/session/prompt/kimi.txt +95 -0
- package/dist/src/engine/session/prompt/max-steps.txt +16 -0
- package/dist/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/dist/src/engine/session/prompt/plan.txt +26 -0
- package/dist/src/engine/session/prompt/trinity.txt +97 -0
- package/dist/src/engine/session/prompt.js +35 -2
- package/dist/src/engine/session/prompt.js.map +1 -1
- package/dist/src/engine/skill/prompt/customize-opencode.md +377 -0
- package/dist/src/engine/tool/apply_patch.txt +33 -0
- package/dist/src/engine/tool/edit.txt +10 -0
- package/dist/src/engine/tool/glob.txt +6 -0
- package/dist/src/engine/tool/grep.txt +8 -0
- package/dist/src/engine/tool/lsp.txt +24 -0
- package/dist/src/engine/tool/plan-enter.txt +14 -0
- package/dist/src/engine/tool/plan-exit.txt +13 -0
- package/dist/src/engine/tool/question.txt +10 -0
- package/dist/src/engine/tool/read.txt +14 -0
- package/dist/src/engine/tool/repo_clone.txt +5 -0
- package/dist/src/engine/tool/repo_overview.txt +4 -0
- package/dist/src/engine/tool/shell/shell.txt +77 -0
- package/dist/src/engine/tool/skill.txt +5 -0
- package/dist/src/engine/tool/task.txt +58 -0
- package/dist/src/engine/tool/task_status.txt +13 -0
- package/dist/src/engine/tool/todowrite.txt +167 -0
- package/dist/src/engine/tool/tool/apply_patch.txt +33 -0
- package/dist/src/engine/tool/tool/edit.txt +10 -0
- package/dist/src/engine/tool/tool/glob.txt +6 -0
- package/dist/src/engine/tool/tool/grep.txt +8 -0
- package/dist/src/engine/tool/tool/lsp.txt +24 -0
- package/dist/src/engine/tool/tool/plan-enter.txt +14 -0
- package/dist/src/engine/tool/tool/plan-exit.txt +13 -0
- package/dist/src/engine/tool/tool/question.txt +10 -0
- package/dist/src/engine/tool/tool/read.txt +14 -0
- package/dist/src/engine/tool/tool/repo_clone.txt +5 -0
- package/dist/src/engine/tool/tool/repo_overview.txt +4 -0
- package/dist/src/engine/tool/tool/shell/shell.txt +77 -0
- package/dist/src/engine/tool/tool/skill.txt +5 -0
- package/dist/src/engine/tool/tool/task.txt +58 -0
- package/dist/src/engine/tool/tool/task_status.txt +13 -0
- package/dist/src/engine/tool/tool/todowrite.txt +167 -0
- package/dist/src/engine/tool/tool/webfetch.txt +13 -0
- package/dist/src/engine/tool/tool/websearch.txt +14 -0
- package/dist/src/engine/tool/tool/write.txt +8 -0
- package/dist/src/engine/tool/webfetch.txt +13 -0
- package/dist/src/engine/tool/websearch.txt +14 -0
- package/dist/src/engine/tool/write.txt +8 -0
- package/dist/src/tui.js +118 -29
- package/dist/src/tui.js.map +1 -1
- package/package.json +2 -2
- package/scripts/copy_prompts.js +58 -0
- package/scripts/test_bonescript_primer.js +111 -0
- package/scripts/test_tui_render.js +278 -0
- package/src/engine/session/prompt/bonescript.txt +391 -0
- package/src/engine/session/prompt.ts +36 -2
- package/src/tui.ts +115 -30
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# BoneScript — the canonical way to build backends in BoneCode
|
|
2
|
+
|
|
3
|
+
You are working inside a BoneCode project. **BoneScript is a real, deterministic declarative language** that compiles `.bone` system descriptions into complete, runnable Node.js + PostgreSQL backends. It is published on npm as `bonescript-compiler`. The repository is https://github.com/Doorman11991/BoneScript.
|
|
4
|
+
|
|
5
|
+
BoneScript is NOT pseudocode. NOT optional. NOT a generic scripting language. When the user asks you to build a backend feature, your default approach MUST be:
|
|
6
|
+
|
|
7
|
+
1. **Define** — write or update the relevant `.bone` file using the `edit` or `write` tool
|
|
8
|
+
2. **Compile** — run `bonec compile <file>` (or `npm run compile`) via the shell tool
|
|
9
|
+
3. **Extend** — add custom logic only in `extensions/` (these files survive recompilation)
|
|
10
|
+
4. **Check** — run `bonec check <file>` to validate the spec
|
|
11
|
+
|
|
12
|
+
You must NEVER hand-write models, routes, controllers, services, schemas, or migrations. The compiler generates all of that from `.bone` files. You must NEVER run `prisma migrate`, `drizzle-kit push`, `typeorm migration`, `nest g`, `rails generate`, etc.
|
|
13
|
+
|
|
14
|
+
## When BoneScript applies
|
|
15
|
+
|
|
16
|
+
Use BoneScript whenever the user asks for any of these:
|
|
17
|
+
- Domain entities (User, Order, Product, Shop, Session, etc.)
|
|
18
|
+
- Persistence (tables, indexes, migrations)
|
|
19
|
+
- API endpoints (REST or GraphQL)
|
|
20
|
+
- State machines (`pending → paid → shipped → delivered`)
|
|
21
|
+
- Events with delivery guarantees (at_least_once, exactly_once)
|
|
22
|
+
- Saga / multi-step transactional flows with compensation
|
|
23
|
+
- WebSocket channels for realtime
|
|
24
|
+
- Permissions, rate limits, auth (JWT, OAuth2, API key)
|
|
25
|
+
- Background workers, scheduled jobs
|
|
26
|
+
|
|
27
|
+
Use plain Python/TypeScript/etc. only for:
|
|
28
|
+
- **Pure simulations** (game loops, math, render) with no persistence layer
|
|
29
|
+
- **Frontend** components
|
|
30
|
+
- **Custom logic** inside `extensions/` that BoneScript cannot generate (LLM calls, embeddings, file system, git, third-party APIs)
|
|
31
|
+
|
|
32
|
+
If the request is ambiguous (e.g. "a 2D market simulation"), ask the user: "Is this a self-contained simulation script (plain code) or a backend service with persistence (BoneScript)?"
|
|
33
|
+
|
|
34
|
+
## BoneScript syntax — authoritative reference
|
|
35
|
+
|
|
36
|
+
### `system` block
|
|
37
|
+
|
|
38
|
+
Every `.bone` file declares one `system`:
|
|
39
|
+
|
|
40
|
+
```bone
|
|
41
|
+
system Marketplace {
|
|
42
|
+
domain: marketplace
|
|
43
|
+
|
|
44
|
+
// entities, stores, events, capabilities, flows, channels, policies
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `domain:` key picks a starter template (`marketplace`, `saas_platform`, `multiplayer_game`, `iot_system`, `social_network`, `realtime_collaboration`, `ecommerce`, `event_driven`, `api_gateway`, or `blank`).
|
|
49
|
+
|
|
50
|
+
### `entity` — stateful object with constraints, states, relations
|
|
51
|
+
|
|
52
|
+
```bone
|
|
53
|
+
entity Order {
|
|
54
|
+
owns: [
|
|
55
|
+
buyer_id: uuid,
|
|
56
|
+
listing_id: uuid,
|
|
57
|
+
seller_id: uuid,
|
|
58
|
+
quantity: uint,
|
|
59
|
+
total: uint,
|
|
60
|
+
status: string
|
|
61
|
+
]
|
|
62
|
+
constraints: [
|
|
63
|
+
quantity >= 1,
|
|
64
|
+
total > 0,
|
|
65
|
+
status in ["pending", "paid", "shipped", "delivered", "cancelled"]
|
|
66
|
+
]
|
|
67
|
+
states: pending -> paid -> shipped -> delivered | cancelled
|
|
68
|
+
auth: jwt
|
|
69
|
+
index: [buyer_id, seller_id, status]
|
|
70
|
+
relation listing: belongs_to Listing
|
|
71
|
+
relation buyer: belongs_to Buyer
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Field types: `string`, `uint`, `int`, `float`, `bool`, `uuid`, `timestamp`, `json`, `optional<T>`.
|
|
76
|
+
Constraints: `>=`, `<=`, `==`, `in [...]`, `field.length in N..M`, `field.unique`.
|
|
77
|
+
States are unidirectional unless explicitly branched with `|` (terminal states).
|
|
78
|
+
|
|
79
|
+
### `store` — generated database table
|
|
80
|
+
|
|
81
|
+
```bone
|
|
82
|
+
store OrderStore {
|
|
83
|
+
engine: postgresql
|
|
84
|
+
schema: {
|
|
85
|
+
id: uuid,
|
|
86
|
+
buyer_id: uuid,
|
|
87
|
+
listing_id: uuid,
|
|
88
|
+
quantity: uint,
|
|
89
|
+
total: uint,
|
|
90
|
+
status: string,
|
|
91
|
+
state: string,
|
|
92
|
+
created_at: timestamp,
|
|
93
|
+
updated_at: timestamp
|
|
94
|
+
}
|
|
95
|
+
partition: buyer_id // optional — for sharding
|
|
96
|
+
replicas: 1
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The compiler emits SQL migrations with proper indexes, FK constraints, and triggers. Never write migration SQL by hand.
|
|
101
|
+
|
|
102
|
+
### `event` — durable, typed message with delivery semantics
|
|
103
|
+
|
|
104
|
+
```bone
|
|
105
|
+
event OrderPlaced {
|
|
106
|
+
payload: {
|
|
107
|
+
order_id: uuid,
|
|
108
|
+
buyer_id: uuid,
|
|
109
|
+
total: uint,
|
|
110
|
+
placed_at: timestamp
|
|
111
|
+
}
|
|
112
|
+
delivery: at_least_once // or exactly_once
|
|
113
|
+
ttl: 30d // 1h, 7d, 90d, etc.
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`at_least_once` retries with exponential backoff; `exactly_once` deduplicates via the `event_processed` table. Switch modes globally with `EVENT_MODE=durable|in_process` env var.
|
|
118
|
+
|
|
119
|
+
### `capability` — generated endpoint with state-machine enforcement
|
|
120
|
+
|
|
121
|
+
```bone
|
|
122
|
+
capability place_order(buyer: Buyer, listing: Listing, quantity: uint) {
|
|
123
|
+
requires: [
|
|
124
|
+
buyer.state == "active",
|
|
125
|
+
listing.state == "active",
|
|
126
|
+
listing.stock >= quantity,
|
|
127
|
+
buyer.balance >= listing.price * quantity
|
|
128
|
+
]
|
|
129
|
+
effects: [
|
|
130
|
+
listing.stock -= quantity,
|
|
131
|
+
buyer.balance -= listing.price * quantity
|
|
132
|
+
]
|
|
133
|
+
emits: OrderPlaced
|
|
134
|
+
sync: transactional // or eventual / realtime
|
|
135
|
+
timeout: 30s
|
|
136
|
+
idempotent: false
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The compiler generates an Express route, validates the `requires` predicates, applies the `effects` atomically in a SQL transaction, publishes the event via the outbox, and enforces `timeout`. Never touch the generated route file.
|
|
141
|
+
|
|
142
|
+
### `pipeline:` capability — multi-step with auto-rollback
|
|
143
|
+
|
|
144
|
+
```bone
|
|
145
|
+
capability checkout(buyer: Buyer, cart: Cart) {
|
|
146
|
+
pipeline: {
|
|
147
|
+
validate_inventory(cart)
|
|
148
|
+
charge_payment(buyer, cart.total) as payment
|
|
149
|
+
create_order(buyer, cart, payment)
|
|
150
|
+
on_error: rollback
|
|
151
|
+
}
|
|
152
|
+
sync: transactional
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `algorithm:` capability — built-in algorithm catalog
|
|
157
|
+
|
|
158
|
+
```bone
|
|
159
|
+
capability find_route(start: string, end: string) {
|
|
160
|
+
algorithm: shortest_path using { graph: road_network, source: start, target: end }
|
|
161
|
+
returns: json
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Available: `shortest_path`, `topological_sort`, `binary_search`, `bipartite_matching`, `round_robin`, `weighted_average`, `percentile`, `rank_by`, `consistent_hash`.
|
|
166
|
+
|
|
167
|
+
### `flow` — saga with backward compensation
|
|
168
|
+
|
|
169
|
+
```bone
|
|
170
|
+
flow checkout {
|
|
171
|
+
step validate: place_order(buyer, listing, quantity)
|
|
172
|
+
compensate: cancel_order(order)
|
|
173
|
+
|
|
174
|
+
step pay: process_payment(order, buyer)
|
|
175
|
+
compensate: cancel_order(order)
|
|
176
|
+
|
|
177
|
+
step confirm: ship_order(seller, order)
|
|
178
|
+
compensate: cancel_order(order)
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If any step fails, the compiler runs all preceding `compensate` actions in reverse order.
|
|
183
|
+
|
|
184
|
+
### `channel` — WebSocket pub/sub
|
|
185
|
+
|
|
186
|
+
```bone
|
|
187
|
+
channel game_lobby {
|
|
188
|
+
transport: websocket
|
|
189
|
+
ordering: causal // or fifo / unordered
|
|
190
|
+
participants: set<Player>
|
|
191
|
+
persistence: last_100 // last_N messages retained
|
|
192
|
+
filter: participant.id == event.player_id
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `policy` — rate limit + access control + audit
|
|
197
|
+
|
|
198
|
+
```bone
|
|
199
|
+
policy api_limits {
|
|
200
|
+
rate_limit: 200 per 1m // per 1s, 1m, 1h, 1d
|
|
201
|
+
access: [buyer, seller, admin]
|
|
202
|
+
audit: true
|
|
203
|
+
encryption: in_transit // or at_rest, both, none
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `extension_point` — escape hatch for custom logic
|
|
208
|
+
|
|
209
|
+
```bone
|
|
210
|
+
extension_point calculate_shipping_cost(order: Order) {
|
|
211
|
+
returns: uint
|
|
212
|
+
stable: true // compilation fails if not implemented
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Implement in `extensions/`:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// extensions/shipping.ts
|
|
220
|
+
export async function calculate_shipping_cost(order: { id: string; total: number; ... }): Promise<number> {
|
|
221
|
+
// custom logic here — preserved across recompilation
|
|
222
|
+
return Math.ceil(order.total * 0.05)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Cross-entity constraints
|
|
227
|
+
|
|
228
|
+
```bone
|
|
229
|
+
constraint listing_price_limit: Listing.price <= 1000000
|
|
230
|
+
constraint order_quantity_limit: Order.quantity <= 100
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## What gets generated from a `.bone` file
|
|
234
|
+
|
|
235
|
+
Running `bonec compile shop.bone` produces:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
output/
|
|
239
|
+
├── src/
|
|
240
|
+
│ ├── index.ts Express server with all routes wired
|
|
241
|
+
│ ├── db.ts Postgres connection pool
|
|
242
|
+
│ ├── events.ts Durable event bus (transactional outbox)
|
|
243
|
+
│ ├── auth.ts JWT / OAuth2 / API key middleware
|
|
244
|
+
│ ├── publishers.ts Typed event publisher functions
|
|
245
|
+
│ ├── health.ts /health/live, /health/ready, /health/metrics
|
|
246
|
+
│ ├── flows.ts Saga runtime with backward compensation
|
|
247
|
+
│ ├── websocket.ts WebSocket server (if channels declared)
|
|
248
|
+
│ ├── routes/ One file per entity — CRUD + capabilities
|
|
249
|
+
│ ├── state_machines/ One file per entity with states
|
|
250
|
+
│ └── models/ TypeScript interfaces + Zod validators
|
|
251
|
+
├── migrations/ SQL schemas with indexes, triggers, FKs
|
|
252
|
+
├── openapi.json OpenAPI 3.0 schema
|
|
253
|
+
├── Dockerfile
|
|
254
|
+
├── docker-compose.yaml Postgres + Redis for local dev
|
|
255
|
+
├── k8s/deployment.yaml
|
|
256
|
+
└── .github/workflows/ CI/CD pipeline
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Never edit anything in `output/` (or `generated/`). It's overwritten on every compile.** All your custom code goes in `extensions/`.
|
|
260
|
+
|
|
261
|
+
## CLI commands
|
|
262
|
+
|
|
263
|
+
| Command | Purpose |
|
|
264
|
+
|---------|---------|
|
|
265
|
+
| `bonec init <name> --domain <template>` | Scaffold a new project |
|
|
266
|
+
| `bonec compile <file>` | Full 7-stage compile → runnable backend |
|
|
267
|
+
| `bonec check <file>` | Validate without generating |
|
|
268
|
+
| `bonec watch <file>` | Recompile on save |
|
|
269
|
+
| `bonec diff <old> <new>` | Show schema migration diff |
|
|
270
|
+
| `bonec fmt <file>` | Format in place |
|
|
271
|
+
| `bonec test [output-dir]` | Run generated regression tests |
|
|
272
|
+
| `bonec verify-determinism <file>` | Confirm two compiles produce identical output |
|
|
273
|
+
|
|
274
|
+
The compiler is on npm: `npm install -g bonescript-compiler`. Inside a BoneCode project, `npm run compile` typically wraps `bonec compile`.
|
|
275
|
+
|
|
276
|
+
## Worked example — 2D market simulation done right
|
|
277
|
+
|
|
278
|
+
User: "build me a 2D market simulation with 2000 shops over 100 simulated years"
|
|
279
|
+
|
|
280
|
+
The first question to ask: **is it a simulation script or a backend?**
|
|
281
|
+
- If it's just a runnable visualization with no need for persistent state, REST APIs, or multiplayer — write plain Python/TS.
|
|
282
|
+
- If shops have state, transactions are queryable, multiple users can poke at the world, OR you want to run the simulation as a service — use BoneScript.
|
|
283
|
+
|
|
284
|
+
For the backend version:
|
|
285
|
+
|
|
286
|
+
1. Create `bone/market.bone`:
|
|
287
|
+
|
|
288
|
+
```bone
|
|
289
|
+
system Market {
|
|
290
|
+
domain: marketplace
|
|
291
|
+
|
|
292
|
+
entity Shop {
|
|
293
|
+
owns: [
|
|
294
|
+
name: string,
|
|
295
|
+
x_pos: float,
|
|
296
|
+
y_pos: float,
|
|
297
|
+
specialty: string,
|
|
298
|
+
gold: uint,
|
|
299
|
+
reputation: float
|
|
300
|
+
]
|
|
301
|
+
constraints: [
|
|
302
|
+
specialty in ["food", "tools", "weapons", "luxury", "general"],
|
|
303
|
+
gold >= 0,
|
|
304
|
+
reputation >= 0,
|
|
305
|
+
reputation <= 1
|
|
306
|
+
]
|
|
307
|
+
states: founded -> active -> struggling -> bankrupt | thriving
|
|
308
|
+
index: [specialty]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
entity Transaction {
|
|
312
|
+
owns: [
|
|
313
|
+
shop_id: uuid,
|
|
314
|
+
year: uint,
|
|
315
|
+
amount: uint,
|
|
316
|
+
kind: string
|
|
317
|
+
]
|
|
318
|
+
constraints: [
|
|
319
|
+
amount > 0,
|
|
320
|
+
year >= 0,
|
|
321
|
+
kind in ["sale", "purchase", "tax"]
|
|
322
|
+
]
|
|
323
|
+
index: [shop_id, year]
|
|
324
|
+
relation shop: belongs_to Shop
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
event TransactionRecorded {
|
|
328
|
+
payload: {
|
|
329
|
+
transaction_id: uuid,
|
|
330
|
+
shop_id: uuid,
|
|
331
|
+
year: uint,
|
|
332
|
+
amount: uint
|
|
333
|
+
}
|
|
334
|
+
delivery: at_least_once
|
|
335
|
+
ttl: 90d
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
capability record_transaction(shop: Shop, year: uint, amount: uint, kind: string) {
|
|
339
|
+
requires: [
|
|
340
|
+
shop.state in ["active", "thriving", "struggling"],
|
|
341
|
+
amount > 0
|
|
342
|
+
]
|
|
343
|
+
effects: [
|
|
344
|
+
shop.gold = shop.gold + amount
|
|
345
|
+
]
|
|
346
|
+
emits: TransactionRecorded
|
|
347
|
+
sync: transactional
|
|
348
|
+
timeout: 5s
|
|
349
|
+
idempotent: true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
extension_point simulate_year(year: uint) {
|
|
353
|
+
returns: json
|
|
354
|
+
stable: true
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
flow advance_year {
|
|
358
|
+
step demand: simulate_year(year)
|
|
359
|
+
compensate: noop()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
policy api_limits {
|
|
363
|
+
rate_limit: 1000 per 1m
|
|
364
|
+
access: [user, admin]
|
|
365
|
+
audit: true
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
2. `npm run compile` (or `bonec compile bone/market.bone`)
|
|
371
|
+
|
|
372
|
+
3. Implement `simulate_year` in `extensions/simulation.ts`:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
export async function simulate_year(year: number) {
|
|
376
|
+
// Read all shops, calculate demand, call record_transaction for each
|
|
377
|
+
// This is the only place where you write custom logic.
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
4. The generated backend gives you `POST /shops`, `GET /shops/:id`, `POST /shops/:id/record_transaction`, `GET /transactions?shop_id=...`, the state machine, durable events, OpenAPI spec, and a TypeScript SDK — all from the `.bone` file.
|
|
382
|
+
|
|
383
|
+
5. The 2000-shop × 100-year loop lives in a runner script that calls the generated capabilities (or in `simulate_year` itself).
|
|
384
|
+
|
|
385
|
+
This is how you build real backends in BoneCode. Don't fall back to writing raw Python or hand-rolled Express routes when the user asks for a backend feature. If the user actually wants a script, ask first.
|
|
386
|
+
|
|
387
|
+
## Reference links (for the user, not for you to fetch)
|
|
388
|
+
|
|
389
|
+
- BoneScript: https://github.com/Doorman11991/BoneScript
|
|
390
|
+
- Compiler: https://www.npmjs.com/package/bonescript-compiler
|
|
391
|
+
- OpenCode plugin: https://github.com/Doorman11991/opencode-bonescript-backend
|
|
@@ -598,6 +598,11 @@ async function buildSystemPromptWithRAG(
|
|
|
598
598
|
// Base system prompt (provider-specific, from OpenCode)
|
|
599
599
|
const base = getSystemPrompt(model_id, provider_id, agent_name);
|
|
600
600
|
|
|
601
|
+
// BoneScript primer — loaded for every session so the model knows about
|
|
602
|
+
// BoneScript before any tool call. Without this, models default to
|
|
603
|
+
// generic Python/TS and never use the .bone workflow.
|
|
604
|
+
const bonescriptPrimer = loadBonescriptPrimer();
|
|
605
|
+
|
|
601
606
|
// Environment context
|
|
602
607
|
const envContext = [
|
|
603
608
|
`Working directory: ${worktree}`,
|
|
@@ -621,7 +626,7 @@ async function buildSystemPromptWithRAG(
|
|
|
621
626
|
const project_id = sessionRow.rows[0]?.project_id || "";
|
|
622
627
|
if (!project_id) {
|
|
623
628
|
// No project linked yet — skip RAG context
|
|
624
|
-
return [base, envContext, instructions].filter(Boolean).join("\n\n");
|
|
629
|
+
return [base, bonescriptPrimer, envContext, instructions].filter(Boolean).join("\n\n");
|
|
625
630
|
}
|
|
626
631
|
|
|
627
632
|
const ctxResult = await buildContext({
|
|
@@ -641,7 +646,36 @@ async function buildSystemPromptWithRAG(
|
|
|
641
646
|
}
|
|
642
647
|
}
|
|
643
648
|
|
|
644
|
-
return [base, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
|
|
649
|
+
return [base, bonescriptPrimer, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ─── BoneScript primer loader ─────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
let _bonescriptPrimer: string | null = null;
|
|
655
|
+
function loadBonescriptPrimer(): string {
|
|
656
|
+
if (_bonescriptPrimer !== null) return _bonescriptPrimer;
|
|
657
|
+
try {
|
|
658
|
+
const fs = require("fs");
|
|
659
|
+
const path = require("path");
|
|
660
|
+
// Look for the primer in the prompt directory next to this compiled module.
|
|
661
|
+
// After compilation, this lives at dist/src/engine/session/prompt.js, so the
|
|
662
|
+
// .txt file is at dist/src/engine/session/prompt/bonescript.txt.
|
|
663
|
+
const candidates = [
|
|
664
|
+
path.join(__dirname, "prompt", "bonescript.txt"),
|
|
665
|
+
path.join(__dirname, "..", "..", "..", "src", "engine", "session", "prompt", "bonescript.txt"),
|
|
666
|
+
];
|
|
667
|
+
for (const candidate of candidates) {
|
|
668
|
+
if (fs.existsSync(candidate)) {
|
|
669
|
+
_bonescriptPrimer = fs.readFileSync(candidate, "utf-8");
|
|
670
|
+
return _bonescriptPrimer || "";
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
_bonescriptPrimer = "";
|
|
674
|
+
return "";
|
|
675
|
+
} catch {
|
|
676
|
+
_bonescriptPrimer = "";
|
|
677
|
+
return "";
|
|
678
|
+
}
|
|
645
679
|
}
|
|
646
680
|
|
|
647
681
|
// ─── Language Model Factory ───────────────────────────────────────────────────
|
package/src/tui.ts
CHANGED
|
@@ -393,19 +393,78 @@ interface ToolTrack {
|
|
|
393
393
|
* "text" — pass deltas through verbatim (with partial-fence detection)
|
|
394
394
|
* "fence" — buffer everything; emit marker on close fence
|
|
395
395
|
*/
|
|
396
|
+
/**
|
|
397
|
+
* Apply lightweight inline markdown styling to a streamed chunk.
|
|
398
|
+
*
|
|
399
|
+
* - `# `, `## `, `### ` at line-start become bold colored headers
|
|
400
|
+
* - `**text**` becomes bold
|
|
401
|
+
* - `` `code` `` becomes inverted gray
|
|
402
|
+
* - `- ` and `* ` at line-start become a bullet
|
|
403
|
+
*
|
|
404
|
+
* Operates per-line on the chunk. Cross-chunk markdown (a `**` opening in one
|
|
405
|
+
* delta and closing in another) is not handled — we just leave those raw.
|
|
406
|
+
* That's acceptable because it's rare and the model's output is still readable.
|
|
407
|
+
*/
|
|
408
|
+
function applyInlineMarkdown(text: string): string {
|
|
409
|
+
if (!text) return text;
|
|
410
|
+
// Split on newlines so we can match line-anchored patterns (headers, bullets)
|
|
411
|
+
const lines = text.split("\n");
|
|
412
|
+
const styled = lines.map((line) => {
|
|
413
|
+
// Headers
|
|
414
|
+
let m = line.match(/^(#{1,6})\s+(.*)$/);
|
|
415
|
+
if (m) {
|
|
416
|
+
const level = m[1].length;
|
|
417
|
+
const content = m[2];
|
|
418
|
+
if (level === 1) return `${CYAN}${BOLD}${content}${R}`;
|
|
419
|
+
if (level === 2) return `${CYAN}${BOLD}${content}${R}`;
|
|
420
|
+
if (level === 3) return `${WHITE}${BOLD}${content}${R}`;
|
|
421
|
+
return `${WHITE}${content}${R}`;
|
|
422
|
+
}
|
|
423
|
+
// Bullets at line start
|
|
424
|
+
m = line.match(/^(\s*)[-*]\s+(.*)$/);
|
|
425
|
+
if (m) {
|
|
426
|
+
line = `${m[1]}${GRAY}•${R} ${m[2]}`;
|
|
427
|
+
}
|
|
428
|
+
// Numbered lists
|
|
429
|
+
m = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
430
|
+
if (m) {
|
|
431
|
+
line = `${m[1]}${GRAY}${m[2]}${R} ${m[3]}`;
|
|
432
|
+
}
|
|
433
|
+
// Bold (**text**)
|
|
434
|
+
line = line.replace(/\*\*([^*]+)\*\*/g, `${BOLD}$1${R}`);
|
|
435
|
+
// Inline code (`code`)
|
|
436
|
+
line = line.replace(/`([^`]+)`/g, `${GRAY}$1${R}`);
|
|
437
|
+
return line;
|
|
438
|
+
});
|
|
439
|
+
return styled.join("\n");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Stateful code-fence collapser.
|
|
444
|
+
*
|
|
445
|
+
* Replaces ```lang ... ``` blocks with a one-line marker
|
|
446
|
+
* " ┃ code: lang, N lines" so streaming code blocks don't flood the TUI.
|
|
447
|
+
*
|
|
448
|
+
* State:
|
|
449
|
+
* "text" — pass through verbatim (with partial-fence buffering)
|
|
450
|
+
* "fence" — silently buffer body; emit final marker on close
|
|
451
|
+
*
|
|
452
|
+
* Newline handling:
|
|
453
|
+
* We strip up to one trailing newline before the opening fence and one
|
|
454
|
+
* leading newline after the closing fence so the marker sits on its own
|
|
455
|
+
* line without doubling blank lines around it.
|
|
456
|
+
*/
|
|
396
457
|
function makeCodeFenceCollapser() {
|
|
397
458
|
let mode: "text" | "fence" = "text";
|
|
398
459
|
let lang = "";
|
|
399
|
-
let buffered = "";
|
|
400
|
-
let pending = "";
|
|
401
|
-
|
|
402
|
-
const lines = (s: string) => s.split("\n").length;
|
|
460
|
+
let buffered = "";
|
|
461
|
+
let pending = "";
|
|
403
462
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return
|
|
408
|
-
}
|
|
463
|
+
const countLines = (s: string) => {
|
|
464
|
+
if (!s) return 0;
|
|
465
|
+
const trimmed = s.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
466
|
+
return trimmed ? trimmed.split("\n").length : 0;
|
|
467
|
+
};
|
|
409
468
|
|
|
410
469
|
return {
|
|
411
470
|
feed(chunk: string): string {
|
|
@@ -416,23 +475,26 @@ function makeCodeFenceCollapser() {
|
|
|
416
475
|
|
|
417
476
|
while (i < buf.length) {
|
|
418
477
|
if (mode === "text") {
|
|
419
|
-
// Look for ``` to enter fence mode
|
|
420
478
|
const fenceIdx = buf.indexOf("```", i);
|
|
421
479
|
if (fenceIdx === -1) {
|
|
422
|
-
// No fence in remaining text
|
|
423
|
-
//
|
|
480
|
+
// No fence in remaining text. Hold last 2 chars in case they're
|
|
481
|
+
// the start of an upcoming fence.
|
|
424
482
|
const safeEnd = Math.max(i, buf.length - 2);
|
|
425
483
|
result += buf.slice(i, safeEnd);
|
|
426
484
|
pending = buf.slice(safeEnd);
|
|
427
485
|
i = buf.length;
|
|
428
486
|
break;
|
|
429
487
|
}
|
|
430
|
-
// Emit text up to the fence
|
|
431
|
-
|
|
432
|
-
|
|
488
|
+
// Emit text up to the fence, but trim a single trailing newline so
|
|
489
|
+
// the marker sits on its own line without extra blank space.
|
|
490
|
+
let preFence = buf.slice(i, fenceIdx);
|
|
491
|
+
if (preFence.endsWith("\n")) preFence = preFence.slice(0, -1);
|
|
492
|
+
result += preFence;
|
|
493
|
+
|
|
494
|
+
// Read the language declaration (everything up to the next newline).
|
|
433
495
|
const nlIdx = buf.indexOf("\n", fenceIdx + 3);
|
|
434
496
|
if (nlIdx === -1) {
|
|
435
|
-
//
|
|
497
|
+
// Opening line not complete yet — buffer for next feed.
|
|
436
498
|
pending = buf.slice(fenceIdx);
|
|
437
499
|
i = buf.length;
|
|
438
500
|
break;
|
|
@@ -441,31 +503,36 @@ function makeCodeFenceCollapser() {
|
|
|
441
503
|
i = nlIdx + 1;
|
|
442
504
|
mode = "fence";
|
|
443
505
|
buffered = "";
|
|
444
|
-
//
|
|
445
|
-
|
|
506
|
+
// Don't emit anything yet — the final marker is written when we
|
|
507
|
+
// see the closing fence so we have the line count.
|
|
446
508
|
continue;
|
|
447
509
|
}
|
|
448
510
|
|
|
449
511
|
// mode === "fence" — look for closing ```
|
|
450
512
|
const closeIdx = buf.indexOf("```", i);
|
|
451
513
|
if (closeIdx === -1) {
|
|
514
|
+
// No close fence yet — buffer the body.
|
|
452
515
|
buffered += buf.slice(i);
|
|
453
|
-
// Hold last 2 chars in case they're part of an upcoming close fence
|
|
516
|
+
// Hold last 2 chars in case they're part of an upcoming close fence.
|
|
454
517
|
const safeEnd = Math.max(i, buf.length - 2);
|
|
455
518
|
buffered = buffered.slice(0, buffered.length - (buf.length - safeEnd));
|
|
456
519
|
pending = buf.slice(safeEnd);
|
|
457
520
|
i = buf.length;
|
|
458
521
|
break;
|
|
459
522
|
}
|
|
460
|
-
|
|
523
|
+
|
|
524
|
+
// Closing fence found.
|
|
461
525
|
buffered += buf.slice(i, closeIdx);
|
|
462
|
-
const lineCount =
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
result += `\
|
|
526
|
+
const lineCount = countLines(buffered);
|
|
527
|
+
// Emit the marker on its own line with leading and trailing newlines.
|
|
528
|
+
// The outer indenter will prepend " " to each line break.
|
|
529
|
+
result += `\n${GRAY}┃ code: ${lang}, ${lineCount} line${lineCount === 1 ? "" : "s"}${R}`;
|
|
466
530
|
i = closeIdx + 3;
|
|
467
|
-
// Skip
|
|
531
|
+
// Skip a single newline immediately after the close fence to avoid
|
|
532
|
+
// doubling blank lines.
|
|
468
533
|
if (buf[i] === "\n") i++;
|
|
534
|
+
// Add a trailing newline so the next text starts on a fresh line.
|
|
535
|
+
result += "\n";
|
|
469
536
|
mode = "text";
|
|
470
537
|
lang = "";
|
|
471
538
|
buffered = "";
|
|
@@ -474,12 +541,12 @@ function makeCodeFenceCollapser() {
|
|
|
474
541
|
return result;
|
|
475
542
|
},
|
|
476
543
|
flush(): string {
|
|
477
|
-
const tail =
|
|
544
|
+
const tail = pending;
|
|
545
|
+
pending = "";
|
|
478
546
|
if (mode === "fence") {
|
|
479
|
-
|
|
480
|
-
const lineCount = lines(buffered.replace(/\n+$/, ""));
|
|
547
|
+
const lineCount = countLines(buffered);
|
|
481
548
|
mode = "text";
|
|
482
|
-
return tail + `\
|
|
549
|
+
return tail + `\n${GRAY}┃ code: ${lang}, ${lineCount}+ lines${R}\n`;
|
|
483
550
|
}
|
|
484
551
|
return tail;
|
|
485
552
|
},
|
|
@@ -563,8 +630,10 @@ async function streamPrompt(opts: {
|
|
|
563
630
|
// code blocks appear as "[code: lang]" placeholders instead of
|
|
564
631
|
// dumping raw source.
|
|
565
632
|
const piece = collapseCodeFences(text);
|
|
633
|
+
// Apply lightweight markdown styling (headers, bold, inline code)
|
|
634
|
+
const styled = applyInlineMarkdown(piece);
|
|
566
635
|
// Print with leading-newline indenting (so each new line gets the 3-space prefix)
|
|
567
|
-
const indented =
|
|
636
|
+
const indented = styled.replace(/\n/g, `\n `);
|
|
568
637
|
out(indented);
|
|
569
638
|
fullText += text;
|
|
570
639
|
continue;
|
|
@@ -727,6 +796,22 @@ export async function runTUI(opts: {
|
|
|
727
796
|
port: number; token: string; model: string;
|
|
728
797
|
provider: string; worktree: string; sessionId?: string;
|
|
729
798
|
}): Promise<void> {
|
|
799
|
+
// Force UTF-8 output on Windows so emoji and box-drawing chars render
|
|
800
|
+
// correctly. Without this, console code page 437/850 corrupts multi-byte
|
|
801
|
+
// sequences into "??" or mojibake.
|
|
802
|
+
if (process.platform === "win32") {
|
|
803
|
+
try {
|
|
804
|
+
// Set output encoding for stdout/stderr
|
|
805
|
+
(process.stdout as any).setEncoding?.("utf-8");
|
|
806
|
+
(process.stderr as any).setEncoding?.("utf-8");
|
|
807
|
+
// Switch the console to UTF-8 (code page 65001)
|
|
808
|
+
const { execSync } = require("child_process");
|
|
809
|
+
execSync("chcp 65001", { stdio: "ignore" });
|
|
810
|
+
} catch {
|
|
811
|
+
// chcp not available — that's fine, we tried
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
730
815
|
let { model, provider } = opts;
|
|
731
816
|
const { port, token, worktree } = opts;
|
|
732
817
|
let sessionId = opts.sessionId || null;
|