@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 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 { clearPendingAuth, pendingAuth, pendingAuthForEmail } from "./lib/auth-pending.js";
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/mcp to upgrade, then run `askthew-mcp upgrade --finalize`.");
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 = credentialsPath();
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
- : credentials
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 = credentialsPath();
457
+ const file = identityPath();
382
458
  if (fs.existsSync(file))
383
459
  fs.rmSync(file);
384
- log("Logged out of Ask The W local free tier.");
460
+ log("Removed Ask The W local free install identity.");
385
461
  return;
386
462
  }
387
- if (subcommand !== "login" && subcommand !== "verify") {
388
- throw new Error("Usage: askthew-mcp auth login --email <email> [--no-telemetry] | askthew-mcp auth verify --code <code> [--email <email>]");
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
- if (credentials.identityKind === "local_install" && credentials.localIdentity) {
468
- ensureLocalIdentity({ telemetryOptOut: subcommand === "opt-out" });
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
- console.log(JSON.stringify(await uploadLocalStore({ store, credentials }), null, 2));
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 { configPath, credentialsPath, identityPath, writePrivateJson } from "./lib/paths.js";
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 with auth writes host config and agent instructions", () => {
115
+ test("free install ignores stale legacy credentials and writes local identity", () => {
124
116
  const fixture = makeFixture();
125
117
  try {
126
- writeCredentials(fixture.dataDir);
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 a pending verification code", () => {
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, /Pending code for founder@example\.com/);
166
- assert.match(result.stdout, /askthew-mcp auth verify --code/);
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 require pending state and do not issue a new code", () => {
209
+ test("removed auth code commands do not issue or verify email codes", () => {
173
210
  const fixture = makeFixture();
174
211
  try {
175
- for (const args of [
176
- ["auth", "verify", "--code", "123456"],
177
- ["auth", "login", "--email", "founder@example.com", "--code", "123456"],
178
- ]) {
179
- const result = runCli({
180
- args,
181
- cwd: fixture.project,
182
- home: fixture.home,
183
- dataDir: fixture.dataDir,
184
- extraEnv: { ASKTHEW_API_URL: "http://127.0.0.1:9" },
185
- });
186
- assert.equal(result.status, 1);
187
- assert.match(result.stderr, /No pending Ask The W login request/);
188
- assert.doesNotMatch(result.stdout, /Code sent/);
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(credentialsPath({ ASKTHEW_DATA_DIR: fixture.dataDir })), false);
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 authenticated free credentials", () => {
25
- const mode = resolveMcpMode({
26
- ASKTHEW_CLI_TOKEN: "cli_token",
27
- ASKTHEW_USER_ID: "user_1",
28
- ASKTHEW_CLI_TOKEN_ID: "cli_token_1",
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, "free_mode_no_credentials");
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 marks malformed free credentials as pending auth", () => {
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, "invalid_cli_credentials");
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/mcp?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=review_session",
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
  });
@@ -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 { credentialsPath, writePrivateJson } from "./lib/paths.js";
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, 0);
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, 0);
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\/mcp/);
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\/mcp/);
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\/mcp/, toolName);
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
- accountStatus?: "new_dormant" | "existing_active";
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
- const creds = readJsonFile(credentialsPath(env));
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: "cli_free_tier_credentials",
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: fs.existsSync(credentialsPath(env)) ? "invalid_cli_credentials" : "free_mode_no_credentials",
42
+ reason: "free_mode_no_identity",
59
43
  };
60
44
  }
61
45
  return {
@@ -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
  }
@@ -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: input.credentials.identityKind === "local_install"
35
- ? {
36
- kind: "local_install",
37
- installId: input.credentials.installId,
38
- emailClaimed: Boolean(input.credentials.email),
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.identityKind === "local_install" && input.credentials.localIdentity) {
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
- const signed = input.credentials.identityKind === "local_install" && input.credentials.localIdentity
60
- ? signLocalIdentityPayload({ identity: input.credentials.localIdentity, body })
61
- : null;
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
- ...(signed
67
- ? {
68
- "X-AskTheW-Install-Id": input.credentials.localIdentity.installId,
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/mcp?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=${encodeURIComponent(tool)}`,
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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",
@@ -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 {};
@@ -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
- }