@askthew/mcp-plugin 0.4.12 → 0.4.14

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
@@ -61,18 +61,24 @@ The v1 MCP tools are:
61
61
 
62
62
  ## CLI
63
63
 
64
+ If the binary is not on your PATH, prefix commands with `npx -y --package @askthew/mcp-plugin`.
65
+
64
66
  ```bash
65
67
  askthew-mcp # stdio MCP server
66
68
  askthew-mcp install --host claude_code
67
69
  askthew-mcp install --host claude_code --bind <paid_bind_token>
70
+ askthew-mcp doctor
71
+ askthew-mcp identity status
68
72
  askthew-mcp bind
69
73
  askthew-mcp token rotate
70
74
  askthew-mcp token revoke
71
75
  askthew-mcp export
72
76
  askthew-mcp delete-me --confirm
77
+ askthew-mcp uninstall --host claude_code
73
78
  ```
74
79
 
75
80
  `token revoke` revokes only the current token. Install-level revocation is handled by the workspace web endpoint or `delete-me`.
81
+ `uninstall` removes the MCP config and marked agent-instructions block, but leaves the local identity and outbox on disk so you can reinstall or delete cloud data intentionally.
76
82
 
77
83
  ## Development
78
84
 
package/dist/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
2
3
  import { spawn } from "node:child_process";
3
4
  import prompts from "prompts";
4
5
  import { AskTheWCloudClient, completeSignup, loadCloudToken, saveCloudToken, startSignup } from "./cloud-client.js";
6
+ import { cloudTokenPath, installMetadataPath, localStorePath } from "./lib/paths.js";
5
7
  import { Outbox } from "./outbox.js";
6
- import { DEFAULT_SERVER_NAME, dataDirSummary, installHostConfig, packageVersion, writeBehaviorInstructions, writeInstallMetadata, } from "./install.js";
8
+ import { DEFAULT_SERVER_NAME, dataDirSummary, installHostConfig, packageVersion, removeBehaviorInstructions, resolveSettingsPath, uninstallHostConfig, writeBehaviorInstructions, writeInstallMetadata, } from "./install.js";
7
9
  import { runStdioServer } from "./index.js";
8
10
  function usage() {
9
11
  return [
@@ -12,7 +14,10 @@ function usage() {
12
14
  "Usage:",
13
15
  " askthew-mcp",
14
16
  " askthew-mcp install --host <claude_code|codex|cursor> [--bind <paid-bind-token>] [--skip-auth]",
17
+ " askthew-mcp uninstall --host <claude_code|codex|cursor> [--dry-run]",
15
18
  " askthew-mcp bind [--allow-pending]",
19
+ " askthew-mcp identity status [--json]",
20
+ " askthew-mcp doctor [--host <claude_code|codex|cursor>] [--json]",
16
21
  " askthew-mcp token rotate|revoke",
17
22
  " askthew-mcp export",
18
23
  " askthew-mcp delete-me --confirm",
@@ -170,6 +175,22 @@ async function installCommand(argv) {
170
175
  process.exitCode = 1;
171
176
  }
172
177
  }
178
+ async function uninstallCommand(argv) {
179
+ const args = parseArgs(argv);
180
+ const hostType = requireHost(stringArg(args, "--host"));
181
+ const dryRun = boolArg(args, "--dry-run");
182
+ const config = uninstallHostConfig({
183
+ hostType,
184
+ serverName: DEFAULT_SERVER_NAME,
185
+ dryRun,
186
+ });
187
+ const instructions = removeBehaviorInstructions({ hostType, dryRun });
188
+ console.log(`${dryRun ? "Would uninstall" : "Uninstalled"} Ask The W for ${hostType}.`);
189
+ console.log(`${config.removed ? "Removed" : "No matching"} MCP config: ${config.settingsPath}`);
190
+ console.log(`${instructions.removed ? "Removed" : "No matching"} agent instructions: ${instructions.filePath}`);
191
+ console.log(`Local identity and outbox were left in ${dataDirSummary()}.`);
192
+ console.log("Run `askthew-mcp delete-me --confirm` before uninstall if you also want to delete install-scoped cloud data.");
193
+ }
173
194
  function openBrowser(url) {
174
195
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
175
196
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -223,9 +244,121 @@ async function bindCommand(argv) {
223
244
  }
224
245
  throw new Error("Timed out waiting for workspace binding.");
225
246
  }
247
+ function pendingOutboxCount() {
248
+ if (!fs.existsSync(localStorePath()))
249
+ return 0;
250
+ try {
251
+ return new Outbox().pendingCount();
252
+ }
253
+ catch {
254
+ return null;
255
+ }
256
+ }
257
+ function printObject(value, asJson) {
258
+ if (asJson) {
259
+ console.log(JSON.stringify(value, null, 2));
260
+ return;
261
+ }
262
+ for (const [key, entry] of Object.entries(value)) {
263
+ if (entry === undefined)
264
+ continue;
265
+ if (typeof entry === "object" && entry !== null) {
266
+ console.log(`${key}: ${JSON.stringify(entry)}`);
267
+ }
268
+ else {
269
+ console.log(`${key}: ${entry}`);
270
+ }
271
+ }
272
+ }
273
+ async function identityStatusCommand(argv) {
274
+ const args = parseArgs(argv);
275
+ const tokenFile = loadCloudToken();
276
+ const client = new AskTheWCloudClient({ apiUrl: apiUrl(args) });
277
+ const status = {
278
+ ok: true,
279
+ configured: Boolean(tokenFile?.token),
280
+ data_dir: dataDirSummary(),
281
+ token_path: cloudTokenPath(),
282
+ install_metadata_path: installMetadataPath(),
283
+ outbox_pending: pendingOutboxCount(),
284
+ api_url: stringArg(args, "--api-url") || tokenFile?.api_url || "https://app.askthew.com",
285
+ };
286
+ if (!tokenFile?.token) {
287
+ status.message = "No local Ask The W identity is configured. Run `askthew-mcp install --host <host>`.";
288
+ printObject(status, boolArg(args, "--json"));
289
+ return;
290
+ }
291
+ status.install_id = tokenFile.install_id;
292
+ status.tier = tokenFile.tier ?? "free";
293
+ status.token_purpose = tokenFile.token_purpose ?? "device";
294
+ const cloud = await client.request("/me", { method: "GET" });
295
+ status.cloud_ok = cloud.ok;
296
+ status.cloud_status = cloud.status;
297
+ status.cloud = cloud.body;
298
+ printObject(status, boolArg(args, "--json"));
299
+ }
300
+ async function probeHealth(baseUrl) {
301
+ try {
302
+ const response = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/v1/agent/health`, {
303
+ method: "GET",
304
+ headers: { Accept: "application/json" },
305
+ });
306
+ const body = await response.json().catch(() => ({
307
+ ok: false,
308
+ code: "invalid_health_response",
309
+ message: "Health endpoint did not return JSON.",
310
+ }));
311
+ return { ok: response.ok && body?.ok !== false, status: response.status, body };
312
+ }
313
+ catch (error) {
314
+ return {
315
+ ok: false,
316
+ status: 0,
317
+ body: {
318
+ ok: false,
319
+ code: "network_error",
320
+ message: error instanceof Error ? error.message : "Could not reach Ask The W.",
321
+ },
322
+ };
323
+ }
324
+ }
325
+ async function doctorCommand(argv) {
326
+ const args = parseArgs(argv);
327
+ const host = stringArg(args, "--host");
328
+ const hostType = host ? requireHost(host) : null;
329
+ const baseUrl = apiUrl(args);
330
+ const tokenFile = loadCloudToken();
331
+ const health = await probeHealth(baseUrl);
332
+ const pendingCount = pendingOutboxCount();
333
+ const checks = {
334
+ cli_version: packageVersion(),
335
+ api_url: baseUrl,
336
+ api_health: health,
337
+ data_dir: dataDirSummary(),
338
+ token_file_exists: fs.existsSync(cloudTokenPath()),
339
+ install_metadata_exists: fs.existsSync(installMetadataPath()),
340
+ outbox_exists: fs.existsSync(localStorePath()),
341
+ outbox_pending: pendingCount,
342
+ identity_configured: Boolean(tokenFile?.token),
343
+ };
344
+ if (hostType) {
345
+ checks.host = hostType;
346
+ checks.settings_path = resolveSettingsPath({ hostType });
347
+ }
348
+ if (tokenFile?.token) {
349
+ const cloud = await new AskTheWCloudClient({ apiUrl: baseUrl }).request("/me", { method: "GET" });
350
+ checks.identity_cloud = cloud;
351
+ }
352
+ const ok = Boolean(health.ok);
353
+ printObject({ ok, checks }, boolArg(args, "--json"));
354
+ if (!ok && boolArg(args, "--strict")) {
355
+ process.exitCode = 1;
356
+ }
357
+ }
226
358
  async function tokenCommand(argv) {
227
359
  const [action] = argv;
228
- const client = new AskTheWCloudClient();
360
+ const args = parseArgs(argv.slice(1));
361
+ const client = new AskTheWCloudClient({ apiUrl: apiUrl(args) });
229
362
  if (action === "rotate") {
230
363
  const result = await client.request("/token/rotate", { method: "POST", body: "{}" });
231
364
  const body = result.body;
@@ -248,8 +381,9 @@ async function tokenCommand(argv) {
248
381
  }
249
382
  throw new Error("Usage: askthew-mcp token rotate|revoke");
250
383
  }
251
- async function exportCommand() {
252
- const client = new AskTheWCloudClient();
384
+ async function exportCommand(argv = []) {
385
+ const args = parseArgs(argv);
386
+ const client = new AskTheWCloudClient({ apiUrl: apiUrl(args) });
253
387
  const result = await client.request("/export", { method: "GET" });
254
388
  if (!result.ok) {
255
389
  const body = result.body;
@@ -266,7 +400,7 @@ async function deleteMeCommand(argv) {
266
400
  if (tokenFile?.tier === "paid") {
267
401
  console.error("This install is workspace-bound. Install-scoped data will be deleted; workspace data is retained.");
268
402
  }
269
- const client = new AskTheWCloudClient();
403
+ const client = new AskTheWCloudClient({ apiUrl: apiUrl(args) });
270
404
  const result = await client.request("/me", { method: "DELETE" });
271
405
  const body = result.body;
272
406
  if (!result.ok || body.ok === false)
@@ -291,16 +425,31 @@ async function main() {
291
425
  await installCommand(argv);
292
426
  return;
293
427
  }
428
+ if (command === "uninstall") {
429
+ await uninstallCommand(argv);
430
+ return;
431
+ }
294
432
  if (command === "bind") {
295
433
  await bindCommand(argv);
296
434
  return;
297
435
  }
436
+ if (command === "identity") {
437
+ const [action, ...rest] = argv;
438
+ if (action !== "status")
439
+ throw new Error("Usage: askthew-mcp identity status [--json]");
440
+ await identityStatusCommand(rest);
441
+ return;
442
+ }
443
+ if (command === "doctor") {
444
+ await doctorCommand(argv);
445
+ return;
446
+ }
298
447
  if (command === "token") {
299
448
  await tokenCommand(argv);
300
449
  return;
301
450
  }
302
451
  if (command === "export") {
303
- await exportCommand();
452
+ await exportCommand(argv);
304
453
  return;
305
454
  }
306
455
  if (command === "delete-me") {
@@ -13,6 +13,7 @@ export type CloudClientOptions = {
13
13
  fetcher?: Fetcher;
14
14
  env?: NodeJS.ProcessEnv;
15
15
  };
16
+ export declare function trimBaseUrl(value?: string): string;
16
17
  export declare function loadCloudToken(env?: NodeJS.ProcessEnv): CloudTokenFile | null;
17
18
  export declare function saveCloudToken(tokenFile: CloudTokenFile, env?: NodeJS.ProcessEnv): void;
18
19
  export declare class AskTheWCloudClient {
@@ -24,12 +25,12 @@ export declare class AskTheWCloudClient {
24
25
  request(path: string, init?: RequestInit): Promise<{
25
26
  ok: boolean;
26
27
  status: number;
27
- body: any;
28
+ body: Record<string, unknown>;
28
29
  }>;
29
30
  captureSignal(payload: Record<string, unknown>): Promise<{
30
31
  ok: boolean;
31
32
  status: number;
32
- body: any;
33
+ body: Record<string, unknown>;
33
34
  }>;
34
35
  listSignals(input?: {
35
36
  sessionId?: string;
@@ -43,7 +44,7 @@ export declare class AskTheWCloudClient {
43
44
  createDecision(payload: Record<string, unknown>): Promise<{
44
45
  ok: boolean;
45
46
  status: number;
46
- body: any;
47
+ body: Record<string, unknown>;
47
48
  }>;
48
49
  listDecisions(input?: {
49
50
  limit?: number;
@@ -55,12 +56,12 @@ export declare class AskTheWCloudClient {
55
56
  recap(input: Record<string, unknown>): Promise<{
56
57
  ok: boolean;
57
58
  status: number;
58
- body: any;
59
+ body: Record<string, unknown>;
59
60
  }>;
60
61
  coach(input: Record<string, unknown>): Promise<{
61
62
  ok: boolean;
62
63
  status: number;
63
- body: any;
64
+ body: Record<string, unknown>;
64
65
  }>;
65
66
  }
66
67
  export declare function startSignup(input: {
@@ -68,7 +69,11 @@ export declare function startSignup(input: {
68
69
  apiUrl?: string;
69
70
  clientHint?: string;
70
71
  fetcher?: Fetcher;
71
- }): Promise<any>;
72
+ }): Promise<Record<string, unknown> | {
73
+ ok: boolean;
74
+ code: string;
75
+ message: string;
76
+ }>;
72
77
  export declare function completeSignup(input: {
73
78
  email: string;
74
79
  code: string;
@@ -77,4 +82,8 @@ export declare function completeSignup(input: {
77
82
  tokenPurpose?: "conversation" | "device";
78
83
  fetcher?: Fetcher;
79
84
  env?: NodeJS.ProcessEnv;
80
- }): Promise<any>;
85
+ }): Promise<Record<string, unknown> | {
86
+ ok: boolean;
87
+ code: string;
88
+ message: string;
89
+ }>;
@@ -1,7 +1,41 @@
1
1
  import { cloudTokenPath, readJsonFile, writePrivateJson } from "./lib/paths.js";
2
- function trimBaseUrl(value) {
2
+ export function trimBaseUrl(value) {
3
3
  return (value?.trim() || "https://app.askthew.com").replace(/\/+$/, "");
4
4
  }
5
+ function errorMessage(error, fallback) {
6
+ return error instanceof Error && error.message ? error.message : fallback;
7
+ }
8
+ async function readResponseBody(response) {
9
+ const text = await response.text().catch(() => "");
10
+ if (!text.trim()) {
11
+ return {
12
+ ok: false,
13
+ code: response.ok ? "empty_response" : `http_${response.status}`,
14
+ message: `Ask The W returned HTTP ${response.status} with no JSON response body.`,
15
+ };
16
+ }
17
+ try {
18
+ const parsed = JSON.parse(text);
19
+ if (typeof parsed === "object" && parsed !== null) {
20
+ return parsed;
21
+ }
22
+ return { ok: response.ok, value: parsed };
23
+ }
24
+ catch {
25
+ return {
26
+ ok: false,
27
+ code: "invalid_json_response",
28
+ message: `Ask The W returned HTTP ${response.status} with a non-JSON response body.`,
29
+ };
30
+ }
31
+ }
32
+ function networkFailure(error) {
33
+ return {
34
+ ok: false,
35
+ code: "network_error",
36
+ message: `Could not reach Ask The W: ${errorMessage(error, "network request failed")}`,
37
+ };
38
+ }
5
39
  export function loadCloudToken(env = process.env) {
6
40
  return readJsonFile(cloudTokenPath(env));
7
41
  }
@@ -58,16 +92,21 @@ export class AskTheWCloudClient {
58
92
  },
59
93
  };
60
94
  }
61
- const response = await this.fetcher(`${this.apiUrl}/api/v1/agent${path}`, {
62
- ...init,
63
- headers: {
64
- "Content-Type": "application/json",
65
- Authorization: `Bearer ${this.token}`,
66
- ...(init.headers ?? {}),
67
- },
68
- });
69
- const body = await response.json().catch(() => ({}));
70
- return { ok: response.ok, status: response.status, body };
95
+ try {
96
+ const response = await this.fetcher(`${this.apiUrl}/api/v1/agent${path}`, {
97
+ ...init,
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ Authorization: `Bearer ${this.token}`,
101
+ ...(init.headers ?? {}),
102
+ },
103
+ });
104
+ const body = await readResponseBody(response);
105
+ return { ok: response.ok, status: response.status, body };
106
+ }
107
+ catch (error) {
108
+ return { ok: false, status: 0, body: networkFailure(error) };
109
+ }
71
110
  }
72
111
  async captureSignal(payload) {
73
112
  return this.request("/signals", {
@@ -117,27 +156,47 @@ export class AskTheWCloudClient {
117
156
  }
118
157
  export async function startSignup(input) {
119
158
  const base = trimBaseUrl(input.apiUrl);
120
- const response = await (input.fetcher ?? fetch)(`${base}/api/v1/agent/signup`, {
121
- method: "POST",
122
- headers: { "Content-Type": "application/json" },
123
- body: JSON.stringify({ email: input.email, client_hint: input.clientHint }),
124
- });
125
- return response.json();
159
+ try {
160
+ const response = await (input.fetcher ?? fetch)(`${base}/api/v1/agent/signup`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: JSON.stringify({ email: input.email, client_hint: input.clientHint }),
164
+ });
165
+ return readResponseBody(response);
166
+ }
167
+ catch (error) {
168
+ return networkFailure(error);
169
+ }
126
170
  }
127
171
  export async function completeSignup(input) {
128
172
  const base = trimBaseUrl(input.apiUrl);
129
- const response = await (input.fetcher ?? fetch)(`${base}/api/v1/agent/verify`, {
130
- method: "POST",
131
- headers: { "Content-Type": "application/json" },
132
- body: JSON.stringify({
133
- email: input.email,
134
- code: input.code,
135
- client_hint: input.clientHint,
136
- token_purpose: input.tokenPurpose ?? "device",
137
- }),
138
- });
139
- const body = await response.json();
140
- if (response.ok && body?.ok && body.token && body.install_id) {
173
+ let response;
174
+ let body;
175
+ try {
176
+ response = await (input.fetcher ?? fetch)(`${base}/api/v1/agent/verify`, {
177
+ method: "POST",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify({
180
+ email: input.email,
181
+ code: input.code,
182
+ client_hint: input.clientHint,
183
+ token_purpose: input.tokenPurpose ?? "device",
184
+ }),
185
+ });
186
+ body = await readResponseBody(response);
187
+ }
188
+ catch (error) {
189
+ return networkFailure(error);
190
+ }
191
+ if (response.ok &&
192
+ typeof body === "object" &&
193
+ body !== null &&
194
+ "ok" in body &&
195
+ body.ok &&
196
+ "token" in body &&
197
+ typeof body.token === "string" &&
198
+ "install_id" in body &&
199
+ typeof body.install_id === "string") {
141
200
  saveCloudToken({
142
201
  token: body.token,
143
202
  install_id: body.install_id,
package/dist/install.d.ts CHANGED
@@ -82,6 +82,11 @@ export declare function writeBehaviorInstructions(input: InstructionInstallInput
82
82
  text: string;
83
83
  wroteFile: boolean;
84
84
  };
85
+ export declare function removeBehaviorInstructions(input: InstructionInstallInput): {
86
+ filePath: string;
87
+ removed: boolean;
88
+ wroteFile: boolean;
89
+ };
85
90
  export declare function writeInstallMetadata(input: {
86
91
  hostType: SupportedHostType;
87
92
  apiUrl?: string;
package/dist/install.js CHANGED
@@ -201,8 +201,9 @@ export function uninstallHostConfig(input) {
201
201
  let next = raw;
202
202
  let removed = false;
203
203
  if (input.hostType === "codex") {
204
- next = `${removeCodexTomlServer(raw, serverName)}\n`;
205
- removed = next !== raw;
204
+ const stripped = removeCodexTomlServer(raw, serverName);
205
+ next = `${stripped}${stripped ? "\n" : ""}`;
206
+ removed = stripped.trimEnd() !== raw.trimEnd();
206
207
  }
207
208
  else {
208
209
  const parsed = raw.trim() ? JSON.parse(raw) : {};
@@ -224,9 +225,9 @@ export function uninstallHostConfig(input) {
224
225
  next = `${JSON.stringify({ ...parsed, mcpServers: servers }, null, 2)}\n`;
225
226
  }
226
227
  }
227
- if (!input.dryRun)
228
+ if (removed && !input.dryRun)
228
229
  fs.writeFileSync(settingsPath, next, "utf8");
229
- return { settingsPath, removed, wroteFile: !input.dryRun };
230
+ return { settingsPath, removed, wroteFile: removed && !input.dryRun };
230
231
  }
231
232
  export function instructionFileForHost(hostType, cwd = process.cwd()) {
232
233
  return path.join(path.resolve(cwd), hostType === "claude_code" ? "CLAUDE.md" : "AGENTS.md");
@@ -269,6 +270,38 @@ export function writeBehaviorInstructions(input) {
269
270
  wroteFile: !input.dryRun,
270
271
  };
271
272
  }
273
+ function removeMarkedBlock(existing) {
274
+ const start = existing.indexOf(INSTRUCTIONS_START);
275
+ const end = existing.indexOf(INSTRUCTIONS_END);
276
+ if (start < 0 || end < start) {
277
+ return { text: existing, removed: false };
278
+ }
279
+ const after = end + INSTRUCTIONS_END.length;
280
+ const text = `${existing.slice(0, start).trimEnd()}${existing.slice(after).trimStart() ? "\n\n" : ""}${existing
281
+ .slice(after)
282
+ .trimStart()}`.trimEnd();
283
+ return { text: text ? `${text}\n` : "", removed: true };
284
+ }
285
+ export function removeBehaviorInstructions(input) {
286
+ const filePath = instructionFileForHost(input.hostType, input.cwd);
287
+ if (!fs.existsSync(filePath)) {
288
+ return {
289
+ filePath,
290
+ removed: false,
291
+ wroteFile: false,
292
+ };
293
+ }
294
+ const existing = fs.readFileSync(filePath, "utf8");
295
+ const next = removeMarkedBlock(existing);
296
+ if (next.removed && !input.dryRun) {
297
+ fs.writeFileSync(filePath, next.text, "utf8");
298
+ }
299
+ return {
300
+ filePath,
301
+ removed: next.removed,
302
+ wroteFile: next.removed && !input.dryRun,
303
+ };
304
+ }
272
305
  export function writeInstallMetadata(input) {
273
306
  const metadata = {
274
307
  host: input.hostType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "private": false,
5
5
  "description": "Ask The W cloud-shim MCP plugin for coding-agent decisions and signals.",
6
6
  "type": "module",