@askthew/mcp-plugin 0.4.7 → 0.4.8

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 CHANGED
@@ -43,6 +43,8 @@ npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install
43
43
 
44
44
  Free install is local-first: it generates `~/.askthew/identity.json`, writes MCP config and agent instructions immediately, and never requires an email code before local capture works. The email is an unverified claim used for upgrade onboarding only; data is keyed by the generated install ID, not email alone.
45
45
 
46
+ Installer bookkeeping is separate from local capture data: install receipts live under `~/.local/state/askthew/install-receipts.json` by default, or `$XDG_STATE_HOME/askthew/install-receipts.json` when `XDG_STATE_HOME` is set. This lets `uninstall` remove host config and repo instruction blocks even after `~/.askthew` has been wiped.
47
+
46
48
  Telemetry is aggregate-only and opt-out. Ask The W does not receive code, file contents, file paths, file names, command text, summaries, or decision content in free mode telemetry. Aggregate summaries are signed by the local install identity and stored under the generated install ID. Opt out with `--no-telemetry`, `ASKTHEW_TELEMETRY=off`, or `askthew-mcp telemetry opt-out`.
47
49
 
48
50
  ## Refresh vs Uninstall
package/dist/cli.js CHANGED
@@ -13,16 +13,16 @@ 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, formatInstallCommand, installBehaviorInstructions, installHostConfig, sendInstallHeartbeat, uninstallBehaviorInstructions, uninstallHostConfig, } from "./install.js";
16
+ import { createHostConfigSnippet, findInstallReceipts, formatInstallCommand, installBehaviorInstructions, installHostConfig, removeInstallReceipts, sendInstallHeartbeat, uninstallBehaviorInstructions, uninstallHostConfig, 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
22
  " askthew-mcp",
23
- " askthew-mcp install --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>] [--dry-run] [--no-agent-instructions]",
24
- " askthew-mcp install --host <claude_code|codex|cursor> --free [--email <email>] [--api-url <url>] [--server-name <name>]",
25
- " askthew-mcp refresh --host <claude_code|codex|cursor> [--free] [--token <install-token>] [--api-url <url>] [--server-name <name>] [--dry-run]",
23
+ " askthew-mcp install --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>] [--server-entrypoint <path>] [--dry-run] [--no-agent-instructions]",
24
+ " askthew-mcp install --host <claude_code|codex|cursor> --free [--email <email>] [--api-url <url>] [--server-name <name>] [--server-entrypoint <path>]",
25
+ " askthew-mcp refresh --host <claude_code|codex|cursor> [--free] [--token <install-token>] [--api-url <url>] [--server-name <name>] [--server-entrypoint <path>] [--dry-run]",
26
26
  " askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>] [--dry-run] [--keep-local-data] [--keep-auth] [--keep-agent-instructions]",
27
27
  " askthew-mcp identify --email <email> [--no-telemetry]",
28
28
  " askthew-mcp identity status",
@@ -47,6 +47,7 @@ function parseInstallArgs(argv) {
47
47
  let installAgentInstructions = true;
48
48
  let free = false;
49
49
  let email = process.env.ASKTHEW_EMAIL?.trim() || "";
50
+ let serverEntrypoint = process.env.ASKTHEW_SERVER_ENTRYPOINT?.trim() || "";
50
51
  for (let index = 0; index < argv.length; index += 1) {
51
52
  const argument = argv[index];
52
53
  if (argument === "--dry-run") {
@@ -103,6 +104,11 @@ function parseInstallArgs(argv) {
103
104
  index += 1;
104
105
  continue;
105
106
  }
107
+ if (argument === "--server-entrypoint") {
108
+ serverEntrypoint = next;
109
+ index += 1;
110
+ continue;
111
+ }
106
112
  throw new Error(`Unknown argument: ${argument}`);
107
113
  }
108
114
  if (!hostType) {
@@ -128,6 +134,7 @@ function parseInstallArgs(argv) {
128
134
  installAgentInstructions,
129
135
  free,
130
136
  email: email || undefined,
137
+ serverEntrypoint: serverEntrypoint ? path.resolve(serverEntrypoint) : undefined,
131
138
  };
132
139
  }
133
140
  function normalizeInstallToken(token) {
@@ -204,6 +211,17 @@ async function main() {
204
211
  dryRun: options.dryRun,
205
212
  })
206
213
  : null;
214
+ const receipt = result.wroteFile
215
+ ? writeInstallReceipt({
216
+ hostType: options.hostType,
217
+ serverName: options.serverName,
218
+ settingsPath: result.settingsPath,
219
+ cwd: process.cwd(),
220
+ instructionPaths: instructions?.paths ?? [],
221
+ serverEntrypoint: options.serverEntrypoint,
222
+ packageVersion: packageVersion(),
223
+ })
224
+ : null;
207
225
  const heartbeatSent = result.wroteFile && !options.free
208
226
  ? await sendInstallHeartbeat(options).catch(() => false)
209
227
  : false;
@@ -212,6 +230,9 @@ async function main() {
212
230
  if (instructions) {
213
231
  console.log(`Agent instructions: ${instructions.paths?.join(", ") ?? instructions.path}`);
214
232
  }
233
+ if (receipt) {
234
+ console.log(`Install receipt: ${receipt.hostType}/${receipt.serverName} at ${receipt.cwd}`);
235
+ }
215
236
  console.log(`Install command: ${formatInstallCommand(options)}`);
216
237
  if (result.wroteFile) {
217
238
  if (freeIdentity) {
@@ -344,14 +365,17 @@ async function runUninstallCommand(argv) {
344
365
  if (!hostType) {
345
366
  throw new Error("Usage: askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>]");
346
367
  }
368
+ const receipts = findInstallReceipts({ hostType, serverName });
347
369
  const config = uninstallHostConfig({ hostType, serverName, dryRun });
348
- const instructions = keepAgentInstructions
349
- ? null
350
- : uninstallBehaviorInstructions({ hostType, dryRun });
370
+ const instructionCwds = Array.from(new Set([process.cwd(), ...receipts.map((receipt) => receipt.cwd)]));
371
+ const instructionPaths = keepAgentInstructions
372
+ ? []
373
+ : instructionCwds.flatMap((cwd) => uninstallBehaviorInstructions({ hostType, dryRun, cwd }).paths);
351
374
  const dataDir = askTheWDataDir();
352
375
  const hadLocalData = fs.existsSync(dataDir);
353
376
  const authFile = identityPath();
354
377
  const hadAuth = fs.existsSync(authFile);
378
+ const removedReceipts = dryRun ? receipts.length : removeInstallReceipts({ hostType, serverName });
355
379
  if (!keepLocalData && !dryRun) {
356
380
  fs.rmSync(dataDir, { recursive: true, force: true });
357
381
  }
@@ -366,11 +390,14 @@ async function runUninstallCommand(argv) {
366
390
  : config.foundConfigFile
367
391
  ? `No MCP server "${config.removedServerName}" found in host config.`
368
392
  : "No host config file found.");
369
- if (instructions) {
370
- console.log(instructions.paths.length > 0
371
- ? `Agent instructions ${dryRun ? "would be removed from" : "removed from"}: ${instructions.paths.join(", ")}`
393
+ if (!keepAgentInstructions) {
394
+ console.log(instructionPaths.length > 0
395
+ ? `Agent instructions ${dryRun ? "would be removed from" : "removed from"}: ${Array.from(new Set(instructionPaths)).join(", ")}`
372
396
  : "No Ask The W agent instruction blocks found.");
373
397
  }
398
+ console.log(removedReceipts > 0
399
+ ? `${dryRun ? "Would remove" : "Removed"} ${removedReceipts} install receipt${removedReceipts === 1 ? "" : "s"}.`
400
+ : "No install receipts found.");
374
401
  console.log(keepLocalData
375
402
  ? "Local data kept."
376
403
  : hadLocalData
@@ -417,6 +444,17 @@ async function runRefreshCommand(argv) {
417
444
  dryRun: options.dryRun,
418
445
  })
419
446
  : null;
447
+ const receipt = install.wroteFile
448
+ ? writeInstallReceipt({
449
+ hostType: options.hostType,
450
+ serverName: options.serverName,
451
+ settingsPath: install.settingsPath,
452
+ cwd: process.cwd(),
453
+ instructionPaths: installedInstructions?.paths ?? [],
454
+ serverEntrypoint: options.serverEntrypoint,
455
+ packageVersion: packageVersion(),
456
+ })
457
+ : null;
420
458
  if (options.free && freeIdentity && !options.dryRun) {
421
459
  await tryRegisterFreeInstall({
422
460
  identity: freeIdentity,
@@ -436,6 +474,9 @@ async function runRefreshCommand(argv) {
436
474
  if (installedInstructions) {
437
475
  console.log(`Installed instructions: ${installedInstructions.paths.join(", ") || installedInstructions.path}`);
438
476
  }
477
+ if (receipt) {
478
+ console.log(`Install receipt: ${receipt.hostType}/${receipt.serverName} at ${receipt.cwd}`);
479
+ }
439
480
  if (options.free) {
440
481
  console.log(freeIdentity
441
482
  ? `Local identity preserved: ${freeIdentity.installId}`
package/dist/index.d.ts CHANGED
@@ -158,4 +158,8 @@ export interface AskTheWMcpServerOptions {
158
158
  }
159
159
  export declare function normalizeInstallTokenInput(token: string | undefined): string;
160
160
  export declare function createAskTheWMcpServer(options?: AskTheWMcpServerOptions): McpServer;
161
+ export declare function runInitializeHandshake(input?: {
162
+ entrypoint?: string;
163
+ timeoutMs?: number;
164
+ }): Promise<void>;
161
165
  export {};
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { spawn } from "node:child_process";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
@@ -1914,16 +1915,92 @@ const isDirectIndexExecution = Boolean(process.argv[1]) &&
1914
1915
  return invokedPath === modulePath;
1915
1916
  }
1916
1917
  })();
1917
- if (isDirectIndexExecution) {
1918
- const server = createAskTheWMcpServer();
1919
- const transport = new StdioServerTransport();
1920
- server.connect(transport).catch((error) => {
1921
- if (error instanceof Error) {
1922
- console.error(error.message);
1923
- }
1924
- else {
1925
- console.error("Ask The W MCP server failed to start.", error);
1926
- }
1927
- process.exit(1);
1918
+ export async function runInitializeHandshake(input = {}) {
1919
+ const modulePath = input.entrypoint ?? fileURLToPath(import.meta.url);
1920
+ const child = spawn(process.execPath, [modulePath], {
1921
+ cwd: process.cwd(),
1922
+ env: process.env,
1923
+ stdio: ["pipe", "pipe", "pipe"],
1928
1924
  });
1925
+ let stdout = "";
1926
+ let stderr = "";
1927
+ child.stdout.setEncoding("utf8");
1928
+ child.stderr.setEncoding("utf8");
1929
+ child.stdout.on("data", (chunk) => {
1930
+ stdout += chunk;
1931
+ });
1932
+ child.stderr.on("data", (chunk) => {
1933
+ stderr += chunk;
1934
+ });
1935
+ const initialize = {
1936
+ jsonrpc: "2.0",
1937
+ id: 1,
1938
+ method: "initialize",
1939
+ params: {
1940
+ protocolVersion: "2024-11-05",
1941
+ capabilities: {},
1942
+ clientInfo: { name: "askthew-initialize-handshake", version: "1.0.0" },
1943
+ },
1944
+ };
1945
+ child.stdin.write(`${JSON.stringify(initialize)}\n`);
1946
+ try {
1947
+ await new Promise((resolve, reject) => {
1948
+ const timeout = setTimeout(() => {
1949
+ reject(new Error(`Timed out waiting for initialize response. stdout=${stdout} stderr=${stderr}`));
1950
+ }, input.timeoutMs ?? 3000);
1951
+ const failOnExit = (code) => {
1952
+ clearTimeout(timeout);
1953
+ reject(new Error(`MCP stdio server exited before initialize. code=${code} stdout=${stdout} stderr=${stderr}`));
1954
+ };
1955
+ const check = () => {
1956
+ const lineEnd = stdout.indexOf("\n");
1957
+ if (lineEnd === -1)
1958
+ return;
1959
+ clearTimeout(timeout);
1960
+ child.off("exit", failOnExit);
1961
+ const response = JSON.parse(stdout.slice(0, lineEnd));
1962
+ if (response.id !== 1 || response.jsonrpc !== "2.0" || !response.result) {
1963
+ reject(new Error(`Unexpected initialize response: ${JSON.stringify(response)}`));
1964
+ return;
1965
+ }
1966
+ resolve();
1967
+ };
1968
+ child.stdout.on("data", check);
1969
+ child.once("exit", failOnExit);
1970
+ check();
1971
+ });
1972
+ }
1973
+ finally {
1974
+ child.kill();
1975
+ }
1976
+ }
1977
+ if (isDirectIndexExecution) {
1978
+ if (process.argv[2] === "initialize-handshake") {
1979
+ runInitializeHandshake()
1980
+ .then(() => {
1981
+ console.log("Ask The W MCP initialize handshake succeeded.");
1982
+ })
1983
+ .catch((error) => {
1984
+ if (error instanceof Error) {
1985
+ console.error(error.message);
1986
+ }
1987
+ else {
1988
+ console.error("Ask The W MCP initialize handshake failed.", error);
1989
+ }
1990
+ process.exit(1);
1991
+ });
1992
+ }
1993
+ else {
1994
+ const server = createAskTheWMcpServer();
1995
+ const transport = new StdioServerTransport();
1996
+ server.connect(transport).catch((error) => {
1997
+ if (error instanceof Error) {
1998
+ console.error(error.message);
1999
+ }
2000
+ else {
2001
+ console.error("Ask The W MCP server failed to start.", error);
2002
+ }
2003
+ process.exit(1);
2004
+ });
2005
+ }
1929
2006
  }
package/dist/install.d.ts CHANGED
@@ -9,6 +9,7 @@ interface HostConfigInput {
9
9
  free?: boolean;
10
10
  email?: string;
11
11
  cwd?: string;
12
+ serverEntrypoint?: string;
12
13
  }
13
14
  interface InstallHostConfigInput extends HostConfigInput {
14
15
  dryRun?: boolean;
@@ -22,6 +23,17 @@ interface UninstallHostConfigInput {
22
23
  homeDirectory?: string;
23
24
  cwd?: string;
24
25
  }
26
+ export interface InstallReceipt {
27
+ hostType: SupportedHostType;
28
+ serverName: string;
29
+ settingsPath: string;
30
+ cwd: string;
31
+ instructionPaths: string[];
32
+ dataDir: string;
33
+ serverEntrypoint?: string;
34
+ installedAt: string;
35
+ packageVersion?: string;
36
+ }
25
37
  export declare function resolveSettingsPath(input: {
26
38
  hostType: SupportedHostType;
27
39
  homeDirectory?: string;
@@ -107,6 +119,20 @@ export declare function installHostConfig(input: InstallHostConfigInput): {
107
119
  wroteFile: boolean;
108
120
  nextStep: string;
109
121
  };
122
+ export declare function readInstallReceipts(env?: NodeJS.ProcessEnv): InstallReceipt[];
123
+ export declare function writeInstallReceipt(receipt: Omit<InstallReceipt, "dataDir" | "installedAt"> & {
124
+ dataDir?: string;
125
+ installedAt?: string;
126
+ }, env?: NodeJS.ProcessEnv): InstallReceipt;
127
+ export declare function findInstallReceipts(input: {
128
+ hostType: SupportedHostType;
129
+ serverName?: string;
130
+ }, env?: NodeJS.ProcessEnv): InstallReceipt[];
131
+ export declare function removeInstallReceipts(input: {
132
+ hostType: SupportedHostType;
133
+ serverName?: string;
134
+ cwd?: string;
135
+ }, env?: NodeJS.ProcessEnv): number;
110
136
  export declare function uninstallHostConfig(input: UninstallHostConfigInput): {
111
137
  settingsPath: string;
112
138
  json: string;
package/dist/install.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { askTheWDataDir, installReceiptsPath, readJsonFile, writePrivateJson } from "./lib/paths.js";
4
5
  import { resolvePluginScope } from "./scope.js";
5
6
  const ASKTHEW_INSTRUCTIONS_START = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_START -->";
6
7
  const ASKTHEW_INSTRUCTIONS_END = "<!-- ASKTHEW_PLUGIN_INSTRUCTIONS_END -->";
8
+ const INSTALL_RECEIPTS_SCHEMA_VERSION = 1;
7
9
  function isRecord(value) {
8
10
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
11
  }
@@ -19,21 +21,29 @@ export function resolveSettingsPath(input) {
19
21
  }
20
22
  export function createServerEntry(input) {
21
23
  const scope = resolvePluginScope(input.cwd ?? process.cwd());
24
+ const env = {
25
+ ASKTHEW_API_URL: input.apiUrl,
26
+ ...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
27
+ ...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
28
+ ...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
29
+ ASKTHEW_HOST_TYPE: input.hostType,
30
+ ASKTHEW_SERVER_NAME: input.serverName,
31
+ ASKTHEW_REPO_NAME: scope.repoName,
32
+ ...(scope.repoRoot ? { ASKTHEW_REPO_ROOT: scope.repoRoot } : {}),
33
+ ...(scope.appPath ? { ASKTHEW_APP_PATH: scope.appPath } : {}),
34
+ ...(scope.serviceName ? { ASKTHEW_SERVICE_NAME: scope.serviceName } : {}),
35
+ };
36
+ if (input.serverEntrypoint) {
37
+ return {
38
+ command: "node",
39
+ args: [path.resolve(input.serverEntrypoint)],
40
+ env,
41
+ };
42
+ }
22
43
  return {
23
44
  command: "npx",
24
45
  args: ["-y", "--prefer-online", "--package", "@askthew/mcp-plugin@latest", "askthew-mcp"],
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
- },
46
+ env,
37
47
  };
38
48
  }
39
49
  export function createHostConfigSnippet(input) {
@@ -150,6 +160,9 @@ export function formatInstallCommand(input) {
150
160
  parts.push("--email", JSON.stringify(input.email));
151
161
  }
152
162
  }
163
+ if (input.serverEntrypoint) {
164
+ parts.push("--server-entrypoint", JSON.stringify(input.serverEntrypoint));
165
+ }
153
166
  return parts.join(" ");
154
167
  }
155
168
  export function verificationNextStep(hostType) {
@@ -188,6 +201,7 @@ export function installHostConfig(input) {
188
201
  clientLabel: input.clientLabel,
189
202
  free: input.free,
190
203
  cwd: input.cwd,
204
+ serverEntrypoint: input.serverEntrypoint,
191
205
  };
192
206
  const json = input.hostType === "codex"
193
207
  ? mergeCodexSettings({
@@ -211,6 +225,68 @@ export function installHostConfig(input) {
211
225
  nextStep: verificationNextStep(input.hostType),
212
226
  };
213
227
  }
228
+ function normalizeInstructionPaths(paths) {
229
+ return Array.from(new Set(paths.map((entry) => path.resolve(entry))));
230
+ }
231
+ function normalizeReceipt(receipt) {
232
+ return {
233
+ ...receipt,
234
+ settingsPath: path.resolve(receipt.settingsPath),
235
+ cwd: path.resolve(receipt.cwd),
236
+ instructionPaths: normalizeInstructionPaths(receipt.instructionPaths ?? []),
237
+ dataDir: path.resolve(receipt.dataDir || askTheWDataDir()),
238
+ serverEntrypoint: receipt.serverEntrypoint ? path.resolve(receipt.serverEntrypoint) : undefined,
239
+ };
240
+ }
241
+ function receiptKey(receipt) {
242
+ return `${receipt.hostType}\u0000${receipt.serverName}\u0000${path.resolve(receipt.cwd)}`;
243
+ }
244
+ export function readInstallReceipts(env = process.env) {
245
+ const parsed = readJsonFile(installReceiptsPath(env));
246
+ if (!parsed || parsed.schemaVersion !== INSTALL_RECEIPTS_SCHEMA_VERSION || !Array.isArray(parsed.installs)) {
247
+ return [];
248
+ }
249
+ return parsed.installs
250
+ .filter((receipt) => Boolean(receipt?.hostType && receipt.serverName && receipt.settingsPath && receipt.cwd))
251
+ .map(normalizeReceipt);
252
+ }
253
+ export function writeInstallReceipt(receipt, env = process.env) {
254
+ const nextReceipt = normalizeReceipt({
255
+ ...receipt,
256
+ dataDir: receipt.dataDir ?? askTheWDataDir(env),
257
+ installedAt: receipt.installedAt ?? new Date().toISOString(),
258
+ });
259
+ const receipts = readInstallReceipts(env).filter((entry) => receiptKey(entry) !== receiptKey(nextReceipt));
260
+ receipts.push(nextReceipt);
261
+ receipts.sort((left, right) => left.installedAt.localeCompare(right.installedAt));
262
+ writePrivateJson(installReceiptsPath(env), {
263
+ schemaVersion: INSTALL_RECEIPTS_SCHEMA_VERSION,
264
+ installs: receipts,
265
+ });
266
+ return nextReceipt;
267
+ }
268
+ export function findInstallReceipts(input, env = process.env) {
269
+ return readInstallReceipts(env).filter((receipt) => receipt.hostType === input.hostType &&
270
+ (!input.serverName || receipt.serverName === input.serverName));
271
+ }
272
+ export function removeInstallReceipts(input, env = process.env) {
273
+ const cwd = input.cwd ? path.resolve(input.cwd) : undefined;
274
+ const receipts = readInstallReceipts(env);
275
+ const next = receipts.filter((receipt) => {
276
+ if (receipt.hostType !== input.hostType)
277
+ return true;
278
+ if (input.serverName && receipt.serverName !== input.serverName)
279
+ return true;
280
+ if (cwd && receipt.cwd !== cwd)
281
+ return true;
282
+ return false;
283
+ });
284
+ writePrivateJson(installReceiptsPath(env), {
285
+ schemaVersion: INSTALL_RECEIPTS_SCHEMA_VERSION,
286
+ installs: next,
287
+ });
288
+ return receipts.length - next.length;
289
+ }
214
290
  export function uninstallHostConfig(input) {
215
291
  const settingsPath = resolveSettingsPath({
216
292
  hostType: input.hostType,
@@ -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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "private": false,
5
5
  "description": "Ask The W plugin connector for local-first coding-agent decisions, signals, and review.",
6
6
  "type": "module",