@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/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 { buildLocalTimeline, buildTimelineInsights as buildLocalTimelineInsights, renderTimelineMarkdown as renderLocalTimelineMarkdown } from "./lib/timeline-insights.js";
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: "0.4.7",
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("find_signal_by_summary", "Find recent signals by summary text without needing an opaque signal id first.", {
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", "Coach the local coding-agent session. Use for natural prompts like: Coach me on this session.", {
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
- qualityScore: coaching.qualityScore,
1394
- biggestGap: coaching.biggestGap,
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 qualityScore = Math.max(0, Math.min(100, 35 +
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
- : null;
1638
- const biggestGap = failureMode ?? (!hasDecision
1639
- ? "Promote the clearest captured signal into a decision before the trail goes stale."
1640
- : !hasVerification
1641
- ? "Capture one verification result so the decision trail records whether the work actually held."
1642
- : implementationCount > decisionCount * 3
1643
- ? "There are many implementation updates per decision; collapse the important why into one decision."
1644
- : "The trail is usable. Keep the next decision tied to a verification result.");
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);