@shoppingjaws/peer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/index.js +345 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ShoppingJaws
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @shoppingjaws/peer
2
+
3
+ A CLI tool for managing WezTerm panes, peer groups, and inter-pane messaging.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Bun](https://bun.sh/) runtime
8
+ - [WezTerm](https://wezfurlong.org/wezterm/) terminal
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -g @shoppingjaws/peer
14
+ ```
15
+
16
+ ## Commands
17
+
18
+ ```
19
+ peer <list|peek|inbox|new-pane|new-tab|history|clean>
20
+ ```
21
+
22
+ ### `peer list`
23
+
24
+ List panes in the same tab and peer group.
25
+
26
+ ```bash
27
+ peer list
28
+ ```
29
+
30
+ ```
31
+ WINID TABID PANEID RELATION WORKSPACE SIZE TITLE
32
+ 0 0 0 self default 120x40 ~
33
+ 0 0 1 child default 120x20 ~
34
+ ```
35
+
36
+ `RELATION` shows the relationship from your perspective:
37
+ - `self` — current pane
38
+ - `child` — pane you created
39
+ - `parent` — pane that created you
40
+ - `none` — same tab but no peer relation
41
+
42
+ ### `peer peek <pane-id>`
43
+
44
+ Read terminal output from a pane. Only works for panes in the same tab or peer group.
45
+
46
+ ```bash
47
+ peer peek 1
48
+ peer peek 1 --start-line 0 --end-line 50
49
+ ```
50
+
51
+ | Option | Description |
52
+ |---|---|
53
+ | `--start-line N` | Start reading from line N |
54
+ | `--end-line N` | Stop reading at line N |
55
+
56
+ ### `peer new-pane [opts] [-- cmd]`
57
+
58
+ Split a new pane in the current tab and register it as a child in the peer group. Options are passed through to `wezterm cli split-pane`.
59
+
60
+ ```bash
61
+ peer new-pane
62
+ peer new-pane --horizontal
63
+ peer new-pane -- bash
64
+ ```
65
+
66
+ ### `peer new-tab [opts] [-- cmd]`
67
+
68
+ Spawn a new tab and register it as a child in the peer group. Options are passed through to `wezterm cli spawn`.
69
+
70
+ ```bash
71
+ peer new-tab
72
+ peer new-tab -- zsh
73
+ ```
74
+
75
+ ### `peer inbox send <pane-id> <message>`
76
+
77
+ Send a message to the specified pane.
78
+
79
+ ```bash
80
+ peer inbox send 1 "build done"
81
+ ```
82
+
83
+ ### `peer inbox open`
84
+
85
+ Show unread messages and mark them as read.
86
+
87
+ ```bash
88
+ peer inbox open
89
+ ```
90
+
91
+ ```
92
+ [#1 from:0 2025-04-05 12:00:00] build done
93
+ 1 message(s) marked as read.
94
+ ```
95
+
96
+ ### `peer history [pane-id]`
97
+
98
+ Show message history. Optionally filter by a specific pane.
99
+
100
+ ```bash
101
+ peer history
102
+ peer history 1
103
+ ```
104
+
105
+ ### `peer clean`
106
+
107
+ Delete all messages for the current session.
108
+
109
+ ```bash
110
+ peer clean
111
+ ```
112
+
113
+ ## Data Storage
114
+
115
+ Messages and peer group data are stored in `/tmp/peer/peer.db` (SQLite), isolated per session (`WEZTERM_UNIX_SOCKET`).
116
+
117
+ ## License
118
+
119
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // db.ts
5
+ import { Database } from "bun:sqlite";
6
+ import { existsSync, mkdirSync } from "fs";
7
+ var DB_DIR = "/tmp/peer";
8
+ var DB_PATH = `${DB_DIR}/peer.db`;
9
+ function initDb() {
10
+ if (!existsSync(DB_DIR)) {
11
+ mkdirSync(DB_DIR, { recursive: true });
12
+ }
13
+ const db = new Database(DB_PATH, { create: true });
14
+ db.run(`
15
+ CREATE TABLE IF NOT EXISTS messages (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ session_id TEXT NOT NULL,
18
+ from_pane TEXT NOT NULL,
19
+ to_pane TEXT NOT NULL,
20
+ message TEXT NOT NULL,
21
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
22
+ read_at TEXT DEFAULT NULL
23
+ )
24
+ `);
25
+ db.run(`
26
+ CREATE INDEX IF NOT EXISTS idx_messages_to_pane ON messages(session_id, to_pane, read_at)
27
+ `);
28
+ db.run(`
29
+ CREATE TABLE IF NOT EXISTS peer_edges (
30
+ session_id TEXT NOT NULL,
31
+ from_pane_id TEXT NOT NULL,
32
+ to_pane_id TEXT NOT NULL,
33
+ relation TEXT NOT NULL CHECK(relation IN ('parent', 'child')),
34
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ PRIMARY KEY (session_id, from_pane_id, to_pane_id)
36
+ )
37
+ `);
38
+ return db;
39
+ }
40
+
41
+ // pane.ts
42
+ var {$ } = globalThis.Bun;
43
+ function getMyPaneId() {
44
+ const paneId = process.env.WEZTERM_PANE;
45
+ if (!paneId) {
46
+ console.error("Error: WEZTERM_PANE environment variable is not set.");
47
+ process.exit(1);
48
+ }
49
+ return paneId;
50
+ }
51
+ function getSessionId() {
52
+ const socket = process.env.WEZTERM_UNIX_SOCKET;
53
+ if (!socket) {
54
+ console.error("Error: WEZTERM_UNIX_SOCKET environment variable is not set.");
55
+ process.exit(1);
56
+ }
57
+ return socket;
58
+ }
59
+ async function listPanes() {
60
+ const result = await $`wezterm cli list --format json`.quiet();
61
+ const panes = JSON.parse(result.stdout.toString());
62
+ return panes.map((p) => ({
63
+ window_id: p.window_id,
64
+ tab_id: p.tab_id,
65
+ pane_id: p.pane_id,
66
+ workspace: p.workspace,
67
+ size: `${p.size.cols}x${p.size.rows}`,
68
+ title: p.title
69
+ }));
70
+ }
71
+ async function getTabPaneIds() {
72
+ const myPaneId = getMyPaneId();
73
+ const panes = await listPanes();
74
+ const myPane = panes.find((p) => p.pane_id === Number(myPaneId));
75
+ if (!myPane)
76
+ return new Set;
77
+ return new Set(panes.filter((p) => p.tab_id === myPane.tab_id).map((p) => p.pane_id));
78
+ }
79
+ function getPeerGroupPaneIds(db) {
80
+ const sessionId = getSessionId();
81
+ const myPaneId = getMyPaneId();
82
+ const rows = db.prepare("SELECT DISTINCT to_pane_id FROM peer_edges WHERE session_id = ? AND from_pane_id = ?").all(sessionId, myPaneId);
83
+ return new Set(rows.map((r) => r.to_pane_id));
84
+ }
85
+ function registerPeerEdge(db, fromPaneId, toPaneId, relation) {
86
+ const sessionId = getSessionId();
87
+ const reverseRelation = relation === "child" ? "parent" : "child";
88
+ db.prepare("INSERT OR IGNORE INTO peer_edges (session_id, from_pane_id, to_pane_id, relation) VALUES (?, ?, ?, ?)").run(sessionId, fromPaneId, toPaneId, relation);
89
+ db.prepare("INSERT OR IGNORE INTO peer_edges (session_id, from_pane_id, to_pane_id, relation) VALUES (?, ?, ?, ?)").run(sessionId, toPaneId, fromPaneId, reverseRelation);
90
+ }
91
+ function getPeerRelation(db, paneId) {
92
+ const sessionId = getSessionId();
93
+ const myPaneId = getMyPaneId();
94
+ const row = db.prepare("SELECT relation FROM peer_edges WHERE session_id = ? AND from_pane_id = ? AND to_pane_id = ?").get(sessionId, myPaneId, paneId);
95
+ return row?.relation ?? null;
96
+ }
97
+
98
+ // commands/clean.ts
99
+ function cmdClean() {
100
+ const sessionId = getSessionId();
101
+ const db = initDb();
102
+ const result = db.prepare("SELECT COUNT(*) as count FROM messages WHERE session_id = ?").get(sessionId);
103
+ db.prepare("DELETE FROM messages WHERE session_id = ?").run(sessionId);
104
+ console.log(`Deleted ${result.count} messages. Database cleaned.`);
105
+ db.close();
106
+ }
107
+
108
+ // commands/history.ts
109
+ function cmdHistory(filterPaneId) {
110
+ const myPaneId = getMyPaneId();
111
+ const sessionId = getSessionId();
112
+ const db = initDb();
113
+ let messages;
114
+ if (filterPaneId) {
115
+ messages = db.prepare(`SELECT id, from_pane, to_pane, message, created_at, read_at FROM messages
116
+ WHERE session_id = ? AND ((from_pane = ? AND to_pane = ?) OR (from_pane = ? AND to_pane = ?))
117
+ ORDER BY created_at ASC`).all(sessionId, myPaneId, filterPaneId, filterPaneId, myPaneId);
118
+ } else {
119
+ messages = db.prepare(`SELECT id, from_pane, to_pane, message, created_at, read_at FROM messages
120
+ WHERE session_id = ? AND (from_pane = ? OR to_pane = ?)
121
+ ORDER BY created_at ASC`).all(sessionId, myPaneId, myPaneId);
122
+ }
123
+ if (messages.length === 0) {
124
+ console.log("No messages.");
125
+ db.close();
126
+ return;
127
+ }
128
+ for (const m of messages) {
129
+ const status = m.read_at ? "\u2713read" : "unread";
130
+ console.log(`[#${m.id} from:${m.from_pane} \u2192 to:${m.to_pane} ${m.created_at} ${status}]`);
131
+ console.log(m.message);
132
+ }
133
+ db.close();
134
+ }
135
+
136
+ // commands/list.ts
137
+ async function cmdList() {
138
+ const myPaneId = getMyPaneId();
139
+ const panes = await listPanes();
140
+ const myPane = panes.find((p) => p.pane_id === Number(myPaneId));
141
+ if (!myPane) {
142
+ console.error(`Error: Current pane ${myPaneId} not found.`);
143
+ process.exit(1);
144
+ }
145
+ const db = initDb();
146
+ const peerPaneIds = getPeerGroupPaneIds(db);
147
+ const sameTabIds = new Set(panes.filter((p) => p.tab_id === myPane.tab_id).map((p) => p.pane_id));
148
+ const visiblePanes = panes.filter((p) => sameTabIds.has(p.pane_id) || peerPaneIds.has(String(p.pane_id)));
149
+ console.log("WINID\tTABID\tPANEID\tRELATION\tWORKSPACE\tSIZE\tTITLE");
150
+ for (const p of visiblePanes) {
151
+ const relation = p.pane_id === Number(myPaneId) ? "self" : getPeerRelation(db, String(p.pane_id)) ?? "none";
152
+ console.log(`${p.window_id} ${p.tab_id} ${p.pane_id} ${relation} ${p.workspace} ${p.size} ${p.title}`);
153
+ }
154
+ db.close();
155
+ }
156
+
157
+ // commands/new-pane.ts
158
+ var {$: $2 } = globalThis.Bun;
159
+ async function cmdNewPane(args) {
160
+ const myPaneId = getMyPaneId();
161
+ const splitArgs = [];
162
+ let commandArgs = [];
163
+ const separatorIndex = args.indexOf("--");
164
+ if (separatorIndex >= 0) {
165
+ splitArgs.push(...args.slice(0, separatorIndex));
166
+ commandArgs = args.slice(separatorIndex + 1);
167
+ } else {
168
+ splitArgs.push(...args);
169
+ }
170
+ if (commandArgs.length > 0) {
171
+ splitArgs.push("--", ...commandArgs);
172
+ }
173
+ const result = await $2`wezterm cli split-pane ${splitArgs}`.quiet();
174
+ const newPaneId = result.stdout.toString().trim();
175
+ if (!newPaneId) {
176
+ console.error("Error: Failed to create new pane.");
177
+ process.exit(1);
178
+ }
179
+ const db = initDb();
180
+ registerPeerEdge(db, myPaneId, newPaneId, "child");
181
+ db.close();
182
+ console.log(`Created pane ${newPaneId} and added to peer group.`);
183
+ }
184
+
185
+ // commands/new-tab.ts
186
+ var {$: $3 } = globalThis.Bun;
187
+ async function cmdNewTab(args) {
188
+ const myPaneId = getMyPaneId();
189
+ const spawnArgs = [];
190
+ let commandArgs = [];
191
+ const separatorIndex = args.indexOf("--");
192
+ if (separatorIndex >= 0) {
193
+ spawnArgs.push(...args.slice(0, separatorIndex));
194
+ commandArgs = args.slice(separatorIndex + 1);
195
+ } else {
196
+ spawnArgs.push(...args);
197
+ }
198
+ if (commandArgs.length > 0) {
199
+ spawnArgs.push("--", ...commandArgs);
200
+ }
201
+ const result = await $3`wezterm cli spawn ${spawnArgs}`.quiet();
202
+ const newPaneId = result.stdout.toString().trim();
203
+ if (!newPaneId) {
204
+ console.error("Error: Failed to create new tab.");
205
+ process.exit(1);
206
+ }
207
+ const db = initDb();
208
+ registerPeerEdge(db, myPaneId, newPaneId, "child");
209
+ db.close();
210
+ console.log(`Created tab with pane ${newPaneId} and added to peer group.`);
211
+ }
212
+
213
+ // commands/open.ts
214
+ function cmdOpen() {
215
+ const myPaneId = getMyPaneId();
216
+ const sessionId = getSessionId();
217
+ const db = initDb();
218
+ const messages = db.prepare("SELECT id, from_pane, message, created_at FROM messages WHERE session_id = ? AND to_pane = ? AND read_at IS NULL ORDER BY created_at ASC").all(sessionId, myPaneId);
219
+ if (messages.length === 0) {
220
+ console.log("No unread messages.");
221
+ db.close();
222
+ return;
223
+ }
224
+ for (const m of messages) {
225
+ console.log(`[#${m.id} from:${m.from_pane} ${m.created_at}] ${m.message}`);
226
+ }
227
+ const ids = messages.map((m) => m.id);
228
+ const placeholders = ids.map(() => "?").join(",");
229
+ db.prepare(`UPDATE messages SET read_at = datetime('now') WHERE id IN (${placeholders})`).run(...ids);
230
+ console.log(`${messages.length} message(s) marked as read.`);
231
+ db.close();
232
+ }
233
+
234
+ // commands/peek.ts
235
+ var {$: $4 } = globalThis.Bun;
236
+ async function cmdPeek(paneId, args) {
237
+ const myPaneId = getMyPaneId();
238
+ const tabPanes = await getTabPaneIds();
239
+ const db = initDb();
240
+ const peerPaneIds = getPeerGroupPaneIds(db);
241
+ db.close();
242
+ if (!tabPanes.has(Number(paneId)) && !peerPaneIds.has(paneId)) {
243
+ console.error(`Error: Pane ${paneId} is not in the same tab or peer group as pane ${myPaneId}.`);
244
+ process.exit(1);
245
+ }
246
+ const opts = [];
247
+ for (let i = 0;i < args.length; i++) {
248
+ const arg = args[i];
249
+ const next = args[i + 1];
250
+ if (arg === "--start-line" && next) {
251
+ opts.push("--start-line", next);
252
+ i++;
253
+ } else if (arg === "--end-line" && next) {
254
+ opts.push("--end-line", next);
255
+ i++;
256
+ }
257
+ }
258
+ const result = await $4`wezterm cli get-text --pane-id ${paneId} ${opts}`.quiet();
259
+ console.log(result.stdout.toString());
260
+ }
261
+
262
+ // commands/send.ts
263
+ async function cmdSend(paneId, message) {
264
+ const myPaneId = getMyPaneId();
265
+ const sessionId = getSessionId();
266
+ const tabPanes = await getTabPaneIds();
267
+ const db = initDb();
268
+ const peerPaneIds = getPeerGroupPaneIds(db);
269
+ if (!tabPanes.has(Number(paneId)) && !peerPaneIds.has(paneId)) {
270
+ console.error(`Error: Pane ${paneId} is not in the same tab or peer group as pane ${myPaneId}.`);
271
+ db.close();
272
+ process.exit(1);
273
+ }
274
+ const stmt = db.prepare("INSERT INTO messages (session_id, from_pane, to_pane, message) VALUES (?, ?, ?, ?)");
275
+ const result = stmt.run(sessionId, myPaneId, paneId, message);
276
+ console.log(`Sent message to pane ${paneId} (id: ${result.lastInsertRowid})`);
277
+ db.close();
278
+ }
279
+
280
+ // index.ts
281
+ var [command, ...args] = process.argv.slice(2);
282
+ switch (command) {
283
+ case "list":
284
+ await cmdList();
285
+ break;
286
+ case "peek": {
287
+ const paneId = args[0];
288
+ if (!paneId) {
289
+ console.error("Usage: peer peek <pane-id> [--start-line N] [--end-line N]");
290
+ process.exit(1);
291
+ }
292
+ await cmdPeek(paneId, args.slice(1));
293
+ break;
294
+ }
295
+ case "inbox": {
296
+ const subcommand = args[0];
297
+ switch (subcommand) {
298
+ case "send": {
299
+ const paneId = args[1];
300
+ const message = args.slice(2).join(" ");
301
+ if (!paneId || !message) {
302
+ console.error("Usage: peer inbox send <pane-id> <message>");
303
+ process.exit(1);
304
+ }
305
+ await cmdSend(paneId, message);
306
+ break;
307
+ }
308
+ case "open":
309
+ cmdOpen();
310
+ break;
311
+ default:
312
+ console.log("Usage: peer inbox <open|send>");
313
+ console.log("");
314
+ console.log("Subcommands:");
315
+ console.log(" open Show and mark as read unread messages");
316
+ console.log(" send <pane-id> <msg> Send a message to a pane");
317
+ process.exit(subcommand ? 1 : 0);
318
+ }
319
+ break;
320
+ }
321
+ case "new-pane":
322
+ await cmdNewPane(args);
323
+ break;
324
+ case "new-tab":
325
+ await cmdNewTab(args);
326
+ break;
327
+ case "history":
328
+ cmdHistory(args[0]);
329
+ break;
330
+ case "clean":
331
+ cmdClean();
332
+ break;
333
+ default:
334
+ console.log("Usage: peer <list|peek|inbox|new-pane|new-tab|history|clean>");
335
+ console.log("");
336
+ console.log("Commands:");
337
+ console.log(" list List panes (same tab + peer group)");
338
+ console.log(" peek <pane-id> Read terminal text from a pane");
339
+ console.log(" inbox <open|send> Manage inbox messages");
340
+ console.log(" new-pane [opts] [-- cmd] Split a new pane in current tab");
341
+ console.log(" new-tab [opts] [-- cmd] Spawn a new tab and add to peer group");
342
+ console.log(" history [pane-id] Show message history");
343
+ console.log(" clean Reset the message database");
344
+ process.exit(command ? 1 : 0);
345
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@shoppingjaws/peer",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for managing wezterm panes, peer groups, and inter-pane messaging",
5
+ "type": "module",
6
+ "bin": {
7
+ "peer": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "bun build index.ts --outdir dist --target bun",
14
+ "prepublishOnly": "bun run build",
15
+ "test": "docker build -f Dockerfile.test -t template-bun-test . && docker run --rm template-bun-test",
16
+ "format": "biome format --write",
17
+ "format:check": "biome format",
18
+ "lint": "biome lint --fix",
19
+ "lint:check": "biome lint",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "wezterm",
24
+ "terminal",
25
+ "pane",
26
+ "cli"
27
+ ],
28
+ "author": "ShoppingJaws",
29
+ "license": "MIT",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/shoppingjaws/wezterm-peer.git"
36
+ },
37
+ "devDependencies": {
38
+ "@types/bun": "latest"
39
+ },
40
+ "peerDependencies": {
41
+ "typescript": "^5"
42
+ }
43
+ }