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.
- package/dist/agent-commands.js +1 -1
- package/dist/agent-commands.js.map +1 -1
- package/dist/claude-runner.d.ts +29 -0
- package/dist/claude-runner.js +246 -44
- package/dist/claude-runner.js.map +1 -1
- package/dist/create-ftown-session.d.ts +16 -0
- package/dist/create-ftown-session.js +70 -5
- package/dist/create-ftown-session.js.map +1 -1
- package/dist/cursor-hook-installer.d.ts +0 -2
- package/dist/cursor-hook-installer.js +3 -14
- package/dist/cursor-hook-installer.js.map +1 -1
- package/dist/ftown-sessions-cli.js +120 -2
- package/dist/ftown-sessions-cli.js.map +1 -1
- package/dist/hook-installer.js +1 -1
- package/dist/hook-installer.js.map +1 -1
- package/dist/index.js +245 -42
- package/dist/index.js.map +1 -1
- package/dist/local-api-server.d.ts +2 -0
- package/dist/local-api-server.js +197 -4
- package/dist/local-api-server.js.map +1 -1
- package/dist/remove-ftown-session.d.ts +23 -0
- package/dist/remove-ftown-session.js +32 -0
- package/dist/remove-ftown-session.js.map +1 -0
- package/dist/session-registry.d.ts +11 -1
- package/dist/session-registry.js +3 -3
- package/dist/session-registry.js.map +1 -1
- package/dist/session-store.d.ts +6 -1
- package/dist/session-store.js +32 -1
- package/dist/session-store.js.map +1 -1
- package/dist/tmux.d.ts +24 -0
- package/dist/tmux.js +149 -0
- package/dist/tmux.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/hooks/notify.sh +34 -12
- package/package.json +1 -1
- package/skills/ftown-sessions/SKILL.md +49 -0
- package/skills/bridge-harness/SKILL.md +0 -43
package/dist/session-store.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/session-store.js
CHANGED
|
@@ -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":"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,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"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"}
|
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
|
package/dist/tmux.js.map
ADDED
|
@@ -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 [ -
|
|
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"
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
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
|
@@ -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.
|