@unoverse-platform/studio 0.1.90 → 0.1.91

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.
Files changed (130) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +130 -0
  13. package/vendor/base/src/auth/auth.ts +278 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +128 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +199 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +198 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  53. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  54. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  55. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  56. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  57. package/vendor/base/src/logger.ts +33 -0
  58. package/vendor/base/src/manifests/compose.ts +539 -0
  59. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  60. package/vendor/base/src/manifests/executor/context.ts +107 -0
  61. package/vendor/base/src/manifests/executor/index.ts +47 -0
  62. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  63. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  64. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  65. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  66. package/vendor/base/src/manifests/executor/send.ts +65 -0
  67. package/vendor/base/src/manifests/executor/session.ts +46 -0
  68. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  69. package/vendor/base/src/manifests/index.ts +280 -0
  70. package/vendor/base/src/manifests/integrity.ts +93 -0
  71. package/vendor/base/src/manifests/rows.ts +33 -0
  72. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  73. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  74. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  75. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  76. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  77. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  78. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  79. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  80. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  81. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  82. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  83. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  86. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  87. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  88. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  89. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  90. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  91. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  92. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  93. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  94. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  95. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  96. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  97. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  98. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  99. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  100. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  101. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  102. package/vendor/base/src/manifests/source.ts +187 -0
  103. package/vendor/base/src/manifests/watch.ts +67 -0
  104. package/vendor/base/src/markdown/document.ts +141 -0
  105. package/vendor/base/src/paths.ts +180 -0
  106. package/vendor/base/src/platform/audioManager.ts +100 -0
  107. package/vendor/base/src/platform/clientTransport.ts +65 -0
  108. package/vendor/base/src/platform/executionContext.ts +154 -0
  109. package/vendor/base/src/platform/index.ts +10 -0
  110. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  111. package/vendor/base/src/platform/redis.ts +54 -0
  112. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  113. package/vendor/base/src/pluginBase.ts +248 -0
  114. package/vendor/base/src/plugins/README.md +57 -0
  115. package/vendor/base/src/plugins/discovery.ts +87 -0
  116. package/vendor/base/src/plugins/index.ts +16 -0
  117. package/vendor/base/src/plugins/loader.ts +179 -0
  118. package/vendor/base/src/plugins/redis.ts +36 -0
  119. package/vendor/base/src/plugins/state.ts +53 -0
  120. package/vendor/base/src/registry.ts +95 -0
  121. package/vendor/base/src/security/permissions.ts +93 -0
  122. package/vendor/base/src/security/rules.ts +73 -0
  123. package/vendor/base/src/template/SafeExpression.ts +556 -0
  124. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  125. package/vendor/base/src/template/index.ts +22 -0
  126. package/vendor/base/src/types.ts +50 -0
  127. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  128. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  129. package/vendor/base/src/workflow/routing.ts +122 -0
  130. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,547 @@
1
+ /**
2
+ * ONE HANDLER PER TOOL, transcribed verbatim from the retired smart-document package
3
+ * (service/mcpHandlers.ts) — only the `api` parameter became the raw `redis` client. The
4
+ * error shapes are the AGENT'S RECOVERY PROTOCOL (STALE_SECTION returns the fresh hash and
5
+ * body so the model retries without a human): changing a field name here breaks agents, not
6
+ * types.
7
+ */
8
+ import {
9
+ findSection,
10
+ findSectionIndex,
11
+ getDoc,
12
+ mutateDoc,
13
+ resetDoc as resetDocStore,
14
+ } from "./store.js";
15
+ import { findDisallowedHeading, hashSection, makeSection, renderMarkdown } from "./sectionizer.js";
16
+ import type {
17
+ Doc,
18
+ Section,
19
+ SectionLevel,
20
+ ServiceResult,
21
+ } from "./types.js";
22
+
23
+ function staleSection(doc: Doc, s: Section): ServiceResult {
24
+ return {
25
+ ok: false,
26
+ error: "STALE_SECTION",
27
+ currentHash: s.hash,
28
+ currentBody: s.body,
29
+ currentVersion: doc.version,
30
+ hint: "Section changed since you read it. Use currentBody and currentHash and retry.",
31
+ };
32
+ }
33
+
34
+ function staleDoc(doc: Doc): ServiceResult {
35
+ return {
36
+ ok: false,
37
+ error: "STALE_DOC",
38
+ currentVersion: doc.version,
39
+ hint: "Document changed since you last read the outline. Call outline() again and retry.",
40
+ };
41
+ }
42
+
43
+ function notFound(id: string): ServiceResult {
44
+ return { ok: false, error: "NOT_FOUND", hint: `No section with id '${id}'.` };
45
+ }
46
+
47
+ function invalidParams(message: string): ServiceResult {
48
+ return { ok: false, error: "INVALID_PARAMS", hint: message };
49
+ }
50
+
51
+ function rehash(s: Section) {
52
+ s.hash = hashSection(s.heading, s.body);
53
+ }
54
+
55
+ // ---------- READ ----------
56
+
57
+ export async function handleOutline(
58
+ redis: any,
59
+ key: string,
60
+ sectionizeAt: SectionLevel,
61
+ ): Promise<ServiceResult> {
62
+ const doc = await getDoc(redis, key, sectionizeAt);
63
+ if (!doc) return { ok: false, error: "NOT_INITIALISED" };
64
+ return {
65
+ ok: true,
66
+ version: doc.version,
67
+ sections: doc.sections.map((s) => ({
68
+ id: s.id,
69
+ level: s.level,
70
+ heading: s.heading,
71
+ parentId: s.parentId,
72
+ hash: s.hash,
73
+ wordCount: countWords(s.body),
74
+ })),
75
+ };
76
+ }
77
+
78
+ function countWords(text: string): number {
79
+ const t = text.trim();
80
+ if (!t) return 0;
81
+ return t.split(/\s+/).length;
82
+ }
83
+
84
+ export async function handleReadSection(
85
+ redis: any,
86
+ key: string,
87
+ params: { id?: string; includeChildren?: boolean },
88
+ sectionizeAt: SectionLevel,
89
+ ): Promise<ServiceResult> {
90
+ if (typeof params?.id !== "string") return invalidParams("id must be a string");
91
+ const doc = await getDoc(redis, key, sectionizeAt);
92
+ if (!doc) return { ok: false, error: "NOT_INITIALISED" };
93
+ const s = findSection(doc, params.id);
94
+ if (!s) return notFound(params.id);
95
+
96
+ let children: any[] | undefined;
97
+ if (params.includeChildren) {
98
+ children = doc.sections
99
+ .filter((c) => c.parentId === s.id)
100
+ .map((c) => ({ id: c.id, level: c.level, heading: c.heading, body: c.body, hash: c.hash }));
101
+ }
102
+
103
+ return {
104
+ ok: true,
105
+ version: doc.version,
106
+ id: s.id,
107
+ level: s.level,
108
+ heading: s.heading,
109
+ body: s.body,
110
+ hash: s.hash,
111
+ ...(children ? { children } : {}),
112
+ };
113
+ }
114
+
115
+ // ---------- EDIT ----------
116
+
117
+ export async function handleUpdateSection(
118
+ redis: any,
119
+ key: string,
120
+ params: {
121
+ id?: string;
122
+ expectedHash?: string;
123
+ heading?: string;
124
+ body?: string;
125
+ expectedDocVersion?: number;
126
+ },
127
+ sectionizeAt: SectionLevel,
128
+ ): Promise<ServiceResult> {
129
+ if (typeof params?.id !== "string") return invalidParams("id must be a string");
130
+ if (typeof params?.expectedHash !== "string") return invalidParams("expectedHash is required");
131
+ if (params.heading === undefined && params.body === undefined) {
132
+ return invalidParams("provide at least one of heading or body");
133
+ }
134
+
135
+ const result = await mutateDoc(redis, key, (doc) => {
136
+ if (params.expectedDocVersion !== undefined && params.expectedDocVersion !== doc.version) {
137
+ return { ok: false, error: "STALE_DOC" };
138
+ }
139
+ const s = findSection(doc, params.id!);
140
+ if (!s) return { ok: false, error: "NOT_FOUND" };
141
+ if (s.hash !== params.expectedHash) {
142
+ return { ok: false, error: "STALE_SECTION" };
143
+ }
144
+ if (typeof params.body === "string") {
145
+ const bad = findDisallowedHeading(params.body, sectionizeAt);
146
+ if (bad) return { ok: false, error: "INVALID_STRUCTURE", extra: { line: bad } };
147
+ s.body = params.body;
148
+ }
149
+ if (typeof params.heading === "string") {
150
+ s.heading = params.heading;
151
+ }
152
+ rehash(s);
153
+ return { ok: true };
154
+ });
155
+
156
+ return resolveMutation(redis, key, result, params.id, sectionizeAt);
157
+ }
158
+
159
+ export async function handleAppendToSection(
160
+ redis: any,
161
+ key: string,
162
+ params: {
163
+ id?: string;
164
+ expectedHash?: string;
165
+ text?: string;
166
+ expectedDocVersion?: number;
167
+ },
168
+ sectionizeAt: SectionLevel,
169
+ ): Promise<ServiceResult> {
170
+ if (typeof params?.id !== "string") return invalidParams("id must be a string");
171
+ if (typeof params?.expectedHash !== "string") return invalidParams("expectedHash is required");
172
+ if (typeof params?.text !== "string") return invalidParams("text must be a string");
173
+
174
+ const result = await mutateDoc(redis, key, (doc) => {
175
+ if (params.expectedDocVersion !== undefined && params.expectedDocVersion !== doc.version) {
176
+ return { ok: false, error: "STALE_DOC" };
177
+ }
178
+ const s = findSection(doc, params.id!);
179
+ if (!s) return { ok: false, error: "NOT_FOUND" };
180
+ if (s.hash !== params.expectedHash) {
181
+ return { ok: false, error: "STALE_SECTION" };
182
+ }
183
+ const appended = s.body.length === 0 ? params.text! : s.body + "\n\n" + params.text!;
184
+ const bad = findDisallowedHeading(appended, sectionizeAt);
185
+ if (bad) return { ok: false, error: "INVALID_STRUCTURE", extra: { line: bad } };
186
+ s.body = appended;
187
+ rehash(s);
188
+ return { ok: true };
189
+ });
190
+
191
+ return resolveMutation(redis, key, result, params.id, sectionizeAt);
192
+ }
193
+
194
+ export async function handleReplaceInSection(
195
+ redis: any,
196
+ key: string,
197
+ params: {
198
+ id?: string;
199
+ expectedHash?: string;
200
+ old_str?: string;
201
+ new_str?: string;
202
+ expectedDocVersion?: number;
203
+ },
204
+ sectionizeAt: SectionLevel,
205
+ ): Promise<ServiceResult> {
206
+ if (typeof params?.id !== "string") return invalidParams("id must be a string");
207
+ if (typeof params?.expectedHash !== "string") return invalidParams("expectedHash is required");
208
+ if (typeof params?.old_str !== "string" || params.old_str.length === 0) {
209
+ return invalidParams("old_str must be a non-empty string");
210
+ }
211
+ if (typeof params?.new_str !== "string") return invalidParams("new_str must be a string");
212
+
213
+ const result = await mutateDoc(redis, key, (doc) => {
214
+ if (params.expectedDocVersion !== undefined && params.expectedDocVersion !== doc.version) {
215
+ return { ok: false, error: "STALE_DOC" };
216
+ }
217
+ const s = findSection(doc, params.id!);
218
+ if (!s) return { ok: false, error: "NOT_FOUND" };
219
+ if (s.hash !== params.expectedHash) {
220
+ return { ok: false, error: "STALE_SECTION" };
221
+ }
222
+ const matches = countOccurrences(s.body, params.old_str!);
223
+ if (matches === 0) return { ok: false, error: "NOT_FOUND" };
224
+ if (matches > 1) return { ok: false, error: "NOT_UNIQUE", extra: { matches } };
225
+ const next = s.body.replace(params.old_str!, params.new_str!);
226
+ const bad = findDisallowedHeading(next, sectionizeAt);
227
+ if (bad) return { ok: false, error: "INVALID_STRUCTURE", extra: { line: bad } };
228
+ s.body = next;
229
+ rehash(s);
230
+ return { ok: true };
231
+ });
232
+
233
+ return resolveMutation(redis, key, result, params.id, sectionizeAt);
234
+ }
235
+
236
+ function countOccurrences(haystack: string, needle: string): number {
237
+ if (!needle) return 0;
238
+ let count = 0;
239
+ let idx = 0;
240
+ while ((idx = haystack.indexOf(needle, idx)) !== -1) {
241
+ count++;
242
+ idx += needle.length;
243
+ }
244
+ return count;
245
+ }
246
+
247
+ export async function handleInsertSection(
248
+ redis: any,
249
+ key: string,
250
+ params: {
251
+ afterId?: string;
252
+ beforeId?: string;
253
+ parentId?: string;
254
+ level?: number;
255
+ heading?: string;
256
+ body?: string;
257
+ expectedDocVersion?: number;
258
+ },
259
+ sectionizeAt: SectionLevel,
260
+ ): Promise<ServiceResult> {
261
+ const placementKeys = ["afterId", "beforeId", "parentId"].filter(
262
+ (k) => typeof (params as any)[k] === "string",
263
+ );
264
+ // Placement is required only when the doc already has sections.
265
+ // An empty doc implicitly appends at root.
266
+ if (params.level !== 1 && params.level !== 2) {
267
+ return invalidParams("level must be 1 or 2");
268
+ }
269
+ if (params.level > sectionizeAt) {
270
+ return invalidParams(`level ${params.level} exceeds sectionizeAt ${sectionizeAt}`);
271
+ }
272
+ if (typeof params.heading !== "string") return invalidParams("heading is required");
273
+ const body = typeof params.body === "string" ? params.body : "";
274
+ const bad = findDisallowedHeading(body, sectionizeAt);
275
+ if (bad) {
276
+ return { ok: false, error: "INVALID_STRUCTURE", hint: `body contains disallowed heading: ${bad}` };
277
+ }
278
+
279
+ let newId = "";
280
+ const result = await mutateDoc(redis, key, (doc) => {
281
+ if (params.expectedDocVersion !== undefined && params.expectedDocVersion !== doc.version) {
282
+ return { ok: false, error: "STALE_DOC" };
283
+ }
284
+
285
+ const newSection = makeSection(
286
+ params.level as SectionLevel,
287
+ params.heading!,
288
+ body,
289
+ null,
290
+ doc.sections.length,
291
+ );
292
+ newId = newSection.id;
293
+
294
+ if (params.afterId) {
295
+ const idx = findSectionIndex(doc, params.afterId);
296
+ if (idx === -1) return { ok: false, error: "INVALID_PLACEMENT" };
297
+ doc.sections.splice(idx + 1, 0, newSection);
298
+ } else if (params.beforeId) {
299
+ const idx = findSectionIndex(doc, params.beforeId);
300
+ if (idx === -1) return { ok: false, error: "INVALID_PLACEMENT" };
301
+ doc.sections.splice(idx, 0, newSection);
302
+ } else if (params.parentId) {
303
+ const parentIdx = findSectionIndex(doc, params.parentId);
304
+ if (parentIdx === -1) return { ok: false, error: "INVALID_PLACEMENT" };
305
+ const parent = doc.sections[parentIdx];
306
+ if (newSection.level <= parent.level) {
307
+ return { ok: false, error: "INVALID_PLACEMENT" };
308
+ }
309
+ let insertAt = parentIdx + 1;
310
+ while (
311
+ insertAt < doc.sections.length &&
312
+ doc.sections[insertAt].level > parent.level
313
+ ) {
314
+ insertAt++;
315
+ }
316
+ doc.sections.splice(insertAt, 0, newSection);
317
+ } else {
318
+ // No placement key — only valid when the doc is empty, or append to the end.
319
+ if (doc.sections.length > 1) {
320
+ // Doc has content but no placement specified — append at the root end.
321
+ doc.sections.push(newSection);
322
+ } else if (doc.sections.length === 1) {
323
+ // Single existing section — append after it at the root.
324
+ doc.sections.push(newSection);
325
+ } else {
326
+ // Empty doc — this is the first section.
327
+ doc.sections.push(newSection);
328
+ }
329
+ }
330
+ return { ok: true };
331
+ });
332
+
333
+ if (!result.ok) {
334
+ const doc = await getDoc(redis, key, sectionizeAt);
335
+ return errorFrom(result, doc);
336
+ }
337
+
338
+ const doc = result.doc;
339
+ const s = findSection(doc, newId)!;
340
+ return {
341
+ ok: true,
342
+ version: doc.version,
343
+ id: s.id,
344
+ hash: s.hash,
345
+ };
346
+ }
347
+
348
+ export async function handleDeleteSection(
349
+ redis: any,
350
+ key: string,
351
+ params: {
352
+ id?: string;
353
+ expectedHash?: string;
354
+ cascade?: boolean;
355
+ expectedDocVersion?: number;
356
+ },
357
+ sectionizeAt: SectionLevel,
358
+ ): Promise<ServiceResult> {
359
+ if (typeof params?.id !== "string") return invalidParams("id must be a string");
360
+ if (typeof params?.expectedHash !== "string") return invalidParams("expectedHash is required");
361
+
362
+ const result = await mutateDoc(redis, key, (doc) => {
363
+ if (params.expectedDocVersion !== undefined && params.expectedDocVersion !== doc.version) {
364
+ return { ok: false, error: "STALE_DOC" };
365
+ }
366
+ const idx = findSectionIndex(doc, params.id!);
367
+ if (idx === -1) return { ok: false, error: "NOT_FOUND" };
368
+ const target = doc.sections[idx];
369
+ if (target.hash !== params.expectedHash) {
370
+ return { ok: false, error: "STALE_SECTION" };
371
+ }
372
+
373
+ const cascade = params.cascade === true;
374
+ const children = doc.sections.filter((c) => c.parentId === target.id);
375
+ if (cascade) {
376
+ const toRemove = new Set<string>([target.id]);
377
+ // Expand to all descendants (only 2 levels given H1/H2, but safe to generalise)
378
+ let grew = true;
379
+ while (grew) {
380
+ grew = false;
381
+ for (const s of doc.sections) {
382
+ if (s.parentId && toRemove.has(s.parentId) && !toRemove.has(s.id)) {
383
+ toRemove.add(s.id);
384
+ grew = true;
385
+ }
386
+ }
387
+ }
388
+ doc.sections = doc.sections.filter((s) => !toRemove.has(s.id));
389
+ } else {
390
+ for (const c of children) {
391
+ c.parentId = target.parentId;
392
+ }
393
+ doc.sections.splice(idx, 1);
394
+ }
395
+ return { ok: true };
396
+ });
397
+
398
+ return resolveMutation(redis, key, result, undefined, sectionizeAt);
399
+ }
400
+
401
+ export async function handleMoveSection(
402
+ redis: any,
403
+ key: string,
404
+ params: {
405
+ id?: string;
406
+ expectedHash?: string;
407
+ afterId?: string;
408
+ beforeId?: string;
409
+ parentId?: string;
410
+ expectedDocVersion?: number;
411
+ },
412
+ sectionizeAt: SectionLevel,
413
+ ): Promise<ServiceResult> {
414
+ if (typeof params?.id !== "string") return invalidParams("id must be a string");
415
+ if (typeof params?.expectedHash !== "string") return invalidParams("expectedHash is required");
416
+ const placementKeys = ["afterId", "beforeId", "parentId"].filter(
417
+ (k) => typeof (params as any)[k] === "string",
418
+ );
419
+ if (placementKeys.length !== 1) {
420
+ return invalidParams("exactly one of afterId, beforeId, or parentId is required");
421
+ }
422
+
423
+ const result = await mutateDoc(redis, key, (doc) => {
424
+ if (params.expectedDocVersion !== undefined && params.expectedDocVersion !== doc.version) {
425
+ return { ok: false, error: "STALE_DOC" };
426
+ }
427
+ const idx = findSectionIndex(doc, params.id!);
428
+ if (idx === -1) return { ok: false, error: "NOT_FOUND" };
429
+ const s = doc.sections[idx];
430
+ if (s.hash !== params.expectedHash) {
431
+ return { ok: false, error: "STALE_SECTION" };
432
+ }
433
+
434
+ // Remove from current position
435
+ doc.sections.splice(idx, 1);
436
+
437
+ // Re-insert at new position
438
+ if (params.afterId) {
439
+ const i = findSectionIndex(doc, params.afterId);
440
+ if (i === -1) return { ok: false, error: "INVALID_PLACEMENT" };
441
+ doc.sections.splice(i + 1, 0, s);
442
+ } else if (params.beforeId) {
443
+ const i = findSectionIndex(doc, params.beforeId);
444
+ if (i === -1) return { ok: false, error: "INVALID_PLACEMENT" };
445
+ doc.sections.splice(i, 0, s);
446
+ } else if (params.parentId) {
447
+ const pIdx = findSectionIndex(doc, params.parentId);
448
+ if (pIdx === -1) return { ok: false, error: "INVALID_PLACEMENT" };
449
+ const parent = doc.sections[pIdx];
450
+ if (s.level <= parent.level) {
451
+ return { ok: false, error: "INVALID_PLACEMENT" };
452
+ }
453
+ let insertAt = pIdx + 1;
454
+ while (insertAt < doc.sections.length && doc.sections[insertAt].level > parent.level) {
455
+ insertAt++;
456
+ }
457
+ doc.sections.splice(insertAt, 0, s);
458
+ }
459
+ return { ok: true };
460
+ });
461
+
462
+ return resolveMutation(redis, key, result, params.id, sectionizeAt);
463
+ }
464
+
465
+ // ---------- BULK ----------
466
+
467
+ export async function handleResetDoc(
468
+ redis: any,
469
+ key: string,
470
+ initialMarkdown: string,
471
+ sectionizeAt: SectionLevel,
472
+ ): Promise<ServiceResult> {
473
+ const doc = await resetDocStore(redis, key, initialMarkdown, sectionizeAt);
474
+ return {
475
+ ok: true,
476
+ version: doc.version,
477
+ sectionCount: doc.sections.length,
478
+ };
479
+ }
480
+
481
+ // ---------- helpers ----------
482
+
483
+ type ServiceSuccess = Extract<ServiceResult, { ok: true }>;
484
+
485
+ async function resolveMutation(
486
+ redis: any,
487
+ key: string,
488
+ result:
489
+ | { ok: true; doc: Doc }
490
+ | { ok: false; error: string; extra?: Record<string, any> },
491
+ sectionId: string | undefined,
492
+ sectionizeAt: SectionLevel,
493
+ ): Promise<ServiceResult> {
494
+ if (!result.ok) {
495
+ const doc = await getDoc(redis, key, sectionizeAt);
496
+ return errorFrom(result, doc, sectionId);
497
+ }
498
+ const doc = result.doc;
499
+ const response: ServiceSuccess = { ok: true, version: doc.version };
500
+ if (sectionId) {
501
+ const s = findSection(doc, sectionId);
502
+ if (s) response.hash = s.hash;
503
+ }
504
+ return response;
505
+ }
506
+
507
+ function errorFrom(
508
+ r: { ok: false; error: string; extra?: Record<string, any> },
509
+ doc: Doc | null,
510
+ sectionId?: string,
511
+ ): ServiceResult {
512
+ const base: any = { ok: false, error: r.error };
513
+ if (doc) {
514
+ base.currentVersion = doc.version;
515
+ if (r.error === "STALE_SECTION" && sectionId) {
516
+ const s = findSection(doc, sectionId);
517
+ if (s) {
518
+ base.currentHash = s.hash;
519
+ base.currentBody = s.body;
520
+ base.hint = "Section changed since you read it. Retry with currentHash + currentBody.";
521
+ }
522
+ }
523
+ if (r.error === "STALE_DOC") {
524
+ base.hint = "Document changed since you last read the outline. Retry after outline().";
525
+ }
526
+ }
527
+ if (r.extra) Object.assign(base, r.extra);
528
+ if (r.error === "NOT_UNIQUE" && !base.hint) {
529
+ base.hint = "old_str matched multiple times. Widen it with surrounding context.";
530
+ }
531
+ if (r.error === "INVALID_STRUCTURE" && !base.hint) {
532
+ base.hint = "Body contained an H1 or H2, which would corrupt the outline. Use H3+ inside bodies.";
533
+ }
534
+ if (r.error === "INVALID_PLACEMENT" && !base.hint) {
535
+ base.hint = "Placement target id was not found or violates hierarchy rules.";
536
+ }
537
+ if (r.error === "CONCURRENT_UPDATE" && !base.hint) {
538
+ base.hint = "Too many concurrent writers. Retry.";
539
+ }
540
+ return base;
541
+ }
542
+
543
+ // ---------- RENDER (used by executor) ----------
544
+
545
+ export function renderDoc(doc: Doc): string {
546
+ return renderMarkdown(doc.sections);
547
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * THE DOC STORE, as a call form: `docstore: <op>` in a node's call list.
3
+ *
4
+ * What smart-document actually needed from code, extracted to where computation belongs
5
+ * (DECLARATIVE_NODES.md §2 — the awsSigV4 rule: the executor computes, the manifest
6
+ * describes). A long markdown document lives in Redis, sectioned so an agent edits it
7
+ * hash-checked section by section instead of rewriting a blob; the design rationale is
8
+ * _legacy/nodes/smart-document/CLAUDE.md.
9
+ *
10
+ * The ops are exactly the retired node's methods plus `render`, which is what its
11
+ * graph-triggered executeNode did: initialise from config if cold, then hand back the
12
+ * whole doc as markdown for a renderer.
13
+ */
14
+ import type { SectionLevel, ServiceResult } from "./types.js";
15
+ import { getDoc, initDoc, keyFor } from "./store.js";
16
+ import { renderMarkdown } from "./sectionizer.js";
17
+ import {
18
+ handleOutline,
19
+ handleReadSection,
20
+ handleUpdateSection,
21
+ handleAppendToSection,
22
+ handleReplaceInSection,
23
+ handleInsertSection,
24
+ handleDeleteSection,
25
+ handleMoveSection,
26
+ handleResetDoc,
27
+ } from "./handlers.js";
28
+
29
+ /**
30
+ * Ops that CHANGE the doc — the set the hybrid contract keys on: after any of these arrives
31
+ * over the service channel, the node's workflow channel re-fires so a downstream renderer
32
+ * receives the fresh markdown. Read ops must NOT re-fire: re-rendering on reads re-emitted
33
+ * the full document on every navigation call and flooded the renderer (the retired node
34
+ * learned this live — its MUTATING_METHODS guard exists for exactly this).
35
+ */
36
+ export const MUTATING_DOC_OPS = new Set([
37
+ "updateSection",
38
+ "appendToSection",
39
+ "replaceInSection",
40
+ "insertSection",
41
+ "deleteSection",
42
+ "moveSection",
43
+ "resetDoc",
44
+ ]);
45
+
46
+ export interface DocScope {
47
+ userId?: string;
48
+ workflowId?: string;
49
+ conversationId?: string;
50
+ nodeId?: string;
51
+ }
52
+
53
+ /**
54
+ * The doc's identity, derived by the EXECUTOR from the run's own ids — never by the
55
+ * manifest, which could otherwise read another conversation's document by naming its key.
56
+ *
57
+ * Same derivation as the retired node's resolveKey: scoped to user + workflow +
58
+ * conversation + node instance so the doc persists across runs and chats while staying
59
+ * isolated; conversationId falls back to workflowId because on this platform it tracks the
60
+ * workflow and is reused across chats.
61
+ */
62
+ export function docKeyFor(scope: DocScope | undefined): string | null {
63
+ const userId = scope?.userId ?? "";
64
+ const workflowId = scope?.workflowId ?? "";
65
+ const nodeId = scope?.nodeId ?? "";
66
+ const conversationId = scope?.conversationId || workflowId;
67
+ if (!userId || !workflowId || !nodeId) return null;
68
+ return keyFor(userId, workflowId, conversationId, nodeId);
69
+ }
70
+
71
+ function level(config: Record<string, any> | undefined): SectionLevel {
72
+ return config?.sectionizeAt === 1 ? 1 : 2;
73
+ }
74
+
75
+ /**
76
+ * One docstore op. Errors come back STRUCTURED, never thrown, for the same reason the
77
+ * retired node shaped them: the caller is usually a MODEL, and `{ ok: false, error, hint }`
78
+ * is a recoverable instruction where an exception would end the turn.
79
+ */
80
+ export async function performDoc(
81
+ op: string,
82
+ params: Record<string, any>,
83
+ redis: any,
84
+ scope: DocScope | undefined,
85
+ config: Record<string, any> | undefined,
86
+ ): Promise<ServiceResult | { ok: true; version: number; markdown: string }> {
87
+ if (!redis) {
88
+ // The one non-recoverable case: a platform with no Redis has nowhere to keep a doc.
89
+ throw new Error("docstore requires Redis, and the platform has none configured");
90
+ }
91
+ const key = docKeyFor(scope);
92
+ if (!key) {
93
+ return {
94
+ ok: false,
95
+ error: "NOT_INITIALISED",
96
+ hint: "docstore has no userId/workflowId on the execution context to derive its state key.",
97
+ };
98
+ }
99
+
100
+ const at = level(config);
101
+ const initial = String(config?.initialMarkdown ?? "");
102
+ await initDoc(redis, key, initial, at);
103
+
104
+ switch (op) {
105
+ case "render": {
106
+ // The workflow channel's op: the WHOLE doc as markdown, for a downstream renderer.
107
+ const doc = await getDoc(redis, key, at);
108
+ return { ok: true, version: doc?.version ?? 0, markdown: doc ? renderMarkdown(doc.sections) : "" };
109
+ }
110
+ case "outline":
111
+ return handleOutline(redis, key, at);
112
+ case "readSection":
113
+ return handleReadSection(redis, key, params, at);
114
+ case "updateSection":
115
+ return handleUpdateSection(redis, key, params, at);
116
+ case "appendToSection":
117
+ return handleAppendToSection(redis, key, params, at);
118
+ case "replaceInSection":
119
+ return handleReplaceInSection(redis, key, params, at);
120
+ case "insertSection":
121
+ return handleInsertSection(redis, key, params, at);
122
+ case "deleteSection":
123
+ return handleDeleteSection(redis, key, params, at);
124
+ case "moveSection":
125
+ return handleMoveSection(redis, key, params, at);
126
+ case "resetDoc":
127
+ return handleResetDoc(redis, key, initial, at);
128
+ default:
129
+ return {
130
+ ok: false,
131
+ error: "UNKNOWN_METHOD",
132
+ hint: `Unknown docstore op '${op}'. Available: render, outline, readSection, updateSection, appendToSection, replaceInSection, insertSection, deleteSection, moveSection, resetDoc.`,
133
+ };
134
+ }
135
+ }