@askthew/mcp-plugin 0.4.9 → 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.
Files changed (3) hide show
  1. package/README.md +9 -11
  2. package/dist/index.js +74 -564
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ Connect a local coding agent to Ask The W. The fastest path is free and local-fi
6
6
  npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp install --host claude_code --free --email you@founder.com
7
7
  ```
8
8
 
9
- This captures decisions and session signals to `~/.askthew/store.sqlite` and lets your agent run `review_decisions`, `review_session`, `recap`, `coach`, and `promote_signal_to_decision` without onboarding into the web app.
9
+ This captures decisions and session signals to `~/.askthew/store.sqlite` and lets your agent list the trail, keep decisions, and get limited local recap and coaching without onboarding into the web app.
10
10
 
11
11
  Founder-friendly promise: install from npm, then ask your coding agent to review the last session. You should see value in under 60 seconds.
12
12
 
@@ -19,7 +19,7 @@ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and o
19
19
  - Adds marked project instructions so future coding-agent sessions know when to send Ask The W updates.
20
20
  - Free mode stores full-fidelity signals and decisions locally in SQLite.
21
21
  - Paid workspace mode sends a startup heartbeat so Ask The W can show the plugin as installed.
22
- - Exposes `capture_session_signal` plus v1 API tools for decisions, outcomes, signals, outcome detail graphs, and north star reads or updates.
22
+ - Exposes a small MCP surface for capture, signals, decisions, recap, and coaching.
23
23
  - Redacts obvious secrets from summaries, evidence excerpts, commands, and metadata before sending.
24
24
  - Adds lightweight workspace metadata such as host type, repo name, app path, and server name.
25
25
 
@@ -27,10 +27,10 @@ This package runs a small MCP server that lets Codex, Claude Code, Cursor, and o
27
27
 
28
28
  - It does not send full transcripts by default.
29
29
  - It does not send local free-tier content to Ask The W unless you upgrade and run sync upload.
30
- - It does not link outcomes, score confidence, dedupe signals, or update the graph locally.
30
+ - It does not expose internal workspace APIs through the plugin.
31
31
  - It does not include the Ask The W app, private server code, Supabase code, or internal analytics code.
32
32
 
33
- Ask The W performs inference, linking, approval state, dedupe, and outcome updates in the app.
33
+ Ask The W keeps the local plugin focused on the trail your agent can use while it works.
34
34
 
35
35
  ## Free Local Install
36
36
 
@@ -183,15 +183,13 @@ The session-signal tool remains the main automatic capture path.
183
183
 
184
184
  Use compact summaries and short evidence excerpts. Do not send full transcripts. By default, write tools return compact responses; use `echo: "full"` only when you need the larger payload for debugging.
185
185
 
186
- The plugin also exposes v1 API tools that map to the app's authenticated routes:
186
+ The plugin exposes a small public tool surface:
187
187
 
188
188
  | Tool | Purpose |
189
189
  |---|---|
190
- | `list_decisions`, `get_decision`, `create_decision`, `update_decision`, `delete_decision` | Work with decision feed entries. |
191
- | `review_session`, `recap`, `coach`, `promote_signal_to_decision`, `find_signal_by_summary` | Review local sessions, get session coaching, find recent evidence, and turn signals into decisions in free mode. |
192
- | `list_outcomes`, `get_outcome`, `list_outcome_signals`, `create_outcome`, `update_outcome`, `delete_outcome` | Work with outcomes and their linked signals. |
193
- | `get_north_star`, `update_north_star` | Read or update the workspace north star. API updates are allowed only for private workspaces. |
194
- | `list_signals`, `get_signal` | Read workspace signals. |
190
+ | `capture_session_signal`, `list_signals` | Capture and list the local signal trail. |
191
+ | `list_decisions`, `get_decision`, `create_decision`, `promote_signal_to_decision` | Keep the decisions worth remembering. |
192
+ | `recap`, `coach` | Get up to three local recaps and three coaching nudges. After that, the plugin returns `Limit reached. Upgrade to the paid plan.` |
195
193
 
196
194
  Free PLG helpers:
197
195
 
@@ -202,7 +200,7 @@ npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp digest -
202
200
 
203
201
  The hook prompts when staged files recently had implementation signals but no linked decision. The weekly digest writes `~/Documents/askthew-digest-YYYY-WW.md`.
204
202
 
205
- API mutations are text-only and are recorded back into the workspace signal feed. Decision and outcome deletes require a `confirmText` value that exactly matches the stored decision headline or outcome name after whitespace normalization. North star delete is not available through the API.
203
+ Decision writes are text-only and are recorded with the local trail.
206
204
 
207
205
  ## Troubleshooting
208
206
 
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { resolveMcpMode } from "./lib/free-tier-policy.js";
11
11
  import { LocalStore } from "./lib/local-store.js";
12
12
  import { buildTelemetryPayload, flushTelemetryOutbox } from "./lib/telemetry.js";
13
13
  import { ensureLocalIdentity } from "./lib/local-identity.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
16
  const requirePackageJson = createRequire(import.meta.url);
17
17
  function packageVersion() {
@@ -321,6 +321,8 @@ const echoSchema = z.enum(["summary", "full"]).optional();
321
321
  const cursorSchema = z.string().optional();
322
322
  const idempotencyKeySchema = z.string().min(1).max(200).optional();
323
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.";
324
326
  function traceId() {
325
327
  return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
326
328
  }
@@ -622,6 +624,7 @@ export function createAskTheWMcpServer(options = {}) {
622
624
  const compactWriteResponse = (input) => localResponse({
623
625
  ok: input.ok !== false,
624
626
  id: input.id ?? null,
627
+ ...(input.decisionId ? { decisionId: input.decisionId } : {}),
625
628
  ...(input.sessionId ? { sessionId: input.sessionId } : {}),
626
629
  ...(typeof input.sequence === "number" ? { sequence: input.sequence } : {}),
627
630
  ...(typeof input.signalCount === "number" ? { signalCount: input.signalCount } : {}),
@@ -666,88 +669,27 @@ export function createAskTheWMcpServer(options = {}) {
666
669
  }
667
670
  return null;
668
671
  };
669
- const localSessionAnalysisResponse = (payload) => {
670
- if (!localStore) {
671
- return localToolError({
672
- code: "local_store_unavailable",
673
- message: "The local Ask The W store is unavailable.",
674
- retryable: true,
675
- hint: "Retry after restarting the plugin host.",
676
- });
677
- }
678
- const loginRequired = requireFreeIdentity();
679
- if (loginRequired)
680
- return loginRequired;
681
- const scopeKey = currentScopeKey();
682
- const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
683
- const allSessionIds = localStore.listSessionIds({ limit: 100000, scopeKey });
684
- const allowedSessionIds = new Set(allSessionIds.slice(0, 3));
685
- if (sessionId && allSessionIds.length > 3 && !allowedSessionIds.has(sessionId)) {
686
- return localToolError({
687
- code: "free_tier_limit",
688
- message: "The free plugin can analyze the latest three local sessions.",
689
- retryable: false,
690
- hint: "Upgrade to review more than three sessions in the workspace dashboard.",
691
- extra: {
692
- tool: "analyze_session",
693
- limit: 3,
694
- upgradeUrl: "https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=analyze_session",
695
- cta: "Upgrade to analyze more than three sessions in the workspace dashboard.",
696
- },
697
- });
698
- }
699
- const limit = Math.min(50, payload.limit ?? 50);
700
- const signals = sessionId
701
- ? localStore.listSignals({ sessionId, scopeKey, cursor: payload.cursor, limit })
702
- : [];
703
- const allSignals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
704
- const decisions = sessionId
705
- ? localStore.listDecisions({ sessionId, scopeKey, limit: 100000 })
706
- : [];
707
- const decisionCandidates = listDecisionCandidates({ store: localStore, sessionId, scopeKey, limit: 25 }).candidates;
708
- const counts = signals.reduce((accumulator, signal) => {
709
- accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
710
- return accumulator;
711
- }, {});
712
- const allCounts = allSignals.reduce((accumulator, signal) => {
713
- accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
714
- return accumulator;
715
- }, {});
716
- const nextCursor = signals.length >= limit ? signals.at(-1)?.capturedAt ?? null : null;
717
- if ((payload.format ?? "markdown") === "json") {
718
- return budgetedLocalResponse({
719
- ok: true,
720
- tier: "free",
721
- sessionId,
722
- format: "json",
723
- signals: payload.compact
724
- ? signals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id)))
725
- : signals.map((signal) => signalWithDecision(localStore, signal)),
726
- decisions: payload.compact
727
- ? decisions.map((decision) => ({ id: decision.id, headline: decision.headline, status: decision.status, signalIds: decision.sourceSignalIds }))
728
- : decisions.map((decision) => decisionWithSignals(localStore, decision)),
729
- decisionCandidates,
730
- nextCursor,
731
- counts: {
732
- totalSignals: allSignals.length,
733
- byKind: allCounts,
734
- },
735
- }, payload.max_chars);
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);
736
690
  }
737
- return budgetedLocalResponse({
738
- ok: true,
739
- tier: "free",
740
- sessionId,
741
- format: "markdown",
742
- rendered: renderSessionMarkdown({ sessionId, signals: allSignals, decisions, decisionCandidates }),
743
- ...(payload.compact
744
- ? { signals: allSignals.map((signal) => compactSignal(signal, localStore.getDecisionForSignal(signal.id))) }
745
- : {}),
746
- counts: {
747
- totalSignals: allSignals.length,
748
- byKind: Object.keys(allCounts).length ? allCounts : counts,
749
- },
750
- }, payload.max_chars);
691
+ localStore.setMeta(key, String(count + 1));
692
+ return null;
751
693
  };
752
694
  server.tool("capture_session_signal", {
753
695
  sessionId: z.string().min(1),
@@ -792,6 +734,19 @@ export function createAskTheWMcpServer(options = {}) {
792
734
  scopeKey,
793
735
  limit: 100000,
794
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;
795
750
  if (sessionSignal.kind === "final_summary" && mode.cliCredentials) {
796
751
  localStore.enqueueTelemetry(buildTelemetryPayload({
797
752
  store: localStore,
@@ -808,6 +763,7 @@ export function createAskTheWMcpServer(options = {}) {
808
763
  if (!payload.echo) {
809
764
  return compactWriteResponse({
810
765
  id: signal.id,
766
+ decisionId: autoDecision?.id,
811
767
  sessionId: signal.sessionId,
812
768
  sequence: signal.sequence,
813
769
  signalCount: sessionSignalCount,
@@ -822,12 +778,14 @@ export function createAskTheWMcpServer(options = {}) {
822
778
  signalCount: sessionSignalCount,
823
779
  summary: signal.summary,
824
780
  kind: signal.kind,
781
+ ...(autoDecision ? { decisionId: autoDecision.id } : {}),
825
782
  });
826
783
  }
827
784
  return localResponse({
828
785
  ok: true,
829
786
  tier: "free",
830
787
  signal,
788
+ ...(autoDecision ? { decision: decisionWithSignals(localStore, autoDecision) } : {}),
831
789
  note: localStore.usingJsonFallback
832
790
  ? "Captured locally in JSON fallback mode because SQLite was unavailable."
833
791
  : "Captured locally in SQLite. Aggregate telemetry only may flush on final_summary unless opted out.",
@@ -1003,179 +961,6 @@ export function createAskTheWMcpServer(options = {}) {
1003
961
  ? { ok: true, id: upstreamId(upstream), sequence: upstreamSequence(upstream) ?? 1 }
1004
962
  : upstream);
1005
963
  });
1006
- server.tool("update_decision", {
1007
- id: z.string().min(1),
1008
- headline: z.string().min(1).optional(),
1009
- why: z.string().optional(),
1010
- status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
1011
- alignment: z.enum(["aligned", "orthogonal", "conflicts", "ambiguous"]).optional(),
1012
- outcomeId: z.string().min(1).optional(),
1013
- idempotencyKey: idempotencyKeySchema,
1014
- echo: echoSchema,
1015
- }, async (payload) => {
1016
- if (mode.mode !== "paid" && localStore) {
1017
- const loginRequired = requireFreeIdentity();
1018
- if (loginRequired)
1019
- return loginRequired;
1020
- const decision = localStore.updateDecision(payload.id, {
1021
- ...(payload.headline ? { headline: payload.headline } : {}),
1022
- ...(payload.why !== undefined ? { why: payload.why } : {}),
1023
- ...(payload.status ? { status: payload.status } : {}),
1024
- ...(payload.alignment !== undefined ? { alignment: payload.alignment } : {}),
1025
- });
1026
- if (!decision) {
1027
- return localToolError({
1028
- code: "not_found",
1029
- message: "Decision not found in the local Ask The W store.",
1030
- retryable: false,
1031
- hint: "Check the decision id before updating.",
1032
- });
1033
- }
1034
- const warnings = detectDecisionConflicts({
1035
- decision,
1036
- decisions: localStore.listDecisions({ limit: 100000, scopeKey: currentScopeKey() }),
1037
- });
1038
- if (!payload.echo) {
1039
- return compactWriteResponse({ id: decision.id, sequence: localStore.stats().decisions, warnings });
1040
- }
1041
- return localResponse(payload.echo === "summary"
1042
- ? { ok: true, id: decision.id, sequence: localStore.stats().decisions, headline: decision.headline, warnings }
1043
- : { ok: true, tier: "free", decision: decisionWithSignals(localStore, decision), warnings });
1044
- }
1045
- const upstream = await postToServer(payload.echo === "full"
1046
- ? `/api/decisions/${encodeURIComponent(payload.id)}`
1047
- : withResponseShape(`/api/decisions/${encodeURIComponent(payload.id)}`), {
1048
- headline: payload.headline,
1049
- why: payload.why,
1050
- status: payload.status,
1051
- alignment: payload.alignment,
1052
- outcomeId: payload.outcomeId,
1053
- }, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
1054
- const failure = upstreamFailure(upstream);
1055
- if (failure)
1056
- return localResponse(failure);
1057
- if (!payload.echo) {
1058
- return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
1059
- }
1060
- return localResponse(payload.echo === "summary"
1061
- ? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
1062
- : upstream);
1063
- });
1064
- server.tool("delete_decision", {
1065
- id: z.string().min(1),
1066
- confirmText: z.string().min(1),
1067
- idempotencyKey: idempotencyKeySchema,
1068
- }, async (payload) => {
1069
- if (mode.mode !== "paid" && localStore) {
1070
- return localResponse(paidFeatureNudge("delete_decision"));
1071
- }
1072
- return apiToolResponse(`/api/decisions/${encodeURIComponent(payload.id)}`, {
1073
- confirmText: payload.confirmText,
1074
- }, "DELETE", { idempotencyKey: payload.idempotencyKey });
1075
- });
1076
- server.tool("list_outcomes", paidDescription("List outcomes from your workspace.", mode.mode), {
1077
- limit: z.number().int().positive().max(300).optional(),
1078
- cursor: cursorSchema,
1079
- }, async (payload) => {
1080
- if (mode.mode === "free")
1081
- return localResponse(paidFeatureNudge("list_outcomes"));
1082
- return apiToolResponse(routeWithQuery("/api/outcomes", {
1083
- limit: payload.limit,
1084
- cursor: payload.cursor,
1085
- }));
1086
- });
1087
- server.tool("get_outcome", paidDescription("Get outcome detail from your workspace.", mode.mode), {
1088
- id: z.string().min(1),
1089
- }, async (payload) => mode.mode === "free"
1090
- ? localResponse(paidFeatureNudge("get_outcome"))
1091
- : apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`));
1092
- server.tool("list_outcome_signals", paidDescription("List signals linked to an outcome.", mode.mode), {
1093
- id: z.string().min(1),
1094
- limit: z.number().int().positive().max(300).optional(),
1095
- cursor: cursorSchema,
1096
- }, async (payload) => mode.mode === "free"
1097
- ? localResponse(paidFeatureNudge("list_outcome_signals"))
1098
- : apiToolResponse(routeWithQuery(`/api/outcomes/${encodeURIComponent(payload.id)}/signals`, {
1099
- limit: payload.limit,
1100
- cursor: payload.cursor,
1101
- })));
1102
- server.tool("create_outcome", paidDescription("Create a new outcome.", mode.mode), {
1103
- name: z.string().min(1),
1104
- summary: z.string().optional(),
1105
- causalHypothesis: z.string().optional(),
1106
- suggestedAction: z.string().optional(),
1107
- idempotencyKey: idempotencyKeySchema,
1108
- }, async (payload) => {
1109
- if (mode.mode === "free")
1110
- return localResponse(paidFeatureNudge("create_outcome"));
1111
- return apiToolResponse("/api/outcomes", {
1112
- name: payload.name,
1113
- summary: payload.summary,
1114
- causalHypothesis: payload.causalHypothesis,
1115
- suggestedAction: payload.suggestedAction,
1116
- }, "POST", { idempotencyKey: payload.idempotencyKey });
1117
- });
1118
- server.tool("update_outcome", paidDescription("Update an outcome.", mode.mode), {
1119
- id: z.string().min(1),
1120
- name: z.string().min(1).optional(),
1121
- summary: z.string().optional(),
1122
- causalHypothesis: z.string().optional(),
1123
- suggestedAction: z.string().optional(),
1124
- status: z.enum(["active", "achieved", "abandoned", "archived"]).optional(),
1125
- idempotencyKey: idempotencyKeySchema,
1126
- echo: echoSchema,
1127
- }, async (payload) => {
1128
- if (mode.mode === "free")
1129
- return localResponse(paidFeatureNudge("update_outcome"));
1130
- const upstream = await postToServer(payload.echo === "full"
1131
- ? `/api/outcomes/${encodeURIComponent(payload.id)}`
1132
- : withResponseShape(`/api/outcomes/${encodeURIComponent(payload.id)}`), {
1133
- name: payload.name,
1134
- summary: payload.summary,
1135
- causalHypothesis: payload.causalHypothesis,
1136
- suggestedAction: payload.suggestedAction,
1137
- status: payload.status,
1138
- }, options, { method: "PATCH", idempotencyKey: payload.idempotencyKey });
1139
- const failure = upstreamFailure(upstream);
1140
- if (failure)
1141
- return localResponse(failure);
1142
- if (!payload.echo) {
1143
- return compactWriteResponse({ id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 });
1144
- }
1145
- return localResponse(payload.echo === "summary"
1146
- ? { ok: true, id: upstreamId(upstream) ?? payload.id, sequence: upstreamSequence(upstream) ?? 1 }
1147
- : upstream);
1148
- });
1149
- server.tool("delete_outcome", paidDescription("Delete an outcome.", mode.mode), {
1150
- id: z.string().min(1),
1151
- confirmText: z.string().min(1),
1152
- idempotencyKey: idempotencyKeySchema,
1153
- }, async (payload) => {
1154
- if (mode.mode === "free")
1155
- return localResponse(paidFeatureNudge("delete_outcome"));
1156
- return apiToolResponse(`/api/outcomes/${encodeURIComponent(payload.id)}`, {
1157
- confirmText: payload.confirmText,
1158
- }, "DELETE", { idempotencyKey: payload.idempotencyKey });
1159
- });
1160
- server.tool("get_north_star", paidDescription("Read the workspace north-star metric.", mode.mode), {}, async () => mode.mode === "free"
1161
- ? localResponse(paidFeatureNudge("get_north_star"))
1162
- : apiToolResponse("/api/north-star"));
1163
- server.tool("update_north_star", paidDescription("Update the workspace north-star metric.", mode.mode), {
1164
- metric: z.string().min(1),
1165
- current: z.string().min(1),
1166
- target: z.string().min(1),
1167
- reason: z.string().min(1),
1168
- idempotencyKey: idempotencyKeySchema,
1169
- }, async (payload) => {
1170
- if (mode.mode === "free")
1171
- return localResponse(paidFeatureNudge("update_north_star"));
1172
- return apiToolResponse("/api/north-star", {
1173
- metric: payload.metric,
1174
- current: payload.current,
1175
- target: payload.target,
1176
- reason: payload.reason,
1177
- }, "POST", { idempotencyKey: payload.idempotencyKey });
1178
- });
1179
964
  server.tool("list_signals", {
1180
965
  limit: z.number().int().positive().max(300).optional(),
1181
966
  cursor: z.string().optional(),
@@ -1213,109 +998,7 @@ export function createAskTheWMcpServer(options = {}) {
1213
998
  max_chars: payload.max_chars ?? 8000,
1214
999
  }));
1215
1000
  });
1216
- server.tool("find_signal_by_summary", "Find recent signals by summary text without needing an opaque signal id first.", {
1217
- query: z.string().min(1),
1218
- sessionId: z.string().optional(),
1219
- limit: z.number().int().positive().max(50).default(5),
1220
- compact: z.boolean().optional(),
1221
- max_chars: maxCharsSchema,
1222
- }, async (payload) => {
1223
- if (mode.mode !== "paid" && localStore) {
1224
- return localResponse(paidFeatureNudge("find_signal_by_summary"));
1225
- }
1226
- return apiToolResponse(routeWithQuery("/api/signals", {
1227
- query: payload.query,
1228
- sessionId: payload.sessionId,
1229
- limit: payload.limit,
1230
- compact: payload.compact ?? true,
1231
- max_chars: payload.max_chars ?? 8000,
1232
- }));
1233
- });
1234
- server.tool("get_signal", {
1235
- id: z.string().min(1),
1236
- }, async (payload) => {
1237
- if (mode.mode !== "paid" && localStore) {
1238
- const loginRequired = requireFreeIdentity();
1239
- if (loginRequired)
1240
- return loginRequired;
1241
- const signal = localStore.getSignal(Number(payload.id));
1242
- return signal
1243
- ? localResponse({ ok: true, tier: "free", signal: signalWithDecision(localStore, signal) })
1244
- : localToolError({
1245
- code: "not_found",
1246
- message: "Signal not found in the local Ask The W store.",
1247
- retryable: false,
1248
- hint: "Check the signal id or list local signals first.",
1249
- });
1250
- }
1251
- return apiToolResponse(`/api/signals/${encodeURIComponent(payload.id)}`);
1252
- });
1253
- server.tool("review_decisions", "Review captured decisions. Use for natural prompts like: What did I decide yesterday?", {
1254
- since: z.string().optional(),
1255
- status: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
1256
- format: z.enum(["markdown", "json"]).default("markdown"),
1257
- limit: z.number().int().positive().max(300).default(50),
1258
- cursor: cursorSchema,
1259
- sessionId: z.string().optional(),
1260
- compact: z.boolean().optional(),
1261
- max_chars: maxCharsSchema,
1262
- }, async (payload) => {
1263
- if (mode.mode !== "paid")
1264
- return localResponse(paidFeatureNudge("review_decisions"));
1265
- if (mode.mode === "paid") {
1266
- return apiToolResponse(routeWithQuery("/api/decisions", {
1267
- since: payload.since,
1268
- status: payload.status,
1269
- limit: payload.limit,
1270
- cursor: payload.cursor,
1271
- sessionId: payload.sessionId,
1272
- compact: payload.compact,
1273
- max_chars: payload.max_chars,
1274
- }));
1275
- }
1276
- return localResponse(paidFeatureNudge("review_decisions"));
1277
- });
1278
- server.tool("review_session", "Review the current session trail. Use for natural prompts like: Show me my session trail.", {
1279
- sessionId: z.string().optional(),
1280
- format: z.enum(["markdown", "json"]).default("markdown"),
1281
- cursor: cursorSchema,
1282
- limit: z.number().int().positive().max(50).default(50),
1283
- compact: z.boolean().optional(),
1284
- max_chars: maxCharsSchema,
1285
- }, async (payload) => {
1286
- if (mode.mode !== "paid")
1287
- return localResponse(paidFeatureNudge("review_session"));
1288
- if (!localStore || mode.mode === "paid") {
1289
- return apiToolResponse(routeWithQuery("/api/signals", {
1290
- sessionId: payload.sessionId,
1291
- cursor: payload.cursor,
1292
- limit: payload.limit,
1293
- compact: payload.compact,
1294
- max_chars: payload.max_chars,
1295
- }));
1296
- }
1297
- return localResponse(paidFeatureNudge("review_session"));
1298
- });
1299
- server.tool("analyze_session", "Analyze the latest local coding-agent session.", {
1300
- sessionId: z.string().optional(),
1301
- format: z.enum(["markdown", "json"]).default("markdown"),
1302
- cursor: cursorSchema,
1303
- limit: z.number().int().positive().max(50).default(50),
1304
- compact: z.boolean().optional(),
1305
- max_chars: maxCharsSchema,
1306
- }, async (payload) => {
1307
- if (!localStore || mode.mode === "paid") {
1308
- return apiToolResponse(routeWithQuery("/api/signals", {
1309
- sessionId: payload.sessionId,
1310
- cursor: payload.cursor,
1311
- limit: payload.limit,
1312
- compact: payload.compact,
1313
- max_chars: payload.max_chars,
1314
- }));
1315
- }
1316
- return localSessionAnalysisResponse(payload);
1317
- });
1318
- server.tool("recap", "Summarize the latest local coding-agent session as a digest, standup, or share-ready recap.", {
1001
+ server.tool("recap", "Get a concise recap of the latest local coding-agent session.", {
1319
1002
  format: z.enum(["digest", "standup", "share"]).default("digest"),
1320
1003
  sessionId: z.string().optional(),
1321
1004
  compact: z.boolean().optional(),
@@ -1326,6 +1009,9 @@ export function createAskTheWMcpServer(options = {}) {
1326
1009
  const loginRequired = requireFreeIdentity();
1327
1010
  if (loginRequired)
1328
1011
  return loginRequired;
1012
+ const limited = consumeLimitedLocalUse("recap");
1013
+ if (limited)
1014
+ return limited;
1329
1015
  const scopeKey = currentScopeKey();
1330
1016
  const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
1331
1017
  const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
@@ -1341,19 +1027,18 @@ export function createAskTheWMcpServer(options = {}) {
1341
1027
  : {}),
1342
1028
  }, payload.max_chars);
1343
1029
  });
1344
- server.tool("coach", "Coach the local coding-agent session. Use for natural prompts like: Coach me on this session.", {
1345
- scope: z.enum(["session", "week", "patterns"]).default("session"),
1030
+ server.tool("coach", "Get one concise coaching nudge for the local coding-agent session.", {
1346
1031
  sessionId: z.string().optional(),
1347
1032
  max_chars: maxCharsSchema,
1348
1033
  }, async (payload) => {
1349
- if (payload.scope === "week" || payload.scope === "patterns") {
1350
- return localResponse(paidFeatureNudge("coach"));
1351
- }
1352
1034
  if (!localStore || mode.mode === "paid")
1353
1035
  return localResponse(paidFeatureNudge("coach"));
1354
1036
  const loginRequired = requireFreeIdentity();
1355
1037
  if (loginRequired)
1356
1038
  return loginRequired;
1039
+ const limited = consumeLimitedLocalUse("coach");
1040
+ if (limited)
1041
+ return limited;
1357
1042
  const scopeKey = currentScopeKey();
1358
1043
  const sessionId = payload.sessionId ?? localStore.mostRecentSessionId({ scopeKey });
1359
1044
  const signals = sessionId ? localStore.listSignals({ sessionId, scopeKey, limit: 100000 }) : [];
@@ -1362,12 +1047,9 @@ export function createAskTheWMcpServer(options = {}) {
1362
1047
  return budgetedLocalResponse({
1363
1048
  ok: true,
1364
1049
  tier: "free",
1365
- scope: "session",
1366
1050
  sessionId,
1367
- qualityScore: coaching.qualityScore,
1368
- biggestGap: coaching.biggestGap,
1369
- failureMode: coaching.failureMode,
1370
- rendered: `Decision quality score: ${coaching.qualityScore}/100\nBiggest gap: ${coaching.biggestGap}`,
1051
+ nudge: coaching.nudge,
1052
+ rendered: coaching.nudge,
1371
1053
  }, payload.max_chars);
1372
1054
  });
1373
1055
  server.tool("promote_signal_to_decision", "Copy a captured signal summary into a linked local decision.", {
@@ -1401,6 +1083,21 @@ export function createAskTheWMcpServer(options = {}) {
1401
1083
  extra: { tool: "promote_signal_to_decision" },
1402
1084
  });
1403
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
+ }
1404
1101
  if (payload.idempotencyKey) {
1405
1102
  const existingId = localStore.getMeta(`idempotency:promote_signal_to_decision:${payload.idempotencyKey}`);
1406
1103
  if (existingId) {
@@ -1443,90 +1140,8 @@ export function createAskTheWMcpServer(options = {}) {
1443
1140
  warnings,
1444
1141
  });
1445
1142
  });
1446
- server.tool("list_decision_candidates", "List local signals that look like decision moments and can be promoted.", {
1447
- sessionId: z.string().optional(),
1448
- limit: z.number().int().positive().max(300).default(50),
1449
- cursor: cursorSchema,
1450
- max_chars: maxCharsSchema,
1451
- }, async (payload) => {
1452
- if (mode.mode !== "paid")
1453
- return localResponse(paidFeatureNudge("list_decision_candidates"));
1454
- return apiToolResponse(routeWithQuery("/api/signals/decision-candidates", {
1455
- sessionId: payload.sessionId,
1456
- limit: payload.limit,
1457
- cursor: payload.cursor,
1458
- max_chars: payload.max_chars,
1459
- }));
1460
- });
1461
- server.tool("search_trail", "Search local signals and decisions together.", {
1462
- query: z.string().min(1),
1463
- sessionId: z.string().optional(),
1464
- limit: z.number().int().positive().max(100).default(25),
1465
- cursor: cursorSchema,
1466
- compact: z.boolean().optional(),
1467
- max_chars: maxCharsSchema,
1468
- }, async (payload) => {
1469
- if (mode.mode !== "paid")
1470
- return localResponse(paidFeatureNudge("search_trail"));
1471
- return apiToolResponse(routeWithQuery("/api/search/trail", {
1472
- query: payload.query,
1473
- sessionId: payload.sessionId,
1474
- limit: payload.limit,
1475
- cursor: payload.cursor,
1476
- compact: payload.compact,
1477
- max_chars: payload.max_chars,
1478
- }));
1479
- });
1480
- server.tool("export_decisions", paidDescription("Export decisions from your workspace.", mode.mode), {
1481
- format: z.enum(["json", "markdown", "jsonl"]).default("json"),
1482
- cursor: cursorSchema,
1483
- limit: z.number().int().positive().max(300).optional(),
1484
- max_chars: maxCharsSchema,
1485
- }, async (payload) => mode.mode === "free"
1486
- ? localResponse(paidFeatureNudge("export_decisions"))
1487
- : apiToolResponse(routeWithQuery("/api/export/timeline", {
1488
- format: payload.format,
1489
- cursor: payload.cursor,
1490
- limit: payload.limit ?? 50,
1491
- max_chars: payload.max_chars ?? 8000,
1492
- })));
1493
- server.tool("view_timeline", "View signals and decisions counts bucketed by session, day, or month.", {
1494
- scope: z.enum(["day", "month", "session"]).default("day"),
1495
- range: z.enum(["7D", "30D", "90D", "12M", "CUSTOM"]).default("30D"),
1496
- start: z.string().optional(),
1497
- end: z.string().optional(),
1498
- limit: z.number().int().positive().max(300).optional(),
1499
- outcomeId: z.string().optional(),
1500
- decisionStatus: z.enum(["proposed", "committed", "shipped", "abandoned"]).optional(),
1501
- signalSource: z.string().optional(),
1502
- max_chars: maxCharsSchema,
1503
- }, async (payload) => {
1504
- if (mode.mode === "paid") {
1505
- return apiToolResponse(routeWithQuery("/api/analytics/timeline-counts", {
1506
- scope: payload.scope,
1507
- range: payload.range,
1508
- start: payload.start,
1509
- end: payload.end,
1510
- limit: payload.limit,
1511
- outcomeId: payload.outcomeId,
1512
- decisionStatus: payload.decisionStatus,
1513
- signalSource: payload.signalSource,
1514
- }));
1515
- }
1516
- return localResponse(paidFeatureNudge("view_timeline"));
1517
- });
1518
1143
  return server;
1519
1144
  }
1520
- function renderDecisionDigest(decisions) {
1521
- if (decisions.length === 0) {
1522
- return "# Decisions\n\nNo local decisions captured yet.";
1523
- }
1524
- return [
1525
- "# Decisions",
1526
- "",
1527
- ...decisions.map((decision) => [`## ${decision.headline}`, `- id: ${decision.id}`, `- status: ${decision.status}`, `- created: ${decision.createdAt}`, decision.why ? `- why: ${decision.why}` : "- why: not captured"].join("\n")),
1528
- ].join("\n\n");
1529
- }
1530
1145
  function buildSessionCoach(input) {
1531
1146
  const verificationCount = input.signals.filter((signal) => signal.kind === "verification_result").length;
1532
1147
  const implementationCount = input.signals.filter((signal) => signal.kind === "implementation_update").length;
@@ -1536,61 +1151,20 @@ function buildSessionCoach(input) {
1536
1151
  const hasVerification = verificationCount > 0;
1537
1152
  const hasDecision = decisionCount > 0;
1538
1153
  const hasDirection = directionCount > 0;
1539
- const qualityScore = Math.max(0, Math.min(100, 35 +
1540
- Math.min(25, decisionCount * 12) +
1541
- (hasVerification ? 25 : 0) +
1542
- (hasDirection ? 10 : 0) -
1543
- Math.max(0, implementationCount - decisionCount) * 3));
1544
- const failureMode = implementationCount >= 3 && verificationCount === 0
1154
+ const nudge = implementationCount >= 3 && verificationCount === 0
1545
1155
  ? `You captured ${implementationCount} implementation updates but no verification_result; run one check and capture it before ending.`
1546
1156
  : input.signals.length >= 6 && finalSummaryCount === 0
1547
1157
  ? `You captured ${input.signals.length} signals but no final_summary; close the session with the outcome and remaining risk.`
1548
1158
  : decisionCount === 0 && directionCount > 0
1549
1159
  ? "Direction changed, but no decision was captured; promote the clearest direction_change signal."
1550
- : null;
1551
- const biggestGap = failureMode ?? (!hasDecision
1552
- ? "Promote the clearest captured signal into a decision before the trail goes stale."
1553
- : !hasVerification
1554
- ? "Capture one verification result so the decision trail records whether the work actually held."
1555
- : implementationCount > decisionCount * 3
1556
- ? "There are many implementation updates per decision; collapse the important why into one decision."
1557
- : "The trail is usable. Keep the next decision tied to a verification result.");
1558
- return { qualityScore, biggestGap, failureMode };
1559
- }
1560
- function renderSessionMarkdown(input) {
1561
- const counts = input.signals.reduce((accumulator, signal) => {
1562
- accumulator[signal.kind] = (accumulator[signal.kind] ?? 0) + 1;
1563
- return accumulator;
1564
- }, {});
1565
- const activeDecisions = input.decisions.filter((decision) => decision.status !== "abandoned");
1566
- const coaching = buildSessionCoach({
1567
- signals: input.signals.map((signal) => ({ ...signal, filesTouched: [], commandsRun: [] })),
1568
- decisions: input.decisions,
1569
- });
1570
- const lines = [
1571
- "# Session Review",
1572
- `Session: ${input.sessionId ?? "none"}`,
1573
- `Signals: ${input.signals.length}`,
1574
- `Signal kinds: ${Object.entries(counts).map(([kind, count]) => `${kind} ${count}`).join(", ") || "none"}`,
1575
- `Active decisions: ${activeDecisions.length}`,
1576
- `Coaching tip: ${coaching.biggestGap}`,
1577
- "",
1578
- "## Signals By Kind",
1579
- ...Object.entries(counts)
1580
- .sort(([left], [right]) => left.localeCompare(right))
1581
- .map(([kind, count]) => `- ${kind}: ${count}`),
1582
- "",
1583
- "## Decision Candidates",
1584
- ...(input.decisionCandidates?.length
1585
- ? input.decisionCandidates.slice(0, 10).map((candidate) => `- signal ${candidate.signalId}: ${candidate.summary} (${candidate.suggestedStatus})`)
1586
- : ["- none"]),
1587
- "",
1588
- "## Active Decisions",
1589
- ...(activeDecisions.length
1590
- ? activeDecisions.map((decision) => `- ${decision.headline} (${decision.status})${decision.why ? ` - ${decision.why}` : ""}`)
1591
- : ["- none"]),
1592
- ];
1593
- return lines.slice(0, 300).join("\n");
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 };
1594
1168
  }
1595
1169
  function decisionalWeight(signal) {
1596
1170
  const kindWeight = signal.kind === "direction_change" ? 4 : signal.kind === "verification_result" ? 3 : signal.kind === "implementation_update" ? 2 : 1;
@@ -1709,21 +1283,6 @@ function candidateFromSignal(signal, linkedDecision) {
1709
1283
  capturedAt: signal.capturedAt,
1710
1284
  };
1711
1285
  }
1712
- function listDecisionCandidates(input) {
1713
- const signals = input.store.listSignals({
1714
- sessionId: input.sessionId ?? undefined,
1715
- scopeKey: input.scopeKey,
1716
- cursor: input.cursor,
1717
- limit: Math.max(1, Math.min(300, input.limit ?? 50)),
1718
- });
1719
- const candidates = signals
1720
- .map((signal) => candidateFromSignal(signal, input.store.getDecisionForSignal(signal.id)))
1721
- .filter((candidate) => Boolean(candidate));
1722
- return {
1723
- candidates,
1724
- nextCursor: signals.length >= (input.limit ?? 50) ? signals.at(-1)?.capturedAt ?? null : null,
1725
- };
1726
- }
1727
1286
  function normalizedDecisionTerms(text) {
1728
1287
  const stop = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "keep", "use", "adopt", "remove", "drop", "replace", "defer", "ship", "commit"]);
1729
1288
  return String(text ?? "")
@@ -1768,55 +1327,6 @@ function detectDecisionConflicts(input) {
1768
1327
  overlappingTerms: entry.overlap.slice(0, 5),
1769
1328
  }));
1770
1329
  }
1771
- function searchTrail(input) {
1772
- const terms = String(input.query ?? "")
1773
- .toLowerCase()
1774
- .split(/\s+/)
1775
- .filter(Boolean);
1776
- const limit = Math.max(1, Math.min(100, input.limit ?? 25));
1777
- const haystackMatches = (value) => terms.every((term) => value.toLowerCase().includes(term));
1778
- const signals = input.store
1779
- .listSignals({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
1780
- .filter((signal) => haystackMatches([
1781
- signal.summary,
1782
- signal.kind,
1783
- signal.filesTouched.join(" "),
1784
- signal.commandsRun.join(" "),
1785
- JSON.stringify(signal.evidence),
1786
- JSON.stringify(signal.metadata),
1787
- ].join(" ")))
1788
- .map((signal) => ({
1789
- type: "signal",
1790
- id: String(signal.id),
1791
- createdAt: signal.capturedAt,
1792
- score: terms.length,
1793
- result: input.compact ? compactSignal(signal, input.store.getDecisionForSignal(signal.id)) : signalWithDecision(input.store, signal),
1794
- }));
1795
- const decisions = input.store
1796
- .listDecisions({ scopeKey: input.scopeKey, sessionId: input.sessionId ?? undefined, cursor: input.cursor, limit: 100000 })
1797
- .filter((decision) => haystackMatches([decision.headline, decision.why ?? "", decision.rawContent, decision.files.join(" "), decision.status].join(" ")))
1798
- .map((decision) => ({
1799
- type: "decision",
1800
- id: decision.id,
1801
- createdAt: decision.createdAt,
1802
- score: terms.length,
1803
- result: input.compact
1804
- ? {
1805
- id: decision.id,
1806
- headline: decision.headline,
1807
- status: decision.status,
1808
- signalIds: decision.sourceSignalIds,
1809
- }
1810
- : decisionWithSignals(input.store, decision),
1811
- }));
1812
- const matches = [...signals, ...decisions]
1813
- .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
1814
- .slice(0, limit);
1815
- return {
1816
- matches,
1817
- nextCursor: matches.length >= limit ? matches.at(-1)?.createdAt ?? null : null,
1818
- };
1819
- }
1820
1330
  const isDirectIndexExecution = Boolean(process.argv[1]) &&
1821
1331
  (() => {
1822
1332
  const modulePath = fileURLToPath(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askthew/mcp-plugin",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "private": false,
5
5
  "description": "Ask The W plugin connector for local-first coding-agent decisions, signals, and review.",
6
6
  "type": "module",