@askthew/mcp-plugin 0.4.3 → 0.4.4

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/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";
@@ -28,13 +26,12 @@ function usage() {
28
26
  " askthew-mcp identify --email <email> [--no-telemetry]",
29
27
  " askthew-mcp identity status",
30
28
  " askthew-mcp auth login --email <email> [--no-telemetry]",
31
- " askthew-mcp auth verify --code <code> [--email <email>]",
32
29
  " askthew-mcp auth logout | status",
33
30
  " askthew-mcp telemetry status | opt-out | opt-in | preview",
34
31
  " askthew-mcp local stats | reset --hard",
35
32
  " askthew-mcp install-hook --pre-commit",
36
33
  " askthew-mcp digest --weekly",
37
- " askthew-mcp sync upload [--dry-run]",
34
+ " askthew-mcp sync upload [--token <workspace-install-token>] [--dry-run]",
38
35
  " 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
36
  ].join("\n");
40
37
  }
@@ -340,7 +337,7 @@ async function runUninstallCommand(argv) {
340
337
  fs.rmSync(ensureAskTheWDataDir(), { recursive: true, force: true });
341
338
  }
342
339
  if (!keepAuth && !dryRun) {
343
- const file = credentialsPath();
340
+ const file = identityPath();
344
341
  if (fs.existsSync(file))
345
342
  fs.rmSync(file, { force: true });
346
343
  }
@@ -362,55 +359,27 @@ function argValue(argv, name) {
362
359
  }
363
360
  export async function runAuthCommand(argv, deps = {}) {
364
361
  const log = deps.log ?? console.log;
365
- const verifyCode = deps.verifyMagicLinkCode ?? verifyMagicLinkCodeDefault;
366
362
  const registerInstall = deps.registerFreeInstall ?? tryRegisterFreeInstall;
367
363
  const [subcommand] = argv;
368
364
  if (subcommand === "status") {
369
365
  const identity = loadLocalIdentity();
370
- const credentials = loadCliCredentials();
371
366
  log(identity
372
367
  ? `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>\`.`);
368
+ : `No local identity yet. Run \`askthew-mcp identify --email <your-email>\`, or install with \`--free --email <your-email>\`.`);
378
369
  return;
379
370
  }
380
371
  if (subcommand === "logout") {
381
- const file = credentialsPath();
372
+ const file = identityPath();
382
373
  if (fs.existsSync(file))
383
374
  fs.rmSync(file);
384
- log("Logged out of Ask The W local free tier.");
375
+ log("Removed Ask The W local free install identity.");
385
376
  return;
386
377
  }
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>]");
378
+ if (subcommand !== "login") {
379
+ throw new Error("Usage: askthew-mcp auth login --email <email> [--no-telemetry] | askthew-mcp auth logout | status");
389
380
  }
390
381
  ensureAskTheWDataDir();
391
382
  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
383
  if (!email)
415
384
  throw new Error("Missing --email.");
416
385
  const noTelemetry = argv.includes("--no-telemetry");
@@ -464,15 +433,8 @@ async function runTelemetryCommand(argv) {
464
433
  return;
465
434
  }
466
435
  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"}`);
436
+ ensureLocalIdentity({ telemetryOptOut: subcommand === "opt-out" });
437
+ console.log(`Telemetry: ${subcommand === "opt-out" ? "off" : "on"}`);
476
438
  return;
477
439
  }
478
440
  if (subcommand === "preview") {
@@ -525,7 +487,7 @@ async function runDigestCommand(argv) {
525
487
  }
526
488
  async function runSyncCommand(argv) {
527
489
  if (argv[0] !== "upload")
528
- throw new Error("Usage: askthew-mcp sync upload [--dry-run]");
490
+ throw new Error("Usage: askthew-mcp sync upload [--token <workspace-install-token>] [--dry-run]");
529
491
  const credentials = loadCliCredentials();
530
492
  if (!credentials)
531
493
  throw new Error("No local identity. Run `askthew-mcp identify --email <your-email>` first.");
@@ -534,7 +496,11 @@ async function runSyncCommand(argv) {
534
496
  console.log(JSON.stringify(syncDryRun(store), null, 2));
535
497
  return;
536
498
  }
537
- console.log(JSON.stringify(await uploadLocalStore({ store, credentials }), null, 2));
499
+ const syncToken = argValue(argv, "--token")?.trim() || process.env.ASKTHEW_INSTALL_TOKEN?.trim();
500
+ if (!syncToken) {
501
+ throw new Error("Missing workspace install token. Pass `--token <workspace-install-token>` after upgrading.");
502
+ }
503
+ console.log(JSON.stringify(await uploadLocalStore({ store, credentials, syncToken }), null, 2));
538
504
  }
539
505
  const isDirectCliExecution = Boolean(process.argv[1]) && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
540
506
  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,15 @@ 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);
143
136
  }
144
137
  finally {
145
138
  fs.rmSync(fixture.root, { recursive: true, force: true });
146
139
  }
147
140
  });
148
- test("auth status reports a pending verification code", () => {
141
+ test("auth status reports missing local identity without pending-code guidance", () => {
149
142
  const fixture = makeFixture();
150
143
  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
144
  const result = runCli({
159
145
  args: ["auth", "status"],
160
146
  cwd: fixture.project,
@@ -162,31 +148,35 @@ test("auth status reports a pending verification code", () => {
162
148
  dataDir: fixture.dataDir,
163
149
  });
164
150
  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/);
151
+ assert.match(result.stdout, /No local identity yet/);
152
+ assert.doesNotMatch(result.stdout, /verify --code|Pending code/);
167
153
  }
168
154
  finally {
169
155
  fs.rmSync(fixture.root, { recursive: true, force: true });
170
156
  }
171
157
  });
172
- test("auth code commands require pending state and do not issue a new code", () => {
158
+ test("removed auth code commands do not issue or verify email codes", () => {
173
159
  const fixture = makeFixture();
174
160
  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
- }
161
+ const verify = runCli({
162
+ args: ["auth", "verify", "--code", "123456"],
163
+ cwd: fixture.project,
164
+ home: fixture.home,
165
+ dataDir: fixture.dataDir,
166
+ extraEnv: { ASKTHEW_API_URL: "http://127.0.0.1:9" },
167
+ });
168
+ assert.equal(verify.status, 1);
169
+ assert.match(verify.stderr, /Usage: askthew-mcp auth login/);
170
+ const loginWithCode = runCli({
171
+ args: ["auth", "login", "--email", "founder@example.com", "--code", "123456"],
172
+ cwd: fixture.project,
173
+ home: fixture.home,
174
+ dataDir: fixture.dataDir,
175
+ extraEnv: { ASKTHEW_API_URL: "http://127.0.0.1:9" },
176
+ });
177
+ assert.equal(loginWithCode.status, 0, loginWithCode.stderr);
178
+ assert.match(loginWithCode.stdout, /No email code is required/);
179
+ assert.doesNotMatch(loginWithCode.stdout, /Code sent|Logged in/);
190
180
  }
191
181
  finally {
192
182
  fs.rmSync(fixture.root, { recursive: true, force: true });
@@ -200,14 +190,6 @@ test("auth login now identifies the local free install without requesting an ema
200
190
  await withCliEnv(fixture.dataDir, async () => {
201
191
  await runAuthCommand(["login", "--email", "ymtest89+test5@gmail.com"], {
202
192
  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
193
  registerFreeInstall: async ({ identity }) => {
212
194
  calls.push({ type: "register", installId: identity.installId, emailClaim: identity.emailClaim });
213
195
  return { ok: true, registeredAt: new Date().toISOString() };
@@ -218,57 +200,10 @@ test("auth login now identifies the local free install without requesting an ema
218
200
  assert.equal(calls[0].type, "register");
219
201
  assert.equal(calls[0].emailClaim, "ymtest89+test5@gmail.com");
220
202
  assert.equal(fs.existsSync(identityPath({ ASKTHEW_DATA_DIR: fixture.dataDir })), true);
221
- assert.equal(fs.existsSync(credentialsPath({ ASKTHEW_DATA_DIR: fixture.dataDir })), false);
203
+ assert.equal(fs.existsSync(path.join(fixture.dataDir, "credentials.json")), false);
222
204
  assert.match(logs.join("\n"), /No email code is required/);
223
205
  }
224
206
  finally {
225
207
  fs.rmSync(fixture.root, { recursive: true, force: true });
226
208
  }
227
209
  });
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
  });
@@ -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 });
@@ -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);
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.4",
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
- }