myshell-tools 1.0.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +318 -0
  4. package/data/orchestrator.json +113 -0
  5. package/package.json +49 -0
  6. package/src/auth/recovery.mjs +328 -0
  7. package/src/auth/refresh.mjs +373 -0
  8. package/src/chef.mjs +348 -0
  9. package/src/cli/doctor.mjs +568 -0
  10. package/src/cli/reset.mjs +447 -0
  11. package/src/cli/status.mjs +379 -0
  12. package/src/cli.mjs +429 -0
  13. package/src/commands/doctor.mjs +375 -0
  14. package/src/commands/help.mjs +324 -0
  15. package/src/commands/status.mjs +331 -0
  16. package/src/monitor/health.mjs +486 -0
  17. package/src/monitor/performance.mjs +442 -0
  18. package/src/monitor/report.mjs +535 -0
  19. package/src/orchestrator/classify.mjs +391 -0
  20. package/src/orchestrator/confidence.mjs +151 -0
  21. package/src/orchestrator/handoffs.mjs +231 -0
  22. package/src/orchestrator/review.mjs +222 -0
  23. package/src/providers/balance.mjs +201 -0
  24. package/src/providers/claude.mjs +236 -0
  25. package/src/providers/codex.mjs +255 -0
  26. package/src/providers/detect.mjs +185 -0
  27. package/src/providers/errors.mjs +373 -0
  28. package/src/providers/select.mjs +162 -0
  29. package/src/repl-enhanced.mjs +417 -0
  30. package/src/repl.mjs +321 -0
  31. package/src/state/archive.mjs +366 -0
  32. package/src/state/atomic.mjs +116 -0
  33. package/src/state/cleanup.mjs +440 -0
  34. package/src/state/recovery.mjs +461 -0
  35. package/src/state/session.mjs +147 -0
  36. package/src/ui/errors.mjs +456 -0
  37. package/src/ui/formatter.mjs +327 -0
  38. package/src/ui/icons.mjs +318 -0
  39. package/src/ui/progress.mjs +468 -0
  40. package/templates/prompts/confidence-format.txt +14 -0
  41. package/templates/prompts/ic-with-feedback.txt +41 -0
  42. package/templates/prompts/ic.txt +13 -0
  43. package/templates/prompts/manager-review.txt +40 -0
  44. package/templates/prompts/manager.txt +14 -0
  45. package/templates/prompts/worker.txt +12 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * atomic.mjs — Atomic file operations for safe concurrent access
3
+ * Adapted from archive/dual-brain/hooks/atomic-write.mjs
4
+ */
5
+
6
+ import { openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, statSync } from 'fs';
7
+ import { constants } from 'fs';
8
+
9
+ const LOCK_TIMEOUT_MS = 5000;
10
+ const STALE_LOCK_MS = 10000;
11
+
12
+ /**
13
+ * Atomically write JSON data to filePath using tmp-file + rename
14
+ * Tmp file is in the same directory to avoid cross-device rename issues
15
+ */
16
+ export function atomicWriteJSON(filePath, data) {
17
+ const tmp = filePath + '.tmp.' + process.pid;
18
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
19
+ renameSync(tmp, filePath);
20
+ }
21
+
22
+ /**
23
+ * Atomically append a line to a JSONL file
24
+ */
25
+ export function atomicAppendJSONL(filePath, data) {
26
+ const line = JSON.stringify(data) + '\n';
27
+ const tmp = filePath + '.tmp.' + process.pid;
28
+
29
+ // Read existing content if file exists
30
+ let existing = '';
31
+ try {
32
+ existing = readFileSync(filePath, 'utf8');
33
+ } catch {
34
+ // File doesn't exist, start with empty
35
+ }
36
+
37
+ // Write existing + new line to tmp file
38
+ writeFileSync(tmp, existing + line);
39
+ renameSync(tmp, filePath);
40
+ }
41
+
42
+ /**
43
+ * Acquire a .lock file using O_EXCL for atomic creation
44
+ * Returns true if lock acquired, false otherwise
45
+ * Steals stale locks (older than STALE_LOCK_MS)
46
+ */
47
+ function acquireLock(lockPath) {
48
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
49
+
50
+ while (Date.now() < deadline) {
51
+ try {
52
+ const fd = openSync(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
53
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
54
+ closeSync(fd);
55
+ return true;
56
+ } catch (err) {
57
+ if (err.code !== 'EEXIST') throw err;
58
+
59
+ // Check for stale lock
60
+ try {
61
+ const stat = statSync(lockPath);
62
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
63
+ // Stale lock — process likely died, steal it
64
+ try { unlinkSync(lockPath); } catch {}
65
+ continue;
66
+ }
67
+ } catch {
68
+ // Lock disappeared between our check — retry
69
+ continue;
70
+ }
71
+
72
+ // Wait briefly before retrying
73
+ const waitMs = 10 + Math.floor(Math.random() * 20);
74
+ const end = Date.now() + waitMs;
75
+ while (Date.now() < end) { /* spin */ }
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+
81
+ function releaseLock(lockPath) {
82
+ try { unlinkSync(lockPath); } catch {}
83
+ }
84
+
85
+ /**
86
+ * Locked read-modify-write cycle
87
+ *
88
+ * 1. Acquire .lock file (O_EXCL atomic creation)
89
+ * 2. Read current JSON (or use defaultValue if missing/corrupt)
90
+ * 3. Call modifyFn(currentData) → newData
91
+ * 4. Atomic write newData via tmp+rename
92
+ * 5. Release lock
93
+ */
94
+ export function lockedReadModifyWrite(filePath, modifyFn, defaultValue = {}) {
95
+ const lockPath = filePath + '.lock';
96
+ const locked = acquireLock(lockPath);
97
+
98
+ if (!locked) {
99
+ throw new Error(`Lock acquisition timed out after ${LOCK_TIMEOUT_MS}ms for ${filePath}`);
100
+ }
101
+
102
+ try {
103
+ let current;
104
+ try {
105
+ current = JSON.parse(readFileSync(filePath, 'utf8'));
106
+ } catch {
107
+ current = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
108
+ }
109
+
110
+ const updated = modifyFn(current);
111
+ atomicWriteJSON(filePath, updated);
112
+ return updated;
113
+ } finally {
114
+ releaseLock(lockPath);
115
+ }
116
+ }
@@ -0,0 +1,440 @@
1
+ /**
2
+ * cleanup.mjs — State maintenance and cleanup operations
3
+ */
4
+
5
+ import { existsSync, readFileSync, readdirSync, statSync, unlinkSync, rmSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { cleanupOldArchives } from './archive.mjs';
8
+
9
+ // Temporary mock for getStorageStats until it's implemented
10
+ function getStorageStats(workspace) {
11
+ return {
12
+ totalSize: 0,
13
+ fileCount: 0,
14
+ sessionCount: 0,
15
+ archiveCount: 0
16
+ };
17
+ }
18
+
19
+ export { getStorageStats };
20
+ import { cleanupStaleLocks, validateSessionIntegrity } from './recovery.mjs';
21
+
22
+ /**
23
+ * Comprehensive state cleanup
24
+ */
25
+ export function performStateCleanup(options = {}) {
26
+ const {
27
+ workspace = process.cwd(),
28
+ maxArchiveAge = 30, // days
29
+ cleanLocks = true,
30
+ cleanTemps = true,
31
+ validateSessions = true,
32
+ verbose = false
33
+ } = options;
34
+
35
+ const results = {
36
+ startTime: new Date().toISOString(),
37
+ workspace,
38
+ operations: [],
39
+ errors: [],
40
+ beforeStats: getStorageStats(workspace),
41
+ afterStats: null
42
+ };
43
+
44
+ function log(operation, details) {
45
+ results.operations.push({ operation, ...details, timestamp: new Date().toISOString() });
46
+ if (verbose) {
47
+ console.log(`🧹 ${operation}: ${details.message || 'completed'}`);
48
+ }
49
+ }
50
+
51
+ function error(operation, err) {
52
+ const errorMsg = err.message || err.toString();
53
+ results.errors.push({ operation, error: errorMsg, timestamp: new Date().toISOString() });
54
+ if (verbose) {
55
+ console.warn(`❌ ${operation}: ${errorMsg}`);
56
+ }
57
+ }
58
+
59
+ try {
60
+ // Clean up stale locks
61
+ if (cleanLocks) {
62
+ try {
63
+ const staleLocks = cleanupStaleLocks(workspace);
64
+ log('Lock Cleanup', {
65
+ message: `Removed ${staleLocks.length} stale lock files`,
66
+ count: staleLocks.length,
67
+ files: staleLocks
68
+ });
69
+ } catch (err) {
70
+ error('Lock Cleanup', err);
71
+ }
72
+ }
73
+
74
+ // Clean up temporary files
75
+ if (cleanTemps) {
76
+ try {
77
+ const tempCount = cleanupTempFiles(workspace);
78
+ log('Temp File Cleanup', {
79
+ message: `Removed ${tempCount} temporary files`,
80
+ count: tempCount
81
+ });
82
+ } catch (err) {
83
+ error('Temp File Cleanup', err);
84
+ }
85
+ }
86
+
87
+ // Clean old archives
88
+ try {
89
+ const archiveCleanup = cleanupOldArchives(maxArchiveAge, workspace);
90
+ log('Archive Cleanup', {
91
+ message: `Deleted ${archiveCleanup.deleted} old archives, preserved ${archiveCleanup.preserved}`,
92
+ deleted: archiveCleanup.deleted,
93
+ preserved: archiveCleanup.preserved,
94
+ errors: archiveCleanup.errors
95
+ });
96
+
97
+ if (archiveCleanup.errors.length > 0) {
98
+ for (const err of archiveCleanup.errors) {
99
+ error('Archive Cleanup', { message: err });
100
+ }
101
+ }
102
+ } catch (err) {
103
+ error('Archive Cleanup', err);
104
+ }
105
+
106
+ // Validate session integrity
107
+ if (validateSessions) {
108
+ try {
109
+ const integrity = validateSessionIntegrity(workspace);
110
+ log('Session Validation', {
111
+ message: `Session validation ${integrity.valid ? 'passed' : 'failed'}`,
112
+ valid: integrity.valid,
113
+ issues: integrity.issues,
114
+ repairs: integrity.repairs
115
+ });
116
+
117
+ if (!integrity.valid) {
118
+ for (const issue of integrity.issues) {
119
+ error('Session Validation', { message: issue });
120
+ }
121
+ }
122
+ } catch (err) {
123
+ error('Session Validation', err);
124
+ }
125
+ }
126
+
127
+ // Clean up orphaned plan files
128
+ try {
129
+ const orphanCount = cleanupOrphanedPlans(workspace);
130
+ log('Plan Cleanup', {
131
+ message: `Removed ${orphanCount} orphaned plan files`,
132
+ count: orphanCount
133
+ });
134
+ } catch (err) {
135
+ error('Plan Cleanup', err);
136
+ }
137
+
138
+ // Final storage stats
139
+ results.afterStats = getStorageStats(workspace);
140
+ results.spaceSaved = results.beforeStats.totalSize - results.afterStats.totalSize;
141
+
142
+ log('Cleanup Complete', {
143
+ message: `Cleanup completed, saved ${formatBytes(results.spaceSaved)}`,
144
+ spaceSaved: results.spaceSaved,
145
+ operationCount: results.operations.length,
146
+ errorCount: results.errors.length
147
+ });
148
+
149
+ } catch (globalError) {
150
+ error('Global Cleanup', globalError);
151
+ }
152
+
153
+ results.endTime = new Date().toISOString();
154
+ results.duration = new Date(results.endTime) - new Date(results.startTime);
155
+
156
+ return results;
157
+ }
158
+
159
+ /**
160
+ * Clean up temporary files (.tmp, .lock, etc.)
161
+ */
162
+ function cleanupTempFiles(workspace) {
163
+ const cortexDir = join(workspace, '.cortex');
164
+ if (!existsSync(cortexDir)) return 0;
165
+
166
+ let count = 0;
167
+
168
+ function cleanDir(dir) {
169
+ try {
170
+ const entries = readdirSync(dir);
171
+
172
+ for (const entry of entries) {
173
+ const fullPath = join(dir, entry);
174
+ const stat = statSync(fullPath);
175
+
176
+ if (stat.isDirectory()) {
177
+ cleanDir(fullPath);
178
+ } else if (isTempFile(entry)) {
179
+ try {
180
+ unlinkSync(fullPath);
181
+ count++;
182
+ } catch {
183
+ // File might be in use
184
+ }
185
+ }
186
+ }
187
+ } catch {
188
+ // Directory access error
189
+ }
190
+ }
191
+
192
+ cleanDir(cortexDir);
193
+ return count;
194
+ }
195
+
196
+ /**
197
+ * Check if file is temporary
198
+ */
199
+ function isTempFile(filename) {
200
+ return filename.endsWith('.tmp') ||
201
+ filename.includes('.tmp.') ||
202
+ filename.endsWith('.bak') ||
203
+ filename.endsWith('~') ||
204
+ filename.startsWith('.#');
205
+ }
206
+
207
+ /**
208
+ * Clean up orphaned plan files
209
+ */
210
+ function cleanupOrphanedPlans(workspace) {
211
+ const plansDir = join(workspace, '.cortex', 'plans');
212
+ if (!existsSync(plansDir)) return 0;
213
+
214
+ let count = 0;
215
+ const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
216
+
217
+ try {
218
+ const planFiles = readdirSync(plansDir)
219
+ .filter(f => f.endsWith('.json'))
220
+ .map(f => join(plansDir, f));
221
+
222
+ for (const file of planFiles) {
223
+ try {
224
+ const plan = JSON.parse(readFileSync(file, 'utf8'));
225
+ const updated = new Date(plan.updated || plan.created).getTime();
226
+
227
+ // Remove plans that are old and completed/failed
228
+ if (updated < oneWeekAgo && (plan.state === 'completed' || plan.state === 'failed')) {
229
+ unlinkSync(file);
230
+ count++;
231
+ }
232
+ } catch {
233
+ // Invalid plan file, remove it
234
+ try {
235
+ unlinkSync(file);
236
+ count++;
237
+ } catch {
238
+ // Can't remove, skip
239
+ }
240
+ }
241
+ }
242
+ } catch {
243
+ // Directory access error
244
+ }
245
+
246
+ return count;
247
+ }
248
+
249
+ /**
250
+ * Reset all state (nuclear option)
251
+ */
252
+ export function resetAllState(workspace = process.cwd(), preserveAuth = true) {
253
+ const cortexDir = join(workspace, '.cortex');
254
+
255
+ if (!existsSync(cortexDir)) {
256
+ return { reset: false, reason: 'No .cortex directory found' };
257
+ }
258
+
259
+ const results = {
260
+ reset: true,
261
+ preservedAuth: preserveAuth,
262
+ removedDirs: [],
263
+ preservedDirs: [],
264
+ errors: []
265
+ };
266
+
267
+ try {
268
+ const entries = readdirSync(cortexDir);
269
+
270
+ for (const entry of entries) {
271
+ const fullPath = join(cortexDir, entry);
272
+ const stat = statSync(fullPath);
273
+
274
+ if (stat.isDirectory()) {
275
+ if (preserveAuth && entry === 'auth') {
276
+ results.preservedDirs.push(entry);
277
+ } else {
278
+ try {
279
+ rmSync(fullPath, { recursive: true, force: true });
280
+ results.removedDirs.push(entry);
281
+ } catch (error) {
282
+ results.errors.push(`Failed to remove ${entry}: ${error.message}`);
283
+ }
284
+ }
285
+ } else {
286
+ // Remove loose files
287
+ try {
288
+ unlinkSync(fullPath);
289
+ } catch (error) {
290
+ results.errors.push(`Failed to remove file ${entry}: ${error.message}`);
291
+ }
292
+ }
293
+ }
294
+
295
+ } catch (error) {
296
+ results.errors.push(`Failed to read .cortex directory: ${error.message}`);
297
+ }
298
+
299
+ return results;
300
+ }
301
+
302
+ /**
303
+ * Get detailed cleanup status and recommendations
304
+ */
305
+ export function getCleanupStatus(workspace = process.cwd()) {
306
+ const stats = getStorageStats(workspace);
307
+ const cortexDir = join(workspace, '.cortex');
308
+
309
+ const status = {
310
+ totalSize: stats.totalSize,
311
+ breakdown: stats.breakdown,
312
+ recommendations: [],
313
+ issues: []
314
+ };
315
+
316
+ // Check for large archive directories
317
+ if (stats.breakdown.archives > 10 * 1024 * 1024) { // > 10MB
318
+ status.recommendations.push({
319
+ type: 'cleanup',
320
+ priority: 'medium',
321
+ message: `Archive directory is ${formatBytes(stats.breakdown.archives)}. Consider cleaning old archives.`,
322
+ action: 'Run cleanup with archive age limit'
323
+ });
324
+ }
325
+
326
+ // Check for many session files
327
+ const sessionDir = join(cortexDir, 'sessions');
328
+ if (existsSync(sessionDir)) {
329
+ try {
330
+ const sessionFiles = readdirSync(sessionDir).length;
331
+ if (sessionFiles > 50) {
332
+ status.recommendations.push({
333
+ type: 'archive',
334
+ priority: 'low',
335
+ message: `${sessionFiles} session files found. Consider archiving old sessions.`,
336
+ action: 'Archive completed sessions'
337
+ });
338
+ }
339
+ } catch {}
340
+ }
341
+
342
+ // Check for stale locks
343
+ const staleLocks = findStaleLocks(workspace);
344
+ if (staleLocks.length > 0) {
345
+ status.issues.push({
346
+ type: 'locks',
347
+ severity: 'warning',
348
+ message: `${staleLocks.length} stale lock files found`,
349
+ action: 'Run cleanup to remove stale locks'
350
+ });
351
+ }
352
+
353
+ // Check session integrity
354
+ try {
355
+ const integrity = validateSessionIntegrity(workspace);
356
+ if (!integrity.valid) {
357
+ status.issues.push({
358
+ type: 'integrity',
359
+ severity: 'error',
360
+ message: `Session integrity issues: ${integrity.issues.join(', ')}`,
361
+ action: 'Run cleanup with session validation'
362
+ });
363
+ }
364
+ } catch {}
365
+
366
+ return status;
367
+ }
368
+
369
+ /**
370
+ * Find stale lock files
371
+ */
372
+ function findStaleLocks(workspace) {
373
+ const staleLocks = [];
374
+ const staleThreshold = 10 * 60 * 1000; // 10 minutes
375
+ const now = Date.now();
376
+
377
+ function findLocks(dir) {
378
+ if (!existsSync(dir)) return;
379
+
380
+ try {
381
+ const entries = readdirSync(dir);
382
+
383
+ for (const entry of entries) {
384
+ const fullPath = join(dir, entry);
385
+ const stat = statSync(fullPath);
386
+
387
+ if (stat.isDirectory()) {
388
+ findLocks(fullPath);
389
+ } else if (entry.endsWith('.lock')) {
390
+ const age = now - stat.mtimeMs;
391
+ if (age > staleThreshold) {
392
+ staleLocks.push(fullPath);
393
+ }
394
+ }
395
+ }
396
+ } catch {}
397
+ }
398
+
399
+ findLocks(join(workspace, '.cortex'));
400
+ return staleLocks;
401
+ }
402
+
403
+ /**
404
+ * Format bytes for human-readable display
405
+ */
406
+ function formatBytes(bytes) {
407
+ if (bytes === 0) return '0 B';
408
+
409
+ const k = 1024;
410
+ const sizes = ['B', 'KB', 'MB', 'GB'];
411
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
412
+
413
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
414
+ }
415
+
416
+ /**
417
+ * Schedule automatic cleanup
418
+ */
419
+ export function scheduleAutoCleanup(interval = 24 * 60 * 60 * 1000, options = {}) {
420
+ const cleanup = () => {
421
+ console.log('🧹 Running automatic state cleanup...');
422
+ const results = performStateCleanup({ ...options, verbose: false });
423
+
424
+ if (results.errors.length > 0) {
425
+ console.warn(`⚠️ Cleanup completed with ${results.errors.length} errors`);
426
+ } else {
427
+ console.log(`✅ Cleanup completed, saved ${formatBytes(results.spaceSaved)}`);
428
+ }
429
+ };
430
+
431
+ // Run cleanup on interval
432
+ const intervalId = setInterval(cleanup, interval);
433
+
434
+ // Also run cleanup on process exit
435
+ process.on('exit', () => {
436
+ clearInterval(intervalId);
437
+ });
438
+
439
+ return intervalId;
440
+ }