@joehe71/office-tools 0.1.0 → 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.
Files changed (3) hide show
  1. package/dist/index.js +872 -0
  2. package/icon.svg +7 -0
  3. package/package.json +2 -1
package/dist/index.js ADDED
@@ -0,0 +1,872 @@
1
+ import { exec, execFile, execSync, spawn } from 'node:child_process';
2
+ import { chmodSync, existsSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
+ // ── Module-level state ────────────────────────────────────────────────────
8
+ let _extensionPath = '';
9
+ /** Track running watch processes: resolved file path → { process, url, lastActivity } */
10
+ const watchProcesses = new Map();
11
+ /** Auto-stop watch after this many milliseconds of inactivity (5 minutes). */
12
+ const WATCH_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
13
+ /** Interval to check for idle watch processes (every 60 seconds). */
14
+ let watchIdleChecker = null;
15
+ function touchWatchActivity(filePath) {
16
+ const entry = watchProcesses.get(filePath);
17
+ if (entry)
18
+ entry.lastActivity = Date.now();
19
+ }
20
+ function startWatchIdleChecker() {
21
+ if (watchIdleChecker)
22
+ return;
23
+ watchIdleChecker = setInterval(() => {
24
+ const now = Date.now();
25
+ for (const [file, entry] of watchProcesses) {
26
+ if (now - entry.lastActivity > WATCH_IDLE_TIMEOUT_MS) {
27
+ try {
28
+ entry.proc.kill();
29
+ }
30
+ catch { /* ignore */ }
31
+ try {
32
+ execSync(`${getOfficeCLIBinary()} unwatch "${file}"`, { timeout: 5000, stdio: 'pipe' });
33
+ }
34
+ catch { /* ignore */ }
35
+ watchProcesses.delete(file);
36
+ }
37
+ }
38
+ if (watchProcesses.size === 0 && watchIdleChecker) {
39
+ clearInterval(watchIdleChecker);
40
+ watchIdleChecker = null;
41
+ }
42
+ }, 60_000);
43
+ }
44
+ function getBinPath() {
45
+ const suffix = process.platform === 'win32' ? 'officecli.exe' : 'officecli';
46
+ return join(_extensionPath, 'bin', suffix);
47
+ }
48
+ function getBinDir() {
49
+ return join(_extensionPath, 'bin');
50
+ }
51
+ function getDataDir() {
52
+ return join(_extensionPath, 'data');
53
+ }
54
+ /** Resolve file path: if relative, place it in the extension's data/ directory. */
55
+ function resolveFilePath(filePath) {
56
+ // Already absolute — use as-is
57
+ if (filePath.startsWith('/'))
58
+ return filePath;
59
+ // Home dir shortcut
60
+ if (filePath.startsWith('~'))
61
+ return filePath.replace(/^~/, process.env.HOME || '/Users/jinhan');
62
+ // Relative — place in data/ directory
63
+ const dataDir = getDataDir();
64
+ mkdirSync(dataDir, { recursive: true });
65
+ return join(dataDir, filePath);
66
+ }
67
+ // ── Helpers ────────────────────────────────────────────────────────────────
68
+ function resolvePath(filePath, cwd) {
69
+ if (filePath.startsWith('/'))
70
+ return filePath;
71
+ if (filePath.startsWith('~'))
72
+ return filePath.replace(/^~/, process.env.HOME || '/Users/jinhan');
73
+ return cwd ? `${cwd}/${filePath}` : filePath;
74
+ }
75
+ function getOfficeCLIBinary() {
76
+ const local = getBinPath();
77
+ if (existsSync(local))
78
+ return local;
79
+ // Fallback to PATH
80
+ return 'officecli';
81
+ }
82
+ async function runOfficeCLI(args, cwd) {
83
+ const binary = getOfficeCLIBinary();
84
+ try {
85
+ // Use execFile to avoid shell interpretation of special characters
86
+ const { stdout, stderr } = await execFileAsync(binary, args, {
87
+ cwd,
88
+ timeout: 30_000,
89
+ maxBuffer: 10 * 1024 * 1024,
90
+ });
91
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
92
+ }
93
+ catch (err) {
94
+ const e = err;
95
+ const stderr = e.stderr?.trim() || '';
96
+ const stdout = e.stdout?.trim() || '';
97
+ const message = e.message || String(err);
98
+ throw new Error(stderr || stdout || message);
99
+ }
100
+ }
101
+ function isOfficeCLIInstalled() {
102
+ try {
103
+ execSync(`${getOfficeCLIBinary()} --version`, { timeout: 5000 });
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ function getOfficeCLIVersion() {
111
+ try {
112
+ return execSync(`${getOfficeCLIBinary()} --version`, { timeout: 5000, encoding: 'utf-8' }).trim();
113
+ }
114
+ catch {
115
+ return 'unknown';
116
+ }
117
+ }
118
+ function ensureFileExists(filePath) {
119
+ if (!existsSync(filePath)) {
120
+ throw new Error(`File not found: ${filePath}. Make sure the path is correct and the file exists.`);
121
+ }
122
+ }
123
+ /** Open a URL in the user's default browser. */
124
+ function openBrowser(url) {
125
+ if (process.platform === 'darwin') {
126
+ execSync(`open "${url}"`, { timeout: 5000, stdio: 'pipe' });
127
+ }
128
+ else if (process.platform === 'win32') {
129
+ execSync(`start "" "${url}"`, { timeout: 5000, stdio: 'pipe' });
130
+ }
131
+ else {
132
+ execSync(`xdg-open "${url}"`, { timeout: 5000, stdio: 'pipe' });
133
+ }
134
+ }
135
+ /** Start a live preview for a file. Returns the URL or throws on error. */
136
+ async function startPreviewForFile(filePath, port) {
137
+ const resolved = resolveFilePath(filePath);
138
+ ensureFileExists(resolved);
139
+ // Already running?
140
+ if (watchProcesses.has(resolved)) {
141
+ return watchProcesses.get(resolved).url;
142
+ }
143
+ const binary = getOfficeCLIBinary();
144
+ const args = ['watch', resolved];
145
+ if (port)
146
+ args.push('--port', String(port));
147
+ const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
148
+ watchProcesses.set(resolved, { proc: child, url: '(waiting for server to start...)', lastActivity: Date.now() });
149
+ const url = await new Promise((resolve, reject) => {
150
+ const timeout = setTimeout(() => {
151
+ reject(new Error('Timed out waiting for preview server to start (15s).'));
152
+ }, 15_000);
153
+ let buffer = '';
154
+ const onData = (chunk) => {
155
+ buffer += chunk.toString();
156
+ const match = buffer.match(/https?:\/\/localhost[:/]\d+/);
157
+ if (match) {
158
+ clearTimeout(timeout);
159
+ child.stdout?.off('data', onData);
160
+ resolve(match[0]);
161
+ }
162
+ };
163
+ child.stdout?.on('data', onData);
164
+ child.on('error', (err) => {
165
+ clearTimeout(timeout);
166
+ watchProcesses.delete(resolved);
167
+ reject(err);
168
+ });
169
+ child.on('exit', (code) => {
170
+ clearTimeout(timeout);
171
+ watchProcesses.delete(resolved);
172
+ if (code && code !== 0) {
173
+ reject(new Error(`officecli watch exited with code ${code}`));
174
+ }
175
+ });
176
+ });
177
+ const tracked = watchProcesses.get(resolved);
178
+ if (tracked)
179
+ tracked.url = url;
180
+ // Start idle checker to auto-stop watch after inactivity
181
+ startWatchIdleChecker();
182
+ try {
183
+ openBrowser(url);
184
+ }
185
+ catch { /* best-effort */ }
186
+ return url;
187
+ }
188
+ /** Map Node.js platform to OfficeCLI platform string. */
189
+ function mapPlatform() {
190
+ const map = { darwin: 'mac', linux: 'linux', win32: 'win' };
191
+ return map[process.platform] || process.platform;
192
+ }
193
+ /** Map Node.js arch to OfficeCLI arch string. */
194
+ function mapArch() {
195
+ // OfficeCLI release assets use: arm64, x64
196
+ const map = { arm64: 'arm64', x64: 'x64' };
197
+ return map[process.arch] || process.arch;
198
+ }
199
+ // ── Tool: setup_officecli ─────────────────────────────────────────────────
200
+ function registerSetupTool(ctx) {
201
+ ctx.subscriptions.push(ctx.tools.register({
202
+ name: 'setup_officecli',
203
+ title: 'Setup OfficeCLI',
204
+ description: 'Download and install OfficeCLI binary into the extension directory. Call this when OfficeCLI is not installed and you need to set it up. This is a one-time setup — once installed, all other office tools will work.',
205
+ inputSchema: { type: 'object', properties: {} },
206
+ risk: 'medium',
207
+ async execute() {
208
+ if (isOfficeCLIInstalled()) {
209
+ return {
210
+ content: [{
211
+ type: 'text',
212
+ text: [
213
+ `✅ OfficeCLI is already installed.`,
214
+ `Version: ${getOfficeCLIVersion()}`,
215
+ `Location: \`${getOfficeCLIBinary()}\``,
216
+ '',
217
+ 'No setup needed — all office tools are ready to use.',
218
+ ].join('\n'),
219
+ }],
220
+ };
221
+ }
222
+ const binDir = getBinDir();
223
+ const binPath = getBinPath();
224
+ try {
225
+ mkdirSync(binDir, { recursive: true });
226
+ const os = mapPlatform();
227
+ const arch = mapArch();
228
+ const ext = process.platform === 'win32' ? '.exe' : '';
229
+ const fileName = `officecli-${os}-${arch}${ext}`;
230
+ const url = `https://github.com/iOfficeAI/OfficeCLI/releases/latest/download/${fileName}`;
231
+ const tmpPath = `${binDir}/${fileName}`;
232
+ // Download the binary directly (not an archive)
233
+ execSync(`curl -fSL -o "${tmpPath}" "${url}"`, { timeout: 120_000, stdio: 'pipe', shell: 'bash' });
234
+ // Verify download succeeded
235
+ if (!existsSync(tmpPath)) {
236
+ throw new Error(`Download failed: ${tmpPath} not found`);
237
+ }
238
+ // Move to expected location if different
239
+ if (tmpPath !== binPath) {
240
+ execSync(`mv "${tmpPath}" "${binPath}"`, { stdio: 'pipe' });
241
+ }
242
+ // Make executable
243
+ if (existsSync(binPath)) {
244
+ chmodSync(binPath, 0o755);
245
+ }
246
+ if (!existsSync(binPath)) {
247
+ throw new Error(`Binary not found at ${binPath} after download.`);
248
+ }
249
+ const version = getOfficeCLIVersion();
250
+ return {
251
+ content: [{
252
+ type: 'text',
253
+ text: [
254
+ `✅ OfficeCLI installed successfully!`,
255
+ `Version: ${version}`,
256
+ `Location: \`${binPath}\``,
257
+ '',
258
+ 'All office tools are now ready to use.',
259
+ ].join('\n'),
260
+ }],
261
+ };
262
+ }
263
+ catch (err) {
264
+ const message = err instanceof Error ? err.message : String(err);
265
+ return {
266
+ content: [{
267
+ type: 'text',
268
+ text: [
269
+ `❌ Failed to install OfficeCLI: ${message}`,
270
+ '',
271
+ '**Manual install alternatives:**',
272
+ '```bash',
273
+ 'curl -fsSL https://d.officecli.ai/install.sh | bash',
274
+ '```',
275
+ 'or `brew install officecli` or `npm install -g @officecli/officecli`',
276
+ '',
277
+ 'After installing, restart Finch or re-enable the extension.',
278
+ ].join('\n'),
279
+ }],
280
+ isError: true,
281
+ };
282
+ }
283
+ },
284
+ }));
285
+ }
286
+ // ── Tool: check_officecli ─────────────────────────────────────────────────
287
+ function registerCheckTool(ctx) {
288
+ ctx.subscriptions.push(ctx.tools.register({
289
+ name: 'check_officecli',
290
+ title: 'Check OfficeCLI',
291
+ description: 'Check whether OfficeCLI is installed and show available tools. Call this first before using any other office tools.',
292
+ inputSchema: { type: 'object', properties: {} },
293
+ risk: 'low',
294
+ async execute() {
295
+ const binPath = getBinPath();
296
+ if (!isOfficeCLIInstalled()) {
297
+ const localExists = existsSync(binPath);
298
+ return {
299
+ content: [{
300
+ type: 'text',
301
+ text: [
302
+ '**OfficeCLI is not available.**',
303
+ localExists
304
+ ? `Binary exists at \`${binPath}\` but may not be executable. Try calling \`setup_officecli\` to reinstall.`
305
+ : `Not found in PATH or at \`${binPath}\`.`,
306
+ '',
307
+ '**To install, call the `setup_officecli` tool.**',
308
+ '',
309
+ '**Manual install alternatives:**',
310
+ '```bash',
311
+ 'curl -fsSL https://d.officecli.ai/install.sh | bash',
312
+ '```',
313
+ 'or `brew install officecli` or `npm install -g @officecli/officecli`',
314
+ ].join('\n'),
315
+ }],
316
+ };
317
+ }
318
+ const version = getOfficeCLIVersion();
319
+ const binary = getOfficeCLIBinary();
320
+ const location = binary === 'officecli' ? 'PATH (installed via brew/npm/manual install)' : `\`${binary}\``;
321
+ return {
322
+ content: [{
323
+ type: 'text',
324
+ text: [
325
+ `✅ OfficeCLI is installed.`,
326
+ `Version: ${version}`,
327
+ `Location: ${location}`,
328
+ '',
329
+ '**Strategy — work top-down:**',
330
+ '1. `read_office_file` — inspect document structure (outline/html/text/issues/stats)',
331
+ '2. `get_office_element` — drill into specific elements with `--depth N`',
332
+ '3. `modify_office_file` — add/set/remove/move elements, find & replace',
333
+ '4. `save_office_file` — flush changes to disk when done',
334
+ '',
335
+ '**When unsure about property names, value formats, or syntax:**',
336
+ 'Run `inspect_office_file` with `action=help` and a `helpQuery` to look up the exact schema.',
337
+ 'Example: `officecli help pptx shape` lists all shape properties.',
338
+ '',
339
+ '**Available tools:**',
340
+ '- `read_office_file` — view document content',
341
+ '- `create_office_file` — create new Office files',
342
+ '- `modify_office_file` — add, set, remove, move elements, find & replace',
343
+ '- `get_office_element` — get structured JSON for any element',
344
+ '- `inspect_office_file` — validate, run help queries',
345
+ '- `preview_office_file` — live preview in browser + click-to-select elements',
346
+ '- `save_office_file` — save and close a file',
347
+ ].join('\n'),
348
+ }],
349
+ };
350
+ },
351
+ }));
352
+ }
353
+ // ── Tool: read_office_file ────────────────────────────────────────────────
354
+ function registerReadTool(ctx) {
355
+ ctx.subscriptions.push(ctx.tools.register({
356
+ name: 'read_office_file',
357
+ title: 'Read Office File',
358
+ description: 'Read content from a Word (.docx), Excel (.xlsx), or PowerPoint (.pptx) file. This is your L1 (first-layer) inspection tool.\n\n' +
359
+ '**View modes:**\n' +
360
+ '- `outline` — Document structure (headings, slides, sheets, cell values). Best for understanding the overall document.\n' +
361
+ '- `html` — Static HTML snapshot of the rendered document. Best for seeing layout and formatting.\n' +
362
+ '- `text` — Plain text extraction. Best for getting just the words.\n' +
363
+ '- `issues` — Formatting/content/structure problems. Use `--limit N` to cap results.\n' +
364
+ '- `stats` — Document statistics (pages, words, shapes, etc.).\n' +
365
+ '- `annotated` — Text with formatting annotations.\n\n' +
366
+ '**Usage flow:** Start with `outline` to understand structure, then drill deeper with `get_office_element`.\n' +
367
+ 'Use `html` mode for a visual preview of the rendered document.',
368
+ inputSchema: {
369
+ type: 'object',
370
+ properties: {
371
+ filePath: { type: 'string', description: 'Path to the .docx, .xlsx, or .pptx file. Relative or absolute.' },
372
+ viewMode: { type: 'string', enum: ['outline', 'html', 'text', 'issues', 'stats', 'annotated'], description: 'View mode (default: outline).' },
373
+ limit: { type: 'number', description: 'Max results for issues mode (e.g. 10).' },
374
+ },
375
+ required: ['filePath'],
376
+ },
377
+ risk: 'low',
378
+ async execute(input) {
379
+ const { filePath, viewMode = 'outline', limit } = input;
380
+ const resolved = resolveFilePath(filePath);
381
+ ensureFileExists(resolved);
382
+ const args = ['view', resolved, viewMode];
383
+ if (limit != null && viewMode === 'issues') {
384
+ args.push('--limit', String(limit));
385
+ }
386
+ const { stdout } = await runOfficeCLI(args);
387
+ const modeLabel = viewMode.charAt(0).toUpperCase() + viewMode.slice(1);
388
+ return {
389
+ content: [{
390
+ type: 'text',
391
+ text: [
392
+ `📄 ${filePath} — ${modeLabel}`,
393
+ '',
394
+ stdout || '(empty)',
395
+ ].join('\n'),
396
+ }],
397
+ };
398
+ },
399
+ }));
400
+ }
401
+ // ── Tool: create_office_file ──────────────────────────────────────────────
402
+ function registerCreateTool(ctx) {
403
+ ctx.subscriptions.push(ctx.tools.register({
404
+ name: 'create_office_file',
405
+ title: 'Create Office File',
406
+ description: 'Create a new Word (.docx), Excel (.xlsx), or PowerPoint (.pptx) file. File type is inferred from the extension.\n\n' +
407
+ 'After creation:\n' +
408
+ '- PPT: auto-starts live preview in browser\n' +
409
+ '- Word/Excel: asks user if they want live preview\n\n' +
410
+ 'OfficeCLI auto-starts a resident session on first access (60s idle timeout), ' +
411
+ 'so subsequent edits are fast with no file I/O overhead.',
412
+ inputSchema: {
413
+ type: 'object',
414
+ properties: {
415
+ filePath: { type: 'string', description: 'Path for the new file, e.g. "report.docx", "data.xlsx", "deck.pptx". Extension determines file type.' },
416
+ },
417
+ required: ['filePath'],
418
+ },
419
+ risk: 'medium',
420
+ async execute(input) {
421
+ const { filePath } = input;
422
+ const resolved = resolveFilePath(filePath);
423
+ if (existsSync(resolved)) {
424
+ return { content: [{ type: 'text', text: `File already exists: ${filePath}. Use modify_office_file to edit it, or choose a different path.` }] };
425
+ }
426
+ await runOfficeCLI(['create', resolved]);
427
+ try {
428
+ const url = await startPreviewForFile(filePath);
429
+ openBrowser(url);
430
+ return {
431
+ content: [{
432
+ type: 'text',
433
+ text: [
434
+ `✅ Created new file: ${filePath}`,
435
+ '',
436
+ `👁 Live preview started: ${url}`,
437
+ '',
438
+ 'The preview has been opened in your browser. Use `modify_office_file` to add content — the browser will auto-refresh.',
439
+ 'When done, call `preview_office_file(action=stop)` to shut down the server.',
440
+ ].join('\n'),
441
+ }],
442
+ };
443
+ }
444
+ catch (err) {
445
+ const message = err instanceof Error ? err.message : String(err);
446
+ return {
447
+ content: [{
448
+ type: 'text',
449
+ text: [
450
+ `✅ Created new file: ${filePath}`,
451
+ '',
452
+ `⚠️ Failed to start preview: ${message}`,
453
+ '',
454
+ 'Use `read_office_file` to view it, or `modify_office_file` to add content.',
455
+ ].join('\n'),
456
+ }],
457
+ };
458
+ }
459
+ },
460
+ }));
461
+ }
462
+ // ── Tool: modify_office_file ──────────────────────────────────────────────
463
+ function registerModifyTool(ctx) {
464
+ ctx.subscriptions.push(ctx.tools.register({
465
+ name: 'modify_office_file',
466
+ title: 'Modify Office File',
467
+ description: 'Modify elements in an Office file (Word/Excel/PowerPoint). This is your L2 (DOM-level) editing tool.\n\n' +
468
+ '**Commands:**\n' +
469
+ '- `add` — Add new elements (slides, shapes, cells, sheets, paragraphs, tables, charts, etc.)\n' +
470
+ '- `set` — Modify properties of existing elements (text, font, color, position, bold, etc.)\n' +
471
+ '- `remove` — Remove elements\n' +
472
+ '- `move` — Move an element to a different parent or position\n' +
473
+ '- `find-and-replace` — Find and replace text across the document\n\n' +
474
+ '**Element paths** (1-based, XPath-style):\n' +
475
+ '- PPT: `/slide[1]`, `/slide[1]/shape[2]`, or `/slide[1]/shape[@name=Title 1]` for stable IDs\n' +
476
+ '- Excel: `/Sheet1`, `/Sheet1/A1`, `/Sheet1/row[3]/cell[B]`\n' +
477
+ '- Word: `/body/p[3]`, `/body/table[1]/row[1]/cell[2]`\n' +
478
+ '- Root: `/` for document-level properties\n\n' +
479
+ '**Element types** (for `add`):\n' +
480
+ '- PPT: `slide`, `shape`, `table`, `chart`, `picture`, `textbox`, `connector`, `group`, `animation`, `transition`\n' +
481
+ '- Excel: `sheet`, `row`, `cell`, `table`, `chart`, `pivotTable`, `picture`, `sparkline`, `comment`\n' +
482
+ '- Word: `paragraph`, `run`, `table`, `picture`, `shape`, `textbox`, `chart`, `section`, `header`, `footer`, `comment`, `bookmark`, `hyperlink`\n\n' +
483
+ '**Props** (key=value):\n' +
484
+ '- Text: `text`, `title`, `font`, `size`, `bold=true`, `italic=true`, `color=FF0000`, `highlight=yellow`\n' +
485
+ '- Position: `x`, `y`, `w`, `h` with units like `cm`, `pt`, `px`\n' +
486
+ '- Colors: hex (`FF0000`), named (`red`), theme (`accent1`)\n' +
487
+ '- Excel cells: `value`, `formula=SUM(A1:A10)`, `bold=true`, `numberFormat=#,##0`\n' +
488
+ '- Spacing: `12pt`, `0.5cm`, `1.5x`\n' +
489
+ '- Dotted aliases: `font.color=red`, `font.bold=true`, `font.size=14pt`\n\n' +
490
+ '**Find & Replace** (set command with `find`/`replace`):\n' +
491
+ 'Set `command=set` and include `find` and `replace` in props to do find-and-replace.\n' +
492
+ 'Example: props={"find":"draft","replace":"final"} finds "draft" across the document and replaces it with "final".\n' +
493
+ 'Use `regex=true` in props for regex matching.\n\n' +
494
+ '**Clone existing elements:**\n' +
495
+ 'Set `command=add` and include `from` in props to clone an element.\n' +
496
+ 'Example: props={"from":"/slide[1]"} clones the first slide.\n\n' +
497
+ '**IMPORTANT — When unsure about property names, value formats, or command syntax:**\n' +
498
+ 'Run `inspect_office_file` with `helpQuery` set to look up the exact schema.',
499
+ inputSchema: {
500
+ type: 'object',
501
+ properties: {
502
+ filePath: { type: 'string', description: 'Path to the .docx, .xlsx, or .pptx file.' },
503
+ command: { type: 'string', enum: ['add', 'set', 'remove', 'move'], description: 'Operation: add (create new), set (modify), remove (delete), move (reposition).' },
504
+ elementPath: { type: 'string', description: 'Path to the target element. E.g. "/" (root), "/slide[1]", "/slide[1]/shape[2]", "/Sheet1/A1", "/body/p[3]". For add, this is the parent where the new element goes.' },
505
+ elementType: { type: 'string', description: 'Element type to add (required for add command). E.g. "slide", "shape", "paragraph", "run", "table", "chart", "picture", "sheet", "row", "cell", "pivotTable", "comment", "section".' },
506
+ props: {
507
+ type: 'object',
508
+ description: 'Key-value properties. Examples: {"title":"Q4 Report","text":"Hello","font":"Arial","size":24,"color":"FFFFFF","bold":true,"background":"1A1A2E","x":"2cm","y":"5cm"}. For find/replace: {"find":"draft","replace":"final"} or {"find":"\\d+%","regex":true,"replace":"[redacted]"} on the root path "/". For clone: {"from":"/slide[1]"} on add command. For Excel: {"value":123,"formula":"SUM(A1:A10)"}.',
509
+ properties: {},
510
+ additionalProperties: true,
511
+ },
512
+ targetPath: { type: 'string', description: 'For move command: destination parent path. For add with --after/--before: anchor element path. Optional.' },
513
+ },
514
+ required: ['filePath', 'command', 'elementPath'],
515
+ },
516
+ risk: 'medium',
517
+ async execute(input) {
518
+ const { filePath, command, elementPath, elementType, props, targetPath } = input;
519
+ const resolved = resolveFilePath(filePath);
520
+ if (command !== 'add') {
521
+ ensureFileExists(resolved);
522
+ }
523
+ const args = [command, resolved, elementPath];
524
+ if (elementType && command === 'add') {
525
+ args.push('--type', elementType);
526
+ }
527
+ if (targetPath) {
528
+ if (command === 'move') {
529
+ args.push('--to', targetPath);
530
+ }
531
+ else {
532
+ args.push('--after', targetPath);
533
+ }
534
+ }
535
+ if (props && typeof props === 'object' && Object.keys(props).length > 0) {
536
+ for (const [key, value] of Object.entries(props)) {
537
+ const strVal = String(value);
538
+ args.push('--prop', `${key}=${strVal}`);
539
+ }
540
+ }
541
+ const { stdout, stderr } = await runOfficeCLI(args);
542
+ // Touch watch activity if preview is running for this file
543
+ touchWatchActivity(resolved);
544
+ // If no preview running, start one
545
+ let previewInfo = '';
546
+ if (!watchProcesses.has(resolved)) {
547
+ try {
548
+ const url = await startPreviewForFile(filePath);
549
+ previewInfo = `\n\n👁 Live preview started: ${url}`;
550
+ }
551
+ catch {
552
+ // Preview failed to start — not critical, just skip
553
+ }
554
+ }
555
+ return {
556
+ content: [{
557
+ type: 'text',
558
+ text: [
559
+ `✅ ${command} on ${filePath} (path: ${elementPath})`,
560
+ stderr ? `\n${stderr}` : '',
561
+ stdout ? `\n${stdout}` : '',
562
+ previewInfo,
563
+ ].filter(Boolean).join(''),
564
+ }],
565
+ };
566
+ },
567
+ }));
568
+ }
569
+ // ── Tool: get_office_element ──────────────────────────────────────────────
570
+ function registerGetTool(ctx) {
571
+ ctx.subscriptions.push(ctx.tools.register({
572
+ name: 'get_office_element',
573
+ title: 'Get Office Element',
574
+ description: 'Get detailed structured information (JSON) about a specific element in an Office file. ' +
575
+ 'Use this to inspect element properties, cell values, formulas, styles, positions, etc. ' +
576
+ 'Use `--depth N` to expand N levels of children.\n\n' +
577
+ '**Examples:**\n' +
578
+ '- `get data.xlsx /Sheet1/B2 --json` — get a single cell\'s value and properties\n' +
579
+ '- `get deck.pptx \'/slide[1]\' --depth 1` — list all shapes on slide 1\n' +
580
+ '- `get report.docx /` — full document structure\n' +
581
+ '- `get report.docx \'/body/p[3]\' --depth 2 --json` — paragraph with children\n\n' +
582
+ '**Stable ID addressing:** Elements with stable IDs return paths like `/slide[1]/shape[@id=550950021]` ' +
583
+ 'which persist across insert/delete operations (unlike positional indices).\n\n' +
584
+ 'Use this before modifying an element to see its current state.',
585
+ inputSchema: {
586
+ type: 'object',
587
+ properties: {
588
+ filePath: { type: 'string', description: 'Path to the .docx, .xlsx, or .pptx file.' },
589
+ elementPath: { type: 'string', description: 'Path to the element. Use "/" for full document. E.g. "/slide[1]", "/slide[1]/shape[1]", "/Sheet1/B2", "/body/p[3]". Also supports stable IDs: "/slide[1]/shape[@id=550950021]".' },
590
+ depth: { type: 'number', description: 'How many levels of children to expand (default: 1). Use 0 for just the element itself, 2+ for deeper children.' },
591
+ },
592
+ required: ['filePath', 'elementPath'],
593
+ },
594
+ risk: 'low',
595
+ async execute(input) {
596
+ const { filePath, elementPath, depth } = input;
597
+ const resolved = resolveFilePath(filePath);
598
+ ensureFileExists(resolved);
599
+ const args = ['get', resolved, elementPath, '--json'];
600
+ if (depth != null) {
601
+ args.push('--depth', String(depth));
602
+ }
603
+ const { stdout } = await runOfficeCLI(args);
604
+ try {
605
+ const parsed = JSON.parse(stdout);
606
+ const depthInfo = depth != null ? ` (depth=${depth})` : '';
607
+ return {
608
+ content: [{
609
+ type: 'text',
610
+ text: [
611
+ `📋 ${filePath} → ${elementPath}${depthInfo}`,
612
+ '',
613
+ '```json',
614
+ JSON.stringify(parsed, null, 2),
615
+ '```',
616
+ ].join('\n'),
617
+ }],
618
+ };
619
+ }
620
+ catch {
621
+ return { content: [{ type: 'text', text: stdout || '(no output)' }] };
622
+ }
623
+ },
624
+ }));
625
+ }
626
+ // ── Tool: inspect_office_file ─────────────────────────────────────────────
627
+ function registerInspectTool(ctx) {
628
+ ctx.subscriptions.push(ctx.tools.register({
629
+ name: 'inspect_office_file',
630
+ title: 'Inspect Office File',
631
+ description: 'Run diagnostics, validation, or help queries for Office files.\n\n' +
632
+ '**Actions:**\n' +
633
+ '- `validate` — Validate the file against the OpenXML schema. Catches structural issues.\n' +
634
+ '- `help` — Query the OfficeCLI help system for property names, value formats, and syntax.\n' +
635
+ ' Use this when you are unsure about command syntax instead of guessing.\n\n' +
636
+ '**Help query examples:**\n' +
637
+ '- `officecli help` — list all commands\n' +
638
+ '- `officecli help pptx` — list all PPTX element types\n' +
639
+ '- `officecli help pptx shape` — full shape schema with properties, aliases, examples\n' +
640
+ '- `officecli help docx paragraph` — paragraph properties\n' +
641
+ '- `officecli help xlsx cell` — cell properties, value formats\n' +
642
+ '- `officecli help pptx set shape` — only props usable with the `set` command\n' +
643
+ '- `officecli help pptx shape --json` — structured machine-readable schema\n\n' +
644
+ '**Format aliases:** `word`→`docx`, `excel`→`xlsx`, `ppt`/`powerpoint`→`pptx`.\n\n' +
645
+ '**IMPORTANT:** When you are unsure about property names, value formats, or command syntax, ' +
646
+ 'ALWAYS run a help query here instead of guessing. One help query beats guess-fail-retry loops.',
647
+ inputSchema: {
648
+ type: 'object',
649
+ properties: {
650
+ filePath: { type: 'string', description: 'Path to the file to validate. Only needed for validate action.' },
651
+ action: { type: 'string', enum: ['validate', 'help'], description: 'Action: validate the file or query the help system.' },
652
+ helpQuery: { type: 'string', description: 'For help action: the help query string. E.g. "pptx shape", "docx paragraph", "xlsx cell", "pptx", "". Empty string = general help.' },
653
+ },
654
+ required: ['action'],
655
+ },
656
+ risk: 'low',
657
+ async execute(input) {
658
+ const { filePath, action, helpQuery } = input;
659
+ if (action === 'help') {
660
+ const args = ['help'];
661
+ if (helpQuery) {
662
+ args.push(helpQuery);
663
+ }
664
+ const { stdout } = await runOfficeCLI(args);
665
+ return {
666
+ content: [{
667
+ type: 'text',
668
+ text: [
669
+ '📖 OfficeCLI Help',
670
+ helpQuery ? `Query: \`officecli help ${helpQuery}\`` : 'General help',
671
+ '',
672
+ '```',
673
+ stdout || '(no output)',
674
+ '```',
675
+ '',
676
+ '**Tip:** For more specific help, run again with a more targeted query like `pptx shape`, `docx paragraph`, or `xlsx cell`.',
677
+ ].join('\n'),
678
+ }],
679
+ };
680
+ }
681
+ if (!filePath) {
682
+ return { content: [{ type: 'text', text: 'filePath is required for validate action.' }], isError: true };
683
+ }
684
+ const resolved = resolveFilePath(filePath);
685
+ ensureFileExists(resolved);
686
+ const { stdout } = await runOfficeCLI(['validate', resolved]);
687
+ return {
688
+ content: [{
689
+ type: 'text',
690
+ text: [
691
+ `🔍 Validation result for ${filePath}`,
692
+ '',
693
+ stdout || 'No issues found — file is valid.',
694
+ ].join('\n'),
695
+ }],
696
+ };
697
+ },
698
+ }));
699
+ }
700
+ // ── Tool: save_office_file ────────────────────────────────────────────────
701
+ function registerSaveTool(ctx) {
702
+ ctx.subscriptions.push(ctx.tools.register({
703
+ name: 'save_office_file',
704
+ title: 'Save Office File',
705
+ description: 'Save and close an Office file. Flushes the resident session to disk.\n\n' +
706
+ 'OfficeCLI auto-starts a resident session on first file access (60s idle timeout). ' +
707
+ 'Its own reads (get/query/view) always see the latest edits, so you generally do NOT need to ' +
708
+ 'save mid-workflow. Call this only when:\n' +
709
+ '- You are done editing the file\n' +
710
+ '- A non-officecli program needs to read the file (e.g. Word, Excel, PowerPoint, a renderer)\n' +
711
+ '- You want to explicitly release the file handle',
712
+ inputSchema: {
713
+ type: 'object',
714
+ properties: {
715
+ filePath: { type: 'string', description: 'Path to the .docx, .xlsx, or .pptx file to save and close.' },
716
+ },
717
+ required: ['filePath'],
718
+ },
719
+ risk: 'low',
720
+ async execute(input) {
721
+ const { filePath } = input;
722
+ const resolved = resolveFilePath(filePath);
723
+ ensureFileExists(resolved);
724
+ const { stdout } = await runOfficeCLI(['close', resolved]);
725
+ return {
726
+ content: [{
727
+ type: 'text',
728
+ text: `✅ Saved and closed: ${filePath}${stdout ? '\n' + stdout : ''}`,
729
+ }],
730
+ };
731
+ },
732
+ }));
733
+ }
734
+ // ── Tool: preview_office_file ─────────────────────────────────────────────
735
+ function registerPreviewTool(ctx) {
736
+ ctx.subscriptions.push(ctx.tools.register({
737
+ name: 'preview_office_file',
738
+ title: 'Preview Office File',
739
+ description: 'Start, stop, or read selected elements from a live HTML preview of an Office file.\n\n' +
740
+ '**Actions:**\n' +
741
+ '- `start` — Launch a live preview server (default port 26315). The browser auto-refreshes on every file change. ' +
742
+ 'You and the user can click elements in the browser to select them, then use `action=selected` to inspect them.\n' +
743
+ '- `stop` — Shut down the preview server for the given file.\n' +
744
+ '- `selected` — Get structured JSON for whatever element the user clicked in the browser preview.\n\n' +
745
+ '**Workflow:**\n' +
746
+ '1. Start preview: `preview_office_file(action=start, filePath=deck.pptx)`\n' +
747
+ '2. Open the returned URL in browser\n' +
748
+ '3. After the user clicks an element: `preview_office_file(action=selected, filePath=deck.pptx)`\n' +
749
+ '4. Use the returned path with `modify_office_file` to edit it\n' +
750
+ '5. Stop preview when done: `preview_office_file(action=stop, filePath=deck.pptx)`',
751
+ inputSchema: {
752
+ type: 'object',
753
+ properties: {
754
+ filePath: { type: 'string', description: 'Path to the .docx, .xlsx, or .pptx file.' },
755
+ action: { type: 'string', enum: ['start', 'stop', 'selected'], description: 'start: launch live preview. stop: shut down preview. selected: get the currently selected element in the browser.' },
756
+ port: { type: 'number', description: 'Port for the preview server (default: 26315). Only used with start action.' },
757
+ },
758
+ required: ['filePath', 'action'],
759
+ },
760
+ risk: 'low',
761
+ async execute(input) {
762
+ const { filePath, action, port } = input;
763
+ const resolved = resolveFilePath(filePath);
764
+ ensureFileExists(resolved);
765
+ // ── selected: read browser selection ──
766
+ if (action === 'selected') {
767
+ const { stdout } = await runOfficeCLI(['get', resolved, 'selected', '--json']);
768
+ if (!stdout) {
769
+ return { content: [{ type: 'text', text: 'No element is currently selected in the browser. Click an element in the preview first.' }] };
770
+ }
771
+ try {
772
+ const parsed = JSON.parse(stdout);
773
+ return {
774
+ content: [{
775
+ type: 'text',
776
+ text: [
777
+ '🎯 Selected element:',
778
+ '',
779
+ '```json',
780
+ JSON.stringify(parsed, null, 2),
781
+ '```',
782
+ '',
783
+ 'Use `modify_office_file` with the returned `path` to edit the selected element.',
784
+ ].join('\n'),
785
+ }],
786
+ };
787
+ }
788
+ catch {
789
+ return { content: [{ type: 'text', text: stdout }] };
790
+ }
791
+ }
792
+ // ── stop: shut down preview server ──
793
+ if (action === 'stop') {
794
+ // Kill tracked process if we have it
795
+ const tracked = watchProcesses.get(resolved);
796
+ if (tracked) {
797
+ tracked.proc.kill();
798
+ watchProcesses.delete(resolved);
799
+ }
800
+ // Also send unwatch command
801
+ try {
802
+ await runOfficeCLI(['unwatch', resolved]);
803
+ }
804
+ catch {
805
+ // unwatch may fail if already stopped — not an error
806
+ }
807
+ return { content: [{ type: 'text', text: `⏹ Preview stopped for ${filePath}` }] };
808
+ }
809
+ // ── start: launch watch server ──
810
+ try {
811
+ const url = await startPreviewForFile(filePath, port);
812
+ return {
813
+ content: [{
814
+ type: 'text',
815
+ text: [
816
+ `👁 Preview started for ${filePath}`,
817
+ '',
818
+ `URL: ${url}`,
819
+ '',
820
+ 'The preview has been opened in your browser. Click elements to select them,',
821
+ 'then call this tool again with `action=selected` to inspect the selection.',
822
+ 'When done, call `action=stop` to shut down the server.',
823
+ ].join('\n'),
824
+ }],
825
+ };
826
+ }
827
+ catch (err) {
828
+ const message = err instanceof Error ? err.message : String(err);
829
+ return {
830
+ content: [{
831
+ type: 'text',
832
+ text: `❌ Failed to start preview for ${filePath}: ${message}`,
833
+ }],
834
+ isError: true,
835
+ };
836
+ }
837
+ },
838
+ }));
839
+ }
840
+ // ── Activation ────────────────────────────────────────────────────────────
841
+ const OFFICE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#D83B01" d="M19.94 5.59v12.8q0 .67-.39 1.2q-.39.52-1.05.7l-5.73 1.65q-.12.03-.27.06h-.22q-.33 0-.6-.09t-.55-.24l-3.75-2.12q-.21-.12-.33-.31t-.12-.43q0-.36.26-.61q.25-.25.61-.25h4.86V6.14L9 7.44q-.43.16-.7.56q-.27.38-.27.85v6.73q0 .42-.21.76q-.2.34-.57.54l-1.72.94q-.24.13-.48.13q-.41 0-.7-.29t-.29-.71V7.47q0-.52.27-.97q.28-.5.73-.76l6.16-3.5q.21-.12.45-.18t.48-.06q.17 0 .31.03q.14.02.31.07l5.73 1.59q.33.09.59.27t.45.43q.2.26.3.56q.1.31.1.64m-1.32 12.8V5.59q0-.23-.12-.4q-.15-.19-.37-.23l-2.82-.78Q15 4.09 14.65 4q-.33-.11-.65-.19v16.4L18.13 19q.22-.04.37-.21q.12-.17.12-.4"/></svg>`;
842
+ export function activate(ctx) {
843
+ // Store extension path for local binary resolution
844
+ _extensionPath = ctx.extension.extensionPath;
845
+ ctx.logger.info(`office-tools activating (extension path: ${_extensionPath})`);
846
+ // Register custom icon
847
+ ctx.icons.register('office-tools-icons', {
848
+ 'office': { svg: OFFICE_ICON_SVG },
849
+ });
850
+ registerCheckTool(ctx);
851
+ registerSetupTool(ctx);
852
+ registerReadTool(ctx);
853
+ registerCreateTool(ctx);
854
+ registerModifyTool(ctx);
855
+ registerGetTool(ctx);
856
+ registerInspectTool(ctx);
857
+ registerPreviewTool(ctx);
858
+ registerSaveTool(ctx);
859
+ ctx.logger.info('office-tools activated — 9 tools registered');
860
+ }
861
+ export function deactivate() {
862
+ // Kill all running watch processes
863
+ for (const [file, tracked] of watchProcesses) {
864
+ try {
865
+ tracked.proc.kill();
866
+ }
867
+ catch {
868
+ // ignore
869
+ }
870
+ }
871
+ watchProcesses.clear();
872
+ }
package/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
2
+ <g>
3
+ <path d="M 317.00 511.10 C316.17,511.56 359.71,511.95 413.75,511.97 L 512.00 512.00 L 152.20 512.00 C237.80,512.00 303.98,511.62 303.45,511.14 C302.93,510.66 245.35,491.28 175.50,468.06 C73.44,434.13 48.06,425.36 46.26,423.37 C42.00,418.68 42.07,413.44 46.48,408.21 C48.89,405.34 48.91,405.23 47.07,403.56 C46.03,402.62 44.69,400.65 44.08,399.18 C43.28,397.26 43.05,356.38 43.24,254.29 C43.47,125.62 43.65,111.92 45.10,110.48 C45.97,109.60 70.95,98.79 100.60,86.45 C130.24,74.11 155.40,63.59 156.50,63.07 C157.60,62.54 173.35,55.97 191.50,48.46 C227.87,33.40 298.49,3.99 304.00,1.60 C307.15,0.23 292.13,0.07 153.75,0.04 L 0.00 0.00 L 412.33 0.00 C357.66,0.00 313.08,0.17 313.08,0.38 L 313.08 0.38 C313.31,0.60 332.85,6.28 356.50,13.01 C459.96,42.47 464.17,43.61 466.68,46.32 C466.87,46.52 467.04,46.73 467.25,46.96 L 470.00 50.03 L 470.00 462.03 L 466.75 465.47 C463.80,468.60 461.67,469.43 443.50,474.52 C432.50,477.61 422.15,480.55 420.50,481.06 C411.26,483.91 388.20,490.48 373.50,494.44 C370.20,495.33 364.58,496.94 361.00,498.01 C357.42,499.08 346.40,502.27 336.50,505.11 C326.60,507.94 317.83,510.63 317.00,511.10 ZM 297.00 442.93 C306.62,444.51 315.74,445.84 317.25,445.90 L 320.00 446.00 L 320.00 268.06 C320.00,170.19 319.65,89.90 319.23,89.64 C318.81,89.38 303.40,92.93 284.98,97.53 C266.57,102.12 245.20,107.45 237.50,109.37 C229.80,111.28 202.12,118.20 176.00,124.74 L 128.50 136.63 L 128.00 252.21 C127.51,365.16 127.46,367.84 125.54,369.95 C124.47,371.14 109.05,379.36 91.29,388.23 C73.53,397.09 59.00,404.70 59.00,405.15 L 59.00 405.17 C59.00,405.24 59.00,405.30 59.01,405.36 C59.19,406.34 62.23,406.80 113.00,414.52 C140.23,418.66 166.55,422.70 171.50,423.50 C176.45,424.30 194.00,426.99 210.50,429.48 C227.00,431.97 243.20,434.44 246.50,434.97 C249.80,435.50 258.58,436.87 266.00,438.00 C273.42,439.13 287.38,441.35 297.00,442.93 Z" fill="rgb(253,87,34)"/>
4
+ <path d="M 412.33 0.00 L 512.00 0.00 L 414.25 0.03 C336.70,0.06 316.83,0.32 318.11,1.29 C318.90,1.89 327.35,4.61 337.76,7.65 C323.34,3.50 313.25,0.54 313.08,0.38 L 313.08 0.38 C313.08,0.17 357.66,0.00 412.33,0.00 ZM 252.97 22.92 C287.47,8.51 301.90,2.24 302.77,1.37 C303.25,0.89 299.56,0.57 285.89,0.36 C302.99,0.59 305.47,0.96 304.00,1.60 C300.99,2.90 278.61,12.25 252.97,22.92 ZM 345.74 9.94 C352.42,11.84 358.72,13.64 364.66,15.34 L 356.50 13.01 C352.79,11.96 349.19,10.93 345.74,9.94 ZM 465.38 45.26 C466.03,45.64 466.38,45.97 466.70,46.33 C466.88,46.52 467.04,46.72 467.24,46.95 L 470.00 50.03 L 467.25 46.96 C467.04,46.73 466.87,46.52 466.68,46.32 C466.35,45.96 465.99,45.63 465.38,45.26 Z" fill="rgb(254,224,206)"/>
5
+ <path d="M 0.00 256.00 L 0.00 0.00 L 152.07 0.00 C280.81,0.00 303.93,0.21 302.77,1.37 C301.49,2.65 271.21,15.46 191.50,48.46 C173.35,55.97 157.60,62.54 156.50,63.07 C155.40,63.59 130.24,74.11 100.60,86.45 C70.95,98.79 45.97,109.60 45.10,110.48 C43.65,111.92 43.47,125.62 43.24,254.29 C43.05,356.38 43.28,397.26 44.08,399.18 C44.69,400.65 46.03,402.62 47.07,403.56 C48.91,405.23 48.89,405.34 46.48,408.21 C42.07,413.44 42.00,418.68 46.26,423.37 C48.06,425.36 73.44,434.13 175.50,468.06 C245.35,491.28 302.93,510.66 303.45,511.14 C303.98,511.62 237.80,512.00 152.20,512.00 L 0.00 512.00 L 0.00 256.00 ZM 317.00 511.10 C317.83,510.63 326.60,507.94 336.50,505.11 C346.40,502.27 357.42,499.08 361.00,498.01 C364.58,496.94 370.20,495.33 373.50,494.44 C388.20,490.48 411.26,483.91 420.50,481.06 C422.15,480.55 432.50,477.61 443.50,474.52 C461.67,469.43 463.80,468.60 466.75,465.47 L 470.00 462.03 L 470.00 256.03 L 470.00 50.03 L 467.24 46.95 C464.32,43.67 468.88,45.06 342.11,8.91 C329.80,5.40 319.00,1.97 318.11,1.29 C316.83,0.32 336.70,0.06 414.25,0.03 L 512.00 0.00 L 512.00 256.00 L 512.00 512.00 L 413.75 511.97 C359.71,511.95 316.17,511.56 317.00,511.10 ZM 297.00 442.93 C287.38,441.35 273.42,439.13 266.00,438.00 C258.58,436.87 249.80,435.50 246.50,434.97 C243.20,434.44 227.00,431.97 210.50,429.48 C194.00,426.99 176.45,424.30 171.50,423.50 C166.55,422.70 140.23,418.66 113.00,414.52 C58.72,406.27 59.00,406.32 59.00,405.15 C59.00,404.70 73.53,397.09 91.29,388.23 C109.05,379.36 124.47,371.14 125.54,369.95 C127.46,367.84 127.51,365.16 128.00,252.21 L 128.50 136.63 L 176.00 124.74 C202.12,118.20 229.80,111.28 237.50,109.37 C245.20,107.45 266.57,102.12 284.98,97.53 C303.40,92.93 318.81,89.38 319.23,89.64 C319.65,89.90 320.00,170.19 320.00,268.06 L 320.00 446.00 L 317.25 445.90 C315.74,445.84 306.62,444.51 297.00,442.93 Z" fill="rgb(254,254,254)"/>
6
+ </g>
7
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joehe71/office-tools",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Office tools for Finch — read, create, edit Word, Excel, and PowerPoint files using OfficeCLI.",
6
6
  "author": {
@@ -263,6 +263,7 @@
263
263
  "files": [
264
264
  "dist/",
265
265
  "i18n/",
266
+ "icon.svg",
266
267
  "README.md",
267
268
  "package.json"
268
269
  ]