cc4pm 1.8.0
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/README.md +17 -0
- package/.claude-plugin/plugin.json +25 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/README.zh-CN.md +134 -0
- package/contexts/dev.md +20 -0
- package/contexts/research.md +26 -0
- package/contexts/review.md +22 -0
- package/examples/CLAUDE.md +100 -0
- package/examples/statusline.json +19 -0
- package/examples/user-CLAUDE.md +109 -0
- package/install.sh +17 -0
- package/manifests/install-components.json +173 -0
- package/manifests/install-modules.json +335 -0
- package/manifests/install-profiles.json +75 -0
- package/package.json +117 -0
- package/schemas/ecc-install-config.schema.json +58 -0
- package/schemas/hooks.schema.json +197 -0
- package/schemas/install-components.schema.json +56 -0
- package/schemas/install-modules.schema.json +105 -0
- package/schemas/install-profiles.schema.json +45 -0
- package/schemas/install-state.schema.json +210 -0
- package/schemas/package-manager.schema.json +23 -0
- package/schemas/plugin.schema.json +58 -0
- package/scripts/ci/catalog.js +83 -0
- package/scripts/ci/validate-agents.js +81 -0
- package/scripts/ci/validate-commands.js +135 -0
- package/scripts/ci/validate-hooks.js +239 -0
- package/scripts/ci/validate-install-manifests.js +211 -0
- package/scripts/ci/validate-no-personal-paths.js +63 -0
- package/scripts/ci/validate-rules.js +81 -0
- package/scripts/ci/validate-skills.js +54 -0
- package/scripts/claw.js +468 -0
- package/scripts/doctor.js +110 -0
- package/scripts/ecc.js +194 -0
- package/scripts/hooks/auto-tmux-dev.js +88 -0
- package/scripts/hooks/check-console-log.js +71 -0
- package/scripts/hooks/check-hook-enabled.js +12 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +100 -0
- package/scripts/hooks/insaits-security-monitor.py +269 -0
- package/scripts/hooks/insaits-security-wrapper.js +88 -0
- package/scripts/hooks/post-bash-build-complete.js +27 -0
- package/scripts/hooks/post-bash-pr-created.js +36 -0
- package/scripts/hooks/post-edit-console-warn.js +54 -0
- package/scripts/hooks/post-edit-format.js +109 -0
- package/scripts/hooks/post-edit-typecheck.js +96 -0
- package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
- package/scripts/hooks/pre-compact.js +48 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +168 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +120 -0
- package/scripts/hooks/session-end-marker.js +15 -0
- package/scripts/hooks/session-end.js +299 -0
- package/scripts/hooks/session-start.js +97 -0
- package/scripts/hooks/suggest-compact.js +80 -0
- package/scripts/install-apply.js +137 -0
- package/scripts/install-plan.js +254 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install/apply.js +23 -0
- package/scripts/lib/install/config.js +82 -0
- package/scripts/lib/install/request.js +113 -0
- package/scripts/lib/install/runtime.js +42 -0
- package/scripts/lib/install-executor.js +605 -0
- package/scripts/lib/install-lifecycle.js +763 -0
- package/scripts/lib/install-manifests.js +305 -0
- package/scripts/lib/install-state.js +120 -0
- package/scripts/lib/install-targets/antigravity-project.js +9 -0
- package/scripts/lib/install-targets/claude-home.js +10 -0
- package/scripts/lib/install-targets/codex-home.js +10 -0
- package/scripts/lib/install-targets/cursor-project.js +10 -0
- package/scripts/lib/install-targets/helpers.js +89 -0
- package/scripts/lib/install-targets/opencode-home.js +10 -0
- package/scripts/lib/install-targets/registry.js +64 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.d.ts +119 -0
- package/scripts/lib/package-manager.js +431 -0
- package/scripts/lib/project-detect.js +428 -0
- package/scripts/lib/resolve-formatter.js +185 -0
- package/scripts/lib/session-adapters/canonical-session.js +138 -0
- package/scripts/lib/session-adapters/claude-history.js +149 -0
- package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
- package/scripts/lib/session-adapters/registry.js +111 -0
- package/scripts/lib/session-aliases.d.ts +136 -0
- package/scripts/lib/session-aliases.js +481 -0
- package/scripts/lib/session-manager.d.ts +131 -0
- package/scripts/lib/session-manager.js +464 -0
- package/scripts/lib/shell-split.js +86 -0
- package/scripts/lib/skill-improvement/amendify.js +89 -0
- package/scripts/lib/skill-improvement/evaluate.js +59 -0
- package/scripts/lib/skill-improvement/health.js +118 -0
- package/scripts/lib/skill-improvement/observations.js +108 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
- package/scripts/lib/utils.d.ts +183 -0
- package/scripts/lib/utils.js +543 -0
- package/scripts/list-installed.js +90 -0
- package/scripts/orchestrate-codex-worker.sh +92 -0
- package/scripts/orchestrate-worktrees.js +108 -0
- package/scripts/orchestration-status.js +62 -0
- package/scripts/repair.js +97 -0
- package/scripts/session-inspect.js +150 -0
- package/scripts/setup-package-manager.js +204 -0
- package/scripts/skill-create-output.js +244 -0
- package/scripts/uninstall.js +96 -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
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager Library for Claude Code.
|
|
3
|
+
* Provides CRUD operations for session files stored as markdown in ~/.claude/sessions/.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Parsed metadata from a session filename */
|
|
7
|
+
export interface SessionFilenameMeta {
|
|
8
|
+
/** Original filename */
|
|
9
|
+
filename: string;
|
|
10
|
+
/** Short ID extracted from filename, or "no-id" for old format */
|
|
11
|
+
shortId: string;
|
|
12
|
+
/** Date string in YYYY-MM-DD format */
|
|
13
|
+
date: string;
|
|
14
|
+
/** Parsed Date object from the date string */
|
|
15
|
+
datetime: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Metadata parsed from session markdown content */
|
|
19
|
+
export interface SessionMetadata {
|
|
20
|
+
title: string | null;
|
|
21
|
+
date: string | null;
|
|
22
|
+
started: string | null;
|
|
23
|
+
lastUpdated: string | null;
|
|
24
|
+
completed: string[];
|
|
25
|
+
inProgress: string[];
|
|
26
|
+
notes: string;
|
|
27
|
+
context: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Statistics computed from session content */
|
|
31
|
+
export interface SessionStats {
|
|
32
|
+
totalItems: number;
|
|
33
|
+
completedItems: number;
|
|
34
|
+
inProgressItems: number;
|
|
35
|
+
lineCount: number;
|
|
36
|
+
hasNotes: boolean;
|
|
37
|
+
hasContext: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A session object returned by getAllSessions and getSessionById */
|
|
41
|
+
export interface Session extends SessionFilenameMeta {
|
|
42
|
+
/** Full filesystem path to the session file */
|
|
43
|
+
sessionPath: string;
|
|
44
|
+
/** Whether the file has any content */
|
|
45
|
+
hasContent?: boolean;
|
|
46
|
+
/** File size in bytes */
|
|
47
|
+
size: number;
|
|
48
|
+
/** Last modification time */
|
|
49
|
+
modifiedTime: Date;
|
|
50
|
+
/** File creation time (falls back to ctime on Linux) */
|
|
51
|
+
createdTime: Date;
|
|
52
|
+
/** Session markdown content (only when includeContent=true) */
|
|
53
|
+
content?: string | null;
|
|
54
|
+
/** Parsed metadata (only when includeContent=true) */
|
|
55
|
+
metadata?: SessionMetadata;
|
|
56
|
+
/** Session statistics (only when includeContent=true) */
|
|
57
|
+
stats?: SessionStats;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Pagination result from getAllSessions */
|
|
61
|
+
export interface SessionListResult {
|
|
62
|
+
sessions: Session[];
|
|
63
|
+
total: number;
|
|
64
|
+
offset: number;
|
|
65
|
+
limit: number;
|
|
66
|
+
hasMore: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GetAllSessionsOptions {
|
|
70
|
+
/** Maximum number of sessions to return (default: 50) */
|
|
71
|
+
limit?: number;
|
|
72
|
+
/** Number of sessions to skip (default: 0) */
|
|
73
|
+
offset?: number;
|
|
74
|
+
/** Filter by date in YYYY-MM-DD format */
|
|
75
|
+
date?: string | null;
|
|
76
|
+
/** Search in short ID */
|
|
77
|
+
search?: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse a session filename to extract date and short ID.
|
|
82
|
+
* @returns Parsed metadata, or null if the filename doesn't match the expected pattern
|
|
83
|
+
*/
|
|
84
|
+
export function parseSessionFilename(filename: string): SessionFilenameMeta | null;
|
|
85
|
+
|
|
86
|
+
/** Get the full filesystem path for a session filename */
|
|
87
|
+
export function getSessionPath(filename: string): string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read session markdown content from disk.
|
|
91
|
+
* @returns Content string, or null if the file doesn't exist
|
|
92
|
+
*/
|
|
93
|
+
export function getSessionContent(sessionPath: string): string | null;
|
|
94
|
+
|
|
95
|
+
/** Parse session metadata from markdown content */
|
|
96
|
+
export function parseSessionMetadata(content: string | null): SessionMetadata;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Calculate statistics for a session.
|
|
100
|
+
* Accepts either a file path (absolute, ending in .tmp) or pre-read content string.
|
|
101
|
+
* Supports both Unix (/path/to/session.tmp) and Windows (C:\path\to\session.tmp) paths.
|
|
102
|
+
*/
|
|
103
|
+
export function getSessionStats(sessionPathOrContent: string): SessionStats;
|
|
104
|
+
|
|
105
|
+
/** Get the title from a session file, or "Untitled Session" if none */
|
|
106
|
+
export function getSessionTitle(sessionPath: string): string;
|
|
107
|
+
|
|
108
|
+
/** Get human-readable file size (e.g., "1.2 KB") */
|
|
109
|
+
export function getSessionSize(sessionPath: string): string;
|
|
110
|
+
|
|
111
|
+
/** Get all sessions with optional filtering and pagination */
|
|
112
|
+
export function getAllSessions(options?: GetAllSessionsOptions): SessionListResult;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Find a session by short ID or filename.
|
|
116
|
+
* @param sessionId - Short ID prefix, full filename, or filename without .tmp
|
|
117
|
+
* @param includeContent - Whether to read and parse the session content
|
|
118
|
+
*/
|
|
119
|
+
export function getSessionById(sessionId: string, includeContent?: boolean): Session | null;
|
|
120
|
+
|
|
121
|
+
/** Write markdown content to a session file */
|
|
122
|
+
export function writeSessionContent(sessionPath: string, content: string): boolean;
|
|
123
|
+
|
|
124
|
+
/** Append content to an existing session file */
|
|
125
|
+
export function appendSessionContent(sessionPath: string, content: string): boolean;
|
|
126
|
+
|
|
127
|
+
/** Delete a session file */
|
|
128
|
+
export function deleteSession(sessionPath: string): boolean;
|
|
129
|
+
|
|
130
|
+
/** Check if a session file exists and is a regular file */
|
|
131
|
+
export function sessionExists(sessionPath: string): boolean;
|