@fonz/tgcc 0.6.19 → 0.7.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/README.md +77 -0
- package/dist/plugin/index.d.ts +41 -0
- package/dist/plugin/index.js +161 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/openclaw.plugin.json +55 -0
- package/dist/plugin/package.json +6 -0
- package/dist/plugin/skills/tgcc-agents/SKILL.md +161 -0
- package/dist/plugin/src/client.d.ts +167 -0
- package/dist/plugin/src/client.js +523 -0
- package/dist/plugin/src/client.js.map +1 -0
- package/dist/plugin/src/events.d.ts +44 -0
- package/dist/plugin/src/events.js +226 -0
- package/dist/plugin/src/events.js.map +1 -0
- package/dist/plugin/src/permissions.d.ts +21 -0
- package/dist/plugin/src/permissions.js +78 -0
- package/dist/plugin/src/permissions.js.map +1 -0
- package/dist/plugin/src/tools/tgcc-kill.d.ts +6 -0
- package/dist/plugin/src/tools/tgcc-kill.js +52 -0
- package/dist/plugin/src/tools/tgcc-kill.js.map +1 -0
- package/dist/plugin/src/tools/tgcc-send.d.ts +9 -0
- package/dist/plugin/src/tools/tgcc-send.js +61 -0
- package/dist/plugin/src/tools/tgcc-send.js.map +1 -0
- package/dist/plugin/src/tools/tgcc-spawn.d.ts +9 -0
- package/dist/plugin/src/tools/tgcc-spawn.js +79 -0
- package/dist/plugin/src/tools/tgcc-spawn.js.map +1 -0
- package/dist/plugin/src/tools/tgcc-status.d.ts +9 -0
- package/dist/plugin/src/tools/tgcc-status.js +74 -0
- package/dist/plugin/src/tools/tgcc-status.js.map +1 -0
- package/dist/streaming.js +5 -2
- package/dist/streaming.js.map +1 -1
- package/package.json +10 -3
- package/plugin/openclaw.plugin.json +55 -0
- package/plugin/skills/tgcc-agents/SKILL.md +161 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TgccSupervisorClient — Unix socket client for the TGCC Supervisor Protocol.
|
|
3
|
+
*
|
|
4
|
+
* Connects to TGCC's ctl socket, registers as a supervisor, and provides
|
|
5
|
+
* async methods for interacting with TGCC-managed agents and CC processes.
|
|
6
|
+
*
|
|
7
|
+
* Adapted from the OpenClaw fork (src/agents/tgcc-supervisor/client.ts)
|
|
8
|
+
* to use only the plugin API — no OpenClaw internal imports.
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from "node:events";
|
|
11
|
+
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
12
|
+
export interface SupervisorCommand {
|
|
13
|
+
type: "command";
|
|
14
|
+
requestId: string;
|
|
15
|
+
action: string;
|
|
16
|
+
params?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface SupervisorResponse {
|
|
19
|
+
type: "response";
|
|
20
|
+
requestId: string;
|
|
21
|
+
result?: unknown;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SupervisorEvent {
|
|
25
|
+
type: "event";
|
|
26
|
+
event: string;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface TgccAgentStatus {
|
|
30
|
+
id: string;
|
|
31
|
+
type: "persistent" | "ephemeral";
|
|
32
|
+
state: "idle" | "active";
|
|
33
|
+
sessionId: string | null;
|
|
34
|
+
repo: string;
|
|
35
|
+
supervisorSubscribed: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface TgccStatusResult {
|
|
38
|
+
agents: TgccAgentStatus[];
|
|
39
|
+
sessions?: Array<{
|
|
40
|
+
id: string;
|
|
41
|
+
agentId: string;
|
|
42
|
+
messageCount?: number;
|
|
43
|
+
totalCostUsd?: number;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export interface TgccResultEvent {
|
|
47
|
+
agentId: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
text: string;
|
|
50
|
+
cost_usd?: number;
|
|
51
|
+
duration_ms?: number;
|
|
52
|
+
is_error?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface TgccProcessExitEvent {
|
|
55
|
+
agentId: string;
|
|
56
|
+
sessionId: string;
|
|
57
|
+
exitCode: number | null;
|
|
58
|
+
}
|
|
59
|
+
export interface TgccSessionTakeoverEvent {
|
|
60
|
+
agentId: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
exitCode: number | null;
|
|
63
|
+
}
|
|
64
|
+
export interface TgccApiErrorEvent {
|
|
65
|
+
agentId: string;
|
|
66
|
+
sessionId: string;
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
export interface TgccPermissionRequestEvent {
|
|
70
|
+
agentId: string;
|
|
71
|
+
toolName: string;
|
|
72
|
+
requestId: string;
|
|
73
|
+
description: string;
|
|
74
|
+
}
|
|
75
|
+
export interface TgccSupervisorClientConfig {
|
|
76
|
+
socket: string;
|
|
77
|
+
logger: PluginLogger;
|
|
78
|
+
reconnectInitialMs?: number;
|
|
79
|
+
reconnectMaxMs?: number;
|
|
80
|
+
heartbeatMs?: number;
|
|
81
|
+
}
|
|
82
|
+
export declare class TgccSupervisorClient extends EventEmitter {
|
|
83
|
+
private log;
|
|
84
|
+
private socketPath;
|
|
85
|
+
private socket;
|
|
86
|
+
private connected;
|
|
87
|
+
private destroyed;
|
|
88
|
+
private reconnectDelay;
|
|
89
|
+
private reconnectInitialMs;
|
|
90
|
+
private reconnectMaxMs;
|
|
91
|
+
private heartbeatMs;
|
|
92
|
+
private reconnectTimer;
|
|
93
|
+
private heartbeatTimer;
|
|
94
|
+
private pongTimer;
|
|
95
|
+
private pendingRequests;
|
|
96
|
+
private lineBuffer;
|
|
97
|
+
constructor(config: TgccSupervisorClientConfig);
|
|
98
|
+
start(): void;
|
|
99
|
+
stop(): void;
|
|
100
|
+
isConnected(): boolean;
|
|
101
|
+
sendMessage(agentId: string, text: string, opts?: {
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
subscribe?: boolean;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
sessionId: string;
|
|
106
|
+
state: string;
|
|
107
|
+
subscribed?: boolean;
|
|
108
|
+
}>;
|
|
109
|
+
sendToCC(agentId: string, text: string): Promise<{
|
|
110
|
+
sent: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
getStatus(agentId?: string): Promise<TgccStatusResult>;
|
|
113
|
+
killCC(agentId: string): Promise<unknown>;
|
|
114
|
+
subscribe(agentId: string, sessionId?: string): Promise<unknown>;
|
|
115
|
+
unsubscribe(agentId: string): Promise<unknown>;
|
|
116
|
+
respondToPermission(agentId: string, permissionRequestId: string, decision: "allow" | "deny"): Promise<unknown>;
|
|
117
|
+
createAgent(params: {
|
|
118
|
+
agentId?: string;
|
|
119
|
+
repo: string;
|
|
120
|
+
model?: string;
|
|
121
|
+
permissionMode?: string;
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
agentId: string;
|
|
125
|
+
state: string;
|
|
126
|
+
}>;
|
|
127
|
+
destroyAgent(agentId: string): Promise<{
|
|
128
|
+
destroyed: boolean;
|
|
129
|
+
}>;
|
|
130
|
+
getLog(agentId: string, opts?: {
|
|
131
|
+
offset?: number;
|
|
132
|
+
limit?: number;
|
|
133
|
+
grep?: string;
|
|
134
|
+
since?: number;
|
|
135
|
+
type?: string;
|
|
136
|
+
}): Promise<{
|
|
137
|
+
totalLines: number;
|
|
138
|
+
returnedLines: number;
|
|
139
|
+
offset: number;
|
|
140
|
+
lines: Array<{
|
|
141
|
+
ts: number;
|
|
142
|
+
type: string;
|
|
143
|
+
text: string;
|
|
144
|
+
}>;
|
|
145
|
+
}>;
|
|
146
|
+
private connect;
|
|
147
|
+
private scheduleReconnect;
|
|
148
|
+
private register;
|
|
149
|
+
private startHeartbeat;
|
|
150
|
+
private sendPing;
|
|
151
|
+
private forceReconnect;
|
|
152
|
+
private handleData;
|
|
153
|
+
private handleMessage;
|
|
154
|
+
private handleResponse;
|
|
155
|
+
private handleEvent;
|
|
156
|
+
private syncStateAfterConnect;
|
|
157
|
+
private handleReverseCommand;
|
|
158
|
+
private handleExecCommand;
|
|
159
|
+
private handleRestartServiceCommand;
|
|
160
|
+
private handleNotifyCommand;
|
|
161
|
+
private sendResponse;
|
|
162
|
+
private sendCommand;
|
|
163
|
+
private sendRaw;
|
|
164
|
+
private clearTimers;
|
|
165
|
+
private clearHeartbeat;
|
|
166
|
+
private rejectAllPending;
|
|
167
|
+
}
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TgccSupervisorClient — Unix socket client for the TGCC Supervisor Protocol.
|
|
4
|
+
*
|
|
5
|
+
* Connects to TGCC's ctl socket, registers as a supervisor, and provides
|
|
6
|
+
* async methods for interacting with TGCC-managed agents and CC processes.
|
|
7
|
+
*
|
|
8
|
+
* Adapted from the OpenClaw fork (src/agents/tgcc-supervisor/client.ts)
|
|
9
|
+
* to use only the plugin API — no OpenClaw internal imports.
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.TgccSupervisorClient = void 0;
|
|
16
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
18
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
19
|
+
const node_events_1 = require("node:events");
|
|
20
|
+
const DEFAULT_RECONNECT_INITIAL_MS = 1_000;
|
|
21
|
+
const DEFAULT_RECONNECT_MAX_MS = 30_000;
|
|
22
|
+
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
23
|
+
const HEARTBEAT_PONG_TIMEOUT_MS = 5_000;
|
|
24
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Safety patterns for reverse-exec
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const EXEC_DENY_PATTERNS = [
|
|
29
|
+
/rm\s+(-[a-z]*f[a-z]*\s+)?\//i,
|
|
30
|
+
/\bsudo\b/,
|
|
31
|
+
/\bmkfs\b/,
|
|
32
|
+
/\bdd\b.*\bof=\//,
|
|
33
|
+
/:\(\)\{/,
|
|
34
|
+
/\bchmod\s+777\s+\//,
|
|
35
|
+
/\bchown\b.*\//,
|
|
36
|
+
/\|\s*sh\b/,
|
|
37
|
+
/\$\(/,
|
|
38
|
+
/`[^`]*`/,
|
|
39
|
+
];
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Client
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
class TgccSupervisorClient extends node_events_1.EventEmitter {
|
|
44
|
+
log;
|
|
45
|
+
socketPath;
|
|
46
|
+
socket = null;
|
|
47
|
+
connected = false;
|
|
48
|
+
destroyed = false;
|
|
49
|
+
reconnectDelay;
|
|
50
|
+
reconnectInitialMs;
|
|
51
|
+
reconnectMaxMs;
|
|
52
|
+
heartbeatMs;
|
|
53
|
+
reconnectTimer = null;
|
|
54
|
+
heartbeatTimer = null;
|
|
55
|
+
pongTimer = null;
|
|
56
|
+
pendingRequests = new Map();
|
|
57
|
+
lineBuffer = "";
|
|
58
|
+
constructor(config) {
|
|
59
|
+
super();
|
|
60
|
+
this.log = config.logger;
|
|
61
|
+
this.socketPath = config.socket;
|
|
62
|
+
this.reconnectInitialMs = config.reconnectInitialMs ?? DEFAULT_RECONNECT_INITIAL_MS;
|
|
63
|
+
this.reconnectMaxMs = config.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
|
|
64
|
+
this.heartbeatMs = config.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
65
|
+
this.reconnectDelay = this.reconnectInitialMs;
|
|
66
|
+
}
|
|
67
|
+
// ── Public lifecycle ─────────────────────────────────────────────────
|
|
68
|
+
start() {
|
|
69
|
+
if (this.destroyed)
|
|
70
|
+
return;
|
|
71
|
+
this.connect();
|
|
72
|
+
}
|
|
73
|
+
stop() {
|
|
74
|
+
this.destroyed = true;
|
|
75
|
+
this.clearTimers();
|
|
76
|
+
this.rejectAllPending("Client stopped");
|
|
77
|
+
if (this.socket) {
|
|
78
|
+
this.socket.removeAllListeners();
|
|
79
|
+
this.socket.destroy();
|
|
80
|
+
this.socket = null;
|
|
81
|
+
}
|
|
82
|
+
this.connected = false;
|
|
83
|
+
}
|
|
84
|
+
isConnected() {
|
|
85
|
+
return this.connected;
|
|
86
|
+
}
|
|
87
|
+
// ── Public API methods ───────────────────────────────────────────────
|
|
88
|
+
async sendMessage(agentId, text, opts) {
|
|
89
|
+
return (await this.sendCommand("send_message", {
|
|
90
|
+
agentId,
|
|
91
|
+
text,
|
|
92
|
+
sessionId: opts?.sessionId,
|
|
93
|
+
subscribe: opts?.subscribe ?? true,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
async sendToCC(agentId, text) {
|
|
97
|
+
return (await this.sendCommand("send_to_cc", { agentId, text }));
|
|
98
|
+
}
|
|
99
|
+
async getStatus(agentId) {
|
|
100
|
+
const params = {};
|
|
101
|
+
if (agentId)
|
|
102
|
+
params.agentId = agentId;
|
|
103
|
+
return (await this.sendCommand("status", params));
|
|
104
|
+
}
|
|
105
|
+
async killCC(agentId) {
|
|
106
|
+
return this.sendCommand("kill_cc", { agentId });
|
|
107
|
+
}
|
|
108
|
+
async subscribe(agentId, sessionId) {
|
|
109
|
+
const params = { agentId };
|
|
110
|
+
if (sessionId)
|
|
111
|
+
params.sessionId = sessionId;
|
|
112
|
+
return this.sendCommand("subscribe", params);
|
|
113
|
+
}
|
|
114
|
+
async unsubscribe(agentId) {
|
|
115
|
+
return this.sendCommand("unsubscribe", { agentId });
|
|
116
|
+
}
|
|
117
|
+
async respondToPermission(agentId, permissionRequestId, decision) {
|
|
118
|
+
return this.sendCommand("permission_response", { agentId, permissionRequestId, decision });
|
|
119
|
+
}
|
|
120
|
+
async createAgent(params) {
|
|
121
|
+
return (await this.sendCommand("create_agent", {
|
|
122
|
+
agentId: params.agentId,
|
|
123
|
+
repo: params.repo,
|
|
124
|
+
model: params.model,
|
|
125
|
+
permissionMode: params.permissionMode,
|
|
126
|
+
timeoutMs: params.timeoutMs,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
async destroyAgent(agentId) {
|
|
130
|
+
return (await this.sendCommand("destroy_agent", { agentId }));
|
|
131
|
+
}
|
|
132
|
+
async getLog(agentId, opts) {
|
|
133
|
+
const params = { agentId };
|
|
134
|
+
if (opts?.offset != null)
|
|
135
|
+
params.offset = opts.offset;
|
|
136
|
+
if (opts?.limit != null)
|
|
137
|
+
params.limit = opts.limit;
|
|
138
|
+
if (opts?.grep)
|
|
139
|
+
params.grep = opts.grep;
|
|
140
|
+
if (opts?.since != null)
|
|
141
|
+
params.since = opts.since;
|
|
142
|
+
if (opts?.type)
|
|
143
|
+
params.type = opts.type;
|
|
144
|
+
return (await this.sendCommand("get_log", params));
|
|
145
|
+
}
|
|
146
|
+
// ── Connection management ────────────────────────────────────────────
|
|
147
|
+
connect() {
|
|
148
|
+
if (this.destroyed)
|
|
149
|
+
return;
|
|
150
|
+
this.log.info(`[tgcc] connecting to ctl socket: ${this.socketPath}`);
|
|
151
|
+
const sock = node_net_1.default.createConnection({ path: this.socketPath });
|
|
152
|
+
this.socket = sock;
|
|
153
|
+
sock.on("connect", () => {
|
|
154
|
+
this.log.info("[tgcc] connected to ctl socket");
|
|
155
|
+
this.connected = true;
|
|
156
|
+
this.reconnectDelay = this.reconnectInitialMs;
|
|
157
|
+
this.lineBuffer = "";
|
|
158
|
+
this.register();
|
|
159
|
+
this.startHeartbeat();
|
|
160
|
+
this.emit("connected");
|
|
161
|
+
});
|
|
162
|
+
sock.on("data", (data) => {
|
|
163
|
+
this.handleData(data);
|
|
164
|
+
});
|
|
165
|
+
sock.on("error", (err) => {
|
|
166
|
+
this.log.warn(`[tgcc] socket error: ${err.message}`);
|
|
167
|
+
});
|
|
168
|
+
sock.on("close", () => {
|
|
169
|
+
const wasConnected = this.connected;
|
|
170
|
+
this.connected = false;
|
|
171
|
+
this.clearTimers();
|
|
172
|
+
this.socket = null;
|
|
173
|
+
if (wasConnected) {
|
|
174
|
+
this.log.info("[tgcc] socket closed, scheduling reconnect");
|
|
175
|
+
this.emit("disconnected");
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.emit("connectFailed");
|
|
179
|
+
}
|
|
180
|
+
this.scheduleReconnect();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
scheduleReconnect() {
|
|
184
|
+
if (this.destroyed)
|
|
185
|
+
return;
|
|
186
|
+
if (this.reconnectTimer)
|
|
187
|
+
return;
|
|
188
|
+
const delay = this.reconnectDelay;
|
|
189
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.reconnectMaxMs);
|
|
190
|
+
this.log.info(`[tgcc] reconnecting in ${delay}ms`);
|
|
191
|
+
this.reconnectTimer = setTimeout(() => {
|
|
192
|
+
this.reconnectTimer = null;
|
|
193
|
+
this.connect();
|
|
194
|
+
}, delay);
|
|
195
|
+
this.reconnectTimer.unref?.();
|
|
196
|
+
}
|
|
197
|
+
register() {
|
|
198
|
+
this.sendRaw({
|
|
199
|
+
type: "register_supervisor",
|
|
200
|
+
agentId: "openclaw",
|
|
201
|
+
capabilities: ["exec", "notify"],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// ── Heartbeat ────────────────────────────────────────────────────────
|
|
205
|
+
startHeartbeat() {
|
|
206
|
+
this.clearHeartbeat();
|
|
207
|
+
this.heartbeatTimer = setInterval(() => {
|
|
208
|
+
this.sendPing();
|
|
209
|
+
}, this.heartbeatMs);
|
|
210
|
+
this.heartbeatTimer.unref?.();
|
|
211
|
+
}
|
|
212
|
+
sendPing() {
|
|
213
|
+
if (!this.connected)
|
|
214
|
+
return;
|
|
215
|
+
const requestId = node_crypto_1.default.randomUUID();
|
|
216
|
+
this.sendRaw({
|
|
217
|
+
type: "command",
|
|
218
|
+
requestId,
|
|
219
|
+
action: "ping",
|
|
220
|
+
});
|
|
221
|
+
this.pongTimer = setTimeout(() => {
|
|
222
|
+
this.log.warn("[tgcc] pong timeout, forcing reconnect");
|
|
223
|
+
this.forceReconnect();
|
|
224
|
+
}, HEARTBEAT_PONG_TIMEOUT_MS);
|
|
225
|
+
this.pongTimer.unref?.();
|
|
226
|
+
this.pendingRequests.set(requestId, {
|
|
227
|
+
resolve: () => {
|
|
228
|
+
if (this.pongTimer) {
|
|
229
|
+
clearTimeout(this.pongTimer);
|
|
230
|
+
this.pongTimer = null;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
reject: () => { },
|
|
234
|
+
timer: setTimeout(() => {
|
|
235
|
+
this.pendingRequests.delete(requestId);
|
|
236
|
+
}, HEARTBEAT_PONG_TIMEOUT_MS + 1_000),
|
|
237
|
+
});
|
|
238
|
+
this.pendingRequests.get(requestId).timer.unref?.();
|
|
239
|
+
}
|
|
240
|
+
forceReconnect() {
|
|
241
|
+
this.rejectAllPending("Connection lost");
|
|
242
|
+
if (this.socket) {
|
|
243
|
+
this.socket.removeAllListeners();
|
|
244
|
+
this.socket.destroy();
|
|
245
|
+
this.socket = null;
|
|
246
|
+
}
|
|
247
|
+
this.connected = false;
|
|
248
|
+
this.clearTimers();
|
|
249
|
+
this.scheduleReconnect();
|
|
250
|
+
}
|
|
251
|
+
// ── Data parsing ─────────────────────────────────────────────────────
|
|
252
|
+
handleData(data) {
|
|
253
|
+
this.lineBuffer += data.toString("utf-8");
|
|
254
|
+
const lines = this.lineBuffer.split("\n");
|
|
255
|
+
this.lineBuffer = lines.pop() ?? "";
|
|
256
|
+
for (const line of lines) {
|
|
257
|
+
const trimmed = line.trim();
|
|
258
|
+
if (!trimmed)
|
|
259
|
+
continue;
|
|
260
|
+
try {
|
|
261
|
+
const msg = JSON.parse(trimmed);
|
|
262
|
+
this.handleMessage(msg);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
this.log.warn(`[tgcc] failed to parse message: ${trimmed.slice(0, 200)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
handleMessage(msg) {
|
|
270
|
+
if (msg.type === "response") {
|
|
271
|
+
this.handleResponse(msg);
|
|
272
|
+
}
|
|
273
|
+
else if (msg.type === "event") {
|
|
274
|
+
this.handleEvent(msg);
|
|
275
|
+
}
|
|
276
|
+
else if (msg.type === "command") {
|
|
277
|
+
void this.handleReverseCommand(msg);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
handleResponse(msg) {
|
|
281
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
282
|
+
if (!pending)
|
|
283
|
+
return;
|
|
284
|
+
this.pendingRequests.delete(msg.requestId);
|
|
285
|
+
clearTimeout(pending.timer);
|
|
286
|
+
if (msg.error) {
|
|
287
|
+
pending.reject(new Error(msg.error));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
pending.resolve(msg.result);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
handleEvent(msg) {
|
|
294
|
+
const eventName = msg.event;
|
|
295
|
+
this.log.info(`[tgcc] event: ${String(eventName)} agentId=${String(msg.agentId ?? "?")}`);
|
|
296
|
+
switch (eventName) {
|
|
297
|
+
case "result":
|
|
298
|
+
this.emit("tgcc:result", {
|
|
299
|
+
agentId: msg.agentId,
|
|
300
|
+
sessionId: msg.sessionId,
|
|
301
|
+
text: msg.text,
|
|
302
|
+
cost_usd: msg.cost_usd,
|
|
303
|
+
duration_ms: msg.duration_ms,
|
|
304
|
+
is_error: msg.is_error,
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
case "process_exit":
|
|
308
|
+
this.emit("tgcc:process_exit", {
|
|
309
|
+
agentId: msg.agentId,
|
|
310
|
+
sessionId: msg.sessionId,
|
|
311
|
+
exitCode: msg.exitCode ?? null,
|
|
312
|
+
});
|
|
313
|
+
break;
|
|
314
|
+
case "session_takeover":
|
|
315
|
+
this.emit("tgcc:session_takeover", {
|
|
316
|
+
agentId: msg.agentId,
|
|
317
|
+
sessionId: msg.sessionId,
|
|
318
|
+
exitCode: msg.exitCode ?? null,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
case "api_error":
|
|
322
|
+
this.emit("tgcc:api_error", {
|
|
323
|
+
agentId: msg.agentId,
|
|
324
|
+
sessionId: msg.sessionId,
|
|
325
|
+
message: msg.message,
|
|
326
|
+
});
|
|
327
|
+
break;
|
|
328
|
+
case "permission_request":
|
|
329
|
+
this.emit("tgcc:permission_request", {
|
|
330
|
+
agentId: msg.agentId,
|
|
331
|
+
toolName: msg.toolName,
|
|
332
|
+
requestId: msg.requestId,
|
|
333
|
+
description: msg.description,
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
case "registered":
|
|
337
|
+
this.log.info("[tgcc] supervisor registered");
|
|
338
|
+
this.emit("registered");
|
|
339
|
+
void this.syncStateAfterConnect();
|
|
340
|
+
break;
|
|
341
|
+
case "bridge_started":
|
|
342
|
+
case "cc_spawned":
|
|
343
|
+
case "agent_created":
|
|
344
|
+
case "agent_destroyed":
|
|
345
|
+
case "state_changed":
|
|
346
|
+
case "build_result":
|
|
347
|
+
case "git_commit":
|
|
348
|
+
case "context_pressure":
|
|
349
|
+
case "failure_loop":
|
|
350
|
+
case "stuck":
|
|
351
|
+
case "task_milestone":
|
|
352
|
+
case "cc_message":
|
|
353
|
+
case "subagent_spawn":
|
|
354
|
+
case "budget_alert":
|
|
355
|
+
this.emit(`tgcc:${eventName}`, msg);
|
|
356
|
+
break;
|
|
357
|
+
default:
|
|
358
|
+
this.emit(`tgcc:${eventName}`, msg);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async syncStateAfterConnect() {
|
|
363
|
+
try {
|
|
364
|
+
const status = await this.getStatus();
|
|
365
|
+
this.emit("tgcc:status_sync", status);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
this.log.warn(`[tgcc] failed to sync state after connect: ${err instanceof Error ? err.message : String(err)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// ── Reverse command handling (TGCC → OpenClaw) ───────────────────────
|
|
372
|
+
async handleReverseCommand(msg) {
|
|
373
|
+
const { requestId, action, params } = msg;
|
|
374
|
+
this.log.info(`[tgcc] reverse command: ${action} (requestId=${requestId})`);
|
|
375
|
+
try {
|
|
376
|
+
switch (action) {
|
|
377
|
+
case "exec":
|
|
378
|
+
await this.handleExecCommand(requestId, params ?? {});
|
|
379
|
+
break;
|
|
380
|
+
case "restart_service":
|
|
381
|
+
await this.handleRestartServiceCommand(requestId, params ?? {});
|
|
382
|
+
break;
|
|
383
|
+
case "notify":
|
|
384
|
+
this.handleNotifyCommand(requestId, params ?? {});
|
|
385
|
+
break;
|
|
386
|
+
default:
|
|
387
|
+
this.sendResponse(requestId, undefined, `Unknown reverse command: ${action}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
392
|
+
this.log.warn(`[tgcc] reverse command ${action} failed: ${errMsg}`);
|
|
393
|
+
this.sendResponse(requestId, undefined, errMsg);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async handleExecCommand(requestId, params) {
|
|
397
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
398
|
+
const agentId = typeof params.agentId === "string" ? params.agentId : "unknown";
|
|
399
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 60_000;
|
|
400
|
+
if (!command.trim()) {
|
|
401
|
+
this.sendResponse(requestId, undefined, "Empty command");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
for (const pattern of EXEC_DENY_PATTERNS) {
|
|
405
|
+
if (pattern.test(command)) {
|
|
406
|
+
this.log.warn(`[tgcc] reverse exec DENIED (agent=${agentId}): ${command}`);
|
|
407
|
+
this.sendResponse(requestId, undefined, `Command denied by safety gate: ${command.slice(0, 100)}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
this.log.info(`[tgcc] reverse exec (agent=${agentId}): ${command.slice(0, 200)}`);
|
|
412
|
+
return new Promise((resolve) => {
|
|
413
|
+
(0, node_child_process_1.exec)(command, {
|
|
414
|
+
timeout: timeoutMs,
|
|
415
|
+
maxBuffer: 1024 * 1024,
|
|
416
|
+
cwd: typeof params.cwd === "string" ? params.cwd : undefined,
|
|
417
|
+
}, (err, stdout, stderr) => {
|
|
418
|
+
const exitCode = err && "code" in err ? (err.code ?? 1) : err ? 1 : 0;
|
|
419
|
+
this.sendResponse(requestId, {
|
|
420
|
+
exitCode,
|
|
421
|
+
stdout: stdout.slice(0, 50_000),
|
|
422
|
+
stderr: stderr.slice(0, 50_000),
|
|
423
|
+
});
|
|
424
|
+
resolve();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async handleRestartServiceCommand(requestId, params) {
|
|
429
|
+
const service = typeof params.service === "string" ? params.service : "";
|
|
430
|
+
const agentId = typeof params.agentId === "string" ? params.agentId : "unknown";
|
|
431
|
+
this.log.info(`[tgcc] reverse restart_service (agent=${agentId}, service=${service})`);
|
|
432
|
+
if (service === "tgcc") {
|
|
433
|
+
return new Promise((resolve) => {
|
|
434
|
+
(0, node_child_process_1.exec)(`tmux send-keys -t tgcc C-c '' Enter; sleep 1; tmux send-keys -t tgcc 'cd ~/Botverse/tgcc && node dist/cli.js run' Enter`, { timeout: 15_000 }, (err) => {
|
|
435
|
+
if (err) {
|
|
436
|
+
this.sendResponse(requestId, undefined, `Failed to restart tgcc: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.sendResponse(requestId, { restarted: true, service });
|
|
440
|
+
}
|
|
441
|
+
resolve();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
this.sendResponse(requestId, undefined, `Unknown service: ${service}`);
|
|
446
|
+
}
|
|
447
|
+
handleNotifyCommand(requestId, params) {
|
|
448
|
+
const message = typeof params.message === "string" ? params.message : "";
|
|
449
|
+
const target = typeof params.target === "string" ? params.target : "";
|
|
450
|
+
if (!message.trim()) {
|
|
451
|
+
this.sendResponse(requestId, undefined, "Empty notification message");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.emit("tgcc:reverse_notify", { target, message });
|
|
455
|
+
this.sendResponse(requestId, { notified: true });
|
|
456
|
+
}
|
|
457
|
+
sendResponse(requestId, result, error) {
|
|
458
|
+
this.sendRaw({
|
|
459
|
+
type: "response",
|
|
460
|
+
requestId,
|
|
461
|
+
...(error ? { error } : { result }),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
// ── Command sending ──────────────────────────────────────────────────
|
|
465
|
+
sendCommand(action, params) {
|
|
466
|
+
return new Promise((resolve, reject) => {
|
|
467
|
+
if (!this.connected || !this.socket) {
|
|
468
|
+
reject(new Error("Not connected to TGCC"));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const requestId = node_crypto_1.default.randomUUID();
|
|
472
|
+
const timer = setTimeout(() => {
|
|
473
|
+
this.pendingRequests.delete(requestId);
|
|
474
|
+
reject(new Error(`TGCC command timeout: ${action}`));
|
|
475
|
+
}, COMMAND_TIMEOUT_MS);
|
|
476
|
+
timer.unref?.();
|
|
477
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
478
|
+
this.sendRaw({
|
|
479
|
+
type: "command",
|
|
480
|
+
requestId,
|
|
481
|
+
action,
|
|
482
|
+
params,
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
sendRaw(msg) {
|
|
487
|
+
if (!this.socket || this.socket.destroyed)
|
|
488
|
+
return;
|
|
489
|
+
try {
|
|
490
|
+
this.socket.write(JSON.stringify(msg) + "\n");
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
this.log.warn(`[tgcc] failed to send: ${err instanceof Error ? err.message : String(err)}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// ── Timer management ─────────────────────────────────────────────────
|
|
497
|
+
clearTimers() {
|
|
498
|
+
this.clearHeartbeat();
|
|
499
|
+
if (this.reconnectTimer) {
|
|
500
|
+
clearTimeout(this.reconnectTimer);
|
|
501
|
+
this.reconnectTimer = null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
clearHeartbeat() {
|
|
505
|
+
if (this.heartbeatTimer) {
|
|
506
|
+
clearInterval(this.heartbeatTimer);
|
|
507
|
+
this.heartbeatTimer = null;
|
|
508
|
+
}
|
|
509
|
+
if (this.pongTimer) {
|
|
510
|
+
clearTimeout(this.pongTimer);
|
|
511
|
+
this.pongTimer = null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
rejectAllPending(reason) {
|
|
515
|
+
for (const [, pending] of this.pendingRequests) {
|
|
516
|
+
clearTimeout(pending.timer);
|
|
517
|
+
pending.reject(new Error(reason));
|
|
518
|
+
}
|
|
519
|
+
this.pendingRequests.clear();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
exports.TgccSupervisorClient = TgccSupervisorClient;
|
|
523
|
+
//# sourceMappingURL=client.js.map
|