@liquidmetal-ai/precip 1.0.0
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/.prettierrc +9 -0
- package/CHANGELOG.md +8 -0
- package/eslint.config.mjs +28 -0
- package/package.json +53 -0
- package/src/engine/agent.ts +478 -0
- package/src/engine/llm-provider.test.ts +275 -0
- package/src/engine/llm-provider.ts +330 -0
- package/src/engine/stream-parser.ts +170 -0
- package/src/index.ts +142 -0
- package/src/mounts/mount-manager.test.ts +516 -0
- package/src/mounts/mount-manager.ts +327 -0
- package/src/mounts/mount-registry.ts +196 -0
- package/src/mounts/zod-to-string.test.ts +154 -0
- package/src/mounts/zod-to-string.ts +213 -0
- package/src/presets/agent-tools.ts +57 -0
- package/src/presets/index.ts +5 -0
- package/src/sandbox/README.md +1321 -0
- package/src/sandbox/bridges/README.md +571 -0
- package/src/sandbox/bridges/actor.test.ts +229 -0
- package/src/sandbox/bridges/actor.ts +195 -0
- package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
- package/src/sandbox/bridges/bucket.test.ts +300 -0
- package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
- package/src/sandbox/bridges/console-multiple.test.ts +187 -0
- package/src/sandbox/bridges/console.test.ts +157 -0
- package/src/sandbox/bridges/console.ts +122 -0
- package/src/sandbox/bridges/fetch.ts +93 -0
- package/src/sandbox/bridges/index.ts +78 -0
- package/src/sandbox/bridges/readable-stream.ts +323 -0
- package/src/sandbox/bridges/response.test.ts +154 -0
- package/src/sandbox/bridges/response.ts +123 -0
- package/src/sandbox/bridges/review-fixes.test.ts +331 -0
- package/src/sandbox/bridges/search.test.ts +475 -0
- package/src/sandbox/bridges/search.ts +264 -0
- package/src/sandbox/bridges/shared/body-methods.ts +93 -0
- package/src/sandbox/bridges/shared/cleanup.ts +112 -0
- package/src/sandbox/bridges/shared/convert.ts +76 -0
- package/src/sandbox/bridges/shared/headers.ts +181 -0
- package/src/sandbox/bridges/shared/index.ts +36 -0
- package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
- package/src/sandbox/bridges/shared/path-parser.ts +109 -0
- package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
- package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
- package/src/sandbox/bridges/shared/response-object.ts +280 -0
- package/src/sandbox/bridges/shared/result-builder.ts +130 -0
- package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
- package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
- package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
- package/src/sandbox/bridges/storage.ts +421 -0
- package/src/sandbox/bridges/text-decoder.ts +190 -0
- package/src/sandbox/bridges/text-encoder.ts +102 -0
- package/src/sandbox/bridges/types.ts +39 -0
- package/src/sandbox/bridges/utils.ts +123 -0
- package/src/sandbox/index.ts +6 -0
- package/src/sandbox/quickjs-wasm.d.ts +9 -0
- package/src/sandbox/sandbox.test.ts +191 -0
- package/src/sandbox/sandbox.ts +831 -0
- package/src/sandbox/test-helper.ts +43 -0
- package/src/sandbox/test-mocks.ts +154 -0
- package/src/sandbox/user-stream.test.ts +77 -0
- package/src/skills/frontmatter.test.ts +305 -0
- package/src/skills/frontmatter.ts +200 -0
- package/src/skills/index.ts +9 -0
- package/src/skills/skills-loader.test.ts +237 -0
- package/src/skills/skills-loader.ts +200 -0
- package/src/tools/actor-storage-tools.ts +250 -0
- package/src/tools/code-tools.test.ts +199 -0
- package/src/tools/code-tools.ts +444 -0
- package/src/tools/file-tools.ts +206 -0
- package/src/tools/registry.ts +125 -0
- package/src/tools/script-tools.ts +145 -0
- package/src/tools/smartbucket-tools.ts +203 -0
- package/src/tools/sql-tools.ts +213 -0
- package/src/tools/tool-factory.ts +119 -0
- package/src/types.ts +512 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +33 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount Manager - Routes path prefixes to Raindrop resources
|
|
3
|
+
*
|
|
4
|
+
* Supports mounting Buckets, SmartBuckets, KvCache, SqlDatabase, and Actors with path prefixes.
|
|
5
|
+
* All mounts require an explicit type field - no automatic type detection.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - @user-data/profile.json → USER_BUCKET.get("profile.json")
|
|
9
|
+
* - @logs/2024/jan.txt → LOG_BUCKET.get("2024/jan.txt")
|
|
10
|
+
* - @knowledge/query → SMART_BUCKET.search({input: "query"})
|
|
11
|
+
* - sql:analytics → ANALYTICS_DB
|
|
12
|
+
* - kv:cache → CACHE_KV
|
|
13
|
+
* - actor:session → SESSION_ACTOR (accessed via actor.get('session', 'instance-name'))
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
Logger,
|
|
18
|
+
MountConfig,
|
|
19
|
+
MountInfo,
|
|
20
|
+
ActorMountInfo,
|
|
21
|
+
ParsedPath,
|
|
22
|
+
MountProvider
|
|
23
|
+
} from '../types.js';
|
|
24
|
+
import { zodToTypeString } from './zod-to-string.js';
|
|
25
|
+
import { createMountInfo, registerStandardMountTypes } from './mount-registry.js';
|
|
26
|
+
|
|
27
|
+
// Initialize standard mount types on module load
|
|
28
|
+
registerStandardMountTypes();
|
|
29
|
+
|
|
30
|
+
export class MountManager implements MountProvider {
|
|
31
|
+
private mounts = new Map<string, MountInfo>();
|
|
32
|
+
private logger?: Logger;
|
|
33
|
+
|
|
34
|
+
constructor(config: MountConfig = {}, logger?: Logger) {
|
|
35
|
+
this.logger = logger;
|
|
36
|
+
|
|
37
|
+
for (const [name, input] of Object.entries(config)) {
|
|
38
|
+
this.mountConfig(name, input);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mount a configuration (discriminated union with explicit type)
|
|
44
|
+
*/
|
|
45
|
+
private mountConfig(name: string, config: MountConfig[string]): void {
|
|
46
|
+
// Use registry to create mount info
|
|
47
|
+
const info = createMountInfo(name, config);
|
|
48
|
+
this.mounts.set(name, info);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Unmount a resource
|
|
53
|
+
*/
|
|
54
|
+
unmount(name: string): boolean {
|
|
55
|
+
return this.mounts.delete(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get mount by name
|
|
60
|
+
*/
|
|
61
|
+
getMount(name: string): MountInfo | undefined {
|
|
62
|
+
return this.mounts.get(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all mounts
|
|
67
|
+
*/
|
|
68
|
+
getAllMounts(): Map<string, MountInfo> {
|
|
69
|
+
return new Map(this.mounts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse a path to extract mount name and resource path
|
|
74
|
+
*
|
|
75
|
+
* Format: /mount-name/path/to/resource
|
|
76
|
+
* Examples:
|
|
77
|
+
* /uploads/images/photo.jpg → { mountName: 'uploads', path: 'images/photo.jpg' }
|
|
78
|
+
* /cache/user:123 → { mountName: 'cache', path: 'user:123' }
|
|
79
|
+
* /analytics → { mountName: 'analytics', path: '' }
|
|
80
|
+
*/
|
|
81
|
+
parsePath(fullPath: string): ParsedPath {
|
|
82
|
+
// Must start with /
|
|
83
|
+
if (!fullPath.startsWith('/')) {
|
|
84
|
+
throw new Error(`Invalid path: ${fullPath}. Paths must start with /`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const withoutSlash = fullPath.slice(1);
|
|
88
|
+
|
|
89
|
+
if (withoutSlash === '') {
|
|
90
|
+
throw new Error(`Invalid path: ${fullPath}. Mount name required.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const slashIndex = withoutSlash.indexOf('/');
|
|
94
|
+
|
|
95
|
+
if (slashIndex === -1) {
|
|
96
|
+
// Just /mount-name with no path
|
|
97
|
+
return { mountName: withoutSlash, path: '', fullPath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const mountName = withoutSlash.slice(0, slashIndex);
|
|
101
|
+
const path = withoutSlash.slice(slashIndex + 1);
|
|
102
|
+
|
|
103
|
+
return { mountName, path, fullPath };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a path to a mount and resource path
|
|
108
|
+
*/
|
|
109
|
+
resolvePath(fullPath: string): { mount: MountInfo; path: string } {
|
|
110
|
+
const parsed = this.parsePath(fullPath);
|
|
111
|
+
const mount = this.mounts.get(parsed.mountName);
|
|
112
|
+
|
|
113
|
+
if (!mount) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Mount not found: ${parsed.mountName}. Available mounts: ${Array.from(this.mounts.keys()).join(', ')}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { mount, path: parsed.path };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List all mounts by type
|
|
124
|
+
*/
|
|
125
|
+
getMountsByType<T extends MountInfo['type']>(type: T): MountInfo[] {
|
|
126
|
+
return Array.from(this.mounts.values()).filter(m => m.type === type);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all actor mounts with their schemas
|
|
131
|
+
*/
|
|
132
|
+
getActorMounts(): Map<string, ActorMountInfo> {
|
|
133
|
+
const actorMounts = new Map<string, ActorMountInfo>();
|
|
134
|
+
for (const [name, mount] of this.mounts) {
|
|
135
|
+
if (mount.type === 'actor') {
|
|
136
|
+
actorMounts.set(name, mount);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return actorMounts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create sandbox globals from mounts
|
|
144
|
+
* Returns globals that can be injected into QuickJS sandbox
|
|
145
|
+
*/
|
|
146
|
+
createSandboxGlobals(): Record<string, any> {
|
|
147
|
+
const globals: Record<string, any> = {};
|
|
148
|
+
|
|
149
|
+
// Create bucket global with all bucket and smartbucket mounts
|
|
150
|
+
const bucketMounts = [
|
|
151
|
+
...this.getMountsByType('bucket'),
|
|
152
|
+
...this.getMountsByType('smartbucket')
|
|
153
|
+
];
|
|
154
|
+
if (bucketMounts.length > 0) {
|
|
155
|
+
globals.bucket = {
|
|
156
|
+
// These will be replaced with actual async functions by the tool
|
|
157
|
+
_mounts: bucketMounts.map(m => ({ name: m.name, type: m.type })),
|
|
158
|
+
_type: 'bucket'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create kv global with all KV mounts
|
|
163
|
+
const kvMounts = this.getMountsByType('kv');
|
|
164
|
+
if (kvMounts.length > 0) {
|
|
165
|
+
globals.kv = {
|
|
166
|
+
_mounts: kvMounts.map(m => m.name),
|
|
167
|
+
_type: 'kv'
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Create db global with all database mounts
|
|
172
|
+
const dbMounts = this.getMountsByType('database');
|
|
173
|
+
if (dbMounts.length > 0) {
|
|
174
|
+
globals.db = {
|
|
175
|
+
_mounts: dbMounts.map(m => m.name),
|
|
176
|
+
_type: 'database'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Create actor global placeholder (will be replaced by actor bridge)
|
|
181
|
+
const actorMounts = this.getActorMounts();
|
|
182
|
+
if (actorMounts.size > 0) {
|
|
183
|
+
globals.actor = {
|
|
184
|
+
_mounts: Array.from(actorMounts.entries()).map(([name, mount]) => ({
|
|
185
|
+
name,
|
|
186
|
+
methods: Object.keys(mount.methods)
|
|
187
|
+
})),
|
|
188
|
+
_type: 'actor'
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Create actor-storage placeholder (will be handled by storage bridge)
|
|
193
|
+
const actorStorageMounts = this.getMountsByType('actor-storage');
|
|
194
|
+
if (actorStorageMounts.length > 0) {
|
|
195
|
+
globals.actorStorage = {
|
|
196
|
+
_mounts: actorStorageMounts.map(m => m.name),
|
|
197
|
+
_type: 'actor-storage'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return globals;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Generate a formatted description of all mounts for inclusion in system prompts
|
|
206
|
+
* Returns a structured string that describes available resources
|
|
207
|
+
*/
|
|
208
|
+
getMountsDescription(): string {
|
|
209
|
+
if (this.mounts.size === 0) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
|
|
215
|
+
// Header
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push('AVAILABLE STORAGE:');
|
|
218
|
+
lines.push('');
|
|
219
|
+
lines.push('All paths start with /<mount-name>/. Use these functions:');
|
|
220
|
+
lines.push('');
|
|
221
|
+
|
|
222
|
+
// Group by type
|
|
223
|
+
const bucketMounts = this.getMountsByType('bucket');
|
|
224
|
+
const smartbucketMounts = this.getMountsByType('smartbucket');
|
|
225
|
+
const kvMounts = this.getMountsByType('kv');
|
|
226
|
+
const dbMounts = this.getMountsByType('database');
|
|
227
|
+
const actorMounts = Array.from(this.getActorMounts().values());
|
|
228
|
+
const actorStorageMounts = this.getMountsByType('actor-storage');
|
|
229
|
+
|
|
230
|
+
// File storage (buckets and smartbuckets)
|
|
231
|
+
const allFileMounts = [...bucketMounts, ...smartbucketMounts];
|
|
232
|
+
if (allFileMounts.length > 0) {
|
|
233
|
+
lines.push('## File Storage');
|
|
234
|
+
lines.push('Durable object storage for files and binary data.');
|
|
235
|
+
lines.push('read() returns Response-like with .body (stream), .size, .uploaded');
|
|
236
|
+
lines.push('write() accepts optional { contentType, customMetadata }');
|
|
237
|
+
lines.push('');
|
|
238
|
+
for (const mount of allFileMounts) {
|
|
239
|
+
const desc = mount.description ? ` - ${mount.description}` : '';
|
|
240
|
+
const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
|
|
241
|
+
const isSmartBucket = mount.type === 'smartbucket';
|
|
242
|
+
lines.push(` /${mount.name}/...${desc}${mode}${isSmartBucket ? ' [searchable]' : ''}`);
|
|
243
|
+
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// KV storage
|
|
248
|
+
if (kvMounts.length > 0) {
|
|
249
|
+
lines.push('## Key-Value Cache');
|
|
250
|
+
lines.push('Ephemeral key-value storage with optional expiration. Eventually consistent.');
|
|
251
|
+
lines.push('write() accepts optional { ttl, expiration, metadata }');
|
|
252
|
+
lines.push('');
|
|
253
|
+
for (const mount of kvMounts) {
|
|
254
|
+
const desc = mount.description ? ` - ${mount.description}` : '';
|
|
255
|
+
const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
|
|
256
|
+
lines.push(` /${mount.name}/<key>${desc}${mode}`);
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Actor storage
|
|
262
|
+
if (actorStorageMounts.length > 0) {
|
|
263
|
+
lines.push('## Actor State');
|
|
264
|
+
lines.push('Strongly consistent, transactional storage local to this actor instance.');
|
|
265
|
+
lines.push('list() supports range queries: { start, end, startAfter, reverse, limit }');
|
|
266
|
+
lines.push('');
|
|
267
|
+
for (const mount of actorStorageMounts) {
|
|
268
|
+
const desc = mount.description ? ` - ${mount.description}` : '';
|
|
269
|
+
const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
|
|
270
|
+
lines.push(` /${mount.name}/<key>${desc}${mode}`);
|
|
271
|
+
}
|
|
272
|
+
lines.push('');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Databases
|
|
276
|
+
if (dbMounts.length > 0) {
|
|
277
|
+
lines.push('## Databases');
|
|
278
|
+
lines.push('Use query() and execute() with these mounts:');
|
|
279
|
+
lines.push('');
|
|
280
|
+
for (const mount of dbMounts) {
|
|
281
|
+
const desc = mount.description ? ` - ${mount.description}` : '';
|
|
282
|
+
const mode = mount.mode === 'rw' ? ' (read-write)' : ' (read-only)';
|
|
283
|
+
lines.push(` /${mount.name}${desc}${mode}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push('');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Actors
|
|
289
|
+
if (actorMounts.length > 0) {
|
|
290
|
+
lines.push('## Actors');
|
|
291
|
+
lines.push('Use actor(path, instanceId) to get a stub, then call methods.');
|
|
292
|
+
lines.push('');
|
|
293
|
+
|
|
294
|
+
for (const mount of actorMounts) {
|
|
295
|
+
const desc = mount.description ? ` - ${mount.description}` : '';
|
|
296
|
+
lines.push(`### /${mount.name}${desc}`);
|
|
297
|
+
lines.push('');
|
|
298
|
+
lines.push('Methods:');
|
|
299
|
+
|
|
300
|
+
for (const [methodName, schema] of Object.entries(mount.methods)) {
|
|
301
|
+
// Format tuple params as a function signature using .describe() for names
|
|
302
|
+
const tupleItems = (schema.params as any)._def?.items ?? [];
|
|
303
|
+
const paramsStr = tupleItems.length === 0
|
|
304
|
+
? ''
|
|
305
|
+
: tupleItems.map((item: any, i: number) => {
|
|
306
|
+
const name = item.description ?? `arg${i}`;
|
|
307
|
+
return `${name}: ${zodToTypeString(item)}`;
|
|
308
|
+
}).join(', ');
|
|
309
|
+
const returnsStr = zodToTypeString(schema.returns);
|
|
310
|
+
lines.push(` ${methodName}(${paramsStr}) → ${returnsStr}`);
|
|
311
|
+
lines.push(` ${schema.description}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
lines.push('');
|
|
315
|
+
lines.push('Example:');
|
|
316
|
+
lines.push(` const stub = await actor("/${mount.name}", "<instance-id>");`);
|
|
317
|
+
const firstMethod = Object.keys(mount.methods)[0];
|
|
318
|
+
if (firstMethod) {
|
|
319
|
+
lines.push(` const result = await stub.${firstMethod}(...);`);
|
|
320
|
+
}
|
|
321
|
+
lines.push('');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount Registry - Extensible mount type system
|
|
3
|
+
*
|
|
4
|
+
* Replaces repetitive switch statements with a registration-based pattern.
|
|
5
|
+
* Makes it easy to add new mount types without modifying core code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
BucketMountConfig,
|
|
10
|
+
SmartBucketMountConfig,
|
|
11
|
+
KvMountConfig,
|
|
12
|
+
SqlMountConfig,
|
|
13
|
+
ActorMountConfig,
|
|
14
|
+
ActorStorageMountConfig,
|
|
15
|
+
BucketMountInfo,
|
|
16
|
+
SmartBucketMountInfo,
|
|
17
|
+
KvMountInfo,
|
|
18
|
+
SqlMountInfo,
|
|
19
|
+
ActorMountInfo,
|
|
20
|
+
ActorStorageMountInfo,
|
|
21
|
+
MountConfigType,
|
|
22
|
+
MountInfo,
|
|
23
|
+
MountType,
|
|
24
|
+
MountMode
|
|
25
|
+
} from '../types.js';
|
|
26
|
+
|
|
27
|
+
// Internal base interfaces (not exported)
|
|
28
|
+
interface BaseMountConfig {
|
|
29
|
+
description?: string;
|
|
30
|
+
mode?: MountMode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BaseMountInfo extends BaseMountConfig {
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handler for a specific mount type
|
|
39
|
+
*/
|
|
40
|
+
interface MountHandler<TConfig extends BaseMountConfig, TInfo extends BaseMountInfo> {
|
|
41
|
+
/** Validate the mount configuration */
|
|
42
|
+
validate?: (config: TConfig) => void;
|
|
43
|
+
|
|
44
|
+
/** Create a MountInfo from a name and config */
|
|
45
|
+
createInfo: (name: string, config: TConfig) => TInfo;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const mountHandlers = new Map<MountType, MountHandler<any, any>>();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a mount type handler
|
|
52
|
+
*/
|
|
53
|
+
export function registerMountType<TConfig extends BaseMountConfig & { type: MountType }, TInfo extends BaseMountInfo>(
|
|
54
|
+
type: MountType,
|
|
55
|
+
handler: MountHandler<TConfig, TInfo>
|
|
56
|
+
): void {
|
|
57
|
+
mountHandlers.set(type, handler);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register standard mount types
|
|
62
|
+
*/
|
|
63
|
+
export function registerStandardMountTypes(): void {
|
|
64
|
+
// Bucket
|
|
65
|
+
registerMountType<BucketMountConfig, BucketMountInfo>('bucket', {
|
|
66
|
+
validate: (config) => {
|
|
67
|
+
if (!config.resource) {
|
|
68
|
+
throw new Error(`Bucket mount requires a resource`);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
createInfo: (name, config) => ({
|
|
72
|
+
name,
|
|
73
|
+
type: 'bucket',
|
|
74
|
+
resource: config.resource,
|
|
75
|
+
description: config.description,
|
|
76
|
+
mode: config.mode
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// SmartBucket
|
|
81
|
+
registerMountType<SmartBucketMountConfig, SmartBucketMountInfo>('smartbucket', {
|
|
82
|
+
validate: (config) => {
|
|
83
|
+
if (!config.resource) {
|
|
84
|
+
throw new Error(`SmartBucket mount requires a resource`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
createInfo: (name, config) => ({
|
|
88
|
+
name,
|
|
89
|
+
type: 'smartbucket',
|
|
90
|
+
resource: config.resource,
|
|
91
|
+
description: config.description,
|
|
92
|
+
mode: config.mode
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// KvCache
|
|
97
|
+
registerMountType<KvMountConfig, KvMountInfo>('kv', {
|
|
98
|
+
validate: (config) => {
|
|
99
|
+
if (!config.resource) {
|
|
100
|
+
throw new Error(`KvCache mount requires a resource`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
createInfo: (name, config) => ({
|
|
104
|
+
name,
|
|
105
|
+
type: 'kv',
|
|
106
|
+
resource: config.resource,
|
|
107
|
+
description: config.description,
|
|
108
|
+
mode: config.mode
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// SqlDatabase
|
|
113
|
+
registerMountType<SqlMountConfig, SqlMountInfo>('database', {
|
|
114
|
+
validate: (config) => {
|
|
115
|
+
if (!config.resource) {
|
|
116
|
+
throw new Error(`Database mount requires a resource`);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
createInfo: (name, config) => ({
|
|
120
|
+
name,
|
|
121
|
+
type: 'database',
|
|
122
|
+
resource: config.resource,
|
|
123
|
+
description: config.description,
|
|
124
|
+
mode: config.mode
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Actor
|
|
129
|
+
registerMountType<ActorMountConfig, ActorMountInfo>('actor', {
|
|
130
|
+
validate: (config) => {
|
|
131
|
+
if (!config.resource) {
|
|
132
|
+
throw new Error(`Actor mount requires a resource`);
|
|
133
|
+
}
|
|
134
|
+
if (!config.methods || Object.keys(config.methods).length === 0) {
|
|
135
|
+
throw new Error(`Actor mount requires at least one method schema`);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
createInfo: (name, config) => ({
|
|
139
|
+
name,
|
|
140
|
+
type: 'actor',
|
|
141
|
+
resource: config.resource,
|
|
142
|
+
description: config.description,
|
|
143
|
+
mode: config.mode,
|
|
144
|
+
methods: config.methods
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ActorStorage
|
|
149
|
+
registerMountType<ActorStorageMountConfig, ActorStorageMountInfo>('actor-storage', {
|
|
150
|
+
validate: (config) => {
|
|
151
|
+
if (!config.resource) {
|
|
152
|
+
throw new Error(`ActorStorage mount requires a resource`);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
createInfo: (name, config) => ({
|
|
156
|
+
name,
|
|
157
|
+
type: 'actor-storage',
|
|
158
|
+
resource: config.resource,
|
|
159
|
+
description: config.description,
|
|
160
|
+
mode: config.mode
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a MountInfo from a name and config
|
|
167
|
+
*/
|
|
168
|
+
export function createMountInfo(name: string, config: MountConfigType): MountInfo {
|
|
169
|
+
const handler = mountHandlers.get(config.type);
|
|
170
|
+
|
|
171
|
+
if (!handler) {
|
|
172
|
+
throw new Error(`Unknown mount type: ${config.type}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Validate if handler provides validation
|
|
176
|
+
if (handler.validate) {
|
|
177
|
+
handler.validate(config);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Create the mount info
|
|
181
|
+
return handler.createInfo(name, config);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get all registered mount types
|
|
186
|
+
*/
|
|
187
|
+
export function getRegisteredMountTypes(): MountType[] {
|
|
188
|
+
return Array.from(mountHandlers.keys());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if a mount type is registered
|
|
193
|
+
*/
|
|
194
|
+
export function isMountTypeRegistered(type: MountType): boolean {
|
|
195
|
+
return mountHandlers.has(type);
|
|
196
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for zodToTypeString utility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { zodToTypeString } from './zod-to-string.js';
|
|
8
|
+
|
|
9
|
+
describe('zodToTypeString', () => {
|
|
10
|
+
describe('primitives', () => {
|
|
11
|
+
it('should convert z.string() to "string"', () => {
|
|
12
|
+
expect(zodToTypeString(z.string())).toBe('string');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should convert z.number() to "number"', () => {
|
|
16
|
+
expect(zodToTypeString(z.number())).toBe('number');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should convert z.boolean() to "boolean"', () => {
|
|
20
|
+
expect(zodToTypeString(z.boolean())).toBe('boolean');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should convert z.null() to "null"', () => {
|
|
24
|
+
expect(zodToTypeString(z.null())).toBe('null');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should convert z.undefined() to "undefined"', () => {
|
|
28
|
+
expect(zodToTypeString(z.undefined())).toBe('undefined');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('literals and enums', () => {
|
|
33
|
+
it('should convert z.literal("hello") to "\'hello\'"', () => {
|
|
34
|
+
expect(zodToTypeString(z.literal('hello'))).toBe("'hello'");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should convert z.literal(42) to "42"', () => {
|
|
38
|
+
expect(zodToTypeString(z.literal(42))).toBe('42');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should convert z.enum to union of literals', () => {
|
|
42
|
+
const result = zodToTypeString(z.enum(['light', 'dark']));
|
|
43
|
+
expect(result).toBe("'light' | 'dark'");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('arrays', () => {
|
|
48
|
+
it('should convert z.array(z.string()) to "Array<string>"', () => {
|
|
49
|
+
expect(zodToTypeString(z.array(z.string()))).toBe('Array<string>');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should convert z.array(z.number()) to "Array<number>"', () => {
|
|
53
|
+
expect(zodToTypeString(z.array(z.number()))).toBe('Array<number>');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('objects', () => {
|
|
58
|
+
it('should convert empty z.object({}) to "{}"', () => {
|
|
59
|
+
expect(zodToTypeString(z.object({}))).toBe('{}');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should convert simple object', () => {
|
|
63
|
+
const schema = z.object({ name: z.string() });
|
|
64
|
+
expect(zodToTypeString(schema)).toBe('{ name: string }');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should convert object with multiple properties', () => {
|
|
68
|
+
const schema = z.object({
|
|
69
|
+
name: z.string(),
|
|
70
|
+
age: z.number()
|
|
71
|
+
});
|
|
72
|
+
expect(zodToTypeString(schema)).toBe('{ name: string, age: number }');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should mark optional properties', () => {
|
|
76
|
+
const schema = z.object({
|
|
77
|
+
name: z.string(),
|
|
78
|
+
age: z.number().optional()
|
|
79
|
+
});
|
|
80
|
+
expect(zodToTypeString(schema)).toBe('{ name: string, age?: number }');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('unions', () => {
|
|
85
|
+
it('should convert z.union to pipe-separated types', () => {
|
|
86
|
+
const schema = z.union([z.string(), z.number()]);
|
|
87
|
+
expect(zodToTypeString(schema)).toBe('string | number');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('optional and nullable', () => {
|
|
92
|
+
it('should unwrap optional', () => {
|
|
93
|
+
expect(zodToTypeString(z.string().optional())).toBe('string');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should show nullable as union with null', () => {
|
|
97
|
+
expect(zodToTypeString(z.string().nullable())).toBe('string | null');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('records and maps', () => {
|
|
102
|
+
it('should convert z.record to Record<string, T>', () => {
|
|
103
|
+
expect(zodToTypeString(z.record(z.number()))).toBe('Record<string, number>');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('tuples', () => {
|
|
108
|
+
it('should convert z.tuple to bracketed types', () => {
|
|
109
|
+
const schema = z.tuple([z.string(), z.number()]);
|
|
110
|
+
expect(zodToTypeString(schema)).toBe('[string, number]');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('complex nested types', () => {
|
|
115
|
+
it('should handle nested objects', () => {
|
|
116
|
+
const schema = z.object({
|
|
117
|
+
user: z.object({
|
|
118
|
+
name: z.string()
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
const result = zodToTypeString(schema);
|
|
122
|
+
expect(result).toContain('user');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle array of objects', () => {
|
|
126
|
+
const schema = z.array(z.object({ id: z.string() }));
|
|
127
|
+
expect(zodToTypeString(schema)).toBe('Array<{ id: string }>');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('depth limiting', () => {
|
|
132
|
+
it('should abbreviate deeply nested types', () => {
|
|
133
|
+
// Create a deeply nested structure
|
|
134
|
+
const deep = z.object({
|
|
135
|
+
a: z.object({
|
|
136
|
+
b: z.object({
|
|
137
|
+
c: z.object({
|
|
138
|
+
d: z.object({
|
|
139
|
+
e: z.object({
|
|
140
|
+
f: z.string()
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = zodToTypeString(deep);
|
|
149
|
+
// Should not throw and should return something reasonable
|
|
150
|
+
expect(result).toBeDefined();
|
|
151
|
+
expect(result.length).toBeLessThan(500); // Shouldn't explode
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|