@berthdev/berth 0.0.0-stage → 0.2.1-rc.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/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/src/agents/claude-name.d.ts +1 -0
- package/dist/src/agents/claude-name.js +57 -0
- package/dist/src/agents/claude-name.js.map +1 -0
- package/dist/src/agents/commands.d.ts +23 -0
- package/dist/src/agents/commands.js +48 -0
- package/dist/src/agents/commands.js.map +1 -0
- package/dist/src/agents/hooks.d.ts +13 -0
- package/dist/src/agents/hooks.js +141 -0
- package/dist/src/agents/hooks.js.map +1 -0
- package/dist/src/agents/registry.d.ts +33 -0
- package/dist/src/agents/registry.js +141 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/cli/main.d.ts +4 -0
- package/dist/src/cli/main.js +236 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/config/index.d.ts +21 -0
- package/dist/src/config/index.js +77 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/context.d.ts +9 -0
- package/dist/src/context.js +12 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/errors.d.ts +13 -0
- package/dist/src/errors.js +17 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/iterm2/install.d.ts +29 -0
- package/dist/src/iterm2/install.js +112 -0
- package/dist/src/iterm2/install.js.map +1 -0
- package/dist/src/present/attach.d.ts +16 -0
- package/dist/src/present/attach.js +31 -0
- package/dist/src/present/attach.js.map +1 -0
- package/dist/src/present/state.d.ts +14 -0
- package/dist/src/present/state.js +63 -0
- package/dist/src/present/state.js.map +1 -0
- package/dist/src/registry/doctor.d.ts +7 -0
- package/dist/src/registry/doctor.js +31 -0
- package/dist/src/registry/doctor.js.map +1 -0
- package/dist/src/registry/sessions.d.ts +47 -0
- package/dist/src/registry/sessions.js +138 -0
- package/dist/src/registry/sessions.js.map +1 -0
- package/dist/src/restore/ensure.d.ts +17 -0
- package/dist/src/restore/ensure.js +57 -0
- package/dist/src/restore/ensure.js.map +1 -0
- package/dist/src/restore/lock.d.ts +6 -0
- package/dist/src/restore/lock.js +74 -0
- package/dist/src/restore/lock.js.map +1 -0
- package/dist/src/shell.d.ts +6 -0
- package/dist/src/shell.js +68 -0
- package/dist/src/shell.js.map +1 -0
- package/dist/src/snapshot/store.d.ts +29 -0
- package/dist/src/snapshot/store.js +90 -0
- package/dist/src/snapshot/store.js.map +1 -0
- package/dist/src/snapshot/take.d.ts +5 -0
- package/dist/src/snapshot/take.js +33 -0
- package/dist/src/snapshot/take.js.map +1 -0
- package/dist/src/tmux/client.d.ts +49 -0
- package/dist/src/tmux/client.js +111 -0
- package/dist/src/tmux/client.js.map +1 -0
- package/dist/src/tmux/install.d.ts +4 -0
- package/dist/src/tmux/install.js +64 -0
- package/dist/src/tmux/install.js.map +1 -0
- package/dist/src/tools.d.ts +5 -0
- package/dist/src/tools.js +16 -0
- package/dist/src/tools.js.map +1 -0
- package/package.json +49 -4
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** One managed session as recorded in a snapshot (implementation §2.1). */
|
|
2
|
+
export interface SnapshotSession {
|
|
3
|
+
name: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
agent: string;
|
|
6
|
+
argv: string;
|
|
7
|
+
conversation: string;
|
|
8
|
+
origin: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Snapshot {
|
|
11
|
+
version: 1;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
sessions: SnapshotSession[];
|
|
14
|
+
}
|
|
15
|
+
export interface StoredSnapshot extends Snapshot {
|
|
16
|
+
dir: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class SnapshotStore {
|
|
19
|
+
readonly root: string;
|
|
20
|
+
constructor(root: string);
|
|
21
|
+
/** Snapshot directories, newest first. */
|
|
22
|
+
list(): string[];
|
|
23
|
+
read(dir: string): StoredSnapshot;
|
|
24
|
+
newest(): StoredSnapshot | undefined;
|
|
25
|
+
/** Write atomically: build in a temporary directory, then rename into place. */
|
|
26
|
+
write(sessions: SnapshotSession[], scrollback: Record<string, string>, now?: Date): string;
|
|
27
|
+
prune(keep: number): void;
|
|
28
|
+
scrollback(dir: string, session: string): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { BerthError, ExitCode } from '../errors.js';
|
|
4
|
+
const SNAPSHOT_VERSION = 1;
|
|
5
|
+
const DIR_PATTERN = /^\d{8}T\d{6}(\d{3})?Z(-\d+)?$/;
|
|
6
|
+
function timestampDir(date) {
|
|
7
|
+
return date.toISOString().replace(/[-:]/g, '').replace(/\.(\d{3})Z$/, '$1Z');
|
|
8
|
+
}
|
|
9
|
+
/** Session names may contain '/' and other characters a file name cannot. */
|
|
10
|
+
function scrollbackFile(session) {
|
|
11
|
+
return `${encodeURIComponent(session)}.txt`;
|
|
12
|
+
}
|
|
13
|
+
export class SnapshotStore {
|
|
14
|
+
root;
|
|
15
|
+
constructor(root) {
|
|
16
|
+
this.root = root;
|
|
17
|
+
}
|
|
18
|
+
/** Snapshot directories, newest first. */
|
|
19
|
+
list() {
|
|
20
|
+
if (!existsSync(this.root))
|
|
21
|
+
return [];
|
|
22
|
+
return readdirSync(this.root)
|
|
23
|
+
.filter((name) => DIR_PATTERN.test(name) && existsSync(join(this.root, name, 'sessions.json')))
|
|
24
|
+
.sort()
|
|
25
|
+
.reverse()
|
|
26
|
+
.map((name) => join(this.root, name));
|
|
27
|
+
}
|
|
28
|
+
read(dir) {
|
|
29
|
+
const file = join(dir, 'sessions.json');
|
|
30
|
+
let data;
|
|
31
|
+
try {
|
|
32
|
+
data = JSON.parse(readFileSync(file, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new BerthError(`cannot read snapshot ${file}: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
const snapshot = data;
|
|
38
|
+
if (snapshot.version !== SNAPSHOT_VERSION) {
|
|
39
|
+
throw new BerthError(`unsupported snapshot version ${String(snapshot.version)} in ${file}`);
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(snapshot.sessions))
|
|
42
|
+
throw new BerthError(`malformed snapshot ${file}`);
|
|
43
|
+
return { ...snapshot, dir };
|
|
44
|
+
}
|
|
45
|
+
newest() {
|
|
46
|
+
const [dir] = this.list();
|
|
47
|
+
return dir ? this.read(dir) : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Write atomically: build in a temporary directory, then rename into place. */
|
|
50
|
+
write(sessions, scrollback, now = new Date()) {
|
|
51
|
+
mkdirSync(this.root, { recursive: true });
|
|
52
|
+
let name = timestampDir(now);
|
|
53
|
+
for (let n = 2; existsSync(join(this.root, name)); n++)
|
|
54
|
+
name = `${timestampDir(now)}-${n}`;
|
|
55
|
+
const tmp = join(this.root, `.tmp-${process.pid}-${name}`);
|
|
56
|
+
mkdirSync(join(tmp, 'scrollback'), { recursive: true });
|
|
57
|
+
const snapshot = { version: SNAPSHOT_VERSION, createdAt: now.toISOString(), sessions };
|
|
58
|
+
writeFileSync(join(tmp, 'sessions.json'), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
59
|
+
for (const [session, text] of Object.entries(scrollback)) {
|
|
60
|
+
writeFileSync(join(tmp, 'scrollback', scrollbackFile(session)), text);
|
|
61
|
+
}
|
|
62
|
+
// Concurrent snapshots (tmux hooks run in the background) may pick the same name.
|
|
63
|
+
for (let n = 2;; n++) {
|
|
64
|
+
const dir = join(this.root, name);
|
|
65
|
+
try {
|
|
66
|
+
renameSync(tmp, dir);
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const code = error.code;
|
|
71
|
+
if ((code !== 'ENOTEMPTY' && code !== 'EEXIST') || n > 50) {
|
|
72
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
name = `${timestampDir(now)}-${n}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
prune(keep) {
|
|
80
|
+
for (const dir of this.list().slice(keep))
|
|
81
|
+
rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
scrollback(dir, session) {
|
|
84
|
+
const file = join(dir, 'scrollback', scrollbackFile(session));
|
|
85
|
+
if (!existsSync(file))
|
|
86
|
+
throw new BerthError(`no saved scrollback for ${session}`, ExitCode.notFound);
|
|
87
|
+
return readFileSync(file, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/snapshot/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
2
|
+
/** Record every managed live session; returns the snapshot directory. */
|
|
3
|
+
/** Snapshot, never throwing: for callers whose own work must not fail on it. */
|
|
4
|
+
export declare function snapshotQuietly(ctx: Context): void;
|
|
5
|
+
export declare function takeSnapshot(ctx: Context, now?: Date): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { claudeConversationByName } from '../agents/claude-name.js';
|
|
2
|
+
/** Record every managed live session; returns the snapshot directory. */
|
|
3
|
+
/** Snapshot, never throwing: for callers whose own work must not fail on it. */
|
|
4
|
+
export function snapshotQuietly(ctx) {
|
|
5
|
+
try {
|
|
6
|
+
takeSnapshot(ctx);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// The next change or hook snapshots again.
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function takeSnapshot(ctx, now = new Date()) {
|
|
13
|
+
const sessions = [];
|
|
14
|
+
const scrollback = {};
|
|
15
|
+
for (const s of ctx.tmux.listSessions()) {
|
|
16
|
+
if (!s.meta.agent)
|
|
17
|
+
continue;
|
|
18
|
+
const cwd = s.meta.cwd ?? s.path;
|
|
19
|
+
let conversation = s.meta.conversation ?? '';
|
|
20
|
+
// No hook-reported id: fall back to the name key (spec §4.2).
|
|
21
|
+
if (!conversation && s.meta.agent === 'claude') {
|
|
22
|
+
conversation = claudeConversationByName(cwd, s.name) ?? '';
|
|
23
|
+
if (conversation)
|
|
24
|
+
ctx.tmux.setMeta(s.name, { conversation });
|
|
25
|
+
}
|
|
26
|
+
sessions.push({ name: s.name, cwd, agent: s.meta.agent, argv: s.meta.argv ?? '', conversation, origin: s.meta.origin ?? 'none' });
|
|
27
|
+
scrollback[s.name] = ctx.tmux.capturePane(s.name, ctx.config.scrollbackLines);
|
|
28
|
+
}
|
|
29
|
+
const dir = ctx.store.write(sessions, scrollback, now);
|
|
30
|
+
ctx.store.prune(ctx.config.retention);
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=take.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"take.js","sourceRoot":"","sources":["../../../src/snapshot/take.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,yEAAyE;AACzE,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,IAAI,CAAC;QACH,YAAY,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACP,2CAA2C;IAC7C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAY,EAAE,GAAG,GAAG,IAAI,IAAI,EAAE;IACzD,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,MAAM,UAAU,GAA2B,EAAE,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;QACxC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK;YAAE,SAAS;QAC5B,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC;QACjC,IAAI,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;QAC7C,8DAA8D;QAC9D,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/C,YAAY,GAAG,wBAAwB,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC3D,IAAI,YAAY;gBAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC;QAClI,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;IACvD,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACtC,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Berth metadata stored on a tmux session as user options (spec §4). */
|
|
2
|
+
export declare const META_KEYS: readonly ["agent", "cwd", "argv", "conversation", "origin", "state", "note"];
|
|
3
|
+
export type MetaKey = (typeof META_KEYS)[number];
|
|
4
|
+
export type SessionMeta = Partial<Record<MetaKey, string>>;
|
|
5
|
+
export interface LiveSession {
|
|
6
|
+
name: string;
|
|
7
|
+
attachedClients: number;
|
|
8
|
+
path: string;
|
|
9
|
+
meta: SessionMeta;
|
|
10
|
+
}
|
|
11
|
+
export interface TmuxTarget {
|
|
12
|
+
bin: string;
|
|
13
|
+
/** `-L` socket name; undefined addresses the default server (or $TMUX inside a pane). */
|
|
14
|
+
socket?: string;
|
|
15
|
+
/** `-S` socket path; takes precedence over `socket`. Used by hooks running inside a pane. */
|
|
16
|
+
socketPath?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class Tmux {
|
|
19
|
+
readonly target: TmuxTarget;
|
|
20
|
+
constructor(target: TmuxTarget);
|
|
21
|
+
private baseArgs;
|
|
22
|
+
/** Run tmux; returns stdout. Throws BerthError on failure unless `allowFail`. */
|
|
23
|
+
run(args: string[], allowFail?: boolean): {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
};
|
|
28
|
+
serverRunning(): boolean;
|
|
29
|
+
listSessions(): LiveSession[];
|
|
30
|
+
/** Session that owns a pane, e.g. $TMUX_PANE inside an agent's hook. */
|
|
31
|
+
sessionOfPane(pane: string): string | undefined;
|
|
32
|
+
/** Working directory of the session's active pane, as the shell or agent left it. */
|
|
33
|
+
paneCurrentPath(name: string): string | undefined;
|
|
34
|
+
hasSession(name: string): boolean;
|
|
35
|
+
newSession(name: string, cwd: string, command: string): void;
|
|
36
|
+
setMeta(name: string, meta: SessionMeta): void;
|
|
37
|
+
/**
|
|
38
|
+
* Make tmux send the session's name, prefixed by its agent state, as the
|
|
39
|
+
* outer terminal's title (OSC 2). Per session, so switch-client updates it.
|
|
40
|
+
*/
|
|
41
|
+
setTitle(name: string): void;
|
|
42
|
+
renameSession(from: string, to: string): void;
|
|
43
|
+
killSession(name: string): void;
|
|
44
|
+
capturePane(name: string, lines: number): string;
|
|
45
|
+
/** Command that attaches a terminal to a session. */
|
|
46
|
+
attachArgv(name: string): string[];
|
|
47
|
+
/** Server-wide kill, allowed only on Berth test sockets (implementation §5.1). */
|
|
48
|
+
killServer(): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { BerthError, ExitCode } from '../errors.js';
|
|
3
|
+
/** Berth metadata stored on a tmux session as user options (spec §4). */
|
|
4
|
+
export const META_KEYS = ['agent', 'cwd', 'argv', 'conversation', 'origin', 'state', 'note'];
|
|
5
|
+
const SEP = '\x1f';
|
|
6
|
+
const TEST_SOCKET_PREFIX = 'berth-test-';
|
|
7
|
+
export class Tmux {
|
|
8
|
+
target;
|
|
9
|
+
constructor(target) {
|
|
10
|
+
this.target = target;
|
|
11
|
+
}
|
|
12
|
+
baseArgs() {
|
|
13
|
+
// -u: UTF-8 output whatever the locale. Without it tmux replaces non-ASCII
|
|
14
|
+
// bytes and the field separator with '_' (launchd-started editors and
|
|
15
|
+
// iTerm2 AutoLaunch scripts often have no locale), breaking every row.
|
|
16
|
+
if (this.target.socketPath)
|
|
17
|
+
return ['-u', '-S', this.target.socketPath];
|
|
18
|
+
return this.target.socket ? ['-u', '-L', this.target.socket] : ['-u'];
|
|
19
|
+
}
|
|
20
|
+
/** Run tmux; returns stdout. Throws BerthError on failure unless `allowFail`. */
|
|
21
|
+
run(args, allowFail = false) {
|
|
22
|
+
const result = spawnSync(this.target.bin, [...this.baseArgs(), ...args], { encoding: 'utf8' });
|
|
23
|
+
if (result.error) {
|
|
24
|
+
throw new BerthError(`tmux unavailable (${this.target.bin}): ${result.error.message}`, ExitCode.tmuxUnavailable);
|
|
25
|
+
}
|
|
26
|
+
const ok = result.status === 0;
|
|
27
|
+
if (!ok && !allowFail) {
|
|
28
|
+
throw new BerthError(`tmux ${args[0]} failed: ${result.stderr.trim()}`, ExitCode.failure);
|
|
29
|
+
}
|
|
30
|
+
return { ok, stdout: result.stdout, stderr: result.stderr };
|
|
31
|
+
}
|
|
32
|
+
serverRunning() {
|
|
33
|
+
return this.run(['list-sessions', '-F', '#{session_name}'], true).ok;
|
|
34
|
+
}
|
|
35
|
+
listSessions() {
|
|
36
|
+
const fields = ['#{session_name}', '#{session_attached}', '#{session_path}', ...META_KEYS.map((k) => `#{@berth_${k}}`)];
|
|
37
|
+
const result = this.run(['list-sessions', '-F', fields.join(SEP)], true);
|
|
38
|
+
if (!result.ok)
|
|
39
|
+
return [];
|
|
40
|
+
return result.stdout
|
|
41
|
+
.split('\n')
|
|
42
|
+
.filter((line) => line.length > 0)
|
|
43
|
+
.map((line) => {
|
|
44
|
+
const [name, attached, path, ...values] = line.split(SEP);
|
|
45
|
+
const meta = {};
|
|
46
|
+
META_KEYS.forEach((key, i) => {
|
|
47
|
+
if (values[i])
|
|
48
|
+
meta[key] = values[i];
|
|
49
|
+
});
|
|
50
|
+
return { name, attachedClients: Number(attached) || 0, path, meta };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Session that owns a pane, e.g. $TMUX_PANE inside an agent's hook. */
|
|
54
|
+
sessionOfPane(pane) {
|
|
55
|
+
const result = this.run(['display-message', '-p', '-t', pane, '#{session_name}'], true);
|
|
56
|
+
const name = result.stdout.trim();
|
|
57
|
+
return result.ok && name ? name : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** Working directory of the session's active pane, as the shell or agent left it. */
|
|
60
|
+
paneCurrentPath(name) {
|
|
61
|
+
const result = this.run(['display-message', '-p', '-t', `=${name}:`, '#{pane_current_path}'], true);
|
|
62
|
+
const path = result.stdout.trim();
|
|
63
|
+
return result.ok && path ? path : undefined;
|
|
64
|
+
}
|
|
65
|
+
hasSession(name) {
|
|
66
|
+
return this.run(['has-session', '-t', `=${name}`], true).ok;
|
|
67
|
+
}
|
|
68
|
+
newSession(name, cwd, command) {
|
|
69
|
+
const args = ['new-session', '-d', '-s', name, '-c', cwd];
|
|
70
|
+
this.run(command ? [...args, command] : args);
|
|
71
|
+
}
|
|
72
|
+
setMeta(name, meta) {
|
|
73
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
// One line per session in list output: no newlines or separators in values.
|
|
77
|
+
this.run(['set-option', '-t', `=${name}:`, `@berth_${key}`, value.replace(/[\r\n\x1f]+/g, ' ')]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Make tmux send the session's name, prefixed by its agent state, as the
|
|
82
|
+
* outer terminal's title (OSC 2). Per session, so switch-client updates it.
|
|
83
|
+
*/
|
|
84
|
+
setTitle(name) {
|
|
85
|
+
const glyph = '#{?#{==:#{@berth_state},working},● ,}#{?#{==:#{@berth_state},waiting},◐ ,}#{?#{==:#{@berth_state},done},✓ ,}';
|
|
86
|
+
this.run(['set-option', '-t', `=${name}:`, 'set-titles', 'on'], true);
|
|
87
|
+
this.run(['set-option', '-t', `=${name}:`, 'set-titles-string', `${glyph}#S`], true);
|
|
88
|
+
}
|
|
89
|
+
renameSession(from, to) {
|
|
90
|
+
this.run(['rename-session', '-t', `=${from}`, to]);
|
|
91
|
+
}
|
|
92
|
+
killSession(name) {
|
|
93
|
+
this.run(['kill-session', '-t', `=${name}`]);
|
|
94
|
+
}
|
|
95
|
+
capturePane(name, lines) {
|
|
96
|
+
const result = this.run(['capture-pane', '-p', '-S', `-${lines}`, '-t', `=${name}:`], true);
|
|
97
|
+
return result.ok ? result.stdout : '';
|
|
98
|
+
}
|
|
99
|
+
/** Command that attaches a terminal to a session. */
|
|
100
|
+
attachArgv(name) {
|
|
101
|
+
return [this.target.bin, ...this.baseArgs(), 'attach-session', '-t', `=${name}`];
|
|
102
|
+
}
|
|
103
|
+
/** Server-wide kill, allowed only on Berth test sockets (implementation §5.1). */
|
|
104
|
+
killServer() {
|
|
105
|
+
if (!this.target.socket?.startsWith(TEST_SOCKET_PREFIX)) {
|
|
106
|
+
throw new BerthError('refusing to kill a tmux server that is not a Berth test server');
|
|
107
|
+
}
|
|
108
|
+
this.run(['kill-server'], true);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/tmux/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAEpD,yEAAyE;AACzE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAU,CAAC;AAWtG,MAAM,GAAG,GAAG,MAAM,CAAC;AACnB,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAUzC,MAAM,OAAO,IAAI;IACM;IAArB,YAAqB,MAAkB;QAAlB,WAAM,GAAN,MAAM,CAAY;IAAG,CAAC;IAEnC,QAAQ;QACd,2EAA2E;QAC3E,sEAAsE;QACtE,uEAAuE;QACvE,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU;YAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACxE,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxE,CAAC;IAED,iFAAiF;IACjF,GAAG,CAAC,IAAc,EAAE,SAAS,GAAG,KAAK;QACnC,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC/F,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,IAAI,UAAU,CAAC,qBAAqB,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;QACnH,CAAC;QACD,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,UAAU,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC5F,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAC9D,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,IAAI,EAAE,iBAAiB,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;IACvE,CAAC;IAED,YAAY;QACV,MAAM,MAAM,GAAG,CAAC,iBAAiB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;QACxH,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACzE,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC,MAAM;aACjB,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;aACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACZ,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC1D,MAAM,IAAI,GAAgB,EAAE,CAAC;YAC7B,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;gBAC3B,IAAI,MAAM,CAAC,CAAC,CAAC;oBAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACvC,CAAC,CAAC,CAAC;YACH,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACtE,CAAC,CAAC,CAAC;IACP,CAAC;IAED,wEAAwE;IACxE,aAAa,CAAC,IAAY;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,CAAC,EAAE,IAAI,CAAC,CAAC;QACxF,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9C,CAAC;IAED,qFAAqF;IACrF,eAAe,CAAC,IAAY;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,CAAC;QACpG,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9C,CAAC;IAED,UAAU,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;IAC9D,CAAC;IAED,UAAU,CAAC,IAAY,EAAE,GAAW,EAAE,OAAe;QACnD,MAAM,IAAI,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAC1D,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,CAAC,IAAY,EAAE,IAAiB;QACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAS;YAClC,4EAA4E;YAC5E,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,UAAU,GAAG,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;QACnG,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,IAAY;QACnB,MAAM,KAAK,GAAG,8GAA8G,CAAC;QAC7H,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QACtE,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,mBAAmB,EAAE,GAAG,KAAK,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IACvF,CAAC;IAED,aAAa,CAAC,IAAY,EAAE,EAAU;QACpC,IAAI,CAAC,GAAG,CAAC,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,IAAI,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,KAAa;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5F,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,CAAC;IAED,qDAAqD;IACrD,UAAU,CAAC,IAAY;QACrB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,kFAAkF;IAClF,UAAU;QACR,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,UAAU,CAAC,gEAAgE,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Tmux } from './client.js';
|
|
2
|
+
export declare function tmuxConfPath(env?: NodeJS.ProcessEnv): string;
|
|
3
|
+
export declare function installTmux(tmux: Tmux, cliPath: string, env?: NodeJS.ProcessEnv): string;
|
|
4
|
+
export declare function uninstallTmux(tmux: Tmux, env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { shellQuote } from '../shell.js';
|
|
5
|
+
/**
|
|
6
|
+
* `berth tmux install` (spec §5.3): a managed block in the user's tmux config
|
|
7
|
+
* that snapshots on session create / rename / close. Hooks use a fixed array
|
|
8
|
+
* index so the user's own hooks for the same events stay in place.
|
|
9
|
+
*/
|
|
10
|
+
const BEGIN = '# >>> berth >>>';
|
|
11
|
+
const END = '# <<< berth <<<';
|
|
12
|
+
const EVENTS = ['session-created', 'session-renamed', 'session-closed'];
|
|
13
|
+
const ATTACH_EVENTS = ['client-attached', 'client-session-changed'];
|
|
14
|
+
const INDEX = 97;
|
|
15
|
+
function hookArgs(cliPath) {
|
|
16
|
+
// Hooks are global in the user's tmux config and run in whichever server
|
|
17
|
+
// fires them: pass that server's socket so state stays per server. #{q:}
|
|
18
|
+
// quotes values for the shell (session names may contain quotes).
|
|
19
|
+
const berth = `${shellQuote(process.execPath)} ${shellQuote(cliPath)} --socket-path #{q:socket_path}`;
|
|
20
|
+
const snapshot = `${berth} snapshot >/dev/null 2>&1`;
|
|
21
|
+
// A bell asks for attention: agents without hooks become `waiting`.
|
|
22
|
+
const bell = `${berth} bell --session #{q:session_name} >/dev/null 2>&1`;
|
|
23
|
+
// Marks iTerm2 panes with their session and shows state on the new client (spec §7.3).
|
|
24
|
+
const attached = `${berth} attached --session #{q:session_name} --tty #{q:client_tty} --termtype #{q:client_termtype} >/dev/null 2>&1`;
|
|
25
|
+
return [
|
|
26
|
+
...EVENTS.map((event) => ['set-hook', '-g', `${event}[${INDEX}]`, `run-shell -b ${JSON.stringify(snapshot)}`]),
|
|
27
|
+
['set-hook', '-g', `alert-bell[${INDEX}]`, `run-shell -b ${JSON.stringify(bell)}`],
|
|
28
|
+
...ATTACH_EVENTS.map((event) => ['set-hook', '-g', `${event}[${INDEX}]`, `run-shell -b ${JSON.stringify(attached)}`]),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function tmuxConfPath(env = process.env) {
|
|
32
|
+
return join(env.HOME || homedir(), '.tmux.conf');
|
|
33
|
+
}
|
|
34
|
+
function withoutBlock(text) {
|
|
35
|
+
const start = text.indexOf(BEGIN);
|
|
36
|
+
const end = text.indexOf(END);
|
|
37
|
+
if (start < 0 || end < start)
|
|
38
|
+
return text;
|
|
39
|
+
return (text.slice(0, start) + text.slice(end + END.length)).replace(/\n{3,}/g, '\n\n').replace(/^\n+/, '');
|
|
40
|
+
}
|
|
41
|
+
export function installTmux(tmux, cliPath, env = process.env) {
|
|
42
|
+
const file = tmuxConfPath(env);
|
|
43
|
+
const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
44
|
+
const lines = hookArgs(cliPath).map((args) => args.map((a, i) => (i === 3 ? `'${a.replace(/'/g, `'\\''`)}'` : a)).join(' '));
|
|
45
|
+
const block = [BEGIN, '# Managed by berth; remove with `berth tmux uninstall`.', ...lines, END].join('\n');
|
|
46
|
+
const base = withoutBlock(current).replace(/\s*$/, '');
|
|
47
|
+
writeFileSync(file, `${base ? `${base}\n\n` : ''}${block}\n`);
|
|
48
|
+
if (tmux.serverRunning())
|
|
49
|
+
for (const args of hookArgs(cliPath))
|
|
50
|
+
tmux.run(args, true);
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
export function uninstallTmux(tmux, env = process.env) {
|
|
54
|
+
const file = tmuxConfPath(env);
|
|
55
|
+
if (existsSync(file)) {
|
|
56
|
+
const rest = withoutBlock(readFileSync(file, 'utf8')).replace(/\s*$/, '');
|
|
57
|
+
writeFileSync(file, rest ? `${rest}\n` : '');
|
|
58
|
+
}
|
|
59
|
+
if (tmux.serverRunning())
|
|
60
|
+
for (const event of [...EVENTS, 'alert-bell', ...ATTACH_EVENTS])
|
|
61
|
+
tmux.run(['set-hook', '-gu', `${event}[${INDEX}]`], true);
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../../src/tmux/install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Context } from './context.js';
|
|
2
|
+
import type { Tools } from './iterm2/install.js';
|
|
3
|
+
/** Absolute path of a binary on PATH, or the name unchanged when not found. */
|
|
4
|
+
export declare function resolveBinary(name: string, env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
export declare function toolsFor(ctx: Context, cliPath: string): Tools;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
3
|
+
/** Absolute path of a binary on PATH, or the name unchanged when not found. */
|
|
4
|
+
export function resolveBinary(name, env = process.env) {
|
|
5
|
+
if (isAbsolute(name))
|
|
6
|
+
return name;
|
|
7
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) {
|
|
8
|
+
if (dir && existsSync(join(dir, name)))
|
|
9
|
+
return join(dir, name);
|
|
10
|
+
}
|
|
11
|
+
return name;
|
|
12
|
+
}
|
|
13
|
+
export function toolsFor(ctx, cliPath) {
|
|
14
|
+
return { node: process.execPath, cli: cliPath, tmux: resolveBinary(ctx.config.tmuxBin), socket: ctx.config.tmuxSocket, path: process.env.PATH };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIxD,+EAA+E;AAC/E,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG;IAC3D,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACpD,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAY,EAAE,OAAe;IACpD,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;AAClJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@berthdev/berth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1-rc.0",
|
|
4
|
+
"description": "Keep terminal-based coding agents running in tmux and bring each back, conversation included.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"berth": "dist/src/cli/main.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/src",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"smol-toml": "^1.3.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.11.0",
|
|
24
|
+
"typescript": "^5.6.0"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/maxzyma/berth.git",
|
|
29
|
+
"directory": "packages/core"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"tmux",
|
|
33
|
+
"coding agent",
|
|
34
|
+
"claude code",
|
|
35
|
+
"codex",
|
|
36
|
+
"agents",
|
|
37
|
+
"terminal"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"prepublishOnly": "tsc -b"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/maxzyma/berth#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/maxzyma/berth/issues"
|
|
50
|
+
}
|
|
51
|
+
}
|