pi-interactive-shell 0.3.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.
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, cpSync, symlinkSync, unlinkSync, readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { execSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const packageRoot = join(__dirname, "..");
11
+
12
+ const EXTENSION_DIR = join(homedir(), ".pi", "agent", "extensions", "interactive-shell");
13
+ const SKILL_DIR = join(homedir(), ".pi", "agent", "skills", "interactive-shell");
14
+
15
+ function log(msg) {
16
+ console.log(`[pi-interactive-shell] ${msg}`);
17
+ }
18
+
19
+ function main() {
20
+ const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf-8"));
21
+ log(`Installing version ${pkg.version}...`);
22
+
23
+ // Create extension directory
24
+ log(`Creating ${EXTENSION_DIR}`);
25
+ mkdirSync(EXTENSION_DIR, { recursive: true });
26
+
27
+ // Files to copy
28
+ const files = [
29
+ "package.json",
30
+ "index.ts",
31
+ "config.ts",
32
+ "overlay-component.ts",
33
+ "pty-session.ts",
34
+ "session-manager.ts",
35
+ "README.md",
36
+ "SKILL.md",
37
+ "CHANGELOG.md",
38
+ ];
39
+
40
+ // Copy files
41
+ for (const file of files) {
42
+ const src = join(packageRoot, file);
43
+ const dest = join(EXTENSION_DIR, file);
44
+ if (existsSync(src)) {
45
+ cpSync(src, dest);
46
+ log(`Copied ${file}`);
47
+ }
48
+ }
49
+
50
+ // Copy scripts directory
51
+ const scriptsDir = join(packageRoot, "scripts");
52
+ const destScriptsDir = join(EXTENSION_DIR, "scripts");
53
+ if (existsSync(scriptsDir)) {
54
+ mkdirSync(destScriptsDir, { recursive: true });
55
+ cpSync(scriptsDir, destScriptsDir, { recursive: true });
56
+ log("Copied scripts/");
57
+ }
58
+
59
+ // Run npm install in extension directory
60
+ log("Running npm install...");
61
+ try {
62
+ execSync("npm install", { cwd: EXTENSION_DIR, stdio: "inherit" });
63
+ } catch (error) {
64
+ log(`Warning: npm install failed: ${error.message}`);
65
+ log("You may need to run 'npm install' manually in the extension directory.");
66
+ }
67
+
68
+ // Create skill symlink
69
+ log(`Creating skill symlink at ${SKILL_DIR}`);
70
+ mkdirSync(SKILL_DIR, { recursive: true });
71
+ const skillLink = join(SKILL_DIR, "SKILL.md");
72
+ const skillTarget = join(EXTENSION_DIR, "SKILL.md");
73
+
74
+ try {
75
+ if (existsSync(skillLink)) {
76
+ unlinkSync(skillLink);
77
+ }
78
+ symlinkSync(skillTarget, skillLink);
79
+ log("Skill symlink created");
80
+ } catch (error) {
81
+ log(`Warning: Could not create skill symlink: ${error.message}`);
82
+ log(`You can create it manually: ln -sf ${skillTarget} ${skillLink}`);
83
+ }
84
+
85
+ log("");
86
+ log("Installation complete!");
87
+ log("");
88
+ log("Restart pi to load the extension.");
89
+ log("");
90
+ log("Usage:");
91
+ log(' interactive_shell({ command: \'pi "Fix all bugs"\', mode: "hands-free" })');
92
+ log("");
93
+ }
94
+
95
+ main();
@@ -0,0 +1,226 @@
1
+ import { PtyTerminalSession } from "./pty-session.js";
2
+
3
+ export interface BackgroundSession {
4
+ id: string;
5
+ name: string;
6
+ command: string;
7
+ reason?: string;
8
+ session: PtyTerminalSession;
9
+ startedAt: Date;
10
+ }
11
+
12
+ export interface ActiveSession {
13
+ id: string;
14
+ command: string;
15
+ write: (data: string) => void;
16
+ setUpdateInterval?: (intervalMs: number) => void;
17
+ setQuietThreshold?: (thresholdMs: number) => void;
18
+ startedAt: Date;
19
+ }
20
+
21
+ // Human-readable session slug generation
22
+ const SLUG_ADJECTIVES = [
23
+ "amber", "brisk", "calm", "clear", "cool", "crisp", "dawn", "ember",
24
+ "fast", "fresh", "gentle", "keen", "kind", "lucky", "mellow", "mild",
25
+ "neat", "nimble", "nova", "quick", "quiet", "rapid", "sharp", "swift",
26
+ "tender", "tidy", "vivid", "warm", "wild", "young",
27
+ ];
28
+
29
+ const SLUG_NOUNS = [
30
+ "atlas", "bloom", "breeze", "cedar", "cloud", "comet", "coral", "cove",
31
+ "crest", "delta", "dune", "ember", "falcon", "fjord", "glade", "haven",
32
+ "kelp", "lagoon", "meadow", "mist", "nexus", "orbit", "pine", "reef",
33
+ "ridge", "river", "sage", "shell", "shore", "summit", "trail", "zephyr",
34
+ ];
35
+
36
+ function randomChoice<T>(arr: T[]): T {
37
+ return arr[Math.floor(Math.random() * arr.length)];
38
+ }
39
+
40
+ // Track used IDs to avoid collisions
41
+ const usedIds = new Set<string>();
42
+
43
+ export function generateSessionId(name?: string): string {
44
+ // If a custom name is provided, use simple counter approach
45
+ if (name) {
46
+ let counter = 1;
47
+ let id = name;
48
+ while (usedIds.has(id)) {
49
+ counter++;
50
+ id = `${name}-${counter}`;
51
+ }
52
+ usedIds.add(id);
53
+ return id;
54
+ }
55
+
56
+ // Generate human-readable slug
57
+ for (let attempt = 0; attempt < 20; attempt++) {
58
+ const adj = randomChoice(SLUG_ADJECTIVES);
59
+ const noun = randomChoice(SLUG_NOUNS);
60
+ const base = `${adj}-${noun}`;
61
+
62
+ if (!usedIds.has(base)) {
63
+ usedIds.add(base);
64
+ return base;
65
+ }
66
+
67
+ // Try with suffix
68
+ for (let i = 2; i <= 9; i++) {
69
+ const candidate = `${base}-${i}`;
70
+ if (!usedIds.has(candidate)) {
71
+ usedIds.add(candidate);
72
+ return candidate;
73
+ }
74
+ }
75
+ }
76
+
77
+ // Fallback: timestamp-based
78
+ const fallback = `shell-${Date.now().toString(36)}`;
79
+ usedIds.add(fallback);
80
+ return fallback;
81
+ }
82
+
83
+ export function releaseSessionId(id: string): void {
84
+ usedIds.delete(id);
85
+ }
86
+
87
+ // Derive a friendly display name from command (e.g., "pi Fix all bugs" -> "pi Fix all bugs")
88
+ export function deriveSessionName(command: string): string {
89
+ const trimmed = command.trim();
90
+ if (trimmed.length <= 60) return trimmed;
91
+
92
+ // Truncate with ellipsis
93
+ return trimmed.slice(0, 57) + "...";
94
+ }
95
+
96
+ export class ShellSessionManager {
97
+ private sessions = new Map<string, BackgroundSession>();
98
+ private exitWatchers = new Map<string, NodeJS.Timeout>();
99
+ private cleanupTimers = new Map<string, NodeJS.Timeout>();
100
+ private activeSessions = new Map<string, ActiveSession>();
101
+
102
+ // Active hands-free session management
103
+ registerActive(
104
+ id: string,
105
+ command: string,
106
+ write: (data: string) => void,
107
+ setUpdateInterval?: (intervalMs: number) => void,
108
+ setQuietThreshold?: (thresholdMs: number) => void,
109
+ ): void {
110
+ this.activeSessions.set(id, {
111
+ id,
112
+ command,
113
+ write,
114
+ setUpdateInterval,
115
+ setQuietThreshold,
116
+ startedAt: new Date(),
117
+ });
118
+ }
119
+
120
+ unregisterActive(id: string): void {
121
+ this.activeSessions.delete(id);
122
+ releaseSessionId(id);
123
+ }
124
+
125
+ getActive(id: string): ActiveSession | undefined {
126
+ return this.activeSessions.get(id);
127
+ }
128
+
129
+ writeToActive(id: string, data: string): boolean {
130
+ const session = this.activeSessions.get(id);
131
+ if (!session) return false;
132
+ session.write(data);
133
+ return true;
134
+ }
135
+
136
+ setActiveUpdateInterval(id: string, intervalMs: number): boolean {
137
+ const session = this.activeSessions.get(id);
138
+ if (!session?.setUpdateInterval) return false;
139
+ session.setUpdateInterval(intervalMs);
140
+ return true;
141
+ }
142
+
143
+ setActiveQuietThreshold(id: string, thresholdMs: number): boolean {
144
+ const session = this.activeSessions.get(id);
145
+ if (!session?.setQuietThreshold) return false;
146
+ session.setQuietThreshold(thresholdMs);
147
+ return true;
148
+ }
149
+
150
+ listActive(): ActiveSession[] {
151
+ return Array.from(this.activeSessions.values());
152
+ }
153
+
154
+ // Background session management
155
+ add(command: string, session: PtyTerminalSession, name?: string, reason?: string): string {
156
+ const id = generateSessionId(name);
157
+ this.sessions.set(id, {
158
+ id,
159
+ name: name || deriveSessionName(command),
160
+ command,
161
+ reason,
162
+ session,
163
+ startedAt: new Date(),
164
+ });
165
+
166
+ session.setEventHandlers({});
167
+
168
+ const checkExit = setInterval(() => {
169
+ if (session.exited) {
170
+ clearInterval(checkExit);
171
+ this.exitWatchers.delete(id);
172
+ const cleanupTimer = setTimeout(() => {
173
+ this.cleanupTimers.delete(id);
174
+ this.remove(id);
175
+ }, 30000);
176
+ this.cleanupTimers.set(id, cleanupTimer);
177
+ }
178
+ }, 1000);
179
+ this.exitWatchers.set(id, checkExit);
180
+
181
+ return id;
182
+ }
183
+
184
+ get(id: string): BackgroundSession | undefined {
185
+ // Cancel auto-cleanup timer when session is being reattached
186
+ const cleanupTimer = this.cleanupTimers.get(id);
187
+ if (cleanupTimer) {
188
+ clearTimeout(cleanupTimer);
189
+ this.cleanupTimers.delete(id);
190
+ }
191
+ return this.sessions.get(id);
192
+ }
193
+
194
+ remove(id: string): void {
195
+ const watcher = this.exitWatchers.get(id);
196
+ if (watcher) {
197
+ clearInterval(watcher);
198
+ this.exitWatchers.delete(id);
199
+ }
200
+
201
+ const cleanupTimer = this.cleanupTimers.get(id);
202
+ if (cleanupTimer) {
203
+ clearTimeout(cleanupTimer);
204
+ this.cleanupTimers.delete(id);
205
+ }
206
+
207
+ const session = this.sessions.get(id);
208
+ if (session) {
209
+ session.session.dispose();
210
+ this.sessions.delete(id);
211
+ releaseSessionId(id);
212
+ }
213
+ }
214
+
215
+ list(): BackgroundSession[] {
216
+ return Array.from(this.sessions.values());
217
+ }
218
+
219
+ killAll(): void {
220
+ for (const [id] of this.sessions) {
221
+ this.remove(id);
222
+ }
223
+ }
224
+ }
225
+
226
+ export const sessionManager = new ShellSessionManager();