dealdesk-plugin 0.0.0-stage → 0.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 (50) hide show
  1. package/.agents/plugins/marketplace.json +24 -0
  2. package/.claude-plugin/marketplace.json +29 -0
  3. package/.claude-plugin/plugin.json +18 -0
  4. package/.codex-plugin/plugin.json +30 -0
  5. package/.cursor-plugin/marketplace.json +29 -0
  6. package/.cursor-plugin/mcp.json +11 -0
  7. package/.cursor-plugin/plugin.json +18 -0
  8. package/.mcp.json +8 -0
  9. package/AGENTS.md +13 -0
  10. package/LICENSE +21 -0
  11. package/README.md +124 -3
  12. package/assets/logo-animated.svg +50 -0
  13. package/assets/logo.svg +8 -0
  14. package/bin/dealdesk-plugin.js +717 -0
  15. package/commands/dealdesk-craft-quote.md +15 -0
  16. package/commands/dealdesk-create-card.md +14 -0
  17. package/commands/dealdesk-directory-activity.md +12 -0
  18. package/commands/dealdesk-evaluate-configuration.md +10 -0
  19. package/commands/dealdesk-export-analysis.md +10 -0
  20. package/commands/dealdesk-find-or-create-customer.md +12 -0
  21. package/commands/dealdesk-help.md +62 -0
  22. package/commands/dealdesk-log-card-activity.md +13 -0
  23. package/commands/dealdesk-publish-portfolio.md +13 -0
  24. package/commands/dealdesk-quote-from-portfolio.md +12 -0
  25. package/commands/dealdesk-share-quote.md +13 -0
  26. package/mcp.json +9 -0
  27. package/package.json +59 -6
  28. package/plugin.json +36 -0
  29. package/scripts/bump-version.js +56 -0
  30. package/scripts/package-plugin-zip.js +130 -0
  31. package/scripts/test-project-scope.js +29 -0
  32. package/scripts/validate-npm-release.js +23 -0
  33. package/scripts/validate-skills.js +56 -0
  34. package/scripts/version-files.js +10 -0
  35. package/scripts/zip-archive.js +139 -0
  36. package/skills/README.md +30 -0
  37. package/skills/dealdesk-craft-quote/SKILL.md +21 -0
  38. package/skills/dealdesk-create-card/SKILL.md +59 -0
  39. package/skills/dealdesk-directory-activity/SKILL.md +14 -0
  40. package/skills/dealdesk-evaluate-configuration/SKILL.md +10 -0
  41. package/skills/dealdesk-export-analysis/SKILL.md +11 -0
  42. package/skills/dealdesk-find-or-create-customer/SKILL.md +14 -0
  43. package/skills/dealdesk-help/SKILL.md +63 -0
  44. package/skills/dealdesk-log-card-activity/SKILL.md +36 -0
  45. package/skills/dealdesk-manage-cases/SKILL.md +29 -0
  46. package/skills/dealdesk-manage-orders/SKILL.md +12 -0
  47. package/skills/dealdesk-portfolio-browse/SKILL.md +12 -0
  48. package/skills/dealdesk-publish-portfolio/SKILL.md +16 -0
  49. package/skills/dealdesk-quote-from-portfolio/SKILL.md +15 -0
  50. package/skills/dealdesk-share-quote/SKILL.md +16 -0
@@ -0,0 +1,717 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * DealDesk plugin local connector for agenthouse.
6
+ * Speaks MCP over stdio and connects to the agenthouse DealDesk MCP endpoint.
7
+ *
8
+ * Sign-in is OAuth: the connector opens the agenthouse page, the user chooses
9
+ * the project, and the grant is stored for later launches.
10
+ *
11
+ * Env:
12
+ * AGENTHOUSE_API_URL (default https://api.agenthouse.org)
13
+ * AGENTHOUSE_API_KEY optional override for automation that cannot open a browser
14
+ * AGENTHOUSE_PROJECT_ID optional tenant id when a tool omits projectId
15
+ */
16
+
17
+ const http = require('http');
18
+ const https = require('https');
19
+ const crypto = require('crypto');
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const { URL } = require('url');
24
+ const { spawn } = require('child_process');
25
+ const readline = require('readline');
26
+
27
+ const API_URL = String(process.env.AGENTHOUSE_API_URL || 'https://api.agenthouse.org').replace(/\/$/, '');
28
+ const API_KEY = String(process.env.AGENTHOUSE_API_KEY || process.env.AGENTHOUSE_TOKEN || '').trim();
29
+ const PROJECT_ID = String(process.env.AGENTHOUSE_PROJECT_ID || '').trim();
30
+ const SCOPE = 'dealdesk:access';
31
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
32
+ const SESSION_PATH = path.join(os.homedir(), '.agenthouse', 'dealdesk-oauth.json');
33
+ const LOCK_PATH = path.join(os.homedir(), '.agenthouse', 'dealdesk-oauth.lock');
34
+ const ERROR_PATH = path.join(os.homedir(), '.agenthouse', 'dealdesk-oauth.error');
35
+ const URL_PATH = path.join(os.homedir(), '.agenthouse', 'dealdesk-oauth.url');
36
+
37
+ const session = {
38
+ accessToken: '',
39
+ refreshToken: '',
40
+ expiresAt: 0,
41
+ clientId: '',
42
+ redirectUri: '',
43
+ projectId: PROJECT_ID,
44
+ apiUrl: API_URL
45
+ };
46
+
47
+ let authorizeUrl = '';
48
+ let oauthMeta = null;
49
+
50
+ function log(msg) {
51
+ process.stderr.write(`[dealdesk-plugin] ${msg}\n`);
52
+ }
53
+
54
+ function resourceUrl() {
55
+ return `${API_URL}/mcp/dealdesk`;
56
+ }
57
+
58
+ function requestRaw(targetUrl, options) {
59
+ const target = new URL(targetUrl);
60
+ const lib = target.protocol === 'http:' ? http : https;
61
+ const body = options.body || null;
62
+ const headers = Object.assign({}, options.headers || {});
63
+ if (body != null && headers['Content-Length'] == null) {
64
+ headers['Content-Length'] = Buffer.byteLength(body);
65
+ }
66
+ return new Promise((resolve, reject) => {
67
+ const req = lib.request({
68
+ protocol: target.protocol,
69
+ hostname: target.hostname,
70
+ port: target.port || (target.protocol === 'http:' ? 80 : 443),
71
+ path: `${target.pathname}${target.search}`,
72
+ method: options.method || 'GET',
73
+ headers
74
+ }, (res) => {
75
+ let data = '';
76
+ res.setEncoding('utf8');
77
+ res.on('data', (chunk) => { data += chunk; });
78
+ res.on('end', () => {
79
+ resolve({ statusCode: res.statusCode || 0, body: data });
80
+ });
81
+ });
82
+ req.on('error', reject);
83
+ if (body != null) req.write(body);
84
+ req.end();
85
+ });
86
+ }
87
+
88
+ async function requestJson(targetUrl, options) {
89
+ const response = await requestRaw(targetUrl, options || {});
90
+ let json = null;
91
+ if (response.body) {
92
+ try {
93
+ json = JSON.parse(response.body);
94
+ } catch (err) {
95
+ json = null;
96
+ }
97
+ }
98
+ return { statusCode: response.statusCode, body: response.body, json };
99
+ }
100
+
101
+ function pickProjectId(data, depth) {
102
+ if (!data || typeof data !== 'object' || (depth || 0) > 5) return {};
103
+ if (!Array.isArray(data)) {
104
+ const listed = Array.isArray(data.projectIds)
105
+ ? data.projectIds
106
+ : (Array.isArray(data.project_ids) ? data.project_ids : null);
107
+ if (Array.isArray(listed)) {
108
+ const ids = listed.map((value) => String(value || '').trim()).filter(Boolean);
109
+ if (ids.length > 1) return { ambiguous: true, ids };
110
+ if (ids.length === 1) return { id: ids[0] };
111
+ }
112
+ for (const key of ['projectId', 'project_id', 'tenantId', 'tenant_id', 'currentProjectId']) {
113
+ if (typeof data[key] === 'string' && data[key].trim()) return { id: data[key].trim() };
114
+ }
115
+ if (data.project && typeof data.project === 'object') {
116
+ const nested = pickProjectId(data.project, (depth || 0) + 1);
117
+ if (nested.id || nested.ambiguous) return nested;
118
+ }
119
+ }
120
+ const list = Array.isArray(data) ? data : (data.projects || data.items || null);
121
+ if (Array.isArray(list)) {
122
+ if (list.length > 1) return { ambiguous: true };
123
+ if (list.length === 1 && list[0] && typeof list[0] === 'object') {
124
+ return pickProjectId({
125
+ projectId: list[0].projectId || list[0].project_id || list[0].id
126
+ }, (depth || 0) + 1);
127
+ }
128
+ }
129
+ if (!Array.isArray(data) && data.data && data.data !== data) {
130
+ return pickProjectId(data.data, (depth || 0) + 1);
131
+ }
132
+ return {};
133
+ }
134
+
135
+ function projectFromAccessToken(token) {
136
+ const parts = String(token || '').split('.');
137
+ if (parts.length < 2) return {};
138
+ try {
139
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
140
+ return pickProjectId(payload);
141
+ } catch (err) {
142
+ return {};
143
+ }
144
+ }
145
+
146
+ function validAccess() {
147
+ return Boolean(session.accessToken) && session.expiresAt - 60000 > Date.now();
148
+ }
149
+
150
+ function loadSession() {
151
+ let raw;
152
+ try {
153
+ raw = fs.readFileSync(SESSION_PATH, 'utf8');
154
+ } catch (err) {
155
+ return;
156
+ }
157
+ let saved;
158
+ try {
159
+ saved = JSON.parse(raw);
160
+ } catch (err) {
161
+ return;
162
+ }
163
+ if (!saved || saved.apiUrl !== API_URL) return;
164
+ session.accessToken = String(saved.accessToken || '');
165
+ session.refreshToken = String(saved.refreshToken || '');
166
+ session.expiresAt = Number(saved.expiresAt) || 0;
167
+ session.clientId = String(saved.clientId || '');
168
+ session.redirectUri = String(saved.redirectUri || '');
169
+ if (!PROJECT_ID && saved.projectId) session.projectId = String(saved.projectId);
170
+ }
171
+
172
+ function persistSession() {
173
+ const dir = path.dirname(SESSION_PATH);
174
+ fs.mkdirSync(dir, { recursive: true });
175
+ const payload = {
176
+ apiUrl: API_URL,
177
+ accessToken: session.accessToken,
178
+ refreshToken: session.refreshToken,
179
+ expiresAt: session.expiresAt,
180
+ clientId: session.clientId,
181
+ redirectUri: session.redirectUri,
182
+ projectId: session.projectId || ''
183
+ };
184
+ const tmp = `${SESSION_PATH}.tmp`;
185
+ fs.writeFileSync(tmp, JSON.stringify(payload), { mode: 0o600 });
186
+ fs.renameSync(tmp, SESSION_PATH);
187
+ }
188
+
189
+ function applyTokenResponse(body) {
190
+ if (!body || !body.access_token) {
191
+ throw new Error('agenthouse did not return an access token');
192
+ }
193
+ session.accessToken = String(body.access_token);
194
+ if (body.refresh_token) session.refreshToken = String(body.refresh_token);
195
+ const expiresIn = Number(body.expires_in);
196
+ session.expiresAt = Date.now() + ((Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) * 1000);
197
+ const picked = pickProjectId(body);
198
+ const fromJwt = projectFromAccessToken(session.accessToken);
199
+ if (!PROJECT_ID) {
200
+ if (picked.ambiguous || fromJwt.ambiguous) session.projectId = '';
201
+ else session.projectId = picked.id || fromJwt.id || session.projectId || '';
202
+ }
203
+ persistSession();
204
+ }
205
+
206
+ async function discover() {
207
+ if (oauthMeta) return oauthMeta;
208
+ const fallback = {
209
+ authorization_endpoint: `${API_URL}/api/oauth/authorize`,
210
+ token_endpoint: `${API_URL}/api/oauth/token`,
211
+ registration_endpoint: `${API_URL}/api/oauth/register`
212
+ };
213
+ try {
214
+ const response = await requestJson(`${API_URL}/.well-known/oauth-authorization-server`);
215
+ const meta = response.json;
216
+ if (response.statusCode >= 200 && response.statusCode < 300 && meta && meta.authorization_endpoint && meta.token_endpoint) {
217
+ oauthMeta = meta;
218
+ return oauthMeta;
219
+ }
220
+ } catch (err) {
221
+ log(`oauth discovery: ${err.message}`);
222
+ }
223
+ oauthMeta = fallback;
224
+ return oauthMeta;
225
+ }
226
+
227
+ async function tokenRequest(fields) {
228
+ const meta = await discover();
229
+ const body = new URLSearchParams(fields).toString();
230
+ const response = await requestJson(meta.token_endpoint, {
231
+ method: 'POST',
232
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
233
+ body
234
+ });
235
+ if (response.statusCode < 200 || response.statusCode >= 300 || !response.json || !response.json.access_token) {
236
+ const description = response.json && (response.json.error_description || response.json.error);
237
+ throw new Error(description || `token request failed (${response.statusCode})`);
238
+ }
239
+ return response.json;
240
+ }
241
+
242
+ async function refreshSession() {
243
+ if (!session.refreshToken || !session.clientId) return false;
244
+ try {
245
+ const body = await tokenRequest({
246
+ grant_type: 'refresh_token',
247
+ refresh_token: session.refreshToken,
248
+ client_id: session.clientId,
249
+ resource: resourceUrl()
250
+ });
251
+ applyTokenResponse(body);
252
+ return true;
253
+ } catch (err) {
254
+ log(`Could not refresh DealDesk sign-in: ${err.message}`);
255
+ session.accessToken = '';
256
+ session.refreshToken = '';
257
+ session.expiresAt = 0;
258
+ return false;
259
+ }
260
+ }
261
+
262
+ async function discoverProjectId() {
263
+ if (session.projectId) return session.projectId;
264
+ const fromJwt = projectFromAccessToken(session.accessToken);
265
+ if (fromJwt.id) {
266
+ session.projectId = fromJwt.id;
267
+ persistSession();
268
+ return session.projectId;
269
+ }
270
+ if (fromJwt.ambiguous) {
271
+ log('This account has more than one project. Set AGENTHOUSE_PROJECT_ID to the project you want.');
272
+ return '';
273
+ }
274
+ for (const suffix of ['/api/me', '/api/projects']) {
275
+ try {
276
+ const response = await requestJson(`${API_URL}${suffix}`, {
277
+ headers: { Authorization: `Bearer ${session.accessToken}`, Accept: 'application/json' }
278
+ });
279
+ if (response.statusCode < 200 || response.statusCode >= 300) continue;
280
+ const picked = pickProjectId(response.json);
281
+ if (picked.id) {
282
+ session.projectId = picked.id;
283
+ persistSession();
284
+ return session.projectId;
285
+ }
286
+ if (picked.ambiguous) {
287
+ log('This account has more than one project. Set AGENTHOUSE_PROJECT_ID to the project you want.');
288
+ return '';
289
+ }
290
+ } catch (err) {
291
+ log(`project lookup ${suffix}: ${err.message}`);
292
+ }
293
+ }
294
+ return '';
295
+ }
296
+
297
+ function openBrowser(url) {
298
+ return new Promise((resolve) => {
299
+ let child;
300
+ if (process.platform === 'win32') {
301
+ child = spawn('rundll32.exe', ['url.dll,FileProtocolHandler', url], {
302
+ detached: true,
303
+ stdio: 'ignore',
304
+ windowsHide: true
305
+ });
306
+ } else if (process.platform === 'darwin') {
307
+ child = spawn('open', [url], { detached: true, stdio: 'ignore' });
308
+ } else {
309
+ child = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
310
+ }
311
+ child.on('error', (err) => {
312
+ log(`Could not open the browser (${err.message}). Sign in at: ${url}`);
313
+ resolve(false);
314
+ });
315
+ child.on('spawn', () => resolve(true));
316
+ child.unref();
317
+ });
318
+ }
319
+
320
+ function pidAlive(pid) {
321
+ if (!Number.isInteger(pid) || pid <= 0) return false;
322
+ try {
323
+ process.kill(pid, 0);
324
+ return true;
325
+ } catch (err) {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ function readLock() {
331
+ let data;
332
+ try {
333
+ data = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf8'));
334
+ } catch (err) {
335
+ return null;
336
+ }
337
+ if (!data || data.apiUrl !== API_URL) return null;
338
+ if (data.pid === 'pending' && Date.now() - Number(data.at || 0) < 15000) return data;
339
+ if (pidAlive(Number(data.pid))) return data;
340
+ return null;
341
+ }
342
+
343
+ function spawnLoginHelper() {
344
+ if (readLock()) return;
345
+ fs.mkdirSync(path.dirname(LOCK_PATH), { recursive: true });
346
+ try {
347
+ const fd = fs.openSync(LOCK_PATH, 'wx');
348
+ fs.writeFileSync(fd, JSON.stringify({ pid: 'pending', apiUrl: API_URL, at: Date.now() }));
349
+ fs.closeSync(fd);
350
+ } catch (err) {
351
+ if (err.code === 'EEXIST') {
352
+ if (readLock()) return;
353
+ try { fs.unlinkSync(LOCK_PATH); } catch (unlinkErr) { return; }
354
+ spawnLoginHelper();
355
+ return;
356
+ }
357
+ throw err;
358
+ }
359
+ try { fs.unlinkSync(ERROR_PATH); } catch (err) { /* no previous error */ }
360
+ try { fs.unlinkSync(URL_PATH); } catch (err) { /* no previous url */ }
361
+ const child = spawn(process.execPath, [__filename, '--login'], {
362
+ detached: true,
363
+ stdio: 'ignore',
364
+ windowsHide: true,
365
+ env: process.env
366
+ });
367
+ child.on('error', (err) => {
368
+ try { fs.unlinkSync(LOCK_PATH); } catch (unlinkErr) { /* lock already removed */ }
369
+ try { fs.writeFileSync(ERROR_PATH, err.message); } catch (writeErr) { /* best effort */ }
370
+ log(`Could not open DealDesk sign-in: ${err.message}`);
371
+ });
372
+ child.unref();
373
+ log('Opening the agenthouse sign-in page for DealDesk.');
374
+ }
375
+
376
+ function listenLoopback(port) {
377
+ const server = http.createServer();
378
+ return new Promise((resolve, reject) => {
379
+ const onError = (err) => {
380
+ server.removeListener('listening', onListening);
381
+ reject(err);
382
+ };
383
+ const onListening = () => {
384
+ server.removeListener('error', onError);
385
+ resolve(server);
386
+ };
387
+ server.once('error', onError);
388
+ server.once('listening', onListening);
389
+ server.listen(port, '127.0.0.1');
390
+ });
391
+ }
392
+
393
+ function htmlPage(title, message) {
394
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><title>${title}</title></head><body style="font:16px/1.4 system-ui,sans-serif;padding:32px"><h1>${title}</h1><p>${message}</p></body></html>`;
395
+ }
396
+
397
+ function safeEqual(a, b) {
398
+ const left = Buffer.from(String(a));
399
+ const right = Buffer.from(String(b));
400
+ if (left.length !== right.length) return false;
401
+ return crypto.timingSafeEqual(left, right);
402
+ }
403
+
404
+ async function interactiveLogin() {
405
+ const meta = await discover();
406
+ const savedPort = session.redirectUri ? Number(new URL(session.redirectUri).port) : 0;
407
+ let server;
408
+ try {
409
+ server = await listenLoopback(savedPort || 8731);
410
+ } catch (err) {
411
+ if (err.code !== 'EADDRINUSE') throw err;
412
+ server = await listenLoopback(0);
413
+ }
414
+ const port = server.address().port;
415
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
416
+ try {
417
+ if (!session.clientId || session.redirectUri !== redirectUri) {
418
+ if (!meta.registration_endpoint) throw new Error('agenthouse OAuth registration is unavailable');
419
+ const created = await requestJson(meta.registration_endpoint, {
420
+ method: 'POST',
421
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
422
+ body: JSON.stringify({
423
+ client_name: 'DealDesk',
424
+ redirect_uris: [redirectUri],
425
+ grant_types: ['authorization_code', 'refresh_token'],
426
+ response_types: ['code'],
427
+ token_endpoint_auth_method: 'none',
428
+ application_type: 'native',
429
+ scope: SCOPE
430
+ })
431
+ });
432
+ if (!created.json || !created.json.client_id) {
433
+ throw new Error('Could not register DealDesk for sign-in');
434
+ }
435
+ session.clientId = created.json.client_id;
436
+ session.redirectUri = redirectUri;
437
+ persistSession();
438
+ }
439
+
440
+ const verifier = crypto.randomBytes(32).toString('base64url');
441
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
442
+ const state = crypto.randomBytes(16).toString('base64url');
443
+ const authorize = new URL(meta.authorization_endpoint);
444
+ authorize.searchParams.set('response_type', 'code');
445
+ authorize.searchParams.set('client_id', session.clientId);
446
+ authorize.searchParams.set('redirect_uri', redirectUri);
447
+ authorize.searchParams.set('scope', SCOPE);
448
+ authorize.searchParams.set('state', state);
449
+ authorize.searchParams.set('code_challenge', challenge);
450
+ authorize.searchParams.set('code_challenge_method', 'S256');
451
+ authorize.searchParams.set('resource', resourceUrl());
452
+ authorizeUrl = authorize.toString();
453
+ try {
454
+ fs.writeFileSync(URL_PATH, authorizeUrl, { mode: 0o600 });
455
+ } catch (err) {
456
+ log(`Could not record the sign-in page: ${err.message}`);
457
+ }
458
+
459
+ const code = await new Promise((resolve, reject) => {
460
+ const timer = setTimeout(() => {
461
+ reject(new Error('DealDesk sign-in timed out. Start the connector again when you are ready to sign in.'));
462
+ }, LOGIN_TIMEOUT_MS);
463
+ const finish = (err, value) => {
464
+ clearTimeout(timer);
465
+ server.removeListener('request', onRequest);
466
+ if (err) reject(err);
467
+ else resolve(value);
468
+ };
469
+ const onRequest = (req, res) => {
470
+ const incoming = new URL(req.url || '/', redirectUri);
471
+ if (incoming.pathname !== '/callback') {
472
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
473
+ res.end('Not found');
474
+ return;
475
+ }
476
+ const returnedState = incoming.searchParams.get('state') || '';
477
+ if (!safeEqual(returnedState, state)) {
478
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
479
+ res.end(htmlPage('DealDesk', 'This sign-in attempt did not match. Return to the app and try again.'));
480
+ return;
481
+ }
482
+ const oauthError = incoming.searchParams.get('error');
483
+ if (oauthError) {
484
+ const description = incoming.searchParams.get('error_description') || oauthError;
485
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
486
+ res.end(htmlPage('DealDesk', 'Sign-in was not completed. You can close this window.'));
487
+ finish(new Error(description));
488
+ return;
489
+ }
490
+ const authCode = incoming.searchParams.get('code');
491
+ if (!authCode) {
492
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
493
+ res.end(htmlPage('DealDesk', 'Sign-in did not return a code. You can close this window and try again.'));
494
+ return;
495
+ }
496
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
497
+ res.end(htmlPage('DealDesk connected', 'You are signed in. You can close this window and return to your assistant.'));
498
+ finish(null, authCode);
499
+ };
500
+ server.on('request', onRequest);
501
+ openBrowser(authorizeUrl).then((opened) => {
502
+ if (!opened) log(`Sign in at: ${authorizeUrl}`);
503
+ });
504
+ });
505
+
506
+ const body = await tokenRequest({
507
+ grant_type: 'authorization_code',
508
+ code,
509
+ redirect_uri: redirectUri,
510
+ client_id: session.clientId,
511
+ code_verifier: verifier,
512
+ resource: resourceUrl()
513
+ });
514
+ applyTokenResponse(body);
515
+ await discoverProjectId();
516
+ log('DealDesk sign-in completed.');
517
+ return session.accessToken;
518
+ } finally {
519
+ if (server) server.close();
520
+ }
521
+ }
522
+
523
+ async function bootstrap() {
524
+ if (API_KEY) {
525
+ log(`connected to ${API_URL}`);
526
+ return;
527
+ }
528
+ loadSession();
529
+ if (validAccess()) {
530
+ await discoverProjectId();
531
+ log(`using saved DealDesk sign-in for ${API_URL}`);
532
+ return;
533
+ }
534
+ if (await refreshSession()) {
535
+ await discoverProjectId();
536
+ log(`refreshed DealDesk sign-in for ${API_URL}`);
537
+ return;
538
+ }
539
+ spawnLoginHelper();
540
+ }
541
+
542
+ async function loginMain() {
543
+ loadSession();
544
+ fs.mkdirSync(path.dirname(LOCK_PATH), { recursive: true });
545
+ fs.writeFileSync(LOCK_PATH, JSON.stringify({ pid: process.pid, apiUrl: API_URL, at: Date.now() }));
546
+ try {
547
+ if (!validAccess() && !(await refreshSession())) await interactiveLogin();
548
+ else await discoverProjectId();
549
+ try { fs.unlinkSync(ERROR_PATH); } catch (err) { /* clear stale error */ }
550
+ } catch (err) {
551
+ const message = String(err && err.message || err);
552
+ try { fs.writeFileSync(ERROR_PATH, message); } catch (writeErr) { log(writeErr.message); }
553
+ log(message);
554
+ process.exitCode = 1;
555
+ } finally {
556
+ try { fs.unlinkSync(LOCK_PATH); } catch (err) { /* lock already removed */ }
557
+ }
558
+ }
559
+
560
+ let bootPromise = null;
561
+ function boot() {
562
+ if (!bootPromise) bootPromise = bootstrap();
563
+ return bootPromise;
564
+ }
565
+
566
+ async function getBearer() {
567
+ await boot();
568
+ if (API_KEY) return API_KEY;
569
+ if (validAccess()) return session.accessToken;
570
+ spawnLoginHelper();
571
+ let loggedUrl = false;
572
+ const started = Date.now();
573
+ while (Date.now() - started < LOGIN_TIMEOUT_MS) {
574
+ loadSession();
575
+ if (validAccess()) {
576
+ await discoverProjectId();
577
+ log('DealDesk sign-in is ready.');
578
+ return session.accessToken;
579
+ }
580
+ if (!loggedUrl) {
581
+ try {
582
+ const url = fs.readFileSync(URL_PATH, 'utf8').trim();
583
+ if (url) {
584
+ log(`Sign in at: ${url}`);
585
+ loggedUrl = true;
586
+ }
587
+ } catch (err) { /* page not ready yet */ }
588
+ }
589
+ let errorText = '';
590
+ try { errorText = fs.readFileSync(ERROR_PATH, 'utf8').trim(); } catch (err) { /* still signing in */ }
591
+ if (errorText && !readLock()) throw new Error(errorText);
592
+ await new Promise((resolve) => setTimeout(resolve, 400));
593
+ }
594
+ throw new Error('DealDesk sign-in timed out. Finish the agenthouse page, then try again.');
595
+ }
596
+
597
+ function postMcp(messages, token) {
598
+ const body = (Array.isArray(messages) ? messages : [messages])
599
+ .map((message) => JSON.stringify(message))
600
+ .join('\n') + '\n';
601
+ const headers = {
602
+ 'Content-Type': 'application/json',
603
+ Accept: 'application/json, text/event-stream',
604
+ 'Content-Length': Buffer.byteLength(body)
605
+ };
606
+ if (token) headers.Authorization = `Bearer ${token}`;
607
+ return requestRaw(resourceUrl(), { method: 'POST', headers, body }).then((response) => {
608
+ if (response.statusCode === 401) {
609
+ let description = 'Unauthorized';
610
+ try {
611
+ const parsed = JSON.parse(response.body);
612
+ description = parsed.error_description || parsed.error || description;
613
+ } catch (err) {
614
+ description = response.body || description;
615
+ }
616
+ const error = new Error(description);
617
+ error.statusCode = 401;
618
+ throw error;
619
+ }
620
+ const lines = response.body.split('\n').map((line) => line.trim()).filter(Boolean);
621
+ const parsed = [];
622
+ for (const line of lines) {
623
+ try {
624
+ parsed.push(JSON.parse(line));
625
+ } catch (err) {
626
+ log(`parse response: ${err.message}`);
627
+ }
628
+ }
629
+ if (!parsed.length && response.statusCode >= 400) {
630
+ throw new Error(response.body || `DealDesk request failed (${response.statusCode})`);
631
+ }
632
+ return parsed;
633
+ });
634
+ }
635
+
636
+ function injectProject(msg) {
637
+ if (!msg || typeof msg !== 'object') return msg;
638
+ if (msg.method === 'tools/call' && msg.params) {
639
+ const args = msg.params.arguments || {};
640
+ const projectId = PROJECT_ID || session.projectId;
641
+ if (!args.projectId && projectId) {
642
+ msg.params.arguments = Object.assign({}, args, { projectId });
643
+ }
644
+ }
645
+ return msg;
646
+ }
647
+
648
+ function needsAuth(batch) {
649
+ return batch.some((message) => message && message.method && message.method !== 'initialize' && !String(message.method).startsWith('notifications/'));
650
+ }
651
+
652
+ async function postAuthed(batch) {
653
+ const token = await getBearer();
654
+ try {
655
+ return await postMcp(batch, token);
656
+ } catch (err) {
657
+ if (err.statusCode === 401 && !API_KEY) {
658
+ session.accessToken = '';
659
+ session.expiresAt = 0;
660
+ if (await refreshSession()) return postMcp(batch, session.accessToken);
661
+ }
662
+ throw err;
663
+ }
664
+ }
665
+
666
+ async function handleLine(line) {
667
+ const trimmed = line.trim();
668
+ if (!trimmed) return;
669
+ let msg;
670
+ try {
671
+ msg = JSON.parse(trimmed);
672
+ } catch (err) {
673
+ process.stdout.write(JSON.stringify({
674
+ jsonrpc: '2.0',
675
+ id: null,
676
+ error: { code: -32700, message: 'Parse error' }
677
+ }) + '\n');
678
+ return;
679
+ }
680
+ const batch = Array.isArray(msg) ? msg.map(injectProject) : [injectProject(msg)];
681
+ try {
682
+ const responses = needsAuth(batch)
683
+ ? await postAuthed(batch)
684
+ : await postMcp(batch, API_KEY || (validAccess() ? session.accessToken : ''));
685
+ for (const response of responses) {
686
+ process.stdout.write(JSON.stringify(response) + '\n');
687
+ }
688
+ } catch (err) {
689
+ log(String(err && err.message || err));
690
+ for (const message of batch) {
691
+ if (message && message.id !== undefined && message.id !== null) {
692
+ process.stdout.write(JSON.stringify({
693
+ jsonrpc: '2.0',
694
+ id: message.id,
695
+ error: { code: -32000, message: String(err && err.message || err) }
696
+ }) + '\n');
697
+ }
698
+ }
699
+ }
700
+ }
701
+
702
+ function main() {
703
+ if (process.argv.includes('--login')) {
704
+ loginMain().then(() => process.exit(process.exitCode || 0));
705
+ return;
706
+ }
707
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
708
+ rl.on('line', (line) => {
709
+ handleLine(line).catch((err) => log(String(err && err.message || err)));
710
+ });
711
+ rl.on('close', () => process.exit(0));
712
+ boot().catch((err) => log(String(err && err.message || err)));
713
+ }
714
+
715
+ if (require.main === module) main();
716
+
717
+ module.exports = { pickProjectId, projectFromAccessToken };