shellwise 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -31,11 +31,17 @@ Tab/Shift+Tab to navigate, Enter to select, Esc to dismiss
31
31
 
32
32
  ## Install
33
33
 
34
+ > **Important:** This is a CLI tool — install it **globally**.
35
+
34
36
  ```bash
37
+ # Recommended
35
38
  bun install -g shellwise
39
+
40
+ # Or with npm
41
+ npm install -g shellwise
36
42
  ```
37
43
 
38
- That's it. Shell integration is auto-injected into your `~/.zshrc` or `~/.bashrc` on install. Restart your terminal to activate.
44
+ Shell integration is auto-injected into your `~/.zshrc` or `~/.bashrc` on install. Restart your terminal to activate.
39
45
 
40
46
  ### Manual setup
41
47
 
@@ -43,10 +49,10 @@ If auto-setup didn't work, add to your shell config:
43
49
 
44
50
  ```bash
45
51
  # ~/.zshrc
46
- eval "$(sw init zsh)"
52
+ eval "$(shellwise init zsh)"
47
53
 
48
54
  # ~/.bashrc
49
- eval "$(sw init bash)"
55
+ eval "$(shellwise init bash)"
50
56
  ```
51
57
 
52
58
  ## Usage
@@ -76,29 +82,31 @@ Press `Ctrl+R` to open full fuzzy search:
76
82
 
77
83
  ### Commands
78
84
 
85
+ Both `shellwise` and `sw` work as the command name:
86
+
79
87
  ```bash
80
- sw search [--query <text>] # Interactive fuzzy search (Ctrl+R)
81
- sw suggest --query <text> # Get top suggestion (used by shell hook)
82
- sw add --command <cmd> # Save a command to history
83
- sw init <zsh|bash> # Output shell integration script
84
- sw import [zsh|bash] # Import existing shell history
85
- sw stats # Show usage statistics
86
- sw prune --days <n> # Remove entries older than n days
87
- sw daemon start|stop|status # Manage background daemon
88
+ shellwise search [--query <text>] # Interactive fuzzy search (Ctrl+R)
89
+ shellwise suggest --query <text> # Get top suggestion (used by shell hook)
90
+ shellwise add --command <cmd> # Save a command to history
91
+ shellwise init <zsh|bash> # Output shell integration script
92
+ shellwise import [zsh|bash] # Import existing shell history
93
+ shellwise stats # Show usage statistics
94
+ shellwise prune --days <n> # Remove entries older than n days
95
+ shellwise daemon start|stop|status # Manage background daemon
88
96
  ```
89
97
 
90
98
  ### Import existing history
91
99
 
92
100
  ```bash
93
- sw import zsh # Import from ~/.zsh_history
94
- sw import bash # Import from ~/.bash_history
101
+ shellwise import zsh # Import from ~/.zsh_history
102
+ shellwise import bash # Import from ~/.bash_history
95
103
  ```
96
104
 
97
105
  ## Architecture
98
106
 
99
107
  ```
100
108
  ┌──────────────┐ TCP (persistent) ┌──────────────────┐
101
- │ Zsh/Bash │◄────────────────────────►│ sw daemon
109
+ │ Zsh/Bash │◄────────────────────────►│ shellwise daemon
102
110
  │ (shell) │ ~1-3ms round-trip │ (Bun process) │
103
111
  └──────────────┘ └────────┬─────────┘
104
112
 
@@ -132,6 +140,7 @@ sw import bash # Import from ~/.bash_history
132
140
 
133
141
  ```bash
134
142
  bun remove -g shellwise
143
+ # or: npm uninstall -g shellwise
135
144
  ```
136
145
 
137
146
  Shell integration is automatically removed from your config on uninstall.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "shellwise",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Smart command history with inline auto-suggest and fuzzy search for your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "shellwise": "./bin/sw.js",
7
8
  "sw": "./bin/sw.js"
8
9
  },
9
10
  "files": [
package/scripts/setup.sh CHANGED
@@ -10,15 +10,33 @@ DIM='\033[2m'
10
10
  BOLD='\033[1m'
11
11
  RESET='\033[0m'
12
12
 
13
+ # Detect if this is a local (not global) install
14
+ if [[ -z "$(command -v shellwise 2>/dev/null)" && -z "$(command -v sw 2>/dev/null)" ]]; then
15
+ # Check if we're inside node_modules (local install)
16
+ if [[ "$PWD" == *"node_modules"* ]] || [[ "${INIT_CWD:-}" == *"node_modules"* ]]; then
17
+ echo -e "${YELLOW}${BOLD}[shellwise]${RESET} This is a CLI tool — install it globally:"
18
+ echo -e " ${BOLD}bun install -g shellwise${RESET}"
19
+ echo -e " ${DIM}or: npm install -g shellwise${RESET}"
20
+ exit 0
21
+ fi
22
+ fi
23
+
13
24
  MARKER="# shellwise shell integration"
14
25
 
15
- # Detect sw binary path
26
+ # Detect shellwise binary path (prefer 'shellwise' over 'sw' to avoid conflicts)
16
27
  SW_BIN=""
17
- if command -v sw &>/dev/null; then
18
- SW_BIN="sw"
19
- else
20
- # Try common global bin paths
21
- for p in "$HOME/.bun/bin/sw" "$HOME/.local/bin/sw" "$(npm prefix -g 2>/dev/null)/bin/sw"; do
28
+ if command -v shellwise &>/dev/null; then
29
+ SW_BIN="shellwise"
30
+ elif command -v sw &>/dev/null; then
31
+ # Verify it's actually shellwise, not another tool
32
+ if sw --help 2>&1 | grep -q "shellwise" 2>/dev/null; then
33
+ SW_BIN="sw"
34
+ fi
35
+ fi
36
+
37
+ # Try common global bin paths
38
+ if [[ -z "$SW_BIN" ]]; then
39
+ for p in "$HOME/.bun/bin/shellwise" "$HOME/.local/bin/shellwise" "$(npm prefix -g 2>/dev/null)/bin/shellwise"; do
22
40
  if [[ -x "$p" ]]; then
23
41
  SW_BIN="$p"
24
42
  break
@@ -27,8 +45,8 @@ else
27
45
  fi
28
46
 
29
47
  if [[ -z "$SW_BIN" ]]; then
30
- echo -e "${YELLOW}[shellwise]${RESET} Could not find sw binary. Add manually:"
31
- echo ' eval "$(sw init zsh)" # add to ~/.zshrc'
48
+ echo -e "${YELLOW}[shellwise]${RESET} Could not find shellwise binary. Add manually:"
49
+ echo ' eval "$(shellwise init zsh)" # add to ~/.zshrc'
32
50
  exit 0
33
51
  fi
34
52
 
@@ -75,14 +93,16 @@ case "$SHELL_NAME" in
75
93
  *)
76
94
  echo -e "${YELLOW}[shellwise]${RESET} Unsupported shell: $SHELL_NAME"
77
95
  echo ' Supported: zsh, bash'
78
- echo ' Add manually: eval "$(sw init zsh)"'
96
+ echo ' Add manually: eval "$(shellwise init zsh)"'
79
97
  exit 0
80
98
  ;;
81
99
  esac
82
100
 
83
101
  # Start daemon for fast suggest
84
- if command -v sw &>/dev/null; then
85
- sw daemon start &>/dev/null || true
102
+ if command -v shellwise &>/dev/null; then
103
+ shellwise daemon start &>/dev/null || true
104
+ elif [[ -n "$SW_BIN" ]]; then
105
+ $SW_BIN daemon start &>/dev/null || true
86
106
  fi
87
107
 
88
108
  echo -e "${GREEN}${BOLD}[shellwise]${RESET} Restart your terminal or run: ${BOLD}source ~/.${SHELL_NAME}rc${RESET}"
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];
@@ -30,7 +29,7 @@ function parseFlags(args: string[]): Record<string, string> {
30
29
  function printHelp(): void {
31
30
  console.log(`shellwise - Smart command history with fuzzy search
32
31
 
33
- Usage: sw <command> [options]
32
+ Usage: shellwise <command> [options] (or: sw <command>)
34
33
 
35
34
  Commands:
36
35
  search [--query <text>] Interactive fuzzy search (Ctrl+R)
@@ -43,8 +42,8 @@ Commands:
43
42
  daemon start|stop|status Manage background daemon (faster suggest)
44
43
 
45
44
  Setup:
46
- Add to ~/.zshrc: eval "$(sw init zsh)"
47
- Add to ~/.bashrc: eval "$(sw init bash)"
45
+ Add to ~/.zshrc: eval "$(shellwise init zsh)"
46
+ Add to ~/.bashrc: eval "$(shellwise init bash)"
48
47
 
49
48
  Features:
50
49
  - Auto-save: commands are recorded automatically
@@ -81,12 +80,14 @@ async function main(): Promise<void> {
81
80
  case "add": {
82
81
  const flags = parseFlags(args.slice(1));
83
82
  if (!flags.command) {
84
- console.error("Usage: sw add --command <cmd>");
83
+ console.error("Usage: shellwise add --command <cmd>");
85
84
  process.exit(1);
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
@@ -105,10 +106,10 @@ async function main(): Promise<void> {
105
106
  case "init": {
106
107
  const shell = args[1];
107
108
  if (!shell) {
108
- console.error("Usage: sw init <zsh|bash>");
109
+ console.error("Usage: shellwise init <zsh|bash>");
109
110
  process.exit(1);
110
111
  }
111
- runInit(shell, "sw");
112
+ runInit(shell, "shellwise");
112
113
  break;
113
114
  }
114
115
 
@@ -138,7 +139,7 @@ async function main(): Promise<void> {
138
139
  return;
139
140
  }
140
141
  // Fork to background
141
- const proc = Bun.spawn(["sw", "daemon", "_run"], {
142
+ const proc = Bun.spawn(["shellwise", "daemon", "_run"], {
142
143
  stdio: ["ignore", "ignore", "ignore"],
143
144
  // @ts-ignore - Bun supports detached
144
145
  detached: true,
@@ -178,7 +179,7 @@ async function main(): Promise<void> {
178
179
  break;
179
180
  }
180
181
  default:
181
- console.error("Usage: sw daemon start|stop|status");
182
+ console.error("Usage: shellwise daemon start|stop|status");
182
183
  process.exit(1);
183
184
  }
184
185
  break;
@@ -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"]);