clawborrator-mcp 0.0.1 → 0.0.3
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/dist/hook-entry.d.ts +1 -0
- package/dist/hook-entry.js +23 -0
- package/dist/hook-entry.js.map +1 -0
- package/dist/index.js +49 -3
- package/dist/index.js.map +1 -1
- package/dist/install-hooks.d.ts +13 -0
- package/dist/install-hooks.js +139 -0
- package/dist/install-hooks.js.map +1 -0
- package/dist-hook/clawborrator-tail.mjs +369 -0
- package/package.json +9 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Standalone bundled hook entry. esbuild rolls this + everything it
|
|
2
|
+
// transitively imports (sidecar, transcript, log, hook) into a single
|
|
3
|
+
// dist-hook/clawborrator-tail.mjs that gets copied into a project's
|
|
4
|
+
// .claude/hooks/ at `claw session init` time.
|
|
5
|
+
//
|
|
6
|
+
// Hooks then run as plain `node .claude/hooks/clawborrator-tail.mjs
|
|
7
|
+
// <HookName>` — no npx, no registry lookup, no install. Same shape
|
|
8
|
+
// as the original clawborrator-channel package.
|
|
9
|
+
//
|
|
10
|
+
// Usage from .claude/settings.json:
|
|
11
|
+
// { "type": "command", "command": "node \".claude/hooks/clawborrator-tail.mjs\" PreToolUse" }
|
|
12
|
+
import { runHook } from './hook.js';
|
|
13
|
+
import { log } from './log.js';
|
|
14
|
+
const hookName = process.argv[2];
|
|
15
|
+
if (!hookName) {
|
|
16
|
+
log.error('hook entry invoked without a hook name argument');
|
|
17
|
+
process.exit(0); // never fail Claude's hook chain
|
|
18
|
+
}
|
|
19
|
+
runHook(hookName).catch((err) => {
|
|
20
|
+
log.error('hook fatal', { error: String(err) });
|
|
21
|
+
process.exit(0);
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=hook-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-entry.js","sourceRoot":"","sources":["../src/hook-entry.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,sEAAsE;AACtE,oEAAoE;AACpE,8CAA8C;AAC9C,EAAE;AACF,oEAAoE;AACpE,mEAAmE;AACnE,gDAAgD;AAChD,EAAE;AACF,oCAAoC;AACpC,gGAAgG;AAEhG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjC,IAAI,CAAC,QAAQ,EAAE,CAAC;IACd,GAAG,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;IAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAE,iCAAiC;AACrD,CAAC;AAED,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACvC,GAAG,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
// gets an empty tools/list response and the WS work proceeds in the
|
|
12
12
|
// background.
|
|
13
13
|
import { hostname } from 'node:os';
|
|
14
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { resolve, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
14
17
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
15
18
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
19
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -21,9 +24,12 @@ import { runHook } from './hook.js';
|
|
|
21
24
|
import { writeSidecar, deleteSidecar } from './sidecar.js';
|
|
22
25
|
import { TOOL_DEFINITIONS, callTool } from './tools/index.js';
|
|
23
26
|
import { enqueueRoutedPrompt } from './inbox.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// the long-lived MCP
|
|
27
|
+
import { installHooks } from './install-hooks.js';
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
// Dispatch on top-level flags BEFORE booting the long-lived MCP.
|
|
30
|
+
// --hook=<HookName> short-lived hook spawn (fast path)
|
|
31
|
+
// --print-hook-file stream the bundled dist-hook/.mjs to stdout
|
|
32
|
+
// (none) long-lived stdio MCP server
|
|
27
33
|
const hookFlag = process.argv.find((a) => a.startsWith('--hook='));
|
|
28
34
|
if (hookFlag) {
|
|
29
35
|
const name = hookFlag.slice('--hook='.length);
|
|
@@ -32,6 +38,20 @@ if (hookFlag) {
|
|
|
32
38
|
process.exit(0); // never fail the operator's actual Claude flow
|
|
33
39
|
});
|
|
34
40
|
}
|
|
41
|
+
else if (process.argv.includes('--print-hook-file')) {
|
|
42
|
+
// Used by `claw session init` to materialize a local copy of the
|
|
43
|
+
// bundled hook entry into the project's .claude/hooks/ folder.
|
|
44
|
+
// dist-hook/clawborrator-tail.mjs is built by esbuild at publish
|
|
45
|
+
// time and shipped in the package's `files` allow-list.
|
|
46
|
+
// Compiled __dirname is dist/, so the bundle is at ../dist-hook.
|
|
47
|
+
const candidate = resolve(__dirname, '..', 'dist-hook', 'clawborrator-tail.mjs');
|
|
48
|
+
if (!existsSync(candidate)) {
|
|
49
|
+
process.stderr.write(`[clawborrator-mcp] bundled hook file missing at ${candidate}\n`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(readFileSync(candidate, 'utf8'));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
35
55
|
else {
|
|
36
56
|
main().catch((err) => {
|
|
37
57
|
log.error('fatal', { error: String(err), stack: err?.stack });
|
|
@@ -50,6 +70,32 @@ async function main() {
|
|
|
50
70
|
log.info('clawborrator-mcp starting', { hubUrl: config.hubUrl });
|
|
51
71
|
const cwd = process.cwd();
|
|
52
72
|
const host = hostname();
|
|
73
|
+
// Self-install / refresh project hooks in .claude/. Idempotent —
|
|
74
|
+
// touches files only when the desired state diverges from disk, so
|
|
75
|
+
// re-runs of `claude` cost nothing. Operators no longer need to
|
|
76
|
+
// run a separate `init` command — dropping a .mcp.json with a
|
|
77
|
+
// valid channel token is the only setup step.
|
|
78
|
+
try {
|
|
79
|
+
const r = installHooks(cwd);
|
|
80
|
+
if (r.hookFileWritten || r.settingsWritten) {
|
|
81
|
+
log.info('hooks installed', {
|
|
82
|
+
hookFile: r.hookFileWritten,
|
|
83
|
+
settings: r.settingsWritten,
|
|
84
|
+
added: r.added,
|
|
85
|
+
refreshed: r.refreshed,
|
|
86
|
+
alreadyOk: r.alreadyOk,
|
|
87
|
+
path: r.hookFilePath,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
log.debug('hooks already up-to-date', { added: r.added, refreshed: r.refreshed, alreadyOk: r.alreadyOk });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
// Don't fail boot — operator can still drive Claude; just log so
|
|
96
|
+
// they see hooks aren't capturing.
|
|
97
|
+
log.warn('install-hooks failed', { error: e?.message ?? String(e) });
|
|
98
|
+
}
|
|
53
99
|
// Open the channel-side WS to hub.
|
|
54
100
|
const client = new ChannelClient(config, {
|
|
55
101
|
onWelcome: (m) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,qDAAqD;AACrD,yEAAyE;AACzE,sCAAsC;AACtC,yEAAyE;AACzE,mCAAmC;AACnC,EAAE;AACF,mEAAmE;AACnE,iEAAiE;AACjE,kEAAkE;AAClE,oEAAoE;AACpE,cAAc;AAEd,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AACnG,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install/maintain .claude/settings.json + .claude/hooks/clawborrator-
|
|
3
|
+
* tail.mjs for the project at `cwd`. Returns a summary describing what
|
|
4
|
+
* changed; useful for boot-time logging.
|
|
5
|
+
*/
|
|
6
|
+
export declare function installHooks(cwd: string): {
|
|
7
|
+
hookFileWritten: boolean;
|
|
8
|
+
settingsWritten: boolean;
|
|
9
|
+
added: number;
|
|
10
|
+
refreshed: number;
|
|
11
|
+
alreadyOk: number;
|
|
12
|
+
hookFilePath: string;
|
|
13
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Self-installer for Claude Code hook config + the bundled hook
|
|
2
|
+
// entry. Called once per long-lived MCP startup. Idempotent —
|
|
3
|
+
// touches files only when the desired state differs from what's on
|
|
4
|
+
// disk, so re-launches don't churn mtimes or git diffs.
|
|
5
|
+
//
|
|
6
|
+
// Two artifacts maintained:
|
|
7
|
+
// 1. <cwd>/.claude/hooks/clawborrator-tail.mjs — bytewise copy of
|
|
8
|
+
// this package's bundled dist-hook/clawborrator-tail.mjs. Hooks
|
|
9
|
+
// run `node` against this path so per-hook latency is just
|
|
10
|
+
// Node startup, not an npx + registry round-trip.
|
|
11
|
+
// 2. <cwd>/.claude/settings.json — 12 hook entries (one per
|
|
12
|
+
// Claude-Code hook name), each invoking the local .mjs above.
|
|
13
|
+
// We tag every entry with a #clawborrator-hook sentinel so we
|
|
14
|
+
// can detect + refresh our own entries without disturbing
|
|
15
|
+
// anything else the operator added.
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
|
|
17
|
+
import { resolve, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { log } from './log.js';
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const HOOK_NAMES = [
|
|
22
|
+
'SessionStart',
|
|
23
|
+
'SessionEnd',
|
|
24
|
+
'UserPromptSubmit',
|
|
25
|
+
'PreToolUse',
|
|
26
|
+
'PostToolUse',
|
|
27
|
+
'PostToolUseFailure',
|
|
28
|
+
'TaskCreated',
|
|
29
|
+
'SubagentStart',
|
|
30
|
+
'SubagentStop',
|
|
31
|
+
'TaskCompleted',
|
|
32
|
+
'Stop',
|
|
33
|
+
'Notification',
|
|
34
|
+
];
|
|
35
|
+
const TAG = 'clawborrator-hook';
|
|
36
|
+
function isOurHook(h) {
|
|
37
|
+
return h.type === 'command' && h.command.includes(`#${TAG}`);
|
|
38
|
+
}
|
|
39
|
+
function hookCommand(name) {
|
|
40
|
+
// Hooks run with cwd = project root. Quoting the path keeps Windows
|
|
41
|
+
// paths with spaces working.
|
|
42
|
+
return `node ".claude/hooks/clawborrator-tail.mjs" ${name} #${TAG}`;
|
|
43
|
+
}
|
|
44
|
+
// Find the bundled dist-hook/clawborrator-tail.mjs that ships next
|
|
45
|
+
// to this compiled module. Compiled __dirname is .../dist or
|
|
46
|
+
// .../dist-bundled depending on the build flavor; the bundle lives
|
|
47
|
+
// alongside at .../dist-hook/.
|
|
48
|
+
function findBundledHookFile() {
|
|
49
|
+
const candidates = [
|
|
50
|
+
resolve(__dirname, '..', 'dist-hook', 'clawborrator-tail.mjs'),
|
|
51
|
+
];
|
|
52
|
+
for (const c of candidates) {
|
|
53
|
+
if (existsSync(c))
|
|
54
|
+
return c;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Install/maintain .claude/settings.json + .claude/hooks/clawborrator-
|
|
60
|
+
* tail.mjs for the project at `cwd`. Returns a summary describing what
|
|
61
|
+
* changed; useful for boot-time logging.
|
|
62
|
+
*/
|
|
63
|
+
export function installHooks(cwd) {
|
|
64
|
+
const claudeDir = resolve(cwd, '.claude');
|
|
65
|
+
const hooksDir = resolve(claudeDir, 'hooks');
|
|
66
|
+
const hookFile = resolve(hooksDir, 'clawborrator-tail.mjs');
|
|
67
|
+
const settings = resolve(claudeDir, 'settings.json');
|
|
68
|
+
const bundleSource = findBundledHookFile();
|
|
69
|
+
if (!bundleSource) {
|
|
70
|
+
log.warn('install-hooks: bundled hook file not found in this package; skipping');
|
|
71
|
+
return { hookFileWritten: false, settingsWritten: false, added: 0, refreshed: 0, alreadyOk: 0, hookFilePath: hookFile };
|
|
72
|
+
}
|
|
73
|
+
if (!existsSync(claudeDir))
|
|
74
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
75
|
+
if (!existsSync(hooksDir))
|
|
76
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
77
|
+
// 1) Copy the bundled .mjs only if its bytes differ from what's
|
|
78
|
+
// already on disk. Compare exact bytes — cheaper than hashing
|
|
79
|
+
// and good enough for files this small.
|
|
80
|
+
const bundleBytes = readFileSync(bundleSource);
|
|
81
|
+
let hookFileWritten = false;
|
|
82
|
+
if (!existsSync(hookFile) || !readFileSync(hookFile).equals(bundleBytes)) {
|
|
83
|
+
writeFileSync(hookFile, bundleBytes);
|
|
84
|
+
try {
|
|
85
|
+
chmodSync(hookFile, 0o755);
|
|
86
|
+
}
|
|
87
|
+
catch { /* Windows */ }
|
|
88
|
+
hookFileWritten = true;
|
|
89
|
+
}
|
|
90
|
+
// 2) Read existing settings.json (tolerate missing/empty).
|
|
91
|
+
let s = {};
|
|
92
|
+
let originalText = '';
|
|
93
|
+
if (existsSync(settings)) {
|
|
94
|
+
try {
|
|
95
|
+
originalText = readFileSync(settings, 'utf8');
|
|
96
|
+
if (originalText.trim())
|
|
97
|
+
s = JSON.parse(originalText);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
log.warn('install-hooks: settings.json unparseable, leaving alone', { error: e?.message ?? String(e) });
|
|
101
|
+
return { hookFileWritten, settingsWritten: false, added: 0, refreshed: 0, alreadyOk: 0, hookFilePath: hookFile };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!s.hooks)
|
|
105
|
+
s.hooks = {};
|
|
106
|
+
let added = 0, refreshed = 0, alreadyOk = 0;
|
|
107
|
+
for (const name of HOOK_NAMES) {
|
|
108
|
+
const arr = (s.hooks[name] ??= []);
|
|
109
|
+
let entry = arr.find((e) => (e.matcher ?? '.*') === '.*');
|
|
110
|
+
if (!entry) {
|
|
111
|
+
entry = { matcher: '.*', hooks: [] };
|
|
112
|
+
arr.push(entry);
|
|
113
|
+
}
|
|
114
|
+
const desiredCmd = hookCommand(name);
|
|
115
|
+
const existingIdx = entry.hooks.findIndex(isOurHook);
|
|
116
|
+
if (existingIdx >= 0) {
|
|
117
|
+
if (entry.hooks[existingIdx].command !== desiredCmd) {
|
|
118
|
+
entry.hooks[existingIdx] = { type: 'command', command: desiredCmd };
|
|
119
|
+
refreshed++;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
alreadyOk++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
entry.hooks.push({ type: 'command', command: desiredCmd });
|
|
127
|
+
added++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 3) Write only if the serialized form changed.
|
|
131
|
+
const newText = JSON.stringify(s, null, 2) + '\n';
|
|
132
|
+
let settingsWritten = false;
|
|
133
|
+
if (newText !== originalText) {
|
|
134
|
+
writeFileSync(settings, newText);
|
|
135
|
+
settingsWritten = true;
|
|
136
|
+
}
|
|
137
|
+
return { hookFileWritten, settingsWritten, added, refreshed, alreadyOk, hookFilePath: hookFile };
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=install-hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-hooks.js","sourceRoot":"","sources":["../src/install-hooks.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,8DAA8D;AAC9D,mEAAmE;AACnE,wDAAwD;AACxD,EAAE;AACF,4BAA4B;AAC5B,oEAAoE;AACpE,qEAAqE;AACrE,gEAAgE;AAChE,uDAAuD;AACvD,8DAA8D;AAC9D,mEAAmE;AACnE,mEAAmE;AACnE,+DAA+D;AAC/D,yCAAyC;AAEzC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,UAAU,GAAG;IACjB,cAAc;IACd,YAAY;IACZ,kBAAkB;IAClB,YAAY;IACZ,aAAa;IACb,oBAAoB;IACpB,aAAa;IACb,eAAe;IACf,cAAc;IACd,eAAe;IACf,MAAM;IACN,cAAc;CACN,CAAC;AAEX,MAAM,GAAG,GAAG,mBAAmB,CAAC;AAWhC,SAAS,SAAS,CAAC,CAAuC;IACxD,OAAO,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,oEAAoE;IACpE,6BAA6B;IAC7B,OAAO,8CAA8C,IAAI,KAAK,GAAG,EAAE,CAAC;AACtE,CAAC;AAED,mEAAmE;AACnE,6DAA6D;AAC7D,mEAAmE;AACnE,+BAA+B;AAC/B,SAAS,mBAAmB;IAC1B,MAAM,UAAU,GAAG;QACjB,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,uBAAuB,CAAC;KAC/D,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,UAAU,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IAQtC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAI,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAI,OAAO,CAAC,QAAQ,EAAE,uBAAuB,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAI,OAAO,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAEtD,MAAM,YAAY,GAAG,mBAAmB,EAAE,CAAC;IAC3C,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;QACjF,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;IAC1H,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtE,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAG,SAAS,CAAC,QAAQ,EAAG,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtE,gEAAgE;IAChE,iEAAiE;IACjE,2CAA2C;IAC3C,MAAM,WAAW,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAC/C,IAAI,eAAe,GAAG,KAAK,CAAC;IAC5B,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;QACzE,aAAa,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACrC,IAAI,CAAC;YAAC,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,aAAa,CAAC,CAAC;QAC3D,eAAe,GAAG,IAAI,CAAC;IACzB,CAAC;IAED,2DAA2D;IAC3D,IAAI,CAAC,GAAkB,EAAE,CAAC;IAC1B,IAAI,YAAY,GAAG,EAAE,CAAC;IACtB,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,YAAY,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC9C,IAAI,YAAY,CAAC,IAAI,EAAE;gBAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,GAAG,CAAC,IAAI,CAAC,yDAAyD,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACxG,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;QACnH,CAAC;IACH,CAAC;IACD,IAAI,CAAC,CAAC,CAAC,KAAK;QAAE,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;IAE3B,IAAI,KAAK,GAAG,CAAC,EAAE,SAAS,GAAG,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAiB,CAAC,CAAC,CAAC,KAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAClD,IAAI,KAAK,GAA2B,GAAG,CAAC,IAAI,CAAC,CAAC,CAAa,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;QAC9F,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YACrC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;YACrB,IAAI,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;gBACpD,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;gBACpE,SAAS,EAAE,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,SAAS,EAAE,CAAC;YACd,CAAC;QACH,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;YAC3D,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC;IAED,gDAAgD;IAChD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IAClD,IAAI,eAAe,GAAG,KAAK,CAAC;IAC5B,IAAI,OAAO,KAAK,YAAY,EAAE,CAAC;QAC7B,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACjC,eAAe,GAAG,IAAI,CAAC;IACzB,CAAC;IAED,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AACnG,CAAC"}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/sidecar.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, chmodSync, existsSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
// src/log.ts
|
|
8
|
+
var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
9
|
+
var MIN = LEVELS[process.env.CLAWBORRATOR_LOG_LEVEL || "info"] ?? LEVELS.info;
|
|
10
|
+
function emit(level, msg, fields) {
|
|
11
|
+
if (LEVELS[level] < MIN) return;
|
|
12
|
+
const line = JSON.stringify({
|
|
13
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14
|
+
level,
|
|
15
|
+
msg,
|
|
16
|
+
...fields
|
|
17
|
+
});
|
|
18
|
+
process.stderr.write(line + "\n");
|
|
19
|
+
}
|
|
20
|
+
var log = {
|
|
21
|
+
debug: (msg, fields) => emit("debug", msg, fields),
|
|
22
|
+
info: (msg, fields) => emit("info", msg, fields),
|
|
23
|
+
warn: (msg, fields) => emit("warn", msg, fields),
|
|
24
|
+
error: (msg, fields) => emit("error", msg, fields)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/sidecar.ts
|
|
28
|
+
function sidecarPath(cwd) {
|
|
29
|
+
return resolve(cwd, ".claude", "clawborrator.session.json");
|
|
30
|
+
}
|
|
31
|
+
function readSidecar(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(sidecarPath(cwd), "utf8");
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function findSidecar(start) {
|
|
40
|
+
let dir = resolve(start);
|
|
41
|
+
for (let i = 0; i < 32; i++) {
|
|
42
|
+
const found = readSidecar(dir);
|
|
43
|
+
if (found) return found;
|
|
44
|
+
const parent = resolve(dir, "..");
|
|
45
|
+
if (parent === dir) break;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/transcript.ts
|
|
52
|
+
import { openSync, readSync, closeSync, statSync } from "node:fs";
|
|
53
|
+
var DEFAULT_TAIL_BYTES = 256 * 1024;
|
|
54
|
+
function readTranscriptMessages(path, tailBytes = DEFAULT_TAIL_BYTES) {
|
|
55
|
+
try {
|
|
56
|
+
const stat = statSync(path);
|
|
57
|
+
const start = Math.max(0, stat.size - tailBytes);
|
|
58
|
+
const fd = openSync(path, "r");
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
const buf = Buffer.alloc(stat.size - start);
|
|
62
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
63
|
+
raw = buf.toString("utf8");
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
if (!raw) return [];
|
|
68
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
69
|
+
if (start > 0 && lines.length > 0) lines.shift();
|
|
70
|
+
return lines.map((l) => {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(l);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}).filter((m) => m !== null);
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function extractTextBlocksBeforeToolUse(messages, toolUseId) {
|
|
82
|
+
if (!toolUseId || !Array.isArray(messages)) return [];
|
|
83
|
+
let targetIdx = -1;
|
|
84
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
85
|
+
const m = messages[i];
|
|
86
|
+
if (m?.type !== "assistant") continue;
|
|
87
|
+
const content = m.message?.content;
|
|
88
|
+
if (!Array.isArray(content)) continue;
|
|
89
|
+
if (content.some((c) => c?.type === "tool_use" && c.id === toolUseId)) {
|
|
90
|
+
targetIdx = i;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (targetIdx < 0) return [];
|
|
95
|
+
const out = [];
|
|
96
|
+
for (let i = targetIdx - 1; i >= 0; i--) {
|
|
97
|
+
const m = messages[i];
|
|
98
|
+
if (m?.type === "user") break;
|
|
99
|
+
if (m?.type !== "assistant") continue;
|
|
100
|
+
const content = m.message?.content;
|
|
101
|
+
if (!Array.isArray(content)) continue;
|
|
102
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
103
|
+
const c = content[j];
|
|
104
|
+
if (c?.type === "text" && typeof c.text === "string" && c.text.trim()) {
|
|
105
|
+
out.unshift(c.text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function messageContainsToolUse(messages, toolUseId) {
|
|
112
|
+
if (!toolUseId || !Array.isArray(messages)) return false;
|
|
113
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
114
|
+
const m = messages[i];
|
|
115
|
+
if (m?.type !== "assistant") continue;
|
|
116
|
+
const content = m.message?.content;
|
|
117
|
+
if (!Array.isArray(content)) continue;
|
|
118
|
+
if (content.some((c) => c?.type === "tool_use" && c.id === toolUseId)) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function hasThinkingBlocksBeforeToolUse(messages, toolUseId) {
|
|
123
|
+
if (!toolUseId || !Array.isArray(messages)) return false;
|
|
124
|
+
let targetIdx = -1;
|
|
125
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
126
|
+
const m = messages[i];
|
|
127
|
+
if (m?.type !== "assistant") continue;
|
|
128
|
+
const content = m.message?.content;
|
|
129
|
+
if (!Array.isArray(content)) continue;
|
|
130
|
+
if (content.some((c) => c?.type === "tool_use" && c.id === toolUseId)) {
|
|
131
|
+
targetIdx = i;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (targetIdx < 0) return false;
|
|
136
|
+
for (let i = targetIdx - 1; i >= 0; i--) {
|
|
137
|
+
const m = messages[i];
|
|
138
|
+
if (m?.type === "user") break;
|
|
139
|
+
if (m?.type !== "assistant") continue;
|
|
140
|
+
const content = m.message?.content;
|
|
141
|
+
if (!Array.isArray(content)) continue;
|
|
142
|
+
if (content.some((c) => c?.type === "thinking")) return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function joinTextAfterLastToolUse(content) {
|
|
147
|
+
if (!Array.isArray(content)) return "";
|
|
148
|
+
let lastToolIdx = -1;
|
|
149
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
150
|
+
if (content[j]?.type === "tool_use") {
|
|
151
|
+
lastToolIdx = j;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const parts = content.slice(lastToolIdx + 1).filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => c.text);
|
|
156
|
+
return parts.join("\n").trim();
|
|
157
|
+
}
|
|
158
|
+
function extractFromLastAssistantMessage(v) {
|
|
159
|
+
if (!v) return "";
|
|
160
|
+
if (typeof v === "string") return v.trim();
|
|
161
|
+
const obj = v;
|
|
162
|
+
const content = Array.isArray(obj.content) ? obj.content : obj.message && Array.isArray(obj.message.content) ? obj.message.content : null;
|
|
163
|
+
if (content) return joinTextAfterLastToolUse(content);
|
|
164
|
+
if (typeof obj.text === "string") return obj.text.trim();
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
function extractFinalAnswerFromTranscript(path, tailBytes = DEFAULT_TAIL_BYTES) {
|
|
168
|
+
const messages = readTranscriptMessages(path, tailBytes);
|
|
169
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
170
|
+
const m = messages[i];
|
|
171
|
+
if (m?.type !== "assistant") continue;
|
|
172
|
+
const content = m.message?.content;
|
|
173
|
+
if (!Array.isArray(content)) continue;
|
|
174
|
+
const joined = joinTextAfterLastToolUse(content);
|
|
175
|
+
if (joined) return joined;
|
|
176
|
+
}
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/hook.ts
|
|
181
|
+
var HOOK_TO_EVENT = {
|
|
182
|
+
UserPromptSubmit: { kind: "chat", type: "prompt" },
|
|
183
|
+
PreToolUse: { kind: "tail", type: "PreToolUse" },
|
|
184
|
+
PostToolUse: { kind: "tail", type: "PostToolUse" },
|
|
185
|
+
PostToolUseFailure: { kind: "tail", type: "PostToolUseFailure" },
|
|
186
|
+
Stop: { kind: "tail", type: "Stop" },
|
|
187
|
+
Notification: { kind: "tail", type: "Notification" },
|
|
188
|
+
SessionStart: { kind: "tail", type: "SessionStart" },
|
|
189
|
+
SessionEnd: { kind: "tail", type: "SessionEnd" },
|
|
190
|
+
TaskCreated: { kind: "tail", type: "TaskCreated" },
|
|
191
|
+
SubagentStart: { kind: "tail", type: "SubagentStart" },
|
|
192
|
+
SubagentStop: { kind: "tail", type: "SubagentStop" },
|
|
193
|
+
TaskCompleted: { kind: "tail", type: "TaskCompleted" }
|
|
194
|
+
};
|
|
195
|
+
var TOOLS_SKIPPED_FOR_PRE_TEXT = /* @__PURE__ */ new Set([
|
|
196
|
+
"mcp__clawborrator__reply"
|
|
197
|
+
]);
|
|
198
|
+
async function readStdin() {
|
|
199
|
+
return new Promise((res) => {
|
|
200
|
+
let buf = "";
|
|
201
|
+
process.stdin.setEncoding("utf8");
|
|
202
|
+
process.stdin.on("data", (chunk) => {
|
|
203
|
+
buf += chunk;
|
|
204
|
+
});
|
|
205
|
+
process.stdin.on("end", () => res(buf));
|
|
206
|
+
process.stdin.on("close", () => res(buf));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function postEvent(sidecar, body, timeoutMs) {
|
|
210
|
+
const ctl = new AbortController();
|
|
211
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch(`${sidecar.hubUrl}/api/channel/event`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"Authorization": `Bearer ${sidecar.channelToken}`,
|
|
217
|
+
"Content-Type": "application/json"
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(body),
|
|
220
|
+
signal: ctl.signal
|
|
221
|
+
});
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
const text = await res.text().catch(() => "");
|
|
224
|
+
log.warn("hub rejected event", { kind: body.kind, type: body.type, status: res.status, body: text.slice(0, 240) });
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
log.warn("event POST failed", { kind: body.kind, type: body.type, error: e?.message ?? String(e) });
|
|
230
|
+
return false;
|
|
231
|
+
} finally {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function enrichStopAssistantText(payload, hookName2) {
|
|
236
|
+
if (typeof payload.assistant_text === "string" && payload.assistant_text.trim()) return;
|
|
237
|
+
const fromLAM = extractFromLastAssistantMessage(payload.last_assistant_message);
|
|
238
|
+
if (fromLAM) {
|
|
239
|
+
payload.assistant_text = fromLAM;
|
|
240
|
+
log.debug("stop: assistant_text from last_assistant_message", { chars: fromLAM.length });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (typeof payload.text === "string" && payload.text.trim() || typeof payload.response === "string" && payload.response.trim()) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
247
|
+
if (!transcriptPath) {
|
|
248
|
+
log.debug("stop: no transcript_path on payload, no assistant_text recoverable", { hookName: hookName2 });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
252
|
+
const text = extractFinalAnswerFromTranscript(transcriptPath, DEFAULT_TAIL_BYTES);
|
|
253
|
+
if (text) {
|
|
254
|
+
payload.assistant_text = text;
|
|
255
|
+
log.info("stop: assistant_text extracted from transcript", { chars: text.length });
|
|
256
|
+
} else {
|
|
257
|
+
log.debug("stop: transcript had no assistant-text block in tail window", { hookName: hookName2, path: transcriptPath });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function shipPreToolAssistantText(sidecar, payload) {
|
|
261
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
262
|
+
const toolUseId = String(payload.tool_use_id ?? payload.toolUseId ?? "");
|
|
263
|
+
const toolName = String(payload.tool_name ?? payload.toolName ?? "");
|
|
264
|
+
if (!transcriptPath || !toolUseId) return;
|
|
265
|
+
if (TOOLS_SKIPPED_FOR_PRE_TEXT.has(toolName)) return;
|
|
266
|
+
let messages = readTranscriptMessages(transcriptPath, DEFAULT_TAIL_BYTES);
|
|
267
|
+
let foundTarget = messageContainsToolUse(messages, toolUseId);
|
|
268
|
+
let retries = 0;
|
|
269
|
+
while (!foundTarget && retries < 4) {
|
|
270
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
271
|
+
messages = readTranscriptMessages(transcriptPath, DEFAULT_TAIL_BYTES);
|
|
272
|
+
foundTarget = messageContainsToolUse(messages, toolUseId);
|
|
273
|
+
retries++;
|
|
274
|
+
}
|
|
275
|
+
const blocks = extractTextBlocksBeforeToolUse(messages, toolUseId);
|
|
276
|
+
const hasThinking = blocks.length === 0 ? hasThinkingBlocksBeforeToolUse(messages, toolUseId) : false;
|
|
277
|
+
log.debug("PreToolUse extract", {
|
|
278
|
+
toolUseId: toolUseId.slice(0, 24),
|
|
279
|
+
messages: messages.length,
|
|
280
|
+
targetFound: foundTarget,
|
|
281
|
+
retries,
|
|
282
|
+
blocks: blocks.length,
|
|
283
|
+
placeholder: hasThinking
|
|
284
|
+
});
|
|
285
|
+
const baseMs = Date.now();
|
|
286
|
+
const tsAt = (i) => new Date(baseMs + i).toISOString();
|
|
287
|
+
if (blocks.length > 0) {
|
|
288
|
+
await Promise.all(blocks.map(
|
|
289
|
+
(text, i) => postEvent(sidecar, {
|
|
290
|
+
sessionId: sidecar.sessionId,
|
|
291
|
+
kind: "chat",
|
|
292
|
+
type: "assistant_text",
|
|
293
|
+
payload: { text, toolUseId },
|
|
294
|
+
ts: tsAt(i)
|
|
295
|
+
}, 800)
|
|
296
|
+
));
|
|
297
|
+
} else if (hasThinking) {
|
|
298
|
+
await postEvent(sidecar, {
|
|
299
|
+
sessionId: sidecar.sessionId,
|
|
300
|
+
kind: "chat",
|
|
301
|
+
type: "assistant_text",
|
|
302
|
+
payload: {
|
|
303
|
+
text: "(extended thinking \u2014 content not in transcript)",
|
|
304
|
+
toolUseId,
|
|
305
|
+
placeholder: true
|
|
306
|
+
},
|
|
307
|
+
ts: tsAt(0)
|
|
308
|
+
}, 800);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function runHook(hookName2) {
|
|
312
|
+
const map = HOOK_TO_EVENT[hookName2];
|
|
313
|
+
if (!map) {
|
|
314
|
+
log.warn("unknown hook name; skipping", { hookName: hookName2 });
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
const stdinRaw = await readStdin();
|
|
318
|
+
if (stdinRaw) process.stdout.write(stdinRaw);
|
|
319
|
+
let payload = {};
|
|
320
|
+
try {
|
|
321
|
+
if (stdinRaw.trim()) payload = JSON.parse(stdinRaw);
|
|
322
|
+
} catch {
|
|
323
|
+
payload = { rawStdin: stdinRaw.slice(0, 2e3) };
|
|
324
|
+
}
|
|
325
|
+
const sidecar = findSidecar(process.cwd());
|
|
326
|
+
if (!sidecar) {
|
|
327
|
+
log.warn("hook fired but no sidecar found \u2014 channel must not be running", { cwd: process.cwd() });
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
if (hookName2 === "Stop") {
|
|
332
|
+
await enrichStopAssistantText(payload, hookName2);
|
|
333
|
+
const reply = typeof payload.assistant_text === "string" ? payload.assistant_text.trim() : "";
|
|
334
|
+
if (reply) {
|
|
335
|
+
await postEvent(sidecar, {
|
|
336
|
+
sessionId: sidecar.sessionId,
|
|
337
|
+
kind: "chat",
|
|
338
|
+
type: "reply",
|
|
339
|
+
payload: { text: reply },
|
|
340
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
341
|
+
}, 2e3);
|
|
342
|
+
}
|
|
343
|
+
} else if (hookName2 === "SubagentStop") {
|
|
344
|
+
} else if (hookName2 === "PreToolUse") {
|
|
345
|
+
await shipPreToolAssistantText(sidecar, payload);
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {
|
|
348
|
+
log.warn("pre-event enrichment threw", { error: e?.message ?? String(e), hookName: hookName2 });
|
|
349
|
+
}
|
|
350
|
+
await postEvent(sidecar, {
|
|
351
|
+
sessionId: sidecar.sessionId,
|
|
352
|
+
kind: map.kind,
|
|
353
|
+
type: map.type,
|
|
354
|
+
payload,
|
|
355
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
356
|
+
}, 5e3);
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/hook-entry.ts
|
|
361
|
+
var hookName = process.argv[2];
|
|
362
|
+
if (!hookName) {
|
|
363
|
+
log.error("hook entry invoked without a hook name argument");
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
runHook(hookName).catch((err) => {
|
|
367
|
+
log.error("hook fatal", { error: String(err) });
|
|
368
|
+
process.exit(0);
|
|
369
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawborrator-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clawborrator channel for hub_v1 — MCP server that connects Claude Code to a hub over WebSocket, with hooks for activity capture and tools for cross-session routing.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
+
"dist-hook",
|
|
31
32
|
"bin",
|
|
32
33
|
"README.md"
|
|
33
34
|
],
|
|
@@ -35,10 +36,12 @@
|
|
|
35
36
|
"node": ">=20"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"build":
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
39
|
+
"build": "npm run build:tsc && npm run build:hook",
|
|
40
|
+
"build:tsc": "tsc -p tsconfig.json",
|
|
41
|
+
"build:hook": "esbuild src/hook-entry.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist-hook/clawborrator-tail.mjs --banner:js=\"#!/usr/bin/env node\"",
|
|
42
|
+
"dev": "tsx watch src/index.ts",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
+
"start": "node dist/index.js",
|
|
42
45
|
"prepublishOnly": "npm run build"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
@@ -48,6 +51,7 @@
|
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"@types/node": "^22.7.5",
|
|
50
53
|
"@types/ws": "^8.5.13",
|
|
54
|
+
"esbuild": "^0.24.2",
|
|
51
55
|
"tsx": "^4.19.1",
|
|
52
56
|
"typescript": "^5.6.2"
|
|
53
57
|
}
|