@askthew/mcp-plugin 0.4.0 → 0.4.2

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.
Files changed (45) hide show
  1. package/README.md +24 -13
  2. package/dist/auth-pending.test.d.ts +1 -0
  3. package/dist/auth-pending.test.js +56 -0
  4. package/dist/cli-actions.test.d.ts +1 -0
  5. package/dist/cli-actions.test.js +71 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.js +293 -37
  8. package/dist/cli.test.d.ts +1 -0
  9. package/dist/cli.test.js +274 -0
  10. package/dist/free-tier-policy.test.d.ts +1 -0
  11. package/dist/free-tier-policy.test.js +57 -0
  12. package/dist/index.d.ts +47 -13
  13. package/dist/index.js +1103 -106
  14. package/dist/index.test.js +609 -6
  15. package/dist/install.d.ts +40 -0
  16. package/dist/install.js +155 -18
  17. package/dist/install.test.js +62 -2
  18. package/dist/lib/auth-pending.d.ts +23 -0
  19. package/dist/lib/auth-pending.js +36 -0
  20. package/dist/lib/cli-actions.d.ts +28 -0
  21. package/dist/lib/cli-actions.js +104 -0
  22. package/dist/lib/free-install-registration.d.ts +27 -0
  23. package/dist/lib/free-install-registration.js +52 -0
  24. package/dist/lib/free-tier-policy.d.ts +5 -1
  25. package/dist/lib/free-tier-policy.js +16 -1
  26. package/dist/lib/local-identity.d.ts +44 -0
  27. package/dist/lib/local-identity.js +81 -0
  28. package/dist/lib/local-store.d.ts +33 -2
  29. package/dist/lib/local-store.js +191 -19
  30. package/dist/lib/paths.d.ts +2 -0
  31. package/dist/lib/paths.js +6 -0
  32. package/dist/lib/telemetry.js +28 -2
  33. package/dist/lib/timeline-insights.d.ts +23 -0
  34. package/dist/lib/timeline-insights.js +115 -0
  35. package/dist/lib/upgrade-nudge.d.ts +1 -1
  36. package/dist/lib/upgrade-nudge.js +8 -1
  37. package/dist/local-identity.test.d.ts +1 -0
  38. package/dist/local-identity.test.js +29 -0
  39. package/dist/local-store.test.js +34 -0
  40. package/dist/scope.d.ts +1 -1
  41. package/dist/scope.js +56 -2
  42. package/dist/scope.test.js +17 -0
  43. package/dist/timeline-insights.test.d.ts +1 -0
  44. package/dist/timeline-insights.test.js +85 -0
  45. package/package.json +2 -2
@@ -0,0 +1,274 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { runAuthCommand } from "./cli.js";
9
+ import { configPath, credentialsPath, identityPath, writePrivateJson } from "./lib/paths.js";
10
+ const cliPath = fileURLToPath(new URL("./cli.js", import.meta.url));
11
+ function makeFixture() {
12
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-cli-install-"));
13
+ const home = path.join(root, "home");
14
+ const dataDir = path.join(root, "data");
15
+ const project = path.join(root, "project");
16
+ fs.mkdirSync(home, { recursive: true });
17
+ fs.mkdirSync(dataDir, { recursive: true });
18
+ fs.mkdirSync(project, { recursive: true });
19
+ fs.writeFileSync(path.join(project, "package.json"), "{}", "utf8");
20
+ return { root, home, dataDir, project };
21
+ }
22
+ function runCli(input) {
23
+ return spawnSync(process.execPath, [cliPath, ...input.args], {
24
+ cwd: input.cwd,
25
+ encoding: "utf8",
26
+ env: {
27
+ ...process.env,
28
+ HOME: input.home,
29
+ USERPROFILE: input.home,
30
+ ASKTHEW_DATA_DIR: input.dataDir,
31
+ ASKTHEW_EMAIL: "founder@example.com",
32
+ ASKTHEW_CLI_TOKEN: "",
33
+ ASKTHEW_CLI_TOKEN_ID: "",
34
+ ASKTHEW_USER_ID: "",
35
+ ASKTHEW_INSTALL_TOKEN: "",
36
+ ASKTHEW_FREE_MODE: "",
37
+ ...(input.extraEnv ?? {}),
38
+ },
39
+ });
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
+ async function withCliEnv(dataDir, fn) {
50
+ const previous = {
51
+ ASKTHEW_DATA_DIR: process.env.ASKTHEW_DATA_DIR,
52
+ ASKTHEW_EMAIL: process.env.ASKTHEW_EMAIL,
53
+ ASKTHEW_CLI_TOKEN: process.env.ASKTHEW_CLI_TOKEN,
54
+ ASKTHEW_CLI_TOKEN_ID: process.env.ASKTHEW_CLI_TOKEN_ID,
55
+ ASKTHEW_USER_ID: process.env.ASKTHEW_USER_ID,
56
+ ASKTHEW_INSTALL_TOKEN: process.env.ASKTHEW_INSTALL_TOKEN,
57
+ ASKTHEW_FREE_MODE: process.env.ASKTHEW_FREE_MODE,
58
+ };
59
+ process.env.ASKTHEW_DATA_DIR = dataDir;
60
+ process.env.ASKTHEW_EMAIL = "founder@example.com";
61
+ delete process.env.ASKTHEW_CLI_TOKEN;
62
+ delete process.env.ASKTHEW_CLI_TOKEN_ID;
63
+ delete process.env.ASKTHEW_USER_ID;
64
+ delete process.env.ASKTHEW_INSTALL_TOKEN;
65
+ delete process.env.ASKTHEW_FREE_MODE;
66
+ try {
67
+ return await fn();
68
+ }
69
+ finally {
70
+ for (const [key, value] of Object.entries(previous)) {
71
+ if (value === undefined) {
72
+ delete process.env[key];
73
+ }
74
+ else {
75
+ process.env[key] = value;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ test("free install without prior auth writes config, instructions, and local identity", () => {
81
+ const fixture = makeFixture();
82
+ try {
83
+ const result = runCli({
84
+ args: ["install", "--host", "claude_code", "--free", "--email", "founder@example.com", "--api-url", "http://127.0.0.1:9"],
85
+ cwd: fixture.project,
86
+ home: fixture.home,
87
+ dataDir: fixture.dataDir,
88
+ });
89
+ assert.equal(result.status, 0, result.stderr);
90
+ assert.match(result.stdout, /Free local mode installed/);
91
+ assert.match(result.stdout, /Free install identity saved locally|Free install identity registered/);
92
+ assert.equal(fs.existsSync(path.join(fixture.home, ".claude.json")), true);
93
+ assert.match(fs.readFileSync(path.join(fixture.project, "CLAUDE.md"), "utf8"), /capture_session_signal/);
94
+ const identity = JSON.parse(fs.readFileSync(path.join(fixture.dataDir, "identity.json"), "utf8"));
95
+ assert.match(identity.installId, /^[0-9a-f-]{36}$/);
96
+ assert.equal(identity.emailClaim, "founder@example.com");
97
+ assert.equal(typeof identity.privateKey, "string");
98
+ assert.equal(typeof identity.publicKey, "string");
99
+ }
100
+ finally {
101
+ fs.rmSync(fixture.root, { recursive: true, force: true });
102
+ }
103
+ });
104
+ test("free install dry-run stays non-mutating and does not create identity", () => {
105
+ const fixture = makeFixture();
106
+ try {
107
+ const result = runCli({
108
+ args: ["install", "--host", "codex", "--free", "--email", "founder@example.com", "--dry-run"],
109
+ cwd: fixture.project,
110
+ home: fixture.home,
111
+ dataDir: fixture.dataDir,
112
+ });
113
+ assert.equal(result.status, 0, result.stderr);
114
+ assert.equal(fs.existsSync(path.join(fixture.home, ".codex", "config.toml")), false);
115
+ assert.equal(fs.existsSync(path.join(fixture.project, "CLAUDE.md")), false);
116
+ assert.equal(fs.existsSync(path.join(fixture.project, "AGENTS.md")), false);
117
+ assert.equal(fs.existsSync(path.join(fixture.dataDir, "identity.json")), false);
118
+ }
119
+ finally {
120
+ fs.rmSync(fixture.root, { recursive: true, force: true });
121
+ }
122
+ });
123
+ test("free install with auth writes host config and agent instructions", () => {
124
+ const fixture = makeFixture();
125
+ try {
126
+ writeCredentials(fixture.dataDir);
127
+ const result = runCli({
128
+ args: ["install", "--host", "claude_code", "--free", "--api-url", "http://127.0.0.1:9"],
129
+ cwd: fixture.project,
130
+ home: fixture.home,
131
+ dataDir: fixture.dataDir,
132
+ });
133
+ assert.equal(result.status, 0, result.stderr);
134
+ assert.match(result.stdout, /Free local mode installed/);
135
+ const settings = JSON.parse(fs.readFileSync(path.join(fixture.home, ".claude.json"), "utf8"));
136
+ const projectEntries = Object.values(settings.projects);
137
+ assert.equal(projectEntries.length, 1);
138
+ const server = projectEntries[0].mcpServers.askthew;
139
+ assert.equal(server.env.ASKTHEW_FREE_MODE, "1");
140
+ assert.equal("ASKTHEW_CLI_TOKEN" in server.env, false);
141
+ assert.equal("ASKTHEW_INSTALL_TOKEN" in server.env, false);
142
+ assert.match(fs.readFileSync(path.join(fixture.project, "CLAUDE.md"), "utf8"), /capture_session_signal/);
143
+ }
144
+ finally {
145
+ fs.rmSync(fixture.root, { recursive: true, force: true });
146
+ }
147
+ });
148
+ test("auth status reports a pending verification code", () => {
149
+ const fixture = makeFixture();
150
+ 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
+ const result = runCli({
159
+ args: ["auth", "status"],
160
+ cwd: fixture.project,
161
+ home: fixture.home,
162
+ dataDir: fixture.dataDir,
163
+ });
164
+ 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/);
167
+ }
168
+ finally {
169
+ fs.rmSync(fixture.root, { recursive: true, force: true });
170
+ }
171
+ });
172
+ test("auth code commands require pending state and do not issue a new code", () => {
173
+ const fixture = makeFixture();
174
+ 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
+ }
190
+ }
191
+ finally {
192
+ fs.rmSync(fixture.root, { recursive: true, force: true });
193
+ }
194
+ });
195
+ test("auth login now identifies the local free install without requesting an email code", async () => {
196
+ const fixture = makeFixture();
197
+ const calls = [];
198
+ const logs = [];
199
+ try {
200
+ await withCliEnv(fixture.dataDir, async () => {
201
+ await runAuthCommand(["login", "--email", "ymtest89+test5@gmail.com"], {
202
+ 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
+ registerFreeInstall: async ({ identity }) => {
212
+ calls.push({ type: "register", installId: identity.installId, emailClaim: identity.emailClaim });
213
+ return { ok: true, registeredAt: new Date().toISOString() };
214
+ },
215
+ });
216
+ });
217
+ assert.equal(calls.length, 1);
218
+ assert.equal(calls[0].type, "register");
219
+ assert.equal(calls[0].emailClaim, "ymtest89+test5@gmail.com");
220
+ assert.equal(fs.existsSync(identityPath({ ASKTHEW_DATA_DIR: fixture.dataDir })), true);
221
+ assert.equal(fs.existsSync(credentialsPath({ ASKTHEW_DATA_DIR: fixture.dataDir })), false);
222
+ assert.match(logs.join("\n"), /No email code is required/);
223
+ }
224
+ finally {
225
+ fs.rmSync(fixture.root, { recursive: true, force: true });
226
+ }
227
+ });
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
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
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 { resolveMcpMode } from "./lib/free-tier-policy.js";
7
+ function withTempDataDir(fn) {
8
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "askthew-mode-"));
9
+ try {
10
+ return fn({ ASKTHEW_DATA_DIR: dataDir }, dataDir);
11
+ }
12
+ finally {
13
+ fs.rmSync(dataDir, { recursive: true, force: true });
14
+ }
15
+ }
16
+ test("mode resolution prefers paid install tokens", () => {
17
+ const mode = resolveMcpMode({
18
+ ASKTHEW_INSTALL_TOKEN: "atw_token",
19
+ ASKTHEW_FREE_MODE: "1",
20
+ });
21
+ assert.equal(mode.mode, "paid");
22
+ assert.equal(mode.reason, "workspace_install_token");
23
+ });
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",
29
+ });
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
+ test("mode resolution distinguishes pending free auth from no identity", () => {
35
+ withTempDataDir((env) => {
36
+ const pending = resolveMcpMode({
37
+ ...env,
38
+ ASKTHEW_FREE_MODE: "1",
39
+ });
40
+ const none = resolveMcpMode(env);
41
+ assert.equal(pending.mode, "free_pending_auth");
42
+ assert.equal(pending.reason, "free_mode_no_credentials");
43
+ assert.equal(none.mode, "unauthenticated");
44
+ assert.equal(none.reason, "no_identity");
45
+ });
46
+ });
47
+ test("mode resolution marks malformed free credentials as pending auth", () => {
48
+ withTempDataDir((env, dataDir) => {
49
+ fs.writeFileSync(path.join(dataDir, "credentials.json"), "{\"not\":\"credentials\"}\n", "utf8");
50
+ const mode = resolveMcpMode({
51
+ ...env,
52
+ ASKTHEW_FREE_MODE: "1",
53
+ });
54
+ assert.equal(mode.mode, "free_pending_auth");
55
+ assert.equal(mode.reason, "invalid_cli_credentials");
56
+ });
57
+ });
package/dist/index.d.ts CHANGED
@@ -8,36 +8,56 @@ export declare const codingSessionSignalSchema: z.ZodObject<{
8
8
  evidence: z.ZodDefault<z.ZodArray<z.ZodObject<{
9
9
  role: z.ZodEnum<["user", "assistant", "system"]>;
10
10
  excerpt: z.ZodString;
11
+ kind: z.ZodOptional<z.ZodEnum<["excerpt", "diff", "prompt_diff"]>>;
12
+ diff: z.ZodOptional<z.ZodString>;
13
+ before: z.ZodOptional<z.ZodString>;
14
+ after: z.ZodOptional<z.ZodString>;
11
15
  }, "strip", z.ZodTypeAny, {
12
16
  role: "user" | "assistant" | "system";
13
17
  excerpt: string;
18
+ diff?: string | undefined;
19
+ kind?: "diff" | "excerpt" | "prompt_diff" | undefined;
20
+ before?: string | undefined;
21
+ after?: string | undefined;
14
22
  }, {
15
23
  role: "user" | "assistant" | "system";
16
24
  excerpt: string;
25
+ diff?: string | undefined;
26
+ kind?: "diff" | "excerpt" | "prompt_diff" | undefined;
27
+ before?: string | undefined;
28
+ after?: string | undefined;
17
29
  }>, "many">>;
18
30
  filesTouched: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
19
31
  commandsRun: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
20
32
  metadata: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
21
33
  }, "strip", z.ZodTypeAny, {
22
34
  sessionId: string;
23
- sequence: number;
24
35
  kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
36
+ sequence: number;
25
37
  summary: string;
26
38
  evidence: {
27
39
  role: "user" | "assistant" | "system";
28
40
  excerpt: string;
41
+ diff?: string | undefined;
42
+ kind?: "diff" | "excerpt" | "prompt_diff" | undefined;
43
+ before?: string | undefined;
44
+ after?: string | undefined;
29
45
  }[];
30
46
  filesTouched: string[];
31
47
  commandsRun: string[];
32
48
  metadata: Record<string, unknown>;
33
49
  }, {
34
50
  sessionId: string;
35
- sequence: number;
36
51
  kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
52
+ sequence: number;
37
53
  summary: string;
38
54
  evidence?: {
39
55
  role: "user" | "assistant" | "system";
40
56
  excerpt: string;
57
+ diff?: string | undefined;
58
+ kind?: "diff" | "excerpt" | "prompt_diff" | undefined;
59
+ before?: string | undefined;
60
+ after?: string | undefined;
41
61
  }[] | undefined;
42
62
  filesTouched?: string[] | undefined;
43
63
  commandsRun?: string[] | undefined;
@@ -82,31 +102,44 @@ export declare const provenanceSignalSchema: z.ZodObject<{
82
102
  installToken?: string | undefined;
83
103
  }>;
84
104
  export type ProvenanceSignal = z.infer<typeof provenanceSignalSchema>;
105
+ type AskTheWConfig = {
106
+ redaction?: {
107
+ enabled?: boolean;
108
+ };
109
+ digest?: {
110
+ footer?: boolean;
111
+ };
112
+ };
113
+ export declare function loadAskTheWConfig(env?: NodeJS.ProcessEnv): AskTheWConfig;
85
114
  export declare function redactProvenanceSignal(input: ProvenanceSignal): {
86
- decision: string;
87
- rationale: string;
88
- framework: string | undefined;
89
- filesAffected: string[];
90
- originatingPrompt: string | undefined;
91
- installToken: string | undefined;
92
- metadata: Record<string, unknown>;
93
115
  sessionId: string;
116
+ metadata: Record<string, unknown>;
94
117
  source: string;
118
+ decision: string;
119
+ rationale: string;
95
120
  confidence: number;
121
+ filesAffected: string[];
122
+ framework?: string | undefined;
96
123
  pendingApproval?: boolean | undefined;
124
+ originatingPrompt?: string | undefined;
125
+ installToken?: string | undefined;
97
126
  };
98
127
  export declare function redactCodingSessionSignal(input: CodingSessionSignal): {
128
+ sessionId: string;
129
+ kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
130
+ sequence: number;
99
131
  summary: string;
100
132
  evidence: {
101
- excerpt: string;
102
133
  role: "user" | "assistant" | "system";
134
+ excerpt: string;
135
+ diff?: string | undefined;
136
+ kind?: "diff" | "excerpt" | "prompt_diff" | undefined;
137
+ before?: string | undefined;
138
+ after?: string | undefined;
103
139
  }[];
104
140
  filesTouched: string[];
105
141
  commandsRun: string[];
106
142
  metadata: Record<string, unknown>;
107
- sessionId: string;
108
- sequence: number;
109
- kind: "setup_complete" | "session_checkpoint" | "direction_change" | "implementation_update" | "verification_result" | "final_summary";
110
143
  };
111
144
  export interface AskTheWMcpServerOptions {
112
145
  credentials?: {
@@ -125,3 +158,4 @@ export interface AskTheWMcpServerOptions {
125
158
  }
126
159
  export declare function normalizeInstallTokenInput(token: string | undefined): string;
127
160
  export declare function createAskTheWMcpServer(options?: AskTheWMcpServerOptions): McpServer;
161
+ export {};