@mohou/contract 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ status: locked
3
+ updated: 2026-10-01
4
+ ---
5
+
6
+ # @mohou/contract
7
+
8
+ Role: `definition`.
9
+
10
+ App id parse, manifest resolve, `defineApp`, import side checks, `ctx` types, and failure codes. No I/O. Product catalog: [app surface](../../../docs/product/app-surface.md).
@@ -0,0 +1,10 @@
1
+ import type { Branded } from '@mohou/values';
2
+ /** Reverse-DNS app id admitted at this boundary. */
3
+ export type AppId = Branded<'AppId'>;
4
+ /**
5
+ * Admit an app id. Callers do not re-parse the brand.
6
+ * @param value - untrusted id text
7
+ * @returns the branded id
8
+ */
9
+ export declare function parseAppId(value: string): AppId;
10
+ //# sourceMappingURL=app-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-id.d.ts","sourceRoot":"","sources":["../../src/app-id.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAM5C,oDAAoD;AACpD,MAAM,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAEpC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAK/C"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Codes this definition emits. Other surfaces declare their own codes.
3
+ * Callers match `code`, not the message.
4
+ * @module @mohou/contract
5
+ */
6
+ export declare const definitionCodes: readonly ["app-id-invalid", "manifest-invalid", "import-forbidden", "import-escape", "define-app-invalid"];
7
+ /** A failure emitted by this definition. */
8
+ export type DefinitionCode = (typeof definitionCodes)[number];
9
+ /** Failure a caller branches on. The message is for a person. */
10
+ export declare class ContractError extends Error {
11
+ readonly code: DefinitionCode;
12
+ /**
13
+ * @param code - one of {@link definitionCodes}
14
+ * @param message - human text; not the match key
15
+ * @param options - optional `cause` for the original failure
16
+ */
17
+ constructor(code: DefinitionCode, message: string, options?: {
18
+ cause?: unknown;
19
+ });
20
+ }
21
+ //# sourceMappingURL=codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../../src/codes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,4GAMlB,CAAA;AAEV,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7D,iEAAiE;AACjE,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAE7B;;;;OAIG;gBACS,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKjF"}
@@ -0,0 +1,198 @@
1
+ import type { AppId } from './app-id.ts';
2
+ import type { AppWorkbench } from './workbench.ts';
3
+ import { agentEventType, llmEventType } from './event-type.ts';
4
+ /** A cell accepted by SQLite. */
5
+ export type SqlValue = null | number | string | bigint | Uint8Array;
6
+ /** One result row. Column names are the keys. */
7
+ export interface SqlRow {
8
+ [column: string]: SqlValue;
9
+ }
10
+ /** Positional or named parameters. Named keys omit the sigil. */
11
+ export type SqlParams = readonly SqlValue[] | Record<string, SqlValue>;
12
+ /** The host-owned `kv` table. `query` and `run` cannot see it. */
13
+ export interface AppKeyValueTable {
14
+ get(key: string): Promise<unknown>;
15
+ set(key: string, value: unknown): Promise<void>;
16
+ delete(key: string): Promise<void>;
17
+ clear(): Promise<void>;
18
+ }
19
+ /** One SQLite file. `kv()` is the key-value table. SQL is everything else. */
20
+ export interface AppStorage {
21
+ kv(): AppKeyValueTable;
22
+ query(sql: string, params?: SqlParams): Promise<SqlRow[]>;
23
+ run(sql: string, params?: SqlParams): Promise<{
24
+ changes: number;
25
+ lastInsertRowid: number | bigint;
26
+ }>;
27
+ transaction<T>(work: (tx: AppStorage) => Promise<T>): Promise<T>;
28
+ }
29
+ /** `ctx.http` request. Object `body` is sent as JSON. */
30
+ export interface AppHttpRequest {
31
+ url: string;
32
+ method?: string;
33
+ headers?: Record<string, string>;
34
+ query?: Record<string, string | number | boolean | null | undefined>;
35
+ body?: unknown;
36
+ timeout?: number;
37
+ signal?: AbortSignal;
38
+ }
39
+ /** `ctx.http` result. Never a platform `Response`. */
40
+ export interface AppHttpResponse {
41
+ ok: boolean;
42
+ status: number;
43
+ headers: Record<string, string>;
44
+ text: string;
45
+ /** Parsed body when the content type contains json and parsing succeeds, otherwise null. */
46
+ json: unknown;
47
+ }
48
+ /** Options shared by `ctx.llm` and `ctx.agent`. Omitted limits use host policy. */
49
+ export interface AppModelOptions {
50
+ provider?: string;
51
+ model?: string;
52
+ system?: string;
53
+ schema?: unknown;
54
+ maxTokens?: number;
55
+ /** Attempts in total, including the first. */
56
+ retryTimes?: number;
57
+ signal?: AbortSignal;
58
+ }
59
+ /** Why an agent turn ended. `kind` stays open so a provider can add a reason. */
60
+ export interface AppAgentTurnEndReason {
61
+ kind: string;
62
+ error?: unknown;
63
+ reason?: unknown;
64
+ }
65
+ /** Observation only. The `ctx.agent` return stays a string. */
66
+ export type AppAgentEvent = {
67
+ type: typeof agentEventType.status;
68
+ status: 'running' | 'idle';
69
+ } | {
70
+ type: typeof agentEventType.textDelta;
71
+ text: string;
72
+ } | {
73
+ type: typeof agentEventType.tool;
74
+ phase: 'start' | 'end';
75
+ name: string;
76
+ args?: unknown;
77
+ result?: unknown;
78
+ } | {
79
+ type: typeof agentEventType.turn;
80
+ phase: 'start';
81
+ turn: number;
82
+ } | {
83
+ type: typeof agentEventType.turn;
84
+ phase: 'end';
85
+ turn: number;
86
+ reason?: AppAgentTurnEndReason;
87
+ } | {
88
+ type: typeof agentEventType.error;
89
+ message: string;
90
+ } | {
91
+ type: typeof agentEventType.done;
92
+ text: string;
93
+ };
94
+ /** Observation for `ctx.llm` when `stream` is true. The return stays a string. */
95
+ export type AppLlmEvent = {
96
+ type: typeof llmEventType.status;
97
+ status: 'running' | 'idle';
98
+ } | {
99
+ type: typeof llmEventType.textDelta;
100
+ text: string;
101
+ } | {
102
+ type: typeof llmEventType.error;
103
+ message: string;
104
+ } | {
105
+ type: typeof llmEventType.done;
106
+ text: string;
107
+ };
108
+ /** `ctx.llm` options. `stream` defaults to false. */
109
+ export interface AppLlmOptions extends AppModelOptions {
110
+ /** When true, the call returns a stream the method reads with `for await`. It does not reach the UI by itself. */
111
+ stream?: boolean;
112
+ }
113
+ /** `ctx.agent` options. `stream` defaults to false. */
114
+ export interface AppAgentOptions extends AppModelOptions {
115
+ /** When true, the call returns a stream the method reads with `for await`. It does not reach the UI by itself. */
116
+ stream?: boolean;
117
+ maxIterations?: number;
118
+ cwdType?: 'app' | 'process' | 'temp' | 'custom';
119
+ cwd?: string;
120
+ }
121
+ /** Public host policy visible to an app. No secrets. */
122
+ export interface AppConfig {
123
+ theme: 'light' | 'dark' | 'system';
124
+ palette: string;
125
+ locale: string;
126
+ chatLanguage: string;
127
+ hostPort: number;
128
+ llm: {
129
+ provider: string;
130
+ model: string;
131
+ } | null;
132
+ }
133
+ /** One OS snapshot. `loadavg` is null where the OS does not provide it. */
134
+ export interface AppSystemMetrics {
135
+ platform: string;
136
+ arch: string;
137
+ hostname: string;
138
+ uptimeSec: number;
139
+ loadavg: {
140
+ '1m': number;
141
+ '5m': number;
142
+ '15m': number;
143
+ } | null;
144
+ memory: {
145
+ total: number;
146
+ free: number;
147
+ used: number;
148
+ usedRatio: number;
149
+ };
150
+ cpu: {
151
+ count: number;
152
+ model: string;
153
+ speedMHz: number;
154
+ };
155
+ collectedAt: string;
156
+ }
157
+ /** One secret, by the name the owner stored. Listing is not on this object. */
158
+ export interface AppCredentials {
159
+ get(name: string): Promise<string | undefined>;
160
+ }
161
+ /** First argument of every `api` method. `State` is the object the author declared. */
162
+ export interface AppContext<State extends object = Record<string, never>> {
163
+ appId: AppId;
164
+ appDir: string;
165
+ storage: AppStorage;
166
+ state: State;
167
+ credentials: AppCredentials;
168
+ config: AppConfig;
169
+ log(...args: unknown[]): void;
170
+ signal?: AbortSignal;
171
+ push(name: string, params?: unknown): void;
172
+ http(url: string | AppHttpRequest, opts?: Omit<AppHttpRequest, 'url'>): Promise<AppHttpResponse>;
173
+ bash(command: string): Promise<{
174
+ stdout: string;
175
+ stderr: string;
176
+ exitCode: number;
177
+ }>;
178
+ pwsh(command: string): Promise<{
179
+ stdout: string;
180
+ stderr: string;
181
+ exitCode: number;
182
+ }>;
183
+ system: {
184
+ metrics(): Promise<AppSystemMetrics>;
185
+ };
186
+ llm(prompt: string, opts: AppLlmOptions & {
187
+ stream: true;
188
+ }): AsyncIterable<AppLlmEvent> & Promise<string>;
189
+ llm(prompt: string, opts?: AppLlmOptions): Promise<string>;
190
+ agent(goal: string, opts: AppAgentOptions & {
191
+ stream: true;
192
+ }): AsyncIterable<AppAgentEvent> & Promise<string>;
193
+ agent(goal: string, opts?: AppAgentOptions): Promise<string>;
194
+ mcp(serverId: string, toolName: string, args?: Record<string, unknown>): Promise<unknown>;
195
+ /** Present only when the manifest kind is `workbench`. */
196
+ workbench?: AppWorkbench;
197
+ }
198
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { AppContext } from './context.ts';
2
+ /** One backend method as the host calls it. `args` is the unchecked JSON. */
3
+ export type AppApiMethod<State extends object = Record<string, never>> = (ctx: AppContext<State>, args: unknown) => unknown;
4
+ /**
5
+ * Bound for a method `defineApp` accepts.
6
+ * A named `args` object is inferred from the method the author wrote.
7
+ * An unnamed parameter stays `any`: the host may pass any object.
8
+ * `defineApp` returns the inferred method, not this bound.
9
+ */
10
+ type DeclaredMethod<State extends object> = (ctx: AppContext<State>, args?: any) => unknown;
11
+ /** What `defineApp` accepts. `api` keeps the methods the author wrote. */
12
+ export interface AppDefinition<State extends object = Record<string, never>> {
13
+ name: string;
14
+ description: string;
15
+ api: Record<string, DeclaredMethod<State>>;
16
+ state?: State;
17
+ }
18
+ /**
19
+ * Validate and return the same definition object.
20
+ * `State` is inferred from `state`. An omitted `state` has no keys.
21
+ * @param def - author declaration
22
+ */
23
+ export declare function defineApp<State extends object, Definition extends AppDefinition<State>>(def: Definition & AppDefinition<State>): Definition;
24
+ export {};
25
+ //# sourceMappingURL=define-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-app.d.ts","sourceRoot":"","sources":["../../src/define-app.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C,6EAA6E;AAC7E,MAAM,MAAM,YAAY,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CACvE,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EACtB,IAAI,EAAE,OAAO,KACV,OAAO,CAAA;AAEZ;;;;;GAKG;AACH,KAAK,cAAc,CAAC,KAAK,SAAS,MAAM,IAAI,CAC1C,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC,EAEtB,IAAI,CAAC,EAAE,GAAG,KACP,OAAO,CAAA;AAEZ,0EAA0E;AAC1E,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IAC1C,KAAK,CAAC,EAAE,KAAK,CAAA;CACd;AAMD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,UAAU,SAAS,aAAa,CAAC,KAAK,CAAC,EACrF,GAAG,EAAE,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC,GACrC,UAAU,CAWZ"}
@@ -0,0 +1,16 @@
1
+ /** Required files and side trees. Callers use these names; they do not spell them again. */
2
+ export declare const appEntries: {
3
+ readonly manifest: "manifest.json";
4
+ readonly ui: "ui.tsx";
5
+ readonly backend: "main.api.ts";
6
+ readonly stylesheet: "ui.css";
7
+ };
8
+ /** Optional trees. `schema` is numbered SQL. `assets` is UI files loaded by URL. The others are import sides. */
9
+ export declare const appTrees: {
10
+ readonly ui: "ui";
11
+ readonly api: "api";
12
+ readonly shared: "shared";
13
+ readonly schema: "schema";
14
+ readonly assets: "assets";
15
+ };
16
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../src/entries.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,eAAO,MAAM,UAAU;;;;;CAKb,CAAA;AAEV,iHAAiH;AACjH,eAAO,MAAM,QAAQ;;;;;;CAMX,CAAA"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Every `type` on `ctx.llm` when `stream` is true.
3
+ * `done` is that call's final string. The method return is not one of these.
4
+ */
5
+ export declare const llmEventType: {
6
+ readonly status: "status";
7
+ readonly textDelta: "text-delta";
8
+ readonly error: "error";
9
+ readonly done: "done";
10
+ };
11
+ /**
12
+ * Every `type` on `ctx.agent` when `stream` is true.
13
+ * `done` is that call's final string. `tool` and `turn` are agent-only.
14
+ */
15
+ export declare const agentEventType: {
16
+ readonly tool: "tool";
17
+ readonly turn: "turn";
18
+ readonly status: "status";
19
+ readonly textDelta: "text-delta";
20
+ readonly error: "error";
21
+ readonly done: "done";
22
+ };
23
+ /** Every `type` on either model stream. */
24
+ export declare const eventType: {
25
+ readonly tool: "tool";
26
+ readonly turn: "turn";
27
+ readonly status: "status";
28
+ readonly textDelta: "text-delta";
29
+ readonly error: "error";
30
+ readonly done: "done";
31
+ };
32
+ export type LlmEventType = (typeof llmEventType)[keyof typeof llmEventType];
33
+ export type AgentEventType = (typeof agentEventType)[keyof typeof agentEventType];
34
+ export type EventType = (typeof eventType)[keyof typeof eventType];
35
+ //# sourceMappingURL=event-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-type.d.ts","sourceRoot":"","sources":["../../src/event-type.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,YAAY;;;;;CAKf,CAAA;AAEV;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;CAIjB,CAAA;AAEV,2CAA2C;AAC3C,eAAO,MAAM,SAAS;;;;;;;CAEZ,CAAA;AAEV,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAA;AAE3E,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,CAAA;AAEjF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAA"}
@@ -0,0 +1,17 @@
1
+ import { type DefinitionCode } from './codes.ts';
2
+ /** Which side of the app is importing. */
3
+ export type ImportSide = 'ui' | 'backend' | 'shared';
4
+ /**
5
+ * Classify a relative or side-crossing import. Bare allowlist checks stay with the host table.
6
+ * @param specifier - the import specifier as written
7
+ * @param side - the file's side
8
+ * @returns a code when the specifier is illegal, otherwise undefined
9
+ */
10
+ export declare function classifyImport(specifier: string, side: ImportSide, fromFile?: string): DefinitionCode | undefined;
11
+ /**
12
+ * Throw when {@link classifyImport} returns a code.
13
+ * @param specifier - the import specifier as written
14
+ * @param side - the file's side
15
+ */
16
+ export declare function assertImportAllowed(specifier: string, side: ImportSide): void;
17
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/imports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAG/D,0CAA0C;AAC1C,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,CAAA;AAsBpD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,SAAW,GAAG,cAAc,GAAG,SAAS,CAWnH;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAI7E"}
@@ -0,0 +1,12 @@
1
+ /** App definition: ids, manifest, imports, `defineApp`, `ctx`. No I/O. @module @mohou/contract */
2
+ export declare const packageId: "@mohou/contract";
3
+ export { parseAppId, type AppId } from './app-id.ts';
4
+ export { appEntries, appTrees } from './entries.ts';
5
+ export { resolveManifest, type Manifest } from './manifest.ts';
6
+ export { builtinWorkbenchId, workbenchEntries, type AppListItem, type AppWorkbench, type WorkbenchEntry, } from './workbench.ts';
7
+ export { assertImportAllowed, classifyImport, type ImportSide } from './imports.ts';
8
+ export { defineApp, type AppApiMethod, type AppDefinition } from './define-app.ts';
9
+ export { ContractError, definitionCodes, type DefinitionCode } from './codes.ts';
10
+ export { agentEventType, eventType, llmEventType, type AgentEventType, type EventType, type LlmEventType } from './event-type.ts';
11
+ export type { AppAgentEvent, AppAgentOptions, AppAgentTurnEndReason, AppConfig, AppContext, AppCredentials, AppHttpRequest, AppHttpResponse, AppKeyValueTable, AppLlmEvent, AppLlmOptions, AppModelOptions, AppStorage, AppSystemMetrics, SqlParams, SqlRow, SqlValue, } from './context.ts';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAElG,eAAO,MAAM,SAAS,EAAG,iBAA0B,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AACnF,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAClF,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAA;AAChF,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACjI,YAAY,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACrB,SAAS,EACT,UAAU,EACV,cAAc,EACd,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,aAAa,EACb,eAAe,EACf,UAAU,EACV,gBAAgB,EAChB,SAAS,EACT,MAAM,EACN,QAAQ,GACT,MAAM,cAAc,CAAA"}
@@ -0,0 +1,20 @@
1
+ import { type AppId } from './app-id.ts';
2
+ import { appEntries } from './entries.ts';
3
+ /** Manifest fields the loader accepts. Acronym derivation is not done here. */
4
+ export interface Manifest {
5
+ readonly id: AppId;
6
+ readonly name: string;
7
+ readonly description: string;
8
+ readonly version: string;
9
+ readonly entry: typeof appEntries.ui;
10
+ readonly acronym?: string;
11
+ readonly tags?: readonly string[];
12
+ readonly kind?: 'workbench';
13
+ }
14
+ /**
15
+ * Admit manifest JSON. `directoryName` is the app directory name, not a path.
16
+ * @param raw - parsed JSON, still untrusted
17
+ * @param directoryName - directory that contains the file
18
+ */
19
+ export declare function resolveManifest(raw: unknown, directoryName: string): Manifest;
20
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/manifest.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC,+EAA+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,UAAU,CAAC,EAAE,CAAA;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAiBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,GAAG,QAAQ,CA+B7E"}
@@ -0,0 +1,38 @@
1
+ /** Builtin workbench id. `setDefaultWorkbench` of this id shows the panel library. */
2
+ export declare const builtinWorkbenchId = "default";
3
+ /** One app on the owner list. Omitted fields were not present. */
4
+ export interface AppListItem {
5
+ readonly id: string;
6
+ readonly name: string;
7
+ readonly description: string;
8
+ readonly version: string;
9
+ readonly acronym: string;
10
+ readonly tags?: readonly string[];
11
+ readonly createdAt?: string;
12
+ readonly updatedAt?: string;
13
+ readonly activity?: {
14
+ readonly openCount: number;
15
+ readonly lastOpenedAt: string;
16
+ };
17
+ readonly kind?: 'workbench';
18
+ }
19
+ /** One workbench the slot can show. `default` is the one the slot will use. */
20
+ export interface WorkbenchEntry {
21
+ readonly id: string;
22
+ readonly name: string;
23
+ readonly builtin: boolean;
24
+ readonly default: boolean;
25
+ }
26
+ /**
27
+ * Extra ctx on a workbench app. Absent on every other app.
28
+ * The methods do not read another app's files or storage, and they do not change settings.
29
+ */
30
+ export interface AppWorkbench {
31
+ listApps(): Promise<readonly AppListItem[]>;
32
+ openApp(appId: string, title?: string): Promise<void>;
33
+ listWorkbenches(): Promise<readonly WorkbenchEntry[]>;
34
+ setDefaultWorkbench(id: string): Promise<void>;
35
+ }
36
+ /** One list for the builtin library and for `ctx.workbench.listWorkbenches`. */
37
+ export declare function workbenchEntries(apps: readonly AppListItem[], storedId: string | undefined, builtinName: string): readonly WorkbenchEntry[];
38
+ //# sourceMappingURL=workbench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workbench.d.ts","sourceRoot":"","sources":["../../src/workbench.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,YAAY,CAAA;AAE3C,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAA;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAC5B;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,IAAI,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAA;IAC3C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,eAAe,IAAI,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CAAA;IACrD,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/C;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,WAAW,EAAE,EAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,WAAW,EAAE,MAAM,GAClB,SAAS,cAAc,EAAE,CAe3B"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@mohou/contract",
3
+ "version": "1.0.20",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^22.19.0 || >=24.0.0"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "lib/types",
15
+ "README.md",
16
+ "!**/*.tsbuildinfo"
17
+ ],
18
+ "main": "./src/index.ts",
19
+ "types": "./lib/types/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./lib/types/index.d.ts",
23
+ "default": "./src/index.ts"
24
+ },
25
+ "./event-type": "./src/event-type.ts",
26
+ "./src/*": "./src/*",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "dependencies": {
30
+ "@mohou/values": "^1.0.20"
31
+ }
32
+ }
package/src/app-id.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Branded } from '@mohou/values'
2
+
3
+ import { ContractError } from './codes.ts'
4
+
5
+ const appIdPattern = /^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/
6
+
7
+ /** Reverse-DNS app id admitted at this boundary. */
8
+ export type AppId = Branded<'AppId'>
9
+
10
+ /**
11
+ * Admit an app id. Callers do not re-parse the brand.
12
+ * @param value - untrusted id text
13
+ * @returns the branded id
14
+ */
15
+ export function parseAppId(value: string): AppId {
16
+ if (!appIdPattern.test(value)) {
17
+ throw new ContractError('app-id-invalid', `app id is not reverse-DNS: ${value}`)
18
+ }
19
+ return value as AppId
20
+ }
package/src/codes.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Codes this definition emits. Other surfaces declare their own codes.
3
+ * Callers match `code`, not the message.
4
+ * @module @mohou/contract
5
+ */
6
+
7
+ export const definitionCodes = [
8
+ 'app-id-invalid',
9
+ 'manifest-invalid',
10
+ 'import-forbidden',
11
+ 'import-escape',
12
+ 'define-app-invalid',
13
+ ] as const
14
+
15
+ /** A failure emitted by this definition. */
16
+ export type DefinitionCode = (typeof definitionCodes)[number]
17
+
18
+ /** Failure a caller branches on. The message is for a person. */
19
+ export class ContractError extends Error {
20
+ readonly code: DefinitionCode
21
+
22
+ /**
23
+ * @param code - one of {@link definitionCodes}
24
+ * @param message - human text; not the match key
25
+ * @param options - optional `cause` for the original failure
26
+ */
27
+ constructor(code: DefinitionCode, message: string, options?: { cause?: unknown }) {
28
+ super(message, options)
29
+ this.name = 'ContractError'
30
+ this.code = code
31
+ }
32
+ }
package/src/context.ts ADDED
@@ -0,0 +1,154 @@
1
+ import type { AppId } from './app-id.ts'
2
+ import type { AppWorkbench } from './workbench.ts'
3
+
4
+ import { agentEventType, llmEventType } from './event-type.ts'
5
+
6
+ /** A cell accepted by SQLite. */
7
+ export type SqlValue = null | number | string | bigint | Uint8Array
8
+
9
+ /** One result row. Column names are the keys. */
10
+ export interface SqlRow {
11
+ [column: string]: SqlValue
12
+ }
13
+
14
+ /** Positional or named parameters. Named keys omit the sigil. */
15
+ export type SqlParams = readonly SqlValue[] | Record<string, SqlValue>
16
+
17
+ /** The host-owned `kv` table. `query` and `run` cannot see it. */
18
+ export interface AppKeyValueTable {
19
+ get(key: string): Promise<unknown>
20
+ set(key: string, value: unknown): Promise<void>
21
+ delete(key: string): Promise<void>
22
+ clear(): Promise<void>
23
+ }
24
+
25
+ /** One SQLite file. `kv()` is the key-value table. SQL is everything else. */
26
+ export interface AppStorage {
27
+ kv(): AppKeyValueTable
28
+ query(sql: string, params?: SqlParams): Promise<SqlRow[]>
29
+ run(sql: string, params?: SqlParams): Promise<{ changes: number; lastInsertRowid: number | bigint }>
30
+ transaction<T>(work: (tx: AppStorage) => Promise<T>): Promise<T>
31
+ }
32
+
33
+ /** `ctx.http` request. Object `body` is sent as JSON. */
34
+ export interface AppHttpRequest {
35
+ url: string
36
+ method?: string
37
+ headers?: Record<string, string>
38
+ query?: Record<string, string | number | boolean | null | undefined>
39
+ body?: unknown
40
+ timeout?: number
41
+ signal?: AbortSignal
42
+ }
43
+
44
+ /** `ctx.http` result. Never a platform `Response`. */
45
+ export interface AppHttpResponse {
46
+ ok: boolean
47
+ status: number
48
+ headers: Record<string, string>
49
+ text: string
50
+ /** Parsed body when the content type contains json and parsing succeeds, otherwise null. */
51
+ json: unknown
52
+ }
53
+
54
+ /** Options shared by `ctx.llm` and `ctx.agent`. Omitted limits use host policy. */
55
+ export interface AppModelOptions {
56
+ provider?: string
57
+ model?: string
58
+ system?: string
59
+ schema?: unknown
60
+ maxTokens?: number
61
+ /** Attempts in total, including the first. */
62
+ retryTimes?: number
63
+ signal?: AbortSignal
64
+ }
65
+
66
+ /** Why an agent turn ended. `kind` stays open so a provider can add a reason. */
67
+ export interface AppAgentTurnEndReason {
68
+ kind: string
69
+ error?: unknown
70
+ reason?: unknown
71
+ }
72
+
73
+ /** Observation only. The `ctx.agent` return stays a string. */
74
+ export type AppAgentEvent =
75
+ | { type: typeof agentEventType.status; status: 'running' | 'idle' }
76
+ | { type: typeof agentEventType.textDelta; text: string }
77
+ | { type: typeof agentEventType.tool; phase: 'start' | 'end'; name: string; args?: unknown; result?: unknown }
78
+ | { type: typeof agentEventType.turn; phase: 'start'; turn: number }
79
+ | { type: typeof agentEventType.turn; phase: 'end'; turn: number; reason?: AppAgentTurnEndReason }
80
+ | { type: typeof agentEventType.error; message: string }
81
+ | { type: typeof agentEventType.done; text: string }
82
+
83
+ /** Observation for `ctx.llm` when `stream` is true. The return stays a string. */
84
+ export type AppLlmEvent =
85
+ | { type: typeof llmEventType.status; status: 'running' | 'idle' }
86
+ | { type: typeof llmEventType.textDelta; text: string }
87
+ | { type: typeof llmEventType.error; message: string }
88
+ | { type: typeof llmEventType.done; text: string }
89
+
90
+ /** `ctx.llm` options. `stream` defaults to false. */
91
+ export interface AppLlmOptions extends AppModelOptions {
92
+ /** When true, the call returns a stream the method reads with `for await`. It does not reach the UI by itself. */
93
+ stream?: boolean
94
+ }
95
+
96
+ /** `ctx.agent` options. `stream` defaults to false. */
97
+ export interface AppAgentOptions extends AppModelOptions {
98
+ /** When true, the call returns a stream the method reads with `for await`. It does not reach the UI by itself. */
99
+ stream?: boolean
100
+ maxIterations?: number
101
+ cwdType?: 'app' | 'process' | 'temp' | 'custom'
102
+ cwd?: string
103
+ }
104
+
105
+ /** Public host policy visible to an app. No secrets. */
106
+ export interface AppConfig {
107
+ theme: 'light' | 'dark' | 'system'
108
+ palette: string
109
+ locale: string
110
+ chatLanguage: string
111
+ hostPort: number
112
+ llm: { provider: string; model: string } | null
113
+ }
114
+
115
+ /** One OS snapshot. `loadavg` is null where the OS does not provide it. */
116
+ export interface AppSystemMetrics {
117
+ platform: string
118
+ arch: string
119
+ hostname: string
120
+ uptimeSec: number
121
+ loadavg: { '1m': number; '5m': number; '15m': number } | null
122
+ memory: { total: number; free: number; used: number; usedRatio: number }
123
+ cpu: { count: number; model: string; speedMHz: number }
124
+ collectedAt: string
125
+ }
126
+
127
+ /** One secret, by the name the owner stored. Listing is not on this object. */
128
+ export interface AppCredentials {
129
+ get(name: string): Promise<string | undefined>
130
+ }
131
+
132
+ /** First argument of every `api` method. `State` is the object the author declared. */
133
+ export interface AppContext<State extends object = Record<string, never>> {
134
+ appId: AppId
135
+ appDir: string
136
+ storage: AppStorage
137
+ state: State
138
+ credentials: AppCredentials
139
+ config: AppConfig
140
+ log(...args: unknown[]): void
141
+ signal?: AbortSignal
142
+ push(name: string, params?: unknown): void
143
+ http(url: string | AppHttpRequest, opts?: Omit<AppHttpRequest, 'url'>): Promise<AppHttpResponse>
144
+ bash(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>
145
+ pwsh(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>
146
+ system: { metrics(): Promise<AppSystemMetrics> }
147
+ llm(prompt: string, opts: AppLlmOptions & { stream: true }): AsyncIterable<AppLlmEvent> & Promise<string>
148
+ llm(prompt: string, opts?: AppLlmOptions): Promise<string>
149
+ agent(goal: string, opts: AppAgentOptions & { stream: true }): AsyncIterable<AppAgentEvent> & Promise<string>
150
+ agent(goal: string, opts?: AppAgentOptions): Promise<string>
151
+ mcp(serverId: string, toolName: string, args?: Record<string, unknown>): Promise<unknown>
152
+ /** Present only when the manifest kind is `workbench`. */
153
+ workbench?: AppWorkbench
154
+ }
@@ -0,0 +1,52 @@
1
+ import type { AppContext } from './context.ts'
2
+ import { ContractError } from './codes.ts'
3
+
4
+ /** One backend method as the host calls it. `args` is the unchecked JSON. */
5
+ export type AppApiMethod<State extends object = Record<string, never>> = (
6
+ ctx: AppContext<State>,
7
+ args: unknown,
8
+ ) => unknown
9
+
10
+ /**
11
+ * Bound for a method `defineApp` accepts.
12
+ * A named `args` object is inferred from the method the author wrote.
13
+ * An unnamed parameter stays `any`: the host may pass any object.
14
+ * `defineApp` returns the inferred method, not this bound.
15
+ */
16
+ type DeclaredMethod<State extends object> = (
17
+ ctx: AppContext<State>,
18
+ // oxlint-disable-next-line typescript/no-explicit-any -- unnamed args accept the unchecked object; a named object is inferred
19
+ args?: any,
20
+ ) => unknown
21
+
22
+ /** What `defineApp` accepts. `api` keeps the methods the author wrote. */
23
+ export interface AppDefinition<State extends object = Record<string, never>> {
24
+ name: string
25
+ description: string
26
+ api: Record<string, DeclaredMethod<State>>
27
+ state?: State
28
+ }
29
+
30
+ function isObject(value: unknown): value is object {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
32
+ }
33
+
34
+ /**
35
+ * Validate and return the same definition object.
36
+ * `State` is inferred from `state`. An omitted `state` has no keys.
37
+ * @param def - author declaration
38
+ */
39
+ export function defineApp<State extends object, Definition extends AppDefinition<State>>(
40
+ def: Definition & AppDefinition<State>,
41
+ ): Definition {
42
+ if (!def.name || !def.description) {
43
+ throw new ContractError('define-app-invalid', 'defineApp requires name and description')
44
+ }
45
+ if (!isObject(def.api)) {
46
+ throw new ContractError('define-app-invalid', 'defineApp.api must be an object')
47
+ }
48
+ if (def.state !== undefined && !isObject(def.state)) {
49
+ throw new ContractError('define-app-invalid', 'defineApp.state must be an object')
50
+ }
51
+ return def
52
+ }
package/src/entries.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** Required files and side trees. Callers use these names; they do not spell them again. */
2
+ export const appEntries = {
3
+ manifest: 'manifest.json',
4
+ ui: 'ui.tsx',
5
+ backend: 'main.api.ts',
6
+ stylesheet: 'ui.css',
7
+ } as const
8
+
9
+ /** Optional trees. `schema` is numbered SQL. `assets` is UI files loaded by URL. The others are import sides. */
10
+ export const appTrees = {
11
+ ui: 'ui',
12
+ api: 'api',
13
+ shared: 'shared',
14
+ schema: 'schema',
15
+ assets: 'assets',
16
+ } as const
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Every `type` on `ctx.llm` when `stream` is true.
3
+ * `done` is that call's final string. The method return is not one of these.
4
+ */
5
+ export const llmEventType = {
6
+ status: 'status',
7
+ textDelta: 'text-delta',
8
+ error: 'error',
9
+ done: 'done',
10
+ } as const
11
+
12
+ /**
13
+ * Every `type` on `ctx.agent` when `stream` is true.
14
+ * `done` is that call's final string. `tool` and `turn` are agent-only.
15
+ */
16
+ export const agentEventType = {
17
+ ...llmEventType,
18
+ tool: 'tool',
19
+ turn: 'turn',
20
+ } as const
21
+
22
+ /** Every `type` on either model stream. */
23
+ export const eventType = {
24
+ ...agentEventType,
25
+ } as const
26
+
27
+ export type LlmEventType = (typeof llmEventType)[keyof typeof llmEventType]
28
+
29
+ export type AgentEventType = (typeof agentEventType)[keyof typeof agentEventType]
30
+
31
+ export type EventType = (typeof eventType)[keyof typeof eventType]
package/src/imports.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { ContractError, type DefinitionCode } from './codes.ts'
2
+ import { appEntries, appTrees } from './entries.ts'
3
+
4
+ /** Which side of the app is importing. */
5
+ export type ImportSide = 'ui' | 'backend' | 'shared'
6
+
7
+ function crosses(specifier: string, tree: string): boolean {
8
+ return specifier === tree || specifier.startsWith(`${tree}/`) || specifier.includes(`/${tree}/`)
9
+ }
10
+
11
+ function escapesApp(specifier: string, fromFile: string): boolean {
12
+ if (specifier.startsWith('/') || /^[A-Za-z]:[\\/]/.test(specifier)) return true
13
+ if (!specifier.startsWith('.')) return false
14
+ const parts = fromFile.split('/').slice(0, -1).filter(part => part.length > 0)
15
+ for (const part of specifier.split('/')) {
16
+ if (part === '' || part === '.') continue
17
+ if (part === '..') {
18
+ if (parts.length === 0) return true
19
+ parts.pop()
20
+ } else {
21
+ parts.push(part)
22
+ }
23
+ }
24
+ return false
25
+ }
26
+
27
+ /**
28
+ * Classify a relative or side-crossing import. Bare allowlist checks stay with the host table.
29
+ * @param specifier - the import specifier as written
30
+ * @param side - the file's side
31
+ * @returns a code when the specifier is illegal, otherwise undefined
32
+ */
33
+ export function classifyImport(specifier: string, side: ImportSide, fromFile = 'ui.tsx'): DefinitionCode | undefined {
34
+ if (escapesApp(specifier, fromFile)) return 'import-escape'
35
+ if (crosses(specifier, appTrees.assets)) return 'import-forbidden'
36
+ if (side === 'ui' && (specifier === appEntries.backend || crosses(specifier, appTrees.api))) {
37
+ return 'import-forbidden'
38
+ }
39
+ if (side === 'backend' && crosses(specifier, appTrees.ui)) return 'import-forbidden'
40
+ if (side === 'shared' && (crosses(specifier, appTrees.ui) || crosses(specifier, appTrees.api) || specifier === appEntries.backend)) {
41
+ return 'import-forbidden'
42
+ }
43
+ return undefined
44
+ }
45
+
46
+ /**
47
+ * Throw when {@link classifyImport} returns a code.
48
+ * @param specifier - the import specifier as written
49
+ * @param side - the file's side
50
+ */
51
+ export function assertImportAllowed(specifier: string, side: ImportSide): void {
52
+ const code = classifyImport(specifier, side)
53
+ if (code === undefined) return
54
+ throw new ContractError(code, `${side} cannot import ${specifier}`)
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /** App definition: ids, manifest, imports, `defineApp`, `ctx`. No I/O. @module @mohou/contract */
2
+
3
+ export const packageId = '@mohou/contract' as const
4
+
5
+ export { parseAppId, type AppId } from './app-id.ts'
6
+ export { appEntries, appTrees } from './entries.ts'
7
+ export { resolveManifest, type Manifest } from './manifest.ts'
8
+ export {
9
+ builtinWorkbenchId,
10
+ workbenchEntries,
11
+ type AppListItem,
12
+ type AppWorkbench,
13
+ type WorkbenchEntry,
14
+ } from './workbench.ts'
15
+ export { assertImportAllowed, classifyImport, type ImportSide } from './imports.ts'
16
+ export { defineApp, type AppApiMethod, type AppDefinition } from './define-app.ts'
17
+ export { ContractError, definitionCodes, type DefinitionCode } from './codes.ts'
18
+ export { agentEventType, eventType, llmEventType, type AgentEventType, type EventType, type LlmEventType } from './event-type.ts'
19
+ export type {
20
+ AppAgentEvent,
21
+ AppAgentOptions,
22
+ AppAgentTurnEndReason,
23
+ AppConfig,
24
+ AppContext,
25
+ AppCredentials,
26
+ AppHttpRequest,
27
+ AppHttpResponse,
28
+ AppKeyValueTable,
29
+ AppLlmEvent,
30
+ AppLlmOptions,
31
+ AppModelOptions,
32
+ AppStorage,
33
+ AppSystemMetrics,
34
+ SqlParams,
35
+ SqlRow,
36
+ SqlValue,
37
+ } from './context.ts'
@@ -0,0 +1,97 @@
1
+ import { ContractError } from './codes.ts'
2
+ import { parseAppId, type AppId } from './app-id.ts'
3
+ import { appEntries } from './entries.ts'
4
+
5
+ /** Manifest fields the loader accepts. Acronym derivation is not done here. */
6
+ export interface Manifest {
7
+ readonly id: AppId
8
+ readonly name: string
9
+ readonly description: string
10
+ readonly version: string
11
+ readonly entry: typeof appEntries.ui
12
+ readonly acronym?: string
13
+ readonly tags?: readonly string[]
14
+ readonly kind?: 'workbench'
15
+ }
16
+
17
+ const acronymToken = /^[\p{L}\p{N}]{2}$/u
18
+ const tagToken = /^[a-z][a-z0-9-]*$/
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
22
+ }
23
+
24
+ function requiredString(record: Record<string, unknown>, key: string): string {
25
+ const value = record[key]
26
+ if (typeof value !== 'string' || value.length === 0) {
27
+ throw new ContractError('manifest-invalid', `manifest ${key} is required`)
28
+ }
29
+ return value
30
+ }
31
+
32
+ /**
33
+ * Admit manifest JSON. `directoryName` is the app directory name, not a path.
34
+ * @param raw - parsed JSON, still untrusted
35
+ * @param directoryName - directory that contains the file
36
+ */
37
+ export function resolveManifest(raw: unknown, directoryName: string): Manifest {
38
+ if (!isRecord(raw)) {
39
+ throw new ContractError('manifest-invalid', 'manifest must be an object')
40
+ }
41
+ const name = requiredString(raw, 'name')
42
+ const description = requiredString(raw, 'description')
43
+ const version = requiredString(raw, 'version')
44
+ const entry = requiredString(raw, 'entry')
45
+ if (entry !== appEntries.ui) {
46
+ throw new ContractError('manifest-invalid', `manifest entry must be ${appEntries.ui}`)
47
+ }
48
+ const idText = requiredString(raw, 'id')
49
+ let id: AppId
50
+ try {
51
+ id = parseAppId(idText)
52
+ } catch (error) {
53
+ throw new ContractError('app-id-invalid', `app id is not reverse-DNS: ${idText}`, { cause: error })
54
+ }
55
+ if (id !== directoryName) {
56
+ throw new ContractError('app-id-invalid', `app id ${idText} does not match directory ${directoryName}`)
57
+ }
58
+ const acronym = resolveAcronym(raw.acronym)
59
+ const tags = resolveTags(raw.tags)
60
+ const kind = resolveKind(raw.kind)
61
+ const admitted = { id, name, description, version, entry: appEntries.ui }
62
+ return {
63
+ ...admitted,
64
+ ...acronym === undefined ? {} : { acronym },
65
+ ...tags === undefined ? {} : { tags },
66
+ ...kind === undefined ? {} : { kind },
67
+ }
68
+ }
69
+
70
+ function resolveAcronym(value: unknown): string | undefined {
71
+ if (value === undefined) return undefined
72
+ if (typeof value !== 'string' || !acronymToken.test(value)) {
73
+ throw new ContractError('manifest-invalid', 'manifest acronym must be two letters or digits')
74
+ }
75
+ return value
76
+ }
77
+
78
+ function resolveKind(value: unknown): 'workbench' | undefined {
79
+ if (value === undefined || value === 'app') return undefined
80
+ if (value === 'workbench') return 'workbench'
81
+ throw new ContractError('manifest-invalid', 'manifest kind must be app or workbench')
82
+ }
83
+
84
+ function resolveTags(value: unknown): readonly string[] | undefined {
85
+ if (value === undefined) return undefined
86
+ if (!Array.isArray(value) || value.length === 0) {
87
+ throw new ContractError('manifest-invalid', 'manifest tags must be a non-empty list')
88
+ }
89
+ const tags: string[] = []
90
+ for (const tag of value) {
91
+ if (typeof tag !== 'string' || !tagToken.test(tag)) {
92
+ throw new ContractError('manifest-invalid', 'manifest tag must be a lowercase token')
93
+ }
94
+ if (!tags.includes(tag)) tags.push(tag)
95
+ }
96
+ return tags
97
+ }
@@ -0,0 +1,57 @@
1
+ /** Builtin workbench id. `setDefaultWorkbench` of this id shows the panel library. */
2
+ export const builtinWorkbenchId = 'default'
3
+
4
+ /** One app on the owner list. Omitted fields were not present. */
5
+ export interface AppListItem {
6
+ readonly id: string
7
+ readonly name: string
8
+ readonly description: string
9
+ readonly version: string
10
+ readonly acronym: string
11
+ readonly tags?: readonly string[]
12
+ readonly createdAt?: string
13
+ readonly updatedAt?: string
14
+ readonly activity?: { readonly openCount: number; readonly lastOpenedAt: string }
15
+ readonly kind?: 'workbench'
16
+ }
17
+
18
+ /** One workbench the slot can show. `default` is the one the slot will use. */
19
+ export interface WorkbenchEntry {
20
+ readonly id: string
21
+ readonly name: string
22
+ readonly builtin: boolean
23
+ readonly default: boolean
24
+ }
25
+
26
+ /**
27
+ * Extra ctx on a workbench app. Absent on every other app.
28
+ * The methods do not read another app's files or storage, and they do not change settings.
29
+ */
30
+ export interface AppWorkbench {
31
+ listApps(): Promise<readonly AppListItem[]>
32
+ openApp(appId: string, title?: string): Promise<void>
33
+ listWorkbenches(): Promise<readonly WorkbenchEntry[]>
34
+ setDefaultWorkbench(id: string): Promise<void>
35
+ }
36
+
37
+ /** One list for the builtin library and for `ctx.workbench.listWorkbenches`. */
38
+ export function workbenchEntries(
39
+ apps: readonly AppListItem[],
40
+ storedId: string | undefined,
41
+ builtinName: string,
42
+ ): readonly WorkbenchEntry[] {
43
+ const active = storedId !== undefined
44
+ && storedId !== builtinWorkbenchId
45
+ && apps.some(app => app.id === storedId && app.kind === 'workbench')
46
+ ? storedId
47
+ : builtinWorkbenchId
48
+ return [
49
+ { id: builtinWorkbenchId, name: builtinName, builtin: true, default: active === builtinWorkbenchId },
50
+ ...apps.filter(app => app.kind === 'workbench').map(app => ({
51
+ id: app.id,
52
+ name: app.name,
53
+ builtin: false,
54
+ default: app.id === active,
55
+ })),
56
+ ]
57
+ }