@askthew/mcp-plugin 0.4.7 → 0.4.9

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/README.md CHANGED
@@ -43,6 +43,8 @@ npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install
43
43
 
44
44
  Free install is local-first: it generates `~/.askthew/identity.json`, writes MCP config and agent instructions immediately, and never requires an email code before local capture works. The email is an unverified claim used for upgrade onboarding only; data is keyed by the generated install ID, not email alone.
45
45
 
46
+ Installer bookkeeping is separate from local capture data: install receipts live under `~/.local/state/askthew/install-receipts.json` by default, or `$XDG_STATE_HOME/askthew/install-receipts.json` when `XDG_STATE_HOME` is set. This lets `uninstall` remove host config and repo instruction blocks even after `~/.askthew` has been wiped.
47
+
46
48
  Telemetry is aggregate-only and opt-out. Ask The W does not receive code, file contents, file paths, file names, command text, summaries, or decision content in free mode telemetry. Aggregate summaries are signed by the local install identity and stored under the generated install ID. Opt out with `--no-telemetry`, `ASKTHEW_TELEMETRY=off`, or `askthew-mcp telemetry opt-out`.
47
49
 
48
50
  ## Refresh vs Uninstall
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
- import { createAskTheWMcpServer } from "./index.js";
7
+ import { createAskTheWMcpServer, runInitializeHandshake } from "./index.js";
8
8
  import { askTheWDataDir, ensureAskTheWDataDir, identityPath } from "./lib/paths.js";
9
9
  import { loadCliCredentials } from "./lib/free-tier-policy.js";
10
10
  import { describeFreeIdentity, tryRegisterFreeInstall } from "./lib/free-install-registration.js";
@@ -13,17 +13,21 @@ import { LocalStore } from "./lib/local-store.js";
13
13
  import { buildTelemetryPayload } from "./lib/telemetry.js";
14
14
  import { syncDryRun, uploadLocalStore } from "./lib/upgrade-sync.js";
15
15
  import { installPreCommitHook, localScopeKey, preCommitDecisionGap, stagedFiles, writeWeeklyDigest, } from "./lib/cli-actions.js";
16
- import { createHostConfigSnippet, formatInstallCommand, installBehaviorInstructions, installHostConfig, sendInstallHeartbeat, uninstallBehaviorInstructions, uninstallHostConfig, } from "./install.js";
16
+ import { createHostConfigSnippet, defaultServerNameForTier, findInstallReceipts, formatInstallCommand, installBehaviorInstructions, installHostConfig, packageVersion, removeInstallReceipts, sendInstallHeartbeat, uninstallBehaviorInstructions, uninstallHostConfig, upgradePinnedHostConfig, writeInstallReceipt, } from "./install.js";
17
17
  function usage() {
18
18
  return [
19
19
  "Ask The W Coding Agent Connector",
20
20
  "",
21
21
  "Usage:",
22
- " askthew-mcp",
23
- " askthew-mcp install --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>] [--dry-run] [--no-agent-instructions]",
24
- " askthew-mcp install --host <claude_code|codex|cursor> --free [--email <email>] [--api-url <url>] [--server-name <name>]",
25
- " askthew-mcp refresh --host <claude_code|codex|cursor> [--free] [--token <install-token>] [--api-url <url>] [--server-name <name>] [--dry-run]",
22
+ " askthew-mcp [stdio MCP server when stdin is piped]",
23
+ " 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]",
26
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",
27
31
  " askthew-mcp identify --email <email> [--no-telemetry]",
28
32
  " askthew-mcp identity status",
29
33
  " askthew-mcp auth login --email <email> [--no-telemetry]",
@@ -43,10 +47,12 @@ function parseInstallArgs(argv) {
43
47
  let token = normalizeInstallToken(process.env.ASKTHEW_INSTALL_TOKEN) || "";
44
48
  let apiUrl = process.env.ASKTHEW_API_URL?.trim() || "";
45
49
  let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || "";
50
+ let serverNameExplicit = Boolean(serverName);
46
51
  let dryRun = false;
47
52
  let installAgentInstructions = true;
48
53
  let free = false;
49
54
  let email = process.env.ASKTHEW_EMAIL?.trim() || "";
55
+ let serverEntrypoint = process.env.ASKTHEW_SERVER_ENTRYPOINT?.trim() || "";
50
56
  for (let index = 0; index < argv.length; index += 1) {
51
57
  const argument = argv[index];
52
58
  if (argument === "--dry-run") {
@@ -95,6 +101,7 @@ function parseInstallArgs(argv) {
95
101
  }
96
102
  if (argument === "--server-name") {
97
103
  serverName = next;
104
+ serverNameExplicit = true;
98
105
  index += 1;
99
106
  continue;
100
107
  }
@@ -103,6 +110,11 @@ function parseInstallArgs(argv) {
103
110
  index += 1;
104
111
  continue;
105
112
  }
113
+ if (argument === "--server-entrypoint") {
114
+ serverEntrypoint = next;
115
+ index += 1;
116
+ continue;
117
+ }
106
118
  throw new Error(`Unknown argument: ${argument}`);
107
119
  }
108
120
  if (!hostType) {
@@ -115,7 +127,7 @@ function parseInstallArgs(argv) {
115
127
  apiUrl = "https://app.askthew.com";
116
128
  }
117
129
  if (!serverName) {
118
- serverName = "askthew";
130
+ serverName = defaultServerNameForTier(free);
119
131
  }
120
132
  return {
121
133
  hostType,
@@ -124,25 +136,17 @@ function parseInstallArgs(argv) {
124
136
  token,
125
137
  apiUrl,
126
138
  serverName,
139
+ serverNameExplicit,
127
140
  dryRun,
128
141
  installAgentInstructions,
129
142
  free,
130
143
  email: email || undefined,
144
+ serverEntrypoint: serverEntrypoint ? path.resolve(serverEntrypoint) : undefined,
131
145
  };
132
146
  }
133
147
  function normalizeInstallToken(token) {
134
148
  return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
135
149
  }
136
- function packageVersion() {
137
- try {
138
- const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
139
- const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
140
- return typeof parsed.version === "string" ? parsed.version : "unknown";
141
- }
142
- catch {
143
- return "unknown";
144
- }
145
- }
146
150
  function detectLoginEmail() {
147
151
  for (const value of [
148
152
  process.env.ASKTHEW_EMAIL,
@@ -182,6 +186,14 @@ async function main() {
182
186
  console.log(usage());
183
187
  return;
184
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
+ }
185
197
  if (command === "print-config") {
186
198
  const options = parseInstallArgs(argv);
187
199
  const snippet = createHostConfigSnippet(options);
@@ -189,6 +201,7 @@ async function main() {
189
201
  return;
190
202
  }
191
203
  if (command === "install") {
204
+ console.error(`Downloading @askthew/mcp-plugin@${packageVersion()}...`);
192
205
  const options = parseInstallArgs(argv);
193
206
  let freeIdentity = null;
194
207
  if (options.free && !options.dryRun) {
@@ -204,13 +217,30 @@ async function main() {
204
217
  dryRun: options.dryRun,
205
218
  })
206
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;
207
231
  const heartbeatSent = result.wroteFile && !options.free
208
232
  ? await sendInstallHeartbeat(options).catch(() => false)
209
233
  : false;
210
234
  console.log(result.wroteFile ? "Ask The W plugin install complete." : "Ask The W plugin dry run complete.");
211
235
  console.log(`Settings path: ${result.settingsPath}`);
212
236
  if (instructions) {
213
- console.log(`Agent instructions: ${instructions.paths?.join(", ") ?? instructions.path}`);
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()}`);
214
244
  }
215
245
  console.log(`Install command: ${formatInstallCommand(options)}`);
216
246
  if (result.wroteFile) {
@@ -236,6 +266,7 @@ async function main() {
236
266
  console.log(`Next step: ${result.nextStep}`);
237
267
  if (!result.wroteFile) {
238
268
  console.log("");
269
+ console.log("Planned host config:");
239
270
  console.log(result.json);
240
271
  }
241
272
  return;
@@ -285,22 +316,24 @@ async function main() {
285
316
  return;
286
317
  }
287
318
  if (command === "upgrade") {
288
- console.log("Open https://askthew.com/plugin to upgrade, then run `askthew-mcp upgrade --finalize`.");
289
- if (argv.includes("--finalize")) {
290
- console.log("Finalize will rewrite host config after a paid workspace token is available in the web app.");
291
- }
319
+ await runUpgradeCommand(argv);
292
320
  return;
293
321
  }
294
322
  if (command) {
295
323
  throw new Error(`Unknown command "${command}".\n\n${usage()}`);
296
324
  }
325
+ if (process.stdin.isTTY) {
326
+ console.log(usage());
327
+ return;
328
+ }
297
329
  const server = createAskTheWMcpServer();
298
330
  const transport = new StdioServerTransport();
299
331
  await server.connect(transport);
300
332
  }
301
333
  async function runUninstallCommand(argv) {
302
334
  let hostType;
303
- let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || "askthew";
335
+ let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || undefined;
336
+ let serverNameExplicit = Boolean(serverName);
304
337
  let dryRun = false;
305
338
  let keepLocalData = false;
306
339
  let keepAuth = false;
@@ -336,6 +369,7 @@ async function runUninstallCommand(argv) {
336
369
  }
337
370
  if (argument === "--server-name") {
338
371
  serverName = next;
372
+ serverNameExplicit = true;
339
373
  index += 1;
340
374
  continue;
341
375
  }
@@ -344,14 +378,22 @@ async function runUninstallCommand(argv) {
344
378
  if (!hostType) {
345
379
  throw new Error("Usage: askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>]");
346
380
  }
347
- const config = uninstallHostConfig({ hostType, serverName, dryRun });
348
- const instructions = keepAgentInstructions
349
- ? null
350
- : uninstallBehaviorInstructions({ hostType, dryRun });
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)])),
387
+ });
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);
351
392
  const dataDir = askTheWDataDir();
352
393
  const hadLocalData = fs.existsSync(dataDir);
353
394
  const authFile = identityPath();
354
395
  const hadAuth = fs.existsSync(authFile);
396
+ const removedReceipts = dryRun ? receipts.length : removeInstallReceipts({ hostType, serverName });
355
397
  if (!keepLocalData && !dryRun) {
356
398
  fs.rmSync(dataDir, { recursive: true, force: true });
357
399
  }
@@ -366,11 +408,14 @@ async function runUninstallCommand(argv) {
366
408
  : config.foundConfigFile
367
409
  ? `No MCP server "${config.removedServerName}" found in host config.`
368
410
  : "No host config file found.");
369
- if (instructions) {
370
- console.log(instructions.paths.length > 0
371
- ? `Agent instructions ${dryRun ? "would be removed from" : "removed from"}: ${instructions.paths.join(", ")}`
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(", ")}`
372
414
  : "No Ask The W agent instruction blocks found.");
373
415
  }
416
+ console.log(removedReceipts > 0
417
+ ? `${dryRun ? "Would remove" : "Removed"} ${removedReceipts} install receipt${removedReceipts === 1 ? "" : "s"}.`
418
+ : "No install receipts found.");
374
419
  console.log(keepLocalData
375
420
  ? "Local data kept."
376
421
  : hadLocalData
@@ -392,6 +437,9 @@ async function runRefreshCommand(argv) {
392
437
  const options = {
393
438
  ...parsed,
394
439
  free: !isPaidRefresh,
440
+ serverName: parsed.serverNameExplicit
441
+ ? parsed.serverName
442
+ : defaultServerNameForTier(!isPaidRefresh),
395
443
  };
396
444
  const existingIdentity = loadLocalIdentity();
397
445
  let freeIdentity = existingIdentity;
@@ -417,6 +465,17 @@ async function runRefreshCommand(argv) {
417
465
  dryRun: options.dryRun,
418
466
  })
419
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;
420
479
  if (options.free && freeIdentity && !options.dryRun) {
421
480
  await tryRegisterFreeInstall({
422
481
  identity: freeIdentity,
@@ -436,6 +495,9 @@ async function runRefreshCommand(argv) {
436
495
  if (installedInstructions) {
437
496
  console.log(`Installed instructions: ${installedInstructions.paths.join(", ") || installedInstructions.path}`);
438
497
  }
498
+ if (receipt) {
499
+ console.log(`Install receipt: ${receipt.hostType}/${receipt.serverName} at ${receipt.cwd}`);
500
+ }
439
501
  if (options.free) {
440
502
  console.log(freeIdentity
441
503
  ? `Local identity preserved: ${freeIdentity.installId}`
@@ -456,6 +518,98 @@ async function runRefreshCommand(argv) {
456
518
  console.log(install.json);
457
519
  }
458
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,
525
+ });
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"}`);
529
+ }
530
+ console.log("Ask The W MCP doctor passed.");
531
+ console.log(`Package version: ${expectedVersion}`);
532
+ console.log(`serverInfo.version: ${result.serverInfoVersion}`);
533
+ }
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;
542
+ }
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;
574
+ }
575
+ throw new Error(`Unknown argument: ${argument}`);
576
+ }
577
+ if (!hostType) {
578
+ throw new Error("Usage: askthew-mcp upgrade --host <claude_code|codex|cursor> [--server-name <name>] [--version <version>] [--dry-run]");
579
+ }
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
+ }
588
+ }
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);
611
+ }
612
+ }
459
613
  function argValue(argv, name) {
460
614
  const index = argv.indexOf(name);
461
615
  return index >= 0 ? argv[index + 1] : undefined;
package/dist/index.d.ts CHANGED
@@ -158,4 +158,11 @@ export interface AskTheWMcpServerOptions {
158
158
  }
159
159
  export declare function normalizeInstallTokenInput(token: string | undefined): string;
160
160
  export declare function createAskTheWMcpServer(options?: AskTheWMcpServerOptions): McpServer;
161
+ export declare function runInitializeHandshake(input?: {
162
+ entrypoint?: string;
163
+ env?: NodeJS.ProcessEnv;
164
+ timeoutMs?: number;
165
+ }): Promise<{
166
+ serverInfoVersion?: string;
167
+ }>;
161
168
  export {};