crewdesk 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 +9 -0
- package/README.md +54 -0
- package/apps/server/dist/cli.js +2050 -0
- package/apps/web/dist/assets/index-hoPwNvfD.js +18 -0
- package/apps/web/dist/assets/index-lYVPS1ZE.css +1 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/index.html +14 -0
- package/bin/crewdesk-hook.mjs +8 -0
- package/bin/crewdesk.mjs +23 -0
- package/docs/API.md +36 -0
- package/docs/CONFIGURATION.md +40 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/HOOKS.md +28 -0
- package/docs/INSTALLATION.md +37 -0
- package/docs/SECURITY.md +10 -0
- package/docs/TROUBLESHOOTING.md +11 -0
- package/package.json +75 -0
- package/packages/hook/dist/hook.js +457 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// src/handlers.ts
|
|
2
|
+
import fs3 from "node:fs";
|
|
3
|
+
import path3 from "node:path";
|
|
4
|
+
|
|
5
|
+
// ../shared/src/operations.ts
|
|
6
|
+
function summarizeToolInput(toolName, input) {
|
|
7
|
+
const str = (k) => typeof input[k] === "string" ? input[k] : void 0;
|
|
8
|
+
switch (toolName) {
|
|
9
|
+
case "Bash":
|
|
10
|
+
case "PowerShell":
|
|
11
|
+
return str("command") ?? "";
|
|
12
|
+
case "Write":
|
|
13
|
+
case "Edit":
|
|
14
|
+
case "MultiEdit":
|
|
15
|
+
case "Read":
|
|
16
|
+
case "NotebookEdit":
|
|
17
|
+
return str("file_path") ?? str("notebook_path") ?? "";
|
|
18
|
+
case "WebFetch":
|
|
19
|
+
return str("url") ?? "";
|
|
20
|
+
case "WebSearch":
|
|
21
|
+
return str("query") ?? "";
|
|
22
|
+
case "Glob":
|
|
23
|
+
case "Grep":
|
|
24
|
+
return str("pattern") ?? "";
|
|
25
|
+
default: {
|
|
26
|
+
const json = JSON.stringify(input);
|
|
27
|
+
return json.length > 500 ? json.slice(0, 497) + "..." : json;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
32
|
+
[/\brm\s+(-[a-z]*[rf][a-z]*\s+)+/i, "Recursive or forced file deletion"],
|
|
33
|
+
[/\brm\s+/i, "Deletes files"],
|
|
34
|
+
[/\bRemove-Item\b/i, "Deletes files"],
|
|
35
|
+
[/\b(del|erase|rmdir|rd)\s+/i, "Deletes files"],
|
|
36
|
+
[/\bgit\s+(reset\s+--hard|clean\s+-[a-z]*f|push\s+(.*\s)?(-f|--force)|branch\s+-D|checkout\s+--\s)/i, "Discards git history or changes"],
|
|
37
|
+
[/\b(drop|truncate)\s+(table|database|schema)\b/i, "Drops database objects"],
|
|
38
|
+
[/\bdelete\s+from\b/i, "Deletes database rows"],
|
|
39
|
+
[/\bmkfs\b|\bformat\s+[a-z]:/i, "Formats a filesystem"],
|
|
40
|
+
[/\bdd\s+if=/i, "Raw disk write"],
|
|
41
|
+
[/\bchmod\s+-R\b|\bchown\s+-R\b/i, "Recursive permission change"],
|
|
42
|
+
[/>\s*\/dev\/sd[a-z]/i, "Writes to a block device"],
|
|
43
|
+
[/\b(shutdown|reboot|Stop-Computer|Restart-Computer)\b/i, "Shuts down or restarts the machine"],
|
|
44
|
+
[/\bkill(all)?\s+-9\b|\bStop-Process\b|\btaskkill\b/i, "Kills processes"],
|
|
45
|
+
[/\bnpm\s+publish\b|\bpnpm\s+publish\b/i, "Publishes a package"]
|
|
46
|
+
];
|
|
47
|
+
function assessRisk(toolName, input) {
|
|
48
|
+
if (toolName === "Bash" || toolName === "PowerShell") {
|
|
49
|
+
const cmd = typeof input.command === "string" ? input.command : "";
|
|
50
|
+
for (const [re, reason] of DESTRUCTIVE_PATTERNS) {
|
|
51
|
+
if (re.test(cmd)) return { destructive: true, reason };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { destructive: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/client.ts
|
|
58
|
+
import fs from "node:fs";
|
|
59
|
+
import os from "node:os";
|
|
60
|
+
import path from "node:path";
|
|
61
|
+
var RETRY_DELAYS_MS = [100, 500, 1e3, 2e3];
|
|
62
|
+
function hubHome() {
|
|
63
|
+
return process.env.CREWDESK_HOME ?? path.join(os.homedir(), ".crewdesk");
|
|
64
|
+
}
|
|
65
|
+
function hubUrl() {
|
|
66
|
+
if (process.env.CREWDESK_URL) return process.env.CREWDESK_URL.replace(/\/+$/, "");
|
|
67
|
+
let host = "127.0.0.1";
|
|
68
|
+
let port = 7777;
|
|
69
|
+
try {
|
|
70
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(hubHome(), "config.json"), "utf8"));
|
|
71
|
+
if (typeof cfg.host === "string") host = cfg.host;
|
|
72
|
+
if (typeof cfg.port === "number") port = cfg.port;
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
if (process.env.CREWDESK_PORT) port = Number(process.env.CREWDESK_PORT);
|
|
76
|
+
if (host === "0.0.0.0") host = "127.0.0.1";
|
|
77
|
+
return `http://${host}:${port}`;
|
|
78
|
+
}
|
|
79
|
+
var HubUnavailable = class extends Error {
|
|
80
|
+
};
|
|
81
|
+
var HubHttpError = class extends Error {
|
|
82
|
+
constructor(status, body) {
|
|
83
|
+
super(`hub responded ${status}: ${JSON.stringify(body)}`);
|
|
84
|
+
this.status = status;
|
|
85
|
+
this.body = body;
|
|
86
|
+
}
|
|
87
|
+
status;
|
|
88
|
+
body;
|
|
89
|
+
};
|
|
90
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
91
|
+
async function request(method, url, body, opts = {}) {
|
|
92
|
+
const retries = opts.retries ?? RETRY_DELAYS_MS;
|
|
93
|
+
let lastErr;
|
|
94
|
+
for (let attempt = 0; attempt <= retries.length; attempt++) {
|
|
95
|
+
if (attempt > 0) await sleep(retries[attempt - 1]);
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(hubUrl() + url, {
|
|
98
|
+
method,
|
|
99
|
+
headers: body !== void 0 ? { "content-type": "application/json" } : {},
|
|
100
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
101
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
|
|
102
|
+
});
|
|
103
|
+
const text = await res.text();
|
|
104
|
+
const json = text ? JSON.parse(text) : void 0;
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
if (res.status >= 500) {
|
|
107
|
+
lastErr = new HubHttpError(res.status, json);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
throw new HubHttpError(res.status, json);
|
|
111
|
+
}
|
|
112
|
+
return json;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err instanceof HubHttpError && err.status < 500) throw err;
|
|
115
|
+
lastErr = err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw new HubUnavailable(`hub unavailable at ${hubUrl()}: ${lastErr?.message ?? lastErr}`);
|
|
119
|
+
}
|
|
120
|
+
function debugLog(msg) {
|
|
121
|
+
if (!process.env.CREWDESK_HOOK_DEBUG) return;
|
|
122
|
+
try {
|
|
123
|
+
const file = path.join(hubHome(), "logs", "hook.log");
|
|
124
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
125
|
+
fs.appendFileSync(file, `${(/* @__PURE__ */ new Date()).toISOString()} [${process.pid}] ${msg}
|
|
126
|
+
`);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/session.ts
|
|
132
|
+
import { execFileSync } from "node:child_process";
|
|
133
|
+
import fs2 from "node:fs";
|
|
134
|
+
import path2 from "node:path";
|
|
135
|
+
function sessionCacheFile(sessionId) {
|
|
136
|
+
return path2.join(hubHome(), "sessions", `${sessionId.replace(/[^A-Za-z0-9_.-]/g, "_")}.json`);
|
|
137
|
+
}
|
|
138
|
+
function readSessionCache(sessionId) {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(fs2.readFileSync(sessionCacheFile(sessionId), "utf8"));
|
|
141
|
+
} catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function writeSessionCache(sessionId, data) {
|
|
146
|
+
try {
|
|
147
|
+
const file = sessionCacheFile(sessionId);
|
|
148
|
+
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
149
|
+
fs2.writeFileSync(file, JSON.stringify(data));
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function removeSessionCache(sessionId) {
|
|
154
|
+
try {
|
|
155
|
+
fs2.rmSync(sessionCacheFile(sessionId), { force: true });
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function agentMeta(input, pid) {
|
|
160
|
+
const cwd = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
|
|
161
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
162
|
+
return {
|
|
163
|
+
name: process.env.CREWDESK_AGENT_NAME || void 0,
|
|
164
|
+
project: process.env.CREWDESK_PROJECT || path2.basename(cwd),
|
|
165
|
+
cwd,
|
|
166
|
+
pid: pid ?? readSessionCache(input.session_id).pid,
|
|
167
|
+
account: configDir ? path2.basename(configDir) : void 0,
|
|
168
|
+
source: "hook"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function processTable() {
|
|
172
|
+
const table = /* @__PURE__ */ new Map();
|
|
173
|
+
if (process.platform === "win32") {
|
|
174
|
+
const out = execFileSync(
|
|
175
|
+
"powershell.exe",
|
|
176
|
+
[
|
|
177
|
+
"-NoProfile",
|
|
178
|
+
"-NonInteractive",
|
|
179
|
+
"-Command",
|
|
180
|
+
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId)`t$($_.ParentProcessId)`t$($_.Name)" }'
|
|
181
|
+
],
|
|
182
|
+
{ encoding: "utf8", timeout: 8e3, windowsHide: true }
|
|
183
|
+
);
|
|
184
|
+
for (const line of out.split(/\r?\n/)) {
|
|
185
|
+
const [pid, ppid, name] = line.split(" ");
|
|
186
|
+
if (pid && ppid) table.set(Number(pid), { pid: Number(pid), ppid: Number(ppid), name: name ?? "" });
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const out = execFileSync("ps", ["-A", "-o", "pid=,ppid=,args="], { encoding: "utf8", timeout: 5e3 });
|
|
190
|
+
for (const line of out.split("\n")) {
|
|
191
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)/);
|
|
192
|
+
if (m) table.set(Number(m[1]), { pid: Number(m[1]), ppid: Number(m[2]), name: path2.basename(m[3]) });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return table;
|
|
196
|
+
}
|
|
197
|
+
function findClaudePid() {
|
|
198
|
+
if (process.env.CREWDESK_CLAUDE_PID) return Number(process.env.CREWDESK_CLAUDE_PID) || void 0;
|
|
199
|
+
if (process.env.CLAUDE_PID && Number(process.env.CLAUDE_PID) > 0) return Number(process.env.CLAUDE_PID);
|
|
200
|
+
try {
|
|
201
|
+
const table = processTable();
|
|
202
|
+
let cur = table.get(process.pid);
|
|
203
|
+
const seen = /* @__PURE__ */ new Set();
|
|
204
|
+
let nodeCandidate;
|
|
205
|
+
while (cur && !seen.has(cur.pid)) {
|
|
206
|
+
seen.add(cur.pid);
|
|
207
|
+
const name = cur.name.toLowerCase();
|
|
208
|
+
if (cur.pid !== process.pid) {
|
|
209
|
+
if (/^claude(\.exe)?$/.test(name)) return cur.pid;
|
|
210
|
+
if (!nodeCandidate && /^(node|bun)(\.exe)?$/.test(name)) nodeCandidate = cur.pid;
|
|
211
|
+
}
|
|
212
|
+
cur = table.get(cur.ppid);
|
|
213
|
+
}
|
|
214
|
+
return nodeCandidate;
|
|
215
|
+
} catch {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/handlers.ts
|
|
221
|
+
function hookSettings() {
|
|
222
|
+
let cfg = {};
|
|
223
|
+
try {
|
|
224
|
+
cfg = JSON.parse(fs3.readFileSync(path3.join(hubHome(), "config.json"), "utf8"));
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
const num = (env, key, dflt) => {
|
|
228
|
+
const v = process.env[env] ?? cfg[key];
|
|
229
|
+
const n = Number(v);
|
|
230
|
+
return v !== void 0 && v !== "" && Number.isFinite(n) && n >= 0 ? n : dflt;
|
|
231
|
+
};
|
|
232
|
+
const idleEnv = process.env.CREWDESK_IDLE_PROMPTS;
|
|
233
|
+
return {
|
|
234
|
+
questionWaitSeconds: num("CREWDESK_QUESTION_WAIT", "questionWaitSeconds", 600),
|
|
235
|
+
idleWaitSeconds: num("CREWDESK_IDLE_WAIT", "idleWaitSeconds", 1800),
|
|
236
|
+
permissionWaitSeconds: num("CREWDESK_PERMISSION_WAIT", "permissionWaitSeconds", 43200),
|
|
237
|
+
idlePrompts: idleEnv !== void 0 && idleEnv !== "" ? /^(1|true|yes|on)$/i.test(idleEnv) : cfg.idlePrompts !== false
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function createEvent(input, body) {
|
|
241
|
+
const res = await request("POST", "/api/events", {
|
|
242
|
+
sessionId: input.session_id,
|
|
243
|
+
agent: agentMeta(input),
|
|
244
|
+
...body
|
|
245
|
+
});
|
|
246
|
+
return res.event;
|
|
247
|
+
}
|
|
248
|
+
async function waitForDecision(ev, sessionId, maxWaitMs) {
|
|
249
|
+
const deadline = Date.now() + maxWaitMs;
|
|
250
|
+
let failures = 0;
|
|
251
|
+
while (Date.now() < deadline) {
|
|
252
|
+
const pollMs = Math.max(0, Math.min(25e3, deadline - Date.now()));
|
|
253
|
+
try {
|
|
254
|
+
const r = await request(
|
|
255
|
+
"GET",
|
|
256
|
+
`/api/events/${ev.id}/wait?sessionId=${encodeURIComponent(sessionId)}&timeout=${pollMs}`,
|
|
257
|
+
void 0,
|
|
258
|
+
{ timeoutMs: pollMs + 1e4, retries: [] }
|
|
259
|
+
);
|
|
260
|
+
failures = 0;
|
|
261
|
+
if (r.state === "resolved") return r.response;
|
|
262
|
+
if (r.state === "cancelled") return null;
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err instanceof HubHttpError) return null;
|
|
265
|
+
failures++;
|
|
266
|
+
if (failures > 30) return null;
|
|
267
|
+
await sleep(Math.min(2e3, 100 * 2 ** failures));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await request("POST", `/api/events/${ev.id}/cancel`, { sessionId, reason: "Timed out waiting; continuing in the terminal." }, { retries: [] }).catch(() => {
|
|
271
|
+
});
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
async function onSessionStart(input) {
|
|
275
|
+
const pid = findClaudePid();
|
|
276
|
+
writeSessionCache(input.session_id, { pid });
|
|
277
|
+
const meta = agentMeta(input, pid);
|
|
278
|
+
await request("POST", "/api/agents/register", { sessionId: input.session_id, ...meta });
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
async function onSessionEnd(input) {
|
|
282
|
+
removeSessionCache(input.session_id);
|
|
283
|
+
await request(
|
|
284
|
+
"POST",
|
|
285
|
+
"/api/events",
|
|
286
|
+
{ sessionId: input.session_id, type: "session.stopped", message: `Session ended (${input.reason ?? "exit"})`, agent: agentMeta(input) },
|
|
287
|
+
{ retries: [], timeoutMs: 1e3 }
|
|
288
|
+
);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
async function onPromptSubmit(input) {
|
|
292
|
+
await createEvent(input, { type: "task.started", message: String(input.prompt ?? "").slice(0, 2e3) });
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
var QUIET_NOTIFICATIONS = /* @__PURE__ */ new Set(["permission_prompt", "idle_prompt"]);
|
|
296
|
+
async function onNotification(input) {
|
|
297
|
+
const notificationType = String(input.notification_type ?? input.type ?? "");
|
|
298
|
+
await createEvent(input, {
|
|
299
|
+
type: "notification.created",
|
|
300
|
+
message: String(input.message ?? "Claude needs your attention"),
|
|
301
|
+
// Permission/idle notifications duplicate requests the hub already shows; log them silently.
|
|
302
|
+
payload: { notificationType, silent: QUIET_NOTIFICATIONS.has(notificationType) }
|
|
303
|
+
});
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
async function onPermission(input) {
|
|
307
|
+
const toolName = String(input.tool_name ?? "");
|
|
308
|
+
const toolInput = input.tool_input ?? {};
|
|
309
|
+
if (toolName === "AskUserQuestion") return null;
|
|
310
|
+
const summary = summarizeToolInput(toolName, toolInput);
|
|
311
|
+
const risk = assessRisk(toolName, toolInput);
|
|
312
|
+
const ev = await createEvent(input, {
|
|
313
|
+
type: "permission.created",
|
|
314
|
+
message: `${toolName}: ${summary}`.slice(0, 2e3),
|
|
315
|
+
payload: {
|
|
316
|
+
toolName,
|
|
317
|
+
toolInput,
|
|
318
|
+
summary,
|
|
319
|
+
cwd: input.cwd,
|
|
320
|
+
destructive: risk.destructive,
|
|
321
|
+
destructiveReason: risk.reason,
|
|
322
|
+
permissionMode: input.permission_mode,
|
|
323
|
+
transport: "hook"
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
const response = ev.response && (ev.status === "PROCESSING" || ev.status === "RESOLVED") ? ev.response : await waitForDecision(ev, input.session_id, hookSettings().permissionWaitSeconds * 1e3);
|
|
327
|
+
if (!response) return null;
|
|
328
|
+
if (response.action === "allow") {
|
|
329
|
+
return { hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow" } } };
|
|
330
|
+
}
|
|
331
|
+
if (response.action === "deny") {
|
|
332
|
+
const via = response.source === "policy" ? "a Crewdesk policy" : "Crewdesk";
|
|
333
|
+
return {
|
|
334
|
+
hookSpecificOutput: {
|
|
335
|
+
hookEventName: "PermissionRequest",
|
|
336
|
+
decision: { behavior: "deny", message: response.response ? `Denied by ${via}: ${response.response}` : `Denied by ${via}.` }
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function buildAnswers(questions, response) {
|
|
343
|
+
const answers = {};
|
|
344
|
+
for (const q of questions) {
|
|
345
|
+
const a = response.answers?.[q.question] ?? response.response;
|
|
346
|
+
if (a) answers[q.question] = a;
|
|
347
|
+
}
|
|
348
|
+
return answers;
|
|
349
|
+
}
|
|
350
|
+
async function onQuestion(input) {
|
|
351
|
+
if (input.tool_name !== "AskUserQuestion") return null;
|
|
352
|
+
const toolInput = input.tool_input ?? {};
|
|
353
|
+
const questions = Array.isArray(toolInput.questions) ? toolInput.questions : [];
|
|
354
|
+
if (questions.length === 0) return null;
|
|
355
|
+
const ev = await createEvent(input, {
|
|
356
|
+
type: "question.created",
|
|
357
|
+
message: questions.map((q) => q.question).join("\n"),
|
|
358
|
+
payload: { questions, transport: "hook" }
|
|
359
|
+
});
|
|
360
|
+
const response = await waitForDecision(ev, input.session_id, hookSettings().questionWaitSeconds * 1e3);
|
|
361
|
+
if (!response || response.action !== "answer") return null;
|
|
362
|
+
const answers = buildAnswers(questions, response);
|
|
363
|
+
if (Object.keys(answers).length === 0) return null;
|
|
364
|
+
return {
|
|
365
|
+
hookSpecificOutput: {
|
|
366
|
+
hookEventName: "PreToolUse",
|
|
367
|
+
permissionDecision: "allow",
|
|
368
|
+
permissionDecisionReason: "Answered from Crewdesk",
|
|
369
|
+
updatedInput: { ...toolInput, answers }
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
async function onStop(input) {
|
|
374
|
+
const settings = hookSettings();
|
|
375
|
+
const last = typeof input.last_assistant_message === "string" ? input.last_assistant_message : "";
|
|
376
|
+
const message = last.length > 4e3 ? last.slice(0, 3997) + "..." : last;
|
|
377
|
+
await createEvent(input, { type: "task.completed", message: message.slice(0, 500) || "Turn finished" });
|
|
378
|
+
if (!settings.idlePrompts || settings.idleWaitSeconds <= 0) return null;
|
|
379
|
+
const ev = await createEvent(input, {
|
|
380
|
+
type: "prompt.created",
|
|
381
|
+
message: message || "Claude finished and is waiting for your next instruction.",
|
|
382
|
+
payload: { lastMessage: message, stopHookActive: Boolean(input.stop_hook_active), transport: "hook" }
|
|
383
|
+
});
|
|
384
|
+
const response = await waitForDecision(ev, input.session_id, settings.idleWaitSeconds * 1e3);
|
|
385
|
+
if (!response || response.action !== "prompt" || !response.response?.trim()) return null;
|
|
386
|
+
return { decision: "block", reason: response.response };
|
|
387
|
+
}
|
|
388
|
+
var HANDLERS = {
|
|
389
|
+
register: onSessionStart,
|
|
390
|
+
"session-start": onSessionStart,
|
|
391
|
+
"session-end": onSessionEnd,
|
|
392
|
+
end: onSessionEnd,
|
|
393
|
+
prompt: onPromptSubmit,
|
|
394
|
+
notification: onNotification,
|
|
395
|
+
permission: onPermission,
|
|
396
|
+
question: onQuestion,
|
|
397
|
+
stop: onStop
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// src/cli.ts
|
|
401
|
+
var USAGE = `crewdesk-hook <command>
|
|
402
|
+
|
|
403
|
+
Claude Code hook bridge for Crewdesk. Reads the hook JSON from stdin.
|
|
404
|
+
|
|
405
|
+
Commands:
|
|
406
|
+
register SessionStart: register the session with the hub
|
|
407
|
+
permission PermissionRequest: forward and wait for Allow/Deny
|
|
408
|
+
question PreToolUse(AskUserQuestion): answer from the dashboard
|
|
409
|
+
notification Notification: forward notifications
|
|
410
|
+
prompt UserPromptSubmit: mark the session as working
|
|
411
|
+
stop Stop: report completion and wait for a follow-up prompt
|
|
412
|
+
session-end SessionEnd: mark the session offline
|
|
413
|
+
|
|
414
|
+
Environment:
|
|
415
|
+
CREWDESK_URL hub base URL (default http://127.0.0.1:7777)
|
|
416
|
+
CREWDESK_AGENT_NAME display name for this session
|
|
417
|
+
CREWDESK_HOOK_DEBUG log to ~/.crewdesk/logs/hook.log
|
|
418
|
+
`;
|
|
419
|
+
async function readStdin() {
|
|
420
|
+
if (process.stdin.isTTY) return "";
|
|
421
|
+
const chunks = [];
|
|
422
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
423
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
424
|
+
}
|
|
425
|
+
async function main() {
|
|
426
|
+
const cmd = process.argv[2];
|
|
427
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
428
|
+
process.stdout.write(USAGE);
|
|
429
|
+
return cmd ? 0 : 1;
|
|
430
|
+
}
|
|
431
|
+
const handler = HANDLERS[cmd];
|
|
432
|
+
if (!handler) {
|
|
433
|
+
process.stderr.write(`crewdesk-hook: unknown command "${cmd}"
|
|
434
|
+
`);
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
let input;
|
|
438
|
+
try {
|
|
439
|
+
input = JSON.parse(await readStdin());
|
|
440
|
+
if (!input || typeof input.session_id !== "string") throw new Error("missing session_id");
|
|
441
|
+
} catch (err) {
|
|
442
|
+
debugLog(`${cmd}: bad stdin: ${err.message}`);
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const out = await handler(input);
|
|
447
|
+
debugLog(`${cmd}: ${out ? JSON.stringify(out) : "no output"}`);
|
|
448
|
+
if (out) await new Promise((r) => process.stdout.write(JSON.stringify(out), () => r()));
|
|
449
|
+
} catch (err) {
|
|
450
|
+
debugLog(`${cmd}: ${err.message}`);
|
|
451
|
+
}
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
main().then(
|
|
455
|
+
(code) => process.exit(code),
|
|
456
|
+
() => process.exit(0)
|
|
457
|
+
);
|