@askthew/mcp-plugin 0.4.9 → 0.4.11
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 -181
- 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 +157 -1845
- 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 { paidDescription, 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,1884 +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
|
-
}
|
|
271
|
-
return {
|
|
272
|
-
...parsed,
|
|
273
|
-
decision: redactRawSignalText(parsed.decision),
|
|
274
|
-
rationale: redactRawSignalText(parsed.rationale),
|
|
275
|
-
framework: typeof parsed.framework === "string" ? redactRawSignalText(parsed.framework) : undefined,
|
|
276
|
-
filesAffected: parsed.filesAffected.map((filePath) => sanitizeFilePath(filePath)),
|
|
277
|
-
originatingPrompt: typeof parsed.originatingPrompt === "string"
|
|
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
|
-
}
|
|
57
|
+
function toolJson(value) {
|
|
291
58
|
return {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
function traceId() {
|
|
325
|
-
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
326
|
-
}
|
|
327
|
-
function structuredError(input) {
|
|
328
|
-
return {
|
|
67
|
+
function needsSignup() {
|
|
68
|
+
return toolJson({
|
|
329
69
|
ok: false,
|
|
330
|
-
code:
|
|
331
|
-
message:
|
|
332
|
-
|
|
333
|
-
hint: input.hint ?? "",
|
|
334
|
-
traceId: input.traceId ?? traceId(),
|
|
335
|
-
...(typeof input.status === "number" ? { status: input.status } : {}),
|
|
336
|
-
...(input.extra ?? {}),
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
function withResponseShape(route, responseShape = "v2") {
|
|
340
|
-
const [path, query = ""] = route.split("?");
|
|
341
|
-
const searchParams = new URLSearchParams(query);
|
|
342
|
-
searchParams.set("response_shape", responseShape);
|
|
343
|
-
const nextQuery = searchParams.toString();
|
|
344
|
-
return nextQuery ? `${path}?${nextQuery}` : path;
|
|
345
|
-
}
|
|
346
|
-
function routeWithQuery(route, params) {
|
|
347
|
-
const searchParams = new URLSearchParams();
|
|
348
|
-
for (const [key, value] of Object.entries(params)) {
|
|
349
|
-
if (value === undefined || value === null || value === "") {
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
searchParams.set(key, String(value));
|
|
353
|
-
}
|
|
354
|
-
const query = searchParams.toString();
|
|
355
|
-
return query ? `${route}?${query}` : route;
|
|
356
|
-
}
|
|
357
|
-
function credentials(overrides) {
|
|
358
|
-
const legacyHostType = process.env.ASKTHEW_HOST_TYPE?.trim();
|
|
359
|
-
const hostType = legacyHostType === "claude_code" || legacyHostType === "codex" || legacyHostType === "cursor"
|
|
360
|
-
? legacyHostType
|
|
361
|
-
: undefined;
|
|
362
|
-
const overrideHostType = overrides?.hostType;
|
|
363
|
-
const clientId = normalizeClientId(overrides?.clientId) ||
|
|
364
|
-
normalizeClientId(process.env.ASKTHEW_CLIENT_ID?.trim()) ||
|
|
365
|
-
overrideHostType ||
|
|
366
|
-
hostType ||
|
|
367
|
-
"mcp_client";
|
|
368
|
-
return {
|
|
369
|
-
installToken: normalizeInstallTokenInput(overrides?.installToken) ||
|
|
370
|
-
normalizeInstallTokenInput(process.env.ASKTHEW_INSTALL_TOKEN),
|
|
371
|
-
userId: overrides?.userId?.trim() || process.env.ASKTHEW_USER_ID?.trim(),
|
|
372
|
-
apiKey: overrides?.apiKey?.trim() || process.env.ASKTHEW_API_KEY?.trim(),
|
|
373
|
-
serverName: overrides?.serverName?.trim() || process.env.ASKTHEW_SERVER_NAME?.trim(),
|
|
374
|
-
clientId,
|
|
375
|
-
clientLabel: overrides?.clientLabel?.trim() || process.env.ASKTHEW_CLIENT_LABEL?.trim(),
|
|
376
|
-
hostType: overrideHostType || hostType,
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
function hasServerIdentity(overrides) {
|
|
380
|
-
const { installToken, userId } = credentials(overrides);
|
|
381
|
-
return Boolean(installToken || userId);
|
|
382
|
-
}
|
|
383
|
-
async function postToServer(route, payload, options, request) {
|
|
384
|
-
if (!hasServerIdentity(options?.credentials)) {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
const { installToken, userId, apiKey, clientId, clientLabel, hostType } = credentials(options?.credentials);
|
|
388
|
-
const fetcher = options?.fetchImpl ?? fetch;
|
|
389
|
-
const method = request?.method ?? "POST";
|
|
390
|
-
const bodyPayload = {
|
|
391
|
-
...payload,
|
|
392
|
-
installToken: installToken || undefined,
|
|
393
|
-
userId: userId || undefined,
|
|
394
|
-
clientId,
|
|
395
|
-
clientLabel: clientLabel || undefined,
|
|
396
|
-
hostType: hostType || undefined,
|
|
397
|
-
};
|
|
398
|
-
const response = await fetcher(`${apiBaseUrl(options?.apiBaseUrl)}${route}`, {
|
|
399
|
-
method,
|
|
400
|
-
headers: {
|
|
401
|
-
...(method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
402
|
-
...(request?.idempotencyKey ? { "Idempotency-Key": request.idempotencyKey } : {}),
|
|
403
|
-
...(installToken
|
|
404
|
-
? { Authorization: `Bearer ${installToken}` }
|
|
405
|
-
: apiKey
|
|
406
|
-
? { Authorization: `Bearer ${apiKey}` }
|
|
407
|
-
: {}),
|
|
408
|
-
},
|
|
409
|
-
...(method === "GET" ? {} : { body: JSON.stringify(bodyPayload) }),
|
|
410
|
-
}).catch(() => null);
|
|
411
|
-
if (!response) {
|
|
412
|
-
return structuredError({
|
|
413
|
-
code: "network_error",
|
|
414
|
-
message: "Ask The W server could not be reached.",
|
|
415
|
-
retryable: true,
|
|
416
|
-
hint: "Check your network connection or retry the same idempotency key.",
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
const body = await response.json().catch(() => null);
|
|
420
|
-
if (!response.ok) {
|
|
421
|
-
if (body && typeof body === "object") {
|
|
422
|
-
const record = body;
|
|
423
|
-
return {
|
|
424
|
-
...structuredError({
|
|
425
|
-
code: typeof record.code === "string" ? record.code : "http_error",
|
|
426
|
-
message: typeof record.message === "string"
|
|
427
|
-
? record.message
|
|
428
|
-
: typeof record.error === "string"
|
|
429
|
-
? record.error
|
|
430
|
-
: `Ask The W request failed with HTTP ${response.status}.`,
|
|
431
|
-
retryable: response.status === 429 || response.status >= 500,
|
|
432
|
-
hint: typeof record.hint === "string"
|
|
433
|
-
? record.hint
|
|
434
|
-
: response.status >= 500
|
|
435
|
-
? "Retry with the same Idempotency-Key if this was a write."
|
|
436
|
-
: "Check the tool input and retry.",
|
|
437
|
-
traceId: typeof record.traceId === "string" ? record.traceId : undefined,
|
|
438
|
-
status: response.status,
|
|
439
|
-
}),
|
|
440
|
-
...record,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
return structuredError({
|
|
444
|
-
code: "http_error",
|
|
445
|
-
message: `Ask The W request failed with HTTP ${response.status}.`,
|
|
446
|
-
retryable: response.status === 429 || response.status >= 500,
|
|
447
|
-
hint: response.status >= 500 ? "Retry with the same Idempotency-Key if this was a write." : "Check the tool input and retry.",
|
|
448
|
-
status: response.status,
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
return body;
|
|
452
|
-
}
|
|
453
|
-
function runtimeMetadata(options) {
|
|
454
|
-
const scope = resolvePluginScope(process.cwd());
|
|
455
|
-
const { serverName, clientId, clientLabel } = credentials(options?.credentials);
|
|
456
|
-
const extraMetadata = typeof options?.runtimeMetadata === "function"
|
|
457
|
-
? options.runtimeMetadata()
|
|
458
|
-
: (options?.runtimeMetadata ?? {});
|
|
459
|
-
return {
|
|
460
|
-
repository: scope.repoName,
|
|
461
|
-
repo_name: scope.repoName,
|
|
462
|
-
scope_key: localScopeKey(),
|
|
463
|
-
...(scope.repoRoot ? { repo_root: sanitizeFilePath(scope.repoRoot) } : {}),
|
|
464
|
-
...(scope.appPath ? { app_path: sanitizeFilePath(scope.appPath) } : {}),
|
|
465
|
-
...(scope.serviceName ? { service_name: scope.serviceName } : {}),
|
|
466
|
-
...(serverName ? { server_name: serverName } : {}),
|
|
467
|
-
...(clientId ? { client_id: clientId } : {}),
|
|
468
|
-
...(clientLabel ? { client_label: clientLabel } : {}),
|
|
469
|
-
...extraMetadata,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
function localScopeKey(cwd = process.cwd()) {
|
|
473
|
-
const scope = resolvePluginScope(cwd);
|
|
474
|
-
return [scope.repoRoot || scope.repoName || cwd, scope.appPath ?? "", scope.serviceName ?? ""]
|
|
475
|
-
.filter(Boolean)
|
|
476
|
-
.join("::")
|
|
477
|
-
.replace(/\s+/g, " ")
|
|
478
|
-
.slice(0, 500);
|
|
479
|
-
}
|
|
480
|
-
function startupSessionId(options) {
|
|
481
|
-
const scope = resolvePluginScope(process.cwd());
|
|
482
|
-
const { clientId } = credentials(options?.credentials);
|
|
483
|
-
const scopeKey = [scope.repoName, scope.appPath, scope.serviceName]
|
|
484
|
-
.filter((part) => Boolean(part))
|
|
485
|
-
.join(":") || "workspace";
|
|
486
|
-
return ["mcp-startup", clientId || "mcp_client", scopeKey]
|
|
487
|
-
.join(":")
|
|
488
|
-
.replace(/[^a-zA-Z0-9:_-]+/g, "_")
|
|
489
|
-
.slice(0, 240);
|
|
490
|
-
}
|
|
491
|
-
function loginCommandHint() {
|
|
492
|
-
for (const value of [
|
|
493
|
-
process.env.ASKTHEW_EMAIL,
|
|
494
|
-
process.env.GIT_AUTHOR_EMAIL,
|
|
495
|
-
process.env.GIT_COMMITTER_EMAIL,
|
|
496
|
-
process.env.EMAIL,
|
|
497
|
-
]) {
|
|
498
|
-
const email = String(value ?? "").trim();
|
|
499
|
-
if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
|
500
|
-
return `npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp identify --email ${email}`;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
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
|
+
});
|
|
504
73
|
}
|
|
505
|
-
|
|
506
|
-
if (!hasServerIdentity(options?.credentials)) {
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
const { installToken, apiKey, clientId, clientLabel, hostType, serverName } = credentials(options?.credentials);
|
|
510
|
-
if (!installToken) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
74
|
+
function defaultScopeKey() {
|
|
513
75
|
const scope = resolvePluginScope(process.cwd());
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
520
|
-
},
|
|
521
|
-
body: JSON.stringify({
|
|
522
|
-
installToken,
|
|
523
|
-
clientId,
|
|
524
|
-
...(clientLabel ? { clientLabel } : {}),
|
|
525
|
-
hostType,
|
|
526
|
-
...(serverName ? { serverName } : {}),
|
|
527
|
-
repoName: scope.repoName,
|
|
528
|
-
...(scope.repoRoot ? { repoRoot: scope.repoRoot } : {}),
|
|
529
|
-
...(scope.appPath ? { appPath: scope.appPath } : {}),
|
|
530
|
-
...(scope.serviceName ? { serviceName: scope.serviceName } : {}),
|
|
531
|
-
}),
|
|
532
|
-
}).catch(() => null);
|
|
533
|
-
}
|
|
534
|
-
async function sendStartupSetupSignal(options) {
|
|
535
|
-
const sessionSignal = {
|
|
536
|
-
sessionId: startupSessionId(options),
|
|
537
|
-
sequence: 0,
|
|
538
|
-
kind: "setup_complete",
|
|
539
|
-
summary: "Ask The W plugin server started for this coding-agent session.",
|
|
540
|
-
evidence: [],
|
|
541
|
-
filesTouched: [],
|
|
542
|
-
commandsRun: [],
|
|
543
|
-
metadata: {
|
|
544
|
-
...runtimeMetadata(options),
|
|
545
|
-
automated: true,
|
|
546
|
-
operational: true,
|
|
547
|
-
origin: "mcp_server_startup",
|
|
548
|
-
},
|
|
549
|
-
};
|
|
550
|
-
await postToServer("/api/ingest/mcp", {
|
|
551
|
-
sessionSignal: redactCodingSessionSignal(sessionSignal),
|
|
552
|
-
}, options);
|
|
553
|
-
}
|
|
554
|
-
async function sendStartupSignals(options) {
|
|
555
|
-
await sendStartupHeartbeat(options).catch(() => null);
|
|
556
|
-
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";
|
|
557
81
|
}
|
|
558
82
|
export function createAskTheWMcpServer(options = {}) {
|
|
559
|
-
const initialMode = resolveMcpMode();
|
|
560
|
-
if (initialMode.mode === "free_pending_auth") {
|
|
561
|
-
ensureLocalIdentity({
|
|
562
|
-
emailClaim: process.env.ASKTHEW_EMAIL,
|
|
563
|
-
apiUrl: options.apiBaseUrl,
|
|
564
|
-
telemetryOptOut: process.env.ASKTHEW_TELEMETRY === "off",
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
const resolvedMode = resolveMcpMode();
|
|
568
|
-
const optionInstallToken = normalizeInstallTokenInput(options.credentials?.installToken);
|
|
569
|
-
const mode = optionInstallToken
|
|
570
|
-
? { mode: "paid", installToken: optionInstallToken, reason: "options_install_token" }
|
|
571
|
-
: resolvedMode;
|
|
572
|
-
const localStore = mode.mode === "paid" ? null : LocalStore.open();
|
|
573
|
-
if (localStore && mode.mode === "free" && mode.cliCredentials) {
|
|
574
|
-
void flushTelemetryOutbox({
|
|
575
|
-
store: localStore,
|
|
576
|
-
credentials: mode.cliCredentials,
|
|
577
|
-
apiUrl: options.apiBaseUrl,
|
|
578
|
-
fetchImpl: options.fetchImpl,
|
|
579
|
-
}).catch(() => null);
|
|
580
|
-
}
|
|
581
83
|
const server = new McpServer({
|
|
582
|
-
name: "Ask The W
|
|
84
|
+
name: "Ask The W MCP",
|
|
583
85
|
version: packageVersion(),
|
|
584
86
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
return localResponse(next);
|
|
620
|
-
};
|
|
621
|
-
const currentScopeKey = () => localScopeKey();
|
|
622
|
-
const compactWriteResponse = (input) => localResponse({
|
|
623
|
-
ok: input.ok !== false,
|
|
624
|
-
id: input.id ?? null,
|
|
625
|
-
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
626
|
-
...(typeof input.sequence === "number" ? { sequence: input.sequence } : {}),
|
|
627
|
-
...(typeof input.signalCount === "number" ? { signalCount: input.signalCount } : {}),
|
|
628
|
-
...(input.warnings && input.warnings.length > 0 ? { warnings: input.warnings } : {}),
|
|
87
|
+
const outbox = options.outbox ?? new Outbox();
|
|
88
|
+
const client = () => new AskTheWCloudClient({
|
|
89
|
+
apiUrl: options.apiUrl,
|
|
90
|
+
fetcher: options.fetcher,
|
|
91
|
+
env: options.env,
|
|
92
|
+
});
|
|
93
|
+
server.tool("askthew_start_signup", {
|
|
94
|
+
email: z.string().email(),
|
|
95
|
+
clientHint: z.enum(["claude_code", "codex", "cursor", "web_agent"]).optional(),
|
|
96
|
+
}, async (payload) => {
|
|
97
|
+
const body = await startSignup({
|
|
98
|
+
email: payload.email,
|
|
99
|
+
apiUrl: options.apiUrl,
|
|
100
|
+
clientHint: payload.clientHint,
|
|
101
|
+
fetcher: options.fetcher,
|
|
102
|
+
});
|
|
103
|
+
return toolJson(body);
|
|
104
|
+
});
|
|
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(),
|
|
109
|
+
}, async (payload) => {
|
|
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);
|
|
629
120
|
});
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
record.signalId ??
|
|
636
|
-
record.entry?.id ??
|
|
637
|
-
record.decision?.id ??
|
|
638
|
-
record.data?.id ??
|
|
639
|
-
record.data?.outcome?.id ??
|
|
640
|
-
record.data?.decision?.id ??
|
|
641
|
-
null);
|
|
642
|
-
};
|
|
643
|
-
const upstreamSequence = (upstream) => {
|
|
644
|
-
if (!upstream || typeof upstream !== "object")
|
|
645
|
-
return null;
|
|
646
|
-
const record = upstream;
|
|
647
|
-
const sequence = record.sequence ?? record.entry?.sequence ?? record.data?.sequence ?? null;
|
|
648
|
-
return typeof sequence === "number" ? sequence : null;
|
|
649
|
-
};
|
|
650
|
-
const upstreamFailure = (upstream) => upstream && typeof upstream === "object" && upstream.ok === false
|
|
651
|
-
? upstream
|
|
652
|
-
: null;
|
|
653
|
-
const requireFreeIdentity = () => {
|
|
654
|
-
if (mode.mode === "unauthenticated" || mode.mode === "free_pending_auth") {
|
|
655
|
-
const loginCommand = loginCommandHint();
|
|
656
|
-
return localToolError({
|
|
657
|
-
code: "free_tier_login_required",
|
|
658
|
-
message: "Free local mode needs a local install identity before capture.",
|
|
659
|
-
retryable: false,
|
|
660
|
-
hint: `Run \`${loginCommand}\` or reinstall with \`--free --email <your-email>\`, then restart or reload the MCP host.`,
|
|
661
|
-
extra: {
|
|
662
|
-
loginCommand,
|
|
663
|
-
supportEmail: "support@askthew.com",
|
|
664
|
-
},
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
return null;
|
|
668
|
-
};
|
|
669
|
-
const localSessionAnalysisResponse = (payload) => {
|
|
670
|
-
if (!localStore) {
|
|
671
|
-
return localToolError({
|
|
672
|
-
code: "local_store_unavailable",
|
|
673
|
-
message: "The local Ask The W store is unavailable.",
|
|
674
|
-
retryable: true,
|
|
675
|
-
hint: "Retry after restarting the plugin host.",
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
const loginRequired = requireFreeIdentity();
|
|
679
|
-
if (loginRequired)
|
|
680
|
-
return loginRequired;
|
|
681
|
-
const scopeKey = currentScopeKey();
|
|
682
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
683
|
-
const allSessionIds = localStore.listSessionIds({ limit: 100000, scopeKey });
|
|
684
|
-
const allowedSessionIds = new Set(allSessionIds.slice(0, 3));
|
|
685
|
-
if (sessionId && allSessionIds.length > 3 && !allowedSessionIds.has(sessionId)) {
|
|
686
|
-
return localToolError({
|
|
687
|
-
code: "free_tier_limit",
|
|
688
|
-
message: "The free plugin can analyze the latest three local sessions.",
|
|
689
|
-
retryable: false,
|
|
690
|
-
hint: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
691
|
-
extra: {
|
|
692
|
-
tool: "analyze_session",
|
|
693
|
-
limit: 3,
|
|
694
|
-
upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=analyze_session",
|
|
695
|
-
cta: "Upgrade to analyze more than three sessions in the workspace dashboard.",
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
const limit = Math.min(50, payload.limit ?? 50);
|
|
700
|
-
const signals = sessionId
|
|
701
|
-
? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
|
|
702
|
-
: [];
|
|
703
|
-
const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
704
|
-
const decisions = sessionId
|
|
705
|
-
? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
|
|
706
|
-
: [];
|
|
707
|
-
const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
|
|
708
|
-
const counts = signals.reduce((accumulator, signal) => {
|
|
709
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
710
|
-
return accumulator;
|
|
711
|
-
}, {});
|
|
712
|
-
const allCounts = allSignals.reduce((accumulator, signal) => {
|
|
713
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
714
|
-
return accumulator;
|
|
715
|
-
}, {});
|
|
716
|
-
const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
|
|
717
|
-
if ((payload.format ?? "markdown") === "json") {
|
|
718
|
-
return budgetedLocalResponse({
|
|
719
|
-
ok: true,
|
|
720
|
-
tier: "free",
|
|
721
|
-
sessionId,
|
|
722
|
-
format: "json",
|
|
723
|
-
signals: payload.compact
|
|
724
|
-
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
725
|
-
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
726
|
-
decisions: payload.compact
|
|
727
|
-
? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
|
|
728
|
-
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
729
|
-
decisionCandidates,
|
|
730
|
-
nextCursor,
|
|
731
|
-
counts: {
|
|
732
|
-
totalSignals: allSignals.length,
|
|
733
|
-
byKind: allCounts,
|
|
734
|
-
},
|
|
735
|
-
}, payload.max_chars);
|
|
736
|
-
}
|
|
737
|
-
return budgetedLocalResponse({
|
|
738
|
-
ok: true,
|
|
739
|
-
tier: "free",
|
|
740
|
-
sessionId,
|
|
741
|
-
format: "markdown",
|
|
742
|
-
rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
|
|
743
|
-
...(payload.compact
|
|
744
|
-
? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
745
|
-
: {}),
|
|
746
|
-
counts: {
|
|
747
|
-
totalSignals: allSignals.length,
|
|
748
|
-
byKind: Object.keys(allCounts).length ? allCounts : counts,
|
|
749
|
-
},
|
|
750
|
-
}, payload.max_chars);
|
|
751
|
-
};
|
|
752
|
-
server.tool("capture_session_signal", {
|
|
753
|
-
sessionId: z.string().min(1),
|
|
754
|
-
sequence: z.number().int().nonnegative(),
|
|
755
|
-
kind: sessionSignalKindSchema,
|
|
756
|
-
summary: z.string().min(1).max(2000),
|
|
757
|
-
evidence: z
|
|
758
|
-
.array(evidenceEntrySchema)
|
|
759
|
-
.default([]),
|
|
760
|
-
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
761
|
-
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
762
|
-
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
763
|
-
idempotencyKey: idempotencyKeySchema,
|
|
764
|
-
echo: echoSchema,
|
|
765
|
-
}, async (payload) => {
|
|
766
|
-
const sessionSignal = redactCodingSessionSignal({
|
|
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({
|
|
767
126
|
...payload,
|
|
768
|
-
|
|
769
|
-
...runtimeMetadata(options),
|
|
770
|
-
...(payload.metadata ?? {}),
|
|
771
|
-
},
|
|
127
|
+
scopeKey: payload.scopeKey || defaultScopeKey(),
|
|
772
128
|
});
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
metadata: sessionSignal.metadata,
|
|
788
|
-
scopeKey,
|
|
789
|
-
});
|
|
790
|
-
const sessionSignalCount = localStore.listSignals({
|
|
791
|
-
sessionId: signal.sessionId,
|
|
792
|
-
scopeKey,
|
|
793
|
-
limit: 100000,
|
|
794
|
-
}).length;
|
|
795
|
-
if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
|
|
796
|
-
localStore.enqueueTelemetry(buildTelemetryPayload({
|
|
797
|
-
store: localStore,
|
|
798
|
-
credentials: mode.cliCredentials,
|
|
799
|
-
sessionId: sessionSignal.sessionId,
|
|
800
|
-
}));
|
|
801
|
-
void flushTelemetryOutbox({
|
|
802
|
-
store: localStore,
|
|
803
|
-
credentials: mode.cliCredentials,
|
|
804
|
-
apiUrl: options.apiBaseUrl,
|
|
805
|
-
fetchImpl: options.fetchImpl,
|
|
806
|
-
}).catch(() => null);
|
|
807
|
-
}
|
|
808
|
-
if (!payload.echo) {
|
|
809
|
-
return compactWriteResponse({
|
|
810
|
-
id: signal.id,
|
|
811
|
-
sessionId: signal.sessionId,
|
|
812
|
-
sequence: signal.sequence,
|
|
813
|
-
signalCount: sessionSignalCount,
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
if (payload.echo === "summary") {
|
|
817
|
-
return localResponse({
|
|
818
|
-
ok: true,
|
|
819
|
-
id: signal.id,
|
|
820
|
-
sessionId: signal.sessionId,
|
|
821
|
-
sequence: signal.sequence,
|
|
822
|
-
signalCount: sessionSignalCount,
|
|
823
|
-
summary: signal.summary,
|
|
824
|
-
kind: signal.kind,
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
return localResponse({
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return toolJson({
|
|
131
|
+
ok: false,
|
|
132
|
+
code: "invalid_signal",
|
|
133
|
+
message: parsed.error.issues.map((issue) => issue.message).join("; "),
|
|
134
|
+
});
|
|
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({
|
|
828
143
|
ok: true,
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
? "Captured locally in JSON fallback mode because SQLite was unavailable."
|
|
833
|
-
: "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
|
|
144
|
+
queued: false,
|
|
145
|
+
id: body.id,
|
|
146
|
+
sequence: body.sequence ?? sanitized.sequence,
|
|
834
147
|
});
|
|
835
148
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (!payload.echo) {
|
|
843
|
-
return compactWriteResponse({
|
|
844
|
-
id: upstreamId(upstream),
|
|
845
|
-
sessionId: sessionSignal.sessionId,
|
|
846
|
-
sequence: sessionSignal.sequence,
|
|
847
|
-
signalCount: upstream && typeof upstream === "object" && typeof upstream.signalCount === "number"
|
|
848
|
-
? upstream.signalCount
|
|
849
|
-
: 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,
|
|
850
155
|
});
|
|
851
156
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
? upstream.signalCount
|
|
860
|
-
: 1,
|
|
861
|
-
summary: sessionSignal.summary,
|
|
862
|
-
kind: sessionSignal.kind,
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
return {
|
|
866
|
-
content: [
|
|
867
|
-
{
|
|
868
|
-
type: "text",
|
|
869
|
-
text: JSON.stringify({
|
|
870
|
-
ok: true,
|
|
871
|
-
signal: sessionSignal,
|
|
872
|
-
upstream,
|
|
873
|
-
note: "Ask The W stores this as source material and performs inference in the app.",
|
|
874
|
-
}, null, 2),
|
|
875
|
-
},
|
|
876
|
-
],
|
|
877
|
-
};
|
|
878
|
-
});
|
|
879
|
-
server.tool("list_decisions", {
|
|
880
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
881
|
-
cursor: z.string().optional(),
|
|
882
|
-
sessionId: z.string().optional(),
|
|
883
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
884
|
-
since: z.string().optional(),
|
|
885
|
-
compact: z.boolean().optional(),
|
|
886
|
-
max_chars: maxCharsSchema,
|
|
887
|
-
}, async (payload) => {
|
|
888
|
-
if (mode.mode !== "paid" && localStore) {
|
|
889
|
-
const loginRequired = requireFreeIdentity();
|
|
890
|
-
if (loginRequired)
|
|
891
|
-
return loginRequired;
|
|
892
|
-
const decisions = localStore.listDecisions({
|
|
893
|
-
limit: payload.limit ?? 5,
|
|
894
|
-
cursor: payload.cursor,
|
|
895
|
-
sessionId: payload.sessionId,
|
|
896
|
-
status: payload.status,
|
|
897
|
-
since: payload.since,
|
|
898
|
-
scopeKey: currentScopeKey(),
|
|
899
|
-
});
|
|
900
|
-
return budgetedLocalResponse({
|
|
901
|
-
ok: true,
|
|
902
|
-
tier: "free",
|
|
903
|
-
decisions: payload.compact !== false
|
|
904
|
-
? decisions.map((decision) => ({
|
|
905
|
-
id: decision.id,
|
|
906
|
-
headline: decision.headline,
|
|
907
|
-
status: decision.status,
|
|
908
|
-
signalIds: decision.sourceSignalIds,
|
|
909
|
-
}))
|
|
910
|
-
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
911
|
-
nextCursor: decisions.length >= (payload.limit ?? 5) ? decisions.at(-1)?.createdAt ?? null : null,
|
|
912
|
-
}, payload.max_chars ?? 8000);
|
|
913
|
-
}
|
|
914
|
-
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
915
|
-
limit: payload.limit ?? 5,
|
|
916
|
-
cursor: payload.cursor,
|
|
917
|
-
sessionId: payload.sessionId,
|
|
918
|
-
status: payload.status,
|
|
919
|
-
since: payload.since,
|
|
920
|
-
compact: payload.compact ?? true,
|
|
921
|
-
max_chars: payload.max_chars ?? 8000,
|
|
922
|
-
}));
|
|
923
|
-
});
|
|
924
|
-
server.tool("get_decision", {
|
|
925
|
-
id: z.string().min(1),
|
|
926
|
-
}, async (payload) => {
|
|
927
|
-
if (mode.mode !== "paid" && localStore) {
|
|
928
|
-
const loginRequired = requireFreeIdentity();
|
|
929
|
-
if (loginRequired)
|
|
930
|
-
return loginRequired;
|
|
931
|
-
const decision = localStore.getDecision(payload.id);
|
|
932
|
-
return decision
|
|
933
|
-
? localResponse({ ok: true, tier: "free", decision: decisionWithSignals(localStore, decision) })
|
|
934
|
-
: localToolError({
|
|
935
|
-
code: "not_found",
|
|
936
|
-
message: "Decision not found in the local Ask The W store.",
|
|
937
|
-
retryable: false,
|
|
938
|
-
hint: "Check the decision id or search/list local decisions first.",
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`);
|
|
942
|
-
});
|
|
943
|
-
server.tool("create_decision", {
|
|
944
|
-
content: z.string().min(1),
|
|
945
|
-
idempotencyKey: idempotencyKeySchema,
|
|
946
|
-
echo: echoSchema,
|
|
947
|
-
}, async (payload) => {
|
|
948
|
-
if (mode.mode !== "paid" && localStore) {
|
|
949
|
-
const loginRequired = requireFreeIdentity();
|
|
950
|
-
if (loginRequired)
|
|
951
|
-
return loginRequired;
|
|
952
|
-
const scopeKey = currentScopeKey();
|
|
953
|
-
if (payload.idempotencyKey) {
|
|
954
|
-
const existingId = localStore.getMeta(`idempotency:create_decision:${scopeKey}:${payload.idempotencyKey}`);
|
|
955
|
-
if (existingId) {
|
|
956
|
-
const existing = localStore.getDecision(existingId);
|
|
957
|
-
if (existing) {
|
|
958
|
-
return payload.echo === "full"
|
|
959
|
-
? localResponse({ ok: true, tier: "free", decision: decisionWithSignals(localStore, existing), idempotent: true })
|
|
960
|
-
: compactWriteResponse({ id: existing.id, sequence: localStore.stats().decisions });
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
const decision = localStore.createDecision({
|
|
965
|
-
rawContent: payload.content,
|
|
966
|
-
sessionId: localStore.mostRecentSessionId({ scopeKey }),
|
|
967
|
-
scopeKey,
|
|
968
|
-
});
|
|
969
|
-
if (payload.idempotencyKey) {
|
|
970
|
-
localStore.setMeta(`idempotency:create_decision:${scopeKey}:${payload.idempotencyKey}`, decision.id);
|
|
971
|
-
}
|
|
972
|
-
const warnings = detectDecisionConflicts({
|
|
973
|
-
decision,
|
|
974
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey }),
|
|
975
|
-
});
|
|
976
|
-
if (!payload.echo) {
|
|
977
|
-
return compactWriteResponse({
|
|
978
|
-
id: decision.id,
|
|
979
|
-
sequence: localStore.stats().decisions,
|
|
980
|
-
warnings,
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
return localResponse({
|
|
984
|
-
ok: true,
|
|
985
|
-
...(payload.echo === "summary"
|
|
986
|
-
? { id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
987
|
-
: { tier: "free", decision: decisionWithSignals(localStore, decision), warnings }),
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
const upstream = await postToServer(payload.echo === "full" ? "/api/decisions" : withResponseShape("/api/decisions"), {
|
|
991
|
-
content: payload.content,
|
|
992
|
-
}, options, { method: "POST", idempotencyKey: payload.idempotencyKey });
|
|
993
|
-
const failure = upstreamFailure(upstream);
|
|
994
|
-
if (failure)
|
|
995
|
-
return localResponse(failure);
|
|
996
|
-
if (!payload.echo) {
|
|
997
|
-
return compactWriteResponse({
|
|
998
|
-
id: upstreamId(upstream),
|
|
999
|
-
sequence: upstreamSequence(upstream) ?? 1,
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
return localResponse(payload.echo === "summary"
|
|
1003
|
-
? { ok: true, id: upstreamId(upstream), sequence: upstreamSequence(upstream) ?? 1 }
|
|
1004
|
-
: upstream);
|
|
1005
|
-
});
|
|
1006
|
-
server.tool("update_decision", {
|
|
1007
|
-
id: z.string().min(1),
|
|
1008
|
-
headline: z.string().min(1).optional(),
|
|
1009
|
-
why: z.string().optional(),
|
|
1010
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1011
|
-
alignment: z.enum(["aligned", "orthogonal", "conflicts", "ambiguous"]).optional(),
|
|
1012
|
-
outcomeId: z.string().min(1).optional(),
|
|
1013
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1014
|
-
echo: echoSchema,
|
|
1015
|
-
}, async (payload) => {
|
|
1016
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1017
|
-
const loginRequired = requireFreeIdentity();
|
|
1018
|
-
if (loginRequired)
|
|
1019
|
-
return loginRequired;
|
|
1020
|
-
const decision = localStore.updateDecision(payload.id, {
|
|
1021
|
-
...(payload.headline ? { headline: payload.headline } : {}),
|
|
1022
|
-
...(payload.why !== undefined ? { why: payload.why } : {}),
|
|
1023
|
-
...(payload.status ? { status: payload.status } : {}),
|
|
1024
|
-
...(payload.alignment !== undefined ? { alignment: payload.alignment } : {}),
|
|
1025
|
-
});
|
|
1026
|
-
if (!decision) {
|
|
1027
|
-
return localToolError({
|
|
1028
|
-
code: "not_found",
|
|
1029
|
-
message: "Decision not found in the local Ask The W store.",
|
|
1030
|
-
retryable: false,
|
|
1031
|
-
hint: "Check the decision id before updating.",
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
const warnings = detectDecisionConflicts({
|
|
1035
|
-
decision,
|
|
1036
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey: currentScopeKey() }),
|
|
1037
|
-
});
|
|
1038
|
-
if (!payload.echo) {
|
|
1039
|
-
return compactWriteResponse({ id: decision.id, sequence: localStore.stats().decisions, warnings });
|
|
1040
|
-
}
|
|
1041
|
-
return localResponse(payload.echo === "summary"
|
|
1042
|
-
? { ok: true, id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
1043
|
-
: { ok: true, tier: "free", decision: decisionWithSignals(localStore, decision), warnings });
|
|
1044
|
-
}
|
|
1045
|
-
const upstream = await postToServer(payload.echo === "full"
|
|
1046
|
-
? `/api/decisions/${encodeURIComponent(payload.id)}`
|
|
1047
|
-
: withResponseShape(`/api/decisions/${encodeURIComponent(payload.id)}`), {
|
|
1048
|
-
headline: payload.headline,
|
|
1049
|
-
why: payload.why,
|
|
1050
|
-
status: payload.status,
|
|
1051
|
-
alignment: payload.alignment,
|
|
1052
|
-
outcomeId: payload.outcomeId,
|
|
1053
|
-
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
1054
|
-
const failure = upstreamFailure(upstream);
|
|
1055
|
-
if (failure)
|
|
1056
|
-
return localResponse(failure);
|
|
1057
|
-
if (!payload.echo) {
|
|
1058
|
-
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
1059
|
-
}
|
|
1060
|
-
return localResponse(payload.echo === "summary"
|
|
1061
|
-
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
1062
|
-
: upstream);
|
|
1063
|
-
});
|
|
1064
|
-
server.tool("delete_decision", {
|
|
1065
|
-
id: z.string().min(1),
|
|
1066
|
-
confirmText: z.string().min(1),
|
|
1067
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1068
|
-
}, async (payload) => {
|
|
1069
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1070
|
-
return localResponse(paidFeatureNudge("delete_decision"));
|
|
1071
|
-
}
|
|
1072
|
-
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
|
|
1073
|
-
confirmText: payload.confirmText,
|
|
1074
|
-
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
1075
|
-
});
|
|
1076
|
-
server.tool("list_outcomes", paidDescription("List outcomes from your workspace.", mode.mode), {
|
|
1077
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1078
|
-
cursor: cursorSchema,
|
|
1079
|
-
}, async (payload) => {
|
|
1080
|
-
if (mode.mode === "free")
|
|
1081
|
-
return localResponse(paidFeatureNudge("list_outcomes"));
|
|
1082
|
-
return apiToolResponse(routeWithQuery("/api/outcomes", {
|
|
1083
|
-
limit: payload.limit,
|
|
1084
|
-
cursor: payload.cursor,
|
|
1085
|
-
}));
|
|
1086
|
-
});
|
|
1087
|
-
server.tool("get_outcome", paidDescription("Get outcome detail from your workspace.", mode.mode), {
|
|
1088
|
-
id: z.string().min(1),
|
|
1089
|
-
}, async (payload) => mode.mode === "free"
|
|
1090
|
-
? localResponse(paidFeatureNudge("get_outcome"))
|
|
1091
|
-
: apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`));
|
|
1092
|
-
server.tool("list_outcome_signals", paidDescription("List signals linked to an outcome.", mode.mode), {
|
|
1093
|
-
id: z.string().min(1),
|
|
1094
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1095
|
-
cursor: cursorSchema,
|
|
1096
|
-
}, async (payload) => mode.mode === "free"
|
|
1097
|
-
? localResponse(paidFeatureNudge("list_outcome_signals"))
|
|
1098
|
-
: apiToolResponse(routeWithQuery(`/api/outcomes/${encodeURIComponent(payload.id)}/signals`, {
|
|
1099
|
-
limit: payload.limit,
|
|
1100
|
-
cursor: payload.cursor,
|
|
1101
|
-
})));
|
|
1102
|
-
server.tool("create_outcome", paidDescription("Create a new outcome.", mode.mode), {
|
|
1103
|
-
name: z.string().min(1),
|
|
1104
|
-
summary: z.string().optional(),
|
|
1105
|
-
causalHypothesis: z.string().optional(),
|
|
1106
|
-
suggestedAction: z.string().optional(),
|
|
1107
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1108
|
-
}, async (payload) => {
|
|
1109
|
-
if (mode.mode === "free")
|
|
1110
|
-
return localResponse(paidFeatureNudge("create_outcome"));
|
|
1111
|
-
return apiToolResponse("/api/outcomes", {
|
|
1112
|
-
name: payload.name,
|
|
1113
|
-
summary: payload.summary,
|
|
1114
|
-
causalHypothesis: payload.causalHypothesis,
|
|
1115
|
-
suggestedAction: payload.suggestedAction,
|
|
1116
|
-
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1117
|
-
});
|
|
1118
|
-
server.tool("update_outcome", paidDescription("Update an outcome.", mode.mode), {
|
|
1119
|
-
id: z.string().min(1),
|
|
1120
|
-
name: z.string().min(1).optional(),
|
|
1121
|
-
summary: z.string().optional(),
|
|
1122
|
-
causalHypothesis: z.string().optional(),
|
|
1123
|
-
suggestedAction: z.string().optional(),
|
|
1124
|
-
status: z.enum(["active", "achieved", "abandoned", "archived"]).optional(),
|
|
1125
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1126
|
-
echo: echoSchema,
|
|
1127
|
-
}, async (payload) => {
|
|
1128
|
-
if (mode.mode === "free")
|
|
1129
|
-
return localResponse(paidFeatureNudge("update_outcome"));
|
|
1130
|
-
const upstream = await postToServer(payload.echo === "full"
|
|
1131
|
-
? `/api/outcomes/${encodeURIComponent(payload.id)}`
|
|
1132
|
-
: withResponseShape(`/api/outcomes/${encodeURIComponent(payload.id)}`), {
|
|
1133
|
-
name: payload.name,
|
|
1134
|
-
summary: payload.summary,
|
|
1135
|
-
causalHypothesis: payload.causalHypothesis,
|
|
1136
|
-
suggestedAction: payload.suggestedAction,
|
|
1137
|
-
status: payload.status,
|
|
1138
|
-
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
1139
|
-
const failure = upstreamFailure(upstream);
|
|
1140
|
-
if (failure)
|
|
1141
|
-
return localResponse(failure);
|
|
1142
|
-
if (!payload.echo) {
|
|
1143
|
-
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
1144
|
-
}
|
|
1145
|
-
return localResponse(payload.echo === "summary"
|
|
1146
|
-
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
1147
|
-
: upstream);
|
|
1148
|
-
});
|
|
1149
|
-
server.tool("delete_outcome", paidDescription("Delete an outcome.", mode.mode), {
|
|
1150
|
-
id: z.string().min(1),
|
|
1151
|
-
confirmText: z.string().min(1),
|
|
1152
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1153
|
-
}, async (payload) => {
|
|
1154
|
-
if (mode.mode === "free")
|
|
1155
|
-
return localResponse(paidFeatureNudge("delete_outcome"));
|
|
1156
|
-
return apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`, {
|
|
1157
|
-
confirmText: payload.confirmText,
|
|
1158
|
-
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
1159
|
-
});
|
|
1160
|
-
server.tool("get_north_star", paidDescription("Read the workspace north-star metric.", mode.mode), {}, async () => mode.mode === "free"
|
|
1161
|
-
? localResponse(paidFeatureNudge("get_north_star"))
|
|
1162
|
-
: apiToolResponse("/api/north-star"));
|
|
1163
|
-
server.tool("update_north_star", paidDescription("Update the workspace north-star metric.", mode.mode), {
|
|
1164
|
-
metric: z.string().min(1),
|
|
1165
|
-
current: z.string().min(1),
|
|
1166
|
-
target: z.string().min(1),
|
|
1167
|
-
reason: z.string().min(1),
|
|
1168
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1169
|
-
}, async (payload) => {
|
|
1170
|
-
if (mode.mode === "free")
|
|
1171
|
-
return localResponse(paidFeatureNudge("update_north_star"));
|
|
1172
|
-
return apiToolResponse("/api/north-star", {
|
|
1173
|
-
metric: payload.metric,
|
|
1174
|
-
current: payload.current,
|
|
1175
|
-
target: payload.target,
|
|
1176
|
-
reason: payload.reason,
|
|
1177
|
-
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
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
|
+
});
|
|
1178
164
|
});
|
|
1179
165
|
server.tool("list_signals", {
|
|
1180
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1181
|
-
cursor: z.string().optional(),
|
|
1182
166
|
sessionId: z.string().optional(),
|
|
1183
167
|
since: z.string().optional(),
|
|
1184
|
-
|
|
1185
|
-
max_chars: maxCharsSchema,
|
|
168
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
1186
169
|
}, async (payload) => {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
limit: payload.limit ?? 10,
|
|
1193
|
-
cursor: payload.cursor,
|
|
1194
|
-
sessionId: payload.sessionId,
|
|
1195
|
-
since: payload.since,
|
|
1196
|
-
scopeKey: currentScopeKey(),
|
|
1197
|
-
});
|
|
1198
|
-
return budgetedLocalResponse({
|
|
1199
|
-
ok: true,
|
|
1200
|
-
tier: "free",
|
|
1201
|
-
signals: payload.compact !== false
|
|
1202
|
-
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
1203
|
-
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
1204
|
-
nextCursor: signals.length >= (payload.limit ?? 10) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
1205
|
-
}, payload.max_chars ?? 8000);
|
|
1206
|
-
}
|
|
1207
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1208
|
-
limit: payload.limit ?? 10,
|
|
1209
|
-
cursor: payload.cursor,
|
|
1210
|
-
sessionId: payload.sessionId,
|
|
1211
|
-
since: payload.since,
|
|
1212
|
-
compact: payload.compact ?? true,
|
|
1213
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1214
|
-
}));
|
|
170
|
+
const currentClient = client();
|
|
171
|
+
if (!currentClient.hasToken())
|
|
172
|
+
return needsSignup();
|
|
173
|
+
const result = await currentClient.listSignals(payload);
|
|
174
|
+
return toolJson(result.body);
|
|
1215
175
|
});
|
|
1216
|
-
server.tool("
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}, async (payload) => {
|
|
1223
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1224
|
-
return localResponse(paidFeatureNudge("find_signal_by_summary"));
|
|
1225
|
-
}
|
|
1226
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1227
|
-
query: payload.query,
|
|
1228
|
-
sessionId: payload.sessionId,
|
|
1229
|
-
limit: payload.limit,
|
|
1230
|
-
compact: payload.compact ?? true,
|
|
1231
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1232
|
-
}));
|
|
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);
|
|
1233
182
|
});
|
|
1234
|
-
server.tool("
|
|
1235
|
-
|
|
183
|
+
server.tool("list_decisions", {
|
|
184
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
1236
185
|
}, async (payload) => {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
return signal
|
|
1243
|
-
? localResponse({ ok: true, tier: "free", signal: signalWithDecision(localStore, signal) })
|
|
1244
|
-
: localToolError({
|
|
1245
|
-
code: "not_found",
|
|
1246
|
-
message: "Signal not found in the local Ask The W store.",
|
|
1247
|
-
retryable: false,
|
|
1248
|
-
hint: "Check the signal id or list local signals first.",
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
return apiToolResponse(`/api/signals/${encodeURIComponent(payload.id)}`);
|
|
186
|
+
const currentClient = client();
|
|
187
|
+
if (!currentClient.hasToken())
|
|
188
|
+
return needsSignup();
|
|
189
|
+
const result = await currentClient.listDecisions(payload);
|
|
190
|
+
return toolJson(result.body);
|
|
1252
191
|
});
|
|
1253
|
-
server.tool("
|
|
192
|
+
server.tool("recap", {
|
|
1254
193
|
since: z.string().optional(),
|
|
1255
|
-
|
|
1256
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1257
|
-
limit: z.number().int().positive().max(300).default(50),
|
|
1258
|
-
cursor: cursorSchema,
|
|
1259
|
-
sessionId: z.string().optional(),
|
|
1260
|
-
compact: z.boolean().optional(),
|
|
1261
|
-
max_chars: maxCharsSchema,
|
|
1262
|
-
}, async (payload) => {
|
|
1263
|
-
if (mode.mode !== "paid")
|
|
1264
|
-
return localResponse(paidFeatureNudge("review_decisions"));
|
|
1265
|
-
if (mode.mode === "paid") {
|
|
1266
|
-
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
1267
|
-
since: payload.since,
|
|
1268
|
-
status: payload.status,
|
|
1269
|
-
limit: payload.limit,
|
|
1270
|
-
cursor: payload.cursor,
|
|
1271
|
-
sessionId: payload.sessionId,
|
|
1272
|
-
compact: payload.compact,
|
|
1273
|
-
max_chars: payload.max_chars,
|
|
1274
|
-
}));
|
|
1275
|
-
}
|
|
1276
|
-
return localResponse(paidFeatureNudge("review_decisions"));
|
|
1277
|
-
});
|
|
1278
|
-
server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
|
|
1279
|
-
sessionId: z.string().optional(),
|
|
1280
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1281
|
-
cursor: cursorSchema,
|
|
1282
|
-
limit: z.number().int().positive().max(50).default(50),
|
|
1283
|
-
compact: z.boolean().optional(),
|
|
1284
|
-
max_chars: maxCharsSchema,
|
|
1285
|
-
}, async (payload) => {
|
|
1286
|
-
if (mode.mode !== "paid")
|
|
1287
|
-
return localResponse(paidFeatureNudge("review_session"));
|
|
1288
|
-
if (!localStore || mode.mode === "paid") {
|
|
1289
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1290
|
-
sessionId: payload.sessionId,
|
|
1291
|
-
cursor: payload.cursor,
|
|
1292
|
-
limit: payload.limit,
|
|
1293
|
-
compact: payload.compact,
|
|
1294
|
-
max_chars: payload.max_chars,
|
|
1295
|
-
}));
|
|
1296
|
-
}
|
|
1297
|
-
return localResponse(paidFeatureNudge("review_session"));
|
|
1298
|
-
});
|
|
1299
|
-
server.tool("analyze_session", "Analyze the latest local coding-agent session.", {
|
|
1300
|
-
sessionId: z.string().optional(),
|
|
1301
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1302
|
-
cursor: cursorSchema,
|
|
1303
|
-
limit: z.number().int().positive().max(50).default(50),
|
|
1304
|
-
compact: z.boolean().optional(),
|
|
1305
|
-
max_chars: maxCharsSchema,
|
|
194
|
+
maxSignals: z.number().int().min(1).max(200).optional(),
|
|
1306
195
|
}, async (payload) => {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
max_chars: payload.max_chars,
|
|
1314
|
-
}));
|
|
1315
|
-
}
|
|
1316
|
-
return localSessionAnalysisResponse(payload);
|
|
1317
|
-
});
|
|
1318
|
-
server.tool("recap", "Summarize the latest local coding-agent session as a digest, standup, or share-ready recap.", {
|
|
1319
|
-
format: z.enum(["digest", "standup", "share"]).default("digest"),
|
|
1320
|
-
sessionId: z.string().optional(),
|
|
1321
|
-
compact: z.boolean().optional(),
|
|
1322
|
-
max_chars: maxCharsSchema,
|
|
1323
|
-
}, async (payload) => {
|
|
1324
|
-
if (!localStore || mode.mode === "paid")
|
|
1325
|
-
return localResponse(paidFeatureNudge("recap"));
|
|
1326
|
-
const loginRequired = requireFreeIdentity();
|
|
1327
|
-
if (loginRequired)
|
|
1328
|
-
return loginRequired;
|
|
1329
|
-
const scopeKey = currentScopeKey();
|
|
1330
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1331
|
-
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1332
|
-
const decisions = sessionId ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1333
|
-
return budgetedLocalResponse({
|
|
1334
|
-
ok: true,
|
|
1335
|
-
tier: "free",
|
|
1336
|
-
sessionId,
|
|
1337
|
-
format: payload.format,
|
|
1338
|
-
rendered: renderRecap({ format: payload.format, signals, decisions }),
|
|
1339
|
-
...(payload.compact
|
|
1340
|
-
? { signals: signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
1341
|
-
: {}),
|
|
1342
|
-
}, payload.max_chars);
|
|
1343
|
-
});
|
|
1344
|
-
server.tool("coach", "Coach the local coding-agent session. Use for natural prompts like: Coach me on this session.", {
|
|
1345
|
-
scope: z.enum(["session", "week", "patterns"]).default("session"),
|
|
1346
|
-
sessionId: z.string().optional(),
|
|
1347
|
-
max_chars: maxCharsSchema,
|
|
1348
|
-
}, async (payload) => {
|
|
1349
|
-
if (payload.scope === "week" || payload.scope === "patterns") {
|
|
1350
|
-
return localResponse(paidFeatureNudge("coach"));
|
|
1351
|
-
}
|
|
1352
|
-
if (!localStore || mode.mode === "paid")
|
|
1353
|
-
return localResponse(paidFeatureNudge("coach"));
|
|
1354
|
-
const loginRequired = requireFreeIdentity();
|
|
1355
|
-
if (loginRequired)
|
|
1356
|
-
return loginRequired;
|
|
1357
|
-
const scopeKey = currentScopeKey();
|
|
1358
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1359
|
-
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1360
|
-
const decisions = sessionId ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1361
|
-
const coaching = buildSessionCoach({ signals, decisions });
|
|
1362
|
-
return budgetedLocalResponse({
|
|
1363
|
-
ok: true,
|
|
1364
|
-
tier: "free",
|
|
1365
|
-
scope: "session",
|
|
1366
|
-
sessionId,
|
|
1367
|
-
qualityScore: coaching.qualityScore,
|
|
1368
|
-
biggestGap: coaching.biggestGap,
|
|
1369
|
-
failureMode: coaching.failureMode,
|
|
1370
|
-
rendered: `Decision quality score: ${coaching.qualityScore}/100\nBiggest gap: ${coaching.biggestGap}`,
|
|
1371
|
-
}, payload.max_chars);
|
|
1372
|
-
});
|
|
1373
|
-
server.tool("promote_signal_to_decision", "Copy a captured signal summary into a linked local decision.", {
|
|
1374
|
-
signalId: z.union([z.string(), z.number()]),
|
|
1375
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).default("proposed"),
|
|
1376
|
-
why: z.string().optional(),
|
|
1377
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1378
|
-
}, async (payload) => {
|
|
1379
|
-
if (!localStore || mode.mode === "paid")
|
|
1380
|
-
return localResponse(paidFeatureNudge("promote_signal_to_decision"));
|
|
1381
|
-
const loginRequired = requireFreeIdentity();
|
|
1382
|
-
if (loginRequired)
|
|
1383
|
-
return loginRequired;
|
|
1384
|
-
const numericSignalId = typeof payload.signalId === "number" ? payload.signalId : Number(payload.signalId);
|
|
1385
|
-
if (!Number.isFinite(numericSignalId)) {
|
|
1386
|
-
return localToolError({
|
|
1387
|
-
code: "invalid_input",
|
|
1388
|
-
message: "Invalid signalId.",
|
|
1389
|
-
retryable: false,
|
|
1390
|
-
hint: "Use the numeric local signal id.",
|
|
1391
|
-
extra: { field: "signalId" },
|
|
1392
|
-
});
|
|
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);
|
|
1393
202
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
code: "
|
|
1398
|
-
message: "
|
|
1399
|
-
retryable: false,
|
|
1400
|
-
hint: "List signals first, then pass the numeric local signal id.",
|
|
1401
|
-
extra: { tool: "promote_signal_to_decision" },
|
|
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.",
|
|
1402
208
|
});
|
|
1403
209
|
}
|
|
1404
|
-
if (payload.idempotencyKey) {
|
|
1405
|
-
const existingId = localStore.getMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`);
|
|
1406
|
-
if (existingId) {
|
|
1407
|
-
const existing = localStore.getDecision(existingId);
|
|
1408
|
-
if (existing) {
|
|
1409
|
-
return localResponse({
|
|
1410
|
-
ok: true,
|
|
1411
|
-
id: existing.id,
|
|
1412
|
-
sequence: localStore.stats().decisions,
|
|
1413
|
-
decision: decisionWithSignals(localStore, existing),
|
|
1414
|
-
linkedSignalId: signal.id,
|
|
1415
|
-
idempotent: true,
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
const decision = localStore.createDecision({
|
|
1421
|
-
rawContent: signal.summary,
|
|
1422
|
-
headline: signal.summary,
|
|
1423
|
-
why: payload.why ?? null,
|
|
1424
|
-
status: payload.status,
|
|
1425
|
-
sessionId: signal.sessionId,
|
|
1426
|
-
files: signal.filesTouched,
|
|
1427
|
-
sourceSignalIds: [signal.id],
|
|
1428
|
-
scopeKey: signal.scopeKey ?? currentScopeKey(),
|
|
1429
|
-
});
|
|
1430
|
-
if (payload.idempotencyKey) {
|
|
1431
|
-
localStore.setMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`, decision.id);
|
|
1432
|
-
}
|
|
1433
|
-
const warnings = detectDecisionConflicts({
|
|
1434
|
-
decision,
|
|
1435
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey: decision.scopeKey }),
|
|
1436
|
-
});
|
|
1437
|
-
return localResponse({
|
|
1438
|
-
ok: true,
|
|
1439
|
-
id: decision.id,
|
|
1440
|
-
sequence: localStore.stats().decisions,
|
|
1441
|
-
decision: decisionWithSignals(localStore, decision),
|
|
1442
|
-
linkedSignalId: signal.id,
|
|
1443
|
-
warnings,
|
|
1444
|
-
});
|
|
1445
|
-
});
|
|
1446
|
-
server.tool("list_decision_candidates", "List local signals that look like decision moments and can be promoted.", {
|
|
1447
|
-
sessionId: z.string().optional(),
|
|
1448
|
-
limit: z.number().int().positive().max(300).default(50),
|
|
1449
|
-
cursor: cursorSchema,
|
|
1450
|
-
max_chars: maxCharsSchema,
|
|
1451
|
-
}, async (payload) => {
|
|
1452
|
-
if (mode.mode !== "paid")
|
|
1453
|
-
return localResponse(paidFeatureNudge("list_decision_candidates"));
|
|
1454
|
-
return apiToolResponse(routeWithQuery("/api/signals/decision-candidates", {
|
|
1455
|
-
sessionId: payload.sessionId,
|
|
1456
|
-
limit: payload.limit,
|
|
1457
|
-
cursor: payload.cursor,
|
|
1458
|
-
max_chars: payload.max_chars,
|
|
1459
|
-
}));
|
|
1460
|
-
});
|
|
1461
|
-
server.tool("search_trail", "Search local signals and decisions together.", {
|
|
1462
|
-
query: z.string().min(1),
|
|
1463
|
-
sessionId: z.string().optional(),
|
|
1464
|
-
limit: z.number().int().positive().max(100).default(25),
|
|
1465
|
-
cursor: cursorSchema,
|
|
1466
|
-
compact: z.boolean().optional(),
|
|
1467
|
-
max_chars: maxCharsSchema,
|
|
1468
|
-
}, async (payload) => {
|
|
1469
|
-
if (mode.mode !== "paid")
|
|
1470
|
-
return localResponse(paidFeatureNudge("search_trail"));
|
|
1471
|
-
return apiToolResponse(routeWithQuery("/api/search/trail", {
|
|
1472
|
-
query: payload.query,
|
|
1473
|
-
sessionId: payload.sessionId,
|
|
1474
|
-
limit: payload.limit,
|
|
1475
|
-
cursor: payload.cursor,
|
|
1476
|
-
compact: payload.compact,
|
|
1477
|
-
max_chars: payload.max_chars,
|
|
1478
|
-
}));
|
|
1479
210
|
});
|
|
1480
|
-
server.tool("
|
|
1481
|
-
|
|
1482
|
-
cursor: cursorSchema,
|
|
1483
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1484
|
-
max_chars: maxCharsSchema,
|
|
1485
|
-
}, async (payload) => mode.mode === "free"
|
|
1486
|
-
? localResponse(paidFeatureNudge("export_decisions"))
|
|
1487
|
-
: apiToolResponse(routeWithQuery("/api/export/timeline", {
|
|
1488
|
-
format: payload.format,
|
|
1489
|
-
cursor: payload.cursor,
|
|
1490
|
-
limit: payload.limit ?? 50,
|
|
1491
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1492
|
-
})));
|
|
1493
|
-
server.tool("view_timeline", "View signals and decisions counts bucketed by session, day, or month.", {
|
|
1494
|
-
scope: z.enum(["day", "month", "session"]).default("day"),
|
|
1495
|
-
range: z.enum(["7D", "30D", "90D", "12M", "CUSTOM"]).default("30D"),
|
|
1496
|
-
start: z.string().optional(),
|
|
1497
|
-
end: z.string().optional(),
|
|
1498
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1499
|
-
outcomeId: z.string().optional(),
|
|
1500
|
-
decisionStatus: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1501
|
-
signalSource: z.string().optional(),
|
|
1502
|
-
max_chars: maxCharsSchema,
|
|
211
|
+
server.tool("coach", {
|
|
212
|
+
focus: z.string().max(1000).optional(),
|
|
1503
213
|
}, async (payload) => {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
range: payload.range,
|
|
1508
|
-
start: payload.start,
|
|
1509
|
-
end: payload.end,
|
|
1510
|
-
limit: payload.limit,
|
|
1511
|
-
outcomeId: payload.outcomeId,
|
|
1512
|
-
decisionStatus: payload.decisionStatus,
|
|
1513
|
-
signalSource: payload.signalSource,
|
|
1514
|
-
}));
|
|
1515
|
-
}
|
|
1516
|
-
return localResponse(paidFeatureNudge("view_timeline"));
|
|
1517
|
-
});
|
|
1518
|
-
return server;
|
|
1519
|
-
}
|
|
1520
|
-
function renderDecisionDigest(decisions) {
|
|
1521
|
-
if (decisions.length === 0) {
|
|
1522
|
-
return "# Decisions\n\nNo local decisions captured yet.";
|
|
1523
|
-
}
|
|
1524
|
-
return [
|
|
1525
|
-
"# Decisions",
|
|
1526
|
-
"",
|
|
1527
|
-
...decisions.map((decision) => [`## ${decision.headline}`, `- id: ${decision.id}`, `- status: ${decision.status}`, `- created: ${decision.createdAt}`, decision.why ? `- why: ${decision.why}` : "- why: not captured"].join("\n")),
|
|
1528
|
-
].join("\n\n");
|
|
1529
|
-
}
|
|
1530
|
-
function buildSessionCoach(input) {
|
|
1531
|
-
const verificationCount = input.signals.filter((signal) => signal.kind === "verification_result").length;
|
|
1532
|
-
const implementationCount = input.signals.filter((signal) => signal.kind === "implementation_update").length;
|
|
1533
|
-
const directionCount = input.signals.filter((signal) => signal.kind === "direction_change").length;
|
|
1534
|
-
const finalSummaryCount = input.signals.filter((signal) => signal.kind === "final_summary").length;
|
|
1535
|
-
const decisionCount = input.decisions.length;
|
|
1536
|
-
const hasVerification = verificationCount > 0;
|
|
1537
|
-
const hasDecision = decisionCount > 0;
|
|
1538
|
-
const hasDirection = directionCount > 0;
|
|
1539
|
-
const qualityScore = Math.max(0, Math.min(100, 35 +
|
|
1540
|
-
Math.min(25, decisionCount * 12) +
|
|
1541
|
-
(hasVerification ? 25 : 0) +
|
|
1542
|
-
(hasDirection ? 10 : 0) -
|
|
1543
|
-
Math.max(0, implementationCount - decisionCount) * 3));
|
|
1544
|
-
const failureMode = implementationCount >= 3 && verificationCount === 0
|
|
1545
|
-
? `You captured ${implementationCount} implementation updates but no verification_result; run one check and capture it before ending.`
|
|
1546
|
-
: input.signals.length >= 6 && finalSummaryCount === 0
|
|
1547
|
-
? `You captured ${input.signals.length} signals but no final_summary; close the session with the outcome and remaining risk.`
|
|
1548
|
-
: decisionCount === 0 && directionCount > 0
|
|
1549
|
-
? "Direction changed, but no decision was captured; promote the clearest direction_change signal."
|
|
1550
|
-
: null;
|
|
1551
|
-
const biggestGap = failureMode ?? (!hasDecision
|
|
1552
|
-
? "Promote the clearest captured signal into a decision before the trail goes stale."
|
|
1553
|
-
: !hasVerification
|
|
1554
|
-
? "Capture one verification result so the decision trail records whether the work actually held."
|
|
1555
|
-
: implementationCount > decisionCount * 3
|
|
1556
|
-
? "There are many implementation updates per decision; collapse the important why into one decision."
|
|
1557
|
-
: "The trail is usable. Keep the next decision tied to a verification result.");
|
|
1558
|
-
return { qualityScore, biggestGap, failureMode };
|
|
1559
|
-
}
|
|
1560
|
-
function renderSessionMarkdown(input) {
|
|
1561
|
-
const counts = input.signals.reduce((accumulator, signal) => {
|
|
1562
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1563
|
-
return accumulator;
|
|
1564
|
-
}, {});
|
|
1565
|
-
const activeDecisions = input.decisions.filter((decision) => decision.status !== "abandoned");
|
|
1566
|
-
const coaching = buildSessionCoach({
|
|
1567
|
-
signals: input.signals.map((signal) => ({ ...signal, filesTouched: [], commandsRun: [] })),
|
|
1568
|
-
decisions: input.decisions,
|
|
1569
|
-
});
|
|
1570
|
-
const lines = [
|
|
1571
|
-
"# Session Review",
|
|
1572
|
-
`Session: ${input.sessionId ?? "none"}`,
|
|
1573
|
-
`Signals: ${input.signals.length}`,
|
|
1574
|
-
`Signal kinds: ${Object.entries(counts).map(([kind, count]) => `${kind} ${count}`).join(", ") || "none"}`,
|
|
1575
|
-
`Active decisions: ${activeDecisions.length}`,
|
|
1576
|
-
`Coaching tip: ${coaching.biggestGap}`,
|
|
1577
|
-
"",
|
|
1578
|
-
"## Signals By Kind",
|
|
1579
|
-
...Object.entries(counts)
|
|
1580
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
1581
|
-
.map(([kind, count]) => `- ${kind}: ${count}`),
|
|
1582
|
-
"",
|
|
1583
|
-
"## Decision Candidates",
|
|
1584
|
-
...(input.decisionCandidates?.length
|
|
1585
|
-
? input.decisionCandidates.slice(0, 10).map((candidate) => `- signal ${candidate.signalId}: ${candidate.summary} (${candidate.suggestedStatus})`)
|
|
1586
|
-
: ["- none"]),
|
|
1587
|
-
"",
|
|
1588
|
-
"## Active Decisions",
|
|
1589
|
-
...(activeDecisions.length
|
|
1590
|
-
? activeDecisions.map((decision) => `- ${decision.headline} (${decision.status})${decision.why ? ` - ${decision.why}` : ""}`)
|
|
1591
|
-
: ["- none"]),
|
|
1592
|
-
];
|
|
1593
|
-
return lines.slice(0, 300).join("\n");
|
|
1594
|
-
}
|
|
1595
|
-
function decisionalWeight(signal) {
|
|
1596
|
-
const kindWeight = signal.kind === "direction_change" ? 4 : signal.kind === "verification_result" ? 3 : signal.kind === "implementation_update" ? 2 : 1;
|
|
1597
|
-
const textWeight = /\b(decid|chose|commit|reject|approve|ship|verify|blocked|risk)\b/i.test(signal.summary) ? 2 : 0;
|
|
1598
|
-
return kindWeight + textWeight;
|
|
1599
|
-
}
|
|
1600
|
-
function renderRecap(input) {
|
|
1601
|
-
if (input.format === "standup") {
|
|
1602
|
-
const blockers = input.signals.filter((signal) => /\b(block|fail|error|risk|stuck)\b/i.test(signal.summary));
|
|
1603
|
-
return [
|
|
1604
|
-
"# Standup Recap",
|
|
1605
|
-
"## Yesterday",
|
|
1606
|
-
input.decisions.length ? `- Captured ${input.decisions.length} decisions.` : "- No decisions captured.",
|
|
1607
|
-
"## Today",
|
|
1608
|
-
input.signals.length ? `- Review ${input.signals.length} session signals and promote the strongest one.` : "- Capture the first useful signal.",
|
|
1609
|
-
"## Blockers",
|
|
1610
|
-
...(blockers.length ? blockers.slice(0, 5).map((signal) => `- ${signal.summary}`) : ["- None captured."]),
|
|
1611
|
-
].join("\n");
|
|
1612
|
-
}
|
|
1613
|
-
if (input.format === "share") {
|
|
1614
|
-
return [
|
|
1615
|
-
"# Ask The W Session Share",
|
|
1616
|
-
"",
|
|
1617
|
-
`Signals captured: ${input.signals.length}`,
|
|
1618
|
-
`Decisions captured: ${input.decisions.length}`,
|
|
1619
|
-
"",
|
|
1620
|
-
"## Highlights",
|
|
1621
|
-
...input.signals
|
|
1622
|
-
.slice()
|
|
1623
|
-
.sort((left, right) => decisionalWeight(right) - decisionalWeight(left))
|
|
1624
|
-
.slice(0, 8)
|
|
1625
|
-
.map((signal) => `- ${signal.summary}`),
|
|
1626
|
-
"",
|
|
1627
|
-
"_Captured by Ask The W._",
|
|
1628
|
-
].join("\n");
|
|
1629
|
-
}
|
|
1630
|
-
return [
|
|
1631
|
-
"# Session Digest",
|
|
1632
|
-
"",
|
|
1633
|
-
...input.signals
|
|
1634
|
-
.slice()
|
|
1635
|
-
.sort((left, right) => decisionalWeight(right) - decisionalWeight(left))
|
|
1636
|
-
.slice(0, 28)
|
|
1637
|
-
.map((signal, index) => `${index + 1}. [${signal.kind}] ${signal.summary}`),
|
|
1638
|
-
].join("\n").split("\n").slice(0, 30).join("\n");
|
|
1639
|
-
}
|
|
1640
|
-
function renderSessionFeed(signals) {
|
|
1641
|
-
if (signals.length === 0) {
|
|
1642
|
-
return "# Session\n\nNo local signals captured yet.";
|
|
1643
|
-
}
|
|
1644
|
-
return [
|
|
1645
|
-
"# Session",
|
|
1646
|
-
"",
|
|
1647
|
-
...signals.map((signal) => [
|
|
1648
|
-
`## ${signal.sequence ?? signal.id}. ${signal.kind}`,
|
|
1649
|
-
signal.summary,
|
|
1650
|
-
`- captured: ${signal.capturedAt}`,
|
|
1651
|
-
signal.filesTouched.length ? `- files: ${signal.filesTouched.join(", ")}` : "- files: none",
|
|
1652
|
-
signal.commandsRun.length ? `- commands: ${signal.commandsRun.join(" | ")}` : "- commands: none",
|
|
1653
|
-
].join("\n")),
|
|
1654
|
-
].join("\n\n");
|
|
1655
|
-
}
|
|
1656
|
-
function compactSignal(signal, decision) {
|
|
1657
|
-
return {
|
|
1658
|
-
id: signal.id,
|
|
1659
|
-
kind: signal.kind,
|
|
1660
|
-
summary: signal.summary,
|
|
1661
|
-
files: signal.filesTouched,
|
|
1662
|
-
decisionId: decision?.id ?? null,
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
|
-
function decisionWithSignals(store, decision) {
|
|
1666
|
-
return {
|
|
1667
|
-
...decision,
|
|
1668
|
-
contributingSignals: store.listSignalsByIds(decision.sourceSignalIds),
|
|
1669
|
-
};
|
|
1670
|
-
}
|
|
1671
|
-
function signalWithDecision(store, signal) {
|
|
1672
|
-
const decision = store.getDecisionForSignal(signal.id);
|
|
1673
|
-
return {
|
|
1674
|
-
...signal,
|
|
1675
|
-
decisionId: decision?.id ?? null,
|
|
1676
|
-
decision: decision
|
|
1677
|
-
? {
|
|
1678
|
-
id: decision.id,
|
|
1679
|
-
headline: decision.headline,
|
|
1680
|
-
status: decision.status,
|
|
1681
|
-
why: decision.why,
|
|
1682
|
-
}
|
|
1683
|
-
: null,
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
function candidateFromSignal(signal, linkedDecision) {
|
|
1687
|
-
if (linkedDecision)
|
|
1688
|
-
return null;
|
|
1689
|
-
const text = [signal.summary, ...signal.evidence.map((entry) => {
|
|
1690
|
-
if (entry && typeof entry === "object") {
|
|
1691
|
-
const record = entry;
|
|
1692
|
-
return [record.excerpt, record.diff, record.before, record.after].filter(Boolean).join(" ");
|
|
1693
|
-
}
|
|
1694
|
-
return String(entry ?? "");
|
|
1695
|
-
})].join(" ");
|
|
1696
|
-
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);
|
|
1697
|
-
if (!hasDecisionLanguage && signal.kind !== "direction_change") {
|
|
1698
|
-
return null;
|
|
1699
|
-
}
|
|
1700
|
-
const because = /\bbecause\b/i.test(text);
|
|
1701
|
-
return {
|
|
1702
|
-
id: `candidate_${signal.id}`,
|
|
1703
|
-
signalId: signal.id,
|
|
1704
|
-
sessionId: signal.sessionId,
|
|
1705
|
-
summary: signal.summary,
|
|
1706
|
-
suggestedStatus: signal.kind === "verification_result" ? "shipped" : "proposed",
|
|
1707
|
-
why: because ? "The signal includes an explicit because/reason clause." : "The signal uses decision language.",
|
|
1708
|
-
files: signal.filesTouched,
|
|
1709
|
-
capturedAt: signal.capturedAt,
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
function listDecisionCandidates(input) {
|
|
1713
|
-
const signals = input.store.listSignals({
|
|
1714
|
-
sessionId: input.sessionId ?? undefined,
|
|
1715
|
-
scopeKey: input.scopeKey,
|
|
1716
|
-
cursor: input.cursor,
|
|
1717
|
-
limit: Math.max(1, Math.min(300, input.limit ?? 50)),
|
|
1718
|
-
});
|
|
1719
|
-
const candidates = signals
|
|
1720
|
-
.map((signal) => candidateFromSignal(signal, input.store.getDecisionForSignal(signal.id)))
|
|
1721
|
-
.filter((candidate) => Boolean(candidate));
|
|
1722
|
-
return {
|
|
1723
|
-
candidates,
|
|
1724
|
-
nextCursor: signals.length >= (input.limit ?? 50) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
function normalizedDecisionTerms(text) {
|
|
1728
|
-
const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "keep", "use", "adopt", "remove", "drop", "replace", "defer", "ship", "commit"]);
|
|
1729
|
-
return String(text ?? "")
|
|
1730
|
-
.toLowerCase()
|
|
1731
|
-
.replace(/[^a-z0-9\s-]/g, " ")
|
|
1732
|
-
.split(/\s+/)
|
|
1733
|
-
.filter((term) => term.length >= 4 && !stop.has(term));
|
|
1734
|
-
}
|
|
1735
|
-
function decisionPolarity(text) {
|
|
1736
|
-
if (/\b(remove|drop|abandon|defer|reject|disable|stop|sunset|do not|don't|won't|will not)\b/i.test(text))
|
|
1737
|
-
return "negative";
|
|
1738
|
-
if (/\b(keep|use|adopt|enable|add|ship|commit|standardize|choose|approve|go with)\b/i.test(text))
|
|
1739
|
-
return "positive";
|
|
1740
|
-
return "neutral";
|
|
1741
|
-
}
|
|
1742
|
-
function detectDecisionConflicts(input) {
|
|
1743
|
-
const polarity = decisionPolarity(`${input.decision.headline} ${input.decision.rawContent}`);
|
|
1744
|
-
if (polarity === "neutral")
|
|
1745
|
-
return [];
|
|
1746
|
-
const terms = new Set(normalizedDecisionTerms(`${input.decision.headline} ${input.decision.rawContent}`));
|
|
1747
|
-
if (terms.size === 0)
|
|
1748
|
-
return [];
|
|
1749
|
-
return input.decisions
|
|
1750
|
-
.filter((prior) => prior.id !== input.decision.id)
|
|
1751
|
-
.filter((prior) => !input.decision.scopeKey || prior.scopeKey === input.decision.scopeKey)
|
|
1752
|
-
.map((prior) => {
|
|
1753
|
-
const priorPolarity = decisionPolarity(`${prior.headline} ${prior.rawContent}`);
|
|
1754
|
-
const priorTerms = normalizedDecisionTerms(`${prior.headline} ${prior.rawContent}`);
|
|
1755
|
-
const overlap = priorTerms.filter((term) => terms.has(term));
|
|
1756
|
-
return {
|
|
1757
|
-
prior,
|
|
1758
|
-
priorPolarity,
|
|
1759
|
-
overlap,
|
|
1760
|
-
};
|
|
1761
|
-
})
|
|
1762
|
-
.filter((entry) => entry.priorPolarity !== "neutral" && entry.priorPolarity !== polarity && entry.overlap.length > 0)
|
|
1763
|
-
.slice(0, 3)
|
|
1764
|
-
.map((entry) => ({
|
|
1765
|
-
code: "possible_conflict",
|
|
1766
|
-
message: `This may conflict with "${entry.prior.headline}".`,
|
|
1767
|
-
conflictingDecisionId: entry.prior.id,
|
|
1768
|
-
overlappingTerms: entry.overlap.slice(0, 5),
|
|
1769
|
-
}));
|
|
1770
|
-
}
|
|
1771
|
-
function searchTrail(input) {
|
|
1772
|
-
const terms = String(input.query ?? "")
|
|
1773
|
-
.toLowerCase()
|
|
1774
|
-
.split(/\s+/)
|
|
1775
|
-
.filter(Boolean);
|
|
1776
|
-
const limit = Math.max(1, Math.min(100, input.limit ?? 25));
|
|
1777
|
-
const haystackMatches = (value) => terms.every((term) => value.toLowerCase().includes(term));
|
|
1778
|
-
const signals = input.store
|
|
1779
|
-
.listSignals({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1780
|
-
.filter((signal) => haystackMatches([
|
|
1781
|
-
signal.summary,
|
|
1782
|
-
signal.kind,
|
|
1783
|
-
signal.filesTouched.join(" "),
|
|
1784
|
-
signal.commandsRun.join(" "),
|
|
1785
|
-
JSON.stringify(signal.evidence),
|
|
1786
|
-
JSON.stringify(signal.metadata),
|
|
1787
|
-
].join(" ")))
|
|
1788
|
-
.map((signal) => ({
|
|
1789
|
-
type: "signal",
|
|
1790
|
-
id: String(signal.id),
|
|
1791
|
-
createdAt: signal.capturedAt,
|
|
1792
|
-
score: terms.length,
|
|
1793
|
-
result: input.compact ? compactSignal(signal, input.store.getDecisionForSignal(signal.id)) : signalWithDecision(input.store, signal),
|
|
1794
|
-
}));
|
|
1795
|
-
const decisions = input.store
|
|
1796
|
-
.listDecisions({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1797
|
-
.filter((decision) => haystackMatches([decision.headline, decision.why ?? "", decision.rawContent, decision.files.join(" "), decision.status].join(" ")))
|
|
1798
|
-
.map((decision) => ({
|
|
1799
|
-
type: "decision",
|
|
1800
|
-
id: decision.id,
|
|
1801
|
-
createdAt: decision.createdAt,
|
|
1802
|
-
score: terms.length,
|
|
1803
|
-
result: input.compact
|
|
1804
|
-
? {
|
|
1805
|
-
id: decision.id,
|
|
1806
|
-
headline: decision.headline,
|
|
1807
|
-
status: decision.status,
|
|
1808
|
-
signalIds: decision.sourceSignalIds,
|
|
1809
|
-
}
|
|
1810
|
-
: decisionWithSignals(input.store, decision),
|
|
1811
|
-
}));
|
|
1812
|
-
const matches = [...signals, ...decisions]
|
|
1813
|
-
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
|
1814
|
-
.slice(0, limit);
|
|
1815
|
-
return {
|
|
1816
|
-
matches,
|
|
1817
|
-
nextCursor: matches.length >= limit ? matches.at(-1)?.createdAt ?? null : null,
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
const isDirectIndexExecution = Boolean(process.argv[1]) &&
|
|
1821
|
-
(() => {
|
|
1822
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
1823
|
-
const invokedPath = path.resolve(process.argv[1]);
|
|
214
|
+
const currentClient = client();
|
|
215
|
+
if (!currentClient.hasToken())
|
|
216
|
+
return needsSignup();
|
|
1824
217
|
try {
|
|
1825
|
-
|
|
218
|
+
const result = await currentClient.coach(payload);
|
|
219
|
+
return toolJson(result.body);
|
|
1826
220
|
}
|
|
1827
221
|
catch {
|
|
1828
|
-
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
|
+
});
|
|
1829
227
|
}
|
|
1830
|
-
})();
|
|
1831
|
-
export async function runInitializeHandshake(input = {}) {
|
|
1832
|
-
const modulePath = input.entrypoint ?? fileURLToPath(import.meta.url);
|
|
1833
|
-
const child = spawn(process.execPath, [modulePath], {
|
|
1834
|
-
cwd: process.cwd(),
|
|
1835
|
-
env: input.env ?? process.env,
|
|
1836
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1837
|
-
});
|
|
1838
|
-
let stdout = "";
|
|
1839
|
-
let stderr = "";
|
|
1840
|
-
child.stdout.setEncoding("utf8");
|
|
1841
|
-
child.stderr.setEncoding("utf8");
|
|
1842
|
-
child.stdout.on("data", (chunk) => {
|
|
1843
|
-
stdout += chunk;
|
|
1844
228
|
});
|
|
1845
|
-
|
|
1846
|
-
stderr += chunk;
|
|
1847
|
-
});
|
|
1848
|
-
const initialize = {
|
|
1849
|
-
jsonrpc: "2.0",
|
|
1850
|
-
id: 1,
|
|
1851
|
-
method: "initialize",
|
|
1852
|
-
params: {
|
|
1853
|
-
protocolVersion: "2024-11-05",
|
|
1854
|
-
capabilities: {},
|
|
1855
|
-
clientInfo: { name: "askthew-initialize-handshake", version: "1.0.0" },
|
|
1856
|
-
},
|
|
1857
|
-
};
|
|
1858
|
-
child.stdin.write(`${JSON.stringify(initialize)}\n`);
|
|
1859
|
-
try {
|
|
1860
|
-
return await new Promise((resolve, reject) => {
|
|
1861
|
-
const timeout = setTimeout(() => {
|
|
1862
|
-
reject(new Error(`Timed out waiting for initialize response. stdout=${stdout} stderr=${stderr}`));
|
|
1863
|
-
}, input.timeoutMs ?? 3000);
|
|
1864
|
-
const failOnExit = (code) => {
|
|
1865
|
-
clearTimeout(timeout);
|
|
1866
|
-
reject(new Error(`MCP stdio server exited before initialize. code=${code} stdout=${stdout} stderr=${stderr}`));
|
|
1867
|
-
};
|
|
1868
|
-
const check = () => {
|
|
1869
|
-
const lineEnd = stdout.indexOf("\n");
|
|
1870
|
-
if (lineEnd === -1)
|
|
1871
|
-
return;
|
|
1872
|
-
clearTimeout(timeout);
|
|
1873
|
-
child.off("exit", failOnExit);
|
|
1874
|
-
const response = JSON.parse(stdout.slice(0, lineEnd));
|
|
1875
|
-
if (response.id !== 1 || response.jsonrpc !== "2.0" || !response.result) {
|
|
1876
|
-
reject(new Error(`Unexpected initialize response: ${JSON.stringify(response)}`));
|
|
1877
|
-
return;
|
|
1878
|
-
}
|
|
1879
|
-
resolve({
|
|
1880
|
-
serverInfoVersion: typeof response.result?.serverInfo?.version === "string"
|
|
1881
|
-
? response.result.serverInfo.version
|
|
1882
|
-
: undefined,
|
|
1883
|
-
});
|
|
1884
|
-
};
|
|
1885
|
-
child.stdout.on("data", check);
|
|
1886
|
-
child.once("exit", failOnExit);
|
|
1887
|
-
check();
|
|
1888
|
-
});
|
|
1889
|
-
}
|
|
1890
|
-
finally {
|
|
1891
|
-
child.kill();
|
|
1892
|
-
}
|
|
229
|
+
return server;
|
|
1893
230
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
console.log("Ask The W MCP initialize handshake succeeded.");
|
|
1899
|
-
})
|
|
1900
|
-
.catch((error) => {
|
|
1901
|
-
if (error instanceof Error) {
|
|
1902
|
-
console.error(error.message);
|
|
1903
|
-
}
|
|
1904
|
-
else {
|
|
1905
|
-
console.error("Ask The W MCP initialize handshake failed.", error);
|
|
1906
|
-
}
|
|
1907
|
-
process.exit(1);
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
else {
|
|
1911
|
-
const server = createAskTheWMcpServer();
|
|
1912
|
-
const transport = new StdioServerTransport();
|
|
1913
|
-
server.connect(transport).catch((error) => {
|
|
1914
|
-
if (error instanceof Error) {
|
|
1915
|
-
console.error(error.message);
|
|
1916
|
-
}
|
|
1917
|
-
else {
|
|
1918
|
-
console.error("Ask The W MCP server failed to start.", error);
|
|
1919
|
-
}
|
|
1920
|
-
process.exit(1);
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
231
|
+
export async function runStdioServer(options = {}) {
|
|
232
|
+
const server = createAskTheWMcpServer(options);
|
|
233
|
+
const transport = new StdioServerTransport();
|
|
234
|
+
await server.connect(transport);
|
|
1923
235
|
}
|