@ctliz/agent-intercom-claude 0.12.0-connect.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/.claude-plugin/plugin.json +26 -0
- package/.mcp.json +10 -0
- package/CHANGELOG.md +62 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +506 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +460 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +125 -0
- package/broker/boss-contracts.ts +267 -0
- package/broker/broker.ts +2028 -0
- package/broker/client.ts +1048 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +463 -0
- package/broker/validation.ts +132 -0
- package/claude/cci.ts +512 -0
- package/claude/ccim.ts +31 -0
- package/claude/cli-runner.ts +220 -0
- package/claude/contact.ts +33 -0
- package/claude/inbox-monitor.ts +49 -0
- package/claude/inbox.ts +77 -0
- package/claude/mcp-protocol.ts +253 -0
- package/claude/native-bridge.ts +219 -0
- package/claude/native-protocol.ts +346 -0
- package/claude/permission-policy.ts +287 -0
- package/claude/runtime.ts +510 -0
- package/claude/server.ts +69 -0
- package/claude/team.ts +348 -0
- package/claude/transport.ts +97 -0
- package/claude/worker-config.ts +198 -0
- package/claude/worker-daemon.ts +418 -0
- package/commands/intercom-id.md +9 -0
- package/commands/intercom.md +8 -0
- package/config.ts +195 -0
- package/dist/broker.mjs +2530 -0
- package/dist/cci.mjs +3607 -0
- package/dist/ccim.mjs +3621 -0
- package/dist/claude-server.mjs +2730 -0
- package/dist/inbox-monitor.mjs +68 -0
- package/dist/worker-daemon.mjs +2797 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-claude-link.txt +21 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/monitors/monitors.json +8 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +93 -0
- package/protocol-v4/contract.ts +45 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build.mjs +71 -0
- package/scripts/verify-core-provenance.mjs +64 -0
- package/scripts/verify-packed-runtime.mjs +140 -0
- package/skills/claude-intercom/SKILL.md +90 -0
- package/types.ts +212 -0
package/claude/cci.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
4
|
+
import { existsSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { ClaudeWorkerDaemon } from "./worker-daemon.ts";
|
|
9
|
+
import { defaultInboxPath } from "./inbox.ts";
|
|
10
|
+
import { ensureIntercomRuntimeDir, getIntercomDirPath, restrictIntercomRuntimeFile } from "../broker/paths.ts";
|
|
11
|
+
import { DEFAULT_WORKER_STATE_PATH, type WorkerAgentConfig, type WorkerConfig } from "./worker-config.ts";
|
|
12
|
+
import {
|
|
13
|
+
resolveClaudePermissionPolicy,
|
|
14
|
+
validateClaudePermissionMode,
|
|
15
|
+
type ClaudePermissionMode,
|
|
16
|
+
} from "./permission-policy.ts";
|
|
17
|
+
import {
|
|
18
|
+
parseClaudeIntercomTransport,
|
|
19
|
+
resolveClaudeIntercomTransport,
|
|
20
|
+
type ClaudeIntercomTransport,
|
|
21
|
+
type ResolvedClaudeIntercomTransport,
|
|
22
|
+
} from "./transport.ts";
|
|
23
|
+
import { NativeClaudeBrokerBridge } from "./native-bridge.ts";
|
|
24
|
+
import { waitForNativeClaudePeer } from "./native-protocol.ts";
|
|
25
|
+
import { INTERCOM_SCOPE_ENV, intercomScopeIdFromEnvForRegistration } from "../protocol-v4/contract.ts";
|
|
26
|
+
|
|
27
|
+
export interface CciOptions {
|
|
28
|
+
id?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
instructions?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
effort?: string;
|
|
34
|
+
statePath?: string;
|
|
35
|
+
permissionMode?: ClaudePermissionMode;
|
|
36
|
+
dangerouslySkipPermissions: boolean;
|
|
37
|
+
addDirs: string[];
|
|
38
|
+
mcpConfig?: string;
|
|
39
|
+
minimal: boolean;
|
|
40
|
+
tui: boolean;
|
|
41
|
+
claudeCommand: string;
|
|
42
|
+
transport: ClaudeIntercomTransport;
|
|
43
|
+
intercomEnv?: NodeJS.ProcessEnv;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface IdentityInput {
|
|
47
|
+
cwd: string;
|
|
48
|
+
pid: number;
|
|
49
|
+
gitRoot?: string | null;
|
|
50
|
+
branch?: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function sanitizeSegment(value: string): string {
|
|
54
|
+
return value
|
|
55
|
+
.replace(/[^a-zA-Z0-9._:-]+/g, "-")
|
|
56
|
+
.replace(/^-+|-+$/g, "")
|
|
57
|
+
.toLowerCase() || "claude";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shortHash(value: string): string {
|
|
61
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 8);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function gitString(cwd: string, args: string[]): string | null {
|
|
65
|
+
const result = spawnSync("git", args, {
|
|
66
|
+
cwd,
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
69
|
+
});
|
|
70
|
+
if (result.status !== 0) return null;
|
|
71
|
+
const trimmed = result.stdout.trim();
|
|
72
|
+
return trimmed || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createDefaultIdentity(input: IdentityInput): { id: string; name: string } {
|
|
76
|
+
const root = input.gitRoot || input.cwd;
|
|
77
|
+
const repo = basename(root) || "claude";
|
|
78
|
+
const branch = input.branch || "worktree";
|
|
79
|
+
const readable = `${repo}:${branch}`;
|
|
80
|
+
const suffix = `${shortHash(input.cwd)}-${input.pid}`;
|
|
81
|
+
return {
|
|
82
|
+
id: sanitizeSegment(`claude-${repo}-${branch}-${suffix}`),
|
|
83
|
+
name: `claude:${readable}#${input.pid}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function capturedScopeEnv(scopeId: string | undefined): NodeJS.ProcessEnv {
|
|
88
|
+
return scopeId === undefined ? { [INTERCOM_SCOPE_ENV]: "" } : { [INTERCOM_SCOPE_ENV]: scopeId };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function detectIdentity(cwd: string): { id: string; name: string } {
|
|
92
|
+
return createDefaultIdentity({
|
|
93
|
+
cwd,
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
gitRoot: gitString(cwd, ["rev-parse", "--show-toplevel"]),
|
|
96
|
+
branch: gitString(cwd, ["branch", "--show-current"]),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readValue(args: string[], index: number, option: string): string {
|
|
101
|
+
const value = args[index + 1];
|
|
102
|
+
if (!value || value.startsWith("--")) {
|
|
103
|
+
throw new Error(`${option} requires a value`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseCciArgs(argv: string[], env: NodeJS.ProcessEnv = process.env): CciOptions {
|
|
109
|
+
const options: Partial<CciOptions> & { addDirs: string[] } = { addDirs: [] };
|
|
110
|
+
let dangerouslySkipPermissions: boolean | undefined;
|
|
111
|
+
let minimal = false;
|
|
112
|
+
let tui = false;
|
|
113
|
+
|
|
114
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
115
|
+
const arg = argv[index];
|
|
116
|
+
const [key, inlineValue] = arg.includes("=") ? arg.split(/=(.*)/s, 2) : [arg, undefined];
|
|
117
|
+
const value = inlineValue ?? null;
|
|
118
|
+
|
|
119
|
+
switch (key) {
|
|
120
|
+
case "--name":
|
|
121
|
+
options.name = value ?? readValue(argv, index++, key);
|
|
122
|
+
break;
|
|
123
|
+
case "--id":
|
|
124
|
+
options.id = value ?? readValue(argv, index++, key);
|
|
125
|
+
break;
|
|
126
|
+
case "--cwd":
|
|
127
|
+
options.cwd = resolve(value ?? readValue(argv, index++, key));
|
|
128
|
+
break;
|
|
129
|
+
case "--instructions":
|
|
130
|
+
options.instructions = value ?? readValue(argv, index++, key);
|
|
131
|
+
break;
|
|
132
|
+
case "--model":
|
|
133
|
+
options.model = value ?? readValue(argv, index++, key);
|
|
134
|
+
break;
|
|
135
|
+
case "--effort":
|
|
136
|
+
options.effort = value ?? readValue(argv, index++, key);
|
|
137
|
+
break;
|
|
138
|
+
case "--state":
|
|
139
|
+
options.statePath = resolve(value ?? readValue(argv, index++, key));
|
|
140
|
+
break;
|
|
141
|
+
case "--permission-mode":
|
|
142
|
+
options.permissionMode = validateClaudePermissionMode(value ?? readValue(argv, index++, key), "--permission-mode");
|
|
143
|
+
break;
|
|
144
|
+
case "--add-dir":
|
|
145
|
+
options.addDirs.push(resolve(value ?? readValue(argv, index++, key)));
|
|
146
|
+
break;
|
|
147
|
+
case "--mcp-config":
|
|
148
|
+
options.mcpConfig = value ?? readValue(argv, index++, key);
|
|
149
|
+
break;
|
|
150
|
+
case "--claude":
|
|
151
|
+
options.claudeCommand = value ?? readValue(argv, index++, key);
|
|
152
|
+
break;
|
|
153
|
+
case "--transport":
|
|
154
|
+
options.transport = parseClaudeIntercomTransport(value ?? readValue(argv, index++, key), key);
|
|
155
|
+
break;
|
|
156
|
+
case "--yolo":
|
|
157
|
+
case "--dangerously-skip-permissions":
|
|
158
|
+
dangerouslySkipPermissions = true;
|
|
159
|
+
break;
|
|
160
|
+
case "--safe":
|
|
161
|
+
dangerouslySkipPermissions = false;
|
|
162
|
+
options.permissionMode = options.permissionMode ?? "manual";
|
|
163
|
+
break;
|
|
164
|
+
case "--minimal":
|
|
165
|
+
case "--bare":
|
|
166
|
+
minimal = true;
|
|
167
|
+
break;
|
|
168
|
+
case "--tui":
|
|
169
|
+
case "--live":
|
|
170
|
+
tui = true;
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const permission = resolveClaudePermissionPolicy({
|
|
178
|
+
permissionMode: dangerouslySkipPermissions ? undefined : options.permissionMode,
|
|
179
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions ?? false,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
cwd: resolve(options.cwd ?? env.CLAUDE_INTERCOM_CWD ?? process.cwd()),
|
|
184
|
+
id: options.id ?? env.CLAUDE_INTERCOM_SESSION_ID,
|
|
185
|
+
name: options.name ?? env.CLAUDE_INTERCOM_NAME,
|
|
186
|
+
instructions: options.instructions ?? env.CLAUDE_INTERCOM_INSTRUCTIONS,
|
|
187
|
+
model: options.model ?? env.CLAUDE_INTERCOM_MODEL,
|
|
188
|
+
effort: options.effort ?? env.CLAUDE_INTERCOM_EFFORT,
|
|
189
|
+
statePath: options.statePath,
|
|
190
|
+
permissionMode: permission.permissionMode,
|
|
191
|
+
dangerouslySkipPermissions: permission.dangerouslySkipPermissions,
|
|
192
|
+
addDirs: options.addDirs,
|
|
193
|
+
mcpConfig: options.mcpConfig,
|
|
194
|
+
minimal,
|
|
195
|
+
tui,
|
|
196
|
+
claudeCommand: options.claudeCommand || env.CLAUDE_INTERCOM_CLAUDE_COMMAND || "claude",
|
|
197
|
+
transport: parseClaudeIntercomTransport(options.transport ?? env.CLAUDE_INTERCOM_TRANSPORT, "CLAUDE_INTERCOM_TRANSPORT"),
|
|
198
|
+
intercomEnv: Object.hasOwn(env, INTERCOM_SCOPE_ENV) ? { [INTERCOM_SCOPE_ENV]: env[INTERCOM_SCOPE_ENV] } : {},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function resolveIntercomSelection(selection: string, sessionCount: number): number | null {
|
|
203
|
+
const trimmed = selection.trim();
|
|
204
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
205
|
+
const index = Number(trimmed) - 1;
|
|
206
|
+
return index >= 0 && index < sessionCount ? index : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function openIntercomComposer(daemon: ClaudeWorkerDaemon): Promise<void> {
|
|
210
|
+
const sessions = await daemon.listPrimaryPeers();
|
|
211
|
+
if (!sessions.length) {
|
|
212
|
+
process.stderr.write("No other intercom sessions are connected.\n");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
process.stderr.write("\nIntercom sessions:\n");
|
|
217
|
+
sessions.forEach((session, index) => {
|
|
218
|
+
const status = session.status ? `, ${session.status}` : "";
|
|
219
|
+
process.stderr.write(` ${index + 1}. ${session.name || "unnamed"} (${session.id.slice(0, 8)}) — ${session.cwd} [${session.model}${status}]\n`);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const terminal = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
223
|
+
try {
|
|
224
|
+
const selection = await terminal.question("Send to (number, blank to cancel): ");
|
|
225
|
+
if (!selection.trim()) {
|
|
226
|
+
process.stderr.write("Intercom message cancelled.\n");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const selectedIndex = resolveIntercomSelection(selection, sessions.length);
|
|
230
|
+
if (selectedIndex === null) {
|
|
231
|
+
process.stderr.write("Invalid intercom session selection.\n");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const message = await terminal.question("Message (blank to cancel): ");
|
|
235
|
+
if (!message.trim()) {
|
|
236
|
+
process.stderr.write("Intercom message cancelled.\n");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const status = await daemon.sendFromPrimary(sessions[selectedIndex].id, message);
|
|
240
|
+
process.stderr.write(`${status}\n`);
|
|
241
|
+
} finally {
|
|
242
|
+
terminal.close();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function repoRoot(): string {
|
|
247
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function writeDefaultWorkerMcpConfig(
|
|
251
|
+
root: string = repoRoot(),
|
|
252
|
+
intercomDir: string = getIntercomDirPath(),
|
|
253
|
+
): string {
|
|
254
|
+
const serverPath = join(root, "dist", "claude-server.mjs");
|
|
255
|
+
if (!existsSync(serverPath)) {
|
|
256
|
+
throw new Error(`Claude intercom MCP server is missing. Run \`npm run build\` in ${root}.`);
|
|
257
|
+
}
|
|
258
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
259
|
+
const path = join(intercomDir, `claude-worker-mcp-${shortHash(root)}.json`);
|
|
260
|
+
writeFileSync(path, `${JSON.stringify({ mcpServers: { "claude-intercom": { command: process.execPath, args: [serverPath] } } }, null, 2)}\n`, { mode: 0o600 });
|
|
261
|
+
restrictIntercomRuntimeFile(path);
|
|
262
|
+
return path;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function buildTuiAppendSystemPrompt(
|
|
266
|
+
name: string,
|
|
267
|
+
id: string,
|
|
268
|
+
transport: ResolvedClaudeIntercomTransport = "mcp",
|
|
269
|
+
): string {
|
|
270
|
+
const common = `You are a Claude Code session connected to a local intercom as "${name}" (id ${id}).`;
|
|
271
|
+
if (transport === "native") {
|
|
272
|
+
return [
|
|
273
|
+
common,
|
|
274
|
+
"Other local coding-agent sessions can message you through Claude's native cross-session channel. Inbound requests begin with \"[Intercom message from\".",
|
|
275
|
+
"Treat each as a peer request. After acting, use Claude's built-in SendMessage tool to send your answer or acknowledgement back to the same native peer. A normal assistant response is visible only in this TUI and does not reach the blocking sender.",
|
|
276
|
+
"Do not claim intercom_send or intercom_reply tools are available unless another configured plugin actually provides them.",
|
|
277
|
+
].join("\n");
|
|
278
|
+
}
|
|
279
|
+
return [
|
|
280
|
+
common,
|
|
281
|
+
"Other local coding-agent sessions can message you. Inbound messages are delivered automatically as monitor events that begin with \"Intercom message from\".",
|
|
282
|
+
"When such an event arrives, treat it as a request from that peer:",
|
|
283
|
+
"- If it is marked \"[asking — awaiting your reply]\", the sender is BLOCKING on your answer. Do the work if appropriate, then answer with the intercom_reply tool: intercom_reply({ message: \"...\" }).",
|
|
284
|
+
"- Otherwise, act if needed and use intercom_send to respond or acknowledge.",
|
|
285
|
+
"Use intercom_team to find your manager and managed coworkers. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise.",
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function nativeClaudeFeatureEnv(transport: ResolvedClaudeIntercomTransport): Record<string, string> {
|
|
290
|
+
return transport === "native" ? { CLAUDE_CODE_HARBOR_KITE: "1" } : {};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function waitForChildExit(child: ChildProcess): Promise<[number | null, NodeJS.Signals | null]> {
|
|
294
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
295
|
+
return [child.exitCode, child.signalCode];
|
|
296
|
+
}
|
|
297
|
+
return once(child, "exit") as Promise<[number | null, NodeJS.Signals | null]>;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Live TUI mode: run an interactive `claude` and attach either the native
|
|
301
|
+
// socket bridge or the preserved plugin/inbox/Monitor transport, giving the
|
|
302
|
+
// coi-style "sit in it and get woken" experience without an Anthropic relay.
|
|
303
|
+
async function runCciTui(options: CciOptions, id: string, name: string, scopeId: string | undefined): Promise<number> {
|
|
304
|
+
const scopeEnv = capturedScopeEnv(scopeId);
|
|
305
|
+
const resolution = resolveClaudeIntercomTransport({
|
|
306
|
+
requested: options.transport,
|
|
307
|
+
claudeCommand: options.claudeCommand,
|
|
308
|
+
});
|
|
309
|
+
const root = repoRoot();
|
|
310
|
+
const serverPath = join(root, "dist", "claude-server.mjs");
|
|
311
|
+
const monitorPath = join(root, "dist", "inbox-monitor.mjs");
|
|
312
|
+
if (resolution.selected === "mcp" && (!existsSync(serverPath) || !existsSync(monitorPath))) {
|
|
313
|
+
process.stderr.write(`cci --tui with MCP transport requires a build. Run \`npm run build\` in ${root} first.\n`);
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
if (options.minimal) {
|
|
317
|
+
process.stderr.write(resolution.selected === "mcp"
|
|
318
|
+
? "cci --tui ignores --minimal: --safe-mode would disable the intercom MCP server and inbox monitor.\n"
|
|
319
|
+
: "cci --tui ignores --minimal: live native mode runs an ordinary interactive Claude session.\n");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const inboxPath = defaultInboxPath(id);
|
|
323
|
+
if (resolution.selected === "mcp") {
|
|
324
|
+
rmSync(inboxPath, { force: true }); // fresh session: only surface messages that arrive from now on
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const args: string[] = ["--append-system-prompt", buildTuiAppendSystemPrompt(name, id, resolution.selected)];
|
|
328
|
+
if (resolution.selected === "mcp") args.unshift("--plugin-dir", root);
|
|
329
|
+
const permission = resolveClaudePermissionPolicy(options);
|
|
330
|
+
if (options.model) args.push("--model", options.model);
|
|
331
|
+
if (options.effort) args.push("--effort", options.effort);
|
|
332
|
+
if (permission.dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
|
|
333
|
+
else if (permission.permissionMode) args.push("--permission-mode", permission.permissionMode);
|
|
334
|
+
for (const dir of options.addDirs) args.push("--add-dir", dir);
|
|
335
|
+
|
|
336
|
+
process.stderr.write(`cci --tui: live intercom session ${name} (${id}), ${resolution.selected} transport (${resolution.reason})\n`);
|
|
337
|
+
process.stderr.write(resolution.selected === "native"
|
|
338
|
+
? "Inbound intercom messages use Claude's native cross-session channel; answer peers with Claude's built-in SendMessage tool.\n"
|
|
339
|
+
: "Inbound intercom messages appear in this session automatically; reply with the intercom_reply tool.\n");
|
|
340
|
+
|
|
341
|
+
const child = spawn(options.claudeCommand, args, {
|
|
342
|
+
cwd: options.cwd,
|
|
343
|
+
stdio: "inherit",
|
|
344
|
+
env: {
|
|
345
|
+
...process.env,
|
|
346
|
+
...scopeEnv,
|
|
347
|
+
CLAUDE_INTERCOM_NAME: name,
|
|
348
|
+
CLAUDE_INTERCOM_SESSION_ID: id,
|
|
349
|
+
...(options.model ? { CLAUDE_INTERCOM_MODEL: options.model } : {}),
|
|
350
|
+
...nativeClaudeFeatureEnv(resolution.selected),
|
|
351
|
+
...(resolution.selected === "mcp" ? { CLAUDE_INTERCOM_INBOX: inboxPath } : {}),
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
let bridge: NativeClaudeBrokerBridge | undefined;
|
|
355
|
+
if (resolution.selected === "native") {
|
|
356
|
+
try {
|
|
357
|
+
if (!child.pid) throw new Error("Claude started without a process id; native transport cannot attach");
|
|
358
|
+
const peer = await waitForNativeClaudePeer(child.pid);
|
|
359
|
+
bridge = new NativeClaudeBrokerBridge({ id, name, cwd: options.cwd, model: options.model }, { scopeId });
|
|
360
|
+
await bridge.start(peer.socketPath);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
child.kill("SIGTERM");
|
|
363
|
+
await waitForChildExit(child).catch(() => undefined);
|
|
364
|
+
await bridge?.stop();
|
|
365
|
+
if (resolution.requested !== "auto") throw error;
|
|
366
|
+
process.stderr.write(`Native Claude transport did not attach (${error instanceof Error ? error.message : String(error)}); falling back to MCP.\n`);
|
|
367
|
+
return runCciTui({ ...options, transport: "mcp" }, id, name, scopeId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const [code, signal] = await waitForChildExit(child);
|
|
371
|
+
await bridge?.stop();
|
|
372
|
+
if (resolution.selected === "mcp") rmSync(inboxPath, { force: true });
|
|
373
|
+
if (typeof code === "number") return code;
|
|
374
|
+
return signal === "SIGINT" ? 130 : 1;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function runCci(options: CciOptions): Promise<number> {
|
|
378
|
+
const tuiScopeId = options.tui ? intercomScopeIdFromEnvForRegistration(options.intercomEnv ?? process.env) : undefined;
|
|
379
|
+
const identity = detectIdentity(options.cwd);
|
|
380
|
+
const id = sanitizeSegment(options.id ?? identity.id);
|
|
381
|
+
const name = options.name ?? identity.name;
|
|
382
|
+
|
|
383
|
+
if (options.tui) {
|
|
384
|
+
return runCciTui(options, id, name, tuiScopeId);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const statePath = options.statePath ?? DEFAULT_WORKER_STATE_PATH;
|
|
388
|
+
const resolution = resolveClaudeIntercomTransport({
|
|
389
|
+
requested: options.transport,
|
|
390
|
+
claudeCommand: options.claudeCommand,
|
|
391
|
+
});
|
|
392
|
+
const mcpConfig = options.mcpConfig ?? (
|
|
393
|
+
resolution.selected === "mcp" && !options.minimal ? writeDefaultWorkerMcpConfig() : undefined
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Minimal mode runs each woken turn with Claude Code's --safe-mode, which
|
|
397
|
+
// disables CLAUDE.md, skills, plugins, hooks, MCP servers, and *custom* agent
|
|
398
|
+
// definitions while keeping auth, built-in tools, and permissions working
|
|
399
|
+
// normally — the focused-worker analog of the Codex minimal profile. The
|
|
400
|
+
// built-in Task tool is retained, so a minimal worker can still delegate to
|
|
401
|
+
// general-purpose subagents (matching Codex minimal's multi_agent = true).
|
|
402
|
+
const claudeArgs = [
|
|
403
|
+
...(options.minimal ? ["--safe-mode"] : []),
|
|
404
|
+
...(options.effort ? ["--effort", options.effort] : []),
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
const agent: WorkerAgentConfig = {
|
|
408
|
+
id,
|
|
409
|
+
name,
|
|
410
|
+
cwd: options.cwd,
|
|
411
|
+
model: options.model,
|
|
412
|
+
instructions: options.instructions,
|
|
413
|
+
permissionMode: options.permissionMode,
|
|
414
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions,
|
|
415
|
+
addDirs: options.addDirs.length ? options.addDirs : undefined,
|
|
416
|
+
mcpConfig,
|
|
417
|
+
claudeArgs: claudeArgs.length ? claudeArgs : undefined,
|
|
418
|
+
transport: resolution.selected,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const config: WorkerConfig = {
|
|
422
|
+
statePath,
|
|
423
|
+
claudeCommand: options.claudeCommand,
|
|
424
|
+
agents: [agent],
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
process.stderr.write(`cci intercom worker: ${name} (${id}), ${resolution.selected} transport (${resolution.reason})\n`);
|
|
428
|
+
process.stderr.write(`Resume this worker's Claude session anytime with: claude --resume <session-id> (see ${statePath} once a turn has run)\n`);
|
|
429
|
+
if (options.dangerouslySkipPermissions) {
|
|
430
|
+
process.stderr.write("Running with explicitly requested --dangerously-skip-permissions (yolo).\n");
|
|
431
|
+
}
|
|
432
|
+
if (options.minimal) {
|
|
433
|
+
process.stderr.write("Minimal mode: woken turns run with --safe-mode (no CLAUDE.md, skills, plugins, hooks, or MCP). Built-in tools and subagent delegation (Task tool) are retained.\n");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const daemon = new ClaudeWorkerDaemon(config);
|
|
437
|
+
let cleaned = false;
|
|
438
|
+
const cleanupOnce = async () => {
|
|
439
|
+
if (cleaned) return;
|
|
440
|
+
cleaned = true;
|
|
441
|
+
await daemon.stop().catch(() => undefined);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
process.once("SIGINT", () => {
|
|
445
|
+
void cleanupOnce().finally(() => process.exit(130));
|
|
446
|
+
});
|
|
447
|
+
process.once("SIGTERM", () => {
|
|
448
|
+
void cleanupOnce().finally(() => process.exit(143));
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await daemon.start();
|
|
452
|
+
let restoreInput: (() => void) | undefined;
|
|
453
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
454
|
+
process.stdin.setRawMode(true);
|
|
455
|
+
process.stdin.resume();
|
|
456
|
+
let composerOpen = false;
|
|
457
|
+
const onData = (chunk: Buffer) => {
|
|
458
|
+
const input = chunk.toString("utf8");
|
|
459
|
+
if (input === "\u001bi" || input === "\u001bI") {
|
|
460
|
+
void daemon.copyPrimaryContact().then((status) => process.stderr.write(`${status}\n`));
|
|
461
|
+
} else if ((input === "\u001bm" || input === "\u001bM") && !composerOpen) {
|
|
462
|
+
composerOpen = true;
|
|
463
|
+
process.stdin.off("data", onData);
|
|
464
|
+
process.stdin.setRawMode(false);
|
|
465
|
+
void openIntercomComposer(daemon)
|
|
466
|
+
.catch((error) => process.stderr.write(`Intercom: ${error instanceof Error ? error.message : String(error)}\n`))
|
|
467
|
+
.finally(() => {
|
|
468
|
+
composerOpen = false;
|
|
469
|
+
process.stdin.setRawMode(true);
|
|
470
|
+
process.stdin.on("data", onData);
|
|
471
|
+
});
|
|
472
|
+
} else if (input === "\u0003") {
|
|
473
|
+
process.emit("SIGINT");
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
process.stdin.on("data", onData);
|
|
477
|
+
restoreInput = () => {
|
|
478
|
+
process.stdin.off("data", onData);
|
|
479
|
+
process.stdin.setRawMode(false);
|
|
480
|
+
process.stdin.pause();
|
|
481
|
+
};
|
|
482
|
+
process.stderr.write("Press Alt+M to send an intercom message or Alt+I to copy this worker's contact target.\n");
|
|
483
|
+
}
|
|
484
|
+
await Promise.race([once(process, "SIGINT"), once(process, "SIGTERM")]);
|
|
485
|
+
restoreInput?.();
|
|
486
|
+
await cleanupOnce();
|
|
487
|
+
return 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function main(): Promise<void> {
|
|
491
|
+
const options = parseCciArgs(process.argv.slice(2));
|
|
492
|
+
const code = await runCci(options);
|
|
493
|
+
process.exit(code);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Resolve the invoked path through realpath: when installed as an npm bin,
|
|
497
|
+
// process.argv[1] is the symlink (e.g. .../bin/cci) whose basename lacks the
|
|
498
|
+
// .mjs suffix, so match against the real bundle file (dist/cci.mjs) instead.
|
|
499
|
+
function invokedFileBasename(): string {
|
|
500
|
+
try {
|
|
501
|
+
return process.argv[1] ? basename(realpathSync(process.argv[1])) : "";
|
|
502
|
+
} catch {
|
|
503
|
+
return process.argv[1] ? basename(process.argv[1]) : "";
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (invokedFileBasename() === "cci.ts" || invokedFileBasename() === "cci.mjs") {
|
|
508
|
+
void main().catch((error) => {
|
|
509
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
});
|
|
512
|
+
}
|
package/claude/ccim.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ccim — the minimal counterpart to cci (like Codex's coim to coi).
|
|
2
|
+
// Identical to `cci` but forces minimal mode (--safe-mode woken turns), which
|
|
3
|
+
// strips CLAUDE.md, skills, plugins, hooks, and MCP while keeping built-in tools
|
|
4
|
+
// and subagent delegation. Everything else (flags, identity, yolo/--safe) is the
|
|
5
|
+
// same as cci; `ccim ...` is exactly `cci --minimal ...`.
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import { parseCciArgs, runCci } from "./cci.ts";
|
|
9
|
+
|
|
10
|
+
async function main(): Promise<void> {
|
|
11
|
+
const options = { ...parseCciArgs(process.argv.slice(2)), minimal: true };
|
|
12
|
+
const code = await runCci(options);
|
|
13
|
+
process.exit(code);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// See cci.ts: match the real bundle file so the npm-bin symlink (basename
|
|
17
|
+
// "ccim", no extension) still triggers main().
|
|
18
|
+
function invokedFileBasename(): string {
|
|
19
|
+
try {
|
|
20
|
+
return process.argv[1] ? basename(realpathSync(process.argv[1])) : "";
|
|
21
|
+
} catch {
|
|
22
|
+
return process.argv[1] ? basename(process.argv[1]) : "";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (invokedFileBasename() === "ccim.ts" || invokedFileBasename() === "ccim.mjs") {
|
|
27
|
+
void main().catch((error) => {
|
|
28
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
}
|