@askthew/mcp-plugin 0.4.10 → 0.4.12
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 +43 -179
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +255 -723
- package/dist/cloud-client.d.ts +80 -0
- package/dist/cloud-client.js +150 -0
- package/dist/index.d.ts +20 -123
- package/dist/index.js +155 -1353
- package/dist/install.d.ts +33 -119
- package/dist/install.js +140 -614
- package/dist/lib/paths.d.ts +2 -2
- package/dist/lib/paths.js +6 -6
- package/dist/outbox.d.ts +32 -0
- package/dist/outbox.js +101 -0
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.js +52 -0
- package/package.json +2 -2
- package/dist/lib/cli-actions.d.ts +0 -28
- package/dist/lib/cli-actions.js +0 -104
- package/dist/lib/free-install-registration.d.ts +0 -27
- package/dist/lib/free-install-registration.js +0 -52
- package/dist/lib/free-tier-policy.d.ts +0 -22
- package/dist/lib/free-tier-policy.js +0 -52
- package/dist/lib/local-identity.d.ts +0 -44
- package/dist/lib/local-identity.js +0 -81
- package/dist/lib/local-store.d.ts +0 -130
- package/dist/lib/local-store.js +0 -606
- package/dist/lib/loopback-auth.d.ts +0 -8
- package/dist/lib/loopback-auth.js +0 -30
- package/dist/lib/telemetry.d.ts +0 -25
- package/dist/lib/telemetry.js +0 -155
- package/dist/lib/timeline-insights.d.ts +0 -23
- package/dist/lib/timeline-insights.js +0 -115
- package/dist/lib/tip-engine.d.ts +0 -18
- package/dist/lib/tip-engine.js +0 -237
- package/dist/lib/upgrade-nudge.d.ts +0 -19
- package/dist/lib/upgrade-nudge.js +0 -37
- package/dist/lib/upgrade-sync.d.ts +0 -38
- package/dist/lib/upgrade-sync.js +0 -60
package/dist/index.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import fs from "node:fs";
|
|
5
3
|
import { createRequire } from "node:module";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
4
|
import { z } from "zod";
|
|
5
|
+
import { AskTheWCloudClient, completeSignup, startSignup } from "./cloud-client.js";
|
|
6
|
+
import { Outbox, sendOutboxPayload } from "./outbox.js";
|
|
7
|
+
import { redactSignalPayload } from "./redaction.js";
|
|
9
8
|
import { resolvePluginScope } from "./scope.js";
|
|
10
|
-
import { resolveMcpMode } from "./lib/free-tier-policy.js";
|
|
11
|
-
import { LocalStore } from "./lib/local-store.js";
|
|
12
|
-
import { buildTelemetryPayload, flushTelemetryOutbox } from "./lib/telemetry.js";
|
|
13
|
-
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
14
|
-
import { paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
|
|
15
|
-
import { configPath, readJsonFile } from "./lib/paths.js";
|
|
16
9
|
const requirePackageJson = createRequire(import.meta.url);
|
|
17
10
|
function packageVersion() {
|
|
18
11
|
try {
|
|
@@ -27,12 +20,8 @@ const evidenceRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
|
27
20
|
const evidenceEntrySchema = z.object({
|
|
28
21
|
role: evidenceRoleSchema,
|
|
29
22
|
excerpt: z.string().min(1).max(2000),
|
|
30
|
-
kind: z.enum(["excerpt", "diff", "prompt_diff"]).optional(),
|
|
31
|
-
diff: z.string().max(12000).optional(),
|
|
32
|
-
before: z.string().max(6000).optional(),
|
|
33
|
-
after: z.string().max(6000).optional(),
|
|
34
23
|
});
|
|
35
|
-
const
|
|
24
|
+
const signalKindSchema = z.enum([
|
|
36
25
|
"setup_complete",
|
|
37
26
|
"session_checkpoint",
|
|
38
27
|
"direction_change",
|
|
@@ -40,1394 +29,207 @@ const sessionSignalKindSchema = z.enum([
|
|
|
40
29
|
"verification_result",
|
|
41
30
|
"final_summary",
|
|
42
31
|
]);
|
|
32
|
+
const idempotencyKeySchema = z
|
|
33
|
+
.string()
|
|
34
|
+
.min(16)
|
|
35
|
+
.refine((value) => !/^[A-Za-z0-9_.:/-]+-\d+$/.test(value), {
|
|
36
|
+
message: "Use a ULID or 16+ char random key, not sessionId-sequence.",
|
|
37
|
+
});
|
|
43
38
|
export const codingSessionSignalSchema = z.object({
|
|
44
39
|
sessionId: z.string().min(1),
|
|
45
40
|
sequence: z.number().int().nonnegative(),
|
|
46
|
-
kind:
|
|
41
|
+
kind: signalKindSchema,
|
|
47
42
|
summary: z.string().min(1).max(2000),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
idempotencyKey: idempotencyKeySchema,
|
|
44
|
+
clientId: z.enum(["claude_code", "codex", "cursor", "web_agent"]).optional(),
|
|
45
|
+
scopeKey: z.string().min(1).default("global"),
|
|
46
|
+
evidence: z.array(evidenceEntrySchema).default([]),
|
|
51
47
|
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
52
48
|
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
53
49
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
54
50
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
framework: z.string().optional(),
|
|
61
|
-
filesAffected: z.array(z.string()).default([]),
|
|
62
|
-
sessionId: z.string().min(1),
|
|
63
|
-
pendingApproval: z.boolean().optional(),
|
|
64
|
-
originatingPrompt: z.string().optional(),
|
|
65
|
-
installToken: z.string().optional(),
|
|
66
|
-
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
51
|
+
const decisionSchema = z.object({
|
|
52
|
+
content: z.string().min(1).max(4000),
|
|
53
|
+
sourceSignalIds: z.array(z.string().uuid()).default([]),
|
|
54
|
+
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).default("proposed"),
|
|
55
|
+
outcomeId: z.string().uuid().optional(),
|
|
67
56
|
});
|
|
68
|
-
|
|
69
|
-
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
70
|
-
{ name: "aws_secret_key", pattern: /\b[A-Za-z0-9+/]{40}\b/g },
|
|
71
|
-
{ name: "gcp_api_key", pattern: /\bAIza[0-9A-Za-z\-_]{35}\b/g },
|
|
72
|
-
{ name: "azure_storage_key", pattern: /[A-Za-z0-9+/]{86}==/g },
|
|
73
|
-
{ name: "stripe_key", pattern: /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
74
|
-
{ name: "sendgrid_key", pattern: /\bSG\.[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{22,}\b/g },
|
|
75
|
-
{ name: "twilio_key", pattern: /\bSK[0-9a-fA-F]{32}\b/g },
|
|
76
|
-
{ name: "slack_token", pattern: /\bxox[baprs]-[0-9A-Za-z\-]{10,}\b/g },
|
|
77
|
-
{ name: "slack_webhook", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g },
|
|
78
|
-
{ name: "openai_env_assignment", pattern: /\bOPENAI_API_KEY\s*[=:]\s*["']?sk-[A-Za-z0-9_-]{20,}["']?/g },
|
|
79
|
-
{ name: "openai_key", pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
80
|
-
{ name: "bearer_token", pattern: /\bBearer\s+[A-Za-z0-9._-]+\b/g },
|
|
81
|
-
{ name: "anthropic_key", pattern: /\bsk-ant-[A-Za-z0-9\-_]{32,}\b/g },
|
|
82
|
-
{ name: "github_pat_classic", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
83
|
-
{ name: "github_pat_fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
|
|
84
|
-
{ name: "github_oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
|
|
85
|
-
{ name: "github_app_token", pattern: /\bghs_[A-Za-z0-9]{36}\b/g },
|
|
86
|
-
{ name: "gitlab_pat", pattern: /\bglpat-[A-Za-z0-9\-_]{20,}\b/g },
|
|
87
|
-
{ name: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
88
|
-
{ name: "pem_private_key", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
89
|
-
{ name: "db_dsn_with_creds", pattern: /(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[^:]+:[^@\s]+@[^\s"'`]+/gi },
|
|
90
|
-
{ name: "atw_token", pattern: /\b(?:atw|askthew)_[a-z]+_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[0-9a-f]{16,}\b/gi },
|
|
91
|
-
{ name: "uuid_token", pattern: /\b\w{2,20}_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:_[A-Za-z0-9]{8,})?\b/g },
|
|
92
|
-
{ name: "env_var_assignment", pattern: /(?:^|[\s;|&])(?:export\s+)?[A-Z][A-Z0-9_]{4,}(?:_KEY|_TOKEN|_SECRET|_PASSWORD|_PASS|_CREDENTIAL|_API_KEY|_PRIVATE|_AUTH)\s*[=:]\s*["']?[A-Za-z0-9+/=._~-]{8,}["']?/gm },
|
|
93
|
-
{ name: "email", pattern: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g },
|
|
94
|
-
{ name: "us_phone", pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/g },
|
|
95
|
-
{ name: "ssn", pattern: /\b(?!000|666|9\d{2})\d{3}[-\s](?!00)\d{2}[-\s](?!0{4})\d{4}\b/g },
|
|
96
|
-
{ name: "credit_card", pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g },
|
|
97
|
-
];
|
|
98
|
-
const SENSITIVE_QUERY_PARAMS = new Set([
|
|
99
|
-
"token",
|
|
100
|
-
"key",
|
|
101
|
-
"api_key",
|
|
102
|
-
"apikey",
|
|
103
|
-
"secret",
|
|
104
|
-
"password",
|
|
105
|
-
"passwd",
|
|
106
|
-
"pass",
|
|
107
|
-
"auth",
|
|
108
|
-
"access_token",
|
|
109
|
-
"refresh_token",
|
|
110
|
-
"id_token",
|
|
111
|
-
"client_secret",
|
|
112
|
-
"authorization",
|
|
113
|
-
"credential",
|
|
114
|
-
"sig",
|
|
115
|
-
"signature",
|
|
116
|
-
]);
|
|
117
|
-
const SENSITIVE_FLAG_NAMES = new Set([
|
|
118
|
-
"token",
|
|
119
|
-
"password",
|
|
120
|
-
"passwd",
|
|
121
|
-
"pass",
|
|
122
|
-
"secret",
|
|
123
|
-
"key",
|
|
124
|
-
"api-key",
|
|
125
|
-
"apikey",
|
|
126
|
-
"api_key",
|
|
127
|
-
"auth",
|
|
128
|
-
"access-token",
|
|
129
|
-
"private-key",
|
|
130
|
-
"credential",
|
|
131
|
-
"credentials",
|
|
132
|
-
"authorization",
|
|
133
|
-
"client-secret",
|
|
134
|
-
"client_secret",
|
|
135
|
-
]);
|
|
136
|
-
const SENSITIVE_PATH_SEGMENTS = new Set([
|
|
137
|
-
".env",
|
|
138
|
-
".ssh",
|
|
139
|
-
".aws",
|
|
140
|
-
".gcp",
|
|
141
|
-
".azure",
|
|
142
|
-
".gnupg",
|
|
143
|
-
".pgp",
|
|
144
|
-
"secrets",
|
|
145
|
-
"secret",
|
|
146
|
-
"credentials",
|
|
147
|
-
"credential",
|
|
148
|
-
"private",
|
|
149
|
-
"keys",
|
|
150
|
-
"certs",
|
|
151
|
-
"certificates",
|
|
152
|
-
"vault",
|
|
153
|
-
".netrc",
|
|
154
|
-
".npmrc",
|
|
155
|
-
".pypirc",
|
|
156
|
-
".docker",
|
|
157
|
-
"kubeconfig",
|
|
158
|
-
".kube",
|
|
159
|
-
]);
|
|
160
|
-
const ENTROPY_THRESHOLD = 4.5;
|
|
161
|
-
const ENTROPY_TOKEN_PATTERN = /[A-Za-z0-9+/=_\-]{20,}/g;
|
|
162
|
-
export function loadAskTheWConfig(env = process.env) {
|
|
163
|
-
return readJsonFile(configPath(env)) ?? {};
|
|
164
|
-
}
|
|
165
|
-
function isRedactionEnabled() {
|
|
166
|
-
return loadAskTheWConfig().redaction?.enabled !== false;
|
|
167
|
-
}
|
|
168
|
-
function sanitizeUrl(raw) {
|
|
169
|
-
try {
|
|
170
|
-
const url = new URL(raw);
|
|
171
|
-
if (url.username || url.password) {
|
|
172
|
-
url.username = "[REDACTED]";
|
|
173
|
-
url.password = "[REDACTED]";
|
|
174
|
-
}
|
|
175
|
-
for (const key of Array.from(url.searchParams.keys())) {
|
|
176
|
-
if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) {
|
|
177
|
-
url.searchParams.set(key, "[REDACTED]");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return url.toString().replace(/%5BREDACTED%5D/gi, "[REDACTED]");
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return raw;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function sanitizeUrls(text) {
|
|
187
|
-
return text.replace(/https?:\/\/[^\s"'`<>)\\]*/g, (match) => sanitizeUrl(match));
|
|
188
|
-
}
|
|
189
|
-
function scrubCommandFlags(command) {
|
|
190
|
-
let result = command.replace(/(-{1,2})([\w-]+)([ =])(["'])([^"']+)\4/g, (match, dashes, flagName, separator, quote) => SENSITIVE_FLAG_NAMES.has(String(flagName).toLowerCase())
|
|
191
|
-
? `${dashes}${flagName}${separator}${quote}[REDACTED]${quote}`
|
|
192
|
-
: match);
|
|
193
|
-
result = result.replace(/(-{1,2})([\w-]+)([ =])([^\s"'`]+)/g, (match, dashes, flagName, separator) => SENSITIVE_FLAG_NAMES.has(String(flagName).toLowerCase())
|
|
194
|
-
? `${dashes}${flagName}${separator}[REDACTED]`
|
|
195
|
-
: match);
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
|
-
function shannonEntropy(value) {
|
|
199
|
-
const freq = new Map();
|
|
200
|
-
for (const char of value) {
|
|
201
|
-
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
202
|
-
}
|
|
203
|
-
let entropy = 0;
|
|
204
|
-
for (const count of freq.values()) {
|
|
205
|
-
const probability = count / value.length;
|
|
206
|
-
entropy -= probability * Math.log2(probability);
|
|
207
|
-
}
|
|
208
|
-
return entropy;
|
|
209
|
-
}
|
|
210
|
-
function looksLikeSecret(token) {
|
|
211
|
-
const hasDigit = /\d/.test(token);
|
|
212
|
-
const hasUpper = /[A-Z]/.test(token);
|
|
213
|
-
const hasLower = /[a-z]/.test(token);
|
|
214
|
-
const hasSpecial = /[+/=_\-]/.test(token);
|
|
215
|
-
return hasDigit && hasLower && (hasUpper || hasSpecial);
|
|
216
|
-
}
|
|
217
|
-
function redactHighEntropyTokens(text) {
|
|
218
|
-
return text.replace(ENTROPY_TOKEN_PATTERN, (match) => {
|
|
219
|
-
if (match.includes("/") && match.length < 60) {
|
|
220
|
-
return match;
|
|
221
|
-
}
|
|
222
|
-
if (!looksLikeSecret(match)) {
|
|
223
|
-
return match;
|
|
224
|
-
}
|
|
225
|
-
return shannonEntropy(match) >= ENTROPY_THRESHOLD ? "[REDACTED]" : match;
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
function redactTargetedPatterns(text) {
|
|
229
|
-
return REDACTION_PATTERNS.reduce((accumulator, entry) => {
|
|
230
|
-
entry.pattern.lastIndex = 0;
|
|
231
|
-
return accumulator.replace(entry.pattern, "[REDACTED]");
|
|
232
|
-
}, text);
|
|
233
|
-
}
|
|
234
|
-
function redactRawSignalText(text) {
|
|
235
|
-
return redactTargetedPatterns(sanitizeUrls(text));
|
|
236
|
-
}
|
|
237
|
-
function redactOperationalContext(text) {
|
|
238
|
-
return redactHighEntropyTokens(scrubCommandFlags(redactRawSignalText(text)));
|
|
239
|
-
}
|
|
240
|
-
function sanitizeFilePath(filePath) {
|
|
241
|
-
let result = filePath.replace(/^(?:[A-Za-z]:\\|\/(?:Users|home|root|var|etc)\/[^/]+\/)/, "~/");
|
|
242
|
-
result = redactRawSignalText(result);
|
|
243
|
-
const segments = result.split(/[/\\]/);
|
|
244
|
-
if (segments.some((segment) => SENSITIVE_PATH_SEGMENTS.has(segment.toLowerCase()))) {
|
|
245
|
-
const basename = segments[segments.length - 1] ?? result;
|
|
246
|
-
return `[SENSITIVE_PATH]/${basename}`;
|
|
247
|
-
}
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
function redactMetadata(value, key) {
|
|
251
|
-
if (typeof value === "string") {
|
|
252
|
-
const redacted = redactOperationalContext(value);
|
|
253
|
-
if (key && /(?:^|_)(?:path|root|file|dir|directory)(?:$|_)/i.test(key)) {
|
|
254
|
-
return sanitizeFilePath(redacted);
|
|
255
|
-
}
|
|
256
|
-
return redacted;
|
|
257
|
-
}
|
|
258
|
-
if (Array.isArray(value)) {
|
|
259
|
-
return value.map((entry) => redactMetadata(entry));
|
|
260
|
-
}
|
|
261
|
-
if (typeof value === "object" && value !== null) {
|
|
262
|
-
return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactMetadata(entry, entryKey)]));
|
|
263
|
-
}
|
|
264
|
-
return value;
|
|
265
|
-
}
|
|
266
|
-
export function redactProvenanceSignal(input) {
|
|
267
|
-
const parsed = provenanceSignalSchema.parse(input);
|
|
268
|
-
if (!isRedactionEnabled()) {
|
|
269
|
-
return parsed;
|
|
270
|
-
}
|
|
57
|
+
function toolJson(value) {
|
|
271
58
|
return {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
? redactOperationalContext(parsed.originatingPrompt)
|
|
279
|
-
: undefined,
|
|
280
|
-
installToken: typeof parsed.installToken === "string"
|
|
281
|
-
? redactOperationalContext(parsed.installToken)
|
|
282
|
-
: undefined,
|
|
283
|
-
metadata: redactMetadata(parsed.metadata),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
export function redactCodingSessionSignal(input) {
|
|
287
|
-
const parsed = codingSessionSignalSchema.parse(input);
|
|
288
|
-
if (!isRedactionEnabled()) {
|
|
289
|
-
return parsed;
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
...parsed,
|
|
293
|
-
summary: redactRawSignalText(parsed.summary),
|
|
294
|
-
evidence: parsed.evidence.map((entry) => ({
|
|
295
|
-
...entry,
|
|
296
|
-
excerpt: redactOperationalContext(entry.excerpt),
|
|
297
|
-
diff: typeof entry.diff === "string" ? redactOperationalContext(entry.diff) : undefined,
|
|
298
|
-
before: typeof entry.before === "string" ? redactOperationalContext(entry.before) : undefined,
|
|
299
|
-
after: typeof entry.after === "string" ? redactOperationalContext(entry.after) : undefined,
|
|
300
|
-
})),
|
|
301
|
-
filesTouched: parsed.filesTouched.map((filePath) => sanitizeFilePath(filePath)),
|
|
302
|
-
commandsRun: parsed.commandsRun.map((command) => redactOperationalContext(command)),
|
|
303
|
-
metadata: redactMetadata(parsed.metadata),
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: JSON.stringify(value, null, 2),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
304
65
|
};
|
|
305
66
|
}
|
|
306
|
-
function
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
function normalizeClientId(value) {
|
|
310
|
-
return String(value ?? "")
|
|
311
|
-
.trim()
|
|
312
|
-
.toLowerCase()
|
|
313
|
-
.replace(/[^a-z0-9]+/g, "_")
|
|
314
|
-
.replace(/^_+|_+$/g, "")
|
|
315
|
-
.slice(0, 64);
|
|
316
|
-
}
|
|
317
|
-
export function normalizeInstallTokenInput(token) {
|
|
318
|
-
return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
319
|
-
}
|
|
320
|
-
const echoSchema = z.enum(["summary", "full"]).optional();
|
|
321
|
-
const cursorSchema = z.string().optional();
|
|
322
|
-
const idempotencyKeySchema = z.string().min(1).max(200).optional();
|
|
323
|
-
const maxCharsSchema = z.number().int().positive().max(100000).optional();
|
|
324
|
-
const LIMITED_LOCAL_TOOL_LIMIT = 3;
|
|
325
|
-
const LIMIT_REACHED_MESSAGE = "Limit reached. Upgrade to the paid plan.";
|
|
326
|
-
function traceId() {
|
|
327
|
-
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
328
|
-
}
|
|
329
|
-
function structuredError(input) {
|
|
330
|
-
return {
|
|
67
|
+
function needsSignup() {
|
|
68
|
+
return toolJson({
|
|
331
69
|
ok: false,
|
|
332
|
-
code:
|
|
333
|
-
message:
|
|
334
|
-
|
|
335
|
-
hint: input.hint ?? "",
|
|
336
|
-
traceId: input.traceId ?? traceId(),
|
|
337
|
-
...(typeof input.status === "number" ? { status: input.status } : {}),
|
|
338
|
-
...(input.extra ?? {}),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function withResponseShape(route, responseShape = "v2") {
|
|
342
|
-
const [path, query = ""] = route.split("?");
|
|
343
|
-
const searchParams = new URLSearchParams(query);
|
|
344
|
-
searchParams.set("response_shape", responseShape);
|
|
345
|
-
const nextQuery = searchParams.toString();
|
|
346
|
-
return nextQuery ? `${path}?${nextQuery}` : path;
|
|
347
|
-
}
|
|
348
|
-
function routeWithQuery(route, params) {
|
|
349
|
-
const searchParams = new URLSearchParams();
|
|
350
|
-
for (const [key, value] of Object.entries(params)) {
|
|
351
|
-
if (value === undefined || value === null || value === "") {
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
searchParams.set(key, String(value));
|
|
355
|
-
}
|
|
356
|
-
const query = searchParams.toString();
|
|
357
|
-
return query ? `${route}?${query}` : route;
|
|
358
|
-
}
|
|
359
|
-
function credentials(overrides) {
|
|
360
|
-
const legacyHostType = process.env.ASKTHEW_HOST_TYPE?.trim();
|
|
361
|
-
const hostType = legacyHostType === "claude_code" || legacyHostType === "codex" || legacyHostType === "cursor"
|
|
362
|
-
? legacyHostType
|
|
363
|
-
: undefined;
|
|
364
|
-
const overrideHostType = overrides?.hostType;
|
|
365
|
-
const clientId = normalizeClientId(overrides?.clientId) ||
|
|
366
|
-
normalizeClientId(process.env.ASKTHEW_CLIENT_ID?.trim()) ||
|
|
367
|
-
overrideHostType ||
|
|
368
|
-
hostType ||
|
|
369
|
-
"mcp_client";
|
|
370
|
-
return {
|
|
371
|
-
installToken: normalizeInstallTokenInput(overrides?.installToken) ||
|
|
372
|
-
normalizeInstallTokenInput(process.env.ASKTHEW_INSTALL_TOKEN),
|
|
373
|
-
userId: overrides?.userId?.trim() || process.env.ASKTHEW_USER_ID?.trim(),
|
|
374
|
-
apiKey: overrides?.apiKey?.trim() || process.env.ASKTHEW_API_KEY?.trim(),
|
|
375
|
-
serverName: overrides?.serverName?.trim() || process.env.ASKTHEW_SERVER_NAME?.trim(),
|
|
376
|
-
clientId,
|
|
377
|
-
clientLabel: overrides?.clientLabel?.trim() || process.env.ASKTHEW_CLIENT_LABEL?.trim(),
|
|
378
|
-
hostType: overrideHostType || hostType,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
function hasServerIdentity(overrides) {
|
|
382
|
-
const { installToken, userId } = credentials(overrides);
|
|
383
|
-
return Boolean(installToken || userId);
|
|
384
|
-
}
|
|
385
|
-
async function postToServer(route, payload, options, request) {
|
|
386
|
-
if (!hasServerIdentity(options?.credentials)) {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
const { installToken, userId, apiKey, clientId, clientLabel, hostType } = credentials(options?.credentials);
|
|
390
|
-
const fetcher = options?.fetchImpl ?? fetch;
|
|
391
|
-
const method = request?.method ?? "POST";
|
|
392
|
-
const bodyPayload = {
|
|
393
|
-
...payload,
|
|
394
|
-
installToken: installToken || undefined,
|
|
395
|
-
userId: userId || undefined,
|
|
396
|
-
clientId,
|
|
397
|
-
clientLabel: clientLabel || undefined,
|
|
398
|
-
hostType: hostType || undefined,
|
|
399
|
-
};
|
|
400
|
-
const response = await fetcher(`${apiBaseUrl(options?.apiBaseUrl)}${route}`, {
|
|
401
|
-
method,
|
|
402
|
-
headers: {
|
|
403
|
-
...(method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
404
|
-
...(request?.idempotencyKey ? { "Idempotency-Key": request.idempotencyKey } : {}),
|
|
405
|
-
...(installToken
|
|
406
|
-
? { Authorization: `Bearer ${installToken}` }
|
|
407
|
-
: apiKey
|
|
408
|
-
? { Authorization: `Bearer ${apiKey}` }
|
|
409
|
-
: {}),
|
|
410
|
-
},
|
|
411
|
-
...(method === "GET" ? {} : { body: JSON.stringify(bodyPayload) }),
|
|
412
|
-
}).catch(() => null);
|
|
413
|
-
if (!response) {
|
|
414
|
-
return structuredError({
|
|
415
|
-
code: "network_error",
|
|
416
|
-
message: "Ask The W server could not be reached.",
|
|
417
|
-
retryable: true,
|
|
418
|
-
hint: "Check your network connection or retry the same idempotency key.",
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
const body = await response.json().catch(() => null);
|
|
422
|
-
if (!response.ok) {
|
|
423
|
-
if (body && typeof body === "object") {
|
|
424
|
-
const record = body;
|
|
425
|
-
return {
|
|
426
|
-
...structuredError({
|
|
427
|
-
code: typeof record.code === "string" ? record.code : "http_error",
|
|
428
|
-
message: typeof record.message === "string"
|
|
429
|
-
? record.message
|
|
430
|
-
: typeof record.error === "string"
|
|
431
|
-
? record.error
|
|
432
|
-
: `Ask The W request failed with HTTP ${response.status}.`,
|
|
433
|
-
retryable: response.status === 429 || response.status >= 500,
|
|
434
|
-
hint: typeof record.hint === "string"
|
|
435
|
-
? record.hint
|
|
436
|
-
: response.status >= 500
|
|
437
|
-
? "Retry with the same Idempotency-Key if this was a write."
|
|
438
|
-
: "Check the tool input and retry.",
|
|
439
|
-
traceId: typeof record.traceId === "string" ? record.traceId : undefined,
|
|
440
|
-
status: response.status,
|
|
441
|
-
}),
|
|
442
|
-
...record,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
return structuredError({
|
|
446
|
-
code: "http_error",
|
|
447
|
-
message: `Ask The W request failed with HTTP ${response.status}.`,
|
|
448
|
-
retryable: response.status === 429 || response.status >= 500,
|
|
449
|
-
hint: response.status >= 500 ? "Retry with the same Idempotency-Key if this was a write." : "Check the tool input and retry.",
|
|
450
|
-
status: response.status,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
return body;
|
|
454
|
-
}
|
|
455
|
-
function runtimeMetadata(options) {
|
|
456
|
-
const scope = resolvePluginScope(process.cwd());
|
|
457
|
-
const { serverName, clientId, clientLabel } = credentials(options?.credentials);
|
|
458
|
-
const extraMetadata = typeof options?.runtimeMetadata === "function"
|
|
459
|
-
? options.runtimeMetadata()
|
|
460
|
-
: (options?.runtimeMetadata ?? {});
|
|
461
|
-
return {
|
|
462
|
-
repository: scope.repoName,
|
|
463
|
-
repo_name: scope.repoName,
|
|
464
|
-
scope_key: localScopeKey(),
|
|
465
|
-
...(scope.repoRoot ? { repo_root: sanitizeFilePath(scope.repoRoot) } : {}),
|
|
466
|
-
...(scope.appPath ? { app_path: sanitizeFilePath(scope.appPath) } : {}),
|
|
467
|
-
...(scope.serviceName ? { service_name: scope.serviceName } : {}),
|
|
468
|
-
...(serverName ? { server_name: serverName } : {}),
|
|
469
|
-
...(clientId ? { client_id: clientId } : {}),
|
|
470
|
-
...(clientLabel ? { client_label: clientLabel } : {}),
|
|
471
|
-
...extraMetadata,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
function localScopeKey(cwd = process.cwd()) {
|
|
475
|
-
const scope = resolvePluginScope(cwd);
|
|
476
|
-
return [scope.repoRoot || scope.repoName || cwd, scope.appPath ?? "", scope.serviceName ?? ""]
|
|
477
|
-
.filter(Boolean)
|
|
478
|
-
.join("::")
|
|
479
|
-
.replace(/\s+/g, " ")
|
|
480
|
-
.slice(0, 500);
|
|
481
|
-
}
|
|
482
|
-
function startupSessionId(options) {
|
|
483
|
-
const scope = resolvePluginScope(process.cwd());
|
|
484
|
-
const { clientId } = credentials(options?.credentials);
|
|
485
|
-
const scopeKey = [scope.repoName, scope.appPath, scope.serviceName]
|
|
486
|
-
.filter((part) => Boolean(part))
|
|
487
|
-
.join(":") || "workspace";
|
|
488
|
-
return ["mcp-startup", clientId || "mcp_client", scopeKey]
|
|
489
|
-
.join(":")
|
|
490
|
-
.replace(/[^a-zA-Z0-9:_-]+/g, "_")
|
|
491
|
-
.slice(0, 240);
|
|
492
|
-
}
|
|
493
|
-
function loginCommandHint() {
|
|
494
|
-
for (const value of [
|
|
495
|
-
process.env.ASKTHEW_EMAIL,
|
|
496
|
-
process.env.GIT_AUTHOR_EMAIL,
|
|
497
|
-
process.env.GIT_COMMITTER_EMAIL,
|
|
498
|
-
process.env.EMAIL,
|
|
499
|
-
]) {
|
|
500
|
-
const email = String(value ?? "").trim();
|
|
501
|
-
if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
|
502
|
-
return `npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp identify --email ${email}`;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return "npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp identify --email <your-email>";
|
|
70
|
+
code: "needs_signup",
|
|
71
|
+
message: "Ask The W needs signup before this tool can run. Call askthew_start_signup({email}), then askthew_complete_signup({email, code}).",
|
|
72
|
+
});
|
|
506
73
|
}
|
|
507
|
-
|
|
508
|
-
if (!hasServerIdentity(options?.credentials)) {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const { installToken, apiKey, clientId, clientLabel, hostType, serverName } = credentials(options?.credentials);
|
|
512
|
-
if (!installToken) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
74
|
+
function defaultScopeKey() {
|
|
515
75
|
const scope = resolvePluginScope(process.cwd());
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
522
|
-
},
|
|
523
|
-
body: JSON.stringify({
|
|
524
|
-
installToken,
|
|
525
|
-
clientId,
|
|
526
|
-
...(clientLabel ? { clientLabel } : {}),
|
|
527
|
-
hostType,
|
|
528
|
-
...(serverName ? { serverName } : {}),
|
|
529
|
-
repoName: scope.repoName,
|
|
530
|
-
...(scope.repoRoot ? { repoRoot: scope.repoRoot } : {}),
|
|
531
|
-
...(scope.appPath ? { appPath: scope.appPath } : {}),
|
|
532
|
-
...(scope.serviceName ? { serviceName: scope.serviceName } : {}),
|
|
533
|
-
}),
|
|
534
|
-
}).catch(() => null);
|
|
535
|
-
}
|
|
536
|
-
async function sendStartupSetupSignal(options) {
|
|
537
|
-
const sessionSignal = {
|
|
538
|
-
sessionId: startupSessionId(options),
|
|
539
|
-
sequence: 0,
|
|
540
|
-
kind: "setup_complete",
|
|
541
|
-
summary: "Ask The W plugin server started for this coding-agent session.",
|
|
542
|
-
evidence: [],
|
|
543
|
-
filesTouched: [],
|
|
544
|
-
commandsRun: [],
|
|
545
|
-
metadata: {
|
|
546
|
-
...runtimeMetadata(options),
|
|
547
|
-
automated: true,
|
|
548
|
-
operational: true,
|
|
549
|
-
origin: "mcp_server_startup",
|
|
550
|
-
},
|
|
551
|
-
};
|
|
552
|
-
await postToServer("/api/ingest/mcp", {
|
|
553
|
-
sessionSignal: redactCodingSessionSignal(sessionSignal),
|
|
554
|
-
}, options);
|
|
555
|
-
}
|
|
556
|
-
async function sendStartupSignals(options) {
|
|
557
|
-
await sendStartupHeartbeat(options).catch(() => null);
|
|
558
|
-
await sendStartupSetupSignal(options).catch(() => null);
|
|
76
|
+
if (scope.repoRoot)
|
|
77
|
+
return `repo:${scope.repoRoot}`;
|
|
78
|
+
if (scope.serviceName)
|
|
79
|
+
return `project:${scope.serviceName}`;
|
|
80
|
+
return "global";
|
|
559
81
|
}
|
|
560
82
|
export function createAskTheWMcpServer(options = {}) {
|
|
561
|
-
const initialMode = resolveMcpMode();
|
|
562
|
-
if (initialMode.mode === "free_pending_auth") {
|
|
563
|
-
ensureLocalIdentity({
|
|
564
|
-
emailClaim: process.env.ASKTHEW_EMAIL,
|
|
565
|
-
apiUrl: options.apiBaseUrl,
|
|
566
|
-
telemetryOptOut: process.env.ASKTHEW_TELEMETRY === "off",
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
const resolvedMode = resolveMcpMode();
|
|
570
|
-
const optionInstallToken = normalizeInstallTokenInput(options.credentials?.installToken);
|
|
571
|
-
const mode = optionInstallToken
|
|
572
|
-
? { mode: "paid", installToken: optionInstallToken, reason: "options_install_token" }
|
|
573
|
-
: resolvedMode;
|
|
574
|
-
const localStore = mode.mode === "paid" ? null : LocalStore.open();
|
|
575
|
-
if (localStore && mode.mode === "free" && mode.cliCredentials) {
|
|
576
|
-
void flushTelemetryOutbox({
|
|
577
|
-
store: localStore,
|
|
578
|
-
credentials: mode.cliCredentials,
|
|
579
|
-
apiUrl: options.apiBaseUrl,
|
|
580
|
-
fetchImpl: options.fetchImpl,
|
|
581
|
-
}).catch(() => null);
|
|
582
|
-
}
|
|
583
83
|
const server = new McpServer({
|
|
584
|
-
name: "Ask The W
|
|
84
|
+
name: "Ask The W MCP",
|
|
585
85
|
version: packageVersion(),
|
|
586
86
|
});
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return {
|
|
593
|
-
content: [
|
|
594
|
-
{
|
|
595
|
-
type: "text",
|
|
596
|
-
text: JSON.stringify(upstream, null, 2),
|
|
597
|
-
},
|
|
598
|
-
],
|
|
599
|
-
};
|
|
600
|
-
};
|
|
601
|
-
const localResponse = (value) => toolJson(value);
|
|
602
|
-
const localToolError = (input) => localResponse(structuredError(input));
|
|
603
|
-
const budgetedLocalResponse = (value, maxChars) => {
|
|
604
|
-
if (!maxChars || JSON.stringify(value, null, 2).length <= maxChars) {
|
|
605
|
-
return localResponse(value);
|
|
606
|
-
}
|
|
607
|
-
const next = { ...value, truncated: true, maxChars };
|
|
608
|
-
for (const key of ["signals", "decisions", "decisionCandidates", "matches"]) {
|
|
609
|
-
const list = next[key];
|
|
610
|
-
if (Array.isArray(list)) {
|
|
611
|
-
while (list.length > 0 && JSON.stringify(next, null, 2).length > maxChars) {
|
|
612
|
-
list.pop();
|
|
613
|
-
}
|
|
614
|
-
next[key] = list;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (typeof next.rendered === "string" && JSON.stringify(next, null, 2).length > maxChars) {
|
|
618
|
-
const overhead = JSON.stringify({ ...next, rendered: "" }, null, 2).length + 80;
|
|
619
|
-
next.rendered = `${next.rendered.slice(0, Math.max(0, maxChars - overhead)).trimEnd()}\n\n[truncated to max_chars=${maxChars}]`;
|
|
620
|
-
}
|
|
621
|
-
return localResponse(next);
|
|
622
|
-
};
|
|
623
|
-
const currentScopeKey = () => localScopeKey();
|
|
624
|
-
const compactWriteResponse = (input) => localResponse({
|
|
625
|
-
ok: input.ok !== false,
|
|
626
|
-
id: input.id ?? null,
|
|
627
|
-
...(input.decisionId ? { decisionId: input.decisionId } : {}),
|
|
628
|
-
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
629
|
-
...(typeof input.sequence === "number" ? { sequence: input.sequence } : {}),
|
|
630
|
-
...(typeof input.signalCount === "number" ? { signalCount: input.signalCount } : {}),
|
|
631
|
-
...(input.warnings && input.warnings.length > 0 ? { warnings: input.warnings } : {}),
|
|
632
|
-
});
|
|
633
|
-
const upstreamId = (upstream) => {
|
|
634
|
-
if (!upstream || typeof upstream !== "object")
|
|
635
|
-
return null;
|
|
636
|
-
const record = upstream;
|
|
637
|
-
return (record.id ??
|
|
638
|
-
record.signalId ??
|
|
639
|
-
record.entry?.id ??
|
|
640
|
-
record.decision?.id ??
|
|
641
|
-
record.data?.id ??
|
|
642
|
-
record.data?.outcome?.id ??
|
|
643
|
-
record.data?.decision?.id ??
|
|
644
|
-
null);
|
|
645
|
-
};
|
|
646
|
-
const upstreamSequence = (upstream) => {
|
|
647
|
-
if (!upstream || typeof upstream !== "object")
|
|
648
|
-
return null;
|
|
649
|
-
const record = upstream;
|
|
650
|
-
const sequence = record.sequence ?? record.entry?.sequence ?? record.data?.sequence ?? null;
|
|
651
|
-
return typeof sequence === "number" ? sequence : null;
|
|
652
|
-
};
|
|
653
|
-
const upstreamFailure = (upstream) => upstream && typeof upstream === "object" && upstream.ok === false
|
|
654
|
-
? upstream
|
|
655
|
-
: null;
|
|
656
|
-
const requireFreeIdentity = () => {
|
|
657
|
-
if (mode.mode === "unauthenticated" || mode.mode === "free_pending_auth") {
|
|
658
|
-
const loginCommand = loginCommandHint();
|
|
659
|
-
return localToolError({
|
|
660
|
-
code: "free_tier_login_required",
|
|
661
|
-
message: "Free local mode needs a local install identity before capture.",
|
|
662
|
-
retryable: false,
|
|
663
|
-
hint: `Run \`${loginCommand}\` or reinstall with \`--free --email <your-email>\`, then restart or reload the MCP host.`,
|
|
664
|
-
extra: {
|
|
665
|
-
loginCommand,
|
|
666
|
-
supportEmail: "support@askthew.com",
|
|
667
|
-
},
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
return null;
|
|
671
|
-
};
|
|
672
|
-
const limitReachedResponse = (tool) => localResponse({
|
|
673
|
-
ok: false,
|
|
674
|
-
code: "free_tier_limit_reached",
|
|
675
|
-
tool,
|
|
676
|
-
message: LIMIT_REACHED_MESSAGE,
|
|
677
|
-
limit: LIMITED_LOCAL_TOOL_LIMIT,
|
|
678
|
-
pricingUrl: "https://askthew.com/pricing",
|
|
679
|
-
upgradeUrl: `https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-limit&utm_campaign=mcp-free&tool=${tool}`,
|
|
680
|
-
supportEmail: "support@askthew.com",
|
|
87
|
+
const outbox = options.outbox ?? new Outbox();
|
|
88
|
+
const client = () => new AskTheWCloudClient({
|
|
89
|
+
apiUrl: options.apiUrl,
|
|
90
|
+
fetcher: options.fetcher,
|
|
91
|
+
env: options.env,
|
|
681
92
|
});
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const key = `usage:${tool}`;
|
|
686
|
-
const current = Number(localStore.getMeta(key) || "0");
|
|
687
|
-
const count = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : 0;
|
|
688
|
-
if (count >= LIMITED_LOCAL_TOOL_LIMIT) {
|
|
689
|
-
return limitReachedResponse(tool);
|
|
690
|
-
}
|
|
691
|
-
localStore.setMeta(key, String(count + 1));
|
|
692
|
-
return null;
|
|
693
|
-
};
|
|
694
|
-
server.tool("capture_session_signal", {
|
|
695
|
-
sessionId: z.string().min(1),
|
|
696
|
-
sequence: z.number().int().nonnegative(),
|
|
697
|
-
kind: sessionSignalKindSchema,
|
|
698
|
-
summary: z.string().min(1).max(2000),
|
|
699
|
-
evidence: z
|
|
700
|
-
.array(evidenceEntrySchema)
|
|
701
|
-
.default([]),
|
|
702
|
-
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
703
|
-
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
704
|
-
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
705
|
-
idempotencyKey: idempotencyKeySchema,
|
|
706
|
-
echo: echoSchema,
|
|
93
|
+
server.tool("askthew_start_signup", {
|
|
94
|
+
email: z.string().email(),
|
|
95
|
+
clientHint: z.enum(["claude_code", "codex", "cursor", "web_agent"]).optional(),
|
|
707
96
|
}, async (payload) => {
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
},
|
|
97
|
+
const body = await startSignup({
|
|
98
|
+
email: payload.email,
|
|
99
|
+
apiUrl: options.apiUrl,
|
|
100
|
+
clientHint: payload.clientHint,
|
|
101
|
+
fetcher: options.fetcher,
|
|
714
102
|
});
|
|
715
|
-
|
|
716
|
-
if (mode.mode !== "paid" && localStore) {
|
|
717
|
-
const loginRequired = requireFreeIdentity();
|
|
718
|
-
if (loginRequired) {
|
|
719
|
-
return loginRequired;
|
|
720
|
-
}
|
|
721
|
-
const signal = localStore.insertSignal({
|
|
722
|
-
sessionId: sessionSignal.sessionId,
|
|
723
|
-
sequence: sessionSignal.sequence,
|
|
724
|
-
kind: sessionSignal.kind,
|
|
725
|
-
summary: sessionSignal.summary,
|
|
726
|
-
evidence: sessionSignal.evidence,
|
|
727
|
-
filesTouched: sessionSignal.filesTouched,
|
|
728
|
-
commandsRun: sessionSignal.commandsRun,
|
|
729
|
-
metadata: sessionSignal.metadata,
|
|
730
|
-
scopeKey,
|
|
731
|
-
});
|
|
732
|
-
const sessionSignalCount = localStore.listSignals({
|
|
733
|
-
sessionId: signal.sessionId,
|
|
734
|
-
scopeKey,
|
|
735
|
-
limit: 100000,
|
|
736
|
-
}).length;
|
|
737
|
-
const existingDecision = localStore.getDecisionForSignal(signal.id);
|
|
738
|
-
const decisionCandidate = candidateFromSignal(signal, existingDecision);
|
|
739
|
-
const autoDecision = decisionCandidate
|
|
740
|
-
? localStore.createDecision({
|
|
741
|
-
rawContent: signal.summary,
|
|
742
|
-
headline: signal.summary,
|
|
743
|
-
status: decisionCandidate.suggestedStatus,
|
|
744
|
-
sessionId: signal.sessionId,
|
|
745
|
-
files: signal.filesTouched,
|
|
746
|
-
sourceSignalIds: [signal.id],
|
|
747
|
-
scopeKey,
|
|
748
|
-
})
|
|
749
|
-
: null;
|
|
750
|
-
if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
|
|
751
|
-
localStore.enqueueTelemetry(buildTelemetryPayload({
|
|
752
|
-
store: localStore,
|
|
753
|
-
credentials: mode.cliCredentials,
|
|
754
|
-
sessionId: sessionSignal.sessionId,
|
|
755
|
-
}));
|
|
756
|
-
void flushTelemetryOutbox({
|
|
757
|
-
store: localStore,
|
|
758
|
-
credentials: mode.cliCredentials,
|
|
759
|
-
apiUrl: options.apiBaseUrl,
|
|
760
|
-
fetchImpl: options.fetchImpl,
|
|
761
|
-
}).catch(() => null);
|
|
762
|
-
}
|
|
763
|
-
if (!payload.echo) {
|
|
764
|
-
return compactWriteResponse({
|
|
765
|
-
id: signal.id,
|
|
766
|
-
decisionId: autoDecision?.id,
|
|
767
|
-
sessionId: signal.sessionId,
|
|
768
|
-
sequence: signal.sequence,
|
|
769
|
-
signalCount: sessionSignalCount,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
if (payload.echo === "summary") {
|
|
773
|
-
return localResponse({
|
|
774
|
-
ok: true,
|
|
775
|
-
id: signal.id,
|
|
776
|
-
sessionId: signal.sessionId,
|
|
777
|
-
sequence: signal.sequence,
|
|
778
|
-
signalCount: sessionSignalCount,
|
|
779
|
-
summary: signal.summary,
|
|
780
|
-
kind: signal.kind,
|
|
781
|
-
...(autoDecision ? { decisionId: autoDecision.id } : {}),
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
return localResponse({
|
|
785
|
-
ok: true,
|
|
786
|
-
tier: "free",
|
|
787
|
-
signal,
|
|
788
|
-
...(autoDecision ? { decision: decisionWithSignals(localStore, autoDecision) } : {}),
|
|
789
|
-
note: localStore.usingJsonFallback
|
|
790
|
-
? "Captured locally in JSON fallback mode because SQLite was unavailable."
|
|
791
|
-
: "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
const upstream = await postToServer(payload.echo === "full" ? "/api/ingest/mcp" : withResponseShape("/api/ingest/mcp"), {
|
|
795
|
-
sessionSignal,
|
|
796
|
-
}, options, { idempotencyKey: payload.idempotencyKey });
|
|
797
|
-
const failure = upstreamFailure(upstream);
|
|
798
|
-
if (failure)
|
|
799
|
-
return localResponse(failure);
|
|
800
|
-
if (!payload.echo) {
|
|
801
|
-
return compactWriteResponse({
|
|
802
|
-
id: upstreamId(upstream),
|
|
803
|
-
sessionId: sessionSignal.sessionId,
|
|
804
|
-
sequence: sessionSignal.sequence,
|
|
805
|
-
signalCount: upstream && typeof upstream === "object" && typeof upstream.signalCount === "number"
|
|
806
|
-
? upstream.signalCount
|
|
807
|
-
: 1,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
if (payload.echo === "summary") {
|
|
811
|
-
return localResponse({
|
|
812
|
-
ok: true,
|
|
813
|
-
id: upstreamId(upstream),
|
|
814
|
-
sessionId: sessionSignal.sessionId,
|
|
815
|
-
sequence: sessionSignal.sequence,
|
|
816
|
-
signalCount: upstream && typeof upstream === "object" && typeof upstream.signalCount === "number"
|
|
817
|
-
? upstream.signalCount
|
|
818
|
-
: 1,
|
|
819
|
-
summary: sessionSignal.summary,
|
|
820
|
-
kind: sessionSignal.kind,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
return {
|
|
824
|
-
content: [
|
|
825
|
-
{
|
|
826
|
-
type: "text",
|
|
827
|
-
text: JSON.stringify({
|
|
828
|
-
ok: true,
|
|
829
|
-
signal: sessionSignal,
|
|
830
|
-
upstream,
|
|
831
|
-
note: "Ask The W stores this as source material and performs inference in the app.",
|
|
832
|
-
}, null, 2),
|
|
833
|
-
},
|
|
834
|
-
],
|
|
835
|
-
};
|
|
836
|
-
});
|
|
837
|
-
server.tool("list_decisions", {
|
|
838
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
839
|
-
cursor: z.string().optional(),
|
|
840
|
-
sessionId: z.string().optional(),
|
|
841
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
842
|
-
since: z.string().optional(),
|
|
843
|
-
compact: z.boolean().optional(),
|
|
844
|
-
max_chars: maxCharsSchema,
|
|
845
|
-
}, async (payload) => {
|
|
846
|
-
if (mode.mode !== "paid" && localStore) {
|
|
847
|
-
const loginRequired = requireFreeIdentity();
|
|
848
|
-
if (loginRequired)
|
|
849
|
-
return loginRequired;
|
|
850
|
-
const decisions = localStore.listDecisions({
|
|
851
|
-
limit: payload.limit ?? 5,
|
|
852
|
-
cursor: payload.cursor,
|
|
853
|
-
sessionId: payload.sessionId,
|
|
854
|
-
status: payload.status,
|
|
855
|
-
since: payload.since,
|
|
856
|
-
scopeKey: currentScopeKey(),
|
|
857
|
-
});
|
|
858
|
-
return budgetedLocalResponse({
|
|
859
|
-
ok: true,
|
|
860
|
-
tier: "free",
|
|
861
|
-
decisions: payload.compact !== false
|
|
862
|
-
? decisions.map((decision) => ({
|
|
863
|
-
id: decision.id,
|
|
864
|
-
headline: decision.headline,
|
|
865
|
-
status: decision.status,
|
|
866
|
-
signalIds: decision.sourceSignalIds,
|
|
867
|
-
}))
|
|
868
|
-
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
869
|
-
nextCursor: decisions.length >= (payload.limit ?? 5) ? decisions.at(-1)?.createdAt ?? null : null,
|
|
870
|
-
}, payload.max_chars ?? 8000);
|
|
871
|
-
}
|
|
872
|
-
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
873
|
-
limit: payload.limit ?? 5,
|
|
874
|
-
cursor: payload.cursor,
|
|
875
|
-
sessionId: payload.sessionId,
|
|
876
|
-
status: payload.status,
|
|
877
|
-
since: payload.since,
|
|
878
|
-
compact: payload.compact ?? true,
|
|
879
|
-
max_chars: payload.max_chars ?? 8000,
|
|
880
|
-
}));
|
|
103
|
+
return toolJson(body);
|
|
881
104
|
});
|
|
882
|
-
server.tool("
|
|
883
|
-
|
|
105
|
+
server.tool("askthew_complete_signup", {
|
|
106
|
+
email: z.string().email(),
|
|
107
|
+
code: z.string().regex(/^\d{6}$/),
|
|
108
|
+
clientHint: z.enum(["claude_code", "codex", "cursor", "web_agent"]).optional(),
|
|
884
109
|
}, async (payload) => {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
retryable: false,
|
|
896
|
-
hint: "Check the decision id or search/list local decisions first.",
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`);
|
|
110
|
+
const body = await completeSignup({
|
|
111
|
+
email: payload.email,
|
|
112
|
+
code: payload.code,
|
|
113
|
+
apiUrl: options.apiUrl,
|
|
114
|
+
clientHint: payload.clientHint,
|
|
115
|
+
tokenPurpose: "device",
|
|
116
|
+
fetcher: options.fetcher,
|
|
117
|
+
env: options.env,
|
|
118
|
+
});
|
|
119
|
+
return toolJson(body);
|
|
900
120
|
});
|
|
901
|
-
server.tool("
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const existing = localStore.getDecision(existingId);
|
|
915
|
-
if (existing) {
|
|
916
|
-
return payload.echo === "full"
|
|
917
|
-
? localResponse({ ok: true, tier: "free", decision: decisionWithSignals(localStore, existing), idempotent: true })
|
|
918
|
-
: compactWriteResponse({ id: existing.id, sequence: localStore.stats().decisions });
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
const decision = localStore.createDecision({
|
|
923
|
-
rawContent: payload.content,
|
|
924
|
-
sessionId: localStore.mostRecentSessionId({ scopeKey }),
|
|
925
|
-
scopeKey,
|
|
926
|
-
});
|
|
927
|
-
if (payload.idempotencyKey) {
|
|
928
|
-
localStore.setMeta(`idempotency:create_decision:${scopeKey}:${payload.idempotencyKey}`, decision.id);
|
|
929
|
-
}
|
|
930
|
-
const warnings = detectDecisionConflicts({
|
|
931
|
-
decision,
|
|
932
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey }),
|
|
121
|
+
server.tool("capture_session_signal", codingSessionSignalSchema.shape, async (payload) => {
|
|
122
|
+
const currentClient = client();
|
|
123
|
+
if (!currentClient.hasToken())
|
|
124
|
+
return needsSignup();
|
|
125
|
+
const parsed = codingSessionSignalSchema.safeParse({
|
|
126
|
+
...payload,
|
|
127
|
+
scopeKey: payload.scopeKey || defaultScopeKey(),
|
|
128
|
+
});
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return toolJson({
|
|
131
|
+
ok: false,
|
|
132
|
+
code: "invalid_signal",
|
|
133
|
+
message: parsed.error.issues.map((issue) => issue.message).join("; "),
|
|
933
134
|
});
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
return
|
|
135
|
+
}
|
|
136
|
+
const sanitized = redactSignalPayload(parsed.data);
|
|
137
|
+
const localOutboxId = outbox.append(sanitized);
|
|
138
|
+
const result = await sendOutboxPayload(currentClient, sanitized);
|
|
139
|
+
if (result.kind === "sent") {
|
|
140
|
+
outbox.markSent(localOutboxId);
|
|
141
|
+
const body = result.body;
|
|
142
|
+
return toolJson({
|
|
942
143
|
ok: true,
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
144
|
+
queued: false,
|
|
145
|
+
id: body.id,
|
|
146
|
+
sequence: body.sequence ?? sanitized.sequence,
|
|
946
147
|
});
|
|
947
148
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if (!payload.echo) {
|
|
955
|
-
return compactWriteResponse({
|
|
956
|
-
id: upstreamId(upstream),
|
|
957
|
-
sequence: upstreamSequence(upstream) ?? 1,
|
|
149
|
+
if (result.kind === "failed") {
|
|
150
|
+
outbox.markFailed(localOutboxId, result.code, result.message);
|
|
151
|
+
return toolJson({
|
|
152
|
+
ok: false,
|
|
153
|
+
code: result.code,
|
|
154
|
+
message: result.message,
|
|
958
155
|
});
|
|
959
156
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
:
|
|
157
|
+
const retryEstimateSeconds = outbox.backoff(localOutboxId, 0, result.retryAfterSeconds);
|
|
158
|
+
return toolJson({
|
|
159
|
+
ok: true,
|
|
160
|
+
queued: true,
|
|
161
|
+
local_outbox_id: localOutboxId,
|
|
162
|
+
retry_estimate_seconds: retryEstimateSeconds,
|
|
163
|
+
});
|
|
963
164
|
});
|
|
964
165
|
server.tool("list_signals", {
|
|
965
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
966
|
-
cursor: z.string().optional(),
|
|
967
166
|
sessionId: z.string().optional(),
|
|
968
167
|
since: z.string().optional(),
|
|
969
|
-
|
|
970
|
-
max_chars: maxCharsSchema,
|
|
168
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
971
169
|
}, async (payload) => {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
limit: payload.limit ?? 10,
|
|
978
|
-
cursor: payload.cursor,
|
|
979
|
-
sessionId: payload.sessionId,
|
|
980
|
-
since: payload.since,
|
|
981
|
-
scopeKey: currentScopeKey(),
|
|
982
|
-
});
|
|
983
|
-
return budgetedLocalResponse({
|
|
984
|
-
ok: true,
|
|
985
|
-
tier: "free",
|
|
986
|
-
signals: payload.compact !== false
|
|
987
|
-
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
988
|
-
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
989
|
-
nextCursor: signals.length >= (payload.limit ?? 10) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
990
|
-
}, payload.max_chars ?? 8000);
|
|
991
|
-
}
|
|
992
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
993
|
-
limit: payload.limit ?? 10,
|
|
994
|
-
cursor: payload.cursor,
|
|
995
|
-
sessionId: payload.sessionId,
|
|
996
|
-
since: payload.since,
|
|
997
|
-
compact: payload.compact ?? true,
|
|
998
|
-
max_chars: payload.max_chars ?? 8000,
|
|
999
|
-
}));
|
|
170
|
+
const currentClient = client();
|
|
171
|
+
if (!currentClient.hasToken())
|
|
172
|
+
return needsSignup();
|
|
173
|
+
const result = await currentClient.listSignals(payload);
|
|
174
|
+
return toolJson(result.body);
|
|
1000
175
|
});
|
|
1001
|
-
server.tool("
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
if (!localStore || mode.mode === "paid")
|
|
1008
|
-
return localResponse(paidFeatureNudge("recap"));
|
|
1009
|
-
const loginRequired = requireFreeIdentity();
|
|
1010
|
-
if (loginRequired)
|
|
1011
|
-
return loginRequired;
|
|
1012
|
-
const limited = consumeLimitedLocalUse("recap");
|
|
1013
|
-
if (limited)
|
|
1014
|
-
return limited;
|
|
1015
|
-
const scopeKey = currentScopeKey();
|
|
1016
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1017
|
-
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1018
|
-
const decisions = sessionId ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1019
|
-
return budgetedLocalResponse({
|
|
1020
|
-
ok: true,
|
|
1021
|
-
tier: "free",
|
|
1022
|
-
sessionId,
|
|
1023
|
-
format: payload.format,
|
|
1024
|
-
rendered: renderRecap({ format: payload.format, signals, decisions }),
|
|
1025
|
-
...(payload.compact
|
|
1026
|
-
? { signals: signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
1027
|
-
: {}),
|
|
1028
|
-
}, payload.max_chars);
|
|
176
|
+
server.tool("create_decision", decisionSchema.shape, async (payload) => {
|
|
177
|
+
const currentClient = client();
|
|
178
|
+
if (!currentClient.hasToken())
|
|
179
|
+
return needsSignup();
|
|
180
|
+
const result = await currentClient.createDecision(payload);
|
|
181
|
+
return toolJson(result.body);
|
|
1029
182
|
});
|
|
1030
|
-
server.tool("
|
|
1031
|
-
|
|
1032
|
-
max_chars: maxCharsSchema,
|
|
183
|
+
server.tool("list_decisions", {
|
|
184
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
1033
185
|
}, async (payload) => {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const limited = consumeLimitedLocalUse("coach");
|
|
1040
|
-
if (limited)
|
|
1041
|
-
return limited;
|
|
1042
|
-
const scopeKey = currentScopeKey();
|
|
1043
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1044
|
-
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1045
|
-
const decisions = sessionId ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1046
|
-
const coaching = buildSessionCoach({ signals, decisions });
|
|
1047
|
-
return budgetedLocalResponse({
|
|
1048
|
-
ok: true,
|
|
1049
|
-
tier: "free",
|
|
1050
|
-
sessionId,
|
|
1051
|
-
nudge: coaching.nudge,
|
|
1052
|
-
rendered: coaching.nudge,
|
|
1053
|
-
}, payload.max_chars);
|
|
186
|
+
const currentClient = client();
|
|
187
|
+
if (!currentClient.hasToken())
|
|
188
|
+
return needsSignup();
|
|
189
|
+
const result = await currentClient.listDecisions(payload);
|
|
190
|
+
return toolJson(result.body);
|
|
1054
191
|
});
|
|
1055
|
-
server.tool("
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
why: z.string().optional(),
|
|
1059
|
-
idempotencyKey: idempotencyKeySchema,
|
|
192
|
+
server.tool("recap", {
|
|
193
|
+
since: z.string().optional(),
|
|
194
|
+
maxSignals: z.number().int().min(1).max(200).optional(),
|
|
1060
195
|
}, async (payload) => {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (!Number.isFinite(numericSignalId)) {
|
|
1068
|
-
return localToolError({
|
|
1069
|
-
code: "invalid_input",
|
|
1070
|
-
message: "Invalid signalId.",
|
|
1071
|
-
retryable: false,
|
|
1072
|
-
hint: "Use the numeric local signal id.",
|
|
1073
|
-
extra: { field: "signalId" },
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
const signal = localStore.getSignal(numericSignalId);
|
|
1077
|
-
if (!signal) {
|
|
1078
|
-
return localToolError({
|
|
1079
|
-
code: "not_found",
|
|
1080
|
-
message: "Signal not found in the local Ask The W store.",
|
|
1081
|
-
retryable: false,
|
|
1082
|
-
hint: "List signals first, then pass the numeric local signal id.",
|
|
1083
|
-
extra: { tool: "promote_signal_to_decision" },
|
|
1084
|
-
});
|
|
196
|
+
const currentClient = client();
|
|
197
|
+
if (!currentClient.hasToken())
|
|
198
|
+
return needsSignup();
|
|
199
|
+
try {
|
|
200
|
+
const result = await currentClient.recap(payload);
|
|
201
|
+
return toolJson(result.body);
|
|
1085
202
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}) ?? linkedDecision;
|
|
1092
|
-
return localResponse({
|
|
1093
|
-
ok: true,
|
|
1094
|
-
id: decision.id,
|
|
1095
|
-
sequence: localStore.stats().decisions,
|
|
1096
|
-
decision: decisionWithSignals(localStore, decision),
|
|
1097
|
-
linkedSignalId: signal.id,
|
|
1098
|
-
reused: true,
|
|
203
|
+
catch {
|
|
204
|
+
return toolJson({
|
|
205
|
+
ok: false,
|
|
206
|
+
code: "unreachable",
|
|
207
|
+
message: "Couldn't reach Ask The W. Captures are queued; recap will be available when you're back online.",
|
|
1099
208
|
});
|
|
1100
209
|
}
|
|
1101
|
-
if (payload.idempotencyKey) {
|
|
1102
|
-
const existingId = localStore.getMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`);
|
|
1103
|
-
if (existingId) {
|
|
1104
|
-
const existing = localStore.getDecision(existingId);
|
|
1105
|
-
if (existing) {
|
|
1106
|
-
return localResponse({
|
|
1107
|
-
ok: true,
|
|
1108
|
-
id: existing.id,
|
|
1109
|
-
sequence: localStore.stats().decisions,
|
|
1110
|
-
decision: decisionWithSignals(localStore, existing),
|
|
1111
|
-
linkedSignalId: signal.id,
|
|
1112
|
-
idempotent: true,
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
const decision = localStore.createDecision({
|
|
1118
|
-
rawContent: signal.summary,
|
|
1119
|
-
headline: signal.summary,
|
|
1120
|
-
why: payload.why ?? null,
|
|
1121
|
-
status: payload.status,
|
|
1122
|
-
sessionId: signal.sessionId,
|
|
1123
|
-
files: signal.filesTouched,
|
|
1124
|
-
sourceSignalIds: [signal.id],
|
|
1125
|
-
scopeKey: signal.scopeKey ?? currentScopeKey(),
|
|
1126
|
-
});
|
|
1127
|
-
if (payload.idempotencyKey) {
|
|
1128
|
-
localStore.setMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`, decision.id);
|
|
1129
|
-
}
|
|
1130
|
-
const warnings = detectDecisionConflicts({
|
|
1131
|
-
decision,
|
|
1132
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey: decision.scopeKey }),
|
|
1133
|
-
});
|
|
1134
|
-
return localResponse({
|
|
1135
|
-
ok: true,
|
|
1136
|
-
id: decision.id,
|
|
1137
|
-
sequence: localStore.stats().decisions,
|
|
1138
|
-
decision: decisionWithSignals(localStore, decision),
|
|
1139
|
-
linkedSignalId: signal.id,
|
|
1140
|
-
warnings,
|
|
1141
|
-
});
|
|
1142
210
|
});
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
const finalSummaryCount = input.signals.filter((signal) => signal.kind === "final_summary").length;
|
|
1150
|
-
const decisionCount = input.decisions.length;
|
|
1151
|
-
const hasVerification = verificationCount > 0;
|
|
1152
|
-
const hasDecision = decisionCount > 0;
|
|
1153
|
-
const hasDirection = directionCount > 0;
|
|
1154
|
-
const nudge = implementationCount >= 3 && verificationCount === 0
|
|
1155
|
-
? `You captured ${implementationCount} implementation updates but no verification_result; run one check and capture it before ending.`
|
|
1156
|
-
: input.signals.length >= 6 && finalSummaryCount === 0
|
|
1157
|
-
? `You captured ${input.signals.length} signals but no final_summary; close the session with the outcome and remaining risk.`
|
|
1158
|
-
: decisionCount === 0 && directionCount > 0
|
|
1159
|
-
? "Direction changed, but no decision was captured; promote the clearest direction_change signal."
|
|
1160
|
-
: !hasDecision
|
|
1161
|
-
? "Promote the clearest captured signal into a decision before the trail goes stale."
|
|
1162
|
-
: !hasVerification
|
|
1163
|
-
? "Capture one verification result so the decision trail records whether the work actually held."
|
|
1164
|
-
: implementationCount > decisionCount * 3
|
|
1165
|
-
? "There are many implementation updates per decision; collapse the important why into one decision."
|
|
1166
|
-
: "The trail is usable. Keep the next decision tied to a verification result.";
|
|
1167
|
-
return { nudge };
|
|
1168
|
-
}
|
|
1169
|
-
function decisionalWeight(signal) {
|
|
1170
|
-
const kindWeight = signal.kind === "direction_change" ? 4 : signal.kind === "verification_result" ? 3 : signal.kind === "implementation_update" ? 2 : 1;
|
|
1171
|
-
const textWeight = /\b(decid|chose|commit|reject|approve|ship|verify|blocked|risk)\b/i.test(signal.summary) ? 2 : 0;
|
|
1172
|
-
return kindWeight + textWeight;
|
|
1173
|
-
}
|
|
1174
|
-
function renderRecap(input) {
|
|
1175
|
-
if (input.format === "standup") {
|
|
1176
|
-
const blockers = input.signals.filter((signal) => /\b(block|fail|error|risk|stuck)\b/i.test(signal.summary));
|
|
1177
|
-
return [
|
|
1178
|
-
"# Standup Recap",
|
|
1179
|
-
"## Yesterday",
|
|
1180
|
-
input.decisions.length ? `- Captured ${input.decisions.length} decisions.` : "- No decisions captured.",
|
|
1181
|
-
"## Today",
|
|
1182
|
-
input.signals.length ? `- Review ${input.signals.length} session signals and promote the strongest one.` : "- Capture the first useful signal.",
|
|
1183
|
-
"## Blockers",
|
|
1184
|
-
...(blockers.length ? blockers.slice(0, 5).map((signal) => `- ${signal.summary}`) : ["- None captured."]),
|
|
1185
|
-
].join("\n");
|
|
1186
|
-
}
|
|
1187
|
-
if (input.format === "share") {
|
|
1188
|
-
return [
|
|
1189
|
-
"# Ask The W Session Share",
|
|
1190
|
-
"",
|
|
1191
|
-
`Signals captured: ${input.signals.length}`,
|
|
1192
|
-
`Decisions captured: ${input.decisions.length}`,
|
|
1193
|
-
"",
|
|
1194
|
-
"## Highlights",
|
|
1195
|
-
...input.signals
|
|
1196
|
-
.slice()
|
|
1197
|
-
.sort((left, right) => decisionalWeight(right) - decisionalWeight(left))
|
|
1198
|
-
.slice(0, 8)
|
|
1199
|
-
.map((signal) => `- ${signal.summary}`),
|
|
1200
|
-
"",
|
|
1201
|
-
"_Captured by Ask The W._",
|
|
1202
|
-
].join("\n");
|
|
1203
|
-
}
|
|
1204
|
-
return [
|
|
1205
|
-
"# Session Digest",
|
|
1206
|
-
"",
|
|
1207
|
-
...input.signals
|
|
1208
|
-
.slice()
|
|
1209
|
-
.sort((left, right) => decisionalWeight(right) - decisionalWeight(left))
|
|
1210
|
-
.slice(0, 28)
|
|
1211
|
-
.map((signal, index) => `${index + 1}. [${signal.kind}] ${signal.summary}`),
|
|
1212
|
-
].join("\n").split("\n").slice(0, 30).join("\n");
|
|
1213
|
-
}
|
|
1214
|
-
function renderSessionFeed(signals) {
|
|
1215
|
-
if (signals.length === 0) {
|
|
1216
|
-
return "# Session\n\nNo local signals captured yet.";
|
|
1217
|
-
}
|
|
1218
|
-
return [
|
|
1219
|
-
"# Session",
|
|
1220
|
-
"",
|
|
1221
|
-
...signals.map((signal) => [
|
|
1222
|
-
`## ${signal.sequence ?? signal.id}. ${signal.kind}`,
|
|
1223
|
-
signal.summary,
|
|
1224
|
-
`- captured: ${signal.capturedAt}`,
|
|
1225
|
-
signal.filesTouched.length ? `- files: ${signal.filesTouched.join(", ")}` : "- files: none",
|
|
1226
|
-
signal.commandsRun.length ? `- commands: ${signal.commandsRun.join(" | ")}` : "- commands: none",
|
|
1227
|
-
].join("\n")),
|
|
1228
|
-
].join("\n\n");
|
|
1229
|
-
}
|
|
1230
|
-
function compactSignal(signal, decision) {
|
|
1231
|
-
return {
|
|
1232
|
-
id: signal.id,
|
|
1233
|
-
kind: signal.kind,
|
|
1234
|
-
summary: signal.summary,
|
|
1235
|
-
files: signal.filesTouched,
|
|
1236
|
-
decisionId: decision?.id ?? null,
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
function decisionWithSignals(store, decision) {
|
|
1240
|
-
return {
|
|
1241
|
-
...decision,
|
|
1242
|
-
contributingSignals: store.listSignalsByIds(decision.sourceSignalIds),
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
function signalWithDecision(store, signal) {
|
|
1246
|
-
const decision = store.getDecisionForSignal(signal.id);
|
|
1247
|
-
return {
|
|
1248
|
-
...signal,
|
|
1249
|
-
decisionId: decision?.id ?? null,
|
|
1250
|
-
decision: decision
|
|
1251
|
-
? {
|
|
1252
|
-
id: decision.id,
|
|
1253
|
-
headline: decision.headline,
|
|
1254
|
-
status: decision.status,
|
|
1255
|
-
why: decision.why,
|
|
1256
|
-
}
|
|
1257
|
-
: null,
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
function candidateFromSignal(signal, linkedDecision) {
|
|
1261
|
-
if (linkedDecision)
|
|
1262
|
-
return null;
|
|
1263
|
-
const text = [signal.summary, ...signal.evidence.map((entry) => {
|
|
1264
|
-
if (entry && typeof entry === "object") {
|
|
1265
|
-
const record = entry;
|
|
1266
|
-
return [record.excerpt, record.diff, record.before, record.after].filter(Boolean).join(" ");
|
|
1267
|
-
}
|
|
1268
|
-
return String(entry ?? "");
|
|
1269
|
-
})].join(" ");
|
|
1270
|
-
const hasDecisionLanguage = /\b(decid(?:e|ed|ing)?|chose|choose|commit(?:ted)?|approved?|reject(?:ed)?|let'?s go with|go with|we will|we're going to|standardize|adopt|defer|drop|keep|remove|replace)\b/i.test(text);
|
|
1271
|
-
if (!hasDecisionLanguage && signal.kind !== "direction_change") {
|
|
1272
|
-
return null;
|
|
1273
|
-
}
|
|
1274
|
-
const because = /\bbecause\b/i.test(text);
|
|
1275
|
-
return {
|
|
1276
|
-
id: `candidate_${signal.id}`,
|
|
1277
|
-
signalId: signal.id,
|
|
1278
|
-
sessionId: signal.sessionId,
|
|
1279
|
-
summary: signal.summary,
|
|
1280
|
-
suggestedStatus: signal.kind === "verification_result" ? "shipped" : "proposed",
|
|
1281
|
-
why: because ? "The signal includes an explicit because/reason clause." : "The signal uses decision language.",
|
|
1282
|
-
files: signal.filesTouched,
|
|
1283
|
-
capturedAt: signal.capturedAt,
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
function normalizedDecisionTerms(text) {
|
|
1287
|
-
const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "keep", "use", "adopt", "remove", "drop", "replace", "defer", "ship", "commit"]);
|
|
1288
|
-
return String(text ?? "")
|
|
1289
|
-
.toLowerCase()
|
|
1290
|
-
.replace(/[^a-z0-9\s-]/g, " ")
|
|
1291
|
-
.split(/\s+/)
|
|
1292
|
-
.filter((term) => term.length >= 4 && !stop.has(term));
|
|
1293
|
-
}
|
|
1294
|
-
function decisionPolarity(text) {
|
|
1295
|
-
if (/\b(remove|drop|abandon|defer|reject|disable|stop|sunset|do not|don't|won't|will not)\b/i.test(text))
|
|
1296
|
-
return "negative";
|
|
1297
|
-
if (/\b(keep|use|adopt|enable|add|ship|commit|standardize|choose|approve|go with)\b/i.test(text))
|
|
1298
|
-
return "positive";
|
|
1299
|
-
return "neutral";
|
|
1300
|
-
}
|
|
1301
|
-
function detectDecisionConflicts(input) {
|
|
1302
|
-
const polarity = decisionPolarity(`${input.decision.headline} ${input.decision.rawContent}`);
|
|
1303
|
-
if (polarity === "neutral")
|
|
1304
|
-
return [];
|
|
1305
|
-
const terms = new Set(normalizedDecisionTerms(`${input.decision.headline} ${input.decision.rawContent}`));
|
|
1306
|
-
if (terms.size === 0)
|
|
1307
|
-
return [];
|
|
1308
|
-
return input.decisions
|
|
1309
|
-
.filter((prior) => prior.id !== input.decision.id)
|
|
1310
|
-
.filter((prior) => !input.decision.scopeKey || prior.scopeKey === input.decision.scopeKey)
|
|
1311
|
-
.map((prior) => {
|
|
1312
|
-
const priorPolarity = decisionPolarity(`${prior.headline} ${prior.rawContent}`);
|
|
1313
|
-
const priorTerms = normalizedDecisionTerms(`${prior.headline} ${prior.rawContent}`);
|
|
1314
|
-
const overlap = priorTerms.filter((term) => terms.has(term));
|
|
1315
|
-
return {
|
|
1316
|
-
prior,
|
|
1317
|
-
priorPolarity,
|
|
1318
|
-
overlap,
|
|
1319
|
-
};
|
|
1320
|
-
})
|
|
1321
|
-
.filter((entry) => entry.priorPolarity !== "neutral" && entry.priorPolarity !== polarity && entry.overlap.length > 0)
|
|
1322
|
-
.slice(0, 3)
|
|
1323
|
-
.map((entry) => ({
|
|
1324
|
-
code: "possible_conflict",
|
|
1325
|
-
message: `This may conflict with "${entry.prior.headline}".`,
|
|
1326
|
-
conflictingDecisionId: entry.prior.id,
|
|
1327
|
-
overlappingTerms: entry.overlap.slice(0, 5),
|
|
1328
|
-
}));
|
|
1329
|
-
}
|
|
1330
|
-
const isDirectIndexExecution = Boolean(process.argv[1]) &&
|
|
1331
|
-
(() => {
|
|
1332
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
1333
|
-
const invokedPath = path.resolve(process.argv[1]);
|
|
211
|
+
server.tool("coach", {
|
|
212
|
+
focus: z.string().max(1000).optional(),
|
|
213
|
+
}, async (payload) => {
|
|
214
|
+
const currentClient = client();
|
|
215
|
+
if (!currentClient.hasToken())
|
|
216
|
+
return needsSignup();
|
|
1334
217
|
try {
|
|
1335
|
-
|
|
218
|
+
const result = await currentClient.coach(payload);
|
|
219
|
+
return toolJson(result.body);
|
|
1336
220
|
}
|
|
1337
221
|
catch {
|
|
1338
|
-
return
|
|
222
|
+
return toolJson({
|
|
223
|
+
ok: false,
|
|
224
|
+
code: "unreachable",
|
|
225
|
+
message: "Couldn't reach Ask The W. Captures are queued; coach will be available when you're back online.",
|
|
226
|
+
});
|
|
1339
227
|
}
|
|
1340
|
-
})();
|
|
1341
|
-
export async function runInitializeHandshake(input = {}) {
|
|
1342
|
-
const modulePath = input.entrypoint ?? fileURLToPath(import.meta.url);
|
|
1343
|
-
const child = spawn(process.execPath, [modulePath], {
|
|
1344
|
-
cwd: process.cwd(),
|
|
1345
|
-
env: input.env ?? process.env,
|
|
1346
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1347
|
-
});
|
|
1348
|
-
let stdout = "";
|
|
1349
|
-
let stderr = "";
|
|
1350
|
-
child.stdout.setEncoding("utf8");
|
|
1351
|
-
child.stderr.setEncoding("utf8");
|
|
1352
|
-
child.stdout.on("data", (chunk) => {
|
|
1353
|
-
stdout += chunk;
|
|
1354
228
|
});
|
|
1355
|
-
|
|
1356
|
-
stderr += chunk;
|
|
1357
|
-
});
|
|
1358
|
-
const initialize = {
|
|
1359
|
-
jsonrpc: "2.0",
|
|
1360
|
-
id: 1,
|
|
1361
|
-
method: "initialize",
|
|
1362
|
-
params: {
|
|
1363
|
-
protocolVersion: "2024-11-05",
|
|
1364
|
-
capabilities: {},
|
|
1365
|
-
clientInfo: { name: "askthew-initialize-handshake", version: "1.0.0" },
|
|
1366
|
-
},
|
|
1367
|
-
};
|
|
1368
|
-
child.stdin.write(`${JSON.stringify(initialize)}\n`);
|
|
1369
|
-
try {
|
|
1370
|
-
return await new Promise((resolve, reject) => {
|
|
1371
|
-
const timeout = setTimeout(() => {
|
|
1372
|
-
reject(new Error(`Timed out waiting for initialize response. stdout=${stdout} stderr=${stderr}`));
|
|
1373
|
-
}, input.timeoutMs ?? 3000);
|
|
1374
|
-
const failOnExit = (code) => {
|
|
1375
|
-
clearTimeout(timeout);
|
|
1376
|
-
reject(new Error(`MCP stdio server exited before initialize. code=${code} stdout=${stdout} stderr=${stderr}`));
|
|
1377
|
-
};
|
|
1378
|
-
const check = () => {
|
|
1379
|
-
const lineEnd = stdout.indexOf("\n");
|
|
1380
|
-
if (lineEnd === -1)
|
|
1381
|
-
return;
|
|
1382
|
-
clearTimeout(timeout);
|
|
1383
|
-
child.off("exit", failOnExit);
|
|
1384
|
-
const response = JSON.parse(stdout.slice(0, lineEnd));
|
|
1385
|
-
if (response.id !== 1 || response.jsonrpc !== "2.0" || !response.result) {
|
|
1386
|
-
reject(new Error(`Unexpected initialize response: ${JSON.stringify(response)}`));
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
resolve({
|
|
1390
|
-
serverInfoVersion: typeof response.result?.serverInfo?.version === "string"
|
|
1391
|
-
? response.result.serverInfo.version
|
|
1392
|
-
: undefined,
|
|
1393
|
-
});
|
|
1394
|
-
};
|
|
1395
|
-
child.stdout.on("data", check);
|
|
1396
|
-
child.once("exit", failOnExit);
|
|
1397
|
-
check();
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
finally {
|
|
1401
|
-
child.kill();
|
|
1402
|
-
}
|
|
229
|
+
return server;
|
|
1403
230
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
console.log("Ask The W MCP initialize handshake succeeded.");
|
|
1409
|
-
})
|
|
1410
|
-
.catch((error) => {
|
|
1411
|
-
if (error instanceof Error) {
|
|
1412
|
-
console.error(error.message);
|
|
1413
|
-
}
|
|
1414
|
-
else {
|
|
1415
|
-
console.error("Ask The W MCP initialize handshake failed.", error);
|
|
1416
|
-
}
|
|
1417
|
-
process.exit(1);
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
else {
|
|
1421
|
-
const server = createAskTheWMcpServer();
|
|
1422
|
-
const transport = new StdioServerTransport();
|
|
1423
|
-
server.connect(transport).catch((error) => {
|
|
1424
|
-
if (error instanceof Error) {
|
|
1425
|
-
console.error(error.message);
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
console.error("Ask The W MCP server failed to start.", error);
|
|
1429
|
-
}
|
|
1430
|
-
process.exit(1);
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
231
|
+
export async function runStdioServer(options = {}) {
|
|
232
|
+
const server = createAskTheWMcpServer(options);
|
|
233
|
+
const transport = new StdioServerTransport();
|
|
234
|
+
await server.connect(transport);
|
|
1433
235
|
}
|