@askthew/mcp-plugin 0.4.0 → 0.4.3
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 +24 -13
- 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 +293 -37
- 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 +47 -13
- package/dist/index.js +1103 -106
- package/dist/index.test.js +609 -6
- package/dist/install.d.ts +40 -0
- package/dist/install.js +155 -18
- package/dist/install.test.js +62 -2
- 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 +5 -1
- package/dist/lib/free-tier-policy.js +16 -1
- package/dist/lib/local-identity.d.ts +44 -0
- package/dist/lib/local-identity.js +81 -0
- package/dist/lib/local-store.d.ts +33 -2
- package/dist/lib/local-store.js +191 -19
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/telemetry.js +28 -2
- package/dist/lib/timeline-insights.d.ts +23 -0
- package/dist/lib/timeline-insights.js +115 -0
- package/dist/lib/upgrade-nudge.d.ts +1 -1
- package/dist/lib/upgrade-nudge.js +8 -1
- package/dist/local-identity.test.d.ts +1 -0
- package/dist/local-identity.test.js +29 -0
- package/dist/local-store.test.js +34 -0
- package/dist/scope.d.ts +1 -1
- package/dist/scope.js +56 -2
- package/dist/scope.test.js +17 -0
- package/dist/timeline-insights.test.d.ts +1 -0
- package/dist/timeline-insights.test.js +85 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,10 +3,20 @@ import { z } from "zod";
|
|
|
3
3
|
import { resolvePluginScope } from "./scope.js";
|
|
4
4
|
import { resolveMcpMode } from "./lib/free-tier-policy.js";
|
|
5
5
|
import { LocalStore } from "./lib/local-store.js";
|
|
6
|
-
import { analyzeLocalPatterns } from "./lib/tip-engine.js";
|
|
7
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";
|
|
8
9
|
import { paidDescription, paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
|
|
10
|
+
import { configPath, readJsonFile } from "./lib/paths.js";
|
|
9
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
|
+
});
|
|
10
20
|
const sessionSignalKindSchema = z.enum([
|
|
11
21
|
"setup_complete",
|
|
12
22
|
"session_checkpoint",
|
|
@@ -21,10 +31,7 @@ export const codingSessionSignalSchema = z.object({
|
|
|
21
31
|
kind: sessionSignalKindSchema,
|
|
22
32
|
summary: z.string().min(1).max(2000),
|
|
23
33
|
evidence: z
|
|
24
|
-
.array(
|
|
25
|
-
role: evidenceRoleSchema,
|
|
26
|
-
excerpt: z.string().min(1).max(500),
|
|
27
|
-
}))
|
|
34
|
+
.array(evidenceEntrySchema)
|
|
28
35
|
.default([]),
|
|
29
36
|
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
30
37
|
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
@@ -53,7 +60,9 @@ const REDACTION_PATTERNS = [
|
|
|
53
60
|
{ name: "twilio_key", pattern: /\bSK[0-9a-fA-F]{32}\b/g },
|
|
54
61
|
{ name: "slack_token", pattern: /\bxox[baprs]-[0-9A-Za-z\-]{10,}\b/g },
|
|
55
62
|
{ name: "slack_webhook", pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g },
|
|
56
|
-
{ name: "
|
|
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 },
|
|
57
66
|
{ name: "anthropic_key", pattern: /\bsk-ant-[A-Za-z0-9\-_]{32,}\b/g },
|
|
58
67
|
{ name: "github_pat_classic", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
59
68
|
{ name: "github_pat_fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
|
|
@@ -135,6 +144,12 @@ const SENSITIVE_PATH_SEGMENTS = new Set([
|
|
|
135
144
|
]);
|
|
136
145
|
const ENTROPY_THRESHOLD = 4.5;
|
|
137
146
|
const ENTROPY_TOKEN_PATTERN = /[A-Za-z0-9+/=_\-]{20,}/g;
|
|
147
|
+
export function loadAskTheWConfig(env = process.env) {
|
|
148
|
+
return readJsonFile(configPath(env)) ?? {};
|
|
149
|
+
}
|
|
150
|
+
function isRedactionEnabled() {
|
|
151
|
+
return loadAskTheWConfig().redaction?.enabled !== false;
|
|
152
|
+
}
|
|
138
153
|
function sanitizeUrl(raw) {
|
|
139
154
|
try {
|
|
140
155
|
const url = new URL(raw);
|
|
@@ -235,6 +250,9 @@ function redactMetadata(value, key) {
|
|
|
235
250
|
}
|
|
236
251
|
export function redactProvenanceSignal(input) {
|
|
237
252
|
const parsed = provenanceSignalSchema.parse(input);
|
|
253
|
+
if (!isRedactionEnabled()) {
|
|
254
|
+
return parsed;
|
|
255
|
+
}
|
|
238
256
|
return {
|
|
239
257
|
...parsed,
|
|
240
258
|
decision: redactRawSignalText(parsed.decision),
|
|
@@ -252,12 +270,18 @@ export function redactProvenanceSignal(input) {
|
|
|
252
270
|
}
|
|
253
271
|
export function redactCodingSessionSignal(input) {
|
|
254
272
|
const parsed = codingSessionSignalSchema.parse(input);
|
|
273
|
+
if (!isRedactionEnabled()) {
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
255
276
|
return {
|
|
256
277
|
...parsed,
|
|
257
278
|
summary: redactRawSignalText(parsed.summary),
|
|
258
279
|
evidence: parsed.evidence.map((entry) => ({
|
|
259
280
|
...entry,
|
|
260
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,
|
|
261
285
|
})),
|
|
262
286
|
filesTouched: parsed.filesTouched.map((filePath) => sanitizeFilePath(filePath)),
|
|
263
287
|
commandsRun: parsed.commandsRun.map((command) => redactOperationalContext(command)),
|
|
@@ -278,6 +302,32 @@ function normalizeClientId(value) {
|
|
|
278
302
|
export function normalizeInstallTokenInput(token) {
|
|
279
303
|
return String(token ?? "").trim().replace(/^['"]/, "").replace(/['"]$/, "");
|
|
280
304
|
}
|
|
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
|
+
}
|
|
281
331
|
function routeWithQuery(route, params) {
|
|
282
332
|
const searchParams = new URLSearchParams();
|
|
283
333
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -334,6 +384,7 @@ async function postToServer(route, payload, options, request) {
|
|
|
334
384
|
method,
|
|
335
385
|
headers: {
|
|
336
386
|
...(method === "GET" ? {} : { "Content-Type": "application/json" }),
|
|
387
|
+
...(request?.idempotencyKey ? { "Idempotency-Key": request.idempotencyKey } : {}),
|
|
337
388
|
...(installToken
|
|
338
389
|
? { Authorization: `Bearer ${installToken}` }
|
|
339
390
|
: apiKey
|
|
@@ -343,18 +394,44 @@ async function postToServer(route, payload, options, request) {
|
|
|
343
394
|
...(method === "GET" ? {} : { body: JSON.stringify(bodyPayload) }),
|
|
344
395
|
}).catch(() => null);
|
|
345
396
|
if (!response) {
|
|
346
|
-
return {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
});
|
|
350
403
|
}
|
|
351
404
|
const body = await response.json().catch(() => null);
|
|
352
405
|
if (!response.ok) {
|
|
353
|
-
|
|
354
|
-
|
|
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.",
|
|
355
433
|
status: response.status,
|
|
356
|
-
|
|
357
|
-
};
|
|
434
|
+
});
|
|
358
435
|
}
|
|
359
436
|
return body;
|
|
360
437
|
}
|
|
@@ -367,6 +444,7 @@ function runtimeMetadata(options) {
|
|
|
367
444
|
return {
|
|
368
445
|
repository: scope.repoName,
|
|
369
446
|
repo_name: scope.repoName,
|
|
447
|
+
scope_key: localScopeKey(),
|
|
370
448
|
...(scope.repoRoot ? { repo_root: sanitizeFilePath(scope.repoRoot) } : {}),
|
|
371
449
|
...(scope.appPath ? { app_path: sanitizeFilePath(scope.appPath) } : {}),
|
|
372
450
|
...(scope.serviceName ? { service_name: scope.serviceName } : {}),
|
|
@@ -376,6 +454,14 @@ function runtimeMetadata(options) {
|
|
|
376
454
|
...extraMetadata,
|
|
377
455
|
};
|
|
378
456
|
}
|
|
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
|
+
}
|
|
379
465
|
function startupSessionId(options) {
|
|
380
466
|
const scope = resolvePluginScope(process.cwd());
|
|
381
467
|
const { clientId } = credentials(options?.credentials);
|
|
@@ -387,6 +473,20 @@ function startupSessionId(options) {
|
|
|
387
473
|
.replace(/[^a-zA-Z0-9:_-]+/g, "_")
|
|
388
474
|
.slice(0, 240);
|
|
389
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
|
+
}
|
|
390
490
|
async function sendStartupHeartbeat(options) {
|
|
391
491
|
if (!hasServerIdentity(options?.credentials)) {
|
|
392
492
|
return;
|
|
@@ -421,7 +521,7 @@ async function sendStartupSetupSignal(options) {
|
|
|
421
521
|
sessionId: startupSessionId(options),
|
|
422
522
|
sequence: 0,
|
|
423
523
|
kind: "setup_complete",
|
|
424
|
-
summary: "Ask The W
|
|
524
|
+
summary: "Ask The W plugin server started for this coding-agent session.",
|
|
425
525
|
evidence: [],
|
|
426
526
|
filesTouched: [],
|
|
427
527
|
commandsRun: [],
|
|
@@ -441,20 +541,24 @@ async function sendStartupSignals(options) {
|
|
|
441
541
|
await sendStartupSetupSignal(options).catch(() => null);
|
|
442
542
|
}
|
|
443
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
|
+
}
|
|
444
552
|
const resolvedMode = resolveMcpMode();
|
|
445
553
|
const optionInstallToken = normalizeInstallTokenInput(options.credentials?.installToken);
|
|
446
554
|
const mode = optionInstallToken
|
|
447
555
|
? { mode: "paid", installToken: optionInstallToken, reason: "options_install_token" }
|
|
448
556
|
: resolvedMode;
|
|
449
557
|
const localStore = mode.mode === "paid" ? null : LocalStore.open();
|
|
450
|
-
if (localStore) {
|
|
558
|
+
if (localStore && mode.mode === "free" && mode.cliCredentials) {
|
|
451
559
|
void flushTelemetryOutbox({
|
|
452
560
|
store: localStore,
|
|
453
|
-
credentials: mode.cliCredentials
|
|
454
|
-
userId: "unauthenticated",
|
|
455
|
-
cliToken: "",
|
|
456
|
-
cliTokenId: "none",
|
|
457
|
-
},
|
|
561
|
+
credentials: mode.cliCredentials,
|
|
458
562
|
apiUrl: options.apiBaseUrl,
|
|
459
563
|
fetchImpl: options.fetchImpl,
|
|
460
564
|
}).catch(() => null);
|
|
@@ -463,11 +567,11 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
463
567
|
name: "Ask The W Coding Agent Connector",
|
|
464
568
|
version: "0.4.0",
|
|
465
569
|
});
|
|
466
|
-
if (options.sendStartupHeartbeat !== false) {
|
|
570
|
+
if (options.sendStartupHeartbeat !== false && mode.mode === "paid") {
|
|
467
571
|
void sendStartupSignals(options);
|
|
468
572
|
}
|
|
469
|
-
const apiToolResponse = async (route, payload = {}, method = "GET") => {
|
|
470
|
-
const upstream = await postToServer(route, payload, options, { method });
|
|
573
|
+
const apiToolResponse = async (route, payload = {}, method = "GET", request) => {
|
|
574
|
+
const upstream = await postToServer(route, payload, options, { method, idempotencyKey: request?.idempotencyKey });
|
|
471
575
|
return {
|
|
472
576
|
content: [
|
|
473
577
|
{
|
|
@@ -478,13 +582,71 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
478
582
|
};
|
|
479
583
|
};
|
|
480
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;
|
|
481
638
|
const requireFreeIdentity = () => {
|
|
482
|
-
if (mode.mode === "unauthenticated") {
|
|
483
|
-
|
|
484
|
-
|
|
639
|
+
if (mode.mode === "unauthenticated" || mode.mode === "free_pending_auth") {
|
|
640
|
+
const loginCommand = loginCommandHint();
|
|
641
|
+
return localToolError({
|
|
485
642
|
code: "free_tier_login_required",
|
|
486
|
-
message: "
|
|
487
|
-
|
|
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
|
+
},
|
|
488
650
|
});
|
|
489
651
|
}
|
|
490
652
|
return null;
|
|
@@ -495,14 +657,13 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
495
657
|
kind: sessionSignalKindSchema,
|
|
496
658
|
summary: z.string().min(1).max(2000),
|
|
497
659
|
evidence: z
|
|
498
|
-
.array(
|
|
499
|
-
role: evidenceRoleSchema,
|
|
500
|
-
excerpt: z.string().min(1).max(500),
|
|
501
|
-
}))
|
|
660
|
+
.array(evidenceEntrySchema)
|
|
502
661
|
.default([]),
|
|
503
662
|
filesTouched: z.array(z.string().min(1).max(500)).default([]),
|
|
504
663
|
commandsRun: z.array(z.string().min(1).max(500)).default([]),
|
|
505
664
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
665
|
+
idempotencyKey: idempotencyKeySchema,
|
|
666
|
+
echo: echoSchema,
|
|
506
667
|
}, async (payload) => {
|
|
507
668
|
const sessionSignal = redactCodingSessionSignal({
|
|
508
669
|
...payload,
|
|
@@ -511,6 +672,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
511
672
|
...(payload.metadata ?? {}),
|
|
512
673
|
},
|
|
513
674
|
});
|
|
675
|
+
const scopeKey = currentScopeKey();
|
|
514
676
|
if (mode.mode !== "paid" && localStore) {
|
|
515
677
|
const loginRequired = requireFreeIdentity();
|
|
516
678
|
if (loginRequired) {
|
|
@@ -525,7 +687,13 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
525
687
|
filesTouched: sessionSignal.filesTouched,
|
|
526
688
|
commandsRun: sessionSignal.commandsRun,
|
|
527
689
|
metadata: sessionSignal.metadata,
|
|
690
|
+
scopeKey,
|
|
528
691
|
});
|
|
692
|
+
const sessionSignalCount = localStore.listSignals({
|
|
693
|
+
sessionId: signal.sessionId,
|
|
694
|
+
scopeKey,
|
|
695
|
+
limit: 100000,
|
|
696
|
+
}).length;
|
|
529
697
|
if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
|
|
530
698
|
localStore.enqueueTelemetry(buildTelemetryPayload({
|
|
531
699
|
store: localStore,
|
|
@@ -539,6 +707,25 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
539
707
|
fetchImpl: options.fetchImpl,
|
|
540
708
|
}).catch(() => null);
|
|
541
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
|
+
}
|
|
542
729
|
return localResponse({
|
|
543
730
|
ok: true,
|
|
544
731
|
tier: "free",
|
|
@@ -548,9 +735,35 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
548
735
|
: "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
|
|
549
736
|
});
|
|
550
737
|
}
|
|
551
|
-
const upstream = await postToServer("/api/ingest/mcp", {
|
|
738
|
+
const upstream = await postToServer(payload.echo === "full" ? "/api/ingest/mcp" : withResponseShape("/api/ingest/mcp"), {
|
|
552
739
|
sessionSignal,
|
|
553
|
-
}, options);
|
|
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
|
+
}
|
|
554
767
|
return {
|
|
555
768
|
content: [
|
|
556
769
|
{
|
|
@@ -568,20 +781,46 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
568
781
|
server.tool("list_decisions", {
|
|
569
782
|
limit: z.number().int().positive().max(300).optional(),
|
|
570
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,
|
|
571
789
|
}, async (payload) => {
|
|
572
790
|
if (mode.mode !== "paid" && localStore) {
|
|
573
791
|
const loginRequired = requireFreeIdentity();
|
|
574
792
|
if (loginRequired)
|
|
575
793
|
return loginRequired;
|
|
576
|
-
|
|
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({
|
|
577
803
|
ok: true,
|
|
578
804
|
tier: "free",
|
|
579
|
-
decisions:
|
|
580
|
-
|
|
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);
|
|
581
815
|
}
|
|
582
816
|
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
583
|
-
limit: payload.limit,
|
|
817
|
+
limit: payload.limit ?? 5,
|
|
584
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,
|
|
585
824
|
}));
|
|
586
825
|
});
|
|
587
826
|
server.tool("get_decision", {
|
|
@@ -592,29 +831,79 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
592
831
|
if (loginRequired)
|
|
593
832
|
return loginRequired;
|
|
594
833
|
const decision = localStore.getDecision(payload.id);
|
|
595
|
-
return
|
|
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
|
+
});
|
|
596
842
|
}
|
|
597
843
|
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`);
|
|
598
844
|
});
|
|
599
845
|
server.tool("create_decision", {
|
|
600
846
|
content: z.string().min(1),
|
|
847
|
+
idempotencyKey: idempotencyKeySchema,
|
|
848
|
+
echo: echoSchema,
|
|
601
849
|
}, async (payload) => {
|
|
602
850
|
if (mode.mode !== "paid" && localStore) {
|
|
603
851
|
const loginRequired = requireFreeIdentity();
|
|
604
852
|
if (loginRequired)
|
|
605
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
|
+
}
|
|
606
885
|
return localResponse({
|
|
607
886
|
ok: true,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
sessionId: localStore.mostRecentSessionId(),
|
|
612
|
-
}),
|
|
887
|
+
...(payload.echo === "summary"
|
|
888
|
+
? { id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
889
|
+
: { tier: "free", decision: decisionWithSignals(localStore, decision), warnings }),
|
|
613
890
|
});
|
|
614
891
|
}
|
|
615
|
-
|
|
892
|
+
const upstream = await postToServer(payload.echo === "full" ? "/api/decisions" : withResponseShape("/api/decisions"), {
|
|
616
893
|
content: payload.content,
|
|
617
|
-
}, "POST");
|
|
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);
|
|
618
907
|
});
|
|
619
908
|
server.tool("update_decision", {
|
|
620
909
|
id: z.string().min(1),
|
|
@@ -623,6 +912,8 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
623
912
|
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
624
913
|
alignment: z.enum(["aligned", "orthogonal", "conflicts", "ambiguous"]).optional(),
|
|
625
914
|
outcomeId: z.string().min(1).optional(),
|
|
915
|
+
idempotencyKey: idempotencyKeySchema,
|
|
916
|
+
echo: echoSchema,
|
|
626
917
|
}, async (payload) => {
|
|
627
918
|
if (mode.mode !== "paid" && localStore) {
|
|
628
919
|
const loginRequired = requireFreeIdentity();
|
|
@@ -634,40 +925,76 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
634
925
|
...(payload.status ? { status: payload.status } : {}),
|
|
635
926
|
...(payload.alignment !== undefined ? { alignment: payload.alignment } : {}),
|
|
636
927
|
});
|
|
637
|
-
|
|
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 });
|
|
638
946
|
}
|
|
639
|
-
|
|
947
|
+
const upstream = await postToServer(payload.echo === "full"
|
|
948
|
+
? `/api/decisions/${encodeURIComponent(payload.id)}`
|
|
949
|
+
: withResponseShape(`/api/decisions/${encodeURIComponent(payload.id)}`), {
|
|
640
950
|
headline: payload.headline,
|
|
641
951
|
why: payload.why,
|
|
642
952
|
status: payload.status,
|
|
643
953
|
alignment: payload.alignment,
|
|
644
954
|
outcomeId: payload.outcomeId,
|
|
645
|
-
}, "PATCH");
|
|
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);
|
|
646
965
|
});
|
|
647
966
|
server.tool("delete_decision", {
|
|
648
967
|
id: z.string().min(1),
|
|
649
968
|
confirmText: z.string().min(1),
|
|
969
|
+
idempotencyKey: idempotencyKeySchema,
|
|
650
970
|
}, async (payload) => {
|
|
651
971
|
if (mode.mode !== "paid" && localStore) {
|
|
652
972
|
const loginRequired = requireFreeIdentity();
|
|
653
973
|
if (loginRequired)
|
|
654
974
|
return loginRequired;
|
|
655
975
|
if (payload.confirmText !== payload.id) {
|
|
656
|
-
return
|
|
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
|
+
});
|
|
657
982
|
}
|
|
658
983
|
return localResponse({ ok: localStore.deleteDecision(payload.id), tier: "free" });
|
|
659
984
|
}
|
|
660
985
|
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
|
|
661
986
|
confirmText: payload.confirmText,
|
|
662
|
-
}, "DELETE");
|
|
987
|
+
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
663
988
|
});
|
|
664
989
|
server.tool("list_outcomes", paidDescription("List outcomes from your workspace.", mode.mode), {
|
|
665
990
|
limit: z.number().int().positive().max(300).optional(),
|
|
991
|
+
cursor: cursorSchema,
|
|
666
992
|
}, async (payload) => {
|
|
667
993
|
if (mode.mode === "free")
|
|
668
994
|
return localResponse(paidFeatureNudge("list_outcomes"));
|
|
669
995
|
return apiToolResponse(routeWithQuery("/api/outcomes", {
|
|
670
996
|
limit: payload.limit,
|
|
997
|
+
cursor: payload.cursor,
|
|
671
998
|
}));
|
|
672
999
|
});
|
|
673
1000
|
server.tool("get_outcome", paidDescription("Get outcome detail from your workspace.", mode.mode), {
|
|
@@ -677,14 +1004,20 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
677
1004
|
: apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`));
|
|
678
1005
|
server.tool("list_outcome_signals", paidDescription("List signals linked to an outcome.", mode.mode), {
|
|
679
1006
|
id: z.string().min(1),
|
|
1007
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
1008
|
+
cursor: cursorSchema,
|
|
680
1009
|
}, async (payload) => mode.mode === "free"
|
|
681
1010
|
? localResponse(paidFeatureNudge("list_outcome_signals"))
|
|
682
|
-
: apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}/signals
|
|
1011
|
+
: apiToolResponse(routeWithQuery(`/api/outcomes/${encodeURIComponent(payload.id)}/signals`, {
|
|
1012
|
+
limit: payload.limit,
|
|
1013
|
+
cursor: payload.cursor,
|
|
1014
|
+
})));
|
|
683
1015
|
server.tool("create_outcome", paidDescription("Create a new outcome.", mode.mode), {
|
|
684
1016
|
name: z.string().min(1),
|
|
685
1017
|
summary: z.string().optional(),
|
|
686
1018
|
causalHypothesis: z.string().optional(),
|
|
687
1019
|
suggestedAction: z.string().optional(),
|
|
1020
|
+
idempotencyKey: idempotencyKeySchema,
|
|
688
1021
|
}, async (payload) => {
|
|
689
1022
|
if (mode.mode === "free")
|
|
690
1023
|
return localResponse(paidFeatureNudge("create_outcome"));
|
|
@@ -693,7 +1026,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
693
1026
|
summary: payload.summary,
|
|
694
1027
|
causalHypothesis: payload.causalHypothesis,
|
|
695
1028
|
suggestedAction: payload.suggestedAction,
|
|
696
|
-
}, "POST");
|
|
1029
|
+
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
697
1030
|
});
|
|
698
1031
|
server.tool("update_outcome", paidDescription("Update an outcome.", mode.mode), {
|
|
699
1032
|
id: z.string().min(1),
|
|
@@ -702,26 +1035,40 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
702
1035
|
causalHypothesis: z.string().optional(),
|
|
703
1036
|
suggestedAction: z.string().optional(),
|
|
704
1037
|
status: z.enum(["active", "achieved", "abandoned", "archived"]).optional(),
|
|
1038
|
+
idempotencyKey: idempotencyKeySchema,
|
|
1039
|
+
echo: echoSchema,
|
|
705
1040
|
}, async (payload) => {
|
|
706
1041
|
if (mode.mode === "free")
|
|
707
1042
|
return localResponse(paidFeatureNudge("update_outcome"));
|
|
708
|
-
|
|
1043
|
+
const upstream = await postToServer(payload.echo === "full"
|
|
1044
|
+
? `/api/outcomes/${encodeURIComponent(payload.id)}`
|
|
1045
|
+
: withResponseShape(`/api/outcomes/${encodeURIComponent(payload.id)}`), {
|
|
709
1046
|
name: payload.name,
|
|
710
1047
|
summary: payload.summary,
|
|
711
1048
|
causalHypothesis: payload.causalHypothesis,
|
|
712
1049
|
suggestedAction: payload.suggestedAction,
|
|
713
1050
|
status: payload.status,
|
|
714
|
-
}, "PATCH");
|
|
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);
|
|
715
1061
|
});
|
|
716
1062
|
server.tool("delete_outcome", paidDescription("Delete an outcome.", mode.mode), {
|
|
717
1063
|
id: z.string().min(1),
|
|
718
1064
|
confirmText: z.string().min(1),
|
|
1065
|
+
idempotencyKey: idempotencyKeySchema,
|
|
719
1066
|
}, async (payload) => {
|
|
720
1067
|
if (mode.mode === "free")
|
|
721
1068
|
return localResponse(paidFeatureNudge("delete_outcome"));
|
|
722
1069
|
return apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`, {
|
|
723
1070
|
confirmText: payload.confirmText,
|
|
724
|
-
}, "DELETE");
|
|
1071
|
+
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
725
1072
|
});
|
|
726
1073
|
server.tool("get_north_star", paidDescription("Read the workspace north-star metric.", mode.mode), {}, async () => mode.mode === "free"
|
|
727
1074
|
? localResponse(paidFeatureNudge("get_north_star"))
|
|
@@ -731,6 +1078,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
731
1078
|
current: z.string().min(1),
|
|
732
1079
|
target: z.string().min(1),
|
|
733
1080
|
reason: z.string().min(1),
|
|
1081
|
+
idempotencyKey: idempotencyKeySchema,
|
|
734
1082
|
}, async (payload) => {
|
|
735
1083
|
if (mode.mode === "free")
|
|
736
1084
|
return localResponse(paidFeatureNudge("update_north_star"));
|
|
@@ -739,25 +1087,85 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
739
1087
|
current: payload.current,
|
|
740
1088
|
target: payload.target,
|
|
741
1089
|
reason: payload.reason,
|
|
742
|
-
}, "POST");
|
|
1090
|
+
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
743
1091
|
});
|
|
744
1092
|
server.tool("list_signals", {
|
|
745
1093
|
limit: z.number().int().positive().max(300).optional(),
|
|
746
1094
|
cursor: z.string().optional(),
|
|
1095
|
+
sessionId: z.string().optional(),
|
|
1096
|
+
since: z.string().optional(),
|
|
1097
|
+
compact: z.boolean().optional(),
|
|
1098
|
+
max_chars: maxCharsSchema,
|
|
747
1099
|
}, async (payload) => {
|
|
748
1100
|
if (mode.mode !== "paid" && localStore) {
|
|
749
1101
|
const loginRequired = requireFreeIdentity();
|
|
750
1102
|
if (loginRequired)
|
|
751
1103
|
return loginRequired;
|
|
752
|
-
|
|
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({
|
|
753
1112
|
ok: true,
|
|
754
1113
|
tier: "free",
|
|
755
|
-
signals:
|
|
756
|
-
|
|
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);
|
|
757
1119
|
}
|
|
758
1120
|
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
759
|
-
limit: payload.limit,
|
|
1121
|
+
limit: payload.limit ?? 10,
|
|
760
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,
|
|
761
1169
|
}));
|
|
762
1170
|
});
|
|
763
1171
|
server.tool("get_signal", {
|
|
@@ -768,25 +1176,46 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
768
1176
|
if (loginRequired)
|
|
769
1177
|
return loginRequired;
|
|
770
1178
|
const signal = localStore.getSignal(Number(payload.id));
|
|
771
|
-
return
|
|
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
|
+
});
|
|
772
1187
|
}
|
|
773
1188
|
return apiToolResponse(`/api/signals/${encodeURIComponent(payload.id)}`);
|
|
774
1189
|
});
|
|
775
|
-
server.tool("review_decisions", {
|
|
1190
|
+
server.tool("review_decisions", "Review captured decisions. Use for natural prompts like: What did I decide yesterday?", {
|
|
776
1191
|
since: z.string().optional(),
|
|
777
1192
|
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
778
1193
|
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
779
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,
|
|
780
1199
|
}, async (payload) => {
|
|
781
1200
|
if (mode.mode === "paid") {
|
|
782
1201
|
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
783
1202
|
since: payload.since,
|
|
784
1203
|
status: payload.status,
|
|
785
1204
|
limit: payload.limit,
|
|
1205
|
+
cursor: payload.cursor,
|
|
1206
|
+
sessionId: payload.sessionId,
|
|
1207
|
+
compact: payload.compact,
|
|
1208
|
+
max_chars: payload.max_chars,
|
|
786
1209
|
}));
|
|
787
1210
|
}
|
|
788
|
-
if (!localStore)
|
|
789
|
-
return
|
|
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
|
+
}
|
|
790
1219
|
const loginRequired = requireFreeIdentity();
|
|
791
1220
|
if (loginRequired)
|
|
792
1221
|
return loginRequired;
|
|
@@ -794,86 +1223,380 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
794
1223
|
since: payload.since,
|
|
795
1224
|
status: payload.status,
|
|
796
1225
|
limit: payload.limit,
|
|
1226
|
+
cursor: payload.cursor,
|
|
1227
|
+
sessionId: payload.sessionId,
|
|
1228
|
+
scopeKey: currentScopeKey(),
|
|
797
1229
|
});
|
|
798
|
-
return
|
|
1230
|
+
return budgetedLocalResponse({
|
|
799
1231
|
ok: true,
|
|
800
1232
|
tier: "free",
|
|
801
1233
|
format: payload.format,
|
|
802
1234
|
rendered: renderDecisionDigest(decisions),
|
|
803
|
-
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)),
|
|
804
1243
|
count: decisions.length,
|
|
1244
|
+
nextCursor: decisions.length >= payload.limit ? decisions.at(-1)?.createdAt ?? null : null,
|
|
805
1245
|
copyHint: "Copy this output to back up your decisions - `export_decisions` is a paid feature.",
|
|
806
|
-
});
|
|
1246
|
+
}, payload.max_chars);
|
|
807
1247
|
});
|
|
808
|
-
server.tool("review_session", {
|
|
1248
|
+
server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
|
|
809
1249
|
sessionId: z.string().optional(),
|
|
810
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,
|
|
811
1255
|
}, async (payload) => {
|
|
812
1256
|
if (!localStore || mode.mode === "paid") {
|
|
813
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
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
|
+
}));
|
|
814
1264
|
}
|
|
815
1265
|
const loginRequired = requireFreeIdentity();
|
|
816
1266
|
if (loginRequired)
|
|
817
1267
|
return loginRequired;
|
|
818
|
-
const
|
|
819
|
-
const
|
|
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;
|
|
820
1295
|
const counts = signals.reduce((accumulator, signal) => {
|
|
821
1296
|
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
822
1297
|
return accumulator;
|
|
823
1298
|
}, {});
|
|
824
|
-
|
|
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({
|
|
825
1325
|
ok: true,
|
|
826
1326
|
tier: "free",
|
|
827
1327
|
sessionId,
|
|
828
|
-
format:
|
|
829
|
-
rendered:
|
|
830
|
-
|
|
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
|
+
: {}),
|
|
831
1333
|
counts: {
|
|
832
|
-
totalSignals:
|
|
833
|
-
byKind: counts,
|
|
1334
|
+
totalSignals: allSignals.length,
|
|
1335
|
+
byKind: Object.keys(allCounts).length ? allCounts : counts,
|
|
834
1336
|
},
|
|
835
|
-
});
|
|
1337
|
+
}, payload.max_chars);
|
|
836
1338
|
});
|
|
837
|
-
server.tool("
|
|
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"),
|
|
838
1341
|
sessionId: z.string().optional(),
|
|
839
|
-
|
|
1342
|
+
compact: z.boolean().optional(),
|
|
1343
|
+
max_chars: maxCharsSchema,
|
|
840
1344
|
}, async (payload) => {
|
|
841
|
-
if (!localStore || mode.mode === "paid")
|
|
842
|
-
return localResponse(paidFeatureNudge("
|
|
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"));
|
|
843
1372
|
}
|
|
1373
|
+
if (!localStore || mode.mode === "paid")
|
|
1374
|
+
return localResponse(paidFeatureNudge("coach"));
|
|
844
1375
|
const loginRequired = requireFreeIdentity();
|
|
845
1376
|
if (loginRequired)
|
|
846
1377
|
return loginRequired;
|
|
847
|
-
const
|
|
848
|
-
const
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
const
|
|
852
|
-
|
|
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
|
+
});
|
|
853
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({
|
|
854
1488
|
ok: true,
|
|
855
1489
|
tier: "free",
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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,
|
|
870
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);
|
|
871
1524
|
});
|
|
872
1525
|
server.tool("export_decisions", paidDescription("Export decisions from your workspace.", mode.mode), {
|
|
873
1526
|
format: z.enum(["json", "markdown", "jsonl"]).default("json"),
|
|
874
|
-
|
|
1527
|
+
cursor: cursorSchema,
|
|
1528
|
+
limit: z.number().int().positive().max(300).optional(),
|
|
1529
|
+
max_chars: maxCharsSchema,
|
|
1530
|
+
}, async (payload) => mode.mode === "free"
|
|
875
1531
|
? localResponse(paidFeatureNudge("export_decisions"))
|
|
876
|
-
: apiToolResponse("/api/export/timeline"
|
|
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
|
+
});
|
|
877
1600
|
return server;
|
|
878
1601
|
}
|
|
879
1602
|
function renderDecisionDigest(decisions) {
|
|
@@ -886,6 +1609,116 @@ function renderDecisionDigest(decisions) {
|
|
|
886
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")),
|
|
887
1610
|
].join("\n\n");
|
|
888
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
|
+
}
|
|
889
1722
|
function renderSessionFeed(signals) {
|
|
890
1723
|
if (signals.length === 0) {
|
|
891
1724
|
return "# Session\n\nNo local signals captured yet.";
|
|
@@ -902,3 +1735,167 @@ function renderSessionFeed(signals) {
|
|
|
902
1735
|
].join("\n")),
|
|
903
1736
|
].join("\n\n");
|
|
904
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
|
+
}
|