ftown-bridge 0.3.16 → 0.5.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.
Files changed (37) hide show
  1. package/dist/agent-commands.js +1 -1
  2. package/dist/agent-commands.js.map +1 -1
  3. package/dist/claude-runner.d.ts +29 -0
  4. package/dist/claude-runner.js +246 -44
  5. package/dist/claude-runner.js.map +1 -1
  6. package/dist/create-ftown-session.d.ts +16 -0
  7. package/dist/create-ftown-session.js +70 -5
  8. package/dist/create-ftown-session.js.map +1 -1
  9. package/dist/cursor-hook-installer.d.ts +0 -2
  10. package/dist/cursor-hook-installer.js +3 -14
  11. package/dist/cursor-hook-installer.js.map +1 -1
  12. package/dist/ftown-sessions-cli.js +120 -2
  13. package/dist/ftown-sessions-cli.js.map +1 -1
  14. package/dist/hook-installer.js +1 -1
  15. package/dist/hook-installer.js.map +1 -1
  16. package/dist/index.js +245 -42
  17. package/dist/index.js.map +1 -1
  18. package/dist/local-api-server.d.ts +2 -0
  19. package/dist/local-api-server.js +197 -4
  20. package/dist/local-api-server.js.map +1 -1
  21. package/dist/remove-ftown-session.d.ts +23 -0
  22. package/dist/remove-ftown-session.js +32 -0
  23. package/dist/remove-ftown-session.js.map +1 -0
  24. package/dist/session-registry.d.ts +11 -1
  25. package/dist/session-registry.js +3 -3
  26. package/dist/session-registry.js.map +1 -1
  27. package/dist/session-store.d.ts +6 -1
  28. package/dist/session-store.js +32 -1
  29. package/dist/session-store.js.map +1 -1
  30. package/dist/tmux.d.ts +24 -0
  31. package/dist/tmux.js +149 -0
  32. package/dist/tmux.js.map +1 -0
  33. package/dist/types.d.ts +9 -0
  34. package/hooks/notify.sh +34 -12
  35. package/package.json +1 -1
  36. package/skills/ftown-sessions/SKILL.md +49 -0
  37. package/skills/bridge-harness/SKILL.md +0 -43
@@ -1,6 +1,7 @@
1
- import type { Session } from './types.js';
1
+ import type { ArchivedSession, Session } from './types.js';
2
2
  export declare class SessionStore {
3
3
  private readonly sessionsDir;
4
+ private readonly archivePath;
4
5
  private readonly writeLocks;
5
6
  constructor(dataDir: string);
6
7
  private sessionDir;
@@ -11,6 +12,10 @@ export declare class SessionStore {
11
12
  listSessions(): Promise<Session[]>;
12
13
  appendTerminalData(sessionId: string, data: string): Promise<void>;
13
14
  deleteSession(sessionId: string): Promise<void>;
15
+ /** Append a tombstone for a removed session to <dataDir>/archive.jsonl. */
16
+ archiveSession(session: Session): Promise<void>;
17
+ /** All tombstones, newest last. Missing file and corrupt lines are tolerated. */
18
+ listArchived(): Promise<ArchivedSession[]>;
14
19
  clearTerminalLog(sessionId: string): Promise<void>;
15
20
  loadTerminalLog(sessionId: string): Promise<string>;
16
21
  }
@@ -1,11 +1,13 @@
1
1
  import { readFile, writeFile, mkdir, readdir, appendFile, rm, truncate } from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { join, dirname } from 'node:path';
3
3
  import { existsSync } from 'node:fs';
4
4
  export class SessionStore {
5
5
  sessionsDir;
6
+ archivePath;
6
7
  writeLocks = new Map();
7
8
  constructor(dataDir) {
8
9
  this.sessionsDir = join(dataDir, 'sessions');
10
+ this.archivePath = join(dataDir, 'archive.jsonl');
9
11
  }
10
12
  sessionDir(sessionId) {
11
13
  return join(this.sessionsDir, sessionId);
@@ -60,6 +62,35 @@ export class SessionStore {
60
62
  await rm(dir, { recursive: true, force: true });
61
63
  }
62
64
  }
65
+ /** Append a tombstone for a removed session to <dataDir>/archive.jsonl. */
66
+ async archiveSession(session) {
67
+ await mkdir(dirname(this.archivePath), { recursive: true });
68
+ const record = { ...session, removedAt: new Date().toISOString() };
69
+ // Tombstones retain session env (API keys); owner-only like session-registry.
70
+ await appendFile(this.archivePath, `${JSON.stringify(record)}\n`, { encoding: 'utf-8', mode: 0o600 });
71
+ }
72
+ /** All tombstones, newest last. Missing file and corrupt lines are tolerated. */
73
+ async listArchived() {
74
+ if (!existsSync(this.archivePath)) {
75
+ return [];
76
+ }
77
+ const raw = await readFile(this.archivePath, 'utf-8');
78
+ const archived = [];
79
+ for (const line of raw.split('\n')) {
80
+ if (!line.trim())
81
+ continue;
82
+ try {
83
+ const record = JSON.parse(line);
84
+ if (record && typeof record.id === 'string' && typeof record.removedAt === 'string') {
85
+ archived.push(record);
86
+ }
87
+ }
88
+ catch {
89
+ // Skip corrupt lines (e.g. partial write from a crashed bridge).
90
+ }
91
+ }
92
+ return archived;
93
+ }
63
94
  async clearTerminalLog(sessionId) {
64
95
  const filePath = this.terminalLogPath(sessionId);
65
96
  if (existsSync(filePath)) {
@@ -1 +1 @@
1
- {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAIrC,MAAM,OAAO,YAAY;IACN,WAAW,CAAS;IACpB,WAAW,CAAS;IACpB,UAAU,GAA+B,IAAI,GAAG,EAAE,CAAC;IAEpE,YAAY,OAAe;QACzB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IACpD,CAAC;IAEO,UAAU,CAAC,SAAiB;QAClC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAC3C,CAAC;IAEO,eAAe,CAAC,SAAiB;QACvC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC,CAAC;IAC1D,CAAC;IAEO,eAAe,CAAC,SAAiB;QACvC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAgB;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,MAAM,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC/F,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,SAAiB;QACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YAClC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACnD,IAAI,OAAO,EAAE,CAAC;oBACZ,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACpG,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,SAAiB,EAAE,IAAY;QACtD,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QAEjD,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrE,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QACzE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACxC,MAAM,OAAO,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,SAAiB;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QACvC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,cAAc,CAAC,OAAgB;QACnC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAoB,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;QACpF,8EAA8E;QAC9E,MAAM,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACxG,CAAC;IAED,iFAAiF;IACjF,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YAClC,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACtD,MAAM,QAAQ,GAAsB,EAAE,CAAC;QACvC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,SAAS;YAC3B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAoB,CAAC;gBACnD,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;oBACpF,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,iEAAiE;YACnE,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,SAAiB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACrE,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;YAC3D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACxC,MAAM,OAAO,CAAC;QAChB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,SAAiB;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACrC,CAAC;CAEF"}
package/dist/tmux.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** Dedicated tmux server socket so ftown never touches the user's tmux. */
2
+ export declare const TMUX_SOCKET_NAME = "ftown";
3
+ export declare function isTmuxAvailable(): boolean;
4
+ export declare function tmuxSessionName(sessionId: string): string;
5
+ /** Read (and remove) the real exit code of the command that ran inside tmux. */
6
+ export declare function readAndClearExitCode(sessionId: string): number | undefined;
7
+ export interface CreateTmuxSessionOptions {
8
+ sessionId: string;
9
+ command: string;
10
+ cwd: string;
11
+ cols: number;
12
+ rows: number;
13
+ /**
14
+ * Full environment for the command. Delivered via a 0600 file sourced
15
+ * inside the session — never via `new-session -e` argv (visible to other
16
+ * local users) and never via the tmux server env (stale after restarts).
17
+ */
18
+ env: Record<string, string>;
19
+ }
20
+ export declare function createTmuxSession(options: CreateTmuxSessionOptions): Promise<void>;
21
+ export declare function hasTmuxSession(sessionId: string): boolean;
22
+ /** Session ids of all live ftown-* sessions on the dedicated socket. */
23
+ export declare function listFtownTmuxSessions(): string[];
24
+ export declare function killTmuxSession(sessionId: string): Promise<boolean>;
package/dist/tmux.js ADDED
@@ -0,0 +1,149 @@
1
+ import { execFile, execFileSync } from 'node:child_process';
2
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const execFileAsync = promisify(execFile);
7
+ /** Dedicated tmux server socket so ftown never touches the user's tmux. */
8
+ export const TMUX_SOCKET_NAME = 'ftown';
9
+ const SESSION_PREFIX = 'ftown-';
10
+ // new-session -e requires tmux >= 3.2; older versions fall back to direct spawn.
11
+ const MIN_TMUX_VERSION = 3.02;
12
+ let tmuxAvailable;
13
+ export function isTmuxAvailable() {
14
+ if (tmuxAvailable === undefined) {
15
+ try {
16
+ const output = execFileSync('tmux', ['-V'], { encoding: 'utf8' });
17
+ const match = /(\d+)\.(\d+)/.exec(output);
18
+ // Unparseable versions (e.g. "tmux next-3.7") are assumed recent enough.
19
+ tmuxAvailable = match
20
+ ? Number(match[1]) + Number(match[2]) / 100 >= MIN_TMUX_VERSION
21
+ : true;
22
+ }
23
+ catch {
24
+ tmuxAvailable = false;
25
+ }
26
+ }
27
+ return tmuxAvailable;
28
+ }
29
+ export function tmuxSessionName(sessionId) {
30
+ return `${SESSION_PREFIX}${sessionId}`;
31
+ }
32
+ function shellQuote(value) {
33
+ return `'${value.replaceAll("'", `'\\''`)}'`;
34
+ }
35
+ function exitFilePath(sessionId) {
36
+ return join(tmpdir(), `ftown-exit-${sessionId}`);
37
+ }
38
+ /** Read (and remove) the real exit code of the command that ran inside tmux. */
39
+ export function readAndClearExitCode(sessionId) {
40
+ const path = exitFilePath(sessionId);
41
+ try {
42
+ const raw = readFileSync(path, 'utf8').trim();
43
+ rmSync(path, { force: true });
44
+ const code = Number.parseInt(raw, 10);
45
+ return Number.isNaN(code) ? undefined : code;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ function tmuxConfigPath() {
52
+ const dir = join(homedir(), '.ftown');
53
+ mkdirSync(dir, { recursive: true });
54
+ const path = join(dir, 'tmux.conf');
55
+ // Rewritten on every server start so option changes ship with bridge updates.
56
+ // Only applied when the ftown server starts; never touches ~/.tmux.conf.
57
+ writeFileSync(path, [
58
+ 'set -g status off',
59
+ 'set -g prefix None',
60
+ 'set -g prefix2 None',
61
+ 'unbind-key -a -T prefix',
62
+ 'unbind-key -a -T root',
63
+ 'set -g mouse off',
64
+ 'set -g history-limit 100000',
65
+ 'set -g remain-on-exit off',
66
+ 'set -g destroy-unattached off',
67
+ 'set -g exit-empty on',
68
+ 'setw -g window-size latest',
69
+ 'setw -g aggressive-resize on',
70
+ 'set -s escape-time 0',
71
+ 'set -g default-terminal "xterm-256color"',
72
+ ].join('\n') + '\n');
73
+ return path;
74
+ }
75
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
76
+ function envFilePath(sessionId) {
77
+ return join(tmpdir(), `ftown-env-${sessionId}`);
78
+ }
79
+ export async function createTmuxSession(options) {
80
+ const name = tmuxSessionName(options.sessionId);
81
+ // Replace any stale session with the same name (e.g. retry after error).
82
+ await killTmuxSession(options.sessionId);
83
+ rmSync(exitFilePath(options.sessionId), { force: true });
84
+ const envFile = envFilePath(options.sessionId);
85
+ const exports = Object.entries(options.env)
86
+ .filter(([key]) => ENV_KEY_PATTERN.test(key))
87
+ .map(([key, value]) => `export ${key}=${shellQuote(value)}`)
88
+ .join('\n');
89
+ writeFileSync(envFile, exports + '\n', { mode: 0o600 });
90
+ // Capture the real exit code so it survives the tmux attach client, whose
91
+ // own exit code is unrelated. An EXIT trap fires even on explicit `exit`.
92
+ const inner = [
93
+ // buildEnv strips these, but the tmux server env may still carry them.
94
+ 'unset NO_COLOR FORCE_COLOR',
95
+ `. ${shellQuote(envFile)}`,
96
+ `command rm -f ${shellQuote(envFile)}`,
97
+ `__ftown_exit_file=${shellQuote(exitFilePath(options.sessionId))}`,
98
+ `trap 'printf "%s" "$?" > "$__ftown_exit_file"' EXIT`,
99
+ options.command,
100
+ ].join('\n');
101
+ const args = [
102
+ '-L', TMUX_SOCKET_NAME,
103
+ '-f', tmuxConfigPath(),
104
+ 'new-session', '-d',
105
+ '-s', name,
106
+ '-c', options.cwd,
107
+ '-x', String(options.cols),
108
+ '-y', String(options.rows),
109
+ ];
110
+ // Single-string shell-command keeps compatibility across tmux versions.
111
+ args.push(`/bin/zsh -l -c ${shellQuote(inner)}`);
112
+ await execFileAsync('tmux', args, { env: options.env });
113
+ }
114
+ export function hasTmuxSession(sessionId) {
115
+ try {
116
+ execFileSync('tmux', ['-L', TMUX_SOCKET_NAME, 'has-session', '-t', `=${tmuxSessionName(sessionId)}`], { stdio: 'ignore' });
117
+ return true;
118
+ }
119
+ catch {
120
+ return false;
121
+ }
122
+ }
123
+ /** Session ids of all live ftown-* sessions on the dedicated socket. */
124
+ export function listFtownTmuxSessions() {
125
+ try {
126
+ const output = execFileSync('tmux', ['-L', TMUX_SOCKET_NAME, 'list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' });
127
+ return output
128
+ .split('\n')
129
+ .filter((name) => name.startsWith(SESSION_PREFIX))
130
+ .map((name) => name.slice(SESSION_PREFIX.length));
131
+ }
132
+ catch {
133
+ return [];
134
+ }
135
+ }
136
+ export async function killTmuxSession(sessionId) {
137
+ try {
138
+ await execFileAsync('tmux', [
139
+ '-L', TMUX_SOCKET_NAME,
140
+ 'kill-session',
141
+ '-t', `=${tmuxSessionName(sessionId)}`,
142
+ ]);
143
+ return true;
144
+ }
145
+ catch {
146
+ return false;
147
+ }
148
+ }
149
+ //# sourceMappingURL=tmux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux.js","sourceRoot":"","sources":["../src/tmux.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type ShellType = 'claude' | 'cursor' | 'shell' | 'zai' | 'kimi' | 'opencode' | 'deepseek' | 'fireworks';
2
+ export type SessionRuntime = 'tmux' | 'direct';
2
3
  export interface Session {
3
4
  id: string;
4
5
  name: string;
@@ -15,8 +16,13 @@ export interface Session {
15
16
  cursorSessionId?: string;
16
17
  env?: Record<string, string>;
17
18
  parentSessionId?: string;
19
+ runtime?: SessionRuntime;
18
20
  }
19
21
  export type SessionStatus = 'pending' | 'running' | 'completed' | 'error';
22
+ /** Tombstone written to <dataDir>/archive.jsonl when a session is removed. */
23
+ export interface ArchivedSession extends Session {
24
+ removedAt: string;
25
+ }
20
26
  export interface SessionMessage {
21
27
  sessionId: string;
22
28
  type: SessionMessageType;
@@ -46,6 +52,7 @@ export interface CreateSessionPayload {
46
52
  claudeSessionId?: string;
47
53
  cursorSessionId?: string;
48
54
  parentSessionId?: string;
55
+ orchestrator?: boolean;
49
56
  }
50
57
  export interface BridgeExecPayload {
51
58
  command: string;
@@ -70,6 +77,8 @@ export interface UpdateSessionParentPayload {
70
77
  }
71
78
  export interface RemoveSessionPayload {
72
79
  sessionId: string;
80
+ /** Only remove if the session is completed/error (bulk-clear race guard). */
81
+ onlyIfFinished?: boolean;
73
82
  }
74
83
  export interface ClearTerminalPayload {
75
84
  sessionId: string;
package/hooks/notify.sh CHANGED
@@ -4,6 +4,7 @@ INPUT=$(cat)
4
4
  PORT="${FTOWN_HOOK_PORT:-}"
5
5
  SESSION_ID="${FTOWN_SESSION_ID:-}"
6
6
  TOKEN="${FTOWN_HOOK_TOKEN:-}"
7
+ SOURCE=""
7
8
 
8
9
  BRIDGE_JSON="${HOME}/.ftown/bridge.json"
9
10
  REGISTRY="${HOME}/.ftown/session-registry.json"
@@ -15,15 +16,23 @@ if [ -z "$PORT" ] && [ -f "$BRIDGE_JSON" ]; then
15
16
  fi
16
17
  fi
17
18
 
18
- if [ -z "$SESSION_ID" ] && [ -n "$INPUT" ]; then
19
+ if [ -n "$SESSION_ID" ]; then
20
+ SOURCE="env"
21
+ elif [ -n "$INPUT" ]; then
19
22
  CONV=$(echo "$INPUT" | jq -r '.conversation_id // empty' 2>/dev/null)
20
23
  WS=$(echo "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
21
24
  if [ -f "$REGISTRY" ]; then
22
25
  if [ -n "$CONV" ]; then
23
26
  SESSION_ID=$(jq -r --arg c "$CONV" '.byConversation[$c] // empty' "$REGISTRY" 2>/dev/null)
27
+ if [ -n "$SESSION_ID" ]; then
28
+ SOURCE="conversation"
29
+ fi
24
30
  fi
25
31
  if [ -z "$SESSION_ID" ] && [ -n "$WS" ]; then
26
32
  SESSION_ID=$(jq -r --arg w "$WS" '.byWorkspace[$w] // empty' "$REGISTRY" 2>/dev/null)
33
+ if [ -n "$SESSION_ID" ]; then
34
+ SOURCE="workspace"
35
+ fi
27
36
  fi
28
37
  fi
29
38
  fi
@@ -32,19 +41,32 @@ if [ -z "$PORT" ] || [ -z "$SESSION_ID" ]; then
32
41
  exit 0
33
42
  fi
34
43
 
35
- PAYLOAD=$(echo "$INPUT" | jq -c --arg sid "$SESSION_ID" '. + {ftown_session_id: $sid}' 2>/dev/null)
44
+ PAYLOAD=$(echo "$INPUT" | jq -c --arg sid "$SESSION_ID" --arg src "$SOURCE" \
45
+ '. + {ftown_session_id: $sid, ftown_session_source: $src}' 2>/dev/null)
36
46
  if [ -z "$PAYLOAD" ]; then
37
- PAYLOAD=$(jq -nc --arg sid "$SESSION_ID" --arg ev "${HOOK_EVENT_NAME:-hook}" \
38
- '{ftown_session_id: $sid, hook_event_name: $ev}')
47
+ PAYLOAD=$(jq -nc --arg sid "$SESSION_ID" --arg src "$SOURCE" --arg ev "${HOOK_EVENT_NAME:-hook}" \
48
+ '{ftown_session_id: $sid, ftown_session_source: $src, hook_event_name: $ev}')
39
49
  fi
40
50
 
41
- AUTH_ARGS=()
42
- if [ -n "$TOKEN" ]; then
43
- AUTH_ARGS+=(-H "Authorization: Bearer ${TOKEN}")
44
- fi
51
+ post_hook() {
52
+ local port="$1" token="$2"
53
+ local auth=()
54
+ if [ -n "$token" ]; then
55
+ auth=(-H "Authorization: Bearer ${token}")
56
+ fi
57
+ curl -sf -X POST "http://localhost:${port}/hook" \
58
+ -H "Content-Type: application/json" \
59
+ "${auth[@]}" \
60
+ -d "$PAYLOAD" > /dev/null 2>&1
61
+ }
45
62
 
46
- curl -s -X POST "http://localhost:${PORT}/hook" \
47
- -H "Content-Type: application/json" \
48
- "${AUTH_ARGS[@]}" \
49
- -d "$PAYLOAD" > /dev/null 2>&1
63
+ if ! post_hook "$PORT" "$TOKEN" && [ -f "$BRIDGE_JSON" ]; then
64
+ # Env vars are stale inside tmux sessions that outlive their bridge; the
65
+ # current bridge rewrites bridge.json with the live port/token on startup.
66
+ BPORT=$(jq -r '.port // empty' "$BRIDGE_JSON" 2>/dev/null)
67
+ BTOKEN=$(jq -r '.token // empty' "$BRIDGE_JSON" 2>/dev/null)
68
+ if [ -n "$BPORT" ] && { [ "$BPORT" != "$PORT" ] || [ "$BTOKEN" != "$TOKEN" ]; }; then
69
+ post_hook "$BPORT" "$BTOKEN"
70
+ fi
71
+ fi
50
72
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftown-bridge",
3
- "version": "0.3.16",
3
+ "version": "0.5.0",
4
4
  "description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,8 +47,39 @@ Skill copy (same binary via wrapper): `scripts/ftown-sessions` in this skill dir
47
47
 
48
48
  # Liveness
49
49
  ~/.ftown/ftown-sessions running <session-id>
50
+
51
+ # Stop and remove a session (kept as a tombstone in the archive)
52
+ ~/.ftown/ftown-sessions remove <session-id>
53
+
54
+ # List archived (removed) sessions: id, name, removedAt, shellType
55
+ ~/.ftown/ftown-sessions archive
56
+
57
+ # Recreate a removed session from its tombstone (resumes the agent
58
+ # conversation when a claude/cursor session id was recorded; the revived
59
+ # session gets a NEW id)
60
+ ~/.ftown/ftown-sessions revive <session-id>
61
+ ```
62
+
63
+ ## Messaging
64
+
65
+ Send a short text line into another session's terminal. The message is sanitized
66
+ (control characters stripped, capped at 2000 chars) and delivered as
67
+ `[ftown msg from <sender>] <text>` followed by submit, so the target agent reads it
68
+ as a normal prompt.
69
+
70
+ ```bash
71
+ # Tell a specific session
72
+ ~/.ftown/ftown-sessions tell <session-id> "tests are green, ship it"
73
+
74
+ # Tell my parent / children / siblings (resolved via FTOWN_SESSION_ID)
75
+ ~/.ftown/ftown-sessions tell --parent "child finished phase 1"
76
+ ~/.ftown/ftown-sessions tell --children "pause and report status"
77
+ ~/.ftown/ftown-sessions tell --siblings "I grabbed the lock, stand by"
50
78
  ```
51
79
 
80
+ Sender is resolved from `FTOWN_SESSION_ID` (falls back to `unknown` when unset).
81
+ Fan-out targets are messaged sequentially, one JSON result line per target.
82
+
52
83
  ### Create options
53
84
 
54
85
  | Flag | Description |
@@ -60,6 +91,7 @@ Skill copy (same binary via wrapper): `scripts/ftown-sessions` in this skill dir
60
91
  | `--command` | Full command override (skips `--shell` builder) |
61
92
  | `--parent` | Set parent to `$FTOWN_SESSION_ID` |
62
93
  | `--parent-id` | Explicit parent session UUID |
94
+ | `--orchestrator` | Brief the new agent (non-`shell`) to spawn and coordinate sibling sessions |
63
95
  | `--model` | Cursor model name |
64
96
 
65
97
  Returns JSON with the new `session.id` — use that id for `screen` / `grep` / `keys`.
@@ -82,8 +114,21 @@ $CLI grep <child-id> --pattern 'FAIL|Error'
82
114
  Spawned ftown sessions receive:
83
115
 
84
116
  - `FTOWN_SESSION_ID` — this session (use with `--parent`)
117
+ - `FTOWN_PARENT_SESSION_ID` — the parent session id, set on children spawned with `--parent` / `--parent-id`
85
118
  - `FTOWN_HOOK_PORT` / `FTOWN_HOOK_TOKEN` — hook forwarding (not for cross-session control)
86
119
 
120
+ Agent children (any `--shell` except `shell`) spawned with a parent also get an
121
+ automatic one-paragraph briefing prepended to their first input: it states their
122
+ name/id and parent name/id, and how to reach parent and siblings via `tell`. The
123
+ creator's `--prompt` follows after a `Task:` line.
124
+
125
+ An agent session created with `--orchestrator` additionally gets a one-paragraph
126
+ briefing teaching it to spawn worker sessions with `create --parent`, that those
127
+ children report back via `tell` (arriving as `[ftown msg from <name>]` lines in its
128
+ terminal), and how to inspect/message any session with `list` / `screen` / `grep` /
129
+ `tell`. When both apply, the child paragraph comes first, then the orchestrator
130
+ paragraph, separated by a blank line.
131
+
87
132
  ## HTTP API (optional)
88
133
 
89
134
  The CLI wraps the loopback API. Raw access if needed:
@@ -95,6 +140,10 @@ The CLI wraps the loopback API. Raw access if needed:
95
140
  | GET | `/api/sessions/:id/screen` | Terminal lines |
96
141
  | POST | `/api/sessions/:id/grep` | Search |
97
142
  | POST | `/api/sessions/:id/keys` | Send keys |
143
+ | POST | `/api/sessions/:id/message` | Deliver a message line (`{ text, from? }`) |
144
+ | DELETE | `/api/sessions/:id` | Remove (tombstone-archived) |
145
+ | GET | `/api/archive` | List removed-session tombstones |
146
+ | POST | `/api/sessions/:id/revive` | Recreate a removed session (new id) |
98
147
 
99
148
  ## If the CLI is missing
100
149
 
@@ -1,43 +0,0 @@
1
- ---
2
- name: bridge-harness
3
- description: Control local ftown-bridge sessions via auto-deployed ~/.ftown/bin/ftown-harness. Triggers on bridge harness, /bridge-harness, bridge sessions.
4
- ---
5
-
6
- # bridge-harness
7
-
8
- ## Entry (auto-deployed)
9
-
10
- ```bash
11
- ~/.ftown/bin/ftown-harness <cmd>
12
- ```
13
-
14
- Read `~/.ftown/harness-agent.md` on each bridge start. Never curl/lsof the local bridge API.
15
-
16
- ## Playbook
17
-
18
- ```bash
19
- ftown-harness status
20
- ftown-harness here -n 25 # tails log even if process dead (status=error)
21
- ftown-harness ls --tail 3 # log=N on each row; previews dead sessions with logs
22
- ftown-harness grep ftown "error|FAIL" -C 2
23
- ```
24
-
25
- ## Commands
26
-
27
- | Cmd | Notes |
28
- |-----|-------|
29
- | `here -n N` | Workspace walk-up; **tails when dead** if log exists |
30
- | `ls --tail N` | Shows `log=lines`; preview any session with logs |
31
- | `tail` / `grep` | ANSI+OSC stripped; `grep -C 2` context |
32
- | `send` | `--dry-run` first; `-s` submit; only when user asks |
33
- | `--json` | `ftown-harness --json ls` etc. |
34
-
35
- Lookup: exact name → substring → id prefix.
36
-
37
- ## Dead vs error
38
-
39
- `status=error` + `alive=false` does **not** mean no logs. Use `here`/`tail` — they read persisted terminal logs.
40
-
41
- ## context-mode
42
-
43
- Use `ftown-harness` in Bash only. No curl/wget to 127.0.0.1.