@martinloop/mcp 0.3.8 → 0.3.9

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 (103) hide show
  1. package/README.md +148 -105
  2. package/dist/resources.js +148 -148
  3. package/dist/server.js +0 -0
  4. package/dist/tools/preflight.d.ts +0 -2
  5. package/dist/tools/preflight.js +2 -42
  6. package/dist/vendor/adapters/openai-compatible.js +6 -6
  7. package/dist/vendor/contracts/index.d.ts +0 -2
  8. package/dist/vendor/contracts/index.js +0 -2
  9. package/dist/vendor/core/index.d.ts +0 -2
  10. package/dist/vendor/core/index.js +0 -2
  11. package/package.json +86 -83
  12. package/server.json +21 -21
  13. package/LICENSE +0 -216
  14. package/dist/vendor/adapters/adapters/src/claude-cli.d.ts +0 -146
  15. package/dist/vendor/adapters/adapters/src/claude-cli.js +0 -1100
  16. package/dist/vendor/adapters/adapters/src/cli-bridge.d.ts +0 -103
  17. package/dist/vendor/adapters/adapters/src/cli-bridge.js +0 -569
  18. package/dist/vendor/adapters/adapters/src/codex-launcher.d.ts +0 -78
  19. package/dist/vendor/adapters/adapters/src/codex-launcher.js +0 -663
  20. package/dist/vendor/adapters/adapters/src/direct-provider.d.ts +0 -10
  21. package/dist/vendor/adapters/adapters/src/direct-provider.js +0 -40
  22. package/dist/vendor/adapters/adapters/src/index.d.ts +0 -8
  23. package/dist/vendor/adapters/adapters/src/index.js +0 -8
  24. package/dist/vendor/adapters/adapters/src/openai-compatible.d.ts +0 -62
  25. package/dist/vendor/adapters/adapters/src/openai-compatible.js +0 -306
  26. package/dist/vendor/adapters/adapters/src/runtime-support.d.ts +0 -17
  27. package/dist/vendor/adapters/adapters/src/runtime-support.js +0 -59
  28. package/dist/vendor/adapters/adapters/src/stub-agent-cli.d.ts +0 -8
  29. package/dist/vendor/adapters/adapters/src/stub-agent-cli.js +0 -40
  30. package/dist/vendor/adapters/adapters/src/stub-direct-provider.d.ts +0 -8
  31. package/dist/vendor/adapters/adapters/src/stub-direct-provider.js +0 -12
  32. package/dist/vendor/adapters/adapters/src/verifier-only.d.ts +0 -9
  33. package/dist/vendor/adapters/adapters/src/verifier-only.js +0 -63
  34. package/dist/vendor/adapters/contracts/src/context-graph.d.ts +0 -48
  35. package/dist/vendor/adapters/contracts/src/context-graph.js +0 -26
  36. package/dist/vendor/adapters/contracts/src/execution-policy.d.ts +0 -78
  37. package/dist/vendor/adapters/contracts/src/execution-policy.js +0 -43
  38. package/dist/vendor/adapters/contracts/src/governance.d.ts +0 -24
  39. package/dist/vendor/adapters/contracts/src/governance.js +0 -11
  40. package/dist/vendor/adapters/contracts/src/identity.d.ts +0 -29
  41. package/dist/vendor/adapters/contracts/src/identity.js +0 -30
  42. package/dist/vendor/adapters/contracts/src/index.d.ts +0 -412
  43. package/dist/vendor/adapters/contracts/src/index.js +0 -225
  44. package/dist/vendor/adapters/contracts/src/operator.d.ts +0 -19
  45. package/dist/vendor/adapters/contracts/src/operator.js +0 -11
  46. package/dist/vendor/adapters/contracts/src/trajectory.d.ts +0 -26
  47. package/dist/vendor/adapters/contracts/src/trajectory.js +0 -27
  48. package/dist/vendor/adapters/core/src/compiler.d.ts +0 -52
  49. package/dist/vendor/adapters/core/src/compiler.js +0 -52
  50. package/dist/vendor/adapters/core/src/context-graph.d.ts +0 -3
  51. package/dist/vendor/adapters/core/src/context-graph.js +0 -371
  52. package/dist/vendor/adapters/core/src/context-integrity.d.ts +0 -26
  53. package/dist/vendor/adapters/core/src/context-integrity.js +0 -80
  54. package/dist/vendor/adapters/core/src/grounding.d.ts +0 -38
  55. package/dist/vendor/adapters/core/src/grounding.js +0 -273
  56. package/dist/vendor/adapters/core/src/identity.d.ts +0 -20
  57. package/dist/vendor/adapters/core/src/identity.js +0 -86
  58. package/dist/vendor/adapters/core/src/index.d.ts +0 -186
  59. package/dist/vendor/adapters/core/src/index.js +0 -1495
  60. package/dist/vendor/adapters/core/src/leash.d.ts +0 -48
  61. package/dist/vendor/adapters/core/src/leash.js +0 -497
  62. package/dist/vendor/adapters/core/src/persistence/compiler.d.ts +0 -18
  63. package/dist/vendor/adapters/core/src/persistence/compiler.js +0 -34
  64. package/dist/vendor/adapters/core/src/persistence/index.d.ts +0 -10
  65. package/dist/vendor/adapters/core/src/persistence/index.js +0 -5
  66. package/dist/vendor/adapters/core/src/persistence/integrity.d.ts +0 -39
  67. package/dist/vendor/adapters/core/src/persistence/integrity.js +0 -252
  68. package/dist/vendor/adapters/core/src/persistence/ledger.d.ts +0 -23
  69. package/dist/vendor/adapters/core/src/persistence/ledger.js +0 -9
  70. package/dist/vendor/adapters/core/src/persistence/memory-store.d.ts +0 -63
  71. package/dist/vendor/adapters/core/src/persistence/memory-store.js +0 -115
  72. package/dist/vendor/adapters/core/src/persistence/runs-reader.d.ts +0 -66
  73. package/dist/vendor/adapters/core/src/persistence/runs-reader.js +0 -140
  74. package/dist/vendor/adapters/core/src/persistence/store.d.ts +0 -89
  75. package/dist/vendor/adapters/core/src/persistence/store.js +0 -125
  76. package/dist/vendor/adapters/core/src/persistence/trace-store.d.ts +0 -85
  77. package/dist/vendor/adapters/core/src/persistence/trace-store.js +0 -125
  78. package/dist/vendor/adapters/core/src/policy-compiler.d.ts +0 -2
  79. package/dist/vendor/adapters/core/src/policy-compiler.js +0 -195
  80. package/dist/vendor/adapters/core/src/policy.d.ts +0 -156
  81. package/dist/vendor/adapters/core/src/policy.js +0 -704
  82. package/dist/vendor/adapters/core/src/repo-analyzer.d.ts +0 -53
  83. package/dist/vendor/adapters/core/src/repo-analyzer.js +0 -263
  84. package/dist/vendor/adapters/core/src/rollback.d.ts +0 -15
  85. package/dist/vendor/adapters/core/src/rollback.js +0 -253
  86. package/dist/vendor/adapters/core/src/routing.d.ts +0 -82
  87. package/dist/vendor/adapters/core/src/routing.js +0 -336
  88. package/dist/vendor/adapters/core/src/test-integrity.d.ts +0 -64
  89. package/dist/vendor/adapters/core/src/test-integrity.js +0 -197
  90. package/dist/vendor/adapters/core/src/trajectory.d.ts +0 -18
  91. package/dist/vendor/adapters/core/src/trajectory.js +0 -295
  92. package/dist/vendor/contracts/delivery.d.ts +0 -47
  93. package/dist/vendor/contracts/delivery.js +0 -18
  94. package/dist/vendor/core/delivery/index.d.ts +0 -6
  95. package/dist/vendor/core/delivery/index.js +0 -4
  96. package/dist/vendor/core/delivery/message-client.d.ts +0 -18
  97. package/dist/vendor/core/delivery/message-client.js +0 -38
  98. package/dist/vendor/core/delivery/message-ledger.d.ts +0 -35
  99. package/dist/vendor/core/delivery/message-ledger.js +0 -97
  100. package/dist/vendor/core/delivery/message-schema.d.ts +0 -16
  101. package/dist/vendor/core/delivery/message-schema.js +0 -111
  102. package/dist/vendor/core/delivery/update-check.d.ts +0 -19
  103. package/dist/vendor/core/delivery/update-check.js +0 -107
@@ -1,295 +0,0 @@
1
- import { cloneCircuitBreakDecision, cloneTrajectoryAssessment } from "../../../contracts/index.js";
2
- const STOP_WORDS = new Set([
3
- "about",
4
- "after",
5
- "again",
6
- "along",
7
- "also",
8
- "been",
9
- "before",
10
- "being",
11
- "between",
12
- "could",
13
- "failed",
14
- "failure",
15
- "from",
16
- "have",
17
- "into",
18
- "just",
19
- "keep",
20
- "made",
21
- "make",
22
- "need",
23
- "only",
24
- "over",
25
- "same",
26
- "should",
27
- "still",
28
- "than",
29
- "that",
30
- "their",
31
- "them",
32
- "then",
33
- "there",
34
- "these",
35
- "they",
36
- "this",
37
- "those",
38
- "touching",
39
- "without",
40
- "would"
41
- ]);
42
- const STALL_FAILURE_CLASSES = new Set([
43
- "logic_error",
44
- "verification_failure",
45
- "repo_grounding_failure",
46
- "scope_creep",
47
- "no_progress"
48
- ]);
49
- export function assessTrajectory(input) {
50
- const attempts = input.attempts.filter((attempt) => attempt.index > 0);
51
- if (attempts.length === 0) {
52
- return cloneTrajectoryAssessment({
53
- status: "healthy",
54
- confidence: "low",
55
- summary: "No prior attempts exist, so Martin has no drift evidence yet.",
56
- signals: []
57
- });
58
- }
59
- const signals = [];
60
- const recentAttempts = attempts.slice(-4);
61
- const lastThree = recentAttempts.slice(-3);
62
- const lastTwo = recentAttempts.slice(-2);
63
- const oscillation = detectFailureOscillation(lastThree);
64
- if (oscillation) {
65
- signals.push(oscillation);
66
- }
67
- const repetition = detectAttemptRepetition(lastThree);
68
- if (repetition) {
69
- signals.push(repetition);
70
- }
71
- const verificationStall = detectVerificationStall(lastTwo);
72
- if (verificationStall) {
73
- signals.push(verificationStall);
74
- }
75
- const objectiveDrift = detectObjectiveDrift({
76
- objective: input.objective,
77
- verificationPlan: input.verificationPlan,
78
- attempts: lastTwo
79
- });
80
- if (objectiveDrift) {
81
- signals.push(objectiveDrift);
82
- }
83
- const status = deriveTrajectoryStatus(signals);
84
- const confidence = deriveTrajectoryConfidence(signals);
85
- const recommendedIntervention = deriveRecommendedIntervention(signals);
86
- if (signals.length === 0) {
87
- return cloneTrajectoryAssessment({
88
- status: "healthy",
89
- confidence: "medium",
90
- summary: "Recent attempts stay aligned enough that Martin does not need a circuit break yet.",
91
- signals: []
92
- });
93
- }
94
- const summary = summarizeTrajectory(status, signals);
95
- return cloneTrajectoryAssessment({
96
- status,
97
- confidence,
98
- summary,
99
- signals,
100
- ...(recommendedIntervention ? { recommendedIntervention } : {})
101
- });
102
- }
103
- export function decideCircuitBreak(input) {
104
- const assessment = assessTrajectory(input);
105
- const remainingIterations = input.remainingIterations ?? Number.POSITIVE_INFINITY;
106
- const stopForHighSignal = assessment.signals.some((signal) => signal.severity === "high");
107
- const stopForLateDrift = assessment.status === "drifting" &&
108
- remainingIterations <= 1 &&
109
- assessment.signals.some((signal) => signal.code === "objective_drift");
110
- const stopForLateStall = assessment.status === "watch" &&
111
- remainingIterations <= 1 &&
112
- assessment.signals.some((signal) => signal.code === "verification_stall");
113
- const shouldStop = stopForHighSignal || stopForLateDrift || stopForLateStall;
114
- return cloneCircuitBreakDecision({
115
- shouldStop,
116
- reason: shouldStop
117
- ? assessment.summary
118
- : "Trajectory signals do not justify a hard circuit break yet.",
119
- assessment,
120
- ...(assessment.recommendedIntervention
121
- ? { recommendedIntervention: assessment.recommendedIntervention }
122
- : {})
123
- });
124
- }
125
- function detectFailureOscillation(attempts) {
126
- if (attempts.length < 3) {
127
- return undefined;
128
- }
129
- const [first, second, third] = attempts;
130
- if (!first?.failureClass ||
131
- !second?.failureClass ||
132
- !third?.failureClass ||
133
- first.failureClass === second.failureClass ||
134
- first.failureClass !== third.failureClass) {
135
- return undefined;
136
- }
137
- return {
138
- code: "failure_oscillation",
139
- severity: "high",
140
- summary: `Attempts ${first.index}, ${second.index}, and ${third.index} oscillate between failure classes instead of converging.`,
141
- attemptIndexes: [first.index, second.index, third.index]
142
- };
143
- }
144
- function detectAttemptRepetition(attempts) {
145
- if (attempts.length < 3) {
146
- return undefined;
147
- }
148
- const tokenSets = attempts.map((attempt) => toTokenSet(attempt.summary));
149
- if (tokenSets.some((tokens) => tokens.size < 4)) {
150
- return undefined;
151
- }
152
- const pairScores = [
153
- jaccardSimilarity(tokenSets[0], tokenSets[1]),
154
- jaccardSimilarity(tokenSets[1], tokenSets[2]),
155
- jaccardSimilarity(tokenSets[0], tokenSets[2])
156
- ];
157
- if (pairScores.every((score) => score >= 0.55)) {
158
- const matchedTerms = intersectSets(tokenSets[0], new Set(intersectSets(tokenSets[1], tokenSets[2])));
159
- return {
160
- code: "attempt_repetition",
161
- severity: "high",
162
- summary: `Attempts ${attempts.map((attempt) => attempt.index).join(", ")} are materially repetitive and are not exploring a new path.`,
163
- attemptIndexes: attempts.map((attempt) => attempt.index),
164
- ...(matchedTerms.length > 0 ? { matchedTerms: matchedTerms.slice(0, 6) } : {})
165
- };
166
- }
167
- return undefined;
168
- }
169
- function detectVerificationStall(attempts) {
170
- if (attempts.length < 2) {
171
- return undefined;
172
- }
173
- const relevant = attempts.filter((attempt) => {
174
- const failureClass = attempt.failureClass;
175
- return failureClass !== undefined && STALL_FAILURE_CLASSES.has(failureClass);
176
- });
177
- if (relevant.length < 2) {
178
- return undefined;
179
- }
180
- const tokenSets = relevant.map((attempt) => toTokenSet(attempt.summary));
181
- const overlap = tokenSets
182
- .slice(1)
183
- .map((tokens, index) => jaccardSimilarity(tokenSets[index], tokens))
184
- .reduce((highest, score) => Math.max(highest, score), 0);
185
- if (overlap < 0.35) {
186
- return undefined;
187
- }
188
- const latest = relevant.at(-1);
189
- return {
190
- code: "verification_stall",
191
- severity: "medium",
192
- summary: `Recent attempts keep landing on the same verifier-facing failure surface (${latest.failureClass}).`,
193
- attemptIndexes: relevant.map((attempt) => attempt.index)
194
- };
195
- }
196
- function detectObjectiveDrift(input) {
197
- if (input.attempts.length < 2) {
198
- return undefined;
199
- }
200
- const objectiveTerms = toTokenSet(`${input.objective} ${(input.verificationPlan ?? []).join(" ")}`);
201
- if (objectiveTerms.size < 4) {
202
- return undefined;
203
- }
204
- const summaryTerms = input.attempts.map((attempt) => toTokenSet(attempt.summary));
205
- const overlaps = summaryTerms.map((terms) => jaccardSimilarity(terms, objectiveTerms));
206
- if (overlaps.some((score) => score >= 0.18)) {
207
- return undefined;
208
- }
209
- const sharedDriftTerms = intersectSets(summaryTerms[0], summaryTerms[1]);
210
- if (sharedDriftTerms.length < 3) {
211
- return undefined;
212
- }
213
- return {
214
- code: "objective_drift",
215
- severity: "medium",
216
- summary: "Recent attempt summaries are converging on work that does not overlap with the original objective or verifier plan.",
217
- attemptIndexes: input.attempts.map((attempt) => attempt.index),
218
- matchedTerms: sharedDriftTerms.slice(0, 6)
219
- };
220
- }
221
- function deriveTrajectoryStatus(signals) {
222
- if (signals.some((signal) => signal.severity === "high")) {
223
- return "stalled";
224
- }
225
- if (signals.some((signal) => signal.code === "objective_drift")) {
226
- return "drifting";
227
- }
228
- if (signals.some((signal) => signal.severity === "medium")) {
229
- return "watch";
230
- }
231
- return "healthy";
232
- }
233
- function deriveTrajectoryConfidence(signals) {
234
- if (signals.some((signal) => signal.severity === "high")) {
235
- return "high";
236
- }
237
- if (signals.some((signal) => signal.severity === "medium")) {
238
- return "medium";
239
- }
240
- return "low";
241
- }
242
- function deriveRecommendedIntervention(signals) {
243
- if (signals.some((signal) => signal.code === "failure_oscillation" || signal.code === "attempt_repetition")) {
244
- return "escalate_human";
245
- }
246
- if (signals.some((signal) => signal.code === "objective_drift")) {
247
- return "tighten_task";
248
- }
249
- if (signals.some((signal) => signal.code === "verification_stall")) {
250
- return "run_verifier";
251
- }
252
- return undefined;
253
- }
254
- function summarizeTrajectory(status, signals) {
255
- const lead = signals[0];
256
- if (!lead) {
257
- return "Trajectory is healthy.";
258
- }
259
- switch (status) {
260
- case "stalled":
261
- return `Trajectory stalled: ${lead.summary}`;
262
- case "drifting":
263
- return `Trajectory drift detected: ${lead.summary}`;
264
- case "watch":
265
- return `Trajectory warning: ${lead.summary}`;
266
- default:
267
- return lead.summary;
268
- }
269
- }
270
- function toTokenSet(text) {
271
- return new Set((text ?? "")
272
- .toLowerCase()
273
- .match(/[a-z][a-z0-9_/-]{2,}/g)
274
- ?.flatMap((token) => splitCompositeToken(token))
275
- .map((token) => token.trim())
276
- .filter((token) => token.length >= 3 && !STOP_WORDS.has(token)) ?? []);
277
- }
278
- function splitCompositeToken(token) {
279
- return token
280
- .split(/[-_/]/g)
281
- .flatMap((part) => part.split(/(?=[A-Z])/g))
282
- .map((part) => part.toLowerCase())
283
- .filter(Boolean);
284
- }
285
- function jaccardSimilarity(left, right) {
286
- if (left.size === 0 || right.size === 0) {
287
- return 0;
288
- }
289
- const intersection = intersectSets(left, right).length;
290
- const union = new Set([...left, ...right]).size;
291
- return union === 0 ? 0 : intersection / union;
292
- }
293
- function intersectSets(left, right) {
294
- return [...left].filter((term) => right.has(term));
295
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * R4 Delivery — M1 Contract
3
- *
4
- * Shared with Lane B (Control Plane). Do not modify unilaterally.
5
- * The server selects one message; the client never downloads the catalog.
6
- */
7
- export declare const DELIVERY_MESSAGE_SCHEMA_VERSION: "delivery-message/1";
8
- export declare const DELIVERY_RECORD_SCHEMA_VERSION: "delivery-record/1";
9
- export declare const MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION: "martin-message-selection/1";
10
- export declare const ALLOWED_ACTION_TYPES: readonly ["upgrade_cli", "upgrade_mcp", "submit_feedback", "open_release_notes", "dismiss", "view_spend_report", "view_run_explain"];
11
- export type ActionType = (typeof ALLOWED_ACTION_TYPES)[number];
12
- export type MessageKind = "update" | "feedback_request" | "milestone";
13
- export interface DeliveryMessage {
14
- schemaVersion: typeof DELIVERY_MESSAGE_SCHEMA_VERSION;
15
- id: string;
16
- revision: number;
17
- kind: MessageKind;
18
- title: string;
19
- body: string;
20
- action: {
21
- type: ActionType;
22
- url?: string;
23
- targetVersion?: string;
24
- };
25
- expiresAt: string;
26
- cooldownHours: number;
27
- }
28
- export interface MessageSelectionResponse {
29
- schemaVersion: typeof MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION;
30
- message?: DeliveryMessage;
31
- }
32
- /** Local cooldown/dismissal state — persisted in .martin/delivery-record.json */
33
- export interface DeliveryRecord {
34
- schemaVersion: typeof DELIVERY_RECORD_SCHEMA_VERSION;
35
- lastMessageId?: string;
36
- lastShownAtEpochMs?: number;
37
- dismissedIds: string[];
38
- cooldownUntilEpochMs?: number;
39
- cachedMessage?: DeliveryMessage;
40
- cachedAtEpochMs?: number;
41
- }
42
- /** Structured update field for --json and MCP responses */
43
- export interface UpdateAvailableField {
44
- targetVersion: string;
45
- kind: "cli" | "mcp";
46
- message?: string;
47
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * R4 Delivery — M1 Contract
3
- *
4
- * Shared with Lane B (Control Plane). Do not modify unilaterally.
5
- * The server selects one message; the client never downloads the catalog.
6
- */
7
- export const DELIVERY_MESSAGE_SCHEMA_VERSION = "delivery-message/1";
8
- export const DELIVERY_RECORD_SCHEMA_VERSION = "delivery-record/1";
9
- export const MESSAGE_SELECTION_RESPONSE_SCHEMA_VERSION = "martin-message-selection/1";
10
- export const ALLOWED_ACTION_TYPES = [
11
- "upgrade_cli",
12
- "upgrade_mcp",
13
- "submit_feedback",
14
- "open_release_notes",
15
- "dismiss",
16
- "view_spend_report",
17
- "view_run_explain",
18
- ];
@@ -1,6 +0,0 @@
1
- export { fetchSelectedMessage } from "./message-client.js";
2
- export type { MessageClientOptions, MessageSelectRequest } from "./message-client.js";
3
- export { cacheMessage, isCooldownExpired, isDismissed, loadDeliveryRecord, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./message-ledger.js";
4
- export { parseMessageSelectionResponse } from "./message-schema.js";
5
- export type { ParseFailure, ParseResult, ValidationError } from "./message-schema.js";
6
- export { getCliInstalledVersion, getMcpInstalledVersion, isNewerVersion } from "./update-check.js";
@@ -1,4 +0,0 @@
1
- export { fetchSelectedMessage } from "./message-client.js";
2
- export { cacheMessage, isCooldownExpired, isDismissed, loadDeliveryRecord, recordDismissed, recordShown, resolveDefaultLedgerPath, saveDeliveryRecord, } from "./message-ledger.js";
3
- export { parseMessageSelectionResponse } from "./message-schema.js";
4
- export { getCliInstalledVersion, getMcpInstalledVersion, isNewerVersion } from "./update-check.js";
@@ -1,18 +0,0 @@
1
- import { type DeliveryMessage } from "../../contracts/index.js";
2
- export interface MessageSelectRequest {
3
- installId?: string;
4
- clientVersion: string;
5
- clientKind: "cli" | "mcp";
6
- /** Enum signal — never a transcript or raw content */
7
- trigger?: "first_verified_run" | "milestone_reached" | "version_check";
8
- }
9
- export interface MessageClientOptions {
10
- endpoint?: string;
11
- timeoutMs?: number;
12
- }
13
- /**
14
- * Fetch a server-selected message from the Control Plane.
15
- * Returns null on any network failure, timeout, or validation error.
16
- * Never throws — the primary command must never be affected.
17
- */
18
- export declare function fetchSelectedMessage(request: MessageSelectRequest, options?: MessageClientOptions): Promise<DeliveryMessage | null>;
@@ -1,38 +0,0 @@
1
- import { parseMessageSelectionResponse } from "./message-schema.js";
2
- const DEFAULT_ENDPOINT = "https://api.martinloop.com/v1/messages/select";
3
- const REQUEST_TIMEOUT_MS = 4_000;
4
- /**
5
- * Fetch a server-selected message from the Control Plane.
6
- * Returns null on any network failure, timeout, or validation error.
7
- * Never throws — the primary command must never be affected.
8
- */
9
- export async function fetchSelectedMessage(request, options = {}) {
10
- const endpoint = options.endpoint ?? process.env["MARTIN_MESSAGE_ENDPOINT"] ?? DEFAULT_ENDPOINT;
11
- const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
12
- let controller;
13
- let timeoutId;
14
- try {
15
- controller = new AbortController();
16
- timeoutId = setTimeout(() => controller.abort(), timeoutMs);
17
- const response = await fetch(endpoint, {
18
- method: "POST",
19
- headers: { "Content-Type": "application/json", "Accept": "application/json" },
20
- body: JSON.stringify(request),
21
- signal: controller.signal,
22
- });
23
- if (!response.ok)
24
- return null;
25
- const raw = await response.text();
26
- const result = parseMessageSelectionResponse(raw);
27
- if (!result.ok)
28
- return null;
29
- return result.message ?? null;
30
- }
31
- catch {
32
- return null;
33
- }
34
- finally {
35
- if (timeoutId !== undefined)
36
- clearTimeout(timeoutId);
37
- }
38
- }
@@ -1,35 +0,0 @@
1
- import { type DeliveryMessage, type DeliveryRecord } from "../../contracts/index.js";
2
- /**
3
- * Load DeliveryRecord from disk. Returns empty record on any read/parse failure — never throws.
4
- */
5
- export declare function loadDeliveryRecord(ledgerPath: string): DeliveryRecord;
6
- /**
7
- * Persist DeliveryRecord atomically via temp-file + rename.
8
- */
9
- export declare function saveDeliveryRecord(ledgerPath: string, record: DeliveryRecord): void;
10
- /**
11
- * Returns true when the cooldown period has elapsed and the message should be shown.
12
- * Clock-skew safe: if now < lastShownAtEpochMs (clock jumped back), treat cooldown as expired.
13
- */
14
- export declare function isCooldownExpired(record: DeliveryRecord, nowMs: number): boolean;
15
- /**
16
- * Returns true if this message id was permanently dismissed.
17
- */
18
- export declare function isDismissed(record: DeliveryRecord, messageId: string): boolean;
19
- /**
20
- * Record that a message was shown. Updates cooldown window.
21
- */
22
- export declare function recordShown(record: DeliveryRecord, message: DeliveryMessage, nowMs: number): DeliveryRecord;
23
- /**
24
- * Record that a message was permanently dismissed.
25
- */
26
- export declare function recordDismissed(record: DeliveryRecord, messageId: string): DeliveryRecord;
27
- /**
28
- * Cache a fetched message for offline use.
29
- */
30
- export declare function cacheMessage(record: DeliveryRecord, message: DeliveryMessage, nowMs: number): DeliveryRecord;
31
- /**
32
- * Resolve the default ledger path: $MARTIN_STATE_DIR/delivery-record.json
33
- * or ~/.martin/delivery-record.json.
34
- */
35
- export declare function resolveDefaultLedgerPath(): string;
@@ -1,97 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { DELIVERY_RECORD_SCHEMA_VERSION } from "../../contracts/index.js";
5
- const EMPTY_RECORD = {
6
- schemaVersion: DELIVERY_RECORD_SCHEMA_VERSION,
7
- dismissedIds: [],
8
- };
9
- /**
10
- * Load DeliveryRecord from disk. Returns empty record on any read/parse failure — never throws.
11
- */
12
- export function loadDeliveryRecord(ledgerPath) {
13
- try {
14
- const raw = fs.readFileSync(ledgerPath, "utf8");
15
- const parsed = JSON.parse(raw);
16
- if (typeof parsed === "object" &&
17
- parsed !== null &&
18
- !Array.isArray(parsed) &&
19
- parsed["schemaVersion"] === DELIVERY_RECORD_SCHEMA_VERSION) {
20
- return parsed;
21
- }
22
- }
23
- catch {
24
- // file missing or corrupt — start fresh
25
- }
26
- return { ...EMPTY_RECORD, dismissedIds: [] };
27
- }
28
- /**
29
- * Persist DeliveryRecord atomically via temp-file + rename.
30
- */
31
- export function saveDeliveryRecord(ledgerPath, record) {
32
- const dir = path.dirname(ledgerPath);
33
- fs.mkdirSync(dir, { recursive: true });
34
- const tmp = path.join(os.tmpdir(), `martin-dlv-${process.pid}-${Date.now()}.json`);
35
- fs.writeFileSync(tmp, JSON.stringify(record, null, 2), "utf8");
36
- fs.renameSync(tmp, ledgerPath);
37
- }
38
- /**
39
- * Returns true when the cooldown period has elapsed and the message should be shown.
40
- * Clock-skew safe: if now < lastShownAtEpochMs (clock jumped back), treat cooldown as expired.
41
- */
42
- export function isCooldownExpired(record, nowMs) {
43
- const until = record.cooldownUntilEpochMs;
44
- if (until === undefined)
45
- return true;
46
- const last = record.lastShownAtEpochMs ?? 0;
47
- // If clock appears to have gone backwards past last-shown, treat as expired
48
- if (nowMs < last)
49
- return true;
50
- return nowMs >= until;
51
- }
52
- /**
53
- * Returns true if this message id was permanently dismissed.
54
- */
55
- export function isDismissed(record, messageId) {
56
- return record.dismissedIds.includes(messageId);
57
- }
58
- /**
59
- * Record that a message was shown. Updates cooldown window.
60
- */
61
- export function recordShown(record, message, nowMs) {
62
- return {
63
- ...record,
64
- lastMessageId: message.id,
65
- lastShownAtEpochMs: nowMs,
66
- cooldownUntilEpochMs: nowMs + message.cooldownHours * 60 * 60 * 1000,
67
- };
68
- }
69
- /**
70
- * Record that a message was permanently dismissed.
71
- */
72
- export function recordDismissed(record, messageId) {
73
- if (record.dismissedIds.includes(messageId))
74
- return record;
75
- return {
76
- ...record,
77
- dismissedIds: [...record.dismissedIds, messageId],
78
- };
79
- }
80
- /**
81
- * Cache a fetched message for offline use.
82
- */
83
- export function cacheMessage(record, message, nowMs) {
84
- return {
85
- ...record,
86
- cachedMessage: message,
87
- cachedAtEpochMs: nowMs,
88
- };
89
- }
90
- /**
91
- * Resolve the default ledger path: $MARTIN_STATE_DIR/delivery-record.json
92
- * or ~/.martin/delivery-record.json.
93
- */
94
- export function resolveDefaultLedgerPath() {
95
- const stateDir = process.env["MARTIN_STATE_DIR"] ?? path.join(os.homedir(), ".martin");
96
- return path.join(stateDir, "delivery-record.json");
97
- }
@@ -1,16 +0,0 @@
1
- import { type DeliveryMessage } from "../../contracts/index.js";
2
- export type ValidationError = "schema_version_unknown" | "schema_unknown_kind" | "schema_unknown_action_type" | "schema_invalid_id" | "schema_invalid_date" | "schema_invalid_cooldown" | "schema_url_not_https" | "schema_text_too_long" | "schema_response_too_large" | "schema_malformed";
3
- export interface ParseResult {
4
- ok: true;
5
- message: DeliveryMessage | undefined;
6
- }
7
- export interface ParseFailure {
8
- ok: false;
9
- error: ValidationError;
10
- detail?: string;
11
- }
12
- /**
13
- * Parse and validate a raw response body from the message selection endpoint.
14
- * Returns ok:false on any violation — caller must render nothing and log the error.
15
- */
16
- export declare function parseMessageSelectionResponse(raw: string): ParseResult | ParseFailure;