@primusclaw/connector 0.0.0-stage → 0.1.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 +37 -0
- package/README.md +419 -3
- package/dist/cli.js +3972 -0
- package/dist/terminal-hook.js +206 -0
- package/package.json +35 -6
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/terminals.ts
|
|
4
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
5
|
+
import fs2 from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/children.ts
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
function processStart(pid) {
|
|
12
|
+
try {
|
|
13
|
+
if (process.platform === "linux") {
|
|
14
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
15
|
+
const rest = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
16
|
+
return rest[19] ?? null;
|
|
17
|
+
}
|
|
18
|
+
return execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
var isAlive = (pid) => {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return e.code === "EPERM";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/terminals.ts
|
|
33
|
+
var SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
34
|
+
var TERMINALS_DIR = "claw-connector-terminals";
|
|
35
|
+
var terminalsDir = (configDir) => path.join(configDir, TERMINALS_DIR);
|
|
36
|
+
var UNREADABLE_GRACE_MS = 6e4;
|
|
37
|
+
var MAX_STDIN = 1 << 20;
|
|
38
|
+
function localPidDomain() {
|
|
39
|
+
if (process.platform !== "linux") return null;
|
|
40
|
+
try {
|
|
41
|
+
return `linux:${fs2.readFileSync("/etc/machine-id", "utf8").trim()}:${fs2.readlinkSync("/proc/self/ns/pid")}`;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var isObject = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
47
|
+
function holdsStill(row, localDomain) {
|
|
48
|
+
const foreign = row.pidDomain !== null && localDomain !== null && row.pidDomain !== localDomain;
|
|
49
|
+
if (foreign) return { open: true, foreign };
|
|
50
|
+
if (!isAlive(row.pid)) return { open: false, foreign };
|
|
51
|
+
if (row.procStart !== null && processStart(row.pid) !== row.procStart) return { open: false, foreign };
|
|
52
|
+
return { open: true, foreign };
|
|
53
|
+
}
|
|
54
|
+
function parseRow(doc) {
|
|
55
|
+
if (!isObject(doc) || doc.v !== 1) return null;
|
|
56
|
+
if (typeof doc.pid !== "number" || !Number.isInteger(doc.pid) || doc.pid <= 1) return null;
|
|
57
|
+
if (typeof doc.sessionId !== "string" || !SESSION_ID_RE.test(doc.sessionId)) return null;
|
|
58
|
+
return {
|
|
59
|
+
v: 1,
|
|
60
|
+
pid: doc.pid,
|
|
61
|
+
sessionId: doc.sessionId.toLowerCase(),
|
|
62
|
+
procStart: typeof doc.procStart === "string" && doc.procStart ? doc.procStart : null,
|
|
63
|
+
cwd: typeof doc.cwd === "string" ? doc.cwd : "",
|
|
64
|
+
at: typeof doc.at === "number" ? doc.at : 0,
|
|
65
|
+
pidDomain: typeof doc.pidDomain === "string" ? doc.pidDomain : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function ancestorsOf(pid, max) {
|
|
69
|
+
const out = [];
|
|
70
|
+
let p = pid;
|
|
71
|
+
for (let i = 0; i < max && p > 1; i++) {
|
|
72
|
+
out.push(p);
|
|
73
|
+
let parent = 0;
|
|
74
|
+
try {
|
|
75
|
+
if (process.platform === "linux") {
|
|
76
|
+
const stat = fs2.readFileSync(`/proc/${p}/stat`, "utf8");
|
|
77
|
+
parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
78
|
+
} else {
|
|
79
|
+
parent = Number(execFileSync2("ps", ["-o", "ppid=", "-p", String(p)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(parent) || parent <= 1) break;
|
|
85
|
+
p = parent;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function commOfProcess(pid) {
|
|
90
|
+
try {
|
|
91
|
+
if (process.platform === "linux") return fs2.readFileSync(`/proc/${pid}/comm`, "utf8").trim() || null;
|
|
92
|
+
return path.basename(execFileSync2("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()) || null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function claudePid(env, ppid, deps) {
|
|
98
|
+
const chain = (deps.ancestors ?? ancestorsOf)(ppid, 8);
|
|
99
|
+
const fromEnv = Number(env.CLAUDE_PID);
|
|
100
|
+
if (Number.isInteger(fromEnv) && fromEnv > 1 && (chain.length === 0 ? isAlive(fromEnv) : chain.includes(fromEnv))) return fromEnv;
|
|
101
|
+
const comm = deps.commOf ?? commOfProcess;
|
|
102
|
+
for (const p of chain) if (comm(p) === "claude") return p;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function writeRow(dir, row) {
|
|
106
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
107
|
+
const file = path.join(dir, `${row.pid}.json`);
|
|
108
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
109
|
+
fs2.writeFileSync(tmp, JSON.stringify(row) + "\n", { mode: 384 });
|
|
110
|
+
fs2.renameSync(tmp, file);
|
|
111
|
+
}
|
|
112
|
+
function prune(dir, localDomain, now) {
|
|
113
|
+
let names;
|
|
114
|
+
try {
|
|
115
|
+
names = fs2.readdirSync(dir);
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
const file = path.join(dir, name);
|
|
121
|
+
try {
|
|
122
|
+
if (name.endsWith(".tmp")) {
|
|
123
|
+
if (now - fs2.statSync(file).mtimeMs > UNREADABLE_GRACE_MS) fs2.rmSync(file, { force: true });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!name.endsWith(".json")) continue;
|
|
127
|
+
const row = parseRow(JSON.parse(fs2.readFileSync(file, "utf8")));
|
|
128
|
+
if (row && !holdsStill(row, localDomain).open) fs2.rmSync(file, { force: true });
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function runHook(i, deps = {}) {
|
|
134
|
+
let doc;
|
|
135
|
+
try {
|
|
136
|
+
doc = JSON.parse(i.stdin);
|
|
137
|
+
} catch {
|
|
138
|
+
return { done: "ignored", why: "stdin is not JSON" };
|
|
139
|
+
}
|
|
140
|
+
if (!isObject(doc) || typeof doc.session_id !== "string" || !SESSION_ID_RE.test(doc.session_id)) return { done: "ignored", why: "no session id" };
|
|
141
|
+
const sessionId = doc.session_id.toLowerCase();
|
|
142
|
+
const dir = terminalsDir(i.configDir);
|
|
143
|
+
const localDomain = deps.localDomain !== void 0 ? deps.localDomain : localPidDomain();
|
|
144
|
+
const now = (deps.now ?? Date.now)();
|
|
145
|
+
if (i.event === "start") {
|
|
146
|
+
const entry = i.env.CLAUDE_CODE_ENTRYPOINT;
|
|
147
|
+
if (entry !== "cli" && !(entry === void 0 && i.env.CLAUDE_CODE_SESSION_ATTENDED === "1")) return { done: "ignored", why: `not a terminal session (entrypoint ${entry ?? "unset"})` };
|
|
148
|
+
if (i.env.CLAUDE_CODE_SESSION_ATTENDED === "0") return { done: "ignored", why: "unattended session" };
|
|
149
|
+
const pid2 = claudePid(i.env, i.ppid, deps);
|
|
150
|
+
if (pid2 === null) return { done: "ignored", why: "cannot tell which process is claude" };
|
|
151
|
+
const procStart = processStart(pid2);
|
|
152
|
+
if (procStart === null) return { done: "ignored", why: `cannot read the start time of pid ${pid2}` };
|
|
153
|
+
writeRow(dir, { v: 1, pid: pid2, procStart, sessionId, cwd: typeof doc.cwd === "string" ? doc.cwd.slice(0, 1024) : "", at: now, pidDomain: localDomain });
|
|
154
|
+
prune(dir, localDomain, now);
|
|
155
|
+
return { done: "registered" };
|
|
156
|
+
}
|
|
157
|
+
const pid = claudePid(i.env, i.ppid, deps);
|
|
158
|
+
let removed = false;
|
|
159
|
+
if (pid !== null) {
|
|
160
|
+
const file = path.join(dir, `${pid}.json`);
|
|
161
|
+
try {
|
|
162
|
+
const row = parseRow(JSON.parse(fs2.readFileSync(file, "utf8")));
|
|
163
|
+
if (row?.sessionId === sessionId) {
|
|
164
|
+
fs2.rmSync(file, { force: true });
|
|
165
|
+
removed = true;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
prune(dir, localDomain, now);
|
|
171
|
+
return removed ? { done: "unregistered" } : { done: "ignored", why: "no row to remove" };
|
|
172
|
+
}
|
|
173
|
+
function readStdin(stream, ms = 1500) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
let buf = "";
|
|
176
|
+
const done = () => {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
resolve(buf);
|
|
179
|
+
};
|
|
180
|
+
const timer = setTimeout(done, ms);
|
|
181
|
+
stream.setEncoding?.("utf8");
|
|
182
|
+
stream.on("data", (c) => {
|
|
183
|
+
if (buf.length < MAX_STDIN) buf += c;
|
|
184
|
+
});
|
|
185
|
+
stream.on("end", done);
|
|
186
|
+
stream.on("error", done);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/terminal-hook.ts
|
|
191
|
+
async function main() {
|
|
192
|
+
const [event, flag, dir] = process.argv.slice(2);
|
|
193
|
+
if (event !== "start" && event !== "end" || flag !== "--dir" || !dir) {
|
|
194
|
+
process.stderr.write("usage: terminal-hook start|end --dir <Claude config dir>\n");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const stdin = await readStdin(process.stdin);
|
|
198
|
+
const r = runHook({ event, configDir: dir, stdin, env: process.env, ppid: process.ppid });
|
|
199
|
+
if (process.env.CLAW_CONNECTOR_HOOK_DEBUG) process.stderr.write(`terminal-hook ${event}: ${r.done}${r.why ? ` (${r.why})` : ""}
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
main().then(() => process.exit(0), (e) => {
|
|
203
|
+
process.stderr.write(`terminal-hook: ${e instanceof Error ? e.message : String(e)}
|
|
204
|
+
`);
|
|
205
|
+
process.exit(0);
|
|
206
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@primusclaw/connector",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@primusclaw/connector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Connects the coding agents on your own machine to Claw, over outbound HTTPS only.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/AMD-AGI/PrimusClaw.git",
|
|
9
|
+
"directory": "connector"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/AMD-AGI/PrimusClaw/tree/main/connector#readme",
|
|
12
|
+
"bugs": { "url": "https://github.com/AMD-AGI/PrimusClaw/issues" },
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": { "claw-connector": "dist/cli.js" },
|
|
15
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
16
|
+
"engines": { "node": ">=22" },
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "node scripts/build.mjs",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "tsx --test test/*.test.ts",
|
|
22
|
+
"prepack": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@agentclientprotocol/sdk": "^2.1.0",
|
|
26
|
+
"smol-toml": "^1.9.1",
|
|
27
|
+
"zod": "^4.6.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^26.6.3",
|
|
31
|
+
"esbuild": "^0.28.2",
|
|
32
|
+
"tsx": "^4.23.15",
|
|
33
|
+
"typescript": "^7.0.2"
|
|
34
|
+
}
|
|
35
|
+
}
|