@askthew/mcp-plugin 0.4.8 → 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/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, findInstallReceipts, formatInstallCommand, installBehaviorInstructions, installHostConfig, removeInstallReceipts, sendInstallHeartbeat, uninstallBehaviorInstructions, uninstallHostConfig, writeInstallReceipt, } 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>] [--server-entrypoint <path>] [--dry-run] [--no-agent-instructions]",
24
- " askthew-mcp install --host <claude_code|codex|cursor> --free [--email <email>] [--api-url <url>] [--server-name <name>] [--server-entrypoint <path>]",
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]",
25
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,6 +47,7 @@ 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;
@@ -96,6 +101,7 @@ function parseInstallArgs(argv) {
96
101
  }
97
102
  if (argument === "--server-name") {
98
103
  serverName = next;
104
+ serverNameExplicit = true;
99
105
  index += 1;
100
106
  continue;
101
107
  }
@@ -121,7 +127,7 @@ function parseInstallArgs(argv) {
121
127
  apiUrl = "https://app.askthew.com";
122
128
  }
123
129
  if (!serverName) {
124
- serverName = "askthew";
130
+ serverName = defaultServerNameForTier(free);
125
131
  }
126
132
  return {
127
133
  hostType,
@@ -130,6 +136,7 @@ function parseInstallArgs(argv) {
130
136
  token,
131
137
  apiUrl,
132
138
  serverName,
139
+ serverNameExplicit,
133
140
  dryRun,
134
141
  installAgentInstructions,
135
142
  free,
@@ -140,16 +147,6 @@ function parseInstallArgs(argv) {
140
147
  function normalizeInstallToken(token) {
141
148
  return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
142
149
  }
143
- function packageVersion() {
144
- try {
145
- const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
146
- const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
147
- return typeof parsed.version === "string" ? parsed.version : "unknown";
148
- }
149
- catch {
150
- return "unknown";
151
- }
152
- }
153
150
  function detectLoginEmail() {
154
151
  for (const value of [
155
152
  process.env.ASKTHEW_EMAIL,
@@ -189,6 +186,14 @@ async function main() {
189
186
  console.log(usage());
190
187
  return;
191
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
+ }
192
197
  if (command === "print-config") {
193
198
  const options = parseInstallArgs(argv);
194
199
  const snippet = createHostConfigSnippet(options);
@@ -196,6 +201,7 @@ async function main() {
196
201
  return;
197
202
  }
198
203
  if (command === "install") {
204
+ console.error(`Downloading @askthew/mcp-plugin@${packageVersion()}...`);
199
205
  const options = parseInstallArgs(argv);
200
206
  let freeIdentity = null;
201
207
  if (options.free && !options.dryRun) {
@@ -228,11 +234,14 @@ async function main() {
228
234
  console.log(result.wroteFile ? "Ask The W plugin install complete." : "Ask The W plugin dry run complete.");
229
235
  console.log(`Settings path: ${result.settingsPath}`);
230
236
  if (instructions) {
231
- 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}`);
232
238
  }
233
239
  if (receipt) {
234
240
  console.log(`Install receipt: ${receipt.hostType}/${receipt.serverName} at ${receipt.cwd}`);
235
241
  }
242
+ else if (options.dryRun) {
243
+ console.log(`Would write install receipt: ${options.hostType}/${options.serverName} at ${process.cwd()}`);
244
+ }
236
245
  console.log(`Install command: ${formatInstallCommand(options)}`);
237
246
  if (result.wroteFile) {
238
247
  if (freeIdentity) {
@@ -257,6 +266,7 @@ async function main() {
257
266
  console.log(`Next step: ${result.nextStep}`);
258
267
  if (!result.wroteFile) {
259
268
  console.log("");
269
+ console.log("Planned host config:");
260
270
  console.log(result.json);
261
271
  }
262
272
  return;
@@ -306,22 +316,24 @@ async function main() {
306
316
  return;
307
317
  }
308
318
  if (command === "upgrade") {
309
- console.log("Open https://askthew.com/plugin to upgrade, then run `askthew-mcp upgrade --finalize`.");
310
- if (argv.includes("--finalize")) {
311
- console.log("Finalize will rewrite host config after a paid workspace token is available in the web app.");
312
- }
319
+ await runUpgradeCommand(argv);
313
320
  return;
314
321
  }
315
322
  if (command) {
316
323
  throw new Error(`Unknown command "${command}".\n\n${usage()}`);
317
324
  }
325
+ if (process.stdin.isTTY) {
326
+ console.log(usage());
327
+ return;
328
+ }
318
329
  const server = createAskTheWMcpServer();
319
330
  const transport = new StdioServerTransport();
320
331
  await server.connect(transport);
321
332
  }
322
333
  async function runUninstallCommand(argv) {
323
334
  let hostType;
324
- let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || "askthew";
335
+ let serverName = process.env.ASKTHEW_SERVER_NAME?.trim() || undefined;
336
+ let serverNameExplicit = Boolean(serverName);
325
337
  let dryRun = false;
326
338
  let keepLocalData = false;
327
339
  let keepAuth = false;
@@ -357,6 +369,7 @@ async function runUninstallCommand(argv) {
357
369
  }
358
370
  if (argument === "--server-name") {
359
371
  serverName = next;
372
+ serverNameExplicit = true;
360
373
  index += 1;
361
374
  continue;
362
375
  }
@@ -366,7 +379,12 @@ async function runUninstallCommand(argv) {
366
379
  throw new Error("Usage: askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>]");
367
380
  }
368
381
  const receipts = findInstallReceipts({ hostType, serverName });
369
- const config = uninstallHostConfig({ hostType, serverName, dryRun });
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
+ });
370
388
  const instructionCwds = Array.from(new Set([process.cwd(), ...receipts.map((receipt) => receipt.cwd)]));
371
389
  const instructionPaths = keepAgentInstructions
372
390
  ? []
@@ -419,6 +437,9 @@ async function runRefreshCommand(argv) {
419
437
  const options = {
420
438
  ...parsed,
421
439
  free: !isPaidRefresh,
440
+ serverName: parsed.serverNameExplicit
441
+ ? parsed.serverName
442
+ : defaultServerNameForTier(!isPaidRefresh),
422
443
  };
423
444
  const existingIdentity = loadLocalIdentity();
424
445
  let freeIdentity = existingIdentity;
@@ -497,6 +518,98 @@ async function runRefreshCommand(argv) {
497
518
  console.log(install.json);
498
519
  }
499
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
+ }
500
613
  function argValue(argv, name) {
501
614
  const index = argv.indexOf(name);
502
615
  return index >= 0 ? argv[index + 1] : undefined;
package/dist/index.d.ts CHANGED
@@ -160,6 +160,9 @@ export declare function normalizeInstallTokenInput(token: string | undefined): s
160
160
  export declare function createAskTheWMcpServer(options?: AskTheWMcpServerOptions): McpServer;
161
161
  export declare function runInitializeHandshake(input?: {
162
162
  entrypoint?: string;
163
+ env?: NodeJS.ProcessEnv;
163
164
  timeoutMs?: number;
164
- }): Promise<void>;
165
+ }): Promise<{
166
+ serverInfoVersion?: string;
167
+ }>;
165
168
  export {};
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { spawn } from "node:child_process";
4
4
  import fs from "node:fs";
5
+ import { createRequire } from "node:module";
5
6
  import path from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { z } from "zod";
@@ -10,9 +11,18 @@ import { resolveMcpMode } from "./lib/free-tier-policy.js";
10
11
  import { LocalStore } from "./lib/local-store.js";
11
12
  import { buildTelemetryPayload, flushTelemetryOutbox } from "./lib/telemetry.js";
12
13
  import { ensureLocalIdentity } from "./lib/local-identity.js";
13
- import { buildLocalTimeline, buildTimelineInsights as buildLocalTimelineInsights, renderTimelineMarkdown as renderLocalTimelineMarkdown } from "./lib/timeline-insights.js";
14
14
  import { paidDescription, paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
15
15
  import { configPath, readJsonFile } from "./lib/paths.js";
16
+ const requirePackageJson = createRequire(import.meta.url);
17
+ function packageVersion() {
18
+ try {
19
+ const manifest = requirePackageJson("../package.json");
20
+ return typeof manifest.version === "string" ? manifest.version : "unknown";
21
+ }
22
+ catch {
23
+ return "unknown";
24
+ }
25
+ }
16
26
  const evidenceRoleSchema = z.enum(["user", "assistant", "system"]);
17
27
  const evidenceEntrySchema = z.object({
18
28
  role: evidenceRoleSchema,
@@ -570,7 +580,7 @@ export function createAskTheWMcpServer(options = {}) {
570
580
  }
571
581
  const server = new McpServer({
572
582
  name: "Ask The W Coding Agent Connector",
573
- version: "0.4.7",
583
+ version: packageVersion(),
574
584
  });
575
585
  if (options.sendStartupHeartbeat !== false && mode.mode === "paid") {
576
586
  void sendStartupSignals(options);
@@ -656,6 +666,89 @@ export function createAskTheWMcpServer(options = {}) {
656
666
  }
657
667
  return null;
658
668
  };
669
+ const localSessionAnalysisResponse = (payload) => {
670
+ if (!localStore) {
671
+ return localToolError({
672
+ code: "local_store_unavailable",
673
+ message: "The local Ask The W store is unavailable.",
674
+ retryable: true,
675
+ hint: "Retry after restarting the plugin host.",
676
+ });
677
+ }
678
+ const loginRequired = requireFreeIdentity();
679
+ if (loginRequired)
680
+ return loginRequired;
681
+ const scopeKey = currentScopeKey();
682
+ const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
683
+ const allSessionIds = localStore.listSessionIds({ limit: 100000, scopeKey });
684
+ const allowedSessionIds = new Set(allSessionIds.slice(0, 3));
685
+ if (sessionId && allSessionIds.length > 3 && !allowedSessionIds.has(sessionId)) {
686
+ return localToolError({
687
+ code: "free_tier_limit",
688
+ message: "The free plugin can analyze the latest three local sessions.",
689
+ retryable: false,
690
+ hint: "Upgrade to review more than three sessions in the workspace dashboard.",
691
+ extra: {
692
+ tool: "analyze_session",
693
+ limit: 3,
694
+ upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=analyze_session",
695
+ cta: "Upgrade to analyze more than three sessions in the workspace dashboard.",
696
+ },
697
+ });
698
+ }
699
+ const limit = Math.min(50, payload.limit ?? 50);
700
+ const signals = sessionId
701
+ ? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
702
+ : [];
703
+ const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
704
+ const decisions = sessionId
705
+ ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
706
+ : [];
707
+ const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
708
+ const counts = signals.reduce((accumulator, signal) => {
709
+ accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
710
+ return accumulator;
711
+ }, {});
712
+ const allCounts = allSignals.reduce((accumulator, signal) => {
713
+ accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
714
+ return accumulator;
715
+ }, {});
716
+ const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
717
+ if ((payload.format ?? "markdown") === "json") {
718
+ return budgetedLocalResponse({
719
+ ok: true,
720
+ tier: "free",
721
+ sessionId,
722
+ format: "json",
723
+ signals: payload.compact
724
+ ? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
725
+ : signals.map((signal) => signalWithDecision(localStore, signal)),
726
+ decisions: payload.compact
727
+ ? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
728
+ : decisions.map((decision) => decisionWithSignals(localStore, decision)),
729
+ decisionCandidates,
730
+ nextCursor,
731
+ counts: {
732
+ totalSignals: allSignals.length,
733
+ byKind: allCounts,
734
+ },
735
+ }, payload.max_chars);
736
+ }
737
+ return budgetedLocalResponse({
738
+ ok: true,
739
+ tier: "free",
740
+ sessionId,
741
+ format: "markdown",
742
+ rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
743
+ ...(payload.compact
744
+ ? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
745
+ : {}),
746
+ counts: {
747
+ totalSignals: allSignals.length,
748
+ byKind: Object.keys(allCounts).length ? allCounts : counts,
749
+ },
750
+ }, payload.max_chars);
751
+ };
659
752
  server.tool("capture_session_signal", {
660
753
  sessionId: z.string().min(1),
661
754
  sequence: z.number().int().nonnegative(),
@@ -974,18 +1067,7 @@ export function createAskTheWMcpServer(options = {}) {
974
1067
  idempotencyKey: idempotencyKeySchema,
975
1068
  }, async (payload) => {
976
1069
  if (mode.mode !== "paid" && localStore) {
977
- const loginRequired = requireFreeIdentity();
978
- if (loginRequired)
979
- return loginRequired;
980
- if (payload.confirmText !== payload.id) {
981
- return localToolError({
982
- code: "confirmation_required",
983
- message: "confirmText must match the decision id.",
984
- retryable: false,
985
- hint: "Pass the exact decision id as confirmText.",
986
- });
987
- }
988
- return localResponse({ ok: localStore.deleteDecision(payload.id), tier: "free" });
1070
+ return localResponse(paidFeatureNudge("delete_decision"));
989
1071
  }
990
1072
  return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
991
1073
  confirmText: payload.confirmText,
@@ -1139,31 +1221,7 @@ export function createAskTheWMcpServer(options = {}) {
1139
1221
  max_chars: maxCharsSchema,
1140
1222
  }, async (payload) => {
1141
1223
  if (mode.mode !== "paid" && localStore) {
1142
- const loginRequired = requireFreeIdentity();
1143
- if (loginRequired)
1144
- return loginRequired;
1145
- const normalizedQuery = payload.query.toLowerCase();
1146
- const signals = localStore
1147
- .listSignals({
1148
- limit: 100000,
1149
- sessionId: payload.sessionId,
1150
- scopeKey: currentScopeKey(),
1151
- })
1152
- .filter((signal) => [
1153
- signal.summary,
1154
- signal.kind,
1155
- ...signal.filesTouched,
1156
- ...signal.commandsRun,
1157
- ].join("\n").toLowerCase().includes(normalizedQuery))
1158
- .slice(0, payload.limit);
1159
- return budgetedLocalResponse({
1160
- ok: true,
1161
- tier: "free",
1162
- query: payload.query,
1163
- signals: payload.compact === false
1164
- ? signals.map((signal) => signalWithDecision(localStore, signal))
1165
- : signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))),
1166
- }, payload.max_chars ?? 8000);
1224
+ return localResponse(paidFeatureNudge("find_signal_by_summary"));
1167
1225
  }
1168
1226
  return apiToolResponse(routeWithQuery("/api/signals", {
1169
1227
  query: payload.query,
@@ -1202,6 +1260,8 @@ export function createAskTheWMcpServer(options = {}) {
1202
1260
  compact: z.boolean().optional(),
1203
1261
  max_chars: maxCharsSchema,
1204
1262
  }, async (payload) => {
1263
+ if (mode.mode !== "paid")
1264
+ return localResponse(paidFeatureNudge("review_decisions"));
1205
1265
  if (mode.mode === "paid") {
1206
1266
  return apiToolResponse(routeWithQuery("/api/decisions", {
1207
1267
  since: payload.since,
@@ -1213,42 +1273,7 @@ export function createAskTheWMcpServer(options = {}) {
1213
1273
  max_chars: payload.max_chars,
1214
1274
  }));
1215
1275
  }
1216
- if (!localStore) {
1217
- return localToolError({
1218
- code: "local_store_unavailable",
1219
- message: "The local Ask The W store is unavailable.",
1220
- retryable: true,
1221
- hint: "Retry after restarting the plugin host.",
1222
- });
1223
- }
1224
- const loginRequired = requireFreeIdentity();
1225
- if (loginRequired)
1226
- return loginRequired;
1227
- const decisions = localStore.listDecisions({
1228
- since: payload.since,
1229
- status: payload.status,
1230
- limit: payload.limit,
1231
- cursor: payload.cursor,
1232
- sessionId: payload.sessionId,
1233
- scopeKey: currentScopeKey(),
1234
- });
1235
- return budgetedLocalResponse({
1236
- ok: true,
1237
- tier: "free",
1238
- format: payload.format,
1239
- rendered: renderDecisionDigest(decisions),
1240
- decisions: payload.compact
1241
- ? decisions.map((decision) => ({
1242
- id: decision.id,
1243
- headline: decision.headline,
1244
- status: decision.status,
1245
- signalIds: decision.sourceSignalIds,
1246
- }))
1247
- : decisions.map((decision) => decisionWithSignals(localStore, decision)),
1248
- count: decisions.length,
1249
- nextCursor: decisions.length >= payload.limit ? decisions.at(-1)?.createdAt ?? null : null,
1250
- copyHint: "Copy this output to back up your decisions - `export_decisions` is a paid feature.",
1251
- }, payload.max_chars);
1276
+ return localResponse(paidFeatureNudge("review_decisions"));
1252
1277
  });
1253
1278
  server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
1254
1279
  sessionId: z.string().optional(),
@@ -1258,6 +1283,8 @@ export function createAskTheWMcpServer(options = {}) {
1258
1283
  compact: z.boolean().optional(),
1259
1284
  max_chars: maxCharsSchema,
1260
1285
  }, async (payload) => {
1286
+ if (mode.mode !== "paid")
1287
+ return localResponse(paidFeatureNudge("review_session"));
1261
1288
  if (!localStore || mode.mode === "paid") {
1262
1289
  return apiToolResponse(routeWithQuery("/api/signals", {
1263
1290
  sessionId: payload.sessionId,
@@ -1267,79 +1294,26 @@ export function createAskTheWMcpServer(options = {}) {
1267
1294
  max_chars: payload.max_chars,
1268
1295
  }));
1269
1296
  }
1270
- const loginRequired = requireFreeIdentity();
1271
- if (loginRequired)
1272
- return loginRequired;
1273
- const scopeKey = currentScopeKey();
1274
- const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
1275
- const allSessionIds = localStore.listSessionIds({ limit: 100000, scopeKey });
1276
- const allowedSessionIds = new Set(allSessionIds.slice(0, 3));
1277
- if (sessionId && allSessionIds.length > 3 && !allowedSessionIds.has(sessionId)) {
1278
- return localToolError({
1279
- code: "free_tier_limit",
1280
- message: "The free plugin can review the latest three local sessions.",
1281
- retryable: false,
1282
- hint: "Upgrade to review more than three sessions in the workspace dashboard.",
1283
- extra: {
1284
- tool: "review_session",
1285
- limit: 3,
1286
- upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=review_session",
1287
- cta: "Upgrade to review more than three sessions in the workspace dashboard.",
1288
- },
1289
- });
1290
- }
1291
- const limit = Math.min(50, payload.limit ?? 50);
1292
- const signals = sessionId
1293
- ? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
1294
- : [];
1295
- const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
1296
- const decisions = sessionId
1297
- ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
1298
- : [];
1299
- const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
1300
- const counts = signals.reduce((accumulator, signal) => {
1301
- accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
1302
- return accumulator;
1303
- }, {});
1304
- const allCounts = allSignals.reduce((accumulator, signal) => {
1305
- accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
1306
- return accumulator;
1307
- }, {});
1308
- const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
1309
- if (payload.format === "json") {
1310
- return budgetedLocalResponse({
1311
- ok: true,
1312
- tier: "free",
1313
- sessionId,
1314
- format: "json",
1315
- signals: payload.compact
1316
- ? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
1317
- : signals.map((signal) => signalWithDecision(localStore, signal)),
1318
- decisions: payload.compact
1319
- ? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
1320
- : decisions.map((decision) => decisionWithSignals(localStore, decision)),
1321
- decisionCandidates,
1322
- nextCursor,
1323
- counts: {
1324
- totalSignals: allSignals.length,
1325
- byKind: allCounts,
1326
- },
1327
- }, payload.max_chars);
1297
+ return localResponse(paidFeatureNudge("review_session"));
1298
+ });
1299
+ server.tool("analyze_session", "Analyze the latest local coding-agent session.", {
1300
+ sessionId: z.string().optional(),
1301
+ format: z.enum(["markdown", "json"]).default("markdown"),
1302
+ cursor: cursorSchema,
1303
+ limit: z.number().int().positive().max(50).default(50),
1304
+ compact: z.boolean().optional(),
1305
+ max_chars: maxCharsSchema,
1306
+ }, async (payload) => {
1307
+ if (!localStore || mode.mode === "paid") {
1308
+ return apiToolResponse(routeWithQuery("/api/signals", {
1309
+ sessionId: payload.sessionId,
1310
+ cursor: payload.cursor,
1311
+ limit: payload.limit,
1312
+ compact: payload.compact,
1313
+ max_chars: payload.max_chars,
1314
+ }));
1328
1315
  }
1329
- return budgetedLocalResponse({
1330
- ok: true,
1331
- tier: "free",
1332
- sessionId,
1333
- format: "markdown",
1334
- rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
1335
- ...(payload.compact
1336
- ? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
1337
- : {}),
1338
- counts: {
1339
- totalSignals: allSignals.length,
1340
- byKind: Object.keys(allCounts).length ? allCounts : counts,
1341
- },
1342
- }, payload.max_chars);
1316
+ return localSessionAnalysisResponse(payload);
1343
1317
  });
1344
1318
  server.tool("recap", "Summarize the latest local coding-agent session as a digest, standup, or share-ready recap.", {
1345
1319
  format: z.enum(["digest", "standup", "share"]).default("digest"),
@@ -1475,27 +1449,14 @@ export function createAskTheWMcpServer(options = {}) {
1475
1449
  cursor: cursorSchema,
1476
1450
  max_chars: maxCharsSchema,
1477
1451
  }, async (payload) => {
1478
- if (!localStore || mode.mode === "paid")
1452
+ if (mode.mode !== "paid")
1479
1453
  return localResponse(paidFeatureNudge("list_decision_candidates"));
1480
- const loginRequired = requireFreeIdentity();
1481
- if (loginRequired)
1482
- return loginRequired;
1483
- const scopeKey = currentScopeKey();
1484
- const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
1485
- const result = listDecisionCandidates({
1486
- store: localStore,
1487
- sessionId,
1488
- scopeKey,
1454
+ return apiToolResponse(routeWithQuery("/api/signals/decision-candidates", {
1455
+ sessionId: payload.sessionId,
1489
1456
  limit: payload.limit,
1490
1457
  cursor: payload.cursor,
1491
- });
1492
- return budgetedLocalResponse({
1493
- ok: true,
1494
- tier: "free",
1495
- sessionId,
1496
- decisionCandidates: result.candidates,
1497
- nextCursor: result.nextCursor,
1498
- }, payload.max_chars);
1458
+ max_chars: payload.max_chars,
1459
+ }));
1499
1460
  });
1500
1461
  server.tool("search_trail", "Search local signals and decisions together.", {
1501
1462
  query: z.string().min(1),
@@ -1505,27 +1466,16 @@ export function createAskTheWMcpServer(options = {}) {
1505
1466
  compact: z.boolean().optional(),
1506
1467
  max_chars: maxCharsSchema,
1507
1468
  }, async (payload) => {
1508
- if (!localStore || mode.mode === "paid")
1469
+ if (mode.mode !== "paid")
1509
1470
  return localResponse(paidFeatureNudge("search_trail"));
1510
- const loginRequired = requireFreeIdentity();
1511
- if (loginRequired)
1512
- return loginRequired;
1513
- const result = searchTrail({
1514
- store: localStore,
1471
+ return apiToolResponse(routeWithQuery("/api/search/trail", {
1515
1472
  query: payload.query,
1516
- scopeKey: currentScopeKey(),
1517
1473
  sessionId: payload.sessionId,
1518
1474
  limit: payload.limit,
1519
1475
  cursor: payload.cursor,
1520
1476
  compact: payload.compact,
1521
- });
1522
- return budgetedLocalResponse({
1523
- ok: true,
1524
- tier: "free",
1525
- query: payload.query,
1526
- matches: result.matches,
1527
- nextCursor: result.nextCursor,
1528
- }, payload.max_chars);
1477
+ max_chars: payload.max_chars,
1478
+ }));
1529
1479
  });
1530
1480
  server.tool("export_decisions", paidDescription("Export decisions from your workspace.", mode.mode), {
1531
1481
  format: z.enum(["json", "markdown", "jsonl"]).default("json"),
@@ -1563,44 +1513,7 @@ export function createAskTheWMcpServer(options = {}) {
1563
1513
  signalSource: payload.signalSource,
1564
1514
  }));
1565
1515
  }
1566
- if (!localStore) {
1567
- return localToolError({
1568
- code: "local_store_unavailable",
1569
- message: "The local Ask The W store is unavailable.",
1570
- retryable: true,
1571
- hint: "Retry after restarting the plugin host.",
1572
- });
1573
- }
1574
- const loginRequired = requireFreeIdentity();
1575
- if (loginRequired)
1576
- return loginRequired;
1577
- const scopeKey = currentScopeKey();
1578
- const points = buildLocalTimeline({
1579
- scope: payload.scope,
1580
- signals: localStore.listSignals({ scopeKey, limit: 100000 }),
1581
- decisions: localStore.listDecisions({ scopeKey, limit: 100000 }),
1582
- limit: payload.limit,
1583
- });
1584
- const totals = points.reduce((accumulator, point) => ({
1585
- signals: accumulator.signals + point.signalCount,
1586
- decisions: accumulator.decisions + point.decisionCount,
1587
- }), { signals: 0, decisions: 0 });
1588
- return budgetedLocalResponse({
1589
- ok: true,
1590
- tier: "free",
1591
- scope: payload.scope,
1592
- period: {
1593
- start: points[0]?.startedAt ?? points[0]?.x ?? "",
1594
- end: points.at(-1)?.endedAt ?? points.at(-1)?.x ?? "",
1595
- label: "Local timeline",
1596
- tz: "UTC",
1597
- },
1598
- points,
1599
- totals,
1600
- insights: buildLocalTimelineInsights(points),
1601
- narrative: `Local timeline: ${totals.signals} signals, ${totals.decisions} decisions.`,
1602
- markdownTable: renderLocalTimelineMarkdown(points, payload.scope),
1603
- }, payload.max_chars);
1516
+ return localResponse(paidFeatureNudge("view_timeline"));
1604
1517
  });
1605
1518
  return server;
1606
1519
  }
@@ -1919,7 +1832,7 @@ export async function runInitializeHandshake(input = {}) {
1919
1832
  const modulePath = input.entrypoint ?? fileURLToPath(import.meta.url);
1920
1833
  const child = spawn(process.execPath, [modulePath], {
1921
1834
  cwd: process.cwd(),
1922
- env: process.env,
1835
+ env: input.env ?? process.env,
1923
1836
  stdio: ["pipe", "pipe", "pipe"],
1924
1837
  });
1925
1838
  let stdout = "";
@@ -1944,7 +1857,7 @@ export async function runInitializeHandshake(input = {}) {
1944
1857
  };
1945
1858
  child.stdin.write(`${JSON.stringify(initialize)}\n`);
1946
1859
  try {
1947
- await new Promise((resolve, reject) => {
1860
+ return await new Promise((resolve, reject) => {
1948
1861
  const timeout = setTimeout(() => {
1949
1862
  reject(new Error(`Timed out waiting for initialize response. stdout=${stdout} stderr=${stderr}`));
1950
1863
  }, input.timeoutMs ?? 3000);
@@ -1963,7 +1876,11 @@ export async function runInitializeHandshake(input = {}) {
1963
1876
  reject(new Error(`Unexpected initialize response: ${JSON.stringify(response)}`));
1964
1877
  return;
1965
1878
  }
1966
- resolve();
1879
+ resolve({
1880
+ serverInfoVersion: typeof response.result?.serverInfo?.version === "string"
1881
+ ? response.result.serverInfo.version
1882
+ : undefined,
1883
+ });
1967
1884
  };
1968
1885
  child.stdout.on("data", check);
1969
1886
  child.once("exit", failOnExit);
package/dist/install.d.ts CHANGED
@@ -19,10 +19,13 @@ interface InstallHostConfigInput extends HostConfigInput {
19
19
  interface UninstallHostConfigInput {
20
20
  hostType: SupportedHostType;
21
21
  serverName?: string;
22
+ cwds?: string[];
22
23
  dryRun?: boolean;
23
24
  homeDirectory?: string;
24
25
  cwd?: string;
25
26
  }
27
+ export declare const DEFAULT_FREE_SERVER_NAME = "askthew-free";
28
+ export declare const DEFAULT_WORKSPACE_SERVER_NAME = "askthew-workspace";
26
29
  export interface InstallReceipt {
27
30
  hostType: SupportedHostType;
28
31
  serverName: string;
@@ -34,6 +37,8 @@ export interface InstallReceipt {
34
37
  installedAt: string;
35
38
  packageVersion?: string;
36
39
  }
40
+ export declare function packageVersion(): string;
41
+ export declare function defaultServerNameForTier(free?: boolean): "askthew-free" | "askthew-workspace";
37
42
  export declare function resolveSettingsPath(input: {
38
43
  hostType: SupportedHostType;
39
44
  homeDirectory?: string;
@@ -137,10 +142,28 @@ export declare function uninstallHostConfig(input: UninstallHostConfigInput): {
137
142
  settingsPath: string;
138
143
  json: string;
139
144
  removedServerName: string;
145
+ removedServerNames: string[];
140
146
  foundConfigFile: boolean;
141
147
  removedServer: boolean;
142
148
  wroteFile: boolean;
143
149
  };
150
+ export declare function upgradePinnedHostConfig(input: {
151
+ hostType: SupportedHostType;
152
+ serverName?: string;
153
+ packageSpec?: string;
154
+ dryRun?: boolean;
155
+ homeDirectory?: string;
156
+ cwd?: string;
157
+ cwds?: string[];
158
+ }): {
159
+ settingsPath: string;
160
+ json: string;
161
+ packageSpec: string;
162
+ foundConfigFile: boolean;
163
+ upgradedServer: boolean;
164
+ upgradedServerNames: string[];
165
+ wroteFile: boolean;
166
+ };
144
167
  export declare function sendInstallHeartbeat(input: HostConfigInput & {
145
168
  cwd?: string;
146
169
  fetchImpl?: typeof fetch;
package/dist/install.js CHANGED
@@ -1,14 +1,44 @@
1
1
  import fs from "node:fs";
2
+ import { createRequire } from "node:module";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { askTheWDataDir, installReceiptsPath, readJsonFile, writePrivateJson } from "./lib/paths.js";
5
6
  import { resolvePluginScope } from "./scope.js";
6
- const ASKTHEW_INSTRUCTIONS_START = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_START -->";
7
- const ASKTHEW_INSTRUCTIONS_END = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_END -->";
7
+ export const DEFAULT_FREE_SERVER_NAME = "askthew-free";
8
+ export const DEFAULT_WORKSPACE_SERVER_NAME = "askthew-workspace";
9
+ const LEGACY_DEFAULT_SERVER_NAME = "askthew";
10
+ const ASKTHEW_INSTRUCTIONS_START = "<!-- @askthew/mcp-plugin v1 - managed block, do not hand-edit -->";
11
+ const ASKTHEW_INSTRUCTIONS_END = "<!-- /@askthew/mcp-plugin v1 -->";
12
+ const LEGACY_ASKTHEW_INSTRUCTIONS_START = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_START -->";
13
+ const LEGACY_ASKTHEW_INSTRUCTIONS_END = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_END -->";
8
14
  const INSTALL_RECEIPTS_SCHEMA_VERSION = 1;
15
+ const requirePackageJson = createRequire(import.meta.url);
9
16
  function isRecord(value) {
10
17
  return typeof value === "object" && value !== null && !Array.isArray(value);
11
18
  }
19
+ export function packageVersion() {
20
+ try {
21
+ const manifest = requirePackageJson("../package.json");
22
+ return typeof manifest.version === "string" ? manifest.version : "unknown";
23
+ }
24
+ catch {
25
+ return "unknown";
26
+ }
27
+ }
28
+ export function defaultServerNameForTier(free) {
29
+ return free ? DEFAULT_FREE_SERVER_NAME : DEFAULT_WORKSPACE_SERVER_NAME;
30
+ }
31
+ function defaultServerNamesToRemove() {
32
+ return [DEFAULT_FREE_SERVER_NAME, DEFAULT_WORKSPACE_SERVER_NAME, LEGACY_DEFAULT_SERVER_NAME];
33
+ }
34
+ function packageSpecFromPin(env = process.env) {
35
+ const pin = env.ASKTHEW_PIN?.trim() || packageVersion();
36
+ if (!pin || pin === "unknown")
37
+ return "@askthew/mcp-plugin@latest";
38
+ if (pin.startsWith("@askthew/mcp-plugin@"))
39
+ return pin;
40
+ return `@askthew/mcp-plugin@${pin}`;
41
+ }
12
42
  export function resolveSettingsPath(input) {
13
43
  const homeDirectory = input.homeDirectory ?? os.homedir();
14
44
  if (input.hostType === "codex") {
@@ -42,7 +72,7 @@ export function createServerEntry(input) {
42
72
  }
43
73
  return {
44
74
  command: "npx",
45
- args: ["-y", "--prefer-online", "--package", "@askthew/mcp-plugin@latest", "askthew-mcp"],
75
+ args: ["-y", "--package", packageSpecFromPin(), "askthew-mcp"],
46
76
  env,
47
77
  };
48
78
  }
@@ -91,14 +121,75 @@ function removeCodexTomlServer(content, serverName) {
91
121
  const sectionPattern = new RegExp(`\\n?\\[mcp_servers\\.(?:${escapedServerName}|${quotedServerName})\\]\\n[\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`, "g");
92
122
  return content.replace(sectionPattern, "").trimEnd();
93
123
  }
124
+ function serverNamesForRemoval(serverName) {
125
+ const trimmed = serverName?.trim();
126
+ if (!trimmed)
127
+ return defaultServerNamesToRemove();
128
+ return trimmed === LEGACY_DEFAULT_SERVER_NAME
129
+ ? [LEGACY_DEFAULT_SERVER_NAME]
130
+ : [trimmed, LEGACY_DEFAULT_SERVER_NAME];
131
+ }
94
132
  function mergeCodexSettings(input) {
95
133
  let next = input.existingSettings.trimEnd();
96
- if (input.serverName !== "askthew") {
97
- next = removeCodexTomlServer(next, "askthew");
134
+ if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME) {
135
+ next = removeCodexTomlServer(next, LEGACY_DEFAULT_SERVER_NAME);
98
136
  }
99
137
  next = removeCodexTomlServer(next, input.serverName);
100
138
  return `${next}${next ? "\n\n" : ""}${createCodexTomlSection(input)}\n`;
101
139
  }
140
+ function expandHome(inputPath, homeDirectory = os.homedir()) {
141
+ if (inputPath === "~")
142
+ return homeDirectory;
143
+ if (inputPath.startsWith("~/"))
144
+ return path.join(homeDirectory, inputPath.slice(2));
145
+ return inputPath;
146
+ }
147
+ function pathAliases(inputPath, homeDirectory = os.homedir()) {
148
+ const aliases = new Set();
149
+ const expanded = expandHome(inputPath, homeDirectory);
150
+ const resolved = path.resolve(expanded);
151
+ aliases.add(resolved);
152
+ if (resolved.startsWith("/private/tmp/"))
153
+ aliases.add(`/tmp/${resolved.slice("/private/tmp/".length)}`);
154
+ if (resolved.startsWith("/tmp/"))
155
+ aliases.add(`/private/tmp/${resolved.slice("/tmp/".length)}`);
156
+ try {
157
+ aliases.add(fs.realpathSync.native(resolved));
158
+ }
159
+ catch {
160
+ try {
161
+ aliases.add(fs.realpathSync(resolved));
162
+ }
163
+ catch {
164
+ // The project may have been deleted; lexical aliases are still useful.
165
+ }
166
+ }
167
+ return aliases;
168
+ }
169
+ function equivalentProjectPath(left, right, homeDirectory = os.homedir()) {
170
+ const leftAliases = pathAliases(left, homeDirectory);
171
+ const rightAliases = pathAliases(right, homeDirectory);
172
+ for (const alias of leftAliases) {
173
+ if (rightAliases.has(alias))
174
+ return true;
175
+ }
176
+ return false;
177
+ }
178
+ function claudeProjectKeys(input) {
179
+ const homeDirectory = input.homeDirectory ?? os.homedir();
180
+ const cwdAliases = new Set();
181
+ for (const cwd of input.cwds) {
182
+ for (const alias of pathAliases(cwd, homeDirectory)) {
183
+ cwdAliases.add(alias);
184
+ }
185
+ }
186
+ for (const projectKey of Object.keys(input.existingProjects)) {
187
+ if (input.cwds.some((cwd) => equivalentProjectPath(projectKey, cwd, homeDirectory))) {
188
+ cwdAliases.add(projectKey);
189
+ }
190
+ }
191
+ return Array.from(cwdAliases);
192
+ }
102
193
  function mergeClaudeCodeSettings(input) {
103
194
  const cwd = path.resolve(input.cwd ?? process.cwd());
104
195
  const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
@@ -106,8 +197,8 @@ function mergeClaudeCodeSettings(input) {
106
197
  const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
107
198
  const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
108
199
  const nextMcpServers = { ...existingMcpServers };
109
- if (input.serverName !== "askthew" && "askthew" in nextMcpServers) {
110
- delete nextMcpServers.askthew;
200
+ if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME && LEGACY_DEFAULT_SERVER_NAME in nextMcpServers) {
201
+ delete nextMcpServers[LEGACY_DEFAULT_SERVER_NAME];
111
202
  }
112
203
  return {
113
204
  ...existingSettings,
@@ -127,8 +218,8 @@ export function mergeHostSettings(input) {
127
218
  const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
128
219
  const existingMcpServers = isRecord(existingSettings.mcpServers) ? existingSettings.mcpServers : {};
129
220
  const nextMcpServers = { ...existingMcpServers };
130
- if (input.serverName !== "askthew" && "askthew" in nextMcpServers) {
131
- delete nextMcpServers.askthew;
221
+ if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME && LEGACY_DEFAULT_SERVER_NAME in nextMcpServers) {
222
+ delete nextMcpServers[LEGACY_DEFAULT_SERVER_NAME];
132
223
  }
133
224
  return {
134
225
  ...existingSettings,
@@ -292,7 +383,7 @@ export function uninstallHostConfig(input) {
292
383
  hostType: input.hostType,
293
384
  homeDirectory: input.homeDirectory,
294
385
  });
295
- const serverName = input.serverName?.trim() || "askthew";
386
+ const serverNames = serverNamesForRemoval(input.serverName);
296
387
  let json = "";
297
388
  let foundConfigFile = false;
298
389
  let removedServer = false;
@@ -300,43 +391,55 @@ export function uninstallHostConfig(input) {
300
391
  foundConfigFile = true;
301
392
  const raw = fs.readFileSync(settingsPath, "utf8");
302
393
  if (input.hostType === "codex") {
303
- removedServer = raw !== removeCodexTomlServer(raw, serverName) || (serverName !== "askthew" && raw !== removeCodexTomlServer(raw, "askthew"));
304
- json = removeCodexTomlServer(raw, serverName);
305
- if (serverName !== "askthew") {
306
- json = removeCodexTomlServer(json, "askthew");
394
+ json = raw;
395
+ for (const serverName of serverNames) {
396
+ const before = json;
397
+ json = removeCodexTomlServer(json, serverName);
398
+ removedServer = removedServer || before !== json;
307
399
  }
308
400
  json = json ? `${json}\n` : "";
309
401
  }
310
402
  else {
311
403
  const parsed = raw.trim() ? JSON.parse(raw) : {};
312
404
  if (input.hostType === "claude_code") {
313
- const cwd = path.resolve(input.cwd ?? process.cwd());
405
+ const cwdInputs = input.cwds && input.cwds.length > 0 ? input.cwds : [input.cwd ?? process.cwd()];
314
406
  const existingProjects = isRecord(parsed.projects) ? parsed.projects : {};
315
- const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
316
- const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
317
- const nextServers = { ...existingMcpServers };
318
- removedServer = serverName in nextServers || (serverName !== "askthew" && "askthew" in nextServers);
319
- delete nextServers[serverName];
320
- if (serverName !== "askthew")
321
- delete nextServers.askthew;
322
- json = JSON.stringify({
323
- ...parsed,
324
- projects: {
325
- ...existingProjects,
326
- [cwd]: {
407
+ const nextProjects = { ...existingProjects };
408
+ const projectKeys = claudeProjectKeys({
409
+ existingProjects,
410
+ cwds: cwdInputs,
411
+ homeDirectory: input.homeDirectory,
412
+ });
413
+ for (const cwd of projectKeys) {
414
+ const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
415
+ const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
416
+ const nextServers = { ...existingMcpServers };
417
+ const beforeCount = Object.keys(nextServers).length;
418
+ for (const serverName of serverNames) {
419
+ delete nextServers[serverName];
420
+ }
421
+ const removedHere = Object.keys(nextServers).length !== beforeCount;
422
+ removedServer = removedServer || removedHere;
423
+ if (removedHere || isRecord(existingProjects[cwd])) {
424
+ nextProjects[cwd] = {
327
425
  ...existingProject,
328
426
  mcpServers: nextServers,
329
- },
330
- },
427
+ };
428
+ }
429
+ }
430
+ json = JSON.stringify({
431
+ ...parsed,
432
+ projects: nextProjects,
331
433
  }, null, 2);
332
434
  }
333
435
  else {
334
436
  const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
335
437
  const nextServers = { ...existingMcpServers };
336
- removedServer = serverName in nextServers || (serverName !== "askthew" && "askthew" in nextServers);
337
- delete nextServers[serverName];
338
- if (serverName !== "askthew")
339
- delete nextServers.askthew;
438
+ const beforeCount = Object.keys(nextServers).length;
439
+ for (const serverName of serverNames) {
440
+ delete nextServers[serverName];
441
+ }
442
+ removedServer = Object.keys(nextServers).length !== beforeCount;
340
443
  json = JSON.stringify({ ...parsed, mcpServers: nextServers }, null, 2);
341
444
  }
342
445
  json = `${json}\n`;
@@ -348,12 +451,147 @@ export function uninstallHostConfig(input) {
348
451
  return {
349
452
  settingsPath,
350
453
  json,
351
- removedServerName: serverName,
454
+ removedServerName: serverNames.join(", "),
455
+ removedServerNames: serverNames,
352
456
  foundConfigFile,
353
457
  removedServer,
354
458
  wroteFile: !input.dryRun && foundConfigFile,
355
459
  };
356
460
  }
461
+ function updateNpxPackageArgs(args, packageSpec) {
462
+ let changed = false;
463
+ const nextArgs = args.map((arg, index) => {
464
+ if (typeof arg !== "string")
465
+ return arg;
466
+ if (arg === "--package" && typeof args[index + 1] === "string")
467
+ return arg;
468
+ if (index > 0 && args[index - 1] === "--package" && arg.startsWith("@askthew/mcp-plugin@")) {
469
+ changed = changed || arg !== packageSpec;
470
+ return packageSpec;
471
+ }
472
+ if (arg.startsWith("@askthew/mcp-plugin@")) {
473
+ changed = changed || arg !== packageSpec;
474
+ return packageSpec;
475
+ }
476
+ return arg;
477
+ });
478
+ return { args: nextArgs, changed };
479
+ }
480
+ function updateServerEntryPackage(entry, packageSpec) {
481
+ if (!isRecord(entry) || !Array.isArray(entry.args))
482
+ return { entry, changed: false };
483
+ const updated = updateNpxPackageArgs(entry.args, packageSpec);
484
+ if (!updated.changed)
485
+ return { entry, changed: false };
486
+ return {
487
+ entry: {
488
+ ...entry,
489
+ args: updated.args,
490
+ },
491
+ changed: true,
492
+ };
493
+ }
494
+ function updateCodexPackageSpec(input) {
495
+ let changed = false;
496
+ let next = input.content;
497
+ for (const serverName of input.serverNames) {
498
+ const escapedServerName = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
499
+ const quotedServerName = escapeTomlString(serverName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
500
+ const sectionPattern = new RegExp(`(\\n?\\[mcp_servers\\.(?:${escapedServerName}|${quotedServerName})\\]\\n[\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|$)`, "g");
501
+ next = next.replace(sectionPattern, (section) => {
502
+ const updated = section.replace(/@askthew\/mcp-plugin@[^"',\]\s]+/g, input.packageSpec);
503
+ changed = changed || updated !== section;
504
+ return updated;
505
+ });
506
+ }
507
+ return { json: next, changed };
508
+ }
509
+ export function upgradePinnedHostConfig(input) {
510
+ const settingsPath = resolveSettingsPath({
511
+ hostType: input.hostType,
512
+ homeDirectory: input.homeDirectory,
513
+ });
514
+ const packageSpec = input.packageSpec?.trim() || packageSpecFromPin();
515
+ const serverNames = serverNamesForRemoval(input.serverName);
516
+ let json = "";
517
+ let foundConfigFile = false;
518
+ let upgradedServer = false;
519
+ const upgradedServerNames = new Set();
520
+ if (fs.existsSync(settingsPath)) {
521
+ foundConfigFile = true;
522
+ const raw = fs.readFileSync(settingsPath, "utf8");
523
+ if (input.hostType === "codex") {
524
+ const updated = updateCodexPackageSpec({ content: raw, serverNames, packageSpec });
525
+ json = updated.json;
526
+ upgradedServer = updated.changed;
527
+ if (updated.changed) {
528
+ for (const serverName of serverNames)
529
+ upgradedServerNames.add(serverName);
530
+ }
531
+ }
532
+ else {
533
+ const parsed = raw.trim() ? JSON.parse(raw) : {};
534
+ if (input.hostType === "claude_code") {
535
+ const existingProjects = isRecord(parsed.projects) ? parsed.projects : {};
536
+ const cwdInputs = input.cwds && input.cwds.length > 0 ? input.cwds : [input.cwd ?? process.cwd()];
537
+ const projectKeys = claudeProjectKeys({
538
+ existingProjects,
539
+ cwds: cwdInputs,
540
+ homeDirectory: input.homeDirectory,
541
+ });
542
+ const nextProjects = { ...existingProjects };
543
+ for (const cwd of projectKeys) {
544
+ const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
545
+ const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
546
+ const nextServers = { ...existingMcpServers };
547
+ let touchedProject = false;
548
+ for (const serverName of serverNames) {
549
+ const updated = updateServerEntryPackage(nextServers[serverName], packageSpec);
550
+ if (updated.changed) {
551
+ nextServers[serverName] = updated.entry;
552
+ upgradedServer = true;
553
+ touchedProject = true;
554
+ upgradedServerNames.add(serverName);
555
+ }
556
+ }
557
+ if (touchedProject) {
558
+ nextProjects[cwd] = {
559
+ ...existingProject,
560
+ mcpServers: nextServers,
561
+ };
562
+ }
563
+ }
564
+ json = JSON.stringify({ ...parsed, projects: nextProjects }, null, 2);
565
+ }
566
+ else {
567
+ const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
568
+ const nextServers = { ...existingMcpServers };
569
+ for (const serverName of serverNames) {
570
+ const updated = updateServerEntryPackage(nextServers[serverName], packageSpec);
571
+ if (updated.changed) {
572
+ nextServers[serverName] = updated.entry;
573
+ upgradedServer = true;
574
+ upgradedServerNames.add(serverName);
575
+ }
576
+ }
577
+ json = JSON.stringify({ ...parsed, mcpServers: nextServers }, null, 2);
578
+ }
579
+ json = `${json}\n`;
580
+ }
581
+ if (!input.dryRun && upgradedServer) {
582
+ fs.writeFileSync(settingsPath, json.endsWith("\n") ? json : `${json}\n`, "utf8");
583
+ }
584
+ }
585
+ return {
586
+ settingsPath,
587
+ json,
588
+ packageSpec,
589
+ foundConfigFile,
590
+ upgradedServer,
591
+ upgradedServerNames: Array.from(upgradedServerNames),
592
+ wroteFile: !input.dryRun && foundConfigFile && upgradedServer,
593
+ };
594
+ }
357
595
  export async function sendInstallHeartbeat(input) {
358
596
  const fetcher = input.fetchImpl ?? fetch;
359
597
  const scope = resolvePluginScope(input.cwd ?? process.cwd());
@@ -424,6 +662,16 @@ function behaviorInstructions(hostType, cwd = process.cwd()) {
424
662
  `- If the tool is unavailable, tell the user to restart or reload ${hostLabel}; do not use \`list_mcp_resources\` or \`list_mcp_resource_templates\` as pass/fail checks.`,
425
663
  "- If you realize later in the conversation that the startup call was missed, send it immediately with `metadata.recovered_missed_startup=true`.",
426
664
  "",
665
+ "Schema:",
666
+ "- `sessionId` (string, required): stable across one session. Derive as `<repo-name>-<YYYYMMDD>-<git-HEAD-short>`. Reuse for every call in the session.",
667
+ "- `sequence` (integer >= 0, required): start at 0, increment by 1 per call within the same `sessionId`.",
668
+ "- `kind` (enum, required): one of `setup_complete`, `session_checkpoint`, `direction_change`, `implementation_update`, `verification_result`, `final_summary`.",
669
+ "- `summary` (string <= 2000, required).",
670
+ "- `evidence`, `filesTouched`, `commandsRun`, `metadata`: optional.",
671
+ "",
672
+ "Example:",
673
+ '{ "sessionId": "thesisengine-20260508-a1b2c3d", "sequence": 0, "kind": "setup_complete", "summary": "..." }',
674
+ "",
427
675
  "Send an update:",
428
676
  "- after the user accepts or rejects product, architecture, or implementation direction",
429
677
  "- before using tools that write files, after meaningful implementation changes",
@@ -431,7 +679,7 @@ function behaviorInstructions(hostType, cwd = process.cwd()) {
431
679
  "- at the final summary",
432
680
  ...(stackGuidance.length > 0 ? ["", "Stack-specific nudges:", ...stackGuidance] : []),
433
681
  "",
434
- "Keep updates compact: short summary, minimal evidence excerpts, files touched, commands run, and useful metadata. Do not send full transcripts. Redact obvious secrets before sending.",
682
+ "Keep updates compact: short summary, minimal evidence excerpts, files touched, commands run, and useful metadata. Do not send full transcripts. Redact obvious secrets in evidence excerpts (commands, file paths, log lines). Server-side redaction (AWS, Stripe, GitHub, JWT, PEM, OpenAI/Anthropic, DSNs, emails, SSN) runs as a safety net, but agent-side redaction is still preferred.",
435
683
  "",
436
684
  ASKTHEW_INSTRUCTIONS_END,
437
685
  "",
@@ -448,10 +696,15 @@ function cursorBehaviorInstructions(cwd = process.cwd()) {
448
696
  ].join("\n");
449
697
  }
450
698
  function upsertMarkedBlock(existing, block) {
451
- const startIndex = existing.indexOf(ASKTHEW_INSTRUCTIONS_START);
452
- const endIndex = existing.indexOf(ASKTHEW_INSTRUCTIONS_END);
453
- if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
454
- const afterEnd = endIndex + ASKTHEW_INSTRUCTIONS_END.length;
699
+ for (const [startMarker, endMarker] of [
700
+ [ASKTHEW_INSTRUCTIONS_START, ASKTHEW_INSTRUCTIONS_END],
701
+ [LEGACY_ASKTHEW_INSTRUCTIONS_START, LEGACY_ASKTHEW_INSTRUCTIONS_END],
702
+ ]) {
703
+ const startIndex = existing.indexOf(startMarker);
704
+ const endIndex = existing.indexOf(endMarker);
705
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex)
706
+ continue;
707
+ const afterEnd = endIndex + endMarker.length;
455
708
  return `${existing.slice(0, startIndex).trimEnd()}\n\n${block.trimEnd()}\n${existing.slice(afterEnd).trimStart()}`.trimEnd() + "\n";
456
709
  }
457
710
  return `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block.trimEnd()}\n`;
@@ -495,7 +748,14 @@ export function uninstallBehaviorInstructions(input) {
495
748
  if (!fs.existsSync(instructionsPath))
496
749
  continue;
497
750
  const existing = fs.readFileSync(instructionsPath, "utf8");
498
- const next = existing.replace(new RegExp(`\\n?${ASKTHEW_INSTRUCTIONS_START}[\\s\\S]*?${ASKTHEW_INSTRUCTIONS_END}\\n?`, "g"), "\n").trimEnd() + "\n";
751
+ let next = existing;
752
+ for (const [startMarker, endMarker] of [
753
+ [ASKTHEW_INSTRUCTIONS_START, ASKTHEW_INSTRUCTIONS_END],
754
+ [LEGACY_ASKTHEW_INSTRUCTIONS_START, LEGACY_ASKTHEW_INSTRUCTIONS_END],
755
+ ]) {
756
+ next = next.replace(new RegExp(`\\n?${startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "g"), "\n");
757
+ }
758
+ next = next.trimEnd() + "\n";
499
759
  if (!input.dryRun)
500
760
  fs.writeFileSync(instructionsPath, next, "utf8");
501
761
  touchedPaths.push(instructionsPath);
@@ -497,10 +497,21 @@ export class LocalStore {
497
497
  openDatabase() {
498
498
  const loaded = tryRequireBetterSqlite3();
499
499
  if (loaded) {
500
- fs.mkdirSync(path.dirname(this.storePath), { recursive: true, mode: 0o700 });
501
- this.db = new loaded(this.storePath);
502
- this.migrate();
503
- return;
500
+ try {
501
+ fs.mkdirSync(path.dirname(this.storePath), { recursive: true, mode: 0o700 });
502
+ this.db = new loaded(this.storePath);
503
+ this.migrate();
504
+ return;
505
+ }
506
+ catch {
507
+ try {
508
+ this.db?.close();
509
+ }
510
+ catch {
511
+ // Ignore close failures while falling back to the JSON store.
512
+ }
513
+ this.db = null;
514
+ }
504
515
  }
505
516
  this.jsonMode = true;
506
517
  this.jsonPath = jsonFallbackStorePath();
@@ -22,7 +22,7 @@ export function paidFeatureNudge(tool) {
22
22
  pricingUrl: PRICING_URL,
23
23
  upgradeUrl: `https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=${encodeURIComponent(tool)}`,
24
24
  supportEmail: SUPPORT_EMAIL,
25
- cta: "Run: npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp upgrade",
25
+ cta: "Run: npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp upgrade --browser",
26
26
  };
27
27
  }
28
28
  export function toolJson(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "private": false,
5
5
  "description": "Ask The W plugin connector for local-first coding-agent decisions, signals, and review.",
6
6
  "type": "module",