@retasc/cli 1.0.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/api.js +33 -0
- package/dist/auth.js +78 -0
- package/dist/commands/claim.js +174 -0
- package/dist/commands/gate.js +135 -0
- package/dist/commands/mcp.js +122 -0
- package/dist/config.js +56 -0
- package/dist/index.js +302 -0
- package/dist/lib/claim.js +145 -0
- package/dist/lib/watchdog.js +66 -0
- package/dist/proxy.js +147 -0
- package/package.json +52 -0
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Liveness watchdog proxy (RTSC-44). A stdio MCP server the harness (Claude Code,
|
|
2
|
+
// Codex, OpenCode, …) spawns. It transparently forwards every JSON-RPC message to
|
|
3
|
+
// the remote Retasc MCP over HTTP and — by watching claim tokens in the responses —
|
|
4
|
+
// keeps claimed issues' leases alive with out-of-band `heartbeat` calls. The LLM
|
|
5
|
+
// never heartbeats. Fail-safe: if this process dies, heartbeats stop and the server
|
|
6
|
+
// reclaims the lease (the correct default) — a broken watchdog is never worse than
|
|
7
|
+
// no watchdog. Self-enforcing: no proxy → no Retasc tools → can't orphan a lease.
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { hostname } from "node:os";
|
|
10
|
+
import { applyObservation, heartbeatRequest, isClaimLost, } from "./lib/watchdog.js";
|
|
11
|
+
const MCP_URL = process.env.RETASC_MCP_URL || "https://mcp.retasc.com/mcp";
|
|
12
|
+
const KEY = process.env.RETASC_MCP_KEY || "";
|
|
13
|
+
const HEARTBEAT_MS = Number(process.env.RETASC_HEARTBEAT_MS) || 10 * 60 * 1000;
|
|
14
|
+
const leases = new Map();
|
|
15
|
+
let hbSeq = -1; // out-of-band heartbeat ids are negative — never collide with the harness's
|
|
16
|
+
// Per-session key (RTSC-50): starts as the workspace key; on startup we mint a
|
|
17
|
+
// session key and switch to it so this session is distinguishable from others.
|
|
18
|
+
let activeKey = KEY;
|
|
19
|
+
// stderr only: stdout is the MCP channel and must carry ONLY protocol messages.
|
|
20
|
+
function log(msg) {
|
|
21
|
+
process.stderr.write(`[retasc-watchdog] ${msg}\n`);
|
|
22
|
+
}
|
|
23
|
+
async function postRemote(body) {
|
|
24
|
+
const res = await fetch(MCP_URL, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${activeKey}`,
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
});
|
|
33
|
+
const text = await res.text();
|
|
34
|
+
return text ? JSON.parse(text) : null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Auto-adopt a per-session key (RTSC-50). Mint one bound to our identity and use
|
|
38
|
+
* it for the rest of the session, so concurrent sessions of the same agent are
|
|
39
|
+
* distinguishable. Fail-soft: if minting is unavailable (older server, error),
|
|
40
|
+
* keep using the workspace key — no worse than before.
|
|
41
|
+
*/
|
|
42
|
+
async function adoptSessionKey() {
|
|
43
|
+
if (!KEY)
|
|
44
|
+
return;
|
|
45
|
+
const label = process.env.RETASC_SESSION_LABEL || `${hostname()}#${process.pid}`;
|
|
46
|
+
try {
|
|
47
|
+
const resp = await postRemote({
|
|
48
|
+
jsonrpc: "2.0",
|
|
49
|
+
id: -1000,
|
|
50
|
+
method: "tools/call",
|
|
51
|
+
params: { name: "mint_session_key", arguments: { label } },
|
|
52
|
+
});
|
|
53
|
+
const r = toolResult(resp);
|
|
54
|
+
if (r && typeof r === "object" && typeof r.key === "string") {
|
|
55
|
+
activeKey = r.key; // switch to the session key
|
|
56
|
+
log(`adopted session key "${r.session ?? label}"`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
log("mint_session_key unavailable — using the workspace key");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
log(`session-key mint failed (${String(e?.message ?? e)}) — using the workspace key`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The tool result payload — the JSON inside result.content[0].text — or the raw result.
|
|
67
|
+
function toolResult(resp) {
|
|
68
|
+
try {
|
|
69
|
+
const t = resp?.result?.content?.[0]?.text;
|
|
70
|
+
return t ? JSON.parse(t) : resp?.result;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return resp?.result;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function handleLine(line) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!trimmed)
|
|
79
|
+
return;
|
|
80
|
+
let msg;
|
|
81
|
+
try {
|
|
82
|
+
msg = JSON.parse(trimmed);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return; // not a JSON-RPC message — ignore
|
|
86
|
+
}
|
|
87
|
+
let resp;
|
|
88
|
+
try {
|
|
89
|
+
resp = await postRemote(msg);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// Forwarding failed: return a JSON-RPC error so the harness doesn't hang.
|
|
93
|
+
if (msg.id !== undefined) {
|
|
94
|
+
process.stdout.write(JSON.stringify({
|
|
95
|
+
jsonrpc: "2.0",
|
|
96
|
+
id: msg.id,
|
|
97
|
+
error: { code: -32000, message: `retasc proxy forward failed: ${String(e?.message ?? e)}` },
|
|
98
|
+
}) + "\n");
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Watch tools/call traffic for claims/releases (request args + result).
|
|
103
|
+
if (msg.method === "tools/call" && resp) {
|
|
104
|
+
applyObservation(leases, {
|
|
105
|
+
toolName: msg.params?.name,
|
|
106
|
+
args: msg.params?.arguments,
|
|
107
|
+
result: toolResult(resp),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Relay the response (requests have an id; notifications don't).
|
|
111
|
+
if (resp != null && msg.id !== undefined) {
|
|
112
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function heartbeatAll() {
|
|
116
|
+
for (const [issueId, token] of [...leases]) {
|
|
117
|
+
try {
|
|
118
|
+
const resp = await postRemote(heartbeatRequest(hbSeq--, issueId, token));
|
|
119
|
+
if (isClaimLost(toolResult(resp))) {
|
|
120
|
+
leases.delete(issueId);
|
|
121
|
+
log(`lease for ${issueId} is gone — stopped tracking`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
log(`heartbeat for ${issueId} failed: ${String(e?.message ?? e)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Run the stdio proxy. Started by the harness; lives as long as the session. */
|
|
130
|
+
export async function runProxy() {
|
|
131
|
+
if (!KEY)
|
|
132
|
+
log("warning: RETASC_MCP_KEY is empty — forwarded requests will be unauthorized");
|
|
133
|
+
// Adopt a per-session key BEFORE serving traffic, so even the first claim is
|
|
134
|
+
// attributed to this session. stdin buffers in the OS pipe meanwhile.
|
|
135
|
+
await adoptSessionKey();
|
|
136
|
+
const timer = setInterval(() => void heartbeatAll(), HEARTBEAT_MS);
|
|
137
|
+
timer.unref?.(); // the timer alone must not keep the process alive
|
|
138
|
+
const rl = createInterface({ input: process.stdin });
|
|
139
|
+
rl.on("line", (line) => void handleLine(line));
|
|
140
|
+
rl.on("close", () => {
|
|
141
|
+
// Don't hard-exit: let in-flight forwards finish and write their responses.
|
|
142
|
+
// With stdin closed and the timer unref'd, the process exits once the event
|
|
143
|
+
// loop drains. (In a real harness stdin stays open for the whole session.)
|
|
144
|
+
clearInterval(timer);
|
|
145
|
+
});
|
|
146
|
+
log(`up → ${MCP_URL} (heartbeat every ${Math.round(HEARTBEAT_MS / 60000)}m)`);
|
|
147
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@retasc/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Retasc CLI — sign in with GitHub, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"retasc": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Retasc",
|
|
17
|
+
"homepage": "https://retasc.com",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Retasc/retasc.git",
|
|
21
|
+
"directory": "cli"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Retasc/retasc/issues"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"retasc",
|
|
28
|
+
"mcp",
|
|
29
|
+
"issue-tracker",
|
|
30
|
+
"agent",
|
|
31
|
+
"ai-agents",
|
|
32
|
+
"work-queue",
|
|
33
|
+
"orchestration",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"codex",
|
|
36
|
+
"cli"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"dev": "tsc --watch",
|
|
41
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"commander": "^12.1.0",
|
|
46
|
+
"convex": "^1.41.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"typescript": "^5.6.0"
|
|
51
|
+
}
|
|
52
|
+
}
|