shellwise 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellwise",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Smart command history with inline auto-suggest and fuzzy search for your terminal",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/add.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { insertCommand } from "../db/queries";
2
2
  import { getHostname } from "../utils/platform";
3
+ import { IGNORED_COMMANDS } from "../utils/constants";
3
4
 
4
5
  interface AddOptions {
5
6
  command: string;
@@ -10,8 +11,6 @@ interface AddOptions {
10
11
  shell?: string;
11
12
  }
12
13
 
13
- const IGNORED_COMMANDS = new Set(["ls", "cd", "pwd", "exit", "clear", "sw"]);
14
-
15
14
  export function runAdd(opts: AddOptions): void {
16
15
  const cmd = opts.command.trim();
17
16
 
@@ -10,15 +10,18 @@ export function runSuggest(query: string, limit: number = 5): void {
10
10
  const db = getDb();
11
11
  const historyResults: string[] = [];
12
12
 
13
+ // Escape LIKE wildcards in user input
14
+ const escapedQuery = query.replace(/[%_\\]/g, "\\$&");
15
+
13
16
  // History: prefix matches
14
17
  const prefixes = db
15
18
  .query<{ command: string }, [string, number]>(
16
19
  `SELECT command FROM command_stats
17
- WHERE command LIKE ? || '%'
20
+ WHERE command LIKE ? || '%' ESCAPE '\\'
18
21
  ORDER BY frecency_score DESC
19
22
  LIMIT ?`
20
23
  )
21
- .all(query, limit);
24
+ .all(escapedQuery, limit);
22
25
 
23
26
  for (const r of prefixes) {
24
27
  if (r.command !== query) historyResults.push(r.command);
@@ -32,11 +35,11 @@ export function runSuggest(query: string, limit: number = 5): void {
32
35
  const contains = db
33
36
  .query<{ command: string }, [string, string, number]>(
34
37
  `SELECT command FROM command_stats
35
- WHERE command LIKE '%' || ? || '%' AND command != ?
38
+ WHERE command LIKE '%' || ? || '%' ESCAPE '\\' AND command != ?
36
39
  ORDER BY frecency_score DESC
37
40
  LIMIT ?`
38
41
  )
39
- .all(query, query, remaining + historyResults.length);
42
+ .all(escapedQuery, query, remaining + historyResults.length);
40
43
 
41
44
  for (const r of contains) {
42
45
  if (!resultSet.has(r.command) && r.command !== query) {
@@ -2,15 +2,15 @@ import { getDb, closeDb } from "../db/connection";
2
2
  import { insertCommand } from "../db/queries";
3
3
  import { getHostname } from "../utils/platform";
4
4
  import { getCommonSuggestions } from "../data/common-commands";
5
+ import { IGNORED_COMMANDS } from "../utils/constants";
5
6
  import { parseRequest, getSocketPath, getPidPath, getDaemonPort } from "./protocol";
6
7
  import { unlinkSync, writeFileSync, existsSync } from "fs";
7
8
  import type { Socket } from "bun";
8
-
9
- const IGNORED_COMMANDS = new Set(["ls", "cd", "pwd", "exit", "clear", "sw"]);
10
9
  const IDLE_TIMEOUT = 30 * 60_000; // 30 min
11
10
 
12
11
  let idleTimer: ReturnType<typeof setTimeout> | null = null;
13
12
  let server: ReturnType<typeof Bun.listen> | null = null;
13
+ let tcpServer: ReturnType<typeof Bun.listen> | null = null;
14
14
 
15
15
  // Pre-warm DB + prepared statements on start
16
16
  let suggestPrefix: ReturnType<ReturnType<typeof getDb>["prepare"]>;
@@ -20,13 +20,13 @@ function initPreparedStatements() {
20
20
  const db = getDb();
21
21
  suggestPrefix = db.prepare(
22
22
  `SELECT command FROM command_stats
23
- WHERE command LIKE ?1 || '%'
23
+ WHERE command LIKE ?1 || '%' ESCAPE '\\'
24
24
  ORDER BY frecency_score DESC
25
25
  LIMIT ?2`
26
26
  );
27
27
  suggestContains = db.prepare(
28
28
  `SELECT command FROM command_stats
29
- WHERE command LIKE '%' || ?1 || '%' AND command != ?1
29
+ WHERE command LIKE '%' || ?1 || '%' ESCAPE '\\' AND command != ?1
30
30
  ORDER BY frecency_score DESC
31
31
  LIMIT ?2`
32
32
  );
@@ -55,8 +55,11 @@ function handleRequest(raw: string): string {
55
55
  const historyResults: string[] = [];
56
56
  const historyLimit = 5;
57
57
 
58
+ // Escape LIKE wildcards in user input
59
+ const escapedQuery = req.query.replace(/[%_\\]/g, "\\$&");
60
+
58
61
  // History: prefix matches
59
- const prefixes = suggestPrefix.all(req.query, historyLimit) as { command: string }[];
62
+ const prefixes = suggestPrefix.all(escapedQuery, historyLimit) as { command: string }[];
60
63
  for (const r of prefixes) {
61
64
  if (r.command !== req.query) historyResults.push(r.command);
62
65
  }
@@ -65,7 +68,7 @@ function handleRequest(raw: string): string {
65
68
  if (historyResults.length < historyLimit) {
66
69
  const remaining = historyLimit - historyResults.length;
67
70
  const resultSet = new Set(historyResults);
68
- const contains = suggestContains.all(req.query, remaining + historyResults.length) as {
71
+ const contains = suggestContains.all(escapedQuery, remaining + historyResults.length) as {
69
72
  command: string;
70
73
  }[];
71
74
  for (const r of contains) {
@@ -151,7 +154,7 @@ export function startServer(): void {
151
154
  });
152
155
 
153
156
  const port = getDaemonPort();
154
- Bun.listen({
157
+ tcpServer = Bun.listen({
155
158
  hostname: "127.0.0.1",
156
159
  port,
157
160
  socket: socketHandlers,
@@ -177,6 +180,10 @@ export function stopServer(): void {
177
180
  server.stop(true);
178
181
  server = null;
179
182
  }
183
+ if (tcpServer) {
184
+ tcpServer.stop(true);
185
+ tcpServer = null;
186
+ }
180
187
  closeDb();
181
188
 
182
189
  const socketPath = getSocketPath();
package/src/db/queries.ts CHANGED
@@ -95,8 +95,10 @@ export function searchCommands(opts: SearchOptions): CommandStats[] {
95
95
  const params: (string | number)[] = [];
96
96
 
97
97
  if (opts.query) {
98
- conditions.push("cs.command LIKE ?");
99
- params.push(`%${opts.query}%`);
98
+ conditions.push("cs.command LIKE ? ESCAPE '\\'");
99
+ // Escape LIKE wildcards in user input
100
+ const escaped = opts.query.replace(/[%_\\]/g, "\\$&");
101
+ params.push(`%${escaped}%`);
100
102
  }
101
103
 
102
104
  if (opts.cwd) {
package/src/index.ts CHANGED
@@ -10,7 +10,6 @@ import { runPrune } from "./cli/prune";
10
10
  import { closeDb } from "./db/connection";
11
11
  import { startServer, isDaemonRunning, getDaemonInfo } from "./daemon/server";
12
12
  import { daemonRequest } from "./daemon/client";
13
- import { getSocketPath, getPidPath, getDaemonPort } from "./daemon/protocol";
14
13
 
15
14
  const args = process.argv.slice(2);
16
15
  const command = args[0];
@@ -86,7 +85,9 @@ async function main(): Promise<void> {
86
85
  }
87
86
 
88
87
  // Try daemon first
89
- const addMsg = `ADD\t${flags.command}\t${flags.cwd || ""}\t${flags["exit-code"] || "0"}\t${flags.duration || "0"}\t${flags.session || ""}\t${flags.shell || ""}\n`;
88
+ // Strip tabs from command to avoid breaking protocol delimiter
89
+ const safeCommand = flags.command.replace(/\t/g, " ");
90
+ const addMsg = `ADD\t${safeCommand}\t${flags.cwd || ""}\t${flags["exit-code"] || "0"}\t${flags.duration || "0"}\t${flags.session || ""}\t${flags.shell || ""}\n`;
90
91
  const addResult = await daemonRequest(addMsg);
91
92
  if (!addResult) {
92
93
  // Fallback: direct
@@ -37,13 +37,12 @@ export function rankResults(
37
37
  }
38
38
 
39
39
  const results: ScoredResult[] = [];
40
+ const maxFrecency = stats.reduce((max, s) => Math.max(max, s.frecency_score), 1);
40
41
 
41
42
  for (const match of matches) {
42
43
  const stat = statsMap.get(match.text);
43
44
  if (!stat) continue;
44
45
 
45
- // Normalize frecency to 0-1 range
46
- const maxFrecency = stats.reduce((max, s) => Math.max(max, s.frecency_score), 1);
47
46
  const normalizedFrecency = stat.frecency_score / maxFrecency;
48
47
 
49
48
  // CWD bonus
@@ -0,0 +1 @@
1
+ export const IGNORED_COMMANDS = new Set(["ls", "cd", "pwd", "exit", "clear", "sw"]);