agentxl 1.1.3 → 1.1.4

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/bin/agentxl.js CHANGED
@@ -1,521 +1,460 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync, existsSync } from "fs";
4
- import { resolve, join, dirname } from "path";
5
- import { fileURLToPath } from "url";
6
- import { homedir } from "os";
7
- import { createInterface } from "readline";
8
- import { config as loadDotenv } from "dotenv";
9
-
10
- // Load .env from project root (before any other imports)
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
13
- const projectRoot = resolve(__dirname, "..");
14
- loadDotenv({ path: join(projectRoot, ".env") });
15
-
16
- // ---------------------------------------------------------------------------
17
- // Package info
18
- // ---------------------------------------------------------------------------
19
-
20
- const pkgPath = join(__dirname, "..", "package.json");
21
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
22
- const VERSION = pkg.version;
23
-
24
- // ---------------------------------------------------------------------------
25
- // Arg parsing
26
- // ---------------------------------------------------------------------------
27
-
28
- const args = process.argv.slice(2);
29
- const command = args[0];
30
-
31
- function getFlag(name) {
32
- const eqIdx = args.findIndex((a) => a.startsWith(`--${name}=`));
33
- if (eqIdx !== -1) return args[eqIdx].split("=")[1];
34
- const spaceIdx = args.indexOf(`--${name}`);
35
- if (spaceIdx !== -1 && args[spaceIdx + 1]) return args[spaceIdx + 1];
36
- return undefined;
37
- }
38
-
39
- const hasFlag = (name) => args.includes(`--${name}`);
40
-
41
- // ---------------------------------------------------------------------------
42
- // Terminal I/O helpers
43
- // ---------------------------------------------------------------------------
44
-
45
- function prompt(question) {
46
- const rl = createInterface({ input: process.stdin, output: process.stdout });
47
- return new Promise((resolve) => {
48
- rl.question(question, (answer) => {
49
- rl.close();
50
- resolve(answer.trim());
51
- });
52
- });
53
- }
54
-
55
- function promptSecret(question) {
56
- return new Promise((resolve) => {
57
- process.stdout.write(question);
58
- const rl = createInterface({ input: process.stdin, terminal: false });
59
- if (process.stdin.isTTY) process.stdin.setRawMode?.(false);
60
- rl.on("line", (line) => {
61
- rl.close();
62
- process.stdout.write("\n");
63
- resolve(line.trim());
64
- });
65
- });
66
- }
67
-
68
- async function openUrl(url) {
69
- const { exec } = await import("child_process");
70
- const cmd =
71
- process.platform === "win32"
72
- ? `start "" "${url}"`
73
- : process.platform === "darwin"
74
- ? `open "${url}"`
75
- : `xdg-open "${url}"`;
76
- exec(cmd);
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Step-by-step progress output
81
- // ---------------------------------------------------------------------------
82
-
83
- /** Print a step status: ✅ done, ⏳ in progress, ❌ failed */
84
- function step(icon, text) {
85
- console.log(` ${icon} ${text}`);
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Auth flow using Pi SDK
90
- // ---------------------------------------------------------------------------
91
-
92
- async function checkAuth() {
93
- const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
94
-
95
- const piAuthPath = join(homedir(), ".pi", "agent", "auth.json");
96
- const agentxlAuthPath = join(homedir(), ".agentxl", "auth.json");
97
- const authPath = existsSync(piAuthPath) ? piAuthPath : agentxlAuthPath;
98
- const authStorage = AuthStorage.create(authPath);
99
-
100
- return authStorage.list().length > 0;
101
- }
102
-
103
- async function runAuthFlow() {
104
- const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
105
-
106
- const piAuthPath = join(homedir(), ".pi", "agent", "auth.json");
107
- const agentxlAuthPath = join(homedir(), ".agentxl", "auth.json");
108
- const authPath = existsSync(piAuthPath) ? piAuthPath : agentxlAuthPath;
109
- const authStorage = AuthStorage.create(authPath);
110
-
111
- // Check if already authenticated
112
- if (authStorage.list().length > 0) {
113
- return true;
114
- }
115
-
116
- console.log(`
117
- No API credentials found. Let's get you set up.
118
-
119
- How would you like to connect?
120
-
121
- Use an existing subscription (no API key needed):
122
- 1. Claude Pro/Max — sign in with your Anthropic account
123
- 2. ChatGPT Plus/Pro — sign in with your OpenAI account
124
- 3. GitHub Copilot sign in with your GitHub account
125
- 4. Gemini — sign in with your Google account
126
-
127
- Use an API key:
128
- 5. Paste an API key (Anthropic, OpenRouter, or OpenAI)
129
-
130
- No account yet?
131
- Create a free OpenRouter account at https://openrouter.ai
132
- Get an API key instantly. Free models available.
133
- `);
134
-
135
- // Build choices OAuth providers + API key
136
- const oauthProviders = authStorage.getOAuthProviders();
137
- const choices = [];
138
- for (const p of oauthProviders) {
139
- choices.push({ type: "oauth", id: p.id, name: p.name, provider: p });
140
- }
141
- choices.push({ type: "apikey", id: "apikey", name: "Paste an API key" });
142
-
143
- const answer = await prompt(" Enter choice (1-" + choices.length + "): ");
144
- const idx = parseInt(answer, 10) - 1;
145
-
146
- if (isNaN(idx) || idx < 0 || idx >= choices.length) {
147
- console.error("\n Invalid choice. Run 'agentxl login' to try again.\n");
148
- return false;
149
- }
150
-
151
- const choice = choices[idx];
152
-
153
- if (choice.type === "oauth") {
154
- console.log(`\n Signing in with ${choice.name}...\n`);
155
-
156
- try {
157
- await authStorage.login(choice.id, {
158
- onAuth: (info) => {
159
- console.log(` 🌐 Opening browser for sign-in...`);
160
- console.log(` ${info.url}\n`);
161
- if (info.instructions) {
162
- console.log(` ${info.instructions}\n`);
163
- }
164
- openUrl(info.url);
165
- },
166
- onPrompt: async (p) => {
167
- const answer = await prompt(` ${p.message}: `);
168
- return answer;
169
- },
170
- onProgress: (message) => {
171
- console.log(` ${message}`);
172
- },
173
- onManualCodeInput: async () => {
174
- const code = await prompt(" Enter the code from the browser: ");
175
- return code;
176
- },
177
- });
178
-
179
- console.log(`\n Signed in with ${choice.name}\n`);
180
- return true;
181
- } catch (err) {
182
- console.error(`\n Sign-in failed: ${err.message}\n`);
183
- return false;
184
- }
185
- } else {
186
- // API key flow
187
- console.log(`
188
- Paste your API key below.
189
-
190
- Key prefixes:
191
- sk-ant-... → Anthropic (Claude)
192
- sk-or-... → OpenRouter (100+ models)
193
- sk-... → OpenAI (GPT-4o)
194
- `);
195
-
196
- const key = await promptSecret(" API key: ");
197
-
198
- if (!key) {
199
- console.error("\n No key entered. Run 'agentxl login' to try again.\n");
200
- return false;
201
- }
202
-
203
- // Auto-detect provider from key prefix
204
- let provider;
205
- if (key.startsWith("sk-ant-")) provider = "anthropic";
206
- else if (key.startsWith("sk-or-")) provider = "openrouter";
207
- else if (key.startsWith("sk-")) provider = "openai";
208
- else {
209
- const p = await prompt(" Could not detect provider. Enter provider name (anthropic/openrouter/openai): ");
210
- provider = p.toLowerCase();
211
- }
212
-
213
- authStorage.set(provider, { type: "api_key", key });
214
- console.log(`\n ✅ API key saved for ${provider}\n`);
215
- return true;
216
- }
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // Commands
221
- // ---------------------------------------------------------------------------
222
-
223
- function printHelp() {
224
- console.log(`
225
- AgentXL v${VERSION} AI agent for Microsoft Excel
226
-
227
- Usage:
228
- agentxl start [options] Start the AgentXL server
229
- agentxl install Register the add-in with Excel (one-time)
230
- agentxl login Set up or change API credentials
231
- agentxl --version Print version
232
- agentxl --help Show this help
233
-
234
- Options:
235
- --port <number> Port to listen on (default: 3001)
236
- --verbose Log all HTTP requests
237
-
238
- Examples:
239
- agentxl install # one-time: register with Excel
240
- agentxl start # run the server
241
- agentxl start --port 3002
242
- agentxl login
243
- `);
244
- }
245
-
246
- async function start() {
247
- const port = parseInt(getFlag("port") || "3001", 10);
248
-
249
- if (isNaN(port) || port < 1 || port > 65535) {
250
- console.error(`Error: Invalid port number. Must be 1-65535.`);
251
- process.exit(1);
252
- }
253
-
254
- console.log(`
255
- ┌──────────────────────────────────────┐
256
- │ AgentXL v${VERSION.padEnd(19)}│
257
- │ AI agent for Microsoft Excel │
258
- └──────────────────────────────────────┘
259
- `);
260
-
261
- // ── Step 1: Load modules ───────────────────────────────────────────────
262
- let ensureCerts, startServer, stopServer, setVerbose, getFolderPickerStrategy;
263
- try {
264
- const certs = await import("../dist/server/certs.js");
265
- const server = await import("../dist/server/index.js");
266
- const picker = await import("../dist/server/folder-picker.js");
267
- ensureCerts = certs.ensureCerts;
268
- startServer = server.startServer;
269
- stopServer = server.stopServer;
270
- setVerbose = server.setVerbose;
271
- getFolderPickerStrategy = picker.getFolderPickerStrategy;
272
- } catch (err) {
273
- step("❌", "Could not load AgentXL server modules");
274
- console.error(" Run 'npm run build' first to compile TypeScript.");
275
- console.error(` ${err.message}`);
276
- process.exit(1);
277
- }
278
-
279
- // ── Step 2: Check auth ─────────────────────────────────────────────────
280
- const hasAuth = await checkAuth();
281
- if (hasAuth) {
282
- step("✅", "Auth ready");
283
- } else {
284
- const authed = await runAuthFlow();
285
- if (!authed) process.exit(1);
286
- step("✅", "Auth ready");
287
- }
288
-
289
- // ── Step 3: HTTPS certificates ─────────────────────────────────────────
290
- try {
291
- const certPair = await ensureCerts();
292
- step("", "HTTPS certificate ready");
293
-
294
- // ── Step 4: Start server ───────────────────────────────────────────────
295
- if (hasFlag("verbose")) setVerbose(true);
296
- await startServer(port, certPair);
297
- step("✅", `Server running at https://localhost:${port}`);
298
- } catch (err) {
299
- step("❌", `Server failed to start: ${err.message}`);
300
- process.exit(1);
301
- }
302
-
303
- // ── Step 5: OCR status ─────────────────────────────────────────────────
304
- if (process.env.AZURE_MISTRAL_ENDPOINT && process.env.AZURE_MISTRAL_API_KEY) {
305
- step("✅", "OCR ready (Azure Mistral)");
306
- } else if (process.env.MISTRAL_API_KEY) {
307
- step("✅", "OCR ready (Mistral direct)");
308
- } else {
309
- step("ℹ️", "OCR not configured — scanned PDFs won't be readable");
310
- step(" ", "Set AZURE_MISTRAL_ENDPOINT + AZURE_MISTRAL_API_KEY in .env");
311
- }
312
-
313
- // ── Step 6: Folder picker strategy ────────────────────────────────────
314
- const pickerStrategy = getFolderPickerStrategy();
315
- const pickerLabels = {
316
- "native-helper": "Native folder picker helper",
317
- "powershell": "PowerShell folder picker (fallback)",
318
- "osascript": "macOS folder picker (osascript)",
319
- "manual-only": "Manual path entry only",
320
- };
321
- const pickerLabel = pickerLabels[pickerStrategy.method] || pickerStrategy.method;
322
- if (pickerStrategy.method === "native-helper") {
323
- step("✅", `Folder picker: ${pickerLabel}`);
324
- } else if (pickerStrategy.method === "powershell" || pickerStrategy.method === "osascript") {
325
- step("⚠️", `Folder picker: ${pickerLabel}`);
326
- if (pickerStrategy.platform === "win32") {
327
- step(" ", "Build the native helper for a better experience:");
328
- step(" ", " npm run build:folder-picker:win");
329
- }
330
- } else {
331
- step("ℹ️", `Folder picker: ${pickerLabel}`);
332
- }
333
-
334
- // ── Step 7: Auto-register add-in with Excel (first run) ─────────────
335
- const manifestPath = resolve(__dirname, "..", "manifest", "manifest.xml");
336
-
337
- if (existsSync(manifestPath)) {
338
- let alreadyRegistered = false;
339
- try {
340
- const devSettings = await import("office-addin-dev-settings");
341
- const registered = await devSettings.getRegisteredAddIns();
342
- alreadyRegistered = registered.some(a => a.manifestPath === manifestPath);
343
- } catch (_) {}
344
-
345
- if (!alreadyRegistered) {
346
- step("⏳", "First run — registering AgentXL with Excel...");
347
- try {
348
- const devSettings = await import("office-addin-dev-settings");
349
- await devSettings.registerAddIn(manifestPath);
350
- step("", "Add-in registered with Excel");
351
- } catch (err) {
352
- step("⚠️", `Auto-registration failed: ${err.message}`);
353
- step(" ", "Run 'agentxl install' manually, or add the manifest folder to Excel Trust Center:");
354
- step(" ", ` ${dirname(manifestPath)}`);
355
- }
356
-
357
- try {
358
- const devSettings = await import("office-addin-dev-settings");
359
- await devSettings.ensureLoopbackIsEnabled(manifestPath, false);
360
- } catch (_) {}
361
- } else {
362
- step("✅", "Excel add-in registered");
363
- }
364
- }
365
-
366
- // ── Post-start guidance ────────────────────────────────────────────────
367
- console.log(`
368
- ─────────────────────────────────────────────────
369
- All systems go!
370
- ─────────────────────────────────────────────────
371
-
372
- 📎 Open ExcelAgentXL is on the Home ribbon
373
- (If you don't see it: Insert My Add-ins SHARED FOLDER → AgentXL)
374
-
375
- 🌐 Or test in browser first:
376
- https://localhost:${port}/taskpane/
377
-
378
- 💬 Try: "What can you help me with in this workbook?"
379
- `);
380
-
381
- // ── Graceful shutdown ──────────────────────────────────────────────────
382
- let shuttingDown = false;
383
- const shutdown = () => {
384
- if (shuttingDown) return;
385
- shuttingDown = true;
386
- console.log("\n AgentXL stopped. Goodbye!\n");
387
- const forceExit = setTimeout(() => process.exit(0), 2000);
388
- forceExit.unref?.();
389
- stopServer().then(() => process.exit(0));
390
- };
391
-
392
- process.on("SIGINT", shutdown);
393
- process.on("SIGTERM", shutdown);
394
- }
395
-
396
- async function install() {
397
- console.log(`
398
- ┌──────────────────────────────────────┐
399
- │ AgentXL Excel Registration │
400
- └──────────────────────────────────────┘
401
- `);
402
-
403
- const manifestPath = resolve(__dirname, "..", "manifest", "manifest.xml");
404
- if (!existsSync(manifestPath)) {
405
- step("❌", `Manifest not found: ${manifestPath}`);
406
- process.exit(1);
407
- }
408
-
409
- // ── Step 1: HTTPS certificates ─────────────────────────────────────────
410
- try {
411
- step("⏳", "Ensuring HTTPS certificates are trusted...");
412
- const devCerts = await import("office-addin-dev-certs");
413
- await devCerts.ensureCertificatesAreInstalled();
414
- step("✅", "HTTPS certificate trusted");
415
- } catch (err) {
416
- step("⚠️", `Certificate setup: ${err.message}`);
417
- }
418
-
419
- // ── Step 2: Register add-in ────────────────────────────────────────────
420
- try {
421
- step("⏳", "Registering AgentXL with Excel...");
422
- const devSettings = await import("office-addin-dev-settings");
423
- await devSettings.registerAddIn(manifestPath);
424
- step("✅", "Add-in registered with Excel");
425
- } catch (err) {
426
- step("❌", `Registration failed: ${err.message}`);
427
- console.log(`
428
- Fallback: manually add this folder to Excel's Trusted Add-in Catalogs:
429
- ${dirname(manifestPath)}
430
-
431
- Steps:
432
- 1. Excel → File → Options → Trust Center → Trust Center Settings
433
- 2. Trusted Add-in Catalogs paste the path above
434
- 3. Check "Show in Menu" → OK → OK
435
- 4. Restart Excel
436
- 5. Insert → My Add-ins → SHARED FOLDER → AgentXL → Add
437
- `);
438
- process.exit(1);
439
- }
440
-
441
- // ── Step 3: Enable loopback ────────────────────────────────────────────
442
- try {
443
- const devSettings = await import("office-addin-dev-settings");
444
- await devSettings.ensureLoopbackIsEnabled(manifestPath, false);
445
- step("✅", "Localhost loopback enabled");
446
- } catch (err) {
447
- step("⚠️", `Loopback: ${err.message}`);
448
- }
449
-
450
- console.log(`
451
- ─────────────────────────────────────────────────
452
- Done! AgentXL is registered with Excel.
453
- ─────────────────────────────────────────────────
454
-
455
- Next steps:
456
- 1. Run: agentxl start
457
- 2. Open Excel → AgentXL appears on the Home ribbon
458
-
459
- To open Excel with AgentXL right now:
460
- agentxl install --open
461
- `);
462
-
463
- // Optionally open Excel
464
- if (hasFlag("open")) {
465
- try {
466
- const devSettings = await import("office-addin-dev-settings");
467
- const manifestLib = await import("office-addin-manifest");
468
- step("⏳", "Opening Excel with AgentXL...");
469
- await devSettings.sideloadAddIn(manifestPath, manifestLib.OfficeApp.Excel, false, devSettings.AppType.Desktop);
470
- } catch (err) {
471
- step("⚠️", `Could not open Excel: ${err.message}`);
472
- }
473
- }
474
- }
475
-
476
- async function login() {
477
- console.log("");
478
- const authed = await runAuthFlow();
479
- if (authed) {
480
- console.log(" Run 'agentxl start' to launch the server.\n");
481
- }
482
- process.exit(authed ? 0 : 1);
483
- }
484
-
485
- // ---------------------------------------------------------------------------
486
- // Main
487
- // ---------------------------------------------------------------------------
488
-
489
- if (hasFlag("version") || command === "--version") {
490
- console.log(VERSION);
491
- process.exit(0);
492
- }
493
-
494
- if (hasFlag("help") || command === "--help" || command === "help") {
495
- printHelp();
496
- process.exit(0);
497
- }
498
-
499
- if (command === "start") {
500
- start().catch((err) => {
501
- console.error(`\n ❌ ${err.message || err}\n`);
502
- process.exit(1);
503
- });
504
- } else if (command === "install") {
505
- install().catch((err) => {
506
- console.error(`\n ❌ ${err.message || err}\n`);
507
- process.exit(1);
508
- });
509
- } else if (command === "login") {
510
- login().catch((err) => {
511
- console.error(`\n ❌ ${err.message || err}\n`);
512
- process.exit(1);
513
- });
514
- } else if (!command) {
515
- printHelp();
516
- process.exit(0);
517
- } else {
518
- console.error(`Unknown command: ${command}`);
519
- console.error(`Run 'agentxl --help' for usage.`);
520
- process.exit(1);
521
- }
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { resolve, join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { homedir } from "os";
7
+ import { createInterface } from "readline";
8
+ import { config as loadDotenv } from "dotenv";
9
+
10
+ // Load .env from project root (before any other imports)
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const projectRoot = resolve(__dirname, "..");
14
+ loadDotenv({ path: join(projectRoot, ".env") });
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Package info
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const pkgPath = join(__dirname, "..", "package.json");
21
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
22
+ const VERSION = pkg.version;
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Arg parsing
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const args = process.argv.slice(2);
29
+ const command = args[0];
30
+
31
+ function getFlag(name) {
32
+ const eqIdx = args.findIndex((a) => a.startsWith(`--${name}=`));
33
+ if (eqIdx !== -1) return args[eqIdx].split("=")[1];
34
+ const spaceIdx = args.indexOf(`--${name}`);
35
+ if (spaceIdx !== -1 && args[spaceIdx + 1]) return args[spaceIdx + 1];
36
+ return undefined;
37
+ }
38
+
39
+ const hasFlag = (name) => args.includes(`--${name}`);
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Terminal I/O helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function prompt(question) {
46
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
47
+ return new Promise((resolve) => {
48
+ rl.question(question, (answer) => {
49
+ rl.close();
50
+ resolve(answer.trim());
51
+ });
52
+ });
53
+ }
54
+
55
+ function promptSecret(question) {
56
+ return new Promise((resolve) => {
57
+ process.stdout.write(question);
58
+ const rl = createInterface({ input: process.stdin, terminal: false });
59
+ if (process.stdin.isTTY) process.stdin.setRawMode?.(false);
60
+ rl.on("line", (line) => {
61
+ rl.close();
62
+ process.stdout.write("\n");
63
+ resolve(line.trim());
64
+ });
65
+ });
66
+ }
67
+
68
+ async function openUrl(url) {
69
+ const { exec } = await import("child_process");
70
+ const cmd =
71
+ process.platform === "win32"
72
+ ? `start "" "${url}"`
73
+ : process.platform === "darwin"
74
+ ? `open "${url}"`
75
+ : `xdg-open "${url}"`;
76
+ exec(cmd);
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Step-by-step progress output
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /** Print a step status: ✅ done, ⏳ in progress, ❌ failed */
84
+ function step(icon, text) {
85
+ console.log(` ${icon} ${text}`);
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Auth flow using Pi SDK
90
+ // ---------------------------------------------------------------------------
91
+
92
+ async function checkAuth() {
93
+ const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
94
+
95
+ const piAuthPath = join(homedir(), ".pi", "agent", "auth.json");
96
+ const agentxlAuthPath = join(homedir(), ".agentxl", "auth.json");
97
+ const authPath = existsSync(piAuthPath) ? piAuthPath : agentxlAuthPath;
98
+ const authStorage = AuthStorage.create(authPath);
99
+
100
+ if (authStorage.list().length > 0) return true;
101
+
102
+ // Also accept env vars — Pi SDK's hasAuth() falls back to them at runtime.
103
+ return Boolean(
104
+ process.env.OPENROUTER_API_KEY ||
105
+ process.env.ANTHROPIC_API_KEY ||
106
+ process.env.OPENAI_API_KEY
107
+ );
108
+ }
109
+
110
+ async function runAuthFlow() {
111
+ const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
112
+
113
+ const piAuthPath = join(homedir(), ".pi", "agent", "auth.json");
114
+ const agentxlAuthPath = join(homedir(), ".agentxl", "auth.json");
115
+ const authPath = existsSync(piAuthPath) ? piAuthPath : agentxlAuthPath;
116
+ const authStorage = AuthStorage.create(authPath);
117
+
118
+ // Check if already authenticated
119
+ if (authStorage.list().length > 0) {
120
+ return true;
121
+ }
122
+
123
+ console.log(`
124
+ No API credentials found. Paste an API key to get started.
125
+
126
+ Key prefixes:
127
+ sk-or-... → OpenRouter (recommended — 100+ models, free tier available)
128
+ sk-ant-... → Anthropic (Claude)
129
+ sk-... → OpenAI (GPT-4o)
130
+
131
+ No account yet?
132
+ Create a free OpenRouter account at https://openrouter.ai/keys
133
+ `);
134
+
135
+ const key = await promptSecret(" API key: ");
136
+
137
+ if (!key) {
138
+ console.error("\n No key entered. Run 'agentxl login' to try again.\n");
139
+ return false;
140
+ }
141
+
142
+ // Auto-detect provider from key prefix
143
+ let provider;
144
+ if (key.startsWith("sk-ant-")) provider = "anthropic";
145
+ else if (key.startsWith("sk-or-")) provider = "openrouter";
146
+ else if (key.startsWith("sk-")) provider = "openai";
147
+ else {
148
+ const p = await prompt(
149
+ " Could not detect provider. Enter provider name (anthropic/openrouter/openai): "
150
+ );
151
+ provider = p.toLowerCase();
152
+ }
153
+
154
+ authStorage.set(provider, { type: "api_key", key });
155
+ console.log(`\n ✅ API key saved for ${provider}\n`);
156
+ return true;
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Commands
161
+ // ---------------------------------------------------------------------------
162
+
163
+ function printHelp() {
164
+ console.log(`
165
+ AgentXL v${VERSION} — AI agent for Microsoft Excel
166
+
167
+ Usage:
168
+ agentxl start [options] Start the AgentXL server
169
+ agentxl install Register the add-in with Excel (one-time)
170
+ agentxl login Set up or change API credentials
171
+ agentxl --version Print version
172
+ agentxl --help Show this help
173
+
174
+ Options:
175
+ --port <number> Port to listen on (default: 3001)
176
+ --verbose Log all HTTP requests
177
+
178
+ Examples:
179
+ agentxl install # one-time: register with Excel
180
+ agentxl start # run the server
181
+ agentxl start --port 3002
182
+ agentxl login
183
+ `);
184
+ }
185
+
186
+ async function start() {
187
+ const port = parseInt(getFlag("port") || "3001", 10);
188
+
189
+ if (isNaN(port) || port < 1 || port > 65535) {
190
+ console.error(`Error: Invalid port number. Must be 1-65535.`);
191
+ process.exit(1);
192
+ }
193
+
194
+ console.log(`
195
+ ┌──────────────────────────────────────┐
196
+ │ AgentXL v${VERSION.padEnd(19)}│
197
+ │ AI agent for Microsoft Excel │
198
+ └──────────────────────────────────────┘
199
+ `);
200
+
201
+ // ── Step 1: Load modules ───────────────────────────────────────────────
202
+ let ensureCerts, startServer, stopServer, setVerbose, getFolderPickerStrategy;
203
+ try {
204
+ const certs = await import("../dist/server/certs.js");
205
+ const server = await import("../dist/server/index.js");
206
+ const picker = await import("../dist/server/folder-picker.js");
207
+ ensureCerts = certs.ensureCerts;
208
+ startServer = server.startServer;
209
+ stopServer = server.stopServer;
210
+ setVerbose = server.setVerbose;
211
+ getFolderPickerStrategy = picker.getFolderPickerStrategy;
212
+ } catch (err) {
213
+ step("❌", "Could not load AgentXL server modules");
214
+ console.error(" Run 'npm run build' first to compile TypeScript.");
215
+ console.error(` ${err.message}`);
216
+ process.exit(1);
217
+ }
218
+
219
+ // ── Step 2: Check auth ─────────────────────────────────────────────────
220
+ const hasAuth = await checkAuth();
221
+ if (hasAuth) {
222
+ step("✅", "Auth ready");
223
+ } else {
224
+ step("ℹ️", "No API key yet — you can paste one in the AgentXL taskpane");
225
+ step(" ", "Or run 'agentxl login' to set one in the terminal");
226
+ }
227
+
228
+ // ── Step 3: HTTPS certificates ─────────────────────────────────────────
229
+ try {
230
+ const certPair = await ensureCerts();
231
+ step("✅", "HTTPS certificate ready");
232
+
233
+ // ── Step 4: Start server ───────────────────────────────────────────────
234
+ if (hasFlag("verbose")) setVerbose(true);
235
+ await startServer(port, certPair);
236
+ step("✅", `Server running at https://localhost:${port}`);
237
+ } catch (err) {
238
+ step("❌", `Server failed to start: ${err.message}`);
239
+ process.exit(1);
240
+ }
241
+
242
+ // ── Step 5: OCR status ─────────────────────────────────────────────────
243
+ if (process.env.AZURE_MISTRAL_ENDPOINT && process.env.AZURE_MISTRAL_API_KEY) {
244
+ step("✅", "OCR ready (Azure Mistral)");
245
+ } else if (process.env.MISTRAL_API_KEY) {
246
+ step("✅", "OCR ready (Mistral direct)");
247
+ } else {
248
+ step("ℹ️", "OCR not configured — scanned PDFs won't be readable");
249
+ step(" ", "Set AZURE_MISTRAL_ENDPOINT + AZURE_MISTRAL_API_KEY in .env");
250
+ }
251
+
252
+ // ── Step 6: Folder picker strategy ────────────────────────────────────
253
+ const pickerStrategy = getFolderPickerStrategy();
254
+ const pickerLabels = {
255
+ "native-helper": "Native folder picker helper",
256
+ "powershell": "PowerShell folder picker (fallback)",
257
+ "osascript": "macOS folder picker (osascript)",
258
+ "manual-only": "Manual path entry only",
259
+ };
260
+ const pickerLabel = pickerLabels[pickerStrategy.method] || pickerStrategy.method;
261
+ if (pickerStrategy.method === "native-helper") {
262
+ step("✅", `Folder picker: ${pickerLabel}`);
263
+ } else if (pickerStrategy.method === "powershell" || pickerStrategy.method === "osascript") {
264
+ step("⚠️", `Folder picker: ${pickerLabel}`);
265
+ if (pickerStrategy.platform === "win32") {
266
+ step(" ", "Build the native helper for a better experience:");
267
+ step(" ", " npm run build:folder-picker:win");
268
+ }
269
+ } else {
270
+ step("ℹ️", `Folder picker: ${pickerLabel}`);
271
+ }
272
+
273
+ // ── Step 7: Auto-register add-in with Excel (first run) ─────────────
274
+ const manifestPath = resolve(__dirname, "..", "manifest", "manifest.xml");
275
+
276
+ if (existsSync(manifestPath)) {
277
+ let alreadyRegistered = false;
278
+ try {
279
+ const devSettings = await import("office-addin-dev-settings");
280
+ const registered = await devSettings.getRegisteredAddIns();
281
+ alreadyRegistered = registered.some(a => a.manifestPath === manifestPath);
282
+ } catch (_) {}
283
+
284
+ if (!alreadyRegistered) {
285
+ step("⏳", "First run — registering AgentXL with Excel...");
286
+ try {
287
+ const devSettings = await import("office-addin-dev-settings");
288
+ await devSettings.registerAddIn(manifestPath);
289
+ step("✅", "Add-in registered with Excel");
290
+ } catch (err) {
291
+ step("⚠️", `Auto-registration failed: ${err.message}`);
292
+ step(" ", "Run 'agentxl install' manually, or add the manifest folder to Excel Trust Center:");
293
+ step(" ", ` ${dirname(manifestPath)}`);
294
+ }
295
+
296
+ try {
297
+ const devSettings = await import("office-addin-dev-settings");
298
+ await devSettings.ensureLoopbackIsEnabled(manifestPath, false);
299
+ } catch (_) {}
300
+ } else {
301
+ step("✅", "Excel add-in registered");
302
+ }
303
+ }
304
+
305
+ // ── Post-start guidance ────────────────────────────────────────────────
306
+ console.log(`
307
+ ─────────────────────────────────────────────────
308
+ All systems go!
309
+ ─────────────────────────────────────────────────
310
+
311
+ 📎 Open Excel → AgentXL is on the Home ribbon
312
+ (If you don't see it: Insert → My Add-ins → SHARED FOLDER → AgentXL)
313
+
314
+ 🌐 Or test in browser first:
315
+ https://localhost:${port}/taskpane/
316
+
317
+ 💬 Try: "What can you help me with in this workbook?"
318
+ `);
319
+
320
+ // ── Graceful shutdown ──────────────────────────────────────────────────
321
+ let shuttingDown = false;
322
+ const shutdown = () => {
323
+ if (shuttingDown) return;
324
+ shuttingDown = true;
325
+ console.log("\n AgentXL stopped. Goodbye!\n");
326
+ const forceExit = setTimeout(() => process.exit(0), 2000);
327
+ forceExit.unref?.();
328
+ stopServer().then(() => process.exit(0));
329
+ };
330
+
331
+ process.on("SIGINT", shutdown);
332
+ process.on("SIGTERM", shutdown);
333
+ }
334
+
335
+ async function install() {
336
+ console.log(`
337
+ ┌──────────────────────────────────────┐
338
+ │ AgentXL Excel Registration │
339
+ └──────────────────────────────────────┘
340
+ `);
341
+
342
+ const manifestPath = resolve(__dirname, "..", "manifest", "manifest.xml");
343
+ if (!existsSync(manifestPath)) {
344
+ step("❌", `Manifest not found: ${manifestPath}`);
345
+ process.exit(1);
346
+ }
347
+
348
+ // ── Step 1: HTTPS certificates ─────────────────────────────────────────
349
+ try {
350
+ step("", "Ensuring HTTPS certificates are trusted...");
351
+ const devCerts = await import("office-addin-dev-certs");
352
+ await devCerts.ensureCertificatesAreInstalled();
353
+ step("", "HTTPS certificate trusted");
354
+ } catch (err) {
355
+ step("⚠️", `Certificate setup: ${err.message}`);
356
+ }
357
+
358
+ // ── Step 2: Register add-in ────────────────────────────────────────────
359
+ try {
360
+ step("⏳", "Registering AgentXL with Excel...");
361
+ const devSettings = await import("office-addin-dev-settings");
362
+ await devSettings.registerAddIn(manifestPath);
363
+ step("✅", "Add-in registered with Excel");
364
+ } catch (err) {
365
+ step("❌", `Registration failed: ${err.message}`);
366
+ console.log(`
367
+ Fallback: manually add this folder to Excel's Trusted Add-in Catalogs:
368
+ ${dirname(manifestPath)}
369
+
370
+ Steps:
371
+ 1. Excel → File → Options → Trust Center → Trust Center Settings
372
+ 2. Trusted Add-in Catalogs paste the path above
373
+ 3. Check "Show in Menu"OKOK
374
+ 4. Restart Excel
375
+ 5. Insert My Add-ins → SHARED FOLDER → AgentXL → Add
376
+ `);
377
+ process.exit(1);
378
+ }
379
+
380
+ // ── Step 3: Enable loopback ────────────────────────────────────────────
381
+ try {
382
+ const devSettings = await import("office-addin-dev-settings");
383
+ await devSettings.ensureLoopbackIsEnabled(manifestPath, false);
384
+ step("✅", "Localhost loopback enabled");
385
+ } catch (err) {
386
+ step("⚠️", `Loopback: ${err.message}`);
387
+ }
388
+
389
+ console.log(`
390
+ ─────────────────────────────────────────────────
391
+ Done! AgentXL is registered with Excel.
392
+ ─────────────────────────────────────────────────
393
+
394
+ Next steps:
395
+ 1. Run: agentxl start
396
+ 2. Open Excel → AgentXL appears on the Home ribbon
397
+
398
+ To open Excel with AgentXL right now:
399
+ agentxl install --open
400
+ `);
401
+
402
+ // Optionally open Excel
403
+ if (hasFlag("open")) {
404
+ try {
405
+ const devSettings = await import("office-addin-dev-settings");
406
+ const manifestLib = await import("office-addin-manifest");
407
+ step("⏳", "Opening Excel with AgentXL...");
408
+ await devSettings.sideloadAddIn(manifestPath, manifestLib.OfficeApp.Excel, false, devSettings.AppType.Desktop);
409
+ } catch (err) {
410
+ step("⚠️", `Could not open Excel: ${err.message}`);
411
+ }
412
+ }
413
+ }
414
+
415
+ async function login() {
416
+ console.log("");
417
+ const authed = await runAuthFlow();
418
+ if (authed) {
419
+ console.log(" Run 'agentxl start' to launch the server.\n");
420
+ }
421
+ process.exit(authed ? 0 : 1);
422
+ }
423
+
424
+ // ---------------------------------------------------------------------------
425
+ // Main
426
+ // ---------------------------------------------------------------------------
427
+
428
+ if (hasFlag("version") || command === "--version") {
429
+ console.log(VERSION);
430
+ process.exit(0);
431
+ }
432
+
433
+ if (hasFlag("help") || command === "--help" || command === "help") {
434
+ printHelp();
435
+ process.exit(0);
436
+ }
437
+
438
+ if (command === "start") {
439
+ start().catch((err) => {
440
+ console.error(`\n ❌ ${err.message || err}\n`);
441
+ process.exit(1);
442
+ });
443
+ } else if (command === "install") {
444
+ install().catch((err) => {
445
+ console.error(`\n ❌ ${err.message || err}\n`);
446
+ process.exit(1);
447
+ });
448
+ } else if (command === "login") {
449
+ login().catch((err) => {
450
+ console.error(`\n ❌ ${err.message || err}\n`);
451
+ process.exit(1);
452
+ });
453
+ } else if (!command) {
454
+ printHelp();
455
+ process.exit(0);
456
+ } else {
457
+ console.error(`Unknown command: ${command}`);
458
+ console.error(`Run 'agentxl --help' for usage.`);
459
+ process.exit(1);
460
+ }