shellwise 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.
@@ -0,0 +1,60 @@
1
+ import { getDb } from "../db/connection";
2
+ import { getCommonSuggestions } from "../data/common-commands";
3
+
4
+ /**
5
+ * Suggest: 5 history + 5 common commands, deduplicated.
6
+ */
7
+ export function runSuggest(query: string, limit: number = 5): void {
8
+ if (!query || query.length < 2) return;
9
+
10
+ const db = getDb();
11
+ const historyResults: string[] = [];
12
+
13
+ // History: prefix matches
14
+ const prefixes = db
15
+ .query<{ command: string }, [string, number]>(
16
+ `SELECT command FROM command_stats
17
+ WHERE command LIKE ? || '%'
18
+ ORDER BY frecency_score DESC
19
+ LIMIT ?`
20
+ )
21
+ .all(query, limit);
22
+
23
+ for (const r of prefixes) {
24
+ if (r.command !== query) historyResults.push(r.command);
25
+ }
26
+
27
+ // History: contains matches (fill remaining)
28
+ if (historyResults.length < limit) {
29
+ const remaining = limit - historyResults.length;
30
+ const resultSet = new Set(historyResults);
31
+
32
+ const contains = db
33
+ .query<{ command: string }, [string, string, number]>(
34
+ `SELECT command FROM command_stats
35
+ WHERE command LIKE '%' || ? || '%' AND command != ?
36
+ ORDER BY frecency_score DESC
37
+ LIMIT ?`
38
+ )
39
+ .all(query, query, remaining + historyResults.length);
40
+
41
+ for (const r of contains) {
42
+ if (!resultSet.has(r.command) && r.command !== query) {
43
+ historyResults.push(r.command);
44
+ if (historyResults.length >= limit) break;
45
+ }
46
+ }
47
+ }
48
+
49
+ // Common commands (deduplicated)
50
+ const seen = new Set(historyResults);
51
+ const commonResults = getCommonSuggestions(query, 10)
52
+ .filter((cmd) => !seen.has(cmd) && cmd !== query)
53
+ .slice(0, 5);
54
+
55
+ const merged = [...historyResults, ...commonResults];
56
+
57
+ if (merged.length > 0) {
58
+ process.stdout.write(merged.join("\n"));
59
+ }
60
+ }
@@ -0,0 +1,41 @@
1
+ import { getSocketPath } from "./protocol";
2
+ import { existsSync } from "fs";
3
+ import { connect } from "net";
4
+
5
+ /**
6
+ * Send request to daemon via Unix socket.
7
+ * Returns response lines or null if daemon unavailable.
8
+ */
9
+ export function daemonRequest(message: string, timeoutMs: number = 500): Promise<string[] | null> {
10
+ const socketPath = getSocketPath();
11
+ if (!existsSync(socketPath)) return Promise.resolve(null);
12
+
13
+ return new Promise((resolve) => {
14
+ const socket = connect(socketPath);
15
+ let data = "";
16
+ const timer = setTimeout(() => {
17
+ socket.destroy();
18
+ resolve(null);
19
+ }, timeoutMs);
20
+
21
+ socket.on("connect", () => {
22
+ socket.write(message);
23
+ });
24
+
25
+ socket.on("data", (chunk) => {
26
+ data += chunk.toString();
27
+ // Response ends with double newline
28
+ if (data.includes("\n\n")) {
29
+ clearTimeout(timer);
30
+ socket.destroy();
31
+ const lines = data.trim().split("\n").filter(Boolean);
32
+ resolve(lines);
33
+ }
34
+ });
35
+
36
+ socket.on("error", () => {
37
+ clearTimeout(timer);
38
+ resolve(null);
39
+ });
40
+ });
41
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Simple text protocol over Unix socket.
3
+ * Request: COMMAND\targ1\targ2\n
4
+ * Response: line1\nline2\n\n (empty line = end)
5
+ */
6
+
7
+ export type RequestType = "SUGGEST" | "ADD" | "STOP" | "PING";
8
+
9
+ export interface SuggestRequest {
10
+ type: "SUGGEST";
11
+ query: string;
12
+ limit: number;
13
+ }
14
+
15
+ export interface AddRequest {
16
+ type: "ADD";
17
+ command: string;
18
+ cwd: string;
19
+ exitCode: number;
20
+ duration: number;
21
+ session: string;
22
+ shell: string;
23
+ }
24
+
25
+ export interface StopRequest {
26
+ type: "STOP";
27
+ }
28
+
29
+ export interface PingRequest {
30
+ type: "PING";
31
+ }
32
+
33
+ export type Request = SuggestRequest | AddRequest | StopRequest | PingRequest;
34
+
35
+ export function serializeRequest(req: Request): string {
36
+ switch (req.type) {
37
+ case "SUGGEST":
38
+ return `SUGGEST\t${req.query}\t${req.limit}\n`;
39
+ case "ADD":
40
+ return `ADD\t${req.command}\t${req.cwd}\t${req.exitCode}\t${req.duration}\t${req.session}\t${req.shell}\n`;
41
+ case "STOP":
42
+ return `STOP\n`;
43
+ case "PING":
44
+ return `PING\n`;
45
+ }
46
+ }
47
+
48
+ export function parseRequest(raw: string): Request | null {
49
+ const line = raw.trim();
50
+ const parts = line.split("\t");
51
+ const type = parts[0] as RequestType;
52
+
53
+ switch (type) {
54
+ case "SUGGEST":
55
+ return { type: "SUGGEST", query: parts[1] || "", limit: parseInt(parts[2]) || 5 };
56
+ case "ADD":
57
+ return {
58
+ type: "ADD",
59
+ command: parts[1] || "",
60
+ cwd: parts[2] || "",
61
+ exitCode: parseInt(parts[3]) || 0,
62
+ duration: parseInt(parts[4]) || 0,
63
+ session: parts[5] || "",
64
+ shell: parts[6] || "",
65
+ };
66
+ case "STOP":
67
+ return { type: "STOP" };
68
+ case "PING":
69
+ return { type: "PING" };
70
+ default:
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function getSocketPath(): string {
76
+ const uid = process.getuid?.() ?? process.pid;
77
+ return `/tmp/shellwise-${uid}.sock`;
78
+ }
79
+
80
+ /** TCP port = 19850 + (uid % 100) to avoid collisions */
81
+ export function getDaemonPort(): number {
82
+ const uid = process.getuid?.() ?? 501;
83
+ return 19850 + (uid % 100);
84
+ }
85
+
86
+ export function getPidPath(): string {
87
+ const uid = process.getuid?.() ?? process.pid;
88
+ return `/tmp/shellwise-${uid}.pid`;
89
+ }
@@ -0,0 +1,222 @@
1
+ import { getDb, closeDb } from "../db/connection";
2
+ import { insertCommand } from "../db/queries";
3
+ import { getHostname } from "../utils/platform";
4
+ import { getCommonSuggestions } from "../data/common-commands";
5
+ import { parseRequest, getSocketPath, getPidPath, getDaemonPort } from "./protocol";
6
+ import { unlinkSync, writeFileSync, existsSync } from "fs";
7
+ import type { Socket } from "bun";
8
+
9
+ const IGNORED_COMMANDS = new Set(["ls", "cd", "pwd", "exit", "clear", "sw"]);
10
+ const IDLE_TIMEOUT = 30 * 60_000; // 30 min
11
+
12
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
13
+ let server: ReturnType<typeof Bun.listen> | null = null;
14
+
15
+ // Pre-warm DB + prepared statements on start
16
+ let suggestPrefix: ReturnType<ReturnType<typeof getDb>["prepare"]>;
17
+ let suggestContains: ReturnType<ReturnType<typeof getDb>["prepare"]>;
18
+
19
+ function initPreparedStatements() {
20
+ const db = getDb();
21
+ suggestPrefix = db.prepare(
22
+ `SELECT command FROM command_stats
23
+ WHERE command LIKE ?1 || '%'
24
+ ORDER BY frecency_score DESC
25
+ LIMIT ?2`
26
+ );
27
+ suggestContains = db.prepare(
28
+ `SELECT command FROM command_stats
29
+ WHERE command LIKE '%' || ?1 || '%' AND command != ?1
30
+ ORDER BY frecency_score DESC
31
+ LIMIT ?2`
32
+ );
33
+ }
34
+
35
+ function resetIdleTimer() {
36
+ if (idleTimer) clearTimeout(idleTimer);
37
+ idleTimer = setTimeout(() => {
38
+ stopServer();
39
+ }, IDLE_TIMEOUT);
40
+ }
41
+
42
+ function handleRequest(raw: string): string {
43
+ const req = parseRequest(raw);
44
+ if (!req) return "\n";
45
+
46
+ resetIdleTimer();
47
+
48
+ switch (req.type) {
49
+ case "PING":
50
+ return "PONG\n\n";
51
+
52
+ case "SUGGEST": {
53
+ if (!req.query || req.query.length < 2) return "\n";
54
+
55
+ const historyResults: string[] = [];
56
+ const historyLimit = 5;
57
+
58
+ // History: prefix matches
59
+ const prefixes = suggestPrefix.all(req.query, historyLimit) as { command: string }[];
60
+ for (const r of prefixes) {
61
+ if (r.command !== req.query) historyResults.push(r.command);
62
+ }
63
+
64
+ // History: contains matches (fill remaining)
65
+ if (historyResults.length < historyLimit) {
66
+ const remaining = historyLimit - historyResults.length;
67
+ const resultSet = new Set(historyResults);
68
+ const contains = suggestContains.all(req.query, remaining + historyResults.length) as {
69
+ command: string;
70
+ }[];
71
+ for (const r of contains) {
72
+ if (!resultSet.has(r.command) && r.command !== req.query) {
73
+ historyResults.push(r.command);
74
+ if (historyResults.length >= historyLimit) break;
75
+ }
76
+ }
77
+ }
78
+
79
+ // Common commands: fill with suggestions not already in history
80
+ const seen = new Set(historyResults);
81
+ const commonResults = getCommonSuggestions(req.query, 10)
82
+ .filter((cmd) => !seen.has(cmd) && cmd !== req.query)
83
+ .slice(0, 5);
84
+
85
+ // Merge: history first, then common
86
+ const merged = [...historyResults, ...commonResults];
87
+
88
+ return merged.length > 0 ? merged.join("\n") + "\n\n" : "\n";
89
+ }
90
+
91
+ case "ADD": {
92
+ const cmd = req.command.trim();
93
+ if (!cmd || cmd.length < 2 || cmd.startsWith(" ")) return "OK\n\n";
94
+ if (req.exitCode !== 0) return "OK\n\n"; // Only save successful commands
95
+ const baseCmd = cmd.split(/\s+/)[0];
96
+ if (IGNORED_COMMANDS.has(baseCmd)) return "OK\n\n";
97
+
98
+ insertCommand({
99
+ command: cmd,
100
+ cwd: req.cwd || undefined,
101
+ exit_code: req.exitCode,
102
+ duration_ms: req.duration,
103
+ hostname: getHostname(),
104
+ session_id: req.session || undefined,
105
+ shell: req.shell || undefined,
106
+ });
107
+ return "OK\n\n";
108
+ }
109
+
110
+ case "STOP":
111
+ setTimeout(() => stopServer(), 50);
112
+ return "BYE\n\n";
113
+ }
114
+ }
115
+
116
+ export function startServer(): void {
117
+ const socketPath = getSocketPath();
118
+ const pidPath = getPidPath();
119
+
120
+ // Cleanup stale socket
121
+ if (existsSync(socketPath)) {
122
+ try {
123
+ unlinkSync(socketPath);
124
+ } catch {}
125
+ }
126
+
127
+ // Init DB + prepared statements
128
+ initPreparedStatements();
129
+
130
+ const socketHandlers = {
131
+ data(socket: Socket, data: Buffer) {
132
+ const lines = data.toString().split("\n");
133
+ for (const line of lines) {
134
+ if (line.trim()) {
135
+ const response = handleRequest(line);
136
+ socket.write(response);
137
+ }
138
+ }
139
+ },
140
+ open() {},
141
+ close() {},
142
+ error(_socket: Socket, error: Error) {
143
+ console.error("[shellwise daemon] socket error:", error.message);
144
+ },
145
+ };
146
+
147
+ // Listen on both Unix socket and TCP (for ztcp from zsh)
148
+ server = Bun.listen({
149
+ unix: socketPath,
150
+ socket: socketHandlers,
151
+ });
152
+
153
+ const port = getDaemonPort();
154
+ Bun.listen({
155
+ hostname: "127.0.0.1",
156
+ port,
157
+ socket: socketHandlers,
158
+ });
159
+
160
+ // Save PID + port
161
+ writeFileSync(pidPath, `${process.pid}\n${port}`);
162
+
163
+ resetIdleTimer();
164
+
165
+ // Cleanup on exit
166
+ const cleanup = () => {
167
+ stopServer();
168
+ process.exit(0);
169
+ };
170
+ process.on("SIGTERM", cleanup);
171
+ process.on("SIGINT", cleanup);
172
+ }
173
+
174
+ export function stopServer(): void {
175
+ if (idleTimer) clearTimeout(idleTimer);
176
+ if (server) {
177
+ server.stop(true);
178
+ server = null;
179
+ }
180
+ closeDb();
181
+
182
+ const socketPath = getSocketPath();
183
+ const pidPath = getPidPath();
184
+ try {
185
+ unlinkSync(socketPath);
186
+ } catch {}
187
+ try {
188
+ unlinkSync(pidPath);
189
+ } catch {}
190
+
191
+ process.exit(0);
192
+ }
193
+
194
+ export function isDaemonRunning(): boolean {
195
+ const pidPath = getPidPath();
196
+ if (!existsSync(pidPath)) return false;
197
+
198
+ try {
199
+ const content = require("fs").readFileSync(pidPath, "utf-8").trim();
200
+ const pid = parseInt(content.split("\n")[0]);
201
+ process.kill(pid, 0); // Check if process exists
202
+ return true;
203
+ } catch {
204
+ // Stale PID file
205
+ try {
206
+ unlinkSync(pidPath);
207
+ } catch {}
208
+ return false;
209
+ }
210
+ }
211
+
212
+ export function getDaemonInfo(): { pid: number; port: number } | null {
213
+ const pidPath = getPidPath();
214
+ if (!existsSync(pidPath)) return null;
215
+ try {
216
+ const content = require("fs").readFileSync(pidPath, "utf-8").trim();
217
+ const lines = content.split("\n");
218
+ return { pid: parseInt(lines[0]), port: parseInt(lines[1]) };
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Common commands grouped by prefix.
3
+ * Used to suggest commands even when user has no history.
4
+ */
5
+ const COMMANDS: Record<string, string[]> = {
6
+ git: [
7
+ "git status",
8
+ "git add .",
9
+ "git commit -m ''",
10
+ "git push",
11
+ "git pull",
12
+ "git branch",
13
+ "git checkout",
14
+ "git diff",
15
+ "git log --oneline",
16
+ "git stash",
17
+ "git merge",
18
+ "git rebase",
19
+ "git fetch",
20
+ "git clone",
21
+ "git reset --soft HEAD~1",
22
+ "git cherry-pick",
23
+ "git remote -v",
24
+ "git tag",
25
+ ],
26
+ npm: [
27
+ "npm install",
28
+ "npm run dev",
29
+ "npm run build",
30
+ "npm run test",
31
+ "npm run start",
32
+ "npm init -y",
33
+ "npm update",
34
+ "npm outdated",
35
+ "npm list --depth=0",
36
+ "npm publish",
37
+ "npm link",
38
+ "npm uninstall",
39
+ "npm cache clean --force",
40
+ "npm audit fix",
41
+ ],
42
+ npx: [
43
+ "npx create-next-app@latest",
44
+ "npx create-react-app",
45
+ "npx prisma generate",
46
+ "npx prisma migrate dev",
47
+ "npx tsc --noEmit",
48
+ "npx eslint .",
49
+ "npx prettier --write .",
50
+ ],
51
+ bun: [
52
+ "bun install",
53
+ "bun run dev",
54
+ "bun run build",
55
+ "bun test",
56
+ "bun add",
57
+ "bun remove",
58
+ "bun init",
59
+ "bun upgrade",
60
+ "bun run start",
61
+ "bun link",
62
+ "bun build --compile",
63
+ ],
64
+ docker: [
65
+ "docker compose up -d",
66
+ "docker compose down",
67
+ "docker compose logs -f",
68
+ "docker compose build",
69
+ "docker compose ps",
70
+ "docker ps",
71
+ "docker images",
72
+ "docker build -t",
73
+ "docker run -it",
74
+ "docker exec -it",
75
+ "docker stop",
76
+ "docker rm",
77
+ "docker rmi",
78
+ "docker system prune -f",
79
+ "docker volume ls",
80
+ "docker network ls",
81
+ ],
82
+ yarn: [
83
+ "yarn install",
84
+ "yarn dev",
85
+ "yarn build",
86
+ "yarn test",
87
+ "yarn add",
88
+ "yarn remove",
89
+ "yarn upgrade",
90
+ "yarn start",
91
+ ],
92
+ pnpm: [
93
+ "pnpm install",
94
+ "pnpm run dev",
95
+ "pnpm run build",
96
+ "pnpm add",
97
+ "pnpm remove",
98
+ "pnpm dlx",
99
+ ],
100
+ brew: [
101
+ "brew install",
102
+ "brew update",
103
+ "brew upgrade",
104
+ "brew list",
105
+ "brew search",
106
+ "brew uninstall",
107
+ "brew cleanup",
108
+ "brew info",
109
+ "brew doctor",
110
+ "brew services list",
111
+ "brew services start",
112
+ "brew services stop",
113
+ ],
114
+ curl: [
115
+ "curl -X GET",
116
+ "curl -X POST",
117
+ "curl -sL",
118
+ "curl -o",
119
+ "curl -I",
120
+ "curl -H 'Content-Type: application/json'",
121
+ "curl -d '{}'",
122
+ ],
123
+ ssh: [
124
+ "ssh-keygen -t ed25519",
125
+ "ssh-copy-id",
126
+ "ssh -i",
127
+ "ssh -L",
128
+ "ssh -p",
129
+ ],
130
+ python: [
131
+ "python -m venv venv",
132
+ "python -m pip install",
133
+ "python -m pytest",
134
+ "python manage.py runserver",
135
+ "python manage.py migrate",
136
+ ],
137
+ pip: [
138
+ "pip install",
139
+ "pip install -r requirements.txt",
140
+ "pip freeze > requirements.txt",
141
+ "pip list",
142
+ "pip uninstall",
143
+ ],
144
+ go: [
145
+ "go run .",
146
+ "go build",
147
+ "go test ./...",
148
+ "go mod tidy",
149
+ "go mod init",
150
+ "go get",
151
+ "go fmt ./...",
152
+ "go vet ./...",
153
+ ],
154
+ cargo: [
155
+ "cargo build",
156
+ "cargo run",
157
+ "cargo test",
158
+ "cargo add",
159
+ "cargo fmt",
160
+ "cargo clippy",
161
+ "cargo init",
162
+ "cargo publish",
163
+ ],
164
+ make: [
165
+ "make build",
166
+ "make test",
167
+ "make clean",
168
+ "make install",
169
+ "make all",
170
+ ],
171
+ kubectl: [
172
+ "kubectl get pods",
173
+ "kubectl get services",
174
+ "kubectl get deployments",
175
+ "kubectl logs",
176
+ "kubectl describe pod",
177
+ "kubectl apply -f",
178
+ "kubectl delete",
179
+ "kubectl exec -it",
180
+ "kubectl port-forward",
181
+ "kubectl config use-context",
182
+ ],
183
+ systemctl: [
184
+ "systemctl status",
185
+ "systemctl start",
186
+ "systemctl stop",
187
+ "systemctl restart",
188
+ "systemctl enable",
189
+ "systemctl disable",
190
+ "systemctl list-units",
191
+ ],
192
+ pm2: [
193
+ "pm2 list",
194
+ "pm2 start",
195
+ "pm2 stop",
196
+ "pm2 restart",
197
+ "pm2 logs",
198
+ "pm2 delete",
199
+ "pm2 monit",
200
+ "pm2 save",
201
+ ],
202
+ tar: [
203
+ "tar -czf archive.tar.gz",
204
+ "tar -xzf",
205
+ "tar -xvf",
206
+ "tar -tf",
207
+ ],
208
+ find: [
209
+ "find . -name",
210
+ "find . -type f",
211
+ "find . -type d",
212
+ "find . -mtime",
213
+ "find . -size",
214
+ ],
215
+ grep: [
216
+ "grep -r",
217
+ "grep -rn",
218
+ "grep -ri",
219
+ "grep -l",
220
+ "grep -v",
221
+ ],
222
+ chmod: [
223
+ "chmod +x",
224
+ "chmod 755",
225
+ "chmod 644",
226
+ "chmod -R",
227
+ ],
228
+ vercel: [
229
+ "vercel deploy",
230
+ "vercel dev",
231
+ "vercel env pull",
232
+ "vercel logs",
233
+ "vercel ls",
234
+ ],
235
+ supabase: [
236
+ "supabase start",
237
+ "supabase stop",
238
+ "supabase db reset",
239
+ "supabase migration new",
240
+ "supabase gen types typescript",
241
+ "supabase db push",
242
+ ],
243
+ };
244
+
245
+ /** Get common command suggestions for a query */
246
+ export function getCommonSuggestions(query: string, limit: number = 5): string[] {
247
+ const q = query.toLowerCase();
248
+ const results: string[] = [];
249
+
250
+ // Try prefix match on command groups
251
+ for (const [prefix, commands] of Object.entries(COMMANDS)) {
252
+ if (prefix.startsWith(q) || q.startsWith(prefix)) {
253
+ for (const cmd of commands) {
254
+ if (cmd.toLowerCase().startsWith(q) || cmd.toLowerCase().includes(q)) {
255
+ results.push(cmd);
256
+ if (results.length >= limit) return results;
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ // Fallback: search all commands
263
+ if (results.length < limit) {
264
+ const seen = new Set(results);
265
+ for (const commands of Object.values(COMMANDS)) {
266
+ for (const cmd of commands) {
267
+ if (!seen.has(cmd) && cmd.toLowerCase().includes(q)) {
268
+ results.push(cmd);
269
+ if (results.length >= limit) return results;
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ return results;
276
+ }
@@ -0,0 +1,29 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { getDbPath } from "../utils/paths";
3
+ import { runMigrations } from "./schema";
4
+
5
+ let db: Database | null = null;
6
+
7
+ export function getDb(): Database {
8
+ if (db) return db;
9
+
10
+ const dbPath = getDbPath();
11
+ db = new Database(dbPath, { create: true });
12
+
13
+ // Performance: WAL mode for concurrent read/write
14
+ db.exec("PRAGMA journal_mode = WAL");
15
+ db.exec("PRAGMA busy_timeout = 5000");
16
+ db.exec("PRAGMA synchronous = NORMAL");
17
+ db.exec("PRAGMA foreign_keys = ON");
18
+
19
+ runMigrations(db);
20
+
21
+ return db;
22
+ }
23
+
24
+ export function closeDb(): void {
25
+ if (db) {
26
+ db.close();
27
+ db = null;
28
+ }
29
+ }