@ctliz/agent-intercom-claude 0.13.1 → 0.14.1

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.
@@ -1,11 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // claude/inbox-monitor.ts
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { setTimeout as delay } from "node:timers/promises";
6
- import { basename } from "node:path";
4
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
5
+ import { setTimeout as delay2 } from "node:timers/promises";
6
+ import { basename as basename3 } from "node:path";
7
+
8
+ // claude/inbox.ts
9
+ import { dirname, join as join2 } from "node:path";
10
+
11
+ // broker/paths.ts
12
+ import { isAbsolute, join, resolve } from "path";
13
+ import { homedir } from "os";
14
+ function getAgentDirPath(env = process.env, homeDir = homedir(), cwd = process.cwd()) {
15
+ const configured = env.PI_CODING_AGENT_DIR?.trim();
16
+ if (!configured) {
17
+ return join(homeDir, ".pi/agent");
18
+ }
19
+ return isAbsolute(configured) ? configured : resolve(cwd, configured);
20
+ }
21
+ function getIntercomDirPath(agentDir = getAgentDirPath()) {
22
+ return join(agentDir, "intercom");
23
+ }
7
24
 
8
25
  // claude/inbox.ts
26
+ function sanitizeInboxSegment(value) {
27
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "session";
28
+ }
29
+ function defaultInboxPath(sessionId, intercomDir = getIntercomDirPath()) {
30
+ return join2(intercomDir, `inbox-${sanitizeInboxSegment(sessionId)}.jsonl`);
31
+ }
9
32
  function formatInboxLine(entry) {
10
33
  const who = entry.fromName ? `${entry.fromName} (${entry.fromId.slice(0, 8)})` : entry.fromId;
11
34
  const kind = entry.expectsReply ? " [asking \u2014 awaiting your reply]" : "";
@@ -26,37 +49,275 @@ function inboxBatchFrom(content, alreadyEmitted) {
26
49
  return { entries, total };
27
50
  }
28
51
 
52
+ // claude/session-lifecycle.ts
53
+ import { execFileSync } from "node:child_process";
54
+ import { closeSync, openSync, readFileSync, readSync, renameSync, statSync, writeFileSync } from "node:fs";
55
+ import { basename as basename2, dirname as dirname3, join as join4 } from "node:path";
56
+ import { StringDecoder } from "node:string_decoder";
57
+ import { setTimeout as delay } from "node:timers/promises";
58
+
59
+ // claude/runtime.ts
60
+ import { randomUUID, createHash } from "crypto";
61
+ import { basename } from "path";
62
+ import { cwd as processCwd } from "process";
63
+
64
+ // broker/client.ts
65
+ import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
66
+
67
+ // broker/framing.ts
68
+ var MAX_FRAME_BYTES = 1024 * 1024;
69
+
70
+ // protocol-v4/contract.ts
71
+ import {
72
+ INTERCOM_PROTOCOL_NAME,
73
+ INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
74
+ INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
75
+ INTERCOM_PROTOCOL_V4_VECTORS,
76
+ INTERCOM_PROTOCOL_VERSION,
77
+ INTERCOM_SCOPE_ENV,
78
+ INTERCOM_SCOPE_ID_PATTERN,
79
+ INTERCOM_SCOPE_ID_PATTERN_SOURCE,
80
+ intercomScopeIdFromEnv,
81
+ parseIntercomScopeId,
82
+ sameIntercomScope
83
+ } from "@ctliz/agent-intercom-core/protocol-v4";
84
+
85
+ // broker/boss-contracts.ts
86
+ import {
87
+ BOSS_CAPABILITY_FEATURE_DIGEST,
88
+ BOSS_POLICY_SEMANTICS_HASH,
89
+ BOSS_RUN_FEATURE,
90
+ BOSS_RUN_FEATURE_CONTRACT,
91
+ INTERCOM_BASE_PROTOCOL_VERSION,
92
+ evaluateBrokerCompatibility,
93
+ parseBossControlEnvelope,
94
+ parseBossParticipantBinding,
95
+ parseBossParticipantCredentialEnvelope,
96
+ parseBossPolicyPrincipal,
97
+ parseBossRunFeatureContract,
98
+ parseBrokerCapabilityAdvertisement,
99
+ isTerminalParticipantState,
100
+ parseParticipantState,
101
+ parseWorkerIdentityV2
102
+ } from "@ctliz/agent-intercom-core/boss";
103
+ import {
104
+ ContractValidationError,
105
+ assertExactKeys,
106
+ assertRecord,
107
+ readHexDigest
108
+ } from "@ctliz/agent-intercom-core/canonical";
109
+
110
+ // broker/spawn.ts
111
+ import { join as join3, dirname as dirname2 } from "path";
112
+ import { fileURLToPath } from "url";
113
+ import { POLICY_SEMANTICS_HASH as POLICY_SEMANTICS_HASH2, POLICY_SEMANTICS_VERSION as POLICY_SEMANTICS_VERSION2 } from "@ctliz/agent-intercom-core";
114
+ var INTERCOM_DIR = getIntercomDirPath();
115
+ var EXTENSION_DIR = join3(dirname2(fileURLToPath(import.meta.url)), "..");
116
+ var BROKER_PID = join3(INTERCOM_DIR, "broker.pid");
117
+ var BROKER_SPAWN_LOCK = join3(INTERCOM_DIR, "broker.spawn.lock");
118
+
119
+ // config.ts
120
+ var DEFAULT_ASK_TIMEOUT_MS = 45 * 1e3;
121
+ var MAX_ASK_TIMEOUT_MS = 120 * 1e3;
122
+
123
+ // claude/team.ts
124
+ import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
125
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
126
+ var BOSS_NONTERMINAL_STATES = new Set(
127
+ PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
128
+ );
129
+
130
+ // claude/runtime.ts
131
+ function shortHash(value) {
132
+ return createHash("sha256").update(value).digest("hex").slice(0, 8);
133
+ }
134
+ var GENERIC_SESSION_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
135
+ function buildClaudeRuntimeIdentity(env = process.env, cwd = env.PWD || processCwd(), pid = process.pid) {
136
+ let sessionId = env.CLAUDE_INTERCOM_SESSION_ID?.trim() || env.CLAUDE_PEER_ID?.trim();
137
+ if (!sessionId && env.AGENT_INTERCOM_SESSION_ID !== void 0) {
138
+ const raw = env.AGENT_INTERCOM_SESSION_ID.trim();
139
+ if (raw) {
140
+ if (!GENERIC_SESSION_ID_REGEX.test(raw)) {
141
+ throw new Error("Invalid AGENT_INTERCOM_SESSION_ID: must match ^[A-Za-z0-9_-]{1,128}$");
142
+ }
143
+ sessionId = raw;
144
+ }
145
+ }
146
+ if (!sessionId) {
147
+ sessionId = `claude-${pid}-${shortHash(cwd)}`;
148
+ }
149
+ const cwdName = basename(cwd) || "workspace";
150
+ const name = env.CLAUDE_INTERCOM_NAME?.trim() || env.CLAUDE_PEER_NAME?.trim() || (env.AGENT_INTERCOM_SESSION_NAME !== void 0 && env.AGENT_INTERCOM_SESSION_NAME.trim() ? env.AGENT_INTERCOM_SESSION_NAME.trim() : void 0) || `claude-${cwdName}-${pid}`;
151
+ return {
152
+ sessionId,
153
+ name,
154
+ cwd,
155
+ model: env.CLAUDE_INTERCOM_MODEL?.trim() || env.CLAUDE_MODEL?.trim() || "claude",
156
+ startedAt: Date.now()
157
+ };
158
+ }
159
+
160
+ // claude/session-lifecycle.ts
161
+ function findClaudeHost(pid = process.ppid) {
162
+ for (let depth = 0; pid > 1 && depth < 16; depth++) {
163
+ let parts;
164
+ try {
165
+ parts = execFileSync(
166
+ "ps",
167
+ ["-p", String(pid), "-o", "ppid=", "-o", "lstart=", "-o", "comm="],
168
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
169
+ ).trim().split(/\s+/);
170
+ } catch {
171
+ return void 0;
172
+ }
173
+ const command = basename2(parts.slice(6).join(" "));
174
+ if (/^claude(?:\.exe)?$/i.test(command)) {
175
+ return { pid, startedAt: parts.slice(1, 6).join(" ") };
176
+ }
177
+ if (!/^(sh|bash|zsh|dash|fish)$/.test(command)) return void 0;
178
+ pid = Number(parts[0]);
179
+ }
180
+ return void 0;
181
+ }
182
+ function claudeSessionMetadataPath(host) {
183
+ return join4(getIntercomDirPath(), `claude-host-${host.pid}.json`);
184
+ }
185
+ function readClaudeSessionMetadata(path, host) {
186
+ try {
187
+ const data = JSON.parse(readFileSync(path, "utf8"));
188
+ if (data.hostStartedAt !== host.startedAt || typeof data.sessionId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(data.sessionId) || typeof data.cwd !== "string" || typeof data.transcriptPath !== "string" || typeof data.startedAt !== "number" || typeof data.ended !== "boolean" || data.name !== void 0 && typeof data.name !== "string" || data.model !== void 0 && typeof data.model !== "string") return void 0;
189
+ return data;
190
+ } catch {
191
+ return void 0;
192
+ }
193
+ }
194
+ function claudeIdentityForSession(metadata, env = process.env, pid = process.pid) {
195
+ const identity = buildClaudeRuntimeIdentity(env, metadata?.cwd ?? env.PWD ?? process.cwd(), pid);
196
+ if (!metadata) return identity;
197
+ if (!env.CLAUDE_INTERCOM_SESSION_ID?.trim() && !env.CLAUDE_PEER_ID?.trim() && !env.AGENT_INTERCOM_SESSION_ID?.trim()) {
198
+ identity.sessionId = `claude-${metadata.sessionId}`;
199
+ }
200
+ identity.name = metadata.name?.trim() || identity.name;
201
+ if (!env.CLAUDE_INTERCOM_MODEL?.trim()) identity.model = metadata.model?.trim() || identity.model;
202
+ identity.startedAt = metadata.startedAt;
203
+ return identity;
204
+ }
205
+ var ClaudeSessionReader = class {
206
+ constructor(path, host) {
207
+ this.path = path;
208
+ this.host = host;
209
+ }
210
+ path;
211
+ host;
212
+ transcriptPath = "";
213
+ sessionId = "";
214
+ offset = 0;
215
+ partial = "";
216
+ decoder = new StringDecoder("utf8");
217
+ name;
218
+ startedAt = 0;
219
+ read() {
220
+ const metadata = readClaudeSessionMetadata(this.path, this.host);
221
+ if (!metadata || metadata.ended) return metadata;
222
+ if (metadata.sessionId !== this.sessionId || metadata.transcriptPath !== this.transcriptPath || metadata.startedAt !== this.startedAt) {
223
+ this.sessionId = metadata.sessionId;
224
+ this.transcriptPath = metadata.transcriptPath;
225
+ this.startedAt = metadata.startedAt;
226
+ this.name = void 0;
227
+ this.partial = "";
228
+ this.decoder = new StringDecoder("utf8");
229
+ try {
230
+ this.offset = Math.max(0, statSync(this.transcriptPath).size - 64 * 1024);
231
+ } catch {
232
+ this.offset = 0;
233
+ }
234
+ }
235
+ let fd;
236
+ try {
237
+ fd = openSync(this.transcriptPath, "r");
238
+ const size = statSync(this.transcriptPath).size;
239
+ if (size < this.offset) {
240
+ this.offset = 0;
241
+ this.partial = "";
242
+ this.decoder = new StringDecoder("utf8");
243
+ }
244
+ const buffer = Buffer.alloc(Math.min(size - this.offset, 64 * 1024));
245
+ const count = readSync(fd, buffer, 0, buffer.length, this.offset);
246
+ this.offset += count;
247
+ const lines = (this.partial + this.decoder.write(buffer.subarray(0, count))).split("\n");
248
+ this.partial = lines.pop() ?? "";
249
+ if (this.partial.length > 64 * 1024) this.partial = "";
250
+ for (const line of lines) {
251
+ if (!line.includes('"custom-title"')) continue;
252
+ try {
253
+ const record = JSON.parse(line);
254
+ if (record.type === "custom-title" && record.sessionId === metadata.sessionId && typeof record.customTitle === "string" && record.customTitle.trim()) this.name = record.customTitle;
255
+ } catch {
256
+ }
257
+ }
258
+ } catch {
259
+ } finally {
260
+ if (fd !== void 0) closeSync(fd);
261
+ }
262
+ return { ...metadata, name: this.name ?? metadata.name };
263
+ }
264
+ };
265
+ async function waitForClaudeSession(reader) {
266
+ for (let attempt = 0; attempt < 20; attempt++) {
267
+ const metadata = reader.read();
268
+ if (metadata) return metadata;
269
+ await delay(50);
270
+ }
271
+ return reader.read();
272
+ }
273
+
29
274
  // claude/inbox-monitor.ts
30
275
  var POLL_MS = 1e3;
31
276
  function readContent(path) {
32
277
  try {
33
- return existsSync(path) ? readFileSync(path, "utf-8") : "";
278
+ return existsSync(path) ? readFileSync2(path, "utf-8") : "";
34
279
  } catch {
35
280
  return "";
36
281
  }
37
282
  }
38
- async function runInboxMonitor(path, signal) {
39
- let emitted = inboxBatchFrom(readContent(path), 0).total;
283
+ async function runInboxMonitor(source, signal) {
284
+ let path = "";
285
+ let emitted = 0;
40
286
  for (; ; ) {
41
287
  if (signal?.aborted) return;
42
- const { entries, total } = inboxBatchFrom(readContent(path), emitted);
288
+ const next = typeof source === "string" ? { path: source } : source();
289
+ const content = readContent(next.path);
290
+ if (path !== next.path) {
291
+ path = next.path;
292
+ const backlog = inboxBatchFrom(content, 0);
293
+ emitted = next.startedAt === void 0 ? backlog.total : backlog.entries.filter((entry) => entry.ts < next.startedAt).length;
294
+ }
295
+ const { entries, total } = inboxBatchFrom(content, emitted);
43
296
  for (const entry of entries) {
44
297
  process.stdout.write(`${formatInboxLine(entry)}
45
298
  `);
46
299
  }
47
300
  emitted = total;
48
- await delay(POLL_MS);
301
+ await delay2(POLL_MS);
49
302
  }
50
303
  }
51
304
  async function main() {
52
- const path = process.env.CLAUDE_INTERCOM_INBOX || process.argv[2];
53
- if (!path) {
54
- process.stderr.write("inbox-monitor: no inbox path (set CLAUDE_INTERCOM_INBOX or pass a path)\n");
55
- process.exit(1);
305
+ const explicitPath = process.env.CLAUDE_INTERCOM_INBOX || process.argv[2];
306
+ const host = findClaudeHost();
307
+ if (!host) {
308
+ if (!explicitPath) throw new Error("Cannot determine this Claude session's inbox");
309
+ await runInboxMonitor(explicitPath);
310
+ return;
56
311
  }
57
- await runInboxMonitor(path);
312
+ const reader = new ClaudeSessionReader(claudeSessionMetadataPath(host), host);
313
+ await waitForClaudeSession(reader);
314
+ await runInboxMonitor(() => {
315
+ const metadata = reader.read();
316
+ const identity = claudeIdentityForSession(metadata, process.env, host.pid);
317
+ return { path: explicitPath || defaultInboxPath(identity.sessionId), startedAt: metadata?.startedAt };
318
+ });
58
319
  }
59
- if (process.argv[1] && (basename(process.argv[1]) === "inbox-monitor.ts" || basename(process.argv[1]) === "inbox-monitor.mjs")) {
320
+ if (process.argv[1] && (basename3(process.argv[1]) === "inbox-monitor.ts" || basename3(process.argv[1]) === "inbox-monitor.mjs")) {
60
321
  void main().catch((error) => {
61
322
  process.stderr.write(`inbox-monitor: ${error instanceof Error ? error.message : String(error)}
62
323
  `);
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ // claude/session-hook.ts
4
+ import { readFileSync as readFileSync3 } from "node:fs";
5
+
6
+ // claude/session-lifecycle.ts
7
+ import { execFileSync } from "node:child_process";
8
+ import { closeSync, openSync, readFileSync as readFileSync2, readSync, renameSync, statSync, writeFileSync } from "node:fs";
9
+ import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
10
+
11
+ // broker/paths.ts
12
+ import { chmodSync, mkdirSync, readFileSync } from "fs";
13
+ import { isAbsolute, join, resolve } from "path";
14
+ import { homedir } from "os";
15
+ var INTERCOM_DIR_MODE = 448;
16
+ function getAgentDirPath(env = process.env, homeDir = homedir(), cwd = process.cwd()) {
17
+ const configured = env.PI_CODING_AGENT_DIR?.trim();
18
+ if (!configured) {
19
+ return join(homeDir, ".pi/agent");
20
+ }
21
+ return isAbsolute(configured) ? configured : resolve(cwd, configured);
22
+ }
23
+ function getIntercomDirPath(agentDir = getAgentDirPath()) {
24
+ return join(agentDir, "intercom");
25
+ }
26
+ function ensureIntercomRuntimeDir(intercomDir = getIntercomDirPath(), platform = process.platform) {
27
+ mkdirSync(intercomDir, { recursive: true, mode: INTERCOM_DIR_MODE });
28
+ if (platform !== "win32") {
29
+ chmodSync(intercomDir, INTERCOM_DIR_MODE);
30
+ }
31
+ }
32
+
33
+ // claude/runtime.ts
34
+ import { randomUUID, createHash } from "crypto";
35
+ import { basename } from "path";
36
+ import { cwd as processCwd } from "process";
37
+
38
+ // broker/client.ts
39
+ import { POLICY_SEMANTICS_HASH, POLICY_SEMANTICS_VERSION } from "@ctliz/agent-intercom-core";
40
+
41
+ // broker/framing.ts
42
+ var MAX_FRAME_BYTES = 1024 * 1024;
43
+
44
+ // protocol-v4/contract.ts
45
+ import {
46
+ INTERCOM_PROTOCOL_NAME,
47
+ INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
48
+ INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
49
+ INTERCOM_PROTOCOL_V4_VECTORS,
50
+ INTERCOM_PROTOCOL_VERSION,
51
+ INTERCOM_SCOPE_ENV,
52
+ INTERCOM_SCOPE_ID_PATTERN,
53
+ INTERCOM_SCOPE_ID_PATTERN_SOURCE,
54
+ intercomScopeIdFromEnv,
55
+ parseIntercomScopeId,
56
+ sameIntercomScope
57
+ } from "@ctliz/agent-intercom-core/protocol-v4";
58
+
59
+ // broker/boss-contracts.ts
60
+ import {
61
+ BOSS_CAPABILITY_FEATURE_DIGEST,
62
+ BOSS_POLICY_SEMANTICS_HASH,
63
+ BOSS_RUN_FEATURE,
64
+ BOSS_RUN_FEATURE_CONTRACT,
65
+ INTERCOM_BASE_PROTOCOL_VERSION,
66
+ evaluateBrokerCompatibility,
67
+ parseBossControlEnvelope,
68
+ parseBossParticipantBinding,
69
+ parseBossParticipantCredentialEnvelope,
70
+ parseBossPolicyPrincipal,
71
+ parseBossRunFeatureContract,
72
+ parseBrokerCapabilityAdvertisement,
73
+ isTerminalParticipantState,
74
+ parseParticipantState,
75
+ parseWorkerIdentityV2
76
+ } from "@ctliz/agent-intercom-core/boss";
77
+ import {
78
+ ContractValidationError,
79
+ assertExactKeys,
80
+ assertRecord,
81
+ readHexDigest
82
+ } from "@ctliz/agent-intercom-core/canonical";
83
+
84
+ // broker/spawn.ts
85
+ import { join as join2, dirname } from "path";
86
+ import { fileURLToPath } from "url";
87
+ import { POLICY_SEMANTICS_HASH as POLICY_SEMANTICS_HASH2, POLICY_SEMANTICS_VERSION as POLICY_SEMANTICS_VERSION2 } from "@ctliz/agent-intercom-core";
88
+ var INTERCOM_DIR = getIntercomDirPath();
89
+ var EXTENSION_DIR = join2(dirname(fileURLToPath(import.meta.url)), "..");
90
+ var BROKER_PID = join2(INTERCOM_DIR, "broker.pid");
91
+ var BROKER_SPAWN_LOCK = join2(INTERCOM_DIR, "broker.spawn.lock");
92
+
93
+ // config.ts
94
+ var DEFAULT_ASK_TIMEOUT_MS = 45 * 1e3;
95
+ var MAX_ASK_TIMEOUT_MS = 120 * 1e3;
96
+
97
+ // claude/team.ts
98
+ import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
99
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
100
+ var BOSS_NONTERMINAL_STATES = new Set(
101
+ PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
102
+ );
103
+
104
+ // claude/runtime.ts
105
+ function shortHash(value) {
106
+ return createHash("sha256").update(value).digest("hex").slice(0, 8);
107
+ }
108
+ var GENERIC_SESSION_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
109
+ function buildClaudeRuntimeIdentity(env = process.env, cwd = env.PWD || processCwd(), pid = process.pid) {
110
+ let sessionId = env.CLAUDE_INTERCOM_SESSION_ID?.trim() || env.CLAUDE_PEER_ID?.trim();
111
+ if (!sessionId && env.AGENT_INTERCOM_SESSION_ID !== void 0) {
112
+ const raw = env.AGENT_INTERCOM_SESSION_ID.trim();
113
+ if (raw) {
114
+ if (!GENERIC_SESSION_ID_REGEX.test(raw)) {
115
+ throw new Error("Invalid AGENT_INTERCOM_SESSION_ID: must match ^[A-Za-z0-9_-]{1,128}$");
116
+ }
117
+ sessionId = raw;
118
+ }
119
+ }
120
+ if (!sessionId) {
121
+ sessionId = `claude-${pid}-${shortHash(cwd)}`;
122
+ }
123
+ const cwdName = basename(cwd) || "workspace";
124
+ const name = env.CLAUDE_INTERCOM_NAME?.trim() || env.CLAUDE_PEER_NAME?.trim() || (env.AGENT_INTERCOM_SESSION_NAME !== void 0 && env.AGENT_INTERCOM_SESSION_NAME.trim() ? env.AGENT_INTERCOM_SESSION_NAME.trim() : void 0) || `claude-${cwdName}-${pid}`;
125
+ return {
126
+ sessionId,
127
+ name,
128
+ cwd,
129
+ model: env.CLAUDE_INTERCOM_MODEL?.trim() || env.CLAUDE_MODEL?.trim() || "claude",
130
+ startedAt: Date.now()
131
+ };
132
+ }
133
+
134
+ // claude/session-lifecycle.ts
135
+ function findClaudeHost(pid = process.ppid) {
136
+ for (let depth = 0; pid > 1 && depth < 16; depth++) {
137
+ let parts;
138
+ try {
139
+ parts = execFileSync(
140
+ "ps",
141
+ ["-p", String(pid), "-o", "ppid=", "-o", "lstart=", "-o", "comm="],
142
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
143
+ ).trim().split(/\s+/);
144
+ } catch {
145
+ return void 0;
146
+ }
147
+ const command = basename2(parts.slice(6).join(" "));
148
+ if (/^claude(?:\.exe)?$/i.test(command)) {
149
+ return { pid, startedAt: parts.slice(1, 6).join(" ") };
150
+ }
151
+ if (!/^(sh|bash|zsh|dash|fish)$/.test(command)) return void 0;
152
+ pid = Number(parts[0]);
153
+ }
154
+ return void 0;
155
+ }
156
+ function claudeSessionMetadataPath(host) {
157
+ return join3(getIntercomDirPath(), `claude-host-${host.pid}.json`);
158
+ }
159
+ function readClaudeSessionMetadata(path, host) {
160
+ try {
161
+ const data = JSON.parse(readFileSync2(path, "utf8"));
162
+ if (data.hostStartedAt !== host.startedAt || typeof data.sessionId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(data.sessionId) || typeof data.cwd !== "string" || typeof data.transcriptPath !== "string" || typeof data.startedAt !== "number" || typeof data.ended !== "boolean" || data.name !== void 0 && typeof data.name !== "string" || data.model !== void 0 && typeof data.model !== "string") return void 0;
163
+ return data;
164
+ } catch {
165
+ return void 0;
166
+ }
167
+ }
168
+ function publishClaudeSessionMetadata(input, host, env = process.env, path = claudeSessionMetadataPath(host)) {
169
+ if (typeof input.session_id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(input.session_id)) return;
170
+ const previous = readClaudeSessionMetadata(path, host);
171
+ const ended = input.hook_event_name === "SessionEnd";
172
+ if (ended && previous?.sessionId !== input.session_id) return;
173
+ if (!ended && (typeof input.cwd !== "string" || typeof input.transcript_path !== "string")) return;
174
+ const name = typeof input.session_title === "string" && input.session_title.trim() ? input.session_title : buildClaudeRuntimeIdentity(env, input.cwd, host.pid).name;
175
+ const data = ended ? { ...previous, ended: true } : {
176
+ hostStartedAt: host.startedAt,
177
+ sessionId: input.session_id,
178
+ cwd: input.cwd,
179
+ transcriptPath: input.transcript_path,
180
+ name,
181
+ ...typeof input.model === "string" ? { model: input.model } : {},
182
+ startedAt: Date.now(),
183
+ ended: false
184
+ };
185
+ ensureIntercomRuntimeDir(dirname2(path));
186
+ const temp = `${path}.${process.pid}.tmp`;
187
+ writeFileSync(temp, `${JSON.stringify(data)}
188
+ `, { mode: 384 });
189
+ renameSync(temp, path);
190
+ }
191
+
192
+ // claude/session-hook.ts
193
+ try {
194
+ const input = JSON.parse(readFileSync3(0, "utf8"));
195
+ const host = findClaudeHost();
196
+ if (host && input && typeof input === "object" && !Array.isArray(input)) {
197
+ publishClaudeSessionMetadata(input, host);
198
+ }
199
+ } catch (error) {
200
+ process.stderr.write(`claude-intercom hook: ${error instanceof Error ? error.message : String(error)}
201
+ `);
202
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/session-hook.mjs\"",
9
+ "timeout": 5
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionEnd": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/session-hook.mjs\"",
20
+ "timeout": 5
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-claude",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "description": "Claude Code MCP plugin and wakeable worker for local messaging with Pi, Codex, OpenCode, Grok Build, and AGY agents.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -27,6 +27,7 @@
27
27
  "!scripts/build-protected-provider.mjs",
28
28
  "skills/**/*",
29
29
  "commands/**/*",
30
+ "hooks/**/*",
30
31
  "monitors/**/*",
31
32
  "types.ts",
32
33
  "config.ts",
package/scripts/build.mjs CHANGED
@@ -46,6 +46,12 @@ const buildResults = await Promise.all([
46
46
  outfile: "dist/ccim.mjs",
47
47
  banner: { js: "#!/usr/bin/env node" },
48
48
  }),
49
+ build({
50
+ ...common,
51
+ entryPoints: ["claude/session-hook.ts"],
52
+ outfile: "dist/session-hook.mjs",
53
+ banner: { js: "#!/usr/bin/env node" },
54
+ }),
49
55
  build({
50
56
  ...common,
51
57
  entryPoints: ["claude/inbox-monitor.ts"],
@@ -68,4 +74,5 @@ await Promise.all([
68
74
  chmod("dist/cci.mjs", 0o755),
69
75
  chmod("dist/ccim.mjs", 0o755),
70
76
  chmod("dist/inbox-monitor.mjs", 0o755),
77
+ chmod("dist/session-hook.mjs", 0o755),
71
78
  ]);
@@ -13,8 +13,10 @@ const coreConsumers = new Set([
13
13
  "ccim.mjs",
14
14
  "claude-server.mjs",
15
15
  "worker-daemon.mjs",
16
+ "session-hook.mjs",
17
+ "inbox-monitor.mjs",
16
18
  ]);
17
- const bundles = [...coreConsumers, "inbox-monitor.mjs"];
19
+ const bundles = [...coreConsumers];
18
20
 
19
21
  function tarString(buffer, offset, length) {
20
22
  const end = buffer.indexOf(0, offset);
@@ -112,6 +114,15 @@ try {
112
114
  throw new Error(`monitors.json missing intercom-inbox command referencing dist/inbox-monitor.mjs: ${JSON.stringify(inboxMonitor)}`);
113
115
  }
114
116
 
117
+ const hooksEntry = entries.get("package/hooks/hooks.json");
118
+ if (!hooksEntry) throw new Error("Packed adapter is missing hooks/hooks.json");
119
+ const hooks = JSON.parse(hooksEntry.toString("utf8")).hooks;
120
+ for (const event of ["SessionStart", "SessionEnd"]) {
121
+ if (!hooks?.[event]?.[0]?.hooks?.[0]?.command?.includes("/dist/session-hook.mjs")) {
122
+ throw new Error(`Packed ${event} hook does not reference session-hook.mjs`);
123
+ }
124
+ }
125
+
115
126
  // 4. Assert Plugin -> Skills & Commands exist in packed archive
116
127
  if (typeof plugin.skills !== "string") throw new Error("plugin.json missing skills declaration");
117
128
  const skillEntry = entries.get("package/skills/claude-intercom/SKILL.md");