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.
- package/CHANGELOG.md +63 -0
- package/README.md +173 -0
- package/SKILL.md +368 -0
- package/config.ts +132 -0
- package/index.ts +795 -0
- package/overlay-component.ts +1211 -0
- package/package.json +56 -0
- package/pty-session.ts +561 -0
- package/scripts/fix-spawn-helper.cjs +37 -0
- package/scripts/install.js +95 -0
- package/session-manager.ts +226 -0
|
@@ -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();
|