@devkitvault/recall 1.2.7 → 1.2.9

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,60 @@ 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
+ Free is **fully local** — save / list / search / run on your machine with **no account**. Cloud **sync** and **Ask** are Pro/Team.
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 list
34
+ recall search docker
35
+ recall run docker
31
36
  ```
32
37
 
33
- No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
38
+ Commands live in `~/.recall/commands.json`.
39
+
40
+ `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
41
 
35
42
  ```sh
36
43
  recall playbook save deploy "docker compose up -d" "pnpm migrate"
37
44
  recall playbook run deploy --dry-run
38
45
  ```
39
46
 
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`)
47
+ Local playbooks follow the same idea when available; cloud playbooks need Pro. `recall playbook run` executes steps. Ask never does.
49
48
 
50
- ![rec start the project](https://recall.devkitvault.com/og/ask-start.gif)
49
+ ## Ask (Pro / Team)
51
50
 
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)
51
+ Natural-language Ask prints a suggestion and never runs it. Requires a Pro or Team plan.
63
52
 
64
53
  ```sh
65
- recall save "docker compose up -d" --name docker --tags docker
66
- recall run docker
54
+ recall auth login
55
+ recall "find the 10 biggest files in this directory"
56
+ rec "that docker command from last week"
67
57
  ```
68
58
 
69
- Run `recall --help` for the full command list. Free plans include **20 AI asks / month**; Pro includes **500**.
59
+ No-args `recall` opens an Ask session (`Recall >`). `/save [name]` stores the last suggestion. `/exit` leaves.
60
+
61
+ ## Auth & sync (Pro / Team)
70
62
 
71
- ## Authentication
63
+ Account + Pro/Team syncs your **local** commands to the cloud and unlocks Ask.
72
64
 
73
65
  ```sh
74
66
  recall auth login
75
- recall auth logout
67
+ recall sync # pull then push
68
+ recall sync --pull # cloud → local
69
+ recall sync --push # local-only → cloud
70
+ recall sync --status
76
71
  recall whoami
77
72
  ```
78
-
79
73
  ## Prefer a standalone binary?
80
74
 
81
75
  - **macOS / Linux:** `curl -fsSL https://devkitvault.com/recall/install.sh | sh`
@@ -96,17 +90,18 @@ recall doctor
96
90
  recall whoami
97
91
  ```
98
92
 
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).
93
+ 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
94
 
101
95
  ## Links
102
96
 
103
97
  | | |
104
98
  | --------- | ----------------------------------------------------------------------------------------- |
105
- | Website | [devkitvault.com/recall](https://devkitvault.com) |
99
+ | Website | [recall.devkitvault.com](https://recall.devkitvault.com) |
106
100
  | Docs | [recall.devkitvault.com/docs](https://recall.devkitvault.com/docs) |
107
101
  | Dashboard | [recall.devkitvault.com](https://recall.devkitvault.com) |
102
+ | Source | [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli) |
108
103
  | VS Code | [Marketplace](https://marketplace.visualstudio.com/items?itemName=devkitvault.recall-cmd) |
109
- | Issues | [github.com/devkitvault/recall](https://github.com/devkitvault/recall/issues) |
104
+ | Issues | [github.com/devkitvault/recall-cli](https://github.com/devkitvault/recall-cli/issues) |
110
105
 
111
106
  ## License
112
107
 
@@ -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,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
  });
@@ -8,48 +8,34 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const child_process_1 = require("child_process");
9
9
  const commander_1 = require("commander");
10
10
  const inquirer_1 = __importDefault(require("inquirer"));
11
- const ora_1 = __importDefault(require("ora"));
12
- const api_1 = require("../lib/api");
13
- const auth_1 = require("../lib/auth");
11
+ const local_vault_1 = require("../lib/local-vault");
14
12
  exports.runCommand = new commander_1.Command('run')
15
- .description('Run a saved command by name or alias')
16
- .argument('<name>', 'Command name or alias')
13
+ .description('Run a saved local command by name')
14
+ .argument('<name>', 'Command name')
17
15
  .option('-d, --dry-run', 'Print command without running it')
18
16
  .option('-c, --confirm', 'Ask for confirmation before running')
19
17
  .action(async (name, opts) => {
20
- const token = await (0, auth_1.requireAuth)();
21
- const spinner = (0, ora_1.default)('Looking up...').start();
22
- try {
23
- // Try by name first, then alias
24
- let cmd;
25
- try {
26
- cmd = await api_1.ApiClient.get(`/commands/by-name/${encodeURIComponent(name)}`, token);
27
- }
28
- catch {
29
- cmd = await api_1.ApiClient.get(`/aliases/${encodeURIComponent(name)}/resolve`, token);
30
- }
31
- spinner.stop();
32
- console.log(chalk_1.default.dim(`\n $ ${cmd.command}\n`));
33
- if (opts.dryRun)
18
+ const cmd = (0, local_vault_1.findLocalByName)(name);
19
+ if (!cmd) {
20
+ console.log(chalk_1.default.red(`\n Command "${name}" not found in local vault.\n`));
21
+ console.log(chalk_1.default.dim(' List: recall list'));
22
+ console.log(chalk_1.default.dim(' Sync from cloud (Pro): recall sync\n'));
23
+ process.exit(1);
24
+ }
25
+ console.log(chalk_1.default.dim(`\n $ ${cmd.command}\n`));
26
+ if (opts.dryRun)
27
+ return;
28
+ if (opts.confirm) {
29
+ const { ok } = await inquirer_1.default.prompt([{
30
+ type: 'confirm',
31
+ name: 'ok',
32
+ message: 'Run this command?',
33
+ default: true,
34
+ }]);
35
+ if (!ok) {
36
+ console.log(chalk_1.default.dim(' Cancelled.\n'));
34
37
  return;
35
- // Confirm if --confirm flag or command has confirm flag
36
- if (opts.confirm) {
37
- const { ok } = await inquirer_1.default.prompt([{
38
- type: 'confirm',
39
- name: 'ok',
40
- message: 'Run this command?',
41
- default: true,
42
- }]);
43
- if (!ok) {
44
- console.log(chalk_1.default.dim(' Cancelled.\n'));
45
- return;
46
- }
47
38
  }
48
- // Log the run
49
- api_1.ApiClient.post(`/commands/${cmd.id}/run`, {}, token).catch(() => { });
50
- (0, child_process_1.execSync)(cmd.command, { stdio: 'inherit' });
51
- }
52
- catch {
53
- spinner.fail(chalk_1.default.red(`Command "${name}" not found`));
54
39
  }
40
+ (0, child_process_1.execSync)(cmd.command, { stdio: 'inherit' });
55
41
  });
@@ -6,68 +6,65 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.saveCommand = 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 shell_history_1 = require("../lib/shell-history");
10
+ const tags_1 = require("../lib/tags");
11
+ const local_vault_1 = require("../lib/local-vault");
12
12
  exports.saveCommand = new commander_1.Command('save')
13
- .description('Save a command to your vault')
14
- .argument('<command>', 'The shell command to save')
13
+ .description('Save a command to your local vault (no account required)')
14
+ .argument('[command]', 'The shell command to save (omit when using --last)')
15
+ .option('-L, --last', 'Save the last command from your shell history')
15
16
  .option('-n, --name <name>', 'Short memorable name')
16
17
  .option('-t, --tags <tags>', 'Comma-separated tags')
17
- .option('-g, --group <group>', 'Save directly to a group (Pro)')
18
- .action(async (command, opts) => {
19
- const token = await (0, auth_1.requireAuth)();
20
- let groupId;
21
- // Resolve group name to ID if provided
22
- if (opts.group) {
23
- const spinner = (0, ora_1.default)(`Looking up group "${opts.group}"...`).start();
18
+ .option('-g, --group <group>', 'Group (Pro / cloud ignored for local save)')
19
+ .addHelpText('after', `
20
+ Examples:
21
+ $ recall save "docker compose up -d" -n start-stack -t docker
22
+ $ recall save --last
23
+ $ recall save --last -n start-stack -t docker
24
+
25
+ Saves to the local vault (~/.recall/commands.json). No login required.
26
+ Sync to the cloud with Pro: recall auth login && recall sync
27
+
28
+ --last reads zsh/bash/PowerShell history (not cmd.exe).
29
+ Bash tip: if the command is missing, run: history -a && recall save --last
30
+ `)
31
+ .action(async (commandArg, opts) => {
32
+ let command = commandArg?.trim();
33
+ if (opts.last) {
34
+ if (command) {
35
+ console.log(chalk_1.default.yellow(' Ignoring the command argument because --last was set.'));
36
+ }
24
37
  try {
25
- const group = await api_1.ApiClient.get(`/groups/by-name/${encodeURIComponent(opts.group)}`, token);
26
- groupId = group.id;
27
- spinner.stop();
38
+ const last = (0, shell_history_1.getLastShellCommand)();
39
+ command = last.command;
40
+ console.log(chalk_1.default.dim(` From ${last.shell} history: ${command}`));
28
41
  }
29
42
  catch (err) {
30
- if (err instanceof api_1.ApiError && err.status === 403) {
31
- spinner.stop();
32
- console.log(chalk_1.default.yellow('\n Groups require a Pro plan.'));
33
- console.log(chalk_1.default.dim(' Run: recall upgrade\n'));
34
- process.exit(1);
35
- }
36
- if (err instanceof api_1.ApiError && err.status === 404) {
37
- // Auto-create the group
38
- spinner.text = `Creating group "${opts.group}"...`;
39
- try {
40
- const newGroup = await api_1.ApiClient.post('/groups', { name: opts.group }, token);
41
- groupId = newGroup.id;
42
- spinner.succeed(chalk_1.default.green(`Created group "${opts.group}"`));
43
- }
44
- catch (createErr) {
45
- spinner.fail(chalk_1.default.red(`Failed to create group "${opts.group}"`));
46
- process.exit(1);
47
- }
48
- }
49
- else {
50
- spinner.fail(chalk_1.default.red(`Failed to look up group "${opts.group}"`));
43
+ if (err instanceof shell_history_1.LastCommandError) {
44
+ console.log(chalk_1.default.red(`\n ${err.message}\n`));
51
45
  process.exit(1);
52
46
  }
47
+ throw err;
53
48
  }
54
49
  }
55
- const spinner = (0, ora_1.default)('Saving...').start();
56
- try {
57
- const saved = await api_1.ApiClient.post('/commands', {
58
- command,
59
- name: opts.name,
60
- tags: opts.tags?.split(',').map((t) => t.trim()),
61
- groupId,
62
- }, token);
63
- spinner.succeed(chalk_1.default.green(`Saved${saved.name ? ` as "${saved.name}"` : ''}` +
64
- (opts.group ? chalk_1.default.dim(` → group "${opts.group}"`) : '')));
50
+ if (!command) {
51
+ console.log(chalk_1.default.red('\n Provide a command, or use --last to take it from shell history.\n'));
52
+ console.log(chalk_1.default.dim(' Examples:'));
53
+ console.log(chalk_1.default.dim(' recall save "docker compose up -d"'));
54
+ console.log(chalk_1.default.dim(' recall save --last\n'));
55
+ process.exit(1);
65
56
  }
66
- catch (err) {
67
- spinner.fail(chalk_1.default.red('Failed to save'));
68
- if (err instanceof api_1.ApiError && err.status === 403 && err.body?.upgrade) {
69
- console.log(chalk_1.default.yellow('\n This feature requires an upgrade.'));
70
- console.log(chalk_1.default.dim(` Upgrade at ${chalk_1.default.white(err.body.upgrade)}\n`));
71
- }
57
+ if (opts.group) {
58
+ console.log(chalk_1.default.yellow(' Groups sync with Pro cloud vault. Saved locally without a group.'));
59
+ console.log(chalk_1.default.dim(' After upgrading: recall sync\n'));
72
60
  }
61
+ const tags = (0, tags_1.parseTags)(opts.tags);
62
+ const saved = (0, local_vault_1.saveLocalCommand)({
63
+ command,
64
+ name: opts.name,
65
+ tags,
66
+ });
67
+ console.log(chalk_1.default.green(` Saved locally${saved.name ? ` as "${saved.name}"` : ''}`));
68
+ console.log(chalk_1.default.dim(` Vault: ${local_vault_1.VAULT_FILE}`));
69
+ console.log(chalk_1.default.dim(' Sync to cloud (Pro): recall sync\n'));
73
70
  });