ecc_infisense 0.0.1

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,481 @@
1
+ /**
2
+ * Session Aliases Library for Claude Code
3
+ * Manages session aliases stored in ~/.claude/session-aliases.json
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const {
10
+ getClaudeDir,
11
+ ensureDir,
12
+ readFile,
13
+ log
14
+ } = require('./utils');
15
+
16
+ // Aliases file path
17
+ function getAliasesPath() {
18
+ return path.join(getClaudeDir(), 'session-aliases.json');
19
+ }
20
+
21
+ // Current alias storage format version
22
+ const ALIAS_VERSION = '1.0';
23
+
24
+ /**
25
+ * Default aliases file structure
26
+ */
27
+ function getDefaultAliases() {
28
+ return {
29
+ version: ALIAS_VERSION,
30
+ aliases: {},
31
+ metadata: {
32
+ totalCount: 0,
33
+ lastUpdated: new Date().toISOString()
34
+ }
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Load aliases from file
40
+ * @returns {object} Aliases object
41
+ */
42
+ function loadAliases() {
43
+ const aliasesPath = getAliasesPath();
44
+
45
+ if (!fs.existsSync(aliasesPath)) {
46
+ return getDefaultAliases();
47
+ }
48
+
49
+ const content = readFile(aliasesPath);
50
+ if (!content) {
51
+ return getDefaultAliases();
52
+ }
53
+
54
+ try {
55
+ const data = JSON.parse(content);
56
+
57
+ // Validate structure
58
+ if (!data.aliases || typeof data.aliases !== 'object') {
59
+ log('[Aliases] Invalid aliases file structure, resetting');
60
+ return getDefaultAliases();
61
+ }
62
+
63
+ // Ensure version field
64
+ if (!data.version) {
65
+ data.version = ALIAS_VERSION;
66
+ }
67
+
68
+ // Ensure metadata
69
+ if (!data.metadata) {
70
+ data.metadata = {
71
+ totalCount: Object.keys(data.aliases).length,
72
+ lastUpdated: new Date().toISOString()
73
+ };
74
+ }
75
+
76
+ return data;
77
+ } catch (err) {
78
+ log(`[Aliases] Error parsing aliases file: ${err.message}`);
79
+ return getDefaultAliases();
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Save aliases to file with atomic write
85
+ * @param {object} aliases - Aliases object to save
86
+ * @returns {boolean} Success status
87
+ */
88
+ function saveAliases(aliases) {
89
+ const aliasesPath = getAliasesPath();
90
+ const tempPath = aliasesPath + '.tmp';
91
+ const backupPath = aliasesPath + '.bak';
92
+
93
+ try {
94
+ // Update metadata
95
+ aliases.metadata = {
96
+ totalCount: Object.keys(aliases.aliases).length,
97
+ lastUpdated: new Date().toISOString()
98
+ };
99
+
100
+ const content = JSON.stringify(aliases, null, 2);
101
+
102
+ // Ensure directory exists
103
+ ensureDir(path.dirname(aliasesPath));
104
+
105
+ // Create backup if file exists
106
+ if (fs.existsSync(aliasesPath)) {
107
+ fs.copyFileSync(aliasesPath, backupPath);
108
+ }
109
+
110
+ // Atomic write: write to temp file, then rename
111
+ fs.writeFileSync(tempPath, content, 'utf8');
112
+
113
+ // On Windows, rename fails with EEXIST if destination exists, so delete first.
114
+ // On Unix/macOS, rename(2) atomically replaces the destination — skip the
115
+ // delete to avoid an unnecessary non-atomic window between unlink and rename.
116
+ if (process.platform === 'win32' && fs.existsSync(aliasesPath)) {
117
+ fs.unlinkSync(aliasesPath);
118
+ }
119
+ fs.renameSync(tempPath, aliasesPath);
120
+
121
+ // Remove backup on success
122
+ if (fs.existsSync(backupPath)) {
123
+ fs.unlinkSync(backupPath);
124
+ }
125
+
126
+ return true;
127
+ } catch (err) {
128
+ log(`[Aliases] Error saving aliases: ${err.message}`);
129
+
130
+ // Restore from backup if exists
131
+ if (fs.existsSync(backupPath)) {
132
+ try {
133
+ fs.copyFileSync(backupPath, aliasesPath);
134
+ log('[Aliases] Restored from backup');
135
+ } catch (restoreErr) {
136
+ log(`[Aliases] Failed to restore backup: ${restoreErr.message}`);
137
+ }
138
+ }
139
+
140
+ // Clean up temp file (best-effort)
141
+ try {
142
+ if (fs.existsSync(tempPath)) {
143
+ fs.unlinkSync(tempPath);
144
+ }
145
+ } catch {
146
+ // Non-critical: temp file will be overwritten on next save
147
+ }
148
+
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Resolve an alias to get session path
155
+ * @param {string} alias - Alias name to resolve
156
+ * @returns {object|null} Alias data or null if not found
157
+ */
158
+ function resolveAlias(alias) {
159
+ if (!alias) return null;
160
+
161
+ // Validate alias name (alphanumeric, dash, underscore)
162
+ if (!/^[a-zA-Z0-9_-]+$/.test(alias)) {
163
+ return null;
164
+ }
165
+
166
+ const data = loadAliases();
167
+ const aliasData = data.aliases[alias];
168
+
169
+ if (!aliasData) {
170
+ return null;
171
+ }
172
+
173
+ return {
174
+ alias,
175
+ sessionPath: aliasData.sessionPath,
176
+ createdAt: aliasData.createdAt,
177
+ title: aliasData.title || null
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Set or update an alias for a session
183
+ * @param {string} alias - Alias name (alphanumeric, dash, underscore)
184
+ * @param {string} sessionPath - Session directory path
185
+ * @param {string} title - Optional title for the alias
186
+ * @returns {object} Result with success status and message
187
+ */
188
+ function setAlias(alias, sessionPath, title = null) {
189
+ // Validate alias name
190
+ if (!alias || alias.length === 0) {
191
+ return { success: false, error: 'Alias name cannot be empty' };
192
+ }
193
+
194
+ // Validate session path
195
+ if (!sessionPath || typeof sessionPath !== 'string' || sessionPath.trim().length === 0) {
196
+ return { success: false, error: 'Session path cannot be empty' };
197
+ }
198
+
199
+ if (alias.length > 128) {
200
+ return { success: false, error: 'Alias name cannot exceed 128 characters' };
201
+ }
202
+
203
+ if (!/^[a-zA-Z0-9_-]+$/.test(alias)) {
204
+ return { success: false, error: 'Alias name must contain only letters, numbers, dashes, and underscores' };
205
+ }
206
+
207
+ // Reserved alias names
208
+ const reserved = ['list', 'help', 'remove', 'delete', 'create', 'set'];
209
+ if (reserved.includes(alias.toLowerCase())) {
210
+ return { success: false, error: `'${alias}' is a reserved alias name` };
211
+ }
212
+
213
+ const data = loadAliases();
214
+ const existing = data.aliases[alias];
215
+ const isNew = !existing;
216
+
217
+ data.aliases[alias] = {
218
+ sessionPath,
219
+ createdAt: existing ? existing.createdAt : new Date().toISOString(),
220
+ updatedAt: new Date().toISOString(),
221
+ title: title || null
222
+ };
223
+
224
+ if (saveAliases(data)) {
225
+ return {
226
+ success: true,
227
+ isNew,
228
+ alias,
229
+ sessionPath,
230
+ title: data.aliases[alias].title
231
+ };
232
+ }
233
+
234
+ return { success: false, error: 'Failed to save alias' };
235
+ }
236
+
237
+ /**
238
+ * List all aliases
239
+ * @param {object} options - Options object
240
+ * @param {string} options.search - Filter aliases by name (partial match)
241
+ * @param {number} options.limit - Maximum number of aliases to return
242
+ * @returns {Array} Array of alias objects
243
+ */
244
+ function listAliases(options = {}) {
245
+ const { search = null, limit = null } = options;
246
+ const data = loadAliases();
247
+
248
+ let aliases = Object.entries(data.aliases).map(([name, info]) => ({
249
+ name,
250
+ sessionPath: info.sessionPath,
251
+ createdAt: info.createdAt,
252
+ updatedAt: info.updatedAt,
253
+ title: info.title
254
+ }));
255
+
256
+ // Sort by updated time (newest first)
257
+ aliases.sort((a, b) => (new Date(b.updatedAt || b.createdAt || 0).getTime() || 0) - (new Date(a.updatedAt || a.createdAt || 0).getTime() || 0));
258
+
259
+ // Apply search filter
260
+ if (search) {
261
+ const searchLower = search.toLowerCase();
262
+ aliases = aliases.filter(a =>
263
+ a.name.toLowerCase().includes(searchLower) ||
264
+ (a.title && a.title.toLowerCase().includes(searchLower))
265
+ );
266
+ }
267
+
268
+ // Apply limit
269
+ if (limit && limit > 0) {
270
+ aliases = aliases.slice(0, limit);
271
+ }
272
+
273
+ return aliases;
274
+ }
275
+
276
+ /**
277
+ * Delete an alias
278
+ * @param {string} alias - Alias name to delete
279
+ * @returns {object} Result with success status
280
+ */
281
+ function deleteAlias(alias) {
282
+ const data = loadAliases();
283
+
284
+ if (!data.aliases[alias]) {
285
+ return { success: false, error: `Alias '${alias}' not found` };
286
+ }
287
+
288
+ const deleted = data.aliases[alias];
289
+ delete data.aliases[alias];
290
+
291
+ if (saveAliases(data)) {
292
+ return {
293
+ success: true,
294
+ alias,
295
+ deletedSessionPath: deleted.sessionPath
296
+ };
297
+ }
298
+
299
+ return { success: false, error: 'Failed to delete alias' };
300
+ }
301
+
302
+ /**
303
+ * Rename an alias
304
+ * @param {string} oldAlias - Current alias name
305
+ * @param {string} newAlias - New alias name
306
+ * @returns {object} Result with success status
307
+ */
308
+ function renameAlias(oldAlias, newAlias) {
309
+ const data = loadAliases();
310
+
311
+ if (!data.aliases[oldAlias]) {
312
+ return { success: false, error: `Alias '${oldAlias}' not found` };
313
+ }
314
+
315
+ // Validate new alias name (same rules as setAlias)
316
+ if (!newAlias || newAlias.length === 0) {
317
+ return { success: false, error: 'New alias name cannot be empty' };
318
+ }
319
+
320
+ if (newAlias.length > 128) {
321
+ return { success: false, error: 'New alias name cannot exceed 128 characters' };
322
+ }
323
+
324
+ if (!/^[a-zA-Z0-9_-]+$/.test(newAlias)) {
325
+ return { success: false, error: 'New alias name must contain only letters, numbers, dashes, and underscores' };
326
+ }
327
+
328
+ const reserved = ['list', 'help', 'remove', 'delete', 'create', 'set'];
329
+ if (reserved.includes(newAlias.toLowerCase())) {
330
+ return { success: false, error: `'${newAlias}' is a reserved alias name` };
331
+ }
332
+
333
+ if (data.aliases[newAlias]) {
334
+ return { success: false, error: `Alias '${newAlias}' already exists` };
335
+ }
336
+
337
+ const aliasData = data.aliases[oldAlias];
338
+ delete data.aliases[oldAlias];
339
+
340
+ aliasData.updatedAt = new Date().toISOString();
341
+ data.aliases[newAlias] = aliasData;
342
+
343
+ if (saveAliases(data)) {
344
+ return {
345
+ success: true,
346
+ oldAlias,
347
+ newAlias,
348
+ sessionPath: aliasData.sessionPath
349
+ };
350
+ }
351
+
352
+ // Restore old alias and remove new alias on failure
353
+ data.aliases[oldAlias] = aliasData;
354
+ delete data.aliases[newAlias];
355
+ // Attempt to persist the rollback
356
+ saveAliases(data);
357
+ return { success: false, error: 'Failed to save renamed alias — rolled back to original' };
358
+ }
359
+
360
+ /**
361
+ * Get session path by alias (convenience function)
362
+ * @param {string} aliasOrId - Alias name or session ID
363
+ * @returns {string|null} Session path or null if not found
364
+ */
365
+ function resolveSessionAlias(aliasOrId) {
366
+ // First try to resolve as alias
367
+ const resolved = resolveAlias(aliasOrId);
368
+ if (resolved) {
369
+ return resolved.sessionPath;
370
+ }
371
+
372
+ // If not an alias, return as-is (might be a session path)
373
+ return aliasOrId;
374
+ }
375
+
376
+ /**
377
+ * Update alias title
378
+ * @param {string} alias - Alias name
379
+ * @param {string|null} title - New title (string or null to clear)
380
+ * @returns {object} Result with success status
381
+ */
382
+ function updateAliasTitle(alias, title) {
383
+ if (title !== null && typeof title !== 'string') {
384
+ return { success: false, error: 'Title must be a string or null' };
385
+ }
386
+
387
+ const data = loadAliases();
388
+
389
+ if (!data.aliases[alias]) {
390
+ return { success: false, error: `Alias '${alias}' not found` };
391
+ }
392
+
393
+ data.aliases[alias].title = title || null;
394
+ data.aliases[alias].updatedAt = new Date().toISOString();
395
+
396
+ if (saveAliases(data)) {
397
+ return {
398
+ success: true,
399
+ alias,
400
+ title
401
+ };
402
+ }
403
+
404
+ return { success: false, error: 'Failed to update alias title' };
405
+ }
406
+
407
+ /**
408
+ * Get all aliases for a specific session
409
+ * @param {string} sessionPath - Session path to find aliases for
410
+ * @returns {Array} Array of alias names
411
+ */
412
+ function getAliasesForSession(sessionPath) {
413
+ const data = loadAliases();
414
+ const aliases = [];
415
+
416
+ for (const [name, info] of Object.entries(data.aliases)) {
417
+ if (info.sessionPath === sessionPath) {
418
+ aliases.push({
419
+ name,
420
+ createdAt: info.createdAt,
421
+ title: info.title
422
+ });
423
+ }
424
+ }
425
+
426
+ return aliases;
427
+ }
428
+
429
+ /**
430
+ * Clean up aliases for non-existent sessions
431
+ * @param {Function} sessionExists - Function to check if session exists
432
+ * @returns {object} Cleanup result
433
+ */
434
+ function cleanupAliases(sessionExists) {
435
+ if (typeof sessionExists !== 'function') {
436
+ return { totalChecked: 0, removed: 0, removedAliases: [], error: 'sessionExists must be a function' };
437
+ }
438
+
439
+ const data = loadAliases();
440
+ const removed = [];
441
+
442
+ for (const [name, info] of Object.entries(data.aliases)) {
443
+ if (!sessionExists(info.sessionPath)) {
444
+ removed.push({ name, sessionPath: info.sessionPath });
445
+ delete data.aliases[name];
446
+ }
447
+ }
448
+
449
+ if (removed.length > 0 && !saveAliases(data)) {
450
+ log('[Aliases] Failed to save after cleanup');
451
+ return {
452
+ success: false,
453
+ totalChecked: Object.keys(data.aliases).length + removed.length,
454
+ removed: removed.length,
455
+ removedAliases: removed,
456
+ error: 'Failed to save after cleanup'
457
+ };
458
+ }
459
+
460
+ return {
461
+ success: true,
462
+ totalChecked: Object.keys(data.aliases).length + removed.length,
463
+ removed: removed.length,
464
+ removedAliases: removed
465
+ };
466
+ }
467
+
468
+ module.exports = {
469
+ getAliasesPath,
470
+ loadAliases,
471
+ saveAliases,
472
+ resolveAlias,
473
+ setAlias,
474
+ listAliases,
475
+ deleteAlias,
476
+ renameAlias,
477
+ resolveSessionAlias,
478
+ updateAliasTitle,
479
+ getAliasesForSession,
480
+ cleanupAliases
481
+ };