@gonzih/cc-tg 0.9.35 → 0.9.37
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 +1 -1
- package/dist/bot.js +18 -0
- package/dist/router.d.ts +49 -0
- package/dist/router.js +129 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/bot.js
CHANGED
|
@@ -16,6 +16,7 @@ import { detectUsageLimit } from "./usage-limit.js";
|
|
|
16
16
|
import { getCurrentToken, rotateToken, getTokenIndex, getTokenCount } from "./tokens.js";
|
|
17
17
|
import { writeChatLog } from "./notifier.js";
|
|
18
18
|
import { CronManager } from "./cron.js";
|
|
19
|
+
import { parseRoutingTag, ensureMetaAgent, routeToMetaAgent } from "./router.js";
|
|
19
20
|
const BOT_COMMANDS = [
|
|
20
21
|
{ command: "start", description: "Reset session and start fresh" },
|
|
21
22
|
{ command: "reset", description: "Reset Claude session" },
|
|
@@ -410,6 +411,23 @@ export class CcTgBot {
|
|
|
410
411
|
await this.handleAgents(chatId, threadId);
|
|
411
412
|
return;
|
|
412
413
|
}
|
|
414
|
+
// #tag / #org/repo routing — delegate to meta-agent instead of local Claude session
|
|
415
|
+
if (this.redis) {
|
|
416
|
+
const routing = parseRoutingTag(text);
|
|
417
|
+
if (routing) {
|
|
418
|
+
// Acknowledge routing immediately so user knows the message was delegated
|
|
419
|
+
await this.replyToChat(chatId, `→ #${routing.namespace}`, threadId);
|
|
420
|
+
this.writeChatMessage("user", "telegram", text, chatId);
|
|
421
|
+
try {
|
|
422
|
+
await ensureMetaAgent(routing.namespace, routing.repoUrl, (toolName, args) => this.callCcAgentTool(toolName, args ?? {}), this.redis);
|
|
423
|
+
await routeToMetaAgent(routing.namespace, routing.strippedMessage, this.redis);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
await this.replyToChat(chatId, `Failed to route to #${routing.namespace}: ${err.message}`, threadId);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
413
431
|
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
414
432
|
try {
|
|
415
433
|
const enriched = await enrichPromptWithUrls(text);
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashtag meta-agent routing.
|
|
3
|
+
*
|
|
4
|
+
* Parses #tag or #org/repo tokens from Telegram messages and routes them to
|
|
5
|
+
* the appropriate cc-agent meta-agent instead of the local Claude session.
|
|
6
|
+
*
|
|
7
|
+
* Tag formats:
|
|
8
|
+
* #repo-name → namespace=repo-name, repo=https://github.com/{DEFAULT_GITHUB_ORG}/repo-name
|
|
9
|
+
* #org/repo → namespace=repo, repo=https://github.com/org/repo
|
|
10
|
+
*/
|
|
11
|
+
import { Redis } from "ioredis";
|
|
12
|
+
/** Callback type matching CcTgBot.callCcAgentTool */
|
|
13
|
+
export type CallToolFn = (toolName: string, args?: Record<string, unknown>) => Promise<string | null>;
|
|
14
|
+
export interface RoutingTag {
|
|
15
|
+
namespace: string;
|
|
16
|
+
repoUrl: string;
|
|
17
|
+
/** Original message with the tag token stripped and whitespace collapsed */
|
|
18
|
+
strippedMessage: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parse the first #tag or #org/repo token from a message.
|
|
22
|
+
* Returns null when no routing tag is present.
|
|
23
|
+
*
|
|
24
|
+
* Examples:
|
|
25
|
+
* "#cc-agent fix the bug" → { namespace: "cc-agent", repoUrl: "…/gonzih/cc-agent", … }
|
|
26
|
+
* "#gonzih/of-stack deploy it" → { namespace: "of-stack", repoUrl: "…/gonzih/of-stack", … }
|
|
27
|
+
* "#org/repo do something" → { namespace: "repo", repoUrl: "…/org/repo", … }
|
|
28
|
+
* "please help #of-stack with this" → { namespace: "of-stack", repoUrl: "…/gonzih/of-stack", … }
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseRoutingTag(text: string): RoutingTag | null;
|
|
31
|
+
/**
|
|
32
|
+
* Ensure a meta-agent for the given namespace is running.
|
|
33
|
+
*
|
|
34
|
+
* Steps:
|
|
35
|
+
* 1. Check cca:meta-agent:status:{namespace} in Redis — return early if already running.
|
|
36
|
+
* 2. Verify the GitHub repo exists; create it (public) if not.
|
|
37
|
+
* 3. Call the start_meta_agent MCP tool via callTool.
|
|
38
|
+
* 4. Poll the Redis status key every 1s until running or META_AGENT_TIMEOUT_MS expires.
|
|
39
|
+
*
|
|
40
|
+
* Throws on failure (repo creation error, tool call failure, or timeout).
|
|
41
|
+
*/
|
|
42
|
+
export declare function ensureMetaAgent(namespace: string, repoUrl: string, callTool: CallToolFn, redis: Redis): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Route a message to a running meta-agent via Redis RPUSH.
|
|
45
|
+
* The cc-agent polls cca:meta:{namespace}:input every 3s (up to 3s delivery latency).
|
|
46
|
+
*
|
|
47
|
+
* No-op when strippedMessage is empty (user sent only the tag token).
|
|
48
|
+
*/
|
|
49
|
+
export declare function routeToMetaAgent(namespace: string, strippedMessage: string, redis: Redis): Promise<void>;
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashtag meta-agent routing.
|
|
3
|
+
*
|
|
4
|
+
* Parses #tag or #org/repo tokens from Telegram messages and routes them to
|
|
5
|
+
* the appropriate cc-agent meta-agent instead of the local Claude session.
|
|
6
|
+
*
|
|
7
|
+
* Tag formats:
|
|
8
|
+
* #repo-name → namespace=repo-name, repo=https://github.com/{DEFAULT_GITHUB_ORG}/repo-name
|
|
9
|
+
* #org/repo → namespace=repo, repo=https://github.com/org/repo
|
|
10
|
+
*/
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
/**
|
|
13
|
+
* Parse the first #tag or #org/repo token from a message.
|
|
14
|
+
* Returns null when no routing tag is present.
|
|
15
|
+
*
|
|
16
|
+
* Examples:
|
|
17
|
+
* "#cc-agent fix the bug" → { namespace: "cc-agent", repoUrl: "…/gonzih/cc-agent", … }
|
|
18
|
+
* "#gonzih/of-stack deploy it" → { namespace: "of-stack", repoUrl: "…/gonzih/of-stack", … }
|
|
19
|
+
* "#org/repo do something" → { namespace: "repo", repoUrl: "…/org/repo", … }
|
|
20
|
+
* "please help #of-stack with this" → { namespace: "of-stack", repoUrl: "…/gonzih/of-stack", … }
|
|
21
|
+
*/
|
|
22
|
+
export function parseRoutingTag(text) {
|
|
23
|
+
const defaultOrg = process.env.DEFAULT_GITHUB_ORG ?? "gonzih";
|
|
24
|
+
// Match #word or #org/repo — each segment: starts with alphanumeric, allows ._- inside
|
|
25
|
+
const match = text.match(/#([a-zA-Z0-9][a-zA-Z0-9._-]*)(?:\/([a-zA-Z0-9][a-zA-Z0-9._-]*))?/);
|
|
26
|
+
if (!match)
|
|
27
|
+
return null;
|
|
28
|
+
const fullMatch = match[0]; // e.g. "#gonzih/of-stack"
|
|
29
|
+
const part1 = match[1]; // org-or-repo
|
|
30
|
+
const part2 = match[2]; // repo (only present in #org/repo format)
|
|
31
|
+
let namespace;
|
|
32
|
+
let repoUrl;
|
|
33
|
+
if (part2) {
|
|
34
|
+
// #org/repo format
|
|
35
|
+
namespace = part2;
|
|
36
|
+
repoUrl = `https://github.com/${part1}/${part2}`;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// #repo format — use DEFAULT_GITHUB_ORG
|
|
40
|
+
namespace = part1;
|
|
41
|
+
repoUrl = `https://github.com/${defaultOrg}/${part1}`;
|
|
42
|
+
}
|
|
43
|
+
// Strip the matched tag token and collapse whitespace
|
|
44
|
+
const strippedMessage = text.replace(fullMatch, "").replace(/\s+/g, " ").trim();
|
|
45
|
+
return { namespace, repoUrl, strippedMessage };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Ensure a meta-agent for the given namespace is running.
|
|
49
|
+
*
|
|
50
|
+
* Steps:
|
|
51
|
+
* 1. Check cca:meta-agent:status:{namespace} in Redis — return early if already running.
|
|
52
|
+
* 2. Verify the GitHub repo exists; create it (public) if not.
|
|
53
|
+
* 3. Call the start_meta_agent MCP tool via callTool.
|
|
54
|
+
* 4. Poll the Redis status key every 1s until running or META_AGENT_TIMEOUT_MS expires.
|
|
55
|
+
*
|
|
56
|
+
* Throws on failure (repo creation error, tool call failure, or timeout).
|
|
57
|
+
*/
|
|
58
|
+
export async function ensureMetaAgent(namespace, repoUrl, callTool, redis) {
|
|
59
|
+
const timeoutMs = parseInt(process.env.META_AGENT_TIMEOUT_MS ?? "10000", 10);
|
|
60
|
+
const statusKey = `cca:meta-agent:status:${namespace}`;
|
|
61
|
+
// Fast path: already running
|
|
62
|
+
const statusRaw = await redis.get(statusKey);
|
|
63
|
+
if (statusRaw) {
|
|
64
|
+
try {
|
|
65
|
+
const status = JSON.parse(statusRaw);
|
|
66
|
+
if (status.status === "running")
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Corrupt status value — fall through and restart
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Derive "org/repo" from the full URL for gh CLI calls
|
|
74
|
+
const orgRepo = repoUrl.replace(/^https:\/\/github\.com\//, "");
|
|
75
|
+
// Verify / create the GitHub repo
|
|
76
|
+
try {
|
|
77
|
+
execSync(`gh repo view ${orgRepo}`, { stdio: "ignore" });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Repo not found — create it
|
|
81
|
+
try {
|
|
82
|
+
execSync(`gh repo create ${orgRepo} --public --description "Meta-agent workspace for ${namespace}"`, { stdio: "pipe" });
|
|
83
|
+
console.log(`[router] created repo ${orgRepo} for namespace=${namespace}`);
|
|
84
|
+
}
|
|
85
|
+
catch (createErr) {
|
|
86
|
+
throw new Error(`Failed to create repo ${orgRepo}: ${createErr.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Start the meta-agent via MCP
|
|
90
|
+
const result = await callTool("start_meta_agent", { namespace, repo_url: repoUrl });
|
|
91
|
+
if (result === null) {
|
|
92
|
+
throw new Error(`start_meta_agent returned null — tool may not be available in cc-agent`);
|
|
93
|
+
}
|
|
94
|
+
// Poll until the meta-agent reports "running"
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
98
|
+
const raw = await redis.get(statusKey);
|
|
99
|
+
if (raw) {
|
|
100
|
+
try {
|
|
101
|
+
const s = JSON.parse(raw);
|
|
102
|
+
if (s.status === "running")
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// ignore parse errors, keep polling
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Meta-agent for ${namespace} did not become ready within ${timeoutMs}ms`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Route a message to a running meta-agent via Redis RPUSH.
|
|
114
|
+
* The cc-agent polls cca:meta:{namespace}:input every 3s (up to 3s delivery latency).
|
|
115
|
+
*
|
|
116
|
+
* No-op when strippedMessage is empty (user sent only the tag token).
|
|
117
|
+
*/
|
|
118
|
+
export async function routeToMetaAgent(namespace, strippedMessage, redis) {
|
|
119
|
+
if (!strippedMessage)
|
|
120
|
+
return;
|
|
121
|
+
const entry = JSON.stringify({
|
|
122
|
+
id: crypto.randomUUID(),
|
|
123
|
+
content: strippedMessage,
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
});
|
|
126
|
+
// FIFO — cc-agent reads via LPOP
|
|
127
|
+
await redis.rpush(`cca:meta:${namespace}:input`, entry);
|
|
128
|
+
console.log(`[router] routed message to meta-agent namespace=${namespace}`);
|
|
129
|
+
}
|