@askthew/mcp-plugin 0.4.8 → 0.4.10
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 +9 -11
- package/dist/cli.js +136 -23
- package/dist/index.d.ts +4 -1
- package/dist/index.js +95 -668
- package/dist/install.d.ts +23 -0
- package/dist/install.js +300 -40
- package/dist/lib/local-store.js +15 -4
- package/dist/lib/upgrade-nudge.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { z } from "zod";
|
|
@@ -10,9 +11,18 @@ import { resolveMcpMode } from "./lib/free-tier-policy.js";
|
|
|
10
11
|
import { LocalStore } from "./lib/local-store.js";
|
|
11
12
|
import { buildTelemetryPayload, flushTelemetryOutbox } from "./lib/telemetry.js";
|
|
12
13
|
import { ensureLocalIdentity } from "./lib/local-identity.js";
|
|
13
|
-
import {
|
|
14
|
-
import { paidDescription, paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
|
|
14
|
+
import { paidFeatureNudge, toolJson } from "./lib/upgrade-nudge.js";
|
|
15
15
|
import { configPath, readJsonFile } from "./lib/paths.js";
|
|
16
|
+
const requirePackageJson = createRequire(import.meta.url);
|
|
17
|
+
function packageVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const manifest = requirePackageJson("../package.json");
|
|
20
|
+
return typeof manifest.version === "string" ? manifest.version : "unknown";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
const evidenceRoleSchema = z.enum(["user", "assistant", "system"]);
|
|
17
27
|
const evidenceEntrySchema = z.object({
|
|
18
28
|
role: evidenceRoleSchema,
|
|
@@ -311,6 +321,8 @@ const echoSchema = z.enum(["summary", "full"]).optional();
|
|
|
311
321
|
const cursorSchema = z.string().optional();
|
|
312
322
|
const idempotencyKeySchema = z.string().min(1).max(200).optional();
|
|
313
323
|
const maxCharsSchema = z.number().int().positive().max(100000).optional();
|
|
324
|
+
const LIMITED_LOCAL_TOOL_LIMIT = 3;
|
|
325
|
+
const LIMIT_REACHED_MESSAGE = "Limit reached. Upgrade to the paid plan.";
|
|
314
326
|
function traceId() {
|
|
315
327
|
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
316
328
|
}
|
|
@@ -570,7 +582,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
570
582
|
}
|
|
571
583
|
const server = new McpServer({
|
|
572
584
|
name: "Ask The W Coding Agent Connector",
|
|
573
|
-
version:
|
|
585
|
+
version: packageVersion(),
|
|
574
586
|
});
|
|
575
587
|
if (options.sendStartupHeartbeat !== false && mode.mode === "paid") {
|
|
576
588
|
void sendStartupSignals(options);
|
|
@@ -612,6 +624,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
612
624
|
const compactWriteResponse = (input) => localResponse({
|
|
613
625
|
ok: input.ok !== false,
|
|
614
626
|
id: input.id ?? null,
|
|
627
|
+
...(input.decisionId ? { decisionId: input.decisionId } : {}),
|
|
615
628
|
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
616
629
|
...(typeof input.sequence === "number" ? { sequence: input.sequence } : {}),
|
|
617
630
|
...(typeof input.signalCount === "number" ? { signalCount: input.signalCount } : {}),
|
|
@@ -656,6 +669,28 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
656
669
|
}
|
|
657
670
|
return null;
|
|
658
671
|
};
|
|
672
|
+
const limitReachedResponse = (tool) => localResponse({
|
|
673
|
+
ok: false,
|
|
674
|
+
code: "free_tier_limit_reached",
|
|
675
|
+
tool,
|
|
676
|
+
message: LIMIT_REACHED_MESSAGE,
|
|
677
|
+
limit: LIMITED_LOCAL_TOOL_LIMIT,
|
|
678
|
+
pricingUrl: "https://askthew.com/pricing",
|
|
679
|
+
upgradeUrl: `https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-limit&utm_campaign=mcp-free&tool=${tool}`,
|
|
680
|
+
supportEmail: "support@askthew.com",
|
|
681
|
+
});
|
|
682
|
+
const consumeLimitedLocalUse = (tool) => {
|
|
683
|
+
if (!localStore)
|
|
684
|
+
return null;
|
|
685
|
+
const key = `usage:${tool}`;
|
|
686
|
+
const current = Number(localStore.getMeta(key) || "0");
|
|
687
|
+
const count = Number.isFinite(current) ? Math.max(0, Math.floor(current)) : 0;
|
|
688
|
+
if (count >= LIMITED_LOCAL_TOOL_LIMIT) {
|
|
689
|
+
return limitReachedResponse(tool);
|
|
690
|
+
}
|
|
691
|
+
localStore.setMeta(key, String(count + 1));
|
|
692
|
+
return null;
|
|
693
|
+
};
|
|
659
694
|
server.tool("capture_session_signal", {
|
|
660
695
|
sessionId: z.string().min(1),
|
|
661
696
|
sequence: z.number().int().nonnegative(),
|
|
@@ -699,6 +734,19 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
699
734
|
scopeKey,
|
|
700
735
|
limit: 100000,
|
|
701
736
|
}).length;
|
|
737
|
+
const existingDecision = localStore.getDecisionForSignal(signal.id);
|
|
738
|
+
const decisionCandidate = candidateFromSignal(signal, existingDecision);
|
|
739
|
+
const autoDecision = decisionCandidate
|
|
740
|
+
? localStore.createDecision({
|
|
741
|
+
rawContent: signal.summary,
|
|
742
|
+
headline: signal.summary,
|
|
743
|
+
status: decisionCandidate.suggestedStatus,
|
|
744
|
+
sessionId: signal.sessionId,
|
|
745
|
+
files: signal.filesTouched,
|
|
746
|
+
sourceSignalIds: [signal.id],
|
|
747
|
+
scopeKey,
|
|
748
|
+
})
|
|
749
|
+
: null;
|
|
702
750
|
if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
|
|
703
751
|
localStore.enqueueTelemetry(buildTelemetryPayload({
|
|
704
752
|
store: localStore,
|
|
@@ -715,6 +763,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
715
763
|
if (!payload.echo) {
|
|
716
764
|
return compactWriteResponse({
|
|
717
765
|
id: signal.id,
|
|
766
|
+
decisionId: autoDecision?.id,
|
|
718
767
|
sessionId: signal.sessionId,
|
|
719
768
|
sequence: signal.sequence,
|
|
720
769
|
signalCount: sessionSignalCount,
|
|
@@ -729,12 +778,14 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
729
778
|
signalCount: sessionSignalCount,
|
|
730
779
|
summary: signal.summary,
|
|
731
780
|
kind: signal.kind,
|
|
781
|
+
...(autoDecision ? { decisionId: autoDecision.id } : {}),
|
|
732
782
|
});
|
|
733
783
|
}
|
|
734
784
|
return localResponse({
|
|
735
785
|
ok: true,
|
|
736
786
|
tier: "free",
|
|
737
787
|
signal,
|
|
788
|
+
...(autoDecision ? { decision: decisionWithSignals(localStore, autoDecision) } : {}),
|
|
738
789
|
note: localStore.usingJsonFallback
|
|
739
790
|
? "Captured locally in JSON fallback mode because SQLite was unavailable."
|
|
740
791
|
: "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
|
|
@@ -910,190 +961,6 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
910
961
|
? { ok: true, id: upstreamId(upstream), sequence: upstreamSequence(upstream) ?? 1 }
|
|
911
962
|
: upstream);
|
|
912
963
|
});
|
|
913
|
-
server.tool("update_decision", {
|
|
914
|
-
id: z.string().min(1),
|
|
915
|
-
headline: z.string().min(1).optional(),
|
|
916
|
-
why: z.string().optional(),
|
|
917
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
918
|
-
alignment: z.enum(["aligned", "orthogonal", "conflicts", "ambiguous"]).optional(),
|
|
919
|
-
outcomeId: z.string().min(1).optional(),
|
|
920
|
-
idempotencyKey: idempotencyKeySchema,
|
|
921
|
-
echo: echoSchema,
|
|
922
|
-
}, async (payload) => {
|
|
923
|
-
if (mode.mode !== "paid" && localStore) {
|
|
924
|
-
const loginRequired = requireFreeIdentity();
|
|
925
|
-
if (loginRequired)
|
|
926
|
-
return loginRequired;
|
|
927
|
-
const decision = localStore.updateDecision(payload.id, {
|
|
928
|
-
...(payload.headline ? { headline: payload.headline } : {}),
|
|
929
|
-
...(payload.why !== undefined ? { why: payload.why } : {}),
|
|
930
|
-
...(payload.status ? { status: payload.status } : {}),
|
|
931
|
-
...(payload.alignment !== undefined ? { alignment: payload.alignment } : {}),
|
|
932
|
-
});
|
|
933
|
-
if (!decision) {
|
|
934
|
-
return localToolError({
|
|
935
|
-
code: "not_found",
|
|
936
|
-
message: "Decision not found in the local Ask The W store.",
|
|
937
|
-
retryable: false,
|
|
938
|
-
hint: "Check the decision id before updating.",
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
const warnings = detectDecisionConflicts({
|
|
942
|
-
decision,
|
|
943
|
-
decisions: localStore.listDecisions({ limit: 100000, scopeKey: currentScopeKey() }),
|
|
944
|
-
});
|
|
945
|
-
if (!payload.echo) {
|
|
946
|
-
return compactWriteResponse({ id: decision.id, sequence: localStore.stats().decisions, warnings });
|
|
947
|
-
}
|
|
948
|
-
return localResponse(payload.echo === "summary"
|
|
949
|
-
? { ok: true, id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
|
|
950
|
-
: { ok: true, tier: "free", decision: decisionWithSignals(localStore, decision), warnings });
|
|
951
|
-
}
|
|
952
|
-
const upstream = await postToServer(payload.echo === "full"
|
|
953
|
-
? `/api/decisions/${encodeURIComponent(payload.id)}`
|
|
954
|
-
: withResponseShape(`/api/decisions/${encodeURIComponent(payload.id)}`), {
|
|
955
|
-
headline: payload.headline,
|
|
956
|
-
why: payload.why,
|
|
957
|
-
status: payload.status,
|
|
958
|
-
alignment: payload.alignment,
|
|
959
|
-
outcomeId: payload.outcomeId,
|
|
960
|
-
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
961
|
-
const failure = upstreamFailure(upstream);
|
|
962
|
-
if (failure)
|
|
963
|
-
return localResponse(failure);
|
|
964
|
-
if (!payload.echo) {
|
|
965
|
-
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
966
|
-
}
|
|
967
|
-
return localResponse(payload.echo === "summary"
|
|
968
|
-
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
969
|
-
: upstream);
|
|
970
|
-
});
|
|
971
|
-
server.tool("delete_decision", {
|
|
972
|
-
id: z.string().min(1),
|
|
973
|
-
confirmText: z.string().min(1),
|
|
974
|
-
idempotencyKey: idempotencyKeySchema,
|
|
975
|
-
}, async (payload) => {
|
|
976
|
-
if (mode.mode !== "paid" && localStore) {
|
|
977
|
-
const loginRequired = requireFreeIdentity();
|
|
978
|
-
if (loginRequired)
|
|
979
|
-
return loginRequired;
|
|
980
|
-
if (payload.confirmText !== payload.id) {
|
|
981
|
-
return localToolError({
|
|
982
|
-
code: "confirmation_required",
|
|
983
|
-
message: "confirmText must match the decision id.",
|
|
984
|
-
retryable: false,
|
|
985
|
-
hint: "Pass the exact decision id as confirmText.",
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
return localResponse({ ok: localStore.deleteDecision(payload.id), tier: "free" });
|
|
989
|
-
}
|
|
990
|
-
return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
|
|
991
|
-
confirmText: payload.confirmText,
|
|
992
|
-
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
993
|
-
});
|
|
994
|
-
server.tool("list_outcomes", paidDescription("List outcomes from your workspace.", mode.mode), {
|
|
995
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
996
|
-
cursor: cursorSchema,
|
|
997
|
-
}, async (payload) => {
|
|
998
|
-
if (mode.mode === "free")
|
|
999
|
-
return localResponse(paidFeatureNudge("list_outcomes"));
|
|
1000
|
-
return apiToolResponse(routeWithQuery("/api/outcomes", {
|
|
1001
|
-
limit: payload.limit,
|
|
1002
|
-
cursor: payload.cursor,
|
|
1003
|
-
}));
|
|
1004
|
-
});
|
|
1005
|
-
server.tool("get_outcome", paidDescription("Get outcome detail from your workspace.", mode.mode), {
|
|
1006
|
-
id: z.string().min(1),
|
|
1007
|
-
}, async (payload) => mode.mode === "free"
|
|
1008
|
-
? localResponse(paidFeatureNudge("get_outcome"))
|
|
1009
|
-
: apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`));
|
|
1010
|
-
server.tool("list_outcome_signals", paidDescription("List signals linked to an outcome.", mode.mode), {
|
|
1011
|
-
id: z.string().min(1),
|
|
1012
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1013
|
-
cursor: cursorSchema,
|
|
1014
|
-
}, async (payload) => mode.mode === "free"
|
|
1015
|
-
? localResponse(paidFeatureNudge("list_outcome_signals"))
|
|
1016
|
-
: apiToolResponse(routeWithQuery(`/api/outcomes/${encodeURIComponent(payload.id)}/signals`, {
|
|
1017
|
-
limit: payload.limit,
|
|
1018
|
-
cursor: payload.cursor,
|
|
1019
|
-
})));
|
|
1020
|
-
server.tool("create_outcome", paidDescription("Create a new outcome.", mode.mode), {
|
|
1021
|
-
name: z.string().min(1),
|
|
1022
|
-
summary: z.string().optional(),
|
|
1023
|
-
causalHypothesis: z.string().optional(),
|
|
1024
|
-
suggestedAction: z.string().optional(),
|
|
1025
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1026
|
-
}, async (payload) => {
|
|
1027
|
-
if (mode.mode === "free")
|
|
1028
|
-
return localResponse(paidFeatureNudge("create_outcome"));
|
|
1029
|
-
return apiToolResponse("/api/outcomes", {
|
|
1030
|
-
name: payload.name,
|
|
1031
|
-
summary: payload.summary,
|
|
1032
|
-
causalHypothesis: payload.causalHypothesis,
|
|
1033
|
-
suggestedAction: payload.suggestedAction,
|
|
1034
|
-
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1035
|
-
});
|
|
1036
|
-
server.tool("update_outcome", paidDescription("Update an outcome.", mode.mode), {
|
|
1037
|
-
id: z.string().min(1),
|
|
1038
|
-
name: z.string().min(1).optional(),
|
|
1039
|
-
summary: z.string().optional(),
|
|
1040
|
-
causalHypothesis: z.string().optional(),
|
|
1041
|
-
suggestedAction: z.string().optional(),
|
|
1042
|
-
status: z.enum(["active", "achieved", "abandoned", "archived"]).optional(),
|
|
1043
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1044
|
-
echo: echoSchema,
|
|
1045
|
-
}, async (payload) => {
|
|
1046
|
-
if (mode.mode === "free")
|
|
1047
|
-
return localResponse(paidFeatureNudge("update_outcome"));
|
|
1048
|
-
const upstream = await postToServer(payload.echo === "full"
|
|
1049
|
-
? `/api/outcomes/${encodeURIComponent(payload.id)}`
|
|
1050
|
-
: withResponseShape(`/api/outcomes/${encodeURIComponent(payload.id)}`), {
|
|
1051
|
-
name: payload.name,
|
|
1052
|
-
summary: payload.summary,
|
|
1053
|
-
causalHypothesis: payload.causalHypothesis,
|
|
1054
|
-
suggestedAction: payload.suggestedAction,
|
|
1055
|
-
status: payload.status,
|
|
1056
|
-
}, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
|
|
1057
|
-
const failure = upstreamFailure(upstream);
|
|
1058
|
-
if (failure)
|
|
1059
|
-
return localResponse(failure);
|
|
1060
|
-
if (!payload.echo) {
|
|
1061
|
-
return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
|
|
1062
|
-
}
|
|
1063
|
-
return localResponse(payload.echo === "summary"
|
|
1064
|
-
? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
|
|
1065
|
-
: upstream);
|
|
1066
|
-
});
|
|
1067
|
-
server.tool("delete_outcome", paidDescription("Delete an outcome.", mode.mode), {
|
|
1068
|
-
id: z.string().min(1),
|
|
1069
|
-
confirmText: z.string().min(1),
|
|
1070
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1071
|
-
}, async (payload) => {
|
|
1072
|
-
if (mode.mode === "free")
|
|
1073
|
-
return localResponse(paidFeatureNudge("delete_outcome"));
|
|
1074
|
-
return apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`, {
|
|
1075
|
-
confirmText: payload.confirmText,
|
|
1076
|
-
}, "DELETE", { idempotencyKey: payload.idempotencyKey });
|
|
1077
|
-
});
|
|
1078
|
-
server.tool("get_north_star", paidDescription("Read the workspace north-star metric.", mode.mode), {}, async () => mode.mode === "free"
|
|
1079
|
-
? localResponse(paidFeatureNudge("get_north_star"))
|
|
1080
|
-
: apiToolResponse("/api/north-star"));
|
|
1081
|
-
server.tool("update_north_star", paidDescription("Update the workspace north-star metric.", mode.mode), {
|
|
1082
|
-
metric: z.string().min(1),
|
|
1083
|
-
current: z.string().min(1),
|
|
1084
|
-
target: z.string().min(1),
|
|
1085
|
-
reason: z.string().min(1),
|
|
1086
|
-
idempotencyKey: idempotencyKeySchema,
|
|
1087
|
-
}, async (payload) => {
|
|
1088
|
-
if (mode.mode === "free")
|
|
1089
|
-
return localResponse(paidFeatureNudge("update_north_star"));
|
|
1090
|
-
return apiToolResponse("/api/north-star", {
|
|
1091
|
-
metric: payload.metric,
|
|
1092
|
-
current: payload.current,
|
|
1093
|
-
target: payload.target,
|
|
1094
|
-
reason: payload.reason,
|
|
1095
|
-
}, "POST", { idempotencyKey: payload.idempotencyKey });
|
|
1096
|
-
});
|
|
1097
964
|
server.tool("list_signals", {
|
|
1098
965
|
limit: z.number().int().positive().max(300).optional(),
|
|
1099
966
|
cursor: z.string().optional(),
|
|
@@ -1131,217 +998,7 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1131
998
|
max_chars: payload.max_chars ?? 8000,
|
|
1132
999
|
}));
|
|
1133
1000
|
});
|
|
1134
|
-
server.tool("
|
|
1135
|
-
query: z.string().min(1),
|
|
1136
|
-
sessionId: z.string().optional(),
|
|
1137
|
-
limit: z.number().int().positive().max(50).default(5),
|
|
1138
|
-
compact: z.boolean().optional(),
|
|
1139
|
-
max_chars: maxCharsSchema,
|
|
1140
|
-
}, async (payload) => {
|
|
1141
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1142
|
-
const loginRequired = requireFreeIdentity();
|
|
1143
|
-
if (loginRequired)
|
|
1144
|
-
return loginRequired;
|
|
1145
|
-
const normalizedQuery = payload.query.toLowerCase();
|
|
1146
|
-
const signals = localStore
|
|
1147
|
-
.listSignals({
|
|
1148
|
-
limit: 100000,
|
|
1149
|
-
sessionId: payload.sessionId,
|
|
1150
|
-
scopeKey: currentScopeKey(),
|
|
1151
|
-
})
|
|
1152
|
-
.filter((signal) => [
|
|
1153
|
-
signal.summary,
|
|
1154
|
-
signal.kind,
|
|
1155
|
-
...signal.filesTouched,
|
|
1156
|
-
...signal.commandsRun,
|
|
1157
|
-
].join("\n").toLowerCase().includes(normalizedQuery))
|
|
1158
|
-
.slice(0, payload.limit);
|
|
1159
|
-
return budgetedLocalResponse({
|
|
1160
|
-
ok: true,
|
|
1161
|
-
tier: "free",
|
|
1162
|
-
query: payload.query,
|
|
1163
|
-
signals: payload.compact === false
|
|
1164
|
-
? signals.map((signal) => signalWithDecision(localStore, signal))
|
|
1165
|
-
: signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))),
|
|
1166
|
-
}, payload.max_chars ?? 8000);
|
|
1167
|
-
}
|
|
1168
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1169
|
-
query: payload.query,
|
|
1170
|
-
sessionId: payload.sessionId,
|
|
1171
|
-
limit: payload.limit,
|
|
1172
|
-
compact: payload.compact ?? true,
|
|
1173
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1174
|
-
}));
|
|
1175
|
-
});
|
|
1176
|
-
server.tool("get_signal", {
|
|
1177
|
-
id: z.string().min(1),
|
|
1178
|
-
}, async (payload) => {
|
|
1179
|
-
if (mode.mode !== "paid" && localStore) {
|
|
1180
|
-
const loginRequired = requireFreeIdentity();
|
|
1181
|
-
if (loginRequired)
|
|
1182
|
-
return loginRequired;
|
|
1183
|
-
const signal = localStore.getSignal(Number(payload.id));
|
|
1184
|
-
return signal
|
|
1185
|
-
? localResponse({ ok: true, tier: "free", signal: signalWithDecision(localStore, signal) })
|
|
1186
|
-
: localToolError({
|
|
1187
|
-
code: "not_found",
|
|
1188
|
-
message: "Signal not found in the local Ask The W store.",
|
|
1189
|
-
retryable: false,
|
|
1190
|
-
hint: "Check the signal id or list local signals first.",
|
|
1191
|
-
});
|
|
1192
|
-
}
|
|
1193
|
-
return apiToolResponse(`/api/signals/${encodeURIComponent(payload.id)}`);
|
|
1194
|
-
});
|
|
1195
|
-
server.tool("review_decisions", "Review captured decisions. Use for natural prompts like: What did I decide yesterday?", {
|
|
1196
|
-
since: z.string().optional(),
|
|
1197
|
-
status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1198
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1199
|
-
limit: z.number().int().positive().max(300).default(50),
|
|
1200
|
-
cursor: cursorSchema,
|
|
1201
|
-
sessionId: z.string().optional(),
|
|
1202
|
-
compact: z.boolean().optional(),
|
|
1203
|
-
max_chars: maxCharsSchema,
|
|
1204
|
-
}, async (payload) => {
|
|
1205
|
-
if (mode.mode === "paid") {
|
|
1206
|
-
return apiToolResponse(routeWithQuery("/api/decisions", {
|
|
1207
|
-
since: payload.since,
|
|
1208
|
-
status: payload.status,
|
|
1209
|
-
limit: payload.limit,
|
|
1210
|
-
cursor: payload.cursor,
|
|
1211
|
-
sessionId: payload.sessionId,
|
|
1212
|
-
compact: payload.compact,
|
|
1213
|
-
max_chars: payload.max_chars,
|
|
1214
|
-
}));
|
|
1215
|
-
}
|
|
1216
|
-
if (!localStore) {
|
|
1217
|
-
return localToolError({
|
|
1218
|
-
code: "local_store_unavailable",
|
|
1219
|
-
message: "The local Ask The W store is unavailable.",
|
|
1220
|
-
retryable: true,
|
|
1221
|
-
hint: "Retry after restarting the plugin host.",
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
const loginRequired = requireFreeIdentity();
|
|
1225
|
-
if (loginRequired)
|
|
1226
|
-
return loginRequired;
|
|
1227
|
-
const decisions = localStore.listDecisions({
|
|
1228
|
-
since: payload.since,
|
|
1229
|
-
status: payload.status,
|
|
1230
|
-
limit: payload.limit,
|
|
1231
|
-
cursor: payload.cursor,
|
|
1232
|
-
sessionId: payload.sessionId,
|
|
1233
|
-
scopeKey: currentScopeKey(),
|
|
1234
|
-
});
|
|
1235
|
-
return budgetedLocalResponse({
|
|
1236
|
-
ok: true,
|
|
1237
|
-
tier: "free",
|
|
1238
|
-
format: payload.format,
|
|
1239
|
-
rendered: renderDecisionDigest(decisions),
|
|
1240
|
-
decisions: payload.compact
|
|
1241
|
-
? decisions.map((decision) => ({
|
|
1242
|
-
id: decision.id,
|
|
1243
|
-
headline: decision.headline,
|
|
1244
|
-
status: decision.status,
|
|
1245
|
-
signalIds: decision.sourceSignalIds,
|
|
1246
|
-
}))
|
|
1247
|
-
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
1248
|
-
count: decisions.length,
|
|
1249
|
-
nextCursor: decisions.length >= payload.limit ? decisions.at(-1)?.createdAt ?? null : null,
|
|
1250
|
-
copyHint: "Copy this output to back up your decisions - `export_decisions` is a paid feature.",
|
|
1251
|
-
}, payload.max_chars);
|
|
1252
|
-
});
|
|
1253
|
-
server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
|
|
1254
|
-
sessionId: z.string().optional(),
|
|
1255
|
-
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
1256
|
-
cursor: cursorSchema,
|
|
1257
|
-
limit: z.number().int().positive().max(50).default(50),
|
|
1258
|
-
compact: z.boolean().optional(),
|
|
1259
|
-
max_chars: maxCharsSchema,
|
|
1260
|
-
}, async (payload) => {
|
|
1261
|
-
if (!localStore || mode.mode === "paid") {
|
|
1262
|
-
return apiToolResponse(routeWithQuery("/api/signals", {
|
|
1263
|
-
sessionId: payload.sessionId,
|
|
1264
|
-
cursor: payload.cursor,
|
|
1265
|
-
limit: payload.limit,
|
|
1266
|
-
compact: payload.compact,
|
|
1267
|
-
max_chars: payload.max_chars,
|
|
1268
|
-
}));
|
|
1269
|
-
}
|
|
1270
|
-
const loginRequired = requireFreeIdentity();
|
|
1271
|
-
if (loginRequired)
|
|
1272
|
-
return loginRequired;
|
|
1273
|
-
const scopeKey = currentScopeKey();
|
|
1274
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1275
|
-
const allSessionIds = localStore.listSessionIds({ limit: 100000, scopeKey });
|
|
1276
|
-
const allowedSessionIds = new Set(allSessionIds.slice(0, 3));
|
|
1277
|
-
if (sessionId && allSessionIds.length > 3 && !allowedSessionIds.has(sessionId)) {
|
|
1278
|
-
return localToolError({
|
|
1279
|
-
code: "free_tier_limit",
|
|
1280
|
-
message: "The free plugin can review the latest three local sessions.",
|
|
1281
|
-
retryable: false,
|
|
1282
|
-
hint: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
1283
|
-
extra: {
|
|
1284
|
-
tool: "review_session",
|
|
1285
|
-
limit: 3,
|
|
1286
|
-
upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=review_session",
|
|
1287
|
-
cta: "Upgrade to review more than three sessions in the workspace dashboard.",
|
|
1288
|
-
},
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
const limit = Math.min(50, payload.limit ?? 50);
|
|
1292
|
-
const signals = sessionId
|
|
1293
|
-
? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
|
|
1294
|
-
: [];
|
|
1295
|
-
const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
1296
|
-
const decisions = sessionId
|
|
1297
|
-
? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
|
|
1298
|
-
: [];
|
|
1299
|
-
const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
|
|
1300
|
-
const counts = signals.reduce((accumulator, signal) => {
|
|
1301
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1302
|
-
return accumulator;
|
|
1303
|
-
}, {});
|
|
1304
|
-
const allCounts = allSignals.reduce((accumulator, signal) => {
|
|
1305
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1306
|
-
return accumulator;
|
|
1307
|
-
}, {});
|
|
1308
|
-
const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
|
|
1309
|
-
if (payload.format === "json") {
|
|
1310
|
-
return budgetedLocalResponse({
|
|
1311
|
-
ok: true,
|
|
1312
|
-
tier: "free",
|
|
1313
|
-
sessionId,
|
|
1314
|
-
format: "json",
|
|
1315
|
-
signals: payload.compact
|
|
1316
|
-
? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
|
|
1317
|
-
: signals.map((signal) => signalWithDecision(localStore, signal)),
|
|
1318
|
-
decisions: payload.compact
|
|
1319
|
-
? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
|
|
1320
|
-
: decisions.map((decision) => decisionWithSignals(localStore, decision)),
|
|
1321
|
-
decisionCandidates,
|
|
1322
|
-
nextCursor,
|
|
1323
|
-
counts: {
|
|
1324
|
-
totalSignals: allSignals.length,
|
|
1325
|
-
byKind: allCounts,
|
|
1326
|
-
},
|
|
1327
|
-
}, payload.max_chars);
|
|
1328
|
-
}
|
|
1329
|
-
return budgetedLocalResponse({
|
|
1330
|
-
ok: true,
|
|
1331
|
-
tier: "free",
|
|
1332
|
-
sessionId,
|
|
1333
|
-
format: "markdown",
|
|
1334
|
-
rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
|
|
1335
|
-
...(payload.compact
|
|
1336
|
-
? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
|
|
1337
|
-
: {}),
|
|
1338
|
-
counts: {
|
|
1339
|
-
totalSignals: allSignals.length,
|
|
1340
|
-
byKind: Object.keys(allCounts).length ? allCounts : counts,
|
|
1341
|
-
},
|
|
1342
|
-
}, payload.max_chars);
|
|
1343
|
-
});
|
|
1344
|
-
server.tool("recap", "Summarize the latest local coding-agent session as a digest, standup, or share-ready recap.", {
|
|
1001
|
+
server.tool("recap", "Get a concise recap of the latest local coding-agent session.", {
|
|
1345
1002
|
format: z.enum(["digest", "standup", "share"]).default("digest"),
|
|
1346
1003
|
sessionId: z.string().optional(),
|
|
1347
1004
|
compact: z.boolean().optional(),
|
|
@@ -1352,6 +1009,9 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1352
1009
|
const loginRequired = requireFreeIdentity();
|
|
1353
1010
|
if (loginRequired)
|
|
1354
1011
|
return loginRequired;
|
|
1012
|
+
const limited = consumeLimitedLocalUse("recap");
|
|
1013
|
+
if (limited)
|
|
1014
|
+
return limited;
|
|
1355
1015
|
const scopeKey = currentScopeKey();
|
|
1356
1016
|
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1357
1017
|
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
@@ -1367,19 +1027,18 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1367
1027
|
: {}),
|
|
1368
1028
|
}, payload.max_chars);
|
|
1369
1029
|
});
|
|
1370
|
-
server.tool("coach", "
|
|
1371
|
-
scope: z.enum(["session", "week", "patterns"]).default("session"),
|
|
1030
|
+
server.tool("coach", "Get one concise coaching nudge for the local coding-agent session.", {
|
|
1372
1031
|
sessionId: z.string().optional(),
|
|
1373
1032
|
max_chars: maxCharsSchema,
|
|
1374
1033
|
}, async (payload) => {
|
|
1375
|
-
if (payload.scope === "week" || payload.scope === "patterns") {
|
|
1376
|
-
return localResponse(paidFeatureNudge("coach"));
|
|
1377
|
-
}
|
|
1378
1034
|
if (!localStore || mode.mode === "paid")
|
|
1379
1035
|
return localResponse(paidFeatureNudge("coach"));
|
|
1380
1036
|
const loginRequired = requireFreeIdentity();
|
|
1381
1037
|
if (loginRequired)
|
|
1382
1038
|
return loginRequired;
|
|
1039
|
+
const limited = consumeLimitedLocalUse("coach");
|
|
1040
|
+
if (limited)
|
|
1041
|
+
return limited;
|
|
1383
1042
|
const scopeKey = currentScopeKey();
|
|
1384
1043
|
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1385
1044
|
const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
|
|
@@ -1388,12 +1047,9 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1388
1047
|
return budgetedLocalResponse({
|
|
1389
1048
|
ok: true,
|
|
1390
1049
|
tier: "free",
|
|
1391
|
-
scope: "session",
|
|
1392
1050
|
sessionId,
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
failureMode: coaching.failureMode,
|
|
1396
|
-
rendered: `Decision quality score: ${coaching.qualityScore}/100\nBiggest gap: ${coaching.biggestGap}`,
|
|
1051
|
+
nudge: coaching.nudge,
|
|
1052
|
+
rendered: coaching.nudge,
|
|
1397
1053
|
}, payload.max_chars);
|
|
1398
1054
|
});
|
|
1399
1055
|
server.tool("promote_signal_to_decision", "Copy a captured signal summary into a linked local decision.", {
|
|
@@ -1427,6 +1083,21 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1427
1083
|
extra: { tool: "promote_signal_to_decision" },
|
|
1428
1084
|
});
|
|
1429
1085
|
}
|
|
1086
|
+
const linkedDecision = localStore.getDecisionForSignal(signal.id);
|
|
1087
|
+
if (linkedDecision) {
|
|
1088
|
+
const decision = localStore.updateDecision(linkedDecision.id, {
|
|
1089
|
+
...(payload.why !== undefined ? { why: payload.why } : {}),
|
|
1090
|
+
status: payload.status,
|
|
1091
|
+
}) ?? linkedDecision;
|
|
1092
|
+
return localResponse({
|
|
1093
|
+
ok: true,
|
|
1094
|
+
id: decision.id,
|
|
1095
|
+
sequence: localStore.stats().decisions,
|
|
1096
|
+
decision: decisionWithSignals(localStore, decision),
|
|
1097
|
+
linkedSignalId: signal.id,
|
|
1098
|
+
reused: true,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1430
1101
|
if (payload.idempotencyKey) {
|
|
1431
1102
|
const existingId = localStore.getMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`);
|
|
1432
1103
|
if (existingId) {
|
|
@@ -1469,151 +1140,8 @@ export function createAskTheWMcpServer(options = {}) {
|
|
|
1469
1140
|
warnings,
|
|
1470
1141
|
});
|
|
1471
1142
|
});
|
|
1472
|
-
server.tool("list_decision_candidates", "List local signals that look like decision moments and can be promoted.", {
|
|
1473
|
-
sessionId: z.string().optional(),
|
|
1474
|
-
limit: z.number().int().positive().max(300).default(50),
|
|
1475
|
-
cursor: cursorSchema,
|
|
1476
|
-
max_chars: maxCharsSchema,
|
|
1477
|
-
}, async (payload) => {
|
|
1478
|
-
if (!localStore || mode.mode === "paid")
|
|
1479
|
-
return localResponse(paidFeatureNudge("list_decision_candidates"));
|
|
1480
|
-
const loginRequired = requireFreeIdentity();
|
|
1481
|
-
if (loginRequired)
|
|
1482
|
-
return loginRequired;
|
|
1483
|
-
const scopeKey = currentScopeKey();
|
|
1484
|
-
const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
|
|
1485
|
-
const result = listDecisionCandidates({
|
|
1486
|
-
store: localStore,
|
|
1487
|
-
sessionId,
|
|
1488
|
-
scopeKey,
|
|
1489
|
-
limit: payload.limit,
|
|
1490
|
-
cursor: payload.cursor,
|
|
1491
|
-
});
|
|
1492
|
-
return budgetedLocalResponse({
|
|
1493
|
-
ok: true,
|
|
1494
|
-
tier: "free",
|
|
1495
|
-
sessionId,
|
|
1496
|
-
decisionCandidates: result.candidates,
|
|
1497
|
-
nextCursor: result.nextCursor,
|
|
1498
|
-
}, payload.max_chars);
|
|
1499
|
-
});
|
|
1500
|
-
server.tool("search_trail", "Search local signals and decisions together.", {
|
|
1501
|
-
query: z.string().min(1),
|
|
1502
|
-
sessionId: z.string().optional(),
|
|
1503
|
-
limit: z.number().int().positive().max(100).default(25),
|
|
1504
|
-
cursor: cursorSchema,
|
|
1505
|
-
compact: z.boolean().optional(),
|
|
1506
|
-
max_chars: maxCharsSchema,
|
|
1507
|
-
}, async (payload) => {
|
|
1508
|
-
if (!localStore || mode.mode === "paid")
|
|
1509
|
-
return localResponse(paidFeatureNudge("search_trail"));
|
|
1510
|
-
const loginRequired = requireFreeIdentity();
|
|
1511
|
-
if (loginRequired)
|
|
1512
|
-
return loginRequired;
|
|
1513
|
-
const result = searchTrail({
|
|
1514
|
-
store: localStore,
|
|
1515
|
-
query: payload.query,
|
|
1516
|
-
scopeKey: currentScopeKey(),
|
|
1517
|
-
sessionId: payload.sessionId,
|
|
1518
|
-
limit: payload.limit,
|
|
1519
|
-
cursor: payload.cursor,
|
|
1520
|
-
compact: payload.compact,
|
|
1521
|
-
});
|
|
1522
|
-
return budgetedLocalResponse({
|
|
1523
|
-
ok: true,
|
|
1524
|
-
tier: "free",
|
|
1525
|
-
query: payload.query,
|
|
1526
|
-
matches: result.matches,
|
|
1527
|
-
nextCursor: result.nextCursor,
|
|
1528
|
-
}, payload.max_chars);
|
|
1529
|
-
});
|
|
1530
|
-
server.tool("export_decisions", paidDescription("Export decisions from your workspace.", mode.mode), {
|
|
1531
|
-
format: z.enum(["json", "markdown", "jsonl"]).default("json"),
|
|
1532
|
-
cursor: cursorSchema,
|
|
1533
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1534
|
-
max_chars: maxCharsSchema,
|
|
1535
|
-
}, async (payload) => mode.mode === "free"
|
|
1536
|
-
? localResponse(paidFeatureNudge("export_decisions"))
|
|
1537
|
-
: apiToolResponse(routeWithQuery("/api/export/timeline", {
|
|
1538
|
-
format: payload.format,
|
|
1539
|
-
cursor: payload.cursor,
|
|
1540
|
-
limit: payload.limit ?? 50,
|
|
1541
|
-
max_chars: payload.max_chars ?? 8000,
|
|
1542
|
-
})));
|
|
1543
|
-
server.tool("view_timeline", "View signals and decisions counts bucketed by session, day, or month.", {
|
|
1544
|
-
scope: z.enum(["day", "month", "session"]).default("day"),
|
|
1545
|
-
range: z.enum(["7D", "30D", "90D", "12M", "CUSTOM"]).default("30D"),
|
|
1546
|
-
start: z.string().optional(),
|
|
1547
|
-
end: z.string().optional(),
|
|
1548
|
-
limit: z.number().int().positive().max(300).optional(),
|
|
1549
|
-
outcomeId: z.string().optional(),
|
|
1550
|
-
decisionStatus: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
|
|
1551
|
-
signalSource: z.string().optional(),
|
|
1552
|
-
max_chars: maxCharsSchema,
|
|
1553
|
-
}, async (payload) => {
|
|
1554
|
-
if (mode.mode === "paid") {
|
|
1555
|
-
return apiToolResponse(routeWithQuery("/api/analytics/timeline-counts", {
|
|
1556
|
-
scope: payload.scope,
|
|
1557
|
-
range: payload.range,
|
|
1558
|
-
start: payload.start,
|
|
1559
|
-
end: payload.end,
|
|
1560
|
-
limit: payload.limit,
|
|
1561
|
-
outcomeId: payload.outcomeId,
|
|
1562
|
-
decisionStatus: payload.decisionStatus,
|
|
1563
|
-
signalSource: payload.signalSource,
|
|
1564
|
-
}));
|
|
1565
|
-
}
|
|
1566
|
-
if (!localStore) {
|
|
1567
|
-
return localToolError({
|
|
1568
|
-
code: "local_store_unavailable",
|
|
1569
|
-
message: "The local Ask The W store is unavailable.",
|
|
1570
|
-
retryable: true,
|
|
1571
|
-
hint: "Retry after restarting the plugin host.",
|
|
1572
|
-
});
|
|
1573
|
-
}
|
|
1574
|
-
const loginRequired = requireFreeIdentity();
|
|
1575
|
-
if (loginRequired)
|
|
1576
|
-
return loginRequired;
|
|
1577
|
-
const scopeKey = currentScopeKey();
|
|
1578
|
-
const points = buildLocalTimeline({
|
|
1579
|
-
scope: payload.scope,
|
|
1580
|
-
signals: localStore.listSignals({ scopeKey, limit: 100000 }),
|
|
1581
|
-
decisions: localStore.listDecisions({ scopeKey, limit: 100000 }),
|
|
1582
|
-
limit: payload.limit,
|
|
1583
|
-
});
|
|
1584
|
-
const totals = points.reduce((accumulator, point) => ({
|
|
1585
|
-
signals: accumulator.signals + point.signalCount,
|
|
1586
|
-
decisions: accumulator.decisions + point.decisionCount,
|
|
1587
|
-
}), { signals: 0, decisions: 0 });
|
|
1588
|
-
return budgetedLocalResponse({
|
|
1589
|
-
ok: true,
|
|
1590
|
-
tier: "free",
|
|
1591
|
-
scope: payload.scope,
|
|
1592
|
-
period: {
|
|
1593
|
-
start: points[0]?.startedAt ?? points[0]?.x ?? "",
|
|
1594
|
-
end: points.at(-1)?.endedAt ?? points.at(-1)?.x ?? "",
|
|
1595
|
-
label: "Local timeline",
|
|
1596
|
-
tz: "UTC",
|
|
1597
|
-
},
|
|
1598
|
-
points,
|
|
1599
|
-
totals,
|
|
1600
|
-
insights: buildLocalTimelineInsights(points),
|
|
1601
|
-
narrative: `Local timeline: ${totals.signals} signals, ${totals.decisions} decisions.`,
|
|
1602
|
-
markdownTable: renderLocalTimelineMarkdown(points, payload.scope),
|
|
1603
|
-
}, payload.max_chars);
|
|
1604
|
-
});
|
|
1605
1143
|
return server;
|
|
1606
1144
|
}
|
|
1607
|
-
function renderDecisionDigest(decisions) {
|
|
1608
|
-
if (decisions.length === 0) {
|
|
1609
|
-
return "# Decisions\n\nNo local decisions captured yet.";
|
|
1610
|
-
}
|
|
1611
|
-
return [
|
|
1612
|
-
"# Decisions",
|
|
1613
|
-
"",
|
|
1614
|
-
...decisions.map((decision) => [`## ${decision.headline}`, `- id: ${decision.id}`, `- status: ${decision.status}`, `- created: ${decision.createdAt}`, decision.why ? `- why: ${decision.why}` : "- why: not captured"].join("\n")),
|
|
1615
|
-
].join("\n\n");
|
|
1616
|
-
}
|
|
1617
1145
|
function buildSessionCoach(input) {
|
|
1618
1146
|
const verificationCount = input.signals.filter((signal) => signal.kind === "verification_result").length;
|
|
1619
1147
|
const implementationCount = input.signals.filter((signal) => signal.kind === "implementation_update").length;
|
|
@@ -1623,61 +1151,20 @@ function buildSessionCoach(input) {
|
|
|
1623
1151
|
const hasVerification = verificationCount > 0;
|
|
1624
1152
|
const hasDecision = decisionCount > 0;
|
|
1625
1153
|
const hasDirection = directionCount > 0;
|
|
1626
|
-
const
|
|
1627
|
-
Math.min(25, decisionCount * 12) +
|
|
1628
|
-
(hasVerification ? 25 : 0) +
|
|
1629
|
-
(hasDirection ? 10 : 0) -
|
|
1630
|
-
Math.max(0, implementationCount - decisionCount) * 3));
|
|
1631
|
-
const failureMode = implementationCount >= 3 && verificationCount === 0
|
|
1154
|
+
const nudge = implementationCount >= 3 && verificationCount === 0
|
|
1632
1155
|
? `You captured ${implementationCount} implementation updates but no verification_result; run one check and capture it before ending.`
|
|
1633
1156
|
: input.signals.length >= 6 && finalSummaryCount === 0
|
|
1634
1157
|
? `You captured ${input.signals.length} signals but no final_summary; close the session with the outcome and remaining risk.`
|
|
1635
1158
|
: decisionCount === 0 && directionCount > 0
|
|
1636
1159
|
? "Direction changed, but no decision was captured; promote the clearest direction_change signal."
|
|
1637
|
-
:
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
return { qualityScore, biggestGap, failureMode };
|
|
1646
|
-
}
|
|
1647
|
-
function renderSessionMarkdown(input) {
|
|
1648
|
-
const counts = input.signals.reduce((accumulator, signal) => {
|
|
1649
|
-
accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
|
|
1650
|
-
return accumulator;
|
|
1651
|
-
}, {});
|
|
1652
|
-
const activeDecisions = input.decisions.filter((decision) => decision.status !== "abandoned");
|
|
1653
|
-
const coaching = buildSessionCoach({
|
|
1654
|
-
signals: input.signals.map((signal) => ({ ...signal, filesTouched: [], commandsRun: [] })),
|
|
1655
|
-
decisions: input.decisions,
|
|
1656
|
-
});
|
|
1657
|
-
const lines = [
|
|
1658
|
-
"# Session Review",
|
|
1659
|
-
`Session: ${input.sessionId ?? "none"}`,
|
|
1660
|
-
`Signals: ${input.signals.length}`,
|
|
1661
|
-
`Signal kinds: ${Object.entries(counts).map(([kind, count]) => `${kind} ${count}`).join(", ") || "none"}`,
|
|
1662
|
-
`Active decisions: ${activeDecisions.length}`,
|
|
1663
|
-
`Coaching tip: ${coaching.biggestGap}`,
|
|
1664
|
-
"",
|
|
1665
|
-
"## Signals By Kind",
|
|
1666
|
-
...Object.entries(counts)
|
|
1667
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
1668
|
-
.map(([kind, count]) => `- ${kind}: ${count}`),
|
|
1669
|
-
"",
|
|
1670
|
-
"## Decision Candidates",
|
|
1671
|
-
...(input.decisionCandidates?.length
|
|
1672
|
-
? input.decisionCandidates.slice(0, 10).map((candidate) => `- signal ${candidate.signalId}: ${candidate.summary} (${candidate.suggestedStatus})`)
|
|
1673
|
-
: ["- none"]),
|
|
1674
|
-
"",
|
|
1675
|
-
"## Active Decisions",
|
|
1676
|
-
...(activeDecisions.length
|
|
1677
|
-
? activeDecisions.map((decision) => `- ${decision.headline} (${decision.status})${decision.why ? ` - ${decision.why}` : ""}`)
|
|
1678
|
-
: ["- none"]),
|
|
1679
|
-
];
|
|
1680
|
-
return lines.slice(0, 300).join("\n");
|
|
1160
|
+
: !hasDecision
|
|
1161
|
+
? "Promote the clearest captured signal into a decision before the trail goes stale."
|
|
1162
|
+
: !hasVerification
|
|
1163
|
+
? "Capture one verification result so the decision trail records whether the work actually held."
|
|
1164
|
+
: implementationCount > decisionCount * 3
|
|
1165
|
+
? "There are many implementation updates per decision; collapse the important why into one decision."
|
|
1166
|
+
: "The trail is usable. Keep the next decision tied to a verification result.";
|
|
1167
|
+
return { nudge };
|
|
1681
1168
|
}
|
|
1682
1169
|
function decisionalWeight(signal) {
|
|
1683
1170
|
const kindWeight = signal.kind === "direction_change" ? 4 : signal.kind === "verification_result" ? 3 : signal.kind === "implementation_update" ? 2 : 1;
|
|
@@ -1796,21 +1283,6 @@ function candidateFromSignal(signal, linkedDecision) {
|
|
|
1796
1283
|
capturedAt: signal.capturedAt,
|
|
1797
1284
|
};
|
|
1798
1285
|
}
|
|
1799
|
-
function listDecisionCandidates(input) {
|
|
1800
|
-
const signals = input.store.listSignals({
|
|
1801
|
-
sessionId: input.sessionId ?? undefined,
|
|
1802
|
-
scopeKey: input.scopeKey,
|
|
1803
|
-
cursor: input.cursor,
|
|
1804
|
-
limit: Math.max(1, Math.min(300, input.limit ?? 50)),
|
|
1805
|
-
});
|
|
1806
|
-
const candidates = signals
|
|
1807
|
-
.map((signal) => candidateFromSignal(signal, input.store.getDecisionForSignal(signal.id)))
|
|
1808
|
-
.filter((candidate) => Boolean(candidate));
|
|
1809
|
-
return {
|
|
1810
|
-
candidates,
|
|
1811
|
-
nextCursor: signals.length >= (input.limit ?? 50) ? signals.at(-1)?.capturedAt ?? null : null,
|
|
1812
|
-
};
|
|
1813
|
-
}
|
|
1814
1286
|
function normalizedDecisionTerms(text) {
|
|
1815
1287
|
const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "keep", "use", "adopt", "remove", "drop", "replace", "defer", "ship", "commit"]);
|
|
1816
1288
|
return String(text ?? "")
|
|
@@ -1855,55 +1327,6 @@ function detectDecisionConflicts(input) {
|
|
|
1855
1327
|
overlappingTerms: entry.overlap.slice(0, 5),
|
|
1856
1328
|
}));
|
|
1857
1329
|
}
|
|
1858
|
-
function searchTrail(input) {
|
|
1859
|
-
const terms = String(input.query ?? "")
|
|
1860
|
-
.toLowerCase()
|
|
1861
|
-
.split(/\s+/)
|
|
1862
|
-
.filter(Boolean);
|
|
1863
|
-
const limit = Math.max(1, Math.min(100, input.limit ?? 25));
|
|
1864
|
-
const haystackMatches = (value) => terms.every((term) => value.toLowerCase().includes(term));
|
|
1865
|
-
const signals = input.store
|
|
1866
|
-
.listSignals({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1867
|
-
.filter((signal) => haystackMatches([
|
|
1868
|
-
signal.summary,
|
|
1869
|
-
signal.kind,
|
|
1870
|
-
signal.filesTouched.join(" "),
|
|
1871
|
-
signal.commandsRun.join(" "),
|
|
1872
|
-
JSON.stringify(signal.evidence),
|
|
1873
|
-
JSON.stringify(signal.metadata),
|
|
1874
|
-
].join(" ")))
|
|
1875
|
-
.map((signal) => ({
|
|
1876
|
-
type: "signal",
|
|
1877
|
-
id: String(signal.id),
|
|
1878
|
-
createdAt: signal.capturedAt,
|
|
1879
|
-
score: terms.length,
|
|
1880
|
-
result: input.compact ? compactSignal(signal, input.store.getDecisionForSignal(signal.id)) : signalWithDecision(input.store, signal),
|
|
1881
|
-
}));
|
|
1882
|
-
const decisions = input.store
|
|
1883
|
-
.listDecisions({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
|
|
1884
|
-
.filter((decision) => haystackMatches([decision.headline, decision.why ?? "", decision.rawContent, decision.files.join(" "), decision.status].join(" ")))
|
|
1885
|
-
.map((decision) => ({
|
|
1886
|
-
type: "decision",
|
|
1887
|
-
id: decision.id,
|
|
1888
|
-
createdAt: decision.createdAt,
|
|
1889
|
-
score: terms.length,
|
|
1890
|
-
result: input.compact
|
|
1891
|
-
? {
|
|
1892
|
-
id: decision.id,
|
|
1893
|
-
headline: decision.headline,
|
|
1894
|
-
status: decision.status,
|
|
1895
|
-
signalIds: decision.sourceSignalIds,
|
|
1896
|
-
}
|
|
1897
|
-
: decisionWithSignals(input.store, decision),
|
|
1898
|
-
}));
|
|
1899
|
-
const matches = [...signals, ...decisions]
|
|
1900
|
-
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
|
1901
|
-
.slice(0, limit);
|
|
1902
|
-
return {
|
|
1903
|
-
matches,
|
|
1904
|
-
nextCursor: matches.length >= limit ? matches.at(-1)?.createdAt ?? null : null,
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
1330
|
const isDirectIndexExecution = Boolean(process.argv[1]) &&
|
|
1908
1331
|
(() => {
|
|
1909
1332
|
const modulePath = fileURLToPath(import.meta.url);
|
|
@@ -1919,7 +1342,7 @@ export async function runInitializeHandshake(input = {}) {
|
|
|
1919
1342
|
const modulePath = input.entrypoint ?? fileURLToPath(import.meta.url);
|
|
1920
1343
|
const child = spawn(process.execPath, [modulePath], {
|
|
1921
1344
|
cwd: process.cwd(),
|
|
1922
|
-
env: process.env,
|
|
1345
|
+
env: input.env ?? process.env,
|
|
1923
1346
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1924
1347
|
});
|
|
1925
1348
|
let stdout = "";
|
|
@@ -1944,7 +1367,7 @@ export async function runInitializeHandshake(input = {}) {
|
|
|
1944
1367
|
};
|
|
1945
1368
|
child.stdin.write(`${JSON.stringify(initialize)}\n`);
|
|
1946
1369
|
try {
|
|
1947
|
-
await new Promise((resolve, reject) => {
|
|
1370
|
+
return await new Promise((resolve, reject) => {
|
|
1948
1371
|
const timeout = setTimeout(() => {
|
|
1949
1372
|
reject(new Error(`Timed out waiting for initialize response. stdout=${stdout} stderr=${stderr}`));
|
|
1950
1373
|
}, input.timeoutMs ?? 3000);
|
|
@@ -1963,7 +1386,11 @@ export async function runInitializeHandshake(input = {}) {
|
|
|
1963
1386
|
reject(new Error(`Unexpected initialize response: ${JSON.stringify(response)}`));
|
|
1964
1387
|
return;
|
|
1965
1388
|
}
|
|
1966
|
-
resolve(
|
|
1389
|
+
resolve({
|
|
1390
|
+
serverInfoVersion: typeof response.result?.serverInfo?.version === "string"
|
|
1391
|
+
? response.result.serverInfo.version
|
|
1392
|
+
: undefined,
|
|
1393
|
+
});
|
|
1967
1394
|
};
|
|
1968
1395
|
child.stdout.on("data", check);
|
|
1969
1396
|
child.once("exit", failOnExit);
|