@askthew/mcp-plugin 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,783 +1,315 @@
1
1
  #!/usr/bin/env node
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { execFileSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
- import { createAskTheWMcpServer, runInitializeHandshake } from "./index.js";
8
- import { askTheWDataDir, ensureAskTheWDataDir, identityPath } from "./lib/paths.js";
9
- import { loadCliCredentials } from "./lib/free-tier-policy.js";
10
- import { describeFreeIdentity, tryRegisterFreeInstall } from "./lib/free-install-registration.js";
11
- import { ensureLocalIdentity, loadLocalIdentity, publicIdentity } from "./lib/local-identity.js";
12
- import { LocalStore } from "./lib/local-store.js";
13
- import { buildTelemetryPayload } from "./lib/telemetry.js";
14
- import { syncDryRun, uploadLocalStore } from "./lib/upgrade-sync.js";
15
- import { installPreCommitHook, localScopeKey, preCommitDecisionGap, stagedFiles, writeWeeklyDigest, } from "./lib/cli-actions.js";
16
- import { createHostConfigSnippet, defaultServerNameForTier, findInstallReceipts, formatInstallCommand, installBehaviorInstructions, installHostConfig, packageVersion, removeInstallReceipts, sendInstallHeartbeat, uninstallBehaviorInstructions, uninstallHostConfig, upgradePinnedHostConfig, writeInstallReceipt, } from "./install.js";
2
+ import { spawn } from "node:child_process";
3
+ import prompts from "prompts";
4
+ import { AskTheWCloudClient, completeSignup, loadCloudToken, saveCloudToken, startSignup } from "./cloud-client.js";
5
+ import { Outbox } from "./outbox.js";
6
+ import { DEFAULT_SERVER_NAME, dataDirSummary, installHostConfig, packageVersion, writeBehaviorInstructions, writeInstallMetadata, } from "./install.js";
7
+ import { runStdioServer } from "./index.js";
17
8
  function usage() {
18
9
  return [
19
- "Ask The W Coding Agent Connector",
10
+ "Ask The W MCP",
20
11
  "",
21
12
  "Usage:",
22
- " askthew-mcp [stdio MCP server when stdin is piped]",
13
+ " askthew-mcp",
14
+ " askthew-mcp install --host <claude_code|codex|cursor> [--bind <paid-bind-token>] [--skip-auth]",
15
+ " askthew-mcp bind [--allow-pending]",
16
+ " askthew-mcp token rotate|revoke",
17
+ " askthew-mcp export",
18
+ " askthew-mcp delete-me --confirm",
23
19
  " askthew-mcp --version",
24
- " askthew-mcp doctor [--server-entrypoint <path>]",
25
- " askthew-mcp install --host <claude_code|codex|cursor> --token <install-token> --api-url <url> [--server-name <name>] [--client-id <id>] [--client-label <label>] [--server-entrypoint <path>] [--dry-run] [--no-agent-instructions]",
26
- " askthew-mcp install --host <claude_code|codex|cursor> --free [--email <email>] [--api-url <url>] [--server-name <name>] [--server-entrypoint <path>] [--dry-run]",
27
- " askthew-mcp refresh --host <claude_code|codex|cursor> [--free] [--token <install-token>] [--api-url <url>] [--server-name <name>] [--server-entrypoint <path>] [--dry-run]",
28
- " askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>] [--dry-run] [--keep-local-data] [--keep-auth] [--keep-agent-instructions]",
29
- " askthew-mcp upgrade --host <claude_code|codex|cursor> [--server-name <name>] [--version <version>] [--dry-run]",
30
- " askthew-mcp upgrade --browser",
31
- " askthew-mcp identify --email <email> [--no-telemetry]",
32
- " askthew-mcp identity status",
33
- " askthew-mcp auth login --email <email> [--no-telemetry]",
34
- " askthew-mcp auth logout | status",
35
- " askthew-mcp telemetry status | opt-out | opt-in | preview",
36
- " askthew-mcp local stats | reset --hard",
37
- " askthew-mcp install-hook --pre-commit",
38
- " askthew-mcp digest --weekly",
39
- " askthew-mcp sync upload [--token <workspace-install-token>] [--dry-run]",
40
- " askthew-mcp print-config --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>]",
41
20
  ].join("\n");
42
21
  }
43
- function parseInstallArgs(argv) {
44
- let hostType;
45
- let clientId = process.env.ASKTHEW_CLIENT_ID?.trim() || "";
46
- let clientLabel = process.env.ASKTHEW_CLIENT_LABEL?.trim() || "";
47
- let token = normalizeInstallToken(process.env.ASKTHEW_INSTALL_TOKEN) || "";
48
- let apiUrl = process.env.ASKTHEW_API_URL?.trim() || "";
49
- let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || "";
50
- let serverNameExplicit = Boolean(serverName);
51
- let dryRun = false;
52
- let installAgentInstructions = true;
53
- let free = false;
54
- let email = process.env.ASKTHEW_EMAIL?.trim() || "";
55
- let serverEntrypoint = process.env.ASKTHEW_SERVER_ENTRYPOINT?.trim() || "";
22
+ function parseArgs(argv) {
23
+ const result = {};
56
24
  for (let index = 0; index < argv.length; index += 1) {
57
- const argument = argv[index];
58
- if (argument === "--dry-run") {
59
- dryRun = true;
60
- continue;
61
- }
62
- if (argument === "--no-agent-instructions") {
63
- installAgentInstructions = false;
64
- continue;
65
- }
66
- if (argument === "--free") {
67
- free = true;
25
+ const arg = argv[index];
26
+ if (!arg.startsWith("--")) {
27
+ result._ = [String(result._ ?? ""), arg].filter(Boolean).join(" ");
68
28
  continue;
69
29
  }
70
30
  const next = argv[index + 1];
71
- if (!next) {
72
- throw new Error(`Missing value for ${argument}.`);
73
- }
74
- if (argument === "--host") {
75
- if (next !== "claude_code" && next !== "codex" && next !== "cursor") {
76
- throw new Error(`Unsupported host "${next}". Expected claude_code, codex, or cursor.`);
77
- }
78
- hostType = next;
79
- index += 1;
80
- continue;
81
- }
82
- if (argument === "--client-id") {
83
- clientId = next;
84
- index += 1;
85
- continue;
86
- }
87
- if (argument === "--client-label") {
88
- clientLabel = next;
89
- index += 1;
90
- continue;
91
- }
92
- if (argument === "--token") {
93
- token = normalizeInstallToken(next);
94
- index += 1;
31
+ if (!next || next.startsWith("--")) {
32
+ result[arg] = true;
95
33
  continue;
96
34
  }
97
- if (argument === "--api-url") {
98
- apiUrl = next;
99
- index += 1;
100
- continue;
101
- }
102
- if (argument === "--server-name") {
103
- serverName = next;
104
- serverNameExplicit = true;
105
- index += 1;
106
- continue;
107
- }
108
- if (argument === "--email") {
109
- email = next;
110
- index += 1;
111
- continue;
112
- }
113
- if (argument === "--server-entrypoint") {
114
- serverEntrypoint = next;
115
- index += 1;
116
- continue;
117
- }
118
- throw new Error(`Unknown argument: ${argument}`);
119
- }
120
- if (!hostType) {
121
- throw new Error("Missing required --host argument.");
122
- }
123
- if (!free && !token) {
124
- throw new Error("Missing required --token argument.");
35
+ result[arg] = next;
36
+ index += 1;
125
37
  }
126
- if (!apiUrl) {
127
- apiUrl = "https://app.askthew.com";
128
- }
129
- if (!serverName) {
130
- serverName = defaultServerNameForTier(free);
131
- }
132
- return {
133
- hostType,
134
- clientId: clientId || undefined,
135
- clientLabel: clientLabel || undefined,
136
- token,
137
- apiUrl,
138
- serverName,
139
- serverNameExplicit,
140
- dryRun,
141
- installAgentInstructions,
142
- free,
143
- email: email || undefined,
144
- serverEntrypoint: serverEntrypoint ? path.resolve(serverEntrypoint) : undefined,
145
- };
38
+ return result;
146
39
  }
147
- function normalizeInstallToken(token) {
148
- return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
40
+ function stringArg(args, name) {
41
+ const value = args[name];
42
+ return typeof value === "string" ? value.trim() : "";
149
43
  }
150
- function detectLoginEmail() {
151
- for (const value of [
152
- process.env.ASKTHEW_EMAIL,
153
- process.env.GIT_AUTHOR_EMAIL,
154
- process.env.GIT_COMMITTER_EMAIL,
155
- process.env.EMAIL,
156
- ]) {
157
- const email = String(value ?? "").trim();
158
- if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
159
- return email;
160
- }
161
- try {
162
- const email = execFileSync("git", ["config", "user.email"], {
163
- encoding: "utf8",
164
- stdio: ["ignore", "pipe", "ignore"],
165
- }).trim();
166
- if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
167
- return email;
168
- }
169
- catch {
170
- return "";
171
- }
172
- return "";
44
+ function boolArg(args, name) {
45
+ return args[name] === true;
173
46
  }
174
- function loginCommandHint() {
175
- const email = detectLoginEmail();
176
- return email
177
- ? `askthew-mcp identify --email ${email}`
178
- : "askthew-mcp identify --email <your-email>";
47
+ function requireHost(value) {
48
+ if (value === "claude_code" || value === "codex" || value === "cursor")
49
+ return value;
50
+ throw new Error("Missing or invalid --host. Expected claude_code, codex, or cursor.");
179
51
  }
180
- function installIdentityEmail(optionsEmail) {
181
- return optionsEmail?.trim() || detectLoginEmail() || undefined;
52
+ function apiUrl(args) {
53
+ return stringArg(args, "--api-url") || process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com";
182
54
  }
183
- async function main() {
184
- const [command, ...argv] = process.argv.slice(2);
185
- if (command === "--help" || command === "-h" || command === "help") {
186
- console.log(usage());
187
- return;
188
- }
189
- if (command === "--version" || command === "-v" || command === "version") {
190
- console.log(packageVersion());
191
- return;
192
- }
193
- if (command === "doctor") {
194
- await runDoctorCommand(argv);
195
- return;
196
- }
197
- if (command === "print-config") {
198
- const options = parseInstallArgs(argv);
199
- const snippet = createHostConfigSnippet(options);
200
- console.log(snippet.json);
201
- return;
202
- }
203
- if (command === "install") {
204
- console.error(`Downloading @askthew/mcp-plugin@${packageVersion()}...`);
205
- const options = parseInstallArgs(argv);
206
- let freeIdentity = null;
207
- if (options.free && !options.dryRun) {
208
- freeIdentity = ensureLocalIdentity({
209
- emailClaim: installIdentityEmail(options.email),
210
- apiUrl: options.apiUrl,
211
- });
212
- }
213
- const result = installHostConfig(options);
214
- const instructions = options.installAgentInstructions
215
- ? installBehaviorInstructions({
216
- hostType: options.hostType,
217
- dryRun: options.dryRun,
218
- })
219
- : null;
220
- const receipt = result.wroteFile
221
- ? writeInstallReceipt({
222
- hostType: options.hostType,
223
- serverName: options.serverName,
224
- settingsPath: result.settingsPath,
225
- cwd: process.cwd(),
226
- instructionPaths: instructions?.paths ?? [],
227
- serverEntrypoint: options.serverEntrypoint,
228
- packageVersion: packageVersion(),
229
- })
230
- : null;
231
- const heartbeatSent = result.wroteFile && !options.free
232
- ? await sendInstallHeartbeat(options).catch(() => false)
233
- : false;
234
- console.log(result.wroteFile ? "Ask The W plugin install complete." : "Ask The W plugin dry run complete.");
235
- console.log(`Settings path: ${result.settingsPath}`);
236
- if (instructions) {
237
- console.log(`${options.dryRun ? "Would update agent instructions" : "Agent instructions"}: ${instructions.paths?.join(", ") ?? instructions.path}`);
238
- }
239
- if (receipt) {
240
- console.log(`Install receipt: ${receipt.hostType}/${receipt.serverName} at ${receipt.cwd}`);
241
- }
242
- else if (options.dryRun) {
243
- console.log(`Would write install receipt: ${options.hostType}/${options.serverName} at ${process.cwd()}`);
244
- }
245
- console.log(`Install command: ${formatInstallCommand(options)}`);
246
- if (result.wroteFile) {
247
- if (freeIdentity) {
248
- const registration = await tryRegisterFreeInstall({
249
- identity: freeIdentity,
250
- deviceLabel: options.clientLabel ?? `${options.hostType} free install`,
251
- repo: {
252
- repoName: process.env.ASKTHEW_REPO_NAME,
253
- repoRoot: process.env.ASKTHEW_REPO_ROOT,
254
- hostType: options.hostType,
255
- },
256
- options: { apiUrl: options.apiUrl },
257
- });
258
- console.log(registration.ok ? "Free install identity registered with Ask The W." : "Free install identity saved locally; cloud registration will retry later.");
259
- }
260
- console.log(options.free
261
- ? "Free local mode installed. Restart or reload your coding app; captures and decisions will write to ~/.askthew/store.sqlite."
262
- : heartbeatSent
263
- ? "Ask The W install heartbeat sent. Refresh the app to confirm the plugin shows Installed."
264
- : "Ask The W install heartbeat could not be sent yet. Restart or reload your coding app, then refresh Ask The W.");
265
- }
266
- console.log(`Next step: ${result.nextStep}`);
267
- if (!result.wroteFile) {
268
- console.log("");
269
- console.log("Planned host config:");
270
- console.log(result.json);
271
- }
272
- return;
273
- }
274
- if (command === "refresh") {
275
- await runRefreshCommand(argv);
276
- return;
277
- }
278
- if (command === "identify") {
279
- await runIdentifyCommand(argv);
280
- return;
281
- }
282
- if (command === "identity") {
283
- await runIdentityCommand(argv);
284
- return;
285
- }
286
- if (command === "uninstall") {
287
- await runUninstallCommand(argv);
288
- return;
289
- }
290
- if (command === "auth") {
291
- await runAuthCommand(argv);
292
- return;
293
- }
294
- if (command === "telemetry") {
295
- await runTelemetryCommand(argv);
296
- return;
297
- }
298
- if (command === "local") {
299
- await runLocalCommand(argv);
300
- return;
301
- }
302
- if (command === "install-hook") {
303
- await runInstallHookCommand(argv);
304
- return;
305
- }
306
- if (command === "hook-check") {
307
- await runHookCheckCommand(argv);
308
- return;
309
- }
310
- if (command === "digest") {
311
- await runDigestCommand(argv);
312
- return;
313
- }
314
- if (command === "sync") {
315
- await runSyncCommand(argv);
316
- return;
317
- }
318
- if (command === "upgrade") {
319
- await runUpgradeCommand(argv);
320
- return;
321
- }
322
- if (command) {
323
- throw new Error(`Unknown command "${command}".\n\n${usage()}`);
324
- }
325
- if (process.stdin.isTTY) {
326
- console.log(usage());
327
- return;
328
- }
329
- const server = createAskTheWMcpServer();
330
- const transport = new StdioServerTransport();
331
- await server.connect(transport);
55
+ async function readEmail() {
56
+ const response = await prompts({
57
+ type: "text",
58
+ name: "email",
59
+ message: "Email for your Ask The W code",
60
+ validate: (value) => (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value) ? true : "Enter a valid email."),
61
+ });
62
+ if (!response.email)
63
+ throw new Error("Signup cancelled.");
64
+ return String(response.email);
332
65
  }
333
- async function runUninstallCommand(argv) {
334
- let hostType;
335
- let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || undefined;
336
- let serverNameExplicit = Boolean(serverName);
337
- let dryRun = false;
338
- let keepLocalData = false;
339
- let keepAuth = false;
340
- let keepAgentInstructions = false;
341
- for (let index = 0; index < argv.length; index += 1) {
342
- const argument = argv[index];
343
- if (argument === "--dry-run") {
344
- dryRun = true;
345
- continue;
346
- }
347
- if (argument === "--keep-local-data") {
348
- keepLocalData = true;
349
- continue;
350
- }
351
- if (argument === "--keep-auth") {
352
- keepAuth = true;
353
- continue;
354
- }
355
- if (argument === "--keep-agent-instructions") {
356
- keepAgentInstructions = true;
357
- continue;
358
- }
359
- const next = argv[index + 1];
360
- if (!next)
361
- throw new Error(`Missing value for ${argument}.`);
362
- if (argument === "--host") {
363
- if (next !== "claude_code" && next !== "codex" && next !== "cursor") {
364
- throw new Error(`Unsupported host "${next}". Expected claude_code, codex, or cursor.`);
365
- }
366
- hostType = next;
367
- index += 1;
368
- continue;
369
- }
370
- if (argument === "--server-name") {
371
- serverName = next;
372
- serverNameExplicit = true;
373
- index += 1;
374
- continue;
375
- }
376
- throw new Error(`Unknown argument: ${argument}`);
377
- }
378
- if (!hostType) {
379
- throw new Error("Usage: askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>]");
380
- }
381
- const receipts = findInstallReceipts({ hostType, serverName });
382
- const config = uninstallHostConfig({
383
- hostType,
384
- serverName: serverNameExplicit ? serverName : undefined,
385
- dryRun,
386
- cwds: Array.from(new Set([process.cwd(), ...receipts.map((receipt) => receipt.cwd)])),
66
+ async function readCode() {
67
+ const response = await prompts({
68
+ type: "text",
69
+ name: "code",
70
+ message: "Six-digit Ask The W code",
71
+ validate: (value) => (/^\d{6}$/.test(value) ? true : "Enter the 6-digit code."),
387
72
  });
388
- const instructionCwds = Array.from(new Set([process.cwd(), ...receipts.map((receipt) => receipt.cwd)]));
389
- const instructionPaths = keepAgentInstructions
390
- ? []
391
- : instructionCwds.flatMap((cwd) => uninstallBehaviorInstructions({ hostType, dryRun, cwd }).paths);
392
- const dataDir = askTheWDataDir();
393
- const hadLocalData = fs.existsSync(dataDir);
394
- const authFile = identityPath();
395
- const hadAuth = fs.existsSync(authFile);
396
- const removedReceipts = dryRun ? receipts.length : removeInstallReceipts({ hostType, serverName });
397
- if (!keepLocalData && !dryRun) {
398
- fs.rmSync(dataDir, { recursive: true, force: true });
399
- }
400
- if (!keepAuth && !dryRun) {
401
- if (fs.existsSync(authFile))
402
- fs.rmSync(authFile, { force: true });
403
- }
404
- console.log(dryRun ? "Ask The W plugin uninstall dry run complete." : "Ask The W plugin uninstall complete.");
405
- console.log(`Settings path: ${config.settingsPath}`);
406
- console.log(config.removedServer
407
- ? `${dryRun ? "Would remove" : "Removed"} MCP server "${config.removedServerName}".`
408
- : config.foundConfigFile
409
- ? `No MCP server "${config.removedServerName}" found in host config.`
410
- : "No host config file found.");
411
- if (!keepAgentInstructions) {
412
- console.log(instructionPaths.length > 0
413
- ? `Agent instructions ${dryRun ? "would be removed from" : "removed from"}: ${Array.from(new Set(instructionPaths)).join(", ")}`
414
- : "No Ask The W agent instruction blocks found.");
415
- }
416
- console.log(removedReceipts > 0
417
- ? `${dryRun ? "Would remove" : "Removed"} ${removedReceipts} install receipt${removedReceipts === 1 ? "" : "s"}.`
418
- : "No install receipts found.");
419
- console.log(keepLocalData
420
- ? "Local data kept."
421
- : hadLocalData
422
- ? `${dryRun ? "Local data would be removed" : "Local data removed"}.`
423
- : "No local data directory found.");
424
- console.log(keepAuth
425
- ? "Auth tokens kept."
426
- : hadAuth
427
- ? `${dryRun ? "Local identity would be removed" : "Local identity removed"}.`
428
- : "No local identity found.");
429
- if (dryRun) {
430
- console.log("");
431
- console.log(config.json);
432
- }
73
+ if (!response.code)
74
+ throw new Error("Signup cancelled.");
75
+ return String(response.code);
433
76
  }
434
- async function runRefreshCommand(argv) {
435
- const parsed = parseInstallArgs(["--free", ...argv]);
436
- const isPaidRefresh = Boolean(parsed.token && !argv.includes("--free"));
437
- const options = {
438
- ...parsed,
439
- free: !isPaidRefresh,
440
- serverName: parsed.serverNameExplicit
441
- ? parsed.serverName
442
- : defaultServerNameForTier(!isPaidRefresh),
443
- };
444
- const existingIdentity = loadLocalIdentity();
445
- let freeIdentity = existingIdentity;
446
- if (options.free && !options.dryRun && !freeIdentity) {
447
- freeIdentity = ensureLocalIdentity({
448
- emailClaim: options.email,
449
- apiUrl: options.apiUrl,
450
- });
451
- }
452
- const uninstall = uninstallHostConfig({
453
- hostType: options.hostType,
454
- serverName: options.serverName,
455
- dryRun: options.dryRun,
77
+ async function exchangePaidBindToken(input) {
78
+ const response = await fetch(`${input.apiUrl.replace(/\/+$/, "")}/api/v1/agent/paid/install-token/exchange`, {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({
82
+ paid_bind_token: input.paidBindToken,
83
+ client_hint: input.hostType,
84
+ }),
456
85
  });
457
- const removedInstructions = uninstallBehaviorInstructions({
458
- hostType: options.hostType,
459
- dryRun: options.dryRun,
86
+ const body = await response.json().catch(() => ({}));
87
+ if (!response.ok || !body?.ok || !body.token || !body.install_id) {
88
+ throw new Error(String(body?.message ?? body?.code ?? "Paid install token exchange failed."));
89
+ }
90
+ saveCloudToken({
91
+ token: body.token,
92
+ install_id: body.install_id,
93
+ tier: "paid",
94
+ workspace_id: body.workspace_id ?? null,
95
+ token_purpose: "device",
96
+ api_url: input.apiUrl,
460
97
  });
461
- const install = installHostConfig(options);
462
- const installedInstructions = options.installAgentInstructions
463
- ? installBehaviorInstructions({
464
- hostType: options.hostType,
465
- dryRun: options.dryRun,
466
- })
467
- : null;
468
- const receipt = install.wroteFile
469
- ? writeInstallReceipt({
470
- hostType: options.hostType,
471
- serverName: options.serverName,
472
- settingsPath: install.settingsPath,
473
- cwd: process.cwd(),
474
- instructionPaths: installedInstructions?.paths ?? [],
475
- serverEntrypoint: options.serverEntrypoint,
476
- packageVersion: packageVersion(),
477
- })
478
- : null;
479
- if (options.free && freeIdentity && !options.dryRun) {
480
- await tryRegisterFreeInstall({
481
- identity: freeIdentity,
482
- deviceLabel: options.clientLabel ?? `${options.hostType} free refresh`,
483
- repo: {
484
- repoName: process.env.ASKTHEW_REPO_NAME,
485
- repoRoot: process.env.ASKTHEW_REPO_ROOT,
486
- hostType: options.hostType,
487
- },
488
- options: { apiUrl: options.apiUrl },
98
+ return body;
99
+ }
100
+ async function authenticateInstall(input) {
101
+ if (input.bindToken) {
102
+ return exchangePaidBindToken({
103
+ paidBindToken: input.bindToken,
104
+ hostType: input.hostType,
105
+ apiUrl: input.apiUrl,
489
106
  });
490
107
  }
491
- console.log(options.dryRun ? "Ask The W plugin refresh dry run complete." : "Ask The W plugin refresh complete.");
492
- console.log(`Plugin package version: ${packageVersion()}`);
493
- console.log(`Settings path: ${install.settingsPath}`);
494
- console.log(`Removed instructions: ${removedInstructions.paths.join(", ") || "none"}`);
495
- if (installedInstructions) {
496
- console.log(`Installed instructions: ${installedInstructions.paths.join(", ") || installedInstructions.path}`);
497
- }
498
- if (receipt) {
499
- console.log(`Install receipt: ${receipt.hostType}/${receipt.serverName} at ${receipt.cwd}`);
500
- }
501
- if (options.free) {
502
- console.log(freeIdentity
503
- ? `Local identity preserved: ${freeIdentity.installId}`
504
- : "Local identity would be created on a non-dry-run refresh.");
505
- console.log("Local data preserved: ~/.askthew/store.sqlite");
506
- }
507
- else {
508
- const heartbeatSent = install.wroteFile
509
- ? await sendInstallHeartbeat(options).catch(() => false)
510
- : false;
511
- console.log(heartbeatSent ? "Paid workspace heartbeat sent." : "Paid workspace heartbeat not sent yet.");
512
- }
513
- console.log(`Next step: ${install.nextStep}`);
514
- if (options.dryRun) {
515
- console.log("");
516
- console.log(uninstall.json);
517
- console.log("");
518
- console.log(install.json);
519
- }
520
- }
521
- async function runDoctorCommand(argv) {
522
- const serverEntrypoint = argValue(argv, "--server-entrypoint")?.trim();
523
- const result = await runInitializeHandshake({
524
- entrypoint: serverEntrypoint ? path.resolve(serverEntrypoint) : undefined,
108
+ const email = await readEmail();
109
+ const started = await startSignup({
110
+ email,
111
+ apiUrl: input.apiUrl,
112
+ clientHint: input.hostType,
113
+ });
114
+ if (!started?.ok)
115
+ throw new Error(String(started?.message ?? started?.code ?? "Could not send signup code."));
116
+ const code = await readCode();
117
+ const completed = await completeSignup({
118
+ email,
119
+ code,
120
+ apiUrl: input.apiUrl,
121
+ clientHint: input.hostType,
122
+ tokenPurpose: "device",
525
123
  });
526
- const expectedVersion = packageVersion();
527
- if (result.serverInfoVersion !== expectedVersion) {
528
- throw new Error(`MCP initialize version mismatch: package.json=${expectedVersion} serverInfo.version=${result.serverInfoVersion ?? "missing"}`);
124
+ if (!completed?.ok) {
125
+ throw new Error(String(completed?.message ?? completed?.code ?? "Could not verify signup code."));
529
126
  }
530
- console.log("Ask The W MCP doctor passed.");
531
- console.log(`Package version: ${expectedVersion}`);
532
- console.log(`serverInfo.version: ${result.serverInfoVersion}`);
127
+ return completed;
533
128
  }
534
- async function runUpgradeCommand(argv) {
535
- if (argv.includes("--browser")) {
536
- console.log("Open https://askthew.com/plugin to manage workspace upgrades.");
537
- return;
538
- }
539
- let hostType = process.env.ASKTHEW_HOST_TYPE?.trim();
540
- if (hostType !== "claude_code" && hostType !== "codex" && hostType !== "cursor") {
541
- hostType = undefined;
129
+ async function installCommand(argv) {
130
+ const args = parseArgs(argv);
131
+ const hostType = requireHost(stringArg(args, "--host"));
132
+ const baseUrl = apiUrl(args);
133
+ const bindToken = stringArg(args, "--bind") || undefined;
134
+ const skipAuth = boolArg(args, "--skip-auth");
135
+ const dryRun = boolArg(args, "--dry-run");
136
+ let workspaceId;
137
+ if (!skipAuth && !dryRun) {
138
+ const auth = await authenticateInstall({ hostType, bindToken, apiUrl: baseUrl });
139
+ workspaceId = auth.workspace_id ?? null;
140
+ }
141
+ else if (!dryRun && !loadCloudToken()) {
142
+ throw new Error("--skip-auth requires an existing ~/.askthew/cloud-token.json.");
542
143
  }
543
- let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || undefined;
544
- let serverNameExplicit = Boolean(serverName);
545
- let version = process.env.ASKTHEW_PIN?.trim() || packageVersion();
546
- let dryRun = false;
547
- for (let index = 0; index < argv.length; index += 1) {
548
- const argument = argv[index];
549
- if (argument === "--dry-run") {
550
- dryRun = true;
551
- continue;
552
- }
553
- const next = argv[index + 1];
554
- if (!next)
555
- throw new Error(`Missing value for ${argument}.`);
556
- if (argument === "--host") {
557
- if (next !== "claude_code" && next !== "codex" && next !== "cursor") {
558
- throw new Error(`Unsupported host "${next}". Expected claude_code, codex, or cursor.`);
559
- }
560
- hostType = next;
561
- index += 1;
562
- continue;
563
- }
564
- if (argument === "--server-name") {
565
- serverName = next;
566
- serverNameExplicit = true;
567
- index += 1;
568
- continue;
569
- }
570
- if (argument === "--version" || argument === "--pin") {
571
- version = next;
572
- index += 1;
573
- continue;
144
+ try {
145
+ const config = installHostConfig({
146
+ hostType,
147
+ apiUrl: baseUrl,
148
+ serverName: DEFAULT_SERVER_NAME,
149
+ dryRun,
150
+ });
151
+ const instructions = writeBehaviorInstructions({ hostType, dryRun });
152
+ if (!dryRun) {
153
+ writeInstallMetadata({
154
+ hostType,
155
+ apiUrl: baseUrl,
156
+ tier: bindToken ? "paid" : "free",
157
+ workspaceId,
158
+ });
574
159
  }
575
- throw new Error(`Unknown argument: ${argument}`);
160
+ console.log(`Ask The W installed for ${hostType}.`);
161
+ console.log(`MCP config: ${config.settingsPath}`);
162
+ console.log(`Agent instructions: ${instructions.filePath}`);
163
+ console.log("Restart your coding agent so it loads the new MCP server and instructions.");
576
164
  }
577
- if (!hostType) {
578
- throw new Error("Usage: askthew-mcp upgrade --host <claude_code|codex|cursor> [--server-name <name>] [--version <version>] [--dry-run]");
165
+ catch (error) {
166
+ const message = error instanceof Error ? error.message : "Unknown install failure.";
167
+ console.error(`Install reached auth, but writing local config failed: ${message}`);
168
+ console.error(`Your token is still saved under ${dataDirSummary()}.`);
169
+ console.error(`Retry local writes with: askthew-mcp install --host ${hostType} --skip-auth`);
170
+ process.exitCode = 1;
579
171
  }
580
- const packageSpec = version.startsWith("@askthew/mcp-plugin@")
581
- ? version
582
- : `@askthew/mcp-plugin@${version}`;
583
- if (!dryRun) {
584
- const handshake = await runInitializeHandshake();
585
- if (handshake.serverInfoVersion !== packageVersion()) {
586
- throw new Error(`Refusing to upgrade pinned host config: MCP initialize returned ${handshake.serverInfoVersion ?? "missing"} but package.json is ${packageVersion()}.`);
587
- }
172
+ }
173
+ function openBrowser(url) {
174
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
175
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
176
+ try {
177
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
178
+ child.unref();
588
179
  }
589
- const receipts = findInstallReceipts({
590
- hostType,
591
- serverName: serverNameExplicit ? serverName : undefined,
592
- });
593
- const result = upgradePinnedHostConfig({
594
- hostType,
595
- serverName: serverNameExplicit ? serverName : undefined,
596
- packageSpec,
597
- dryRun,
598
- cwds: Array.from(new Set([process.cwd(), ...receipts.map((receipt) => receipt.cwd)])),
599
- });
600
- console.log(dryRun ? "Ask The W plugin upgrade dry run complete." : "Ask The W plugin upgrade complete.");
601
- console.log(`Settings path: ${result.settingsPath}`);
602
- console.log(`Pinned package: ${result.packageSpec}`);
603
- console.log(result.upgradedServer
604
- ? `${dryRun ? "Would update" : "Updated"} MCP server pin${result.upgradedServerNames.length ? `: ${result.upgradedServerNames.join(", ")}` : "."}`
605
- : result.foundConfigFile
606
- ? "No Ask The W MCP server pins needed an update."
607
- : "No host config file found.");
608
- if (dryRun && result.json) {
609
- console.log("");
610
- console.log(result.json);
180
+ catch {
181
+ // Printing the URL is enough for headless terminals.
611
182
  }
612
183
  }
613
- function argValue(argv, name) {
614
- const index = argv.indexOf(name);
615
- return index >= 0 ? argv[index + 1] : undefined;
184
+ async function bindCommand(argv) {
185
+ const args = parseArgs(argv);
186
+ const allowPending = boolArg(args, "--allow-pending");
187
+ const client = new AskTheWCloudClient({ apiUrl: apiUrl(args) });
188
+ if (!client.hasToken())
189
+ throw new Error("Run askthew-mcp install first.");
190
+ const outbox = new Outbox();
191
+ if (outbox.pendingCount() > 0) {
192
+ await outbox.flush(client);
193
+ }
194
+ if (outbox.pendingCount() > 0 && !allowPending) {
195
+ const response = await prompts({
196
+ type: "confirm",
197
+ name: "ok",
198
+ message: `${outbox.pendingCount()} captures have not synced yet. Bind anyway?`,
199
+ initial: false,
200
+ });
201
+ if (!response.ok)
202
+ throw new Error("Bind cancelled while captures are pending.");
203
+ }
204
+ const started = await client.request("/paid/bind/start", { method: "POST", body: "{}" });
205
+ const body = started.body;
206
+ if (!started.ok || body.ok === false) {
207
+ throw new Error(String(body.message ?? body.code ?? "Could not start paid binding."));
208
+ }
209
+ const codeDisplay = String(body.code_display ?? "");
210
+ const url = String(body.url ?? "");
211
+ console.log(`Open this URL: ${url}`);
212
+ console.log(`Confirm this terminal code in the browser: ${codeDisplay}`);
213
+ if (url)
214
+ openBrowser(url);
215
+ for (let attempt = 0; attempt < 120; attempt += 1) {
216
+ await new Promise((resolve) => setTimeout(resolve, 5000));
217
+ const status = await client.request(`/paid/bind/status?code_display=${encodeURIComponent(codeDisplay)}`, { method: "GET" });
218
+ const statusBody = status.body;
219
+ if (status.ok && statusBody.status === "completed") {
220
+ console.log("Ask The W install is now bound to the workspace.");
221
+ return;
222
+ }
223
+ }
224
+ throw new Error("Timed out waiting for workspace binding.");
616
225
  }
617
- export async function runAuthCommand(argv, deps = {}) {
618
- const log = deps.log ?? console.log;
619
- const registerInstall = deps.registerFreeInstall ?? tryRegisterFreeInstall;
620
- const [subcommand] = argv;
621
- if (subcommand === "status") {
622
- const identity = loadLocalIdentity();
623
- log(identity
624
- ? `Identified local free install ${identity.installId}${identity.emailClaim ? ` with email claim ${identity.emailClaim}` : ""}. Email claim is unverified until upgrade.`
625
- : `No local identity yet. Run \`askthew-mcp identify --email <your-email>\`, or install with \`--free --email <your-email>\`.`);
226
+ async function tokenCommand(argv) {
227
+ const [action] = argv;
228
+ const client = new AskTheWCloudClient();
229
+ if (action === "rotate") {
230
+ const result = await client.request("/token/rotate", { method: "POST", body: "{}" });
231
+ const body = result.body;
232
+ if (!result.ok || body.ok === false)
233
+ throw new Error(String(body.message ?? body.code ?? "Token rotate failed."));
234
+ const tokenFile = loadCloudToken();
235
+ if (tokenFile && typeof body.token === "string") {
236
+ saveCloudToken({ ...tokenFile, token: body.token });
237
+ }
238
+ console.log("Ask The W token rotated. Restart your coding agent.");
626
239
  return;
627
240
  }
628
- if (subcommand === "logout") {
629
- const file = identityPath();
630
- if (fs.existsSync(file))
631
- fs.rmSync(file);
632
- log("Removed Ask The W local free install identity.");
241
+ if (action === "revoke") {
242
+ const result = await client.request("/token/revoke", { method: "POST", body: "{}" });
243
+ const body = result.body;
244
+ if (!result.ok || body.ok === false)
245
+ throw new Error(String(body.message ?? body.code ?? "Token revoke failed."));
246
+ console.log("Current Ask The W token revoked.");
633
247
  return;
634
248
  }
635
- if (subcommand !== "login") {
636
- throw new Error("Usage: askthew-mcp auth login --email <email> [--no-telemetry] | askthew-mcp auth logout | status");
637
- }
638
- ensureAskTheWDataDir();
639
- const email = argValue(argv, "--email")?.trim();
640
- if (!email)
641
- throw new Error("Missing --email.");
642
- const noTelemetry = argv.includes("--no-telemetry");
643
- const identity = ensureLocalIdentity({ emailClaim: email, telemetryOptOut: noTelemetry });
644
- const registration = await registerInstall({
645
- identity,
646
- deviceLabel: "askthew-mcp",
647
- });
648
- log(`Local free install identified as ${identity.installId}.`);
649
- log(`Email claim: ${identity.emailClaim ?? "none"} (unverified until upgrade).`);
650
- log(`Claim code: ${identity.claimCode}`);
651
- log(registration.ok ? "Registered install with Ask The W." : "Saved locally; cloud registration will retry later.");
652
- log("No email code is required for free local capture.");
653
- return;
249
+ throw new Error("Usage: askthew-mcp token rotate|revoke");
654
250
  }
655
- async function runIdentifyCommand(argv) {
656
- const email = argValue(argv, "--email")?.trim() || detectLoginEmail();
657
- const noTelemetry = argv.includes("--no-telemetry");
658
- if (!email)
659
- throw new Error("Missing --email.");
660
- const identity = ensureLocalIdentity({ emailClaim: email, telemetryOptOut: noTelemetry });
661
- const registration = await tryRegisterFreeInstall({
662
- identity,
663
- deviceLabel: "askthew-mcp",
664
- });
665
- console.log(`Local free install identified as ${identity.installId}.`);
666
- console.log(`Email claim: ${identity.emailClaim ?? "none"} (unverified until upgrade).`);
667
- console.log(`Claim code: ${identity.claimCode}`);
668
- console.log(registration.ok ? "Registered install with Ask The W." : "Saved locally; cloud registration will retry later.");
251
+ async function exportCommand() {
252
+ const client = new AskTheWCloudClient();
253
+ const result = await client.request("/export", { method: "GET" });
254
+ if (!result.ok) {
255
+ const body = result.body;
256
+ throw new Error(String(body.message ?? body.code ?? "Export failed."));
257
+ }
258
+ console.log(JSON.stringify(result.body, null, 2));
669
259
  }
670
- async function runIdentityCommand(argv) {
671
- const [subcommand] = argv;
672
- if (subcommand !== "status") {
673
- throw new Error("Usage: askthew-mcp identity status");
674
- }
675
- const identity = loadLocalIdentity();
676
- if (!identity) {
677
- console.log("No local free install identity yet.");
678
- return;
679
- }
680
- console.log(describeFreeIdentity(publicIdentity(identity)));
681
- console.log("Email claims are attribution only; upgrade/import requires local possession proof.");
260
+ async function deleteMeCommand(argv) {
261
+ const args = parseArgs(argv);
262
+ if (!boolArg(args, "--confirm")) {
263
+ throw new Error("Refusing to delete without --confirm.");
264
+ }
265
+ const tokenFile = loadCloudToken();
266
+ if (tokenFile?.tier === "paid") {
267
+ console.error("This install is workspace-bound. Install-scoped data will be deleted; workspace data is retained.");
268
+ }
269
+ const client = new AskTheWCloudClient();
270
+ const result = await client.request("/me", { method: "DELETE" });
271
+ const body = result.body;
272
+ if (!result.ok || body.ok === false)
273
+ throw new Error(String(body.message ?? body.code ?? "Delete failed."));
274
+ console.log(JSON.stringify(body, null, 2));
682
275
  }
683
- async function runTelemetryCommand(argv) {
684
- const [subcommand] = argv;
685
- const credentials = loadCliCredentials();
686
- if (!credentials)
687
- throw new Error("No local identity. Run `askthew-mcp identify --email <your-email>` first.");
688
- if (subcommand === "status") {
689
- console.log(`Telemetry: ${credentials.telemetryOptOut ? "off" : "on"}`);
276
+ async function main() {
277
+ const [command, ...argv] = process.argv.slice(2);
278
+ if (!command) {
279
+ await runStdioServer();
690
280
  return;
691
281
  }
692
- if (subcommand === "opt-out" || subcommand === "opt-in") {
693
- ensureLocalIdentity({ telemetryOptOut: subcommand === "opt-out" });
694
- console.log(`Telemetry: ${subcommand === "opt-out" ? "off" : "on"}`);
282
+ if (command === "--help" || command === "-h" || command === "help") {
283
+ console.log(usage());
695
284
  return;
696
285
  }
697
- if (subcommand === "preview") {
698
- const store = LocalStore.open();
699
- console.log(JSON.stringify(buildTelemetryPayload({ store, credentials }), null, 2));
286
+ if (command === "--version" || command === "-v" || command === "version") {
287
+ console.log(packageVersion());
700
288
  return;
701
289
  }
702
- throw new Error("Usage: askthew-mcp telemetry status | opt-out | opt-in | preview");
703
- }
704
- async function runLocalCommand(argv) {
705
- const [subcommand, flag] = argv;
706
- const store = LocalStore.open();
707
- if (subcommand === "stats") {
708
- console.log(JSON.stringify(store.stats(), null, 2));
290
+ if (command === "install") {
291
+ await installCommand(argv);
709
292
  return;
710
293
  }
711
- if (subcommand === "reset" && flag === "--hard") {
712
- const dir = ensureAskTheWDataDir();
713
- fs.rmSync(dir, { recursive: true, force: true });
714
- console.log("Local Ask The W data removed.");
294
+ if (command === "bind") {
295
+ await bindCommand(argv);
715
296
  return;
716
297
  }
717
- throw new Error("Usage: askthew-mcp local stats | reset --hard");
718
- }
719
- async function runInstallHookCommand(argv) {
720
- if (!argv.includes("--pre-commit")) {
721
- throw new Error("Usage: askthew-mcp install-hook --pre-commit");
722
- }
723
- const hookPath = installPreCommitHook();
724
- console.log(`Ask The W pre-commit hook installed: ${hookPath}`);
725
- }
726
- async function runHookCheckCommand(argv) {
727
- if (!argv.includes("--pre-commit")) {
728
- throw new Error("Usage: askthew-mcp hook-check --pre-commit");
729
- }
730
- const store = LocalStore.open();
731
- const gap = preCommitDecisionGap({ store, stagedFiles: stagedFiles(), scopeKey: localScopeKey() });
732
- if (gap.missing) {
733
- console.log('Ask The W: this change has no decision attached, draft one?');
734
- console.log("Run: npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp digest --weekly or ask your agent to call promote_signal_to_decision.");
735
- }
736
- }
737
- async function runDigestCommand(argv) {
738
- if (!argv.includes("--weekly")) {
739
- throw new Error("Usage: askthew-mcp digest --weekly");
298
+ if (command === "token") {
299
+ await tokenCommand(argv);
300
+ return;
740
301
  }
741
- const store = LocalStore.open();
742
- const filePath = writeWeeklyDigest({ store });
743
- console.log(`Weekly decision digest written: ${filePath}`);
744
- }
745
- async function runSyncCommand(argv) {
746
- if (argv[0] !== "upload")
747
- throw new Error("Usage: askthew-mcp sync upload [--token <workspace-install-token>] [--dry-run]");
748
- const credentials = loadCliCredentials();
749
- if (!credentials)
750
- throw new Error("No local identity. Run `askthew-mcp identify --email <your-email>` first.");
751
- const store = LocalStore.open();
752
- if (argv.includes("--dry-run")) {
753
- console.log(JSON.stringify(syncDryRun(store), null, 2));
302
+ if (command === "export") {
303
+ await exportCommand();
754
304
  return;
755
305
  }
756
- const syncToken = argValue(argv, "--token")?.trim() || process.env.ASKTHEW_INSTALL_TOKEN?.trim();
757
- if (!syncToken) {
758
- throw new Error("Missing workspace install token. Pass `--token <workspace-install-token>` after upgrading.");
306
+ if (command === "delete-me") {
307
+ await deleteMeCommand(argv);
308
+ return;
759
309
  }
760
- console.log(JSON.stringify(await uploadLocalStore({ store, credentials, syncToken }), null, 2));
761
- }
762
- const isDirectCliExecution = Boolean(process.argv[1]) &&
763
- (() => {
764
- const cliPath = fileURLToPath(import.meta.url);
765
- const invokedPath = path.resolve(process.argv[1]);
766
- try {
767
- return fs.realpathSync(invokedPath) === fs.realpathSync(cliPath);
768
- }
769
- catch {
770
- return invokedPath === cliPath;
771
- }
772
- })();
773
- if (isDirectCliExecution) {
774
- main().catch((error) => {
775
- if (error instanceof Error) {
776
- console.error(error.message);
777
- }
778
- else {
779
- console.error("Ask The W plugin failed to start.", error);
780
- }
781
- process.exit(1);
782
- });
310
+ throw new Error(`Unknown command: ${command}\n\n${usage()}`);
783
311
  }
312
+ main().catch((error) => {
313
+ console.error(error instanceof Error ? error.message : String(error));
314
+ process.exitCode = 1;
315
+ });