@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/README.md +9 -11
- package/dist/cli.js +136 -23
- package/dist/index.d.ts +4 -1
- package/dist/index.js +95 -668
- package/dist/install.d.ts +23 -0
- package/dist/install.js +300 -40
- package/dist/lib/local-store.js +15 -4
- package/dist/lib/upgrade-nudge.js +1 -1
- package/package.json +1 -1
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
|
|
7
|
-
const
|
|
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", "--
|
|
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 !==
|
|
97
|
-
next = removeCodexTomlServer(next,
|
|
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 !==
|
|
110
|
-
delete nextMcpServers
|
|
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 !==
|
|
131
|
-
delete nextMcpServers
|
|
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
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
json = removeCodexTomlServer(json,
|
|
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
|
|
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
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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:
|
|
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
|
|
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
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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);
|
package/dist/lib/local-store.js
CHANGED
|
@@ -497,10 +497,21 @@ export class LocalStore {
|
|
|
497
497
|
openDatabase() {
|
|
498
498
|
const loaded = tryRequireBetterSqlite3();
|
|
499
499
|
if (loaded) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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) {
|