@worca/app 0.0.1
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/README.md +403 -0
- package/agents/clarify.meta.json +19 -0
- package/agents/decomposer.meta.json +21 -0
- package/agents/implementer.meta.json +20 -0
- package/agents/manualTestsChecklist.meta.json +18 -0
- package/agents/manualWebUiTesting.meta.json +18 -0
- package/agents/planReviewer.meta.json +19 -0
- package/agents/planner.meta.json +20 -0
- package/agents/refiner.meta.json +19 -0
- package/agents/reviewer.meta.json +19 -0
- package/agents/worca-cc-clarify.md +67 -0
- package/agents/worca-cc-code-reviewer.md +66 -0
- package/agents/worca-cc-decomposer.md +84 -0
- package/agents/worca-cc-implementer.md +69 -0
- package/agents/worca-cc-manual-tests-checklist.md +63 -0
- package/agents/worca-cc-manual-web-ui-testing.md +64 -0
- package/agents/worca-cc-plan-refiner.md +69 -0
- package/agents/worca-cc-plan-reviewer.md +70 -0
- package/agents/worca-cc-planner.md +70 -0
- package/agents/worca-cc-workspace-reviewer.md +56 -0
- package/agents/worca-cc-workspace-scanner.md +55 -0
- package/agents/workspaceReviewer.meta.json +20 -0
- package/agents/workspaceScanner.meta.json +18 -0
- package/package.json +61 -0
- package/scripts/install.mjs +209 -0
- package/skills/worca/SKILL.md +66 -0
- package/src/cli/worca-cc.mjs +1520 -0
- package/src/core/agent-gen.mjs +206 -0
- package/src/core/agent-registry.mjs +417 -0
- package/src/core/agent-store.mjs +143 -0
- package/src/core/artifacts.mjs +2019 -0
- package/src/core/channels.mjs +302 -0
- package/src/core/chat/allowlist.mjs +27 -0
- package/src/core/chat/channel-host.mjs +562 -0
- package/src/core/chat/channel-protocol.mjs +117 -0
- package/src/core/chat/channel-worker-child.mjs +211 -0
- package/src/core/chat/chat-context.mjs +66 -0
- package/src/core/chat/command-router.mjs +343 -0
- package/src/core/chat/notifier.mjs +120 -0
- package/src/core/chat/parser.mjs +30 -0
- package/src/core/chat/rate-limiter.mjs +133 -0
- package/src/core/chat/redact.mjs +27 -0
- package/src/core/chat/renderers.mjs +136 -0
- package/src/core/claude-runner.mjs +1356 -0
- package/src/core/config.mjs +882 -0
- package/src/core/cost-budget.mjs +103 -0
- package/src/core/db.mjs +864 -0
- package/src/core/fanout.mjs +48 -0
- package/src/core/folder-dialog.mjs +138 -0
- package/src/core/fs-browse.mjs +49 -0
- package/src/core/git-info.mjs +200 -0
- package/src/core/guardrail-store.mjs +204 -0
- package/src/core/guardrails.mjs +302 -0
- package/src/core/marketplaces.mjs +267 -0
- package/src/core/migrate-fs-to-db.mjs +612 -0
- package/src/core/model-env.mjs +74 -0
- package/src/core/orchestrator.mjs +4279 -0
- package/src/core/overview-agent.mjs +124 -0
- package/src/core/phases.mjs +1279 -0
- package/src/core/pipeline-delete.mjs +428 -0
- package/src/core/plugin-api.mjs +13 -0
- package/src/core/plugin-config.mjs +100 -0
- package/src/core/plugin-inventory.mjs +50 -0
- package/src/core/plugin-manifest.mjs +447 -0
- package/src/core/plugin-models.mjs +130 -0
- package/src/core/plugin-repo.mjs +303 -0
- package/src/core/plugin-shim-child.mjs +76 -0
- package/src/core/plugin-shim.mjs +197 -0
- package/src/core/plugin-store.mjs +485 -0
- package/src/core/plugin-workflows.mjs +179 -0
- package/src/core/plugins-lock.mjs +49 -0
- package/src/core/preflight-node.mjs +122 -0
- package/src/core/preflight.mjs +341 -0
- package/src/core/projects.mjs +157 -0
- package/src/core/protocol.mjs +257 -0
- package/src/core/recoverable-error.mjs +51 -0
- package/src/core/results.mjs +188 -0
- package/src/core/run-context.mjs +1375 -0
- package/src/core/run-log.mjs +64 -0
- package/src/core/run-manifest.mjs +317 -0
- package/src/core/runners.mjs +167 -0
- package/src/core/settings.mjs +682 -0
- package/src/core/skills.mjs +210 -0
- package/src/core/sources.mjs +232 -0
- package/src/core/stats.mjs +182 -0
- package/src/core/store.mjs +67 -0
- package/src/core/title.mjs +64 -0
- package/src/core/workflow-validator.mjs +185 -0
- package/src/core/workflows.mjs +568 -0
- package/src/core/workspace-scan.mjs +420 -0
- package/src/core/workspaces.mjs +353 -0
- package/src/core/worktree.mjs +708 -0
- package/src/feature.mjs +9 -0
- package/ui/public/app.js +10647 -0
- package/ui/public/assets/worca-favicon.png +0 -0
- package/ui/public/assets/worca-logo.png +0 -0
- package/ui/public/chat-settings-view.mjs +89 -0
- package/ui/public/composer-core.mjs +211 -0
- package/ui/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-400-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-500-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-600-normal.woff2 +0 -0
- package/ui/public/fonts/poppins-latin-700-normal.woff2 +0 -0
- package/ui/public/guardrails-view.mjs +244 -0
- package/ui/public/index.html +1145 -0
- package/ui/public/log-filter.mjs +81 -0
- package/ui/public/log-line.mjs +86 -0
- package/ui/public/models-view.mjs +433 -0
- package/ui/public/plugins-view.mjs +430 -0
- package/ui/public/results-view.mjs +121 -0
- package/ui/public/source-pane.mjs +156 -0
- package/ui/public/stats-view.mjs +523 -0
- package/ui/public/style.css +1557 -0
- package/ui/server.mjs +3573 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/core/chat/notifier.mjs
|
|
2
|
+
// Outbound run-event notifications (chat-connectivity-design.md §4.5).
|
|
3
|
+
// attach(orch, {runId, entry}) subscribes to the orchestrator's question/done/
|
|
4
|
+
// error events — riding the exception-isolated _emit, so nothing here can
|
|
5
|
+
// break a run — renders them to NormalizedMessages, and fans out to every
|
|
6
|
+
// enabled+connected outbound channel's notifyChatIds through a per-channel
|
|
7
|
+
// host-side rate limiter. Workers surface platform 429s; the limiter ladder
|
|
8
|
+
// retries them.
|
|
9
|
+
//
|
|
10
|
+
// Core module (no Express, no orchestrator import): ui/server.mjs wires it
|
|
11
|
+
// today; a CLI runner can attach the same notifier later.
|
|
12
|
+
|
|
13
|
+
import { readPluginConfig } from '../plugin-config.mjs';
|
|
14
|
+
import { parseIdList } from './allowlist.mjs';
|
|
15
|
+
import { createRateLimiter } from './rate-limiter.mjs';
|
|
16
|
+
import { renderDone, renderError, renderQuestion } from './renderers.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{channelHost: object, getPrefs: () => {notify:object, channels:object},
|
|
20
|
+
* chatContext: {isMuted(k):boolean, incrementMuted(k):void},
|
|
21
|
+
* logger?: (level:string, msg:string) => void,
|
|
22
|
+
* ratePerMin?: number}} deps
|
|
23
|
+
*/
|
|
24
|
+
export function createNotifier({ channelHost, getPrefs, chatContext, logger = () => {}, ratePerMin = 20 }) {
|
|
25
|
+
const limiters = new Map(); // "plugin/channelId" -> rate limiter
|
|
26
|
+
|
|
27
|
+
const limiterFor = (key) => {
|
|
28
|
+
let l = limiters.get(key);
|
|
29
|
+
if (!l) { l = createRateLimiter({ ratePerMin, logger }); limiters.set(key, l); }
|
|
30
|
+
return l;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function deliver(message) {
|
|
34
|
+
let prefs;
|
|
35
|
+
try { prefs = getPrefs(); } catch { prefs = { notify: {}, channels: {} }; }
|
|
36
|
+
for (const entry of channelHost.list()) {
|
|
37
|
+
const key = `${entry.plugin}/${entry.channelId}`;
|
|
38
|
+
if (!entry.capabilities?.outbound) continue;
|
|
39
|
+
if (prefs.channels?.[key]?.enabled === false) continue;
|
|
40
|
+
let chatIds = [];
|
|
41
|
+
try { chatIds = parseIdList(readPluginConfig(entry.plugin, entry.configSchema).notifyChatIds); }
|
|
42
|
+
catch { /* unreadable config: nothing to send to */ }
|
|
43
|
+
for (const chatId of chatIds) {
|
|
44
|
+
const chatKey = `${entry.platform}:${chatId}`;
|
|
45
|
+
if (chatContext.isMuted(chatKey)) {
|
|
46
|
+
try { chatContext.incrementMuted(chatKey); } catch { /* best effort */ }
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Sequenced per channel by the limiter's FIFO; failures are logged,
|
|
50
|
+
// never thrown (the caller is an event listener on the run).
|
|
51
|
+
limiterFor(key).send(message, (m) => channelHost.sendMessage({
|
|
52
|
+
plugin: entry.plugin, channelId: entry.channelId, chatId, message: m,
|
|
53
|
+
})).then((sent) => {
|
|
54
|
+
if (!sent) logger('warn', `chat notify dropped after rate-limit retries (${key} -> ${chatId})`);
|
|
55
|
+
}).catch((err) => {
|
|
56
|
+
logger('error', `chat notify failed (${key} -> ${chatId}): ${err?.message || err}`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
/**
|
|
64
|
+
* Subscribe one live run. `entry` is the server's runs-Map record — title
|
|
65
|
+
* is read at SEND time (titles generate mid-run).
|
|
66
|
+
*/
|
|
67
|
+
attach(orch, { runId, entry } = {}) {
|
|
68
|
+
const meta = () => ({
|
|
69
|
+
runId,
|
|
70
|
+
title: entry?.title || orch?.state?.title || '',
|
|
71
|
+
totalCostUsd: orch?.state?.totalCostUsd,
|
|
72
|
+
totalActiveMs: orch?.state?.totalActiveMs,
|
|
73
|
+
});
|
|
74
|
+
const guard = (fn) => (payload) => {
|
|
75
|
+
try { fn(payload); } catch (err) { logger('error', `chat notifier: ${err?.message || err}`); }
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
orch.on('question', guard((payload) => {
|
|
79
|
+
if (getPrefsSafe().notify.question === false) return;
|
|
80
|
+
deliver(renderQuestion(meta(), payload || {}));
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
orch.on('error', guard((payload) => {
|
|
84
|
+
if (getPrefsSafe().notify.error === false) return;
|
|
85
|
+
deliver(renderError(meta(), payload || {}));
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
orch.on('done', guard((payload) => {
|
|
89
|
+
const status = payload?.status || 'done';
|
|
90
|
+
if (status === 'error') return; // the richer 'error' event already went out
|
|
91
|
+
const prefs = getPrefsSafe().notify;
|
|
92
|
+
if (status === 'paused' ? prefs.paused === false : prefs.done === false) return;
|
|
93
|
+
deliver(renderDone(meta(), payload || {}));
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
function getPrefsSafe() {
|
|
97
|
+
try { return getPrefs(); } catch { return { notify: {} }; }
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/** The Test button / smoke path: send one message to one channel's
|
|
102
|
+
* notifyChatIds directly (no prefs gating — an explicit user action). */
|
|
103
|
+
async sendTest(plugin, channelId, message) {
|
|
104
|
+
const entry = channelHost.list().find((e) => e.plugin === plugin && e.channelId === channelId);
|
|
105
|
+
if (!entry) throw new Error(`no such chat channel "${plugin}/${channelId}"`);
|
|
106
|
+
const chatIds = parseIdList(readPluginConfig(entry.plugin, entry.configSchema).notifyChatIds);
|
|
107
|
+
if (!chatIds.length) throw new Error('notifyChatIds is not configured for this channel');
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const chatId of chatIds) {
|
|
110
|
+
try {
|
|
111
|
+
await channelHost.sendMessage({ plugin, channelId, chatId, message });
|
|
112
|
+
results.push({ chatId, ok: true });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
results.push({ chatId, ok: false, error: { kind: err?.kind || 'plugin', message: err?.message || String(err) } });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { ok: results.every((r) => r.ok), results };
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/core/chat/parser.mjs
|
|
2
|
+
// Chat command parser, ported verbatim from the pre-1.0
|
|
3
|
+
// worca-ui/server/integrations/commands/parser.js. Strips bot @mentions
|
|
4
|
+
// (anywhere in the text) and handles Telegram's /command@botname suffix.
|
|
5
|
+
|
|
6
|
+
const MENTION_RE = /^@\S+$/i;
|
|
7
|
+
|
|
8
|
+
// Allow `-` inside command names (namespaced commands); hyphens must not lead
|
|
9
|
+
// or trail. Underscore-only commands still match.
|
|
10
|
+
const COMMAND_RE = /^\/([a-z_][a-z0-9_-]*)(?:@\S+)?$/i;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {{ command: string, args: string[] } | null} null for non-commands
|
|
15
|
+
*/
|
|
16
|
+
export function parseCommand(text) {
|
|
17
|
+
if (!text || !text.trim()) return null;
|
|
18
|
+
|
|
19
|
+
const tokens = text.trim().split(/\s+/);
|
|
20
|
+
const filtered = tokens.filter((t) => !MENTION_RE.test(t));
|
|
21
|
+
if (filtered.length === 0) return null;
|
|
22
|
+
|
|
23
|
+
const match = COMMAND_RE.exec(filtered[0]);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
command: match[1].toLowerCase(),
|
|
28
|
+
args: filtered.slice(1),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/core/chat/rate-limiter.mjs
|
|
2
|
+
// Outbound delivery rate limiter, ported from pre-1.0
|
|
3
|
+
// integrations/rate_limiter.js: token bucket + FIFO worker + 429 retry ladder
|
|
4
|
+
// + ring buffer of recently sent messages. Policy limiting lives HOST-side
|
|
5
|
+
// (design §4.5) — workers only surface platform 429s; this ladder retries them.
|
|
6
|
+
// The only port deltas: injectable `now`, retry keyed on err.kind==='rate-limit'
|
|
7
|
+
// (PluginOpError vocabulary) in addition to err.status===429, and logger
|
|
8
|
+
// injection instead of console.warn.
|
|
9
|
+
|
|
10
|
+
const BACKOFF_DELAYS = [1000, 5000, 30000];
|
|
11
|
+
|
|
12
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
|
|
14
|
+
export class RingBuffer {
|
|
15
|
+
constructor(size = 100) {
|
|
16
|
+
this._size = size;
|
|
17
|
+
this._buf = new Array(size);
|
|
18
|
+
this._head = 0;
|
|
19
|
+
this._count = 0;
|
|
20
|
+
this.dropped = 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
push(item) {
|
|
24
|
+
if (this._count < this._size) this._count++;
|
|
25
|
+
else this.dropped++;
|
|
26
|
+
this._buf[this._head] = item;
|
|
27
|
+
this._head = (this._head + 1) % this._size;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
toArray() {
|
|
31
|
+
if (this._count < this._size) return this._buf.slice(0, this._count);
|
|
32
|
+
const result = [];
|
|
33
|
+
for (let i = 0; i < this._size; i++) result.push(this._buf[(this._head + i) % this._size]);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class TokenBucket {
|
|
39
|
+
constructor(ratePerMin, { now = Date.now.bind(Date) } = {}) {
|
|
40
|
+
this._rate = ratePerMin;
|
|
41
|
+
this._tokens = ratePerMin;
|
|
42
|
+
this._lastRefill = now();
|
|
43
|
+
this._now = now;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
tryConsume() {
|
|
47
|
+
const t = this._now();
|
|
48
|
+
const mins = (t - this._lastRefill) / 60000;
|
|
49
|
+
this._tokens = Math.min(this._rate, this._tokens + mins * this._rate);
|
|
50
|
+
this._lastRefill = t;
|
|
51
|
+
if (this._tokens >= 1) {
|
|
52
|
+
this._tokens -= 1;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isRateLimited = (err) => err?.status === 429 || err?.kind === 'rate-limit';
|
|
60
|
+
|
|
61
|
+
export function createRateLimiter({
|
|
62
|
+
ratePerMin = 20,
|
|
63
|
+
ringSize = 100,
|
|
64
|
+
_sleep = defaultSleep,
|
|
65
|
+
logger = () => {},
|
|
66
|
+
} = {}) {
|
|
67
|
+
const ring = new RingBuffer(ringSize);
|
|
68
|
+
const bucket = new TokenBucket(ratePerMin);
|
|
69
|
+
let droppedMessages = 0;
|
|
70
|
+
|
|
71
|
+
async function trySend(msg, sendFn) {
|
|
72
|
+
for (let attempt = 0; attempt <= BACKOFF_DELAYS.length; attempt++) {
|
|
73
|
+
if (attempt > 0) await _sleep(BACKOFF_DELAYS[attempt - 1]);
|
|
74
|
+
try {
|
|
75
|
+
await sendFn(msg);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (isRateLimited(err)) {
|
|
79
|
+
if (attempt < BACKOFF_DELAYS.length) continue;
|
|
80
|
+
logger('warn', 'chat rate limiter: 429 exhausted after all retries — dropping message');
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pendingQueue = [];
|
|
90
|
+
let workerRunning = false;
|
|
91
|
+
|
|
92
|
+
async function runWorker() {
|
|
93
|
+
if (workerRunning) return;
|
|
94
|
+
workerRunning = true;
|
|
95
|
+
while (pendingQueue.length > 0) {
|
|
96
|
+
if (!bucket.tryConsume()) {
|
|
97
|
+
await _sleep(Math.ceil(60000 / ratePerMin));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const { msg, sendFn, resolve, reject } = pendingQueue.shift();
|
|
101
|
+
try {
|
|
102
|
+
const sent = await trySend(msg, sendFn);
|
|
103
|
+
if (sent) {
|
|
104
|
+
const prevDropped = ring.dropped;
|
|
105
|
+
ring.push(msg);
|
|
106
|
+
droppedMessages += ring.dropped - prevDropped;
|
|
107
|
+
} else {
|
|
108
|
+
droppedMessages++;
|
|
109
|
+
}
|
|
110
|
+
resolve(sent);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
reject(err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
workerRunning = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
/** Queue one message; resolves true (sent) / false (dropped after 429s). */
|
|
120
|
+
send(msg, sendFn) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
pendingQueue.push({ msg, sendFn, resolve, reject });
|
|
123
|
+
runWorker();
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
getStats() {
|
|
127
|
+
return { dropped_messages: droppedMessages };
|
|
128
|
+
},
|
|
129
|
+
getRing() {
|
|
130
|
+
return ring.toArray();
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/core/chat/redact.mjs
|
|
2
|
+
// Secret scrubbing for every worker-originated string (log frames, status
|
|
3
|
+
// details, send errors) BEFORE it reaches the host logger or the UI
|
|
4
|
+
// (chat-connectivity-design.md §4.4). Ported from the pre-1.0
|
|
5
|
+
// integrations/index.js redactSecrets, extended with generic Bearer/token
|
|
6
|
+
// scrubs for the platforms added in 1.0 (Teams AAD tokens, app secrets).
|
|
7
|
+
// Workers legitimately hold bot tokens; this is the belt-and-braces layer for
|
|
8
|
+
// the strings they emit, not a substitute for not logging secrets.
|
|
9
|
+
|
|
10
|
+
export function redactSecrets(input) {
|
|
11
|
+
return String(input)
|
|
12
|
+
// Telegram bot tokens (123456:ABC-...) — bare or inside api.telegram.org URLs
|
|
13
|
+
.replace(/bot\d+:[A-Za-z0-9_-]+/g, 'bot<redacted>')
|
|
14
|
+
.replace(/\b\d{6,}:[A-Za-z0-9_-]{30,}/g, '<redacted>')
|
|
15
|
+
// Slack incoming-webhook URLs + xox* OAuth tokens + xapp app tokens
|
|
16
|
+
.replace(/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g, 'hooks.slack.com/services/<redacted>')
|
|
17
|
+
.replace(/xox[abeprs]-[A-Za-z0-9-]+/g, 'xox<redacted>')
|
|
18
|
+
.replace(/xapp-[A-Za-z0-9-]+/g, 'xapp-<redacted>')
|
|
19
|
+
// Discord webhook URLs + bot tokens (base64ish triplet)
|
|
20
|
+
.replace(/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/g, 'discord.com/api/webhooks/<redacted>')
|
|
21
|
+
.replace(/\b[\w-]{23,28}\.[\w-]{6,7}\.[\w-]{27,}\b/g, '<redacted>')
|
|
22
|
+
// Authorization header values and JWTs (Teams Bot Framework / AAD)
|
|
23
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer <redacted>')
|
|
24
|
+
.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, '<redacted-jwt>')
|
|
25
|
+
// client_secret=... in URL-encoded bodies (AAD token endpoint)
|
|
26
|
+
.replace(/([?&]|\b)(client_secret|access_token|token)=[^&\s]+/gi, '$1$2=<redacted>');
|
|
27
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// src/core/chat/renderers.mjs
|
|
2
|
+
// 1.0 orchestrator events -> NormalizedMessage (chat-connectivity-design.md
|
|
3
|
+
// §4.5). Style/helpers ported from the pre-1.0 integrations/renderers.js
|
|
4
|
+
// (markdown segments so each platform worker converts to its native format);
|
|
5
|
+
// the event vocabulary is 1.0's: done {status: done|stopped|paused, reason},
|
|
6
|
+
// error {message}, question {id, kind, questions, issues, recovery, agent}.
|
|
7
|
+
//
|
|
8
|
+
// The question renderer is the load-bearing one: it enumerates options with
|
|
9
|
+
// ordinals and embeds the exact reply commands (/approve, /retry, /answer n…)
|
|
10
|
+
// using the run-id wildcard-suffix convention the command router resolves.
|
|
11
|
+
|
|
12
|
+
const md = (value) => ({ kind: 'markdown', value });
|
|
13
|
+
|
|
14
|
+
export function fmtMs(ms) {
|
|
15
|
+
if (ms == null || !Number.isFinite(ms)) return null;
|
|
16
|
+
const totalSec = Math.floor(ms / 1000);
|
|
17
|
+
const m = Math.floor(totalSec / 60);
|
|
18
|
+
const s = totalSec % 60;
|
|
19
|
+
return m > 0 ? `${m}m${String(s).padStart(2, '0')}s` : `${s}s`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fmtUsd(usd) {
|
|
23
|
+
if (usd == null || !Number.isFinite(Number(usd))) return null;
|
|
24
|
+
return `$${Number(usd).toFixed(2)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mdMsg(text, severity) {
|
|
28
|
+
return { title: null, body: [md(text)], severity };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Short wildcard reference the command router resolves: last 4 id chars. */
|
|
32
|
+
export function runRef(runId) {
|
|
33
|
+
const id = String(runId || '');
|
|
34
|
+
return id.length > 4 ? `*${id.slice(-4)}` : id || 'run';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function head(icon, meta) {
|
|
38
|
+
const parts = [`${icon} **Run:** \`${runRef(meta.runId)}\``];
|
|
39
|
+
const title = String(meta.title || '').trim();
|
|
40
|
+
if (title) parts.push(` **Title:** ${title.length > 60 ? `${title.slice(0, 60)}…` : title}`);
|
|
41
|
+
return parts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PAUSE_REASONS = {
|
|
45
|
+
cost_pipeline: 'pipeline cost limit reached',
|
|
46
|
+
cost_total: 'total cost limit reached',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* done event: status done|stopped|paused (+reason for limit pauses).
|
|
51
|
+
* meta may carry {title, totalCostUsd, totalActiveMs} for the summary line.
|
|
52
|
+
*/
|
|
53
|
+
export function renderDone(meta, payload = {}) {
|
|
54
|
+
const status = payload.status || 'done';
|
|
55
|
+
if (status === 'paused') {
|
|
56
|
+
const reason = payload.reason ? (PAUSE_REASONS[payload.reason] || payload.reason) : null;
|
|
57
|
+
const parts = head('⏸', meta);
|
|
58
|
+
parts.push(` **Status:** paused${reason ? ` — ${reason}` : ''}`);
|
|
59
|
+
parts.push(` Resume from the worca-cc UI, or reply: /resume ${runRef(meta.runId)}`);
|
|
60
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
61
|
+
}
|
|
62
|
+
if (status === 'stopped') {
|
|
63
|
+
const parts = head('⏹', meta);
|
|
64
|
+
parts.push(' **Status:** stopped');
|
|
65
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
66
|
+
}
|
|
67
|
+
const parts = head('✅', meta);
|
|
68
|
+
parts.push(' **Status:** completed');
|
|
69
|
+
const dur = fmtMs(meta.totalActiveMs);
|
|
70
|
+
if (dur) parts.push(` **Duration:** ${dur}`);
|
|
71
|
+
const cost = fmtUsd(meta.totalCostUsd);
|
|
72
|
+
if (cost) parts.push(` **Cost:** ${cost}`);
|
|
73
|
+
return mdMsg(parts.join('\n'), 'success');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** error event: {message}. A separate done{status:'error'} follows; the
|
|
77
|
+
* notifier sends only this one (richer) for the failure. */
|
|
78
|
+
export function renderError(meta, payload = {}) {
|
|
79
|
+
const parts = head('\u{1F534}', meta);
|
|
80
|
+
parts.push(' **Status:** failed');
|
|
81
|
+
const msg = String(payload.message || 'unknown error');
|
|
82
|
+
parts.push(` **Error:** ${msg.length > 300 ? `${msg.slice(0, 300)}…` : msg}`);
|
|
83
|
+
return mdMsg(parts.join('\n'), 'error');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* question event: {id, kind: clarify|questions|gate|recovery, questions,
|
|
88
|
+
* issues, recovery, agent}. The reply instructions match the command router:
|
|
89
|
+
* gate/recovery -> /approve <ref> | /retry <ref>
|
|
90
|
+
* clarify/questions -> /answer <ref> <n|text> [| …] (one answer per question:
|
|
91
|
+
* an option number, or free text for questions without options)
|
|
92
|
+
*/
|
|
93
|
+
export function renderQuestion(meta, payload = {}) {
|
|
94
|
+
const ref = runRef(meta.runId);
|
|
95
|
+
const kind = payload.kind || 'questions';
|
|
96
|
+
const parts = head('❓', meta);
|
|
97
|
+
|
|
98
|
+
if (kind === 'gate' || kind === 'recovery') {
|
|
99
|
+
parts.push(` **Status:** waiting for ${kind === 'gate' ? 'approval' : 'a recovery decision'}${payload.agent ? ` (${payload.agent})` : ''}`);
|
|
100
|
+
const issues = Array.isArray(payload.issues) ? payload.issues : [];
|
|
101
|
+
for (const i of issues.slice(0, 5)) {
|
|
102
|
+
const sev = i?.severity ? `[${i.severity}] ` : '';
|
|
103
|
+
const text = String(i?.summary || i?.message || i?.title || '').trim();
|
|
104
|
+
if (text) parts.push(` • ${sev}${text.length > 120 ? `${text.slice(0, 120)}…` : text}`);
|
|
105
|
+
}
|
|
106
|
+
if (issues.length > 5) parts.push(` • …and ${issues.length - 5} more`);
|
|
107
|
+
if (kind === 'recovery' && payload.recovery?.message) {
|
|
108
|
+
parts.push(` **Cause:** ${String(payload.recovery.message).slice(0, 200)}`);
|
|
109
|
+
}
|
|
110
|
+
parts.push(kind === 'gate'
|
|
111
|
+
? ` Reply: /approve ${ref} to continue · /retry ${ref} for another cycle`
|
|
112
|
+
: ` Reply: /approve ${ref} to retry · /abort ${ref} to abort`);
|
|
113
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
parts.push(` **Status:** has questions${payload.agent ? ` from ${payload.agent}` : ''}`);
|
|
117
|
+
const questions = Array.isArray(payload.questions) ? payload.questions : [];
|
|
118
|
+
questions.forEach((q, qi) => {
|
|
119
|
+
parts.push(` **Q${qi + 1}.** ${String(q?.question || q?.id || '').trim()}`);
|
|
120
|
+
(Array.isArray(q?.options) ? q.options : []).forEach((opt, oi) => {
|
|
121
|
+
parts.push(` ${oi + 1}. ${String(opt).trim()}`);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const example = questions.map((q) => ((Array.isArray(q.options) && q.options.length) ? '1' : '<your answer>')).join(' | ') || '1';
|
|
125
|
+
parts.push(` Reply: /answer ${ref} ${example}${questions.length > 1 ? ' (one answer per question, in order, separated by |)' : ''}`);
|
|
126
|
+
return mdMsg(parts.join('\n'), 'warning');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Canned message for the "Test" button / `worca plugin channel` smoke. */
|
|
130
|
+
export function renderTest() {
|
|
131
|
+
return {
|
|
132
|
+
title: 'worca-cc test message',
|
|
133
|
+
body: [md('✅ Chat channel connectivity works. Notifications will appear here.')],
|
|
134
|
+
severity: 'info',
|
|
135
|
+
};
|
|
136
|
+
}
|