@larkup/tool-doc-editor 0.2.30 → 0.3.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/dist/editor.d.ts CHANGED
@@ -1,12 +1,6 @@
1
- /**
2
- * Document editor — orchestrates parsing, editing, and export.
3
- *
4
- * Uses:
5
- * - pdf-lib (MIT) for native PDF form filling
6
- * - Sandbox scripts for DOCX/PPTX editing via python-docx/python-pptx
7
- * - In-memory session management
8
- */
9
1
  import type { EditorSession, FieldEdit, ContentEdit, EditResult, SignatureData } from './types.js';
2
+ /** Hydrate isolated sessions from the host's tenant-bound persistent repository. */
3
+ export declare function runWithDocumentSessions<T>(operation: () => T): Promise<Awaited<T>>;
10
4
  export declare function getSession(sessionId: string): EditorSession | undefined;
11
5
  export declare function getAllSessions(): EditorSession[];
12
6
  export declare function deleteSession(sessionId: string): boolean;
package/dist/editor.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { workspaceHost } from '@larkup/core/workspace-host';
1
3
  /**
2
4
  * Document editor — orchestrates parsing, editing, and export.
3
5
  *
@@ -11,18 +13,50 @@ import { SandboxManager } from '@larkup/sandbox';
11
13
  import { readConfig } from '@larkup/core/config-store';
12
14
  import { parseDocument, enrichPDFWithText } from './parsers.js';
13
15
  import { generateFillDOCXScript, generateFillPPTXScript, generateEditDOCXScript, generateEditPPTXScript, generateExtractPDFTextScript, } from './sandbox-scripts.js';
14
- const sessions = new Map();
16
+ const localSessions = new Map();
17
+ const sessionScope = new AsyncLocalStorage();
18
+ function sessions() {
19
+ return sessionScope.getStore()?.sessions ?? localSessions;
20
+ }
21
+ async function persistSessions() {
22
+ await sessionScope.getStore()?.flush();
23
+ }
24
+ /** Hydrate isolated sessions from the host's tenant-bound persistent repository. */
25
+ export async function runWithDocumentSessions(operation) {
26
+ const host = workspaceHost();
27
+ if (!host || sessionScope.getStore())
28
+ return await operation();
29
+ const stored = ((await host.readState('document-sessions')) ?? []);
30
+ const scoped = new Map(stored.map((session) => [session.id, session]));
31
+ let previous = JSON.stringify(stored);
32
+ let pending = Promise.resolve();
33
+ const flush = () => {
34
+ pending = pending.then(async () => {
35
+ const next = JSON.stringify([...scoped.values()]);
36
+ if (next !== previous) {
37
+ await host.writeState('document-sessions', JSON.parse(next));
38
+ previous = next;
39
+ }
40
+ });
41
+ return pending;
42
+ };
43
+ return await sessionScope.run({ sessions: scoped, flush }, async () => {
44
+ const result = await operation();
45
+ await flush();
46
+ return result;
47
+ });
48
+ }
15
49
  export function getSession(sessionId) {
16
- return sessions.get(sessionId);
50
+ return sessions().get(sessionId);
17
51
  }
18
52
  export function getAllSessions() {
19
- return Array.from(sessions.values()).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
53
+ return Array.from(sessions().values()).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
20
54
  }
21
55
  export function deleteSession(sessionId) {
22
- return sessions.delete(sessionId);
56
+ return sessions().delete(sessionId);
23
57
  }
24
58
  export function restoreSession(session) {
25
- sessions.set(session.id, session);
59
+ sessions().set(session.id, session);
26
60
  }
27
61
  export async function createSession(buffer, fileName, mimeType) {
28
62
  // Parse the document
@@ -75,13 +109,14 @@ export async function createSession(buffer, fileName, mimeType) {
75
109
  createdAt: new Date().toISOString(),
76
110
  updatedAt: new Date().toISOString(),
77
111
  };
78
- sessions.set(session.id, session);
112
+ sessions().set(session.id, session);
113
+ await persistSessions();
79
114
  return session;
80
115
  }
81
116
  export async function applyFieldEdits(sessionId, edits) {
82
- const session = sessions.get(sessionId);
117
+ const session = sessions().get(sessionId);
83
118
  if (!session) {
84
- console.error(`[doc-editor] Session not found: ${sessionId}. Available sessions:`, Array.from(sessions.keys()));
119
+ console.error(`[doc-editor] Session not found: ${sessionId}. Available sessions:`, Array.from(sessions().keys()));
85
120
  return {
86
121
  success: false,
87
122
  parsed: null,
@@ -212,6 +247,7 @@ export async function applyFieldEdits(sessionId, edits) {
212
247
  session.parsed = newParsed;
213
248
  session.edits.push(...edits);
214
249
  session.updatedAt = new Date().toISOString();
250
+ await persistSessions();
215
251
  return {
216
252
  success: true,
217
253
  parsed: newParsed,
@@ -231,7 +267,7 @@ export async function applyFieldEdits(sessionId, edits) {
231
267
  }
232
268
  }
233
269
  export async function applyContentEdits(sessionId, edits) {
234
- const session = sessions.get(sessionId);
270
+ const session = sessions().get(sessionId);
235
271
  if (!session) {
236
272
  return {
237
273
  success: false,
@@ -308,6 +344,7 @@ export async function applyContentEdits(sessionId, edits) {
308
344
  session.currentFileBase64 = newBase64;
309
345
  session.parsed = newParsed;
310
346
  session.updatedAt = new Date().toISOString();
347
+ await persistSessions();
311
348
  return {
312
349
  success: true,
313
350
  parsed: newParsed,
@@ -326,7 +363,7 @@ export async function applyContentEdits(sessionId, edits) {
326
363
  }
327
364
  }
328
365
  export function exportDocument(sessionId) {
329
- const session = sessions.get(sessionId);
366
+ const session = sessions().get(sessionId);
330
367
  if (!session)
331
368
  return null;
332
369
  return {
@@ -421,7 +458,7 @@ async function fillPDFNatively(buffer, edits) {
421
458
  return { buffer: Buffer.from(modifiedBytes), matchedFields };
422
459
  }
423
460
  export async function applySignature(sessionId, data) {
424
- const session = sessions.get(sessionId);
461
+ const session = sessions().get(sessionId);
425
462
  if (!session) {
426
463
  return { success: false, error: 'Session not found' };
427
464
  }
@@ -534,7 +571,8 @@ export async function applySignature(sessionId, data) {
534
571
  session.currentFileBase64 = newBase64;
535
572
  session.parsed = newParsed;
536
573
  session.updatedAt = new Date().toISOString();
537
- sessions.set(sessionId, session);
574
+ await persistSessions();
575
+ sessions().set(sessionId, session);
538
576
  return {
539
577
  success: true,
540
578
  parsed: newParsed,
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  export declare const TOOL_META: {
9
9
  readonly id: "doc-editor";
10
10
  readonly name: "Document Editor";
11
- readonly version: "0.2.28";
11
+ readonly version: "0.3.0";
12
12
  };
13
13
  export declare const TOOL_EXTENSION: {
14
14
  id: string;
@@ -18,3 +18,4 @@ export declare const TOOL_EXTENSION: {
18
18
  export { createSession, getSession, getAllSessions, deleteSession, restoreSession, applyFieldEdits, applyContentEdits, applySignature, exportDocument, } from './editor.js';
19
19
  export { parseDocument, parsePDF, parseDOCX, parsePPTX, parseTXT, detectDocumentType, enrichPDFWithText, } from './parsers.js';
20
20
  export type { DocumentType, DocumentField, DocumentPage, ParsedDocument, FieldEdit, ContentEdit, EditorSession, EditResult, } from './types.js';
21
+ export { runWithDocumentSessions } from './editor.js';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  export const TOOL_META = {
9
9
  id: 'doc-editor',
10
10
  name: 'Document Editor',
11
- version: '0.2.28',
11
+ version: '0.3.0',
12
12
  };
13
13
  export const TOOL_EXTENSION = {
14
14
  id: 'doc-editor',
@@ -18,3 +18,4 @@ export const TOOL_EXTENSION = {
18
18
  // Public API
19
19
  export { createSession, getSession, getAllSessions, deleteSession, restoreSession, applyFieldEdits, applyContentEdits, applySignature, exportDocument, } from './editor.js';
20
20
  export { parseDocument, parsePDF, parseDOCX, parsePPTX, parseTXT, detectDocumentType, enrichPDFWithText, } from './parsers.js';
21
+ export { runWithDocumentSessions } from './editor.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/tool-doc-editor",
3
- "version": "0.2.30",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -40,7 +40,7 @@
40
40
  "jszip": "^3.10.1",
41
41
  "mammoth": "^1.12.0",
42
42
  "pdf-lib": "^1.17.1",
43
- "@larkup/core": "0.7.4",
43
+ "@larkup/core": "0.8.0",
44
44
  "@larkup/sandbox": "0.1.8"
45
45
  },
46
46
  "peerDependencies": {
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "typescript": "^7.0.2",
56
- "@larkup/marketplace": "0.3.8"
56
+ "@larkup/marketplace": "0.3.9"
57
57
  },
58
58
  "license": "Apache-2.0",
59
59
  "repository": {
package/src/editor.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { workspaceHost } from '@larkup/core/workspace-host';
1
3
  /**
2
4
  * Document editor — orchestrates parsing, editing, and export.
3
5
  *
@@ -29,24 +31,58 @@ import {
29
31
  generateExtractPDFTextScript,
30
32
  } from './sandbox-scripts.js';
31
33
 
32
- const sessions = new Map<string, EditorSession>();
34
+ const localSessions = new Map<string, EditorSession>();
35
+ const sessionScope = new AsyncLocalStorage<{
36
+ sessions: Map<string, EditorSession>;
37
+ flush: () => Promise<void>;
38
+ }>();
39
+ function sessions() {
40
+ return sessionScope.getStore()?.sessions ?? localSessions;
41
+ }
42
+ async function persistSessions() {
43
+ await sessionScope.getStore()?.flush();
44
+ }
45
+ /** Hydrate isolated sessions from the host's tenant-bound persistent repository. */
46
+ export async function runWithDocumentSessions<T>(operation: () => T): Promise<Awaited<T>> {
47
+ const host = workspaceHost();
48
+ if (!host || sessionScope.getStore()) return await operation();
49
+ const stored = ((await host.readState('document-sessions')) ?? []) as EditorSession[];
50
+ const scoped = new Map(stored.map((session) => [session.id, session]));
51
+ let previous = JSON.stringify(stored);
52
+ let pending = Promise.resolve();
53
+ const flush = () => {
54
+ pending = pending.then(async () => {
55
+ const next = JSON.stringify([...scoped.values()]);
56
+ if (next !== previous) {
57
+ await host.writeState('document-sessions', JSON.parse(next));
58
+ previous = next;
59
+ }
60
+ });
61
+ return pending;
62
+ };
63
+ return await sessionScope.run({ sessions: scoped, flush }, async () => {
64
+ const result = await operation();
65
+ await flush();
66
+ return result;
67
+ });
68
+ }
33
69
 
34
70
  export function getSession(sessionId: string): EditorSession | undefined {
35
- return sessions.get(sessionId);
71
+ return sessions().get(sessionId);
36
72
  }
37
73
 
38
74
  export function getAllSessions(): EditorSession[] {
39
- return Array.from(sessions.values()).sort(
75
+ return Array.from(sessions().values()).sort(
40
76
  (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
41
77
  );
42
78
  }
43
79
 
44
80
  export function deleteSession(sessionId: string): boolean {
45
- return sessions.delete(sessionId);
81
+ return sessions().delete(sessionId);
46
82
  }
47
83
 
48
84
  export function restoreSession(session: EditorSession): void {
49
- sessions.set(session.id, session);
85
+ sessions().set(session.id, session);
50
86
  }
51
87
 
52
88
  export async function createSession(
@@ -112,16 +148,17 @@ export async function createSession(
112
148
  updatedAt: new Date().toISOString(),
113
149
  };
114
150
 
115
- sessions.set(session.id, session);
151
+ sessions().set(session.id, session);
152
+ await persistSessions();
116
153
  return session;
117
154
  }
118
155
 
119
156
  export async function applyFieldEdits(sessionId: string, edits: FieldEdit[]): Promise<EditResult> {
120
- const session = sessions.get(sessionId);
157
+ const session = sessions().get(sessionId);
121
158
  if (!session) {
122
159
  console.error(
123
160
  `[doc-editor] Session not found: ${sessionId}. Available sessions:`,
124
- Array.from(sessions.keys()),
161
+ Array.from(sessions().keys()),
125
162
  );
126
163
  return {
127
164
  success: false,
@@ -279,6 +316,7 @@ export async function applyFieldEdits(sessionId: string, edits: FieldEdit[]): Pr
279
316
  session.parsed = newParsed;
280
317
  session.edits.push(...edits);
281
318
  session.updatedAt = new Date().toISOString();
319
+ await persistSessions();
282
320
 
283
321
  return {
284
322
  success: true,
@@ -302,7 +340,7 @@ export async function applyContentEdits(
302
340
  sessionId: string,
303
341
  edits: ContentEdit[],
304
342
  ): Promise<EditResult> {
305
- const session = sessions.get(sessionId);
343
+ const session = sessions().get(sessionId);
306
344
  if (!session) {
307
345
  return {
308
346
  success: false,
@@ -390,6 +428,7 @@ export async function applyContentEdits(
390
428
  session.currentFileBase64 = newBase64;
391
429
  session.parsed = newParsed;
392
430
  session.updatedAt = new Date().toISOString();
431
+ await persistSessions();
393
432
 
394
433
  return {
395
434
  success: true,
@@ -411,7 +450,7 @@ export async function applyContentEdits(
411
450
  export function exportDocument(
412
451
  sessionId: string,
413
452
  ): { buffer: Buffer; fileName: string; mimeType: string } | null {
414
- const session = sessions.get(sessionId);
453
+ const session = sessions().get(sessionId);
415
454
  if (!session) return null;
416
455
 
417
456
  return {
@@ -522,7 +561,7 @@ async function fillPDFNatively(
522
561
  }
523
562
 
524
563
  export async function applySignature(sessionId: string, data: SignatureData): Promise<EditResult> {
525
- const session = sessions.get(sessionId);
564
+ const session = sessions().get(sessionId);
526
565
  if (!session) {
527
566
  return { success: false, error: 'Session not found' } as any;
528
567
  }
@@ -651,7 +690,8 @@ export async function applySignature(sessionId: string, data: SignatureData): Pr
651
690
  session.currentFileBase64 = newBase64;
652
691
  session.parsed = newParsed;
653
692
  session.updatedAt = new Date().toISOString();
654
- sessions.set(sessionId, session);
693
+ await persistSessions();
694
+ sessions().set(sessionId, session);
655
695
 
656
696
  return {
657
697
  success: true,
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  export const TOOL_META = {
10
10
  id: 'doc-editor',
11
11
  name: 'Document Editor',
12
- version: '0.2.28',
12
+ version: '0.3.0',
13
13
  } as const;
14
14
 
15
15
  export const TOOL_EXTENSION = {
@@ -51,3 +51,5 @@ export type {
51
51
  EditorSession,
52
52
  EditResult,
53
53
  } from './types.js';
54
+
55
+ export { runWithDocumentSessions } from './editor.js';
@@ -5,7 +5,7 @@
5
5
  "description": "AI-powered form filling and document editing with Canvas-style live preview.",
6
6
  "longDescription": "Open PDF, Word, PowerPoint, and text files in a split-view Canvas. AI fills forms using your knowledge base, edits content on request, and previews changes live. Supports PDF form fields (AcroForm), DOCX paragraphs/tables, PPTX slide text, and plain text editing. Uses pdf-lib (native) for PDF and the Docker sandbox for DOCX/PPTX via python-docx/python-pptx.",
7
7
  "category": "utility",
8
- "version": "0.2.30",
8
+ "version": "0.3.0",
9
9
  "pricing": "free",
10
10
  "emoji": "📝",
11
11
  "icon": "FileEdit",