@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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +37 -7
- package/claude/cci.ts +13 -2
- package/claude/inbox-monitor.ts +34 -10
- package/claude/mcp-protocol.ts +1 -1
- package/claude/native-bridge.ts +8 -1
- package/claude/runtime.ts +72 -22
- package/claude/server.ts +48 -12
- package/claude/session-hook.ts +14 -0
- package/claude/session-lifecycle.ts +165 -0
- package/dist/cci.mjs +36 -1
- package/dist/ccim.mjs +36 -1
- package/dist/claude-server.mjs +257 -42
- package/dist/inbox-monitor.mjs +275 -14
- package/dist/session-hook.mjs +202 -0
- package/hooks/hooks.json +26 -0
- package/package.json +2 -1
- package/scripts/build.mjs +7 -0
- package/scripts/verify-packed-runtime.mjs +12 -1
package/dist/inbox-monitor.mjs
CHANGED
|
@@ -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
|
|
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) ?
|
|
278
|
+
return existsSync(path) ? readFileSync2(path, "utf-8") : "";
|
|
34
279
|
} catch {
|
|
35
280
|
return "";
|
|
36
281
|
}
|
|
37
282
|
}
|
|
38
|
-
async function runInboxMonitor(
|
|
39
|
-
let
|
|
283
|
+
async function runInboxMonitor(source, signal) {
|
|
284
|
+
let path = "";
|
|
285
|
+
let emitted = 0;
|
|
40
286
|
for (; ; ) {
|
|
41
287
|
if (signal?.aborted) return;
|
|
42
|
-
const {
|
|
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
|
|
301
|
+
await delay2(POLL_MS);
|
|
49
302
|
}
|
|
50
303
|
}
|
|
51
304
|
async function main() {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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] && (
|
|
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
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -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.
|
|
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
|
|
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");
|