@askthew/mcp-plugin 0.4.3 → 0.4.5
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 +18 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.js +102 -51
- package/dist/cli.test.js +79 -93
- package/dist/free-tier-policy.test.js +12 -11
- package/dist/index.js +1 -1
- package/dist/index.test.js +13 -15
- package/dist/lib/free-tier-policy.d.ts +1 -2
- package/dist/lib/free-tier-policy.js +3 -19
- package/dist/lib/paths.d.ts +0 -1
- package/dist/lib/paths.js +0 -3
- package/dist/lib/telemetry.js +14 -18
- package/dist/lib/upgrade-nudge.js +1 -1
- package/package.json +1 -1
- package/dist/auth-pending.test.d.ts +0 -1
- package/dist/auth-pending.test.js +0 -56
- package/dist/lib/auth-magic-link.d.ts +0 -22
- package/dist/lib/auth-magic-link.js +0 -43
- package/dist/lib/auth-pending.d.ts +0 -23
- package/dist/lib/auth-pending.js +0 -36
package/README.md
CHANGED
|
@@ -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 @askthew/mcp-plugin@latest 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.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { requestMagicLinkCode as requestMagicLinkCodeDefault, verifyMagicLinkCode as verifyMagicLinkCodeDefault } from "./lib/auth-magic-link.js";
|
|
3
2
|
import { tryRegisterFreeInstall } from "./lib/free-install-registration.js";
|
|
4
3
|
type AuthCommandDeps = {
|
|
5
4
|
log?: (message: string) => void;
|
|
6
|
-
requestMagicLinkCode?: typeof requestMagicLinkCodeDefault;
|
|
7
|
-
verifyMagicLinkCode?: typeof verifyMagicLinkCodeDefault;
|
|
8
5
|
registerFreeInstall?: typeof tryRegisterFreeInstall;
|
|
9
6
|
};
|
|
10
7
|
export declare function runAuthCommand(argv: string[], deps?: AuthCommandDeps): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -5,9 +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 {
|
|
9
|
-
import { verifyMagicLinkCode as verifyMagicLinkCodeDefault, } from "./lib/auth-magic-link.js";
|
|
10
|
-
import { credentialsPath, ensureAskTheWDataDir } from "./lib/paths.js";
|
|
8
|
+
import { ensureAskTheWDataDir, identityPath } from "./lib/paths.js";
|
|
11
9
|
import { loadCliCredentials } from "./lib/free-tier-policy.js";
|
|
12
10
|
import { describeFreeIdentity, tryRegisterFreeInstall } from "./lib/free-install-registration.js";
|
|
13
11
|
import { ensureLocalIdentity, loadLocalIdentity, publicIdentity } from "./lib/local-identity.js";
|
|
@@ -24,17 +22,17 @@ function usage() {
|
|
|
24
22
|
" askthew-mcp",
|
|
25
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]",
|
|
26
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]",
|
|
27
26
|
" askthew-mcp uninstall --host <claude_code|codex|cursor> [--server-name <name>] [--dry-run] [--keep-local-data] [--keep-auth] [--keep-agent-instructions]",
|
|
28
27
|
" askthew-mcp identify --email <email> [--no-telemetry]",
|
|
29
28
|
" askthew-mcp identity status",
|
|
30
29
|
" askthew-mcp auth login --email <email> [--no-telemetry]",
|
|
31
|
-
" askthew-mcp auth verify --code <code> [--email <email>]",
|
|
32
30
|
" askthew-mcp auth logout | status",
|
|
33
31
|
" askthew-mcp telemetry status | opt-out | opt-in | preview",
|
|
34
32
|
" askthew-mcp local stats | reset --hard",
|
|
35
33
|
" askthew-mcp install-hook --pre-commit",
|
|
36
34
|
" askthew-mcp digest --weekly",
|
|
37
|
-
" askthew-mcp sync upload [--dry-run]",
|
|
35
|
+
" askthew-mcp sync upload [--token <workspace-install-token>] [--dry-run]",
|
|
38
36
|
" askthew-mcp print-config --host <claude_code|codex|cursor> --token <install-token> --api-url <url> --server-name <name> [--client-id <id>] [--client-label <label>]",
|
|
39
37
|
].join("\n");
|
|
40
38
|
}
|
|
@@ -135,6 +133,16 @@ function parseInstallArgs(argv) {
|
|
|
135
133
|
function normalizeInstallToken(token) {
|
|
136
134
|
return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
137
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
|
+
}
|
|
138
146
|
function detectLoginEmail() {
|
|
139
147
|
for (const value of [
|
|
140
148
|
process.env.ASKTHEW_EMAIL,
|
|
@@ -232,6 +240,10 @@ async function main() {
|
|
|
232
240
|
}
|
|
233
241
|
return;
|
|
234
242
|
}
|
|
243
|
+
if (command === "refresh") {
|
|
244
|
+
await runRefreshCommand(argv);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
235
247
|
if (command === "identify") {
|
|
236
248
|
await runIdentifyCommand(argv);
|
|
237
249
|
return;
|
|
@@ -273,7 +285,7 @@ async function main() {
|
|
|
273
285
|
return;
|
|
274
286
|
}
|
|
275
287
|
if (command === "upgrade") {
|
|
276
|
-
console.log("Open https://askthew.com/
|
|
288
|
+
console.log("Open https://askthew.com/plugin to upgrade, then run `askthew-mcp upgrade --finalize`.");
|
|
277
289
|
if (argv.includes("--finalize")) {
|
|
278
290
|
console.log("Finalize will rewrite host config after a paid workspace token is available in the web app.");
|
|
279
291
|
}
|
|
@@ -340,7 +352,7 @@ async function runUninstallCommand(argv) {
|
|
|
340
352
|
fs.rmSync(ensureAskTheWDataDir(), { recursive: true, force: true });
|
|
341
353
|
}
|
|
342
354
|
if (!keepAuth && !dryRun) {
|
|
343
|
-
const file =
|
|
355
|
+
const file = identityPath();
|
|
344
356
|
if (fs.existsSync(file))
|
|
345
357
|
fs.rmSync(file, { force: true });
|
|
346
358
|
}
|
|
@@ -356,61 +368,103 @@ async function runUninstallCommand(argv) {
|
|
|
356
368
|
console.log(config.json);
|
|
357
369
|
}
|
|
358
370
|
}
|
|
371
|
+
async function runRefreshCommand(argv) {
|
|
372
|
+
const parsed = parseInstallArgs(["--free", ...argv]);
|
|
373
|
+
const isPaidRefresh = Boolean(parsed.token && !argv.includes("--free"));
|
|
374
|
+
const options = {
|
|
375
|
+
...parsed,
|
|
376
|
+
free: !isPaidRefresh,
|
|
377
|
+
};
|
|
378
|
+
const existingIdentity = loadLocalIdentity();
|
|
379
|
+
let freeIdentity = existingIdentity;
|
|
380
|
+
if (options.free && !options.dryRun && !freeIdentity) {
|
|
381
|
+
freeIdentity = ensureLocalIdentity({
|
|
382
|
+
emailClaim: options.email,
|
|
383
|
+
apiUrl: options.apiUrl,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const uninstall = uninstallHostConfig({
|
|
387
|
+
hostType: options.hostType,
|
|
388
|
+
serverName: options.serverName,
|
|
389
|
+
dryRun: options.dryRun,
|
|
390
|
+
});
|
|
391
|
+
const removedInstructions = uninstallBehaviorInstructions({
|
|
392
|
+
hostType: options.hostType,
|
|
393
|
+
dryRun: options.dryRun,
|
|
394
|
+
});
|
|
395
|
+
const install = installHostConfig(options);
|
|
396
|
+
const installedInstructions = options.installAgentInstructions
|
|
397
|
+
? installBehaviorInstructions({
|
|
398
|
+
hostType: options.hostType,
|
|
399
|
+
dryRun: options.dryRun,
|
|
400
|
+
})
|
|
401
|
+
: null;
|
|
402
|
+
if (options.free && freeIdentity && !options.dryRun) {
|
|
403
|
+
await tryRegisterFreeInstall({
|
|
404
|
+
identity: freeIdentity,
|
|
405
|
+
deviceLabel: options.clientLabel ?? `${options.hostType} free refresh`,
|
|
406
|
+
repo: {
|
|
407
|
+
repoName: process.env.ASKTHEW_REPO_NAME,
|
|
408
|
+
repoRoot: process.env.ASKTHEW_REPO_ROOT,
|
|
409
|
+
hostType: options.hostType,
|
|
410
|
+
},
|
|
411
|
+
options: { apiUrl: options.apiUrl },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
console.log(options.dryRun ? "Ask The W plugin refresh dry run complete." : "Ask The W plugin refresh complete.");
|
|
415
|
+
console.log(`Plugin package version: ${packageVersion()}`);
|
|
416
|
+
console.log(`Settings path: ${install.settingsPath}`);
|
|
417
|
+
console.log(`Removed instructions: ${removedInstructions.paths.join(", ") || "none"}`);
|
|
418
|
+
if (installedInstructions) {
|
|
419
|
+
console.log(`Installed instructions: ${installedInstructions.paths.join(", ") || installedInstructions.path}`);
|
|
420
|
+
}
|
|
421
|
+
if (options.free) {
|
|
422
|
+
console.log(freeIdentity
|
|
423
|
+
? `Local identity preserved: ${freeIdentity.installId}`
|
|
424
|
+
: "Local identity would be created on a non-dry-run refresh.");
|
|
425
|
+
console.log("Local data preserved: ~/.askthew/store.sqlite");
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
const heartbeatSent = install.wroteFile
|
|
429
|
+
? await sendInstallHeartbeat(options).catch(() => false)
|
|
430
|
+
: false;
|
|
431
|
+
console.log(heartbeatSent ? "Paid workspace heartbeat sent." : "Paid workspace heartbeat not sent yet.");
|
|
432
|
+
}
|
|
433
|
+
console.log(`Next step: ${install.nextStep}`);
|
|
434
|
+
if (options.dryRun) {
|
|
435
|
+
console.log("");
|
|
436
|
+
console.log(uninstall.json);
|
|
437
|
+
console.log("");
|
|
438
|
+
console.log(install.json);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
359
441
|
function argValue(argv, name) {
|
|
360
442
|
const index = argv.indexOf(name);
|
|
361
443
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
362
444
|
}
|
|
363
445
|
export async function runAuthCommand(argv, deps = {}) {
|
|
364
446
|
const log = deps.log ?? console.log;
|
|
365
|
-
const verifyCode = deps.verifyMagicLinkCode ?? verifyMagicLinkCodeDefault;
|
|
366
447
|
const registerInstall = deps.registerFreeInstall ?? tryRegisterFreeInstall;
|
|
367
448
|
const [subcommand] = argv;
|
|
368
449
|
if (subcommand === "status") {
|
|
369
450
|
const identity = loadLocalIdentity();
|
|
370
|
-
const credentials = loadCliCredentials();
|
|
371
451
|
log(identity
|
|
372
452
|
? `Identified local free install ${identity.installId}${identity.emailClaim ? ` with email claim ${identity.emailClaim}` : ""}. Email claim is unverified until upgrade.`
|
|
373
|
-
:
|
|
374
|
-
? `Logged in as ${credentials.email ?? credentials.userId}. Telemetry: ${credentials.telemetryOptOut ? "off" : "on"}`
|
|
375
|
-
: pendingAuth()
|
|
376
|
-
? `Not logged in. Pending code for ${pendingAuth()?.email}. Run \`askthew-mcp auth verify --code <6-digit-code>\`.`
|
|
377
|
-
: `No local identity yet. Run \`askthew-mcp identify --email <your-email>\`, or install with \`--free --email <your-email>\`.`);
|
|
453
|
+
: `No local identity yet. Run \`askthew-mcp identify --email <your-email>\`, or install with \`--free --email <your-email>\`.`);
|
|
378
454
|
return;
|
|
379
455
|
}
|
|
380
456
|
if (subcommand === "logout") {
|
|
381
|
-
const file =
|
|
457
|
+
const file = identityPath();
|
|
382
458
|
if (fs.existsSync(file))
|
|
383
459
|
fs.rmSync(file);
|
|
384
|
-
log("
|
|
460
|
+
log("Removed Ask The W local free install identity.");
|
|
385
461
|
return;
|
|
386
462
|
}
|
|
387
|
-
if (subcommand !== "login"
|
|
388
|
-
throw new Error("Usage: askthew-mcp auth login --email <email> [--no-telemetry] | askthew-mcp auth
|
|
463
|
+
if (subcommand !== "login") {
|
|
464
|
+
throw new Error("Usage: askthew-mcp auth login --email <email> [--no-telemetry] | askthew-mcp auth logout | status");
|
|
389
465
|
}
|
|
390
466
|
ensureAskTheWDataDir();
|
|
391
467
|
const email = argValue(argv, "--email")?.trim();
|
|
392
|
-
const code = argValue(argv, "--code")?.trim();
|
|
393
|
-
if (subcommand === "verify" || code) {
|
|
394
|
-
if (!code)
|
|
395
|
-
throw new Error("Missing --code.");
|
|
396
|
-
const pending = email ? pendingAuthForEmail(email) : pendingAuth();
|
|
397
|
-
if (!pending) {
|
|
398
|
-
throw new Error(email
|
|
399
|
-
? `No pending Ask The W login request for ${email}. Run \`askthew-mcp auth login --email ${email}\` first.`
|
|
400
|
-
: "No pending Ask The W login request. Run `askthew-mcp auth login --email <email>` first.");
|
|
401
|
-
}
|
|
402
|
-
if (subcommand === "login") {
|
|
403
|
-
log("Using the pending Ask The W login request. Next time, run `askthew-mcp auth verify --code <6-digit-code>`.");
|
|
404
|
-
}
|
|
405
|
-
const credentials = await verifyCode({
|
|
406
|
-
requestId: pending.requestId,
|
|
407
|
-
code,
|
|
408
|
-
telemetryOptOut: pending.telemetryOptOut,
|
|
409
|
-
});
|
|
410
|
-
clearPendingAuth();
|
|
411
|
-
log(`Logged in. Account status: ${credentials.accountStatus}. Credentials stored with mode 0600.`);
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
468
|
if (!email)
|
|
415
469
|
throw new Error("Missing --email.");
|
|
416
470
|
const noTelemetry = argv.includes("--no-telemetry");
|
|
@@ -464,15 +518,8 @@ async function runTelemetryCommand(argv) {
|
|
|
464
518
|
return;
|
|
465
519
|
}
|
|
466
520
|
if (subcommand === "opt-out" || subcommand === "opt-in") {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
console.log(`Telemetry: ${subcommand === "opt-out" ? "off" : "on"}`);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const next = { ...credentials, telemetryOptOut: subcommand === "opt-out" };
|
|
473
|
-
fs.writeFileSync(credentialsPath(), `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
474
|
-
fs.chmodSync(credentialsPath(), 0o600);
|
|
475
|
-
console.log(`Telemetry: ${next.telemetryOptOut ? "off" : "on"}`);
|
|
521
|
+
ensureLocalIdentity({ telemetryOptOut: subcommand === "opt-out" });
|
|
522
|
+
console.log(`Telemetry: ${subcommand === "opt-out" ? "off" : "on"}`);
|
|
476
523
|
return;
|
|
477
524
|
}
|
|
478
525
|
if (subcommand === "preview") {
|
|
@@ -525,7 +572,7 @@ async function runDigestCommand(argv) {
|
|
|
525
572
|
}
|
|
526
573
|
async function runSyncCommand(argv) {
|
|
527
574
|
if (argv[0] !== "upload")
|
|
528
|
-
throw new Error("Usage: askthew-mcp sync upload [--dry-run]");
|
|
575
|
+
throw new Error("Usage: askthew-mcp sync upload [--token <workspace-install-token>] [--dry-run]");
|
|
529
576
|
const credentials = loadCliCredentials();
|
|
530
577
|
if (!credentials)
|
|
531
578
|
throw new Error("No local identity. Run `askthew-mcp identify --email <your-email>` first.");
|
|
@@ -534,7 +581,11 @@ async function runSyncCommand(argv) {
|
|
|
534
581
|
console.log(JSON.stringify(syncDryRun(store), null, 2));
|
|
535
582
|
return;
|
|
536
583
|
}
|
|
537
|
-
|
|
584
|
+
const syncToken = argValue(argv, "--token")?.trim() || process.env.ASKTHEW_INSTALL_TOKEN?.trim();
|
|
585
|
+
if (!syncToken) {
|
|
586
|
+
throw new Error("Missing workspace install token. Pass `--token <workspace-install-token>` after upgrading.");
|
|
587
|
+
}
|
|
588
|
+
console.log(JSON.stringify(await uploadLocalStore({ store, credentials, syncToken }), null, 2));
|
|
538
589
|
}
|
|
539
590
|
const isDirectCliExecution = Boolean(process.argv[1]) && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
540
591
|
if (isDirectCliExecution) {
|
package/dist/cli.test.js
CHANGED
|
@@ -6,7 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { runAuthCommand } from "./cli.js";
|
|
9
|
-
import {
|
|
9
|
+
import { identityPath } from "./lib/paths.js";
|
|
10
10
|
const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
11
11
|
function makeFixture() {
|
|
12
12
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-cli-install-"));
|
|
@@ -38,14 +38,6 @@ function runCli(input) {
|
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
function writeCredentials(dataDir) {
|
|
42
|
-
fs.writeFileSync(path.join(dataDir, "credentials.json"), `${JSON.stringify({
|
|
43
|
-
email: "founder@example.com",
|
|
44
|
-
userId: "user_1",
|
|
45
|
-
cliToken: "cli_token",
|
|
46
|
-
cliTokenId: "cli_token_1",
|
|
47
|
-
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
48
|
-
}
|
|
49
41
|
async function withCliEnv(dataDir, fn) {
|
|
50
42
|
const previous = {
|
|
51
43
|
ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
|
|
@@ -120,10 +112,10 @@ test("free install dry-run stays non-mutating and does not create identity", ()
|
|
|
120
112
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
121
113
|
}
|
|
122
114
|
});
|
|
123
|
-
test("free install
|
|
115
|
+
test("free install ignores stale legacy credentials and writes local identity", () => {
|
|
124
116
|
const fixture = makeFixture();
|
|
125
117
|
try {
|
|
126
|
-
|
|
118
|
+
fs.writeFileSync(path.join(fixture.dataDir, "credentials.json"), "{\"legacy\":true}\n", "utf8");
|
|
127
119
|
const result = runCli({
|
|
128
120
|
args: ["install", "--host", "claude_code", "--free", "--api-url", "http://127.0.0.1:9"],
|
|
129
121
|
cwd: fixture.project,
|
|
@@ -140,21 +132,66 @@ test("free install with auth writes host config and agent instructions", () => {
|
|
|
140
132
|
assert.equal("ASKTHEW_CLI_TOKEN" in server.env, false);
|
|
141
133
|
assert.equal("ASKTHEW_INSTALL_TOKEN" in server.env, false);
|
|
142
134
|
assert.match(fs.readFileSync(path.join(fixture.project, "CLAUDE.md"), "utf8"), /capture_session_signal/);
|
|
135
|
+
assert.equal(fs.existsSync(path.join(fixture.dataDir, "identity.json")), true);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
test("refresh rewrites host config and instructions while preserving local identity and data", () => {
|
|
142
|
+
const fixture = makeFixture();
|
|
143
|
+
try {
|
|
144
|
+
const install = runCli({
|
|
145
|
+
args: ["install", "--host", "claude_code", "--free", "--email", "founder@example.com", "--api-url", "http://127.0.0.1:9"],
|
|
146
|
+
cwd: fixture.project,
|
|
147
|
+
home: fixture.home,
|
|
148
|
+
dataDir: fixture.dataDir,
|
|
149
|
+
});
|
|
150
|
+
assert.equal(install.status, 0, install.stderr);
|
|
151
|
+
const beforeIdentity = JSON.parse(fs.readFileSync(path.join(fixture.dataDir, "identity.json"), "utf8"));
|
|
152
|
+
fs.writeFileSync(path.join(fixture.dataDir, "store.sqlite"), "keep me", "utf8");
|
|
153
|
+
const refresh = runCli({
|
|
154
|
+
args: ["refresh", "--host", "claude_code", "--api-url", "http://127.0.0.1:9"],
|
|
155
|
+
cwd: fixture.project,
|
|
156
|
+
home: fixture.home,
|
|
157
|
+
dataDir: fixture.dataDir,
|
|
158
|
+
});
|
|
159
|
+
assert.equal(refresh.status, 0, refresh.stderr);
|
|
160
|
+
assert.match(refresh.stdout, /plugin refresh complete/);
|
|
161
|
+
assert.match(refresh.stdout, /Local identity preserved/);
|
|
162
|
+
assert.match(refresh.stdout, /Plugin package version:/);
|
|
163
|
+
const afterIdentity = JSON.parse(fs.readFileSync(path.join(fixture.dataDir, "identity.json"), "utf8"));
|
|
164
|
+
assert.equal(afterIdentity.installId, beforeIdentity.installId);
|
|
165
|
+
assert.equal(afterIdentity.emailClaim, "founder@example.com");
|
|
166
|
+
assert.equal(fs.readFileSync(path.join(fixture.dataDir, "store.sqlite"), "utf8"), "keep me");
|
|
167
|
+
assert.match(fs.readFileSync(path.join(fixture.project, "CLAUDE.md"), "utf8"), /ASKTHEW_PLUGIN_INSTRUCTIONS_START/);
|
|
168
|
+
assert.match(fs.readFileSync(path.join(fixture.project, "AGENTS.md"), "utf8"), /ASKTHEW_PLUGIN_INSTRUCTIONS_START/);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
test("refresh dry-run does not create a local identity", () => {
|
|
175
|
+
const fixture = makeFixture();
|
|
176
|
+
try {
|
|
177
|
+
const refresh = runCli({
|
|
178
|
+
args: ["refresh", "--host", "codex", "--dry-run"],
|
|
179
|
+
cwd: fixture.project,
|
|
180
|
+
home: fixture.home,
|
|
181
|
+
dataDir: fixture.dataDir,
|
|
182
|
+
});
|
|
183
|
+
assert.equal(refresh.status, 0, refresh.stderr);
|
|
184
|
+
assert.match(refresh.stdout, /refresh dry run complete/);
|
|
185
|
+
assert.equal(fs.existsSync(path.join(fixture.dataDir, "identity.json")), false);
|
|
186
|
+
assert.equal(fs.existsSync(path.join(fixture.home, ".codex", "config.toml")), false);
|
|
143
187
|
}
|
|
144
188
|
finally {
|
|
145
189
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
146
190
|
}
|
|
147
191
|
});
|
|
148
|
-
test("auth status reports
|
|
192
|
+
test("auth status reports missing local identity without pending-code guidance", () => {
|
|
149
193
|
const fixture = makeFixture();
|
|
150
194
|
try {
|
|
151
|
-
fs.writeFileSync(path.join(fixture.dataDir, "config.json"), `${JSON.stringify({
|
|
152
|
-
pendingAuth: {
|
|
153
|
-
email: "founder@example.com",
|
|
154
|
-
requestId: "request_1",
|
|
155
|
-
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
156
|
-
},
|
|
157
|
-
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
158
195
|
const result = runCli({
|
|
159
196
|
args: ["auth", "status"],
|
|
160
197
|
cwd: fixture.project,
|
|
@@ -162,31 +199,35 @@ test("auth status reports a pending verification code", () => {
|
|
|
162
199
|
dataDir: fixture.dataDir,
|
|
163
200
|
});
|
|
164
201
|
assert.equal(result.status, 0, result.stderr);
|
|
165
|
-
assert.match(result.stdout, /
|
|
166
|
-
assert.
|
|
202
|
+
assert.match(result.stdout, /No local identity yet/);
|
|
203
|
+
assert.doesNotMatch(result.stdout, /verify --code|Pending code/);
|
|
167
204
|
}
|
|
168
205
|
finally {
|
|
169
206
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
170
207
|
}
|
|
171
208
|
});
|
|
172
|
-
test("auth code commands
|
|
209
|
+
test("removed auth code commands do not issue or verify email codes", () => {
|
|
173
210
|
const fixture = makeFixture();
|
|
174
211
|
try {
|
|
175
|
-
|
|
176
|
-
["auth", "verify", "--code", "123456"],
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
const verify = runCli({
|
|
213
|
+
args: ["auth", "verify", "--code", "123456"],
|
|
214
|
+
cwd: fixture.project,
|
|
215
|
+
home: fixture.home,
|
|
216
|
+
dataDir: fixture.dataDir,
|
|
217
|
+
extraEnv: { ASKTHEW_API_URL: "http://127.0.0.1:9" },
|
|
218
|
+
});
|
|
219
|
+
assert.equal(verify.status, 1);
|
|
220
|
+
assert.match(verify.stderr, /Usage: askthew-mcp auth login/);
|
|
221
|
+
const loginWithCode = runCli({
|
|
222
|
+
args: ["auth", "login", "--email", "founder@example.com", "--code", "123456"],
|
|
223
|
+
cwd: fixture.project,
|
|
224
|
+
home: fixture.home,
|
|
225
|
+
dataDir: fixture.dataDir,
|
|
226
|
+
extraEnv: { ASKTHEW_API_URL: "http://127.0.0.1:9" },
|
|
227
|
+
});
|
|
228
|
+
assert.equal(loginWithCode.status, 0, loginWithCode.stderr);
|
|
229
|
+
assert.match(loginWithCode.stdout, /No email code is required/);
|
|
230
|
+
assert.doesNotMatch(loginWithCode.stdout, /Code sent|Logged in/);
|
|
190
231
|
}
|
|
191
232
|
finally {
|
|
192
233
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
@@ -200,14 +241,6 @@ test("auth login now identifies the local free install without requesting an ema
|
|
|
200
241
|
await withCliEnv(fixture.dataDir, async () => {
|
|
201
242
|
await runAuthCommand(["login", "--email", "ymtest89+test5@gmail.com"], {
|
|
202
243
|
log: (message) => logs.push(message),
|
|
203
|
-
requestMagicLinkCode: async () => {
|
|
204
|
-
calls.push({ type: "unexpected_request" });
|
|
205
|
-
throw new Error("auth login must not request a new code.");
|
|
206
|
-
},
|
|
207
|
-
verifyMagicLinkCode: async () => {
|
|
208
|
-
calls.push({ type: "unexpected_verify" });
|
|
209
|
-
throw new Error("auth login must not verify a code.");
|
|
210
|
-
},
|
|
211
244
|
registerFreeInstall: async ({ identity }) => {
|
|
212
245
|
calls.push({ type: "register", installId: identity.installId, emailClaim: identity.emailClaim });
|
|
213
246
|
return { ok: true, registeredAt: new Date().toISOString() };
|
|
@@ -218,57 +251,10 @@ test("auth login now identifies the local free install without requesting an ema
|
|
|
218
251
|
assert.equal(calls[0].type, "register");
|
|
219
252
|
assert.equal(calls[0].emailClaim, "ymtest89+test5@gmail.com");
|
|
220
253
|
assert.equal(fs.existsSync(identityPath({ ASKTHEW_DATA_DIR: fixture.dataDir })), true);
|
|
221
|
-
assert.equal(fs.existsSync(
|
|
254
|
+
assert.equal(fs.existsSync(path.join(fixture.dataDir, "credentials.json")), false);
|
|
222
255
|
assert.match(logs.join("\n"), /No email code is required/);
|
|
223
256
|
}
|
|
224
257
|
finally {
|
|
225
258
|
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
226
259
|
}
|
|
227
260
|
});
|
|
228
|
-
test("backwards-compatible auth login --code verifies pending state without requesting a new code", async () => {
|
|
229
|
-
const fixture = makeFixture();
|
|
230
|
-
const calls = [];
|
|
231
|
-
const logs = [];
|
|
232
|
-
try {
|
|
233
|
-
await withCliEnv(fixture.dataDir, async () => {
|
|
234
|
-
writePrivateJson(configPath(), {
|
|
235
|
-
pendingAuth: {
|
|
236
|
-
email: "ymtest89+test5@gmail.com",
|
|
237
|
-
requestId: "22222222-2222-4222-8222-222222222222",
|
|
238
|
-
expiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
await runAuthCommand(["login", "--email", "ymtest89+test5@gmail.com", "--code", "150259"], {
|
|
242
|
-
log: (message) => logs.push(message),
|
|
243
|
-
requestMagicLinkCode: async () => {
|
|
244
|
-
calls.push({ type: "unexpected_request" });
|
|
245
|
-
throw new Error("login --code must not request a new code.");
|
|
246
|
-
},
|
|
247
|
-
verifyMagicLinkCode: async (input) => {
|
|
248
|
-
calls.push({ type: "verify", requestId: input.requestId, code: input.code });
|
|
249
|
-
const credentials = {
|
|
250
|
-
email: "ymtest89+test5@gmail.com",
|
|
251
|
-
userId: "user_2",
|
|
252
|
-
cliToken: "cli_token_2",
|
|
253
|
-
cliTokenId: "cli_token_2",
|
|
254
|
-
accountStatus: "new_dormant",
|
|
255
|
-
};
|
|
256
|
-
writePrivateJson(credentialsPath(), credentials);
|
|
257
|
-
return credentials;
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
assert.deepEqual(calls, [
|
|
262
|
-
{
|
|
263
|
-
type: "verify",
|
|
264
|
-
requestId: "22222222-2222-4222-8222-222222222222",
|
|
265
|
-
code: "150259",
|
|
266
|
-
},
|
|
267
|
-
]);
|
|
268
|
-
assert.match(logs.join("\n"), /Using the pending Ask The W login request/);
|
|
269
|
-
assert.match(logs.join("\n"), /Logged in/);
|
|
270
|
-
}
|
|
271
|
-
finally {
|
|
272
|
-
fs.rmSync(fixture.root, { recursive: true, force: true });
|
|
273
|
-
}
|
|
274
|
-
});
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { resolveMcpMode } from "./lib/free-tier-policy.js";
|
|
7
|
+
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
7
8
|
function withTempDataDir(fn) {
|
|
8
9
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-mode-"));
|
|
9
10
|
try {
|
|
@@ -21,15 +22,15 @@ test("mode resolution prefers paid install tokens", () => {
|
|
|
21
22
|
assert.equal(mode.mode, "paid");
|
|
22
23
|
assert.equal(mode.reason, "workspace_install_token");
|
|
23
24
|
});
|
|
24
|
-
test("mode resolution detects
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
test("mode resolution detects local free install identity", () => {
|
|
26
|
+
withTempDataDir((env) => {
|
|
27
|
+
ensureLocalIdentity({ emailClaim: "founder@example.com", env });
|
|
28
|
+
const mode = resolveMcpMode(env);
|
|
29
|
+
assert.equal(mode.mode, "free");
|
|
30
|
+
assert.equal(mode.reason, "local_install_identity");
|
|
31
|
+
assert.equal(mode.cliCredentials?.userId, mode.cliCredentials?.installId);
|
|
32
|
+
assert.equal(mode.cliCredentials?.email, "founder@example.com");
|
|
29
33
|
});
|
|
30
|
-
assert.equal(mode.mode, "free");
|
|
31
|
-
assert.equal(mode.reason, "cli_free_tier_credentials");
|
|
32
|
-
assert.equal(mode.cliCredentials?.userId, "user_1");
|
|
33
34
|
});
|
|
34
35
|
test("mode resolution distinguishes pending free auth from no identity", () => {
|
|
35
36
|
withTempDataDir((env) => {
|
|
@@ -39,12 +40,12 @@ test("mode resolution distinguishes pending free auth from no identity", () => {
|
|
|
39
40
|
});
|
|
40
41
|
const none = resolveMcpMode(env);
|
|
41
42
|
assert.equal(pending.mode, "free_pending_auth");
|
|
42
|
-
assert.equal(pending.reason, "
|
|
43
|
+
assert.equal(pending.reason, "free_mode_no_identity");
|
|
43
44
|
assert.equal(none.mode, "unauthenticated");
|
|
44
45
|
assert.equal(none.reason, "no_identity");
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
|
-
test("mode resolution
|
|
48
|
+
test("mode resolution ignores legacy credentials files", () => {
|
|
48
49
|
withTempDataDir((env, dataDir) => {
|
|
49
50
|
fs.writeFileSync(path.join(dataDir, "credentials.json"), "{\"not\":\"credentials\"}\n", "utf8");
|
|
50
51
|
const mode = resolveMcpMode({
|
|
@@ -52,6 +53,6 @@ test("mode resolution marks malformed free credentials as pending auth", () => {
|
|
|
52
53
|
ASKTHEW_FREE_MODE: "1",
|
|
53
54
|
});
|
|
54
55
|
assert.equal(mode.mode, "free_pending_auth");
|
|
55
|
-
assert.equal(mode.reason, "
|
|
56
|
+
assert.equal(mode.reason, "free_mode_no_identity");
|
|
56
57
|
});
|
|
57
58
|
});
|
package/dist/index.js
CHANGED
|
@@ -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
|
@@ -5,7 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { codingSessionSignalSchema, createAskTheWMcpServer, normalizeInstallTokenInput, redactCodingSessionSignal, redactProvenanceSignal, } from "./index.js";
|
|
7
7
|
import { LocalStore } from "./lib/local-store.js";
|
|
8
|
-
import {
|
|
8
|
+
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
9
9
|
function toolResultJson(result) {
|
|
10
10
|
return JSON.parse(result.content[0].text);
|
|
11
11
|
}
|
|
@@ -19,10 +19,11 @@ async function withFreeEnv(fn) {
|
|
|
19
19
|
ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
|
|
20
20
|
};
|
|
21
21
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-free-tools-"));
|
|
22
|
-
process.env.ASKTHEW_CLI_TOKEN = "cli_free_token";
|
|
23
|
-
process.env.ASKTHEW_USER_ID = "local-user";
|
|
24
|
-
process.env.ASKTHEW_CLI_TOKEN_ID = "cli-token-id";
|
|
25
22
|
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
23
|
+
ensureLocalIdentity({ emailClaim: "founder@example.com" });
|
|
24
|
+
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
25
|
+
delete process.env.ASKTHEW_USER_ID;
|
|
26
|
+
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
26
27
|
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
27
28
|
delete process.env.ASKTHEW_FREE_MODE;
|
|
28
29
|
try {
|
|
@@ -83,16 +84,11 @@ async function withInstalledFreeEnv(fn) {
|
|
|
83
84
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-installed-free-tools-"));
|
|
84
85
|
process.env.ASKTHEW_FREE_MODE = "1";
|
|
85
86
|
process.env.ASKTHEW_DATA_DIR = dataDir;
|
|
87
|
+
ensureLocalIdentity({ emailClaim: "ymtest89+test5@gmail.com" });
|
|
86
88
|
delete process.env.ASKTHEW_CLI_TOKEN;
|
|
87
89
|
delete process.env.ASKTHEW_USER_ID;
|
|
88
90
|
delete process.env.ASKTHEW_CLI_TOKEN_ID;
|
|
89
91
|
delete process.env.ASKTHEW_INSTALL_TOKEN;
|
|
90
|
-
writePrivateJson(credentialsPath(), {
|
|
91
|
-
email: "ymtest89+test5@gmail.com",
|
|
92
|
-
userId: "local-user",
|
|
93
|
-
cliToken: "cli_free_token",
|
|
94
|
-
cliTokenId: "cli-token-id",
|
|
95
|
-
});
|
|
96
92
|
try {
|
|
97
93
|
return await fn();
|
|
98
94
|
}
|
|
@@ -644,7 +640,8 @@ test("authenticated free mode keeps capture, decisions, and review local without
|
|
|
644
640
|
assert.equal(capture.ok, true);
|
|
645
641
|
assert.match(decision.id, /^d_/);
|
|
646
642
|
assert.equal(review.ok, true);
|
|
647
|
-
assert.equal(calls.length,
|
|
643
|
+
assert.equal(calls.length, 1);
|
|
644
|
+
assert.match(calls[0].url, /\/api\/cli\/v1\/free-installs\/register$/);
|
|
648
645
|
const store = LocalStore.open();
|
|
649
646
|
try {
|
|
650
647
|
const stats = store.stats();
|
|
@@ -686,7 +683,8 @@ test("installed free mode with credential file captures locally even if hosted a
|
|
|
686
683
|
assert.equal(capture.sessionId, "session-installed-free");
|
|
687
684
|
assert.equal("code" in capture, false);
|
|
688
685
|
assert.equal(JSON.stringify(capture).includes("local_only_free_feature"), false);
|
|
689
|
-
assert.equal(calls.length,
|
|
686
|
+
assert.equal(calls.length, 1);
|
|
687
|
+
assert.match(calls[0].url, /\/api\/cli\/v1\/free-installs\/register$/);
|
|
690
688
|
const store = LocalStore.open();
|
|
691
689
|
try {
|
|
692
690
|
const signals = store.listSignals({ sessionId: "session-installed-free", limit: 10 });
|
|
@@ -745,7 +743,7 @@ test("free decisions, recap, coach, and promote return human-readable compact ou
|
|
|
745
743
|
assert.equal(response.ok, false);
|
|
746
744
|
assert.equal(response.code, "free_tier_paid_feature");
|
|
747
745
|
assert.equal(response.tool, "coach");
|
|
748
|
-
assert.match(response.upgradeUrl, /askthew\.com\/
|
|
746
|
+
assert.match(response.upgradeUrl, /askthew\.com\/plugin/);
|
|
749
747
|
}
|
|
750
748
|
});
|
|
751
749
|
});
|
|
@@ -912,7 +910,7 @@ test("free review_session markdown is capped and json is cursor-paginated with a
|
|
|
912
910
|
assert.equal(capped.ok, false);
|
|
913
911
|
assert.equal(capped.code, "free_tier_limit");
|
|
914
912
|
assert.equal(capped.limit, 3);
|
|
915
|
-
assert.match(capped.upgradeUrl, /askthew\.com\/
|
|
913
|
+
assert.match(capped.upgradeUrl, /askthew\.com\/plugin/);
|
|
916
914
|
});
|
|
917
915
|
});
|
|
918
916
|
test("paid tools return canonical paywall envelope before transport in free mode", async () => {
|
|
@@ -945,7 +943,7 @@ test("paid tools return canonical paywall envelope before transport in free mode
|
|
|
945
943
|
assert.equal(response.ok, false, toolName);
|
|
946
944
|
assert.equal(response.code, "free_tier_paid_feature", toolName);
|
|
947
945
|
assert.equal(response.tool, toolName, toolName);
|
|
948
|
-
assert.match(response.upgradeUrl, /askthew\.com\/
|
|
946
|
+
assert.match(response.upgradeUrl, /askthew\.com\/plugin/, toolName);
|
|
949
947
|
assert.equal(response.supportEmail, "support@askthew.com", toolName);
|
|
950
948
|
}
|
|
951
949
|
});
|
|
@@ -7,8 +7,7 @@ export interface CliCredentials {
|
|
|
7
7
|
cliTokenId: string;
|
|
8
8
|
apiUrl?: string;
|
|
9
9
|
telemetryOptOut?: boolean;
|
|
10
|
-
|
|
11
|
-
identityKind?: "legacy_token" | "local_install";
|
|
10
|
+
identityKind: "local_install";
|
|
12
11
|
installId?: string;
|
|
13
12
|
localIdentity?: LocalInstallIdentity;
|
|
14
13
|
}
|
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { credentialsPath, readJsonFile } from "./paths.js";
|
|
3
1
|
import { loadLocalIdentity } from "./local-identity.js";
|
|
4
2
|
function clean(value) {
|
|
5
3
|
return String(value ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
6
4
|
}
|
|
7
5
|
export function loadCliCredentials(env = process.env) {
|
|
8
|
-
const explicitToken = clean(env.ASKTHEW_CLI_TOKEN);
|
|
9
|
-
if (explicitToken) {
|
|
10
|
-
return {
|
|
11
|
-
userId: clean(env.ASKTHEW_USER_ID) || "local",
|
|
12
|
-
cliToken: explicitToken,
|
|
13
|
-
cliTokenId: clean(env.ASKTHEW_CLI_TOKEN_ID) || "env",
|
|
14
|
-
apiUrl: clean(env.ASKTHEW_API_URL) || undefined,
|
|
15
|
-
telemetryOptOut: env.ASKTHEW_TELEMETRY === "off",
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
6
|
const localIdentity = loadLocalIdentity(env);
|
|
19
7
|
if (localIdentity) {
|
|
20
8
|
return {
|
|
@@ -29,11 +17,7 @@ export function loadCliCredentials(env = process.env) {
|
|
|
29
17
|
localIdentity,
|
|
30
18
|
};
|
|
31
19
|
}
|
|
32
|
-
|
|
33
|
-
if (!creds?.cliToken || !creds.userId || !creds.cliTokenId) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return creds;
|
|
20
|
+
return null;
|
|
37
21
|
}
|
|
38
22
|
export function resolveMcpMode(env = process.env) {
|
|
39
23
|
const installToken = clean(env.ASKTHEW_INSTALL_TOKEN);
|
|
@@ -49,13 +33,13 @@ export function resolveMcpMode(env = process.env) {
|
|
|
49
33
|
return {
|
|
50
34
|
mode: "free",
|
|
51
35
|
cliCredentials: credentials,
|
|
52
|
-
reason: "
|
|
36
|
+
reason: "local_install_identity",
|
|
53
37
|
};
|
|
54
38
|
}
|
|
55
39
|
if (clean(env.ASKTHEW_FREE_MODE) === "1" || clean(env.ASKTHEW_FREE_MODE).toLowerCase() === "true") {
|
|
56
40
|
return {
|
|
57
41
|
mode: "free_pending_auth",
|
|
58
|
-
reason:
|
|
42
|
+
reason: "free_mode_no_identity",
|
|
59
43
|
};
|
|
60
44
|
}
|
|
61
45
|
return {
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export declare function askTheWDataDir(env?: NodeJS.ProcessEnv): string;
|
|
2
2
|
export declare function ensureAskTheWDataDir(env?: NodeJS.ProcessEnv): string;
|
|
3
3
|
export declare function localStorePath(env?: NodeJS.ProcessEnv): string;
|
|
4
|
-
export declare function credentialsPath(env?: NodeJS.ProcessEnv): string;
|
|
5
4
|
export declare function identityPath(env?: NodeJS.ProcessEnv): string;
|
|
6
5
|
export declare function configPath(env?: NodeJS.ProcessEnv): string;
|
|
7
6
|
export declare function jsonFallbackStorePath(env?: NodeJS.ProcessEnv): string;
|
package/dist/lib/paths.js
CHANGED
|
@@ -20,9 +20,6 @@ export function ensureAskTheWDataDir(env = process.env) {
|
|
|
20
20
|
export function localStorePath(env = process.env) {
|
|
21
21
|
return path.join(askTheWDataDir(env), "store.sqlite");
|
|
22
22
|
}
|
|
23
|
-
export function credentialsPath(env = process.env) {
|
|
24
|
-
return path.join(askTheWDataDir(env), "credentials.json");
|
|
25
|
-
}
|
|
26
23
|
export function identityPath(env = process.env) {
|
|
27
24
|
return path.join(askTheWDataDir(env), "identity.json");
|
|
28
25
|
}
|
package/dist/lib/telemetry.js
CHANGED
|
@@ -31,13 +31,11 @@ export function buildTelemetryPayload(input) {
|
|
|
31
31
|
platform: `${process.platform}-${process.arch}`,
|
|
32
32
|
node: process.version.replace(/^v/, ""),
|
|
33
33
|
},
|
|
34
|
-
identity:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
: { kind: "legacy_token" },
|
|
34
|
+
identity: {
|
|
35
|
+
kind: "local_install",
|
|
36
|
+
installId: input.credentials.installId,
|
|
37
|
+
emailClaimed: Boolean(input.credentials.email),
|
|
38
|
+
},
|
|
41
39
|
});
|
|
42
40
|
}
|
|
43
41
|
export async function flushTelemetryOutbox(input) {
|
|
@@ -46,7 +44,7 @@ export async function flushTelemetryOutbox(input) {
|
|
|
46
44
|
}
|
|
47
45
|
const fetcher = input.fetchImpl ?? fetch;
|
|
48
46
|
const apiUrl = (input.apiUrl ?? input.credentials.apiUrl ?? process.env.ASKTHEW_API_URL ?? "https://app.askthew.com").replace(/\/$/, "");
|
|
49
|
-
if (input.credentials.
|
|
47
|
+
if (input.credentials.localIdentity) {
|
|
50
48
|
await tryRegisterFreeInstall({
|
|
51
49
|
identity: input.credentials.localIdentity,
|
|
52
50
|
deviceLabel: "askthew-mcp",
|
|
@@ -56,20 +54,18 @@ export async function flushTelemetryOutbox(input) {
|
|
|
56
54
|
let sent = 0;
|
|
57
55
|
for (const row of input.store.listTelemetryOutbox({ undeliveredOnly: true, limit: 20 })) {
|
|
58
56
|
const body = JSON.stringify(row.payload);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
if (!input.credentials.localIdentity) {
|
|
58
|
+
input.store.markTelemetryAttempt(row.id, false);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const signed = signLocalIdentityPayload({ identity: input.credentials.localIdentity, body });
|
|
62
62
|
const response = await fetcher(`${apiUrl}/api/cli/v1/telemetry`, {
|
|
63
63
|
method: "POST",
|
|
64
64
|
headers: {
|
|
65
65
|
"Content-Type": "application/json",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"X-AskTheW-Timestamp": signed.timestamp,
|
|
70
|
-
"X-AskTheW-Signature": signed.signature,
|
|
71
|
-
}
|
|
72
|
-
: { Authorization: `Bearer ${input.credentials.cliToken}` }),
|
|
66
|
+
"X-AskTheW-Install-Id": input.credentials.localIdentity.installId,
|
|
67
|
+
"X-AskTheW-Timestamp": signed.timestamp,
|
|
68
|
+
"X-AskTheW-Signature": signed.signature,
|
|
73
69
|
},
|
|
74
70
|
body,
|
|
75
71
|
}).catch(() => null);
|
|
@@ -20,7 +20,7 @@ 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
25
|
cta: "Run: npx @askthew/mcp-plugin upgrade",
|
|
26
26
|
};
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { clearPendingAuth, pendingAuth, pendingAuthForEmail, savePendingAuth } from "./lib/auth-pending.js";
|
|
7
|
-
import { configPath } from "./lib/paths.js";
|
|
8
|
-
function withTempEnv(fn) {
|
|
9
|
-
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-auth-pending-"));
|
|
10
|
-
try {
|
|
11
|
-
return fn({ ASKTHEW_DATA_DIR: dataDir });
|
|
12
|
-
}
|
|
13
|
-
finally {
|
|
14
|
-
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
test("pending auth stores and resolves the request id for the matching email", () => {
|
|
18
|
-
withTempEnv((env) => {
|
|
19
|
-
savePendingAuth({
|
|
20
|
-
email: "Founder@Example.com",
|
|
21
|
-
requestId: "request_1",
|
|
22
|
-
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
23
|
-
telemetryOptOut: true,
|
|
24
|
-
}, env);
|
|
25
|
-
const pending = pendingAuthForEmail("founder@example.com", env);
|
|
26
|
-
assert.equal(pending?.requestId, "request_1");
|
|
27
|
-
assert.equal(pending?.telemetryOptOut, true);
|
|
28
|
-
assert.equal(pendingAuth(env)?.email, "Founder@Example.com");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
test("pending auth ignores other emails and clears expired requests", () => {
|
|
32
|
-
withTempEnv((env) => {
|
|
33
|
-
savePendingAuth({
|
|
34
|
-
email: "founder@example.com",
|
|
35
|
-
requestId: "request_1",
|
|
36
|
-
expiresAt: new Date(Date.now() - 1_000).toISOString(),
|
|
37
|
-
}, env);
|
|
38
|
-
assert.equal(pendingAuthForEmail("other@example.com", env), null);
|
|
39
|
-
assert.equal(pendingAuthForEmail("founder@example.com", env), null);
|
|
40
|
-
const config = JSON.parse(fs.readFileSync(configPath(env), "utf8"));
|
|
41
|
-
assert.equal("pendingAuth" in config, false);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
test("pending auth clear keeps the config file private and removes only pending auth", () => {
|
|
45
|
-
withTempEnv((env) => {
|
|
46
|
-
savePendingAuth({
|
|
47
|
-
email: "founder@example.com",
|
|
48
|
-
requestId: "request_1",
|
|
49
|
-
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
50
|
-
}, env);
|
|
51
|
-
clearPendingAuth(env);
|
|
52
|
-
const config = JSON.parse(fs.readFileSync(configPath(env), "utf8"));
|
|
53
|
-
assert.equal("pendingAuth" in config, false);
|
|
54
|
-
assert.equal((fs.statSync(configPath(env)).mode & 0o777), 0o600);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { CliCredentials } from "./free-tier-policy.js";
|
|
2
|
-
export interface MagicLinkClientOptions {
|
|
3
|
-
apiUrl?: string;
|
|
4
|
-
fetchImpl?: typeof fetch;
|
|
5
|
-
}
|
|
6
|
-
export declare function requestMagicLinkCode(input: {
|
|
7
|
-
email: string;
|
|
8
|
-
deviceLabel?: string;
|
|
9
|
-
apiUrl?: string;
|
|
10
|
-
fetchImpl?: typeof fetch;
|
|
11
|
-
}): Promise<{
|
|
12
|
-
requestId: string;
|
|
13
|
-
expiresAt: string;
|
|
14
|
-
devCode?: string;
|
|
15
|
-
}>;
|
|
16
|
-
export declare function verifyMagicLinkCode(input: {
|
|
17
|
-
requestId: string;
|
|
18
|
-
code: string;
|
|
19
|
-
apiUrl?: string;
|
|
20
|
-
fetchImpl?: typeof fetch;
|
|
21
|
-
telemetryOptOut?: boolean;
|
|
22
|
-
}): Promise<CliCredentials>;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { credentialsPath, writePrivateJson } from "./paths.js";
|
|
2
|
-
function baseUrl(apiUrl) {
|
|
3
|
-
return (apiUrl?.trim() || process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com").replace(/\/$/, "");
|
|
4
|
-
}
|
|
5
|
-
async function requestJson(route, body, options) {
|
|
6
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
7
|
-
const response = await fetcher(`${baseUrl(options.apiUrl)}${route}`, {
|
|
8
|
-
method: "POST",
|
|
9
|
-
headers: { "Content-Type": "application/json" },
|
|
10
|
-
body: JSON.stringify(body),
|
|
11
|
-
});
|
|
12
|
-
const payload = await response.json().catch(() => null);
|
|
13
|
-
if (!response.ok) {
|
|
14
|
-
const message = payload && typeof payload === "object" && "error" in payload
|
|
15
|
-
? String(payload.error)
|
|
16
|
-
: "Ask The W auth request failed.";
|
|
17
|
-
throw new Error(message);
|
|
18
|
-
}
|
|
19
|
-
return payload;
|
|
20
|
-
}
|
|
21
|
-
export async function requestMagicLinkCode(input) {
|
|
22
|
-
return requestJson("/api/cli/v1/magic-link/request", {
|
|
23
|
-
email: input.email,
|
|
24
|
-
deviceLabel: input.deviceLabel,
|
|
25
|
-
}, input);
|
|
26
|
-
}
|
|
27
|
-
export async function verifyMagicLinkCode(input) {
|
|
28
|
-
const verified = await requestJson("/api/cli/v1/magic-link/verify", {
|
|
29
|
-
requestId: input.requestId,
|
|
30
|
-
code: input.code,
|
|
31
|
-
}, input);
|
|
32
|
-
const credentials = {
|
|
33
|
-
email: verified.email,
|
|
34
|
-
userId: verified.userId,
|
|
35
|
-
cliToken: verified.cliToken,
|
|
36
|
-
cliTokenId: verified.cliTokenId,
|
|
37
|
-
accountStatus: verified.accountStatus,
|
|
38
|
-
apiUrl: input.apiUrl,
|
|
39
|
-
telemetryOptOut: input.telemetryOptOut,
|
|
40
|
-
};
|
|
41
|
-
writePrivateJson(credentialsPath(), credentials);
|
|
42
|
-
return credentials;
|
|
43
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
type CliConfig = {
|
|
2
|
-
pendingAuth?: {
|
|
3
|
-
email: string;
|
|
4
|
-
requestId: string;
|
|
5
|
-
expiresAt: string;
|
|
6
|
-
telemetryOptOut?: boolean;
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
export declare function savePendingAuth(input: NonNullable<CliConfig["pendingAuth"]>, env?: NodeJS.ProcessEnv): void;
|
|
10
|
-
export declare function clearPendingAuth(env?: NodeJS.ProcessEnv): void;
|
|
11
|
-
export declare function pendingAuthForEmail(email: string, env?: NodeJS.ProcessEnv): {
|
|
12
|
-
email: string;
|
|
13
|
-
requestId: string;
|
|
14
|
-
expiresAt: string;
|
|
15
|
-
telemetryOptOut?: boolean;
|
|
16
|
-
} | null;
|
|
17
|
-
export declare function pendingAuth(env?: NodeJS.ProcessEnv): {
|
|
18
|
-
email: string;
|
|
19
|
-
requestId: string;
|
|
20
|
-
expiresAt: string;
|
|
21
|
-
telemetryOptOut?: boolean;
|
|
22
|
-
} | null;
|
|
23
|
-
export {};
|
package/dist/lib/auth-pending.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { configPath, readJsonFile, writePrivateJson } from "./paths.js";
|
|
2
|
-
function loadCliConfig(env = process.env) {
|
|
3
|
-
return readJsonFile(configPath(env)) ?? {};
|
|
4
|
-
}
|
|
5
|
-
export function savePendingAuth(input, env = process.env) {
|
|
6
|
-
const config = loadCliConfig(env);
|
|
7
|
-
writePrivateJson(configPath(env), {
|
|
8
|
-
...config,
|
|
9
|
-
pendingAuth: input,
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
export function clearPendingAuth(env = process.env) {
|
|
13
|
-
const config = loadCliConfig(env);
|
|
14
|
-
if (!config.pendingAuth)
|
|
15
|
-
return;
|
|
16
|
-
const { pendingAuth: _pendingAuth, ...next } = config;
|
|
17
|
-
writePrivateJson(configPath(env), next);
|
|
18
|
-
}
|
|
19
|
-
export function pendingAuthForEmail(email, env = process.env) {
|
|
20
|
-
const pending = pendingAuth(env);
|
|
21
|
-
if (!pending || pending.email.toLowerCase() !== email.toLowerCase()) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
return pending;
|
|
25
|
-
}
|
|
26
|
-
export function pendingAuth(env = process.env) {
|
|
27
|
-
const pending = loadCliConfig(env).pendingAuth;
|
|
28
|
-
if (!pending) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
if (Number.isFinite(Date.parse(pending.expiresAt)) && Date.parse(pending.expiresAt) <= Date.now()) {
|
|
32
|
-
clearPendingAuth(env);
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return pending;
|
|
36
|
-
}
|