@devkitvault/recall 1.2.7 → 1.2.10

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
@@ -1,8 +1,10 @@
1
1
  # @devkitvault/recall
2
2
 
3
- **recall** is a CLI for the commands you keep losing. Ask in English; it prints a suggestion and does not run it. If you already saved that command, it can come from your vault.
3
+ **recall** is a CLI command vault for your terminal. Save the commands you keep losing, search them, run them by name, and sync when you want cloud backup.
4
4
 
5
- Works with [recall.devkitvault.com](https://recall.devkitvault.com) and the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=devkitvault.recall-cmd).
5
+ Product site and docs: [recall.devkitvault.com](https://recall.devkitvault.com)
6
+
7
+ Open source: [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli)
6
8
 
7
9
  ## Requirements
8
10
 
@@ -14,68 +16,61 @@ Works with [recall.devkitvault.com](https://recall.devkitvault.com) and the [VS
14
16
  npm install -g @devkitvault/recall
15
17
  ```
16
18
 
17
- `rec` is the same command as `recall`.
19
+ `rec` is the same binary as `recall`.
18
20
 
19
21
  ```sh
20
22
  recall --version
21
23
  recall doctor
22
24
  ```
23
25
 
24
- ## Quick start
26
+ ## Quick start (local vault)
27
+
28
+ Local save / list / search / run work **without an account**. Auth is only needed for **sync**, **Ask** (Pro/Team), and team features.
25
29
 
26
30
  ```sh
27
- recall auth login
28
- recall "find the 10 biggest files in this directory"
29
- rec "that docker command from last week"
30
- recall
31
+ recall save "docker compose up -d" --name docker --tags docker
32
+ recall save --last -n useful -t docker
33
+ recall import history
34
+ recall list
35
+ recall search docker
36
+ recall run docker
31
37
  ```
32
38
 
33
- No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
39
+ `recall import history` reads your shell history file (zsh/bash/PowerShell), lets you pick commands, and saves them locally. It does **not** install a keylogger or live hook.
40
+
41
+ `recall save --last` (or `-L`) reads the last line from zsh / bash / PowerShell history. Not supported in Windows `cmd.exe`. On bash, if the command is missing: `history -a && recall save --last`.
34
42
 
35
43
  ```sh
36
44
  recall playbook save deploy "docker compose up -d" "pnpm migrate"
37
45
  recall playbook run deploy --dry-run
38
46
  ```
39
47
 
40
- `recall playbook run` executes steps. Ask never does.
41
-
42
- Ask prints a suggestion (`source` is `vault`, `cache`, or `recall agent` for a generated answer). Copy or save it yourself.
43
-
44
- ## Ask in the terminal
45
-
46
- These GIFs print a command. None of them execute.
47
-
48
- **`rec "start the project"`** (`source: project`)
48
+ Local playbooks follow the same idea when available; cloud playbooks need Pro. `recall playbook run` executes steps. Ask never does.
49
49
 
50
- ![rec start the project](https://recall.devkitvault.com/og/ask-start.gif)
50
+ ## Ask (Pro / Team)
51
51
 
52
- **`rec "start postgres and run migrations"`** (`source: vault`)
53
-
54
- ![rec start postgres and run migrations](https://recall.devkitvault.com/og/ask-playbook.gif)
55
-
56
- **`rec "free the process using port 3000"`** (`source: recall agent`)
57
-
58
- ![rec free the process using port 3000](https://recall.devkitvault.com/og/ask-port.gif)
59
-
60
- **`rec "that docker command from last week"`** (`source: vault`)
61
-
62
- ![rec that docker command from last week](https://recall.devkitvault.com/og/ask-vault.gif)
52
+ Natural-language Ask prints a suggestion and never runs it. Requires a Pro or Team plan.
63
53
 
64
54
  ```sh
65
- recall save "docker compose up -d" --name docker --tags docker
66
- recall run docker
55
+ recall auth login
56
+ recall "find the 10 biggest files in this directory"
57
+ rec "that docker command from last week"
67
58
  ```
68
59
 
69
- Run `recall --help` for the full command list. Free plans include **20 AI asks / month**; Pro includes **500**.
60
+ No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
61
+
62
+ ## Auth & sync (Pro / Team)
70
63
 
71
- ## Authentication
64
+ Account + Pro/Team syncs your **local** commands to the cloud and unlocks Ask.
72
65
 
73
66
  ```sh
74
67
  recall auth login
75
- recall auth logout
68
+ recall sync # pull then push
69
+ recall sync --pull # cloud → local
70
+ recall sync --push # local-only → cloud
71
+ recall sync --status
76
72
  recall whoami
77
73
  ```
78
-
79
74
  ## Prefer a standalone binary?
80
75
 
81
76
  - **macOS / Linux:** `curl -fsSL https://devkitvault.com/recall/install.sh | sh`
@@ -96,17 +91,18 @@ recall doctor
96
91
  recall whoami
97
92
  ```
98
93
 
99
- If something still fails, open an issue on [GitHub](https://github.com/devkitvault/recall/issues) or visit [recall.devkitvault.com](https://recall.devkitvault.com).
94
+ If something still fails, open an issue on [GitHub](https://github.com/devkitvault/recall-cli/issues) or visit [recall.devkitvault.com](https://recall.devkitvault.com).
100
95
 
101
96
  ## Links
102
97
 
103
98
  | | |
104
99
  | --------- | ----------------------------------------------------------------------------------------- |
105
- | Website | [devkitvault.com/recall](https://devkitvault.com) |
100
+ | Website | [recall.devkitvault.com](https://recall.devkitvault.com) |
106
101
  | Docs | [recall.devkitvault.com/docs](https://recall.devkitvault.com/docs) |
107
102
  | Dashboard | [recall.devkitvault.com](https://recall.devkitvault.com) |
103
+ | Source | [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli) |
108
104
  | VS Code | [Marketplace](https://marketplace.visualstudio.com/items?itemName=devkitvault.recall-cmd) |
109
- | Issues | [github.com/devkitvault/recall](https://github.com/devkitvault/recall/issues) |
105
+ | Issues | [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli/issues) |
110
106
 
111
107
  ## License
112
108
 
@@ -15,6 +15,7 @@ const commander_1 = require("commander");
15
15
  const inquirer_1 = __importDefault(require("inquirer"));
16
16
  const api_1 = require("../lib/api");
17
17
  const auth_1 = require("../lib/auth");
18
+ const local_vault_1 = require("../lib/local-vault");
18
19
  const events_1 = require("../lib/events");
19
20
  const project_1 = require("../lib/project");
20
21
  const runtime_1 = require("../lib/runtime");
@@ -91,11 +92,23 @@ async function promptSaveMeta(existingName) {
91
92
  };
92
93
  }
93
94
  async function saveSuggestion(token, command, name, source, risk, tags) {
94
- await api_1.ApiClient.post('/commands', {
95
+ const local = (0, local_vault_1.saveLocalCommand)({
95
96
  command,
96
97
  name: name?.trim() || undefined,
97
98
  tags,
98
- }, token);
99
+ });
100
+ try {
101
+ const saved = await api_1.ApiClient.post('/commands', {
102
+ command,
103
+ name: name?.trim() || undefined,
104
+ tags,
105
+ }, token);
106
+ if (saved?.id)
107
+ (0, local_vault_1.attachCloudId)(local.id, saved.id);
108
+ }
109
+ catch {
110
+ // Local save succeeded; cloud may require Pro
111
+ }
99
112
  await (0, events_1.track)('ai_accepted', { source, risk, saved: true });
100
113
  console.log(chalk_1.default.green(`\n ${(0, tags_1.formatSavedMessage)(name, tags)}\n`));
101
114
  }
@@ -7,38 +7,27 @@ exports.deleteCommand = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const commander_1 = require("commander");
9
9
  const inquirer_1 = __importDefault(require("inquirer"));
10
- const ora_1 = __importDefault(require("ora"));
11
- const api_1 = require("../lib/api");
12
- const auth_1 = require("../lib/auth");
10
+ const local_vault_1 = require("../lib/local-vault");
13
11
  exports.deleteCommand = new commander_1.Command('delete')
14
- .description('Delete a saved command by name')
12
+ .description('Delete a command from your local vault')
15
13
  .argument('<name>', 'Command name to delete')
16
14
  .option('-f, --force', 'Skip confirmation prompt')
17
15
  .action(async (name, opts) => {
18
- const token = await (0, auth_1.requireAuth)();
19
- // First look up the command to confirm it exists
20
- const spinner = (0, ora_1.default)('Looking up...').start();
21
- let cmd;
22
- try {
23
- cmd = await api_1.ApiClient.get(`/commands/by-name/${encodeURIComponent(name)}`, token);
24
- spinner.stop();
25
- }
26
- catch {
27
- spinner.fail(chalk_1.default.red(`Command "${name}" not found`));
16
+ const cmd = (0, local_vault_1.findLocalByName)(name);
17
+ if (!cmd) {
18
+ console.log(chalk_1.default.red(`\n Command "${name}" not found in local vault.\n`));
28
19
  process.exit(1);
29
20
  }
30
- // Show what will be deleted
31
21
  console.log();
32
22
  console.log(` ${chalk_1.default.dim('Name:')} ${chalk_1.default.white(cmd.name ?? '—')}`);
33
23
  console.log(` ${chalk_1.default.dim('Command:')} ${chalk_1.default.white(cmd.command)}`);
34
24
  console.log();
35
- // Confirm unless --force
36
25
  if (!opts.force) {
37
26
  const { confirm } = await inquirer_1.default.prompt([
38
27
  {
39
28
  type: 'confirm',
40
29
  name: 'confirm',
41
- message: chalk_1.default.red(`Delete "${name}"?`),
30
+ message: chalk_1.default.red(`Delete "${name}" from local vault?`),
42
31
  default: false,
43
32
  },
44
33
  ]);
@@ -47,12 +36,7 @@ exports.deleteCommand = new commander_1.Command('delete')
47
36
  return;
48
37
  }
49
38
  }
50
- const deleteSpinner = (0, ora_1.default)('Deleting...').start();
51
- try {
52
- await api_1.ApiClient.delete(`/commands/${cmd.id}`, token);
53
- deleteSpinner.succeed(chalk_1.default.green(`Deleted "${name}"`));
54
- }
55
- catch {
56
- deleteSpinner.fail(chalk_1.default.red('Failed to delete'));
57
- }
39
+ (0, local_vault_1.deleteLocalByName)(name);
40
+ console.log(chalk_1.default.green(` Deleted "${name}" from local vault.\n`));
41
+ console.log(chalk_1.default.dim(' Cloud copy (if any) is unchanged until you sync.\n'));
58
42
  });
@@ -7,19 +7,22 @@ exports.importCommand = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const commander_1 = require("commander");
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const ora_1 = __importDefault(require("ora"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
11
  const path_1 = __importDefault(require("path"));
12
- const api_1 = require("../lib/api");
13
- const auth_1 = require("../lib/auth");
12
+ const local_vault_1 = require("../lib/local-vault");
13
+ const tags_1 = require("../lib/tags");
14
+ const shell_history_1 = require("../lib/shell-history");
14
15
  function parseJSON(content) {
15
16
  const data = JSON.parse(content);
16
17
  if (!Array.isArray(data))
17
18
  throw new Error('JSON must be an array of commands');
18
19
  return data.map((item) => ({
19
- command: item.command,
20
- name: item.name,
21
- tags: item.tags ?? [],
22
- }));
20
+ command: String(item.command ?? ''),
21
+ name: typeof item.name === 'string' ? item.name : undefined,
22
+ tags: Array.isArray(item.tags)
23
+ ? item.tags.filter((t) => typeof t === 'string')
24
+ : undefined,
25
+ })).filter((e) => e.command.trim());
23
26
  }
24
27
  function parseSh(content) {
25
28
  const lines = content.split('\n');
@@ -28,14 +31,13 @@ function parseSh(content) {
28
31
  let currentTags = [];
29
32
  for (const raw of lines) {
30
33
  const line = raw.trim();
31
- // skip empty lines and shebang
32
34
  if (!line || line.startsWith('#!'))
33
35
  continue;
34
36
  if (line.startsWith('# tags:')) {
35
37
  currentTags = line
36
38
  .replace('# tags:', '')
37
39
  .split(',')
38
- .map(t => t.trim())
40
+ .map((t) => t.trim())
39
41
  .filter(Boolean);
40
42
  continue;
41
43
  }
@@ -43,24 +45,59 @@ function parseSh(content) {
43
45
  currentName = line.replace('#', '').trim();
44
46
  continue;
45
47
  }
46
- // it's a command
47
48
  entries.push({
48
49
  command: line,
49
50
  name: currentName,
50
- tags: currentTags,
51
+ tags: currentTags.length ? currentTags : undefined,
51
52
  });
52
- // reset
53
53
  currentName = undefined;
54
54
  currentTags = [];
55
55
  }
56
56
  return entries;
57
57
  }
58
- exports.importCommand = new commander_1.Command('import')
59
- .description('Import commands from a JSON or shell script file')
60
- .argument('<file>', 'Path to the file to import')
61
- .option('-s, --skip-duplicates', 'Skip commands that already exist by name')
62
- .action(async (file, opts) => {
63
- const token = await (0, auth_1.requireAuth)();
58
+ function commandAlreadyLocal(command, name) {
59
+ if (name && (0, local_vault_1.findLocalByName)(name))
60
+ return true;
61
+ return (0, local_vault_1.listLocalCommands)().some((c) => c.command === command);
62
+ }
63
+ function suggestName(command, used) {
64
+ const base = command
65
+ .replace(/[^a-zA-Z0-9]+/g, '-')
66
+ .replace(/^-|-$/g, '')
67
+ .toLowerCase()
68
+ .slice(0, 32) || 'cmd';
69
+ let name = base;
70
+ let n = 2;
71
+ while (used.has(name) || (0, local_vault_1.findLocalByName)(name)) {
72
+ name = `${base.slice(0, 28)}-${n}`;
73
+ n++;
74
+ }
75
+ used.add(name);
76
+ return name;
77
+ }
78
+ async function importEntries(entries, opts) {
79
+ let imported = 0;
80
+ let skipped = 0;
81
+ const usedNames = new Set();
82
+ for (const entry of entries) {
83
+ if (opts.skipDuplicates && commandAlreadyLocal(entry.command, entry.name)) {
84
+ skipped++;
85
+ continue;
86
+ }
87
+ const name = entry.name
88
+ ?? (opts.nameThem ? suggestName(entry.command, usedNames) : undefined);
89
+ if (name)
90
+ usedNames.add(name);
91
+ (0, local_vault_1.saveLocalCommand)({
92
+ command: entry.command,
93
+ name,
94
+ tags: entry.tags,
95
+ });
96
+ imported++;
97
+ }
98
+ return { imported, skipped };
99
+ }
100
+ async function importFromFile(file, opts) {
64
101
  const filePath = path_1.default.resolve(file);
65
102
  if (!fs_1.default.existsSync(filePath)) {
66
103
  console.error(chalk_1.default.red(`\n File not found: ${filePath}\n`));
@@ -77,7 +114,6 @@ exports.importCommand = new commander_1.Command('import')
77
114
  entries = parseSh(content);
78
115
  }
79
116
  else {
80
- // try JSON first, then sh
81
117
  try {
82
118
  entries = parseJSON(content);
83
119
  }
@@ -87,40 +123,114 @@ exports.importCommand = new commander_1.Command('import')
87
123
  }
88
124
  }
89
125
  catch (err) {
90
- console.error(chalk_1.default.red(`\n Failed to parse file: ${err.message}\n`));
126
+ const message = err instanceof Error ? err.message : String(err);
127
+ console.error(chalk_1.default.red(`\n Failed to parse file: ${message}\n`));
91
128
  process.exit(1);
92
129
  }
93
130
  if (!entries.length) {
94
131
  console.log(chalk_1.default.dim('\n No commands found in file.\n'));
95
132
  return;
96
133
  }
97
- console.log(chalk_1.default.dim(`\n Found ${entries.length} command${entries.length === 1 ? '' : 's'} to import\n`));
98
- let imported = 0;
99
- let skipped = 0;
100
- let failed = 0;
101
- for (const entry of entries) {
102
- const spinner = (0, ora_1.default)(`Importing: ${chalk_1.default.white(entry.name ?? entry.command.slice(0, 40))}`).start();
103
- try {
104
- await api_1.ApiClient.post('/commands', {
105
- command: entry.command,
106
- name: entry.name,
107
- tags: entry.tags,
108
- }, token);
109
- spinner.succeed(chalk_1.default.green(`Imported: ${entry.name ?? entry.command.slice(0, 40)}`));
110
- imported++;
111
- }
112
- catch (err) {
113
- if (opts.skipDuplicates && err.status === 409) {
114
- spinner.warn(chalk_1.default.yellow(`Skipped: ${entry.name ?? entry.command.slice(0, 40)}`));
115
- skipped++;
116
- }
117
- else {
118
- spinner.fail(chalk_1.default.red(`Failed: ${entry.name ?? entry.command.slice(0, 40)}`));
119
- failed++;
120
- }
134
+ console.log(chalk_1.default.dim(`\n Found ${entries.length} command${entries.length === 1 ? '' : 's'} in file\n`));
135
+ const { imported, skipped } = await importEntries(entries, {
136
+ skipDuplicates: opts.skipDuplicates,
137
+ nameThem: false,
138
+ });
139
+ console.log(` ${chalk_1.default.green(`${imported} imported`)} ${chalk_1.default.yellow(`${skipped} skipped`)}`);
140
+ console.log(chalk_1.default.dim(` Vault: ${local_vault_1.VAULT_FILE}\n`));
141
+ }
142
+ exports.importCommand = new commander_1.Command('import')
143
+ .description('Import commands into your local vault')
144
+ .argument('[file]', 'JSON or shell script to import (or use: recall import history)')
145
+ .option('-s, --skip-duplicates', 'Skip commands that already exist locally', true)
146
+ .addHelpText('after', `
147
+ Examples:
148
+ $ recall import history
149
+ $ recall import history -n 20 -y
150
+ $ recall import ./backup.json
151
+ `)
152
+ .action(async (file, opts) => {
153
+ if (!file) {
154
+ console.log(chalk_1.default.dim('\n Usage:'));
155
+ console.log(chalk_1.default.dim(' recall import history # from shell history (curated)'));
156
+ console.log(chalk_1.default.dim(' recall import <file> # from JSON or .sh\n'));
157
+ process.exit(1);
158
+ }
159
+ await importFromFile(file, opts);
160
+ });
161
+ exports.importCommand
162
+ .command('history')
163
+ .description('Import curated commands from shell history into the local vault (not live capture)')
164
+ .option('-n, --limit <count>', 'Max unique recent commands to consider', '40')
165
+ .option('-y, --yes', 'Import all candidates without prompting')
166
+ .option('-i, --interactive', 'Choose which commands to import (default unless --yes)')
167
+ .option('--dry-run', 'Show candidates only; do not write')
168
+ .option('-t, --tags <tags>', 'Tags applied to imported commands')
169
+ .option('--min-length <n>', 'Skip commands shorter than this', '3')
170
+ .option('--no-names', 'Do not auto-generate names')
171
+ .option('--no-skip-duplicates', 'Import even if the same command text already exists')
172
+ .action(async (opts) => {
173
+ const limit = Math.max(1, parseInt(String(opts.limit), 10) || 40);
174
+ const minLength = Math.max(1, parseInt(String(opts.minLength), 10) || 3);
175
+ const tags = (0, tags_1.parseTags)(opts.tags);
176
+ let commands;
177
+ let source;
178
+ let shell;
179
+ try {
180
+ const result = (0, shell_history_1.readHistoryCommands)({ limit, minLength });
181
+ commands = result.commands;
182
+ source = result.source;
183
+ shell = result.shell;
184
+ }
185
+ catch (err) {
186
+ if (err instanceof shell_history_1.LastCommandError) {
187
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
188
+ process.exit(1);
121
189
  }
190
+ throw err;
122
191
  }
123
192
  console.log();
124
- console.log(` ${chalk_1.default.green(`${imported} imported`)} ${chalk_1.default.yellow(`${skipped} skipped`)} ${chalk_1.default.red(`${failed} failed`)}`);
193
+ console.log(chalk_1.default.dim(` Shell: ${shell} · ${source}`));
194
+ console.log(chalk_1.default.dim(` ${commands.length} unique candidate${commands.length === 1 ? '' : 's'} (newest first)\n`));
195
+ let selected = commands;
196
+ const interactive = !opts.yes && (opts.interactive || !opts.dryRun);
197
+ if (opts.yes) {
198
+ selected = commands;
199
+ }
200
+ else if (interactive && !opts.dryRun) {
201
+ const { picked } = await inquirer_1.default.prompt([
202
+ {
203
+ type: 'checkbox',
204
+ name: 'picked',
205
+ message: 'Select commands to save locally',
206
+ pageSize: 16,
207
+ choices: commands.map((c) => ({
208
+ name: c.length > 80 ? `${c.slice(0, 77)}…` : c,
209
+ value: c,
210
+ checked: false,
211
+ })),
212
+ },
213
+ ]);
214
+ selected = picked;
215
+ if (!selected.length) {
216
+ console.log(chalk_1.default.dim('\n Nothing selected.\n'));
217
+ return;
218
+ }
219
+ }
220
+ if (opts.dryRun) {
221
+ for (const [i, c] of selected.entries()) {
222
+ console.log(` ${chalk_1.default.dim(String(i + 1).padStart(2))}. ${c}`);
223
+ }
224
+ console.log(chalk_1.default.dim(`\n Dry run — nothing written. Drop --dry-run to import.\n`));
225
+ return;
226
+ }
227
+ const entries = selected.map((command) => ({ command, tags }));
228
+ const { imported, skipped } = await importEntries(entries, {
229
+ skipDuplicates: opts.skipDuplicates !== false,
230
+ nameThem: opts.names !== false,
231
+ });
125
232
  console.log();
233
+ console.log(` ${chalk_1.default.green(`${imported} imported`)} ${chalk_1.default.yellow(`${skipped} skipped`)}`);
234
+ console.log(chalk_1.default.dim(` Vault: ${local_vault_1.VAULT_FILE}`));
235
+ console.log(chalk_1.default.dim(' Sync to cloud (Pro): recall sync\n'));
126
236
  });
@@ -7,63 +7,44 @@ exports.listCommand = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const cli_table3_1 = __importDefault(require("cli-table3"));
9
9
  const commander_1 = require("commander");
10
- const ora_1 = __importDefault(require("ora"));
11
- const api_1 = require("../lib/api");
12
- const auth_1 = require("../lib/auth");
10
+ const local_vault_1 = require("../lib/local-vault");
13
11
  exports.listCommand = new commander_1.Command('list')
14
- .description('List your saved commands')
12
+ .description('List commands in your local vault')
15
13
  .option('-t, --tag <tag>', 'Filter by tag')
16
- .option('-g, --group <group>', 'Filter by group (Pro)')
14
+ .option('-g, --group <group>', 'Filter by group (Pro cloud — not used locally)')
17
15
  .option('-s, --search <q>', 'Search by keyword')
18
16
  .option('-p, --pinned', 'Show pinned only')
19
17
  .action(async (opts) => {
20
- const token = await (0, auth_1.requireAuth)();
21
- const spinner = (0, ora_1.default)('Fetching...').start();
22
- try {
23
- const params = new URLSearchParams();
24
- if (opts.tag)
25
- params.set('tag', opts.tag);
26
- if (opts.group)
27
- params.set('group', opts.group);
28
- if (opts.search)
29
- params.set('search', opts.search);
30
- const { commands } = await api_1.ApiClient.get(`/commands?${params}`, token);
31
- spinner.stop();
32
- let result = commands;
33
- // Filter pinned only
34
- if (opts.pinned)
35
- result = result.filter((c) => c.pinned);
36
- // Sort: pinned first, then by createdAt
37
- result.sort((a, b) => {
38
- if (a.pinned && !b.pinned)
39
- return -1;
40
- if (!a.pinned && b.pinned)
41
- return 1;
42
- return 0;
43
- });
44
- if (!result.length) {
45
- console.log(chalk_1.default.dim('\n No commands found.\n'));
46
- return;
47
- }
48
- const table = new cli_table3_1.default({
49
- head: [chalk_1.default.cyan('Pin'), chalk_1.default.cyan('Name'), chalk_1.default.cyan('Command'), chalk_1.default.cyan('Tags')],
50
- style: { head: [], border: ['grey'] },
51
- colWidths: [5, 18, 44, 18],
52
- wordWrap: true,
53
- });
54
- for (const cmd of result) {
55
- table.push([
56
- cmd.pinned ? chalk_1.default.yellow('★') : chalk_1.default.dim('·'),
57
- cmd.name ?? chalk_1.default.dim('—'),
58
- cmd.command.length > 42
59
- ? cmd.command.slice(0, 42) + '…'
60
- : cmd.command,
61
- (cmd.tags ?? []).join(', ') || chalk_1.default.dim('—'),
62
- ]);
63
- }
64
- console.log(table.toString());
18
+ if (opts.group) {
19
+ console.log(chalk_1.default.dim(' Local vault has no groups. Showing all local commands.\n'));
65
20
  }
66
- catch {
67
- spinner.fail(chalk_1.default.red('Failed to fetch commands'));
21
+ const result = (0, local_vault_1.listLocalCommands)({
22
+ tag: opts.tag,
23
+ search: opts.search,
24
+ pinned: opts.pinned,
25
+ });
26
+ if (!result.length) {
27
+ console.log(chalk_1.default.dim('\n No commands in local vault.'));
28
+ console.log(chalk_1.default.dim(` Save one: recall save "echo hi" -n hi`));
29
+ console.log(chalk_1.default.dim(` File: ${local_vault_1.VAULT_FILE}\n`));
30
+ return;
68
31
  }
32
+ const table = new cli_table3_1.default({
33
+ head: [chalk_1.default.cyan('Pin'), chalk_1.default.cyan('Name'), chalk_1.default.cyan('Command'), chalk_1.default.cyan('Tags')],
34
+ style: { head: [], border: ['grey'] },
35
+ colWidths: [5, 18, 44, 18],
36
+ wordWrap: true,
37
+ });
38
+ for (const cmd of result) {
39
+ table.push([
40
+ cmd.pinned ? chalk_1.default.yellow('★') : chalk_1.default.dim('·'),
41
+ cmd.name ?? chalk_1.default.dim('—'),
42
+ cmd.command.length > 42
43
+ ? cmd.command.slice(0, 42) + '…'
44
+ : cmd.command,
45
+ (cmd.tags ?? []).join(', ') || chalk_1.default.dim('—'),
46
+ ]);
47
+ }
48
+ console.log(table.toString());
49
+ console.log(chalk_1.default.dim(`\n ${result.length} local · ${local_vault_1.VAULT_FILE}\n`));
69
50
  });
@@ -6,25 +6,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.pinCommand = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const commander_1 = require("commander");
9
- const ora_1 = __importDefault(require("ora"));
10
- const api_1 = require("../lib/api");
11
- const auth_1 = require("../lib/auth");
9
+ const local_vault_1 = require("../lib/local-vault");
12
10
  exports.pinCommand = new commander_1.Command('pin')
13
- .description('Pin or unpin a command to the top of your list')
11
+ .description('Pin or unpin a local command')
14
12
  .argument('<name>', 'Command name')
15
13
  .option('-u, --unpin', 'Unpin instead of pin')
16
14
  .action(async (name, opts) => {
17
- const token = await (0, auth_1.requireAuth)();
18
- const spinner = (0, ora_1.default)('Looking up...').start();
19
- try {
20
- const cmd = await api_1.ApiClient.get(`/commands/by-name/${encodeURIComponent(name)}`, token);
21
- const pinned = !opts.unpin;
22
- await api_1.ApiClient.patch(`/commands/${cmd.id}/pin`, { pinned }, token);
23
- spinner.succeed(pinned
24
- ? chalk_1.default.green(`Pinned "${name}" — it will appear at the top of recall list`)
25
- : chalk_1.default.green(`Unpinned "${name}"`));
26
- }
27
- catch {
28
- spinner.fail(chalk_1.default.red(`Command "${name}" not found`));
15
+ const pinned = !opts.unpin;
16
+ const cmd = (0, local_vault_1.setLocalPinned)(name, pinned);
17
+ if (!cmd) {
18
+ console.log(chalk_1.default.red(`\n Command "${name}" not found in local vault.\n`));
19
+ process.exit(1);
29
20
  }
21
+ console.log(pinned
22
+ ? chalk_1.default.green(` Pinned "${name}" — it will appear at the top of recall list\n`)
23
+ : chalk_1.default.green(` Unpinned "${name}"\n`));
30
24
  });