@rulemetric/cli 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-B3HILODD.js → chunk-2BCPVUSF.js} +3 -3
- package/dist/{chunk-4A7FKR2O.js → chunk-42A3HEEH.js} +50 -3
- package/dist/chunk-42A3HEEH.js.map +7 -0
- package/dist/{chunk-PDURR7O3.js → chunk-46LMTDRE.js} +2 -2
- package/dist/{chunk-ISDA2QFX.js → chunk-4QGATD5Y.js} +209 -2
- package/dist/chunk-4QGATD5Y.js.map +7 -0
- package/dist/{chunk-PIOVE3TN.js → chunk-FUS5C4SB.js} +2 -2
- package/dist/{chunk-PHLMIZKM.js → chunk-JNFIYBFL.js} +30 -5
- package/dist/chunk-JNFIYBFL.js.map +7 -0
- package/dist/{chunk-JM6BQSRE.js → chunk-LCT6MT2V.js} +5 -5
- package/dist/{chunk-RDIH4AG2.js → chunk-MTLAIJED.js} +3 -3
- package/dist/{chunk-NIDLJO6F.js → chunk-QSN77T7C.js} +23 -1
- package/dist/{chunk-NIDLJO6F.js.map → chunk-QSN77T7C.js.map} +2 -2
- package/dist/{chunk-ELL34R3K.js → chunk-XMWHTOCC.js} +3 -3
- package/dist/{chunk-3LHLSY7V.js → chunk-Y2ARLMWZ.js} +3 -3
- package/dist/{chunk-3IU7GFXS.js → chunk-YEBBX4HQ.js} +2 -2
- package/dist/commands/evals/agent.js +9 -11
- package/dist/commands/evals/agent.js.map +2 -2
- package/dist/commands/hooks/install.js +1 -1
- package/dist/commands/service/install.js +180 -11
- package/dist/commands/service/install.js.map +2 -2
- package/dist/commands/service/uninstall.js +33 -2
- package/dist/commands/service/uninstall.js.map +2 -2
- package/dist/dashboard/Dashboard.js +3 -3
- package/dist/dashboard/data.js +2 -2
- package/dist/{dist-I6BCOLNG.js → dist-NEECHU47.js} +6 -2
- package/dist/lib/agent-loop.js +9 -9
- package/dist/lib/handlers/process-announcement.js +3 -3
- package/dist/lib/handlers/process-changelog.js +3 -3
- package/dist/lib/handlers/process-launch.js +3 -3
- package/dist/lib/handlers/process-run-cleanup.js +4 -4
- package/dist/lib/handlers/process-session-goal.js +2 -2
- package/dist/lib/manual-tasks.js +3 -3
- package/dist/lib/statusline-shim.js +1 -1
- package/dist/lib/worktree.js +2 -2
- package/oclif.manifest.json +11 -4
- package/package.json +18 -15
- package/dist/chunk-4A7FKR2O.js.map +0 -7
- package/dist/chunk-ISDA2QFX.js.map +0 -7
- package/dist/chunk-PHLMIZKM.js.map +0 -7
- /package/dist/{chunk-B3HILODD.js.map → chunk-2BCPVUSF.js.map} +0 -0
- /package/dist/{chunk-PDURR7O3.js.map → chunk-46LMTDRE.js.map} +0 -0
- /package/dist/{chunk-PIOVE3TN.js.map → chunk-FUS5C4SB.js.map} +0 -0
- /package/dist/{chunk-JM6BQSRE.js.map → chunk-LCT6MT2V.js.map} +0 -0
- /package/dist/{chunk-RDIH4AG2.js.map → chunk-MTLAIJED.js.map} +0 -0
- /package/dist/{chunk-ELL34R3K.js.map → chunk-XMWHTOCC.js.map} +0 -0
- /package/dist/{chunk-3LHLSY7V.js.map → chunk-Y2ARLMWZ.js.map} +0 -0
- /package/dist/{chunk-3IU7GFXS.js.map → chunk-YEBBX4HQ.js.map} +0 -0
- /package/dist/{dist-I6BCOLNG.js.map → dist-NEECHU47.js.map} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/statusline-shim.ts"],
|
|
4
|
+
"sourcesContent": ["import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// Path where the shim script lives \u2014 readable by the `statusLine` config key.\nexport const STATUSLINE_SHIM_PATH = join(homedir(), '.claude', 'statusline-rulemetric.sh');\n\n/**\n * Write ~/.claude/statusline-rulemetric.sh.\n *\n * The script is invoked by Claude Code on every statusline refresh cycle.\n * Claude Code feeds a JSON payload on stdin that includes `rate_limits`\n * (same shape as GET /api/oauth/usage) using its own in-memory token \u2014\n * so freshness is tied to the statusline cadence, not the proxy capturing\n * a stale keychain token.\n *\n * The script:\n * 1. Reads stdin once (Claude Code closes it after the payload).\n * 2. If jq is available, extracts each rate_limit window and POSTs it to\n * POST /api/internal/research/observe as an oauth_usage observation.\n * 3. If a `prevCommand` is supplied (the user's original statusLine value),\n * it is executed with the same stdin forwarded so the user's prompt text\n * is still displayed \u2014 we must chain, not replace.\n * 4. Exits 0 regardless so Claude Code always gets a clean statusline.\n *\n * Auth is read from the same priority chain as the hook scripts:\n * ~/.config/rulemetric/env (RULEMETRIC_ACCESS_TOKEN / RULEMETRIC_API_KEY)\n * RULEMETRIC_API_KEY / RULEMETRIC_ACCESS_TOKEN env vars\n *\n * Returns the path written.\n */\nexport function writeStatuslineShim(prevCommand?: string): string {\n const chainBlock = prevCommand\n ? `\n# Chain to the original statusLine command so the user's prompt is preserved.\necho \"$STDIN_PAYLOAD\" | ${prevCommand}`\n : '';\n\n const script = `#!/usr/bin/env bash\n# RuleMetric statusline shim \u2014 auto-generated by \\`rulemetric hooks install --statusline-usage\\`\n# Captures Claude Code rate_limits from statusline stdin and forwards to the research pipeline.\n# DO NOT EDIT \u2014 re-run \\`rulemetric hooks install --statusline-usage\\` to regenerate.\n\nset -euo pipefail\n\n# Read stdin once (Claude Code closes the pipe after the payload).\nSTDIN_PAYLOAD=\"$(cat)\"\n\n# \u2500\u2500 Auth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCONFIG_ENV=\"$HOME/.config/rulemetric/env\"\nif [ -f \"$CONFIG_ENV\" ]; then\n # shellcheck source=/dev/null\n source \"$CONFIG_ENV\"\nfi\n\nAPI_URL=\"\\${RULEMETRIC_API_URL:-https://rulemetric.com}\"\nAUTH_TOKEN=\"\\${RULEMETRIC_ACCESS_TOKEN:-\\${RULEMETRIC_API_KEY:-}}\"\n\nif [ -z \"$AUTH_TOKEN\" ]; then\n # No auth \u2014 still chain so the user's statusline works.\n${chainBlock}\n exit 0\nfi\n\n# \u2500\u2500 Extract + POST rate_limit windows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif ! command -v jq &>/dev/null; then\n # jq missing \u2014 chain and bail; don't break the user's statusline.\n${chainBlock}\n exit 0\nfi\n\nOBSERVED_AT=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\nCACHE_DIR=\"$HOME/.config/rulemetric\"\nmkdir -p \"$CACHE_DIR\" 2>/dev/null || true\n\nfor WIRE_KEY in five_hour seven_day seven_day_opus; do\n # Map wire key \u2192 limit_type (case statement avoids bash 4 associative arrays).\n case \"$WIRE_KEY\" in\n five_hour) LIMIT_TYPE=\"five_hour_percent\" ;;\n seven_day) LIMIT_TYPE=\"seven_day_percent\" ;;\n seven_day_opus) LIMIT_TYPE=\"seven_day_opus_percent\" ;;\n *) continue ;;\n esac\n\n # Try both wire shapes:\n # Legacy: { \"rate_limits\": { \"five_hour\": { \"used_percentage\": ..., \"resets_at\": ... } } }\n # Current: { \"five_hour\": { \"used_percentage\": ..., \"resets_at\": ... } }\n WINDOW=\"$(echo \"$STDIN_PAYLOAD\" | jq -c \".rate_limits.\\${WIRE_KEY} // .\\${WIRE_KEY} // empty\" 2>/dev/null || true)\"\n\n if [ -z \"$WINDOW\" ] || [ \"$WINDOW\" = \"null\" ]; then\n continue\n fi\n\n USED_PCT=\"$(echo \"$WINDOW\" | jq '.used_percentage // empty' 2>/dev/null || true)\"\n RESETS_AT_EPOCH=\"$(echo \"$WINDOW\" | jq '.resets_at // empty' 2>/dev/null || true)\"\n\n if [ -z \"$USED_PCT\" ] || [ \"$USED_PCT\" = \"null\" ]; then\n continue\n fi\n\n # Convert unix epoch to ISO-8601 if present.\n if [ -n \"$RESETS_AT_EPOCH\" ] && [ \"$RESETS_AT_EPOCH\" != \"null\" ]; then\n RESETS_AT_ISO=\"$(date -u -r \"$RESETS_AT_EPOCH\" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \\\n || date -u -d \"@$RESETS_AT_EPOCH\" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \\\n || echo \"\")\"\n else\n RESETS_AT_ISO=\"\"\n fi\n\n # Skip stale windows. Claude Code can keep re-emitting a cached rate_limits\n # block after the reset time has passed; storing those rows floods the DB and\n # corrupts the \"current limits\" view.\n if [ -n \"$RESETS_AT_EPOCH\" ] && [ \"$RESETS_AT_EPOCH\" != \"null\" ]; then\n NOW_EPOCH=\"$(date -u +%s)\"\n if [ \"$RESETS_AT_EPOCH\" -lt \"$NOW_EPOCH\" ] 2>/dev/null; then\n continue\n fi\n else\n NOW_EPOCH=\"$(date -u +%s)\"\n fi\n\n # Local duplicate suppression mirrors the API's 30-minute freshness floor.\n # The statusline refreshes often; if the same window/value/reset is already\n # posted recently, another POST only burns an API request and DB connection.\n CACHE_FILE=\"$CACHE_DIR/statusline-usage-\\${LIMIT_TYPE}.cache\"\n CACHE_KEY=\"\\${LIMIT_TYPE}|\\${USED_PCT}|\\${RESETS_AT_ISO}\"\n if [ -f \"$CACHE_FILE\" ]; then\n read -r CACHED_AT CACHED_KEY < \"$CACHE_FILE\" || true\n if [ \"\\${CACHED_KEY:-}\" = \"$CACHE_KEY\" ] && [ $((NOW_EPOCH - \\${CACHED_AT:-0})) -lt 1800 ] 2>/dev/null; then\n continue\n fi\n fi\n printf '%s %s\\n' \"$NOW_EPOCH\" \"$CACHE_KEY\" > \"$CACHE_FILE.tmp\" 2>/dev/null \\\n && mv \"$CACHE_FILE.tmp\" \"$CACHE_FILE\" 2>/dev/null || true\n\n PAYLOAD=\"$(jq -nc \\\\\n --arg provider \"anthropic\" \\\\\n --arg plan \"unknown\" \\\\\n --argjson value \"$USED_PCT\" \\\\\n --arg limitType \"$LIMIT_TYPE\" \\\\\n --arg observedAt \"$OBSERVED_AT\" \\\\\n --arg resetsAt \"$RESETS_AT_ISO\" \\\\\n '{\n provider: $provider,\n plan: $plan,\n limitType: $limitType,\n value: $value,\n unit: \"percentage\",\n comparator: \"=\",\n sourceType: \"oauth_usage\",\n sourceUrl: null,\n confidence: 1.0,\n observedAt: $observedAt,\n resetsAt: (if $resetsAt == \"\" then null else $resetsAt end)\n }')\"\n\n curl -sf -o /dev/null \\\\\n -X POST \"$API_URL/api/internal/research/observe\" \\\\\n -H \"Content-Type: application/json\" \\\\\n -H \"Authorization: Bearer $AUTH_TOKEN\" \\\\\n --max-time 3 \\\\\n --data \"$PAYLOAD\" >/dev/null 2>&1 &\ndone\n\n${chainBlock}\nexit 0\n`;\n\n writeFileSync(STATUSLINE_SHIM_PATH, script, { encoding: 'utf-8' });\n chmodSync(STATUSLINE_SHIM_PATH, 0o755);\n return STATUSLINE_SHIM_PATH;\n}\n\n// The settings object shape for statusLine (Claude Code v1.x+).\nexport const STATUSLINE_SETTINGS_VALUE = {\n type: 'command' as const,\n command: STATUSLINE_SHIM_PATH,\n refreshInterval: 30,\n};\n\n/**\n * Read the current statusLine command from a settings object, if any.\n * Returns undefined if not set or already points to the shim (idempotent).\n * Handles both the legacy string form and the current object form.\n */\nexport function readCurrentStatusLine(\n settings: Record<string, unknown>,\n): string | undefined {\n const current = settings.statusLine;\n if (!current) return undefined;\n if (typeof current === 'string') {\n if (current === STATUSLINE_SHIM_PATH) return undefined;\n return current;\n }\n if (typeof current === 'object' && current !== null) {\n const cmd = (current as Record<string, unknown>).command;\n if (typeof cmd !== 'string') return undefined;\n if (cmd === STATUSLINE_SHIM_PATH) return undefined; // already installed\n return cmd;\n }\n return undefined;\n}\n\n/**\n * Returns true if the settings object already points to the shim.\n */\nexport function isStatuslineShimInstalled(settings: Record<string, unknown>): boolean {\n const current = settings.statusLine;\n if (!current) return false;\n if (typeof current === 'string') return current === STATUSLINE_SHIM_PATH;\n if (typeof current === 'object' && current !== null) {\n return (current as Record<string, unknown>).command === STATUSLINE_SHIM_PATH;\n }\n return false;\n}\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAmC,eAAe,iBAAiB;AACnE,SAAS,eAAe;AACxB,SAAS,YAAY;AAGd,IAAM,uBAAuB,KAAK,QAAQ,GAAG,WAAW,0BAA0B;AA0BlF,SAAS,oBAAoB,aAA8B;AAChE,QAAM,aAAa,cACf;AAAA;AAAA,0BAEoB,WAAW,KAC/B;AAEJ,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBf,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOV,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiGV,UAAU;AAAA;AAAA;AAIV,gBAAc,sBAAsB,QAAQ,EAAE,UAAU,QAAQ,CAAC;AACjE,YAAU,sBAAsB,GAAK;AACrC,SAAO;AACT;AAGO,IAAM,4BAA4B;AAAA,EACvC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,iBAAiB;AACnB;AAOO,SAAS,sBACd,UACoB;AACpB,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,UAAU;AAC/B,QAAI,YAAY,qBAAsB,QAAO;AAC7C,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,MAAO,QAAoC;AACjD,QAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAI,QAAQ,qBAAsB,QAAO;AACzC,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,0BAA0B,UAA4C;AACpF,QAAM,UAAU,SAAS;AACzB,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO,YAAY;AACpD,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAQ,QAAoC,YAAY;AAAA,EAC1D;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -6,16 +6,16 @@ import {
|
|
|
6
6
|
} from "./chunk-EHABPBGH.js";
|
|
7
7
|
import {
|
|
8
8
|
processLaunch
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XMWHTOCC.js";
|
|
10
10
|
import {
|
|
11
11
|
processRunCleanup
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-Y2ARLMWZ.js";
|
|
13
13
|
import {
|
|
14
14
|
processSendMessage
|
|
15
15
|
} from "./chunk-W53GKIZQ.js";
|
|
16
16
|
import {
|
|
17
17
|
processSessionGoal
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-YEBBX4HQ.js";
|
|
19
19
|
import {
|
|
20
20
|
cronRefreshSkills
|
|
21
21
|
} from "./chunk-RQ2TMLKG.js";
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./chunk-YBNW7ARH.js";
|
|
25
25
|
import {
|
|
26
26
|
processAnnouncement
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-2BCPVUSF.js";
|
|
28
28
|
import {
|
|
29
29
|
processConversation
|
|
30
30
|
} from "./chunk-BD7CY42U.js";
|
|
@@ -266,4 +266,4 @@ export {
|
|
|
266
266
|
pollForWork,
|
|
267
267
|
runAgentLoop
|
|
268
268
|
};
|
|
269
|
-
//# sourceMappingURL=chunk-
|
|
269
|
+
//# sourceMappingURL=chunk-LCT6MT2V.js.map
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
} from "./chunk-K3BFZ7KG.js";
|
|
5
5
|
import {
|
|
6
6
|
renderChangelogAnnouncement
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QSN77T7C.js";
|
|
8
8
|
import {
|
|
9
9
|
announcements,
|
|
10
10
|
getDb
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-42A3HEEH.js";
|
|
12
12
|
|
|
13
13
|
// src/lib/handlers/process-changelog.ts
|
|
14
14
|
import * as fs from "node:fs/promises";
|
|
@@ -119,4 +119,4 @@ async function processChangelog(deps = {}) {
|
|
|
119
119
|
export {
|
|
120
120
|
processChangelog
|
|
121
121
|
};
|
|
122
|
-
//# sourceMappingURL=chunk-
|
|
122
|
+
//# sourceMappingURL=chunk-MTLAIJED.js.map
|
|
@@ -203,6 +203,23 @@ function selectEmailProvider() {
|
|
|
203
203
|
return "resend";
|
|
204
204
|
return "noop";
|
|
205
205
|
}
|
|
206
|
+
var RESERVED_TLDS = /* @__PURE__ */ new Set(["test", "example", "invalid", "localhost"]);
|
|
207
|
+
var RESERVED_EXAMPLE_DOMAINS = /^example\.(com|net|org)$/;
|
|
208
|
+
function isUndeliverableRecipient(to) {
|
|
209
|
+
const addr = to.trim().toLowerCase();
|
|
210
|
+
const at = addr.lastIndexOf("@");
|
|
211
|
+
if (at < 0)
|
|
212
|
+
return false;
|
|
213
|
+
const domain = addr.slice(at + 1);
|
|
214
|
+
if (!domain)
|
|
215
|
+
return false;
|
|
216
|
+
const tld = domain.slice(domain.lastIndexOf(".") + 1);
|
|
217
|
+
if (RESERVED_TLDS.has(tld))
|
|
218
|
+
return true;
|
|
219
|
+
if (RESERVED_EXAMPLE_DOMAINS.test(domain))
|
|
220
|
+
return true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
206
223
|
var providerLogged = false;
|
|
207
224
|
async function sendEmail(params) {
|
|
208
225
|
const provider = selectEmailProvider();
|
|
@@ -213,6 +230,11 @@ async function sendEmail(params) {
|
|
|
213
230
|
}
|
|
214
231
|
switch (provider) {
|
|
215
232
|
case "resend":
|
|
233
|
+
if (isUndeliverableRecipient(params.to)) {
|
|
234
|
+
process.stderr.write(`[email] skipping real send to reserved/undeliverable address ${params.to} (RFC 2606/6761) \u2014 routed to noop to protect sender reputation
|
|
235
|
+
`);
|
|
236
|
+
return sendNoopEmail(params);
|
|
237
|
+
}
|
|
216
238
|
return sendResendEmail(params);
|
|
217
239
|
case "smtp":
|
|
218
240
|
return sendSmtpEmail(params);
|
|
@@ -225,4 +247,4 @@ export {
|
|
|
225
247
|
renderChangelogAnnouncement,
|
|
226
248
|
sendEmail
|
|
227
249
|
};
|
|
228
|
-
//# sourceMappingURL=chunk-
|
|
250
|
+
//# sourceMappingURL=chunk-QSN77T7C.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../packages/email/src/templates/changelog-announcement.ts", "../../../packages/email/src/providers/noop.ts", "../../../packages/email/src/providers/resend.ts", "../../../packages/email/src/providers/smtp.ts", "../../../packages/email/src/index.ts"],
|
|
4
|
-
"sourcesContent": ["// Product-announcement email template. Pure function \u2014 given a parsed\n// CHANGELOG entry, returns subject + html + text. No provider work, no\n// env reads, no IO.\n//\n// Design notes:\n// - Conventions mirror invitation.ts: inline styles only, viewport meta,\n// simple table layout, hand-rolled plain-text body, same colour palette.\n// - Section order is fixed (Added \u2192 Changed \u2192 Fixed \u2192 Removed \u2192\n// Deprecated \u2192 Security) regardless of insertion order in the input,\n// so a single deployment can't produce inconsistent ordering across\n// recipients.\n// - Footer carries the unsubscribe URL twice (styled link + raw text)\n// for CAN-SPAM compliance and because some clients suppress link\n// rendering.\n\nimport type { RenderedEmail } from './invitation.js';\n\nexport type ChangelogSectionName =\n | 'Added'\n | 'Changed'\n | 'Fixed'\n | 'Removed'\n | 'Deprecated'\n | 'Security';\n\nexport interface ChangelogAnnouncementInput {\n /** Verbatim version string from CHANGELOG, e.g. \"2026.06.05\". */\n version: string;\n /** ISO date YYYY-MM-DD. */\n date: string;\n /** Section name \u2192 array of bullet items. Items are plain text (no markdown). */\n sections: Partial<Record<ChangelogSectionName, string[]>>;\n /** Fully-qualified unsubscribe URL embedded in the footer. */\n unsubscribeUrl: string;\n /** Product home URL (e.g. https://rulemetric.com). For the header logo link. */\n productUrl: string;\n /** Optional recipient first name to personalize greeting; falls back to \"Hi there,\". */\n recipientName?: string;\n}\n\nconst ACCENT = '#5b6cff';\nconst TEXT_PRIMARY = '#1a1a1a';\nconst TEXT_MUTED = '#666666';\nconst BG_OUTER = '#f5f5f7';\nconst BG_CARD = '#ffffff';\nconst BORDER = '#e5e5e9';\n\n/** Documented render order for sections \u2014 applied regardless of input key order. */\nconst SECTION_ORDER: ChangelogSectionName[] = [\n 'Added',\n 'Changed',\n 'Fixed',\n 'Removed',\n 'Deprecated',\n 'Security',\n];\n\n/**\n * \"June 5, 2026\" \u2014 locale-independent format identical to invitation.ts.\n * Accepts an ISO YYYY-MM-DD string; parsed as UTC to avoid off-by-one in\n * recipient timezones west of the build server.\n */\nfunction formatIsoDate(iso: string): string {\n const months = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n ];\n const [y, m, d] = iso.split('-').map((s) => parseInt(s, 10));\n // Defensive: if the input is malformed we still want a string. Fall back\n // to the raw ISO so the recipient at least sees something coherent.\n if (!y || !m || !d || m < 1 || m > 12) return iso;\n return `${months[m - 1]} ${d}, ${y}`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/** Returns sections in canonical order, dropping missing/empty ones. */\nfunction orderedSections(\n sections: ChangelogAnnouncementInput['sections'],\n): Array<{ name: ChangelogSectionName; items: string[] }> {\n const out: Array<{ name: ChangelogSectionName; items: string[] }> = [];\n for (const name of SECTION_ORDER) {\n const items = sections[name];\n if (items && items.length > 0) {\n out.push({ name, items });\n }\n }\n return out;\n}\n\nexport function renderChangelogAnnouncement(\n input: ChangelogAnnouncementInput,\n): RenderedEmail {\n const { version, date, sections, unsubscribeUrl, productUrl, recipientName } = input;\n\n const formattedDate = formatIsoDate(date);\n const subject = `What's new in RuleMetric \u2014 ${formattedDate}`;\n const greeting = recipientName ? `Hi ${recipientName},` : 'Hi there,';\n const ordered = orderedSections(sections);\n const year = new Date().getUTCFullYear();\n\n // \u2500\u2500 HTML sections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const sectionsHtml = ordered\n .map(({ name, items }) => {\n const lis = items\n .map(\n (it) =>\n ` <li style=\"margin:0 0 6px 0;font-size:15px;line-height:1.5;color:${TEXT_PRIMARY};\">${escapeHtml(it)}</li>`,\n )\n .join('\\n');\n return ` <h3 style=\"margin:24px 0 8px 0;font-size:16px;line-height:1.3;font-weight:600;color:${TEXT_PRIMARY};\">${name}</h3>\n <ul style=\"margin:0 0 8px 0;padding-left:20px;\">\n${lis}\n </ul>`;\n })\n .join('\\n');\n\n // \u2500\u2500 HTML \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const html = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${escapeHtml(subject)}</title>\n </head>\n <body style=\"margin:0;padding:0;background:${BG_OUTER};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:${TEXT_PRIMARY};\">\n <table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:${BG_OUTER};padding:32px 16px;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" width=\"560\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"max-width:560px;background:${BG_CARD};border:1px solid ${BORDER};border-radius:8px;overflow:hidden;\">\n <tr>\n <td style=\"padding:24px 32px 8px 32px;\">\n <a href=\"${escapeHtml(productUrl)}\" style=\"text-decoration:none;color:${ACCENT};font-size:14px;font-weight:600;\">RuleMetric</a>\n </td>\n </tr>\n <tr>\n <td style=\"padding:8px 32px 8px 32px;\">\n <h1 style=\"margin:0 0 8px 0;font-size:22px;line-height:1.3;font-weight:600;color:${TEXT_PRIMARY};\">\n What's new \u2014 ${escapeHtml(formattedDate)}\n </h1>\n <p style=\"margin:0 0 8px 0;font-size:13px;line-height:1.5;color:${TEXT_MUTED};\">\n Release ${escapeHtml(version)}\n </p>\n <p style=\"margin:16px 0 0 0;font-size:15px;line-height:1.5;color:${TEXT_PRIMARY};\">\n ${escapeHtml(greeting)}\n </p>\n <p style=\"margin:8px 0 0 0;font-size:15px;line-height:1.5;color:${TEXT_PRIMARY};\">\n Here's what shipped in the latest RuleMetric release.\n </p>\n </td>\n </tr>\n <tr>\n <td style=\"padding:0 32px 16px 32px;\">\n${sectionsHtml}\n </td>\n </tr>\n <tr>\n <td style=\"padding:16px 32px 24px 32px;border-top:1px solid ${BORDER};\">\n <p style=\"margin:0 0 8px 0;font-size:12px;line-height:1.5;color:${TEXT_MUTED};\">\n You're receiving this because you signed up for RuleMetric product updates. If you'd rather not hear about new releases, you can unsubscribe at any time.\n </p>\n <p style=\"margin:0 0 4px 0;font-size:12px;line-height:1.5;\">\n <a href=\"${escapeHtml(unsubscribeUrl)}\" style=\"color:${ACCENT};text-decoration:underline;\">Unsubscribe</a>\n </p>\n <p style=\"margin:0 0 12px 0;font-size:12px;line-height:1.5;color:${TEXT_MUTED};word-break:break-all;\">\n ${escapeHtml(unsubscribeUrl)}\n </p>\n <p style=\"margin:0;font-size:11px;line-height:1.5;color:${TEXT_MUTED};\">\n RuleMetric \u2022 ${year}\n </p>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n\n // \u2500\u2500 Plain text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const textLines: string[] = [\n `What's new in RuleMetric \u2014 ${formattedDate}`,\n `Release ${version}`,\n '',\n greeting,\n '',\n `Here's what shipped in the latest RuleMetric release.`,\n '',\n ];\n for (const { name, items } of ordered) {\n textLines.push(`${name}:`);\n for (const it of items) {\n textLines.push(` - ${it}`);\n }\n textLines.push('');\n }\n textLines.push(\n '--',\n `You're receiving this because you signed up for RuleMetric product updates.`,\n `Unsubscribe: ${unsubscribeUrl}`,\n '',\n `RuleMetric \u2022 ${year}`,\n );\n const text = textLines.join('\\n');\n\n return { subject, html, text };\n}\n", "// No-op email provider. Used in dev/test when no real email infrastructure\n// is configured. Logs the destination + subject to stderr so operators can\n// see that a send was requested, then returns a synthetic id. Never throws.\n//\n// This is intentionally the default \u2014 it lets the test suite run without\n// any email environment variables and lets local development work without\n// an email account. Switch to `resend` by setting RESEND_API_KEY, or force\n// noop explicitly with RULEMETRIC_EMAIL_PROVIDER=noop.\n\nimport { randomBytes } from 'node:crypto';\nimport type { SendEmailParams, SendEmailResult } from '../index.js';\n\nexport async function sendNoopEmail(params: SendEmailParams): Promise<SendEmailResult> {\n // Use stderr (same convention as the dev-mode invite log) so it doesn't\n // pollute stdout if a caller is parsing JSON from there.\n const keyNote = params.idempotencyKey ? ` idempotencyKey=${params.idempotencyKey}` : '';\n process.stderr.write(\n `[email:noop] would send to=${params.to} subject=${JSON.stringify(params.subject)}${keyNote}\\n`,\n );\n return {\n id: `noop-${randomBytes(8).toString('hex')}`,\n provider: 'noop',\n };\n}\n", "// Resend provider. Activated when RESEND_API_KEY is set. The Resend SDK is\n// imported lazily so that environments without `resend` installed (or where\n// the dependency hasn't been hoisted yet) don't crash at module-load time \u2014\n// only when a send is actually attempted via this provider.\n//\n// `from` is computed from RESEND_FROM_EMAIL (default: onboarding@resend.dev,\n// which works out of the box with a fresh Resend account for testing).\n\nimport type { SendEmailParams, SendEmailResult } from '../index.js';\n\nlet cachedClient: {\n emails: {\n send: (input: ResendSendInput, options?: ResendSendOptions) => Promise<ResendSendResult>;\n };\n} | null = null;\n\ninterface ResendSendInput {\n from: string;\n to: string | string[];\n subject: string;\n html: string;\n text: string;\n}\n\ninterface ResendSendOptions {\n idempotencyKey?: string;\n}\n\ninterface ResendSendResult {\n data: { id: string } | null;\n error: { message: string; name?: string } | null;\n}\n\nasync function getClient() {\n if (cachedClient) return cachedClient;\n const apiKey = process.env.RESEND_API_KEY;\n if (!apiKey) {\n throw new Error('RESEND_API_KEY is not set');\n }\n // Lazy ESM import. Resend ships ESM-compatible types in its modern releases.\n const { Resend } = (await import('resend')) as {\n Resend: new (key: string) => {\n emails: {\n send: (input: ResendSendInput, options?: ResendSendOptions) => Promise<ResendSendResult>;\n };\n };\n };\n cachedClient = new Resend(apiKey);\n return cachedClient;\n}\n\nexport async function sendResendEmail(\n params: SendEmailParams,\n): Promise<SendEmailResult> {\n const fromName = params.fromName ?? 'RuleMetric';\n const fromEmail = process.env.RESEND_FROM_EMAIL ?? 'onboarding@resend.dev';\n const from = `${fromName} <${fromEmail}>`;\n\n const client = await getClient();\n // Idempotency-Key is a request option in the Resend SDK (not a body field).\n // Resend caches responses keyed on (api_key, idempotency_key) for 24 hours \u2014\n // a duplicate send with the same key returns the original response.\n const options: ResendSendOptions | undefined = params.idempotencyKey\n ? { idempotencyKey: params.idempotencyKey }\n : undefined;\n const result = await client.emails.send(\n {\n from,\n to: params.to,\n subject: params.subject,\n html: params.html,\n text: params.text,\n },\n options,\n );\n\n if (result.error) {\n throw new Error(`Resend send failed: ${result.error.message}`);\n }\n if (!result.data) {\n throw new Error('Resend send returned no data and no error');\n }\n\n return { id: result.data.id, provider: 'resend' };\n}\n\n// For tests: reset the cached client so a fresh API key is picked up.\nexport function _resetResendClient() {\n cachedClient = null;\n}\n", "// SMTP provider \u2014 stub for now.\n//\n// We don't ship `nodemailer` yet. If you want SMTP-based delivery (e.g.\n// to use Supabase's project-level SMTP config), this is where it goes:\n// add `nodemailer` to package.json, parse SMTP_* env vars (or\n// SUPABASE_SMTP_*), build a transport, and call `transport.sendMail(...)`.\n//\n// Until that's wired, this stub throws \u2014 so selectEmailProvider() never\n// chooses 'smtp' unless a future env-var (`RULEMETRIC_EMAIL_PROVIDER=smtp`)\n// is explicitly set, in which case the caller will get a clear error\n// rather than silent failure.\n\nimport type { SendEmailParams, SendEmailResult } from '../index.js';\n\nexport async function sendSmtpEmail(\n _params: SendEmailParams,\n): Promise<SendEmailResult> {\n throw new Error(\n 'SMTP provider not implemented. Set RESEND_API_KEY to use Resend, or unset RULEMETRIC_EMAIL_PROVIDER to fall back to noop.',\n );\n}\n", "// @rulemetric/email \u2014 small abstraction over a transactional-email provider.\n//\n// At module load we DON'T pick a provider \u2014 we resolve it on the first call\n// to `sendEmail`. This is so tests can flip env vars (RESEND_API_KEY,\n// RULEMETRIC_EMAIL_PROVIDER) between cases and observe different behaviour\n// without juggling module imports.\n//\n// Provider selection (highest to lowest precedence):\n// 1. RULEMETRIC_EMAIL_PROVIDER env var ('resend' | 'smtp' | 'noop') \u2014 explicit.\n// 2. If RESEND_API_KEY is set \u2192 'resend'.\n// 3. Otherwise \u2192 'noop' (logs to stderr, returns synthetic id).\n//\n// `noop` is the default to keep the test suite from needing real email\n// infrastructure. Failures from real providers bubble up \u2014 the caller (e.g.\n// the invitation route) decides whether a delivery failure should fail the\n// request or just be logged.\n\nimport { sendNoopEmail } from './providers/noop.js';\nimport { sendResendEmail } from './providers/resend.js';\nimport { sendSmtpEmail } from './providers/smtp.js';\n\nexport interface SendEmailParams {\n to: string;\n subject: string;\n html: string;\n text: string;\n /** Display name for the From header. Defaults to 'RuleMetric'. */\n fromName?: string;\n /**\n * Provider-side idempotency key. When set, the provider deduplicates\n * concurrent / retried sends with the same key for a provider-defined\n * window (Resend: 24h). Required for safe-retry email pipelines \u2014\n * callers should derive this from a stable per-recipient row id\n * (e.g. `announcement_deliveries.id`), NOT a per-attempt nonce.\n */\n idempotencyKey?: string;\n}\n\nexport type EmailProvider = 'resend' | 'noop' | 'smtp';\n\nexport interface SendEmailResult {\n id: string;\n provider: EmailProvider;\n}\n\n/** Pure function \u2014 exported for testing. Reads `process.env` each call. */\nexport function selectEmailProvider(): EmailProvider {\n const explicit = process.env.RULEMETRIC_EMAIL_PROVIDER;\n if (explicit === 'resend' || explicit === 'smtp' || explicit === 'noop') {\n return explicit;\n }\n if (process.env.RESEND_API_KEY) return 'resend';\n return 'noop';\n}\n\n// Track whether we've logged the active provider yet \u2014 log-once so dev/test\n// stderr doesn't get spammed.\nlet providerLogged = false;\n\nexport async function sendEmail(params: SendEmailParams): Promise<SendEmailResult> {\n const provider = selectEmailProvider();\n if (!providerLogged) {\n providerLogged = true;\n process.stderr.write(`[email] active provider: ${provider}\\n`);\n }\n switch (provider) {\n case 'resend':\n return sendResendEmail(params);\n case 'smtp':\n return sendSmtpEmail(params);\n case 'noop':\n return sendNoopEmail(params);\n }\n}\n\n/** Test helper: reset the provider-logged flag. */\nexport function _resetEmailLogState() {\n providerLogged = false;\n}\n\nexport { renderInvitation } from './templates/invitation.js';\nexport type { InvitationTemplateInput, RenderedEmail } from './templates/invitation.js';\nexport { renderChangelogAnnouncement } from './templates/changelog-announcement.js';\nexport type {\n ChangelogAnnouncementInput,\n ChangelogSectionName,\n} from './templates/changelog-announcement.js';\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["// Product-announcement email template. Pure function \u2014 given a parsed\n// CHANGELOG entry, returns subject + html + text. No provider work, no\n// env reads, no IO.\n//\n// Design notes:\n// - Conventions mirror invitation.ts: inline styles only, viewport meta,\n// simple table layout, hand-rolled plain-text body, same colour palette.\n// - Section order is fixed (Added \u2192 Changed \u2192 Fixed \u2192 Removed \u2192\n// Deprecated \u2192 Security) regardless of insertion order in the input,\n// so a single deployment can't produce inconsistent ordering across\n// recipients.\n// - Footer carries the unsubscribe URL twice (styled link + raw text)\n// for CAN-SPAM compliance and because some clients suppress link\n// rendering.\n\nimport type { RenderedEmail } from './invitation.js';\n\nexport type ChangelogSectionName =\n | 'Added'\n | 'Changed'\n | 'Fixed'\n | 'Removed'\n | 'Deprecated'\n | 'Security';\n\nexport interface ChangelogAnnouncementInput {\n /** Verbatim version string from CHANGELOG, e.g. \"2026.06.05\". */\n version: string;\n /** ISO date YYYY-MM-DD. */\n date: string;\n /** Section name \u2192 array of bullet items. Items are plain text (no markdown). */\n sections: Partial<Record<ChangelogSectionName, string[]>>;\n /** Fully-qualified unsubscribe URL embedded in the footer. */\n unsubscribeUrl: string;\n /** Product home URL (e.g. https://rulemetric.com). For the header logo link. */\n productUrl: string;\n /** Optional recipient first name to personalize greeting; falls back to \"Hi there,\". */\n recipientName?: string;\n}\n\nconst ACCENT = '#5b6cff';\nconst TEXT_PRIMARY = '#1a1a1a';\nconst TEXT_MUTED = '#666666';\nconst BG_OUTER = '#f5f5f7';\nconst BG_CARD = '#ffffff';\nconst BORDER = '#e5e5e9';\n\n/** Documented render order for sections \u2014 applied regardless of input key order. */\nconst SECTION_ORDER: ChangelogSectionName[] = [\n 'Added',\n 'Changed',\n 'Fixed',\n 'Removed',\n 'Deprecated',\n 'Security',\n];\n\n/**\n * \"June 5, 2026\" \u2014 locale-independent format identical to invitation.ts.\n * Accepts an ISO YYYY-MM-DD string; parsed as UTC to avoid off-by-one in\n * recipient timezones west of the build server.\n */\nfunction formatIsoDate(iso: string): string {\n const months = [\n 'January', 'February', 'March', 'April', 'May', 'June',\n 'July', 'August', 'September', 'October', 'November', 'December',\n ];\n const [y, m, d] = iso.split('-').map((s) => parseInt(s, 10));\n // Defensive: if the input is malformed we still want a string. Fall back\n // to the raw ISO so the recipient at least sees something coherent.\n if (!y || !m || !d || m < 1 || m > 12) return iso;\n return `${months[m - 1]} ${d}, ${y}`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/** Returns sections in canonical order, dropping missing/empty ones. */\nfunction orderedSections(\n sections: ChangelogAnnouncementInput['sections'],\n): Array<{ name: ChangelogSectionName; items: string[] }> {\n const out: Array<{ name: ChangelogSectionName; items: string[] }> = [];\n for (const name of SECTION_ORDER) {\n const items = sections[name];\n if (items && items.length > 0) {\n out.push({ name, items });\n }\n }\n return out;\n}\n\nexport function renderChangelogAnnouncement(\n input: ChangelogAnnouncementInput,\n): RenderedEmail {\n const { version, date, sections, unsubscribeUrl, productUrl, recipientName } = input;\n\n const formattedDate = formatIsoDate(date);\n const subject = `What's new in RuleMetric \u2014 ${formattedDate}`;\n const greeting = recipientName ? `Hi ${recipientName},` : 'Hi there,';\n const ordered = orderedSections(sections);\n const year = new Date().getUTCFullYear();\n\n // \u2500\u2500 HTML sections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const sectionsHtml = ordered\n .map(({ name, items }) => {\n const lis = items\n .map(\n (it) =>\n ` <li style=\"margin:0 0 6px 0;font-size:15px;line-height:1.5;color:${TEXT_PRIMARY};\">${escapeHtml(it)}</li>`,\n )\n .join('\\n');\n return ` <h3 style=\"margin:24px 0 8px 0;font-size:16px;line-height:1.3;font-weight:600;color:${TEXT_PRIMARY};\">${name}</h3>\n <ul style=\"margin:0 0 8px 0;padding-left:20px;\">\n${lis}\n </ul>`;\n })\n .join('\\n');\n\n // \u2500\u2500 HTML \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const html = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${escapeHtml(subject)}</title>\n </head>\n <body style=\"margin:0;padding:0;background:${BG_OUTER};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:${TEXT_PRIMARY};\">\n <table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:${BG_OUTER};padding:32px 16px;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" width=\"560\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"max-width:560px;background:${BG_CARD};border:1px solid ${BORDER};border-radius:8px;overflow:hidden;\">\n <tr>\n <td style=\"padding:24px 32px 8px 32px;\">\n <a href=\"${escapeHtml(productUrl)}\" style=\"text-decoration:none;color:${ACCENT};font-size:14px;font-weight:600;\">RuleMetric</a>\n </td>\n </tr>\n <tr>\n <td style=\"padding:8px 32px 8px 32px;\">\n <h1 style=\"margin:0 0 8px 0;font-size:22px;line-height:1.3;font-weight:600;color:${TEXT_PRIMARY};\">\n What's new \u2014 ${escapeHtml(formattedDate)}\n </h1>\n <p style=\"margin:0 0 8px 0;font-size:13px;line-height:1.5;color:${TEXT_MUTED};\">\n Release ${escapeHtml(version)}\n </p>\n <p style=\"margin:16px 0 0 0;font-size:15px;line-height:1.5;color:${TEXT_PRIMARY};\">\n ${escapeHtml(greeting)}\n </p>\n <p style=\"margin:8px 0 0 0;font-size:15px;line-height:1.5;color:${TEXT_PRIMARY};\">\n Here's what shipped in the latest RuleMetric release.\n </p>\n </td>\n </tr>\n <tr>\n <td style=\"padding:0 32px 16px 32px;\">\n${sectionsHtml}\n </td>\n </tr>\n <tr>\n <td style=\"padding:16px 32px 24px 32px;border-top:1px solid ${BORDER};\">\n <p style=\"margin:0 0 8px 0;font-size:12px;line-height:1.5;color:${TEXT_MUTED};\">\n You're receiving this because you signed up for RuleMetric product updates. If you'd rather not hear about new releases, you can unsubscribe at any time.\n </p>\n <p style=\"margin:0 0 4px 0;font-size:12px;line-height:1.5;\">\n <a href=\"${escapeHtml(unsubscribeUrl)}\" style=\"color:${ACCENT};text-decoration:underline;\">Unsubscribe</a>\n </p>\n <p style=\"margin:0 0 12px 0;font-size:12px;line-height:1.5;color:${TEXT_MUTED};word-break:break-all;\">\n ${escapeHtml(unsubscribeUrl)}\n </p>\n <p style=\"margin:0;font-size:11px;line-height:1.5;color:${TEXT_MUTED};\">\n RuleMetric \u2022 ${year}\n </p>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n\n // \u2500\u2500 Plain text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const textLines: string[] = [\n `What's new in RuleMetric \u2014 ${formattedDate}`,\n `Release ${version}`,\n '',\n greeting,\n '',\n `Here's what shipped in the latest RuleMetric release.`,\n '',\n ];\n for (const { name, items } of ordered) {\n textLines.push(`${name}:`);\n for (const it of items) {\n textLines.push(` - ${it}`);\n }\n textLines.push('');\n }\n textLines.push(\n '--',\n `You're receiving this because you signed up for RuleMetric product updates.`,\n `Unsubscribe: ${unsubscribeUrl}`,\n '',\n `RuleMetric \u2022 ${year}`,\n );\n const text = textLines.join('\\n');\n\n return { subject, html, text };\n}\n", "// No-op email provider. Used in dev/test when no real email infrastructure\n// is configured. Logs the destination + subject to stderr so operators can\n// see that a send was requested, then returns a synthetic id. Never throws.\n//\n// This is intentionally the default \u2014 it lets the test suite run without\n// any email environment variables and lets local development work without\n// an email account. Switch to `resend` by setting RESEND_API_KEY, or force\n// noop explicitly with RULEMETRIC_EMAIL_PROVIDER=noop.\n\nimport { randomBytes } from 'node:crypto';\nimport type { SendEmailParams, SendEmailResult } from '../index.js';\n\nexport async function sendNoopEmail(params: SendEmailParams): Promise<SendEmailResult> {\n // Use stderr (same convention as the dev-mode invite log) so it doesn't\n // pollute stdout if a caller is parsing JSON from there.\n const keyNote = params.idempotencyKey ? ` idempotencyKey=${params.idempotencyKey}` : '';\n process.stderr.write(\n `[email:noop] would send to=${params.to} subject=${JSON.stringify(params.subject)}${keyNote}\\n`,\n );\n return {\n id: `noop-${randomBytes(8).toString('hex')}`,\n provider: 'noop',\n };\n}\n", "// Resend provider. Activated when RESEND_API_KEY is set. The Resend SDK is\n// imported lazily so that environments without `resend` installed (or where\n// the dependency hasn't been hoisted yet) don't crash at module-load time \u2014\n// only when a send is actually attempted via this provider.\n//\n// `from` is computed from RESEND_FROM_EMAIL (default: onboarding@resend.dev,\n// which works out of the box with a fresh Resend account for testing).\n\nimport type { SendEmailParams, SendEmailResult } from '../index.js';\n\nlet cachedClient: {\n emails: {\n send: (input: ResendSendInput, options?: ResendSendOptions) => Promise<ResendSendResult>;\n };\n} | null = null;\n\ninterface ResendSendInput {\n from: string;\n to: string | string[];\n subject: string;\n html: string;\n text: string;\n}\n\ninterface ResendSendOptions {\n idempotencyKey?: string;\n}\n\ninterface ResendSendResult {\n data: { id: string } | null;\n error: { message: string; name?: string } | null;\n}\n\nasync function getClient() {\n if (cachedClient) return cachedClient;\n const apiKey = process.env.RESEND_API_KEY;\n if (!apiKey) {\n throw new Error('RESEND_API_KEY is not set');\n }\n // Lazy ESM import. Resend ships ESM-compatible types in its modern releases.\n const { Resend } = (await import('resend')) as {\n Resend: new (key: string) => {\n emails: {\n send: (input: ResendSendInput, options?: ResendSendOptions) => Promise<ResendSendResult>;\n };\n };\n };\n cachedClient = new Resend(apiKey);\n return cachedClient;\n}\n\nexport async function sendResendEmail(\n params: SendEmailParams,\n): Promise<SendEmailResult> {\n const fromName = params.fromName ?? 'RuleMetric';\n const fromEmail = process.env.RESEND_FROM_EMAIL ?? 'onboarding@resend.dev';\n const from = `${fromName} <${fromEmail}>`;\n\n const client = await getClient();\n // Idempotency-Key is a request option in the Resend SDK (not a body field).\n // Resend caches responses keyed on (api_key, idempotency_key) for 24 hours \u2014\n // a duplicate send with the same key returns the original response.\n const options: ResendSendOptions | undefined = params.idempotencyKey\n ? { idempotencyKey: params.idempotencyKey }\n : undefined;\n const result = await client.emails.send(\n {\n from,\n to: params.to,\n subject: params.subject,\n html: params.html,\n text: params.text,\n },\n options,\n );\n\n if (result.error) {\n throw new Error(`Resend send failed: ${result.error.message}`);\n }\n if (!result.data) {\n throw new Error('Resend send returned no data and no error');\n }\n\n return { id: result.data.id, provider: 'resend' };\n}\n\n// For tests: reset the cached client so a fresh API key is picked up.\nexport function _resetResendClient() {\n cachedClient = null;\n}\n", "// SMTP provider \u2014 stub for now.\n//\n// We don't ship `nodemailer` yet. If you want SMTP-based delivery (e.g.\n// to use Supabase's project-level SMTP config), this is where it goes:\n// add `nodemailer` to package.json, parse SMTP_* env vars (or\n// SUPABASE_SMTP_*), build a transport, and call `transport.sendMail(...)`.\n//\n// Until that's wired, this stub throws \u2014 so selectEmailProvider() never\n// chooses 'smtp' unless a future env-var (`RULEMETRIC_EMAIL_PROVIDER=smtp`)\n// is explicitly set, in which case the caller will get a clear error\n// rather than silent failure.\n\nimport type { SendEmailParams, SendEmailResult } from '../index.js';\n\nexport async function sendSmtpEmail(\n _params: SendEmailParams,\n): Promise<SendEmailResult> {\n throw new Error(\n 'SMTP provider not implemented. Set RESEND_API_KEY to use Resend, or unset RULEMETRIC_EMAIL_PROVIDER to fall back to noop.',\n );\n}\n", "// @rulemetric/email \u2014 small abstraction over a transactional-email provider.\n//\n// At module load we DON'T pick a provider \u2014 we resolve it on the first call\n// to `sendEmail`. This is so tests can flip env vars (RESEND_API_KEY,\n// RULEMETRIC_EMAIL_PROVIDER) between cases and observe different behaviour\n// without juggling module imports.\n//\n// Provider selection (highest to lowest precedence):\n// 1. RULEMETRIC_EMAIL_PROVIDER env var ('resend' | 'smtp' | 'noop') \u2014 explicit.\n// 2. If RESEND_API_KEY is set \u2192 'resend'.\n// 3. Otherwise \u2192 'noop' (logs to stderr, returns synthetic id).\n//\n// `noop` is the default to keep the test suite from needing real email\n// infrastructure. Failures from real providers bubble up \u2014 the caller (e.g.\n// the invitation route) decides whether a delivery failure should fail the\n// request or just be logged.\n\nimport { sendNoopEmail } from './providers/noop.js';\nimport { sendResendEmail } from './providers/resend.js';\nimport { sendSmtpEmail } from './providers/smtp.js';\n\nexport interface SendEmailParams {\n to: string;\n subject: string;\n html: string;\n text: string;\n /** Display name for the From header. Defaults to 'RuleMetric'. */\n fromName?: string;\n /**\n * Provider-side idempotency key. When set, the provider deduplicates\n * concurrent / retried sends with the same key for a provider-defined\n * window (Resend: 24h). Required for safe-retry email pipelines \u2014\n * callers should derive this from a stable per-recipient row id\n * (e.g. `announcement_deliveries.id`), NOT a per-attempt nonce.\n */\n idempotencyKey?: string;\n}\n\nexport type EmailProvider = 'resend' | 'noop' | 'smtp';\n\nexport interface SendEmailResult {\n id: string;\n provider: EmailProvider;\n}\n\n/** Pure function \u2014 exported for testing. Reads `process.env` each call. */\nexport function selectEmailProvider(): EmailProvider {\n const explicit = process.env.RULEMETRIC_EMAIL_PROVIDER;\n if (explicit === 'resend' || explicit === 'smtp' || explicit === 'noop') {\n return explicit;\n }\n if (process.env.RESEND_API_KEY) return 'resend';\n return 'noop';\n}\n\n// Reserved TLDs that, per RFC 2606 (.test/.example/.invalid/.localhost +\n// example.com/net/org) and RFC 6761, are guaranteed never to resolve to a real\n// mailbox.\nconst RESERVED_TLDS = new Set(['test', 'example', 'invalid', 'localhost']);\nconst RESERVED_EXAMPLE_DOMAINS = /^example\\.(com|net|org)$/;\n\n/**\n * True when `to` addresses a reserved / structurally-undeliverable domain.\n *\n * Why this exists: our unit + e2e suites mint thousands of `*@rulemetric.test`\n * users. Handing those to a real provider (Resend) produces guaranteed HARD\n * BOUNCES, and a high bounce rate poisons the sending domain's reputation \u2014\n * which pushes legitimate transactional mail (password-reset links, invites)\n * into recipients' spam folders. An env-only guard\n * (`RULEMETRIC_EMAIL_PROVIDER=noop` in test) proved leaky: any runner that\n * forgets it (root vitest, CI with prod-like email secrets) sends for real.\n * This predicate makes the guard env-independent at the `sendEmail` boundary.\n *\n * Pure; exported for testing. Returns `false` for malformed input so the\n * provider remains responsible for address validation.\n */\nexport function isUndeliverableRecipient(to: string): boolean {\n const addr = to.trim().toLowerCase();\n const at = addr.lastIndexOf('@');\n if (at < 0) return false; // malformed \u2014 let the provider validate\n const domain = addr.slice(at + 1);\n if (!domain) return false;\n const tld = domain.slice(domain.lastIndexOf('.') + 1);\n if (RESERVED_TLDS.has(tld)) return true;\n if (RESERVED_EXAMPLE_DOMAINS.test(domain)) return true;\n return false;\n}\n\n// Track whether we've logged the active provider yet \u2014 log-once so dev/test\n// stderr doesn't get spammed.\nlet providerLogged = false;\n\nexport async function sendEmail(params: SendEmailParams): Promise<SendEmailResult> {\n const provider = selectEmailProvider();\n if (!providerLogged) {\n providerLogged = true;\n process.stderr.write(`[email] active provider: ${provider}\\n`);\n }\n switch (provider) {\n case 'resend':\n // Never hand a reserved/undeliverable address to the real provider \u2014 it\n // would hard-bounce and degrade domain reputation. Treat as a no-op\n // success (the address is undeliverable by definition, so callers that\n // only log failures behave identically).\n if (isUndeliverableRecipient(params.to)) {\n process.stderr.write(\n `[email] skipping real send to reserved/undeliverable address ` +\n `${params.to} (RFC 2606/6761) \u2014 routed to noop to protect sender reputation\\n`,\n );\n return sendNoopEmail(params);\n }\n return sendResendEmail(params);\n case 'smtp':\n // When a real SMTP sender is implemented here, apply the same\n // isUndeliverableRecipient() guard before dispatching.\n return sendSmtpEmail(params);\n case 'noop':\n return sendNoopEmail(params);\n }\n}\n\n/** Test helper: reset the provider-logged flag. */\nexport function _resetEmailLogState() {\n providerLogged = false;\n}\n\nexport { renderInvitation } from './templates/invitation.js';\nexport type { InvitationTemplateInput, RenderedEmail } from './templates/invitation.js';\nexport { renderChangelogAnnouncement } from './templates/changelog-announcement.js';\nexport type {\n ChangelogAnnouncementInput,\n ChangelogSectionName,\n} from './templates/changelog-announcement.js';\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ensureWorktree
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FUS5C4SB.js";
|
|
4
4
|
import {
|
|
5
5
|
PermanentJobError
|
|
6
6
|
} from "./chunk-DGHWRQXL.js";
|
|
@@ -39,7 +39,7 @@ async function processLaunch(payload, helpers) {
|
|
|
39
39
|
log(`Worktree resolved: ${config.projectPath} (branch ${config.gitBranch})`);
|
|
40
40
|
}
|
|
41
41
|
log(`Launching ${tool}...`);
|
|
42
|
-
const { getLauncher } = await import("./dist-
|
|
42
|
+
const { getLauncher } = await import("./dist-NEECHU47.js");
|
|
43
43
|
const launcher = getLauncher(tool);
|
|
44
44
|
const launcherResult = await launcher.launch(config);
|
|
45
45
|
log(`Launch completed: sessionId=${launcherResult.sessionId}`);
|
|
@@ -58,4 +58,4 @@ export {
|
|
|
58
58
|
resolveWorktreeForLaunch,
|
|
59
59
|
processLaunch
|
|
60
60
|
};
|
|
61
|
-
//# sourceMappingURL=chunk-
|
|
61
|
+
//# sourceMappingURL=chunk-XMWHTOCC.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
removeWorktree
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FUS5C4SB.js";
|
|
4
4
|
import {
|
|
5
5
|
agentRuns,
|
|
6
6
|
getDb,
|
|
7
7
|
sessions
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-42A3HEEH.js";
|
|
9
9
|
import {
|
|
10
10
|
PermanentJobError
|
|
11
11
|
} from "./chunk-DGHWRQXL.js";
|
|
@@ -96,4 +96,4 @@ async function processRunCleanup(payload, deps = {}) {
|
|
|
96
96
|
export {
|
|
97
97
|
processRunCleanup
|
|
98
98
|
};
|
|
99
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-Y2ARLMWZ.js.map
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getDb,
|
|
6
6
|
sessionEvents,
|
|
7
7
|
sessions
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-42A3HEEH.js";
|
|
9
9
|
|
|
10
10
|
// src/lib/handlers/process-session-goal.ts
|
|
11
11
|
import { eq, asc, sql } from "drizzle-orm";
|
|
@@ -52,4 +52,4 @@ async function processSessionGoal(payload, deps = {}) {
|
|
|
52
52
|
export {
|
|
53
53
|
processSessionGoal
|
|
54
54
|
};
|
|
55
|
-
//# sourceMappingURL=chunk-
|
|
55
|
+
//# sourceMappingURL=chunk-YEBBX4HQ.js.map
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runAgentLoop
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-LCT6MT2V.js";
|
|
4
4
|
import "../../chunk-JX5UI6RW.js";
|
|
5
5
|
import "../../chunk-EHABPBGH.js";
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
6
|
+
import "../../chunk-XMWHTOCC.js";
|
|
7
|
+
import "../../chunk-Y2ARLMWZ.js";
|
|
8
|
+
import "../../chunk-FUS5C4SB.js";
|
|
9
9
|
import "../../chunk-W53GKIZQ.js";
|
|
10
|
-
import "../../chunk-
|
|
10
|
+
import "../../chunk-YEBBX4HQ.js";
|
|
11
11
|
import "../../chunk-XZXS2W24.js";
|
|
12
12
|
import "../../chunk-RQ2TMLKG.js";
|
|
13
13
|
import "../../chunk-YBNW7ARH.js";
|
|
14
14
|
import "../../chunk-FZKLLNDS.js";
|
|
15
|
-
import "../../chunk-
|
|
15
|
+
import "../../chunk-2BCPVUSF.js";
|
|
16
16
|
import "../../chunk-K3BFZ7KG.js";
|
|
17
|
-
import "../../chunk-
|
|
17
|
+
import "../../chunk-QSN77T7C.js";
|
|
18
18
|
import {
|
|
19
19
|
closeDb
|
|
20
|
-
} from "../../chunk-
|
|
20
|
+
} from "../../chunk-42A3HEEH.js";
|
|
21
21
|
import "../../chunk-BD7CY42U.js";
|
|
22
22
|
import "../../chunk-QJXAULRC.js";
|
|
23
23
|
import "../../chunk-DGHWRQXL.js";
|
|
@@ -25,7 +25,7 @@ import "../../chunk-J7N3DLH6.js";
|
|
|
25
25
|
import "../../chunk-52WIYRZH.js";
|
|
26
26
|
import "../../chunk-IWYCRLEK.js";
|
|
27
27
|
import "../../chunk-EKP32DLN.js";
|
|
28
|
-
import "../../chunk-
|
|
28
|
+
import "../../chunk-4QGATD5Y.js";
|
|
29
29
|
import "../../chunk-OQSQC7VB.js";
|
|
30
30
|
import "../../chunk-KRBQLMOP.js";
|
|
31
31
|
import "../../chunk-3TIMQ3O6.js";
|
|
@@ -133,8 +133,6 @@ var enabled = false;
|
|
|
133
133
|
function initSentry(config) {
|
|
134
134
|
if (enabled)
|
|
135
135
|
return false;
|
|
136
|
-
if (process.env.NODE_ENV === "production")
|
|
137
|
-
return false;
|
|
138
136
|
const dsn = process.env.SENTRY_DSN;
|
|
139
137
|
if (!dsn)
|
|
140
138
|
return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../packages/telemetry/src/init.ts", "../../../../../packages/telemetry/src/otlp-headers.ts", "../../../../../packages/telemetry/src/hono-middleware.ts", "../../../../../packages/telemetry/src/mcp-instrumentation.ts", "../../../../../packages/telemetry/src/span-attributes.ts", "../../../../../packages/telemetry/src/sentry.ts", "../../../src/commands/evals/agent.ts"],
|
|
4
|
-
"sourcesContent": ["import { trace, context, type Tracer } from '@opentelemetry/api';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport {\n BatchSpanProcessor,\n ConsoleSpanExporter,\n SimpleSpanProcessor,\n} from '@opentelemetry/sdk-trace-node';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\nimport { parseOtlpHeaders } from './otlp-headers.js';\n\nlet sdk: NodeSDK | null = null;\n\nexport interface TelemetryConfig {\n serviceName: string;\n serviceVersion?: string;\n}\n\n/**\n * Boot states for `initTelemetry`. Returned discriminator lets callers\n * emit a precise boot log:\n * - 'otlp' \u2014 spans ship over OTLP to a real backend (Honeycomb etc.)\n * - 'console' \u2014 spans print to stdout as JSON (Fly logs ingest them);\n * the \"open + zero third-party\" default\n * - 'off' \u2014 telemetry globally disabled (TELEMETRY_ENABLED=false)\n * OR a prior `initTelemetry()` already booted the SDK\n * in this process (idempotent skip)\n */\nexport type TelemetryBootState = 'otlp' | 'console' | 'silent' | 'off';\n\nexport function isTelemetryEnabled(): boolean {\n return process.env.TELEMETRY_ENABLED !== 'false';\n}\n\n/**\n * Initialize OpenTelemetry tracing. Two transport modes, both built on\n * the same OTel SDK \u2014 only the exporter changes:\n *\n * - OTLP backend \u2192 set `OTEL_EXPORTER_OTLP_ENDPOINT` (+ optional\n * `OTEL_EXPORTER_OTLP_HEADERS` for vendor auth)\n * - Console (stdout) \u2192 the default when no endpoint is set\n *\n * The console path is intentionally on-by-default. Without a real backend\n * configured, spans are written to stdout as JSON and end up in Fly's log\n * stream alongside everything else \u2014 searchable, structured, and you\n * stay inside the open OpenTelemetry spec without paying a third party.\n * Swap to OTLP later by setting one env var; no code change needed.\n *\n * Returns `'off'` when:\n * - TELEMETRY_ENABLED=false (operator kill switch \u2014 beats any config)\n * - The SDK was already started in this process (idempotent skip)\n *\n * See packages/telemetry/src/init.test.ts for the full state table.\n */\nexport function initTelemetry(config: TelemetryConfig): TelemetryBootState {\n if (!isTelemetryEnabled()) {\n return 'off';\n }\n if (sdk !== null) {\n // OTel's global tracer provider gets set on first start(); calling\n // start() twice triggers an SDK warning. Treat second-call as a no-op.\n return 'off';\n }\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? '0.1.0',\n });\n\n const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n let mode: TelemetryBootState;\n let spanProcessor: BatchSpanProcessor | SimpleSpanProcessor | undefined;\n\n if (endpoint) {\n // OTLP path. Headers via OTEL_EXPORTER_OTLP_HEADERS (W3C convention,\n // comma-separated key=value pairs). Parsed explicitly rather than\n // relying on SDK-version-specific auto-discovery \u2014 predictable boot\n // matters more than tracking whatever the current default is.\n // Honeycomb: x-honeycomb-team=hcaik_xxx (plus optional dataset)\n // Grafana Cloud: authorization=Basic <base64creds>\n const headers = parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);\n const exporter = new OTLPTraceExporter({\n url: `${endpoint}/v1/traces`,\n headers,\n });\n spanProcessor = new BatchSpanProcessor(exporter);\n mode = 'otlp';\n } else if (process.env.OTEL_SPAN_EXPORT === 'none') {\n // Silent path: trace context still propagates (so trace IDs are available\n // for correlation and an OTLP endpoint can be added later with zero code\n // change), but NO span processor is registered \u2014 nothing is written to\n // stdout. This exists because the ConsoleSpanExporter below writes one\n // JSON span per ended span, and on the local launchd API (no OTLP\n // endpoint, fixed log file, no Fly-managed rotation) that grew api-stdout\n // to 370MB / ~400k spans. Set OTEL_SPAN_EXPORT=none on that process; Fly\n // leaves it unset so console spans stay grep-able in its managed log stream.\n spanProcessor = undefined;\n mode = 'silent';\n } else {\n // Console (stdout) path. SimpleSpanProcessor \u2014 not batched \u2014 so spans\n // appear in chronological order in the log stream. Acceptable cost for\n // tracing: a span is written per ended span, vs. a batched POST for OTLP.\n // Logs that would otherwise need a UI to make sense (trace tree\n // visualization) are still queryable via `flyctl logs | grep traceId=...`\n // or any log-aggregator that ingests Fly's stream.\n const exporter = new ConsoleSpanExporter();\n spanProcessor = new SimpleSpanProcessor(exporter);\n mode = 'console';\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors: spanProcessor ? [spanProcessor] : [],\n });\n\n sdk.start();\n return mode;\n}\n\nexport async function shutdownTelemetry(): Promise<void> {\n if (sdk) {\n await sdk.shutdown();\n sdk = null;\n }\n}\n\nexport function getTracer(name?: string): Tracer {\n return trace.getTracer(name ?? 'rulemetric');\n}\n\nexport function getTraceId(): string | undefined {\n const span = trace.getSpan(context.active());\n return span?.spanContext().traceId;\n}\n", "// Parse the `OTEL_EXPORTER_OTLP_HEADERS` env var into a plain headers object\n// that the OTLP exporter constructor accepts.\n//\n// OpenTelemetry SDK env-vars spec (\u00A710) defines this as a comma-separated list\n// of `key=value` pairs. We parse it ourselves rather than relying on the\n// `@opentelemetry/exporter-trace-otlp-http` package's auto-discovery, because\n// that auto-discovery has shifted between SDK versions and we want predictable\n// behaviour at boot.\n//\n// Real-world usage:\n// - Honeycomb: `x-honeycomb-team=hcaik_xxx` (and optionally `,x-honeycomb-dataset=...`)\n// - Grafana Cloud: `authorization=Basic <base64>` (note: base64 padding can be `==`)\n//\n// Edge cases (covered by `otlp-headers.test.ts`):\n// - undefined or empty \u2192 {} (caller will get a no-headers exporter)\n// - whitespace around `=` and `,` is trimmed (operator-friendly)\n// - only the first `=` is the separator, so base64 padding (==) survives in\n// values\n// - malformed entries (no `=`) are skipped silently\n// - empty keys are dropped (never inject a \"\" header)\n// - duplicate keys: last value wins, matching URL search-params semantics\n\nexport function parseOtlpHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) return {};\n const out: Record<string, string> = {};\n for (const entry of raw.split(',')) {\n const eq = entry.indexOf('=');\n if (eq === -1) continue;\n const key = entry.slice(0, eq).trim();\n const value = entry.slice(eq + 1).trim();\n if (!key) continue;\n out[key] = value;\n }\n return out;\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport type { MiddlewareHandler } from 'hono';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function telemetryMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n if (!isTelemetryEnabled()) {\n await next();\n return;\n }\n\n const tracer = getTracer();\n const method = c.req.method;\n const path = c.req.path;\n\n await tracer.startActiveSpan(\n `${method} ${path}`,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.method': method,\n 'http.url': c.req.url,\n 'http.route': path,\n },\n },\n async (span) => {\n try {\n await next();\n\n span.setAttribute('http.status_code', c.res.status);\n\n const userId = c.get('userId' as never);\n if (userId) {\n span.setAttribute('rulemetric.user_id', userId as string);\n }\n\n if (c.res.status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function instrumentMcpTool<TArgs, TResult>(\n name: string,\n handler: (args: TArgs) => Promise<TResult>,\n): (args: TArgs) => Promise<TResult> {\n return async (args: TArgs) => {\n if (!isTelemetryEnabled()) {\n return handler(args);\n }\n\n const tracer = getTracer();\n return tracer.startActiveSpan(\n `mcp.tool.${name}`,\n { kind: SpanKind.INTERNAL },\n async (span) => {\n try {\n const result = await handler(args);\n return result;\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { trace, context as otelContext } from '@opentelemetry/api';\n\n/**\n * Set attributes on the currently-active span, if one exists. Safe no-op\n * when no span is active (e.g. running outside of a Hono request handler,\n * or with telemetry disabled).\n *\n * Use this from business-logic code that wants to expose a discriminator\n * for telemetry queries \u2014 e.g. \"which branch did `resolveCallerOrgId` take\"\n * in `apps/api/src/routes/sessions.ts`. Span attributes are searchable in\n * any OpenTelemetry backend (Honeycomb, Grafana Cloud, ...) and in\n * `flyctl logs` when the console exporter is active.\n *\n * Keeping this thin wrapper in the telemetry package means application\n * code never imports `@opentelemetry/api` directly \u2014 the package boundary\n * stays the only place that knows about the OTel SDK shape.\n */\nexport function setSpanAttributes(\n attributes: Record<string, string | number | boolean>,\n): void {\n const span = trace.getSpan(otelContext.active());\n if (!span) return;\n for (const [key, value] of Object.entries(attributes)) {\n span.setAttribute(key, value);\n }\n}\n", "// Thin wrapper around @sentry/node so callers don't have to guard every\n// captureException with `if (process.env.SENTRY_DSN)`. The wrapper handles\n// three concerns:\n//\n// 1. Init only when SENTRY_DSN is set. Otherwise every entry point becomes\n// a silent no-op \u2014 never throws, never logs noise, never opens a\n// network transport.\n// 2. Idempotent init. Double-init is a Sentry SDK foot-gun (the second\n// call wins, but the warning lands at runtime). The module-level\n// `enabled` flag short-circuits the second call.\n// 3. tracesSampleRate = 0 by default. We already have OpenTelemetry\n// shipping spans to a tracing backend; paying Sentry's quota for the\n// same data isn't useful. Sentry's job here is errors-only.\n//\n// Wiring:\n// - `apps/api/src/index.ts` \u2192 initSentry({ serviceName: 'rulemetric-api' })\n// - `apps/api/src/middleware/auth.ts` \u2192 setSentryUser(userId) after JWT verify\n// - `apps/cli/src/commands/evals/listen.ts` \u2192 initSentry({ serviceName: 'rulemetric-worker' })\n// - `apps/cli/src/lib/graphile-worker-runner.ts` runner.events.on('job:failed') \u2192 captureException\n//\n// All four hooks are no-op-safe when DSN is unset, so the wiring doesn't\n// require feature-flag plumbing.\n\nimport * as Sentry from '@sentry/node';\n\nlet enabled = false;\n\nexport interface InitSentryConfig {\n serviceName: string;\n}\n\n/**\n * Initialize Sentry if SENTRY_DSN is set and we're NOT in production.\n *\n * Production gate (`NODE_ENV === 'production'`) is intentional: Sentry is\n * scoped to dev/test environments only. The Fly env doesn't get a\n * SENTRY_DSN secret, so the no-DSN check would already cover the common\n * case \u2014 this gate is belt+suspenders against a misconfigured Docker\n * layer or an accidental `flyctl secrets set`. To enable prod error\n * coverage later, drop the `NODE_ENV === 'production'` branch below.\n *\n * Returns true when init actually ran, false when it was skipped (DSN\n * unset, prior call already initialized, or in production). Idempotent.\n */\nexport function initSentry(config: InitSentryConfig): boolean {\n if (enabled) return false;\n if (process.env.NODE_ENV === 'production') return false;\n const dsn = process.env.SENTRY_DSN;\n if (!dsn) return false;\n\n Sentry.init({\n dsn,\n serverName: config.serviceName,\n environment: process.env.NODE_ENV ?? 'development',\n // We rely on OpenTelemetry for tracing \u2014 Sentry stays errors-only here.\n // Override at the env-var level if you ever want Sentry performance too:\n // `SENTRY_TRACES_SAMPLE_RATE=0.05` (Sentry SDK reads it natively).\n tracesSampleRate: 0,\n });\n\n enabled = true;\n return true;\n}\n\n/**\n * Report an exception. Coerces non-Error throws to Error so Sentry's\n * grouping/stack-trace logic has something to work with. Silent no-op when\n * SENTRY_DSN is unset.\n */\nexport function captureException(\n err: unknown,\n context?: { extra?: Record<string, unknown>; tags?: Record<string, string> },\n): void {\n if (!enabled) return;\n const e = err instanceof Error ? err : new Error(typeof err === 'string' ? err : JSON.stringify(err));\n Sentry.captureException(e, context);\n}\n\n/**\n * Attach (or clear, with null) the active user on the Sentry scope. Called\n * from the API auth middleware so any captured exception during the request\n * carries `user.id`. Silent no-op when SENTRY_DSN is unset.\n */\nexport function setSentryUser(userId: string | null): void {\n if (!enabled) return;\n if (userId === null) {\n Sentry.setUser(null);\n return;\n }\n Sentry.setUser({ id: userId });\n}\n\n/**\n * Flush any queued events with a timeout. Critical to call before\n * `process.exit()` in worker shutdown \u2014 Sentry batches events and the SIGTERM\n * path was previously dropping the final exception (the one we'd most want\n * to see). Resolves immediately when SENTRY_DSN is unset.\n */\nexport async function flushSentry(timeoutMs: number): Promise<void> {\n if (!enabled) return;\n await Sentry.flush(timeoutMs);\n}\n\n/**\n * Reset the module-level state. Test-only \u2014 exported so test suites can\n * unwind the singleton between cases without `vi.resetModules` plumbing in\n * every block.\n */\nexport function _resetSentryForTests(): void {\n enabled = false;\n}\n", "import { initSentry, flushSentry, initTelemetry, shutdownTelemetry } from '@rulemetric/telemetry';\nimport { BaseCommand } from '../../base-command.js';\nimport { loadToken } from '../../lib/auth.js';\nimport { runAgentLoop } from '../../lib/agent-loop.js';\nimport { closeDb } from '@rulemetric/db';\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { execFileSync } from 'node:child_process';\n\n// Anchored under ~/.config/rulemetric (like gateway.pid), NOT os.tmpdir(), to\n// avoid launch-context TMPDIR divergence (launchd vs SSH/sudo/CI).\nconst AGENT_LOCK_PATH = join(homedir(), '.config', 'rulemetric', 'evals-agent.lock');\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n // ESRCH = no such process (stale); EPERM = exists but owned by another user\n return (e as { code?: string }).code === 'EPERM';\n }\n}\n\n// Defends against PID reuse: a SIGKILL/OOM leaves the lock behind and the OS may\n// later assign that PID to an unrelated process \u2014 `pidAlive` alone would then\n// wedge `evals agent` (refuse to start forever). Confirm the holder is actually\n// an evals-agent before treating the lock as held; otherwise it's reclaimable.\nfunction isLiveAgent(pid: number): boolean {\n if (!pidAlive(pid)) return false;\n try {\n const cmd = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return /run\\.js\\s+evals\\s+agent|evals\\s+agent/.test(cmd);\n } catch {\n // ps unavailable/failed \u2014 fall back to liveness only (conservative: held)\n return true;\n }\n}\n\n// Single-instance guard. Each `evals agent` daemon holds its own postgres-js\n// pool; an orphaned, unsupervised fleet (4 stale agents found alive 3+ days on\n// 2026-06-21) was a confirmed driver of the recurring Supavisor 200-client\n// saturation. ATOMIC acquire via O_EXCL (flag 'wx') closes the TOCTOU window\n// where two simultaneously-starting agents could both acquire. On EEXIST the\n// holder is liveness-checked; a stale lock (holder gone) is reclaimed once.\n// See docs/incidents/2026-06-21-pool-exhaustion.md.\nfunction acquireAgentLock(): { ok: true } | { ok: false; holder: number } {\n try {\n mkdirSync(dirname(AGENT_LOCK_PATH), { recursive: true });\n } catch {\n /* ignore \u2014 writeFileSync will surface a real problem */\n }\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n // 'wx' = O_CREAT | O_EXCL: fails atomically with EEXIST if it exists.\n writeFileSync(AGENT_LOCK_PATH, String(process.pid), { flag: 'wx' });\n return { ok: true };\n } catch (e) {\n if ((e as { code?: string }).code !== 'EEXIST') throw e;\n let holder = 0;\n try {\n holder = Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim());\n } catch {\n /* unreadable \u2014 treat as stale */\n }\n if (holder && holder !== process.pid && isLiveAgent(holder)) {\n return { ok: false, holder };\n }\n // stale (holder gone) / ours / unreadable \u2014 drop it and retry the atomic create once\n try { unlinkSync(AGENT_LOCK_PATH); } catch { /* raced with another reclaimer */ }\n }\n }\n // Last resort after a reclaim race \u2014 best-effort non-exclusive write.\n try { writeFileSync(AGENT_LOCK_PATH, String(process.pid)); } catch { /* ignore */ }\n return { ok: true };\n}\n\nfunction releaseAgentLock(): void {\n try {\n if (\n existsSync(AGENT_LOCK_PATH) &&\n Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim()) === process.pid\n ) {\n unlinkSync(AGENT_LOCK_PATH);\n }\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * `rulemetric evals agent` \u2014 Phase 4b parallel-run thin agent.\n *\n * Polls the /api/work endpoints (Phase 4a) instead of binding a Postgres\n * LISTEN/NOTIFY connection. This is the scaffold for the cutover described\n * in docs/plans/2026-05-25-thin-agent-migration.md; the default worker is\n * the sole worker mode since Phase 4c deleted the graphile path.\n *\n * Both commands can run side-by-side on the same user during the cutover \u2014\n * the API's atomic SKIP-LOCKED claim guarantees a job is handed to exactly\n * one agent. Picking which one runs is a launchd / shell choice.\n */\nexport default class EvalAgentCommand extends BaseCommand {\n static override description =\n 'The worker daemon: long-polls /api/work/next, claims jobs, and runs them locally. Installed as a launchd service by `rulemetric service install`.';\n\n static override examples = [\n '<%= config.bin %> evals agent',\n ];\n\n async run(): Promise<void> {\n await this.parse(EvalAgentCommand);\n this.requireAuth();\n\n // Single-instance guard \u2014 refuse to start a second agent. Each extra agent\n // holds its own DB pool; an orphaned fleet was a confirmed cause of pooler\n // saturation (pool-exhaustion RCA 2026-06-21).\n const lock = acquireAgentLock();\n if (!lock.ok) {\n this.log(\n `Another rulemetric evals agent is already running (pid ${lock.holder}). ` +\n 'Refusing to start a second instance.',\n );\n return;\n }\n\n process.env.PG_APPLICATION_NAME ||= 'rulemetric-agent';\n\n const sentryEnabled = initSentry({ serviceName: 'rulemetric-agent' });\n if (sentryEnabled) this.log('[sentry] error capture enabled');\n\n const otelMode = initTelemetry({ serviceName: 'rulemetric-agent' });\n if (otelMode === 'otlp') {\n this.log(`[otel] traces exporting to ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}`);\n } else if (otelMode === 'console') {\n this.log('[otel] cron spans printing to stdout (set OTEL_EXPORTER_OTLP_ENDPOINT to ship to a backend)');\n } else {\n this.log('[otel] disabled (TELEMETRY_ENABLED=false)');\n }\n\n // Early auth sanity check \u2014 the agent loop also reads the token on every\n // request via api-client, but failing here gives a friendlier error than\n // a 401 thirty seconds into the first long-poll.\n const token = loadToken();\n if (!token && !process.env.RULEMETRIC_API_KEY) {\n this.error('No auth token found. Run `rulemetric auth login` first.');\n }\n\n this.log('Starting thin agent (Phase 4b parallel-run)...');\n this.log('Long-polling /api/work/next. Press Ctrl+C to stop.\\n');\n\n const controller = new AbortController();\n\n const shutdown = async (signal: string) => {\n this.log(`\\n[${signal}] shutting down agent...`);\n controller.abort();\n // Flush telemetry + drain the pool on the way out. All members run\n // concurrently under Promise.allSettled (flushSentry 2s, closeDb 5s), so\n // the total stays well under launchd's ~20s shutdown grace.\n await Promise.allSettled([\n flushSentry(2_000),\n shutdownTelemetry(),\n // Drain the postgres-js pool so this agent's Supavisor client sessions\n // are released (Terminate) instead of orphaned as ghosts on every\n // launchd bounce. Pool-exhaustion RCA 2026-06-21.\n closeDb(),\n ]);\n releaseAgentLock();\n process.exit(0);\n };\n process.on('SIGINT', () => { void shutdown('SIGINT'); });\n process.on('SIGTERM', () => { void shutdown('SIGTERM'); });\n\n try {\n await runAgentLoop({\n signal: controller.signal,\n log: (msg) => this.log(msg),\n });\n } finally {\n releaseAgentLock();\n // Drain on the throw path too (defense-in-depth). The signal-handler drain\n // covers normal shutdown; a thrown runAgentLoop must not skip it. closeDb\n // is idempotent, so the happy-path SIGTERM drain stays a no-op.\n await closeDb();\n }\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,eAA4B;AAC5C,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAClC,SACE,oBACA,qBACA,2BACK;AACP,SAAS,gBAAgB;AACzB,SAAS,mBAAmB,4BAA4B;;;ACalD,SAAU,iBAAiB,KAAuB;AACtD,MAAI,CAAC;AAAK,WAAO,CAAA;AACjB,QAAM,MAA8B,CAAA;AACpC,aAAW,SAAS,IAAI,MAAM,GAAG,GAAG;AAClC,UAAM,KAAK,MAAM,QAAQ,GAAG;AAC5B,QAAI,OAAO;AAAI;AACf,UAAM,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,KAAI;AACnC,UAAM,QAAQ,MAAM,MAAM,KAAK,CAAC,EAAE,KAAI;AACtC,QAAI,CAAC;AAAK;AACV,QAAI,GAAG,IAAI;EACb;AACA,SAAO;AACT;;;ADtBA,IAAI,MAAsB;AAmBpB,SAAU,qBAAkB;AAChC,SAAO,QAAQ,IAAI,sBAAsB;AAC3C;AAsBM,SAAU,cAAc,QAAuB;AACnD,MAAI,CAAC,mBAAkB,GAAI;AACzB,WAAO;EACT;AACA,MAAI,QAAQ,MAAM;AAGhB,WAAO;EACT;AAEA,QAAM,WAAW,IAAI,SAAS;IAC5B,CAAC,iBAAiB,GAAG,OAAO;IAC5B,CAAC,oBAAoB,GAAG,OAAO,kBAAkB;GAClD;AAED,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI;AACJ,MAAI;AAEJ,MAAI,UAAU;AAOZ,UAAM,UAAU,iBAAiB,QAAQ,IAAI,0BAA0B;AACvE,UAAM,WAAW,IAAI,kBAAkB;MACrC,KAAK,GAAG,QAAQ;MAChB;KACD;AACD,oBAAgB,IAAI,mBAAmB,QAAQ;AAC/C,WAAO;EACT,WAAW,QAAQ,IAAI,qBAAqB,QAAQ;AASlD,oBAAgB;AAChB,WAAO;EACT,OAAO;AAOL,UAAM,WAAW,IAAI,oBAAmB;AACxC,oBAAgB,IAAI,oBAAoB,QAAQ;AAChD,WAAO;EACT;AAEA,QAAM,IAAI,QAAQ;IAChB;IACA,gBAAgB,gBAAgB,CAAC,aAAa,IAAI,CAAA;GACnD;AAED,MAAI,MAAK;AACT,SAAO;AACT;AAEA,eAAsB,oBAAiB;AACrC,MAAI,KAAK;AACP,UAAM,IAAI,SAAQ;AAClB,UAAM;EACR;AACF;;;AE7HA,SAAS,UAAU,sBAAsB;;;ACAzC,SAAS,YAAAA,WAAU,kBAAAC,uBAAsB;;;ACAzC,SAAS,SAAAC,QAAO,WAAW,mBAAmB;;;
|
|
4
|
+
"sourcesContent": ["import { trace, context, type Tracer } from '@opentelemetry/api';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport {\n BatchSpanProcessor,\n ConsoleSpanExporter,\n SimpleSpanProcessor,\n} from '@opentelemetry/sdk-trace-node';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\nimport { parseOtlpHeaders } from './otlp-headers.js';\n\nlet sdk: NodeSDK | null = null;\n\nexport interface TelemetryConfig {\n serviceName: string;\n serviceVersion?: string;\n}\n\n/**\n * Boot states for `initTelemetry`. Returned discriminator lets callers\n * emit a precise boot log:\n * - 'otlp' \u2014 spans ship over OTLP to a real backend (Honeycomb etc.)\n * - 'console' \u2014 spans print to stdout as JSON (Fly logs ingest them);\n * the \"open + zero third-party\" default\n * - 'off' \u2014 telemetry globally disabled (TELEMETRY_ENABLED=false)\n * OR a prior `initTelemetry()` already booted the SDK\n * in this process (idempotent skip)\n */\nexport type TelemetryBootState = 'otlp' | 'console' | 'silent' | 'off';\n\nexport function isTelemetryEnabled(): boolean {\n return process.env.TELEMETRY_ENABLED !== 'false';\n}\n\n/**\n * Initialize OpenTelemetry tracing. Two transport modes, both built on\n * the same OTel SDK \u2014 only the exporter changes:\n *\n * - OTLP backend \u2192 set `OTEL_EXPORTER_OTLP_ENDPOINT` (+ optional\n * `OTEL_EXPORTER_OTLP_HEADERS` for vendor auth)\n * - Console (stdout) \u2192 the default when no endpoint is set\n *\n * The console path is intentionally on-by-default. Without a real backend\n * configured, spans are written to stdout as JSON and end up in Fly's log\n * stream alongside everything else \u2014 searchable, structured, and you\n * stay inside the open OpenTelemetry spec without paying a third party.\n * Swap to OTLP later by setting one env var; no code change needed.\n *\n * Returns `'off'` when:\n * - TELEMETRY_ENABLED=false (operator kill switch \u2014 beats any config)\n * - The SDK was already started in this process (idempotent skip)\n *\n * See packages/telemetry/src/init.test.ts for the full state table.\n */\nexport function initTelemetry(config: TelemetryConfig): TelemetryBootState {\n if (!isTelemetryEnabled()) {\n return 'off';\n }\n if (sdk !== null) {\n // OTel's global tracer provider gets set on first start(); calling\n // start() twice triggers an SDK warning. Treat second-call as a no-op.\n return 'off';\n }\n\n const resource = new Resource({\n [ATTR_SERVICE_NAME]: config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? '0.1.0',\n });\n\n const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n let mode: TelemetryBootState;\n let spanProcessor: BatchSpanProcessor | SimpleSpanProcessor | undefined;\n\n if (endpoint) {\n // OTLP path. Headers via OTEL_EXPORTER_OTLP_HEADERS (W3C convention,\n // comma-separated key=value pairs). Parsed explicitly rather than\n // relying on SDK-version-specific auto-discovery \u2014 predictable boot\n // matters more than tracking whatever the current default is.\n // Honeycomb: x-honeycomb-team=hcaik_xxx (plus optional dataset)\n // Grafana Cloud: authorization=Basic <base64creds>\n const headers = parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS);\n const exporter = new OTLPTraceExporter({\n url: `${endpoint}/v1/traces`,\n headers,\n });\n spanProcessor = new BatchSpanProcessor(exporter);\n mode = 'otlp';\n } else if (process.env.OTEL_SPAN_EXPORT === 'none') {\n // Silent path: trace context still propagates (so trace IDs are available\n // for correlation and an OTLP endpoint can be added later with zero code\n // change), but NO span processor is registered \u2014 nothing is written to\n // stdout. This exists because the ConsoleSpanExporter below writes one\n // JSON span per ended span, and on the local launchd API (no OTLP\n // endpoint, fixed log file, no Fly-managed rotation) that grew api-stdout\n // to 370MB / ~400k spans. Set OTEL_SPAN_EXPORT=none on that process; Fly\n // leaves it unset so console spans stay grep-able in its managed log stream.\n spanProcessor = undefined;\n mode = 'silent';\n } else {\n // Console (stdout) path. SimpleSpanProcessor \u2014 not batched \u2014 so spans\n // appear in chronological order in the log stream. Acceptable cost for\n // tracing: a span is written per ended span, vs. a batched POST for OTLP.\n // Logs that would otherwise need a UI to make sense (trace tree\n // visualization) are still queryable via `flyctl logs | grep traceId=...`\n // or any log-aggregator that ingests Fly's stream.\n const exporter = new ConsoleSpanExporter();\n spanProcessor = new SimpleSpanProcessor(exporter);\n mode = 'console';\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors: spanProcessor ? [spanProcessor] : [],\n });\n\n sdk.start();\n return mode;\n}\n\nexport async function shutdownTelemetry(): Promise<void> {\n if (sdk) {\n await sdk.shutdown();\n sdk = null;\n }\n}\n\nexport function getTracer(name?: string): Tracer {\n return trace.getTracer(name ?? 'rulemetric');\n}\n\nexport function getTraceId(): string | undefined {\n const span = trace.getSpan(context.active());\n return span?.spanContext().traceId;\n}\n", "// Parse the `OTEL_EXPORTER_OTLP_HEADERS` env var into a plain headers object\n// that the OTLP exporter constructor accepts.\n//\n// OpenTelemetry SDK env-vars spec (\u00A710) defines this as a comma-separated list\n// of `key=value` pairs. We parse it ourselves rather than relying on the\n// `@opentelemetry/exporter-trace-otlp-http` package's auto-discovery, because\n// that auto-discovery has shifted between SDK versions and we want predictable\n// behaviour at boot.\n//\n// Real-world usage:\n// - Honeycomb: `x-honeycomb-team=hcaik_xxx` (and optionally `,x-honeycomb-dataset=...`)\n// - Grafana Cloud: `authorization=Basic <base64>` (note: base64 padding can be `==`)\n//\n// Edge cases (covered by `otlp-headers.test.ts`):\n// - undefined or empty \u2192 {} (caller will get a no-headers exporter)\n// - whitespace around `=` and `,` is trimmed (operator-friendly)\n// - only the first `=` is the separator, so base64 padding (==) survives in\n// values\n// - malformed entries (no `=`) are skipped silently\n// - empty keys are dropped (never inject a \"\" header)\n// - duplicate keys: last value wins, matching URL search-params semantics\n\nexport function parseOtlpHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) return {};\n const out: Record<string, string> = {};\n for (const entry of raw.split(',')) {\n const eq = entry.indexOf('=');\n if (eq === -1) continue;\n const key = entry.slice(0, eq).trim();\n const value = entry.slice(eq + 1).trim();\n if (!key) continue;\n out[key] = value;\n }\n return out;\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport type { MiddlewareHandler } from 'hono';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function telemetryMiddleware(): MiddlewareHandler {\n return async (c, next) => {\n if (!isTelemetryEnabled()) {\n await next();\n return;\n }\n\n const tracer = getTracer();\n const method = c.req.method;\n const path = c.req.path;\n\n await tracer.startActiveSpan(\n `${method} ${path}`,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.method': method,\n 'http.url': c.req.url,\n 'http.route': path,\n },\n },\n async (span) => {\n try {\n await next();\n\n span.setAttribute('http.status_code', c.res.status);\n\n const userId = c.get('userId' as never);\n if (userId) {\n span.setAttribute('rulemetric.user_id', userId as string);\n }\n\n if (c.res.status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { SpanKind, SpanStatusCode } from '@opentelemetry/api';\nimport { getTracer, isTelemetryEnabled } from './init.js';\n\nexport function instrumentMcpTool<TArgs, TResult>(\n name: string,\n handler: (args: TArgs) => Promise<TResult>,\n): (args: TArgs) => Promise<TResult> {\n return async (args: TArgs) => {\n if (!isTelemetryEnabled()) {\n return handler(args);\n }\n\n const tracer = getTracer();\n return tracer.startActiveSpan(\n `mcp.tool.${name}`,\n { kind: SpanKind.INTERNAL },\n async (span) => {\n try {\n const result = await handler(args);\n return result;\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.recordException(err as Error);\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n", "import { trace, context as otelContext } from '@opentelemetry/api';\n\n/**\n * Set attributes on the currently-active span, if one exists. Safe no-op\n * when no span is active (e.g. running outside of a Hono request handler,\n * or with telemetry disabled).\n *\n * Use this from business-logic code that wants to expose a discriminator\n * for telemetry queries \u2014 e.g. \"which branch did `resolveCallerOrgId` take\"\n * in `apps/api/src/routes/sessions.ts`. Span attributes are searchable in\n * any OpenTelemetry backend (Honeycomb, Grafana Cloud, ...) and in\n * `flyctl logs` when the console exporter is active.\n *\n * Keeping this thin wrapper in the telemetry package means application\n * code never imports `@opentelemetry/api` directly \u2014 the package boundary\n * stays the only place that knows about the OTel SDK shape.\n */\nexport function setSpanAttributes(\n attributes: Record<string, string | number | boolean>,\n): void {\n const span = trace.getSpan(otelContext.active());\n if (!span) return;\n for (const [key, value] of Object.entries(attributes)) {\n span.setAttribute(key, value);\n }\n}\n", "// Thin wrapper around @sentry/node so callers don't have to guard every\n// captureException with `if (process.env.SENTRY_DSN)`. The wrapper handles\n// three concerns:\n//\n// 1. Init only when SENTRY_DSN is set. Otherwise every entry point becomes\n// a silent no-op \u2014 never throws, never logs noise, never opens a\n// network transport.\n// 2. Idempotent init. Double-init is a Sentry SDK foot-gun (the second\n// call wins, but the warning lands at runtime). The module-level\n// `enabled` flag short-circuits the second call.\n// 3. tracesSampleRate = 0 by default. We already have OpenTelemetry\n// shipping spans to a tracing backend; paying Sentry's quota for the\n// same data isn't useful. Sentry's job here is errors-only.\n//\n// Wiring:\n// - `apps/api/src/index.ts` \u2192 initSentry({ serviceName: 'rulemetric-api' })\n// - `apps/api/src/middleware/auth.ts` \u2192 setSentryUser(userId) after JWT verify\n// - `apps/cli/src/commands/evals/agent.ts` \u2192 initSentry({ serviceName: 'rulemetric-agent' })\n// (the thin-agent worker \u2014 Phase 4c moved the init here from the old\n// graphile `listen.ts`; that path no longer inits Sentry)\n//\n// All four hooks are no-op-safe when DSN is unset, so the wiring doesn't\n// require feature-flag plumbing.\n\nimport * as Sentry from '@sentry/node';\n\nlet enabled = false;\n\nexport interface InitSentryConfig {\n serviceName: string;\n}\n\n/**\n * Initialize Sentry if SENTRY_DSN is set. Presence of the DSN is now the\n * single switch: set it (incl. as a Fly secret in prod) to enable error\n * capture, leave it unset for a silent no-op.\n *\n * Previously gated to non-production by policy; that gate was dropped so\n * production errors actually get reported. Errors-only (`tracesSampleRate:\n * 0`) keeps Sentry quota bounded \u2014 OpenTelemetry still owns tracing.\n *\n * Returns true when init actually ran, false when it was skipped (DSN unset\n * or a prior call already initialized). Idempotent.\n */\nexport function initSentry(config: InitSentryConfig): boolean {\n if (enabled) return false;\n const dsn = process.env.SENTRY_DSN;\n if (!dsn) return false;\n\n Sentry.init({\n dsn,\n serverName: config.serviceName,\n environment: process.env.NODE_ENV ?? 'development',\n // We rely on OpenTelemetry for tracing \u2014 Sentry stays errors-only here.\n // Override at the env-var level if you ever want Sentry performance too:\n // `SENTRY_TRACES_SAMPLE_RATE=0.05` (Sentry SDK reads it natively).\n tracesSampleRate: 0,\n });\n\n enabled = true;\n return true;\n}\n\n/**\n * Report an exception. Coerces non-Error throws to Error so Sentry's\n * grouping/stack-trace logic has something to work with. Silent no-op when\n * SENTRY_DSN is unset.\n */\nexport function captureException(\n err: unknown,\n context?: { extra?: Record<string, unknown>; tags?: Record<string, string> },\n): void {\n if (!enabled) return;\n const e = err instanceof Error ? err : new Error(typeof err === 'string' ? err : JSON.stringify(err));\n Sentry.captureException(e, context);\n}\n\n/**\n * Attach (or clear, with null) the active user on the Sentry scope. Called\n * from the API auth middleware so any captured exception during the request\n * carries `user.id`. Silent no-op when SENTRY_DSN is unset.\n */\nexport function setSentryUser(userId: string | null): void {\n if (!enabled) return;\n if (userId === null) {\n Sentry.setUser(null);\n return;\n }\n Sentry.setUser({ id: userId });\n}\n\n/**\n * Flush any queued events with a timeout. Critical to call before\n * `process.exit()` in worker shutdown \u2014 Sentry batches events and the SIGTERM\n * path was previously dropping the final exception (the one we'd most want\n * to see). Resolves immediately when SENTRY_DSN is unset.\n */\nexport async function flushSentry(timeoutMs: number): Promise<void> {\n if (!enabled) return;\n await Sentry.flush(timeoutMs);\n}\n\n/**\n * Reset the module-level state. Test-only \u2014 exported so test suites can\n * unwind the singleton between cases without `vi.resetModules` plumbing in\n * every block.\n */\nexport function _resetSentryForTests(): void {\n enabled = false;\n}\n", "import { initSentry, flushSentry, initTelemetry, shutdownTelemetry } from '@rulemetric/telemetry';\nimport { BaseCommand } from '../../base-command.js';\nimport { loadToken } from '../../lib/auth.js';\nimport { runAgentLoop } from '../../lib/agent-loop.js';\nimport { closeDb } from '@rulemetric/db';\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { execFileSync } from 'node:child_process';\n\n// Anchored under ~/.config/rulemetric (like gateway.pid), NOT os.tmpdir(), to\n// avoid launch-context TMPDIR divergence (launchd vs SSH/sudo/CI).\nconst AGENT_LOCK_PATH = join(homedir(), '.config', 'rulemetric', 'evals-agent.lock');\n\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n // ESRCH = no such process (stale); EPERM = exists but owned by another user\n return (e as { code?: string }).code === 'EPERM';\n }\n}\n\n// Defends against PID reuse: a SIGKILL/OOM leaves the lock behind and the OS may\n// later assign that PID to an unrelated process \u2014 `pidAlive` alone would then\n// wedge `evals agent` (refuse to start forever). Confirm the holder is actually\n// an evals-agent before treating the lock as held; otherwise it's reclaimable.\nfunction isLiveAgent(pid: number): boolean {\n if (!pidAlive(pid)) return false;\n try {\n const cmd = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n return /run\\.js\\s+evals\\s+agent|evals\\s+agent/.test(cmd);\n } catch {\n // ps unavailable/failed \u2014 fall back to liveness only (conservative: held)\n return true;\n }\n}\n\n// Single-instance guard. Each `evals agent` daemon holds its own postgres-js\n// pool; an orphaned, unsupervised fleet (4 stale agents found alive 3+ days on\n// 2026-06-21) was a confirmed driver of the recurring Supavisor 200-client\n// saturation. ATOMIC acquire via O_EXCL (flag 'wx') closes the TOCTOU window\n// where two simultaneously-starting agents could both acquire. On EEXIST the\n// holder is liveness-checked; a stale lock (holder gone) is reclaimed once.\n// See docs/incidents/2026-06-21-pool-exhaustion.md.\nfunction acquireAgentLock(): { ok: true } | { ok: false; holder: number } {\n try {\n mkdirSync(dirname(AGENT_LOCK_PATH), { recursive: true });\n } catch {\n /* ignore \u2014 writeFileSync will surface a real problem */\n }\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n // 'wx' = O_CREAT | O_EXCL: fails atomically with EEXIST if it exists.\n writeFileSync(AGENT_LOCK_PATH, String(process.pid), { flag: 'wx' });\n return { ok: true };\n } catch (e) {\n if ((e as { code?: string }).code !== 'EEXIST') throw e;\n let holder = 0;\n try {\n holder = Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim());\n } catch {\n /* unreadable \u2014 treat as stale */\n }\n if (holder && holder !== process.pid && isLiveAgent(holder)) {\n return { ok: false, holder };\n }\n // stale (holder gone) / ours / unreadable \u2014 drop it and retry the atomic create once\n try { unlinkSync(AGENT_LOCK_PATH); } catch { /* raced with another reclaimer */ }\n }\n }\n // Last resort after a reclaim race \u2014 best-effort non-exclusive write.\n try { writeFileSync(AGENT_LOCK_PATH, String(process.pid)); } catch { /* ignore */ }\n return { ok: true };\n}\n\nfunction releaseAgentLock(): void {\n try {\n if (\n existsSync(AGENT_LOCK_PATH) &&\n Number(readFileSync(AGENT_LOCK_PATH, 'utf-8').trim()) === process.pid\n ) {\n unlinkSync(AGENT_LOCK_PATH);\n }\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * `rulemetric evals agent` \u2014 Phase 4b parallel-run thin agent.\n *\n * Polls the /api/work endpoints (Phase 4a) instead of binding a Postgres\n * LISTEN/NOTIFY connection. This is the scaffold for the cutover described\n * in docs/plans/2026-05-25-thin-agent-migration.md; the default worker is\n * the sole worker mode since Phase 4c deleted the graphile path.\n *\n * Both commands can run side-by-side on the same user during the cutover \u2014\n * the API's atomic SKIP-LOCKED claim guarantees a job is handed to exactly\n * one agent. Picking which one runs is a launchd / shell choice.\n */\nexport default class EvalAgentCommand extends BaseCommand {\n static override description =\n 'The worker daemon: long-polls /api/work/next, claims jobs, and runs them locally. Installed as a launchd service by `rulemetric service install`.';\n\n static override examples = [\n '<%= config.bin %> evals agent',\n ];\n\n async run(): Promise<void> {\n await this.parse(EvalAgentCommand);\n this.requireAuth();\n\n // Single-instance guard \u2014 refuse to start a second agent. Each extra agent\n // holds its own DB pool; an orphaned fleet was a confirmed cause of pooler\n // saturation (pool-exhaustion RCA 2026-06-21).\n const lock = acquireAgentLock();\n if (!lock.ok) {\n this.log(\n `Another rulemetric evals agent is already running (pid ${lock.holder}). ` +\n 'Refusing to start a second instance.',\n );\n return;\n }\n\n process.env.PG_APPLICATION_NAME ||= 'rulemetric-agent';\n\n const sentryEnabled = initSentry({ serviceName: 'rulemetric-agent' });\n if (sentryEnabled) this.log('[sentry] error capture enabled');\n\n const otelMode = initTelemetry({ serviceName: 'rulemetric-agent' });\n if (otelMode === 'otlp') {\n this.log(`[otel] traces exporting to ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}`);\n } else if (otelMode === 'console') {\n this.log('[otel] cron spans printing to stdout (set OTEL_EXPORTER_OTLP_ENDPOINT to ship to a backend)');\n } else {\n this.log('[otel] disabled (TELEMETRY_ENABLED=false)');\n }\n\n // Early auth sanity check \u2014 the agent loop also reads the token on every\n // request via api-client, but failing here gives a friendlier error than\n // a 401 thirty seconds into the first long-poll.\n const token = loadToken();\n if (!token && !process.env.RULEMETRIC_API_KEY) {\n this.error('No auth token found. Run `rulemetric auth login` first.');\n }\n\n this.log('Starting thin agent (Phase 4b parallel-run)...');\n this.log('Long-polling /api/work/next. Press Ctrl+C to stop.\\n');\n\n const controller = new AbortController();\n\n const shutdown = async (signal: string) => {\n this.log(`\\n[${signal}] shutting down agent...`);\n controller.abort();\n // Flush telemetry + drain the pool on the way out. All members run\n // concurrently under Promise.allSettled (flushSentry 2s, closeDb 5s), so\n // the total stays well under launchd's ~20s shutdown grace.\n await Promise.allSettled([\n flushSentry(2_000),\n shutdownTelemetry(),\n // Drain the postgres-js pool so this agent's Supavisor client sessions\n // are released (Terminate) instead of orphaned as ghosts on every\n // launchd bounce. Pool-exhaustion RCA 2026-06-21.\n closeDb(),\n ]);\n releaseAgentLock();\n process.exit(0);\n };\n process.on('SIGINT', () => { void shutdown('SIGINT'); });\n process.on('SIGTERM', () => { void shutdown('SIGTERM'); });\n\n try {\n await runAgentLoop({\n signal: controller.signal,\n log: (msg) => this.log(msg),\n });\n } finally {\n releaseAgentLock();\n // Drain on the throw path too (defense-in-depth). The signal-handler drain\n // covers normal shutdown; a thrown runAgentLoop must not skip it. closeDb\n // is idempotent, so the happy-path SIGTERM drain stays a no-op.\n await closeDb();\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["SpanKind", "SpanStatusCode", "trace"]
|
|
7
7
|
}
|