@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 +10 -0
- package/lib/types/app-id.d.ts +10 -0
- package/lib/types/app-id.d.ts.map +1 -0
- package/lib/types/codes.d.ts +21 -0
- package/lib/types/codes.d.ts.map +1 -0
- package/lib/types/context.d.ts +198 -0
- package/lib/types/context.d.ts.map +1 -0
- package/lib/types/define-app.d.ts +25 -0
- package/lib/types/define-app.d.ts.map +1 -0
- package/lib/types/entries.d.ts +16 -0
- package/lib/types/entries.d.ts.map +1 -0
- package/lib/types/event-type.d.ts +35 -0
- package/lib/types/event-type.d.ts.map +1 -0
- package/lib/types/imports.d.ts +17 -0
- package/lib/types/imports.d.ts.map +1 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/manifest.d.ts +20 -0
- package/lib/types/manifest.d.ts.map +1 -0
- package/lib/types/workbench.d.ts +38 -0
- package/lib/types/workbench.d.ts.map +1 -0
- package/package.json +32 -0
- package/src/app-id.ts +20 -0
- package/src/codes.ts +32 -0
- package/src/context.ts +154 -0
- package/src/define-app.ts +52 -0
- package/src/entries.ts +16 -0
- package/src/event-type.ts +31 -0
- package/src/imports.ts +55 -0
- package/src/index.ts +37 -0
- package/src/manifest.ts +97 -0
- package/src/workbench.ts +57 -0
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'
|
package/src/manifest.ts
ADDED
|
@@ -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
|
+
}
|
package/src/workbench.ts
ADDED
|
@@ -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
|
+
}
|