ftown-bridge 0.19.39 → 0.19.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/cli-client.d.ts +17 -0
  2. package/dist/cli-client.js +76 -0
  3. package/dist/cli-client.js.map +1 -0
  4. package/dist/cli-entry.d.ts +2 -0
  5. package/dist/cli-entry.js +22 -0
  6. package/dist/cli-entry.js.map +1 -0
  7. package/dist/codex-installer.d.ts +1 -1
  8. package/dist/codex-installer.js +7 -6
  9. package/dist/codex-installer.js.map +1 -1
  10. package/dist/command-rpc.js +0 -1
  11. package/dist/command-rpc.js.map +1 -1
  12. package/dist/create-ftown-session.d.ts +1 -6
  13. package/dist/create-ftown-session.js +3 -25
  14. package/dist/create-ftown-session.js.map +1 -1
  15. package/dist/cursor-hook-installer.d.ts +1 -1
  16. package/dist/cursor-hook-installer.js +6 -5
  17. package/dist/cursor-hook-installer.js.map +1 -1
  18. package/dist/ensure-ftown-path.js +1 -1
  19. package/dist/env-commands.d.ts +32 -0
  20. package/dist/env-commands.js +162 -0
  21. package/dist/env-commands.js.map +1 -0
  22. package/dist/ftown-cli.d.ts +2 -6
  23. package/dist/ftown-cli.js +25 -79
  24. package/dist/ftown-cli.js.map +1 -1
  25. package/dist/ftown-env-cli.d.ts +2 -31
  26. package/dist/ftown-env-cli.js +7 -176
  27. package/dist/ftown-env-cli.js.map +1 -1
  28. package/dist/ftown-home.d.ts +24 -10
  29. package/dist/ftown-home.js +19 -41
  30. package/dist/ftown-home.js.map +1 -1
  31. package/dist/ftown-sessions-cli.d.ts +1 -1
  32. package/dist/ftown-sessions-cli.js +6 -499
  33. package/dist/ftown-sessions-cli.js.map +1 -1
  34. package/dist/harness-cli.d.ts +1 -1
  35. package/dist/harness-cli.js +6 -626
  36. package/dist/harness-cli.js.map +1 -1
  37. package/dist/harness-commands.d.ts +2 -0
  38. package/dist/harness-commands.js +576 -0
  39. package/dist/harness-commands.js.map +1 -0
  40. package/dist/harness-installer.d.ts +1 -10
  41. package/dist/harness-installer.js +13 -166
  42. package/dist/harness-installer.js.map +1 -1
  43. package/dist/harness-registry.d.ts +1 -27
  44. package/dist/harness-registry.js +1 -20
  45. package/dist/harness-registry.js.map +1 -1
  46. package/dist/hook-installer.d.ts +1 -1
  47. package/dist/hook-installer.js +5 -4
  48. package/dist/hook-installer.js.map +1 -1
  49. package/dist/index.js +10 -108
  50. package/dist/index.js.map +1 -1
  51. package/dist/install-ftown-command-cli.d.ts +3 -3
  52. package/dist/install-ftown-command-cli.js +67 -22
  53. package/dist/install-ftown-command-cli.js.map +1 -1
  54. package/dist/install-ftown-skill.d.ts +6 -0
  55. package/dist/install-ftown-skill.js +111 -10
  56. package/dist/install-ftown-skill.js.map +1 -1
  57. package/dist/install-notify-script.d.ts +1 -1
  58. package/dist/install-notify-script.js +7 -6
  59. package/dist/install-notify-script.js.map +1 -1
  60. package/dist/mail-delivery.d.ts +1 -1
  61. package/dist/mail-delivery.js +3 -5
  62. package/dist/mail-delivery.js.map +1 -1
  63. package/dist/mcp/server.js +1 -1
  64. package/dist/mcp/server.js.map +1 -1
  65. package/dist/muse-plugin-installer.js +4 -0
  66. package/dist/muse-plugin-installer.js.map +1 -1
  67. package/dist/opencode-plugin-installer.d.ts +1 -2
  68. package/dist/opencode-plugin-installer.js +1 -2
  69. package/dist/opencode-plugin-installer.js.map +1 -1
  70. package/dist/provider-env-store.js +1 -1
  71. package/dist/provider-env-store.js.map +1 -1
  72. package/dist/retired-skill-assets.d.ts +3 -0
  73. package/dist/retired-skill-assets.js +11 -0
  74. package/dist/retired-skill-assets.js.map +1 -1
  75. package/dist/sessions-commands.d.ts +2 -0
  76. package/dist/sessions-commands.js +445 -0
  77. package/dist/sessions-commands.js.map +1 -0
  78. package/dist/setup-migrations.d.ts +6 -0
  79. package/dist/setup-migrations.js +28 -0
  80. package/dist/setup-migrations.js.map +1 -0
  81. package/dist/setup-state.d.ts +16 -0
  82. package/dist/setup-state.js +163 -0
  83. package/dist/setup-state.js.map +1 -0
  84. package/dist/setup.d.ts +25 -0
  85. package/dist/setup.js +120 -0
  86. package/dist/setup.js.map +1 -0
  87. package/dist/types.d.ts +1 -0
  88. package/dist/wire-types.js +1 -8
  89. package/dist/wire-types.js.map +1 -1
  90. package/package.json +2 -3
  91. package/pi-extension/API.md +4 -0
  92. package/pi-extension/ftown.js +1 -2
  93. package/skills/ftown/SKILL.md +71 -19
  94. package/skills/ftown/references/orchestrator.md +23 -99
  95. package/skills/ftown/references/sessions.md +37 -42
  96. package/dist/claude-trust.d.ts +0 -11
  97. package/dist/claude-trust.js +0 -47
  98. package/dist/claude-trust.js.map +0 -1
  99. package/dist/install-ftown-cli.d.ts +0 -3
  100. package/dist/install-ftown-cli.js +0 -29
  101. package/dist/install-ftown-cli.js.map +0 -1
  102. package/dist/install-ftown-env-cli.d.ts +0 -3
  103. package/dist/install-ftown-env-cli.js +0 -30
  104. package/dist/install-ftown-env-cli.js.map +0 -1
  105. package/dist/install-ftown-workflows-cli.d.ts +0 -3
  106. package/dist/install-ftown-workflows-cli.js +0 -33
  107. package/dist/install-ftown-workflows-cli.js.map +0 -1
  108. package/dist/workflow-runner-cli.d.ts +0 -2
  109. package/dist/workflow-runner-cli.js +0 -376
  110. package/dist/workflow-runner-cli.js.map +0 -1
  111. package/dist/workflow-runner.d.ts +0 -163
  112. package/dist/workflow-runner.js +0 -307
  113. package/dist/workflow-runner.js.map +0 -1
  114. package/skills/fticket/SKILL.md +0 -166
  115. package/skills/ftown/references/workflows.md +0 -470
  116. package/skills/ftown/scripts/example.flow.mjs +0 -170
@@ -1,629 +1,9 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Local bridge harness — talks to ftown-bridge LocalApiServer via ~/.ftown/bridge.json
4
- */
5
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
6
- import { homedir } from 'node:os';
7
- import { join, resolve } from 'node:path';
8
- import { Command } from 'commander';
9
- import { cleanTerminalLine, formatLogLines, isDisplayableLine } from './harness-format.js';
10
- import { MAIL_TYPES, formatMailMessage, submitSuffix, } from './wire-types.js';
11
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
- const BRIDGE_JSON = join(homedir(), '.ftown', 'bridge.json');
13
- const REGISTRY_JSON = join(homedir(), '.ftown', 'session-registry.json');
14
- function loadPointer() {
15
- if (!existsSync(BRIDGE_JSON)) {
16
- const wrapper = join(homedir(), '.ftown', 'bin', 'ftown-harness');
17
- throw new Error(`Bridge not running (no ${BRIDGE_JSON}). Start ftown-bridge from the UI CLI token, then use ${wrapper}`);
18
- }
19
- const parsed = JSON.parse(readFileSync(BRIDGE_JSON, 'utf8'));
20
- if (!parsed.port || !parsed.token) {
21
- throw new Error(`Invalid ${BRIDGE_JSON} — missing port or token`);
22
- }
23
- return parsed;
2
+ // Compatibility entrypoint; canonical dispatch and implementation are shared.
3
+ import { runFtown } from './ftown-cli.js';
4
+ import { executeCli, isCliEntry } from './cli-entry.js';
5
+ export { isCliEntry } from './cli-entry.js';
6
+ if (isCliEntry(import.meta.url, process.argv[1])) {
7
+ void executeCli(() => runFtown(['harness', ...process.argv.slice(2)]));
24
8
  }
25
- function loadRegistry() {
26
- if (!existsSync(REGISTRY_JSON)) {
27
- return { byWorkspace: {}, byConversation: {} };
28
- }
29
- const parsed = JSON.parse(readFileSync(REGISTRY_JSON, 'utf8'));
30
- return {
31
- byWorkspace: parsed.byWorkspace ?? {},
32
- byConversation: parsed.byConversation ?? {},
33
- };
34
- }
35
- function emit(json, data, text) {
36
- if (json)
37
- console.log(JSON.stringify(data, null, 2));
38
- else
39
- console.log(text);
40
- }
41
- async function api(pointer, method, path, body) {
42
- const url = `http://127.0.0.1:${pointer.port}${path}`;
43
- const res = await fetch(url, {
44
- method,
45
- headers: {
46
- Authorization: `Bearer ${pointer.token}`,
47
- ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
48
- },
49
- body: body !== undefined ? JSON.stringify(body) : undefined,
50
- });
51
- const data = (await res.json());
52
- if (!res.ok) {
53
- throw new Error(data.error ?? `HTTP ${res.status} ${path}`);
54
- }
55
- return data;
56
- }
57
- async function listSessions(pointer) {
58
- const { sessions } = await api(pointer, 'GET', '/api/sessions');
59
- return sessions;
60
- }
61
- async function getSession(pointer, id) {
62
- const { session } = await api(pointer, 'GET', `/api/sessions/${id}`);
63
- return session;
64
- }
65
- async function getLogLineCount(pointer, id) {
66
- const meta = await api(pointer, 'GET', `/api/sessions/${id}/screen?limit=1`);
67
- return meta.totalLines;
68
- }
69
- async function isAlive(pointer, id) {
70
- const { running } = await api(pointer, 'GET', `/api/sessions/${id}/running`);
71
- return running;
72
- }
73
- function resolveSessionId(sessions, query) {
74
- if (UUID_RE.test(query) || query.length >= 8) {
75
- const byPrefix = sessions.filter((s) => s.id === query || s.id.startsWith(query));
76
- if (byPrefix.length === 1)
77
- return byPrefix[0].id;
78
- if (byPrefix.length > 1) {
79
- throw new Error(`Ambiguous id prefix "${query}": ${byPrefix.map((s) => `${s.name} (${s.id.slice(0, 8)}…)`).join(', ')}`);
80
- }
81
- }
82
- const exact = sessions.filter((s) => s.name === query);
83
- if (exact.length === 1)
84
- return exact[0].id;
85
- const fuzzy = sessions.filter((s) => s.name.toLowerCase().includes(query.toLowerCase()));
86
- if (fuzzy.length === 1)
87
- return fuzzy[0].id;
88
- if (fuzzy.length > 1) {
89
- throw new Error(`Ambiguous name "${query}": ${fuzzy.map((s) => s.name).join(', ')} — use full name or id prefix`);
90
- }
91
- throw new Error(`No session matching "${query}"`);
92
- }
93
- function resolveWorkspaceSessionId(cwd) {
94
- const reg = loadRegistry();
95
- let dir = resolve(cwd);
96
- const root = resolve('/');
97
- while (dir.startsWith(root)) {
98
- const id = reg.byWorkspace[dir];
99
- if (id)
100
- return { id, workspace: dir };
101
- const parent = resolve(dir, '..');
102
- if (parent === dir)
103
- break;
104
- dir = parent;
105
- }
106
- return undefined;
107
- }
108
- async function fetchTail(pointer, id, n) {
109
- const totalLines = await getLogLineCount(pointer, id);
110
- if (totalLines === 0)
111
- return [];
112
- const start = Math.max(0, totalLines - n);
113
- const scr = await api(pointer, 'GET', `/api/sessions/${id}/screen?offset=${start}&limit=${n}`);
114
- return formatLogLines(scr.lines);
115
- }
116
- async function printTail(pointer, id, n, opts) {
117
- const lines = await fetchTail(pointer, id, n);
118
- if (opts.json) {
119
- emit(true, { sessionId: id, lines }, '');
120
- return lines;
121
- }
122
- if (opts.header)
123
- console.log(opts.header);
124
- for (const line of lines)
125
- console.log(line);
126
- return lines;
127
- }
128
- async function cmdStatus(pointer, json) {
129
- let alive = false;
130
- try {
131
- await api(pointer, 'GET', '/api/sessions');
132
- alive = true;
133
- }
134
- catch {
135
- alive = false;
136
- }
137
- const data = {
138
- bridge: alive ? 'up' : 'down',
139
- url: `http://127.0.0.1:${pointer.port}`,
140
- auth: BRIDGE_JSON,
141
- cli: pointer.harness ?? join(homedir(), '.ftown', 'bin', 'ftown-harness'),
142
- pid: pointer.pid,
143
- bridgeId: pointer.bridgeId,
144
- since: pointer.startedAt,
145
- };
146
- if (json)
147
- emit(true, data, '');
148
- else {
149
- console.log(`bridge: ${data.bridge}`);
150
- console.log(`url: ${data.url}`);
151
- console.log(`auth: ${data.auth}`);
152
- console.log(`cli: ${data.cli}`);
153
- if (data.pid)
154
- console.log(`pid: ${data.pid}`);
155
- if (data.bridgeId)
156
- console.log(`id: ${data.bridgeId}`);
157
- if (data.since)
158
- console.log(`since: ${data.since}`);
159
- }
160
- }
161
- async function cmdLs(pointer, opts) {
162
- const sessions = await listSessions(pointer);
163
- if (sessions.length === 0) {
164
- emit(opts.json, { sessions: [] }, '(no sessions)');
165
- return;
166
- }
167
- const cwd = resolve(process.cwd());
168
- const hereResolved = resolveWorkspaceSessionId(cwd);
169
- const hereId = hereResolved?.id;
170
- const rows = [];
171
- for (const s of sessions.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))) {
172
- const logLines = await getLogLineCount(pointer, s.id);
173
- const alive = s.status === 'running' ? await isAlive(pointer, s.id) : false;
174
- const hasLog = logLines > 0;
175
- const mark = s.id === hereId;
176
- const row = {
177
- mark,
178
- name: s.name,
179
- id: s.id,
180
- status: s.status,
181
- alive,
182
- shellType: s.shellType,
183
- workingDir: s.workingDir,
184
- logLines,
185
- hasLog,
186
- };
187
- rows.push(row);
188
- if (!opts.json) {
189
- const wd = s.workingDir ? `…${s.workingDir.slice(-30)}` : '';
190
- const logLabel = hasLog ? `log=${logLines}` : 'no log';
191
- const proc = alive ? 'alive' : 'dead ';
192
- console.log(`${mark ? '*' : ' '} ${s.name.padEnd(14)} ${s.id.slice(0, 8)}… ${s.status.padEnd(9)} ${proc} ${logLabel.padEnd(12)} ${(s.shellType ?? '?').padEnd(7)} ${wd}`);
193
- if (opts.tail > 0 && hasLog) {
194
- const lines = await fetchTail(pointer, s.id, opts.tail);
195
- for (const line of lines)
196
- console.log(` ${line.slice(0, 120)}`);
197
- }
198
- }
199
- }
200
- if (opts.json) {
201
- emit(true, { sessions: rows, hereWorkspace: hereResolved?.workspace }, '');
202
- }
203
- else if (hereId) {
204
- console.log('\n* = session for current working directory');
205
- }
206
- }
207
- async function cmdHere(pointer, opts) {
208
- const cwd = resolve(opts.cwd ?? process.cwd());
209
- const resolved = resolveWorkspaceSessionId(cwd);
210
- if (!resolved) {
211
- emit(opts.json, { error: 'no_registry', cwd }, `No registry entry for workspace tree from: ${cwd}`);
212
- return;
213
- }
214
- const { id, workspace } = resolved;
215
- const session = await getSession(pointer, id);
216
- const alive = await isAlive(pointer, id);
217
- const logLines = await getLogLineCount(pointer, id);
218
- const meta = {
219
- session,
220
- registryWorkspace: workspace,
221
- alive,
222
- logLines,
223
- hasLog: logLines > 0,
224
- };
225
- if (opts.json) {
226
- const out = { ...meta };
227
- if (opts.lines > 0 && logLines > 0) {
228
- out.tail = await fetchTail(pointer, id, opts.lines);
229
- }
230
- emit(true, out, '');
231
- return;
232
- }
233
- console.log(`${session.name} (${id})`);
234
- console.log(`registry=${workspace}`);
235
- console.log(`status=${session.status} alive=${alive} shell=${session.shellType ?? '?'} log=${logLines}`);
236
- if (session.workingDir)
237
- console.log(`cwd=${session.workingDir}`);
238
- if (opts.lines <= 0)
239
- return;
240
- if (logLines === 0) {
241
- console.log('---');
242
- console.log('(no terminal log yet)');
243
- return;
244
- }
245
- if (!alive) {
246
- console.log(`---`);
247
- console.log(`# process not running — showing last ${opts.lines} log lines (status=${session.status})`);
248
- }
249
- else {
250
- console.log('---');
251
- }
252
- await printTail(pointer, id, opts.lines, {});
253
- }
254
- async function cmdTail(pointer, query, opts) {
255
- const sessions = await listSessions(pointer);
256
- const id = resolveSessionId(sessions, query);
257
- const session = await getSession(pointer, id);
258
- const logLines = await getLogLineCount(pointer, id);
259
- if (opts.json) {
260
- emit(true, { session, logLines, lines: await fetchTail(pointer, id, opts.lines) }, '');
261
- return;
262
- }
263
- console.log(`# ${session.name} (${id}) log=${logLines}`);
264
- await printTail(pointer, id, opts.lines, {});
265
- }
266
- function printGrepMatch(m, context) {
267
- if (context > 0 && m.before?.length) {
268
- const startLine = m.lineNumber - m.before.length;
269
- for (let i = 0; i < m.before.length; i++) {
270
- const t = cleanTerminalLine(m.before[i]);
271
- if (isDisplayableLine(t))
272
- console.log(` ${startLine + i}- ${t.slice(0, 200)}`);
273
- }
274
- }
275
- const hit = cleanTerminalLine(m.text);
276
- if (isDisplayableLine(hit))
277
- console.log(`${m.lineNumber}: ${hit.slice(0, 200)}`);
278
- if (context > 0 && m.after?.length) {
279
- for (let j = 0; j < m.after.length; j++) {
280
- const t = cleanTerminalLine(m.after[j]);
281
- if (isDisplayableLine(t))
282
- console.log(` ${m.lineNumber + j + 1}- ${t.slice(0, 200)}`);
283
- }
284
- }
285
- }
286
- async function cmdGrep(pointer, query, pattern, opts) {
287
- const sessions = await listSessions(pointer);
288
- const id = resolveSessionId(sessions, query);
289
- const result = await api(pointer, 'POST', `/api/sessions/${id}/grep`, {
290
- pattern,
291
- offset: opts.offset,
292
- limit: opts.limit,
293
- ...(opts.context > 0 ? { context: opts.context } : {}),
294
- });
295
- if (opts.json) {
296
- const cleaned = result.matches.map((m) => ({
297
- lineNumber: m.lineNumber,
298
- text: cleanTerminalLine(m.text),
299
- before: m.before?.map(cleanTerminalLine).filter(isDisplayableLine),
300
- after: m.after?.map(cleanTerminalLine).filter(isDisplayableLine),
301
- }));
302
- emit(true, { totalMatches: result.totalMatches, matches: cleaned }, '');
303
- return;
304
- }
305
- console.log(`# matches ${result.totalMatches} (showing ${result.matches.length})`);
306
- for (const m of result.matches) {
307
- if (opts.context > 0)
308
- printGrepMatch(m, opts.context);
309
- else {
310
- const hit = cleanTerminalLine(m.text);
311
- if (isDisplayableLine(hit))
312
- console.log(`${m.lineNumber}: ${hit.slice(0, 200)}`);
313
- }
314
- }
315
- }
316
- async function cmdSend(pointer, query, text, opts) {
317
- const sessions = await listSessions(pointer);
318
- const id = resolveSessionId(sessions, query);
319
- const session = await getSession(pointer, id);
320
- let keys = text;
321
- if (opts.submit)
322
- keys += submitSuffix(session.shellType);
323
- const plan = {
324
- sessionId: id,
325
- name: session.name,
326
- shellType: session.shellType,
327
- submit: opts.submit,
328
- bytes: keys.length,
329
- dryRun: opts.dryRun,
330
- };
331
- if (opts.dryRun) {
332
- emit(opts.json, plan, `dry-run: would send ${keys.length} bytes to ${session.name} (submit=${opts.submit})`);
333
- return;
334
- }
335
- await api(pointer, 'POST', `/api/sessions/${id}/keys`, { keys });
336
- emit(opts.json, { ...plan, sent: true }, `sent ${keys.length} bytes to ${session.name} (submit=${opts.submit})`);
337
- }
338
- function resolveOwnSessionId() {
339
- const env = process.env.FTOWN_SESSION_ID?.trim();
340
- if (env)
341
- return env;
342
- return resolveWorkspaceSessionId(process.cwd())?.id;
343
- }
344
- async function fetchInbox(pointer, sessionId, opts) {
345
- const params = new URLSearchParams();
346
- params.set('wait', String(opts.wait ?? 0));
347
- if (opts.peek)
348
- params.set('peek', '1');
349
- if (opts.limit !== undefined)
350
- params.set('limit', String(opts.limit));
351
- if (opts.all)
352
- params.set('all', '1');
353
- const { messages } = await api(pointer, 'GET', `/api/sessions/${sessionId}/inbox?${params.toString()}`);
354
- return messages ?? [];
355
- }
356
- async function cmdMailSend(pointer, opts) {
357
- if (!opts.text)
358
- throw new Error('Missing message text');
359
- if (opts.type !== undefined && !MAIL_TYPES.includes(opts.type)) {
360
- throw new Error(`Invalid --type "${opts.type}" — use one of: ${MAIL_TYPES.join(', ')}`);
361
- }
362
- const sessions = await listSessions(pointer);
363
- let targetId;
364
- if (opts.parent) {
365
- const parent = process.env.FTOWN_PARENT_SESSION_ID?.trim();
366
- if (!parent)
367
- throw new Error('--parent requires FTOWN_PARENT_SESSION_ID to be set');
368
- targetId = parent;
369
- }
370
- else {
371
- if (!opts.target)
372
- throw new Error('Missing target (session id/name, or use --parent)');
373
- targetId = resolveSessionId(sessions, opts.target);
374
- }
375
- const self = process.env.FTOWN_SESSION_ID?.trim();
376
- const from = self ?? 'external';
377
- const fromName = self ? sessions.find((s) => s.id === self)?.name : undefined;
378
- const body = { body: opts.text, from };
379
- if (fromName)
380
- body.fromName = fromName;
381
- if (opts.type)
382
- body.type = opts.type;
383
- if (opts.thread)
384
- body.threadId = opts.thread;
385
- const { id } = await api(pointer, 'POST', `/api/sessions/${targetId}/inbox`, body);
386
- emit(opts.json, { id, to: targetId, from, type: opts.type ?? 'message' }, `mail ${id} sent to ${targetId}`);
387
- }
388
- async function cmdMailRead(pointer, opts) {
389
- const sessionId = resolveOwnSessionId();
390
- if (!sessionId) {
391
- throw new Error('Cannot resolve own session — set FTOWN_SESSION_ID or run inside a registered workspace');
392
- }
393
- const messages = await fetchInbox(pointer, sessionId, {
394
- wait: 0,
395
- peek: opts.peek,
396
- limit: opts.limit,
397
- all: opts.all,
398
- });
399
- if (opts.json) {
400
- emit(true, { sessionId, messages }, '');
401
- return;
402
- }
403
- if (messages.length === 0) {
404
- console.log('(no mail)');
405
- return;
406
- }
407
- for (const m of messages)
408
- console.log(formatMailMessage(m));
409
- }
410
- function pumpCountPath(sessionId) {
411
- return `/tmp/ftown-mail-pump-${sessionId}.count`;
412
- }
413
- function readPumpCount(sessionId) {
414
- try {
415
- const n = parseInt(readFileSync(pumpCountPath(sessionId), 'utf8').trim(), 10);
416
- return Number.isFinite(n) && n > 0 ? n : 0;
417
- }
418
- catch {
419
- return 0;
420
- }
421
- }
422
- function writePumpCount(sessionId, count) {
423
- try {
424
- writeFileSync(pumpCountPath(sessionId), `${count}\n`);
425
- }
426
- catch {
427
- /* never break the hook */
428
- }
429
- }
430
- async function readStdinText() {
431
- const chunks = [];
432
- for await (const chunk of process.stdin) {
433
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
434
- }
435
- return Buffer.concat(chunks).toString('utf8');
436
- }
437
- async function resolvePumpSessionId(pointer, hook) {
438
- const env = process.env.FTOWN_SESSION_ID?.trim();
439
- if (env)
440
- return env;
441
- // Cursor hooks report conversation_id, Claude Code hooks report session_id;
442
- // the registry maps either via byConversation.
443
- const conversation = hook.conversation_id?.trim() || hook.session_id?.trim();
444
- if (!conversation)
445
- return undefined;
446
- const { sessionId } = await api(pointer, 'GET', `/api/inbox/resolve?conversation=${encodeURIComponent(conversation)}`);
447
- return sessionId || undefined;
448
- }
449
- const PUMP_STOP_BLOCK_LIMIT = 20;
450
- /** Mail pump for Claude Code hooks. MUST always exit 0 and never write noise. */
451
- async function cmdHookPump() {
452
- const killer = setTimeout(() => process.exit(0), 28_000);
453
- killer.unref();
454
- try {
455
- if (process.stdin.isTTY)
456
- return;
457
- const raw = await readStdinText();
458
- const hook = JSON.parse(raw);
459
- const event = hook.hook_event_name;
460
- if (event !== 'Stop' && event !== 'UserPromptSubmit' && event !== 'SessionStart')
461
- return;
462
- const pointer = loadPointer();
463
- const sessionId = await resolvePumpSessionId(pointer, hook);
464
- if (!sessionId)
465
- return;
466
- if (event === 'Stop') {
467
- if (hook.stop_hook_active === true && readPumpCount(sessionId) >= PUMP_STOP_BLOCK_LIMIT) {
468
- writePumpCount(sessionId, 0);
469
- return;
470
- }
471
- const messages = await fetchInbox(pointer, sessionId, { wait: 0 });
472
- if (messages.length === 0) {
473
- writePumpCount(sessionId, 0);
474
- return;
475
- }
476
- writePumpCount(sessionId, readPumpCount(sessionId) + 1);
477
- const formatted = messages.map(formatMailMessage).join('\n');
478
- console.log(JSON.stringify({
479
- decision: 'block',
480
- reason: `[ftown mail] You received message(s) while finishing your turn:\n${formatted}\n` +
481
- 'Handle them now; reply with `ftown-harness mail send ...` where appropriate.',
482
- }));
483
- return;
484
- }
485
- const messages = await fetchInbox(pointer, sessionId, { wait: 0 });
486
- if (messages.length === 0)
487
- return;
488
- const formatted = messages.map(formatMailMessage).join('\n');
489
- console.log(JSON.stringify({
490
- hookSpecificOutput: {
491
- hookEventName: event,
492
- additionalContext: `[ftown mail]\n${formatted}`,
493
- },
494
- }));
495
- }
496
- catch {
497
- /* bridge down, bad JSON, timeout — never break the session */
498
- }
499
- }
500
- const program = new Command();
501
- program
502
- .name('ftown-harness')
503
- .description('CLI for ftown bridge local API (~/.ftown/bridge.json)')
504
- .option('--json', 'Machine-readable JSON output');
505
- program
506
- .command('status')
507
- .description('Bridge pointer and health')
508
- .action(async (_opts, cmd) => {
509
- const pointer = loadPointer();
510
- await cmdStatus(pointer, !!cmd.parent?.opts().json);
511
- });
512
- program
513
- .command('ls')
514
- .description('List sessions (* = cwd workspace)')
515
- .option('-n, --tail <n>', 'Preview last N log lines per session (incl. dead)', '0')
516
- .action(async (opts, cmd) => {
517
- const pointer = loadPointer();
518
- await cmdLs(pointer, {
519
- tail: parseInt(opts.tail, 10) || 0,
520
- json: !!cmd.parent?.opts().json,
521
- });
522
- });
523
- program
524
- .command('here')
525
- .description('Session for current workspace (from registry)')
526
- .option('-n, --lines <n>', 'Tail lines (works for dead sessions with logs)', '15')
527
- .option('--cwd <path>', 'Workspace path (default: process.cwd())')
528
- .action(async (opts, cmd) => {
529
- const pointer = loadPointer();
530
- await cmdHere(pointer, {
531
- lines: parseInt(opts.lines, 10) || 15,
532
- cwd: opts.cwd,
533
- json: !!cmd.parent?.opts().json,
534
- });
535
- });
536
- program
537
- .command('tail <session>')
538
- .description('Last N lines of terminal (ANSI/OSC stripped)')
539
- .option('-n, --lines <n>', 'Line count', '40')
540
- .action(async (session, opts, cmd) => {
541
- const pointer = loadPointer();
542
- await cmdTail(pointer, session, {
543
- lines: parseInt(opts.lines, 10) || 40,
544
- json: !!cmd.parent?.opts().json,
545
- });
546
- });
547
- program
548
- .command('grep <session> <pattern>')
549
- .description('Search terminal log (regex)')
550
- .option('-n, --limit <n>', 'Max matches', '30')
551
- .option('--offset <n>', 'Match offset', '0')
552
- .option('-C, --context <n>', 'Lines of context before/after each match', '0')
553
- .action(async (session, pattern, opts, cmd) => {
554
- const pointer = loadPointer();
555
- await cmdGrep(pointer, session, pattern, {
556
- limit: parseInt(opts.limit, 10) || 30,
557
- offset: parseInt(opts.offset, 10) || 0,
558
- context: parseInt(opts.context, 10) || 0,
559
- json: !!cmd.parent?.opts().json,
560
- });
561
- });
562
- program
563
- .command('send <session> <text...>')
564
- .description('Send keystrokes (--submit adds Enter for shell type)')
565
- .option('-s, --submit', 'Append submit sequence after text')
566
- .option('-l, --literal', 'Do not interpret escape sequences in text')
567
- .option('--dry-run', 'Print what would be sent without writing to PTY')
568
- .action(async (session, textParts, opts, cmd) => {
569
- const pointer = loadPointer();
570
- let text = textParts.join(' ');
571
- if (!opts.literal) {
572
- text = text.replace(/\\r/g, '\r').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\x1b/g, '\x1b');
573
- }
574
- await cmdSend(pointer, session, text, {
575
- submit: !!opts.submit,
576
- literal: !!opts.literal,
577
- dryRun: !!opts.dryRun,
578
- json: !!cmd.parent?.opts().json,
579
- });
580
- });
581
- const mail = program
582
- .command('mail')
583
- .description('Per-session inbox: send and read inter-agent mail');
584
- mail
585
- .command('send [target] [text...]')
586
- .description('Send mail to a session (id/name, or --parent)')
587
- .option('--type <t>', `Mail type: ${MAIL_TYPES.join(' | ')}`)
588
- .option('--thread <id>', 'Thread id for grouping replies')
589
- .option('-p, --parent', 'Send to FTOWN_PARENT_SESSION_ID')
590
- .action(async (target, textParts, opts, cmd) => {
591
- const pointer = loadPointer();
592
- const parts = opts.parent && target !== undefined ? [target, ...textParts] : textParts;
593
- await cmdMailSend(pointer, {
594
- target: opts.parent ? undefined : target,
595
- text: parts.join(' '),
596
- type: opts.type,
597
- thread: opts.thread,
598
- parent: !!opts.parent,
599
- json: !!cmd.parent?.parent?.opts().json,
600
- });
601
- });
602
- mail
603
- .command('read')
604
- .description('Read own inbox (FTOWN_SESSION_ID or cwd workspace session)')
605
- .option('--peek', 'Do not mark messages as delivered')
606
- .option('--limit <n>', 'Max messages')
607
- .option('--all', 'Include already-delivered messages')
608
- .action(async (opts, cmd) => {
609
- const pointer = loadPointer();
610
- await cmdMailRead(pointer, {
611
- peek: !!opts.peek,
612
- limit: opts.limit !== undefined ? parseInt(opts.limit, 10) || undefined : undefined,
613
- all: !!opts.all,
614
- json: !!cmd.parent?.parent?.opts().json,
615
- });
616
- });
617
- program
618
- .command('hook-pump')
619
- .description('Claude Code hook: deliver pending mail (reads hook JSON from stdin)')
620
- .action(async () => {
621
- await cmdHookPump();
622
- process.exit(0);
623
- });
624
- program.parseAsync(process.argv).catch((err) => {
625
- const msg = err instanceof Error ? err.message : String(err);
626
- console.error(`ftown-harness: ${msg}`);
627
- process.exit(1);
628
- });
629
9
  //# sourceMappingURL=harness-cli.js.map