@askthew/mcp-plugin 0.2.8 → 0.4.2

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.
Files changed (60) hide show
  1. package/README.md +65 -16
  2. package/dist/auth-pending.test.d.ts +1 -0
  3. package/dist/auth-pending.test.js +56 -0
  4. package/dist/cli-actions.test.d.ts +1 -0
  5. package/dist/cli-actions.test.js +71 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.js +412 -18
  8. package/dist/cli.test.d.ts +1 -0
  9. package/dist/cli.test.js +274 -0
  10. package/dist/free-tier-policy.test.d.ts +1 -0
  11. package/dist/free-tier-policy.test.js +57 -0
  12. package/dist/index.d.ts +59 -13
  13. package/dist/index.js +1736 -103
  14. package/dist/index.test.d.ts +1 -0
  15. package/dist/index.test.js +952 -0
  16. package/dist/install.d.ts +56 -1
  17. package/dist/install.js +171 -26
  18. package/dist/install.test.d.ts +1 -0
  19. package/dist/install.test.js +297 -0
  20. package/dist/lib/auth-magic-link.d.ts +22 -0
  21. package/dist/lib/auth-magic-link.js +43 -0
  22. package/dist/lib/auth-pending.d.ts +23 -0
  23. package/dist/lib/auth-pending.js +36 -0
  24. package/dist/lib/cli-actions.d.ts +28 -0
  25. package/dist/lib/cli-actions.js +104 -0
  26. package/dist/lib/free-install-registration.d.ts +27 -0
  27. package/dist/lib/free-install-registration.js +52 -0
  28. package/dist/lib/free-tier-policy.d.ts +23 -0
  29. package/dist/lib/free-tier-policy.js +68 -0
  30. package/dist/lib/local-identity.d.ts +44 -0
  31. package/dist/lib/local-identity.js +81 -0
  32. package/dist/lib/local-store.d.ts +130 -0
  33. package/dist/lib/local-store.js +595 -0
  34. package/dist/lib/loopback-auth.d.ts +8 -0
  35. package/dist/lib/loopback-auth.js +30 -0
  36. package/dist/lib/paths.d.ts +9 -0
  37. package/dist/lib/paths.js +50 -0
  38. package/dist/lib/telemetry.d.ts +25 -0
  39. package/dist/lib/telemetry.js +159 -0
  40. package/dist/lib/timeline-insights.d.ts +23 -0
  41. package/dist/lib/timeline-insights.js +115 -0
  42. package/dist/lib/tip-engine.d.ts +18 -0
  43. package/dist/lib/tip-engine.js +237 -0
  44. package/dist/lib/upgrade-nudge.d.ts +19 -0
  45. package/dist/lib/upgrade-nudge.js +37 -0
  46. package/dist/lib/upgrade-sync.d.ts +38 -0
  47. package/dist/lib/upgrade-sync.js +60 -0
  48. package/dist/local-identity.test.d.ts +1 -0
  49. package/dist/local-identity.test.js +29 -0
  50. package/dist/local-store.test.d.ts +1 -0
  51. package/dist/local-store.test.js +71 -0
  52. package/dist/scope.d.ts +1 -2
  53. package/dist/scope.js +56 -8
  54. package/dist/scope.test.d.ts +1 -0
  55. package/dist/scope.test.js +49 -0
  56. package/dist/timeline-insights.test.d.ts +1 -0
  57. package/dist/timeline-insights.test.js +85 -0
  58. package/dist/tip-engine.test.d.ts +1 -0
  59. package/dist/tip-engine.test.js +51 -0
  60. package/package.json +7 -10
package/dist/install.d.ts CHANGED
@@ -1,17 +1,27 @@
1
1
  export type SupportedHostType = "claude_code" | "codex" | "cursor";
2
2
  interface HostConfigInput {
3
3
  hostType: SupportedHostType;
4
- token: string;
4
+ token?: string;
5
5
  apiUrl: string;
6
6
  serverName: string;
7
7
  clientId?: string;
8
8
  clientLabel?: string;
9
+ free?: boolean;
10
+ email?: string;
11
+ cwd?: string;
9
12
  }
10
13
  interface InstallHostConfigInput extends HostConfigInput {
11
14
  dryRun?: boolean;
12
15
  homeDirectory?: string;
13
16
  cwd?: string;
14
17
  }
18
+ interface UninstallHostConfigInput {
19
+ hostType: SupportedHostType;
20
+ serverName?: string;
21
+ dryRun?: boolean;
22
+ homeDirectory?: string;
23
+ cwd?: string;
24
+ }
15
25
  export declare function resolveSettingsPath(input: {
16
26
  hostType: SupportedHostType;
17
27
  homeDirectory?: string;
@@ -20,8 +30,23 @@ export declare function createServerEntry(input: HostConfigInput): {
20
30
  command: string;
21
31
  args: string[];
22
32
  env: {
33
+ ASKTHEW_SERVICE_NAME?: string | undefined;
34
+ ASKTHEW_APP_PATH?: string | undefined;
35
+ ASKTHEW_REPO_ROOT?: string | undefined;
23
36
  ASKTHEW_HOST_TYPE: SupportedHostType;
24
37
  ASKTHEW_SERVER_NAME: string;
38
+ ASKTHEW_REPO_NAME: string;
39
+ ASKTHEW_CLIENT_LABEL?: string | undefined;
40
+ ASKTHEW_CLIENT_ID?: string | undefined;
41
+ ASKTHEW_FREE_MODE: string;
42
+ ASKTHEW_API_URL: string;
43
+ } | {
44
+ ASKTHEW_SERVICE_NAME?: string | undefined;
45
+ ASKTHEW_APP_PATH?: string | undefined;
46
+ ASKTHEW_REPO_ROOT?: string | undefined;
47
+ ASKTHEW_HOST_TYPE: SupportedHostType;
48
+ ASKTHEW_SERVER_NAME: string;
49
+ ASKTHEW_REPO_NAME: string;
25
50
  ASKTHEW_CLIENT_LABEL?: string | undefined;
26
51
  ASKTHEW_CLIENT_ID?: string | undefined;
27
52
  ASKTHEW_INSTALL_TOKEN: string;
@@ -40,8 +65,23 @@ export declare function createHostConfigSnippet(input: HostConfigInput): {
40
65
  command: string;
41
66
  args: string[];
42
67
  env: {
68
+ ASKTHEW_SERVICE_NAME?: string | undefined;
69
+ ASKTHEW_APP_PATH?: string | undefined;
70
+ ASKTHEW_REPO_ROOT?: string | undefined;
71
+ ASKTHEW_HOST_TYPE: SupportedHostType;
72
+ ASKTHEW_SERVER_NAME: string;
73
+ ASKTHEW_REPO_NAME: string;
74
+ ASKTHEW_CLIENT_LABEL?: string | undefined;
75
+ ASKTHEW_CLIENT_ID?: string | undefined;
76
+ ASKTHEW_FREE_MODE: string;
77
+ ASKTHEW_API_URL: string;
78
+ } | {
79
+ ASKTHEW_SERVICE_NAME?: string | undefined;
80
+ ASKTHEW_APP_PATH?: string | undefined;
81
+ ASKTHEW_REPO_ROOT?: string | undefined;
43
82
  ASKTHEW_HOST_TYPE: SupportedHostType;
44
83
  ASKTHEW_SERVER_NAME: string;
84
+ ASKTHEW_REPO_NAME: string;
45
85
  ASKTHEW_CLIENT_LABEL?: string | undefined;
46
86
  ASKTHEW_CLIENT_ID?: string | undefined;
47
87
  ASKTHEW_INSTALL_TOKEN: string;
@@ -67,6 +107,12 @@ export declare function installHostConfig(input: InstallHostConfigInput): {
67
107
  wroteFile: boolean;
68
108
  nextStep: string;
69
109
  };
110
+ export declare function uninstallHostConfig(input: UninstallHostConfigInput): {
111
+ settingsPath: string;
112
+ json: string;
113
+ removedServerName: string;
114
+ wroteFile: boolean;
115
+ };
70
116
  export declare function sendInstallHeartbeat(input: HostConfigInput & {
71
117
  cwd?: string;
72
118
  fetchImpl?: typeof fetch;
@@ -77,7 +123,16 @@ export declare function installBehaviorInstructions(input: {
77
123
  dryRun?: boolean;
78
124
  }): {
79
125
  path: string;
126
+ paths: string[];
80
127
  wroteFile: boolean;
81
128
  content: string;
82
129
  };
130
+ export declare function uninstallBehaviorInstructions(input: {
131
+ hostType: SupportedHostType;
132
+ cwd?: string;
133
+ dryRun?: boolean;
134
+ }): {
135
+ paths: string[];
136
+ wroteFile: boolean;
137
+ };
83
138
  export {};
package/dist/install.js CHANGED
@@ -18,16 +18,21 @@ export function resolveSettingsPath(input) {
18
18
  return path.join(homeDirectory, ".claude.json");
19
19
  }
20
20
  export function createServerEntry(input) {
21
+ const scope = resolvePluginScope(input.cwd ?? process.cwd());
21
22
  return {
22
23
  command: "npx",
23
24
  args: ["-y", "--prefer-online", "@askthew/mcp-plugin@latest"],
24
25
  env: {
25
- ASKTHEW_INSTALL_TOKEN: input.token,
26
26
  ASKTHEW_API_URL: input.apiUrl,
27
+ ...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
27
28
  ...(input.clientId ? { ASKTHEW_CLIENT_ID: input.clientId } : {}),
28
29
  ...(input.clientLabel ? { ASKTHEW_CLIENT_LABEL: input.clientLabel } : {}),
29
30
  ASKTHEW_HOST_TYPE: input.hostType,
30
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 } : {}),
31
36
  },
32
37
  };
33
38
  }
@@ -124,7 +129,7 @@ export function mergeHostSettings(input) {
124
129
  };
125
130
  }
126
131
  export function formatInstallCommand(input) {
127
- return [
132
+ const parts = [
128
133
  "npx",
129
134
  "-y",
130
135
  "--prefer-online",
@@ -132,17 +137,25 @@ export function formatInstallCommand(input) {
132
137
  "install",
133
138
  "--host",
134
139
  input.hostType,
135
- "--token",
136
- JSON.stringify(input.token),
137
140
  "--api-url",
138
141
  JSON.stringify(input.apiUrl),
139
142
  "--server-name",
140
143
  JSON.stringify(input.serverName),
141
- ].join(" ");
144
+ ];
145
+ if (input.free) {
146
+ parts.push("--free");
147
+ if (input.email) {
148
+ parts.push("--email", JSON.stringify(input.email));
149
+ }
150
+ }
151
+ else {
152
+ parts.splice(7, 0, "--token", JSON.stringify(input.token ?? ""));
153
+ }
154
+ return parts.join(" ");
142
155
  }
143
156
  export function verificationNextStep(hostType) {
144
157
  const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "cursor" ? "Cursor" : "Codex";
145
- return `Refresh Ask The W to confirm the plugin shows Installed. Restart or reload ${hostLabel} if it is already open. On the next Ask The W setup check, choose Always allow if ${hostLabel} asks for tool permission. The installed project instructions tell the coding agent when to send updates automatically. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
158
+ return `Refresh Ask The W to confirm the plugin shows Installed. Restart or reload ${hostLabel} if it is already open. At the start of every new ${hostLabel} session in this repo, the installed project instructions tell the coding agent to send a setup_complete update before plan mode or exploration. Choose Always allow if ${hostLabel} asks for Ask The W tool permission. list_mcp_resources/list_mcp_resource_templates may be empty for this tool-driven connector and are not failure signals.`;
146
159
  }
147
160
  export function installHostConfig(input) {
148
161
  const settingsPath = resolveSettingsPath({
@@ -174,6 +187,8 @@ export function installHostConfig(input) {
174
187
  serverName: input.serverName,
175
188
  clientId: input.clientId,
176
189
  clientLabel: input.clientLabel,
190
+ free: input.free,
191
+ cwd: input.cwd,
177
192
  };
178
193
  const json = input.hostType === "codex"
179
194
  ? mergeCodexSettings({
@@ -197,6 +212,65 @@ export function installHostConfig(input) {
197
212
  nextStep: verificationNextStep(input.hostType),
198
213
  };
199
214
  }
215
+ export function uninstallHostConfig(input) {
216
+ const settingsPath = resolveSettingsPath({
217
+ hostType: input.hostType,
218
+ homeDirectory: input.homeDirectory,
219
+ });
220
+ const serverName = input.serverName?.trim() || "askthew";
221
+ let json = "";
222
+ if (fs.existsSync(settingsPath)) {
223
+ const raw = fs.readFileSync(settingsPath, "utf8");
224
+ if (input.hostType === "codex") {
225
+ json = removeCodexTomlServer(raw, serverName);
226
+ if (serverName !== "askthew") {
227
+ json = removeCodexTomlServer(json, "askthew");
228
+ }
229
+ json = json ? `${json}\n` : "";
230
+ }
231
+ else {
232
+ const parsed = raw.trim() ? JSON.parse(raw) : {};
233
+ if (input.hostType === "claude_code") {
234
+ const cwd = path.resolve(input.cwd ?? process.cwd());
235
+ const existingProjects = isRecord(parsed.projects) ? parsed.projects : {};
236
+ const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
237
+ const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
238
+ const nextServers = { ...existingMcpServers };
239
+ delete nextServers[serverName];
240
+ if (serverName !== "askthew")
241
+ delete nextServers.askthew;
242
+ json = JSON.stringify({
243
+ ...parsed,
244
+ projects: {
245
+ ...existingProjects,
246
+ [cwd]: {
247
+ ...existingProject,
248
+ mcpServers: nextServers,
249
+ },
250
+ },
251
+ }, null, 2);
252
+ }
253
+ else {
254
+ const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
255
+ const nextServers = { ...existingMcpServers };
256
+ delete nextServers[serverName];
257
+ if (serverName !== "askthew")
258
+ delete nextServers.askthew;
259
+ json = JSON.stringify({ ...parsed, mcpServers: nextServers }, null, 2);
260
+ }
261
+ json = `${json}\n`;
262
+ }
263
+ if (!input.dryRun) {
264
+ fs.writeFileSync(settingsPath, json, "utf8");
265
+ }
266
+ }
267
+ return {
268
+ settingsPath,
269
+ json,
270
+ removedServerName: serverName,
271
+ wroteFile: !input.dryRun,
272
+ };
273
+ }
200
274
  export async function sendInstallHeartbeat(input) {
201
275
  const fetcher = input.fetchImpl ?? fetch;
202
276
  const scope = resolvePluginScope(input.cwd ?? process.cwd());
@@ -220,8 +294,41 @@ export async function sendInstallHeartbeat(input) {
220
294
  });
221
295
  return response.ok;
222
296
  }
223
- function behaviorInstructions(hostType) {
297
+ function detectStackGuidance(cwd) {
298
+ const packagePath = path.join(cwd, "package.json");
299
+ if (!fs.existsSync(packagePath)) {
300
+ return [];
301
+ }
302
+ let manifest = {};
303
+ try {
304
+ manifest = JSON.parse(fs.readFileSync(packagePath, "utf8"));
305
+ }
306
+ catch {
307
+ return [];
308
+ }
309
+ const deps = {
310
+ ...(manifest.dependencies ?? {}),
311
+ ...(manifest.devDependencies ?? {}),
312
+ };
313
+ const names = new Set(Object.keys(deps));
314
+ const guidance = [];
315
+ if (names.has("next")) {
316
+ guidance.push("- Next.js detected: after changing route handlers, server actions, middleware, or cache behavior, capture `verification_result` with the command/result.");
317
+ }
318
+ if (names.has("express") || names.has("@types/express")) {
319
+ guidance.push("- Express detected: after changing middleware, request validation, or response envelopes, capture `verification_result` with the command/result.");
320
+ }
321
+ if (names.has("vite") || names.has("@vitejs/plugin-react")) {
322
+ guidance.push("- Vite detected: after changing client entrypoints, env handling, or build config, capture `verification_result` with the command/result.");
323
+ }
324
+ if (names.has("openai") || names.has("@openai/agents") || names.has("@ai-sdk/openai")) {
325
+ guidance.push("- OpenAI SDK detected: after editing prompts, model calls, streaming/SSE, or schema-validated responses, capture `direction_change` for prompt choices and `verification_result` for checked outputs.");
326
+ }
327
+ return guidance.slice(0, 4);
328
+ }
329
+ function behaviorInstructions(hostType, cwd = process.cwd()) {
224
330
  const hostLabel = hostType === "claude_code" ? "Claude Code" : hostType === "cursor" ? "Cursor" : "Codex";
331
+ const stackGuidance = detectStackGuidance(cwd);
225
332
  return [
226
333
  ASKTHEW_INSTRUCTIONS_START,
227
334
  "",
@@ -229,29 +336,32 @@ function behaviorInstructions(hostType) {
229
336
  "",
230
337
  `This workspace uses the Ask The W Plugin. When working in this repo with ${hostLabel}, send compact Ask The W updates automatically with the MCP tool \`capture_session_signal\`.`,
231
338
  "",
339
+ "Startup update:",
340
+ `- At the start of every new ${hostLabel} session in this repo, before plan mode, exploration, or any normal reply, call \`capture_session_signal\` with \`kind: "setup_complete"\` if the tool is available.`,
341
+ `- 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.`,
342
+ "- If you realize later in the conversation that the startup call was missed, send it immediately with `metadata.recovered_missed_startup=true`.",
343
+ "",
232
344
  "Send an update:",
233
345
  "- after the user accepts or rejects product, architecture, or implementation direction",
234
- "- after meaningful implementation changes",
346
+ "- before using tools that write files, after meaningful implementation changes",
235
347
  "- after tests, builds, lint, or other verification",
236
- "- every 8-12 turns in long sessions",
237
348
  "- at the final summary",
349
+ ...(stackGuidance.length > 0 ? ["", "Stack-specific nudges:", ...stackGuidance] : []),
238
350
  "",
239
351
  "Keep updates compact: short summary, minimal evidence excerpts, files touched, commands run, and useful metadata. Do not send full transcripts. Redact obvious secrets before sending.",
240
352
  "",
241
- "If this is the first session after install or reload, immediately send a `setup_complete` update with `capture_session_signal` before continuing. If the host asks for Ask The W tool permission, the user should choose Always allow if available.",
242
- "",
243
353
  ASKTHEW_INSTRUCTIONS_END,
244
354
  "",
245
355
  ].join("\n");
246
356
  }
247
- function cursorBehaviorInstructions() {
357
+ function cursorBehaviorInstructions(cwd = process.cwd()) {
248
358
  return [
249
359
  "---",
250
360
  "description: Ask The W Plugin capture rules",
251
361
  "alwaysApply: true",
252
362
  "---",
253
363
  "",
254
- behaviorInstructions("cursor"),
364
+ behaviorInstructions("cursor", cwd),
255
365
  ].join("\n");
256
366
  }
257
367
  function upsertMarkedBlock(existing, block) {
@@ -265,21 +375,56 @@ function upsertMarkedBlock(existing, block) {
265
375
  }
266
376
  export function installBehaviorInstructions(input) {
267
377
  const cwd = path.resolve(input.cwd ?? process.cwd());
268
- const instructionsPath = input.hostType === "claude_code"
269
- ? path.join(cwd, "CLAUDE.md")
270
- : input.hostType === "cursor"
271
- ? path.join(cwd, ".cursor", "rules", "askthew.mdc")
272
- : path.join(cwd, "AGENTS.md");
273
- const block = input.hostType === "cursor" ? cursorBehaviorInstructions() : behaviorInstructions(input.hostType);
274
- const existing = fs.existsSync(instructionsPath) ? fs.readFileSync(instructionsPath, "utf8") : "";
275
- const next = input.hostType === "cursor" ? block : upsertMarkedBlock(existing, block);
276
- if (!input.dryRun) {
277
- fs.mkdirSync(path.dirname(instructionsPath), { recursive: true });
278
- fs.writeFileSync(instructionsPath, next, "utf8");
378
+ const markdownBlock = behaviorInstructions(input.hostType, cwd);
379
+ const markdownTargets = [path.join(cwd, "CLAUDE.md"), path.join(cwd, "AGENTS.md")];
380
+ const writtenPaths = [];
381
+ let primaryPath = input.hostType === "claude_code" ? markdownTargets[0] : markdownTargets[1];
382
+ for (const instructionsPath of markdownTargets) {
383
+ const existing = fs.existsSync(instructionsPath) ? fs.readFileSync(instructionsPath, "utf8") : "";
384
+ const next = upsertMarkedBlock(existing, markdownBlock);
385
+ if (!input.dryRun) {
386
+ fs.mkdirSync(path.dirname(instructionsPath), { recursive: true });
387
+ fs.writeFileSync(instructionsPath, next, "utf8");
388
+ }
389
+ writtenPaths.push(instructionsPath);
390
+ }
391
+ if (input.hostType === "cursor") {
392
+ const cursorPath = path.join(cwd, ".cursor", "rules", "askthew.mdc");
393
+ if (!input.dryRun) {
394
+ fs.mkdirSync(path.dirname(cursorPath), { recursive: true });
395
+ fs.writeFileSync(cursorPath, cursorBehaviorInstructions(cwd), "utf8");
396
+ }
397
+ writtenPaths.push(cursorPath);
398
+ primaryPath = cursorPath;
399
+ }
400
+ return {
401
+ path: primaryPath,
402
+ paths: writtenPaths,
403
+ wroteFile: !input.dryRun,
404
+ content: markdownBlock,
405
+ };
406
+ }
407
+ export function uninstallBehaviorInstructions(input) {
408
+ const cwd = path.resolve(input.cwd ?? process.cwd());
409
+ const markdownTargets = [path.join(cwd, "CLAUDE.md"), path.join(cwd, "AGENTS.md")];
410
+ const touchedPaths = [];
411
+ for (const instructionsPath of markdownTargets) {
412
+ if (!fs.existsSync(instructionsPath))
413
+ continue;
414
+ const existing = fs.readFileSync(instructionsPath, "utf8");
415
+ const next = existing.replace(new RegExp(`\\n?${ASKTHEW_INSTRUCTIONS_START}[\\s\\S]*?${ASKTHEW_INSTRUCTIONS_END}\\n?`, "g"), "\n").trimEnd() + "\n";
416
+ if (!input.dryRun)
417
+ fs.writeFileSync(instructionsPath, next, "utf8");
418
+ touchedPaths.push(instructionsPath);
419
+ }
420
+ const cursorPath = path.join(cwd, ".cursor", "rules", "askthew.mdc");
421
+ if (input.hostType === "cursor" && fs.existsSync(cursorPath)) {
422
+ if (!input.dryRun)
423
+ fs.rmSync(cursorPath, { force: true });
424
+ touchedPaths.push(cursorPath);
279
425
  }
280
426
  return {
281
- path: instructionsPath,
427
+ paths: touchedPaths,
282
428
  wroteFile: !input.dryRun,
283
- content: next,
284
429
  };
285
430
  }
@@ -0,0 +1 @@
1
+ export {};