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 +23 -14
- package/package.json +2 -1
- package/scripts/setup.sh +31 -11
- package/src/cli/add.ts +1 -2
- package/src/cli/suggest.ts +7 -4
- package/src/daemon/server.ts +14 -7
- package/src/db/queries.ts +4 -2
- package/src/index.ts +11 -10
- package/src/search/scorer.ts +1 -2
- package/src/utils/constants.ts +1 -0
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
|
-
|
|
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 "$(
|
|
52
|
+
eval "$(shellwise init zsh)"
|
|
47
53
|
|
|
48
54
|
# ~/.bashrc
|
|
49
|
-
eval "$(
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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 │◄────────────────────────►│
|
|
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.
|
|
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
|
|
26
|
+
# Detect shellwise binary path (prefer 'shellwise' over 'sw' to avoid conflicts)
|
|
16
27
|
SW_BIN=""
|
|
17
|
-
if command -v
|
|
18
|
-
SW_BIN="
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
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
|
|
31
|
-
echo ' eval "$(
|
|
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 "$(
|
|
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
|
|
85
|
-
|
|
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
|
|
package/src/cli/suggest.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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) {
|
package/src/daemon/server.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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 "$(
|
|
47
|
-
Add to ~/.bashrc: eval "$(
|
|
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:
|
|
83
|
+
console.error("Usage: shellwise add --command <cmd>");
|
|
85
84
|
process.exit(1);
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
// Try daemon first
|
|
89
|
-
|
|
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:
|
|
109
|
+
console.error("Usage: shellwise init <zsh|bash>");
|
|
109
110
|
process.exit(1);
|
|
110
111
|
}
|
|
111
|
-
runInit(shell, "
|
|
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(["
|
|
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:
|
|
182
|
+
console.error("Usage: shellwise daemon start|stop|status");
|
|
182
183
|
process.exit(1);
|
|
183
184
|
}
|
|
184
185
|
break;
|
package/src/search/scorer.ts
CHANGED
|
@@ -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"]);
|