@collegium/sdk 0.0.1-beta.8 → 0.0.1-beta.9

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 CHANGED
@@ -38,10 +38,10 @@ export default defineTool({
38
38
  approval: (args) => ({ body: `save contact "${args.id}": ${args.name}`, presentation: 'verbatim' }),
39
39
  description: 'Save or update a contact.',
40
40
  execute: async (args, { err, settings, storage }) => {
41
- if ((await storage.contacts.list()).length >= settings.maxContacts) {
41
+ if ((await storage.contacts.findMany()).length >= settings.maxContacts) {
42
42
  err.invalidArguments('contact limit reached; delete one first');
43
43
  }
44
- await storage.contacts.put(args.id, { email: args.email, name: args.name });
44
+ await storage.contacts.create({ email: args.email, id: args.id, name: args.name });
45
45
  return `contact ${args.id} saved`;
46
46
  },
47
47
  parameters: z.object({ email: z.email(), id: z.string().min(1), name: z.string().min(1) })
@@ -50,6 +50,8 @@ export default defineTool({
50
50
 
51
51
  A tool with `approval` always stops for a human, who sees the full payload before it runs; one without never gates. The channel and the trace disclose both, line by line. `execute` returns the text the model reads, and raises the two failures a tool controls through `err`: `invalidArguments` continues the turn, `unresolved` ends it as an unconfirmed side effect.
52
52
 
53
+ **Storage.** Each declared collection is a set of records: the schema's output plus `id`, `createdAt`, and `updatedAt`, which the store stamps. The handle has `create`, `findMany`, `findById`, `updateById`, and `deleteById`. `create` takes the schema's input with an optional `id`, minting a cuid2 when none is given. `findMany` with no argument lists everything; with a `where` over the schema's top-level scalar fields and `id` — a value for equality, `{ in: [...] }` for membership, `{ contains: text }` for a case-insensitive substring on a string — and an optional `limit`, it filters. Field names and value types come from the schema, so a bad query does not compile.
54
+
53
55
  **Testing.** `@collegium/sdk/testing` builds the context `execute` receives, over in-memory storage that validates and parses as the deployment's store does. Pass your config; settings go through your schema, so defaults apply.
54
56
 
55
57
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { c as ToolTurnScope, d as defineConfig, i as defineTool, n as ToolContext, o as ToolApprovalPayload, s as ToolDisclosure, t as PluginTool, u as Register } from "./tool-CN2DS668.js";
1
+ import { c as ToolTurnScope, d as defineConfig, i as defineTool, n as ToolContext, o as ToolApprovalPayload, s as ToolDisclosure, t as PluginTool, u as Register } from "./tool-Vqe6D48m.js";
2
2
  export { type PluginTool, type Register, type ToolApprovalPayload, type ToolContext, type ToolDisclosure, type ToolTurnScope, defineConfig, defineTool };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as PluginToolFailureError, c as ToolTurnScope, l as PluginConfig, r as ToolContextFor } from "./tool-CN2DS668.js";
1
+ import { a as PluginToolFailureError, c as ToolTurnScope, l as PluginConfig, r as ToolContextFor } from "./tool-Vqe6D48m.js";
2
2
  import { z } from "zod";
3
3
  //#region src/testing.d.ts
4
4
  /** `settings` as the declared schema accepts them, so defaults apply as they do at boot; `turn` overrides the four facts */
package/dist/testing.js CHANGED
@@ -8,6 +8,82 @@ var PluginToolFailureError = class extends Error {
8
8
  }
9
9
  };
10
10
  //#endregion
11
+ //#region ../../node_modules/.pnpm/es-toolkit@1.50.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
12
+ /**
13
+ * Checks if a given value is a plain object.
14
+ *
15
+ * @param value - The value to check.
16
+ * @returns True if the value is a plain object, otherwise false.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // ✅👇 True
21
+ *
22
+ * isPlainObject({ }); // ✅
23
+ * isPlainObject({ key: 'value' }); // ✅
24
+ * isPlainObject({ key: new Date() }); // ✅
25
+ * isPlainObject(new Object()); // ✅
26
+ * isPlainObject(Object.create(null)); // ✅
27
+ * isPlainObject({ nested: { key: true} }); // ✅
28
+ * isPlainObject(new Proxy({}, {})); // ✅
29
+ * isPlainObject({ [Symbol('tag')]: 'A' }); // ✅
30
+ *
31
+ * // ✅👇 (cross-realms, node context, workers, ...)
32
+ * const runInNewContext = await import('node:vm').then(
33
+ * (mod) => mod.runInNewContext
34
+ * );
35
+ * isPlainObject(runInNewContext('({})')); // ✅
36
+ *
37
+ * // ❌👇 False
38
+ *
39
+ * class Test { };
40
+ * isPlainObject(new Test()) // ❌
41
+ * isPlainObject(10); // ❌
42
+ * isPlainObject(null); // ❌
43
+ * isPlainObject('hello'); // ❌
44
+ * isPlainObject([]); // ❌
45
+ * isPlainObject(new Date()); // ❌
46
+ * isPlainObject(new Uint8Array([1])); // ❌
47
+ * isPlainObject(Buffer.from('ABC')); // ❌
48
+ * isPlainObject(Promise.resolve({})); // ❌
49
+ * isPlainObject(Object.create({})); // ❌
50
+ * isPlainObject(new (class Cls {})); // ❌
51
+ * isPlainObject(globalThis); // ❌,
52
+ * ```
53
+ */
54
+ function isPlainObject(value) {
55
+ if (!value || typeof value !== "object") return false;
56
+ const proto = Object.getPrototypeOf(value);
57
+ if (!(proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null)) return false;
58
+ return Object.prototype.toString.call(value) === "[object Object]";
59
+ }
60
+ //#endregion
61
+ //#region ../core/dist/toolsets/storage/collection-query.utils.js
62
+ /** folds ASCII letters only, matching SQLite's `lower()` without ICU, so the store and this evaluator agree on every string */
63
+ const foldAsciiCase = (text) => text.replace(/[A-Z]+/g, (upper) => upper.toLowerCase());
64
+ const readField = (record, field) => isPlainObject(record) ? record[field] : void 0;
65
+ const matchesScalar = (actual, expected) => expected === null ? actual === null || actual === void 0 : actual === expected;
66
+ const matchesCondition = (actual, condition) => {
67
+ if (condition === null || typeof condition !== "object") return matchesScalar(actual, condition);
68
+ if ("in" in condition) return condition.in.some((candidate) => matchesScalar(actual, candidate));
69
+ return typeof actual === "string" && foldAsciiCase(actual).includes(foldAsciiCase(condition.contains));
70
+ };
71
+ const readConditions = (where) => {
72
+ const conditions = [];
73
+ for (const [field, condition] of Object.entries(where ?? {})) if (condition !== void 0) conditions.push([field, condition]);
74
+ return conditions;
75
+ };
76
+ /** the stated conditions of a query, by field, an undefined one dropped as unstated */
77
+ function collectionQueryConditions(query) {
78
+ return readConditions(query.where);
79
+ }
80
+ /** the query over records already in memory, in their own order — the semantics the store's compiled form must reproduce */
81
+ function applyCollectionQuery(records, query) {
82
+ const conditions = collectionQueryConditions(query);
83
+ const matching = records.filter((record) => conditions.every(([field, condition]) => matchesCondition(readField(record, field), condition)));
84
+ return query.limit === void 0 ? matching : matching.slice(0, query.limit);
85
+ }
86
+ //#endregion
11
87
  //#region ../core/dist/plugins/plugins.utils.js
12
88
  /** what a plugin tool receives as `err`; shared with the SDK's testing entry, so a test raises the failure a deployment would */
13
89
  const PLUGIN_TOOL_ERR = {
@@ -32,17 +108,44 @@ const DEFAULT_TURN = {
32
108
  triggeringPostId: null,
33
109
  turnId: "test-turn"
34
110
  };
111
+ /** the deployment's store mints a cuid2; here any unique string serves, and a test that needs a known id passes one */
35
112
  function createCollection(schema) {
36
113
  const rows = /* @__PURE__ */ new Map();
114
+ const toRecord = ({ createdAt, id, updatedAt, ...value }) => ({
115
+ ...schema.parse(value),
116
+ createdAt,
117
+ id,
118
+ updatedAt
119
+ });
37
120
  return {
38
- delete: (key) => Promise.try(() => rows.delete(key)),
39
- get: (key) => Promise.try(() => rows.has(key) ? schema.parse(rows.get(key)) : null),
40
- list: () => Promise.try(() => [...rows].map(([key, value]) => ({
41
- key,
42
- value: schema.parse(value)
43
- }))),
44
- put: (key, value) => Promise.try(() => {
45
- rows.set(key, schema.parse(value));
121
+ create: ({ id = crypto.randomUUID(), ...data }) => Promise.try(() => {
122
+ if (rows.has(id)) throw new Error(`storage collection already holds a record with id "${id}"`);
123
+ const now = /* @__PURE__ */ new Date();
124
+ rows.set(id, {
125
+ ...schema.parse(data),
126
+ createdAt: now,
127
+ id,
128
+ updatedAt: now
129
+ });
130
+ return toRecord(rows.get(id));
131
+ }),
132
+ deleteById: (id) => Promise.try(() => rows.delete(id)),
133
+ findById: (id) => Promise.try(() => rows.has(id) ? toRecord(rows.get(id)) : null),
134
+ findMany: (query = {}) => Promise.try(() => applyCollectionQuery([...rows.values()].map(toRecord), query)),
135
+ updateById: (id, patch) => Promise.try(() => {
136
+ const row = rows.get(id);
137
+ if (row === void 0) return null;
138
+ const { createdAt, id: _id, updatedAt: _updatedAt, ...value } = row;
139
+ rows.set(id, {
140
+ ...schema.parse({
141
+ ...schema.parse(value),
142
+ ...patch
143
+ }),
144
+ createdAt,
145
+ id,
146
+ updatedAt: /* @__PURE__ */ new Date()
147
+ });
148
+ return toRecord(rows.get(id));
46
149
  })
47
150
  };
48
151
  }
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { Promisable } from "type-fest";
3
3
  //#region src/config.d.ts
4
4
  type CollectionsDeclaration$1 = {
5
- readonly [key: string]: z.ZodType;
5
+ readonly [key: string]: z.ZodObject;
6
6
  };
7
7
  /**
8
8
  * The config file's default export: the settings schema agents are configured by, and the storage
@@ -143,6 +143,54 @@ type PluginToolDeclaration<TContext, TParams extends z.ZodType> = {
143
143
  traceDetail?(args: z.infer<TParams>): string;
144
144
  };
145
145
  //#endregion
146
+ //#region ../core/src/toolsets/storage/collection-query.types.d.ts
147
+ /** the JSON scalars a query may compare; an object, array, or date field is not queryable */
148
+ type Scalar = boolean | null | number | string;
149
+ /**
150
+ * The top-level scalar fields of a record, each stripped of the undefined an optional field
151
+ * carries. The intersection with the scalar union changes nothing for a concrete record and is what
152
+ * lets the compiler see, over a generic one, that every condition is a scalar condition.
153
+ */
154
+ type QueryableFields<TRecord> = { readonly [K in keyof TRecord as Exclude<TRecord[K], undefined> extends Scalar ? K : never]-?: Exclude<TRecord[K], undefined> & Scalar; };
155
+ /** one field's condition: equality, membership, or — on a string field — a case-insensitive substring */
156
+ type CollectionFieldCondition<TField> = ([Extract<TField, string>] extends [never] ? never : {
157
+ readonly contains: string;
158
+ }) | TField | {
159
+ readonly in: readonly TField[];
160
+ };
161
+ /** an AND over field conditions, keyed by the record's own scalar fields */
162
+ type CollectionWhere<TRecord> = { readonly [K in keyof QueryableFields<TRecord>]?: CollectionFieldCondition<QueryableFields<TRecord>[K]>; };
163
+ type CollectionQuery<TRecord> = {
164
+ readonly limit?: number;
165
+ readonly where?: CollectionWhere<TRecord>;
166
+ };
167
+ //#endregion
168
+ //#region ../core/src/toolsets/storage/collection.types.d.ts
169
+ /** what the store stamps on every record beside the declared fields; a schema may not declare any of these */
170
+ type CollectionRecordStamp = {
171
+ readonly createdAt: Date;
172
+ readonly id: string;
173
+ readonly updatedAt: Date;
174
+ };
175
+ type CollectionRecord<TValue> = CollectionRecordStamp & TValue;
176
+ /**
177
+ * A toolset-scoped handle over one declared storage collection. Every record is the schema's
178
+ * output plus the stamp; `create` parses through the schema, so defaults apply, and mints a
179
+ * cuid2 unless an id is given. Reads parse each stored value, so a row an older schema wrote
180
+ * fails loudly rather than leaking a stale shape into the tool.
181
+ */
182
+ type ToolsetCollection<TSchema extends z.ZodObject> = {
183
+ create(data: z.input<TSchema> & {
184
+ readonly id?: string;
185
+ }): Promise<CollectionRecord<z.output<TSchema>>>;
186
+ deleteById(id: string): Promise<boolean>;
187
+ findById(id: string): Promise<CollectionRecord<z.output<TSchema>> | null>;
188
+ /** without a query, every record in insertion order */
189
+ findMany(query?: CollectionQuery<CollectionRecord<z.output<TSchema>>>): Promise<CollectionRecord<z.output<TSchema>>[]>;
190
+ /** the patch merged over the stored fields and the whole parsed again, so a patch cannot leave an invalid record */
191
+ updateById(id: string, patch: Partial<z.input<TSchema>>): Promise<CollectionRecord<z.output<TSchema>> | null>;
192
+ };
193
+ //#endregion
146
194
  //#region ../core/src/toolsets/toolsets.types.d.ts
147
195
  /** `settings`, `storage`, and `turn` are the context's own keys, so a service may not claim them */
148
196
  type ServicesDeclaration = {
@@ -153,19 +201,9 @@ type ServicesDeclaration = {
153
201
  readonly turn?: never;
154
202
  };
155
203
  type CollectionsDeclaration = {
156
- readonly [key: string]: z.ZodType;
204
+ readonly [key: string]: z.ZodObject;
157
205
  };
158
206
  type EmptyDeclaration = {};
159
- /** a toolset-scoped handle over one declared storage collection; rows are validated on write and parsed on read */
160
- type ToolsetCollection<TValue> = {
161
- delete(key: string): Promise<boolean>;
162
- get(key: string): Promise<null | TValue>;
163
- list(): Promise<{
164
- key: string;
165
- value: TValue;
166
- }[]>;
167
- put(key: string, value: TValue): Promise<void>;
168
- };
169
207
  /**
170
208
  * What `execute` receives, assembled from exactly what the toolset declared (§4): each service
171
209
  * under its own name, `settings` and `storage` only when declared, and always the turn. Reaching
@@ -174,7 +212,7 @@ type ToolsetCollection<TValue> = {
174
212
  type ToolsetContext<TServices extends ServicesDeclaration = EmptyDeclaration, TSettings extends undefined | z.ZodType = undefined, TCollections extends CollectionsDeclaration = EmptyDeclaration> = { readonly [K in keyof TServices]: TServices[K] extends ServiceToken<infer TInstance> ? TInstance : never; } & {
175
213
  readonly turn: ToolTurnScope;
176
214
  } & (keyof TCollections extends never ? unknown : {
177
- readonly storage: { readonly [K in keyof TCollections]: ToolsetCollection<z.infer<TCollections[K]>>; };
215
+ readonly storage: { readonly [K in keyof TCollections]: ToolsetCollection<TCollections[K]>; };
178
216
  }) & (TSettings extends z.ZodType ? {
179
217
  readonly settings: z.infer<TSettings>;
180
218
  } : unknown);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@collegium/sdk",
3
3
  "type": "module",
4
- "version": "0.0.1-beta.8",
4
+ "version": "0.0.1-beta.9",
5
5
  "description": "Write a Collegium plugin: declare a toolset with its tools, settings, storage, and skills.",
6
6
  "license": "AGPL-3.0-only",
7
7
  "homepage": "https://collegium.sh",