@holon-run/agentinbox 0.1.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.
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TerminalDispatcher = void 0;
7
+ exports.detectTerminalContext = detectTerminalContext;
8
+ exports.renderAgentPrompt = renderAgentPrompt;
9
+ exports.assignedAgentIdFromContext = assignedAgentIdFromContext;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_util_1 = require("node:util");
12
+ const node_crypto_1 = __importDefault(require("node:crypto"));
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_os_1 = __importDefault(require("node:os"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
17
+ function detectTerminalContext(env = process.env) {
18
+ const tmuxPaneId = normalizeOptionalString(env.TMUX_PANE);
19
+ const termProgram = normalizeOptionalString(env.TERM_PROGRAM);
20
+ const tty = detectTty(env);
21
+ const itermSessionId = normalizeItermSessionId(env.ITERM_SESSION_ID ?? env.TERM_SESSION_ID);
22
+ const runtime = detectRuntimeContext(env);
23
+ if (tmuxPaneId) {
24
+ return {
25
+ runtimeKind: runtime.runtimeKind,
26
+ runtimeSessionId: runtime.runtimeSessionId,
27
+ backend: "tmux",
28
+ tmuxPaneId,
29
+ tty,
30
+ termProgram,
31
+ itermSessionId,
32
+ };
33
+ }
34
+ if (itermSessionId || termProgram === "iTerm.app") {
35
+ return {
36
+ runtimeKind: runtime.runtimeKind,
37
+ runtimeSessionId: runtime.runtimeSessionId,
38
+ backend: "iterm2",
39
+ tty,
40
+ termProgram,
41
+ itermSessionId,
42
+ };
43
+ }
44
+ throw new Error("unable to detect a supported terminal context");
45
+ }
46
+ function renderAgentPrompt(input) {
47
+ const base = `AgentInbox: ${input.newItemCount} new items arrived in inbox ${input.inboxId}.`;
48
+ if (input.summary) {
49
+ return `${base} Summary: ${input.summary}. Please read the inbox, process them, and ack when finished.`;
50
+ }
51
+ return `${base} Please read the inbox, process them, and ack when finished.`;
52
+ }
53
+ function assignedAgentIdFromContext(input) {
54
+ const primary = normalizeOptionalString(input.runtimeSessionId)
55
+ ?? normalizeOptionalString(input.tmuxPaneId)
56
+ ?? normalizeOptionalString(input.itermSessionId)
57
+ ?? normalizeOptionalString(input.tty);
58
+ if (!primary) {
59
+ throw new Error("unable to derive agentId from terminal context");
60
+ }
61
+ const normalized = primary.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase();
62
+ if (normalized.length > 0) {
63
+ return `agent_${input.runtimeKind ?? "unknown"}_${normalized.slice(0, 40)}`;
64
+ }
65
+ const digest = node_crypto_1.default.createHash("sha256").update(primary).digest("hex").slice(0, 16);
66
+ return `agent_${input.runtimeKind ?? "unknown"}_${digest}`;
67
+ }
68
+ class TerminalDispatcher {
69
+ execAsync;
70
+ options;
71
+ constructor(execAsync = execFileAsync, options = {}) {
72
+ this.execAsync = execAsync;
73
+ this.options = options;
74
+ }
75
+ async dispatch(target, prompt) {
76
+ if (target.backend === "tmux") {
77
+ if (!target.tmuxPaneId) {
78
+ throw new Error(`tmux terminal target ${target.targetId} is missing tmuxPaneId`);
79
+ }
80
+ await this.execAsync("tmux", ["send-keys", "-t", target.tmuxPaneId, prompt, "Enter"]);
81
+ return;
82
+ }
83
+ if (target.backend === "iterm2") {
84
+ await dispatchToIterm2(target, prompt, this.execAsync, this.options.iterm2ApiPath);
85
+ return;
86
+ }
87
+ throw new Error(`unsupported terminal backend: ${String(target.backend)}`);
88
+ }
89
+ async probe(target) {
90
+ try {
91
+ if (target.backend === "tmux") {
92
+ if (!target.tmuxPaneId) {
93
+ return false;
94
+ }
95
+ const result = await this.execAsync("tmux", ["display-message", "-p", "-t", target.tmuxPaneId, "#{pane_id}"]);
96
+ return result.stdout.trim() === target.tmuxPaneId;
97
+ }
98
+ if (target.backend === "iterm2") {
99
+ const sessionId = normalizeOptionalString(target.itermSessionId);
100
+ if (!sessionId) {
101
+ return false;
102
+ }
103
+ const it2api = resolveIterm2ApiPath(this.options.iterm2ApiPath);
104
+ const result = await this.execAsync(it2api, ["list-sessions"]);
105
+ return result.stdout.includes(sessionId);
106
+ }
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ return false;
112
+ }
113
+ }
114
+ exports.TerminalDispatcher = TerminalDispatcher;
115
+ function detectTty(env) {
116
+ const direct = tryReadCurrentTty();
117
+ if (direct) {
118
+ return direct;
119
+ }
120
+ const parent = tryReadParentTty(env.PPID);
121
+ return parent;
122
+ }
123
+ function tryReadCurrentTty() {
124
+ try {
125
+ const tty = (0, node_child_process_1.execFileSync)("tty", [], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
126
+ if (!tty || tty === "not a tty") {
127
+ return null;
128
+ }
129
+ return tty;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ function tryReadParentTty(ppid) {
136
+ if (!ppid) {
137
+ return null;
138
+ }
139
+ try {
140
+ const tty = (0, node_child_process_1.execFileSync)("ps", ["-o", "tty=", "-p", ppid], {
141
+ encoding: "utf8",
142
+ stdio: ["ignore", "pipe", "ignore"],
143
+ }).trim();
144
+ if (!tty || tty === "??") {
145
+ return null;
146
+ }
147
+ return tty.startsWith("/dev/") ? tty : `/dev/${tty}`;
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ function normalizeItermSessionId(raw) {
154
+ const value = normalizeOptionalString(raw);
155
+ if (!value) {
156
+ return null;
157
+ }
158
+ const parts = value.split(":");
159
+ return parts[parts.length - 1] || null;
160
+ }
161
+ function detectRuntimeContext(env) {
162
+ const codexThreadId = normalizeOptionalString(env.CODEX_THREAD_ID);
163
+ if (codexThreadId) {
164
+ return {
165
+ runtimeKind: "codex",
166
+ runtimeSessionId: codexThreadId,
167
+ };
168
+ }
169
+ const claudeSessionId = normalizeOptionalString(env.CLAUDE_CODE_SESSION_ID
170
+ ?? env.CLAUDECODE_SESSION_ID
171
+ ?? env.CLAUDE_SESSION_ID);
172
+ if (claudeSessionId) {
173
+ return {
174
+ runtimeKind: "claude_code",
175
+ runtimeSessionId: claudeSessionId,
176
+ };
177
+ }
178
+ // Fallback: try to detect Claude Code from filesystem
179
+ const claudeCodeFromFs = detectClaudeCodeFromFileSystem(env);
180
+ if (claudeCodeFromFs) {
181
+ return claudeCodeFromFs;
182
+ }
183
+ return {
184
+ runtimeKind: "unknown",
185
+ runtimeSessionId: null,
186
+ };
187
+ }
188
+ function detectClaudeCodeFromFileSystem(env) {
189
+ try {
190
+ // Walk up the process tree to find a claude process
191
+ const claudePid = findClaudeProcessInAncestry();
192
+ if (!claudePid) {
193
+ return null;
194
+ }
195
+ // Read Claude Code session file
196
+ const sessionFile = node_path_1.default.join(node_os_1.default.homedir(), ".claude", "sessions", `${String(claudePid)}.json`);
197
+ if (!node_fs_1.default.existsSync(sessionFile)) {
198
+ return null;
199
+ }
200
+ const sessionContent = node_fs_1.default.readFileSync(sessionFile, "utf8");
201
+ const sessionData = JSON.parse(sessionContent);
202
+ // Validate session data structure
203
+ if (!sessionData.sessionId || typeof sessionData.sessionId !== "string") {
204
+ return null;
205
+ }
206
+ // Verify this session matches current working directory
207
+ // This helps avoid picking up wrong claude sessions
208
+ if (sessionData.cwd) {
209
+ const currentProject = node_path_1.default.basename(process.cwd());
210
+ const sessionProject = node_path_1.default.basename(sessionData.cwd);
211
+ if (currentProject !== sessionProject) {
212
+ return null;
213
+ }
214
+ }
215
+ return {
216
+ runtimeKind: "claude_code",
217
+ runtimeSessionId: sessionData.sessionId,
218
+ };
219
+ }
220
+ catch {
221
+ // Silently fail if filesystem detection fails
222
+ return null;
223
+ }
224
+ }
225
+ function findClaudeProcessInAncestry() {
226
+ let currentPid = process.ppid;
227
+ const maxIterations = 10; // Prevent infinite loops
228
+ let iterations = 0;
229
+ while (currentPid && iterations < maxIterations) {
230
+ const processName = tryGetProcessName(currentPid);
231
+ if (processName?.includes("claude")) {
232
+ return currentPid;
233
+ }
234
+ // Get parent of current process
235
+ const parentPid = tryGetParentPid(currentPid);
236
+ if (!parentPid) {
237
+ break;
238
+ }
239
+ currentPid = parentPid;
240
+ iterations++;
241
+ }
242
+ return null;
243
+ }
244
+ function tryGetParentPid(pid) {
245
+ try {
246
+ const output = (0, node_child_process_1.execFileSync)("ps", ["-p", String(pid), "-o", "ppid="], {
247
+ encoding: "utf8",
248
+ stdio: ["ignore", "pipe", "ignore"],
249
+ });
250
+ const ppid = parseInt(output.trim(), 10);
251
+ return isNaN(ppid) || ppid === 0 ? null : ppid;
252
+ }
253
+ catch {
254
+ return null;
255
+ }
256
+ }
257
+ function tryGetProcessName(pid) {
258
+ try {
259
+ const output = (0, node_child_process_1.execFileSync)("ps", ["-p", String(pid), "-o", "comm="], {
260
+ encoding: "utf8",
261
+ stdio: ["ignore", "pipe", "ignore"],
262
+ });
263
+ return output.trim();
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ function normalizeOptionalString(value) {
270
+ if (!value) {
271
+ return null;
272
+ }
273
+ const trimmed = value.trim();
274
+ return trimmed.length > 0 ? trimmed : null;
275
+ }
276
+ async function dispatchToIterm2(target, prompt, execAsync, iterm2ApiPath) {
277
+ const sessionId = normalizeOptionalString(target.itermSessionId);
278
+ if (!sessionId) {
279
+ throw new Error(`iTerm2 terminal target ${target.targetId} is missing itermSessionId`);
280
+ }
281
+ const it2api = resolveIterm2ApiPath(iterm2ApiPath);
282
+ await execAsync(it2api, ["send-text", sessionId, prompt]);
283
+ // Background Codex/Claude sessions only submit reliably when Return is sent
284
+ // in a second call rather than concatenated to the original prompt payload.
285
+ await execAsync(it2api, ["send-text", sessionId, "\r"]);
286
+ }
287
+ function resolveIterm2ApiPath(override) {
288
+ if (override) {
289
+ return override;
290
+ }
291
+ const candidates = [
292
+ "/Applications/iTerm.app/Contents/Resources/it2api",
293
+ "/Applications/iTerm.app/Contents/Resources/utilities/it2api",
294
+ ];
295
+ for (const candidate of candidates) {
296
+ if (node_fs_1.default.existsSync(candidate)) {
297
+ return candidate;
298
+ }
299
+ }
300
+ throw new Error("unable to locate iTerm2 it2api helper");
301
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.nowIso = nowIso;
7
+ exports.generateId = generateId;
8
+ exports.parseJsonArg = parseJsonArg;
9
+ exports.asObject = asObject;
10
+ exports.jsonResponse = jsonResponse;
11
+ const node_crypto_1 = __importDefault(require("node:crypto"));
12
+ function nowIso() {
13
+ return new Date().toISOString();
14
+ }
15
+ function generateId(prefix) {
16
+ return `${prefix}_${node_crypto_1.default.randomUUID()}`;
17
+ }
18
+ function parseJsonArg(raw) {
19
+ if (!raw) {
20
+ return {};
21
+ }
22
+ const parsed = JSON.parse(raw);
23
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
24
+ throw new Error("expected a JSON object");
25
+ }
26
+ return parsed;
27
+ }
28
+ function asObject(value) {
29
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
30
+ return {};
31
+ }
32
+ return value;
33
+ }
34
+ function jsonResponse(data) {
35
+ return JSON.stringify(data, null, 2);
36
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@holon-run/agentinbox",
3
+ "version": "0.1.0",
4
+ "description": "Local event subscription and delivery service for agents.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/holon-run/agentinbox.git"
9
+ },
10
+ "homepage": "https://agentinbox.holon.run",
11
+ "bugs": {
12
+ "url": "https://github.com/holon-run/agentinbox/issues"
13
+ },
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "dist/src/",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "bin": {
26
+ "agentinbox": "dist/src/cli.js"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "dev": "node --watch --loader ts-node/esm src/cli.ts",
31
+ "docs:dev": "mdorigin dev --root docs/site",
32
+ "docs:search": "mdorigin build search --root docs/site",
33
+ "docs:index": "mdorigin build index --root docs/site",
34
+ "docs:build": "npm run docs:search && mdorigin build cloudflare --root docs/site --search dist/search",
35
+ "docs:build:cloudflare": "npm run docs:build",
36
+ "docs:deploy": "npm run docs:index && npm run docs:build && npx wrangler@4 deploy --config wrangler.jsonc",
37
+ "prepack": "npm run build",
38
+ "test": "node --require ts-node/register --test test/*.test.ts"
39
+ },
40
+ "dependencies": {
41
+ "@holon-run/uxc-daemon-client": "^0.13.3",
42
+ "jexl": "^2.3.0",
43
+ "sql.js": "^1.13.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.5.2",
47
+ "@types/sql.js": "^1.4.9",
48
+ "mdorigin": "^0.5.0",
49
+ "ts-node": "^10.9.2",
50
+ "typescript": "^5.9.2"
51
+ }
52
+ }