@askthew/mcp-plugin 0.4.4 → 0.4.6
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 +26 -8
- package/dist/cli-actions.test.js +1 -1
- package/dist/cli.js +125 -12
- package/dist/cli.test.js +114 -0
- package/dist/index.js +3 -3
- package/dist/index.test.js +3 -3
- package/dist/install.d.ts +2 -0
- package/dist/install.js +13 -6
- package/dist/install.test.js +19 -2
- package/dist/lib/cli-actions.js +1 -1
- package/dist/lib/upgrade-nudge.js +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Connect a local coding agent to Ask The W. The fastest path is free and local-first:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx -y --prefer-online @askthew/mcp-plugin@latest install --host claude_code --free --email you@founder.com
|
|
6
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install --host claude_code --free --email you@founder.com
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
This captures decisions and session signals to `~/.askthew/store.sqlite` and lets your agent run `review_decisions`, `review_session`, `recap`, `coach`, and `promote_signal_to_decision` without onboarding into the web app.
|
|
@@ -35,7 +35,7 @@ Ask The W performs inference, linking, approval state, dedupe, and outcome updat
|
|
|
35
35
|
## Free Local Install
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx -y --prefer-online @askthew/mcp-plugin@latest install \
|
|
38
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install \
|
|
39
39
|
--host claude_code \
|
|
40
40
|
--free \
|
|
41
41
|
--email you@founder.com
|
|
@@ -45,6 +45,24 @@ Free install is local-first: it generates `~/.askthew/identity.json`, writes MCP
|
|
|
45
45
|
|
|
46
46
|
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
47
|
|
|
48
|
+
## Refresh vs Uninstall
|
|
49
|
+
|
|
50
|
+
Use `refresh` when you want the latest installed MCP config and instruction blocks without touching local identity or local data:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp refresh --host claude_code
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`refresh` preserves `~/.askthew/identity.json` and `~/.askthew/store.sqlite`, reuses any existing email claim silently, rewrites the host MCP config, and rewrites the marked Ask The W blocks in `CLAUDE.md` and `AGENTS.md`. This is the recommended QA update path after a plugin publish.
|
|
57
|
+
|
|
58
|
+
Use `uninstall` when you want to remove the plugin from a host:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx -y @askthew/mcp-plugin@latest uninstall --host claude_code --keep-local-data --keep-auth
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Without `--keep-local-data`, uninstall removes `~/.askthew`; without `--keep-auth`, it removes the local install identity. Uninstall does not clear npm's cache.
|
|
65
|
+
|
|
48
66
|
## Workspace Install
|
|
49
67
|
|
|
50
68
|
Create a workspace token in Ask The W at `/decisions/settings/connectors`, then run the installer from your coding agent or terminal. Treat the token like a password; anyone with it can write compact source signals into that workspace.
|
|
@@ -54,7 +72,7 @@ Use setup tokens promptly. They expire after 24 hours if the connector never sen
|
|
|
54
72
|
Codex:
|
|
55
73
|
|
|
56
74
|
```bash
|
|
57
|
-
npx -y --prefer-online @askthew/mcp-plugin@latest install \
|
|
75
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install \
|
|
58
76
|
--host codex \
|
|
59
77
|
--token "<ASKTHEW_INSTALL_TOKEN>" \
|
|
60
78
|
--api-url "https://app.askthew.com/" \
|
|
@@ -64,7 +82,7 @@ npx -y --prefer-online @askthew/mcp-plugin@latest install \
|
|
|
64
82
|
Claude Code:
|
|
65
83
|
|
|
66
84
|
```bash
|
|
67
|
-
npx -y --prefer-online @askthew/mcp-plugin@latest install \
|
|
85
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install \
|
|
68
86
|
--host claude_code \
|
|
69
87
|
--token "<ASKTHEW_INSTALL_TOKEN>" \
|
|
70
88
|
--api-url "https://app.askthew.com/" \
|
|
@@ -74,7 +92,7 @@ npx -y --prefer-online @askthew/mcp-plugin@latest install \
|
|
|
74
92
|
Cursor:
|
|
75
93
|
|
|
76
94
|
```bash
|
|
77
|
-
npx -y --prefer-online @askthew/mcp-plugin@latest install \
|
|
95
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install \
|
|
78
96
|
--host cursor \
|
|
79
97
|
--token "<ASKTHEW_INSTALL_TOKEN>" \
|
|
80
98
|
--api-url "https://app.askthew.com/" \
|
|
@@ -112,7 +130,7 @@ Codex example:
|
|
|
112
130
|
```toml
|
|
113
131
|
[mcp_servers.askthew]
|
|
114
132
|
command = "npx"
|
|
115
|
-
args = ["-y", "--prefer-online", "@askthew/mcp-plugin@latest"]
|
|
133
|
+
args = ["-y", "--prefer-online", "--package", "@askthew/mcp-plugin@latest", "askthew-mcp"]
|
|
116
134
|
env = { ASKTHEW_INSTALL_TOKEN = "<ASKTHEW_INSTALL_TOKEN>", ASKTHEW_HOST_TYPE = "codex", ASKTHEW_API_URL = "https://app.askthew.com/", ASKTHEW_SERVER_NAME = "askthew" }
|
|
117
135
|
```
|
|
118
136
|
|
|
@@ -176,8 +194,8 @@ The plugin also exposes v1 API tools that map to the app's authenticated routes:
|
|
|
176
194
|
Free PLG helpers:
|
|
177
195
|
|
|
178
196
|
```bash
|
|
179
|
-
npx @askthew/mcp-plugin install-hook --pre-commit
|
|
180
|
-
npx @askthew/mcp-plugin digest --weekly
|
|
197
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install-hook --pre-commit
|
|
198
|
+
npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp digest --weekly
|
|
181
199
|
```
|
|
182
200
|
|
|
183
201
|
The hook prompts when staged files recently had implementation signals but no linked decision. The weekly digest writes `~/Documents/askthew-digest-YYYY-WW.md`.
|
package/dist/cli-actions.test.js
CHANGED
|
@@ -44,7 +44,7 @@ test("pre-commit hook installer writes the documented inline prompt hook", () =>
|
|
|
44
44
|
const hookPath = installPreCommitHook({ cwd: dir });
|
|
45
45
|
const hook = fs.readFileSync(hookPath, "utf8");
|
|
46
46
|
assert.match(hook, /Ask The W pre-commit decision prompt/);
|
|
47
|
-
assert.match(hook,
|
|
47
|
+
assert.match(hook, /--package @askthew\/mcp-plugin@latest askthew-mcp hook-check --pre-commit/);
|
|
48
48
|
assert.equal((fs.statSync(hookPath).mode & 0o111) > 0, true);
|
|
49
49
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
50
50
|
});
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { createAskTheWMcpServer } from "./index.js";
|
|
8
|
-
import { ensureAskTheWDataDir, identityPath } from "./lib/paths.js";
|
|
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";
|
|
11
11
|
import { ensureLocalIdentity, loadLocalIdentity, publicIdentity } from "./lib/local-identity.js";
|
|
@@ -22,6 +22,7 @@ function usage() {
|
|
|
22
22
|
" askthew-mcp",
|
|
23
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
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]",
|
|
25
26
|
" askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>] [--dry-run] [--keep-local-data] [--keep-auth] [--keep-agent-instructions]",
|
|
26
27
|
" askthew-mcp identify --email <email> [--no-telemetry]",
|
|
27
28
|
" askthew-mcp identity status",
|
|
@@ -132,6 +133,16 @@ function parseInstallArgs(argv) {
|
|
|
132
133
|
function normalizeInstallToken(token) {
|
|
133
134
|
return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
134
135
|
}
|
|
136
|
+
function packageVersion() {
|
|
137
|
+
try {
|
|
138
|
+
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
139
|
+
const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
140
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return "unknown";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
135
146
|
function detectLoginEmail() {
|
|
136
147
|
for (const value of [
|
|
137
148
|
process.env.ASKTHEW_EMAIL,
|
|
@@ -229,6 +240,10 @@ async function main() {
|
|
|
229
240
|
}
|
|
230
241
|
return;
|
|
231
242
|
}
|
|
243
|
+
if (command === "refresh") {
|
|
244
|
+
await runRefreshCommand(argv);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
232
247
|
if (command === "identify") {
|
|
233
248
|
await runIdentifyCommand(argv);
|
|
234
249
|
return;
|
|
@@ -270,7 +285,7 @@ async function main() {
|
|
|
270
285
|
return;
|
|
271
286
|
}
|
|
272
287
|
if (command === "upgrade") {
|
|
273
|
-
console.log("Open https://askthew.com/
|
|
288
|
+
console.log("Open https://askthew.com/plugin to upgrade, then run `askthew-mcp upgrade --finalize`.");
|
|
274
289
|
if (argv.includes("--finalize")) {
|
|
275
290
|
console.log("Finalize will rewrite host config after a paid workspace token is available in the web app.");
|
|
276
291
|
}
|
|
@@ -333,26 +348,114 @@ async function runUninstallCommand(argv) {
|
|
|
333
348
|
const instructions = keepAgentInstructions
|
|
334
349
|
? null
|
|
335
350
|
: uninstallBehaviorInstructions({ hostType, dryRun });
|
|
351
|
+
const dataDir = askTheWDataDir();
|
|
352
|
+
const hadLocalData = fs.existsSync(dataDir);
|
|
353
|
+
const authFile = identityPath();
|
|
354
|
+
const hadAuth = fs.existsSync(authFile);
|
|
336
355
|
if (!keepLocalData && !dryRun) {
|
|
337
|
-
fs.rmSync(
|
|
356
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
338
357
|
}
|
|
339
358
|
if (!keepAuth && !dryRun) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
fs.rmSync(file, { force: true });
|
|
359
|
+
if (fs.existsSync(authFile))
|
|
360
|
+
fs.rmSync(authFile, { force: true });
|
|
343
361
|
}
|
|
344
362
|
console.log(dryRun ? "Ask The W plugin uninstall dry run complete." : "Ask The W plugin uninstall complete.");
|
|
345
363
|
console.log(`Settings path: ${config.settingsPath}`);
|
|
364
|
+
console.log(config.removedServer
|
|
365
|
+
? `${dryRun ? "Would remove" : "Removed"} MCP server "${config.removedServerName}".`
|
|
366
|
+
: config.foundConfigFile
|
|
367
|
+
? `No MCP server "${config.removedServerName}" found in host config.`
|
|
368
|
+
: "No host config file found.");
|
|
346
369
|
if (instructions) {
|
|
347
|
-
console.log(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
370
|
+
console.log(instructions.paths.length > 0
|
|
371
|
+
? `Agent instructions ${dryRun ? "would be removed from" : "removed from"}: ${instructions.paths.join(", ")}`
|
|
372
|
+
: "No Ask The W agent instruction blocks found.");
|
|
373
|
+
}
|
|
374
|
+
console.log(keepLocalData
|
|
375
|
+
? "Local data kept."
|
|
376
|
+
: hadLocalData
|
|
377
|
+
? `${dryRun ? "Local data would be removed" : "Local data removed"}.`
|
|
378
|
+
: "No local data directory found.");
|
|
379
|
+
console.log(keepAuth
|
|
380
|
+
? "Auth tokens kept."
|
|
381
|
+
: hadAuth
|
|
382
|
+
? `${dryRun ? "Local identity would be removed" : "Local identity removed"}.`
|
|
383
|
+
: "No local identity found.");
|
|
351
384
|
if (dryRun) {
|
|
352
385
|
console.log("");
|
|
353
386
|
console.log(config.json);
|
|
354
387
|
}
|
|
355
388
|
}
|
|
389
|
+
async function runRefreshCommand(argv) {
|
|
390
|
+
const parsed = parseInstallArgs(["--free", ...argv]);
|
|
391
|
+
const isPaidRefresh = Boolean(parsed.token && !argv.includes("--free"));
|
|
392
|
+
const options = {
|
|
393
|
+
...parsed,
|
|
394
|
+
free: !isPaidRefresh,
|
|
395
|
+
};
|
|
396
|
+
const existingIdentity = loadLocalIdentity();
|
|
397
|
+
let freeIdentity = existingIdentity;
|
|
398
|
+
if (options.free && !options.dryRun && !freeIdentity) {
|
|
399
|
+
freeIdentity = ensureLocalIdentity({
|
|
400
|
+
emailClaim: options.email,
|
|
401
|
+
apiUrl: options.apiUrl,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const uninstall = uninstallHostConfig({
|
|
405
|
+
hostType: options.hostType,
|
|
406
|
+
serverName: options.serverName,
|
|
407
|
+
dryRun: options.dryRun,
|
|
408
|
+
});
|
|
409
|
+
const removedInstructions = uninstallBehaviorInstructions({
|
|
410
|
+
hostType: options.hostType,
|
|
411
|
+
dryRun: options.dryRun,
|
|
412
|
+
});
|
|
413
|
+
const install = installHostConfig(options);
|
|
414
|
+
const installedInstructions = options.installAgentInstructions
|
|
415
|
+
? installBehaviorInstructions({
|
|
416
|
+
hostType: options.hostType,
|
|
417
|
+
dryRun: options.dryRun,
|
|
418
|
+
})
|
|
419
|
+
: null;
|
|
420
|
+
if (options.free && freeIdentity && !options.dryRun) {
|
|
421
|
+
await tryRegisterFreeInstall({
|
|
422
|
+
identity: freeIdentity,
|
|
423
|
+
deviceLabel: options.clientLabel ?? `${options.hostType} free refresh`,
|
|
424
|
+
repo: {
|
|
425
|
+
repoName: process.env.ASKTHEW_REPO_NAME,
|
|
426
|
+
repoRoot: process.env.ASKTHEW_REPO_ROOT,
|
|
427
|
+
hostType: options.hostType,
|
|
428
|
+
},
|
|
429
|
+
options: { apiUrl: options.apiUrl },
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
console.log(options.dryRun ? "Ask The W plugin refresh dry run complete." : "Ask The W plugin refresh complete.");
|
|
433
|
+
console.log(`Plugin package version: ${packageVersion()}`);
|
|
434
|
+
console.log(`Settings path: ${install.settingsPath}`);
|
|
435
|
+
console.log(`Removed instructions: ${removedInstructions.paths.join(", ") || "none"}`);
|
|
436
|
+
if (installedInstructions) {
|
|
437
|
+
console.log(`Installed instructions: ${installedInstructions.paths.join(", ") || installedInstructions.path}`);
|
|
438
|
+
}
|
|
439
|
+
if (options.free) {
|
|
440
|
+
console.log(freeIdentity
|
|
441
|
+
? `Local identity preserved: ${freeIdentity.installId}`
|
|
442
|
+
: "Local identity would be created on a non-dry-run refresh.");
|
|
443
|
+
console.log("Local data preserved: ~/.askthew/store.sqlite");
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
const heartbeatSent = install.wroteFile
|
|
447
|
+
? await sendInstallHeartbeat(options).catch(() => false)
|
|
448
|
+
: false;
|
|
449
|
+
console.log(heartbeatSent ? "Paid workspace heartbeat sent." : "Paid workspace heartbeat not sent yet.");
|
|
450
|
+
}
|
|
451
|
+
console.log(`Next step: ${install.nextStep}`);
|
|
452
|
+
if (options.dryRun) {
|
|
453
|
+
console.log("");
|
|
454
|
+
console.log(uninstall.json);
|
|
455
|
+
console.log("");
|
|
456
|
+
console.log(install.json);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
356
459
|
function argValue(argv, name) {
|
|
357
460
|
const index = argv.indexOf(name);
|
|
358
461
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
@@ -474,7 +577,7 @@ async function runHookCheckCommand(argv) {
|
|
|
474
577
|
const gap = preCommitDecisionGap({ store, stagedFiles: stagedFiles(), scopeKey: localScopeKey() });
|
|
475
578
|
if (gap.missing) {
|
|
476
579
|
console.log('Ask The W: this change has no decision attached, draft one?');
|
|
477
|
-
console.log("Run: npx @askthew/mcp-plugin digest --weekly or ask your agent to call promote_signal_to_decision.");
|
|
580
|
+
console.log("Run: npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp digest --weekly or ask your agent to call promote_signal_to_decision.");
|
|
478
581
|
}
|
|
479
582
|
}
|
|
480
583
|
async function runDigestCommand(argv) {
|
|
@@ -502,7 +605,17 @@ async function runSyncCommand(argv) {
|
|
|
502
605
|
}
|
|
503
606
|
console.log(JSON.stringify(await uploadLocalStore({ store, credentials, syncToken }), null, 2));
|
|
504
607
|
}
|
|
505
|
-
const isDirectCliExecution = Boolean(process.argv[1]) &&
|
|
608
|
+
const isDirectCliExecution = Boolean(process.argv[1]) &&
|
|
609
|
+
(() => {
|
|
610
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
611
|
+
const invokedPath = path.resolve(process.argv[1]);
|
|
612
|
+
try {
|
|
613
|
+
return fs.realpathSync(invokedPath) === fs.realpathSync(cliPath);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
return invokedPath === cliPath;
|
|
617
|
+
}
|
|
618
|
+
})();
|
|
506
619
|
if (isDirectCliExecution) {
|
|
507
620
|
main().catch((error) => {
|
|
508
621
|
if (error instanceof Error) {
|
package/dist/cli.test.js
CHANGED
|
@@ -38,6 +38,34 @@ function runCli(input) {
|
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
function runCliViaBinSymlink(input) {
|
|
42
|
+
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-cli-bin-"));
|
|
43
|
+
const binPath = path.join(binDir, "askthew-mcp");
|
|
44
|
+
fs.symlinkSync(cliPath, binPath);
|
|
45
|
+
try {
|
|
46
|
+
return spawnSync(binPath, input.args, {
|
|
47
|
+
cwd: input.cwd,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
52
|
+
HOME: input.home,
|
|
53
|
+
USERPROFILE: input.home,
|
|
54
|
+
ASKTHEW_DATA_DIR: input.dataDir,
|
|
55
|
+
ASKTHEW_EMAIL: "founder@example.com",
|
|
56
|
+
ASKTHEW_CLI_TOKEN: "",
|
|
57
|
+
ASKTHEW_CLI_TOKEN_ID: "",
|
|
58
|
+
ASKTHEW_USER_ID: "",
|
|
59
|
+
ASKTHEW_INSTALL_TOKEN: "",
|
|
60
|
+
ASKTHEW_FREE_MODE: "",
|
|
61
|
+
...(input.extraEnv ?? {}),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
fs.rmSync(binDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
41
69
|
async function withCliEnv(dataDir, fn) {
|
|
42
70
|
const previous = {
|
|
43
71
|
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
@@ -93,6 +121,41 @@ test("free install without prior auth writes config, instructions, and local ide
|
|
|
93
121
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
94
122
|
}
|
|
95
123
|
});
|
|
124
|
+
test("published-style bin symlink runs free install instead of silently exiting", () => {
|
|
125
|
+
const fixture = makeFixture();
|
|
126
|
+
try {
|
|
127
|
+
const help = runCliViaBinSymlink({
|
|
128
|
+
args: ["--help"],
|
|
129
|
+
cwd: fixture.project,
|
|
130
|
+
home: fixture.home,
|
|
131
|
+
dataDir: fixture.dataDir,
|
|
132
|
+
});
|
|
133
|
+
assert.equal(help.status, 0, help.stderr);
|
|
134
|
+
assert.match(help.stdout, /Ask The W Coding Agent Connector/);
|
|
135
|
+
const result = runCliViaBinSymlink({
|
|
136
|
+
args: [
|
|
137
|
+
"install",
|
|
138
|
+
"--host",
|
|
139
|
+
"codex",
|
|
140
|
+
"--free",
|
|
141
|
+
"--email",
|
|
142
|
+
"founder@example.com",
|
|
143
|
+
"--api-url",
|
|
144
|
+
"http://127.0.0.1:9",
|
|
145
|
+
"--dry-run",
|
|
146
|
+
],
|
|
147
|
+
cwd: fixture.project,
|
|
148
|
+
home: fixture.home,
|
|
149
|
+
dataDir: fixture.dataDir,
|
|
150
|
+
});
|
|
151
|
+
assert.equal(result.status, 0, result.stderr);
|
|
152
|
+
assert.match(result.stdout, /Ask The W plugin dry run complete/);
|
|
153
|
+
assert.match(result.stdout, /--package @askthew\/mcp-plugin@latest askthew-mcp install/);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
96
159
|
test("free install dry-run stays non-mutating and does not create identity", () => {
|
|
97
160
|
const fixture = makeFixture();
|
|
98
161
|
try {
|
|
@@ -138,6 +201,57 @@ test("free install ignores stale legacy credentials and writes local identity",
|
|
|
138
201
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
139
202
|
}
|
|
140
203
|
});
|
|
204
|
+
test("refresh rewrites host config and instructions while preserving local identity and data", () => {
|
|
205
|
+
const fixture = makeFixture();
|
|
206
|
+
try {
|
|
207
|
+
const install = runCli({
|
|
208
|
+
args: ["install", "--host", "claude_code", "--free", "--email", "founder@example.com", "--api-url", "http://127.0.0.1:9"],
|
|
209
|
+
cwd: fixture.project,
|
|
210
|
+
home: fixture.home,
|
|
211
|
+
dataDir: fixture.dataDir,
|
|
212
|
+
});
|
|
213
|
+
assert.equal(install.status, 0, install.stderr);
|
|
214
|
+
const beforeIdentity = JSON.parse(fs.readFileSync(path.join(fixture.dataDir, "identity.json"), "utf8"));
|
|
215
|
+
fs.writeFileSync(path.join(fixture.dataDir, "store.sqlite"), "keep me", "utf8");
|
|
216
|
+
const refresh = runCli({
|
|
217
|
+
args: ["refresh", "--host", "claude_code", "--api-url", "http://127.0.0.1:9"],
|
|
218
|
+
cwd: fixture.project,
|
|
219
|
+
home: fixture.home,
|
|
220
|
+
dataDir: fixture.dataDir,
|
|
221
|
+
});
|
|
222
|
+
assert.equal(refresh.status, 0, refresh.stderr);
|
|
223
|
+
assert.match(refresh.stdout, /plugin refresh complete/);
|
|
224
|
+
assert.match(refresh.stdout, /Local identity preserved/);
|
|
225
|
+
assert.match(refresh.stdout, /Plugin package version:/);
|
|
226
|
+
const afterIdentity = JSON.parse(fs.readFileSync(path.join(fixture.dataDir, "identity.json"), "utf8"));
|
|
227
|
+
assert.equal(afterIdentity.installId, beforeIdentity.installId);
|
|
228
|
+
assert.equal(afterIdentity.emailClaim, "founder@example.com");
|
|
229
|
+
assert.equal(fs.readFileSync(path.join(fixture.dataDir, "store.sqlite"), "utf8"), "keep me");
|
|
230
|
+
assert.match(fs.readFileSync(path.join(fixture.project, "CLAUDE.md"), "utf8"), /ASKTHEW_PLUGIN_INSTRUCTIONS_START/);
|
|
231
|
+
assert.match(fs.readFileSync(path.join(fixture.project, "AGENTS.md"), "utf8"), /ASKTHEW_PLUGIN_INSTRUCTIONS_START/);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
test("refresh dry-run does not create a local identity", () => {
|
|
238
|
+
const fixture = makeFixture();
|
|
239
|
+
try {
|
|
240
|
+
const refresh = runCli({
|
|
241
|
+
args: ["refresh", "--host", "codex", "--dry-run"],
|
|
242
|
+
cwd: fixture.project,
|
|
243
|
+
home: fixture.home,
|
|
244
|
+
dataDir: fixture.dataDir,
|
|
245
|
+
});
|
|
246
|
+
assert.equal(refresh.status, 0, refresh.stderr);
|
|
247
|
+
assert.match(refresh.stdout, /refresh dry run complete/);
|
|
248
|
+
assert.equal(fs.existsSync(path.join(fixture.dataDir, "identity.json")), false);
|
|
249
|
+
assert.equal(fs.existsSync(path.join(fixture.home, ".codex", "config.toml")), false);
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
141
255
|
test("auth status reports missing local identity without pending-code guidance", () => {
|
|
142
256
|
const fixture = makeFixture();
|
|
143
257
|
try {
|
package/dist/index.js
CHANGED
|
@@ -482,10 +482,10 @@ function loginCommandHint() {
|
|
|
482
482
|
]) {
|
|
483
483
|
const email = String(value ?? "").trim();
|
|
484
484
|
if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
|
485
|
-
return `npx @askthew/mcp-plugin identify --email ${email}`;
|
|
485
|
+
return `npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp identify --email ${email}`;
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
|
-
return "npx @askthew/mcp-plugin identify --email <your-email>";
|
|
488
|
+
return "npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp identify --email <your-email>";
|
|
489
489
|
}
|
|
490
490
|
async function sendStartupHeartbeat(options) {
|
|
491
491
|
if (!hasServerIdentity(options?.credentials)) {
|
|
@@ -1278,7 +1278,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1278
1278
|
extra: {
|
|
1279
1279
|
tool: "review_session",
|
|
1280
1280
|
limit: 3,
|
|
1281
|
-
upgradeUrl: "https://askthew.com/
|
|
1281
|
+
upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=review_session",
|
|
1282
1282
|
cta: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
1283
1283
|
},
|
|
1284
1284
|
});
|
package/dist/index.test.js
CHANGED
|
@@ -743,7 +743,7 @@ test("free decisions, recap, coach, and promote return human-readable compact ou
|
|
|
743
743
|
assert.equal(response.ok, false);
|
|
744
744
|
assert.equal(response.code, "free_tier_paid_feature");
|
|
745
745
|
assert.equal(response.tool, "coach");
|
|
746
|
-
assert.match(response.upgradeUrl, /askthew\.com\/
|
|
746
|
+
assert.match(response.upgradeUrl, /askthew\.com\/plugin/);
|
|
747
747
|
}
|
|
748
748
|
});
|
|
749
749
|
});
|
|
@@ -910,7 +910,7 @@ test("free review_session markdown is capped and json is cursor-paginated with a
|
|
|
910
910
|
assert.equal(capped.ok, false);
|
|
911
911
|
assert.equal(capped.code, "free_tier_limit");
|
|
912
912
|
assert.equal(capped.limit, 3);
|
|
913
|
-
assert.match(capped.upgradeUrl, /askthew\.com\/
|
|
913
|
+
assert.match(capped.upgradeUrl, /askthew\.com\/plugin/);
|
|
914
914
|
});
|
|
915
915
|
});
|
|
916
916
|
test("paid tools return canonical paywall envelope before transport in free mode", async () => {
|
|
@@ -943,7 +943,7 @@ test("paid tools return canonical paywall envelope before transport in free mode
|
|
|
943
943
|
assert.equal(response.ok, false, toolName);
|
|
944
944
|
assert.equal(response.code, "free_tier_paid_feature", toolName);
|
|
945
945
|
assert.equal(response.tool, toolName, toolName);
|
|
946
|
-
assert.match(response.upgradeUrl, /askthew\.com\/
|
|
946
|
+
assert.match(response.upgradeUrl, /askthew\.com\/plugin/, toolName);
|
|
947
947
|
assert.equal(response.supportEmail, "support@askthew.com", toolName);
|
|
948
948
|
}
|
|
949
949
|
});
|
package/dist/install.d.ts
CHANGED
|
@@ -111,6 +111,8 @@ export declare function uninstallHostConfig(input: UninstallHostConfigInput): {
|
|
|
111
111
|
settingsPath: string;
|
|
112
112
|
json: string;
|
|
113
113
|
removedServerName: string;
|
|
114
|
+
foundConfigFile: boolean;
|
|
115
|
+
removedServer: boolean;
|
|
114
116
|
wroteFile: boolean;
|
|
115
117
|
};
|
|
116
118
|
export declare function sendInstallHeartbeat(input: HostConfigInput & {
|
package/dist/install.js
CHANGED
|
@@ -21,7 +21,7 @@ export function createServerEntry(input) {
|
|
|
21
21
|
const scope = resolvePluginScope(input.cwd ?? process.cwd());
|
|
22
22
|
return {
|
|
23
23
|
command: "npx",
|
|
24
|
-
args: ["-y", "--prefer-online", "@askthew/mcp-plugin@latest"],
|
|
24
|
+
args: ["-y", "--prefer-online", "--package", "@askthew/mcp-plugin@latest", "askthew-mcp"],
|
|
25
25
|
env: {
|
|
26
26
|
ASKTHEW_API_URL: input.apiUrl,
|
|
27
27
|
...(input.free ? { ASKTHEW_FREE_MODE: "1" } : { ASKTHEW_INSTALL_TOKEN: input.token ?? "" }),
|
|
@@ -133,24 +133,23 @@ export function formatInstallCommand(input) {
|
|
|
133
133
|
"npx",
|
|
134
134
|
"-y",
|
|
135
135
|
"--prefer-online",
|
|
136
|
+
"--package",
|
|
136
137
|
"@askthew/mcp-plugin@latest",
|
|
138
|
+
"askthew-mcp",
|
|
137
139
|
"install",
|
|
138
140
|
"--host",
|
|
139
141
|
input.hostType,
|
|
142
|
+
...(input.free ? ["--free"] : ["--token", JSON.stringify(input.token ?? "")]),
|
|
140
143
|
"--api-url",
|
|
141
144
|
JSON.stringify(input.apiUrl),
|
|
142
145
|
"--server-name",
|
|
143
146
|
JSON.stringify(input.serverName),
|
|
144
147
|
];
|
|
145
148
|
if (input.free) {
|
|
146
|
-
parts.push("--free");
|
|
147
149
|
if (input.email) {
|
|
148
150
|
parts.push("--email", JSON.stringify(input.email));
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
|
-
else {
|
|
152
|
-
parts.splice(7, 0, "--token", JSON.stringify(input.token ?? ""));
|
|
153
|
-
}
|
|
154
153
|
return parts.join(" ");
|
|
155
154
|
}
|
|
156
155
|
export function verificationNextStep(hostType) {
|
|
@@ -219,9 +218,13 @@ export function uninstallHostConfig(input) {
|
|
|
219
218
|
});
|
|
220
219
|
const serverName = input.serverName?.trim() || "askthew";
|
|
221
220
|
let json = "";
|
|
221
|
+
let foundConfigFile = false;
|
|
222
|
+
let removedServer = false;
|
|
222
223
|
if (fs.existsSync(settingsPath)) {
|
|
224
|
+
foundConfigFile = true;
|
|
223
225
|
const raw = fs.readFileSync(settingsPath, "utf8");
|
|
224
226
|
if (input.hostType === "codex") {
|
|
227
|
+
removedServer = raw !== removeCodexTomlServer(raw, serverName) || (serverName !== "askthew" && raw !== removeCodexTomlServer(raw, "askthew"));
|
|
225
228
|
json = removeCodexTomlServer(raw, serverName);
|
|
226
229
|
if (serverName !== "askthew") {
|
|
227
230
|
json = removeCodexTomlServer(json, "askthew");
|
|
@@ -236,6 +239,7 @@ export function uninstallHostConfig(input) {
|
|
|
236
239
|
const existingProject = isRecord(existingProjects[cwd]) ? existingProjects[cwd] : {};
|
|
237
240
|
const existingMcpServers = isRecord(existingProject.mcpServers) ? existingProject.mcpServers : {};
|
|
238
241
|
const nextServers = { ...existingMcpServers };
|
|
242
|
+
removedServer = serverName in nextServers || (serverName !== "askthew" && "askthew" in nextServers);
|
|
239
243
|
delete nextServers[serverName];
|
|
240
244
|
if (serverName !== "askthew")
|
|
241
245
|
delete nextServers.askthew;
|
|
@@ -253,6 +257,7 @@ export function uninstallHostConfig(input) {
|
|
|
253
257
|
else {
|
|
254
258
|
const existingMcpServers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
255
259
|
const nextServers = { ...existingMcpServers };
|
|
260
|
+
removedServer = serverName in nextServers || (serverName !== "askthew" && "askthew" in nextServers);
|
|
256
261
|
delete nextServers[serverName];
|
|
257
262
|
if (serverName !== "askthew")
|
|
258
263
|
delete nextServers.askthew;
|
|
@@ -268,7 +273,9 @@ export function uninstallHostConfig(input) {
|
|
|
268
273
|
settingsPath,
|
|
269
274
|
json,
|
|
270
275
|
removedServerName: serverName,
|
|
271
|
-
|
|
276
|
+
foundConfigFile,
|
|
277
|
+
removedServer,
|
|
278
|
+
wroteFile: !input.dryRun && foundConfigFile,
|
|
272
279
|
};
|
|
273
280
|
}
|
|
274
281
|
export async function sendInstallHeartbeat(input) {
|
package/dist/install.test.js
CHANGED
|
@@ -29,7 +29,7 @@ test("mergeHostSettings preserves unrelated MCP servers and replaces askthew", (
|
|
|
29
29
|
assert.deepEqual(Object.keys(merged.mcpServers ?? {}).sort(), ["askthew_workspace_a", "github"]);
|
|
30
30
|
assert.deepEqual(merged.mcpServers.askthew_workspace_a, {
|
|
31
31
|
command: "npx",
|
|
32
|
-
args: ["-y", "--prefer-online", "@askthew/mcp-plugin@latest"],
|
|
32
|
+
args: ["-y", "--prefer-online", "--package", "@askthew/mcp-plugin@latest", "askthew-mcp"],
|
|
33
33
|
env: {
|
|
34
34
|
ASKTHEW_INSTALL_TOKEN: "token-123",
|
|
35
35
|
ASKTHEW_API_URL: "https://askthew.example.com",
|
|
@@ -176,7 +176,7 @@ test("formatInstallCommand emits the one-command guided install form", () => {
|
|
|
176
176
|
apiUrl: "https://askthew.example.com",
|
|
177
177
|
serverName: "askthew_workspace_a",
|
|
178
178
|
});
|
|
179
|
-
assert.equal(command, 'npx -y --prefer-online @askthew/mcp-plugin@latest install --host codex --token "token-abc" --api-url "https://askthew.example.com" --server-name "askthew_workspace_a"');
|
|
179
|
+
assert.equal(command, 'npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install --host codex --token "token-abc" --api-url "https://askthew.example.com" --server-name "askthew_workspace_a"');
|
|
180
180
|
});
|
|
181
181
|
test("verificationNextStep points users to every-session startup capture", () => {
|
|
182
182
|
const nextStep = verificationNextStep("codex");
|
|
@@ -289,9 +289,26 @@ test("uninstall removes host config and Ask The W agent instruction blocks", ()
|
|
|
289
289
|
cwd: tempRoot,
|
|
290
290
|
});
|
|
291
291
|
assert.doesNotMatch(fs.readFileSync(removed.settingsPath, "utf8"), /askthew/);
|
|
292
|
+
assert.equal(removed.foundConfigFile, true);
|
|
293
|
+
assert.equal(removed.removedServer, true);
|
|
294
|
+
assert.equal(removed.wroteFile, true);
|
|
292
295
|
assert.equal(instructions.paths.length, 2);
|
|
293
296
|
assert.doesNotMatch(fs.readFileSync(path.join(tempRoot, "AGENTS.md"), "utf8"), /ASKTHEW_PLUGIN_INSTRUCTIONS_START/);
|
|
294
297
|
assert.doesNotMatch(fs.readFileSync(path.join(tempRoot, "CLAUDE.md"), "utf8"), /ASKTHEW_PLUGIN_INSTRUCTIONS_START/);
|
|
295
298
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
296
299
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
297
300
|
});
|
|
301
|
+
test("uninstall reports when no broken-npx install artifacts exist", () => {
|
|
302
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-uninstall-empty-home-"));
|
|
303
|
+
const removed = uninstallHostConfig({
|
|
304
|
+
hostType: "codex",
|
|
305
|
+
serverName: "askthew",
|
|
306
|
+
homeDirectory: tempHome,
|
|
307
|
+
dryRun: true,
|
|
308
|
+
});
|
|
309
|
+
assert.equal(removed.foundConfigFile, false);
|
|
310
|
+
assert.equal(removed.removedServer, false);
|
|
311
|
+
assert.equal(removed.wroteFile, false);
|
|
312
|
+
assert.equal(removed.json, "");
|
|
313
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
314
|
+
});
|
package/dist/lib/cli-actions.js
CHANGED
|
@@ -22,7 +22,7 @@ export function installPreCommitHook(input = {}) {
|
|
|
22
22
|
const hook = [
|
|
23
23
|
"#!/bin/sh",
|
|
24
24
|
"# Ask The W pre-commit decision prompt",
|
|
25
|
-
"npx -y --prefer-online @askthew/mcp-plugin@latest hook-check --pre-commit",
|
|
25
|
+
"npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp hook-check --pre-commit",
|
|
26
26
|
"",
|
|
27
27
|
].join("\n");
|
|
28
28
|
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
@@ -20,9 +20,9 @@ export function paidFeatureNudge(tool) {
|
|
|
20
20
|
tool,
|
|
21
21
|
message: `${feature.label} ${feature.verb} a paid feature. See ${PRICING_URL}.`,
|
|
22
22
|
pricingUrl: PRICING_URL,
|
|
23
|
-
upgradeUrl: `https://askthew.com/
|
|
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 @askthew/mcp-plugin upgrade",
|
|
25
|
+
cta: "Run: npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp upgrade",
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
export function toolJson(value) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askthew/mcp-plugin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"bin": {
|
|
10
|
-
"askthew-mcp": "dist/cli.js"
|
|
10
|
+
"askthew-mcp": "dist/cli.js",
|
|
11
|
+
"mcp-plugin": "dist/cli.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"README.md",
|