@arthony/keybook 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ariyapong W.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # keybook
2
+
3
+ A self-contained, open-source **macOS TUI** for searching the keyboard
4
+ shortcuts and short recipes of your most-used apps. Launch `keybook`,
5
+ fuzzy-search by intent ("finder new tab", "terminal here"), and learn your
6
+ shortcuts along the way.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g @arthony/keybook
12
+ # or run without installing:
13
+ npx @arthony/keybook
14
+ ```
15
+
16
+ Requires Node 22+ and macOS. The package is published under the `@arthony`
17
+ scope, but the commands it installs are still `keybook` (and the `kb` alias).
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ keybook # launch the TUI (alias: kb)
23
+ keybook path # print the data directory
24
+ keybook edit # open the data directory in $EDITOR
25
+ keybook check # validate your data files
26
+ ```
27
+
28
+ In the TUI: type to fuzzy-search, ↑/↓ to move, ⏎ to copy the shortcut (or a
29
+ recipe's command) to the clipboard, ⎋ to quit.
30
+
31
+ ## Your data
32
+
33
+ Shortcuts live in plain YAML files you own — by default `~/.config/keybook/`
34
+ (override with `$KEYBOOK_DATA_DIR`, e.g. point it at a synced/git folder to
35
+ share across machines). On first run the directory is seeded from a bundled
36
+ starter set; your edits are never overwritten.
37
+
38
+ Each entry is a key combo **or** a short recipe:
39
+
40
+ ```yaml
41
+ app: Finder
42
+ entries:
43
+ - action: Open a new tab
44
+ keys: "⌘T"
45
+ tags: [new tab]
46
+ - action: Open Terminal at the current folder
47
+ steps: [Right-click the folder, Services → "New Terminal at Folder"]
48
+ tags: [terminal here]
49
+ ```
50
+
51
+ Add entries by hand, or ask an AI assistant to append them and run
52
+ `keybook check` before committing.
53
+
54
+ ## Development
55
+
56
+ Requires Node 22+ and pnpm. One-shot setup after cloning:
57
+
58
+ ```bash
59
+ git clone https://github.com/Ariyapong/keybook.git
60
+ cd keybook
61
+ ./scripts/dev-setup.sh # checks Node, enables pnpm, installs, runs tests
62
+ ```
63
+
64
+ Or manually:
65
+
66
+ ```bash
67
+ corepack enable # enables pnpm (or: npm i -g pnpm)
68
+ pnpm install
69
+ pnpm test # run the suite
70
+ pnpm build # bundle to dist/cli.js
71
+ pnpm dev # run the TUI from source (tsx)
72
+ ```
73
+
74
+ Also available: `pnpm typecheck`, `pnpm lint`, `pnpm format`.
75
+
76
+ **Layout:** `src/data` (zod schema + YAML loader), `src/config.ts` (data dir +
77
+ first-run seeding), `src/search.ts` (fzf), `src/tui` (Ink components),
78
+ `src/cli.ts` (commander entry), `seed/` (bundled starter data). Design docs are
79
+ in `docs/superpowers/`.
80
+
81
+ If `pnpm lint` reports a missing Biome binary, run `pnpm approve-builds`
82
+ (pnpm skips package build scripts by default).
83
+
84
+ ## License
85
+
86
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { spawn } from "child_process";
5
+ import { Command } from "commander";
6
+ import { render } from "ink";
7
+ import { createElement } from "react";
8
+
9
+ // src/config.ts
10
+ import { cpSync, existsSync, mkdirSync, readdirSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { dirname, join } from "path";
13
+ import { fileURLToPath } from "url";
14
+ var YAML_RE = /\.ya?ml$/;
15
+ function resolveDataDir(env = process.env) {
16
+ if (env.KEYBOOK_DATA_DIR) return { dir: env.KEYBOOK_DATA_DIR, source: "env" };
17
+ if (env.XDG_CONFIG_HOME) return { dir: join(env.XDG_CONFIG_HOME, "keybook"), source: "xdg" };
18
+ return { dir: join(homedir(), ".config", "keybook"), source: "default" };
19
+ }
20
+ function seedDir() {
21
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "seed");
22
+ }
23
+ function ensureDataDir(dir, seed = seedDir()) {
24
+ const hasYaml = existsSync(dir) && readdirSync(dir).some((f) => YAML_RE.test(f));
25
+ if (hasYaml) return { initialized: false, fileCount: 0 };
26
+ mkdirSync(dir, { recursive: true });
27
+ const seedFiles = readdirSync(seed).filter((f) => YAML_RE.test(f));
28
+ for (const f of seedFiles) cpSync(join(seed, f), join(dir, f));
29
+ return { initialized: true, fileCount: seedFiles.length };
30
+ }
31
+
32
+ // src/data/loader.ts
33
+ import { readFileSync, readdirSync as readdirSync2 } from "fs";
34
+ import { join as join2 } from "path";
35
+ import { parse as parseYaml } from "yaml";
36
+
37
+ // src/data/schema.ts
38
+ import { z } from "zod";
39
+ var entrySchema = z.object({
40
+ action: z.string().min(1),
41
+ keys: z.string().min(1).optional(),
42
+ steps: z.array(z.string().min(1)).min(1).optional(),
43
+ command: z.string().min(1).optional(),
44
+ tags: z.array(z.string().min(1)).optional(),
45
+ notes: z.string().min(1).optional(),
46
+ source: z.string().min(1).optional()
47
+ }).strict().refine((e) => Boolean(e.keys) || Boolean(e.steps) || Boolean(e.command), {
48
+ message: "entry must have at least one of: keys, steps, command"
49
+ });
50
+ var fileShape = z.object({
51
+ app: z.string().min(1),
52
+ entries: z.array(z.unknown()).min(1)
53
+ }).strict();
54
+
55
+ // src/data/loader.ts
56
+ var YAML_RE2 = /\.ya?ml$/;
57
+ function loadEntries(dataDir) {
58
+ const entries = [];
59
+ const errors = [];
60
+ let files;
61
+ try {
62
+ files = readdirSync2(dataDir).filter((f) => YAML_RE2.test(f)).sort();
63
+ } catch (err) {
64
+ errors.push({
65
+ file: dataDir,
66
+ entryIndex: null,
67
+ message: `cannot read data dir: ${err.message}`
68
+ });
69
+ return { entries, errors };
70
+ }
71
+ for (const file of files) {
72
+ let raw;
73
+ try {
74
+ raw = parseYaml(readFileSync(join2(dataDir, file), "utf8"));
75
+ } catch (err) {
76
+ errors.push({
77
+ file,
78
+ entryIndex: null,
79
+ message: `YAML parse error: ${err.message}`
80
+ });
81
+ continue;
82
+ }
83
+ const shape = fileShape.safeParse(raw);
84
+ if (!shape.success) {
85
+ errors.push({
86
+ file,
87
+ entryIndex: null,
88
+ message: shape.error.issues[0]?.message ?? "invalid file shape"
89
+ });
90
+ continue;
91
+ }
92
+ const { app, entries: rawEntries } = shape.data;
93
+ rawEntries.forEach((rawEntry, i) => {
94
+ const parsed = entrySchema.safeParse(rawEntry);
95
+ if (!parsed.success) {
96
+ errors.push({
97
+ file,
98
+ entryIndex: i,
99
+ message: parsed.error.issues.map((s) => s.message).join("; ")
100
+ });
101
+ return;
102
+ }
103
+ entries.push({ app, ...parsed.data });
104
+ });
105
+ }
106
+ return { entries, errors };
107
+ }
108
+
109
+ // src/commands.ts
110
+ function runPath(env = process.env) {
111
+ const { dir, source } = resolveDataDir(env);
112
+ return `${dir} (source: ${source})`;
113
+ }
114
+ function runCheck(dir) {
115
+ const { entries, errors } = loadEntries(dir);
116
+ if (errors.length === 0) return { ok: true, lines: [`\u2713 ${entries.length} entries OK`] };
117
+ return {
118
+ ok: false,
119
+ lines: errors.map(
120
+ (e) => `\u2717 ${e.file}${e.entryIndex !== null ? ` [entry ${e.entryIndex}]` : ""}: ${e.message}`
121
+ )
122
+ };
123
+ }
124
+
125
+ // src/tui/App.tsx
126
+ import { Box as Box6, useApp, useInput, useStdout } from "ink";
127
+ import { useMemo, useState } from "react";
128
+
129
+ // src/clipboard.ts
130
+ import { spawnSync } from "child_process";
131
+ function copyToClipboard(text) {
132
+ try {
133
+ const res = spawnSync("pbcopy", { input: text });
134
+ return res.status === 0;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ // src/search.ts
141
+ import { Fzf, extendedMatch } from "fzf";
142
+ function haystack(e) {
143
+ return [e.action, (e.tags ?? []).join(" "), e.app, e.keys ?? "", e.notes ?? ""].filter(Boolean).join(" ");
144
+ }
145
+ function search(entries, query) {
146
+ const q = query.trim();
147
+ if (!q) {
148
+ return [...entries].sort(
149
+ (a, b) => a.app.localeCompare(b.app) || a.action.localeCompare(b.action)
150
+ );
151
+ }
152
+ const fzf = new Fzf(entries, { selector: haystack, match: extendedMatch });
153
+ return fzf.find(q).map((r) => r.item);
154
+ }
155
+
156
+ // src/tui/Footer.tsx
157
+ import { Box, Text } from "ink";
158
+ import { jsx, jsxs } from "react/jsx-runtime";
159
+ function Footer({
160
+ flash,
161
+ errorCount,
162
+ resultCount
163
+ }) {
164
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [
165
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
166
+ "\u2191\u2193 move \u23CE copy \u238B quit (",
167
+ resultCount,
168
+ ")"
169
+ ] }),
170
+ flash ? /* @__PURE__ */ jsx(Text, { color: "green", children: flash }) : errorCount > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
171
+ "\u26A0 ",
172
+ errorCount,
173
+ " skipped \u2014 run `keybook check`"
174
+ ] }) : /* @__PURE__ */ jsx(Text, { children: " " })
175
+ ] });
176
+ }
177
+
178
+ // src/tui/PreviewPane.tsx
179
+ import { Box as Box2, Text as Text2 } from "ink";
180
+
181
+ // src/tui/keycaps.ts
182
+ var GLYPHS = /* @__PURE__ */ new Set(["\u2318", "\u2325", "\u2303", "\u21E7", "\u21EA", "\u23CE", "\u238B", "\u232B", "\u21E5", "\u2423", "\u2191", "\u2193", "\u2190", "\u2192"]);
183
+ function parseSegment(seg) {
184
+ const tokens = [];
185
+ let buf = "";
186
+ for (const ch of seg) {
187
+ if (GLYPHS.has(ch)) {
188
+ if (buf) {
189
+ tokens.push(buf);
190
+ buf = "";
191
+ }
192
+ tokens.push(ch);
193
+ } else if (ch === " ") {
194
+ if (buf) {
195
+ tokens.push(buf);
196
+ buf = "";
197
+ }
198
+ } else {
199
+ buf += ch;
200
+ }
201
+ }
202
+ if (buf) tokens.push(buf);
203
+ return tokens.length ? tokens : [seg];
204
+ }
205
+ function parseKeys(input) {
206
+ return input.split(",").map((s) => s.trim()).filter(Boolean).map(parseSegment);
207
+ }
208
+
209
+ // src/tui/PreviewPane.tsx
210
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
211
+ function KeyCaps({ value }) {
212
+ const chords = parseKeys(value);
213
+ return /* @__PURE__ */ jsx2(Box2, { children: chords.map((seg, si) => (
214
+ // biome-ignore lint/suspicious/noArrayIndexKey: token order is stable; tokens can repeat (e.g. ⌃X⌃E) so the value is not a unique key
215
+ /* @__PURE__ */ jsxs2(Box2, { children: [
216
+ si > 0 ? /* @__PURE__ */ jsx2(Text2, { children: " , " }) : null,
217
+ seg.map((tok, ti) => (
218
+ // biome-ignore lint/suspicious/noArrayIndexKey: see above — repeated tokens make the value non-unique
219
+ /* @__PURE__ */ jsx2(Box2, { marginRight: 1, borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: tok }) }, ti)
220
+ ))
221
+ ] }, si)
222
+ )) });
223
+ }
224
+ function PreviewPane({ entry, width = "50%" }) {
225
+ if (!entry) return null;
226
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, paddingLeft: 2, children: [
227
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: entry.app }),
228
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: entry.action }),
229
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
230
+ entry.keys ? /* @__PURE__ */ jsx2(KeyCaps, { value: entry.keys }) : null,
231
+ entry.steps?.map((s, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
232
+ i + 1,
233
+ ". ",
234
+ s
235
+ ] }, s)),
236
+ entry.command ? /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
237
+ "$ ",
238
+ entry.command
239
+ ] }) : null
240
+ ] }),
241
+ entry.tags?.length ? /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
242
+ "tags: ",
243
+ entry.tags.join(", ")
244
+ ] }) : null,
245
+ entry.notes ? /* @__PURE__ */ jsx2(Text2, { color: "gray", children: entry.notes }) : null
246
+ ] });
247
+ }
248
+
249
+ // src/tui/ResultList.tsx
250
+ import { Box as Box4, Text as Text4 } from "ink";
251
+
252
+ // src/tui/ResultRow.tsx
253
+ import { Box as Box3, Text as Text3 } from "ink";
254
+ import { jsxs as jsxs3 } from "react/jsx-runtime";
255
+ function ResultRow({ entry, selected }) {
256
+ const right = entry.keys ?? "recipe";
257
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
258
+ /* @__PURE__ */ jsxs3(Text3, { color: selected ? "cyan" : void 0, inverse: selected, children: [
259
+ selected ? "\u25B8 " : " ",
260
+ entry.app,
261
+ " \xB7 ",
262
+ entry.action
263
+ ] }),
264
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
265
+ " ",
266
+ right
267
+ ] })
268
+ ] });
269
+ }
270
+
271
+ // src/tui/ResultList.tsx
272
+ import { jsx as jsx3, jsxs as jsxs4 } from "react/jsx-runtime";
273
+ function ResultList({
274
+ results,
275
+ selected,
276
+ query,
277
+ height = 12,
278
+ width = "50%"
279
+ }) {
280
+ if (results.length === 0) {
281
+ return /* @__PURE__ */ jsx3(Box4, { width, justifyContent: "center", children: /* @__PURE__ */ jsxs4(Text4, { color: "gray", children: [
282
+ 'No matches for "',
283
+ query,
284
+ '"'
285
+ ] }) });
286
+ }
287
+ const start = Math.max(0, Math.min(selected - Math.floor(height / 2), results.length - height));
288
+ const visible = results.slice(Math.max(0, start), Math.max(0, start) + height);
289
+ return /* @__PURE__ */ jsx3(Box4, { flexDirection: "column", width, children: visible.map((e, i) => /* @__PURE__ */ jsx3(
290
+ ResultRow,
291
+ {
292
+ entry: e,
293
+ selected: Math.max(0, start) + i === selected
294
+ },
295
+ `${e.app}:${e.action}`
296
+ )) });
297
+ }
298
+
299
+ // src/tui/SearchInput.tsx
300
+ import { Box as Box5, Text as Text5 } from "ink";
301
+ import { jsx as jsx4, jsxs as jsxs5 } from "react/jsx-runtime";
302
+ function SearchInput({ query }) {
303
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
304
+ /* @__PURE__ */ jsx4(Text5, { color: "cyan", children: "search: " }),
305
+ /* @__PURE__ */ jsx4(Text5, { children: query }),
306
+ /* @__PURE__ */ jsx4(Text5, { inverse: true, children: " " })
307
+ ] });
308
+ }
309
+
310
+ // src/tui/layout.ts
311
+ var RESERVED_ROWS = 4;
312
+ function visibleListHeight(terminalRows) {
313
+ const rows = terminalRows && terminalRows > 0 ? terminalRows : 24;
314
+ return Math.max(1, rows - RESERVED_ROWS);
315
+ }
316
+ function columnWidths(terminalCols) {
317
+ const cols = terminalCols && terminalCols > 0 ? terminalCols : 80;
318
+ const left = Math.floor(cols / 2);
319
+ return { left, right: cols - left };
320
+ }
321
+
322
+ // src/tui/App.tsx
323
+ import { jsx as jsx5, jsxs as jsxs6 } from "react/jsx-runtime";
324
+ function App({ entries, errorCount = 0, onCopy = copyToClipboard }) {
325
+ const { exit } = useApp();
326
+ const { stdout } = useStdout();
327
+ const [query, setQuery] = useState("");
328
+ const [selected, setSelected] = useState(0);
329
+ const [flash, setFlash] = useState("");
330
+ const results = useMemo(() => search(entries, query), [entries, query]);
331
+ const sel = results.length ? Math.min(selected, results.length - 1) : 0;
332
+ const current = results[sel];
333
+ const listHeight = visibleListHeight(stdout?.rows);
334
+ const { left, right } = columnWidths(stdout?.columns);
335
+ useInput((input, key) => {
336
+ if (!key.return && flash) setFlash("");
337
+ if (key.escape || key.ctrl && input === "c") {
338
+ exit();
339
+ return;
340
+ }
341
+ if (key.downArrow || key.ctrl && input === "n") {
342
+ setSelected((s) => Math.min(s + 1, results.length - 1));
343
+ return;
344
+ }
345
+ if (key.upArrow || key.ctrl && input === "p") {
346
+ setSelected((s) => Math.max(s - 1, 0));
347
+ return;
348
+ }
349
+ if (key.return) {
350
+ if (current) {
351
+ const text = current.keys ?? current.command ?? `${current.app}: ${current.action}`;
352
+ setFlash(onCopy(text) ? "\u2713 copied!" : "\u2717 copy failed");
353
+ }
354
+ return;
355
+ }
356
+ if (key.backspace || key.delete) {
357
+ setQuery((q) => q.slice(0, -1));
358
+ setSelected(0);
359
+ return;
360
+ }
361
+ if (input && !key.ctrl && !key.meta) {
362
+ setQuery((q) => q + input);
363
+ setSelected(0);
364
+ }
365
+ });
366
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
367
+ /* @__PURE__ */ jsx5(SearchInput, { query }),
368
+ /* @__PURE__ */ jsxs6(Box6, { children: [
369
+ /* @__PURE__ */ jsx5(
370
+ ResultList,
371
+ {
372
+ results,
373
+ selected: sel,
374
+ query,
375
+ height: listHeight,
376
+ width: left
377
+ }
378
+ ),
379
+ /* @__PURE__ */ jsx5(PreviewPane, { entry: current, width: right })
380
+ ] }),
381
+ /* @__PURE__ */ jsx5(Footer, { flash, errorCount, resultCount: results.length })
382
+ ] });
383
+ }
384
+
385
+ // src/cli.ts
386
+ var program = new Command();
387
+ program.name("keybook").description("Search keyboard shortcuts & recipes for your apps").version("0.1.0");
388
+ program.command("path").description("Print the data directory").action(() => {
389
+ console.log(runPath());
390
+ });
391
+ program.command("edit").description("Open the data directory in $EDITOR").action(() => {
392
+ const { dir } = resolveDataDir();
393
+ ensureDataDir(dir);
394
+ const editor = process.env.EDITOR || "code";
395
+ const child = spawn(editor, [dir], { stdio: "inherit" });
396
+ child.on("error", () => spawn("open", [dir], { stdio: "inherit" }));
397
+ });
398
+ program.command("check").description("Validate all data files").action(() => {
399
+ const { dir } = resolveDataDir();
400
+ ensureDataDir(dir);
401
+ const result = runCheck(dir);
402
+ for (const line of result.lines) (result.ok ? console.log : console.error)(line);
403
+ process.exit(result.ok ? 0 : 1);
404
+ });
405
+ program.action(() => {
406
+ const { dir } = resolveDataDir();
407
+ const init = ensureDataDir(dir);
408
+ if (init.initialized) console.log(`Initialized ${dir} from seed (${init.fileCount} files).`);
409
+ const { entries, errors } = loadEntries(dir);
410
+ for (const e of errors) console.error(`\u26A0 ${e.file}: ${e.message}`);
411
+ render(createElement(App, { entries, errorCount: errors.length }));
412
+ });
413
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@arthony/keybook",
3
+ "version": "0.1.0",
4
+ "publishConfig": { "access": "public" },
5
+ "description": "A macOS TUI for searching keyboard shortcuts and recipes of your favorite apps",
6
+ "keywords": [
7
+ "cli",
8
+ "tui",
9
+ "ink",
10
+ "keyboard-shortcuts",
11
+ "shortcuts",
12
+ "cheatsheet",
13
+ "macos",
14
+ "terminal",
15
+ "productivity"
16
+ ],
17
+ "homepage": "https://github.com/Ariyapong/keybook#readme",
18
+ "repository": { "type": "git", "url": "git+https://github.com/Ariyapong/keybook.git" },
19
+ "bugs": { "url": "https://github.com/Ariyapong/keybook/issues" },
20
+ "author": "Ariyapong Wimolnoch",
21
+ "type": "module",
22
+ "bin": { "keybook": "dist/cli.js", "kb": "dist/cli.js" },
23
+ "files": ["dist", "seed", "README.md", "LICENSE"],
24
+ "engines": { "node": ">=22" },
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsx src/cli.ts",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "biome check .",
32
+ "format": "biome format --write .",
33
+ "prepublishOnly": "pnpm build"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^12.1.0",
37
+ "fzf": "^0.5.2",
38
+ "ink": "^5.0.1",
39
+ "react": "^18.3.1",
40
+ "yaml": "^2.5.0",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@biomejs/biome": "^1.9.4",
45
+ "@types/node": "^22.7.0",
46
+ "@types/react": "^18.3.0",
47
+ "ink-testing-library": "^4.0.0",
48
+ "tsup": "^8.3.0",
49
+ "tsx": "^4.19.0",
50
+ "typescript": "^5.6.0",
51
+ "vitest": "^2.1.0"
52
+ },
53
+ "license": "MIT"
54
+ }
@@ -0,0 +1,21 @@
1
+ app: Claude
2
+ entries:
3
+ - action: Start a new chat
4
+ keys: "⌘N"
5
+ notes: Labeled "New Conversation" in the File menu.
6
+ - action: Send the message
7
+ keys: "⏎"
8
+ - action: Insert a newline in the message
9
+ keys: "⇧⏎"
10
+ tags: [new line]
11
+ - action: Search your chats
12
+ keys: "⌘K"
13
+ notes: In-app shortcut (not in the menu bar). Distinct from Edit → Find (⌘F).
14
+ tags: [search, find chats]
15
+ - action: Summon Claude from anywhere (global hotkey)
16
+ steps:
17
+ - Open Claude → Settings
18
+ - Set a global keyboard shortcut for the quick launcher
19
+ - Press that shortcut from any app to summon Claude
20
+ notes: The Claude desktop app supports a configurable global hotkey.
21
+ tags: [global, launcher, quick]
package/seed/edge.yaml ADDED
@@ -0,0 +1,39 @@
1
+ app: Microsoft Edge
2
+ entries:
3
+ - action: Open a new tab
4
+ keys: "⌘T"
5
+ tags: [new tab]
6
+ - action: Reopen the last closed tab
7
+ keys: "⌘⇧T"
8
+ tags: [restore tab]
9
+ - action: Open a new window
10
+ keys: "⌘N"
11
+ - action: Open a new InPrivate window
12
+ keys: "⌘⇧N"
13
+ tags: [incognito, private]
14
+ - action: Close the current tab
15
+ keys: "⌘W"
16
+ - action: Next / previous tab
17
+ keys: "⌃⇥, ⌃⇧⇥"
18
+ - action: Jump to tab 1–8
19
+ keys: "⌘1"
20
+ notes: ⌘1–⌘8 jump to that tab; ⌘9 jumps to the last tab.
21
+ - action: Focus the address bar
22
+ keys: "⌘L"
23
+ tags: [omnibox, url]
24
+ - action: Find on the page
25
+ keys: "⌘F"
26
+ - action: Reload / hard reload
27
+ keys: "⌘R, ⌘⇧R"
28
+ - action: Open Developer Tools
29
+ keys: "⌘⌥I"
30
+ tags: [devtools, inspect]
31
+ - action: Open History
32
+ keys: "⌘Y"
33
+ - action: Bookmark this page
34
+ keys: "⌘D"
35
+ tags: [favorite]
36
+ - action: Zoom in / out / reset
37
+ keys: "⌘+, ⌘-, ⌘0"
38
+ - action: Back / forward
39
+ keys: "⌘[, ⌘]"
@@ -0,0 +1,52 @@
1
+ app: Finder
2
+ entries:
3
+ - action: Open a new tab
4
+ keys: "⌘T"
5
+ tags: [new tab]
6
+ - action: Open a new Finder window
7
+ keys: "⌘N"
8
+ - action: Create a new folder
9
+ keys: "⌘⇧N"
10
+ tags: [new folder]
11
+ - action: Go to folder by path
12
+ keys: "⌘⇧G"
13
+ tags: [go to, path]
14
+ - action: Go up one folder
15
+ keys: "⌘↑"
16
+ - action: Open selected item
17
+ keys: "⌘↓"
18
+ - action: Back / Forward
19
+ keys: "⌘[, ⌘]"
20
+ tags: [history, navigate]
21
+ - action: Rename the selected item
22
+ keys: "⏎"
23
+ notes: In Finder, Return renames (it does not open).
24
+ - action: Quick Look the selected item
25
+ keys: "␣"
26
+ tags: [preview, space]
27
+ - action: Show or hide hidden files
28
+ keys: "⌘⇧."
29
+ tags: [dotfiles, hidden]
30
+ - action: Get Info
31
+ keys: "⌘I"
32
+ - action: Duplicate
33
+ keys: "⌘D"
34
+ - action: Move to Trash
35
+ keys: "⌘⌫"
36
+ tags: [delete]
37
+ - action: Move copied files here (cut/paste)
38
+ keys: "⌘⌥V"
39
+ tags: [move, cut, paste]
40
+ - action: Empty the Trash
41
+ keys: "⌘⇧⌫"
42
+ - action: View as list / columns / gallery
43
+ keys: "⌘2"
44
+ notes: ⌘1 icons, ⌘2 list, ⌘3 columns, ⌘4 gallery.
45
+ - action: Open Terminal at the current folder
46
+ steps:
47
+ - Right-click the folder (or its name in the path bar at the bottom)
48
+ - Choose Services → "New Terminal at Folder"
49
+ notes: >-
50
+ Enable once in System Settings → Keyboard → Keyboard Shortcuts →
51
+ Services → Files and Folders → "New Terminal at Folder".
52
+ tags: [terminal here, open terminal, cd]
package/seed/fork.yaml ADDED
@@ -0,0 +1,27 @@
1
+ app: Fork
2
+ entries:
3
+ - action: Open a repository
4
+ keys: "⌘O"
5
+ - action: Open a new tab
6
+ keys: "⌘T"
7
+ - action: Close the current tab
8
+ keys: "⌘W"
9
+ - action: Commit the staged changes
10
+ keys: "⌘⏎"
11
+ notes: From the commit message box. The Repository → Commit menu item is ⇧⌘C.
12
+ - action: Fetch
13
+ keys: "⇧⌘F"
14
+ notes: Add ⌥ for Quick Fetch (skips the dialog) — ⌥⇧⌘F.
15
+ tags: [fetch]
16
+ - action: Pull
17
+ keys: "⇧⌘L"
18
+ notes: Add ⌥ for Quick Pull — ⌥⇧⌘L.
19
+ tags: [pull]
20
+ - action: Push
21
+ keys: "⇧⌘P"
22
+ notes: Add ⌥ for Quick Push — ⌥⇧⌘P.
23
+ tags: [push]
24
+ - action: Open Quick Launch (command palette)
25
+ keys: "⌘P"
26
+ notes: Fork's fuzzy action launcher.
27
+ tags: [palette, actions]
@@ -0,0 +1,50 @@
1
+ app: macOS
2
+ entries:
3
+ - action: Open Spotlight search
4
+ keys: "⌘␣"
5
+ tags: [search, launch]
6
+ - action: Switch between open apps
7
+ keys: "⌘⇥"
8
+ tags: [app switcher]
9
+ - action: Switch between windows of the current app
10
+ keys: "⌘`"
11
+ - action: Screenshot a region to a file
12
+ keys: "⌘⇧4"
13
+ tags: [screenshot, capture]
14
+ - action: Screenshot the whole screen to a file
15
+ keys: "⌘⇧3"
16
+ tags: [screenshot]
17
+ - action: Screenshot a region to the clipboard
18
+ keys: "⌃⌘⇧4"
19
+ tags: [screenshot, copy]
20
+ - action: Open the screenshot / recording toolbar
21
+ keys: "⌘⇧5"
22
+ tags: [screenshot, record]
23
+ - action: Quit the current app
24
+ keys: "⌘Q"
25
+ - action: Hide the current app
26
+ keys: "⌘H"
27
+ - action: Minimize the window
28
+ keys: "⌘M"
29
+ - action: Close the window
30
+ keys: "⌘W"
31
+ - action: Toggle full screen
32
+ keys: "⌃⌘F"
33
+ - action: Open Mission Control
34
+ keys: "⌃↑"
35
+ - action: Move one space left / right
36
+ keys: "⌃←, ⌃→"
37
+ - action: Lock the screen
38
+ keys: "⌃⌘Q"
39
+ - action: Open the Force Quit dialog
40
+ keys: "⌥⌘⎋"
41
+ tags: [force quit]
42
+ - action: Open the emoji & symbols picker
43
+ keys: "⌃⌘␣"
44
+ tags: [emoji]
45
+ - action: Record your screen
46
+ steps:
47
+ - Press ⌘⇧5
48
+ - Choose "Record Entire Screen" or "Record Selected Portion", then click Record
49
+ - To stop, click the ⏹ stop button in the menu bar
50
+ tags: [screen recording]
package/seed/nano.yaml ADDED
@@ -0,0 +1,51 @@
1
+ app: Nano
2
+ entries:
3
+ - action: Save the file (write out)
4
+ keys: "⌃O"
5
+ tags: [save, write]
6
+ - action: Exit nano
7
+ keys: "⌃X"
8
+ tags: [quit, exit]
9
+ - action: Show help
10
+ keys: "⌃G"
11
+ tags: [help]
12
+ - action: Cut the current line
13
+ keys: "⌃K"
14
+ tags: [cut, delete]
15
+ - action: Paste (uncut) the cut text
16
+ keys: "⌃U"
17
+ tags: [paste, uncut]
18
+ - action: Set a mark, then move to select
19
+ keys: "⌃6"
20
+ notes: ⌃6 (or ⌃^) starts a selection; move the cursor to extend it, then ⌃K cuts it.
21
+ tags: [select, mark]
22
+ - action: Search
23
+ keys: "⌃W"
24
+ notes: After ⌃W, press ⏎ again to repeat the last search.
25
+ tags: [find, search]
26
+ - action: Search and replace
27
+ keys: "⌃\\"
28
+ tags: [replace]
29
+ - action: Go to line / column
30
+ keys: "⌃_"
31
+ tags: [goto, line]
32
+ - action: Go to top / bottom of the file
33
+ keys: "⌥\\, ⌥/"
34
+ tags: [top, bottom]
35
+ - action: Page up / down
36
+ keys: "⌃Y, ⌃V"
37
+ tags: [scroll, page]
38
+ - action: Undo / redo
39
+ keys: "⌥U, ⌥E"
40
+ tags: [undo, redo]
41
+ - action: Show the current cursor position
42
+ keys: "⌃C"
43
+ tags: [position, line number]
44
+ - action: Notation note (Control and Meta keys)
45
+ steps:
46
+ - nano writes ⌃ as "^" (Control) and ⌥ as "M-" (Meta)
47
+ - "Example: ^O means Control-O; M-U means Meta-U"
48
+ notes: >-
49
+ On macOS, Meta = Option. If ⌥ shortcuts don't work in Terminal, enable
50
+ Terminal → Settings → Profiles → Keyboard → "Use Option as Meta key".
51
+ tags: [notation, meta, help]
@@ -0,0 +1,71 @@
1
+ app: Terminal
2
+ entries:
3
+ - action: Open a new tab
4
+ keys: "⌘T"
5
+ tags: [new tab]
6
+ - action: Open a new window
7
+ keys: "⌘N"
8
+ - action: Close the current tab
9
+ keys: "⌘W"
10
+ - action: Next / previous tab
11
+ keys: "⌃⇥, ⌃⇧⇥"
12
+ tags: [switch tab]
13
+ - action: Clear the screen and scrollback
14
+ keys: "⌘K"
15
+ tags: [clear]
16
+ - action: Find in the terminal
17
+ keys: "⌘F"
18
+ - action: Increase / decrease font size
19
+ keys: "⌘+, ⌘-"
20
+ - action: Jump to start / end of line
21
+ keys: "⌃A, ⌃E"
22
+ tags: [cursor, beginning, end, readline]
23
+ - action: Move back / forward one word
24
+ keys: "⌥B, ⌥F"
25
+ tags: [cursor, word, readline]
26
+ - action: Delete to end of line
27
+ keys: "⌃K"
28
+ tags: [readline, kill]
29
+ - action: Delete the previous word
30
+ keys: "⌃W"
31
+ tags: [readline]
32
+ - action: Clear the screen (keep the current line)
33
+ keys: "⌃L"
34
+ tags: [readline]
35
+ - action: Reverse-search command history
36
+ keys: "⌃R"
37
+ tags: [history, search, readline]
38
+ - action: Cancel the current command
39
+ keys: "⌃C"
40
+ tags: [interrupt]
41
+ - action: Delete to the start of the line
42
+ keys: "⌃U"
43
+ tags: [readline, kill]
44
+ - action: Delete the character under the cursor (or EOF / logout)
45
+ keys: "⌃D"
46
+ notes: On an empty line, ⌃D signals end-of-input and can close the shell.
47
+ tags: [readline, delete, eof]
48
+ - action: Paste the last killed (deleted) text
49
+ keys: "⌃Y"
50
+ tags: [readline, yank, paste]
51
+ - action: Delete the next word
52
+ keys: "⌥D"
53
+ tags: [readline, word]
54
+ - action: Swap (transpose) the last two characters
55
+ keys: "⌃T"
56
+ tags: [readline, transpose]
57
+ - action: Suspend the running program, then resume it
58
+ keys: "⌃Z"
59
+ command: fg
60
+ notes: ⌃Z suspends to the background; type `fg` to resume (or `bg` to run it in the background).
61
+ tags: [suspend, background, job]
62
+ - action: Repeat the last command / reuse its last argument
63
+ keys: "!!, !$"
64
+ notes: >-
65
+ !! re-runs the previous command (e.g. `sudo !!`); !$ expands to the last
66
+ argument of the previous command.
67
+ tags: [history, tip, bang]
68
+ - action: Edit the current command line in your $EDITOR
69
+ keys: "⌃X⌃E"
70
+ notes: Opens the half-typed command in $EDITOR; save and quit to run it (bash/zsh).
71
+ tags: [readline, editor, tip]
@@ -0,0 +1,44 @@
1
+ app: Text editing
2
+ entries:
3
+ - action: Move one word left / right
4
+ keys: "⌥←, ⌥→"
5
+ tags: [word, navigation, cursor]
6
+ - action: Move to start / end of the line
7
+ keys: "⌘←, ⌘→"
8
+ tags: [line, home, end]
9
+ - action: Move to start / end of the document
10
+ keys: "⌘↑, ⌘↓"
11
+ tags: [top, bottom, document]
12
+ - action: Delete the previous word
13
+ keys: "⌥⌫"
14
+ tags: [delete, word, backspace]
15
+ - action: Delete to the start of the line
16
+ keys: "⌘⌫"
17
+ tags: [delete, line]
18
+ - action: Delete the next word (forward)
19
+ keys: "fn⌥⌫"
20
+ notes: ⌫ is Delete (Backspace); fn turns it into Forward Delete (⌦).
21
+ tags: [delete, word, forward]
22
+ - action: Select one word left / right
23
+ keys: "⌥⇧←, ⌥⇧→"
24
+ tags: [select, word]
25
+ - action: Select to start / end of the line
26
+ keys: "⌘⇧←, ⌘⇧→"
27
+ tags: [select, line]
28
+ - action: Select to start / end of the document
29
+ keys: "⌘⇧↑, ⌘⇧↓"
30
+ tags: [select, document]
31
+ - action: Select all
32
+ keys: "⌘A"
33
+ tags: [select, all]
34
+ - action: Undo / redo
35
+ keys: "⌘Z, ⇧⌘Z"
36
+ tags: [undo, redo]
37
+ - action: Jump to start / end of line (emacs-style)
38
+ keys: "⌃A, ⌃E"
39
+ notes: Works in most macOS native (Cocoa) text fields, not every app.
40
+ tags: [cursor, readline]
41
+ - action: Delete to end of line (emacs-style)
42
+ keys: "⌃K"
43
+ notes: Cocoa text fields; pairs with ⌃Y to paste it back.
44
+ tags: [delete, kill]
package/seed/tmux.yaml ADDED
@@ -0,0 +1,61 @@
1
+ app: tmux
2
+ entries:
3
+ - action: Prefix key (press before each command below)
4
+ keys: "⌃B"
5
+ notes: Default tmux prefix.
6
+ - action: Create a new window
7
+ keys: "⌃B, c"
8
+ - action: Next / previous window
9
+ keys: "⌃B, n"
10
+ notes: n next, p previous.
11
+ - action: Go to window by number
12
+ keys: "⌃B, 0"
13
+ notes: 0–9.
14
+ - action: Split the pane left / right
15
+ keys: "⌃B, %"
16
+ tags: [vertical split]
17
+ - action: Split the pane top / bottom
18
+ keys: '⌃B, "'
19
+ tags: [horizontal split]
20
+ - action: Move between panes
21
+ keys: "⌃B, →"
22
+ notes: Use ↑ ↓ ← → to move.
23
+ - action: Toggle pane zoom
24
+ keys: "⌃B, z"
25
+ tags: [fullscreen pane, maximize]
26
+ - action: Close the current pane
27
+ keys: "⌃B, x"
28
+ - action: Detach from the session
29
+ keys: "⌃B, d"
30
+ - action: List sessions
31
+ keys: "⌃B, s"
32
+ - action: Rename the current window
33
+ keys: "⌃B, ,"
34
+ - action: Enter copy mode (scroll)
35
+ keys: "⌃B, ["
36
+ tags: [scroll, copy]
37
+ - action: Open the command prompt
38
+ keys: "⌃B, :"
39
+ - action: Reload the tmux config
40
+ command: tmux source-file ~/.tmux.conf
41
+ tags: [reload, config]
42
+ - action: Break the current pane into its own window
43
+ keys: "⌃B, !"
44
+ tags: [pane, window, break]
45
+ - action: Swap the current pane left / right
46
+ keys: "⌃B, {"
47
+ notes: "{ swaps with the previous pane; } swaps with the next."
48
+ tags: [pane, swap, move]
49
+ - action: Cycle through the preset pane layouts
50
+ keys: "⌃B, ␣"
51
+ tags: [layout, arrange]
52
+ - action: Show pane numbers (then press one to jump)
53
+ keys: "⌃B, q"
54
+ tags: [pane, jump]
55
+ - action: Kill the current window
56
+ keys: "⌃B, &"
57
+ notes: Asks for confirmation.
58
+ tags: [window, close, kill]
59
+ - action: Paste the most recent copy-mode buffer
60
+ keys: "⌃B, ]"
61
+ tags: [paste, copy]
package/seed/vim.yaml ADDED
@@ -0,0 +1,66 @@
1
+ app: Vim
2
+ entries:
3
+ - action: Enter Insert mode (before / after the cursor)
4
+ keys: "i, a"
5
+ tags: [insert, edit]
6
+ - action: Return to Normal mode
7
+ keys: "⎋"
8
+ tags: [normal, escape]
9
+ - action: Enter Visual / Visual-Block mode
10
+ keys: "v, ⌃V"
11
+ tags: [select, block]
12
+ - action: Save the file
13
+ keys: ":w⏎"
14
+ tags: [write, save]
15
+ - action: Save and quit
16
+ keys: ":wq⏎"
17
+ notes: ZZ does the same from Normal mode.
18
+ tags: [save, quit, exit]
19
+ - action: Quit without saving
20
+ keys: ":q!⏎"
21
+ tags: [quit, discard, exit]
22
+ - action: Go to start / end of line
23
+ keys: "0, $"
24
+ notes: "^ jumps to the first non-blank character."
25
+ tags: [line, motion]
26
+ - action: Go to top / bottom of file
27
+ keys: "gg, G"
28
+ notes: '":42⏎" jumps to line 42.'
29
+ tags: [motion, goto]
30
+ - action: Move word forward / back
31
+ keys: "w, b"
32
+ tags: [word, motion]
33
+ - action: Jump to the matching bracket
34
+ keys: "%"
35
+ tags: [bracket, paren, match]
36
+ - action: Delete / yank (copy) the current line
37
+ keys: "dd, yy"
38
+ tags: [delete, copy, cut]
39
+ - action: Paste after / before the cursor
40
+ keys: "p, P"
41
+ tags: [paste]
42
+ - action: Delete the character under the cursor
43
+ keys: "x"
44
+ tags: [delete, char]
45
+ - action: Undo / redo
46
+ keys: "u, ⌃R"
47
+ tags: [undo, redo]
48
+ - action: Repeat the last change
49
+ keys: "."
50
+ tags: [repeat, dot]
51
+ - action: Search, then next / previous match
52
+ keys: "/text⏎, n, N"
53
+ tags: [search, find]
54
+ - action: Replace every match in the file
55
+ keys: ":%s/old/new/g⏎"
56
+ notes: Add c at the end (…/gc) to confirm each replacement.
57
+ tags: [replace, substitute, tip]
58
+ - action: Change inside word / quotes / parens
59
+ keys: 'ciw, ci", ci('
60
+ notes: >-
61
+ Text objects — "change inner". Swap c for d to delete instead (diw, di").
62
+ Works with [], {}, <> too.
63
+ tags: [text object, tip, change]
64
+ - action: Indent / outdent the line or selection
65
+ keys: ">>, <<"
66
+ tags: [indent]
@@ -0,0 +1,65 @@
1
+ app: VS Code
2
+ entries:
3
+ - action: Show the Command Palette
4
+ keys: "⌘⇧P"
5
+ tags: [commands, palette]
6
+ - action: Quick Open a file by name
7
+ keys: "⌘P"
8
+ tags: [open file, goto]
9
+ - action: Toggle the integrated terminal
10
+ keys: "⌃`"
11
+ tags: [terminal]
12
+ - action: Toggle the sidebar
13
+ keys: "⌘B"
14
+ - action: Add a cursor below
15
+ keys: "⌘⌥↓"
16
+ tags: [multi cursor]
17
+ - action: Select the next occurrence of the selection
18
+ keys: "⌘D"
19
+ tags: [multi cursor, select]
20
+ - action: Go to a line number
21
+ keys: "⌃G"
22
+ - action: Go to definition
23
+ keys: "F12"
24
+ - action: Rename the symbol
25
+ keys: "F2"
26
+ tags: [refactor]
27
+ - action: Find / Replace in the file
28
+ keys: "⌘F, ⌥⌘F"
29
+ - action: Find in all files
30
+ keys: "⌘⇧F"
31
+ tags: [search]
32
+ - action: Toggle line comment
33
+ keys: "⌘/"
34
+ - action: Move the current line up / down
35
+ keys: "⌥↑, ⌥↓"
36
+ - action: Copy the current line up / down
37
+ keys: "⇧⌥↑, ⇧⌥↓"
38
+ - action: Format the document
39
+ keys: "⇧⌥F"
40
+ - action: Split the editor
41
+ keys: "⌘\\"
42
+ - action: Trigger a Quick Fix
43
+ keys: "⌘."
44
+ tags: [lightbulb]
45
+ - action: Toggle Zen Mode
46
+ keys: "⌘K, Z"
47
+ tags: [focus, fullscreen]
48
+ - action: Delete the current line
49
+ keys: "⌘⇧K"
50
+ tags: [delete line]
51
+ - action: Insert a line below / above
52
+ keys: "⌘⏎, ⇧⌘⏎"
53
+ tags: [new line]
54
+ - action: Select all occurrences of the current selection
55
+ keys: "⌘⇧L"
56
+ tags: [multi cursor, select]
57
+ - action: Go to the next problem (error / warning)
58
+ keys: "F8"
59
+ tags: [errors, diagnostics]
60
+ - action: Navigate back / forward (cursor history)
61
+ keys: "⌃-, ⌃⇧-"
62
+ tags: [navigate, history, goto]
63
+ - action: Open Keyboard Shortcuts
64
+ keys: "⌘K, ⌘S"
65
+ tags: [keybindings, shortcuts]