@askthew/mcp-plugin 0.4.10 → 0.4.12

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.
@@ -1,155 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { resolvePluginScope } from "../scope.js";
3
- import { isTelemetryOptedOut } from "./free-tier-policy.js";
4
- import { tryRegisterFreeInstall } from "./free-install-registration.js";
5
- import { signLocalIdentityPayload } from "./local-identity.js";
6
- export function buildTelemetryPayload(input) {
7
- const now = input.now ?? new Date();
8
- const sessionId = input.sessionId ?? input.store.mostRecentSessionId() ?? "unknown";
9
- const signals = input.store.listSignals({ sessionId, limit: 100000 });
10
- const decisions = input.store.listDecisions({ limit: 100000 });
11
- const stack = detectStack(input.cwd ?? process.cwd());
12
- return stripTelemetry({
13
- schemaVersion: 1,
14
- userId: input.credentials.userId,
15
- cliTokenId: input.credentials.cliTokenId,
16
- sentAt: now.toISOString(),
17
- session: {
18
- id: hashSessionId(input.credentials.userId, sessionId),
19
- startedAt: signals[0]?.capturedAt ?? now.toISOString(),
20
- endedAt: signals.at(-1)?.capturedAt ?? now.toISOString(),
21
- durationMs: durationMs(signals),
22
- hostType: String(signals.at(-1)?.metadata.hostType ?? signals.at(-1)?.metadata.host_type ?? "unknown"),
23
- clientId: String(signals.at(-1)?.metadata.client_id ?? "unknown"),
24
- },
25
- stack,
26
- activity: activityStats(signals),
27
- decisions: decisionStats(decisions),
28
- tools: input.toolUsage ?? {},
29
- plugin: {
30
- version: "0.4.7",
31
- platform: `${process.platform}-${process.arch}`,
32
- node: process.version.replace(/^v/, ""),
33
- },
34
- identity: {
35
- kind: "local_install",
36
- installId: input.credentials.installId,
37
- emailClaimed: Boolean(input.credentials.email),
38
- },
39
- });
40
- }
41
- export async function flushTelemetryOutbox(input) {
42
- if (isTelemetryOptedOut(input.env, input.credentials)) {
43
- return { ok: true, skipped: true, sent: 0 };
44
- }
45
- const fetcher = input.fetchImpl ?? fetch;
46
- const apiUrl = (input.apiUrl ?? input.credentials.apiUrl ?? process.env.ASKTHEW_API_URL ?? "https://app.askthew.com").replace(/\/$/, "");
47
- if (input.credentials.localIdentity) {
48
- await tryRegisterFreeInstall({
49
- identity: input.credentials.localIdentity,
50
- deviceLabel: "askthew-mcp",
51
- options: { apiUrl, fetchImpl: fetcher },
52
- });
53
- }
54
- let sent = 0;
55
- for (const row of input.store.listTelemetryOutbox({ undeliveredOnly: true, limit: 20 })) {
56
- const body = JSON.stringify(row.payload);
57
- if (!input.credentials.localIdentity) {
58
- input.store.markTelemetryAttempt(row.id, false);
59
- continue;
60
- }
61
- const signed = signLocalIdentityPayload({ identity: input.credentials.localIdentity, body });
62
- const response = await fetcher(`${apiUrl}/api/cli/v1/telemetry`, {
63
- method: "POST",
64
- headers: {
65
- "Content-Type": "application/json",
66
- "X-AskTheW-Install-Id": input.credentials.localIdentity.installId,
67
- "X-AskTheW-Timestamp": signed.timestamp,
68
- "X-AskTheW-Signature": signed.signature,
69
- },
70
- body,
71
- }).catch(() => null);
72
- const delivered = Boolean(response?.ok);
73
- input.store.markTelemetryAttempt(row.id, delivered);
74
- if (delivered) {
75
- sent += 1;
76
- }
77
- }
78
- return { ok: true, sent };
79
- }
80
- function hashSessionId(userId, sessionId) {
81
- return crypto.createHash("sha256").update(`${userId}:${sessionId}`).digest("hex");
82
- }
83
- function durationMs(signals) {
84
- if (signals.length < 2) {
85
- return 0;
86
- }
87
- return Math.max(0, new Date(signals.at(-1).capturedAt).getTime() - new Date(signals[0].capturedAt).getTime());
88
- }
89
- function activityStats(signals) {
90
- const counts = {};
91
- let pass = 0;
92
- let fail = 0;
93
- let unknown = 0;
94
- for (const signal of signals) {
95
- counts[signal.kind] = (counts[signal.kind] ?? 0) + 1;
96
- if (signal.kind === "verification_result") {
97
- if (/pass|green|ok|success/i.test(signal.summary)) {
98
- pass += 1;
99
- }
100
- else if (/fail|red|error|broken|timeout/i.test(signal.summary)) {
101
- fail += 1;
102
- }
103
- else {
104
- unknown += 1;
105
- }
106
- }
107
- }
108
- return {
109
- signalCount: signals.length,
110
- signalCountsByKind: counts,
111
- filesTouchedCount: new Set(signals.flatMap((signal) => signal.filesTouched.map((file) => extensionOnly(file)))).size,
112
- commandsRunCount: signals.reduce((total, signal) => total + signal.commandsRun.length, 0),
113
- verification: { pass, fail, unknown },
114
- };
115
- }
116
- function decisionStats(decisions) {
117
- const byStatus = {};
118
- for (const decision of decisions) {
119
- byStatus[decision.status] = (byStatus[decision.status] ?? 0) + 1;
120
- }
121
- return {
122
- createdCount: decisions.length,
123
- byStatus,
124
- };
125
- }
126
- function detectStack(cwd) {
127
- const scope = resolvePluginScope(cwd);
128
- const packageManagers = scope.repoName ? ["npm"] : [];
129
- return {
130
- languages: ["typescript"],
131
- extensions: [".ts", ".tsx", ".sql"],
132
- frameworks: ["next", "supabase"].filter(Boolean),
133
- packageManagers,
134
- monorepo: scope.appPath ? "turborepo" : "unknown",
135
- };
136
- }
137
- function extensionOnly(file) {
138
- const match = file.match(/(\.[A-Za-z0-9]+)$/);
139
- return match?.[1] ?? "";
140
- }
141
- function stripTelemetry(value) {
142
- if (typeof value === "string") {
143
- if (value.includes("/") && !value.startsWith(".")) {
144
- return "[redacted]";
145
- }
146
- return value;
147
- }
148
- if (Array.isArray(value)) {
149
- return value.map(stripTelemetry);
150
- }
151
- if (value && typeof value === "object") {
152
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, stripTelemetry(entry)]));
153
- }
154
- return value;
155
- }
@@ -1,23 +0,0 @@
1
- import type { LocalDecision, LocalSignal } from "./local-store.js";
2
- export interface TimelinePoint {
3
- x: string;
4
- signalCount: number;
5
- decisionCount: number;
6
- startedAt?: string;
7
- endedAt?: string;
8
- durationMinutes?: number;
9
- }
10
- export interface TimelineInsight {
11
- id: string;
12
- type: "trend" | "recommendation";
13
- title: string;
14
- body: string;
15
- }
16
- export declare function buildLocalTimeline(input: {
17
- scope: "day" | "month" | "session";
18
- signals: LocalSignal[];
19
- decisions: LocalDecision[];
20
- limit?: number;
21
- }): TimelinePoint[];
22
- export declare function buildTimelineInsights(points: TimelinePoint[]): TimelineInsight[];
23
- export declare function renderTimelineMarkdown(points: TimelinePoint[], scope: "day" | "month" | "session"): string;
@@ -1,115 +0,0 @@
1
- export function buildLocalTimeline(input) {
2
- if (input.scope === "session")
3
- return buildSessionTimeline(input);
4
- const buckets = new Map();
5
- for (const signal of input.signals) {
6
- const x = input.scope === "month" ? signal.capturedAt.slice(0, 7) : signal.capturedAt.slice(0, 10);
7
- const point = buckets.get(x) ?? { x, signalCount: 0, decisionCount: 0 };
8
- point.signalCount += 1;
9
- buckets.set(x, point);
10
- }
11
- for (const decision of input.decisions) {
12
- const x = input.scope === "month" ? decision.createdAt.slice(0, 7) : decision.createdAt.slice(0, 10);
13
- const point = buckets.get(x) ?? { x, signalCount: 0, decisionCount: 0 };
14
- point.decisionCount += 1;
15
- buckets.set(x, point);
16
- }
17
- return [...buckets.values()].sort((left, right) => left.x.localeCompare(right.x));
18
- }
19
- export function buildTimelineInsights(points) {
20
- const totalSignals = points.reduce((sum, point) => sum + point.signalCount, 0);
21
- const totalDecisions = points.reduce((sum, point) => sum + point.decisionCount, 0);
22
- const insights = [
23
- {
24
- id: "timeline.activity_summary",
25
- type: "trend",
26
- title: "Timeline activity summarized",
27
- body: `This period includes ${totalSignals} signals and ${totalDecisions} decisions.`,
28
- },
29
- ];
30
- if (totalSignals >= 20 && (totalDecisions === 0 || totalSignals / Math.max(1, totalDecisions) >= 8)) {
31
- insights.push({
32
- id: "timeline.signal_to_decision_imbalance",
33
- type: "recommendation",
34
- title: "Signals are far ahead of decisions",
35
- body: `${totalSignals} signals produced ${totalDecisions} decisions. Promote the strongest evidence into decisions before the trail gets noisy.`,
36
- });
37
- }
38
- else {
39
- insights.push({
40
- id: "timeline.review_next_bucket",
41
- type: "recommendation",
42
- title: "Review the next decision bucket",
43
- body: totalDecisions > 0
44
- ? "Open the busiest bucket and check whether the decisions still describe the current plan."
45
- : "Capture one decision for the most important change before the next review.",
46
- });
47
- }
48
- const maxDecision = Math.max(0, ...points.map((point) => point.decisionCount));
49
- const averageDecision = points.reduce((sum, point) => sum + point.decisionCount, 0) / Math.max(1, points.length);
50
- if (maxDecision >= 4 && maxDecision >= averageDecision * 2.5) {
51
- insights.push({
52
- id: "timeline.decision_spike",
53
- type: "trend",
54
- title: "Decision spike detected",
55
- body: `One bucket captured ${maxDecision} decisions, well above the period average of ${averageDecision.toFixed(1)}.`,
56
- });
57
- }
58
- return insights.slice(0, 6);
59
- }
60
- export function renderTimelineMarkdown(points, scope) {
61
- const label = scope === "session" ? "Session" : scope === "month" ? "Month" : "Date";
62
- return [`| ${label} | Signals | Decisions |`, "|---|---:|---:|", ...points.map((point) => `| ${point.x} | ${point.signalCount} | ${point.decisionCount} |`)].join("\n");
63
- }
64
- function buildSessionTimeline(input) {
65
- const sessions = new Map();
66
- for (const signal of input.signals) {
67
- const existing = sessions.get(signal.sessionId) ?? {
68
- x: signal.sessionId,
69
- signalCount: 0,
70
- decisionCount: 0,
71
- startedAt: signal.capturedAt,
72
- endedAt: signal.capturedAt,
73
- durationMinutes: 0,
74
- };
75
- existing.signalCount += 1;
76
- existing.startedAt = minIso(existing.startedAt, signal.capturedAt);
77
- existing.endedAt = maxIso(existing.endedAt, signal.capturedAt);
78
- existing.durationMinutes = durationMinutes(existing.startedAt, existing.endedAt);
79
- sessions.set(signal.sessionId, existing);
80
- }
81
- for (const decision of input.decisions) {
82
- const sessionId = decision.sessionId;
83
- if (!sessionId) {
84
- continue;
85
- }
86
- const existing = sessions.get(sessionId) ?? {
87
- x: sessionId,
88
- signalCount: 0,
89
- decisionCount: 0,
90
- startedAt: decision.createdAt,
91
- endedAt: decision.createdAt,
92
- durationMinutes: 0,
93
- };
94
- existing.decisionCount += 1;
95
- sessions.set(sessionId, existing);
96
- }
97
- return [...sessions.values()]
98
- .sort((left, right) => String(right.endedAt ?? "").localeCompare(String(left.endedAt ?? "")))
99
- .slice(0, input.limit ?? 50);
100
- }
101
- function minIso(left, right) {
102
- if (!left)
103
- return right;
104
- return left <= right ? left : right;
105
- }
106
- function maxIso(left, right) {
107
- if (!left)
108
- return right;
109
- return left >= right ? left : right;
110
- }
111
- function durationMinutes(start, end) {
112
- if (!start || !end)
113
- return 0;
114
- return Math.max(0, Math.round((new Date(end).getTime() - new Date(start).getTime()) / 60000));
115
- }
@@ -1,18 +0,0 @@
1
- import type { LocalDecision, LocalSignal } from "./local-store.js";
2
- export type TipSeverity = "high" | "medium" | "low";
3
- export interface SessionTip {
4
- id: string;
5
- severity: TipSeverity;
6
- tip: string;
7
- alternative: string;
8
- evidence: string[];
9
- }
10
- export interface TipEngineInput {
11
- signals: LocalSignal[];
12
- decisions: LocalDecision[];
13
- now?: Date;
14
- }
15
- type Pattern = (input: TipEngineInput) => SessionTip | null;
16
- export declare function analyzeLocalPatterns(input: TipEngineInput): SessionTip[];
17
- export declare const TIP_PATTERNS: Pattern[];
18
- export {};
@@ -1,237 +0,0 @@
1
- const severityRank = {
2
- high: 0,
3
- medium: 1,
4
- low: 2,
5
- };
6
- export function analyzeLocalPatterns(input) {
7
- const tips = TIP_PATTERNS.map((pattern) => pattern(input)).filter((tip) => Boolean(tip));
8
- return tips
9
- .sort((left, right) => severityRank[left.severity] - severityRank[right.severity] || left.id.localeCompare(right.id))
10
- .slice(0, 3);
11
- }
12
- export const TIP_PATTERNS = [
13
- highProposedStagnation,
14
- verificationFailClusters,
15
- highChurnNoDecision,
16
- directionThrash,
17
- unboundedDecision,
18
- lateNightDecisions,
19
- singleFileObsession,
20
- verificationMissing,
21
- implementationWithoutSummary,
22
- repeatedDirectionChanges,
23
- abandonedSpike,
24
- noRecentDecisionWhy,
25
- longSessionNoCheckpoint,
26
- setupOnlySession,
27
- commandsHeavySession,
28
- filesWithoutSignals,
29
- ];
30
- function highProposedStagnation({ decisions, now = new Date() }) {
31
- const cutoff = now.getTime() - 3 * 24 * 60 * 60 * 1000;
32
- const stuck = decisions.filter((decision) => decision.status === "proposed" && new Date(decision.createdAt).getTime() < cutoff);
33
- if (stuck.length < 3) {
34
- return null;
35
- }
36
- return {
37
- id: "high_proposed_stagnation",
38
- severity: "high",
39
- tip: `${stuck.length} decisions are still proposed after more than 3 days. Block 30 minutes to commit or abandon the oldest ones.`,
40
- alternative: "Timebox each new decision with an explicit deadline in `why`.",
41
- evidence: stuck.slice(0, 5).map((decision) => decision.id),
42
- };
43
- }
44
- function verificationFailClusters({ signals }) {
45
- const verification = signals.filter((signal) => signal.kind === "verification_result").slice(-8);
46
- const failures = verification.filter((signal) => /fail|failed|error|red|broken|timeout/i.test(signal.summary));
47
- if (verification.length < 4 || failures.length < 3) {
48
- return null;
49
- }
50
- return {
51
- id: "verification_fail_clusters",
52
- severity: "medium",
53
- tip: `${failures.length} recent verifications failed. The fix is rarely the next blind attempt.`,
54
- alternative: "Pause, write what specifically failed in a `direction_change` signal, then run a smaller test before the full suite.",
55
- evidence: failures.slice(0, 5).map((signal) => `s_${signal.id}`),
56
- };
57
- }
58
- function highChurnNoDecision({ signals, decisions }) {
59
- const files = new Set(signals.flatMap((signal) => signal.filesTouched));
60
- if (files.size < 12 || decisions.length > 0) {
61
- return null;
62
- }
63
- return {
64
- id: "high_churn_no_decision",
65
- severity: "medium",
66
- tip: `You touched ${files.size} files but logged 0 decisions in this window.`,
67
- alternative: "Capture a single decision for the largest change - even one line of `why` beats none.",
68
- evidence: [],
69
- };
70
- }
71
- function directionThrash({ signals }) {
72
- const changes = signals.filter((signal) => signal.kind === "direction_change");
73
- if (changes.length < 4) {
74
- return null;
75
- }
76
- return {
77
- id: "direction_thrash",
78
- severity: "high",
79
- tip: `${changes.length} direction changes landed in one window. You may be switching approaches faster than evidence can settle.`,
80
- alternative: "Pick one reversible path, write the stop condition, and only change course when that condition is met.",
81
- evidence: changes.slice(0, 5).map((signal) => `s_${signal.id}`),
82
- };
83
- }
84
- function unboundedDecision({ decisions }) {
85
- const weak = decisions.filter((decision) => !decision.why || decision.why.trim().length < 12);
86
- if (weak.length < 2) {
87
- return null;
88
- }
89
- return {
90
- id: "unbounded_decision",
91
- severity: "medium",
92
- tip: `${weak.length} decisions are missing a useful why, so future you will have to reconstruct the reason.`,
93
- alternative: "Add one sentence: what tradeoff made this worth doing now?",
94
- evidence: weak.slice(0, 5).map((decision) => decision.id),
95
- };
96
- }
97
- function lateNightDecisions({ decisions }) {
98
- const late = decisions.filter((decision) => {
99
- const hour = new Date(decision.createdAt).getHours();
100
- return hour <= 5 || hour >= 23;
101
- });
102
- if (late.length < 2) {
103
- return null;
104
- }
105
- return {
106
- id: "late_night_decisions",
107
- severity: "low",
108
- tip: `${late.length} decisions were logged late at night. That can be fine, but reread them when you're fresh.`,
109
- alternative: "Mark late decisions as proposed first, then commit them after a daylight pass.",
110
- evidence: late.slice(0, 5).map((decision) => decision.id),
111
- };
112
- }
113
- function singleFileObsession({ signals }) {
114
- const counts = new Map();
115
- for (const signal of signals) {
116
- for (const file of signal.filesTouched) {
117
- counts.set(file, (counts.get(file) ?? 0) + 1);
118
- }
119
- }
120
- const [file, count] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? [];
121
- if (!file || count < 6) {
122
- return null;
123
- }
124
- return {
125
- id: "single_file_obsession",
126
- severity: "low",
127
- tip: `One file came up in ${count} signals. It may be carrying too much responsibility.`,
128
- alternative: "Before editing it again, name the responsibility you expect this file to own.",
129
- evidence: [],
130
- };
131
- }
132
- function verificationMissing({ signals }) {
133
- const implementation = signals.filter((signal) => signal.kind === "implementation_update");
134
- const verification = signals.filter((signal) => signal.kind === "verification_result");
135
- if (implementation.length < 3 || verification.length > 0) {
136
- return null;
137
- }
138
- return {
139
- id: "verification_missing",
140
- severity: "medium",
141
- tip: `${implementation.length} implementation updates landed without a verification result.`,
142
- alternative: "Run the smallest relevant check now and capture the result, even if it fails.",
143
- evidence: implementation.slice(0, 5).map((signal) => `s_${signal.id}`),
144
- };
145
- }
146
- function implementationWithoutSummary({ signals }) {
147
- const terse = signals.filter((signal) => signal.kind === "implementation_update" && signal.summary.length < 30);
148
- if (terse.length < 3) {
149
- return null;
150
- }
151
- return {
152
- id: "terse_implementation_updates",
153
- severity: "low",
154
- tip: `${terse.length} implementation updates are too terse to be useful later.`,
155
- alternative: "Capture the change plus the reason in one sentence.",
156
- evidence: terse.slice(0, 5).map((signal) => `s_${signal.id}`),
157
- };
158
- }
159
- function repeatedDirectionChanges(input) {
160
- return directionThrash(input);
161
- }
162
- function abandonedSpike({ decisions }) {
163
- const abandoned = decisions.filter((decision) => decision.status === "abandoned");
164
- if (abandoned.length < 3) {
165
- return null;
166
- }
167
- return {
168
- id: "abandoned_spike",
169
- severity: "medium",
170
- tip: `${abandoned.length} decisions were abandoned. That is useful learning, but only if the reason is captured.`,
171
- alternative: "Add the rejection reason to each abandoned decision before moving on.",
172
- evidence: abandoned.slice(0, 5).map((decision) => decision.id),
173
- };
174
- }
175
- function noRecentDecisionWhy({ decisions }) {
176
- const recent = decisions.slice(0, 5);
177
- if (recent.length < 3 || recent.some((decision) => (decision.why ?? "").trim().length > 20)) {
178
- return null;
179
- }
180
- return {
181
- id: "no_recent_decision_why",
182
- severity: "medium",
183
- tip: "Your recent decisions have headlines, but not enough reasoning to explain the tradeoff.",
184
- alternative: "For the next decision, write why the rejected option lost.",
185
- evidence: recent.map((decision) => decision.id),
186
- };
187
- }
188
- function longSessionNoCheckpoint({ signals }) {
189
- if (signals.length < 10 || signals.some((signal) => signal.kind === "session_checkpoint")) {
190
- return null;
191
- }
192
- return {
193
- id: "long_session_no_checkpoint",
194
- severity: "low",
195
- tip: `${signals.length} signals landed without a checkpoint. The session may be hard to review later.`,
196
- alternative: "Capture one checkpoint after each meaningful block of work.",
197
- evidence: [],
198
- };
199
- }
200
- function setupOnlySession({ signals }) {
201
- if (signals.length !== 1 || signals[0]?.kind !== "setup_complete") {
202
- return null;
203
- }
204
- return {
205
- id: "setup_only_session",
206
- severity: "low",
207
- tip: "This session only has startup captured so far.",
208
- alternative: "Capture a checkpoint after the first meaningful decision or verification.",
209
- evidence: signals[0] ? [`s_${signals[0].id}`] : [],
210
- };
211
- }
212
- function commandsHeavySession({ signals }) {
213
- const commands = signals.reduce((total, signal) => total + signal.commandsRun.length, 0);
214
- if (commands < 15 || signals.some((signal) => signal.kind === "direction_change")) {
215
- return null;
216
- }
217
- return {
218
- id: "commands_heavy_session",
219
- severity: "low",
220
- tip: `${commands} commands ran without a captured direction change. Make sure the command loop still has a clear target.`,
221
- alternative: "Write the next expected result before running another broad command.",
222
- evidence: [],
223
- };
224
- }
225
- function filesWithoutSignals({ signals }) {
226
- const files = new Set(signals.flatMap((signal) => signal.filesTouched));
227
- if (files.size < 8 || signals.length >= 5) {
228
- return null;
229
- }
230
- return {
231
- id: "files_without_signals",
232
- severity: "low",
233
- tip: `${files.size} files appeared across only ${signals.length} signals. The capture trail is thin for the amount of movement.`,
234
- alternative: "Add a checkpoint before the next edit so the review has a clear midpoint.",
235
- evidence: [],
236
- };
237
- }
@@ -1,19 +0,0 @@
1
- export declare const PRICING_URL = "https://askthew.com/pricing";
2
- export declare const SUPPORT_EMAIL = "support@askthew.com";
3
- export declare function paidDescription(description: string, mode: "paid" | "free" | "free_pending_auth" | "unauthenticated"): string;
4
- export declare function paidFeatureNudge(tool: string): {
5
- ok: boolean;
6
- code: string;
7
- tool: string;
8
- message: string;
9
- pricingUrl: string;
10
- upgradeUrl: string;
11
- supportEmail: string;
12
- cta: string;
13
- };
14
- export declare function toolJson(value: unknown): {
15
- content: {
16
- type: "text";
17
- text: string;
18
- }[];
19
- };
@@ -1,37 +0,0 @@
1
- export const PRICING_URL = "https://askthew.com/pricing";
2
- export const SUPPORT_EMAIL = "support@askthew.com";
3
- export function paidDescription(description, mode) {
4
- if (mode !== "free") {
5
- return description;
6
- }
7
- return `${description} (paid - ${PRICING_URL})`;
8
- }
9
- export function paidFeatureNudge(tool) {
10
- const feature = tool.includes("outcome")
11
- ? { label: "Outcomes", verb: "are" }
12
- : tool.includes("north_star")
13
- ? { label: "North star", verb: "is" }
14
- : tool.includes("export")
15
- ? { label: "Exports", verb: "are" }
16
- : { label: "This workspace feature", verb: "is" };
17
- return {
18
- ok: false,
19
- code: "free_tier_paid_feature",
20
- tool,
21
- message: `${feature.label} ${feature.verb} a paid feature. See ${PRICING_URL}.`,
22
- pricingUrl: PRICING_URL,
23
- upgradeUrl: `https://askthew.com/plugin?utm_source=mcp-plugin&utm_medium=tool-nudge&utm_campaign=mcp-free&tool=${encodeURIComponent(tool)}`,
24
- supportEmail: SUPPORT_EMAIL,
25
- cta: "Run: npx -y --prefer-online --package @askthew/mcp-plugin@latest askthew-mcp upgrade --browser",
26
- };
27
- }
28
- export function toolJson(value) {
29
- return {
30
- content: [
31
- {
32
- type: "text",
33
- text: JSON.stringify(value, null, 2),
34
- },
35
- ],
36
- };
37
- }
@@ -1,38 +0,0 @@
1
- import type { CliCredentials } from "./free-tier-policy.js";
2
- import type { LocalStore } from "./local-store.js";
3
- export declare function syncDryRun(store: LocalStore): {
4
- pending: {
5
- signals: number;
6
- decisions: number;
7
- };
8
- alreadyUploaded: {
9
- signals: number;
10
- decisions: number;
11
- };
12
- };
13
- export declare function uploadLocalStore(input: {
14
- store: LocalStore;
15
- credentials: CliCredentials;
16
- syncToken?: string;
17
- apiUrl?: string;
18
- fetchImpl?: typeof fetch;
19
- dryRun?: boolean;
20
- }): Promise<{
21
- pending: {
22
- signals: number;
23
- decisions: number;
24
- };
25
- alreadyUploaded: {
26
- signals: number;
27
- decisions: number;
28
- };
29
- ok: boolean;
30
- dryRun: boolean;
31
- uploaded?: undefined;
32
- } | {
33
- ok: boolean;
34
- uploaded: {
35
- signals: number;
36
- decisions: number;
37
- };
38
- }>;