@askthew/mcp-plugin 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +184 -30
- package/dist/index.d.ts +7 -0
- package/dist/index.js +223 -229
- package/dist/install.d.ts +49 -0
- package/dist/install.js +388 -52
- package/dist/lib/local-store.js +15 -4
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +14 -0
- package/dist/lib/upgrade-nudge.js +1 -1
- package/package.json +1 -1
package/dist/install.js
CHANGED
|
@@ -1,12 +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";
|
|
5
|
+
import { askTheWDataDir, installReceiptsPath, readJsonFile, writePrivateJson } from "./lib/paths.js";
|
|
4
6
|
import { resolvePluginScope } from "./scope.js";
|
|
5
|
-
const
|
|
6
|
-
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 -->";
|
|
14
|
+
const INSTALL_RECEIPTS_SCHEMA_VERSION = 1;
|
|
15
|
+
const requirePackageJson = createRequire(import.meta.url);
|
|
7
16
|
function isRecord(value) {
|
|
8
17
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
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
|
+
}
|
|
10
42
|
export function resolveSettingsPath(input) {
|
|
11
43
|
const homeDirectory = input.homeDirectory ?? os.homedir();
|
|
12
44
|
if (input.hostType === "codex") {
|
|
@@ -19,21 +51,29 @@ export function resolveSettingsPath(input) {
|
|
|
19
51
|
}
|
|
20
52
|
export function createServerEntry(input) {
|
|
21
53
|
const scope = resolvePluginScope(input.cwd ?? process.cwd());
|
|
54
|
+
const env = {
|
|
55
|
+
ASKTHEW_API_URL: input.apiUrl,
|
|
56
|
+
...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
|
|
57
|
+
...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
|
|
58
|
+
...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
|
|
59
|
+
ASKTHEW_HOST_TYPE: input.hostType,
|
|
60
|
+
ASKTHEW_SERVER_NAME: input.serverName,
|
|
61
|
+
ASKTHEW_REPO_NAME: scope.repoName,
|
|
62
|
+
...(scope.repoRoot ? { ASKTHEW_REPO_ROOT: scope.repoRoot } : {}),
|
|
63
|
+
...(scope.appPath ? { ASKTHEW_APP_PATH: scope.appPath } : {}),
|
|
64
|
+
...(scope.serviceName ? { ASKTHEW_SERVICE_NAME: scope.serviceName } : {}),
|
|
65
|
+
};
|
|
66
|
+
if (input.serverEntrypoint) {
|
|
67
|
+
return {
|
|
68
|
+
command: "node",
|
|
69
|
+
args: [path.resolve(input.serverEntrypoint)],
|
|
70
|
+
env,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
22
73
|
return {
|
|
23
74
|
command: "npx",
|
|
24
|
-
args: ["-y", "--
|
|
25
|
-
env
|
|
26
|
-
ASKTHEW_API_URL: input.apiUrl,
|
|
27
|
-
...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
|
|
28
|
-
...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
|
|
29
|
-
...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
|
|
30
|
-
ASKTHEW_HOST_TYPE: input.hostType,
|
|
31
|
-
ASKTHEW_SERVER_NAME: input.serverName,
|
|
32
|
-
ASKTHEW_REPO_NAME: scope.repoName,
|
|
33
|
-
...(scope.repoRoot ? { ASKTHEW_REPO_ROOT: scope.repoRoot } : {}),
|
|
34
|
-
...(scope.appPath ? { ASKTHEW_APP_PATH: scope.appPath } : {}),
|
|
35
|
-
...(scope.serviceName ? { ASKTHEW_SERVICE_NAME: scope.serviceName } : {}),
|
|
36
|
-
},
|
|
75
|
+
args: ["-y", "--package", packageSpecFromPin(), "askthew-mcp"],
|
|
76
|
+
env,
|
|
37
77
|
};
|
|
38
78
|
}
|
|
39
79
|
export function createHostConfigSnippet(input) {
|
|
@@ -81,14 +121,75 @@ function removeCodexTomlServer(content, serverName) {
|
|
|
81
121
|
const sectionPattern = new RegExp(`\\n?\\[mcp_servers\\.(?:${escapedServerName}|${quotedServerName})\\]\\n[\\s\\S]*?(?=\\n\\[[^\\]]+\\]|$)`, "g");
|
|
82
122
|
return content.replace(sectionPattern, "").trimEnd();
|
|
83
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
|
+
}
|
|
84
132
|
function mergeCodexSettings(input) {
|
|
85
133
|
let next = input.existingSettings.trimEnd();
|
|
86
|
-
if (input.serverName !==
|
|
87
|
-
next = removeCodexTomlServer(next,
|
|
134
|
+
if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME) {
|
|
135
|
+
next = removeCodexTomlServer(next, LEGACY_DEFAULT_SERVER_NAME);
|
|
88
136
|
}
|
|
89
137
|
next = removeCodexTomlServer(next, input.serverName);
|
|
90
138
|
return `${next}${next ? "\n\n" : ""}${createCodexTomlSection(input)}\n`;
|
|
91
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
|
+
}
|
|
92
193
|
function mergeClaudeCodeSettings(input) {
|
|
93
194
|
const cwd = path.resolve(input.cwd ?? process.cwd());
|
|
94
195
|
const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
|
|
@@ -96,8 +197,8 @@ function mergeClaudeCodeSettings(input) {
|
|
|
96
197
|
const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
|
|
97
198
|
const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
|
|
98
199
|
const nextMcpServers = { ...existingMcpServers };
|
|
99
|
-
if (input.serverName !==
|
|
100
|
-
delete nextMcpServers
|
|
200
|
+
if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME && LEGACY_DEFAULT_SERVER_NAME in nextMcpServers) {
|
|
201
|
+
delete nextMcpServers[LEGACY_DEFAULT_SERVER_NAME];
|
|
101
202
|
}
|
|
102
203
|
return {
|
|
103
204
|
...existingSettings,
|
|
@@ -117,8 +218,8 @@ export function mergeHostSettings(input) {
|
|
|
117
218
|
const existingSettings = isRecord(input.existingSettings) ? input.existingSettings : {};
|
|
118
219
|
const existingMcpServers = isRecord(existingSettings.mcpServers) ? existingSettings.mcpServers : {};
|
|
119
220
|
const nextMcpServers = { ...existingMcpServers };
|
|
120
|
-
if (input.serverName !==
|
|
121
|
-
delete nextMcpServers
|
|
221
|
+
if (input.serverName !== LEGACY_DEFAULT_SERVER_NAME && LEGACY_DEFAULT_SERVER_NAME in nextMcpServers) {
|
|
222
|
+
delete nextMcpServers[LEGACY_DEFAULT_SERVER_NAME];
|
|
122
223
|
}
|
|
123
224
|
return {
|
|
124
225
|
...existingSettings,
|
|
@@ -150,6 +251,9 @@ export function formatInstallCommand(input) {
|
|
|
150
251
|
parts.push("--email", JSON.stringify(input.email));
|
|
151
252
|
}
|
|
152
253
|
}
|
|
254
|
+
if (input.serverEntrypoint) {
|
|
255
|
+
parts.push("--server-entrypoint", JSON.stringify(input.serverEntrypoint));
|
|
256
|
+
}
|
|
153
257
|
return parts.join(" ");
|
|
154
258
|
}
|
|
155
259
|
export function verificationNextStep(hostType) {
|
|
@@ -188,6 +292,7 @@ export function installHostConfig(input) {
|
|
|
188
292
|
clientLabel: input.clientLabel,
|
|
189
293
|
free: input.free,
|
|
190
294
|
cwd: input.cwd,
|
|
295
|
+
serverEntrypoint: input.serverEntrypoint,
|
|
191
296
|
};
|
|
192
297
|
const json = input.hostType === "codex"
|
|
193
298
|
? mergeCodexSettings({
|
|
@@ -211,12 +316,74 @@ export function installHostConfig(input) {
|
|
|
211
316
|
nextStep: verificationNextStep(input.hostType),
|
|
212
317
|
};
|
|
213
318
|
}
|
|
319
|
+
function normalizeInstructionPaths(paths) {
|
|
320
|
+
return Array.from(new Set(paths.map((entry) => path.resolve(entry))));
|
|
321
|
+
}
|
|
322
|
+
function normalizeReceipt(receipt) {
|
|
323
|
+
return {
|
|
324
|
+
...receipt,
|
|
325
|
+
settingsPath: path.resolve(receipt.settingsPath),
|
|
326
|
+
cwd: path.resolve(receipt.cwd),
|
|
327
|
+
instructionPaths: normalizeInstructionPaths(receipt.instructionPaths ?? []),
|
|
328
|
+
dataDir: path.resolve(receipt.dataDir || askTheWDataDir()),
|
|
329
|
+
serverEntrypoint: receipt.serverEntrypoint ? path.resolve(receipt.serverEntrypoint) : undefined,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function receiptKey(receipt) {
|
|
333
|
+
return `${receipt.hostType}\u0000${receipt.serverName}\u0000${path.resolve(receipt.cwd)}`;
|
|
334
|
+
}
|
|
335
|
+
export function readInstallReceipts(env = process.env) {
|
|
336
|
+
const parsed = readJsonFile(installReceiptsPath(env));
|
|
337
|
+
if (!parsed || parsed.schemaVersion !== INSTALL_RECEIPTS_SCHEMA_VERSION || !Array.isArray(parsed.installs)) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
return parsed.installs
|
|
341
|
+
.filter((receipt) => Boolean(receipt?.hostType && receipt.serverName && receipt.settingsPath && receipt.cwd))
|
|
342
|
+
.map(normalizeReceipt);
|
|
343
|
+
}
|
|
344
|
+
export function writeInstallReceipt(receipt, env = process.env) {
|
|
345
|
+
const nextReceipt = normalizeReceipt({
|
|
346
|
+
...receipt,
|
|
347
|
+
dataDir: receipt.dataDir ?? askTheWDataDir(env),
|
|
348
|
+
installedAt: receipt.installedAt ?? new Date().toISOString(),
|
|
349
|
+
});
|
|
350
|
+
const receipts = readInstallReceipts(env).filter((entry) => receiptKey(entry) !== receiptKey(nextReceipt));
|
|
351
|
+
receipts.push(nextReceipt);
|
|
352
|
+
receipts.sort((left, right) => left.installedAt.localeCompare(right.installedAt));
|
|
353
|
+
writePrivateJson(installReceiptsPath(env), {
|
|
354
|
+
schemaVersion: INSTALL_RECEIPTS_SCHEMA_VERSION,
|
|
355
|
+
installs: receipts,
|
|
356
|
+
});
|
|
357
|
+
return nextReceipt;
|
|
358
|
+
}
|
|
359
|
+
export function findInstallReceipts(input, env = process.env) {
|
|
360
|
+
return readInstallReceipts(env).filter((receipt) => receipt.hostType === input.hostType &&
|
|
361
|
+
(!input.serverName || receipt.serverName === input.serverName));
|
|
362
|
+
}
|
|
363
|
+
export function removeInstallReceipts(input, env = process.env) {
|
|
364
|
+
const cwd = input.cwd ? path.resolve(input.cwd) : undefined;
|
|
365
|
+
const receipts = readInstallReceipts(env);
|
|
366
|
+
const next = receipts.filter((receipt) => {
|
|
367
|
+
if (receipt.hostType !== input.hostType)
|
|
368
|
+
return true;
|
|
369
|
+
if (input.serverName && receipt.serverName !== input.serverName)
|
|
370
|
+
return true;
|
|
371
|
+
if (cwd && receipt.cwd !== cwd)
|
|
372
|
+
return true;
|
|
373
|
+
return false;
|
|
374
|
+
});
|
|
375
|
+
writePrivateJson(installReceiptsPath(env), {
|
|
376
|
+
schemaVersion: INSTALL_RECEIPTS_SCHEMA_VERSION,
|
|
377
|
+
installs: next,
|
|
378
|
+
});
|
|
379
|
+
return receipts.length - next.length;
|
|
380
|
+
}
|
|
214
381
|
export function uninstallHostConfig(input) {
|
|
215
382
|
const settingsPath = resolveSettingsPath({
|
|
216
383
|
hostType: input.hostType,
|
|
217
384
|
homeDirectory: input.homeDirectory,
|
|
218
385
|
});
|
|
219
|
-
const
|
|
386
|
+
const serverNames = serverNamesForRemoval(input.serverName);
|
|
220
387
|
let json = "";
|
|
221
388
|
let foundConfigFile = false;
|
|
222
389
|
let removedServer = false;
|
|
@@ -224,43 +391,55 @@ export function uninstallHostConfig(input) {
|
|
|
224
391
|
foundConfigFile = true;
|
|
225
392
|
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
226
393
|
if (input.hostType === "codex") {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
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;
|
|
231
399
|
}
|
|
232
400
|
json = json ? `${json}\n` : "";
|
|
233
401
|
}
|
|
234
402
|
else {
|
|
235
403
|
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
236
404
|
if (input.hostType === "claude_code") {
|
|
237
|
-
const
|
|
405
|
+
const cwdInputs = input.cwds && input.cwds.length > 0 ? input.cwds : [input.cwd ?? process.cwd()];
|
|
238
406
|
const existingProjects = isRecord(parsed.projects) ? parsed.projects : {};
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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] = {
|
|
251
425
|
...existingProject,
|
|
252
426
|
mcpServers: nextServers,
|
|
253
|
-
}
|
|
254
|
-
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
json = JSON.stringify({
|
|
431
|
+
...parsed,
|
|
432
|
+
projects: nextProjects,
|
|
255
433
|
}, null, 2);
|
|
256
434
|
}
|
|
257
435
|
else {
|
|
258
436
|
const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
259
437
|
const nextServers = { ...existingMcpServers };
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
264
443
|
json = JSON.stringify({ ...parsed, mcpServers: nextServers }, null, 2);
|
|
265
444
|
}
|
|
266
445
|
json = `${json}\n`;
|
|
@@ -272,12 +451,147 @@ export function uninstallHostConfig(input) {
|
|
|
272
451
|
return {
|
|
273
452
|
settingsPath,
|
|
274
453
|
json,
|
|
275
|
-
removedServerName:
|
|
454
|
+
removedServerName: serverNames.join(", "),
|
|
455
|
+
removedServerNames: serverNames,
|
|
276
456
|
foundConfigFile,
|
|
277
457
|
removedServer,
|
|
278
458
|
wroteFile: !input.dryRun && foundConfigFile,
|
|
279
459
|
};
|
|
280
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
|
+
}
|
|
281
595
|
export async function sendInstallHeartbeat(input) {
|
|
282
596
|
const fetcher = input.fetchImpl ?? fetch;
|
|
283
597
|
const scope = resolvePluginScope(input.cwd ?? process.cwd());
|
|
@@ -348,6 +662,16 @@ function behaviorInstructions(hostType, cwd = process.cwd()) {
|
|
|
348
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.`,
|
|
349
663
|
"- If you realize later in the conversation that the startup call was missed, send it immediately with `metadata.recovered_missed_startup=true`.",
|
|
350
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
|
+
"",
|
|
351
675
|
"Send an update:",
|
|
352
676
|
"- after the user accepts or rejects product, architecture, or implementation direction",
|
|
353
677
|
"- before using tools that write files, after meaningful implementation changes",
|
|
@@ -355,7 +679,7 @@ function behaviorInstructions(hostType, cwd = process.cwd()) {
|
|
|
355
679
|
"- at the final summary",
|
|
356
680
|
...(stackGuidance.length > 0 ? ["", "Stack-specific nudges:", ...stackGuidance] : []),
|
|
357
681
|
"",
|
|
358
|
-
"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.",
|
|
359
683
|
"",
|
|
360
684
|
ASKTHEW_INSTRUCTIONS_END,
|
|
361
685
|
"",
|
|
@@ -372,10 +696,15 @@ function cursorBehaviorInstructions(cwd = process.cwd()) {
|
|
|
372
696
|
].join("\n");
|
|
373
697
|
}
|
|
374
698
|
function upsertMarkedBlock(existing, block) {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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;
|
|
379
708
|
return `${existing.slice(0, startIndex).trimEnd()}\n\n${block.trimEnd()}\n${existing.slice(afterEnd).trimStart()}`.trimEnd() + "\n";
|
|
380
709
|
}
|
|
381
710
|
return `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block.trimEnd()}\n`;
|
|
@@ -419,7 +748,14 @@ export function uninstallBehaviorInstructions(input) {
|
|
|
419
748
|
if (!fs.existsSync(instructionsPath))
|
|
420
749
|
continue;
|
|
421
750
|
const existing = fs.readFileSync(instructionsPath, "utf8");
|
|
422
|
-
|
|
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";
|
|
423
759
|
if (!input.dryRun)
|
|
424
760
|
fs.writeFileSync(instructionsPath, next, "utf8");
|
|
425
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();
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export declare function askTheWDataDir(env?: NodeJS.ProcessEnv): string;
|
|
2
|
+
export declare function askTheWStateDir(env?: NodeJS.ProcessEnv): string;
|
|
2
3
|
export declare function ensureAskTheWDataDir(env?: NodeJS.ProcessEnv): string;
|
|
3
4
|
export declare function localStorePath(env?: NodeJS.ProcessEnv): string;
|
|
4
5
|
export declare function identityPath(env?: NodeJS.ProcessEnv): string;
|
|
5
6
|
export declare function configPath(env?: NodeJS.ProcessEnv): string;
|
|
6
7
|
export declare function jsonFallbackStorePath(env?: NodeJS.ProcessEnv): string;
|
|
8
|
+
export declare function installReceiptsPath(env?: NodeJS.ProcessEnv): string;
|
|
7
9
|
export declare function writePrivateJson(filePath: string, value: unknown): void;
|
|
8
10
|
export declare function readJsonFile<T>(filePath: string): T | null;
|
package/dist/lib/paths.js
CHANGED
|
@@ -12,6 +12,17 @@ export function askTheWDataDir(env = process.env) {
|
|
|
12
12
|
}
|
|
13
13
|
return path.join(os.homedir(), ".askthew");
|
|
14
14
|
}
|
|
15
|
+
export function askTheWStateDir(env = process.env) {
|
|
16
|
+
const explicit = env.ASKTHEW_STATE_DIR?.trim();
|
|
17
|
+
if (explicit) {
|
|
18
|
+
return path.resolve(explicit);
|
|
19
|
+
}
|
|
20
|
+
const xdgStateHome = env.XDG_STATE_HOME?.trim();
|
|
21
|
+
if (xdgStateHome) {
|
|
22
|
+
return path.join(path.resolve(xdgStateHome), "askthew");
|
|
23
|
+
}
|
|
24
|
+
return path.join(os.homedir(), ".local", "state", "askthew");
|
|
25
|
+
}
|
|
15
26
|
export function ensureAskTheWDataDir(env = process.env) {
|
|
16
27
|
const dir = askTheWDataDir(env);
|
|
17
28
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -29,6 +40,9 @@ export function configPath(env = process.env) {
|
|
|
29
40
|
export function jsonFallbackStorePath(env = process.env) {
|
|
30
41
|
return path.join(askTheWDataDir(env), "store.json");
|
|
31
42
|
}
|
|
43
|
+
export function installReceiptsPath(env = process.env) {
|
|
44
|
+
return path.join(askTheWStateDir(env), "install-receipts.json");
|
|
45
|
+
}
|
|
32
46
|
export function writePrivateJson(filePath, value) {
|
|
33
47
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
34
48
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
@@ -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) {
|