@aaronsb/google-workspace-mcp 2.0.0 → 2.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.
Files changed (75) hide show
  1. package/build/__tests__/server/queue.test.js +5 -0
  2. package/build/__tests__/server/queue.test.js.map +1 -1
  3. package/build/server/formatting/markdown.d.ts +7 -0
  4. package/build/server/formatting/markdown.js +1 -1
  5. package/build/server/formatting/markdown.js.map +1 -1
  6. package/build/server/formatting/next-steps.js +19 -0
  7. package/build/server/formatting/next-steps.js.map +1 -1
  8. package/build/server/handler.d.ts +4 -0
  9. package/build/server/handler.js +17 -1
  10. package/build/server/handler.js.map +1 -1
  11. package/build/server/queue.js +2 -0
  12. package/build/server/queue.js.map +1 -1
  13. package/build/server/scratchpad/__tests__/json-path.test.d.ts +4 -0
  14. package/build/server/scratchpad/__tests__/json-path.test.js +121 -0
  15. package/build/server/scratchpad/__tests__/json-path.test.js.map +1 -0
  16. package/build/server/scratchpad/__tests__/manager.test.d.ts +5 -0
  17. package/build/server/scratchpad/__tests__/manager.test.js +886 -0
  18. package/build/server/scratchpad/__tests__/manager.test.js.map +1 -0
  19. package/build/server/scratchpad/__tests__/validate.test.d.ts +4 -0
  20. package/build/server/scratchpad/__tests__/validate.test.js +112 -0
  21. package/build/server/scratchpad/__tests__/validate.test.js.map +1 -0
  22. package/build/server/scratchpad/adapters/import-doc.d.ts +16 -0
  23. package/build/server/scratchpad/adapters/import-doc.js +123 -0
  24. package/build/server/scratchpad/adapters/import-doc.js.map +1 -0
  25. package/build/server/scratchpad/adapters/import-drive.d.ts +12 -0
  26. package/build/server/scratchpad/adapters/import-drive.js +52 -0
  27. package/build/server/scratchpad/adapters/import-drive.js.map +1 -0
  28. package/build/server/scratchpad/adapters/import-email.d.ts +13 -0
  29. package/build/server/scratchpad/adapters/import-email.js +87 -0
  30. package/build/server/scratchpad/adapters/import-email.js.map +1 -0
  31. package/build/server/scratchpad/adapters/import-meet.d.ts +12 -0
  32. package/build/server/scratchpad/adapters/import-meet.js +109 -0
  33. package/build/server/scratchpad/adapters/import-meet.js.map +1 -0
  34. package/build/server/scratchpad/adapters/import-sheet.d.ts +12 -0
  35. package/build/server/scratchpad/adapters/import-sheet.js +55 -0
  36. package/build/server/scratchpad/adapters/import-sheet.js.map +1 -0
  37. package/build/server/scratchpad/adapters/index.d.ts +15 -0
  38. package/build/server/scratchpad/adapters/index.js +18 -0
  39. package/build/server/scratchpad/adapters/index.js.map +1 -0
  40. package/build/server/scratchpad/adapters/send-calendar.d.ts +15 -0
  41. package/build/server/scratchpad/adapters/send-calendar.js +50 -0
  42. package/build/server/scratchpad/adapters/send-calendar.js.map +1 -0
  43. package/build/server/scratchpad/adapters/send-doc.d.ts +18 -0
  44. package/build/server/scratchpad/adapters/send-doc.js +77 -0
  45. package/build/server/scratchpad/adapters/send-doc.js.map +1 -0
  46. package/build/server/scratchpad/adapters/send-email-draft.d.ts +12 -0
  47. package/build/server/scratchpad/adapters/send-email-draft.js +52 -0
  48. package/build/server/scratchpad/adapters/send-email-draft.js.map +1 -0
  49. package/build/server/scratchpad/adapters/send-email.d.ts +15 -0
  50. package/build/server/scratchpad/adapters/send-email.js +73 -0
  51. package/build/server/scratchpad/adapters/send-email.js.map +1 -0
  52. package/build/server/scratchpad/adapters/send-sheet.d.ts +13 -0
  53. package/build/server/scratchpad/adapters/send-sheet.js +71 -0
  54. package/build/server/scratchpad/adapters/send-sheet.js.map +1 -0
  55. package/build/server/scratchpad/adapters/send-task.d.ts +12 -0
  56. package/build/server/scratchpad/adapters/send-task.js +53 -0
  57. package/build/server/scratchpad/adapters/send-task.js.map +1 -0
  58. package/build/server/scratchpad/adapters/send-workspace.d.ts +11 -0
  59. package/build/server/scratchpad/adapters/send-workspace.js +69 -0
  60. package/build/server/scratchpad/adapters/send-workspace.js.map +1 -0
  61. package/build/server/scratchpad/handler.d.ts +9 -0
  62. package/build/server/scratchpad/handler.js +476 -0
  63. package/build/server/scratchpad/handler.js.map +1 -0
  64. package/build/server/scratchpad/json-path.d.ts +12 -0
  65. package/build/server/scratchpad/json-path.js +75 -0
  66. package/build/server/scratchpad/json-path.js.map +1 -0
  67. package/build/server/scratchpad/manager.d.ts +140 -0
  68. package/build/server/scratchpad/manager.js +561 -0
  69. package/build/server/scratchpad/manager.js.map +1 -0
  70. package/build/server/scratchpad/validate.d.ts +7 -0
  71. package/build/server/scratchpad/validate.js +96 -0
  72. package/build/server/scratchpad/validate.js.map +1 -0
  73. package/build/server/tools.js +48 -1
  74. package/build/server/tools.js.map +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,140 @@
1
+ /**
2
+ * ScratchpadManager — line-addressed content authoring buffer.
3
+ * See ADR-301: Scratchpad Buffer — Service-Agnostic Content Authoring.
4
+ */
5
+ export type ScratchpadFormat = 'text' | 'markdown' | 'json' | 'csv';
6
+ /** Present when scratchpad is a live view of a GWS resource (JSON mode). */
7
+ export interface LiveBinding {
8
+ service: 'docs' | 'sheets';
9
+ resourceId: string;
10
+ account: string;
11
+ }
12
+ /** File reference tracked in the attachment side-table. */
13
+ export interface AttachmentRef {
14
+ refId: string;
15
+ source: 'workspace' | 'drive' | 'import';
16
+ filename: string;
17
+ mimeType: string;
18
+ size: number;
19
+ location: string;
20
+ }
21
+ export interface Scratchpad {
22
+ id: string;
23
+ lines: string[];
24
+ format: ScratchpadFormat;
25
+ attachments: Map<string, AttachmentRef>;
26
+ binding?: LiveBinding;
27
+ label?: string;
28
+ lastTouchedEpoch: number;
29
+ createdAt: Date;
30
+ }
31
+ export interface MutationResult {
32
+ message: string;
33
+ context: string;
34
+ validation: string;
35
+ }
36
+ export interface ScratchpadSummary {
37
+ id: string;
38
+ format: ScratchpadFormat;
39
+ label?: string;
40
+ lineCount: number;
41
+ attachmentCount: number;
42
+ bound: boolean;
43
+ validation: string;
44
+ lastTouchedEpoch: number;
45
+ }
46
+ export declare class ScratchpadManager {
47
+ private scratchpads;
48
+ /**
49
+ * Create a new scratchpad, optionally pre-filled with content.
50
+ */
51
+ create(opts?: {
52
+ label?: string;
53
+ content?: string;
54
+ format?: ScratchpadFormat;
55
+ }): string;
56
+ /**
57
+ * Get a scratchpad by ID. Returns null if not found or GC'd.
58
+ */
59
+ get(id: string): Scratchpad | null;
60
+ /**
61
+ * Touch a scratchpad — resets its epoch to keep it alive.
62
+ */
63
+ private touch;
64
+ /**
65
+ * View buffer content with line numbers and validation status.
66
+ */
67
+ view(id: string, startLine?: number, endLine?: number): string | null;
68
+ /**
69
+ * Insert lines after a given line number. afterLine=0 prepends.
70
+ */
71
+ insertLines(id: string, afterLine: number, content: string): MutationResult | null;
72
+ /**
73
+ * Append lines at the end of the buffer.
74
+ */
75
+ appendLines(id: string, content: string): MutationResult | null;
76
+ /**
77
+ * Replace a range of lines with new content.
78
+ */
79
+ replaceLines(id: string, startLine: number, endLine: number, content: string): MutationResult | null;
80
+ /**
81
+ * Remove line(s) from the buffer.
82
+ */
83
+ removeLines(id: string, startLine: number, endLine?: number): MutationResult | null;
84
+ /**
85
+ * Copy lines from another scratchpad into this one.
86
+ * Source is not modified.
87
+ */
88
+ copyLines(targetId: string, sourceId: string, startLine: number, endLine: number, afterLine: number): MutationResult | null;
89
+ /**
90
+ * Attach a file reference and insert a marker line.
91
+ * Returns the assigned refId (e.g., "att-1").
92
+ */
93
+ attach(id: string, ref: Omit<AttachmentRef, 'refId'>, afterLine?: number): {
94
+ refId: string;
95
+ message: string;
96
+ } | null;
97
+ /**
98
+ * Remove an attachment from the side-table. Marker line is left for the agent.
99
+ */
100
+ detach(id: string, refId: string): string | null;
101
+ /** Get all attachments for a scratchpad. */
102
+ getAttachments(id: string): Map<string, AttachmentRef> | null;
103
+ /** Set a live binding on a scratchpad (used by import adapters). */
104
+ setBinding(id: string, binding: LiveBinding): boolean;
105
+ /** Get the live binding, if any. */
106
+ getBinding(id: string): LiveBinding | undefined;
107
+ /**
108
+ * Get a value at a JSON path. Only valid for json-format scratchpads.
109
+ */
110
+ jsonGet(id: string, path: string): {
111
+ value: unknown;
112
+ lineSpan: string;
113
+ } | {
114
+ error: string;
115
+ } | null;
116
+ /**
117
+ * Set a value at a JSON path. Re-serializes the buffer.
118
+ */
119
+ jsonSet(id: string, path: string, value: unknown): MutationResult | null;
120
+ /**
121
+ * Delete a key or array element at a JSON path.
122
+ */
123
+ jsonDelete(id: string, path: string): MutationResult | null;
124
+ /**
125
+ * Insert a value into an array at a JSON path.
126
+ */
127
+ jsonInsert(id: string, path: string, value: unknown): MutationResult | null;
128
+ /** Get full buffer content as a single string. */
129
+ getContent(id: string): string | null;
130
+ /** Append raw lines to a scratchpad (used by import adapters). */
131
+ appendRawLines(id: string, lines: string[]): boolean;
132
+ /** Set the format of a scratchpad (used by import adapters). */
133
+ setFormat(id: string, format: ScratchpadFormat): boolean;
134
+ /** Discard and invalidate a scratchpad. */
135
+ discard(id: string): boolean;
136
+ /** List all active scratchpads. */
137
+ list(): ScratchpadSummary[];
138
+ private isExpired;
139
+ private gc;
140
+ }
@@ -0,0 +1,561 @@
1
+ /**
2
+ * ScratchpadManager — line-addressed content authoring buffer.
3
+ * See ADR-301: Scratchpad Buffer — Service-Agnostic Content Authoring.
4
+ */
5
+ import { randomUUID } from 'node:crypto';
6
+ import { getEpoch } from '../handler.js';
7
+ import { validate } from './validate.js';
8
+ import { getByPath, setByPath, deleteByPath } from './json-path.js';
9
+ // ── ScratchpadManager ──────────────────────────────────────
10
+ const SCRATCHPAD_MAX_AGE_EPOCHS = 100;
11
+ export class ScratchpadManager {
12
+ scratchpads = new Map();
13
+ /**
14
+ * Create a new scratchpad, optionally pre-filled with content.
15
+ */
16
+ create(opts) {
17
+ this.gc();
18
+ const id = `sp-${randomUUID().slice(0, 12)}`;
19
+ const lines = opts?.content ? normalizeAndSplit(opts.content) : [];
20
+ this.scratchpads.set(id, {
21
+ id,
22
+ lines,
23
+ format: opts?.format ?? 'text',
24
+ attachments: new Map(),
25
+ label: opts?.label,
26
+ lastTouchedEpoch: getEpoch(),
27
+ createdAt: new Date(),
28
+ });
29
+ return id;
30
+ }
31
+ /**
32
+ * Get a scratchpad by ID. Returns null if not found or GC'd.
33
+ */
34
+ get(id) {
35
+ const sp = this.scratchpads.get(id);
36
+ if (!sp)
37
+ return null;
38
+ if (this.isExpired(sp)) {
39
+ this.scratchpads.delete(id);
40
+ return null;
41
+ }
42
+ return sp;
43
+ }
44
+ /**
45
+ * Touch a scratchpad — resets its epoch to keep it alive.
46
+ */
47
+ touch(sp) {
48
+ sp.lastTouchedEpoch = getEpoch();
49
+ }
50
+ /**
51
+ * View buffer content with line numbers and validation status.
52
+ */
53
+ view(id, startLine, endLine) {
54
+ const sp = this.get(id);
55
+ if (!sp)
56
+ return null;
57
+ this.touch(sp);
58
+ const start = startLine ? Math.max(1, startLine) : 1;
59
+ const end = endLine ? Math.min(endLine, sp.lines.length) : sp.lines.length;
60
+ const numbered = formatNumberedLines(sp.lines, start, end);
61
+ const validation = validate(sp.lines, sp.format);
62
+ const label = sp.label ? ` "${sp.label}"` : '';
63
+ const attInfo = sp.attachments.size > 0 ? ` | ${sp.attachments.size} attachment(s)` : '';
64
+ const bindInfo = sp.binding ? ` | bound: ${sp.binding.service}/${sp.binding.resourceId}` : '';
65
+ const header = `Scratchpad: ${sp.id}${label} | ${sp.format} | ${sp.lines.length} lines${attInfo}${bindInfo}`;
66
+ return `${header}\n${numbered}\n${validation}`;
67
+ }
68
+ /**
69
+ * Insert lines after a given line number. afterLine=0 prepends.
70
+ */
71
+ insertLines(id, afterLine, content) {
72
+ const sp = this.get(id);
73
+ if (!sp)
74
+ return null;
75
+ this.touch(sp);
76
+ if (afterLine < 0 || afterLine > sp.lines.length) {
77
+ return {
78
+ message: `Error: afterLine ${afterLine} out of range (0-${sp.lines.length}).`,
79
+ context: '',
80
+ validation: validate(sp.lines, sp.format),
81
+ };
82
+ }
83
+ const newLines = normalizeAndSplit(content);
84
+ sp.lines.splice(afterLine, 0, ...newLines);
85
+ const affectedStart = afterLine + 1;
86
+ const affectedEnd = afterLine + newLines.length;
87
+ return {
88
+ message: `Inserted ${newLines.length} line(s) after line ${afterLine}. Buffer: ${sp.lines.length} lines.`,
89
+ context: formatContext(sp.lines, affectedStart, affectedEnd),
90
+ validation: validate(sp.lines, sp.format),
91
+ };
92
+ }
93
+ /**
94
+ * Append lines at the end of the buffer.
95
+ */
96
+ appendLines(id, content) {
97
+ const sp = this.get(id);
98
+ if (!sp)
99
+ return null;
100
+ this.touch(sp);
101
+ const newLines = normalizeAndSplit(content);
102
+ const affectedStart = sp.lines.length + 1;
103
+ sp.lines.push(...newLines);
104
+ const affectedEnd = sp.lines.length;
105
+ return {
106
+ message: `Appended ${newLines.length} line(s). Buffer: ${sp.lines.length} lines.`,
107
+ context: formatContext(sp.lines, affectedStart, affectedEnd),
108
+ validation: validate(sp.lines, sp.format),
109
+ };
110
+ }
111
+ /**
112
+ * Replace a range of lines with new content.
113
+ */
114
+ replaceLines(id, startLine, endLine, content) {
115
+ const sp = this.get(id);
116
+ if (!sp)
117
+ return null;
118
+ this.touch(sp);
119
+ if (startLine < 1 || startLine > sp.lines.length) {
120
+ return {
121
+ message: `Error: startLine ${startLine} out of range (1-${sp.lines.length}).`,
122
+ context: '',
123
+ validation: validate(sp.lines, sp.format),
124
+ };
125
+ }
126
+ if (endLine < startLine || endLine > sp.lines.length) {
127
+ return {
128
+ message: `Error: endLine ${endLine} out of range (${startLine}-${sp.lines.length}).`,
129
+ context: '',
130
+ validation: validate(sp.lines, sp.format),
131
+ };
132
+ }
133
+ const newLines = normalizeAndSplit(content);
134
+ sp.lines.splice(startLine - 1, endLine - startLine + 1, ...newLines);
135
+ const affectedEnd = startLine + newLines.length - 1;
136
+ return {
137
+ message: `Replaced lines ${startLine}-${endLine}. Buffer: ${sp.lines.length} lines.`,
138
+ context: formatContext(sp.lines, startLine, affectedEnd),
139
+ validation: validate(sp.lines, sp.format),
140
+ };
141
+ }
142
+ /**
143
+ * Remove line(s) from the buffer.
144
+ */
145
+ removeLines(id, startLine, endLine) {
146
+ const sp = this.get(id);
147
+ if (!sp)
148
+ return null;
149
+ this.touch(sp);
150
+ const end = endLine ?? startLine;
151
+ if (startLine < 1 || startLine > sp.lines.length) {
152
+ return {
153
+ message: `Error: startLine ${startLine} out of range (1-${sp.lines.length}).`,
154
+ context: '',
155
+ validation: validate(sp.lines, sp.format),
156
+ };
157
+ }
158
+ if (end < startLine || end > sp.lines.length) {
159
+ return {
160
+ message: `Error: endLine ${end} out of range (${startLine}-${sp.lines.length}).`,
161
+ context: '',
162
+ validation: validate(sp.lines, sp.format),
163
+ };
164
+ }
165
+ sp.lines.splice(startLine - 1, end - startLine + 1);
166
+ const joinLine = Math.min(startLine, sp.lines.length);
167
+ return {
168
+ message: `Removed ${end - startLine + 1} line(s). Buffer: ${sp.lines.length} lines.`,
169
+ context: formatRemoveContext(sp.lines, startLine, joinLine),
170
+ validation: validate(sp.lines, sp.format),
171
+ };
172
+ }
173
+ /**
174
+ * Copy lines from another scratchpad into this one.
175
+ * Source is not modified.
176
+ */
177
+ copyLines(targetId, sourceId, startLine, endLine, afterLine) {
178
+ const target = this.get(targetId);
179
+ const source = this.get(sourceId);
180
+ if (!target)
181
+ return null;
182
+ if (!source) {
183
+ return {
184
+ message: `Error: source scratchpad ${sourceId} not found.`,
185
+ context: '',
186
+ validation: validate(target.lines, target.format),
187
+ };
188
+ }
189
+ if (startLine < 1 || startLine > source.lines.length) {
190
+ return {
191
+ message: `Error: source startLine ${startLine} out of range (1-${source.lines.length}).`,
192
+ context: '',
193
+ validation: validate(target.lines, target.format),
194
+ };
195
+ }
196
+ if (endLine < startLine || endLine > source.lines.length) {
197
+ return {
198
+ message: `Error: source endLine ${endLine} out of range (${startLine}-${source.lines.length}).`,
199
+ context: '',
200
+ validation: validate(target.lines, target.format),
201
+ };
202
+ }
203
+ if (afterLine < 0 || afterLine > target.lines.length) {
204
+ return {
205
+ message: `Error: afterLine ${afterLine} out of range (0-${target.lines.length}).`,
206
+ context: '',
207
+ validation: validate(target.lines, target.format),
208
+ };
209
+ }
210
+ this.touch(target);
211
+ this.touch(source);
212
+ const copied = source.lines.slice(startLine - 1, endLine);
213
+ target.lines.splice(afterLine, 0, ...copied);
214
+ const affectedStart = afterLine + 1;
215
+ const affectedEnd = afterLine + copied.length;
216
+ return {
217
+ message: `Copied ${copied.length} line(s) from ${sourceId}. Buffer: ${target.lines.length} lines.`,
218
+ context: formatContext(target.lines, affectedStart, affectedEnd),
219
+ validation: validate(target.lines, target.format),
220
+ };
221
+ }
222
+ // ── Attachments ─────────────────────────────────────────
223
+ /**
224
+ * Attach a file reference and insert a marker line.
225
+ * Returns the assigned refId (e.g., "att-1").
226
+ */
227
+ attach(id, ref, afterLine) {
228
+ const sp = this.get(id);
229
+ if (!sp)
230
+ return null;
231
+ this.touch(sp);
232
+ const refId = `att-${sp.attachments.size + 1}`;
233
+ sp.attachments.set(refId, { ...ref, refId });
234
+ const marker = `![${ref.filename}](att:${refId} "${ref.filename}, ${formatSize(ref.size)}, from ${ref.source}")`;
235
+ const insertAt = afterLine ?? sp.lines.length;
236
+ sp.lines.splice(insertAt, 0, marker);
237
+ return { refId, message: `Attached ${ref.filename} as ${refId}. Buffer: ${sp.lines.length} lines.` };
238
+ }
239
+ /**
240
+ * Remove an attachment from the side-table. Marker line is left for the agent.
241
+ */
242
+ detach(id, refId) {
243
+ const sp = this.get(id);
244
+ if (!sp)
245
+ return null;
246
+ this.touch(sp);
247
+ if (!sp.attachments.has(refId)) {
248
+ return `Error: attachment ${refId} not found.`;
249
+ }
250
+ const ref = sp.attachments.get(refId);
251
+ sp.attachments.delete(refId);
252
+ return `Detached ${ref.filename} (${refId}). Marker line remains in buffer — remove it with remove_lines if needed.`;
253
+ }
254
+ /** Get all attachments for a scratchpad. */
255
+ getAttachments(id) {
256
+ const sp = this.get(id);
257
+ if (!sp)
258
+ return null;
259
+ return sp.attachments;
260
+ }
261
+ // ── Live binding ───────────────────────────────────────
262
+ /** Set a live binding on a scratchpad (used by import adapters). */
263
+ setBinding(id, binding) {
264
+ const sp = this.get(id);
265
+ if (!sp)
266
+ return false;
267
+ sp.binding = binding;
268
+ return true;
269
+ }
270
+ /** Get the live binding, if any. */
271
+ getBinding(id) {
272
+ const sp = this.get(id);
273
+ return sp?.binding;
274
+ }
275
+ // ── JSON path operations ───────────────────────────────
276
+ /**
277
+ * Get a value at a JSON path. Only valid for json-format scratchpads.
278
+ */
279
+ jsonGet(id, path) {
280
+ const sp = this.get(id);
281
+ if (!sp)
282
+ return null;
283
+ if (sp.format !== 'json')
284
+ return { error: 'json_get requires format: json' };
285
+ this.touch(sp);
286
+ const text = sp.lines.join('\n');
287
+ let obj;
288
+ try {
289
+ obj = JSON.parse(text);
290
+ }
291
+ catch {
292
+ return { error: 'Buffer is not valid JSON. Fix syntax errors first.' };
293
+ }
294
+ try {
295
+ const value = getByPath(obj, path);
296
+ const serialized = JSON.stringify(value, null, 2);
297
+ const lineCount = serialized.split('\n').length;
298
+ return { value, lineSpan: `${lineCount} line(s)` };
299
+ }
300
+ catch (err) {
301
+ return { error: err instanceof Error ? err.message : String(err) };
302
+ }
303
+ }
304
+ /**
305
+ * Set a value at a JSON path. Re-serializes the buffer.
306
+ */
307
+ jsonSet(id, path, value) {
308
+ const sp = this.get(id);
309
+ if (!sp)
310
+ return null;
311
+ if (sp.format !== 'json') {
312
+ return { message: 'Error: json_set requires format: json', context: '', validation: '' };
313
+ }
314
+ this.touch(sp);
315
+ const text = sp.lines.join('\n');
316
+ let obj;
317
+ try {
318
+ obj = JSON.parse(text);
319
+ }
320
+ catch {
321
+ return {
322
+ message: 'Error: buffer is not valid JSON. Fix syntax errors first.',
323
+ context: '',
324
+ validation: validate(sp.lines, sp.format),
325
+ };
326
+ }
327
+ try {
328
+ setByPath(obj, path, value);
329
+ }
330
+ catch (err) {
331
+ return {
332
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
333
+ context: '',
334
+ validation: validate(sp.lines, sp.format),
335
+ };
336
+ }
337
+ sp.lines = JSON.stringify(obj, null, 2).split('\n');
338
+ return {
339
+ message: `Set ${path}. Buffer: ${sp.lines.length} lines.`,
340
+ context: '',
341
+ validation: validate(sp.lines, sp.format),
342
+ };
343
+ }
344
+ /**
345
+ * Delete a key or array element at a JSON path.
346
+ */
347
+ jsonDelete(id, path) {
348
+ const sp = this.get(id);
349
+ if (!sp)
350
+ return null;
351
+ if (sp.format !== 'json') {
352
+ return { message: 'Error: json_delete requires format: json', context: '', validation: '' };
353
+ }
354
+ this.touch(sp);
355
+ const text = sp.lines.join('\n');
356
+ let obj;
357
+ try {
358
+ obj = JSON.parse(text);
359
+ }
360
+ catch {
361
+ return {
362
+ message: 'Error: buffer is not valid JSON. Fix syntax errors first.',
363
+ context: '',
364
+ validation: validate(sp.lines, sp.format),
365
+ };
366
+ }
367
+ try {
368
+ deleteByPath(obj, path);
369
+ }
370
+ catch (err) {
371
+ return {
372
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
373
+ context: '',
374
+ validation: validate(sp.lines, sp.format),
375
+ };
376
+ }
377
+ sp.lines = JSON.stringify(obj, null, 2).split('\n');
378
+ return {
379
+ message: `Deleted ${path}. Buffer: ${sp.lines.length} lines.`,
380
+ context: '',
381
+ validation: validate(sp.lines, sp.format),
382
+ };
383
+ }
384
+ /**
385
+ * Insert a value into an array at a JSON path.
386
+ */
387
+ jsonInsert(id, path, value) {
388
+ const sp = this.get(id);
389
+ if (!sp)
390
+ return null;
391
+ if (sp.format !== 'json') {
392
+ return { message: 'Error: json_insert requires format: json', context: '', validation: '' };
393
+ }
394
+ this.touch(sp);
395
+ const text = sp.lines.join('\n');
396
+ let obj;
397
+ try {
398
+ obj = JSON.parse(text);
399
+ }
400
+ catch {
401
+ return {
402
+ message: 'Error: buffer is not valid JSON. Fix syntax errors first.',
403
+ context: '',
404
+ validation: validate(sp.lines, sp.format),
405
+ };
406
+ }
407
+ try {
408
+ const target = getByPath(obj, path);
409
+ if (!Array.isArray(target)) {
410
+ return {
411
+ message: `Error: ${path} is not an array.`,
412
+ context: '',
413
+ validation: validate(sp.lines, sp.format),
414
+ };
415
+ }
416
+ target.push(value);
417
+ }
418
+ catch (err) {
419
+ return {
420
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`,
421
+ context: '',
422
+ validation: validate(sp.lines, sp.format),
423
+ };
424
+ }
425
+ sp.lines = JSON.stringify(obj, null, 2).split('\n');
426
+ return {
427
+ message: `Inserted into ${path}. Buffer: ${sp.lines.length} lines.`,
428
+ context: '',
429
+ validation: validate(sp.lines, sp.format),
430
+ };
431
+ }
432
+ // ── Buffer access ──────────────────────────────────────
433
+ /** Get full buffer content as a single string. */
434
+ getContent(id) {
435
+ const sp = this.get(id);
436
+ if (!sp)
437
+ return null;
438
+ return sp.lines.join('\n');
439
+ }
440
+ /** Append raw lines to a scratchpad (used by import adapters). */
441
+ appendRawLines(id, lines) {
442
+ const sp = this.get(id);
443
+ if (!sp)
444
+ return false;
445
+ this.touch(sp);
446
+ sp.lines.push(...lines);
447
+ return true;
448
+ }
449
+ /** Set the format of a scratchpad (used by import adapters). */
450
+ setFormat(id, format) {
451
+ const sp = this.get(id);
452
+ if (!sp)
453
+ return false;
454
+ sp.format = format;
455
+ return true;
456
+ }
457
+ /** Discard and invalidate a scratchpad. */
458
+ discard(id) {
459
+ return this.scratchpads.delete(id);
460
+ }
461
+ /** List all active scratchpads. */
462
+ list() {
463
+ this.gc();
464
+ const result = [];
465
+ for (const sp of this.scratchpads.values()) {
466
+ result.push({
467
+ id: sp.id,
468
+ format: sp.format,
469
+ label: sp.label,
470
+ lineCount: sp.lines.length,
471
+ attachmentCount: sp.attachments.size,
472
+ bound: !!sp.binding,
473
+ validation: validate(sp.lines, sp.format),
474
+ lastTouchedEpoch: sp.lastTouchedEpoch,
475
+ });
476
+ }
477
+ return result;
478
+ }
479
+ // ── Garbage collection ─────────────────────────────────
480
+ isExpired(sp) {
481
+ return getEpoch() - sp.lastTouchedEpoch > SCRATCHPAD_MAX_AGE_EPOCHS;
482
+ }
483
+ gc() {
484
+ const expired = [];
485
+ for (const sp of this.scratchpads.values()) {
486
+ if (this.isExpired(sp))
487
+ expired.push(sp.id);
488
+ }
489
+ for (const id of expired)
490
+ this.scratchpads.delete(id);
491
+ }
492
+ }
493
+ // ── Helpers ────────────────────────────────────────────────
494
+ /** Format bytes as human-readable size. */
495
+ function formatSize(bytes) {
496
+ if (bytes < 1024)
497
+ return `${bytes} B`;
498
+ if (bytes < 1024 * 1024)
499
+ return `${(bytes / 1024).toFixed(1)} KB`;
500
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
501
+ }
502
+ /** Normalize CRLF/CR to LF and split into lines. */
503
+ function normalizeAndSplit(content) {
504
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
505
+ }
506
+ /** Format lines with line numbers for display. */
507
+ function formatNumberedLines(lines, start, end) {
508
+ if (lines.length === 0)
509
+ return ' (empty buffer)';
510
+ const width = String(end).length;
511
+ const result = [];
512
+ for (let i = start; i <= end; i++) {
513
+ result.push(`${String(i).padStart(width)} | ${lines[i - 1]}`);
514
+ }
515
+ return result.join('\n');
516
+ }
517
+ /** Format a context marker showing the edit site with surrounding lines. */
518
+ function formatContext(lines, affectedStart, affectedEnd) {
519
+ if (lines.length === 0)
520
+ return '';
521
+ const width = String(Math.min(affectedEnd + 1, lines.length)).length;
522
+ const parts = [];
523
+ // One line before
524
+ if (affectedStart > 1) {
525
+ const ln = affectedStart - 1;
526
+ parts.push(`${String(ln).padStart(width)} | ${lines[ln - 1]}`);
527
+ }
528
+ // First affected line
529
+ parts.push(`${String(affectedStart).padStart(width)} | ${lines[affectedStart - 1]}`);
530
+ // Elide middle if > 2 affected lines
531
+ if (affectedEnd - affectedStart > 1) {
532
+ parts.push(`${' '.repeat(width)} | ...`);
533
+ }
534
+ // Last affected line (if different from first)
535
+ if (affectedEnd > affectedStart) {
536
+ parts.push(`${String(affectedEnd).padStart(width)} | ${lines[affectedEnd - 1]}`);
537
+ }
538
+ // One line after
539
+ if (affectedEnd < lines.length) {
540
+ const ln = affectedEnd + 1;
541
+ parts.push(`${String(ln).padStart(width)} | ${lines[ln - 1]}`);
542
+ }
543
+ return parts.join('\n');
544
+ }
545
+ /** Format context for a remove operation showing the join point. */
546
+ function formatRemoveContext(lines, removedAt, joinLine) {
547
+ if (lines.length === 0)
548
+ return ' (buffer now empty)';
549
+ const width = String(Math.min(joinLine + 1, lines.length)).length;
550
+ const parts = [];
551
+ if (removedAt > 1 && removedAt - 1 <= lines.length) {
552
+ const ln = removedAt - 1;
553
+ parts.push(`${String(ln).padStart(width)} | ${lines[ln - 1]}`);
554
+ }
555
+ if (joinLine >= 1 && joinLine <= lines.length) {
556
+ parts.push(`${String(joinLine).padStart(width)} | ${lines[joinLine - 1]}`);
557
+ }
558
+ return parts.join('\n');
559
+ }
560
+ // Validation: ./validate.ts | JSON path: ./json-path.ts
561
+ //# sourceMappingURL=manager.js.map