pi-link 0.1.8 → 0.1.10

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.
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ // pi-link CLI — launch Pi with session resume by name
4
+ //
5
+ // Usage:
6
+ // pi-link <name> [flags...] Resume or create a named session, connected to link.
7
+ // pi-link resolve <name> Print just the session path (machine-readable).
8
+
9
+ import { readdir, stat } from "fs/promises";
10
+ import { createReadStream } from "fs";
11
+ import { createInterface } from "readline";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+ import { spawn } from "child_process";
15
+
16
+ const SESSIONS_DIR = join(homedir(), ".pi", "agent", "sessions");
17
+
18
+ async function getSessionMeta(filePath) {
19
+ let name;
20
+ let cwd;
21
+ const rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
22
+ for await (const line of rl) {
23
+ if (!line) continue;
24
+ try {
25
+ const entry = JSON.parse(line);
26
+ if (entry.type === "session" && typeof entry.cwd === "string") cwd = entry.cwd;
27
+ if (entry.type === "session_info" && typeof entry.name === "string") {
28
+ name = entry.name.trim().replace(/\s+/g, " ") || undefined;
29
+ }
30
+ } catch {
31
+ // skip malformed lines
32
+ }
33
+ }
34
+ return { name, cwd };
35
+ }
36
+
37
+ async function findSessionsByName(targetName) {
38
+ let cwdDirs;
39
+ try {
40
+ cwdDirs = await readdir(SESSIONS_DIR, { withFileTypes: true });
41
+ } catch {
42
+ return [];
43
+ }
44
+
45
+ const matches = [];
46
+
47
+ for (const dir of cwdDirs) {
48
+ if (!dir.isDirectory()) continue;
49
+ const dirPath = join(SESSIONS_DIR, dir.name);
50
+
51
+ let files;
52
+ try {
53
+ files = await readdir(dirPath);
54
+ } catch {
55
+ continue;
56
+ }
57
+
58
+ for (const file of files) {
59
+ if (!file.endsWith(".jsonl")) continue;
60
+ const filePath = join(dirPath, file);
61
+ try {
62
+ const { name, cwd } = await getSessionMeta(filePath);
63
+ if (name === targetName) {
64
+ const stats = await stat(filePath);
65
+ matches.push({ path: filePath, cwd: cwd || "?", modified: stats.mtime });
66
+ }
67
+ } catch {
68
+ continue;
69
+ }
70
+ }
71
+ }
72
+
73
+ // Local-first: current cwd matches before others, then by modified time
74
+ const localCwd = process.cwd();
75
+ matches.sort((a, b) => {
76
+ const aLocal = a.cwd === localCwd ? 1 : 0;
77
+ const bLocal = b.cwd === localCwd ? 1 : 0;
78
+ if (aLocal !== bLocal) return bLocal - aLocal;
79
+ return b.modified.getTime() - a.modified.getTime();
80
+ });
81
+ return matches;
82
+ }
83
+
84
+ // ── CLI ────────────────────────────────────────────────────────────────────
85
+
86
+ const [command, ...args] = process.argv.slice(2);
87
+
88
+ function printCandidates(name, matches) {
89
+ console.error(`Multiple sessions named "${name}":\n`);
90
+ for (const m of matches) {
91
+ console.error(` ${m.modified.toISOString().slice(0, 19)} cwd: ${m.cwd}`);
92
+ console.error(` ${m.path}\n`);
93
+ }
94
+ console.error(`Use: pi --session <path> --link`);
95
+ process.exit(1);
96
+ }
97
+
98
+ if (command === "resolve") {
99
+ const name = args[0]?.trim().replace(/\s+/g, " ");
100
+ if (!name) {
101
+ console.error("Usage: pi-link resolve <name>");
102
+ process.exit(1);
103
+ }
104
+ const matches = await findSessionsByName(name);
105
+ if (matches.length === 1) {
106
+ process.stdout.write(matches[0].path);
107
+ } else if (matches.length > 1) {
108
+ printCandidates(name, matches);
109
+ }
110
+ } else if (command && command !== "--help" && command !== "-h") {
111
+ // pi-link <name> [flags...] — resolve and launch Pi
112
+ const name = command.trim().replace(/\s+/g, " ");
113
+ if (!name) {
114
+ console.error("Usage: pi-link <name> [pi flags...]");
115
+ process.exit(1);
116
+ }
117
+
118
+ // Reject conflicting flags
119
+ for (const flag of args) {
120
+ const key = flag.split("=")[0];
121
+ if (["--session", "--continue", "-c", "--resume", "-r", "--fork", "--no-session", "--session-dir"].includes(key)) {
122
+ console.error(`Error: ${key} is managed by pi-link. Remove it.`);
123
+ process.exit(1);
124
+ }
125
+ if (key === "--link-name") {
126
+ console.error("Error: --link-name was removed. Use: pi-link <name>");
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ const matches = await findSessionsByName(name);
132
+ if (matches.length > 1) {
133
+ printCandidates(name, matches);
134
+ }
135
+
136
+ const piArgs = [];
137
+ if (matches.length === 1) {
138
+ console.error(`Resuming session: ${matches[0].path}`);
139
+ piArgs.push("--session", matches[0].path);
140
+ } else {
141
+ console.error("No existing session found. Starting new session.");
142
+ }
143
+ piArgs.push("--link", ...args);
144
+
145
+ const isWin = process.platform === "win32";
146
+ const cmd = isWin ? "cmd.exe" : "pi";
147
+ const cmdArgs = isWin ? ["/d", "/c", "pi", ...piArgs] : piArgs;
148
+
149
+ const child = spawn(cmd, cmdArgs, {
150
+ stdio: "inherit",
151
+ env: { ...process.env, PI_LINK_NAME: name },
152
+ });
153
+ child.once("exit", (code, signal) => {
154
+ if (code !== null) process.exit(code);
155
+ process.exit(signal === "SIGINT" ? 130 : 1);
156
+ });
157
+ child.once("error", (err) => {
158
+ console.error(`Failed to start pi: ${err.message}`);
159
+ process.exit(1);
160
+ });
161
+ } else {
162
+ console.error("Usage: pi-link <name> [pi flags...]\n pi-link resolve <name>");
163
+ process.exit(0);
164
+ }