@askthew/mcp-plugin 0.4.8 → 0.4.10

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/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.10",
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",