@rulemetric/cli 0.2.3 → 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-GQCISMAH.js → chunk-2BCPVUSF.js} +6 -6
- package/dist/{chunk-7FAA3YNB.js → chunk-42A3HEEH.js} +50 -3
- package/dist/chunk-42A3HEEH.js.map +7 -0
- package/dist/{chunk-RJ7BHDLM.js → chunk-46LMTDRE.js} +2 -2
- package/dist/{chunk-B5KNTGMJ.js → chunk-4QGATD5Y.js} +209 -2
- package/dist/chunk-4QGATD5Y.js.map +7 -0
- package/dist/{chunk-XPUEGHXN.js → chunk-EHABPBGH.js} +4 -4
- package/dist/{chunk-EOOP6WTC.js → chunk-FUS5C4SB.js} +2 -2
- package/dist/chunk-JFDWKLQC.js +23 -0
- package/dist/chunk-JFDWKLQC.js.map +7 -0
- package/dist/{chunk-PHLMIZKM.js → chunk-JNFIYBFL.js} +30 -5
- package/dist/chunk-JNFIYBFL.js.map +7 -0
- package/dist/{chunk-WFULKPEQ.js → chunk-LCT6MT2V.js} +12 -12
- package/dist/{chunk-R5MFHGHR.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-4WFE4PFP.js → chunk-XMWHTOCC.js} +3 -3
- package/dist/{chunk-AULP2OWH.js → chunk-Y2ARLMWZ.js} +6 -6
- package/dist/{chunk-IGXX624H.js → chunk-YEBBX4HQ.js} +2 -2
- package/dist/commands/crons/list.js +3 -13
- package/dist/commands/crons/list.js.map +2 -2
- package/dist/commands/crons/run.js +16 -14
- package/dist/commands/crons/run.js.map +2 -2
- package/dist/commands/evals/agent.js +19 -21
- package/dist/commands/evals/agent.js.map +2 -2
- package/dist/commands/hooks/install.js +2 -2
- 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/commands/suggestions/accept.js +3 -3
- package/dist/commands/suggestions/dismiss.js +3 -3
- package/dist/dashboard/Dashboard.js +3 -3
- package/dist/dashboard/data.js +2 -2
- package/dist/{dist-CSUYFYKI.js → dist-NEECHU47.js} +6 -2
- package/dist/lib/agent-loop.js +17 -17
- package/dist/lib/handlers/process-announcement.js +3 -3
- package/dist/lib/handlers/process-changelog.js +3 -3
- package/dist/lib/handlers/process-insights.js +2 -2
- 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-meta.js +8 -0
- package/dist/lib/manual-tasks-meta.js.map +7 -0
- package/dist/lib/manual-tasks.js +65 -8
- package/dist/lib/manual-tasks.js.map +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-7FAA3YNB.js.map +0 -7
- package/dist/chunk-B5KNTGMJ.js.map +0 -7
- package/dist/chunk-DU73QUS3.js +0 -75
- package/dist/chunk-DU73QUS3.js.map +0 -7
- package/dist/chunk-PHLMIZKM.js.map +0 -7
- /package/dist/{chunk-GQCISMAH.js.map → chunk-2BCPVUSF.js.map} +0 -0
- /package/dist/{chunk-RJ7BHDLM.js.map → chunk-46LMTDRE.js.map} +0 -0
- /package/dist/{chunk-XPUEGHXN.js.map → chunk-EHABPBGH.js.map} +0 -0
- /package/dist/{chunk-EOOP6WTC.js.map → chunk-FUS5C4SB.js.map} +0 -0
- /package/dist/{chunk-WFULKPEQ.js.map → chunk-LCT6MT2V.js.map} +0 -0
- /package/dist/{chunk-R5MFHGHR.js.map → chunk-MTLAIJED.js.map} +0 -0
- /package/dist/{chunk-4WFE4PFP.js.map → chunk-XMWHTOCC.js.map} +0 -0
- /package/dist/{chunk-AULP2OWH.js.map → chunk-Y2ARLMWZ.js.map} +0 -0
- /package/dist/{chunk-IGXX624H.js.map → chunk-YEBBX4HQ.js.map} +0 -0
- /package/dist/{dist-CSUYFYKI.js.map → dist-NEECHU47.js.map} +0 -0
|
@@ -41,6 +41,8 @@ ${chainBlock}
|
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
43
|
OBSERVED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
44
|
+
CACHE_DIR="$HOME/.config/rulemetric"
|
|
45
|
+
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
44
46
|
|
|
45
47
|
for WIRE_KEY in five_hour seven_day seven_day_opus; do
|
|
46
48
|
# Map wire key \u2192 limit_type (case statement avoids bash 4 associative arrays).
|
|
@@ -74,6 +76,32 @@ for WIRE_KEY in five_hour seven_day seven_day_opus; do
|
|
|
74
76
|
RESETS_AT_ISO=""
|
|
75
77
|
fi
|
|
76
78
|
|
|
79
|
+
# Skip stale windows. Claude Code can keep re-emitting a cached rate_limits
|
|
80
|
+
# block after the reset time has passed; storing those rows floods the DB and
|
|
81
|
+
# corrupts the "current limits" view.
|
|
82
|
+
if [ -n "$RESETS_AT_EPOCH" ] && [ "$RESETS_AT_EPOCH" != "null" ]; then
|
|
83
|
+
NOW_EPOCH="$(date -u +%s)"
|
|
84
|
+
if [ "$RESETS_AT_EPOCH" -lt "$NOW_EPOCH" ] 2>/dev/null; then
|
|
85
|
+
continue
|
|
86
|
+
fi
|
|
87
|
+
else
|
|
88
|
+
NOW_EPOCH="$(date -u +%s)"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Local duplicate suppression mirrors the API's 30-minute freshness floor.
|
|
92
|
+
# The statusline refreshes often; if the same window/value/reset is already
|
|
93
|
+
# posted recently, another POST only burns an API request and DB connection.
|
|
94
|
+
CACHE_FILE="$CACHE_DIR/statusline-usage-\${LIMIT_TYPE}.cache"
|
|
95
|
+
CACHE_KEY="\${LIMIT_TYPE}|\${USED_PCT}|\${RESETS_AT_ISO}"
|
|
96
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
97
|
+
read -r CACHED_AT CACHED_KEY < "$CACHE_FILE" || true
|
|
98
|
+
if [ "\${CACHED_KEY:-}" = "$CACHE_KEY" ] && [ $((NOW_EPOCH - \${CACHED_AT:-0})) -lt 1800 ] 2>/dev/null; then
|
|
99
|
+
continue
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
printf '%s %s
|
|
103
|
+
' "$NOW_EPOCH" "$CACHE_KEY" > "$CACHE_FILE.tmp" 2>/dev/null && mv "$CACHE_FILE.tmp" "$CACHE_FILE" 2>/dev/null || true
|
|
104
|
+
|
|
77
105
|
PAYLOAD="$(jq -nc \\
|
|
78
106
|
--arg provider "anthropic" \\
|
|
79
107
|
--arg plan "unknown" \\
|
|
@@ -100,12 +128,9 @@ for WIRE_KEY in five_hour seven_day seven_day_opus; do
|
|
|
100
128
|
-H "Content-Type: application/json" \\
|
|
101
129
|
-H "Authorization: Bearer $AUTH_TOKEN" \\
|
|
102
130
|
--max-time 3 \\
|
|
103
|
-
--data "$PAYLOAD" &
|
|
131
|
+
--data "$PAYLOAD" >/dev/null 2>&1 &
|
|
104
132
|
done
|
|
105
133
|
|
|
106
|
-
# Wait for background curls (non-blocking from Claude Code's perspective \u2014
|
|
107
|
-
# the shim itself exits promptly; curl jobs are fire-and-forget).
|
|
108
|
-
wait
|
|
109
134
|
${chainBlock}
|
|
110
135
|
exit 0
|
|
111
136
|
`;
|
|
@@ -150,4 +175,4 @@ export {
|
|
|
150
175
|
readCurrentStatusLine,
|
|
151
176
|
isStatuslineShimInstalled
|
|
152
177
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
178
|
+
//# sourceMappingURL=chunk-JNFIYBFL.js.map
|
|
@@ -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
|
+
}
|
|
@@ -3,22 +3,28 @@ import {
|
|
|
3
3
|
} from "./chunk-JX5UI6RW.js";
|
|
4
4
|
import {
|
|
5
5
|
processInsights
|
|
6
|
-
} from "./chunk-
|
|
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
|
+
import {
|
|
20
|
+
cronRefreshSkills
|
|
21
|
+
} from "./chunk-RQ2TMLKG.js";
|
|
22
|
+
import {
|
|
23
|
+
cronSuggestInstructions
|
|
24
|
+
} from "./chunk-YBNW7ARH.js";
|
|
19
25
|
import {
|
|
20
26
|
processAnnouncement
|
|
21
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-2BCPVUSF.js";
|
|
22
28
|
import {
|
|
23
29
|
processConversation
|
|
24
30
|
} from "./chunk-BD7CY42U.js";
|
|
@@ -28,12 +34,6 @@ import {
|
|
|
28
34
|
import {
|
|
29
35
|
isPermanentJobError
|
|
30
36
|
} from "./chunk-DGHWRQXL.js";
|
|
31
|
-
import {
|
|
32
|
-
cronRefreshSkills
|
|
33
|
-
} from "./chunk-RQ2TMLKG.js";
|
|
34
|
-
import {
|
|
35
|
-
cronSuggestInstructions
|
|
36
|
-
} from "./chunk-YBNW7ARH.js";
|
|
37
37
|
import {
|
|
38
38
|
ApiError,
|
|
39
39
|
apiGet,
|
|
@@ -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,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
removeWorktree
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import {
|
|
5
|
-
PermanentJobError
|
|
6
|
-
} from "./chunk-DGHWRQXL.js";
|
|
3
|
+
} from "./chunk-FUS5C4SB.js";
|
|
7
4
|
import {
|
|
8
5
|
agentRuns,
|
|
9
6
|
getDb,
|
|
10
7
|
sessions
|
|
11
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-42A3HEEH.js";
|
|
9
|
+
import {
|
|
10
|
+
PermanentJobError
|
|
11
|
+
} from "./chunk-DGHWRQXL.js";
|
|
12
12
|
|
|
13
13
|
// src/lib/handlers/process-run-cleanup.ts
|
|
14
14
|
import { and, eq, isNotNull } from "drizzle-orm";
|
|
@@ -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,16 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../../chunk-
|
|
4
|
-
import "../../chunk-R5MFHGHR.js";
|
|
5
|
-
import "../../chunk-RQ2TMLKG.js";
|
|
6
|
-
import "../../chunk-YBNW7ARH.js";
|
|
7
|
-
import "../../chunk-FZKLLNDS.js";
|
|
8
|
-
import "../../chunk-K3BFZ7KG.js";
|
|
9
|
-
import "../../chunk-NIDLJO6F.js";
|
|
10
|
-
import "../../chunk-7FAA3YNB.js";
|
|
11
|
-
import "../../chunk-EKP32DLN.js";
|
|
12
|
-
import "../../chunk-OQSQC7VB.js";
|
|
13
|
-
import "../../chunk-KRBQLMOP.js";
|
|
2
|
+
MANUAL_TASK_META
|
|
3
|
+
} from "../../chunk-JFDWKLQC.js";
|
|
14
4
|
import {
|
|
15
5
|
BaseCommand
|
|
16
6
|
} from "../../chunk-S56LOTGQ.js";
|
|
@@ -31,7 +21,7 @@ var CronsList = class _CronsList extends BaseCommand {
|
|
|
31
21
|
async run() {
|
|
32
22
|
await this.parse(_CronsList);
|
|
33
23
|
printTable({
|
|
34
|
-
data:
|
|
24
|
+
data: MANUAL_TASK_META.map((t) => ({
|
|
35
25
|
Name: t.name,
|
|
36
26
|
"Scheduled by": t.scheduledBy,
|
|
37
27
|
Description: t.description
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/commands/crons/list.ts"],
|
|
4
|
-
"sourcesContent": ["import { printTable } from '@oclif/table';\nimport { BaseCommand } from '../../base-command.js';\nimport {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { printTable } from '@oclif/table';\nimport { BaseCommand } from '../../base-command.js';\n// Metadata-only import: pulling the full manual-tasks module here would drag the\n// heavy worker handlers (postgres/OTel/Sentry) into this command's bundle chunk\n// and crash a standalone npm install at load. See manual-tasks-meta.ts.\nimport { MANUAL_TASK_META } from '../../lib/manual-tasks-meta.js';\n\nexport default class CronsList extends BaseCommand {\n static override description = 'List manually-runnable maintenance tasks';\n\n static override examples = [\n '<%= config.bin %> crons list',\n ];\n\n async run(): Promise<void> {\n await this.parse(CronsList);\n printTable({\n data: MANUAL_TASK_META.map((t) => ({\n Name: t.name,\n 'Scheduled by': t.scheduledBy,\n Description: t.description,\n })),\n columns: ['Name', 'Scheduled by', 'Description'],\n });\n this.log('');\n this.log('Scheduled crons run in Supabase pg_cron \u2014 audit them with:');\n this.log(\" psql \\\"$DATABASE_URL\\\" -c 'select jobname, schedule, active from cron.job;'\");\n this.log('Run any task above on demand with: rulemetric crons run <name>');\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;AAAA,SAAS,kBAAkB;AAO3B,IAAqB,YAArB,MAAqB,mBAAkB,YAAY;AAAA,EAC/C,OAAgB,cAAc;AAAA,EAE9B,OAAgB,WAAW;AAAA,IACvB;AAAA,EACJ;AAAA,EAEA,MAAM,MAAqB;AACvB,UAAM,KAAK,MAAM,UAAS;AAC1B,eAAW;AAAA,MACP,MAAM,iBAAiB,IAAI,CAAC,OAAO;AAAA,QAC/B,MAAM,EAAE;AAAA,QACR,gBAAgB,EAAE;AAAA,QAClB,aAAa,EAAE;AAAA,MACnB,EAAE;AAAA,MACF,SAAS,CAAC,QAAQ,gBAAgB,aAAa;AAAA,IACnD,CAAC;AACD,SAAK,IAAI,EAAE;AACX,SAAK,IAAI,iEAA4D;AACrE,SAAK,IAAI,6EAA+E;AACxF,SAAK,IAAI,gEAAgE;AAAA,EAC7E;AACJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "../../chunk-DU73QUS3.js";
|
|
5
|
-
import "../../chunk-R5MFHGHR.js";
|
|
6
|
-
import "../../chunk-RQ2TMLKG.js";
|
|
7
|
-
import "../../chunk-YBNW7ARH.js";
|
|
8
|
-
import "../../chunk-FZKLLNDS.js";
|
|
9
|
-
import "../../chunk-K3BFZ7KG.js";
|
|
10
|
-
import "../../chunk-NIDLJO6F.js";
|
|
11
|
-
import "../../chunk-7FAA3YNB.js";
|
|
12
|
-
import "../../chunk-EKP32DLN.js";
|
|
13
|
-
import "../../chunk-OQSQC7VB.js";
|
|
14
|
-
import "../../chunk-KRBQLMOP.js";
|
|
2
|
+
MANUAL_TASK_META
|
|
3
|
+
} from "../../chunk-JFDWKLQC.js";
|
|
15
4
|
import {
|
|
16
5
|
BaseCommand
|
|
17
6
|
} from "../../chunk-S56LOTGQ.js";
|
|
@@ -45,11 +34,24 @@ var CronsRun = class _CronsRun extends BaseCommand {
|
|
|
45
34
|
async run() {
|
|
46
35
|
const { args, flags } = await this.parse(_CronsRun);
|
|
47
36
|
this.requireAuth();
|
|
48
|
-
const def =
|
|
37
|
+
const def = MANUAL_TASK_META.find((t) => t.name === args.name);
|
|
49
38
|
if (!def) {
|
|
50
39
|
this.error(`Unknown task: ${args.name}
|
|
51
40
|
Run \`rulemetric crons list\` to see available tasks.`);
|
|
52
41
|
}
|
|
42
|
+
let runManualTask;
|
|
43
|
+
try {
|
|
44
|
+
({ runManualTask } = await import("../../lib/manual-tasks.js"));
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const code = err?.code;
|
|
47
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
48
|
+
this.error(
|
|
49
|
+
`\`crons run\` needs the worker runtime, which isn't available in this install.
|
|
50
|
+
These maintenance tasks connect to the database directly \u2014 run them from a full RuleMetric checkout with DATABASE_URL and the worker dependencies installed.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
53
55
|
this.log(`\u25B6 Running ${def.name} \u2026`);
|
|
54
56
|
const startedAt = Date.now();
|
|
55
57
|
const result = await runManualTask(args.name);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/commands/crons/run.ts"],
|
|
4
|
-
"sourcesContent": ["import { Args, Flags } from '@oclif/core';\nimport { BaseCommand } from '../../base-command.js';\nimport {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { Args, Flags } from '@oclif/core';\nimport { BaseCommand } from '../../base-command.js';\n// Metadata-only static import (dependency-free). The executor (runManualTask) and\n// its heavy db/OTel/Sentry handlers are lazy-imported inside run(), so loading\n// this command never pulls `postgres` into its bundle chunk and a standalone\n// npm install no longer crashes at module-load. See manual-tasks-meta.ts.\nimport { MANUAL_TASK_META } from '../../lib/manual-tasks-meta.js';\n\nexport default class CronsRun extends BaseCommand {\n static override description = 'Run a maintenance task synchronously, right now';\n\n static override examples = [\n '<%= config.bin %> crons run cron_check_changelog',\n '<%= config.bin %> crons run cron_suggest_instructions --quiet',\n ];\n\n static override args = {\n name: Args.string({\n description: 'Task name (run `rulemetric crons list` to see all)',\n required: true,\n }),\n };\n\n static override flags = {\n quiet: Flags.boolean({\n char: 'q',\n description: 'Suppress per-line log output; print summary only',\n }),\n };\n\n async run(): Promise<void> {\n const { args, flags } = await this.parse(CronsRun);\n this.requireAuth();\n\n // Validate against the dependency-free metadata first, so an unknown task\n // never triggers the lazy load of the heavy executor below.\n const def = MANUAL_TASK_META.find((t) => t.name === args.name);\n if (!def) {\n this.error(`Unknown task: ${args.name}\\nRun \\`rulemetric crons list\\` to see available tasks.`);\n }\n\n // Lazy-load the executor only when a task is actually run. This keeps the\n // heavy handler chunk (postgres/OTel/Sentry) out of this command's static\n // import graph. Executing a task still needs the worker runtime\n // (DATABASE_URL + those deps); a standalone npm install lacks them, so\n // turn the raw MODULE_NOT_FOUND into an actionable message.\n let runManualTask: typeof import('../../lib/manual-tasks.js').runManualTask;\n try {\n ({ runManualTask } = await import('../../lib/manual-tasks.js'));\n } catch (err) {\n const code = (err as { code?: string } | null)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {\n this.error(\n `\\`crons run\\` needs the worker runtime, which isn't available in this install.\\n` +\n `These maintenance tasks connect to the database directly \u2014 run them from a full ` +\n `RuleMetric checkout with DATABASE_URL and the worker dependencies installed.`,\n );\n }\n throw err;\n }\n\n this.log(`\u25B6 Running ${def.name} \u2026`);\n const startedAt = Date.now();\n const result = await runManualTask(args.name);\n\n if (!flags.quiet) {\n for (const line of result.logs) {\n const prefix = line.level === 'error' || line.level === 'warn' ? `[${line.level}]` : '';\n this.log(` ${prefix} ${line.message}`);\n }\n }\n\n const seconds = ((result.durationMs ?? Date.now() - startedAt) / 1000).toFixed(1);\n if (result.ok) {\n this.log(`\u2713 ${def.name} completed in ${seconds}s (${result.logs.length} log line${result.logs.length === 1 ? '' : 's'})`);\n } else {\n this.error(`\u2717 ${def.name} failed after ${seconds}s: ${result.error}`);\n }\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;AAAA,SAAS,MAAM,aAAa;AAQ5B,IAAqB,WAArB,MAAqB,kBAAiB,YAAY;AAAA,EAC9C,OAAgB,cAAc;AAAA,EAE9B,OAAgB,WAAW;AAAA,IACvB;AAAA,IACA;AAAA,EACJ;AAAA,EAEA,OAAgB,OAAO;AAAA,IACnB,MAAM,KAAK,OAAO;AAAA,MACd,aAAa;AAAA,MACb,UAAU;AAAA,IACd,CAAC;AAAA,EACL;AAAA,EAEA,OAAgB,QAAQ;AAAA,IACpB,OAAO,MAAM,QAAQ;AAAA,MACjB,MAAM;AAAA,MACN,aAAa;AAAA,IACjB,CAAC;AAAA,EACL;AAAA,EAEA,MAAM,MAAqB;AACvB,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,MAAM,SAAQ;AACjD,SAAK,YAAY;AAIjB,UAAM,MAAM,iBAAiB,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI;AAC7D,QAAI,CAAC,KAAK;AACN,WAAK,MAAM,iBAAiB,KAAK,IAAI;AAAA,sDAAyD;AAAA,IAClG;AAOA,QAAI;AACJ,QAAI;AACA,OAAC,EAAE,cAAc,IAAI,MAAM,OAAO,2BAA2B;AAAA,IACjE,SAAS,KAAK;AACV,YAAM,OAAQ,KAAkC;AAChD,UAAI,SAAS,0BAA0B,SAAS,oBAAoB;AAChE,aAAK;AAAA,UACD;AAAA;AAAA,QAGJ;AAAA,MACJ;AACA,YAAM;AAAA,IACV;AAEA,SAAK,IAAI,kBAAa,IAAI,IAAI,SAAI;AAClC,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,SAAS,MAAM,cAAc,KAAK,IAAI;AAE5C,QAAI,CAAC,MAAM,OAAO;AACd,iBAAW,QAAQ,OAAO,MAAM;AAC5B,cAAM,SAAS,KAAK,UAAU,WAAW,KAAK,UAAU,SAAS,IAAI,KAAK,KAAK,MAAM;AACrF,aAAK,IAAI,KAAK,MAAM,IAAI,KAAK,OAAO,EAAE;AAAA,MAC1C;AAAA,IACJ;AAEA,UAAM,YAAY,OAAO,cAAc,KAAK,IAAI,IAAI,aAAa,KAAM,QAAQ,CAAC;AAChF,QAAI,OAAO,IAAI;AACX,WAAK,IAAI,UAAK,IAAI,IAAI,iBAAiB,OAAO,MAAM,OAAO,KAAK,MAAM,YAAY,OAAO,KAAK,WAAW,IAAI,KAAK,GAAG,GAAG;AAAA,IAC5H,OAAO;AACH,WAAK,MAAM,UAAK,IAAI,IAAI,iBAAiB,OAAO,MAAM,OAAO,KAAK,EAAE;AAAA,IACxE;AAAA,EACJ;AACJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runAgentLoop
|
|
3
|
-
} from "../../chunk-
|
|
3
|
+
} from "../../chunk-LCT6MT2V.js";
|
|
4
4
|
import "../../chunk-JX5UI6RW.js";
|
|
5
|
-
import "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
5
|
+
import "../../chunk-EHABPBGH.js";
|
|
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
|
-
import "../../chunk-GQCISMAH.js";
|
|
13
|
-
import "../../chunk-BD7CY42U.js";
|
|
14
|
-
import "../../chunk-QJXAULRC.js";
|
|
15
|
-
import "../../chunk-DGHWRQXL.js";
|
|
16
|
-
import "../../chunk-J7N3DLH6.js";
|
|
17
|
-
import "../../chunk-52WIYRZH.js";
|
|
18
|
-
import "../../chunk-IWYCRLEK.js";
|
|
19
|
-
import "../../chunk-B5KNTGMJ.js";
|
|
20
|
-
import "../../chunk-3TIMQ3O6.js";
|
|
21
|
-
import "../../chunk-YNIH3KMU.js";
|
|
22
|
-
import "../../chunk-QN34TGD3.js";
|
|
23
12
|
import "../../chunk-RQ2TMLKG.js";
|
|
24
13
|
import "../../chunk-YBNW7ARH.js";
|
|
25
14
|
import "../../chunk-FZKLLNDS.js";
|
|
15
|
+
import "../../chunk-2BCPVUSF.js";
|
|
26
16
|
import "../../chunk-K3BFZ7KG.js";
|
|
27
|
-
import "../../chunk-
|
|
17
|
+
import "../../chunk-QSN77T7C.js";
|
|
28
18
|
import {
|
|
29
19
|
closeDb
|
|
30
|
-
} from "../../chunk-
|
|
20
|
+
} from "../../chunk-42A3HEEH.js";
|
|
21
|
+
import "../../chunk-BD7CY42U.js";
|
|
22
|
+
import "../../chunk-QJXAULRC.js";
|
|
23
|
+
import "../../chunk-DGHWRQXL.js";
|
|
24
|
+
import "../../chunk-J7N3DLH6.js";
|
|
25
|
+
import "../../chunk-52WIYRZH.js";
|
|
26
|
+
import "../../chunk-IWYCRLEK.js";
|
|
31
27
|
import "../../chunk-EKP32DLN.js";
|
|
28
|
+
import "../../chunk-4QGATD5Y.js";
|
|
32
29
|
import "../../chunk-OQSQC7VB.js";
|
|
33
30
|
import "../../chunk-KRBQLMOP.js";
|
|
31
|
+
import "../../chunk-3TIMQ3O6.js";
|
|
32
|
+
import "../../chunk-YNIH3KMU.js";
|
|
33
|
+
import "../../chunk-QN34TGD3.js";
|
|
34
34
|
import {
|
|
35
35
|
BaseCommand
|
|
36
36
|
} from "../../chunk-S56LOTGQ.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;
|