agenthub-multiagent-mcp 1.52.0 → 1.53.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.
Files changed (61) hide show
  1. package/dist/captureHookInstall.d.ts +127 -0
  2. package/dist/captureHookInstall.d.ts.map +1 -0
  3. package/dist/captureHookInstall.js +256 -0
  4. package/dist/captureHookInstall.js.map +1 -0
  5. package/dist/captureHookInstall.test.d.ts +10 -0
  6. package/dist/captureHookInstall.test.d.ts.map +1 -0
  7. package/dist/captureHookInstall.test.js +325 -0
  8. package/dist/captureHookInstall.test.js.map +1 -0
  9. package/dist/channel.js +96 -0
  10. package/dist/channel.js.map +1 -1
  11. package/dist/client.d.ts +50 -0
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/client.js +90 -0
  14. package/dist/client.js.map +1 -1
  15. package/dist/commands/setup-shell.js +11 -11
  16. package/dist/daemon.js +142 -3
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/index.js +25 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/session-capture.sh +410 -0
  21. package/dist/sessionCapture.d.ts +163 -0
  22. package/dist/sessionCapture.d.ts.map +1 -0
  23. package/dist/sessionCapture.js +279 -0
  24. package/dist/sessionCapture.js.map +1 -0
  25. package/dist/sessionCapture.test.d.ts +9 -0
  26. package/dist/sessionCapture.test.d.ts.map +1 -0
  27. package/dist/sessionCapture.test.js +366 -0
  28. package/dist/sessionCapture.test.js.map +1 -0
  29. package/dist/sessionCaptureScript.test.d.ts +16 -0
  30. package/dist/sessionCaptureScript.test.d.ts.map +1 -0
  31. package/dist/sessionCaptureScript.test.js +92 -0
  32. package/dist/sessionCaptureScript.test.js.map +1 -0
  33. package/dist/steering.test.d.ts +2 -0
  34. package/dist/steering.test.d.ts.map +1 -0
  35. package/dist/steering.test.js +223 -0
  36. package/dist/steering.test.js.map +1 -0
  37. package/dist/streamingRuntimeLib.d.ts +260 -1
  38. package/dist/streamingRuntimeLib.d.ts.map +1 -1
  39. package/dist/streamingRuntimeLib.js +474 -1
  40. package/dist/streamingRuntimeLib.js.map +1 -1
  41. package/dist/streamingRuntimeLib.test.js +87 -1
  42. package/dist/streamingRuntimeLib.test.js.map +1 -1
  43. package/dist/tools/index.d.ts.map +1 -1
  44. package/dist/tools/index.js +8 -0
  45. package/dist/tools/index.js.map +1 -1
  46. package/dist/tools/sessionControl.d.ts +53 -0
  47. package/dist/tools/sessionControl.d.ts.map +1 -0
  48. package/dist/tools/sessionControl.js +265 -0
  49. package/dist/tools/sessionControl.js.map +1 -0
  50. package/dist/tools/sessionControl.test.d.ts +14 -0
  51. package/dist/tools/sessionControl.test.d.ts.map +1 -0
  52. package/dist/tools/sessionControl.test.js +298 -0
  53. package/dist/tools/sessionControl.test.js.map +1 -0
  54. package/dist/tools/tools.test.js +4 -2
  55. package/dist/tools/tools.test.js.map +1 -1
  56. package/native/agenthub-memvid/agenthub-memvid.linux-x64-gnu.node +0 -0
  57. package/package.json +2 -2
  58. package/skills/commands/start-session.md +77 -77
  59. package/skills/skills/deploy-staging/SKILL.md +164 -164
  60. package/skills/skills/deploy-vps-openclaw/SKILL.md +97 -97
  61. package/skills/skills/karpathy-guidelines/SKILL.md +67 -67
@@ -0,0 +1,410 @@
1
+ #!/bin/bash
2
+ # AgentHub Rich Session Capture Hook
3
+ #
4
+ # Captures full tool_input + tool_response from PostToolUse hook,
5
+ # plus assistant/user messages from the transcript file.
6
+ # Posts structured events to /api/sessions/{id}/events.
7
+ #
8
+ # Two callers, two ways of learning which session to record into:
9
+ #
10
+ # 1. Agent/daemon shifts (add-multiplayer-sessions §1.5). The daemon opens the
11
+ # session and stamps AGENTHUB_SESSION_ID into the child environment. This
12
+ # path is unchanged.
13
+ #
14
+ # 2. Human interactive sessions (§3.2). There is no daemon, so nothing stamps
15
+ # AGENTHUB_SESSION_ID — the hook has to obtain one itself. It does that by
16
+ # asking the server to resolve a session for Claude Code's own session id,
17
+ # and the server refuses unless the person has consented.
18
+ #
19
+ # The interactive path is off unless a consent grant file exists locally, which
20
+ # the MCP writes only after the server confirms that *this user* consented. With
21
+ # no grant file the hook makes no network call at all and exits — so a person
22
+ # who has not consented pays nothing and is recorded nowhere.
23
+
24
+ INPUT=$(cat)
25
+
26
+ SESSION_ID="${AGENTHUB_SESSION_ID:-}"
27
+ API_KEY="${AGENTHUB_API_KEY:-}"
28
+ API_URL="${AGENTHUB_URL:-https://agenthub.contetial.com}"
29
+ CONNECT_TOKEN=""
30
+
31
+ # Resolve a python interpreter. Debian/Ubuntu (the employee-daemon hosts) ship
32
+ # `python3` only — the unqualified `python` this used to call does not exist
33
+ # there, and because the whole substitution is 2>/dev/null the hook produced
34
+ # nothing, silently. `python` is tried first so Windows/Git-Bash checkouts keep
35
+ # using the interpreter that is actually on PATH there.
36
+ PY=$(command -v python || command -v python3)
37
+ [ -z "$PY" ] && exit 0
38
+
39
+ # --- Interactive (human) path: resolve a session id, consent permitting -------
40
+ #
41
+ # Emits four lines: session id, api url, connect token, and the notice JSON to
42
+ # print (empty when it is not time to re-announce). Prints nothing at all when
43
+ # capture is not authorized, which is the common case.
44
+ if [ -z "$SESSION_ID" ]; then
45
+ BOOTSTRAP=$(echo "$INPUT" | "$PY" -c "
46
+ import sys, json, os, time
47
+
48
+ # The grant file is written by the AgentHub MCP *only* after the server confirms
49
+ # this user's capture consent, and removed when consent is withdrawn. Its absence
50
+ # is the fast, offline, fail-closed answer: no consent on record here, so do
51
+ # nothing and talk to nobody.
52
+ grant_path = os.environ.get('AGENTHUB_CAPTURE_GRANT') or os.path.join(
53
+ os.path.expanduser('~'), '.claude', 'agenthub-capture.json')
54
+ try:
55
+ with open(grant_path, 'r') as f:
56
+ grant = json.load(f)
57
+ except Exception:
58
+ sys.exit(0)
59
+
60
+ api_url = (grant.get('api_url') or '').rstrip('/')
61
+ token = grant.get('connect_token') or os.environ.get('AGENTHUB_CONNECT_TOKEN') or ''
62
+ if not api_url or not token:
63
+ sys.exit(0)
64
+
65
+ try:
66
+ data = json.load(sys.stdin)
67
+ except Exception:
68
+ sys.exit(0)
69
+
70
+ claude_sid = data.get('session_id') or ''
71
+ if not claude_sid:
72
+ sys.exit(0)
73
+
74
+ # Per-Claude-session cache, kept beside the grant rather than in /tmp so it
75
+ # behaves the same on the Windows workstations this path exists to serve.
76
+ state_dir = os.path.join(os.path.dirname(grant_path), 'agenthub-capture-state')
77
+ state_file = os.path.join(state_dir, claude_sid.replace(os.sep, '_') + '.json')
78
+ state = {}
79
+ try:
80
+ with open(state_file, 'r') as f:
81
+ state = json.load(f)
82
+ except Exception:
83
+ state = {}
84
+
85
+ # A refusal is remembered for the rest of the session. Without this the hook
86
+ # would re-ask the server on every single tool use of a session it has already
87
+ # been told it may not record.
88
+ if state.get('denied'):
89
+ sys.exit(0)
90
+
91
+ notice_text = state.get('notice_text') or ''
92
+ session_id = state.get('session_id') or ''
93
+ created = False
94
+
95
+ if not session_id:
96
+ import urllib.request, urllib.error
97
+ body = json.dumps({
98
+ 'claude_session_id': claude_sid,
99
+ 'title': (data.get('cwd') or '').replace('\\\\', '/').rstrip('/').split('/')[-1] or 'interactive session',
100
+ 'working_directory': data.get('cwd') or '',
101
+ }).encode()
102
+ req = urllib.request.Request(
103
+ api_url + '/api/session-capture/sessions',
104
+ data=body,
105
+ headers={'Content-Type': 'application/json', 'X-Connect-Token': token},
106
+ method='POST')
107
+ try:
108
+ with urllib.request.urlopen(req, timeout=4) as resp:
109
+ payload = json.loads(resp.read().decode())
110
+ session_id = payload.get('session_id') or ''
111
+ notice_text = payload.get('capture_notice') or ''
112
+ created = True
113
+ except urllib.error.HTTPError:
114
+ # The server declined: consent absent or withdrawn, streaming off, or
115
+ # the credential does not identify a person. Remember it and stay quiet.
116
+ try:
117
+ os.makedirs(state_dir, exist_ok=True)
118
+ with open(state_file, 'w') as f:
119
+ json.dump({'denied': True}, f)
120
+ except Exception:
121
+ pass
122
+ sys.exit(0)
123
+ except Exception:
124
+ # Network trouble is transient — do not record a denial for it, and do
125
+ # not capture this tool call either.
126
+ sys.exit(0)
127
+
128
+ if not session_id:
129
+ sys.exit(0)
130
+
131
+ # --- Visible capture indicator (§3.3) ---------------------------------------
132
+ # A person must never be recorded without an obvious signal that it is
133
+ # happening. The notice is printed when capture starts and re-printed on an
134
+ # interval, so it stays visible in a long session rather than scrolling away
135
+ # once. The wording comes from the server so a client cannot soften or drop it.
136
+ try:
137
+ interval = int(grant.get('notice_interval_sec') or 900)
138
+ except Exception:
139
+ interval = 900
140
+ # Clamped, not merely defaulted. The interval is read from a local file, and an
141
+ # indicator that a local setting can push out to 'never' is not an indicator.
142
+ # The floor keeps a long session from being all banner.
143
+ interval = max(60, min(interval, 1800))
144
+ now = int(time.time())
145
+ last = int(state.get('last_notice') or 0)
146
+ show = created or (now - last) >= interval
147
+
148
+ state_out = {'session_id': session_id, 'notice_text': notice_text,
149
+ 'last_notice': now if show else last}
150
+ try:
151
+ os.makedirs(state_dir, exist_ok=True)
152
+ with open(state_file, 'w') as f:
153
+ json.dump(state_out, f)
154
+ except Exception:
155
+ pass
156
+
157
+ notice_json = ''
158
+ if show:
159
+ msg = ('● RECORDING - AgentHub session capture is ON for this session (' + session_id + ') '
160
+ 'and is sending it to ' + api_url + '. ' + notice_text +
161
+ ' Turn it off any time: set capture consent to off in AgentHub.')
162
+ notice_json = json.dumps({'systemMessage': msg})
163
+
164
+ # Write through the binary buffer, not print(). On Windows, text-mode stdout
165
+ # translates \n to \r\n, and the shell read strips only the \n — the \r would
166
+ # survive into the URL and the session id, producing a request to a host that
167
+ # does not exist. Nothing reports an error; capture just silently never posts.
168
+ #
169
+ # NOTE for anyone editing this file: this Python source is a bash double-quoted
170
+ # string, so a backtick anywhere in it (even inside a comment) is a command
171
+ # substitution that bash runs before Python ever sees the script. One in a
172
+ # comment here consumed the hook's stdin and made capture silently do nothing.
173
+ # Never use a backtick in these blocks; the same goes for a bare dollar sign.
174
+ # sessionCaptureScript.test.ts enforces both.
175
+ out = '\n'.join([session_id, api_url, token, notice_json]) + '\n'
176
+ sys.stdout.buffer.write(out.encode('utf-8'))
177
+ " 2>/dev/null)
178
+
179
+ { read -r BS_SESSION; read -r BS_URL; read -r BS_TOKEN; read -r BS_NOTICE; } <<EOF
180
+ $BOOTSTRAP
181
+ EOF
182
+
183
+ # Belt and braces against the \r above: a stray carriage return in the URL or
184
+ # the session id is invisible in logs and turns every subsequent POST into a
185
+ # request to a nonexistent host, so strip it here too rather than trusting the
186
+ # producer to have got its newlines right.
187
+ BS_SESSION="${BS_SESSION%$'\r'}"
188
+ BS_URL="${BS_URL%$'\r'}"
189
+ BS_TOKEN="${BS_TOKEN%$'\r'}"
190
+ BS_NOTICE="${BS_NOTICE%$'\r'}"
191
+
192
+ SESSION_ID="$BS_SESSION"
193
+ [ -n "$BS_URL" ] && API_URL="$BS_URL"
194
+ CONNECT_TOKEN="$BS_TOKEN"
195
+
196
+ # The indicator is the only thing this hook ever writes to stdout. Claude Code
197
+ # surfaces `systemMessage` to the person in the session.
198
+ [ -n "$BS_NOTICE" ] && echo "$BS_NOTICE"
199
+ fi
200
+
201
+ # No session to record into (unset on the daemon path, or consent absent on the
202
+ # interactive one), or no credential — either way, capture nothing.
203
+ [ -z "$SESSION_ID" ] && exit 0
204
+ [ -z "$API_KEY" ] && [ -z "$CONNECT_TOKEN" ] && exit 0
205
+
206
+ # Re-export so the event-building block below sees the resolved values on both
207
+ # paths. On the daemon path these are already what they were.
208
+ export AGENTHUB_SESSION_ID="$SESSION_ID"
209
+ export AGENTHUB_URL="$API_URL"
210
+ export AGENTHUB_CAPTURE_CONNECT_TOKEN="$CONNECT_TOKEN"
211
+
212
+ # Use python to parse JSON and build rich event payload
213
+ EVENTS_JSON=$(echo "$INPUT" | "$PY" -c "
214
+ import sys, json, os, re
215
+
216
+ TRUNC = 8000 # 8KB max per field (captures full assistant responses)
217
+ TRUNC_SHORT = 1000
218
+
219
+ # Secret patterns to redact
220
+ SECRET_RE = re.compile(r'(api[_-]?key|token|password|secret|authorization|credential)[=:\s]*[^\s,}\"]{8,}', re.IGNORECASE)
221
+
222
+ def redact(s):
223
+ if not s: return s
224
+ return SECRET_RE.sub(lambda m: m.group().split('=')[0] + '=[REDACTED]' if '=' in m.group() else m.group().split(':')[0] + ':[REDACTED]', str(s))
225
+
226
+ def trunc(s, limit=TRUNC):
227
+ s = str(s) if s else ''
228
+ return s[:limit] + '...' if len(s) > limit else s
229
+
230
+ try:
231
+ data = json.load(sys.stdin)
232
+ except:
233
+ sys.exit(0)
234
+
235
+ tool_name = data.get('tool_name', '')
236
+ tool_input = data.get('tool_input', {})
237
+ tool_response = data.get('tool_response', {})
238
+ transcript_path = data.get('transcript_path', '')
239
+ session_id = data.get('session_id', '')
240
+
241
+ if not tool_name:
242
+ sys.exit(0)
243
+
244
+ events = []
245
+
246
+ # --- Report Claude's session_id to server (once per session) ---
247
+ claude_id_file = '/tmp/agenthub-claude-sid-' + os.environ.get('AGENTHUB_SESSION_ID', 'none')
248
+ if session_id and not os.path.exists(claude_id_file):
249
+ try:
250
+ import urllib.request
251
+ req = urllib.request.Request(
252
+ os.environ.get('AGENTHUB_URL', 'https://agenthub.contetial.com') + '/api/sessions/' + os.environ['AGENTHUB_SESSION_ID'] + '/claude-session-id',
253
+ data=json.dumps({'claude_session_id': session_id}).encode(),
254
+ headers={'Content-Type': 'application/json',
255
+ 'X-API-Key': os.environ.get('AGENTHUB_API_KEY', ''),
256
+ 'X-Connect-Token': os.environ.get('AGENTHUB_CAPTURE_CONNECT_TOKEN', '')},
257
+ method='PATCH'
258
+ )
259
+ urllib.request.urlopen(req, timeout=2)
260
+ with open(claude_id_file, 'w') as f:
261
+ f.write(session_id)
262
+ except:
263
+ pass
264
+
265
+ # --- Read transcript for new assistant/user messages ---
266
+ offset_file = '/tmp/agenthub-transcript-offset-' + os.environ.get('AGENTHUB_SESSION_ID', 'none')
267
+ last_offset = 0
268
+ try:
269
+ with open(offset_file, 'r') as f:
270
+ last_offset = int(f.read().strip())
271
+ except:
272
+ pass
273
+
274
+ if transcript_path and os.path.exists(transcript_path):
275
+ try:
276
+ with open(transcript_path, 'r') as f:
277
+ f.seek(last_offset)
278
+ new_lines = f.readlines()
279
+ new_offset = f.tell()
280
+
281
+ for line in new_lines:
282
+ line = line.strip()
283
+ if not line:
284
+ continue
285
+ try:
286
+ entry = json.loads(line)
287
+ entry_type = entry.get('type', '')
288
+
289
+ # Skip non-message entries (progress, queue-operation, etc.)
290
+ if entry_type not in ('user', 'assistant'):
291
+ continue
292
+
293
+ # Content is nested: entry.message.role and entry.message.content
294
+ msg = entry.get('message', {})
295
+ role = msg.get('role', entry_type)
296
+ raw_content = msg.get('content', entry.get('content', ''))
297
+ content = ''
298
+
299
+ # Extract text content from various formats
300
+ if isinstance(raw_content, str):
301
+ content = raw_content
302
+ elif isinstance(raw_content, list):
303
+ parts = []
304
+ for block in raw_content:
305
+ if isinstance(block, dict) and block.get('type') == 'text':
306
+ parts.append(block.get('text', ''))
307
+ content = '\n'.join(parts)
308
+
309
+ if not content or len(content.strip()) < 2:
310
+ continue
311
+
312
+ if role == 'assistant':
313
+ events.append({
314
+ 'event_type': 'assistant_message',
315
+ 'payload': json.dumps({'content': redact(trunc(content))}),
316
+ 'token_count': 0
317
+ })
318
+ elif role == 'user':
319
+ events.append({
320
+ 'event_type': 'user_message',
321
+ 'payload': json.dumps({'content': redact(trunc(content)), 'source': 'prompt'}),
322
+ 'token_count': 0
323
+ })
324
+ except:
325
+ continue
326
+
327
+ # Save offset
328
+ with open(offset_file, 'w') as f:
329
+ f.write(str(new_offset))
330
+ except:
331
+ pass
332
+
333
+ # --- Build tool-specific event ---
334
+ tn = tool_name.lower()
335
+ ti = tool_input if isinstance(tool_input, dict) else {}
336
+ tr = tool_response if isinstance(tool_response, dict) else {}
337
+
338
+ if tn == 'bash':
339
+ payload = {
340
+ 'tool_name': 'Bash',
341
+ 'command': redact(trunc(ti.get('command', ''), TRUNC_SHORT)),
342
+ 'exit_code': tr.get('exit_code', tr.get('exitCode')),
343
+ 'stdout': redact(trunc(tr.get('stdout', tr.get('output', str(tr)[:TRUNC])))),
344
+ }
345
+ events.append({'event_type': 'command_exec', 'payload': json.dumps(payload), 'token_count': 0})
346
+
347
+ elif tn == 'read':
348
+ payload = {
349
+ 'tool_name': 'Read',
350
+ 'file_path': ti.get('file_path', ''),
351
+ 'lines_read': ti.get('limit', 0),
352
+ 'content_preview': redact(trunc(str(tr)[:TRUNC_SHORT], TRUNC_SHORT)),
353
+ }
354
+ events.append({'event_type': 'file_read', 'payload': json.dumps(payload), 'token_count': 0})
355
+
356
+ elif tn == 'edit':
357
+ payload = {
358
+ 'tool_name': 'Edit',
359
+ 'file_path': ti.get('file_path', ''),
360
+ 'old_string': redact(trunc(ti.get('old_string', ''), TRUNC_SHORT)),
361
+ 'new_string': redact(trunc(ti.get('new_string', ''), TRUNC_SHORT)),
362
+ }
363
+ events.append({'event_type': 'file_edit', 'payload': json.dumps(payload), 'token_count': 0})
364
+
365
+ elif tn == 'write':
366
+ content = ti.get('content', '')
367
+ payload = {
368
+ 'tool_name': 'Write',
369
+ 'file_path': ti.get('file_path', ''),
370
+ 'content_preview': redact(trunc(content, TRUNC_SHORT)),
371
+ }
372
+ events.append({'event_type': 'file_write', 'payload': json.dumps(payload), 'token_count': 0})
373
+
374
+ elif tn in ('glob', 'grep'):
375
+ payload = {
376
+ 'tool_name': tool_name,
377
+ 'pattern': ti.get('pattern', ''),
378
+ 'path': ti.get('path', ''),
379
+ 'result_summary': redact(trunc(str(tr), TRUNC_SHORT)),
380
+ }
381
+ events.append({'event_type': 'tool_call', 'payload': json.dumps(payload), 'token_count': 0})
382
+
383
+ else:
384
+ # Generic tool call
385
+ payload = {
386
+ 'tool_name': tool_name,
387
+ 'input_summary': redact(trunc(json.dumps(ti), TRUNC_SHORT)),
388
+ 'result_summary': redact(trunc(str(tr), TRUNC_SHORT)),
389
+ }
390
+ events.append({'event_type': 'tool_call', 'payload': json.dumps(payload), 'token_count': 0})
391
+
392
+ # Output JSON for curl
393
+ if events:
394
+ print(json.dumps({'events': events}))
395
+ " 2>/dev/null)
396
+
397
+ # POST events if any. Both credential headers are sent; the empty one reads as
398
+ # absent server-side, so one curl serves the daemon path (API key) and the
399
+ # interactive path (connect token) without branching.
400
+ if [ -n "$EVENTS_JSON" ] && [ "$EVENTS_JSON" != "null" ]; then
401
+ curl -s --max-time 3 \
402
+ -X POST \
403
+ -H "Content-Type: application/json" \
404
+ -H "X-API-Key: $API_KEY" \
405
+ -H "X-Connect-Token: $CONNECT_TOKEN" \
406
+ -d "$EVENTS_JSON" \
407
+ "${API_URL}/api/sessions/${SESSION_ID}/events" > /dev/null 2>&1 &
408
+ fi
409
+
410
+ exit 0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Session-capture wiring for daemon shifts (add-multiplayer-sessions §1.5).
3
+ *
4
+ * The capture hook (`session-capture.sh`) has always existed but has never
5
+ * produced anything for the live fleet, because two halves were missing:
6
+ *
7
+ * 1. Nothing created a coding session, so `AGENTHUB_SESSION_ID` was never set
8
+ * and the hook exited at its first guard (`session-capture.sh:14`).
9
+ * 2. Nothing registered the hook for the spawned Claude process — the MCP
10
+ * installers cover only the inbox and activity hooks.
11
+ *
12
+ * This module supplies both, and is the single place the daemon touches for
13
+ * capture concerns. Everything here is **fail-soft**: a capture failure logs and
14
+ * returns a null/empty result so the shift still runs. A shift must never die
15
+ * for an observability reason.
16
+ *
17
+ * Gated on `AGENTHUB_SESSION_STREAMING_ENABLED` (default off, matching the
18
+ * server-side flag added in §1.2). With the flag off, `beginShiftCapture`
19
+ * performs no network call, no filesystem access, and returns `null`, and
20
+ * `applyCaptureEnv` leaves the child env untouched — the shift is byte-for-byte
21
+ * what it is today.
22
+ */
23
+ /** Minimal structural view of the parts of `fetch` this module uses. */
24
+ export interface CaptureResponse {
25
+ ok: boolean;
26
+ status: number;
27
+ json(): Promise<unknown>;
28
+ }
29
+ export type FetchLike = (url: string, init: RequestInit) => Promise<CaptureResponse>;
30
+ /** Default bound on every capture HTTP call. Shift startup must not block on it. */
31
+ export declare const CAPTURE_TIMEOUT_MS = 5000;
32
+ /**
33
+ * Identifies our PostToolUse entry inside a settings object so repeated merges
34
+ * never duplicate it (same marker discipline as activityHookInstall.ts /
35
+ * inboxHookInstall.ts, which key off the CLI filename in the command string).
36
+ */
37
+ export declare const CAPTURE_HOOK_MARKER = "session-capture.sh";
38
+ /** Reason recorded on the session when a shift ends. */
39
+ export type ShiftOutcome = "completed" | "failed" | "spawn_error" | "daemon_shutdown";
40
+ /**
41
+ * Live session streaming kill switch. Default **off** — an unset, empty, or
42
+ * unrecognised value means disabled, so the producer stays silent unless an
43
+ * operator deliberately turns it on.
44
+ */
45
+ export declare function sessionStreamingEnabled(env?: NodeJS.ProcessEnv): boolean;
46
+ /**
47
+ * Merge the capture PostToolUse hook into a settings object **without**
48
+ * discarding anything already there: other hook events, other PostToolUse
49
+ * entries, and every non-`hooks` key are preserved. Idempotent — a settings
50
+ * object that already carries the capture hook is returned unchanged.
51
+ *
52
+ * Returns a new object; the input is never mutated.
53
+ */
54
+ export declare function mergeCaptureHook(base: Record<string, unknown>, command: string, timeoutSeconds?: number): Record<string, unknown>;
55
+ /**
56
+ * Locate `session-capture.sh` relative to the compiled module directory.
57
+ * Ordered most-specific first:
58
+ * 1. `dist/session-capture.sh` — the published npm package (build copies it)
59
+ * 2. `mcp-server/session-capture.sh` — running from a source checkout's dist/
60
+ * 3. `<repo>/.claude/hooks/session-capture.sh` — repo checkout fallback
61
+ * Returns `null` when the script cannot be found; the caller then runs the shift
62
+ * without capture rather than inventing a degraded inline copy.
63
+ */
64
+ export declare function resolveCaptureHookScript(moduleDir: string, exists?: (p: string) => boolean): string | null;
65
+ /** Shell command Claude Code runs for the PostToolUse capture hook. */
66
+ export declare function captureHookCommand(scriptPath: string): string;
67
+ export interface CaptureSettingsDeps {
68
+ exists?: (p: string) => boolean;
69
+ writeFile?: (p: string, data: string) => void;
70
+ log?: (msg: string) => void;
71
+ }
72
+ /**
73
+ * Build a memoised factory that materialises the per-daemon-process settings
74
+ * file registering the capture hook, and returns its path ("" when the hook
75
+ * script is unavailable or the write fails).
76
+ *
77
+ * **Why a dedicated file rather than editing `~/.claude/settings.json`:** the
78
+ * daemon already passes a settings file to `run-claude.sh` (arg 4 → `claude
79
+ * --settings`). Writing a fresh file scoped to the spawned process means no
80
+ * user-owned or workspace-owned settings file is ever rewritten, so there is
81
+ * nothing to clobber — a strictly stronger guarantee than merging in place. The
82
+ * file contains only our hook, so hook entries from the user's own settings
83
+ * sources are neither dropped nor double-registered. `mergeCaptureHook` is used
84
+ * (rather than a literal) so the same non-destructive merge is exercised here
85
+ * and reusable by the in-place installer Phase 3 will need for human sessions.
86
+ */
87
+ export declare function createCaptureSettingsFactory(moduleDir: string, settingsPath: string, deps?: CaptureSettingsDeps): () => string;
88
+ export interface SessionApiOptions {
89
+ apiUrl: string;
90
+ apiKey: string;
91
+ timeoutMs?: number;
92
+ fetchImpl?: FetchLike;
93
+ log?: (msg: string) => void;
94
+ }
95
+ export interface OpenSessionOptions extends SessionApiOptions {
96
+ agentId: string;
97
+ title?: string;
98
+ workingDirectory?: string;
99
+ projectId?: string;
100
+ }
101
+ /**
102
+ * Create the coding session that a shift will be captured under.
103
+ *
104
+ * Uses `POST /api/sessions` (`handleCreateSession`) — the plain record-creating
105
+ * endpoint. Deliberately **not** `POST /api/sessions/start`: that one also calls
106
+ * `Orchestrator().DispatchSession`, i.e. it asks a daemon to start a shift. The
107
+ * daemon calling it would dispatch a second shift for work it is already doing.
108
+ *
109
+ * Never throws and never blocks beyond `timeoutMs`. Returns the session id, or
110
+ * `null` when the session could not be created — the caller then runs the shift
111
+ * uncaptured.
112
+ */
113
+ export declare function openCaptureSession(opts: OpenSessionOptions): Promise<string | null>;
114
+ /**
115
+ * Close a captured session. Uses `POST /api/sessions/{id}/terminate`, the only
116
+ * endpoint that moves a session to a terminal state and stamps a reason.
117
+ * Never throws — a shift that already finished must not fail on cleanup.
118
+ */
119
+ export declare function closeCaptureSession(sessionId: string, reason: ShiftOutcome, opts: SessionApiOptions): Promise<boolean>;
120
+ /**
121
+ * Remove the hook's per-session scratch markers.
122
+ *
123
+ * `session-capture.sh` keys both of its markers by `AGENTHUB_SESSION_ID`
124
+ * (`/tmp/agenthub-transcript-offset-<id>`, `/tmp/agenthub-claude-sid-<id>`), so
125
+ * concurrent shifts on one host never collide — each shift gets its own session
126
+ * id. They are, however, never cleaned up by the script itself, so the daemon
127
+ * removes them when the session closes. Best-effort: absent files are fine.
128
+ */
129
+ export declare function cleanupCaptureMarkers(sessionId: string, tmpDir?: string, removeFile?: (p: string) => void): void;
130
+ /** A shift's live capture wiring. `settingsFile` is "" when no hook was registered. */
131
+ export interface ShiftCapture {
132
+ sessionId: string;
133
+ settingsFile: string;
134
+ }
135
+ export interface BeginShiftCaptureOptions extends OpenSessionOptions {
136
+ env?: NodeJS.ProcessEnv;
137
+ /** Memoised factory from `createCaptureSettingsFactory`. */
138
+ ensureSettingsFile: () => string;
139
+ }
140
+ /**
141
+ * Open a session and register the capture hook for one shift.
142
+ *
143
+ * Returns `null` — meaning "run the shift uncaptured" — when streaming is
144
+ * disabled or the session could not be created. With streaming disabled this
145
+ * returns before any I/O of any kind.
146
+ */
147
+ export declare function beginShiftCapture(opts: BeginShiftCaptureOptions): Promise<ShiftCapture | null>;
148
+ /** Close a shift's session and clean up its hook markers. Never throws. */
149
+ export declare function endShiftCapture(capture: ShiftCapture | null, outcome: ShiftOutcome, opts: SessionApiOptions & {
150
+ tmpDir?: string;
151
+ }): Promise<void>;
152
+ /**
153
+ * Stamp the capture variables onto a shift's child environment.
154
+ *
155
+ * `AGENTHUB_SESSION_ID` is what un-gates `session-capture.sh`; the URL and key
156
+ * are re-asserted because the hook reads them from its own environment and
157
+ * would otherwise silently fall back to the compiled-in default host.
158
+ *
159
+ * With `capture === null` the env object is returned untouched — this is the
160
+ * off-path guarantee, expressed as a single early return.
161
+ */
162
+ export declare function applyCaptureEnv(env: Record<string, string>, capture: ShiftCapture | null, apiUrl: string, apiKey: string): Record<string, string>;
163
+ //# sourceMappingURL=sessionCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionCapture.d.ts","sourceRoot":"","sources":["../src/sessionCapture.ts"],"names":[],"mappings":"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"}