@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 +136 -23
- package/dist/index.d.ts +4 -1
- package/dist/index.js +138 -221
- 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/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
|
|
24
|
-
" askthew-mcp
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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() ||
|
|
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({
|
|
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<
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
|
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 (
|
|
1452
|
+
if (mode.mode !== "paid")
|
|
1479
1453
|
return localResponse(paidFeatureNudge("list_decision_candidates"));
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
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 (
|
|
1469
|
+
if (mode.mode !== "paid")
|
|
1509
1470
|
return localResponse(paidFeatureNudge("search_trail"));
|
|
1510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|