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.
- package/.claude-plugin/plugin.json +17 -0
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/agents/component-designer.md +261 -0
- package/agents/plugin-inspector.md +160 -0
- package/agents/release-executor.md +349 -0
- package/commands/infi-component-creator.md +132 -0
- package/commands/infi-ecc-helper.md +44 -0
- package/commands/infi-release.md +83 -0
- package/hooks/hooks.json +53 -0
- package/install.sh +237 -0
- package/package.json +50 -0
- package/rules/common/agents.md +28 -0
- package/rules/common/development-workflow.md +26 -0
- package/rules/common/git-workflow.md +45 -0
- package/rules/common/hooks.md +36 -0
- package/scripts/hooks/pre-compact.js +72 -0
- package/scripts/hooks/session-end.js +72 -0
- package/scripts/hooks/session-start.js +124 -0
- package/scripts/hooks/suggest-compact.js +58 -0
- package/scripts/lib/session-aliases.js +481 -0
- package/scripts/lib/session-manager.js +442 -0
- package/scripts/lib/utils.js +593 -0
- package/skills/component-templates/SKILL.md +306 -0
- package/skills/release-workflow/SKILL.md +180 -0
|
@@ -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
|
+
};
|