@celestea/studio 2.7.1

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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +111 -0
  3. package/dist/app.js +219 -0
  4. package/dist/auth/api-token.d.ts +76 -0
  5. package/dist/auth/api-token.js +172 -0
  6. package/dist/auth/htpasswd.d.ts +22 -0
  7. package/dist/auth/htpasswd.js +61 -0
  8. package/dist/auth/index.d.ts +14 -0
  9. package/dist/auth/index.js +14 -0
  10. package/dist/auth/page.d.ts +19 -0
  11. package/dist/auth/page.js +73 -0
  12. package/dist/auth/rate-limit.d.ts +27 -0
  13. package/dist/auth/rate-limit.js +47 -0
  14. package/dist/auth/token.d.ts +49 -0
  15. package/dist/auth/token.js +112 -0
  16. package/dist/config.d.ts +78 -0
  17. package/dist/config.js +72 -0
  18. package/dist/deployment.d.ts +92 -0
  19. package/dist/deployment.js +154 -0
  20. package/dist/fake-runtime-adapter.d.ts +37 -0
  21. package/dist/fake-runtime-adapter.js +276 -0
  22. package/dist/handlers/auth.d.ts +24 -0
  23. package/dist/handlers/auth.js +155 -0
  24. package/dist/handlers/common.d.ts +76 -0
  25. package/dist/handlers/common.js +127 -0
  26. package/dist/handlers/config-shape.d.ts +76 -0
  27. package/dist/handlers/config-shape.js +227 -0
  28. package/dist/handlers/config.d.ts +26 -0
  29. package/dist/handlers/config.js +161 -0
  30. package/dist/handlers/context-shape.d.ts +33 -0
  31. package/dist/handlers/context-shape.js +59 -0
  32. package/dist/handlers/dialog.d.ts +22 -0
  33. package/dist/handlers/dialog.js +292 -0
  34. package/dist/handlers/exec.d.ts +22 -0
  35. package/dist/handlers/exec.js +131 -0
  36. package/dist/handlers/fs-read.d.ts +63 -0
  37. package/dist/handlers/fs-read.js +159 -0
  38. package/dist/handlers/fs.d.ts +57 -0
  39. package/dist/handlers/fs.js +153 -0
  40. package/dist/handlers/grants-shape.d.ts +45 -0
  41. package/dist/handlers/grants-shape.js +130 -0
  42. package/dist/handlers/grants.d.ts +21 -0
  43. package/dist/handlers/grants.js +267 -0
  44. package/dist/handlers/health.d.ts +37 -0
  45. package/dist/handlers/health.js +139 -0
  46. package/dist/handlers/index.d.ts +40 -0
  47. package/dist/handlers/index.js +85 -0
  48. package/dist/handlers/permissions.d.ts +9 -0
  49. package/dist/handlers/permissions.js +183 -0
  50. package/dist/handlers/plugins.d.ts +18 -0
  51. package/dist/handlers/plugins.js +23 -0
  52. package/dist/handlers/prompts.d.ts +12 -0
  53. package/dist/handlers/prompts.js +120 -0
  54. package/dist/handlers/providers.d.ts +12 -0
  55. package/dist/handlers/providers.js +178 -0
  56. package/dist/handlers/questions.d.ts +23 -0
  57. package/dist/handlers/questions.js +115 -0
  58. package/dist/handlers/session-model.d.ts +38 -0
  59. package/dist/handlers/session-model.js +105 -0
  60. package/dist/handlers/session-move.d.ts +22 -0
  61. package/dist/handlers/session-move.js +188 -0
  62. package/dist/handlers/session-tools.d.ts +19 -0
  63. package/dist/handlers/session-tools.js +75 -0
  64. package/dist/handlers/sessions.d.ts +32 -0
  65. package/dist/handlers/sessions.js +242 -0
  66. package/dist/handlers/usage.d.ts +26 -0
  67. package/dist/handlers/usage.js +77 -0
  68. package/dist/handlers/worker.d.ts +12 -0
  69. package/dist/handlers/worker.js +82 -0
  70. package/dist/handlers/workspaces.d.ts +12 -0
  71. package/dist/handlers/workspaces.js +88 -0
  72. package/dist/harness.test-util.d.ts +101 -0
  73. package/dist/harness.test-util.js +176 -0
  74. package/dist/index.d.ts +33 -0
  75. package/dist/index.js +33 -0
  76. package/dist/main.d.ts +13 -0
  77. package/dist/main.js +35 -0
  78. package/dist/plugins.d.ts +105 -0
  79. package/dist/plugins.js +97 -0
  80. package/dist/question-registry.d.ts +124 -0
  81. package/dist/question-registry.js +212 -0
  82. package/dist/question-rows.d.ts +32 -0
  83. package/dist/question-rows.js +43 -0
  84. package/dist/replay/compare.d.ts +47 -0
  85. package/dist/replay/compare.js +89 -0
  86. package/dist/replay/e2e-replay.d.ts +46 -0
  87. package/dist/replay/e2e-replay.js +91 -0
  88. package/dist/replay/expect-compact.d.ts +31 -0
  89. package/dist/replay/expect-compact.js +92 -0
  90. package/dist/replay/fixtures.d.ts +52 -0
  91. package/dist/replay/fixtures.js +53 -0
  92. package/dist/replay/host.d.ts +32 -0
  93. package/dist/replay/host.js +62 -0
  94. package/dist/replay/index.d.ts +16 -0
  95. package/dist/replay/index.js +16 -0
  96. package/dist/replay/probes.d.ts +55 -0
  97. package/dist/replay/probes.js +225 -0
  98. package/dist/replay/report.d.ts +13 -0
  99. package/dist/replay/report.js +103 -0
  100. package/dist/replay/session-e2e.d.ts +44 -0
  101. package/dist/replay/session-e2e.js +91 -0
  102. package/dist/routes.d.ts +47 -0
  103. package/dist/routes.js +59 -0
  104. package/dist/runtime/attachments-llm.d.ts +13 -0
  105. package/dist/runtime/attachments-llm.js +37 -0
  106. package/dist/runtime/boot-recovery.d.ts +32 -0
  107. package/dist/runtime/boot-recovery.js +71 -0
  108. package/dist/runtime/context-snapshot.d.ts +59 -0
  109. package/dist/runtime/context-snapshot.js +113 -0
  110. package/dist/runtime/engine-grants.d.ts +121 -0
  111. package/dist/runtime/engine-grants.js +338 -0
  112. package/dist/runtime/engine-permissions.d.ts +29 -0
  113. package/dist/runtime/engine-permissions.js +92 -0
  114. package/dist/runtime/engine-plugins.d.ts +189 -0
  115. package/dist/runtime/engine-plugins.js +292 -0
  116. package/dist/runtime/engine-profile.d.ts +28 -0
  117. package/dist/runtime/engine-profile.js +88 -0
  118. package/dist/runtime/engine-session.d.ts +9 -0
  119. package/dist/runtime/engine-session.js +9 -0
  120. package/dist/runtime/fallback-contract.d.ts +32 -0
  121. package/dist/runtime/fallback-contract.js +10 -0
  122. package/dist/runtime/fallback-host.d.ts +134 -0
  123. package/dist/runtime/fallback-host.js +299 -0
  124. package/dist/runtime/host-autowake.d.ts +79 -0
  125. package/dist/runtime/host-autowake.js +86 -0
  126. package/dist/runtime/image-downgrade.d.ts +49 -0
  127. package/dist/runtime/image-downgrade.js +90 -0
  128. package/dist/runtime/inbox-message.d.ts +11 -0
  129. package/dist/runtime/inbox-message.js +20 -0
  130. package/dist/runtime/index.d.ts +17 -0
  131. package/dist/runtime/index.js +17 -0
  132. package/dist/runtime/ledger-view.d.ts +40 -0
  133. package/dist/runtime/ledger-view.js +56 -0
  134. package/dist/runtime/llm-assembly.d.ts +66 -0
  135. package/dist/runtime/llm-assembly.js +114 -0
  136. package/dist/runtime/offline-llm.d.ts +71 -0
  137. package/dist/runtime/offline-llm.js +132 -0
  138. package/dist/runtime/provider-target.d.ts +10 -0
  139. package/dist/runtime/provider-target.js +10 -0
  140. package/dist/runtime/question-host.d.ts +33 -0
  141. package/dist/runtime/question-host.js +41 -0
  142. package/dist/runtime/question-view.d.ts +48 -0
  143. package/dist/runtime/question-view.js +76 -0
  144. package/dist/runtime/real-runtime-adapter.d.ts +95 -0
  145. package/dist/runtime/real-runtime-adapter.js +553 -0
  146. package/dist/runtime/recovery-audit.d.ts +81 -0
  147. package/dist/runtime/recovery-audit.js +95 -0
  148. package/dist/runtime/recovery-view.d.ts +36 -0
  149. package/dist/runtime/recovery-view.js +63 -0
  150. package/dist/runtime/session-compose.d.ts +235 -0
  151. package/dist/runtime/session-compose.js +350 -0
  152. package/dist/runtime/session-grants.d.ts +36 -0
  153. package/dist/runtime/session-grants.js +69 -0
  154. package/dist/runtime/session-lifecycle.d.ts +40 -0
  155. package/dist/runtime/session-lifecycle.js +86 -0
  156. package/dist/runtime/session-publisher.d.ts +40 -0
  157. package/dist/runtime/session-publisher.js +48 -0
  158. package/dist/runtime/session-release.d.ts +53 -0
  159. package/dist/runtime/session-release.js +80 -0
  160. package/dist/runtime/test-util.d.ts +78 -0
  161. package/dist/runtime/test-util.js +162 -0
  162. package/dist/runtime/watchdog-view.d.ts +33 -0
  163. package/dist/runtime/watchdog-view.js +45 -0
  164. package/dist/runtime/worker-bridge.d.ts +76 -0
  165. package/dist/runtime/worker-bridge.js +156 -0
  166. package/dist/runtime/worker-live.d.ts +22 -0
  167. package/dist/runtime/worker-live.js +25 -0
  168. package/dist/runtime/worker-recovery.d.ts +35 -0
  169. package/dist/runtime/worker-recovery.js +64 -0
  170. package/dist/runtime/worker-table.d.ts +58 -0
  171. package/dist/runtime/worker-table.js +70 -0
  172. package/dist/runtime-adapter.d.ts +421 -0
  173. package/dist/runtime-adapter.js +76 -0
  174. package/dist/serial-queue.d.ts +16 -0
  175. package/dist/serial-queue.js +22 -0
  176. package/dist/server.d.ts +44 -0
  177. package/dist/server.js +119 -0
  178. package/dist/settings.d.ts +23 -0
  179. package/dist/settings.js +31 -0
  180. package/dist/sse.d.ts +67 -0
  181. package/dist/sse.js +186 -0
  182. package/dist/static.d.ts +20 -0
  183. package/dist/static.js +120 -0
  184. package/dist/store/builtin-sections.d.ts +52 -0
  185. package/dist/store/builtin-sections.js +118 -0
  186. package/dist/store/celestea-home.d.ts +12 -0
  187. package/dist/store/celestea-home.js +11 -0
  188. package/dist/store/fs-json.d.ts +51 -0
  189. package/dist/store/fs-json.js +119 -0
  190. package/dist/store/grants-audit.d.ts +84 -0
  191. package/dist/store/grants-audit.js +109 -0
  192. package/dist/store/grants-service.d.ts +44 -0
  193. package/dist/store/grants-service.js +41 -0
  194. package/dist/store/grants-tokens.d.ts +62 -0
  195. package/dist/store/grants-tokens.js +105 -0
  196. package/dist/store/grants.d.ts +113 -0
  197. package/dist/store/grants.js +238 -0
  198. package/dist/store/index.d.ts +36 -0
  199. package/dist/store/index.js +36 -0
  200. package/dist/store/mode.d.ts +35 -0
  201. package/dist/store/mode.js +41 -0
  202. package/dist/store/permissions.d.ts +61 -0
  203. package/dist/store/permissions.js +139 -0
  204. package/dist/store/prompts-compose.d.ts +56 -0
  205. package/dist/store/prompts-compose.js +62 -0
  206. package/dist/store/prompts-template.d.ts +26 -0
  207. package/dist/store/prompts-template.js +99 -0
  208. package/dist/store/prompts.d.ts +142 -0
  209. package/dist/store/prompts.js +273 -0
  210. package/dist/store/provider-probe.d.ts +81 -0
  211. package/dist/store/provider-probe.js +154 -0
  212. package/dist/store/providers.d.ts +97 -0
  213. package/dist/store/providers.js +219 -0
  214. package/dist/store/result.d.ts +29 -0
  215. package/dist/store/result.js +30 -0
  216. package/dist/store/session-id.d.ts +119 -0
  217. package/dist/store/session-id.js +220 -0
  218. package/dist/store/session-meta.d.ts +41 -0
  219. package/dist/store/session-meta.js +66 -0
  220. package/dist/store/session-ops.d.ts +109 -0
  221. package/dist/store/session-ops.js +264 -0
  222. package/dist/store/session-tools.d.ts +42 -0
  223. package/dist/store/session-tools.js +69 -0
  224. package/dist/store/sessions.d.ts +162 -0
  225. package/dist/store/sessions.js +307 -0
  226. package/dist/store/validate.d.ts +13 -0
  227. package/dist/store/validate.js +38 -0
  228. package/dist/store/workspaces.d.ts +67 -0
  229. package/dist/store/workspaces.js +228 -0
  230. package/dist/user-questions.d.ts +58 -0
  231. package/dist/user-questions.js +157 -0
  232. package/dist/version.d.ts +14 -0
  233. package/dist/version.js +52 -0
  234. package/package.json +38 -0
  235. package/webdist/assets/index-Bngs7gUm.js +98 -0
  236. package/webdist/assets/index-Bu6ci_rN.css +1 -0
  237. package/webdist/assets/katex-CCbS2qAY.js +257 -0
  238. package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
  239. package/webdist/build-meta.json +7 -0
  240. package/webdist/index.html +261 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * `providers.json` store — `contracts/data-files/providers.schema.json`,
3
+ * `src/providers.rs:52-86,219-241,673-797`.
4
+ *
5
+ * Security rules enforced here, not by convention:
6
+ * - the file holds PLAINTEXT keys and is written through
7
+ * `OpenOptions::mode(0600)` + fsync + rename, i.e. EVERY save re-asserts
8
+ * 0600 (see `save()`);
9
+ * - `public_view` has NO `api_key` key at all (not null, not empty) — the
10
+ * type returned by every read path simply has no such field, so a handler
11
+ * cannot leak it by accident;
12
+ * - update semantics: `api_key` absent/null/blank KEEPS the stored key (the
13
+ * only keep-on-default field), while `models` absent CLEARS the list,
14
+ * `note` absent clears to "", `request_format` absent silently resets to
15
+ * chat_completions and `name` absent falls back to `id`.
16
+ */
17
+ import { readJsonIfExists, writeJsonAtomic } from "./fs-json.js";
18
+ import { badRequest, errText, fail, notFound, ok } from "./result.js";
19
+ import { isHttpUrl } from "./validate.js";
20
+ export const REQUEST_FORMATS = ["chat_completions", "responses", "anthropic_messages"];
21
+ export const PROVIDERS_MODE = 0o600;
22
+ function asString(v, fallback = "") {
23
+ return typeof v === "string" ? v : fallback;
24
+ }
25
+ function nullableInt(v) {
26
+ return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
27
+ }
28
+ function parseModel(raw) {
29
+ if (typeof raw !== "object" || raw === null)
30
+ return null;
31
+ const rec = raw;
32
+ const id = asString(rec["id"]);
33
+ if (id === "")
34
+ return null;
35
+ const efforts = Array.isArray(rec["reasoning_efforts"]) ? rec["reasoning_efforts"].filter((x) => typeof x === "string") : [];
36
+ const model = {
37
+ id,
38
+ name: asString(rec["name"], id) || id,
39
+ reasoning_efforts: efforts,
40
+ context_window: nullableInt(rec["context_window"]),
41
+ max_output_tokens: nullableInt(rec["max_output_tokens"]),
42
+ };
43
+ // W804: preserved only when present, so a legacy row is re-serialized verbatim.
44
+ const input = modalityList(rec["input_modalities"]);
45
+ if (input !== undefined)
46
+ model.input_modalities = input;
47
+ const output = modalityList(rec["output_modalities"]);
48
+ if (output !== undefined)
49
+ model.output_modalities = output;
50
+ return model;
51
+ }
52
+ /** W804: a non-empty array of modality strings, else undefined (absent). */
53
+ function modalityList(v) {
54
+ if (!Array.isArray(v))
55
+ return undefined;
56
+ const out = v.filter((x) => typeof x === "string" && x !== "");
57
+ return out.length === 0 ? undefined : out;
58
+ }
59
+ function parseRow(raw, file) {
60
+ const rec = (raw ?? {});
61
+ const id = asString(rec["id"]);
62
+ const base_url = asString(rec["base_url"]);
63
+ const fmt = asString(rec["request_format"]);
64
+ if (id === "" || base_url === "" || !REQUEST_FORMATS.includes(fmt)) {
65
+ throw new Error(`providers.json '${file}' is malformed: provider row needs id, base_url and a valid request_format`);
66
+ }
67
+ const models = Array.isArray(rec["models"]) ? rec["models"].map(parseModel).filter((m) => m !== null) : [];
68
+ return {
69
+ id,
70
+ name: asString(rec["name"], id) || id,
71
+ note: asString(rec["note"]),
72
+ base_url,
73
+ request_format: fmt,
74
+ api_key: typeof rec["api_key"] === "string" ? rec["api_key"] : null,
75
+ models,
76
+ };
77
+ }
78
+ function load(file) {
79
+ const out = readJsonIfExists(file);
80
+ if (!out.exists)
81
+ return { providers: [], default_model: null };
82
+ if (out.error !== undefined)
83
+ throw new Error(`providers.json '${file}' is malformed: ${out.error}`);
84
+ const rec = (typeof out.value === "object" && out.value !== null ? out.value : {});
85
+ const rows = Array.isArray(rec["providers"]) ? rec["providers"] : [];
86
+ const def = rec["default_model"];
87
+ return {
88
+ providers: rows.map((r) => parseRow(r, file)),
89
+ default_model: typeof def === "string" && def !== "" ? def : null,
90
+ };
91
+ }
92
+ /** Normalize a base_url for the keyless-borrow comparison (trim + no trailing '/'). */
93
+ export function normalizeBaseUrl(url) {
94
+ return url.trim().replace(/\/+$/, "");
95
+ }
96
+ export class ProvidersStore {
97
+ file;
98
+ data;
99
+ constructor(file) {
100
+ this.file = file;
101
+ this.data = load(file);
102
+ }
103
+ /** Internal rows — used by the probe and by `/api/config` model listing. */
104
+ rows() {
105
+ return this.data.providers;
106
+ }
107
+ find(id) {
108
+ return this.data.providers.find((p) => p.id === id);
109
+ }
110
+ defaultModel() {
111
+ return this.data.default_model;
112
+ }
113
+ /** Every id listed by any provider (used to validate `default_model`). */
114
+ modelIds() {
115
+ const out = new Set();
116
+ for (const p of this.data.providers)
117
+ for (const m of p.models)
118
+ out.add(m.id);
119
+ return [...out];
120
+ }
121
+ view(p) {
122
+ const def = this.data.default_model;
123
+ return {
124
+ id: p.id,
125
+ name: p.name,
126
+ note: p.note,
127
+ base_url: p.base_url,
128
+ request_format: p.request_format,
129
+ models: p.models.map((m) => ({ ...m, reasoning_efforts: [...m.reasoning_efforts] })),
130
+ is_default: def !== null && p.models.some((m) => m.id === def),
131
+ has_key: typeof p.api_key === "string" && p.api_key !== "",
132
+ };
133
+ }
134
+ viewOf(p) {
135
+ return this.view(p);
136
+ }
137
+ list() {
138
+ return this.data.providers.map((p) => this.view(p));
139
+ }
140
+ /** GET /api/providers body (no `ok`, never a key). */
141
+ response() {
142
+ return { providers: this.list(), default_model: this.data.default_model };
143
+ }
144
+ save() {
145
+ try {
146
+ writeJsonAtomic(this.file, { providers: this.data.providers, default_model: this.data.default_model }, { mode: PROVIDERS_MODE, fsync: true });
147
+ return ok(undefined);
148
+ }
149
+ catch (e) {
150
+ return fail(500, `providers serialize failed: ${errText(e)}`);
151
+ }
152
+ }
153
+ validate(req) {
154
+ const id = (req.id ?? "").trim();
155
+ if (id === "")
156
+ return badRequest("provider id must not be empty");
157
+ const base_url = (req.base_url ?? "").trim();
158
+ if (base_url === "")
159
+ return badRequest("base_url is required");
160
+ if (!isHttpUrl(base_url))
161
+ return badRequest("base_url must be an http:// or https:// URL");
162
+ const format = (req.request_format ?? "chat_completions");
163
+ if (!REQUEST_FORMATS.includes(format)) {
164
+ return badRequest(`invalid request_format '${format}': expected chat_completions | responses | anthropic_messages`);
165
+ }
166
+ const models = [];
167
+ for (const raw of req.models ?? []) {
168
+ const model = parseModel(raw);
169
+ if (model === null)
170
+ return badRequest("each model needs a non-empty id");
171
+ models.push(model);
172
+ }
173
+ return ok({ id, base_url, format, models });
174
+ }
175
+ /** POST /api/providers — upsert by id; returns the public view. */
176
+ upsert(req) {
177
+ const checked = this.validate(req);
178
+ if (!checked.ok)
179
+ return checked;
180
+ const { id, base_url, format, models } = checked.value;
181
+ const idx = this.data.providers.findIndex((p) => p.id === id);
182
+ const stored = idx >= 0 ? this.data.providers[idx] : undefined;
183
+ const asked = req.api_key;
184
+ const keepKey = asked === undefined || asked === null || asked.trim() === "";
185
+ const row = {
186
+ id,
187
+ name: (req.name ?? "").trim() === "" ? id : (req.name ?? "").trim(),
188
+ note: req.note ?? "",
189
+ base_url,
190
+ request_format: format,
191
+ api_key: keepKey ? (stored?.api_key ?? null) : asked,
192
+ models,
193
+ };
194
+ if (idx >= 0)
195
+ this.data.providers[idx] = row;
196
+ else
197
+ this.data.providers.push(row);
198
+ const saved = this.save();
199
+ if (!saved.ok)
200
+ return saved;
201
+ return ok(this.view(row));
202
+ }
203
+ /** POST /api/providers/{id}/delete — UNTRIMMED raw path value. */
204
+ remove(rawId) {
205
+ const idx = this.data.providers.findIndex((p) => p.id === rawId);
206
+ if (idx < 0)
207
+ return notFound(`unknown provider '${rawId}'`);
208
+ this.data.providers.splice(idx, 1);
209
+ if (this.data.default_model !== null && !this.modelIds().includes(this.data.default_model)) {
210
+ this.data.default_model = null;
211
+ }
212
+ return this.save();
213
+ }
214
+ /** POST /api/providers/default — persist the default model id. */
215
+ setDefaultModel(model) {
216
+ this.data.default_model = model;
217
+ return this.save();
218
+ }
219
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `StoreResult` — how every data-store operation reports failure.
3
+ *
4
+ * The HTTP layer must reproduce the contract error strings verbatim together with
5
+ * their status code, so a store never throws for an expected failure: it
6
+ * returns `{ok:false, status, error}` and the handler turns that into
7
+ * `{ok:false, error}` with the status. Store *bugs* still throw.
8
+ */
9
+ export interface StoreFailure {
10
+ ok: false;
11
+ /** HTTP status the contract assigns to this failure. */
12
+ status: number;
13
+ /** Verbatim contract text (`format!` template filled in). */
14
+ error: string;
15
+ /** Extra fields folded into the response body (fs/browse uses `path`). */
16
+ extra?: Record<string, unknown>;
17
+ }
18
+ export type StoreResult<T> = {
19
+ ok: true;
20
+ value: T;
21
+ } | StoreFailure;
22
+ export declare function ok<T>(value: T): StoreResult<T>;
23
+ export declare function fail<T>(status: number, error: string, extra?: Record<string, unknown>): StoreResult<T>;
24
+ export declare function badRequest<T>(error: string): StoreResult<T>;
25
+ export declare function notFound<T>(error: string): StoreResult<T>;
26
+ export declare function conflict<T>(error: string): StoreResult<T>;
27
+ export declare function serverError<T>(message: string): StoreResult<T>;
28
+ /** `Error` -> message, for the `{e}` placeholders in contract error strings. */
29
+ export declare function errText(e: unknown): string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `StoreResult` — how every data-store operation reports failure.
3
+ *
4
+ * The HTTP layer must reproduce the contract error strings verbatim together with
5
+ * their status code, so a store never throws for an expected failure: it
6
+ * returns `{ok:false, status, error}` and the handler turns that into
7
+ * `{ok:false, error}` with the status. Store *bugs* still throw.
8
+ */
9
+ export function ok(value) {
10
+ return { ok: true, value };
11
+ }
12
+ export function fail(status, error, extra) {
13
+ return extra === undefined ? { ok: false, status, error } : { ok: false, status, error, extra };
14
+ }
15
+ export function badRequest(error) {
16
+ return fail(400, error);
17
+ }
18
+ export function notFound(error) {
19
+ return fail(404, error);
20
+ }
21
+ export function conflict(error) {
22
+ return fail(409, error);
23
+ }
24
+ export function serverError(message) {
25
+ return fail(500, message);
26
+ }
27
+ /** `Error` -> message, for the `{e}` placeholders in contract error strings. */
28
+ export function errText(e) {
29
+ return e instanceof Error ? e.message : String(e);
30
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Session / workspace path rules (`src/workspaces.rs:555-660`).
3
+ *
4
+ * The workspace segment of an id is ALWAYS a registry lookup, never a path
5
+ * component; the session segment is sanitized and then parent-verified, so a
6
+ * crafted id cannot escape the registered workspace directory. These functions
7
+ * are pure — the stores own the filesystem side.
8
+ */
9
+ import { type PlatformInput } from "@celestea/tools";
10
+ /** The host's platform, injectable in tests (the W885 win32 seam). */
11
+ export type PathInput = PlatformInput;
12
+ export type PathInputLike = PlatformInput | string | undefined;
13
+ /** `dirname` under the call's platform. */
14
+ export declare function parentDir(path: string, input?: PathInputLike): string;
15
+ /** `basename` under the call's platform. */
16
+ export declare function baseName(path: string, input?: PathInputLike): string;
17
+ /** The filesystem root of `path` (`/`, `C:\\`, `\\\\\\\\server\\\\share\\\\`). */
18
+ export declare function rootOf(path: string, input?: PathInputLike): string;
19
+ /** Absolute per the call's platform (Windows drive letters included). */
20
+ export declare function isAbsolutePath(path: string, input?: PathInputLike): boolean;
21
+ /** Separators / control chars / whitespace -> '_'; CJK and letters survive. */
22
+ export declare function sanitizeComponent(s: string): string;
23
+ export interface ParsedSessionId {
24
+ workspace: string;
25
+ session: string;
26
+ /** Canonical `<workspace>/<session>` after sanitization. */
27
+ id: string;
28
+ }
29
+ /**
30
+ * Split `<workspace>/<session>`: exactly one '/', both sides non-empty, the
31
+ * session half separator-free after sanitization and never a hidden name.
32
+ */
33
+ export declare function parseSessionId(raw: string): ParsedSessionId | null;
34
+ /** `"<secs>.<nanos>"` — the creation / trash suffix (`now_ts`). */
35
+ export declare function timestampSuffix(nowMs: number): string;
36
+ /** `<sanitized title>-<secs>.<nanos>` — the session directory name. */
37
+ export declare function sessionDirName(title: string, nowMs: number): string;
38
+ /**
39
+ * W779 T2 — the display name of a session DIRECTORY.
40
+ *
41
+ * A directory is `<sanitized title>-<secs>.<nanos>` (and `uniqueDir` may append
42
+ * `-N`), a uniqueness trick that must never reach the GUI:
43
+ * main-1789192174.492000000 -> main
44
+ * v2-1-1700000000.0-2 -> v2-1
45
+ * plain -> plain (nothing to strip)
46
+ * 报告-2024 -> 报告-2024 (digits, but not a timestamp)
47
+ * A name that is ONLY a suffix keeps itself instead of becoming "".
48
+ */
49
+ export declare function stripCreationSuffix(name: string): string;
50
+ /** Folder basename, or null when the path has no usable last component. */
51
+ export declare function workspaceBasename(path: string, input?: PathInputLike): string | null;
52
+ /**
53
+ * W791: the two hidden sibling directories a session can be MOVED into.
54
+ *
55
+ * They live here (the session-directory vocabulary module) rather than in
56
+ * `session-ops.ts` because BOTH sides need them: the mover writes into them, and
57
+ * the scanner (`SessionsStore.listArchived`) reads `<ws>/.celestea-archived/`
58
+ * back. Importing the mover from the scanner would make the two modules
59
+ * circular, which the repo's dependency gate forbids.
60
+ *
61
+ * The names never change: a rename would orphan every archived session on disk.
62
+ */
63
+ export declare const ARCHIVED_DIR = ".celestea-archived";
64
+ export declare const TRASH_DIR = ".celestea-trash";
65
+ /**
66
+ * W877 (slice A) — the workspace-level `.celestea/` container.
67
+ *
68
+ * Slice A sank NEW session dirs into `<ws>/.celestea/sessions/`; W880 moves the
69
+ * canonical container OUT of the workspace into `CELESTEA_HOME`, so this path is
70
+ * now only the MIDDLE read fallback (dual-read compatibility with real data
71
+ * written by slice A).
72
+ */
73
+ export declare const CELESTEA_DIR = ".celestea";
74
+ export declare const SESSIONS_SUBDIR = "sessions";
75
+ /** W880 archive sub-container (under `workspaceHome`). */
76
+ export declare const ARCHIVE_SUBDIR = "archive";
77
+ /** W880 trash sub-container (under `workspaceHome`). */
78
+ export declare const TRASH_SUBDIR = "trash";
79
+ /** W880 workspace prompt-registry file name. */
80
+ export declare const PROMPTS_FILE = "prompts.json";
81
+ /** W880 run_code transient-program sub-container. */
82
+ export declare const RUN_CODE_SUBDIR = "run-code";
83
+ /** W880 canonical live-session root: `<CELESTEA_HOME>/workspaces/<ws>/sessions`. */
84
+ export declare function sessionsRoot(wsPath: string, input?: PathInputLike): string;
85
+ /** W877 transitional root: `<ws>/.celestea/sessions`. */
86
+ export declare function legacySessionsRoot(wsPath: string, input?: PathInputLike): string;
87
+ /**
88
+ * The physical roots a live session may occupy, canonical FIRST:
89
+ * `<home>/.../sessions` -> `<ws>/.celestea/sessions` -> `<ws>`.
90
+ *
91
+ * Pure — the caller owns the filesystem side. `resolve()` uses the first
92
+ * existing candidate and falls back to the canonical root when none exists (so
93
+ * the write side stays consistent); `list()` scans all three and lets the
94
+ * canonical row shadow a same-named legacy one.
95
+ */
96
+ export declare function sessionRoots(wsPath: string, input?: PathInputLike): string[];
97
+ /** `dir` under every live-session root, canonical FIRST. */
98
+ export declare function liveDirCandidates(wsPath: string, dir: string, input?: PathInputLike): string[];
99
+ /** Archive roots, canonical FIRST: `home/archive` -> `.celestea/archive` -> `.celestea-archived`. */
100
+ export declare function archiveRoots(wsPath: string, input?: PathInputLike): string[];
101
+ /** `dir` under every archive root, canonical FIRST. */
102
+ export declare function archiveDirCandidates(wsPath: string, dir: string, input?: PathInputLike): string[];
103
+ /** Trash roots, canonical FIRST. */
104
+ export declare function trashRoots(wsPath: string, input?: PathInputLike): string[];
105
+ /** Workspace prompt-registry candidates, canonical FIRST (write target = `[0]`). */
106
+ export declare function promptsFileCandidates(wsPath: string, input?: PathInputLike): string[];
107
+ /** A hidden name can never become a visible session/workspace directory. */
108
+ export declare function isHiddenName(name: string): boolean;
109
+ /** Workspace rename validation (`validate_workspace_name`). */
110
+ export declare function validateWorkspaceName(raw: string): {
111
+ ok: true;
112
+ name: string;
113
+ } | {
114
+ ok: false;
115
+ error: string;
116
+ };
117
+ /** The host `isAbsolute` (kept exported for pre-W885 callers). */
118
+ export declare function isAbsolute(path: string): boolean;
119
+ export type { PlatformInput };
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Session / workspace path rules (`src/workspaces.rs:555-660`).
3
+ *
4
+ * The workspace segment of an id is ALWAYS a registry lookup, never a path
5
+ * component; the session segment is sanitized and then parent-verified, so a
6
+ * crafted id cannot escape the registered workspace directory. These functions
7
+ * are pure — the stores own the filesystem side.
8
+ */
9
+ import { pathApi } from "@celestea/tools";
10
+ import { workspaceHome } from "./celestea-home.js";
11
+ /**
12
+ * W885 — collapse an input that may be a whole [PlatformInput] or a bare
13
+ * platform id (the pre-W885 signatures took neither, so both are additive).
14
+ */
15
+ function platformOf(input) {
16
+ if (input === undefined)
17
+ return process.platform;
18
+ return typeof input === "string" ? input : (input.platform ?? process.platform);
19
+ }
20
+ /** The path implementation of this call's platform (win32 in a win32 test). */
21
+ function apiOf(input) {
22
+ return pathApi(platformOf(input));
23
+ }
24
+ /** `<a>/<b>` under the call's platform, never string concatenation (W883 E1/E2). */
25
+ function under(input, base, ...segments) {
26
+ return apiOf(input).join(base, ...segments);
27
+ }
28
+ /** `dirname` under the call's platform. */
29
+ export function parentDir(path, input = undefined) {
30
+ return apiOf(input).dirname(path);
31
+ }
32
+ /** `basename` under the call's platform. */
33
+ export function baseName(path, input = undefined) {
34
+ return apiOf(input).basename(path);
35
+ }
36
+ /** The filesystem root of `path` (`/`, `C:\\`, `\\\\\\\\server\\\\share\\\\`). */
37
+ export function rootOf(path, input = undefined) {
38
+ return apiOf(input).parse(path).root;
39
+ }
40
+ /** Absolute per the call's platform (Windows drive letters included). */
41
+ export function isAbsolutePath(path, input = undefined) {
42
+ return apiOf(input).isAbsolute(path);
43
+ }
44
+ /** Separators / control chars / whitespace -> '_'; CJK and letters survive. */
45
+ export function sanitizeComponent(s) {
46
+ let out = "";
47
+ for (const ch of s) {
48
+ const code = ch.codePointAt(0) ?? 0;
49
+ const isControl = code < 0x20 || (code >= 0x7f && code <= 0x9f);
50
+ const isSeparator = ch === "/" || ch === "\\";
51
+ out += isSeparator || isControl || /\s/u.test(ch) ? "_" : ch;
52
+ }
53
+ return out;
54
+ }
55
+ /**
56
+ * Split `<workspace>/<session>`: exactly one '/', both sides non-empty, the
57
+ * session half separator-free after sanitization and never a hidden name.
58
+ */
59
+ export function parseSessionId(raw) {
60
+ const id = raw.trim();
61
+ if (id === "")
62
+ return null;
63
+ const slash = id.indexOf("/");
64
+ if (slash <= 0)
65
+ return null;
66
+ const workspace = id.slice(0, slash);
67
+ const rest = id.slice(slash + 1);
68
+ if (rest === "" || rest.includes("/"))
69
+ return null;
70
+ const session = sanitizeComponent(rest);
71
+ if (session === "" || session === "." || session === ".." || session.startsWith("."))
72
+ return null;
73
+ return { workspace, session, id: `${workspace}/${session}` };
74
+ }
75
+ /** `"<secs>.<nanos>"` — the creation / trash suffix (`now_ts`). */
76
+ export function timestampSuffix(nowMs) {
77
+ const secs = Math.floor(nowMs / 1000);
78
+ const nanos = Math.floor((nowMs - secs * 1000) * 1e6);
79
+ return `${secs}.${nanos}`;
80
+ }
81
+ /** `<sanitized title>-<secs>.<nanos>` — the session directory name. */
82
+ export function sessionDirName(title, nowMs) {
83
+ return `${sanitizeComponent(title)}-${timestampSuffix(nowMs)}`;
84
+ }
85
+ /** The creation tail [`stripCreationSuffix`] removes: `-<secs>.<nanos>[-<n>]`. */
86
+ const CREATION_SUFFIX = /-\d+\.\d+(-\d+)?$/;
87
+ /**
88
+ * W779 T2 — the display name of a session DIRECTORY.
89
+ *
90
+ * A directory is `<sanitized title>-<secs>.<nanos>` (and `uniqueDir` may append
91
+ * `-N`), a uniqueness trick that must never reach the GUI:
92
+ * main-1789192174.492000000 -> main
93
+ * v2-1-1700000000.0-2 -> v2-1
94
+ * plain -> plain (nothing to strip)
95
+ * 报告-2024 -> 报告-2024 (digits, but not a timestamp)
96
+ * A name that is ONLY a suffix keeps itself instead of becoming "".
97
+ */
98
+ export function stripCreationSuffix(name) {
99
+ const stripped = name.replace(CREATION_SUFFIX, "");
100
+ return stripped === "" ? name : stripped;
101
+ }
102
+ /** Folder basename, or null when the path has no usable last component. */
103
+ export function workspaceBasename(path, input = undefined) {
104
+ const base = baseName(path, input);
105
+ return base === "" || base === "/" || base === "\\" ? null : base;
106
+ }
107
+ /**
108
+ * W791: the two hidden sibling directories a session can be MOVED into.
109
+ *
110
+ * They live here (the session-directory vocabulary module) rather than in
111
+ * `session-ops.ts` because BOTH sides need them: the mover writes into them, and
112
+ * the scanner (`SessionsStore.listArchived`) reads `<ws>/.celestea-archived/`
113
+ * back. Importing the mover from the scanner would make the two modules
114
+ * circular, which the repo's dependency gate forbids.
115
+ *
116
+ * The names never change: a rename would orphan every archived session on disk.
117
+ */
118
+ export const ARCHIVED_DIR = ".celestea-archived";
119
+ export const TRASH_DIR = ".celestea-trash";
120
+ /**
121
+ * W877 (slice A) — the workspace-level `.celestea/` container.
122
+ *
123
+ * Slice A sank NEW session dirs into `<ws>/.celestea/sessions/`; W880 moves the
124
+ * canonical container OUT of the workspace into `CELESTEA_HOME`, so this path is
125
+ * now only the MIDDLE read fallback (dual-read compatibility with real data
126
+ * written by slice A).
127
+ */
128
+ export const CELESTEA_DIR = ".celestea";
129
+ export const SESSIONS_SUBDIR = "sessions";
130
+ /** W880 archive sub-container (under `workspaceHome`). */
131
+ export const ARCHIVE_SUBDIR = "archive";
132
+ /** W880 trash sub-container (under `workspaceHome`). */
133
+ export const TRASH_SUBDIR = "trash";
134
+ /** W880 workspace prompt-registry file name. */
135
+ export const PROMPTS_FILE = "prompts.json";
136
+ /** W880 run_code transient-program sub-container. */
137
+ export const RUN_CODE_SUBDIR = "run-code";
138
+ /** W880 canonical live-session root: `<CELESTEA_HOME>/workspaces/<ws>/sessions`. */
139
+ export function sessionsRoot(wsPath, input = undefined) {
140
+ return under(input, workspaceHome(wsPath, inputOf(input)), SESSIONS_SUBDIR);
141
+ }
142
+ /** W877 transitional root: `<ws>/.celestea/sessions`. */
143
+ export function legacySessionsRoot(wsPath, input = undefined) {
144
+ return under(input, wsPath, CELESTEA_DIR, SESSIONS_SUBDIR);
145
+ }
146
+ /**
147
+ * The [PlatformInput] form of a [PathInputLike]: a bare `"win32"` has to become
148
+ * an input object before it can reach `celestea-home`, which resolves its own
149
+ * per-platform defaults (and `homedir` matters for the win32 branch).
150
+ */
151
+ function inputOf(input) {
152
+ if (input === undefined)
153
+ return {};
154
+ if (typeof input === "string")
155
+ return { platform: input };
156
+ return input;
157
+ }
158
+ /**
159
+ * The physical roots a live session may occupy, canonical FIRST:
160
+ * `<home>/.../sessions` -> `<ws>/.celestea/sessions` -> `<ws>`.
161
+ *
162
+ * Pure — the caller owns the filesystem side. `resolve()` uses the first
163
+ * existing candidate and falls back to the canonical root when none exists (so
164
+ * the write side stays consistent); `list()` scans all three and lets the
165
+ * canonical row shadow a same-named legacy one.
166
+ */
167
+ export function sessionRoots(wsPath, input = undefined) {
168
+ return [sessionsRoot(wsPath, input), legacySessionsRoot(wsPath, input), wsPath];
169
+ }
170
+ /** `dir` under every live-session root, canonical FIRST. */
171
+ export function liveDirCandidates(wsPath, dir, input = undefined) {
172
+ return sessionRoots(wsPath, input).map((root) => under(input, root, dir));
173
+ }
174
+ /** Archive roots, canonical FIRST: `home/archive` -> `.celestea/archive` -> `.celestea-archived`. */
175
+ export function archiveRoots(wsPath, input = undefined) {
176
+ return [
177
+ under(input, workspaceHome(wsPath, inputOf(input)), ARCHIVE_SUBDIR),
178
+ under(input, wsPath, CELESTEA_DIR, ARCHIVE_SUBDIR),
179
+ under(input, wsPath, ARCHIVED_DIR),
180
+ ];
181
+ }
182
+ /** `dir` under every archive root, canonical FIRST. */
183
+ export function archiveDirCandidates(wsPath, dir, input = undefined) {
184
+ return archiveRoots(wsPath, input).map((root) => under(input, root, dir));
185
+ }
186
+ /** Trash roots, canonical FIRST. */
187
+ export function trashRoots(wsPath, input = undefined) {
188
+ return [
189
+ under(input, workspaceHome(wsPath, inputOf(input)), TRASH_SUBDIR),
190
+ under(input, wsPath, CELESTEA_DIR, TRASH_SUBDIR),
191
+ under(input, wsPath, TRASH_DIR),
192
+ ];
193
+ }
194
+ /** Workspace prompt-registry candidates, canonical FIRST (write target = `[0]`). */
195
+ export function promptsFileCandidates(wsPath, input = undefined) {
196
+ return [
197
+ under(input, workspaceHome(wsPath, inputOf(input)), PROMPTS_FILE),
198
+ under(input, wsPath, CELESTEA_DIR, PROMPTS_FILE),
199
+ under(input, wsPath, ".celestea-prompts.json"),
200
+ ];
201
+ }
202
+ /** A hidden name can never become a visible session/workspace directory. */
203
+ export function isHiddenName(name) {
204
+ return name === "" || name === "." || name === ".." || name.startsWith(".");
205
+ }
206
+ /** Workspace rename validation (`validate_workspace_name`). */
207
+ export function validateWorkspaceName(raw) {
208
+ if (raw.trim() === "")
209
+ return { ok: false, error: "workspace name must not be empty" };
210
+ const name = sanitizeComponent(raw);
211
+ if (name === "")
212
+ return { ok: false, error: "workspace name must not be empty" };
213
+ if (name === "." || name === "..")
214
+ return { ok: false, error: `invalid workspace name '${raw}'` };
215
+ return { ok: true, name };
216
+ }
217
+ /** The host `isAbsolute` (kept exported for pre-W885 callers). */
218
+ export function isAbsolute(path) {
219
+ return isAbsolutePath(path);
220
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `<session-dir>/session.json` — the optional Studio session metadata
3
+ * (`contracts/data-files/session.schema.json`).
4
+ *
5
+ * The engine NEVER reads this file; only `POST /api/sessions` writes it (and
6
+ * only when `title` and/or `model` and/or `prompt` and/or `mode` is non-empty),
7
+ * `rename`/`branch` re-write it (see W779 below), and `activate`/`compact` honor
8
+ * its `model` override. Missing or corrupt files are tolerated (`None`), never
9
+ * repaired.
10
+ *
11
+ * W779 T2: `title` joins them — the ORIGINAL, un-sanitized session title (CJK,
12
+ * spaces and all), so the GUI can show `我的 会话` instead of the directory name
13
+ * `我的_会话-1700000000.0`. `POST /api/sessions` always has a title, so every
14
+ * session created from now on carries one; the field is optional on read, and a
15
+ * session without it falls back to the de-suffixed directory name.
16
+ *
17
+ * W729 (P0): `mode` joins `model`/`prompt` as a creation-time session property.
18
+ * K8: the default mode is *not* written — the KEY never appears for a session
19
+ * that did not ask for one. W779 T2 adds `title`, so the file itself now always
20
+ * exists; K8's guarantee is per-key (no `mode` key), not per-file.
21
+ */
22
+ import { type SessionMode } from "./mode.js";
23
+ export declare const SESSION_META = "session.json";
24
+ export interface SessionMeta {
25
+ /**
26
+ * W779 T2: the display name, verbatim as the user typed it. Absent = the
27
+ * caller falls back to the directory name without its creation suffix.
28
+ */
29
+ title?: string;
30
+ model?: string;
31
+ prompt?: string;
32
+ /** Declared session mode; ABSENT = `standard` and no key on disk (K8). */
33
+ mode?: SessionMode;
34
+ }
35
+ /**
36
+ * Read the metadata; a corrupt file behaves exactly like a missing one, and a
37
+ * `mode` that is not a declared literal is dropped (never repaired on disk).
38
+ */
39
+ export declare function readSessionMeta(dir: string): SessionMeta | null;
40
+ /** Any of `title`/`model`/`prompt`/`mode` non-empty -> write; all empty -> no file. */
41
+ export declare function writeSessionMeta(dir: string, meta: SessionMeta): void;