@akanjs/cli 3.0.0-beta.18 → 3.0.0-beta.19

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.
Files changed (75) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-0035exh3.js → CodeAgent-6cm97kay.js} +2 -2
  3. package/{CodeAgentRpcHost-qvwfv0n9.js → CodeAgentRpcHost-p7h6n2hp.js} +1 -1
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/{CodeTui-eweg1hep.js → CodeTui-za4jsm41.js} +1 -1
  6. package/{TunnelShare-r5tc2qk5.js → TunnelShare-2j6s511d.js} +2 -2
  7. package/{agent.command-65zqfkg0.js → agent.command-e0wpbbyg.js} +5 -5
  8. package/akanCode.js +4 -4
  9. package/{application.command-2wcj5da3.js → application.command-8wyxqc8q.js} +15 -9
  10. package/{applicationBuildRunner-m0hhgwsx.js → applicationBuildRunner-n9gzaxd0.js} +31 -7
  11. package/{applicationReleasePackager-vykfzgx3.js → applicationReleasePackager-vpggfqgq.js} +1 -1
  12. package/buildBatch.proc.js +3 -3
  13. package/{capacitorApp-2daqhb5h.js → capacitorApp-q3z0qht0.js} +2 -2
  14. package/{cloud.command-960bgbf4.js → cloud.command-4zwa5dsx.js} +34 -12
  15. package/cloud.script-fwx3vb34.js +20 -0
  16. package/{code.command-4feh3ef7.js → code.command-468c9d7s.js} +41 -19
  17. package/commandManifest.json +1 -1
  18. package/{context.command-kzehkx48.js → context.command-dsnw0jby.js} +13 -13
  19. package/context.runner-rs4rj15d.js +22 -0
  20. package/{dependencyScanner-shs82ynp.js → dependencyScanner-0z2h2tks.js} +1 -1
  21. package/{guideline.command-3w2g20x4.js → guideline.command-vhw13pqr.js} +2 -2
  22. package/guidelines/agentRule/agentRule.instruction.md +27 -10
  23. package/guidelines/conventions/conventions.instruction.md +16 -4
  24. package/guidelines/queryRule/queryRule.instruction.md +16 -6
  25. package/guidelines/runtimeRule/runtimeRule.instruction.md +74 -0
  26. package/incrementalBuilder.proc.js +3 -2
  27. package/{index-dw943k93.js → index-10dkrrww.js} +4 -4
  28. package/{index-rpgxs245.js → index-18zybgkc.js} +2 -2
  29. package/{index-mt2154s2.js → index-22xtgmhn.js} +1 -1
  30. package/{index-yw6g3bbv.js → index-2446k7b4.js} +1 -1
  31. package/{index-cjw1sj1x.js → index-3jrp517k.js} +10 -10
  32. package/{index-q9c3c0hy.js → index-4m929vvn.js} +2 -2
  33. package/{index-ef228gzf.js → index-53176s7j.js} +3 -3
  34. package/{index-jwrpsrg3.js → index-667w0yah.js} +1 -1
  35. package/{index-6q589y9t.js → index-7ep8qtvp.js} +2 -2
  36. package/{index-w8kmx1b6.js → index-89hy31kf.js} +30 -7
  37. package/{index-jd01kj5r.js → index-9d6nvkz2.js} +1 -1
  38. package/{index-zq0mr3hh.js → index-a1w6934z.js} +3 -3
  39. package/{index-sbfk9daf.js → index-ab0c38ea.js} +4 -4
  40. package/index-bc88j1ry.js +1166 -0
  41. package/{index-h94faz96.js → index-bftkz6cp.js} +274 -60
  42. package/{index-kcxmv48c.js → index-bhtmj233.js} +4 -1
  43. package/{index-m41ptvat.js → index-bwf965h6.js} +3 -3
  44. package/{index-bwxpk2cj.js → index-crmeqnmf.js} +52 -26
  45. package/{index-8k16atqy.js → index-d2242ksy.js} +23 -6
  46. package/{index-jwexzh42.js → index-e4v6vhxq.js} +222 -56
  47. package/{index-pn8mhpps.js → index-esk82rvk.js} +4 -4
  48. package/{index-4r42ext5.js → index-kwaq3n9n.js} +1 -1
  49. package/{index-gm76f0qb.js → index-n3cv0tm4.js} +1 -1
  50. package/{index-ycy302an.js → index-qdgzsnhg.js} +1 -1
  51. package/{index-wcg582a8.js → index-sq2dzkzm.js} +35 -10
  52. package/{index-myj01w9r.js → index-srmay5jw.js} +15 -2
  53. package/{index-zmj143x7.js → index-txr7dj2g.js} +3 -3
  54. package/{index-erfc1x40.js → index-vqx8bdxz.js} +1 -1
  55. package/{index-d9fjz06h.js → index-zqgj2rt2.js} +4 -4
  56. package/index.js +21 -21
  57. package/{library.command-fp0nhr62.js → library.command-cqr175dy.js} +3 -3
  58. package/{localRegistry.command-8y0ecxb9.js → localRegistry.command-49z55qqz.js} +9 -9
  59. package/{module.command-kqkn48t2.js → module.command-9p8mm3b0.js} +5 -5
  60. package/{package.command-qverjh22.js → package.command-wxyz909t.js} +4 -4
  61. package/package.json +2 -2
  62. package/{page.command-94bwhd0g.js → page.command-gsrsfqqs.js} +3 -3
  63. package/{primitive.command-2ydvtah2.js → primitive.command-ns1mptje.js} +6 -6
  64. package/{quality.command-fa0gth4p.js → quality.command-e76k2q9k.js} +17 -15
  65. package/{repair.command-4qng2hpm.js → repair.command-pkrsm3cw.js} +5 -5
  66. package/{scalar.command-pb2y95sh.js → scalar.command-0cpt3fc8.js} +4 -4
  67. package/{subspace.command-vkxmwp74.js → subspace.command-fktpwp0g.js} +20 -10
  68. package/templates/appSample/lib/task/task.signal.test.ts +1 -1
  69. package/templates/localDev/docker-compose.yaml.template +3 -14
  70. package/{tunnel.command-02cvp9nf.js → tunnel.command-d5jy9gk8.js} +4 -4
  71. package/{workflow.command-132txnrh.js → workflow.command-6tkcysmx.js} +9 -9
  72. package/{workspace.command-d5wkswek.js → workspace.command-gv84eh1p.js} +24 -23
  73. package/cloud.script-qhcnqt1n.js +0 -20
  74. package/context.runner-z0rrcz7b.js +0 -22
  75. package/index-3hpxeys1.js +0 -462
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  TypeScriptDependencyScanner2
4
- } from "./index-kcxmv48c.js";
4
+ } from "./index-bhtmj233.js";
5
5
  import"./index-q0y2jq9j.js";
6
6
  export {
7
7
  TypeScriptDependencyScanner2 as TypeScriptDependencyScanner
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  script,
4
4
  command
5
- } from "./index-ycy302an.js";
5
+ } from "./index-qdgzsnhg.js";
6
6
  import"./index-q0y2jq9j.js";
7
- import"./index-bwxpk2cj.js";
7
+ import"./index-crmeqnmf.js";
8
8
  import"./index-6wwzs1bc.js";
9
9
  import {
10
10
  Prompter2
@@ -96,15 +96,21 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
96
96
  nothing about, and it stays text-only until `option.setLlm({ accepts })` says otherwise — handing bytes to a
97
97
  model that cannot decode them kills the whole turn on a 400, where text-only degrades them to a note the model
98
98
  can repeat back.
99
- - **A refusal is one key, `agent.error.llmRequestFailed`, and it names the host.** The provider's own sentence
100
- rides in `reason` and the hostname in `provider`, so an adaptor an app wrote reports through the same
101
- translated channel the shipped ones do rather than needing a key in the framework's dictionary it cannot
102
- add.
99
+ - **A refusal is `agent.error.llmRequestFailed`, and it names the host** — unless the prompt did not fit. The
100
+ provider's own sentence rides in `reason` and the hostname in `provider`. `LlmOverflow.refusal(host, status,
101
+ reason)` builds it, and reads the one refusal a chat can undo — a prompt past the window, recognized from each
102
+ provider's own sentence — as `agent.error.contextOverflow` with the `limit` it named, which the relay flags so
103
+ the chat compacts and asks again. An adaptor an app wrote throws through the same function, so its refusals
104
+ translate and recover the way the shipped ones do without a key in the framework's dictionary it cannot add.
103
105
  - **`accepts` is answered per model, through `option.setLlm({ accepts })`.** An adaptor answers for an API and one
104
106
  API serves models that differ, so the override rides beside the `model` it is a fact about. It is not a table
105
107
  the framework keeps: a table is a claim about models that ship after it, and getting this wrong is the worst
106
108
  failure available — a provider handed bytes it cannot decode either refuses the turn or accepts it having seen
107
109
  nothing, and the model then answers confidently about a file it never read.
110
+ - **`contextWindow` is declared the same way: `option.setLlm({ contextWindow })`.** Every turn relays it with the
111
+ answer ceiling the adaptor actually sends (`LlmAdaptor.limits` — `AnthropicLlm` always sends one, `OpenaiLlm`
112
+ none), and the chat guards its window with it. It is not a table either; a model id is retired or renamed long
113
+ before a table notices.
108
114
  - **An adaptor answers `null` for "not configured" and *throws* for a refusal it can explain.** The two are
109
115
  different things to be told: collapsing both into `null`, the way the adapter convention otherwise reads, left a
110
116
  user reading `llmUnavailable` — "no model is configured" — about a conversation that had merely outgrown the
@@ -539,18 +545,29 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
539
545
  - **A long conversation summarizes itself, because nothing else is keeping it inside the model's window.** The
540
546
  loop runs in the browser and the relay holds no session, so an uncompacted chat grows until the provider
541
547
  refuses the whole request — a refusal, never a shorter answer, which is why compaction runs *before* the turn
542
- that would have overflowed rather than as a recovery after it. Past `compact.at` estimated tokens (four
543
- characters to a token, over the JSON the turn posts; 24k by default, well under the smallest window a provider
544
- is likely to have, since the tools and the screen context ride on top of it and neither compacts) the history above the last `keep` messages
545
- becomes one message standing in for it, flagged `summary` on the wire — `<Agent.Chat compact={{ at, keep }} />`
546
- tunes it per provider and `{ at: 0 }` turns it off, and `/compact` does the same on demand keeping nothing.
548
+ that would have overflowed. **Two triggers, each before every assistant turn.** `compact.at` is a ceiling on
549
+ cost: past that many estimated transcript tokens (four characters to a token, over the JSON the turn posts —
550
+ except an inlined picture, counted at the ~1,600 tokens a provider bills for its pixels rather than its base64;
551
+ 24k by default) the history compacts, because the relay resends the whole transcript every turn and the app
552
+ pays for each one. The **window guard** compacts once the prompt nears the window the server reports —
553
+ `window − answer ceiling (8,192 when unreported) − compact.buffer (13,000, the margin Claude Code leaves)` —
554
+ measured by the provider's own count of the last turn (`done.usage`, kept on that assistant message) plus the
555
+ four-character estimate for what arrived since, so a Korean transcript the character rule reads as small is
556
+ still caught. The window comes from `option.setLlm({ contextWindow })` or from the first refusal that names it;
557
+ with neither, only the ceiling runs. **A refusal for length is answered once**: a relay flags it `overflow` on
558
+ the wire, and the session compacts and sends the same turn again, and a second refusal in the same send fails
559
+ with `agent.error.contextOverflow`. Either way the history above the last `keep` messages
560
+ becomes one message standing in for it, flagged `summary` on the wire — `<Agent.Chat compact={{ at, keep, buffer }} />`
561
+ tunes it, `{ at: Infinity }` leaves only the window guard, `{ at: 0 }` turns off the ceiling, the guard and the recovery alike, and `/compact` does
562
+ the same on demand keeping nothing. The header draws the estimate against whichever trigger is nearer.
547
563
  **The cut never lands on a `tool` message**: the kept half may not open with a result whose call was summarized
548
564
  away, a shape every provider dialect rejects. A user message is preferred where the tail holds one — everything
549
565
  above it is settled — but one assistant turn that ran ten tools has none, and that is the transcript that
550
566
  outgrows the window, so a cut there opens on the assistant message instead. The summarizing
551
567
  turn carries no tools and no screen context — it summarizes the conversation, it does not act on it — and it is
552
568
  fed a *bounded* digest rather than the transcript itself, since the transcript being summarized is the one that
553
- no longer fits. A summary that cannot be produced leaves the transcript alone, the turn goes out as it would
569
+ no longer fits. The previous summary is the exception: it rides the digest whole, under its own label and outside
570
+ the bound, because it is the only record of what was compacted before it. A summary that cannot be produced leaves the transcript alone, the turn goes out as it would
554
571
  have, and the next one asks again — a summarizer that was momentarily unreachable says nothing about whether
555
572
  this transcript can shrink. One that *landed* and shrank nothing does, so that one is not retried until another
556
573
  threshold's worth has been added. On the
@@ -28,7 +28,7 @@ you fetch on demand — `get_guideline` with the name, or `akan guideline show <
28
28
  | name | covers |
29
29
  |---|---|
30
30
  | `ssrRule` | server-share targets, the `akan.ssr.*` warnings, the client-boundary playbook |
31
- | `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, shipped assets |
31
+ | `runtimeRule` | `web` / `csr` surfaces, gateway vs solo processes, logging, the generated image, shipped assets, database modes |
32
32
  | `queryRule` | slices and hydration, the generated filter methods, full-text search, cascade removal |
33
33
  | `transportRule` | guards across HTTP and websocket, socket identity and cleanup, binary pubsub, mutation verbs |
34
34
  | `mcpRule` | MCP configuration, wire behaviour, resource URIs, OAuth metadata, protocol revisions |
@@ -337,6 +337,14 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
337
337
  - **`assets: { pruneFonts, keepFonts }`** trims from the `dist` copy of `public/` the fonts nothing reads; source
338
338
  trees are never touched. A font with `optimize` on is a build input, not a runtime asset. `keepFonts` belongs to
339
339
  the `akan.config.ts` that owns the font, written against that scope's own `public/`.
340
+ - **Database modes are `database: { modes: [...] }`** — `single` (SQLite + Solid), `multiple` (one SQLite file on
341
+ a host volume + Redis), `cluster` (Postgres + Redis). The build carries the drivers of every declared mode and a
342
+ deployment picks one with `AKAN_DATABASE_MODE` — a declared one, named whenever the build declares several.
343
+ Where the data lives (`POSTGRES_URL`, `REDIS_URI`, `SQLITE_DATABASE_PATH`) is the deployment's env, ahead of
344
+ `env.server.ts`. `q.raw` is written in the dialect's own SQL, so an app running in two modes avoids it.
345
+ - **Backups run through the image's own `main.js`**: `bun main.js ops snapshot` / `ops restore` (offline), and
346
+ `/_akan/ops/*` for a control plane, mounted only when `AKAN_OPS_PUBLIC_KEY` is set and authenticated by a short
347
+ EdDSA token that key verifies. `GET /_akan/app/info` is public. `operationMode` gates none of it.
340
348
 
341
349
  ## React Components And Styling (`**/*.tsx`)
342
350
 
@@ -359,7 +367,7 @@ Full contract: `get_guideline` with `runtimeRule`, or `akan guideline show runti
359
367
  nothing — they covered five control types, keyed on the translated label, and restored over server data.
360
368
  - Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming `className`, and merge the caller last: `cn("base classes", cond && "extra", className)`. `cn` comes from `akanjs/client` (token-aware tailwind-merge) and is the only class-combining function — no `clsx` (removed), no raw `twMerge` imports, no object syntax (`{ x: cond }` → `cond && "x"`).
361
369
  - Multi-slot components take extra named props (`wrapperClassName`, `bodyClassName`), never a `classNames` object.
362
- - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. Escalate the map to `webkit/` when a second module needs it.
370
+ - Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.XStatus["value"]]: string }`. Do not use `Record<...>`. When a second module needs it, the classes become a variant axis of a recipe in `ui/Recipe/`; only a map from the enum to another recipe's variant name (`BadgeVariants["variant"]`) moves to `webkit/`.
363
371
  - Use `<Link>` from `akanjs/ui` for internal navigation; `<a>` only for `mailto:` and external links.
364
372
 
365
373
  ## Naming And Language
@@ -793,7 +801,8 @@ Full contract — the trigger-maintained mirror, tokenizer changes, `AKAN_SEARCH
793
801
  backstop, including for a `text` field *underneath* one of those. Do not work around either.
794
802
  - The role works on a relation (`image: field(File, { text: "thumb" })`) and on an array; an array of objects
795
803
  indexes by leaf key. A field inside a `Map` indexes nothing — there is no fixed path to extract it from.
796
- - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
804
+ - Search runs in every database mode — fts5 on SQLite/libSQL, a weighted `tsvector` (`pg_trgm` for `trigram`) on
805
+ Postgres. The same text matches the same documents; only the order may differ on Postgres.
797
806
 
798
807
  ### Image & File Fields
799
808
 
@@ -918,10 +927,13 @@ export default page()
918
927
  | `common/` | pure, isomorphic, zero-dependency; may import only sibling `common/*` and `akanjs/base`. Cannot import `Err`, so keep throwing code out of it. | camelCase file, filename equals the single export |
919
928
  | `webkit/` | touches `window` / `navigator` / Capacitor, or is a React hook | `use<Thing>.tsx` — `.tsx` even with no JSX |
920
929
  | `srvkit/` | touches `node:*`, `Bun`, `process.env`, a secret, or a server SDK | camelCase file, PascalCase class |
921
- | `ui/` | renders JSX and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`) |
930
+ | `ui/` | renders JSX, or defines a look (a recipe in `Recipe/`, a lib's `tokens.css`), and is not bound to one model | PascalCase component, camelCase sidecar (`swipeCard.util.ts`), `Recipe/<name>.ts` |
922
931
  | `plugin/` | build- or CLI-time `AkanPlugin` | `<name>.plugin.ts`, registered in `akan.config.ts` |
923
932
 
924
933
  - Hooks return a named object of async closures, never a tuple.
934
+ - `ui/` is the presentation layer — markup and the looks it is built from — so a recipe lives there although it is
935
+ neither a component nor a hook. Not `webkit/`: its hooks are `"use client"`, the opposite signal for a function
936
+ server components call. Not `common/`: it cannot import `akanjs/ui`, where `recipe` / `tv` come from.
925
937
  - `libs/<lib>/ui/tokens.css` is the one CSS file a lib owns: plain `:root` custom properties for colors that must **not** follow the theme (a vendor brand color, a fixed surface). Every app whose pages reach that lib compiles it automatically, ahead of the app's own stylesheets, so nothing is imported by hand and no app can forget it. Reference them as `bg-[var(--kakao)]`; `@theme` extensions stay in the app stylesheet, because the color vocabulary is closed per stylesheet. Theme-following colors are the app's, not the lib's.
926
938
  - A layer-root `index.ts` is generated, but a `ui/<Folder>/index.tsx` that builds a namespace is hand-written source. The distinguishing test is that a generated barrel contains nothing but `export * from "./X";` lines.
927
939
  - `ui/<Folder>/index_.tsx` (trailing underscore) is the `"use client"` + `lazy()` boundary, with a server-safe `index.tsx` beside it. Collapsing the pair into one file breaks RSC.
@@ -90,14 +90,15 @@ with it. `conventions` carries the invariants — this is the full contract behi
90
90
  - **A slice endpoint never reaches "unspecified".** The resolver fills `latest` before the query is built, so a
91
91
  client asking for the score order has to name `relevance`; leaving `sort` off gets `latest`, not relevance.
92
92
  - Scope a search with `columns` (`q.search(text, { columns: ["title"] })`) and re-weight with `weights`, a tuple of
93
- finite numbers positional over `["title", "desc", "tag", "filter"]`.
93
+ finite, non-negative numbers positional over `["title", "desc", "tag", "filter"]` — Postgres ranks by weight
94
+ class and has no reading for a negative one.
94
95
 
95
96
 
96
97
  ## Text Search Fields — the `text` role
97
98
 
98
99
  - A field joins the full-text index by declaring one of five roles: `field(String, { text: "title" })`, and likewise
99
100
  `"desc"`, `"tag"`, `"thumb"`, `"filter"`. Nothing else opts a field in, and there is no per-model switch.
100
- - Pick the role by what the value *is*, because `bm25` weights them positionally (`title` 10, `tag` 3, `desc` 1,
101
+ - Pick the role by what the value *is*, because the rank weights them positionally (`title` 10, `tag` 3, `desc` 1,
101
102
  `filter` 0): `title` is the one line a human scans for, `desc` is prose, `tag` is a keyword list, `filter` is a
102
103
  scoping value (status, owner, role) that must be matchable but must never outrank a real title hit.
103
104
  - `thumb` is mirrored for rendering a hit and is **not** indexed — never expect it to match.
@@ -114,15 +115,24 @@ with it. `conventions` carries the invariants — this is the full contract behi
114
115
  - Declaring roles is all the wiring there is. Mirror rows are maintained by SQL triggers — not document hooks —
115
116
  because `updateOneByQuery` and friends fire no hooks, and most searchable-field mutations go through exactly that
116
117
  path.
117
- - Search runs on sqlite/libsql only. `q.search()` against Postgres throws, loudly, rather than returning every row.
118
+ - Search runs in every database mode: fts5 on SQLite/libSQL, a weighted `tsvector` under a GIN index on Postgres.
119
+ For the same text the same documents match — punctuation splits words the way unicode61 does, and a phrase never
120
+ runs from one column into the next — but the order may differ, since Postgres ranks without document frequency.
121
+ Assert on the match set, never on a Postgres order the weights alone do not decide.
122
+ - Postgres reads `AKAN_SEARCH_TOKENIZER` as `unicode61 [remove_diacritics 0|1|2]` (`simple`, with `unaccent` unless
123
+ `0`) or `trigram [case_sensitive 0|1]` (`pg_trgm`); any other spelling is refused. The extension is created when
124
+ the role may, otherwise a role that may runs `CREATE EXTENSION`. It indexes the first 20,000 characters of
125
+ `title`, `tag` and `filter` and 200,000 of `desc` — a tsvector refuses more than 1MB, and the refusal would fail
126
+ the model write. A database whose `LC_CTYPE` is `C` keeps non-ASCII case apart; use a UTF-8 ctype.
118
127
  - `AKAN_SEARCH_ENABLED=0` switches the index off process-wide; unset means on. It never deletes mirror data, and
119
128
  re-enabling reconciles every ref. **Give every process the same value** — a process cannot drop triggers for models
120
129
  it does not mount, so a mixed fleet leaves stale triggers behind.
121
130
  - The tokenizer is `AKAN_SEARCH_TOKENIZER` (or `database.search.tokenizer`, which wins), defaulting to
122
131
  `unicode61 remove_diacritics 2`. Changing it rebuilds the index from the mirror on the next boot — the model
123
- tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The
124
- rebuild takes no cross-process claim, so a fleet restarted at once repeats it in every process; stagger the
125
- restart when the mirror is large.
132
+ tables are never re-read — so it is a safe knob, unlike a `text` role change, which re-reads every row. The schema
133
+ is ensured in one write turn — a SQLite write transaction, a Postgres schema lock — so of a fleet restarted at
134
+ once the first rebuilds and the rest find it current. A SQLite process waits for that turn up to its
135
+ `busy_timeout`, so stagger the restart when the mirror is large.
126
136
 
127
137
  ## Cascade Remove — the `cascade` option
128
138
 
@@ -184,6 +184,80 @@ const config: AppConfig = { docker: "FROM oven/bun:1-slim\n…" }; // verbatim,
184
184
  - `AkanAppConfig.docker` is the resolved declaration; `AkanAppConfig.dockerfile` is the text `akan build` writes
185
185
  to `dist/apps/<app>/Dockerfile`.
186
186
 
187
+ ## Database Modes — `database` In `akan.config.ts`
188
+
189
+ | mode | database | cache · queue · pubsub | runs as |
190
+ |---|---|---|---|
191
+ | `single` | SQLite file | SQLite files (Solid) | one container |
192
+ | `multiple` | one SQLite file (WAL) on a host volume every container on that host opens | Redis | containers on one host |
193
+ | `cluster` | Postgres | Redis | pods on several servers |
194
+
195
+ - **`database: { modes: ["single", "cluster"] }` is what the build can run in**; the first is what `akan start`
196
+ runs. `akan build` carries the drivers of every declared mode (`multiple` → `bullmq`, `ioredis`; `cluster` adds
197
+ `postgres`).
198
+ - **A deployment names its mode with `AKAN_DATABASE_MODE`**, and only a declared one. With one declared mode it may
199
+ leave it out; with several it must name one — the fallback would be a SQLite file inside each pod. `akan start`,
200
+ `build`, `script` and `console` resolve it the same way from the shell. One image can serve an edge site in
201
+ `single` and a cloud cluster in `cluster`: declare both.
202
+ - **Where the data lives is the deployment's env, ahead of `env.server.ts`**: `SQLITE_DATABASE_PATH`,
203
+ `AKAN_SOLID_DB_PATH`, `POSTGRES_URL` (or `POSTGRES_HOST`/`PORT`/`DATABASE`/`USER`/`PASSWORD`),
204
+ `POSTGRES_INSIGHT_URL`, `LIBSQL_URL`. `REDIS_URI` is required outside local development. Pool size, SSL and
205
+ prepared statements ride the Postgres URL (`?max=20&ssl=require`, `prepare=false` behind PgBouncer).
206
+ - **Uploads**: a deployed `multiple`/`cluster` app uses object storage, or a volume every instance mounts with
207
+ `AKAN_STORAGE_SHARED=true`.
208
+ - **`runAdminSql` on Postgres reads as its own login role** holding base-column `SELECT` and nothing else:
209
+ `CREATE ROLE <name> LOGIN PASSWORD '…'` with no other role granted, `POSTGRES_INSIGHT_URL` logging in as it, and
210
+ `GRANT USAGE ON SCHEMA` from the DBA when the app does not own its schema. Every model table grants the role its
211
+ base columns at boot. Without it the console refuses on Postgres; `_doc` is never readable either way.
212
+ - **`akan dbup`** starts what the workspace's apps declare (`--mode multiple` is Redis, `cluster` adds Postgres).
213
+ **`akan db-export <app>` / `akan db-import <app>`** move every model table as stored rows through NDJSON files
214
+ (`--dir`, default `local/transfer`) between whatever the shell's `AKAN_DATABASE_MODE` points at; a rerun
215
+ replaces rows, search is rebuilt after, and sessions, queued jobs and uploaded files do not move.
216
+ - **`q.raw(sql)` is the dialect's own SQL** — `json_extract("_doc", '$.f')` on SQLite, `("_doc" #>> '{f}')` on
217
+ Postgres. An app that runs in both modes avoids it.
218
+
219
+ ## Operations — Snapshots, Restore And The `/_akan/ops` Channel
220
+
221
+ `GET /_akan/app/info` answers `{ appName, repoName, environment, operationMode }` on every deployment — the same
222
+ `AKAN_PUBLIC_*` values the client bundle already carries — so a control plane can check which app a host is
223
+ serving without a key. Everything else is behind the ops channel.
224
+
225
+ | surface | what it does |
226
+ |---|---|
227
+ | `bun main.js ops snapshot [--id] [--out] [--include-solid] [--json]` | `VACUUM INTO` a consistent copy, `integrity_check`, sha256, gzip, optional age encryption, `manifest.json` |
228
+ | `bun main.js ops restore <manifest.json> [--identity <file>]` | offline only: verify, stage, keep the old file as `<db>.pre-restore-<ts>` (its `-wal`/`-shm` beside it), swap |
229
+ | `GET /_akan/ops/info` | the public info plus `akanVersion`, `buildId`, `serverMode`, `databaseMode`, `solo`, `startedAt`, `replicaIdx` |
230
+ | `POST /_akan/ops/snapshot` `{ id, includeSolid?, uploadUrls: { main, solid?, manifest } }` | 202; a child process snapshots, then PUTs each file and the manifest **last** |
231
+ | `GET /_akan/ops/snapshot/:id` | `running` → `uploading` → `done` \| `failed`; kept in memory, so a restart answers 404 |
232
+
233
+ - **The entry is the image's own `main.js`** — a production image carries no `akan` CLI. `AkanApp.start()` hands
234
+ `argv[2] === "ops"` to the command before it boots anything. It reads the container's env — the `AKAN_PUBLIC_*`
235
+ identity and the database paths — so run it with that env (`docker compose exec`), never a bare shell's.
236
+ - **Restore refuses while an app answers `/_akan/app/health` on `PORT`** and names it by its `/_akan/app/info`;
237
+ `--force` is for a port some other app holds.
238
+ - **The channel exists only when `AKAN_OPS_PUBLIC_KEY` is set** (Ed25519: PEM, JWK, base64 SPKI or raw 32 bytes).
239
+ The control plane signs a short EdDSA JWS — `aud`, `iat`, `exp` at most 300s after `iat`, a `jti` the process
240
+ refuses twice — and the host holds only the public half, so a machine someone carries off mints nothing.
241
+ `aud` is `<appName>/<environment>`, or `<appName>/<environment>/<AKAN_OPS_INSTANCE>` when that is set, and then
242
+ only that: a token minted for the app as a whole would replay across every host sharing the name. 30 calls a
243
+ minute per process; every refusal is the same 401. A malformed key leaves the app serving with the channel off.
244
+ - **The app holds no storage credential.** It PUTs to the presigned URLs it was handed (`application/octet-stream`,
245
+ the manifest `application/json`), plain http only to loopback. The manifest landing is the commit marker.
246
+ - **Snapshots cover the SQLite modes only.** `single` copies the main file and, with `includeSolid`, the Solid file;
247
+ `multiple` copies the main file; `cluster` is refused. Paths follow the adaptors' env precedence
248
+ (`SQLITE_DATABASE_PATH`, `AKAN_SOLID_DB_PATH`, `AKAN_SQLITE_DIR`); a solo process uses the path its adaptor
249
+ actually opened, so a gateway deployment that moved the file in `env.server.ts` names it in the env too.
250
+ - **`AKAN_BACKUP_RECIPIENT=age1…[,age1…]` encrypts before upload** — age v1 X25519, so `age -d -i key.txt` opens
251
+ it. Restore takes `--identity` / `AKAN_BACKUP_IDENTITY_FILE`, or a file an operator already decrypted with
252
+ `age -d -o <name without .age>`.
253
+ - **The host keeps the newest `AKAN_OPS_SNAPSHOT_KEEP` (default 2) snapshots** under `AKAN_OPS_SNAPSHOT_DIR`
254
+ (default `<db dir>/snapshots/<id>/`) — the copy an edge still has while its uplink or the bucket is down.
255
+ - **`buildId` is `AKAN_BUILD_ID` when the deployment sets it**, else the git sha `akan build` wrote into
256
+ `akan.build.json` beside `main.js` (`-dirty` when the tree was).
257
+ - **`operationMode` does not gate any of this** — the key does. `edge` means a production host outside the
258
+ cluster: the console refuses it like `cloud`, and `init` / `interval` / `cron` take `operationMode: ["edge"]` to
259
+ run only there. Everything else behaves as `cloud`; `local` is a developer machine.
260
+
187
261
  ## Shipped Assets — `assets` In `akan.config.ts`
188
262
 
189
263
  `akan build` copies the whole `public/` tree into `dist`, lib assets dereferenced, and that copy is the image.
@@ -3,7 +3,7 @@ import {
3
3
  CodegenLock,
4
4
  WorkspaceExecutor,
5
5
  AppExecutor
6
- } from "./index-bwxpk2cj.js";
6
+ } from "./index-crmeqnmf.js";
7
7
  import"./index-q0y2jq9j.js";
8
8
  import"./index-6wwzs1bc.js";
9
9
  import {
@@ -12,7 +12,7 @@ import {
12
12
  AutoImportSync,
13
13
  DevChangePlanner,
14
14
  DevGeneratedIndexSync
15
- } from "./index-w8kmx1b6.js";
15
+ } from "./index-89hy31kf.js";
16
16
  import {
17
17
  HmrWatcher,
18
18
  WatchRootResolver
@@ -173,6 +173,7 @@ class IncrementalBuilder {
173
173
  app: this.#app,
174
174
  routeId: msg.routeId,
175
175
  seeds: msg.seeds,
176
+ graphSeeds: msg.graphSeeds,
176
177
  artifact: this.#artifact,
177
178
  knownEntries: new Set(msg.knownEntries),
178
179
  discovery: this.#discovery
@@ -1,16 +1,16 @@
1
1
  // @bun
2
2
  import {
3
3
  jsonText
4
- } from "./index-yw6g3bbv.js";
4
+ } from "./index-2446k7b4.js";
5
5
  import {
6
6
  akanMcpInstallTargets
7
- } from "./index-rpgxs245.js";
7
+ } from "./index-18zybgkc.js";
8
8
  import {
9
9
  script
10
- } from "./index-ycy302an.js";
10
+ } from "./index-qdgzsnhg.js";
11
11
  import {
12
12
  ContextRunner2
13
- } from "./index-cjw1sj1x.js";
13
+ } from "./index-3jrp517k.js";
14
14
 
15
15
  // pkgs/@akanjs/cli/context/context.script.ts
16
16
  import { Logger } from "akanjs/common";
@@ -7,7 +7,7 @@ import {
7
7
  isScannedRootEntry,
8
8
  AppExecutor,
9
9
  LibExecutor
10
- } from "./index-bwxpk2cj.js";
10
+ } from "./index-crmeqnmf.js";
11
11
  import {
12
12
  FileSys
13
13
  } from "./index-q0y2jq9j.js";
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  workflowRunArtifactPath,
21
21
  workflowSyncDir
22
- } from "./index-yw6g3bbv.js";
22
+ } from "./index-2446k7b4.js";
23
23
 
24
24
  // pkgs/@akanjs/devkit/akanContext.ts
25
25
  import { readdir } from "fs/promises";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-ycy302an.js";
5
+ } from "./index-qdgzsnhg.js";
6
6
 
7
7
  // pkgs/@akanjs/cli/page/page.runner.ts
8
8
  class PageRunner extends runner("page") {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  AppExecutor,
4
4
  LibExecutor
5
- } from "./index-bwxpk2cj.js";
5
+ } from "./index-crmeqnmf.js";
6
6
 
7
7
  // pkgs/@akanjs/devkit/workflow/utils.ts
8
8
  var jsonText = (value, { trailingNewline = true } = {}) => `${JSON.stringify(value, null, 2)}${trailingNewline ? `
@@ -1,14 +1,14 @@
1
1
  // @bun
2
2
  import {
3
3
  AppExecutor
4
- } from "./index-bwxpk2cj.js";
4
+ } from "./index-crmeqnmf.js";
5
5
  import {
6
6
  jsonText,
7
7
  createWorkflowBaselineSummary,
8
8
  buildAkanModuleContextIndex,
9
9
  createWorkflowStepRegistry,
10
10
  toolingRolloutGate
11
- } from "./index-yw6g3bbv.js";
11
+ } from "./index-2446k7b4.js";
12
12
  import {
13
13
  buildResourceList,
14
14
  codexMcpConfigPath,
@@ -18,32 +18,32 @@ import {
18
18
  upsertCodexMcpServerBlock,
19
19
  renderDoctorText,
20
20
  AkanContextAnalyzer
21
- } from "./index-rpgxs245.js";
21
+ } from "./index-18zybgkc.js";
22
22
  import {
23
23
  isPlaceholderAppId2
24
- } from "./index-erfc1x40.js";
24
+ } from "./index-vqx8bdxz.js";
25
25
  import {
26
26
  CommandContainer,
27
27
  runner
28
- } from "./index-ycy302an.js";
28
+ } from "./index-qdgzsnhg.js";
29
29
  import {
30
30
  Prompter2
31
31
  } from "./index-s5rhm04y.js";
32
32
  import {
33
33
  RepairRunner
34
- } from "./index-ef228gzf.js";
34
+ } from "./index-53176s7j.js";
35
35
  import {
36
36
  WorkflowRunner
37
- } from "./index-zmj143x7.js";
37
+ } from "./index-txr7dj2g.js";
38
38
  import {
39
39
  ModuleScript
40
- } from "./index-pn8mhpps.js";
40
+ } from "./index-esk82rvk.js";
41
41
  import {
42
42
  PrimitiveScript
43
- } from "./index-d9fjz06h.js";
43
+ } from "./index-zqgj2rt2.js";
44
44
  import {
45
45
  ScalarScript
46
- } from "./index-q9c3c0hy.js";
46
+ } from "./index-4m929vvn.js";
47
47
  import {
48
48
  getMobileTargets
49
49
  } from "./index-47pj3aee.js";
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-ycy302an.js";
5
+ } from "./index-qdgzsnhg.js";
6
6
  import {
7
7
  generatedFilesForSync,
8
8
  createPassedPrimitiveReport,
9
9
  scalarChangedFiles
10
- } from "./index-yw6g3bbv.js";
10
+ } from "./index-2446k7b4.js";
11
11
  import {
12
12
  pluralizeName
13
13
  } from "./index-nz8prx9g.js";
@@ -5,13 +5,13 @@ import {
5
5
  writeGeneratedSyncState,
6
6
  createRepairReport,
7
7
  renderRepairReport
8
- } from "./index-yw6g3bbv.js";
8
+ } from "./index-2446k7b4.js";
9
9
  import {
10
10
  AkanContextAnalyzer
11
- } from "./index-rpgxs245.js";
11
+ } from "./index-18zybgkc.js";
12
12
  import {
13
13
  runner
14
- } from "./index-ycy302an.js";
14
+ } from "./index-qdgzsnhg.js";
15
15
 
16
16
  // pkgs/@akanjs/cli/repair/repair.runner.ts
17
17
  var commandForShell = (command) => command.startsWith("akan ") ? `bun run ${command}` : command;
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // pkgs/@akanjs/devkit/codeAgent/agent/akanCodeModel.ts
3
- var akanCodeDefaultModel2 = { provider: "deepseek", id: "deepseek-v4-flash" };
3
+ var akanCodeDefaultModel2 = { provider: "deepseek", id: "deepseek-flash" };
4
4
  var catalogueMissesImages = new Set(["deepseek/deepseek-v4-flash"]);
5
5
  var correctInputs = (model) => {
6
6
  if (!model || model.input.includes("image"))
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-ycy302an.js";
5
+ } from "./index-qdgzsnhg.js";
6
6
  import {
7
7
  LibExecutor
8
- } from "./index-bwxpk2cj.js";
8
+ } from "./index-crmeqnmf.js";
9
9
  import {
10
10
  LibSource,
11
11
  formatLibStatuses
@@ -1147,6 +1147,7 @@ var SSR_CLIENT_ALIAS_EXTERNALS = [
1147
1147
  class RouteClientBuilder {
1148
1148
  #app;
1149
1149
  #seeds;
1150
+ #graphSeeds;
1150
1151
  #knownEntries;
1151
1152
  #command;
1152
1153
  #discovery;
@@ -1154,20 +1155,22 @@ class RouteClientBuilder {
1154
1155
  constructor(options) {
1155
1156
  this.#app = options.app;
1156
1157
  this.#seeds = options.seeds;
1158
+ this.#graphSeeds = options.graphSeeds;
1157
1159
  this.#knownEntries = options.knownEntries ?? new Set;
1158
1160
  this.#command = options.command ?? "start";
1159
1161
  this.#discovery = options.discovery;
1160
1162
  this.#entries = options.entries;
1161
1163
  }
1162
1164
  async build() {
1163
- const discovered = this.#entries ?? await (this.#discovery ?? await GraphClientEntryDiscovery.create(this.#app)).discover(this.#seeds);
1164
- const entries = discovered.filter((e) => !this.#knownEntries.has(e));
1165
- if (entries.length === 0)
1165
+ const discovered = this.#entries ?? await (await this.#getDiscovery()).discover(this.#seeds);
1166
+ if (discovered.every((e) => this.#knownEntries.has(e)))
1166
1167
  return this.#emptyResult(discovered);
1168
+ const entries = this.#graphSeeds ? [...new Set([...discovered, ...await (await this.#getDiscovery()).discover(this.#graphSeeds)])].sort() : discovered.filter((e) => !this.#knownEntries.has(e));
1167
1169
  const bootstrapEntries = await this.#createBootstrapEntries(entries);
1168
1170
  const browserBundle = await this.#buildBrowserBundle(bootstrapEntries);
1169
1171
  const ssrBundle = await this.#buildSsrBundle(bootstrapEntries);
1170
1172
  const acceptedEntries = new Set(entries);
1173
+ const routeEntries = new Set(discovered);
1171
1174
  const manifestDelta = {};
1172
1175
  const ssrModuleMap = {};
1173
1176
  const clientDeps = new Set;
@@ -1195,18 +1198,23 @@ class RouteClientBuilder {
1195
1198
  deps.add(path6.resolve(dep));
1196
1199
  const sortedDeps = [...deps].sort();
1197
1200
  clientDepsByEntry[originalEntry] = sortedDeps;
1198
- for (const dep of sortedDeps)
1199
- clientDeps.add(dep);
1201
+ if (routeEntries.has(originalEntry))
1202
+ for (const dep of sortedDeps)
1203
+ clientDeps.add(dep);
1200
1204
  }
1201
1205
  return {
1202
1206
  manifestDelta,
1203
1207
  ssrManifestDelta: { moduleLoading: null, moduleMap: ssrModuleMap },
1204
- newEntries: entries,
1208
+ newEntries: entries.filter((e) => !this.#knownEntries.has(e)),
1205
1209
  discoveredEntries: discovered,
1206
1210
  clientDeps: [...clientDeps].sort(),
1207
1211
  clientDepsByEntry
1208
1212
  };
1209
1213
  }
1214
+ async#getDiscovery() {
1215
+ this.#discovery ??= await GraphClientEntryDiscovery.create(this.#app);
1216
+ return this.#discovery;
1217
+ }
1210
1218
  #emptyResult(discoveredEntries = []) {
1211
1219
  return {
1212
1220
  manifestDelta: {},
@@ -1440,6 +1448,7 @@ var LIB_BARRELS = {
1440
1448
  cnst: { barrel: "cnst", kind: "namespace" },
1441
1449
  srv: { barrel: "srv", kind: "namespace" }
1442
1450
  };
1451
+ var GLOBAL_NAMES = new Set(["fetch"]);
1443
1452
  var DOMAIN_DENYLIST = new Set([
1444
1453
  "Map",
1445
1454
  "Set",
@@ -1642,10 +1651,11 @@ var transformSource = (source, fileName, ctx, resolveExtra) => {
1642
1651
  const sf = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true, scriptKindFor(fileName));
1643
1652
  const bound = collectBoundNames(sf);
1644
1653
  const used = collectUsedReferences(sf);
1654
+ const bareGlobals = collectBareGlobalReferences(sf);
1645
1655
  const namedBySpecifier = new Map;
1646
1656
  const namespaceImports = [];
1647
1657
  for (const name of used) {
1648
- if (bound.has(name))
1658
+ if (bound.has(name) || bareGlobals.has(name))
1649
1659
  continue;
1650
1660
  const target = targetFor(name, ctx) ?? resolveExtra?.(name) ?? null;
1651
1661
  if (!target)
@@ -1743,6 +1753,19 @@ var collectUsedReferences = (sf) => {
1743
1753
  ts2.forEachChild(sf, visit);
1744
1754
  return used;
1745
1755
  };
1756
+ var collectBareGlobalReferences = (sf) => {
1757
+ const bare = new Set;
1758
+ const visit = (node) => {
1759
+ if (ts2.isIdentifier(node) && GLOBAL_NAMES.has(node.text) && isReferencePosition(node)) {
1760
+ const parent = node.parent;
1761
+ if (!(parent && ts2.isPropertyAccessExpression(parent) && parent.expression === node))
1762
+ bare.add(node.text);
1763
+ }
1764
+ ts2.forEachChild(node, visit);
1765
+ };
1766
+ ts2.forEachChild(sf, visit);
1767
+ return bare;
1768
+ };
1746
1769
  var isReferencePosition = (node) => {
1747
1770
  const parent = node.parent;
1748
1771
  if (!parent)
@@ -112,7 +112,7 @@ class SubagentPool2 {
112
112
  }
113
113
  }
114
114
  async#run(id, kind, prompt, signal) {
115
- const { CodeAgent } = await import("./CodeAgent-0035exh3.js");
115
+ const { CodeAgent } = await import("./CodeAgent-6cm97kay.js");
116
116
  const budget = this.#options.parent.tools.subagent;
117
117
  const child = SubagentPool2.childOf(this.#options.parent, kind);
118
118
  const agent = await CodeAgent.create({
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  AsyncDefaultExportDetector,
4
4
  resolveSsrPageEntriesForApp
5
- } from "./index-bwxpk2cj.js";
5
+ } from "./index-crmeqnmf.js";
6
6
  import {
7
7
  BarrelAnalyzer,
8
8
  createBarrelImportsPlugin,
@@ -13,7 +13,7 @@ import {
13
13
  ClientEntriesBundler,
14
14
  VENDOR_SPECIFIERS,
15
15
  RouteClientBuilder
16
- } from "./index-w8kmx1b6.js";
16
+ } from "./index-89hy31kf.js";
17
17
  var __require = import.meta.require;
18
18
 
19
19
  // pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
@@ -419,7 +419,7 @@ class CssImportResolver {
419
419
  }
420
420
  async#firstExisting(basePath) {
421
421
  for (const suffix of CSS_IMPORT_EXTS) {
422
- const candidate = `${basePath}${suffix}`;
422
+ const candidate = path4.normalize(`${basePath}${suffix}`);
423
423
  if (await Bun.file(candidate).exists())
424
424
  return candidate;
425
425
  }