@askthew/mcp-plugin 0.2.8 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -16
- package/dist/auth-pending.test.d.ts +1 -0
- package/dist/auth-pending.test.js +56 -0
- package/dist/cli-actions.test.d.ts +1 -0
- package/dist/cli-actions.test.js +71 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +412 -18
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +274 -0
- package/dist/free-tier-policy.test.d.ts +1 -0
- package/dist/free-tier-policy.test.js +57 -0
- package/dist/index.d.ts +59 -13
- package/dist/index.js +1736 -103
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +952 -0
- package/dist/install.d.ts +56 -1
- package/dist/install.js +171 -26
- package/dist/install.test.d.ts +1 -0
- package/dist/install.test.js +297 -0
- package/dist/lib/auth-magic-link.d.ts +22 -0
- package/dist/lib/auth-magic-link.js +43 -0
- package/dist/lib/auth-pending.d.ts +23 -0
- package/dist/lib/auth-pending.js +36 -0
- package/dist/lib/cli-actions.d.ts +28 -0
- package/dist/lib/cli-actions.js +104 -0
- package/dist/lib/free-install-registration.d.ts +27 -0
- package/dist/lib/free-install-registration.js +52 -0
- package/dist/lib/free-tier-policy.d.ts +23 -0
- package/dist/lib/free-tier-policy.js +68 -0
- package/dist/lib/local-identity.d.ts +44 -0
- package/dist/lib/local-identity.js +81 -0
- package/dist/lib/local-store.d.ts +130 -0
- package/dist/lib/local-store.js +595 -0
- package/dist/lib/loopback-auth.d.ts +8 -0
- package/dist/lib/loopback-auth.js +30 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +50 -0
- package/dist/lib/telemetry.d.ts +25 -0
- package/dist/lib/telemetry.js +159 -0
- package/dist/lib/timeline-insights.d.ts +23 -0
- package/dist/lib/timeline-insights.js +115 -0
- package/dist/lib/tip-engine.d.ts +18 -0
- package/dist/lib/tip-engine.js +237 -0
- package/dist/lib/upgrade-nudge.d.ts +19 -0
- package/dist/lib/upgrade-nudge.js +37 -0
- package/dist/lib/upgrade-sync.d.ts +38 -0
- package/dist/lib/upgrade-sync.js +60 -0
- package/dist/local-identity.test.d.ts +1 -0
- package/dist/local-identity.test.js +29 -0
- package/dist/local-store.test.d.ts +1 -0
- package/dist/local-store.test.js +71 -0
- package/dist/scope.d.ts +1 -2
- package/dist/scope.js +56 -8
- package/dist/scope.test.d.ts +1 -0
- package/dist/scope.test.js +49 -0
- package/dist/timeline-insights.test.d.ts +1 -0
- package/dist/timeline-insights.test.js +85 -0
- package/dist/tip-engine.test.d.ts +1 -0
- package/dist/tip-engine.test.js +51 -0
- package/package.json +7 -10
package/dist/index.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
3
|
+
import { resolvePluginScope } from "./scope.js";
|
|
4
|
+
import { resolveMcpMode } from "./lib/free-tier-policy.js";
|
|
5
|
+
import { LocalStore } from "./lib/local-store.js";
|
|
6
|
+
import { buildTelemetryPayload, flushTelemetryOutbox } from "./lib/telemetry.js";
|
|
7
|
+
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
8
|
+
import { buildLocalTimeline, buildTimelineInsights as buildLocalTimelineInsights, renderTimelineMarkdown as renderLocalTimelineMarkdown } from "./lib/timeline-insights.js";
|
|
9
|
+
import { paidDescription, paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
|
|
10
|
+
import { configPath, readJsonFile } from "./lib/paths.js";
|
|
6
11
|
const evidenceRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
12
|
+
const evidenceEntrySchema = z.object({
|
|
13
|
+
role: evidenceRoleSchema,
|
|
14
|
+
excerpt: z.string().min(1).max(2000),
|
|
15
|
+
kind: z.enum(["excerpt", "diff", "prompt_diff"]).optional(),
|
|
16
|
+
diff: z.string().max(12000).optional(),
|
|
17
|
+
before: z.string().max(6000).optional(),
|
|
18
|
+
after: z.string().max(6000).optional(),
|
|
19
|
+
});
|
|
7
20
|
const sessionSignalKindSchema = z.enum([
|
|
8
21
|
"setup_complete",
|
|
9
22
|
"session_checkpoint",
|
|
@@ -18,10 +31,7 @@ export const codingSessionSignalSchema = z.object({
|
|
|
18
31
|
kind: sessionSignalKindSchema,
|
|
19
32
|
summary: z.string().min(1).max(2000),
|
|
20
33
|
evidence: z
|
|
21
|
-
.array(
|
|
22
|
-
role: evidenceRoleSchema,
|
|
23
|
-
excerpt: z.string().min(1).max(500),
|
|
24
|
-
}))
|
|
34
|
+
.array(evidenceEntrySchema)
|
|
25
35
|
.default([]),
|
|
26
36
|
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
27
37
|
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
@@ -41,85 +51,245 @@ export const provenanceSignalSchema = z.object({
|
|
|
41
51
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
42
52
|
});
|
|
43
53
|
const REDACTION_PATTERNS = [
|
|
44
|
-
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
45
|
-
/\b
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
{ name: "aws_access_key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
55
|
+
{ name: "aws_secret_key", pattern: /\b[A-Za-z0-9+/]{40}\b/g },
|
|
56
|
+
{ name: "gcp_api_key", pattern: /\bAIza[0-9A-Za-z\-_]{35}\b/g },
|
|
57
|
+
{ name: "azure_storage_key", pattern: /[A-Za-z0-9+/]{86}==/g },
|
|
58
|
+
{ name: "stripe_key", pattern: /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
59
|
+
{ name: "sendgrid_key", pattern: /\bSG\.[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{22,}\b/g },
|
|
60
|
+
{ name: "twilio_key", pattern: /\bSK[0-9a-fA-F]{32}\b/g },
|
|
61
|
+
{ name: "slack_token", pattern: /\bxox[baprs]-[0-9A-Za-z\-]{10,}\b/g },
|
|
62
|
+
{ name: "slack_webhook", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g },
|
|
63
|
+
{ name: "openai_env_assignment", pattern: /\bOPENAI_API_KEY\s*[=:]\s*["']?sk-[A-Za-z0-9_-]{20,}["']?/g },
|
|
64
|
+
{ name: "openai_key", pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g },
|
|
65
|
+
{ name: "bearer_token", pattern: /\bBearer\s+[A-Za-z0-9._-]+\b/g },
|
|
66
|
+
{ name: "anthropic_key", pattern: /\bsk-ant-[A-Za-z0-9\-_]{32,}\b/g },
|
|
67
|
+
{ name: "github_pat_classic", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
68
|
+
{ name: "github_pat_fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
|
|
69
|
+
{ name: "github_oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
|
|
70
|
+
{ name: "github_app_token", pattern: /\bghs_[A-Za-z0-9]{36}\b/g },
|
|
71
|
+
{ name: "gitlab_pat", pattern: /\bglpat-[A-Za-z0-9\-_]{20,}\b/g },
|
|
72
|
+
{ name: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
73
|
+
{ name: "pem_private_key", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
74
|
+
{ name: "db_dsn_with_creds", pattern: /(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[^:]+:[^@\s]+@[^\s"'`]+/gi },
|
|
75
|
+
{ 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 },
|
|
76
|
+
{ 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 },
|
|
77
|
+
{ 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 },
|
|
78
|
+
{ name: "email", pattern: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g },
|
|
79
|
+
{ name: "us_phone", pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/g },
|
|
80
|
+
{ name: "ssn", pattern: /\b(?!000|666|9\d{2})\d{3}[-\s](?!00)\d{2}[-\s](?!0{4})\d{4}\b/g },
|
|
81
|
+
{ 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 },
|
|
48
82
|
];
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
const SENSITIVE_QUERY_PARAMS = new Set([
|
|
84
|
+
"token",
|
|
85
|
+
"key",
|
|
86
|
+
"api_key",
|
|
87
|
+
"apikey",
|
|
88
|
+
"secret",
|
|
89
|
+
"password",
|
|
90
|
+
"passwd",
|
|
91
|
+
"pass",
|
|
92
|
+
"auth",
|
|
93
|
+
"access_token",
|
|
94
|
+
"refresh_token",
|
|
95
|
+
"id_token",
|
|
96
|
+
"client_secret",
|
|
97
|
+
"authorization",
|
|
98
|
+
"credential",
|
|
99
|
+
"sig",
|
|
100
|
+
"signature",
|
|
101
|
+
]);
|
|
102
|
+
const SENSITIVE_FLAG_NAMES = new Set([
|
|
103
|
+
"token",
|
|
104
|
+
"password",
|
|
105
|
+
"passwd",
|
|
106
|
+
"pass",
|
|
107
|
+
"secret",
|
|
108
|
+
"key",
|
|
109
|
+
"api-key",
|
|
110
|
+
"apikey",
|
|
111
|
+
"api_key",
|
|
112
|
+
"auth",
|
|
113
|
+
"access-token",
|
|
114
|
+
"private-key",
|
|
115
|
+
"credential",
|
|
116
|
+
"credentials",
|
|
117
|
+
"authorization",
|
|
118
|
+
"client-secret",
|
|
119
|
+
"client_secret",
|
|
120
|
+
]);
|
|
121
|
+
const SENSITIVE_PATH_SEGMENTS = new Set([
|
|
122
|
+
".env",
|
|
123
|
+
".ssh",
|
|
124
|
+
".aws",
|
|
125
|
+
".gcp",
|
|
126
|
+
".azure",
|
|
127
|
+
".gnupg",
|
|
128
|
+
".pgp",
|
|
129
|
+
"secrets",
|
|
130
|
+
"secret",
|
|
131
|
+
"credentials",
|
|
132
|
+
"credential",
|
|
133
|
+
"private",
|
|
134
|
+
"keys",
|
|
135
|
+
"certs",
|
|
136
|
+
"certificates",
|
|
137
|
+
"vault",
|
|
138
|
+
".netrc",
|
|
139
|
+
".npmrc",
|
|
140
|
+
".pypirc",
|
|
141
|
+
".docker",
|
|
142
|
+
"kubeconfig",
|
|
143
|
+
".kube",
|
|
144
|
+
]);
|
|
145
|
+
const ENTROPY_THRESHOLD = 4.5;
|
|
146
|
+
const ENTROPY_TOKEN_PATTERN = /[A-Za-z0-9+/=_\-]{20,}/g;
|
|
147
|
+
export function loadAskTheWConfig(env = process.env) {
|
|
148
|
+
return readJsonFile(configPath(env)) ?? {};
|
|
51
149
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
return redactSecrets(value);
|
|
55
|
-
}
|
|
56
|
-
if (Array.isArray(value)) {
|
|
57
|
-
return value.map((entry) => redactMetadata(entry));
|
|
58
|
-
}
|
|
59
|
-
if (typeof value === "object" && value !== null) {
|
|
60
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, redactMetadata(entry)]));
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
150
|
+
function isRedactionEnabled() {
|
|
151
|
+
return loadAskTheWConfig().redaction?.enabled !== false;
|
|
63
152
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
153
|
+
function sanitizeUrl(raw) {
|
|
154
|
+
try {
|
|
155
|
+
const url = new URL(raw);
|
|
156
|
+
if (url.username || url.password) {
|
|
157
|
+
url.username = "[REDACTED]";
|
|
158
|
+
url.password = "[REDACTED]";
|
|
159
|
+
}
|
|
160
|
+
for (const key of Array.from(url.searchParams.keys())) {
|
|
161
|
+
if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) {
|
|
162
|
+
url.searchParams.set(key, "[REDACTED]");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return url.toString().replace(/%5BREDACTED%5D/gi, "[REDACTED]");
|
|
68
166
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return configuredArea;
|
|
167
|
+
catch {
|
|
168
|
+
return raw;
|
|
72
169
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
170
|
+
}
|
|
171
|
+
function sanitizeUrls(text) {
|
|
172
|
+
return text.replace(/https?:\/\/[^\s"'`<>)\\]*/g, (match) => sanitizeUrl(match));
|
|
173
|
+
}
|
|
174
|
+
function scrubCommandFlags(command) {
|
|
175
|
+
let result = command.replace(/(-{1,2})([\w-]+)([ =])(["'])([^"']+)\4/g, (match, dashes, flagName, separator, quote) => SENSITIVE_FLAG_NAMES.has(String(flagName).toLowerCase())
|
|
176
|
+
? `${dashes}${flagName}${separator}${quote}[REDACTED]${quote}`
|
|
177
|
+
: match);
|
|
178
|
+
result = result.replace(/(-{1,2})([\w-]+)([ =])([^\s"'`]+)/g, (match, dashes, flagName, separator) => SENSITIVE_FLAG_NAMES.has(String(flagName).toLowerCase())
|
|
179
|
+
? `${dashes}${flagName}${separator}[REDACTED]`
|
|
180
|
+
: match);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
function shannonEntropy(value) {
|
|
184
|
+
const freq = new Map();
|
|
185
|
+
for (const char of value) {
|
|
186
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
76
187
|
}
|
|
77
|
-
|
|
78
|
-
|
|
188
|
+
let entropy = 0;
|
|
189
|
+
for (const count of freq.values()) {
|
|
190
|
+
const probability = count / value.length;
|
|
191
|
+
entropy -= probability * Math.log2(probability);
|
|
79
192
|
}
|
|
80
|
-
|
|
81
|
-
|
|
193
|
+
return entropy;
|
|
194
|
+
}
|
|
195
|
+
function looksLikeSecret(token) {
|
|
196
|
+
const hasDigit = /\d/.test(token);
|
|
197
|
+
const hasUpper = /[A-Z]/.test(token);
|
|
198
|
+
const hasLower = /[a-z]/.test(token);
|
|
199
|
+
const hasSpecial = /[+/=_\-]/.test(token);
|
|
200
|
+
return hasDigit && hasLower && (hasUpper || hasSpecial);
|
|
201
|
+
}
|
|
202
|
+
function redactHighEntropyTokens(text) {
|
|
203
|
+
return text.replace(ENTROPY_TOKEN_PATTERN, (match) => {
|
|
204
|
+
if (match.includes("/") && match.length < 60) {
|
|
205
|
+
return match;
|
|
206
|
+
}
|
|
207
|
+
if (!looksLikeSecret(match)) {
|
|
208
|
+
return match;
|
|
209
|
+
}
|
|
210
|
+
return shannonEntropy(match) >= ENTROPY_THRESHOLD ? "[REDACTED]" : match;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function redactTargetedPatterns(text) {
|
|
214
|
+
return REDACTION_PATTERNS.reduce((accumulator, entry) => {
|
|
215
|
+
entry.pattern.lastIndex = 0;
|
|
216
|
+
return accumulator.replace(entry.pattern, "[REDACTED]");
|
|
217
|
+
}, text);
|
|
218
|
+
}
|
|
219
|
+
function redactRawSignalText(text) {
|
|
220
|
+
return redactTargetedPatterns(sanitizeUrls(text));
|
|
221
|
+
}
|
|
222
|
+
function redactOperationalContext(text) {
|
|
223
|
+
return redactHighEntropyTokens(scrubCommandFlags(redactRawSignalText(text)));
|
|
224
|
+
}
|
|
225
|
+
function sanitizeFilePath(filePath) {
|
|
226
|
+
let result = filePath.replace(/^(?:[A-Za-z]:\\|\/(?:Users|home|root|var|etc)\/[^/]+\/)/, "~/");
|
|
227
|
+
result = redactRawSignalText(result);
|
|
228
|
+
const segments = result.split(/[/\\]/);
|
|
229
|
+
if (segments.some((segment) => SENSITIVE_PATH_SEGMENTS.has(segment.toLowerCase()))) {
|
|
230
|
+
const basename = segments[segments.length - 1] ?? result;
|
|
231
|
+
return `[SENSITIVE_PATH]/${basename}`;
|
|
82
232
|
}
|
|
83
|
-
|
|
84
|
-
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
function redactMetadata(value, key) {
|
|
236
|
+
if (typeof value === "string") {
|
|
237
|
+
const redacted = redactOperationalContext(value);
|
|
238
|
+
if (key && /(?:^|_)(?:path|root|file|dir|directory)(?:$|_)/i.test(key)) {
|
|
239
|
+
return sanitizeFilePath(redacted);
|
|
240
|
+
}
|
|
241
|
+
return redacted;
|
|
85
242
|
}
|
|
86
|
-
if (
|
|
87
|
-
return
|
|
243
|
+
if (Array.isArray(value)) {
|
|
244
|
+
return value.map((entry) => redactMetadata(entry));
|
|
88
245
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return "Engineering";
|
|
246
|
+
if (typeof value === "object" && value !== null) {
|
|
247
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactMetadata(entry, entryKey)]));
|
|
92
248
|
}
|
|
93
|
-
return
|
|
249
|
+
return value;
|
|
94
250
|
}
|
|
95
251
|
export function redactProvenanceSignal(input) {
|
|
96
252
|
const parsed = provenanceSignalSchema.parse(input);
|
|
253
|
+
if (!isRedactionEnabled()) {
|
|
254
|
+
return parsed;
|
|
255
|
+
}
|
|
97
256
|
return {
|
|
98
257
|
...parsed,
|
|
258
|
+
decision: redactRawSignalText(parsed.decision),
|
|
259
|
+
rationale: redactRawSignalText(parsed.rationale),
|
|
260
|
+
framework: typeof parsed.framework === "string" ? redactRawSignalText(parsed.framework) : undefined,
|
|
261
|
+
filesAffected: parsed.filesAffected.map((filePath) => sanitizeFilePath(filePath)),
|
|
99
262
|
originatingPrompt: typeof parsed.originatingPrompt === "string"
|
|
100
|
-
?
|
|
263
|
+
? redactOperationalContext(parsed.originatingPrompt)
|
|
101
264
|
: undefined,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
265
|
+
installToken: typeof parsed.installToken === "string"
|
|
266
|
+
? redactOperationalContext(parsed.installToken)
|
|
267
|
+
: undefined,
|
|
268
|
+
metadata: redactMetadata(parsed.metadata),
|
|
106
269
|
};
|
|
107
270
|
}
|
|
108
271
|
export function redactCodingSessionSignal(input) {
|
|
109
272
|
const parsed = codingSessionSignalSchema.parse(input);
|
|
273
|
+
if (!isRedactionEnabled()) {
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
110
276
|
return {
|
|
111
277
|
...parsed,
|
|
112
|
-
summary:
|
|
278
|
+
summary: redactRawSignalText(parsed.summary),
|
|
113
279
|
evidence: parsed.evidence.map((entry) => ({
|
|
114
280
|
...entry,
|
|
115
|
-
excerpt:
|
|
281
|
+
excerpt: redactOperationalContext(entry.excerpt),
|
|
282
|
+
diff: typeof entry.diff === "string" ? redactOperationalContext(entry.diff) : undefined,
|
|
283
|
+
before: typeof entry.before === "string" ? redactOperationalContext(entry.before) : undefined,
|
|
284
|
+
after: typeof entry.after === "string" ? redactOperationalContext(entry.after) : undefined,
|
|
116
285
|
})),
|
|
117
|
-
|
|
286
|
+
filesTouched: parsed.filesTouched.map((filePath) => sanitizeFilePath(filePath)),
|
|
287
|
+
commandsRun: parsed.commandsRun.map((command) => redactOperationalContext(command)),
|
|
118
288
|
metadata: redactMetadata(parsed.metadata),
|
|
119
289
|
};
|
|
120
290
|
}
|
|
121
|
-
function apiBaseUrl() {
|
|
122
|
-
return process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com";
|
|
291
|
+
function apiBaseUrl(override) {
|
|
292
|
+
return override?.trim() || process.env.ASKTHEW_API_URL?.trim() || "https://app.askthew.com";
|
|
123
293
|
}
|
|
124
294
|
function normalizeClientId(value) {
|
|
125
295
|
return String(value ?? "")
|
|
@@ -132,75 +302,202 @@ function normalizeClientId(value) {
|
|
|
132
302
|
export function normalizeInstallTokenInput(token) {
|
|
133
303
|
return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
134
304
|
}
|
|
135
|
-
|
|
305
|
+
const echoSchema = z.enum(["summary", "full"]).optional();
|
|
306
|
+
const cursorSchema = z.string().optional();
|
|
307
|
+
const idempotencyKeySchema = z.string().min(1).max(200).optional();
|
|
308
|
+
const maxCharsSchema = z.number().int().positive().max(100000).optional();
|
|
309
|
+
function traceId() {
|
|
310
|
+
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
311
|
+
}
|
|
312
|
+
function structuredError(input) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
code: input.code,
|
|
316
|
+
message: input.message,
|
|
317
|
+
retryable: Boolean(input.retryable),
|
|
318
|
+
hint: input.hint ?? "",
|
|
319
|
+
traceId: input.traceId ?? traceId(),
|
|
320
|
+
...(typeof input.status === "number" ? { status: input.status } : {}),
|
|
321
|
+
...(input.extra ?? {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function withResponseShape(route, responseShape = "v2") {
|
|
325
|
+
const [path, query = ""] = route.split("?");
|
|
326
|
+
const searchParams = new URLSearchParams(query);
|
|
327
|
+
searchParams.set("response_shape", responseShape);
|
|
328
|
+
const nextQuery = searchParams.toString();
|
|
329
|
+
return nextQuery ? `${path}?${nextQuery}` : path;
|
|
330
|
+
}
|
|
331
|
+
function routeWithQuery(route, params) {
|
|
332
|
+
const searchParams = new URLSearchParams();
|
|
333
|
+
for (const [key, value] of Object.entries(params)) {
|
|
334
|
+
if (value === undefined || value === null || value === "") {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
searchParams.set(key, String(value));
|
|
338
|
+
}
|
|
339
|
+
const query = searchParams.toString();
|
|
340
|
+
return query ? `${route}?${query}` : route;
|
|
341
|
+
}
|
|
342
|
+
function credentials(overrides) {
|
|
136
343
|
const legacyHostType = process.env.ASKTHEW_HOST_TYPE?.trim();
|
|
137
344
|
const hostType = legacyHostType === "claude_code" || legacyHostType === "codex" || legacyHostType === "cursor"
|
|
138
345
|
? legacyHostType
|
|
139
346
|
: undefined;
|
|
140
|
-
const
|
|
347
|
+
const overrideHostType = overrides?.hostType;
|
|
348
|
+
const clientId = normalizeClientId(overrides?.clientId) ||
|
|
349
|
+
normalizeClientId(process.env.ASKTHEW_CLIENT_ID?.trim()) ||
|
|
350
|
+
overrideHostType ||
|
|
351
|
+
hostType ||
|
|
352
|
+
"mcp_client";
|
|
141
353
|
return {
|
|
142
|
-
installToken: normalizeInstallTokenInput(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
354
|
+
installToken: normalizeInstallTokenInput(overrides?.installToken) ||
|
|
355
|
+
normalizeInstallTokenInput(process.env.ASKTHEW_INSTALL_TOKEN),
|
|
356
|
+
userId: overrides?.userId?.trim() || process.env.ASKTHEW_USER_ID?.trim(),
|
|
357
|
+
apiKey: overrides?.apiKey?.trim() || process.env.ASKTHEW_API_KEY?.trim(),
|
|
358
|
+
serverName: overrides?.serverName?.trim() || process.env.ASKTHEW_SERVER_NAME?.trim(),
|
|
146
359
|
clientId,
|
|
147
|
-
clientLabel: process.env.ASKTHEW_CLIENT_LABEL?.trim(),
|
|
148
|
-
hostType,
|
|
360
|
+
clientLabel: overrides?.clientLabel?.trim() || process.env.ASKTHEW_CLIENT_LABEL?.trim(),
|
|
361
|
+
hostType: overrideHostType || hostType,
|
|
149
362
|
};
|
|
150
363
|
}
|
|
151
|
-
function hasServerIdentity() {
|
|
152
|
-
const { installToken, userId } = credentials();
|
|
364
|
+
function hasServerIdentity(overrides) {
|
|
365
|
+
const { installToken, userId } = credentials(overrides);
|
|
153
366
|
return Boolean(installToken || userId);
|
|
154
367
|
}
|
|
155
|
-
async function postToServer(route, payload) {
|
|
156
|
-
if (!hasServerIdentity()) {
|
|
368
|
+
async function postToServer(route, payload, options, request) {
|
|
369
|
+
if (!hasServerIdentity(options?.credentials)) {
|
|
157
370
|
return null;
|
|
158
371
|
}
|
|
159
|
-
const { installToken, userId, apiKey, clientId, clientLabel, hostType } = credentials();
|
|
160
|
-
const
|
|
161
|
-
|
|
372
|
+
const { installToken, userId, apiKey, clientId, clientLabel, hostType } = credentials(options?.credentials);
|
|
373
|
+
const fetcher = options?.fetchImpl ?? fetch;
|
|
374
|
+
const method = request?.method ?? "POST";
|
|
375
|
+
const bodyPayload = {
|
|
376
|
+
...payload,
|
|
377
|
+
installToken: installToken || undefined,
|
|
378
|
+
userId: userId || undefined,
|
|
379
|
+
clientId,
|
|
380
|
+
clientLabel: clientLabel || undefined,
|
|
381
|
+
hostType: hostType || undefined,
|
|
382
|
+
};
|
|
383
|
+
const response = await fetcher(`${apiBaseUrl(options?.apiBaseUrl)}${route}`, {
|
|
384
|
+
method,
|
|
162
385
|
headers: {
|
|
163
|
-
"Content-Type": "application/json",
|
|
164
|
-
...(
|
|
386
|
+
...(method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
387
|
+
...(request?.idempotencyKey ? { "Idempotency-Key": request.idempotencyKey } : {}),
|
|
388
|
+
...(installToken
|
|
389
|
+
? { Authorization: `Bearer ${installToken}` }
|
|
390
|
+
: apiKey
|
|
391
|
+
? { Authorization: `Bearer ${apiKey}` }
|
|
392
|
+
: {}),
|
|
165
393
|
},
|
|
166
|
-
body: JSON.stringify(
|
|
167
|
-
...payload,
|
|
168
|
-
installToken: installToken || undefined,
|
|
169
|
-
userId: userId || undefined,
|
|
170
|
-
clientId,
|
|
171
|
-
clientLabel: clientLabel || undefined,
|
|
172
|
-
hostType: hostType || undefined,
|
|
173
|
-
}),
|
|
394
|
+
...(method === "GET" ? {} : { body: JSON.stringify(bodyPayload) }),
|
|
174
395
|
}).catch(() => null);
|
|
175
|
-
if (!response
|
|
176
|
-
return
|
|
396
|
+
if (!response) {
|
|
397
|
+
return structuredError({
|
|
398
|
+
code: "network_error",
|
|
399
|
+
message: "Ask The W server could not be reached.",
|
|
400
|
+
retryable: true,
|
|
401
|
+
hint: "Check your network connection or retry the same idempotency key.",
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const body = await response.json().catch(() => null);
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
if (body && typeof body === "object") {
|
|
407
|
+
const record = body;
|
|
408
|
+
return {
|
|
409
|
+
...structuredError({
|
|
410
|
+
code: typeof record.code === "string" ? record.code : "http_error",
|
|
411
|
+
message: typeof record.message === "string"
|
|
412
|
+
? record.message
|
|
413
|
+
: typeof record.error === "string"
|
|
414
|
+
? record.error
|
|
415
|
+
: `Ask The W request failed with HTTP ${response.status}.`,
|
|
416
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
417
|
+
hint: typeof record.hint === "string"
|
|
418
|
+
? record.hint
|
|
419
|
+
: response.status >= 500
|
|
420
|
+
? "Retry with the same Idempotency-Key if this was a write."
|
|
421
|
+
: "Check the tool input and retry.",
|
|
422
|
+
traceId: typeof record.traceId === "string" ? record.traceId : undefined,
|
|
423
|
+
status: response.status,
|
|
424
|
+
}),
|
|
425
|
+
...record,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return structuredError({
|
|
429
|
+
code: "http_error",
|
|
430
|
+
message: `Ask The W request failed with HTTP ${response.status}.`,
|
|
431
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
432
|
+
hint: response.status >= 500 ? "Retry with the same Idempotency-Key if this was a write." : "Check the tool input and retry.",
|
|
433
|
+
status: response.status,
|
|
434
|
+
});
|
|
177
435
|
}
|
|
178
|
-
return
|
|
436
|
+
return body;
|
|
179
437
|
}
|
|
180
|
-
function runtimeMetadata() {
|
|
438
|
+
function runtimeMetadata(options) {
|
|
181
439
|
const scope = resolvePluginScope(process.cwd());
|
|
182
|
-
const { serverName, clientId, clientLabel } = credentials();
|
|
440
|
+
const { serverName, clientId, clientLabel } = credentials(options?.credentials);
|
|
441
|
+
const extraMetadata = typeof options?.runtimeMetadata === "function"
|
|
442
|
+
? options.runtimeMetadata()
|
|
443
|
+
: (options?.runtimeMetadata ?? {});
|
|
183
444
|
return {
|
|
184
445
|
repository: scope.repoName,
|
|
185
446
|
repo_name: scope.repoName,
|
|
186
|
-
|
|
187
|
-
...(scope.
|
|
447
|
+
scope_key: localScopeKey(),
|
|
448
|
+
...(scope.repoRoot ? { repo_root: sanitizeFilePath(scope.repoRoot) } : {}),
|
|
449
|
+
...(scope.appPath ? { app_path: sanitizeFilePath(scope.appPath) } : {}),
|
|
188
450
|
...(scope.serviceName ? { service_name: scope.serviceName } : {}),
|
|
189
451
|
...(serverName ? { server_name: serverName } : {}),
|
|
190
452
|
...(clientId ? { client_id: clientId } : {}),
|
|
191
453
|
...(clientLabel ? { client_label: clientLabel } : {}),
|
|
454
|
+
...extraMetadata,
|
|
192
455
|
};
|
|
193
456
|
}
|
|
194
|
-
|
|
195
|
-
|
|
457
|
+
function localScopeKey(cwd = process.cwd()) {
|
|
458
|
+
const scope = resolvePluginScope(cwd);
|
|
459
|
+
return [scope.repoRoot || scope.repoName || cwd, scope.appPath ?? "", scope.serviceName ?? ""]
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.join("::")
|
|
462
|
+
.replace(/\s+/g, " ")
|
|
463
|
+
.slice(0, 500);
|
|
464
|
+
}
|
|
465
|
+
function startupSessionId(options) {
|
|
466
|
+
const scope = resolvePluginScope(process.cwd());
|
|
467
|
+
const { clientId } = credentials(options?.credentials);
|
|
468
|
+
const scopeKey = [scope.repoName, scope.appPath, scope.serviceName]
|
|
469
|
+
.filter((part) => Boolean(part))
|
|
470
|
+
.join(":") || "workspace";
|
|
471
|
+
return ["mcp-startup", clientId || "mcp_client", scopeKey]
|
|
472
|
+
.join(":")
|
|
473
|
+
.replace(/[^a-zA-Z0-9:_-]+/g, "_")
|
|
474
|
+
.slice(0, 240);
|
|
475
|
+
}
|
|
476
|
+
function loginCommandHint() {
|
|
477
|
+
for (const value of [
|
|
478
|
+
process.env.ASKTHEW_EMAIL,
|
|
479
|
+
process.env.GIT_AUTHOR_EMAIL,
|
|
480
|
+
process.env.GIT_COMMITTER_EMAIL,
|
|
481
|
+
process.env.EMAIL,
|
|
482
|
+
]) {
|
|
483
|
+
const email = String(value ?? "").trim();
|
|
484
|
+
if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
|
485
|
+
return `npx @askthew/mcp-plugin identify --email ${email}`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return "npx @askthew/mcp-plugin identify --email <your-email>";
|
|
489
|
+
}
|
|
490
|
+
async function sendStartupHeartbeat(options) {
|
|
491
|
+
if (!hasServerIdentity(options?.credentials)) {
|
|
196
492
|
return;
|
|
197
493
|
}
|
|
198
|
-
const { installToken, apiKey, clientId, clientLabel, hostType, serverName } = credentials();
|
|
494
|
+
const { installToken, apiKey, clientId, clientLabel, hostType, serverName } = credentials(options?.credentials);
|
|
199
495
|
if (!installToken) {
|
|
200
496
|
return;
|
|
201
497
|
}
|
|
202
498
|
const scope = resolvePluginScope(process.cwd());
|
|
203
|
-
|
|
499
|
+
const fetcher = options?.fetchImpl ?? fetch;
|
|
500
|
+
await fetcher(`${apiBaseUrl(options?.apiBaseUrl)}/api/connectors/mcp/heartbeat`, {
|
|
204
501
|
method: "POST",
|
|
205
502
|
headers: {
|
|
206
503
|
"Content-Type": "application/json",
|
|
@@ -219,37 +516,254 @@ async function sendStartupHeartbeat() {
|
|
|
219
516
|
}),
|
|
220
517
|
}).catch(() => null);
|
|
221
518
|
}
|
|
222
|
-
|
|
519
|
+
async function sendStartupSetupSignal(options) {
|
|
520
|
+
const sessionSignal = {
|
|
521
|
+
sessionId: startupSessionId(options),
|
|
522
|
+
sequence: 0,
|
|
523
|
+
kind: "setup_complete",
|
|
524
|
+
summary: "Ask The W plugin server started for this coding-agent session.",
|
|
525
|
+
evidence: [],
|
|
526
|
+
filesTouched: [],
|
|
527
|
+
commandsRun: [],
|
|
528
|
+
metadata: {
|
|
529
|
+
...runtimeMetadata(options),
|
|
530
|
+
automated: true,
|
|
531
|
+
operational: true,
|
|
532
|
+
origin: "mcp_server_startup",
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
await postToServer("/api/ingest/mcp", {
|
|
536
|
+
sessionSignal: redactCodingSessionSignal(sessionSignal),
|
|
537
|
+
}, options);
|
|
538
|
+
}
|
|
539
|
+
async function sendStartupSignals(options) {
|
|
540
|
+
await sendStartupHeartbeat(options).catch(() => null);
|
|
541
|
+
await sendStartupSetupSignal(options).catch(() => null);
|
|
542
|
+
}
|
|
543
|
+
export function createAskTheWMcpServer(options = {}) {
|
|
544
|
+
const initialMode = resolveMcpMode();
|
|
545
|
+
if (initialMode.mode === "free_pending_auth") {
|
|
546
|
+
ensureLocalIdentity({
|
|
547
|
+
emailClaim: process.env.ASKTHEW_EMAIL,
|
|
548
|
+
apiUrl: options.apiBaseUrl,
|
|
549
|
+
telemetryOptOut: process.env.ASKTHEW_TELEMETRY === "off",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const resolvedMode = resolveMcpMode();
|
|
553
|
+
const optionInstallToken = normalizeInstallTokenInput(options.credentials?.installToken);
|
|
554
|
+
const mode = optionInstallToken
|
|
555
|
+
? { mode: "paid", installToken: optionInstallToken, reason: "options_install_token" }
|
|
556
|
+
: resolvedMode;
|
|
557
|
+
const localStore = mode.mode === "paid" ? null : LocalStore.open();
|
|
558
|
+
if (localStore && mode.mode === "free" && mode.cliCredentials) {
|
|
559
|
+
void flushTelemetryOutbox({
|
|
560
|
+
store: localStore,
|
|
561
|
+
credentials: mode.cliCredentials,
|
|
562
|
+
apiUrl: options.apiBaseUrl,
|
|
563
|
+
fetchImpl: options.fetchImpl,
|
|
564
|
+
}).catch(() => null);
|
|
565
|
+
}
|
|
223
566
|
const server = new McpServer({
|
|
224
|
-
name: "
|
|
225
|
-
version: "
|
|
567
|
+
name: "Ask The W Coding Agent Connector",
|
|
568
|
+
version: "0.4.0",
|
|
226
569
|
});
|
|
227
|
-
|
|
570
|
+
if (options.sendStartupHeartbeat !== false && mode.mode === "paid") {
|
|
571
|
+
void sendStartupSignals(options);
|
|
572
|
+
}
|
|
573
|
+
const apiToolResponse = async (route, payload = {}, method = "GET", request) => {
|
|
574
|
+
const upstream = await postToServer(route, payload, options, { method, idempotencyKey: request?.idempotencyKey });
|
|
575
|
+
return {
|
|
576
|
+
content: [
|
|
577
|
+
{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: JSON.stringify(upstream, null, 2),
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
};
|
|
584
|
+
const localResponse = (value) => toolJson(value);
|
|
585
|
+
const localToolError = (input) => localResponse(structuredError(input));
|
|
586
|
+
const budgetedLocalResponse = (value, maxChars) => {
|
|
587
|
+
if (!maxChars || JSON.stringify(value, null, 2).length <= maxChars) {
|
|
588
|
+
return localResponse(value);
|
|
589
|
+
}
|
|
590
|
+
const next = { ...value, truncated: true, maxChars };
|
|
591
|
+
for (const key of ["signals", "decisions", "decisionCandidates", "matches"]) {
|
|
592
|
+
const list = next[key];
|
|
593
|
+
if (Array.isArray(list)) {
|
|
594
|
+
while (list.length > 0 && JSON.stringify(next, null, 2).length > maxChars) {
|
|
595
|
+
list.pop();
|
|
596
|
+
}
|
|
597
|
+
next[key] = list;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (typeof next.rendered === "string" && JSON.stringify(next, null, 2).length > maxChars) {
|
|
601
|
+
const overhead = JSON.stringify({ ...next, rendered: "" }, null, 2).length + 80;
|
|
602
|
+
next.rendered = `${next.rendered.slice(0, Math.max(0, maxChars - overhead)).trimEnd()}\n\n[truncated to max_chars=${maxChars}]`;
|
|
603
|
+
}
|
|
604
|
+
return localResponse(next);
|
|
605
|
+
};
|
|
606
|
+
const currentScopeKey = () => localScopeKey();
|
|
607
|
+
const compactWriteResponse = (input) => localResponse({
|
|
608
|
+
ok: input.ok !== false,
|
|
609
|
+
id: input.id ?? null,
|
|
610
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
611
|
+
...(typeof input.sequence === "number" ? { sequence: input.sequence } : {}),
|
|
612
|
+
...(typeof input.signalCount === "number" ? { signalCount: input.signalCount } : {}),
|
|
613
|
+
...(input.warnings && input.warnings.length > 0 ? { warnings: input.warnings } : {}),
|
|
614
|
+
});
|
|
615
|
+
const upstreamId = (upstream) => {
|
|
616
|
+
if (!upstream || typeof upstream !== "object")
|
|
617
|
+
return null;
|
|
618
|
+
const record = upstream;
|
|
619
|
+
return (record.id ??
|
|
620
|
+
record.signalId ??
|
|
621
|
+
record.entry?.id ??
|
|
622
|
+
record.decision?.id ??
|
|
623
|
+
record.data?.id ??
|
|
624
|
+
record.data?.outcome?.id ??
|
|
625
|
+
record.data?.decision?.id ??
|
|
626
|
+
null);
|
|
627
|
+
};
|
|
628
|
+
const upstreamSequence = (upstream) => {
|
|
629
|
+
if (!upstream || typeof upstream !== "object")
|
|
630
|
+
return null;
|
|
631
|
+
const record = upstream;
|
|
632
|
+
const sequence = record.sequence ?? record.entry?.sequence ?? record.data?.sequence ?? null;
|
|
633
|
+
return typeof sequence === "number" ? sequence : null;
|
|
634
|
+
};
|
|
635
|
+
const upstreamFailure = (upstream) => upstream && typeof upstream === "object" && upstream.ok === false
|
|
636
|
+
? upstream
|
|
637
|
+
: null;
|
|
638
|
+
const requireFreeIdentity = () => {
|
|
639
|
+
if (mode.mode === "unauthenticated" || mode.mode === "free_pending_auth") {
|
|
640
|
+
const loginCommand = loginCommandHint();
|
|
641
|
+
return localToolError({
|
|
642
|
+
code: "free_tier_login_required",
|
|
643
|
+
message: "Free local mode needs a local install identity before capture.",
|
|
644
|
+
retryable: false,
|
|
645
|
+
hint: `Run \`${loginCommand}\` or reinstall with \`--free --email <your-email>\`, then restart or reload the MCP host.`,
|
|
646
|
+
extra: {
|
|
647
|
+
loginCommand,
|
|
648
|
+
supportEmail: "support@askthew.com",
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
};
|
|
228
654
|
server.tool("capture_session_signal", {
|
|
229
655
|
sessionId: z.string().min(1),
|
|
230
656
|
sequence: z.number().int().nonnegative(),
|
|
231
657
|
kind: sessionSignalKindSchema,
|
|
232
658
|
summary: z.string().min(1).max(2000),
|
|
233
659
|
evidence: z
|
|
234
|
-
.array(
|
|
235
|
-
role: evidenceRoleSchema,
|
|
236
|
-
excerpt: z.string().min(1).max(500),
|
|
237
|
-
}))
|
|
660
|
+
.array(evidenceEntrySchema)
|
|
238
661
|
.default([]),
|
|
239
662
|
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
240
663
|
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
241
664
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
665
|
+
idempotencyKey: idempotencyKeySchema,
|
|
666
|
+
echo: echoSchema,
|
|
242
667
|
}, async (payload) => {
|
|
243
668
|
const sessionSignal = redactCodingSessionSignal({
|
|
244
669
|
...payload,
|
|
245
670
|
metadata: {
|
|
246
|
-
...runtimeMetadata(),
|
|
671
|
+
...runtimeMetadata(options),
|
|
247
672
|
...(payload.metadata ?? {}),
|
|
248
673
|
},
|
|
249
674
|
});
|
|
250
|
-
const
|
|
675
|
+
const scopeKey = currentScopeKey();
|
|
676
|
+
if (mode.mode !== "paid" && localStore) {
|
|
677
|
+
const loginRequired = requireFreeIdentity();
|
|
678
|
+
if (loginRequired) {
|
|
679
|
+
return loginRequired;
|
|
680
|
+
}
|
|
681
|
+
const signal = localStore.insertSignal({
|
|
682
|
+
sessionId: sessionSignal.sessionId,
|
|
683
|
+
sequence: sessionSignal.sequence,
|
|
684
|
+
kind: sessionSignal.kind,
|
|
685
|
+
summary: sessionSignal.summary,
|
|
686
|
+
evidence: sessionSignal.evidence,
|
|
687
|
+
filesTouched: sessionSignal.filesTouched,
|
|
688
|
+
commandsRun: sessionSignal.commandsRun,
|
|
689
|
+
metadata: sessionSignal.metadata,
|
|
690
|
+
scopeKey,
|
|
691
|
+
});
|
|
692
|
+
const sessionSignalCount = localStore.listSignals({
|
|
693
|
+
sessionId: signal.sessionId,
|
|
694
|
+
scopeKey,
|
|
695
|
+
limit: 100000,
|
|
696
|
+
}).length;
|
|
697
|
+
if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
|
|
698
|
+
localStore.enqueueTelemetry(buildTelemetryPayload({
|
|
699
|
+
store: localStore,
|
|
700
|
+
credentials: mode.cliCredentials,
|
|
701
|
+
sessionId: sessionSignal.sessionId,
|
|
702
|
+
}));
|
|
703
|
+
void flushTelemetryOutbox({
|
|
704
|
+
store: localStore,
|
|
705
|
+
credentials: mode.cliCredentials,
|
|
706
|
+
apiUrl: options.apiBaseUrl,
|
|
707
|
+
fetchImpl: options.fetchImpl,
|
|
708
|
+
}).catch(() => null);
|
|
709
|
+
}
|
|
710
|
+
if (!payload.echo) {
|
|
711
|
+
return compactWriteResponse({
|
|
712
|
+
id: signal.id,
|
|
713
|
+
sessionId: signal.sessionId,
|
|
714
|
+
sequence: signal.sequence,
|
|
715
|
+
signalCount: sessionSignalCount,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
if (payload.echo === "summary") {
|
|
719
|
+
return localResponse({
|
|
720
|
+
ok: true,
|
|
721
|
+
id: signal.id,
|
|
722
|
+
sessionId: signal.sessionId,
|
|
723
|
+
sequence: signal.sequence,
|
|
724
|
+
signalCount: sessionSignalCount,
|
|
725
|
+
summary: signal.summary,
|
|
726
|
+
kind: signal.kind,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return localResponse({
|
|
730
|
+
ok: true,
|
|
731
|
+
tier: "free",
|
|
732
|
+
signal,
|
|
733
|
+
note: localStore.usingJsonFallback
|
|
734
|
+
? "Captured locally in JSON fallback mode because SQLite was unavailable."
|
|
735
|
+
: "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
const upstream = await postToServer(payload.echo === "full" ? "/api/ingest/mcp" : withResponseShape("/api/ingest/mcp"), {
|
|
251
739
|
sessionSignal,
|
|
252
|
-
});
|
|
740
|
+
}, options, { idempotencyKey: payload.idempotencyKey });
|
|
741
|
+
const failure = upstreamFailure(upstream);
|
|
742
|
+
if (failure)
|
|
743
|
+
return localResponse(failure);
|
|
744
|
+
if (!payload.echo) {
|
|
745
|
+
return compactWriteResponse({
|
|
746
|
+
id: upstreamId(upstream),
|
|
747
|
+
sessionId: sessionSignal.sessionId,
|
|
748
|
+
sequence: sessionSignal.sequence,
|
|
749
|
+
signalCount: upstream && typeof upstream === "object" && typeof upstream.signalCount === "number"
|
|
750
|
+
? upstream.signalCount
|
|
751
|
+
: 1,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
if (payload.echo === "summary") {
|
|
755
|
+
return localResponse({
|
|
756
|
+
ok: true,
|
|
757
|
+
id: upstreamId(upstream),
|
|
758
|
+
sessionId: sessionSignal.sessionId,
|
|
759
|
+
sequence: sessionSignal.sequence,
|
|
760
|
+
signalCount: upstream && typeof upstream === "object" && typeof upstream.signalCount === "number"
|
|
761
|
+
? upstream.signalCount
|
|
762
|
+
: 1,
|
|
763
|
+
summary: sessionSignal.summary,
|
|
764
|
+
kind: sessionSignal.kind,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
253
767
|
return {
|
|
254
768
|
content: [
|
|
255
769
|
{
|
|
@@ -264,5 +778,1124 @@ export function createAskTheWMcpServer() {
|
|
|
264
778
|
],
|
|
265
779
|
};
|
|
266
780
|
});
|
|
781
|
+
server.tool("list_decisions", {
|
|
782
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
783
|
+
cursor: z.string().optional(),
|
|
784
|
+
sessionId: z.string().optional(),
|
|
785
|
+
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
786
|
+
since: z.string().optional(),
|
|
787
|
+
compact: z.boolean().optional(),
|
|
788
|
+
max_chars: maxCharsSchema,
|
|
789
|
+
}, async (payload) => {
|
|
790
|
+
if (mode.mode !== "paid" && localStore) {
|
|
791
|
+
const loginRequired = requireFreeIdentity();
|
|
792
|
+
if (loginRequired)
|
|
793
|
+
return loginRequired;
|
|
794
|
+
const decisions = localStore.listDecisions({
|
|
795
|
+
limit: payload.limit ?? 5,
|
|
796
|
+
cursor: payload.cursor,
|
|
797
|
+
sessionId: payload.sessionId,
|
|
798
|
+
status: payload.status,
|
|
799
|
+
since: payload.since,
|
|
800
|
+
scopeKey: currentScopeKey(),
|
|
801
|
+
});
|
|
802
|
+
return budgetedLocalResponse({
|
|
803
|
+
ok: true,
|
|
804
|
+
tier: "free",
|
|
805
|
+
decisions: payload.compact !== false
|
|
806
|
+
? decisions.map((decision) => ({
|
|
807
|
+
id: decision.id,
|
|
808
|
+
headline: decision.headline,
|
|
809
|
+
status: decision.status,
|
|
810
|
+
signalIds: decision.sourceSignalIds,
|
|
811
|
+
}))
|
|
812
|
+
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
813
|
+
nextCursor: decisions.length >= (payload.limit ?? 5) ? decisions.at(-1)?.createdAt ?? null : null,
|
|
814
|
+
}, payload.max_chars ?? 8000);
|
|
815
|
+
}
|
|
816
|
+
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
817
|
+
limit: payload.limit ?? 5,
|
|
818
|
+
cursor: payload.cursor,
|
|
819
|
+
sessionId: payload.sessionId,
|
|
820
|
+
status: payload.status,
|
|
821
|
+
since: payload.since,
|
|
822
|
+
compact: payload.compact ?? true,
|
|
823
|
+
max_chars: payload.max_chars ?? 8000,
|
|
824
|
+
}));
|
|
825
|
+
});
|
|
826
|
+
server.tool("get_decision", {
|
|
827
|
+
id: z.string().min(1),
|
|
828
|
+
}, async (payload) => {
|
|
829
|
+
if (mode.mode !== "paid" && localStore) {
|
|
830
|
+
const loginRequired = requireFreeIdentity();
|
|
831
|
+
if (loginRequired)
|
|
832
|
+
return loginRequired;
|
|
833
|
+
const decision = localStore.getDecision(payload.id);
|
|
834
|
+
return decision
|
|
835
|
+
? localResponse({ ok: true, tier: "free", decision: decisionWithSignals(localStore, decision) })
|
|
836
|
+
: localToolError({
|
|
837
|
+
code: "not_found",
|
|
838
|
+
message: "Decision not found in the local Ask The W store.",
|
|
839
|
+
retryable: false,
|
|
840
|
+
hint: "Check the decision id or search/list local decisions first.",
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`);
|
|
844
|
+
});
|
|
845
|
+
server.tool("create_decision", {
|
|
846
|
+
content: z.string().min(1),
|
|
847
|
+
idempotencyKey: idempotencyKeySchema,
|
|
848
|
+
echo: echoSchema,
|
|
849
|
+
}, async (payload) => {
|
|
850
|
+
if (mode.mode !== "paid" && localStore) {
|
|
851
|
+
const loginRequired = requireFreeIdentity();
|
|
852
|
+
if (loginRequired)
|
|
853
|
+
return loginRequired;
|
|
854
|
+
const scopeKey = currentScopeKey();
|
|
855
|
+
if (payload.idempotencyKey) {
|
|
856
|
+
const existingId = localStore.getMeta(`idempotency:create_decision:${scopeKey}:${payload.idempotencyKey}`);
|
|
857
|
+
if (existingId) {
|
|
858
|
+
const existing = localStore.getDecision(existingId);
|
|
859
|
+
if (existing) {
|
|
860
|
+
return payload.echo === "full"
|
|
861
|
+
? localResponse({ ok: true, tier: "free", decision: decisionWithSignals(localStore, existing), idempotent: true })
|
|
862
|
+
: compactWriteResponse({ id: existing.id, sequence: localStore.stats().decisions });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const decision = localStore.createDecision({
|
|
867
|
+
rawContent: payload.content,
|
|
868
|
+
sessionId: localStore.mostRecentSessionId({ scopeKey }),
|
|
869
|
+
scopeKey,
|
|
870
|
+
});
|
|
871
|
+
if (payload.idempotencyKey) {
|
|
872
|
+
localStore.setMeta(`idempotency:create_decision:${scopeKey}:${payload.idempotencyKey}`, decision.id);
|
|
873
|
+
}
|
|
874
|
+
const warnings = detectDecisionConflicts({
|
|
875
|
+
decision,
|
|
876
|
+
decisions: localStore.listDecisions({ limit: 100000, scopeKey }),
|
|
877
|
+
});
|
|
878
|
+
if (!payload.echo) {
|
|
879
|
+
return compactWriteResponse({
|
|
880
|
+
id: decision.id,
|
|
881
|
+
sequence: localStore.stats().decisions,
|
|
882
|
+
warnings,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return localResponse({
|
|
886
|
+
ok: true,
|
|
887
|
+
...(payload.echo === "summary"
|
|
888
|
+
? { id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
889
|
+
: { tier: "free", decision: decisionWithSignals(localStore, decision), warnings }),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const upstream = await postToServer(payload.echo === "full" ? "/api/decisions" : withResponseShape("/api/decisions"), {
|
|
893
|
+
content: payload.content,
|
|
894
|
+
}, options, { method: "POST", idempotencyKey: payload.idempotencyKey });
|
|
895
|
+
const failure = upstreamFailure(upstream);
|
|
896
|
+
if (failure)
|
|
897
|
+
return localResponse(failure);
|
|
898
|
+
if (!payload.echo) {
|
|
899
|
+
return compactWriteResponse({
|
|
900
|
+
id: upstreamId(upstream),
|
|
901
|
+
sequence: upstreamSequence(upstream) ?? 1,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return localResponse(payload.echo === "summary"
|
|
905
|
+
? { ok: true, id: upstreamId(upstream), sequence: upstreamSequence(upstream) ?? 1 }
|
|
906
|
+
: upstream);
|
|
907
|
+
});
|
|
908
|
+
server.tool("update_decision", {
|
|
909
|
+
id: z.string().min(1),
|
|
910
|
+
headline: z.string().min(1).optional(),
|
|
911
|
+
why: z.string().optional(),
|
|
912
|
+
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
913
|
+
alignment: z.enum(["aligned", "orthogonal", "conflicts", "ambiguous"]).optional(),
|
|
914
|
+
outcomeId: z.string().min(1).optional(),
|
|
915
|
+
idempotencyKey: idempotencyKeySchema,
|
|
916
|
+
echo: echoSchema,
|
|
917
|
+
}, async (payload) => {
|
|
918
|
+
if (mode.mode !== "paid" && localStore) {
|
|
919
|
+
const loginRequired = requireFreeIdentity();
|
|
920
|
+
if (loginRequired)
|
|
921
|
+
return loginRequired;
|
|
922
|
+
const decision = localStore.updateDecision(payload.id, {
|
|
923
|
+
...(payload.headline ? { headline: payload.headline } : {}),
|
|
924
|
+
...(payload.why !== undefined ? { why: payload.why } : {}),
|
|
925
|
+
...(payload.status ? { status: payload.status } : {}),
|
|
926
|
+
...(payload.alignment !== undefined ? { alignment: payload.alignment } : {}),
|
|
927
|
+
});
|
|
928
|
+
if (!decision) {
|
|
929
|
+
return localToolError({
|
|
930
|
+
code: "not_found",
|
|
931
|
+
message: "Decision not found in the local Ask The W store.",
|
|
932
|
+
retryable: false,
|
|
933
|
+
hint: "Check the decision id before updating.",
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
const warnings = detectDecisionConflicts({
|
|
937
|
+
decision,
|
|
938
|
+
decisions: localStore.listDecisions({ limit: 100000, scopeKey: currentScopeKey() }),
|
|
939
|
+
});
|
|
940
|
+
if (!payload.echo) {
|
|
941
|
+
return compactWriteResponse({ id: decision.id, sequence: localStore.stats().decisions, warnings });
|
|
942
|
+
}
|
|
943
|
+
return localResponse(payload.echo === "summary"
|
|
944
|
+
? { ok: true, id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
945
|
+
: { ok: true, tier: "free", decision: decisionWithSignals(localStore, decision), warnings });
|
|
946
|
+
}
|
|
947
|
+
const upstream = await postToServer(payload.echo === "full"
|
|
948
|
+
? `/api/decisions/${encodeURIComponent(payload.id)}`
|
|
949
|
+
: withResponseShape(`/api/decisions/${encodeURIComponent(payload.id)}`), {
|
|
950
|
+
headline: payload.headline,
|
|
951
|
+
why: payload.why,
|
|
952
|
+
status: payload.status,
|
|
953
|
+
alignment: payload.alignment,
|
|
954
|
+
outcomeId: payload.outcomeId,
|
|
955
|
+
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
956
|
+
const failure = upstreamFailure(upstream);
|
|
957
|
+
if (failure)
|
|
958
|
+
return localResponse(failure);
|
|
959
|
+
if (!payload.echo) {
|
|
960
|
+
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
961
|
+
}
|
|
962
|
+
return localResponse(payload.echo === "summary"
|
|
963
|
+
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
964
|
+
: upstream);
|
|
965
|
+
});
|
|
966
|
+
server.tool("delete_decision", {
|
|
967
|
+
id: z.string().min(1),
|
|
968
|
+
confirmText: z.string().min(1),
|
|
969
|
+
idempotencyKey: idempotencyKeySchema,
|
|
970
|
+
}, async (payload) => {
|
|
971
|
+
if (mode.mode !== "paid" && localStore) {
|
|
972
|
+
const loginRequired = requireFreeIdentity();
|
|
973
|
+
if (loginRequired)
|
|
974
|
+
return loginRequired;
|
|
975
|
+
if (payload.confirmText !== payload.id) {
|
|
976
|
+
return localToolError({
|
|
977
|
+
code: "confirmation_required",
|
|
978
|
+
message: "confirmText must match the decision id.",
|
|
979
|
+
retryable: false,
|
|
980
|
+
hint: "Pass the exact decision id as confirmText.",
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
return localResponse({ ok: localStore.deleteDecision(payload.id), tier: "free" });
|
|
984
|
+
}
|
|
985
|
+
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
|
|
986
|
+
confirmText: payload.confirmText,
|
|
987
|
+
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
988
|
+
});
|
|
989
|
+
server.tool("list_outcomes", paidDescription("List outcomes from your workspace.", mode.mode), {
|
|
990
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
991
|
+
cursor: cursorSchema,
|
|
992
|
+
}, async (payload) => {
|
|
993
|
+
if (mode.mode === "free")
|
|
994
|
+
return localResponse(paidFeatureNudge("list_outcomes"));
|
|
995
|
+
return apiToolResponse(routeWithQuery("/api/outcomes", {
|
|
996
|
+
limit: payload.limit,
|
|
997
|
+
cursor: payload.cursor,
|
|
998
|
+
}));
|
|
999
|
+
});
|
|
1000
|
+
server.tool("get_outcome", paidDescription("Get outcome detail from your workspace.", mode.mode), {
|
|
1001
|
+
id: z.string().min(1),
|
|
1002
|
+
}, async (payload) => mode.mode === "free"
|
|
1003
|
+
? localResponse(paidFeatureNudge("get_outcome"))
|
|
1004
|
+
: apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`));
|
|
1005
|
+
server.tool("list_outcome_signals", paidDescription("List signals linked to an outcome.", mode.mode), {
|
|
1006
|
+
id: z.string().min(1),
|
|
1007
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
1008
|
+
cursor: cursorSchema,
|
|
1009
|
+
}, async (payload) => mode.mode === "free"
|
|
1010
|
+
? localResponse(paidFeatureNudge("list_outcome_signals"))
|
|
1011
|
+
: apiToolResponse(routeWithQuery(`/api/outcomes/${encodeURIComponent(payload.id)}/signals`, {
|
|
1012
|
+
limit: payload.limit,
|
|
1013
|
+
cursor: payload.cursor,
|
|
1014
|
+
})));
|
|
1015
|
+
server.tool("create_outcome", paidDescription("Create a new outcome.", mode.mode), {
|
|
1016
|
+
name: z.string().min(1),
|
|
1017
|
+
summary: z.string().optional(),
|
|
1018
|
+
causalHypothesis: z.string().optional(),
|
|
1019
|
+
suggestedAction: z.string().optional(),
|
|
1020
|
+
idempotencyKey: idempotencyKeySchema,
|
|
1021
|
+
}, async (payload) => {
|
|
1022
|
+
if (mode.mode === "free")
|
|
1023
|
+
return localResponse(paidFeatureNudge("create_outcome"));
|
|
1024
|
+
return apiToolResponse("/api/outcomes", {
|
|
1025
|
+
name: payload.name,
|
|
1026
|
+
summary: payload.summary,
|
|
1027
|
+
causalHypothesis: payload.causalHypothesis,
|
|
1028
|
+
suggestedAction: payload.suggestedAction,
|
|
1029
|
+
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1030
|
+
});
|
|
1031
|
+
server.tool("update_outcome", paidDescription("Update an outcome.", mode.mode), {
|
|
1032
|
+
id: z.string().min(1),
|
|
1033
|
+
name: z.string().min(1).optional(),
|
|
1034
|
+
summary: z.string().optional(),
|
|
1035
|
+
causalHypothesis: z.string().optional(),
|
|
1036
|
+
suggestedAction: z.string().optional(),
|
|
1037
|
+
status: z.enum(["active", "achieved", "abandoned", "archived"]).optional(),
|
|
1038
|
+
idempotencyKey: idempotencyKeySchema,
|
|
1039
|
+
echo: echoSchema,
|
|
1040
|
+
}, async (payload) => {
|
|
1041
|
+
if (mode.mode === "free")
|
|
1042
|
+
return localResponse(paidFeatureNudge("update_outcome"));
|
|
1043
|
+
const upstream = await postToServer(payload.echo === "full"
|
|
1044
|
+
? `/api/outcomes/${encodeURIComponent(payload.id)}`
|
|
1045
|
+
: withResponseShape(`/api/outcomes/${encodeURIComponent(payload.id)}`), {
|
|
1046
|
+
name: payload.name,
|
|
1047
|
+
summary: payload.summary,
|
|
1048
|
+
causalHypothesis: payload.causalHypothesis,
|
|
1049
|
+
suggestedAction: payload.suggestedAction,
|
|
1050
|
+
status: payload.status,
|
|
1051
|
+
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
1052
|
+
const failure = upstreamFailure(upstream);
|
|
1053
|
+
if (failure)
|
|
1054
|
+
return localResponse(failure);
|
|
1055
|
+
if (!payload.echo) {
|
|
1056
|
+
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
1057
|
+
}
|
|
1058
|
+
return localResponse(payload.echo === "summary"
|
|
1059
|
+
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
1060
|
+
: upstream);
|
|
1061
|
+
});
|
|
1062
|
+
server.tool("delete_outcome", paidDescription("Delete an outcome.", mode.mode), {
|
|
1063
|
+
id: z.string().min(1),
|
|
1064
|
+
confirmText: z.string().min(1),
|
|
1065
|
+
idempotencyKey: idempotencyKeySchema,
|
|
1066
|
+
}, async (payload) => {
|
|
1067
|
+
if (mode.mode === "free")
|
|
1068
|
+
return localResponse(paidFeatureNudge("delete_outcome"));
|
|
1069
|
+
return apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`, {
|
|
1070
|
+
confirmText: payload.confirmText,
|
|
1071
|
+
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
1072
|
+
});
|
|
1073
|
+
server.tool("get_north_star", paidDescription("Read the workspace north-star metric.", mode.mode), {}, async () => mode.mode === "free"
|
|
1074
|
+
? localResponse(paidFeatureNudge("get_north_star"))
|
|
1075
|
+
: apiToolResponse("/api/north-star"));
|
|
1076
|
+
server.tool("update_north_star", paidDescription("Update the workspace north-star metric.", mode.mode), {
|
|
1077
|
+
metric: z.string().min(1),
|
|
1078
|
+
current: z.string().min(1),
|
|
1079
|
+
target: z.string().min(1),
|
|
1080
|
+
reason: z.string().min(1),
|
|
1081
|
+
idempotencyKey: idempotencyKeySchema,
|
|
1082
|
+
}, async (payload) => {
|
|
1083
|
+
if (mode.mode === "free")
|
|
1084
|
+
return localResponse(paidFeatureNudge("update_north_star"));
|
|
1085
|
+
return apiToolResponse("/api/north-star", {
|
|
1086
|
+
metric: payload.metric,
|
|
1087
|
+
current: payload.current,
|
|
1088
|
+
target: payload.target,
|
|
1089
|
+
reason: payload.reason,
|
|
1090
|
+
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1091
|
+
});
|
|
1092
|
+
server.tool("list_signals", {
|
|
1093
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
1094
|
+
cursor: z.string().optional(),
|
|
1095
|
+
sessionId: z.string().optional(),
|
|
1096
|
+
since: z.string().optional(),
|
|
1097
|
+
compact: z.boolean().optional(),
|
|
1098
|
+
max_chars: maxCharsSchema,
|
|
1099
|
+
}, async (payload) => {
|
|
1100
|
+
if (mode.mode !== "paid" && localStore) {
|
|
1101
|
+
const loginRequired = requireFreeIdentity();
|
|
1102
|
+
if (loginRequired)
|
|
1103
|
+
return loginRequired;
|
|
1104
|
+
const signals = localStore.listSignals({
|
|
1105
|
+
limit: payload.limit ?? 10,
|
|
1106
|
+
cursor: payload.cursor,
|
|
1107
|
+
sessionId: payload.sessionId,
|
|
1108
|
+
since: payload.since,
|
|
1109
|
+
scopeKey: currentScopeKey(),
|
|
1110
|
+
});
|
|
1111
|
+
return budgetedLocalResponse({
|
|
1112
|
+
ok: true,
|
|
1113
|
+
tier: "free",
|
|
1114
|
+
signals: payload.compact !== false
|
|
1115
|
+
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
1116
|
+
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
1117
|
+
nextCursor: signals.length >= (payload.limit ?? 10) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
1118
|
+
}, payload.max_chars ?? 8000);
|
|
1119
|
+
}
|
|
1120
|
+
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1121
|
+
limit: payload.limit ?? 10,
|
|
1122
|
+
cursor: payload.cursor,
|
|
1123
|
+
sessionId: payload.sessionId,
|
|
1124
|
+
since: payload.since,
|
|
1125
|
+
compact: payload.compact ?? true,
|
|
1126
|
+
max_chars: payload.max_chars ?? 8000,
|
|
1127
|
+
}));
|
|
1128
|
+
});
|
|
1129
|
+
server.tool("find_signal_by_summary", "Find recent signals by summary text without needing an opaque signal id first.", {
|
|
1130
|
+
query: z.string().min(1),
|
|
1131
|
+
sessionId: z.string().optional(),
|
|
1132
|
+
limit: z.number().int().positive().max(50).default(5),
|
|
1133
|
+
compact: z.boolean().optional(),
|
|
1134
|
+
max_chars: maxCharsSchema,
|
|
1135
|
+
}, async (payload) => {
|
|
1136
|
+
if (mode.mode !== "paid" && localStore) {
|
|
1137
|
+
const loginRequired = requireFreeIdentity();
|
|
1138
|
+
if (loginRequired)
|
|
1139
|
+
return loginRequired;
|
|
1140
|
+
const normalizedQuery = payload.query.toLowerCase();
|
|
1141
|
+
const signals = localStore
|
|
1142
|
+
.listSignals({
|
|
1143
|
+
limit: 100000,
|
|
1144
|
+
sessionId: payload.sessionId,
|
|
1145
|
+
scopeKey: currentScopeKey(),
|
|
1146
|
+
})
|
|
1147
|
+
.filter((signal) => [
|
|
1148
|
+
signal.summary,
|
|
1149
|
+
signal.kind,
|
|
1150
|
+
...signal.filesTouched,
|
|
1151
|
+
...signal.commandsRun,
|
|
1152
|
+
].join("\n").toLowerCase().includes(normalizedQuery))
|
|
1153
|
+
.slice(0, payload.limit);
|
|
1154
|
+
return budgetedLocalResponse({
|
|
1155
|
+
ok: true,
|
|
1156
|
+
tier: "free",
|
|
1157
|
+
query: payload.query,
|
|
1158
|
+
signals: payload.compact === false
|
|
1159
|
+
? signals.map((signal) => signalWithDecision(localStore, signal))
|
|
1160
|
+
: signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))),
|
|
1161
|
+
}, payload.max_chars ?? 8000);
|
|
1162
|
+
}
|
|
1163
|
+
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1164
|
+
query: payload.query,
|
|
1165
|
+
sessionId: payload.sessionId,
|
|
1166
|
+
limit: payload.limit,
|
|
1167
|
+
compact: payload.compact ?? true,
|
|
1168
|
+
max_chars: payload.max_chars ?? 8000,
|
|
1169
|
+
}));
|
|
1170
|
+
});
|
|
1171
|
+
server.tool("get_signal", {
|
|
1172
|
+
id: z.string().min(1),
|
|
1173
|
+
}, async (payload) => {
|
|
1174
|
+
if (mode.mode !== "paid" && localStore) {
|
|
1175
|
+
const loginRequired = requireFreeIdentity();
|
|
1176
|
+
if (loginRequired)
|
|
1177
|
+
return loginRequired;
|
|
1178
|
+
const signal = localStore.getSignal(Number(payload.id));
|
|
1179
|
+
return signal
|
|
1180
|
+
? localResponse({ ok: true, tier: "free", signal: signalWithDecision(localStore, signal) })
|
|
1181
|
+
: localToolError({
|
|
1182
|
+
code: "not_found",
|
|
1183
|
+
message: "Signal not found in the local Ask The W store.",
|
|
1184
|
+
retryable: false,
|
|
1185
|
+
hint: "Check the signal id or list local signals first.",
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
return apiToolResponse(`/api/signals/${encodeURIComponent(payload.id)}`);
|
|
1189
|
+
});
|
|
1190
|
+
server.tool("review_decisions", "Review captured decisions. Use for natural prompts like: What did I decide yesterday?", {
|
|
1191
|
+
since: z.string().optional(),
|
|
1192
|
+
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1193
|
+
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1194
|
+
limit: z.number().int().positive().max(300).default(50),
|
|
1195
|
+
cursor: cursorSchema,
|
|
1196
|
+
sessionId: z.string().optional(),
|
|
1197
|
+
compact: z.boolean().optional(),
|
|
1198
|
+
max_chars: maxCharsSchema,
|
|
1199
|
+
}, async (payload) => {
|
|
1200
|
+
if (mode.mode === "paid") {
|
|
1201
|
+
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
1202
|
+
since: payload.since,
|
|
1203
|
+
status: payload.status,
|
|
1204
|
+
limit: payload.limit,
|
|
1205
|
+
cursor: payload.cursor,
|
|
1206
|
+
sessionId: payload.sessionId,
|
|
1207
|
+
compact: payload.compact,
|
|
1208
|
+
max_chars: payload.max_chars,
|
|
1209
|
+
}));
|
|
1210
|
+
}
|
|
1211
|
+
if (!localStore) {
|
|
1212
|
+
return localToolError({
|
|
1213
|
+
code: "local_store_unavailable",
|
|
1214
|
+
message: "The local Ask The W store is unavailable.",
|
|
1215
|
+
retryable: true,
|
|
1216
|
+
hint: "Retry after restarting the plugin host.",
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
const loginRequired = requireFreeIdentity();
|
|
1220
|
+
if (loginRequired)
|
|
1221
|
+
return loginRequired;
|
|
1222
|
+
const decisions = localStore.listDecisions({
|
|
1223
|
+
since: payload.since,
|
|
1224
|
+
status: payload.status,
|
|
1225
|
+
limit: payload.limit,
|
|
1226
|
+
cursor: payload.cursor,
|
|
1227
|
+
sessionId: payload.sessionId,
|
|
1228
|
+
scopeKey: currentScopeKey(),
|
|
1229
|
+
});
|
|
1230
|
+
return budgetedLocalResponse({
|
|
1231
|
+
ok: true,
|
|
1232
|
+
tier: "free",
|
|
1233
|
+
format: payload.format,
|
|
1234
|
+
rendered: renderDecisionDigest(decisions),
|
|
1235
|
+
decisions: payload.compact
|
|
1236
|
+
? decisions.map((decision) => ({
|
|
1237
|
+
id: decision.id,
|
|
1238
|
+
headline: decision.headline,
|
|
1239
|
+
status: decision.status,
|
|
1240
|
+
signalIds: decision.sourceSignalIds,
|
|
1241
|
+
}))
|
|
1242
|
+
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
1243
|
+
count: decisions.length,
|
|
1244
|
+
nextCursor: decisions.length >= payload.limit ? decisions.at(-1)?.createdAt ?? null : null,
|
|
1245
|
+
copyHint: "Copy this output to back up your decisions - `export_decisions` is a paid feature.",
|
|
1246
|
+
}, payload.max_chars);
|
|
1247
|
+
});
|
|
1248
|
+
server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
|
|
1249
|
+
sessionId: z.string().optional(),
|
|
1250
|
+
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1251
|
+
cursor: cursorSchema,
|
|
1252
|
+
limit: z.number().int().positive().max(50).default(50),
|
|
1253
|
+
compact: z.boolean().optional(),
|
|
1254
|
+
max_chars: maxCharsSchema,
|
|
1255
|
+
}, async (payload) => {
|
|
1256
|
+
if (!localStore || mode.mode === "paid") {
|
|
1257
|
+
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1258
|
+
sessionId: payload.sessionId,
|
|
1259
|
+
cursor: payload.cursor,
|
|
1260
|
+
limit: payload.limit,
|
|
1261
|
+
compact: payload.compact,
|
|
1262
|
+
max_chars: payload.max_chars,
|
|
1263
|
+
}));
|
|
1264
|
+
}
|
|
1265
|
+
const loginRequired = requireFreeIdentity();
|
|
1266
|
+
if (loginRequired)
|
|
1267
|
+
return loginRequired;
|
|
1268
|
+
const scopeKey = currentScopeKey();
|
|
1269
|
+
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1270
|
+
const allSessionIds = localStore.listSessionIds({ limit: 100000, scopeKey });
|
|
1271
|
+
const allowedSessionIds = new Set(allSessionIds.slice(0, 3));
|
|
1272
|
+
if (sessionId && allSessionIds.length > 3 && !allowedSessionIds.has(sessionId)) {
|
|
1273
|
+
return localToolError({
|
|
1274
|
+
code: "free_tier_limit",
|
|
1275
|
+
message: "The free plugin can review the latest three local sessions.",
|
|
1276
|
+
retryable: false,
|
|
1277
|
+
hint: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
1278
|
+
extra: {
|
|
1279
|
+
tool: "review_session",
|
|
1280
|
+
limit: 3,
|
|
1281
|
+
upgradeUrl: "https://askthew.com/mcp?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=review_session",
|
|
1282
|
+
cta: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
const limit = Math.min(50, payload.limit ?? 50);
|
|
1287
|
+
const signals = sessionId
|
|
1288
|
+
? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
|
|
1289
|
+
: [];
|
|
1290
|
+
const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1291
|
+
const decisions = sessionId
|
|
1292
|
+
? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
|
|
1293
|
+
: [];
|
|
1294
|
+
const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
|
|
1295
|
+
const counts = signals.reduce((accumulator, signal) => {
|
|
1296
|
+
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1297
|
+
return accumulator;
|
|
1298
|
+
}, {});
|
|
1299
|
+
const allCounts = allSignals.reduce((accumulator, signal) => {
|
|
1300
|
+
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1301
|
+
return accumulator;
|
|
1302
|
+
}, {});
|
|
1303
|
+
const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
|
|
1304
|
+
if (payload.format === "json") {
|
|
1305
|
+
return budgetedLocalResponse({
|
|
1306
|
+
ok: true,
|
|
1307
|
+
tier: "free",
|
|
1308
|
+
sessionId,
|
|
1309
|
+
format: "json",
|
|
1310
|
+
signals: payload.compact
|
|
1311
|
+
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
1312
|
+
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
1313
|
+
decisions: payload.compact
|
|
1314
|
+
? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
|
|
1315
|
+
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
1316
|
+
decisionCandidates,
|
|
1317
|
+
nextCursor,
|
|
1318
|
+
counts: {
|
|
1319
|
+
totalSignals: allSignals.length,
|
|
1320
|
+
byKind: allCounts,
|
|
1321
|
+
},
|
|
1322
|
+
}, payload.max_chars);
|
|
1323
|
+
}
|
|
1324
|
+
return budgetedLocalResponse({
|
|
1325
|
+
ok: true,
|
|
1326
|
+
tier: "free",
|
|
1327
|
+
sessionId,
|
|
1328
|
+
format: "markdown",
|
|
1329
|
+
rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
|
|
1330
|
+
...(payload.compact
|
|
1331
|
+
? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
1332
|
+
: {}),
|
|
1333
|
+
counts: {
|
|
1334
|
+
totalSignals: allSignals.length,
|
|
1335
|
+
byKind: Object.keys(allCounts).length ? allCounts : counts,
|
|
1336
|
+
},
|
|
1337
|
+
}, payload.max_chars);
|
|
1338
|
+
});
|
|
1339
|
+
server.tool("recap", "Summarize the latest local coding-agent session as a digest, standup, or share-ready recap.", {
|
|
1340
|
+
format: z.enum(["digest", "standup", "share"]).default("digest"),
|
|
1341
|
+
sessionId: z.string().optional(),
|
|
1342
|
+
compact: z.boolean().optional(),
|
|
1343
|
+
max_chars: maxCharsSchema,
|
|
1344
|
+
}, async (payload) => {
|
|
1345
|
+
if (!localStore || mode.mode === "paid")
|
|
1346
|
+
return localResponse(paidFeatureNudge("recap"));
|
|
1347
|
+
const loginRequired = requireFreeIdentity();
|
|
1348
|
+
if (loginRequired)
|
|
1349
|
+
return loginRequired;
|
|
1350
|
+
const scopeKey = currentScopeKey();
|
|
1351
|
+
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1352
|
+
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1353
|
+
const decisions = sessionId ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1354
|
+
return budgetedLocalResponse({
|
|
1355
|
+
ok: true,
|
|
1356
|
+
tier: "free",
|
|
1357
|
+
sessionId,
|
|
1358
|
+
format: payload.format,
|
|
1359
|
+
rendered: renderRecap({ format: payload.format, signals, decisions }),
|
|
1360
|
+
...(payload.compact
|
|
1361
|
+
? { signals: signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
1362
|
+
: {}),
|
|
1363
|
+
}, payload.max_chars);
|
|
1364
|
+
});
|
|
1365
|
+
server.tool("coach", "Coach the local coding-agent session. Use for natural prompts like: Coach me on this session.", {
|
|
1366
|
+
scope: z.enum(["session", "week", "patterns"]).default("session"),
|
|
1367
|
+
sessionId: z.string().optional(),
|
|
1368
|
+
max_chars: maxCharsSchema,
|
|
1369
|
+
}, async (payload) => {
|
|
1370
|
+
if (payload.scope === "week" || payload.scope === "patterns") {
|
|
1371
|
+
return localResponse(paidFeatureNudge("coach"));
|
|
1372
|
+
}
|
|
1373
|
+
if (!localStore || mode.mode === "paid")
|
|
1374
|
+
return localResponse(paidFeatureNudge("coach"));
|
|
1375
|
+
const loginRequired = requireFreeIdentity();
|
|
1376
|
+
if (loginRequired)
|
|
1377
|
+
return loginRequired;
|
|
1378
|
+
const scopeKey = currentScopeKey();
|
|
1379
|
+
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1380
|
+
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1381
|
+
const decisions = sessionId ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1382
|
+
const coaching = buildSessionCoach({ signals, decisions });
|
|
1383
|
+
return budgetedLocalResponse({
|
|
1384
|
+
ok: true,
|
|
1385
|
+
tier: "free",
|
|
1386
|
+
scope: "session",
|
|
1387
|
+
sessionId,
|
|
1388
|
+
qualityScore: coaching.qualityScore,
|
|
1389
|
+
biggestGap: coaching.biggestGap,
|
|
1390
|
+
failureMode: coaching.failureMode,
|
|
1391
|
+
rendered: `Decision quality score: ${coaching.qualityScore}/100\nBiggest gap: ${coaching.biggestGap}`,
|
|
1392
|
+
}, payload.max_chars);
|
|
1393
|
+
});
|
|
1394
|
+
server.tool("promote_signal_to_decision", "Copy a captured signal summary into a linked local decision.", {
|
|
1395
|
+
signalId: z.union([z.string(), z.number()]),
|
|
1396
|
+
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).default("proposed"),
|
|
1397
|
+
why: z.string().optional(),
|
|
1398
|
+
idempotencyKey: idempotencyKeySchema,
|
|
1399
|
+
}, async (payload) => {
|
|
1400
|
+
if (!localStore || mode.mode === "paid")
|
|
1401
|
+
return localResponse(paidFeatureNudge("promote_signal_to_decision"));
|
|
1402
|
+
const loginRequired = requireFreeIdentity();
|
|
1403
|
+
if (loginRequired)
|
|
1404
|
+
return loginRequired;
|
|
1405
|
+
const numericSignalId = typeof payload.signalId === "number" ? payload.signalId : Number(payload.signalId);
|
|
1406
|
+
if (!Number.isFinite(numericSignalId)) {
|
|
1407
|
+
return localToolError({
|
|
1408
|
+
code: "invalid_input",
|
|
1409
|
+
message: "Invalid signalId.",
|
|
1410
|
+
retryable: false,
|
|
1411
|
+
hint: "Use the numeric local signal id.",
|
|
1412
|
+
extra: { field: "signalId" },
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
const signal = localStore.getSignal(numericSignalId);
|
|
1416
|
+
if (!signal) {
|
|
1417
|
+
return localToolError({
|
|
1418
|
+
code: "not_found",
|
|
1419
|
+
message: "Signal not found in the local Ask The W store.",
|
|
1420
|
+
retryable: false,
|
|
1421
|
+
hint: "List signals first, then pass the numeric local signal id.",
|
|
1422
|
+
extra: { tool: "promote_signal_to_decision" },
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
if (payload.idempotencyKey) {
|
|
1426
|
+
const existingId = localStore.getMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`);
|
|
1427
|
+
if (existingId) {
|
|
1428
|
+
const existing = localStore.getDecision(existingId);
|
|
1429
|
+
if (existing) {
|
|
1430
|
+
return localResponse({
|
|
1431
|
+
ok: true,
|
|
1432
|
+
id: existing.id,
|
|
1433
|
+
sequence: localStore.stats().decisions,
|
|
1434
|
+
decision: decisionWithSignals(localStore, existing),
|
|
1435
|
+
linkedSignalId: signal.id,
|
|
1436
|
+
idempotent: true,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const decision = localStore.createDecision({
|
|
1442
|
+
rawContent: signal.summary,
|
|
1443
|
+
headline: signal.summary,
|
|
1444
|
+
why: payload.why ?? null,
|
|
1445
|
+
status: payload.status,
|
|
1446
|
+
sessionId: signal.sessionId,
|
|
1447
|
+
files: signal.filesTouched,
|
|
1448
|
+
sourceSignalIds: [signal.id],
|
|
1449
|
+
scopeKey: signal.scopeKey ?? currentScopeKey(),
|
|
1450
|
+
});
|
|
1451
|
+
if (payload.idempotencyKey) {
|
|
1452
|
+
localStore.setMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`, decision.id);
|
|
1453
|
+
}
|
|
1454
|
+
const warnings = detectDecisionConflicts({
|
|
1455
|
+
decision,
|
|
1456
|
+
decisions: localStore.listDecisions({ limit: 100000, scopeKey: decision.scopeKey }),
|
|
1457
|
+
});
|
|
1458
|
+
return localResponse({
|
|
1459
|
+
ok: true,
|
|
1460
|
+
id: decision.id,
|
|
1461
|
+
sequence: localStore.stats().decisions,
|
|
1462
|
+
decision: decisionWithSignals(localStore, decision),
|
|
1463
|
+
linkedSignalId: signal.id,
|
|
1464
|
+
warnings,
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
server.tool("list_decision_candidates", "List local signals that look like decision moments and can be promoted.", {
|
|
1468
|
+
sessionId: z.string().optional(),
|
|
1469
|
+
limit: z.number().int().positive().max(300).default(50),
|
|
1470
|
+
cursor: cursorSchema,
|
|
1471
|
+
max_chars: maxCharsSchema,
|
|
1472
|
+
}, async (payload) => {
|
|
1473
|
+
if (!localStore || mode.mode === "paid")
|
|
1474
|
+
return localResponse(paidFeatureNudge("list_decision_candidates"));
|
|
1475
|
+
const loginRequired = requireFreeIdentity();
|
|
1476
|
+
if (loginRequired)
|
|
1477
|
+
return loginRequired;
|
|
1478
|
+
const scopeKey = currentScopeKey();
|
|
1479
|
+
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1480
|
+
const result = listDecisionCandidates({
|
|
1481
|
+
store: localStore,
|
|
1482
|
+
sessionId,
|
|
1483
|
+
scopeKey,
|
|
1484
|
+
limit: payload.limit,
|
|
1485
|
+
cursor: payload.cursor,
|
|
1486
|
+
});
|
|
1487
|
+
return budgetedLocalResponse({
|
|
1488
|
+
ok: true,
|
|
1489
|
+
tier: "free",
|
|
1490
|
+
sessionId,
|
|
1491
|
+
decisionCandidates: result.candidates,
|
|
1492
|
+
nextCursor: result.nextCursor,
|
|
1493
|
+
}, payload.max_chars);
|
|
1494
|
+
});
|
|
1495
|
+
server.tool("search_trail", "Search local signals and decisions together.", {
|
|
1496
|
+
query: z.string().min(1),
|
|
1497
|
+
sessionId: z.string().optional(),
|
|
1498
|
+
limit: z.number().int().positive().max(100).default(25),
|
|
1499
|
+
cursor: cursorSchema,
|
|
1500
|
+
compact: z.boolean().optional(),
|
|
1501
|
+
max_chars: maxCharsSchema,
|
|
1502
|
+
}, async (payload) => {
|
|
1503
|
+
if (!localStore || mode.mode === "paid")
|
|
1504
|
+
return localResponse(paidFeatureNudge("search_trail"));
|
|
1505
|
+
const loginRequired = requireFreeIdentity();
|
|
1506
|
+
if (loginRequired)
|
|
1507
|
+
return loginRequired;
|
|
1508
|
+
const result = searchTrail({
|
|
1509
|
+
store: localStore,
|
|
1510
|
+
query: payload.query,
|
|
1511
|
+
scopeKey: currentScopeKey(),
|
|
1512
|
+
sessionId: payload.sessionId,
|
|
1513
|
+
limit: payload.limit,
|
|
1514
|
+
cursor: payload.cursor,
|
|
1515
|
+
compact: payload.compact,
|
|
1516
|
+
});
|
|
1517
|
+
return budgetedLocalResponse({
|
|
1518
|
+
ok: true,
|
|
1519
|
+
tier: "free",
|
|
1520
|
+
query: payload.query,
|
|
1521
|
+
matches: result.matches,
|
|
1522
|
+
nextCursor: result.nextCursor,
|
|
1523
|
+
}, payload.max_chars);
|
|
1524
|
+
});
|
|
1525
|
+
server.tool("export_decisions", paidDescription("Export decisions from your workspace.", mode.mode), {
|
|
1526
|
+
format: z.enum(["json", "markdown", "jsonl"]).default("json"),
|
|
1527
|
+
cursor: cursorSchema,
|
|
1528
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
1529
|
+
max_chars: maxCharsSchema,
|
|
1530
|
+
}, async (payload) => mode.mode === "free"
|
|
1531
|
+
? localResponse(paidFeatureNudge("export_decisions"))
|
|
1532
|
+
: apiToolResponse(routeWithQuery("/api/export/timeline", {
|
|
1533
|
+
format: payload.format,
|
|
1534
|
+
cursor: payload.cursor,
|
|
1535
|
+
limit: payload.limit ?? 50,
|
|
1536
|
+
max_chars: payload.max_chars ?? 8000,
|
|
1537
|
+
})));
|
|
1538
|
+
server.tool("view_timeline", "View signals and decisions counts bucketed by session, day, or month.", {
|
|
1539
|
+
scope: z.enum(["day", "month", "session"]).default("day"),
|
|
1540
|
+
range: z.enum(["7D", "30D", "90D", "12M", "CUSTOM"]).default("30D"),
|
|
1541
|
+
start: z.string().optional(),
|
|
1542
|
+
end: z.string().optional(),
|
|
1543
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
1544
|
+
outcomeId: z.string().optional(),
|
|
1545
|
+
decisionStatus: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1546
|
+
signalSource: z.string().optional(),
|
|
1547
|
+
max_chars: maxCharsSchema,
|
|
1548
|
+
}, async (payload) => {
|
|
1549
|
+
if (mode.mode === "paid") {
|
|
1550
|
+
return apiToolResponse(routeWithQuery("/api/analytics/timeline-counts", {
|
|
1551
|
+
scope: payload.scope,
|
|
1552
|
+
range: payload.range,
|
|
1553
|
+
start: payload.start,
|
|
1554
|
+
end: payload.end,
|
|
1555
|
+
limit: payload.limit,
|
|
1556
|
+
outcomeId: payload.outcomeId,
|
|
1557
|
+
decisionStatus: payload.decisionStatus,
|
|
1558
|
+
signalSource: payload.signalSource,
|
|
1559
|
+
}));
|
|
1560
|
+
}
|
|
1561
|
+
if (!localStore) {
|
|
1562
|
+
return localToolError({
|
|
1563
|
+
code: "local_store_unavailable",
|
|
1564
|
+
message: "The local Ask The W store is unavailable.",
|
|
1565
|
+
retryable: true,
|
|
1566
|
+
hint: "Retry after restarting the plugin host.",
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
const loginRequired = requireFreeIdentity();
|
|
1570
|
+
if (loginRequired)
|
|
1571
|
+
return loginRequired;
|
|
1572
|
+
const scopeKey = currentScopeKey();
|
|
1573
|
+
const points = buildLocalTimeline({
|
|
1574
|
+
scope: payload.scope,
|
|
1575
|
+
signals: localStore.listSignals({ scopeKey, limit: 100000 }),
|
|
1576
|
+
decisions: localStore.listDecisions({ scopeKey, limit: 100000 }),
|
|
1577
|
+
limit: payload.limit,
|
|
1578
|
+
});
|
|
1579
|
+
const totals = points.reduce((accumulator, point) => ({
|
|
1580
|
+
signals: accumulator.signals + point.signalCount,
|
|
1581
|
+
decisions: accumulator.decisions + point.decisionCount,
|
|
1582
|
+
}), { signals: 0, decisions: 0 });
|
|
1583
|
+
return budgetedLocalResponse({
|
|
1584
|
+
ok: true,
|
|
1585
|
+
tier: "free",
|
|
1586
|
+
scope: payload.scope,
|
|
1587
|
+
period: {
|
|
1588
|
+
start: points[0]?.startedAt ?? points[0]?.x ?? "",
|
|
1589
|
+
end: points.at(-1)?.endedAt ?? points.at(-1)?.x ?? "",
|
|
1590
|
+
label: "Local timeline",
|
|
1591
|
+
tz: "UTC",
|
|
1592
|
+
},
|
|
1593
|
+
points,
|
|
1594
|
+
totals,
|
|
1595
|
+
insights: buildLocalTimelineInsights(points),
|
|
1596
|
+
narrative: `Local timeline: ${totals.signals} signals, ${totals.decisions} decisions.`,
|
|
1597
|
+
markdownTable: renderLocalTimelineMarkdown(points, payload.scope),
|
|
1598
|
+
}, payload.max_chars);
|
|
1599
|
+
});
|
|
267
1600
|
return server;
|
|
268
1601
|
}
|
|
1602
|
+
function renderDecisionDigest(decisions) {
|
|
1603
|
+
if (decisions.length === 0) {
|
|
1604
|
+
return "# Decisions\n\nNo local decisions captured yet.";
|
|
1605
|
+
}
|
|
1606
|
+
return [
|
|
1607
|
+
"# Decisions",
|
|
1608
|
+
"",
|
|
1609
|
+
...decisions.map((decision) => [`## ${decision.headline}`, `- id: ${decision.id}`, `- status: ${decision.status}`, `- created: ${decision.createdAt}`, decision.why ? `- why: ${decision.why}` : "- why: not captured"].join("\n")),
|
|
1610
|
+
].join("\n\n");
|
|
1611
|
+
}
|
|
1612
|
+
function buildSessionCoach(input) {
|
|
1613
|
+
const verificationCount = input.signals.filter((signal) => signal.kind === "verification_result").length;
|
|
1614
|
+
const implementationCount = input.signals.filter((signal) => signal.kind === "implementation_update").length;
|
|
1615
|
+
const directionCount = input.signals.filter((signal) => signal.kind === "direction_change").length;
|
|
1616
|
+
const finalSummaryCount = input.signals.filter((signal) => signal.kind === "final_summary").length;
|
|
1617
|
+
const decisionCount = input.decisions.length;
|
|
1618
|
+
const hasVerification = verificationCount > 0;
|
|
1619
|
+
const hasDecision = decisionCount > 0;
|
|
1620
|
+
const hasDirection = directionCount > 0;
|
|
1621
|
+
const qualityScore = Math.max(0, Math.min(100, 35 +
|
|
1622
|
+
Math.min(25, decisionCount * 12) +
|
|
1623
|
+
(hasVerification ? 25 : 0) +
|
|
1624
|
+
(hasDirection ? 10 : 0) -
|
|
1625
|
+
Math.max(0, implementationCount - decisionCount) * 3));
|
|
1626
|
+
const failureMode = implementationCount >= 3 && verificationCount === 0
|
|
1627
|
+
? `You captured ${implementationCount} implementation updates but no verification_result; run one check and capture it before ending.`
|
|
1628
|
+
: input.signals.length >= 6 && finalSummaryCount === 0
|
|
1629
|
+
? `You captured ${input.signals.length} signals but no final_summary; close the session with the outcome and remaining risk.`
|
|
1630
|
+
: decisionCount === 0 && directionCount > 0
|
|
1631
|
+
? "Direction changed, but no decision was captured; promote the clearest direction_change signal."
|
|
1632
|
+
: null;
|
|
1633
|
+
const biggestGap = failureMode ?? (!hasDecision
|
|
1634
|
+
? "Promote the clearest captured signal into a decision before the trail goes stale."
|
|
1635
|
+
: !hasVerification
|
|
1636
|
+
? "Capture one verification result so the decision trail records whether the work actually held."
|
|
1637
|
+
: implementationCount > decisionCount * 3
|
|
1638
|
+
? "There are many implementation updates per decision; collapse the important why into one decision."
|
|
1639
|
+
: "The trail is usable. Keep the next decision tied to a verification result.");
|
|
1640
|
+
return { qualityScore, biggestGap, failureMode };
|
|
1641
|
+
}
|
|
1642
|
+
function renderSessionMarkdown(input) {
|
|
1643
|
+
const counts = input.signals.reduce((accumulator, signal) => {
|
|
1644
|
+
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1645
|
+
return accumulator;
|
|
1646
|
+
}, {});
|
|
1647
|
+
const activeDecisions = input.decisions.filter((decision) => decision.status !== "abandoned");
|
|
1648
|
+
const coaching = buildSessionCoach({
|
|
1649
|
+
signals: input.signals.map((signal) => ({ ...signal, filesTouched: [], commandsRun: [] })),
|
|
1650
|
+
decisions: input.decisions,
|
|
1651
|
+
});
|
|
1652
|
+
const lines = [
|
|
1653
|
+
"# Session Review",
|
|
1654
|
+
`Session: ${input.sessionId ?? "none"}`,
|
|
1655
|
+
`Signals: ${input.signals.length}`,
|
|
1656
|
+
`Signal kinds: ${Object.entries(counts).map(([kind, count]) => `${kind} ${count}`).join(", ") || "none"}`,
|
|
1657
|
+
`Active decisions: ${activeDecisions.length}`,
|
|
1658
|
+
`Coaching tip: ${coaching.biggestGap}`,
|
|
1659
|
+
"",
|
|
1660
|
+
"## Signals By Kind",
|
|
1661
|
+
...Object.entries(counts)
|
|
1662
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1663
|
+
.map(([kind, count]) => `- ${kind}: ${count}`),
|
|
1664
|
+
"",
|
|
1665
|
+
"## Decision Candidates",
|
|
1666
|
+
...(input.decisionCandidates?.length
|
|
1667
|
+
? input.decisionCandidates.slice(0, 10).map((candidate) => `- signal ${candidate.signalId}: ${candidate.summary} (${candidate.suggestedStatus})`)
|
|
1668
|
+
: ["- none"]),
|
|
1669
|
+
"",
|
|
1670
|
+
"## Active Decisions",
|
|
1671
|
+
...(activeDecisions.length
|
|
1672
|
+
? activeDecisions.map((decision) => `- ${decision.headline} (${decision.status})${decision.why ? ` - ${decision.why}` : ""}`)
|
|
1673
|
+
: ["- none"]),
|
|
1674
|
+
];
|
|
1675
|
+
return lines.slice(0, 300).join("\n");
|
|
1676
|
+
}
|
|
1677
|
+
function decisionalWeight(signal) {
|
|
1678
|
+
const kindWeight = signal.kind === "direction_change" ? 4 : signal.kind === "verification_result" ? 3 : signal.kind === "implementation_update" ? 2 : 1;
|
|
1679
|
+
const textWeight = /\b(decid|chose|commit|reject|approve|ship|verify|blocked|risk)\b/i.test(signal.summary) ? 2 : 0;
|
|
1680
|
+
return kindWeight + textWeight;
|
|
1681
|
+
}
|
|
1682
|
+
function renderRecap(input) {
|
|
1683
|
+
if (input.format === "standup") {
|
|
1684
|
+
const blockers = input.signals.filter((signal) => /\b(block|fail|error|risk|stuck)\b/i.test(signal.summary));
|
|
1685
|
+
return [
|
|
1686
|
+
"# Standup Recap",
|
|
1687
|
+
"## Yesterday",
|
|
1688
|
+
input.decisions.length ? `- Captured ${input.decisions.length} decisions.` : "- No decisions captured.",
|
|
1689
|
+
"## Today",
|
|
1690
|
+
input.signals.length ? `- Review ${input.signals.length} session signals and promote the strongest one.` : "- Capture the first useful signal.",
|
|
1691
|
+
"## Blockers",
|
|
1692
|
+
...(blockers.length ? blockers.slice(0, 5).map((signal) => `- ${signal.summary}`) : ["- None captured."]),
|
|
1693
|
+
].join("\n");
|
|
1694
|
+
}
|
|
1695
|
+
if (input.format === "share") {
|
|
1696
|
+
return [
|
|
1697
|
+
"# Ask The W Session Share",
|
|
1698
|
+
"",
|
|
1699
|
+
`Signals captured: ${input.signals.length}`,
|
|
1700
|
+
`Decisions captured: ${input.decisions.length}`,
|
|
1701
|
+
"",
|
|
1702
|
+
"## Highlights",
|
|
1703
|
+
...input.signals
|
|
1704
|
+
.slice()
|
|
1705
|
+
.sort((left, right) => decisionalWeight(right) - decisionalWeight(left))
|
|
1706
|
+
.slice(0, 8)
|
|
1707
|
+
.map((signal) => `- ${signal.summary}`),
|
|
1708
|
+
"",
|
|
1709
|
+
"_Captured by Ask The W._",
|
|
1710
|
+
].join("\n");
|
|
1711
|
+
}
|
|
1712
|
+
return [
|
|
1713
|
+
"# Session Digest",
|
|
1714
|
+
"",
|
|
1715
|
+
...input.signals
|
|
1716
|
+
.slice()
|
|
1717
|
+
.sort((left, right) => decisionalWeight(right) - decisionalWeight(left))
|
|
1718
|
+
.slice(0, 28)
|
|
1719
|
+
.map((signal, index) => `${index + 1}. [${signal.kind}] ${signal.summary}`),
|
|
1720
|
+
].join("\n").split("\n").slice(0, 30).join("\n");
|
|
1721
|
+
}
|
|
1722
|
+
function renderSessionFeed(signals) {
|
|
1723
|
+
if (signals.length === 0) {
|
|
1724
|
+
return "# Session\n\nNo local signals captured yet.";
|
|
1725
|
+
}
|
|
1726
|
+
return [
|
|
1727
|
+
"# Session",
|
|
1728
|
+
"",
|
|
1729
|
+
...signals.map((signal) => [
|
|
1730
|
+
`## ${signal.sequence ?? signal.id}. ${signal.kind}`,
|
|
1731
|
+
signal.summary,
|
|
1732
|
+
`- captured: ${signal.capturedAt}`,
|
|
1733
|
+
signal.filesTouched.length ? `- files: ${signal.filesTouched.join(", ")}` : "- files: none",
|
|
1734
|
+
signal.commandsRun.length ? `- commands: ${signal.commandsRun.join(" | ")}` : "- commands: none",
|
|
1735
|
+
].join("\n")),
|
|
1736
|
+
].join("\n\n");
|
|
1737
|
+
}
|
|
1738
|
+
function compactSignal(signal, decision) {
|
|
1739
|
+
return {
|
|
1740
|
+
id: signal.id,
|
|
1741
|
+
kind: signal.kind,
|
|
1742
|
+
summary: signal.summary,
|
|
1743
|
+
files: signal.filesTouched,
|
|
1744
|
+
decisionId: decision?.id ?? null,
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function decisionWithSignals(store, decision) {
|
|
1748
|
+
return {
|
|
1749
|
+
...decision,
|
|
1750
|
+
contributingSignals: store.listSignalsByIds(decision.sourceSignalIds),
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function signalWithDecision(store, signal) {
|
|
1754
|
+
const decision = store.getDecisionForSignal(signal.id);
|
|
1755
|
+
return {
|
|
1756
|
+
...signal,
|
|
1757
|
+
decisionId: decision?.id ?? null,
|
|
1758
|
+
decision: decision
|
|
1759
|
+
? {
|
|
1760
|
+
id: decision.id,
|
|
1761
|
+
headline: decision.headline,
|
|
1762
|
+
status: decision.status,
|
|
1763
|
+
why: decision.why,
|
|
1764
|
+
}
|
|
1765
|
+
: null,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
function candidateFromSignal(signal, linkedDecision) {
|
|
1769
|
+
if (linkedDecision)
|
|
1770
|
+
return null;
|
|
1771
|
+
const text = [signal.summary, ...signal.evidence.map((entry) => {
|
|
1772
|
+
if (entry && typeof entry === "object") {
|
|
1773
|
+
const record = entry;
|
|
1774
|
+
return [record.excerpt, record.diff, record.before, record.after].filter(Boolean).join(" ");
|
|
1775
|
+
}
|
|
1776
|
+
return String(entry ?? "");
|
|
1777
|
+
})].join(" ");
|
|
1778
|
+
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);
|
|
1779
|
+
if (!hasDecisionLanguage && signal.kind !== "direction_change") {
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
const because = /\bbecause\b/i.test(text);
|
|
1783
|
+
return {
|
|
1784
|
+
id: `candidate_${signal.id}`,
|
|
1785
|
+
signalId: signal.id,
|
|
1786
|
+
sessionId: signal.sessionId,
|
|
1787
|
+
summary: signal.summary,
|
|
1788
|
+
suggestedStatus: signal.kind === "verification_result" ? "shipped" : "proposed",
|
|
1789
|
+
why: because ? "The signal includes an explicit because/reason clause." : "The signal uses decision language.",
|
|
1790
|
+
files: signal.filesTouched,
|
|
1791
|
+
capturedAt: signal.capturedAt,
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function listDecisionCandidates(input) {
|
|
1795
|
+
const signals = input.store.listSignals({
|
|
1796
|
+
sessionId: input.sessionId ?? undefined,
|
|
1797
|
+
scopeKey: input.scopeKey,
|
|
1798
|
+
cursor: input.cursor,
|
|
1799
|
+
limit: Math.max(1, Math.min(300, input.limit ?? 50)),
|
|
1800
|
+
});
|
|
1801
|
+
const candidates = signals
|
|
1802
|
+
.map((signal) => candidateFromSignal(signal, input.store.getDecisionForSignal(signal.id)))
|
|
1803
|
+
.filter((candidate) => Boolean(candidate));
|
|
1804
|
+
return {
|
|
1805
|
+
candidates,
|
|
1806
|
+
nextCursor: signals.length >= (input.limit ?? 50) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function normalizedDecisionTerms(text) {
|
|
1810
|
+
const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "keep", "use", "adopt", "remove", "drop", "replace", "defer", "ship", "commit"]);
|
|
1811
|
+
return String(text ?? "")
|
|
1812
|
+
.toLowerCase()
|
|
1813
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
1814
|
+
.split(/\s+/)
|
|
1815
|
+
.filter((term) => term.length >= 4 && !stop.has(term));
|
|
1816
|
+
}
|
|
1817
|
+
function decisionPolarity(text) {
|
|
1818
|
+
if (/\b(remove|drop|abandon|defer|reject|disable|stop|sunset|do not|don't|won't|will not)\b/i.test(text))
|
|
1819
|
+
return "negative";
|
|
1820
|
+
if (/\b(keep|use|adopt|enable|add|ship|commit|standardize|choose|approve|go with)\b/i.test(text))
|
|
1821
|
+
return "positive";
|
|
1822
|
+
return "neutral";
|
|
1823
|
+
}
|
|
1824
|
+
function detectDecisionConflicts(input) {
|
|
1825
|
+
const polarity = decisionPolarity(`${input.decision.headline} ${input.decision.rawContent}`);
|
|
1826
|
+
if (polarity === "neutral")
|
|
1827
|
+
return [];
|
|
1828
|
+
const terms = new Set(normalizedDecisionTerms(`${input.decision.headline} ${input.decision.rawContent}`));
|
|
1829
|
+
if (terms.size === 0)
|
|
1830
|
+
return [];
|
|
1831
|
+
return input.decisions
|
|
1832
|
+
.filter((prior) => prior.id !== input.decision.id)
|
|
1833
|
+
.filter((prior) => !input.decision.scopeKey || prior.scopeKey === input.decision.scopeKey)
|
|
1834
|
+
.map((prior) => {
|
|
1835
|
+
const priorPolarity = decisionPolarity(`${prior.headline} ${prior.rawContent}`);
|
|
1836
|
+
const priorTerms = normalizedDecisionTerms(`${prior.headline} ${prior.rawContent}`);
|
|
1837
|
+
const overlap = priorTerms.filter((term) => terms.has(term));
|
|
1838
|
+
return {
|
|
1839
|
+
prior,
|
|
1840
|
+
priorPolarity,
|
|
1841
|
+
overlap,
|
|
1842
|
+
};
|
|
1843
|
+
})
|
|
1844
|
+
.filter((entry) => entry.priorPolarity !== "neutral" && entry.priorPolarity !== polarity && entry.overlap.length > 0)
|
|
1845
|
+
.slice(0, 3)
|
|
1846
|
+
.map((entry) => ({
|
|
1847
|
+
code: "possible_conflict",
|
|
1848
|
+
message: `This may conflict with "${entry.prior.headline}".`,
|
|
1849
|
+
conflictingDecisionId: entry.prior.id,
|
|
1850
|
+
overlappingTerms: entry.overlap.slice(0, 5),
|
|
1851
|
+
}));
|
|
1852
|
+
}
|
|
1853
|
+
function searchTrail(input) {
|
|
1854
|
+
const terms = String(input.query ?? "")
|
|
1855
|
+
.toLowerCase()
|
|
1856
|
+
.split(/\s+/)
|
|
1857
|
+
.filter(Boolean);
|
|
1858
|
+
const limit = Math.max(1, Math.min(100, input.limit ?? 25));
|
|
1859
|
+
const haystackMatches = (value) => terms.every((term) => value.toLowerCase().includes(term));
|
|
1860
|
+
const signals = input.store
|
|
1861
|
+
.listSignals({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1862
|
+
.filter((signal) => haystackMatches([
|
|
1863
|
+
signal.summary,
|
|
1864
|
+
signal.kind,
|
|
1865
|
+
signal.filesTouched.join(" "),
|
|
1866
|
+
signal.commandsRun.join(" "),
|
|
1867
|
+
JSON.stringify(signal.evidence),
|
|
1868
|
+
JSON.stringify(signal.metadata),
|
|
1869
|
+
].join(" ")))
|
|
1870
|
+
.map((signal) => ({
|
|
1871
|
+
type: "signal",
|
|
1872
|
+
id: String(signal.id),
|
|
1873
|
+
createdAt: signal.capturedAt,
|
|
1874
|
+
score: terms.length,
|
|
1875
|
+
result: input.compact ? compactSignal(signal, input.store.getDecisionForSignal(signal.id)) : signalWithDecision(input.store, signal),
|
|
1876
|
+
}));
|
|
1877
|
+
const decisions = input.store
|
|
1878
|
+
.listDecisions({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1879
|
+
.filter((decision) => haystackMatches([decision.headline, decision.why ?? "", decision.rawContent, decision.files.join(" "), decision.status].join(" ")))
|
|
1880
|
+
.map((decision) => ({
|
|
1881
|
+
type: "decision",
|
|
1882
|
+
id: decision.id,
|
|
1883
|
+
createdAt: decision.createdAt,
|
|
1884
|
+
score: terms.length,
|
|
1885
|
+
result: input.compact
|
|
1886
|
+
? {
|
|
1887
|
+
id: decision.id,
|
|
1888
|
+
headline: decision.headline,
|
|
1889
|
+
status: decision.status,
|
|
1890
|
+
signalIds: decision.sourceSignalIds,
|
|
1891
|
+
}
|
|
1892
|
+
: decisionWithSignals(input.store, decision),
|
|
1893
|
+
}));
|
|
1894
|
+
const matches = [...signals, ...decisions]
|
|
1895
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
|
1896
|
+
.slice(0, limit);
|
|
1897
|
+
return {
|
|
1898
|
+
matches,
|
|
1899
|
+
nextCursor: matches.length >= limit ? matches.at(-1)?.createdAt ?? null : null,
|
|
1900
|
+
};
|
|
1901
|
+
}
|