@happyvertical/smrt-app-shell 0.0.0-stage → 0.60.17

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 (56) hide show
  1. package/AGENTS.md +99 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +173 -2
  5. package/agents/engine.md +85 -0
  6. package/dist/engine/app-runtime.d.ts +138 -0
  7. package/dist/engine/app-runtime.js +545 -0
  8. package/dist/engine/app-worker.d.ts +1 -0
  9. package/dist/engine/app-worker.js +62 -0
  10. package/dist/engine/commands.d.ts +94 -0
  11. package/dist/engine/commands.js +30 -0
  12. package/dist/engine/definition.d.ts +192 -0
  13. package/dist/engine/definition.js +497 -0
  14. package/dist/engine/errors.d.ts +23 -0
  15. package/dist/engine/errors.js +35 -0
  16. package/dist/engine/host.d.ts +153 -0
  17. package/dist/engine/host.js +791 -0
  18. package/dist/engine/index.d.ts +6 -0
  19. package/dist/engine/index.js +7 -0
  20. package/dist/engine/sandbox.d.ts +60 -0
  21. package/dist/engine/sandbox.js +260 -0
  22. package/dist/engine/snapshots.d.ts +7 -0
  23. package/dist/engine/snapshots.js +54 -0
  24. package/dist/engine/store.d.ts +39 -0
  25. package/dist/engine/store.js +32 -0
  26. package/dist/engine/worker-handle.d.ts +29 -0
  27. package/dist/engine/worker-handle.js +89 -0
  28. package/dist/engine/worker-protocol.d.ts +22 -0
  29. package/dist/engine/worker-protocol.js +20 -0
  30. package/dist/lib/objects/App.d.ts +60 -0
  31. package/dist/lib/objects/App.js +273 -0
  32. package/dist/lib/objects/AppVersion.d.ts +22 -0
  33. package/dist/lib/objects/AppVersion.js +62 -0
  34. package/dist/lib/objects/Approval.d.ts +39 -0
  35. package/dist/lib/objects/Approval.js +101 -0
  36. package/dist/lib/objects/AssistantMessage.d.ts +24 -0
  37. package/dist/lib/objects/AssistantMessage.js +63 -0
  38. package/dist/lib/objects/AssistantThread.d.ts +16 -0
  39. package/dist/lib/objects/AssistantThread.js +44 -0
  40. package/dist/lib/objects/index.d.ts +5 -0
  41. package/dist/lib/objects/index.js +5 -0
  42. package/dist/lib/server/approvals.d.ts +69 -0
  43. package/dist/lib/server/approvals.js +239 -0
  44. package/dist/lib/server/builder.d.ts +16 -0
  45. package/dist/lib/server/builder.js +71 -0
  46. package/dist/lib/server/events.d.ts +16 -0
  47. package/dist/lib/server/events.js +14 -0
  48. package/dist/lib/server/host.d.ts +22 -0
  49. package/dist/lib/server/host.js +96 -0
  50. package/dist/lib/server/owner.d.ts +33 -0
  51. package/dist/lib/server/owner.js +92 -0
  52. package/dist/lib/server/version-store.d.ts +20 -0
  53. package/dist/lib/server/version-store.js +115 -0
  54. package/dist/manifest.json +1680 -0
  55. package/dist/smrt-knowledge.okf.md +95 -0
  56. package/package.json +80 -3
@@ -0,0 +1,497 @@
1
+ export const USER_PKG = '@local/app';
2
+ export const FORMAT = 1;
3
+ export const EMPTY_APP = {
4
+ format: 1,
5
+ name: 'My app',
6
+ recipes: [],
7
+ models: {},
8
+ };
9
+ export const RECIPES = {
10
+ profiles: {
11
+ tables: [
12
+ '@happyvertical/smrt-profiles:ProfileType',
13
+ '@happyvertical/smrt-profiles:Profile',
14
+ ],
15
+ models: {
16
+ Profile: {
17
+ parent: '@happyvertical/smrt-profiles:Profile',
18
+ table: 'profiles',
19
+ className: 'Profile',
20
+ builtIn: {
21
+ name: { type: 'text', required: true },
22
+ email: { type: 'text' },
23
+ description: { type: 'text' },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ };
29
+ /** The recipe model an app model extends, if it does. */
30
+ export function recipeModelFor(model, md) {
31
+ if (!md?.extends)
32
+ return undefined;
33
+ for (const r of Object.values(RECIPES))
34
+ for (const [name, rm] of Object.entries(r.models))
35
+ if (name === model && rm.parent === md.extends)
36
+ return rm;
37
+ return undefined;
38
+ }
39
+ export const MAX_CODE_LENGTH = 20_000;
40
+ export const MAX_MODELS = 100;
41
+ export const MAX_FIELDS = 200;
42
+ export const MAX_METHODS = 50;
43
+ export const MAX_PARAMS = 20;
44
+ export const MAX_CHOICES = 200;
45
+ const MODEL_NAME = /^[A-Z][A-Za-z0-9]{0,39}$/;
46
+ // camelCase without consecutive capitals, so snake-casing is unambiguous and
47
+ // matches core's `toSnakeCase` for every legal name.
48
+ const FIELD_NAME = /^[a-z][a-z0-9]{0,39}(?:[A-Z][a-z0-9]{1,39})*$/;
49
+ const RESERVED = new Set([
50
+ 'id',
51
+ 'slug',
52
+ 'context',
53
+ 'createdAt',
54
+ 'updatedAt',
55
+ 'tenantId',
56
+ 'save',
57
+ 'delete',
58
+ 'load',
59
+ 'do',
60
+ 'is',
61
+ 'describe',
62
+ 'constructor',
63
+ 'prototype',
64
+ 'toString',
65
+ 'valueOf',
66
+ 'hasOwnProperty',
67
+ 'typeId',
68
+ 'metaType',
69
+ ]);
70
+ export const FIELD_TYPES = [
71
+ 'text',
72
+ 'number',
73
+ 'integer',
74
+ 'boolean',
75
+ 'date',
76
+ 'money',
77
+ 'choice',
78
+ 'reference',
79
+ 'json',
80
+ ];
81
+ const TYPES = FIELD_TYPES;
82
+ /** The regular expression field and method names must match (camelCase). */
83
+ export const FIELD_NAME_PATTERN = FIELD_NAME.source;
84
+ /** A legal camelCase spelling of a rejected name, e.g. `due_date` -> `dueDate`. */
85
+ export function suggestFieldName(name) {
86
+ const words = String(name)
87
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
88
+ .split(/[^A-Za-z0-9]+/)
89
+ .filter(Boolean)
90
+ .map((w) => w.toLowerCase());
91
+ if (!words.length || /^[0-9]/.test(words[0]))
92
+ return undefined;
93
+ const camel = words[0] +
94
+ words
95
+ .slice(1)
96
+ .map((w) => w[0].toUpperCase() + w.slice(1))
97
+ .join('');
98
+ return FIELD_NAME.test(camel) && camel !== name ? camel : undefined;
99
+ }
100
+ export const snake = (s) => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
101
+ export function isValidModelName(s) {
102
+ return MODEL_NAME.test(s);
103
+ }
104
+ export function isValidFieldName(s) {
105
+ return FIELD_NAME.test(s) && !RESERVED.has(s);
106
+ }
107
+ export function tableFor(model, md) {
108
+ const rm = recipeModelFor(model, md);
109
+ if (rm)
110
+ return rm.table;
111
+ const s = snake(model).replace(/^_/, '');
112
+ return `u_${s.endsWith('s') ? s : `${s}s`}`;
113
+ }
114
+ /** Class name a model is registered under (generated tools use it). */
115
+ export function classNameFor(model, md) {
116
+ return recipeModelFor(model, md)?.className ?? model;
117
+ }
118
+ /** Route segment of the generated REST collection. */
119
+ export function routeFor(model, md) {
120
+ const t = tableFor(model, md);
121
+ return t.startsWith('u_') ? t.slice(2) : t;
122
+ }
123
+ /**
124
+ * Whether a REST request may be served: only `/api/<route>` and
125
+ * `/api/<route>/<id or action>` of the app's own models. Core's generated
126
+ * handler also serves reserved endpoints (`sync/apply`, whose `delete` op
127
+ * deletes records, `_changes`, `_events`, ...) and any shipped class it can
128
+ * find in its registry; none of those belong to the app.
129
+ */
130
+ export function restRouteAllowed(def, rawUrl) {
131
+ let path;
132
+ try {
133
+ path = new URL(rawUrl, 'http://app.local').pathname;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ if (path !== '/api' && !path.startsWith('/api/'))
139
+ return false;
140
+ const parts = path.slice('/api'.length).split('/').filter(Boolean);
141
+ if (parts.length < 1 || parts.length > 2)
142
+ return false;
143
+ const routes = new Set(Object.entries(def.models).map(([m, md]) => routeFor(m, md)));
144
+ if (!routes.has(parts[0]))
145
+ return false;
146
+ return parts.length === 1 || !parts[1].startsWith('_');
147
+ }
148
+ /** Names of the generated tools: `<class>_list`, ... and one per method. */
149
+ export function toolNamesFor(model, md) {
150
+ const cls = classNameFor(model, md).toLowerCase();
151
+ return [
152
+ ...['list', 'get', 'create', 'update', 'delete'],
153
+ ...Object.keys(md.methods ?? {}),
154
+ ].map((a) => `${cls}_${a}`);
155
+ }
156
+ /** Shipped (built-in) fields of an app model. */
157
+ export function builtInFields(model, md) {
158
+ return recipeModelFor(model, md)?.builtIn ?? {};
159
+ }
160
+ export function visible(fields) {
161
+ return Object.fromEntries(Object.entries(fields).filter(([, f]) => !f.hidden));
162
+ }
163
+ /** Core's run-time field type and the SQLite column type it gets. */
164
+ const TYPE_MAP = {
165
+ text: ['text', 'TEXT'],
166
+ number: ['decimal', 'REAL'],
167
+ integer: ['integer', 'INTEGER'],
168
+ boolean: ['boolean', 'INTEGER'],
169
+ date: ['datetime', 'DATETIME'],
170
+ money: ['integer', 'INTEGER'], // minor units (cents)
171
+ choice: ['text', 'TEXT'],
172
+ reference: ['text', 'TEXT'], // the referenced record's id
173
+ json: ['json', 'TEXT'],
174
+ };
175
+ export const sqlType = (t) => TYPE_MAP[t][1];
176
+ export const runtimeType = (t) => TYPE_MAP[t][0];
177
+ /** Storage family of a declared SQLite column type, to compare columns. */
178
+ export function storageFamily(declared) {
179
+ const t = declared.toUpperCase();
180
+ if (/DATE|TIME/.test(t))
181
+ return 'date';
182
+ if (/INT|BOOL/.test(t))
183
+ return 'integer';
184
+ if (/REAL|FLOA|DOUB|DEC|NUMERIC/.test(t))
185
+ return 'real';
186
+ return 'text';
187
+ }
188
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
189
+ /** Check a whole definition. Returns problems; empty means valid. */
190
+ export function validate(def) {
191
+ const errs = [];
192
+ if (!isObj(def))
193
+ return ['definition must be an object'];
194
+ if (def.format !== FORMAT)
195
+ return [`unknown definition format '${String(def.format)}'`];
196
+ if (typeof def.name !== 'string' || !def.name.trim() || def.name.length > 80)
197
+ errs.push('name must be 1 to 80 characters');
198
+ if (!Array.isArray(def.recipes) ||
199
+ def.recipes.some((r) => typeof r !== 'string')) {
200
+ errs.push('recipes must be a list of names');
201
+ return errs;
202
+ }
203
+ if (!isObj(def.models)) {
204
+ errs.push('models must be an object');
205
+ return errs;
206
+ }
207
+ const d = def;
208
+ for (const r of d.recipes)
209
+ if (!Object.hasOwn(RECIPES, r))
210
+ errs.push(`unknown recipe '${r}'`);
211
+ const names = Object.keys(d.models);
212
+ if (names.length > MAX_MODELS)
213
+ errs.push(`at most ${MAX_MODELS} models`);
214
+ const known = new Set(names);
215
+ // Recipe models and the app subclasses must agree.
216
+ for (const r of d.recipes) {
217
+ for (const [name, rm] of Object.entries(RECIPES[r]?.models ?? {})) {
218
+ const md = d.models[name];
219
+ if (!md || md.extends !== rm.parent)
220
+ errs.push(`${name}: recipe '${r}' needs the app model ${name}`);
221
+ }
222
+ }
223
+ for (const [m, md] of Object.entries(d.models)) {
224
+ if (!isObj(md) || !isObj(md.fields)) {
225
+ errs.push(`${m}: a model needs a fields object`);
226
+ continue;
227
+ }
228
+ if (!MODEL_NAME.test(m))
229
+ errs.push(`${m}: model names are PascalCase (e.g. Chore): uppercase first letter, then letters and digits`);
230
+ if (md.label !== undefined && typeof md.label !== 'string')
231
+ errs.push(`${m}: label must be text`);
232
+ let builtIn = {};
233
+ if (md.extends !== undefined) {
234
+ const rm = recipeModelFor(m, md);
235
+ if (!rm || !d.recipes.some((r) => RECIPES[r]?.models[m] === rm))
236
+ errs.push(`${m}: cannot extend '${md.extends}'`);
237
+ builtIn = builtInFields(m, md);
238
+ }
239
+ else if (Object.values(RECIPES).some((r) => Object.hasOwn(r.models, m))) {
240
+ errs.push(`${m}: reserved for a recipe; enable the recipe instead`);
241
+ }
242
+ const fieldNames = Object.keys(md.fields);
243
+ if (fieldNames.length > MAX_FIELDS)
244
+ errs.push(`${m}: too many fields`);
245
+ for (const [f, fd] of Object.entries(md.fields)) {
246
+ const at = `${m}.${f}`;
247
+ if (!FIELD_NAME.test(f))
248
+ errs.push(fieldNameProblem(at, f));
249
+ if (RESERVED.has(f))
250
+ errs.push(`${at}: reserved name`);
251
+ if (Object.hasOwn(builtIn, f))
252
+ errs.push(`${at}: is built in`);
253
+ if (!isObj(fd)) {
254
+ errs.push(`${at}: field must be an object`);
255
+ continue;
256
+ }
257
+ if (!TYPES.includes(fd.type))
258
+ errs.push(`${at}: unknown type '${String(fd.type)}'; use one of ${TYPES.join(', ')}`);
259
+ if (fd.type === 'choice') {
260
+ const c = fd.choices;
261
+ if (!Array.isArray(c) ||
262
+ !c.length ||
263
+ c.length > MAX_CHOICES ||
264
+ c.some((x) => typeof x !== 'string' || !x))
265
+ errs.push(`${at}: choice needs choices (a list of text)`);
266
+ }
267
+ if (fd.type === 'reference' && (!fd.ref || !known.has(fd.ref)))
268
+ errs.push(`${at}: reference needs ref to a model of the app (have: ${names.join(', ') || 'none'})`);
269
+ if (fd.label !== undefined && typeof fd.label !== 'string')
270
+ errs.push(`${at}: label must be text`);
271
+ }
272
+ const methods = md.methods ?? {};
273
+ if (!isObj(methods)) {
274
+ errs.push(`${m}: methods must be an object`);
275
+ continue;
276
+ }
277
+ if (Object.keys(methods).length > MAX_METHODS)
278
+ errs.push(`${m}: too many methods`);
279
+ for (const [name, meth] of Object.entries(methods)) {
280
+ const at = `${m}.${name}()`;
281
+ if (!FIELD_NAME.test(name) || RESERVED.has(name))
282
+ errs.push(RESERVED.has(name)
283
+ ? `${at}: '${name}' is a reserved name`
284
+ : fieldNameProblem(`${m} method`, name));
285
+ if (Object.hasOwn(md.fields, name) || Object.hasOwn(builtIn, name))
286
+ errs.push(`${at}: clashes with a field`);
287
+ if (!isObj(meth) || typeof meth.code !== 'string') {
288
+ errs.push(`${at}: a method needs code`);
289
+ continue;
290
+ }
291
+ if (meth.code.length > MAX_CODE_LENGTH)
292
+ errs.push(`${at}: code is longer than ${MAX_CODE_LENGTH} characters`);
293
+ if (typeof meth.description !== 'string')
294
+ errs.push(`${at}: a method needs a description`);
295
+ const params = meth.params ?? {};
296
+ if (!isObj(params) || Object.keys(params).length > MAX_PARAMS) {
297
+ errs.push(`${at}: params must be an object of at most ${MAX_PARAMS}`);
298
+ continue;
299
+ }
300
+ for (const [p, pd] of Object.entries(params)) {
301
+ if (!FIELD_NAME.test(p))
302
+ errs.push(fieldNameProblem(`${at} param`, p));
303
+ if (!isObj(pd) || !TYPES.includes(pd.type))
304
+ errs.push(`${at}: param '${p}' needs a type, one of ${TYPES.join(', ')}`);
305
+ }
306
+ }
307
+ }
308
+ return errs;
309
+ }
310
+ /** The message for a rejected field, method or parameter name. */
311
+ function fieldNameProblem(at, name) {
312
+ const hint = suggestFieldName(name);
313
+ return `${at}: field names are camelCase (e.g. dueDate): lowercase first letter, then letters and digits, no underscores, spaces or runs of capitals${hint ? `; did you mean '${hint}'?` : ''} (got '${name}')`;
314
+ }
315
+ /** Changes that cannot be applied by only adding things. */
316
+ export function refuseChanges(prev, next, converting = new Set()) {
317
+ const errs = [];
318
+ for (const [m, md] of Object.entries(prev.models)) {
319
+ const nm = next.models[m];
320
+ if (!nm)
321
+ continue;
322
+ for (const [f, fd] of Object.entries(md.fields)) {
323
+ const nf = nm.fields[f];
324
+ if (nf &&
325
+ sqlType(nf.type) !== sqlType(fd.type) &&
326
+ !converting.has(`${m}.${f}`))
327
+ errs.push(`${m}.${f}: use changeFieldType to change ${fd.type} -> ${nf.type}`);
328
+ }
329
+ }
330
+ return errs;
331
+ }
332
+ /** What the app looks like to the assistant and the page. */
333
+ export function describeApp(def, version) {
334
+ const models = {};
335
+ for (const [m, md] of Object.entries(def.models)) {
336
+ const built = builtInFields(m, md);
337
+ models[m] = {
338
+ kind: md.extends ? 'shipped-subclass' : 'app',
339
+ label: md.label ?? m,
340
+ table: tableFor(m, md),
341
+ className: classNameFor(m, md),
342
+ extends: md.extends ?? null,
343
+ fields: { ...built, ...visible(md.fields) },
344
+ ownFields: Object.keys(visible(md.fields)),
345
+ methods: Object.fromEntries(Object.entries(md.methods ?? {}).map(([k, v]) => [
346
+ k,
347
+ { description: v.description, params: v.params ?? {} },
348
+ ])),
349
+ };
350
+ }
351
+ return { name: def.name, version, recipes: def.recipes, models };
352
+ }
353
+ /** A model as core's `registerRuntimeModel` wants it (hidden fields left out). */
354
+ export function toRuntimeModel(model, md) {
355
+ const fields = {};
356
+ for (const [f, fd] of Object.entries(visible(md.fields))) {
357
+ fields[f] = {
358
+ type: runtimeType(fd.type),
359
+ required: !!fd.required && !md.extends,
360
+ ...(fd.type === 'choice' && fd.choices ? { enum: fd.choices } : {}),
361
+ ...(fd.label ? { description: fd.label } : {}),
362
+ };
363
+ }
364
+ const methods = {};
365
+ for (const [name, m] of Object.entries(md.methods ?? {})) {
366
+ methods[name] = {
367
+ description: m.description,
368
+ parameters: Object.entries(m.params ?? {}).map(([p, pd]) => ({
369
+ name: p,
370
+ type: runtimeType(pd.type),
371
+ optional: !pd.required,
372
+ })),
373
+ };
374
+ }
375
+ return {
376
+ name: classNameFor(model, md),
377
+ packageName: USER_PKG,
378
+ fields,
379
+ methods,
380
+ ...(md.extends
381
+ ? { extends: md.extends }
382
+ : { tableName: tableFor(model, md) }),
383
+ };
384
+ }
385
+ /** Convert one stored value to a field type. */
386
+ export function convertValue(v, to) {
387
+ if (v === null || v === undefined || v === '')
388
+ return { ok: true, value: null };
389
+ const s = String(v).trim();
390
+ switch (to.type) {
391
+ case 'text':
392
+ case 'reference':
393
+ return { ok: true, value: typeof v === 'object' ? JSON.stringify(v) : s };
394
+ case 'integer':
395
+ case 'money': {
396
+ const n = Number(s);
397
+ return Number.isFinite(n)
398
+ ? { ok: true, value: Math.round(n) }
399
+ : { ok: false, value: null };
400
+ }
401
+ case 'number': {
402
+ const n = Number(s);
403
+ return Number.isFinite(n)
404
+ ? { ok: true, value: n }
405
+ : { ok: false, value: null };
406
+ }
407
+ case 'boolean': {
408
+ const l = s.toLowerCase();
409
+ if (['1', 'true', 'yes', 'y', 'done'].includes(l))
410
+ return { ok: true, value: 1 };
411
+ if (['0', 'false', 'no', 'n'].includes(l))
412
+ return { ok: true, value: 0 };
413
+ return { ok: false, value: null };
414
+ }
415
+ case 'date': {
416
+ const d = new Date(/^\d+$/.test(s) ? Number(s) : s);
417
+ return Number.isNaN(d.getTime())
418
+ ? { ok: false, value: null }
419
+ : { ok: true, value: d.toISOString() };
420
+ }
421
+ case 'choice':
422
+ return to.choices?.includes(s)
423
+ ? { ok: true, value: s }
424
+ : { ok: false, value: null };
425
+ case 'json':
426
+ try {
427
+ return {
428
+ ok: true,
429
+ value: JSON.stringify(typeof v === 'string' ? JSON.parse(v) : v),
430
+ };
431
+ }
432
+ catch {
433
+ return { ok: false, value: null };
434
+ }
435
+ }
436
+ }
437
+ /**
438
+ * Check a value against a field and convert it for storage. Shared by record
439
+ * writes and by the writes a method proposes, so both are held to one rule.
440
+ * References are only checked for shape here; the runtime checks the target.
441
+ */
442
+ export function coerceValue(fd, v) {
443
+ if (v === null || v === undefined)
444
+ return { ok: true, value: null };
445
+ const bad = (what) => ({
446
+ ok: false,
447
+ error: `must be ${what}`,
448
+ });
449
+ switch (fd.type) {
450
+ case 'text':
451
+ return typeof v === 'string'
452
+ ? { ok: true, value: v }
453
+ : typeof v === 'number' || typeof v === 'boolean'
454
+ ? { ok: true, value: String(v) }
455
+ : bad('text');
456
+ case 'reference':
457
+ return typeof v === 'string' && v
458
+ ? { ok: true, value: v }
459
+ : bad('a record id');
460
+ case 'choice':
461
+ return typeof v === 'string' && fd.choices?.includes(v)
462
+ ? { ok: true, value: v }
463
+ : bad(`one of ${fd.choices?.join(', ')}`);
464
+ case 'integer':
465
+ case 'money':
466
+ case 'number': {
467
+ const n = typeof v === 'number'
468
+ ? v
469
+ : typeof v === 'string' && v.trim() !== ''
470
+ ? Number(v)
471
+ : Number.NaN;
472
+ if (!Number.isFinite(n))
473
+ return bad('a number');
474
+ return { ok: true, value: fd.type === 'number' ? n : Math.round(n) };
475
+ }
476
+ case 'boolean':
477
+ if (v === true || v === 'true' || v === 1)
478
+ return { ok: true, value: true };
479
+ if (v === false || v === 'false' || v === 0)
480
+ return { ok: true, value: false };
481
+ return bad('true or false');
482
+ case 'date': {
483
+ const d = v instanceof Date
484
+ ? v
485
+ : typeof v === 'string' || typeof v === 'number'
486
+ ? new Date(v)
487
+ : null;
488
+ return d && !Number.isNaN(d.getTime())
489
+ ? { ok: true, value: d }
490
+ : bad('a date');
491
+ }
492
+ case 'json':
493
+ return { ok: true, value: v };
494
+ default:
495
+ return bad('a known type');
496
+ }
497
+ }
@@ -0,0 +1,23 @@
1
+ import type { Plan } from './commands.ts';
2
+ export type EngineErrorCode = 'invalid' | 'not_found' | 'refused' | 'approval_required' | 'worker_crashed' | 'migration_failed';
3
+ /** A refusal or mistake the caller can act on (as opposed to a crash). */
4
+ export declare class EngineError extends Error {
5
+ readonly code: EngineErrorCode;
6
+ constructor(message: string, code?: EngineErrorCode);
7
+ }
8
+ /**
9
+ * A risky command or tool call was made without `approved: true`. The shell's
10
+ * approval model sets that only after the owner decided in its own UI.
11
+ */
12
+ export declare class ApprovalRequiredError extends EngineError {
13
+ readonly plan: Plan;
14
+ constructor(plan: Plan);
15
+ }
16
+ /** The app's worker thread is gone. `restartApp()` starts a new one. */
17
+ export declare class WorkerCrashedError extends EngineError {
18
+ constructor(message?: string);
19
+ }
20
+ /** A mistake by the caller inside the app runtime (bad field, missing record). */
21
+ export declare class AppError extends EngineError {
22
+ constructor(message: string);
23
+ }
@@ -0,0 +1,35 @@
1
+ /** A refusal or mistake the caller can act on (as opposed to a crash). */
2
+ export class EngineError extends Error {
3
+ code;
4
+ constructor(message, code = 'invalid') {
5
+ super(message);
6
+ this.name = 'EngineError';
7
+ this.code = code;
8
+ }
9
+ }
10
+ /**
11
+ * A risky command or tool call was made without `approved: true`. The shell's
12
+ * approval model sets that only after the owner decided in its own UI.
13
+ */
14
+ export class ApprovalRequiredError extends EngineError {
15
+ plan;
16
+ constructor(plan) {
17
+ super(`approval required: ${plan.summary}`, 'approval_required');
18
+ this.name = 'ApprovalRequiredError';
19
+ this.plan = plan;
20
+ }
21
+ }
22
+ /** The app's worker thread is gone. `restartApp()` starts a new one. */
23
+ export class WorkerCrashedError extends EngineError {
24
+ constructor(message = 'the app stopped; restart it to continue') {
25
+ super(message, 'worker_crashed');
26
+ this.name = 'WorkerCrashedError';
27
+ }
28
+ }
29
+ /** A mistake by the caller inside the app runtime (bad field, missing record). */
30
+ export class AppError extends EngineError {
31
+ constructor(message) {
32
+ super(message, 'invalid');
33
+ this.name = 'AppError';
34
+ }
35
+ }