duet-kit 0.1.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/README.md +442 -0
- package/dist/attach.d.ts +48 -0
- package/dist/attach.d.ts.map +1 -0
- package/dist/attach.js +205 -0
- package/dist/duet.d.ts +63 -0
- package/dist/duet.d.ts.map +1 -0
- package/dist/duet.js +259 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/llm.d.ts +25 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +170 -0
- package/dist/schema.d.ts +27 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +122 -0
- package/dist/store.d.ts +18 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +58 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +66 -0
package/dist/duet.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Single factory that creates everything: store + LLM bridge + schema access.
|
|
5
|
+
* Follows Zustand conventions - returns a hook with static properties.
|
|
6
|
+
*/
|
|
7
|
+
import { type StoreApi, type UseBoundStore } from 'zustand';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { SchemaFields, FieldDef, InferData, EditResult, LLMBridge } from './types';
|
|
10
|
+
import { DuetSchema } from './schema';
|
|
11
|
+
interface DuetState<T extends SchemaFields> {
|
|
12
|
+
data: InferData<T>;
|
|
13
|
+
set: <K extends keyof T>(field: K, value: z.infer<T[K]['schema']>) => boolean;
|
|
14
|
+
setMany: (updates: Partial<InferData<T>>) => boolean;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
}
|
|
17
|
+
export interface DuetHook<T extends SchemaFields> extends UseBoundStore<StoreApi<DuetState<T>>> {
|
|
18
|
+
llm: LLMBridge<T>;
|
|
19
|
+
schema: DuetSchema<T>;
|
|
20
|
+
}
|
|
21
|
+
export interface DuetOptions {
|
|
22
|
+
/** Optional localStorage key. Omit for in-memory only. For backend sync, use store.subscribe() */
|
|
23
|
+
persist?: string;
|
|
24
|
+
/** Transform the default context string before returning from getContext() */
|
|
25
|
+
transformContext?: (context: string) => string;
|
|
26
|
+
/** Transform the default function schema before returning from getFunctionSchema() */
|
|
27
|
+
transformFunctionSchema?: (schema: object) => object;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a Duet - a Zustand store with LLM bridge and schema access.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const useTripStore = createDuet('TripBudget', {
|
|
35
|
+
* destination: field(z.string(), 'Destination', 'Tokyo'),
|
|
36
|
+
* budget: field(z.number().min(0), 'Budget', 5000),
|
|
37
|
+
* }, { persist: 'trip-data' })
|
|
38
|
+
*
|
|
39
|
+
* // React
|
|
40
|
+
* const { data, set } = useTripStore()
|
|
41
|
+
*
|
|
42
|
+
* // LLM (JSON Patch)
|
|
43
|
+
* useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function createDuet<T extends SchemaFields>(name: string, fields: T, options?: DuetOptions): DuetHook<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Helper to define a field with Zod schema, label, and default value.
|
|
49
|
+
*/
|
|
50
|
+
export declare function field<T>(schema: z.ZodType<T>, label: string, defaultValue: T): FieldDef<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Helper to check if edit was successful
|
|
53
|
+
*/
|
|
54
|
+
export declare function isSuccess(result: EditResult): result is {
|
|
55
|
+
success: true;
|
|
56
|
+
applied: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Get human-readable message from result
|
|
60
|
+
*/
|
|
61
|
+
export declare function getResultMessage(result: EditResult): string;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=duet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duet.d.ts","sourceRoot":"","sources":["../src/duet.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAe,UAAU,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AACnH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAGtC,UAAU,SAAS,CAAC,CAAC,SAAS,YAAY;IACxC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACnB,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,OAAO,CAAC;IAC9E,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC;IACrD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAGD,MAAM,WAAW,QAAQ,CAAC,CAAC,SAAS,YAAY,CAAE,SAAQ,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7F,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,sFAAsF;IACtF,uBAAuB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,YAAY,EAC/C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,WAAgB,GACxB,QAAQ,CAAC,CAAC,CAAC,CAmPb;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,CAAC,GACd,QAAQ,CAAC,CAAC,CAAC,CAEb;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAE1F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAK3D"}
|
package/dist/duet.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Single factory that creates everything: store + LLM bridge + schema access.
|
|
5
|
+
* Follows Zustand conventions - returns a hook with static properties.
|
|
6
|
+
*/
|
|
7
|
+
import { create } from 'zustand';
|
|
8
|
+
import { persist } from 'zustand/middleware';
|
|
9
|
+
import { DuetSchema } from './schema';
|
|
10
|
+
/**
|
|
11
|
+
* Create a Duet - a Zustand store with LLM bridge and schema access.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const useTripStore = createDuet('TripBudget', {
|
|
16
|
+
* destination: field(z.string(), 'Destination', 'Tokyo'),
|
|
17
|
+
* budget: field(z.number().min(0), 'Budget', 5000),
|
|
18
|
+
* }, { persist: 'trip-data' })
|
|
19
|
+
*
|
|
20
|
+
* // React
|
|
21
|
+
* const { data, set } = useTripStore()
|
|
22
|
+
*
|
|
23
|
+
* // LLM (JSON Patch)
|
|
24
|
+
* useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function createDuet(name, fields, options = {}) {
|
|
28
|
+
// Create schema
|
|
29
|
+
const schema = new DuetSchema(name, fields);
|
|
30
|
+
const initialData = schema.getDefaults();
|
|
31
|
+
// Store creator function
|
|
32
|
+
const storeCreator = (set) => ({
|
|
33
|
+
data: initialData,
|
|
34
|
+
set: (field, value) => {
|
|
35
|
+
const result = schema.validate(field, value);
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
console.warn(`Validation failed for ${String(field)}:`, result.error.message);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
set((state) => ({
|
|
41
|
+
data: { ...state.data, [field]: result.data },
|
|
42
|
+
}));
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
setMany: (updates) => {
|
|
46
|
+
const validated = {};
|
|
47
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
48
|
+
if (key in schema.fields) {
|
|
49
|
+
const result = schema.validate(key, value);
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
console.warn(`Validation failed for ${key}:`, result.error.message);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
validated[key] = result.data;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
set((state) => ({
|
|
58
|
+
data: { ...state.data, ...validated },
|
|
59
|
+
}));
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
reset: () => {
|
|
63
|
+
set(() => ({ data: schema.getDefaults() }));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
// Create the Zustand store
|
|
67
|
+
const useStore = options.persist
|
|
68
|
+
? create()(persist(storeCreator, {
|
|
69
|
+
name: options.persist,
|
|
70
|
+
partialize: (state) => ({ data: state.data }),
|
|
71
|
+
}))
|
|
72
|
+
: create()(storeCreator);
|
|
73
|
+
// History log for audit trail
|
|
74
|
+
const patchHistory = [];
|
|
75
|
+
let historyId = 0;
|
|
76
|
+
// Create LLM bridge (JSON Patch format)
|
|
77
|
+
const llmBridge = {
|
|
78
|
+
applyPatch(patch, source = 'llm') {
|
|
79
|
+
const currentData = { ...useStore.getState().data };
|
|
80
|
+
for (const op of patch) {
|
|
81
|
+
// Parse JSON Pointer path: /field or /parent/child
|
|
82
|
+
const pathParts = op.path.replace(/^\//, '').split('/');
|
|
83
|
+
const rootField = pathParts[0];
|
|
84
|
+
if (!(rootField in schema.fields)) {
|
|
85
|
+
const result = { success: false, error: `Unknown field: ${rootField}` };
|
|
86
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
if (op.op === 'remove') {
|
|
90
|
+
if (pathParts.length === 1) {
|
|
91
|
+
currentData[rootField] = schema.getDefaults()[rootField];
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
let target = currentData[rootField];
|
|
95
|
+
for (let i = 1; i < pathParts.length - 1; i++) {
|
|
96
|
+
target = target[pathParts[i]];
|
|
97
|
+
}
|
|
98
|
+
delete target[pathParts[pathParts.length - 1]];
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (op.op === 'replace' || op.op === 'add') {
|
|
103
|
+
if (pathParts.length === 1) {
|
|
104
|
+
const result = schema.validate(rootField, op.value);
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
const editResult = {
|
|
107
|
+
success: false,
|
|
108
|
+
error: `Invalid value for ${rootField}: ${result.error.errors[0]?.message}`
|
|
109
|
+
};
|
|
110
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
|
|
111
|
+
return editResult;
|
|
112
|
+
}
|
|
113
|
+
currentData[rootField] = result.data;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const newValue = JSON.parse(JSON.stringify(currentData[rootField]));
|
|
117
|
+
let target = newValue;
|
|
118
|
+
for (let i = 1; i < pathParts.length - 1; i++) {
|
|
119
|
+
if (!(pathParts[i] in target)) {
|
|
120
|
+
target[pathParts[i]] = {};
|
|
121
|
+
}
|
|
122
|
+
target = target[pathParts[i]];
|
|
123
|
+
}
|
|
124
|
+
target[pathParts[pathParts.length - 1]] = op.value;
|
|
125
|
+
const result = schema.validate(rootField, newValue);
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
const editResult = {
|
|
128
|
+
success: false,
|
|
129
|
+
error: `Invalid value for ${op.path}: ${result.error.errors[0]?.message}`
|
|
130
|
+
};
|
|
131
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
|
|
132
|
+
return editResult;
|
|
133
|
+
}
|
|
134
|
+
currentData[rootField] = result.data;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const committed = useStore.getState().setMany(currentData);
|
|
139
|
+
if (!committed) {
|
|
140
|
+
const result = { success: false, error: 'Failed to commit changes to store' };
|
|
141
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
const result = { success: true, applied: patch.length };
|
|
145
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
|
|
146
|
+
return result;
|
|
147
|
+
},
|
|
148
|
+
applyJSON(json, source = 'llm') {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(json);
|
|
151
|
+
if (Array.isArray(parsed)) {
|
|
152
|
+
return this.applyPatch(parsed, source);
|
|
153
|
+
}
|
|
154
|
+
if (parsed.patch && Array.isArray(parsed.patch)) {
|
|
155
|
+
return this.applyPatch(parsed.patch, source);
|
|
156
|
+
}
|
|
157
|
+
return { success: false, error: 'Expected JSON Patch array or { patch: [...] } format' };
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return { success: false, error: `JSON parse error: ${e.message}` };
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
history() {
|
|
164
|
+
return [...patchHistory];
|
|
165
|
+
},
|
|
166
|
+
clearHistory() {
|
|
167
|
+
patchHistory.length = 0;
|
|
168
|
+
historyId = 0;
|
|
169
|
+
},
|
|
170
|
+
getContext() {
|
|
171
|
+
const data = useStore.getState().data;
|
|
172
|
+
let context = schema.getDescription();
|
|
173
|
+
context += '\nCurrent Values:\n';
|
|
174
|
+
for (const [id, field] of Object.entries(schema.fields)) {
|
|
175
|
+
const value = data[id];
|
|
176
|
+
context += ` ${id}: ${JSON.stringify(value)} (${field.label})\n`;
|
|
177
|
+
}
|
|
178
|
+
context += `
|
|
179
|
+
To edit fields, respond with a JSON Patch array (RFC 6902):
|
|
180
|
+
[{ "op": "replace", "path": "/fieldName", "value": newValue }]
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
- Single edit: [{ "op": "replace", "path": "/budget", "value": 5000 }]
|
|
184
|
+
- Multiple: [{ "op": "replace", "path": "/budget", "value": 5000 }, { "op": "replace", "path": "/days", "value": 14 }]`;
|
|
185
|
+
return options.transformContext ? options.transformContext(context) : context;
|
|
186
|
+
},
|
|
187
|
+
getCompactContext() {
|
|
188
|
+
const data = useStore.getState().data;
|
|
189
|
+
const values = Object.entries(data)
|
|
190
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
191
|
+
.join(', ');
|
|
192
|
+
return `${schema.name}: {${values}}\nJSON Patch: [{"op":"replace","path":"/field","value":x}]`;
|
|
193
|
+
},
|
|
194
|
+
getFunctionSchema() {
|
|
195
|
+
const fnSchema = {
|
|
196
|
+
name: `patch_${schema.name.toLowerCase().replace(/\s+/g, '_')}`,
|
|
197
|
+
description: `Apply JSON Patch operations to ${schema.name}. Uses RFC 6902 format.`,
|
|
198
|
+
parameters: {
|
|
199
|
+
type: 'object',
|
|
200
|
+
properties: {
|
|
201
|
+
patch: {
|
|
202
|
+
type: 'array',
|
|
203
|
+
description: 'JSON Patch operations (RFC 6902)',
|
|
204
|
+
items: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
op: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
enum: ['replace', 'add', 'remove'],
|
|
210
|
+
description: 'Operation type',
|
|
211
|
+
},
|
|
212
|
+
path: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: `JSON Pointer to field. Valid paths: ${schema.getFieldIds().map(f => `/${f}`).join(', ')}`,
|
|
215
|
+
},
|
|
216
|
+
value: {
|
|
217
|
+
description: 'New value (required for replace/add)',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
required: ['op', 'path'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['patch'],
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
return options.transformFunctionSchema ? options.transformFunctionSchema(fnSchema) : fnSchema;
|
|
228
|
+
},
|
|
229
|
+
getCurrentValues() {
|
|
230
|
+
return useStore.getState().data;
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
// Attach llm and schema to the hook
|
|
234
|
+
const hook = useStore;
|
|
235
|
+
hook.llm = llmBridge;
|
|
236
|
+
hook.schema = schema;
|
|
237
|
+
return hook;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Helper to define a field with Zod schema, label, and default value.
|
|
241
|
+
*/
|
|
242
|
+
export function field(schema, label, defaultValue) {
|
|
243
|
+
return { schema, label, default: defaultValue };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Helper to check if edit was successful
|
|
247
|
+
*/
|
|
248
|
+
export function isSuccess(result) {
|
|
249
|
+
return result.success;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get human-readable message from result
|
|
253
|
+
*/
|
|
254
|
+
export function getResultMessage(result) {
|
|
255
|
+
if (result.success) {
|
|
256
|
+
return `Successfully applied ${result.applied} operation(s)`;
|
|
257
|
+
}
|
|
258
|
+
return result.error;
|
|
259
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - Shared state for humans and LLMs
|
|
3
|
+
*
|
|
4
|
+
* A lightweight library for building UIs where both humans and AI
|
|
5
|
+
* can edit the same validated state through a shared schema.
|
|
6
|
+
*
|
|
7
|
+
* Built on Zustand (state) + Zod (validation) + JSON Patch (RFC 6902).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createDuet, field, z } from 'duet-kit'
|
|
14
|
+
*
|
|
15
|
+
* const useTripStore = createDuet('TripBudget', {
|
|
16
|
+
* destination: field(z.string(), 'Destination', 'Tokyo'),
|
|
17
|
+
* budget: field(z.number().min(0).max(100000), 'Budget', 5000),
|
|
18
|
+
* }, { persist: 'trip-data' })
|
|
19
|
+
*
|
|
20
|
+
* // React
|
|
21
|
+
* const { data, set } = useTripStore()
|
|
22
|
+
*
|
|
23
|
+
* // LLM (JSON Patch format)
|
|
24
|
+
* useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export { createDuet, field, isSuccess, getResultMessage, type DuetHook, type DuetOptions } from './duet';
|
|
28
|
+
export { attachLLM } from './attach';
|
|
29
|
+
export type { SchemaFields, FieldDef, InferData, JsonPatchOp, EditResult, HistoryEntry, LLMBridge, } from './types';
|
|
30
|
+
export { z } from 'zod';
|
|
31
|
+
export { createSchema, DuetSchema } from './schema';
|
|
32
|
+
export { createStore, type DuetStore, type StoreOptions } from './store';
|
|
33
|
+
export { createLLMBridge } from './llm';
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAGzG,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,YAAY,EACV,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,WAAW,EACX,UAAU,EACV,YAAY,EACZ,SAAS,GACV,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - Shared state for humans and LLMs
|
|
3
|
+
*
|
|
4
|
+
* A lightweight library for building UIs where both humans and AI
|
|
5
|
+
* can edit the same validated state through a shared schema.
|
|
6
|
+
*
|
|
7
|
+
* Built on Zustand (state) + Zod (validation) + JSON Patch (RFC 6902).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createDuet, field, z } from 'duet-kit'
|
|
14
|
+
*
|
|
15
|
+
* const useTripStore = createDuet('TripBudget', {
|
|
16
|
+
* destination: field(z.string(), 'Destination', 'Tokyo'),
|
|
17
|
+
* budget: field(z.number().min(0).max(100000), 'Budget', 5000),
|
|
18
|
+
* }, { persist: 'trip-data' })
|
|
19
|
+
*
|
|
20
|
+
* // React
|
|
21
|
+
* const { data, set } = useTripStore()
|
|
22
|
+
*
|
|
23
|
+
* // LLM (JSON Patch format)
|
|
24
|
+
* useTripStore.llm.applyJSON('[{"op":"replace","path":"/budget","value":10000}]')
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
// Main API
|
|
28
|
+
export { createDuet, field, isSuccess, getResultMessage } from './duet';
|
|
29
|
+
// Drop-in for existing Zustand + Zod codebases
|
|
30
|
+
export { attachLLM } from './attach';
|
|
31
|
+
// Re-export Zod for convenience
|
|
32
|
+
export { z } from 'zod';
|
|
33
|
+
// Advanced: individual building blocks (createDuet combines these)
|
|
34
|
+
export { createSchema, DuetSchema } from './schema';
|
|
35
|
+
export { createStore } from './store';
|
|
36
|
+
export { createLLMBridge } from './llm';
|
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - LLM Utilities
|
|
3
|
+
*
|
|
4
|
+
* Context generation and JSON Patch application for LLM integration.
|
|
5
|
+
* Uses RFC 6902 JSON Patch format.
|
|
6
|
+
*/
|
|
7
|
+
import type { SchemaFields, EditResult, LLMBridge } from './types';
|
|
8
|
+
import type { DuetSchema } from './schema';
|
|
9
|
+
import type { DuetStore } from './store';
|
|
10
|
+
/**
|
|
11
|
+
* Create an LLM bridge for a schema + store
|
|
12
|
+
*/
|
|
13
|
+
export declare function createLLMBridge<T extends SchemaFields>(schema: DuetSchema<T>, store: DuetStore<T>): LLMBridge<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Helper to check if edit was successful
|
|
16
|
+
*/
|
|
17
|
+
export declare function isSuccess(result: EditResult): result is {
|
|
18
|
+
success: true;
|
|
19
|
+
applied: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Get human-readable message from result
|
|
23
|
+
*/
|
|
24
|
+
export declare function getResultMessage(result: EditResult): string;
|
|
25
|
+
//# sourceMappingURL=llm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../src/llm.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAA0B,UAAU,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AACzG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,YAAY,EACpD,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EACrB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,GAClB,SAAS,CAAC,CAAC,CAAC,CAqKd;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAE1F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAK3D"}
|
package/dist/llm.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - LLM Utilities
|
|
3
|
+
*
|
|
4
|
+
* Context generation and JSON Patch application for LLM integration.
|
|
5
|
+
* Uses RFC 6902 JSON Patch format.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Create an LLM bridge for a schema + store
|
|
9
|
+
*/
|
|
10
|
+
export function createLLMBridge(schema, store) {
|
|
11
|
+
// History log for audit trail
|
|
12
|
+
const patchHistory = [];
|
|
13
|
+
let historyId = 0;
|
|
14
|
+
return {
|
|
15
|
+
/**
|
|
16
|
+
* Apply JSON Patch operations (RFC 6902)
|
|
17
|
+
*/
|
|
18
|
+
applyPatch(patch, source = 'llm') {
|
|
19
|
+
const updates = {};
|
|
20
|
+
for (const op of patch) {
|
|
21
|
+
const fieldName = op.path.replace(/^\//, '').split('/')[0];
|
|
22
|
+
if (!(fieldName in schema.fields)) {
|
|
23
|
+
const result = { success: false, error: `Unknown field: ${fieldName}` };
|
|
24
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
if (op.op === 'remove') {
|
|
28
|
+
const defaultValue = schema.getDefaults()[fieldName];
|
|
29
|
+
updates[fieldName] = defaultValue;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (op.op === 'replace' || op.op === 'add') {
|
|
33
|
+
const result = schema.validate(fieldName, op.value);
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
const editResult = {
|
|
36
|
+
success: false,
|
|
37
|
+
error: `Invalid value for ${fieldName}: ${result.error.errors[0]?.message}`
|
|
38
|
+
};
|
|
39
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result: editResult });
|
|
40
|
+
return editResult;
|
|
41
|
+
}
|
|
42
|
+
updates[fieldName] = result.data;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
store.getState().setMany(updates);
|
|
46
|
+
const result = { success: true, applied: patch.length };
|
|
47
|
+
patchHistory.push({ id: String(++historyId), timestamp: Date.now(), patch, source, result });
|
|
48
|
+
return result;
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Apply edits from JSON string (LLM output)
|
|
52
|
+
*/
|
|
53
|
+
applyJSON(json, source = 'llm') {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(json);
|
|
56
|
+
if (Array.isArray(parsed)) {
|
|
57
|
+
return this.applyPatch(parsed, source);
|
|
58
|
+
}
|
|
59
|
+
if (parsed.patch && Array.isArray(parsed.patch)) {
|
|
60
|
+
return this.applyPatch(parsed.patch, source);
|
|
61
|
+
}
|
|
62
|
+
return { success: false, error: 'Expected JSON Patch array or { patch: [...] } format' };
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return { success: false, error: `JSON parse error: ${e.message}` };
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Generate context for LLM prompt
|
|
70
|
+
*/
|
|
71
|
+
getContext() {
|
|
72
|
+
const data = store.getState().data;
|
|
73
|
+
let context = schema.getDescription();
|
|
74
|
+
context += '\nCurrent Values:\n';
|
|
75
|
+
for (const [id, field] of Object.entries(schema.fields)) {
|
|
76
|
+
const value = data[id];
|
|
77
|
+
context += ` ${id}: ${JSON.stringify(value)} (${field.label})\n`;
|
|
78
|
+
}
|
|
79
|
+
context += `
|
|
80
|
+
To edit fields, respond with a JSON Patch array (RFC 6902):
|
|
81
|
+
[{ "op": "replace", "path": "/fieldName", "value": newValue }]
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
- Single edit: [{ "op": "replace", "path": "/budget", "value": 5000 }]
|
|
85
|
+
- Multiple: [{ "op": "replace", "path": "/budget", "value": 5000 }, { "op": "replace", "path": "/days", "value": 14 }]`;
|
|
86
|
+
return context;
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* Compact context for constrained prompts
|
|
90
|
+
*/
|
|
91
|
+
getCompactContext() {
|
|
92
|
+
const data = store.getState().data;
|
|
93
|
+
const values = Object.entries(data)
|
|
94
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
95
|
+
.join(', ');
|
|
96
|
+
return `${schema.name}: {${values}}\nJSON Patch: [{"op":"replace","path":"/field","value":x}]`;
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Generate OpenAI/Anthropic function calling schema
|
|
100
|
+
*/
|
|
101
|
+
getFunctionSchema() {
|
|
102
|
+
return {
|
|
103
|
+
name: `patch_${schema.name.toLowerCase().replace(/\s+/g, '_')}`,
|
|
104
|
+
description: `Apply JSON Patch operations to ${schema.name}. Uses RFC 6902 format.`,
|
|
105
|
+
parameters: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
patch: {
|
|
109
|
+
type: 'array',
|
|
110
|
+
description: 'JSON Patch operations (RFC 6902)',
|
|
111
|
+
items: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
op: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
enum: ['replace', 'add', 'remove'],
|
|
117
|
+
description: 'Operation type',
|
|
118
|
+
},
|
|
119
|
+
path: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: `JSON Pointer to field. Valid paths: ${schema.getFieldIds().map(f => `/${f}`).join(', ')}`,
|
|
122
|
+
},
|
|
123
|
+
value: {
|
|
124
|
+
description: 'New value (required for replace/add)',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ['op', 'path'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: ['patch'],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* Get current values
|
|
137
|
+
*/
|
|
138
|
+
getCurrentValues() {
|
|
139
|
+
return store.getState().data;
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Get patch history for audit trail
|
|
143
|
+
*/
|
|
144
|
+
history() {
|
|
145
|
+
return [...patchHistory];
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* Clear patch history
|
|
149
|
+
*/
|
|
150
|
+
clearHistory() {
|
|
151
|
+
patchHistory.length = 0;
|
|
152
|
+
historyId = 0;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Helper to check if edit was successful
|
|
158
|
+
*/
|
|
159
|
+
export function isSuccess(result) {
|
|
160
|
+
return result.success;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get human-readable message from result
|
|
164
|
+
*/
|
|
165
|
+
export function getResultMessage(result) {
|
|
166
|
+
if (result.success) {
|
|
167
|
+
return `Successfully applied ${result.applied} operation(s)`;
|
|
168
|
+
}
|
|
169
|
+
return result.error;
|
|
170
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duet-kit - Schema
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around Zod that adds metadata for UI labels and LLM context.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { SchemaFields, FieldDef, InferData } from './types';
|
|
8
|
+
export declare class DuetSchema<T extends SchemaFields> {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly fields: T;
|
|
11
|
+
constructor(name: string, fields: T);
|
|
12
|
+
getDefaults(): InferData<T>;
|
|
13
|
+
validate<K extends keyof T>(field: K, value: unknown): z.SafeParseReturnType<unknown, z.infer<T[K]['schema']>>;
|
|
14
|
+
validateAll(data: Partial<InferData<T>>): {
|
|
15
|
+
success: boolean;
|
|
16
|
+
errors: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
getField(fieldId: string): FieldDef | undefined;
|
|
19
|
+
getFieldIds(): string[];
|
|
20
|
+
getDescription(): string;
|
|
21
|
+
toJSONSchema(): object;
|
|
22
|
+
private getZodTypeDescription;
|
|
23
|
+
private zodToJSONSchema;
|
|
24
|
+
}
|
|
25
|
+
export declare function createSchema<T extends SchemaFields>(name: string, fields: T): DuetSchema<T>;
|
|
26
|
+
export declare function field<T>(schema: z.ZodType<T>, label: string, defaultValue: T): FieldDef<T>;
|
|
27
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGjE,qBAAa,UAAU,CAAC,CAAC,SAAS,YAAY;aAE1B,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,CAAC;gBADT,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC;IAI3B,WAAW,IAAI,SAAS,CAAC,CAAC,CAAC;IAS3B,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAK9G,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAc9F,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAK/C,WAAW,IAAI,MAAM,EAAE;IAKvB,cAAc,IAAI,MAAM;IAYxB,YAAY,IAAI,MAAM;IActB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,eAAe;CAwBxB;AAGD,wBAAgB,YAAY,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAE3F;AAGD,wBAAgB,KAAK,CAAC,CAAC,EACrB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,CAAC,GACd,QAAQ,CAAC,CAAC,CAAC,CAEb"}
|