mixdog 0.9.0 → 0.9.2
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/package.json +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Telegram MarkdownV2 conversion — a faithful JS port of hermes-agent's
|
|
2
|
+
// gateway/platforms/telegram.py (functions: _MDV2_ESCAPE_RE, _escape_mdv2,
|
|
3
|
+
// _strip_mdv2, _TABLE_SEPARATOR_RE, _is_table_row, _split_markdown_table_row,
|
|
4
|
+
// _render_table_block_for_telegram, _wrap_markdown_tables, and the 12-step
|
|
5
|
+
// format_message pipeline). The Python behaviour is battle-tested; this port
|
|
6
|
+
// keeps the same step order and the same table/header/blockquote decisions so
|
|
7
|
+
// output matches the reference.
|
|
8
|
+
//
|
|
9
|
+
// Exported:
|
|
10
|
+
// toMarkdownV2(text) — the main converter (port of format_message)
|
|
11
|
+
// stripMdV2(text) — plain-text fallback (port of _strip_mdv2)
|
|
12
|
+
// escapeMdV2(text) — raw escape helper (port of _escape_mdv2)
|
|
13
|
+
// isParseEntitiesError(err) — detect a MarkdownV2 400 parse failure
|
|
14
|
+
|
|
15
|
+
// Matches every character MarkdownV2 requires to be backslash-escaped outside a
|
|
16
|
+
// code span / fenced block. Port of _MDV2_ESCAPE_RE:
|
|
17
|
+
// Python: r'([_*\[\]()~`>#\+\-=|{}.!\\])'
|
|
18
|
+
const MDV2_ESCAPE_RE = /([_*[\]()~`>#+\-=|{}.!\\])/g;
|
|
19
|
+
|
|
20
|
+
/** Port of _escape_mdv2: backslash-escape all MarkdownV2 specials. */
|
|
21
|
+
export function escapeMdV2(text) {
|
|
22
|
+
return String(text).replace(MDV2_ESCAPE_RE, "\\$1");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Port of _strip_mdv2: remove MarkdownV2 escape backslashes AND the entity
|
|
27
|
+
* markers format_message introduced, producing clean plain text for the
|
|
28
|
+
* parse-error fallback.
|
|
29
|
+
*/
|
|
30
|
+
export function stripMdV2(text) {
|
|
31
|
+
let cleaned = String(text);
|
|
32
|
+
// Remove escape backslashes before special characters.
|
|
33
|
+
cleaned = cleaned.replace(/\\([_*[\]()~`>#+\-=|{}.!\\])/g, "$1");
|
|
34
|
+
// Remove bold markers (*text* → text).
|
|
35
|
+
cleaned = cleaned.replace(/\*([^*]+)\*/g, "$1");
|
|
36
|
+
// Remove italic markers (_text_ → text). Word-boundary guards keep
|
|
37
|
+
// snake_case identifiers intact (mirrors the Python lookbehind/lookahead).
|
|
38
|
+
cleaned = cleaned.replace(/(^|\W)_([^_]+)_(?!\w)/g, "$1$2");
|
|
39
|
+
// Remove strikethrough markers (~text~ → text).
|
|
40
|
+
cleaned = cleaned.replace(/~([^~]+)~/g, "$1");
|
|
41
|
+
// Remove spoiler markers (||text|| → text).
|
|
42
|
+
cleaned = cleaned.replace(/\|\|([^|]+)\|\|/g, "$1");
|
|
43
|
+
return cleaned;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Markdown table → Telegram-friendly row groups ──────────────────────────
|
|
47
|
+
// Port of _TABLE_SEPARATOR_RE / _is_table_row / _split_markdown_table_row /
|
|
48
|
+
// _render_table_block_for_telegram / _wrap_markdown_tables.
|
|
49
|
+
|
|
50
|
+
// Python: r'^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*){1,}\|?\s*$'
|
|
51
|
+
const TABLE_SEPARATOR_RE = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*){1,}\|?\s*$/;
|
|
52
|
+
|
|
53
|
+
function isTableRow(line) {
|
|
54
|
+
const stripped = line.trim();
|
|
55
|
+
return !!stripped && stripped.includes("|");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitMarkdownTableRow(line) {
|
|
59
|
+
let stripped = line.trim();
|
|
60
|
+
if (stripped.startsWith("|")) stripped = stripped.slice(1);
|
|
61
|
+
if (stripped.endsWith("|")) stripped = stripped.slice(0, -1);
|
|
62
|
+
return stripped.split("|").map((cell) => cell.trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderTableBlockForTelegram(tableBlock) {
|
|
66
|
+
if (tableBlock.length < 3) return tableBlock.join("\n");
|
|
67
|
+
|
|
68
|
+
const headers = splitMarkdownTableRow(tableBlock[0]);
|
|
69
|
+
if (headers.length < 2) return tableBlock.join("\n");
|
|
70
|
+
|
|
71
|
+
// Row-label column present when data rows carry one more cell than headers.
|
|
72
|
+
const firstDataRow = tableBlock.length > 2 ? splitMarkdownTableRow(tableBlock[2]) : [];
|
|
73
|
+
const hasRowLabelCol = firstDataRow.length === headers.length + 1;
|
|
74
|
+
|
|
75
|
+
const renderedGroups = [];
|
|
76
|
+
for (let idx = 0; idx < tableBlock.length - 2; idx++) {
|
|
77
|
+
const row = tableBlock[idx + 2];
|
|
78
|
+
const cells = splitMarkdownTableRow(row);
|
|
79
|
+
let heading;
|
|
80
|
+
let dataCells;
|
|
81
|
+
if (hasRowLabelCol) {
|
|
82
|
+
heading = cells.length && cells[0] ? cells[0] : `Row ${idx + 1}`;
|
|
83
|
+
dataCells = cells.slice(1);
|
|
84
|
+
} else {
|
|
85
|
+
heading = cells.find((c) => c) ?? `Row ${idx + 1}`;
|
|
86
|
+
dataCells = cells.slice();
|
|
87
|
+
}
|
|
88
|
+
// Pad / trim dataCells to headers length.
|
|
89
|
+
if (dataCells.length < headers.length) {
|
|
90
|
+
dataCells = dataCells.concat(Array(headers.length - dataCells.length).fill(""));
|
|
91
|
+
} else if (dataCells.length > headers.length) {
|
|
92
|
+
dataCells = dataCells.slice(0, headers.length);
|
|
93
|
+
}
|
|
94
|
+
const bullets = [];
|
|
95
|
+
for (let c = 0; c < headers.length; c++) {
|
|
96
|
+
const value = dataCells[c];
|
|
97
|
+
// Skip a bullet that just duplicates the heading (no row-label column).
|
|
98
|
+
if (!hasRowLabelCol && value === heading) continue;
|
|
99
|
+
bullets.push(`• ${headers[c]}: ${value}`);
|
|
100
|
+
}
|
|
101
|
+
// The heading is emitted as `**heading**` markdown so step 5 (bold) of the
|
|
102
|
+
// main pipeline converts it — matches the Python ordering intentionally.
|
|
103
|
+
const groupLines = [`**${heading}**`, ...bullets];
|
|
104
|
+
renderedGroups.push(groupLines.join("\n"));
|
|
105
|
+
}
|
|
106
|
+
return renderedGroups.join("\n\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wrapMarkdownTables(text) {
|
|
110
|
+
if (!text.includes("|") || !text.includes("-")) return text;
|
|
111
|
+
const lines = text.split("\n");
|
|
112
|
+
const out = [];
|
|
113
|
+
let inFence = false;
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < lines.length) {
|
|
116
|
+
const line = lines[i];
|
|
117
|
+
const stripped = line.replace(/^\s+/, "");
|
|
118
|
+
// Track existing fenced code blocks — never touch content inside.
|
|
119
|
+
if (stripped.startsWith("```")) {
|
|
120
|
+
inFence = !inFence;
|
|
121
|
+
out.push(line);
|
|
122
|
+
i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (inFence) {
|
|
126
|
+
out.push(line);
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Header row (contains '|') immediately followed by a delimiter row.
|
|
131
|
+
if (line.includes("|") && i + 1 < lines.length && TABLE_SEPARATOR_RE.test(lines[i + 1])) {
|
|
132
|
+
const tableBlock = [line, lines[i + 1]];
|
|
133
|
+
let j = i + 2;
|
|
134
|
+
while (j < lines.length && isTableRow(lines[j])) {
|
|
135
|
+
tableBlock.push(lines[j]);
|
|
136
|
+
j++;
|
|
137
|
+
}
|
|
138
|
+
out.push(renderTableBlockForTelegram(tableBlock));
|
|
139
|
+
i = j;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
out.push(line);
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
return out.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Main converter — port of format_message (12-step placeholder pipeline) ──
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Convert standard/assistant markdown into Telegram MarkdownV2.
|
|
152
|
+
*
|
|
153
|
+
* Protected regions (fenced code, inline code, links) are stashed behind
|
|
154
|
+
* `\x00PH{n}\x00` placeholders BEFORE escaping so their interiors survive; the
|
|
155
|
+
* remaining text is converted to MarkdownV2 entities then fully escaped, and
|
|
156
|
+
* placeholders are restored in reverse insertion order.
|
|
157
|
+
*/
|
|
158
|
+
export function toMarkdownV2(content) {
|
|
159
|
+
if (!content) return content;
|
|
160
|
+
|
|
161
|
+
const placeholders = new Map();
|
|
162
|
+
let counter = 0;
|
|
163
|
+
const ph = (value) => {
|
|
164
|
+
const key = `\x00PH${counter}\x00`;
|
|
165
|
+
counter += 1;
|
|
166
|
+
placeholders.set(key, value);
|
|
167
|
+
return key;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let text = String(content);
|
|
171
|
+
|
|
172
|
+
// 0) Rewrite GFM pipe tables into Telegram-friendly row groups first.
|
|
173
|
+
text = wrapMarkdownTables(text);
|
|
174
|
+
|
|
175
|
+
// 1) Protect fenced code blocks (``` ... ```). Inside pre/code only \ and `
|
|
176
|
+
// must be escaped per the MarkdownV2 spec.
|
|
177
|
+
text = text.replace(/(```(?:[^\n]*\n)?[\s\S]*?```)/g, (raw) => {
|
|
178
|
+
// Split off the opening ``` (+ optional language line) and closing ```.
|
|
179
|
+
const openEnd = raw.slice(3).includes("\n") ? raw.indexOf("\n") + 1 : 3;
|
|
180
|
+
const opening = raw.slice(0, openEnd);
|
|
181
|
+
const bodyAndClose = raw.slice(openEnd);
|
|
182
|
+
let body = bodyAndClose.slice(0, -3);
|
|
183
|
+
body = body.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
184
|
+
return ph(opening + body + "```");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 2) Protect inline code (`...`). Escape \ inside per spec.
|
|
188
|
+
text = text.replace(/(`[^`]+`)/g, (m) => ph(m.replace(/\\/g, "\\\\")));
|
|
189
|
+
|
|
190
|
+
// 3) Convert markdown links — escape display text; inside the URL only ')'
|
|
191
|
+
// and '\' need escaping.
|
|
192
|
+
text = text.replace(/\[([^\]]+)\]\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g, (_m, display, url) => {
|
|
193
|
+
const d = escapeMdV2(display);
|
|
194
|
+
const u = url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)");
|
|
195
|
+
return ph(`[${d}](${u})`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// 4) Headers (## Title) → bold *Title* (MarkdownV2 has no headers).
|
|
199
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, (_m, inner) => {
|
|
200
|
+
const stripped = inner.trim().replace(/\*\*(.+?)\*\*/g, "$1");
|
|
201
|
+
return ph(`*${escapeMdV2(stripped)}*`);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// 5) Bold: **text** → *text*.
|
|
205
|
+
text = text.replace(/\*\*(.+?)\*\*/g, (_m, inner) => ph(`*${escapeMdV2(inner)}*`));
|
|
206
|
+
|
|
207
|
+
// 6) Italic: *text* (single asterisk) → _text_. [^*\n]+ avoids matching
|
|
208
|
+
// across newlines (which would corrupt * bullet lists / multi-line text).
|
|
209
|
+
text = text.replace(/\*([^*\n]+)\*/g, (_m, inner) => ph(`_${escapeMdV2(inner)}_`));
|
|
210
|
+
|
|
211
|
+
// 7) Strikethrough: ~~text~~ → ~text~.
|
|
212
|
+
text = text.replace(/~~(.+?)~~/g, (_m, inner) => ph(`~${escapeMdV2(inner)}~`));
|
|
213
|
+
|
|
214
|
+
// 8) Spoiler: ||text|| → ||text|| (protect from | escaping).
|
|
215
|
+
text = text.replace(/\|\|(.+?)\|\|/g, (_m, inner) => ph(`||${escapeMdV2(inner)}||`));
|
|
216
|
+
|
|
217
|
+
// 9) Blockquotes: line-leading >, >>, >>>, **>, **>> etc. Preserve the
|
|
218
|
+
// prefix from escaping; an expandable quote (**> … ||) keeps a trailing ||.
|
|
219
|
+
text = text.replace(/^((?:\*\*)?>{1,3}) (.+)$/gm, (_m, prefix, quoteContent) => {
|
|
220
|
+
if (prefix.startsWith("**") && quoteContent.endsWith("||")) {
|
|
221
|
+
return ph(`${prefix} ${escapeMdV2(quoteContent.slice(0, -2))}||`);
|
|
222
|
+
}
|
|
223
|
+
return ph(`${prefix} ${escapeMdV2(quoteContent)}`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// 10) Escape all remaining special characters in the plain text.
|
|
227
|
+
text = escapeMdV2(text);
|
|
228
|
+
|
|
229
|
+
// 11) Restore placeholders in REVERSE insertion order so a placeholder
|
|
230
|
+
// nested inside another resolves correctly.
|
|
231
|
+
const keys = [...placeholders.keys()].reverse();
|
|
232
|
+
for (const key of keys) {
|
|
233
|
+
text = text.split(key).join(placeholders.get(key));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 12) Safety net: escape bare ( ) { } that slipped past placeholder
|
|
237
|
+
// processing, WITHOUT touching content inside code spans/blocks.
|
|
238
|
+
const codeSplit = text.split(/(```[\s\S]*?```|`[^`]+`)/g);
|
|
239
|
+
const safeParts = [];
|
|
240
|
+
for (let idx = 0; idx < codeSplit.length; idx++) {
|
|
241
|
+
const seg = codeSplit[idx];
|
|
242
|
+
if (idx % 2 === 1) {
|
|
243
|
+
// Inside a code span/block — leave untouched.
|
|
244
|
+
safeParts.push(seg);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
safeParts.push(seg.replace(/[(){}]/g, (ch, s) => {
|
|
248
|
+
// Already escaped.
|
|
249
|
+
if (s > 0 && seg[s - 1] === "\\") return ch;
|
|
250
|
+
// '(' that opens a MarkdownV2 link [text](url).
|
|
251
|
+
if (ch === "(" && s > 0 && seg[s - 1] === "]") return ch;
|
|
252
|
+
// ')' that closes a link URL.
|
|
253
|
+
if (ch === ")") {
|
|
254
|
+
const before = seg.slice(0, s);
|
|
255
|
+
if (before.includes("](http") || before.includes("](")) {
|
|
256
|
+
let depth = 0;
|
|
257
|
+
for (let j = s - 1; j >= Math.max(s - 2000, 0); j--) {
|
|
258
|
+
if (seg[j] === "(") {
|
|
259
|
+
depth -= 1;
|
|
260
|
+
if (depth < 0) {
|
|
261
|
+
if (j > 0 && seg[j - 1] === "]") return ch;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
} else if (seg[j] === ")") {
|
|
265
|
+
depth += 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return "\\" + ch;
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
text = safeParts.join("");
|
|
274
|
+
|
|
275
|
+
return text;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** True when a Bot API error looks like a MarkdownV2 parse failure. */
|
|
279
|
+
export function isParseEntitiesError(err) {
|
|
280
|
+
const status = err?.status;
|
|
281
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
282
|
+
return status === 400 && /can't parse entities|entities/i.test(msg);
|
|
283
|
+
}
|
|
@@ -42,7 +42,7 @@ function isMemoryFile(filePath) {
|
|
|
42
42
|
function isHidden(name) {
|
|
43
43
|
if (HIDDEN_TOOLS.has(name)) return true;
|
|
44
44
|
if (formatToolSurface(name, {}).label === "Memory") return false;
|
|
45
|
-
if (name
|
|
45
|
+
if (name === "reply" || name === "react" || name === "edit_message" || name === "fetch" || name === "download_attachment") return true;
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -116,7 +116,6 @@ export {
|
|
|
116
116
|
HIDDEN_TOOLS,
|
|
117
117
|
isRecallMemory,
|
|
118
118
|
isMemoryFile,
|
|
119
|
-
isHidden,
|
|
120
119
|
buildDedupKey,
|
|
121
120
|
buildToolLine
|
|
122
121
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readdirSync, existsSync, statSync } from "fs";
|
|
2
2
|
import { basename, join, resolve } from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
3
|
import {
|
|
5
4
|
cwdToProjectSlug,
|
|
6
5
|
discoverCurrentClaudeSession,
|
|
@@ -142,6 +141,25 @@ function candidateAffinity(candidate) {
|
|
|
142
141
|
}
|
|
143
142
|
const TRANSCRIPT_MTIME_DECISIVE_MS = 30_000;
|
|
144
143
|
function compareTranscriptCandidates(left, right) {
|
|
144
|
+
// Identity beats recency. A live "parent-chain" candidate is the session
|
|
145
|
+
// that actually forked THIS owner worker (process.ppid walk) — it is the
|
|
146
|
+
// same session that receives injected input. When another co-located
|
|
147
|
+
// session (same cwd) writes its transcript more recently, the mtime rule
|
|
148
|
+
// below would otherwise hand the output-forwarder the wrong session's
|
|
149
|
+
// transcript: input lands in our window while output tails the sibling.
|
|
150
|
+
// Anchoring on the parent-chain session keeps forward output pinned to the
|
|
151
|
+
// owning session and lets it "steal back" the binding from a busier
|
|
152
|
+
// neighbour. Only decisive when exactly one side is the live self session;
|
|
153
|
+
// standalone remote (no parent session) falls through to the heuristics.
|
|
154
|
+
const leftSelf = Boolean(left.active && left.parentChain);
|
|
155
|
+
const rightSelf = Boolean(right.active && right.parentChain);
|
|
156
|
+
if (leftSelf !== rightSelf) return rightSelf ? 1 : -1;
|
|
157
|
+
// A live same-cwd session is a stronger ownership signal than an older
|
|
158
|
+
// transcript's mtime. The previous ordering let a stale-but-recent transcript
|
|
159
|
+
// keep winning after a remote runtime restart, so the output forwarder stayed
|
|
160
|
+
// bound to the old JSONL while inbound was delivered to the live session.
|
|
161
|
+
const affinityDiff = candidateAffinity(right) - candidateAffinity(left);
|
|
162
|
+
if (affinityDiff !== 0 && (left.active || right.active)) return affinityDiff;
|
|
145
163
|
const leftMtime = Number(left.transcriptMtime) || 0;
|
|
146
164
|
const rightMtime = Number(right.transcriptMtime) || 0;
|
|
147
165
|
if (leftMtime > 0 && rightMtime > 0) {
|
|
@@ -149,7 +167,6 @@ function compareTranscriptCandidates(left, right) {
|
|
|
149
167
|
if (mtimeDelta >= TRANSCRIPT_MTIME_DECISIVE_MS) return 1;
|
|
150
168
|
if (-mtimeDelta >= TRANSCRIPT_MTIME_DECISIVE_MS) return -1;
|
|
151
169
|
}
|
|
152
|
-
const affinityDiff = candidateAffinity(right) - candidateAffinity(left);
|
|
153
170
|
if (affinityDiff !== 0) return affinityDiff;
|
|
154
171
|
if (Number(right.exists) !== Number(left.exists)) return Number(right.exists) - Number(left.exists);
|
|
155
172
|
if (right.transcriptMtime !== left.transcriptMtime) return right.transcriptMtime - left.transcriptMtime;
|
|
@@ -181,16 +198,8 @@ function discoverSessionBoundTranscript() {
|
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
export {
|
|
184
|
-
resolveTranscriptForSession,
|
|
185
201
|
findLatestTranscriptByMtime,
|
|
186
|
-
isPidAlive,
|
|
187
202
|
sameResolvedPath,
|
|
188
|
-
transcriptStat,
|
|
189
|
-
sessionTranscriptCandidate,
|
|
190
|
-
latestMtimeTranscriptCandidate,
|
|
191
|
-
candidateAffinity,
|
|
192
|
-
compareTranscriptCandidates,
|
|
193
203
|
detectCurrentSessionTranscript,
|
|
194
|
-
discoverSessionBoundTranscript
|
|
195
|
-
TRANSCRIPT_MTIME_DECISIVE_MS
|
|
204
|
+
discoverSessionBoundTranscript
|
|
196
205
|
};
|
|
@@ -7,6 +7,7 @@ import { getWebhookAuthtoken } from "../../shared/config.mjs";
|
|
|
7
7
|
import { appendFileSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync, existsSync, renameSync, watch as fsWatch } from "fs";
|
|
8
8
|
import { appendFile } from "fs/promises";
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
|
+
import { readMarkdownDocument } from "../../shared/markdown-frontmatter.mjs";
|
|
10
11
|
const WEBHOOKS_DIR = join(DATA_DIR, "webhooks");
|
|
11
12
|
const WEBHOOK_LOG = join(DATA_DIR, "webhook.log");
|
|
12
13
|
let webhookLogBuffer = [];
|
|
@@ -101,15 +102,29 @@ function verifySignature(secret, rawBody, signatureValue, parser) {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// ── Endpoint config loader ─────────────────────────────────────────────
|
|
104
|
-
// Reads DATA_DIR/webhooks/<name>/
|
|
105
|
-
// via POST /webhooks).
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
105
|
+
// Reads DATA_DIR/webhooks/<name>/WEBHOOK.md (written by setup-server.mjs
|
|
106
|
+
// via POST /webhooks). The frontmatter IS the config object; the markdown
|
|
107
|
+
// body is the instructions/prompt. Cached in-memory, invalidated by
|
|
108
|
+
// fs.watch on the webhooks directory. Returns the frontmatter object
|
|
109
|
+
// { secret, parser, channel, model, role, enabled } where routing is by
|
|
110
|
+
// `channel` presence and `role` names a user-workflow entry when set.
|
|
109
111
|
const _endpointCache = new Map();
|
|
110
112
|
let _endpointWatcher = null;
|
|
111
113
|
function _endpointConfigPath(name) {
|
|
112
|
-
return join(WEBHOOKS_DIR, name, "
|
|
114
|
+
return join(WEBHOOKS_DIR, name, "WEBHOOK.md");
|
|
115
|
+
}
|
|
116
|
+
// Per-endpoint HMAC secret is stored in a side file (WEBHOOKS_DIR/<name>/secret),
|
|
117
|
+
// not in WEBHOOK.md frontmatter — frontmatter is a lossy `key: value` format
|
|
118
|
+
// (unquote strips surrounding quotes) and would corrupt user secrets on the
|
|
119
|
+
// save->rewrite round-trip. Read fresh on each verify (no cache): the signing
|
|
120
|
+
// path is not hot and a stale secret would silently reject valid deliveries.
|
|
121
|
+
function _readEndpointSecret(name) {
|
|
122
|
+
try {
|
|
123
|
+
const s = readFileSync(join(WEBHOOKS_DIR, name, "secret"), "utf8").trim();
|
|
124
|
+
return s || null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
113
128
|
}
|
|
114
129
|
function _ensureEndpointWatcher() {
|
|
115
130
|
if (_endpointWatcher) return;
|
|
@@ -117,7 +132,7 @@ function _ensureEndpointWatcher() {
|
|
|
117
132
|
if (!existsSync(WEBHOOKS_DIR)) return;
|
|
118
133
|
_endpointWatcher = fsWatch(WEBHOOKS_DIR, { recursive: true }, (_event, filename) => {
|
|
119
134
|
if (!filename) { _endpointCache.clear(); return; }
|
|
120
|
-
// filename is like "<endpoint>/
|
|
135
|
+
// filename is like "<endpoint>/WEBHOOK.md" or "<endpoint>"
|
|
121
136
|
const parts = String(filename).split(/[\\/]/);
|
|
122
137
|
const endpointName = parts[0];
|
|
123
138
|
if (endpointName) _endpointCache.delete(endpointName);
|
|
@@ -138,7 +153,7 @@ function loadEndpointConfig(name) {
|
|
|
138
153
|
if (!name) return null;
|
|
139
154
|
// A cached entry is only authoritative while the fs.watch handle is
|
|
140
155
|
// armed — otherwise a later mkdir+write of WEBHOOKS_DIR/<name>/
|
|
141
|
-
//
|
|
156
|
+
// WEBHOOK.md has no way to invalidate the cache and a stale `null`
|
|
142
157
|
// (e.g. captured before WEBHOOKS_DIR existed) would pin forever.
|
|
143
158
|
if (_endpointCache.has(name) && _endpointWatcher) return _endpointCache.get(name);
|
|
144
159
|
_ensureEndpointWatcher();
|
|
@@ -151,7 +166,14 @@ function loadEndpointConfig(name) {
|
|
|
151
166
|
return null;
|
|
152
167
|
}
|
|
153
168
|
try {
|
|
154
|
-
|
|
169
|
+
// Frontmatter is the config object; `enabled` arrives as a string and
|
|
170
|
+
// is cast so `endpoint?.enabled === false` gates match a written
|
|
171
|
+
// `enabled: false`.
|
|
172
|
+
const { frontmatter } = readMarkdownDocument(readFileSync(p, "utf8"));
|
|
173
|
+
const cfg = { ...frontmatter };
|
|
174
|
+
if (Object.prototype.hasOwnProperty.call(cfg, "enabled")) {
|
|
175
|
+
cfg.enabled = cfg.enabled !== "false" && cfg.enabled !== false;
|
|
176
|
+
}
|
|
155
177
|
_endpointCache.set(name, cfg);
|
|
156
178
|
return cfg;
|
|
157
179
|
} catch {
|
|
@@ -631,7 +653,7 @@ class WebhookServer {
|
|
|
631
653
|
}
|
|
632
654
|
const _registeredPre = !!(
|
|
633
655
|
_endpointPreCheck
|
|
634
|
-
|| existsSync(join(WEBHOOKS_DIR, name, "
|
|
656
|
+
|| existsSync(join(WEBHOOKS_DIR, name, "WEBHOOK.md"))
|
|
635
657
|
);
|
|
636
658
|
if (!_registeredPre) {
|
|
637
659
|
logWebhook(`rejected: unknown endpoint ${name}`);
|
|
@@ -707,7 +729,7 @@ class WebhookServer {
|
|
|
707
729
|
// routing is reachable only through a registered endpoint.
|
|
708
730
|
const _registered = !!(
|
|
709
731
|
endpoint
|
|
710
|
-
|| existsSync(join(WEBHOOKS_DIR, name, "
|
|
732
|
+
|| existsSync(join(WEBHOOKS_DIR, name, "WEBHOOK.md"))
|
|
711
733
|
);
|
|
712
734
|
if (!_registered) {
|
|
713
735
|
logWebhook(`rejected: unknown endpoint ${name}`);
|
|
@@ -810,7 +832,11 @@ class WebhookServer {
|
|
|
810
832
|
// Returns true when the request may proceed; otherwise writes the
|
|
811
833
|
// appropriate 401/403 response and returns false.
|
|
812
834
|
_verifySignatureGate(name, endpoint, body, headers, res) {
|
|
813
|
-
|
|
835
|
+
// Per-endpoint secret lives in the side file WEBHOOKS_DIR/<name>/secret
|
|
836
|
+
// (plaintext, one line) — NOT in WEBHOOK.md frontmatter, so a user
|
|
837
|
+
// secret containing quotes/colons/newlines round-trips losslessly and
|
|
838
|
+
// setWebhookEnabled (which only rewrites WEBHOOK.md) cannot corrupt it.
|
|
839
|
+
const secret = _readEndpointSecret(name) || this.config.secret;
|
|
814
840
|
const parser = endpoint?.parser || this.config.endpoints?.[name]?.parser;
|
|
815
841
|
if (secret) {
|
|
816
842
|
const signature = extractSignature(headers, parser);
|
|
@@ -836,18 +862,18 @@ class WebhookServer {
|
|
|
836
862
|
res.end(JSON.stringify({ error: "webhook secret required for signed parser" }));
|
|
837
863
|
return false;
|
|
838
864
|
}
|
|
839
|
-
//
|
|
865
|
+
// WEBHOOK.md folder endpoint with no resolved signature
|
|
840
866
|
// mode. handleWebhook's instructions.md branch enqueues the body as
|
|
841
867
|
// an interactive prompt (or dispatches a delegate) — both are
|
|
842
868
|
// privileged. With no per-endpoint secret/parser AND no global
|
|
843
869
|
// secret/parser, there is no signature mode to fall back on, so
|
|
844
870
|
// accepting the request would inject attacker-controlled input.
|
|
845
|
-
// Fail closed. (Endpoints that DO carry a
|
|
871
|
+
// Fail closed. (Endpoints that DO carry a WEBHOOK.md with a
|
|
846
872
|
// secret/parser are handled by the branches above.)
|
|
847
|
-
if (!secret && !parser && existsSync(join(WEBHOOKS_DIR, name, "
|
|
848
|
-
logWebhook(`${name}: rejected (
|
|
873
|
+
if (!secret && !parser && existsSync(join(WEBHOOKS_DIR, name, "WEBHOOK.md"))) {
|
|
874
|
+
logWebhook(`${name}: rejected (WEBHOOK.md endpoint requires a webhook secret)`);
|
|
849
875
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
850
|
-
res.end(JSON.stringify({ error: "webhook secret required for
|
|
876
|
+
res.end(JSON.stringify({ error: "webhook secret required for WEBHOOK.md endpoint" }));
|
|
851
877
|
return false;
|
|
852
878
|
}
|
|
853
879
|
if (!this.noSecretWarned) {
|
|
@@ -1139,21 +1165,24 @@ class WebhookServer {
|
|
|
1139
1165
|
}
|
|
1140
1166
|
// ── Webhook handler ───────────────────────────────────────────────
|
|
1141
1167
|
_readFolderHandler(folderPath) {
|
|
1142
|
-
const
|
|
1168
|
+
const mdPath = join(folderPath, "WEBHOOK.md");
|
|
1143
1169
|
// Routing by channel presence (no `mode` field): an endpoint WITH a
|
|
1144
1170
|
// channel dispatches to the hidden webhook-handler role and reports to
|
|
1145
1171
|
// that channel; an endpoint WITHOUT a channel injects into the current
|
|
1146
1172
|
// (Lead) session. `channel` starts NULL so its absence is detectable;
|
|
1147
1173
|
// `role` defaults to the mandatory webhook-handler for the direct path.
|
|
1148
|
-
// The signature gate (below) fails closed on any
|
|
1174
|
+
// The signature gate (below) fails closed on any WEBHOOK.md
|
|
1149
1175
|
// endpoint lacking a secret, so dropping `mode` does not weaken auth.
|
|
1150
|
-
|
|
1151
|
-
|
|
1176
|
+
// `instructions` carries the markdown body (the prompt) so callers read
|
|
1177
|
+
// one file instead of a config.json + instructions.md pair.
|
|
1178
|
+
const handler = { channel: null, role: "webhook-handler", model: null, instructions: "" };
|
|
1179
|
+
if (existsSync(mdPath)) {
|
|
1152
1180
|
try {
|
|
1153
|
-
const
|
|
1154
|
-
if (
|
|
1155
|
-
if (typeof
|
|
1156
|
-
if (typeof
|
|
1181
|
+
const { frontmatter, body } = readMarkdownDocument(readFileSync(mdPath, "utf8"));
|
|
1182
|
+
if (frontmatter.channel) handler.channel = frontmatter.channel;
|
|
1183
|
+
if (typeof frontmatter.role === "string" && frontmatter.role) handler.role = frontmatter.role;
|
|
1184
|
+
if (typeof frontmatter.model === "string" && frontmatter.model) handler.model = frontmatter.model;
|
|
1185
|
+
handler.instructions = String(body || "").trim();
|
|
1157
1186
|
} catch {
|
|
1158
1187
|
}
|
|
1159
1188
|
}
|
|
@@ -1222,21 +1251,20 @@ ${payload}
|
|
|
1222
1251
|
}
|
|
1223
1252
|
handleWebhook(name, body, headers, res, deliveryId) {
|
|
1224
1253
|
const folderPath = join(WEBHOOKS_DIR, name);
|
|
1225
|
-
const
|
|
1226
|
-
if (existsSync(
|
|
1254
|
+
const mdPath = join(folderPath, "WEBHOOK.md");
|
|
1255
|
+
if (existsSync(mdPath)) {
|
|
1227
1256
|
try {
|
|
1228
|
-
const instructions =
|
|
1229
|
-
const { channel, role, model } = this._readFolderHandler(folderPath);
|
|
1257
|
+
const { channel, role, model, instructions } = this._readFolderHandler(folderPath);
|
|
1230
1258
|
const payloadContent = this._buildFencedPayload(body, headers);
|
|
1231
1259
|
if (channel) {
|
|
1232
1260
|
if (!role) {
|
|
1233
|
-
appendDelivery(name, { id: deliveryId, status: "failed", error: "delegate mode requires role in
|
|
1261
|
+
appendDelivery(name, { id: deliveryId, status: "failed", error: "delegate mode requires role in WEBHOOK.md" });
|
|
1234
1262
|
logWebhook(`${name}: delegate mode requires role - rejected`);
|
|
1235
1263
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1236
1264
|
res.end(JSON.stringify({ status: "rejected", error: "delegate mode requires role" }));
|
|
1237
1265
|
return;
|
|
1238
1266
|
} else if (!model) {
|
|
1239
|
-
appendDelivery(name, { id: deliveryId, status: "failed", error: "delegate mode requires model in
|
|
1267
|
+
appendDelivery(name, { id: deliveryId, status: "failed", error: "delegate mode requires model in WEBHOOK.md" });
|
|
1240
1268
|
logWebhook(`${name}: delegate mode requires model - rejected`);
|
|
1241
1269
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1242
1270
|
res.end(JSON.stringify({ status: "rejected", error: "delegate mode requires model" }));
|
|
@@ -148,7 +148,6 @@ function loadKeytar() {
|
|
|
148
148
|
// Avoids Atomics.wait on main thread (which hangs when SAB is not forwarded to worker).
|
|
149
149
|
function keytarSync(method, ...args) {
|
|
150
150
|
loadKeytar(); // throws if not installed — before spawning child
|
|
151
|
-
const { spawnSync } = require('child_process');
|
|
152
151
|
// Pass service/account/value via env to avoid shell injection entirely.
|
|
153
152
|
const env = {
|
|
154
153
|
...process.env,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"generated_at": "2026-06-
|
|
3
|
+
"generated_at": "2026-06-30T06:23:20.385Z",
|
|
4
4
|
"release_tag": "runtime-v0.4.0",
|
|
5
5
|
"pg": {
|
|
6
6
|
"major": 16,
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"sha256": "8e39bfc3f06d88e9854946d5d83f7d77d9530f99284d7c1c51872c12f389f7e7",
|
|
21
21
|
"size": 23687666
|
|
22
22
|
},
|
|
23
|
+
"linux-arm64": {
|
|
24
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-arm64-pg16.4-pgvector0.8.2.tar.gz",
|
|
25
|
+
"sha256": "64ff170013b9543ad8843c98cbfe14019e6be6ea3d142ec189b2bea0dd1dc797",
|
|
26
|
+
"size": 22885036
|
|
27
|
+
},
|
|
23
28
|
"linux-x64": {
|
|
24
29
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-linux-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
25
30
|
"sha256": "28ee64125a0125ff36c47560c08ce8f38e49aef034c010c7e4867e8cdd783385",
|
|
@@ -29,12 +34,6 @@
|
|
|
29
34
|
"url": "https://github.com/tribgames/mixdog/releases/download/runtime-v0.4.0/mixdog-runtime-win32-x64-pg16.4-pgvector0.8.2.tar.gz",
|
|
30
35
|
"sha256": "6faed8a49b3303b0adb1c5715b2b19c5cc47df2e738d586d0d0bd432f1ca035d",
|
|
31
36
|
"size": 44036958
|
|
32
|
-
},
|
|
33
|
-
"linux-arm64": {
|
|
34
|
-
"unsupported": true,
|
|
35
|
-
"url": "TBD",
|
|
36
|
-
"sha256": "TBD",
|
|
37
|
-
"size": 0
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
}
|