@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.
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VAULT_FILE = void 0;
7
+ exports.readVault = readVault;
8
+ exports.writeVault = writeVault;
9
+ exports.listLocalCommands = listLocalCommands;
10
+ exports.findLocalByName = findLocalByName;
11
+ exports.saveLocalCommand = saveLocalCommand;
12
+ exports.deleteLocalByName = deleteLocalByName;
13
+ exports.setLocalPinned = setLocalPinned;
14
+ exports.markVaultSynced = markVaultSynced;
15
+ exports.replaceLocalFromCloud = replaceLocalFromCloud;
16
+ exports.mergeCloudIntoLocal = mergeCloudIntoLocal;
17
+ exports.localCommandsNeedingPush = localCommandsNeedingPush;
18
+ exports.attachCloudId = attachCloudId;
19
+ const node_crypto_1 = require("node:crypto");
20
+ const node_fs_1 = __importDefault(require("node:fs"));
21
+ const node_os_1 = __importDefault(require("node:os"));
22
+ const node_path_1 = __importDefault(require("node:path"));
23
+ const VAULT_DIR = node_path_1.default.join(process.env.RECALL_HOME?.trim() || node_os_1.default.homedir(), '.recall');
24
+ exports.VAULT_FILE = node_path_1.default.join(VAULT_DIR, 'commands.json');
25
+ function nowIso() {
26
+ return new Date().toISOString();
27
+ }
28
+ function ensureDir() {
29
+ node_fs_1.default.mkdirSync(VAULT_DIR, { recursive: true });
30
+ }
31
+ function normalizeCommand(raw, legacySyncCache) {
32
+ const command = String(raw.command ?? '');
33
+ const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : undefined;
34
+ const tags = Array.isArray(raw.tags)
35
+ ? raw.tags.filter((t) => typeof t === 'string' && t.trim().length > 0)
36
+ : undefined;
37
+ const pinned = Boolean(raw.pinned);
38
+ const createdAt = typeof raw.createdAt === 'string' ? raw.createdAt : nowIso();
39
+ const updatedAt = typeof raw.updatedAt === 'string' ? raw.updatedAt : createdAt;
40
+ let cloudId;
41
+ if (typeof raw.cloudId === 'string' && raw.cloudId) {
42
+ cloudId = raw.cloudId;
43
+ }
44
+ else if (legacySyncCache && typeof raw.id === 'string' && raw.id) {
45
+ // Old cache used API ids as `id`
46
+ cloudId = raw.id;
47
+ }
48
+ const id = !legacySyncCache && typeof raw.id === 'string' && raw.id
49
+ ? raw.id
50
+ : (0, node_crypto_1.randomUUID)();
51
+ return {
52
+ id,
53
+ command,
54
+ name,
55
+ tags: tags?.length ? tags : undefined,
56
+ pinned: pinned || undefined,
57
+ createdAt,
58
+ updatedAt,
59
+ cloudId,
60
+ };
61
+ }
62
+ function emptyVault() {
63
+ return {
64
+ version: 1,
65
+ updatedAt: nowIso(),
66
+ syncedAt: null,
67
+ commands: [],
68
+ };
69
+ }
70
+ function readVault() {
71
+ try {
72
+ if (!node_fs_1.default.existsSync(exports.VAULT_FILE))
73
+ return emptyVault();
74
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(exports.VAULT_FILE, 'utf-8'));
75
+ const rawCommands = Array.isArray(parsed.commands) ? parsed.commands : [];
76
+ const legacySyncCache = parsed.version !== 1;
77
+ const commands = rawCommands
78
+ .filter((c) => c !== null && typeof c === 'object')
79
+ .map((c) => normalizeCommand(c, legacySyncCache))
80
+ .filter((c) => c.command.trim().length > 0);
81
+ return {
82
+ version: 1,
83
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : nowIso(),
84
+ syncedAt: typeof parsed.syncedAt === 'string' ? parsed.syncedAt : null,
85
+ commands,
86
+ };
87
+ }
88
+ catch {
89
+ return emptyVault();
90
+ }
91
+ }
92
+ function writeVault(vault) {
93
+ ensureDir();
94
+ const payload = {
95
+ version: 1,
96
+ updatedAt: nowIso(),
97
+ syncedAt: vault.syncedAt ?? null,
98
+ commands: vault.commands,
99
+ };
100
+ node_fs_1.default.writeFileSync(exports.VAULT_FILE, JSON.stringify(payload, null, 2), { mode: 0o600 });
101
+ }
102
+ function listLocalCommands(filters) {
103
+ let rows = readVault().commands;
104
+ if (filters?.tag) {
105
+ const tag = filters.tag.toLowerCase();
106
+ rows = rows.filter((c) => (c.tags ?? []).some((t) => t.toLowerCase() === tag));
107
+ }
108
+ if (filters?.search) {
109
+ const q = filters.search.toLowerCase();
110
+ rows = rows.filter((c) => c.command.toLowerCase().includes(q) ||
111
+ (c.name?.toLowerCase().includes(q) ?? false) ||
112
+ (c.tags ?? []).some((t) => t.toLowerCase().includes(q)));
113
+ }
114
+ if (filters?.pinned) {
115
+ rows = rows.filter((c) => c.pinned);
116
+ }
117
+ return rows.sort((a, b) => {
118
+ if (a.pinned && !b.pinned)
119
+ return -1;
120
+ if (!a.pinned && b.pinned)
121
+ return 1;
122
+ return b.updatedAt.localeCompare(a.updatedAt);
123
+ });
124
+ }
125
+ function findLocalByName(name) {
126
+ const needle = name.toLowerCase();
127
+ return readVault().commands.find((c) => c.name?.toLowerCase() === needle);
128
+ }
129
+ function saveLocalCommand(input) {
130
+ const vault = readVault();
131
+ const stamp = nowIso();
132
+ const name = input.name?.trim() || undefined;
133
+ if (name) {
134
+ const existing = vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase());
135
+ if (existing) {
136
+ existing.command = input.command;
137
+ existing.tags = input.tags?.length ? input.tags : undefined;
138
+ existing.updatedAt = stamp;
139
+ // Local edit invalidates cloud link until next push maps again
140
+ writeVault(vault);
141
+ return existing;
142
+ }
143
+ }
144
+ const saved = {
145
+ id: (0, node_crypto_1.randomUUID)(),
146
+ command: input.command,
147
+ name,
148
+ tags: input.tags?.length ? input.tags : undefined,
149
+ createdAt: stamp,
150
+ updatedAt: stamp,
151
+ };
152
+ vault.commands.push(saved);
153
+ writeVault(vault);
154
+ return saved;
155
+ }
156
+ function deleteLocalByName(name) {
157
+ const vault = readVault();
158
+ const idx = vault.commands.findIndex((c) => c.name?.toLowerCase() === name.toLowerCase());
159
+ if (idx < 0)
160
+ return undefined;
161
+ const [removed] = vault.commands.splice(idx, 1);
162
+ writeVault(vault);
163
+ return removed;
164
+ }
165
+ function setLocalPinned(name, pinned) {
166
+ const vault = readVault();
167
+ const cmd = vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase());
168
+ if (!cmd)
169
+ return undefined;
170
+ cmd.pinned = pinned || undefined;
171
+ cmd.updatedAt = nowIso();
172
+ writeVault(vault);
173
+ return cmd;
174
+ }
175
+ function markVaultSynced(at = nowIso()) {
176
+ const vault = readVault();
177
+ vault.syncedAt = at;
178
+ writeVault(vault);
179
+ }
180
+ function replaceLocalFromCloud(cloudCommands) {
181
+ const stamp = nowIso();
182
+ const mapped = cloudCommands.map((c) => ({
183
+ id: (0, node_crypto_1.randomUUID)(),
184
+ cloudId: c.id,
185
+ command: c.command,
186
+ name: c.name ?? undefined,
187
+ tags: c.tags?.length ? c.tags : undefined,
188
+ pinned: c.pinned || undefined,
189
+ createdAt: c.createdAt ?? stamp,
190
+ updatedAt: c.updatedAt ?? stamp,
191
+ }));
192
+ writeVault({
193
+ version: 1,
194
+ updatedAt: stamp,
195
+ syncedAt: stamp,
196
+ commands: mapped,
197
+ });
198
+ return mapped;
199
+ }
200
+ /** Merge cloud into local by name (cloud wins on conflict); keep local-only rows. */
201
+ function mergeCloudIntoLocal(cloudCommands) {
202
+ const vault = readVault();
203
+ const stamp = nowIso();
204
+ let added = 0;
205
+ let updated = 0;
206
+ for (const cloud of cloudCommands) {
207
+ const name = cloud.name?.trim();
208
+ const byCloudId = vault.commands.find((c) => c.cloudId === cloud.id);
209
+ const byName = name
210
+ ? vault.commands.find((c) => c.name?.toLowerCase() === name.toLowerCase())
211
+ : undefined;
212
+ const target = byCloudId ?? byName;
213
+ if (target) {
214
+ target.cloudId = cloud.id;
215
+ target.command = cloud.command;
216
+ target.name = name || target.name;
217
+ target.tags = cloud.tags?.length ? cloud.tags : undefined;
218
+ target.pinned = cloud.pinned || undefined;
219
+ target.updatedAt = stamp;
220
+ updated++;
221
+ }
222
+ else {
223
+ vault.commands.push({
224
+ id: (0, node_crypto_1.randomUUID)(),
225
+ cloudId: cloud.id,
226
+ command: cloud.command,
227
+ name: name || undefined,
228
+ tags: cloud.tags?.length ? cloud.tags : undefined,
229
+ pinned: cloud.pinned || undefined,
230
+ createdAt: cloud.createdAt ?? stamp,
231
+ updatedAt: stamp,
232
+ });
233
+ added++;
234
+ }
235
+ }
236
+ vault.syncedAt = stamp;
237
+ writeVault(vault);
238
+ return { local: vault.commands, added, updated };
239
+ }
240
+ function localCommandsNeedingPush() {
241
+ return readVault().commands.filter((c) => !c.cloudId);
242
+ }
243
+ function attachCloudId(localId, cloudId) {
244
+ const vault = readVault();
245
+ const cmd = vault.commands.find((c) => c.id === localId);
246
+ if (!cmd)
247
+ return;
248
+ cmd.cloudId = cloudId;
249
+ cmd.updatedAt = nowIso();
250
+ writeVault(vault);
251
+ }
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LastCommandError = void 0;
7
+ exports.isSelfSaveCommand = isSelfSaveCommand;
8
+ exports.isSkippedHistoryCommand = isSkippedHistoryCommand;
9
+ exports.normalizeHistoryLine = normalizeHistoryLine;
10
+ exports.extractSaveableCommands = extractSaveableCommands;
11
+ exports.readHistoryCommands = readHistoryCommands;
12
+ exports.stripZshExtendedPrefix = stripZshExtendedPrefix;
13
+ exports.findLastSaveableCommand = findLastSaveableCommand;
14
+ exports.detectShellKind = detectShellKind;
15
+ exports.historyFileCandidates = historyFileCandidates;
16
+ exports.readLastCommandFromHistoryFile = readLastCommandFromHistoryFile;
17
+ exports.getLastShellCommand = getLastShellCommand;
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ /** True if this history line is a recall/rec save invocation (skip so we don't save ourselves). */
22
+ function isSelfSaveCommand(line) {
23
+ const t = line.trim();
24
+ return /^(recall|rec)\s+save(\s|$)/.test(t);
25
+ }
26
+ /** Skip trivial / meta lines when bulk-importing history. */
27
+ function isSkippedHistoryCommand(line) {
28
+ const t = line.trim();
29
+ if (!t)
30
+ return true;
31
+ if (isSelfSaveCommand(t))
32
+ return true;
33
+ if (/^(recall|rec)(\s|$)/.test(t))
34
+ return true;
35
+ if (t.length < 2)
36
+ return true;
37
+ const first = t.split(/\s+/)[0]?.toLowerCase() ?? '';
38
+ const trivial = new Set([
39
+ 'cd', 'ls', 'll', 'la', 'pwd', 'clear', 'cls', 'exit', 'logout',
40
+ 'history', 'true', 'false', ':',
41
+ ]);
42
+ if (trivial.has(first) && t.split(/\s+/).length <= 2)
43
+ return true;
44
+ return false;
45
+ }
46
+ /** Normalize one history file line into a bare command, or null if not usable. */
47
+ function normalizeHistoryLine(rawLine) {
48
+ let raw = rawLine.replace(/\r$/, '');
49
+ if (!raw.trim())
50
+ return null;
51
+ if (/^#\d+$/.test(raw.trim()))
52
+ return null;
53
+ raw = stripZshExtendedPrefix(raw).trim();
54
+ if (!raw)
55
+ return null;
56
+ return raw;
57
+ }
58
+ /**
59
+ * Walk history lines (oldest → newest) and return unique saveable commands, newest first.
60
+ */
61
+ function extractSaveableCommands(lines, opts = {}) {
62
+ const minLength = opts.minLength ?? 3;
63
+ const seen = new Set();
64
+ const out = [];
65
+ for (let i = lines.length - 1; i >= 0; i--) {
66
+ const cmd = normalizeHistoryLine(lines[i] ?? '');
67
+ if (!cmd)
68
+ continue;
69
+ if (cmd.length < minLength)
70
+ continue;
71
+ if (isSkippedHistoryCommand(cmd))
72
+ continue;
73
+ if (seen.has(cmd))
74
+ continue;
75
+ seen.add(cmd);
76
+ out.push(cmd);
77
+ if (opts.limit !== undefined && out.length >= opts.limit)
78
+ break;
79
+ }
80
+ return out;
81
+ }
82
+ function readHistoryCommands(opts = {}) {
83
+ const env = opts.env ?? process.env;
84
+ const platform = opts.platform ?? process.platform;
85
+ const home = opts.home ?? os_1.default.homedir();
86
+ const shell = detectShellKind(env, platform);
87
+ if (shell === 'cmd') {
88
+ throw new LastCommandError('import history is not supported in cmd.exe. Use PowerShell, Git Bash, or WSL.', 'unsupported');
89
+ }
90
+ if (shell === 'unknown') {
91
+ throw new LastCommandError('Could not detect your shell. Use zsh, bash, or PowerShell.', 'unsupported');
92
+ }
93
+ const files = historyFileCandidates(shell, env, home);
94
+ for (const file of files) {
95
+ try {
96
+ if (!fs_1.default.existsSync(file))
97
+ continue;
98
+ const text = fs_1.default.readFileSync(file, 'utf8');
99
+ const commands = extractSaveableCommands(text.split('\n'), {
100
+ limit: opts.limit,
101
+ minLength: opts.minLength,
102
+ });
103
+ if (commands.length) {
104
+ return { commands, source: file, shell };
105
+ }
106
+ }
107
+ catch {
108
+ // try next
109
+ }
110
+ }
111
+ const tip = shell === 'bash'
112
+ ? ' If history looks empty, try: history -a && recall import history'
113
+ : '';
114
+ throw new LastCommandError(`No importable commands found in shell history.${tip}`, 'not_found');
115
+ }
116
+ /** Strip zsh extended-history prefix `: <unix>:<duration>;`. */
117
+ function stripZshExtendedPrefix(line) {
118
+ const m = line.match(/^:\s*\d+:\d+;(.*)$/);
119
+ return m ? m[1] : line;
120
+ }
121
+ /**
122
+ * Walk history lines newest-first and return the first saveable command.
123
+ * Caller should pass lines in file order (oldest → newest); we scan from the end.
124
+ */
125
+ function findLastSaveableCommand(lines) {
126
+ for (let i = lines.length - 1; i >= 0; i--) {
127
+ let raw = lines[i]?.replace(/\r$/, '') ?? '';
128
+ if (!raw.trim())
129
+ continue;
130
+ // bash HISTTIMEFORMAT lines are comments starting with #
131
+ if (/^#\d+$/.test(raw.trim()))
132
+ continue;
133
+ raw = stripZshExtendedPrefix(raw).trim();
134
+ if (!raw)
135
+ continue;
136
+ if (isSelfSaveCommand(raw))
137
+ continue;
138
+ return raw;
139
+ }
140
+ return null;
141
+ }
142
+ function detectShellKind(env = process.env, platform = process.platform) {
143
+ if (platform === 'win32') {
144
+ const shell = (env.SHELL || env.ComSpec || '').toLowerCase();
145
+ if (shell.includes('powershell') || shell.includes('pwsh'))
146
+ return 'powershell';
147
+ // Windows Terminal / VS Code often set PSModulePath when in PowerShell
148
+ if (env.PSModulePath && !shell.includes('cmd.exe')) {
149
+ if (env.TERM_PROGRAM || env.WT_SESSION || env.VSCODE_INJECTION) {
150
+ // Prefer PowerShell when clearly in a modern host without bash
151
+ if (!shell.includes('bash') && !shell.includes('zsh'))
152
+ return 'powershell';
153
+ }
154
+ }
155
+ if (shell.includes('bash') || shell.includes('zsh')) {
156
+ return shell.includes('zsh') ? 'zsh' : 'bash';
157
+ }
158
+ if (shell.includes('cmd.exe') || shell.endsWith('\\cmd'))
159
+ return 'cmd';
160
+ // Default Windows: PowerShell is the intended path; cmd is unsupported
161
+ if (!env.SHELL)
162
+ return 'powershell';
163
+ return 'cmd';
164
+ }
165
+ const shell = (env.SHELL || '').toLowerCase();
166
+ if (shell.includes('zsh'))
167
+ return 'zsh';
168
+ if (shell.includes('bash'))
169
+ return 'bash';
170
+ if (shell.includes('pwsh') || shell.includes('powershell'))
171
+ return 'powershell';
172
+ return 'unknown';
173
+ }
174
+ function historyFileCandidates(kind, env = process.env, home = os_1.default.homedir()) {
175
+ if (env.HISTFILE)
176
+ return [env.HISTFILE];
177
+ switch (kind) {
178
+ case 'zsh':
179
+ return [path_1.default.join(home, '.zsh_history')];
180
+ case 'bash':
181
+ return [path_1.default.join(home, '.bash_history')];
182
+ case 'powershell': {
183
+ const candidates = [];
184
+ if (env.APPDATA) {
185
+ candidates.push(path_1.default.join(env.APPDATA, 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'), path_1.default.join(env.APPDATA, 'Microsoft', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'));
186
+ }
187
+ candidates.push(path_1.default.join(home, '.local', 'share', 'powershell', 'PSReadLine', 'ConsoleHost_history.txt'), path_1.default.join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'));
188
+ return candidates;
189
+ }
190
+ default:
191
+ return [];
192
+ }
193
+ }
194
+ function readLastCommandFromHistoryFile(filePath) {
195
+ if (!fs_1.default.existsSync(filePath))
196
+ return null;
197
+ const text = fs_1.default.readFileSync(filePath, 'utf8');
198
+ const lines = text.split('\n');
199
+ return findLastSaveableCommand(lines);
200
+ }
201
+ class LastCommandError extends Error {
202
+ code;
203
+ constructor(message, code) {
204
+ super(message);
205
+ this.code = code;
206
+ this.name = 'LastCommandError';
207
+ }
208
+ }
209
+ exports.LastCommandError = LastCommandError;
210
+ /**
211
+ * Resolve the last typed shell command from on-disk history.
212
+ * Same-session works when the shell appends history as you go (typical zsh / PSReadLine).
213
+ * Bash may need `history -a` first if the file is stale.
214
+ */
215
+ function getLastShellCommand(env = process.env, platform = process.platform, home = os_1.default.homedir()) {
216
+ const shell = detectShellKind(env, platform);
217
+ if (shell === 'cmd') {
218
+ throw new LastCommandError('save --last is not supported in cmd.exe. Use PowerShell, Git Bash, or WSL — or paste the command: recall save "your command"', 'unsupported');
219
+ }
220
+ if (shell === 'unknown') {
221
+ throw new LastCommandError('Could not detect your shell. Use zsh, bash, or PowerShell — or paste the command: recall save "your command"', 'unsupported');
222
+ }
223
+ const files = historyFileCandidates(shell, env, home);
224
+ for (const file of files) {
225
+ try {
226
+ const command = readLastCommandFromHistoryFile(file);
227
+ if (command) {
228
+ return { command, source: file, shell };
229
+ }
230
+ }
231
+ catch {
232
+ // try next candidate
233
+ }
234
+ }
235
+ const tip = shell === 'bash'
236
+ ? ' If you just ran the command in this session, try: history -a && recall save --last'
237
+ : '';
238
+ throw new LastCommandError(`No previous command found in shell history.${tip}`, 'not_found');
239
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devkitvault/recall",
3
- "version": "1.2.7",
4
- "description": "recall CLI — AI terminal copilot with a personal command vault",
3
+ "version": "1.2.10",
4
+ "description": "recall CLI — personal command vault for your terminal (local-first)",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "shell",
@@ -19,12 +19,11 @@
19
19
  "license": "MIT",
20
20
  "homepage": "https://recall.devkitvault.com",
21
21
  "bugs": {
22
- "url": "https://github.com/devkitvault/recall/issues"
22
+ "url": "https://github.com/devkitvault/recall-cli/issues"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "git+https://github.com/devkitvault/recall.git",
27
- "directory": "packages/npm-cli"
26
+ "url": "git+https://github.com/devkitvault/recall-cli.git"
28
27
  },
29
28
  "bin": {
30
29
  "recall": "dist/index.js",