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,461 @@
1
+ /**
2
+ * recovery.mjs — State recovery and interrupted work resumption
3
+ */
4
+
5
+ import { existsSync, readFileSync, readdirSync, statSync, unlinkSync, mkdirSync } from 'fs';
6
+ import { join, basename } from 'path';
7
+ import { atomicWriteJSON, lockedReadModifyWrite } from './atomic.mjs';
8
+ import { loadSession, addMessage } from './session.mjs';
9
+
10
+ /**
11
+ * Plan state management
12
+ */
13
+ const PLAN_STATES = {
14
+ PENDING: 'pending',
15
+ IN_FLIGHT: 'in_flight',
16
+ COMPLETED: 'completed',
17
+ FAILED: 'failed',
18
+ INTERRUPTED: 'interrupted'
19
+ };
20
+
21
+ /**
22
+ * Get plans directory for the workspace
23
+ */
24
+ function getPlansDir(workspace = process.cwd()) {
25
+ return join(workspace, '.cortex', 'plans');
26
+ }
27
+
28
+ /**
29
+ * Get work state directory
30
+ */
31
+ function getWorkStateDir(workspace = process.cwd()) {
32
+ return join(workspace, '.cortex', 'work-state');
33
+ }
34
+
35
+ /**
36
+ * Create a plan file for tracking work
37
+ */
38
+ export function createPlan(planId, description, tasks = [], metadata = {}) {
39
+ const plansDir = getPlansDir();
40
+ if (!existsSync(plansDir)) {
41
+ mkdirSync(plansDir, { recursive: true });
42
+ }
43
+
44
+ const plan = {
45
+ id: planId,
46
+ description,
47
+ tasks,
48
+ state: PLAN_STATES.PENDING,
49
+ created: new Date().toISOString(),
50
+ updated: new Date().toISOString(),
51
+ progress: {
52
+ completed: 0,
53
+ total: tasks.length,
54
+ currentTask: null
55
+ },
56
+ metadata
57
+ };
58
+
59
+ const planPath = join(plansDir, `${planId}.json`);
60
+ atomicWriteJSON(planPath, plan);
61
+
62
+ return plan;
63
+ }
64
+
65
+ /**
66
+ * Update plan state atomically
67
+ */
68
+ export function updatePlan(planId, updates, workspace = process.cwd()) {
69
+ const plansDir = getPlansDir(workspace);
70
+ const planPath = join(plansDir, `${planId}.json`);
71
+
72
+ if (!existsSync(planPath)) {
73
+ throw new Error(`Plan ${planId} not found`);
74
+ }
75
+
76
+ return lockedReadModifyWrite(planPath, (current) => {
77
+ return {
78
+ ...current,
79
+ ...updates,
80
+ updated: new Date().toISOString()
81
+ };
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Get all plans with optional state filter
87
+ */
88
+ export function getPlans(state = null, workspace = process.cwd()) {
89
+ const plansDir = getPlansDir(workspace);
90
+
91
+ if (!existsSync(plansDir)) {
92
+ return [];
93
+ }
94
+
95
+ try {
96
+ const planFiles = readdirSync(plansDir)
97
+ .filter(f => f.endsWith('.json'))
98
+ .map(f => join(plansDir, f));
99
+
100
+ const plans = planFiles.map(file => {
101
+ try {
102
+ return JSON.parse(readFileSync(file, 'utf8'));
103
+ } catch {
104
+ return null;
105
+ }
106
+ }).filter(Boolean);
107
+
108
+ return state ? plans.filter(p => p.state === state) : plans;
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Find interrupted plans
116
+ */
117
+ export function findInterruptedPlans(workspace = process.cwd()) {
118
+ return getPlans(PLAN_STATES.IN_FLIGHT, workspace)
119
+ .concat(getPlans(PLAN_STATES.INTERRUPTED, workspace))
120
+ .sort((a, b) => new Date(b.updated) - new Date(a.updated));
121
+ }
122
+
123
+ /**
124
+ * Mark plan as interrupted (called on process exit)
125
+ */
126
+ export function markPlanInterrupted(planId, currentTask = null, workspace = process.cwd()) {
127
+ try {
128
+ updatePlan(planId, {
129
+ state: PLAN_STATES.INTERRUPTED,
130
+ progress: {
131
+ ...getPlans().find(p => p.id === planId)?.progress,
132
+ currentTask
133
+ }
134
+ }, workspace);
135
+ } catch (error) {
136
+ console.warn(`Failed to mark plan ${planId} as interrupted:`, error.message);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Archive completed or failed plans
142
+ */
143
+ export function archivePlans(planIds, workspace = process.cwd()) {
144
+ const plansDir = getPlansDir(workspace);
145
+ const archiveDir = join(plansDir, 'archive');
146
+
147
+ if (!existsSync(archiveDir)) {
148
+ mkdirSync(archiveDir, { recursive: true });
149
+ }
150
+
151
+ const archived = [];
152
+
153
+ for (const planId of planIds) {
154
+ const planPath = join(plansDir, `${planId}.json`);
155
+ const archivePath = join(archiveDir, `${planId}-${Date.now()}.json`);
156
+
157
+ try {
158
+ if (existsSync(planPath)) {
159
+ const plan = JSON.parse(readFileSync(planPath, 'utf8'));
160
+ plan.archived = new Date().toISOString();
161
+ atomicWriteJSON(archivePath, plan);
162
+ unlinkSync(planPath);
163
+ archived.push(planId);
164
+ }
165
+ } catch (error) {
166
+ console.warn(`Failed to archive plan ${planId}:`, error.message);
167
+ }
168
+ }
169
+
170
+ return archived;
171
+ }
172
+
173
+ /**
174
+ * Clean up stale lock files
175
+ */
176
+ export function cleanupStaleLocks(workspace = process.cwd()) {
177
+ const staleThresholdMs = 10 * 60 * 1000; // 10 minutes
178
+ const now = Date.now();
179
+ const cleaned = [];
180
+
181
+ function findAndCleanLocks(dir) {
182
+ if (!existsSync(dir)) return;
183
+
184
+ try {
185
+ const entries = readdirSync(dir);
186
+
187
+ for (const entry of entries) {
188
+ const path = join(dir, entry);
189
+ const stat = statSync(path);
190
+
191
+ if (stat.isDirectory()) {
192
+ findAndCleanLocks(path);
193
+ } else if (entry.endsWith('.lock')) {
194
+ const age = now - stat.mtimeMs;
195
+ if (age > staleThresholdMs) {
196
+ try {
197
+ unlinkSync(path);
198
+ cleaned.push(path);
199
+ } catch {
200
+ // Lock might be in use, skip
201
+ }
202
+ }
203
+ }
204
+ }
205
+ } catch {
206
+ // Directory access error, skip
207
+ }
208
+ }
209
+
210
+ findAndCleanLocks(join(workspace, '.cortex'));
211
+ return cleaned;
212
+ }
213
+
214
+ /**
215
+ * Recover interrupted work with user interaction
216
+ */
217
+ export async function recoverInterruptedWork(workspace = process.cwd()) {
218
+ const interrupted = findInterruptedPlans(workspace);
219
+
220
+ if (interrupted.length === 0) {
221
+ return { hasInterrupted: false, recovered: [], archived: [] };
222
+ }
223
+
224
+ console.log(`\n🔄 Found ${interrupted.length} interrupted work session(s):`);
225
+
226
+ for (const [index, plan] of interrupted.entries()) {
227
+ const age = Math.round((Date.now() - new Date(plan.updated)) / 1000 / 60);
228
+ const progress = plan.progress ? `${plan.progress.completed}/${plan.progress.total}` : 'unknown';
229
+ console.log(` ${index + 1}. ${plan.description} (${age}m ago, progress: ${progress})`);
230
+ }
231
+
232
+ // For now, provide manual recovery options
233
+ console.log('\nRecovery options:');
234
+ console.log('1. Resume the most recent session');
235
+ console.log('2. Archive all interrupted sessions and start fresh');
236
+ console.log('3. Show detailed recovery information');
237
+
238
+ // In a full implementation, you'd use readline for user input
239
+ // For now, auto-archive old sessions (>24h) and show info for recent ones
240
+ const oldSessions = interrupted.filter(p =>
241
+ Date.now() - new Date(p.updated) > 24 * 60 * 60 * 1000
242
+ );
243
+
244
+ const recentSessions = interrupted.filter(p =>
245
+ Date.now() - new Date(p.updated) <= 24 * 60 * 60 * 1000
246
+ );
247
+
248
+ let archived = [];
249
+ if (oldSessions.length > 0) {
250
+ console.log(`\n🗄️ Auto-archiving ${oldSessions.length} old session(s)...`);
251
+ archived = archivePlans(oldSessions.map(p => p.id), workspace);
252
+ }
253
+
254
+ if (recentSessions.length > 0) {
255
+ console.log('\n💡 Recent sessions available for manual recovery:');
256
+ console.log(' Use --resume <plan-id> to resume a specific session');
257
+ console.log(' Use --archive-all to archive all interrupted sessions');
258
+
259
+ for (const plan of recentSessions) {
260
+ console.log(` Plan: ${plan.id}`);
261
+ console.log(` Description: ${plan.description}`);
262
+ if (plan.progress?.currentTask) {
263
+ console.log(` Last task: ${plan.progress.currentTask}`);
264
+ }
265
+ console.log('');
266
+ }
267
+ }
268
+
269
+ return {
270
+ hasInterrupted: true,
271
+ interrupted: recentSessions,
272
+ recovered: [],
273
+ archived: archived
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Resume a specific plan
279
+ */
280
+ export function resumePlan(planId, workspace = process.cwd()) {
281
+ const plans = getPlans(null, workspace);
282
+ const plan = plans.find(p => p.id === planId);
283
+
284
+ if (!plan) {
285
+ throw new Error(`Plan ${planId} not found`);
286
+ }
287
+
288
+ if (plan.state !== PLAN_STATES.INTERRUPTED) {
289
+ throw new Error(`Plan ${planId} is not in interrupted state (current: ${plan.state})`);
290
+ }
291
+
292
+ // Update plan to in-flight
293
+ updatePlan(planId, { state: PLAN_STATES.IN_FLIGHT }, workspace);
294
+
295
+ addMessage('system', `Resuming interrupted work: ${plan.description}`, {
296
+ type: 'recovery',
297
+ planId: plan.id,
298
+ progress: plan.progress
299
+ });
300
+
301
+ return plan;
302
+ }
303
+
304
+ /**
305
+ * Validate session integrity and repair if possible
306
+ */
307
+ export function validateSessionIntegrity(workspace = process.cwd()) {
308
+ const issues = [];
309
+ const repairs = [];
310
+
311
+ try {
312
+ // Check session file integrity
313
+ const messages = loadSession(workspace);
314
+ let hasCorruption = false;
315
+
316
+ // Look for parsing errors or malformed entries
317
+ const sessionPath = join(workspace, '.cortex', 'sessions', 'current.jsonl');
318
+ if (existsSync(sessionPath)) {
319
+ const content = readFileSync(sessionPath, 'utf8');
320
+ const lines = content.trim().split('\n');
321
+
322
+ for (const [index, line] of lines.entries()) {
323
+ if (!line.trim()) continue;
324
+
325
+ try {
326
+ const message = JSON.parse(line);
327
+ if (!message.timestamp || !message.role || message.content === undefined) {
328
+ issues.push(`Invalid message format at line ${index + 1}`);
329
+ hasCorruption = true;
330
+ }
331
+ } catch {
332
+ issues.push(`JSON parsing error at line ${index + 1}`);
333
+ hasCorruption = true;
334
+ }
335
+ }
336
+ }
337
+
338
+ // Check for orphaned lock files
339
+ const staleLocks = cleanupStaleLocks(workspace);
340
+ if (staleLocks.length > 0) {
341
+ repairs.push(`Cleaned ${staleLocks.length} stale lock file(s)`);
342
+ }
343
+
344
+ // Check for corrupted state files
345
+ const stateDir = join(workspace, '.cortex');
346
+ if (existsSync(stateDir)) {
347
+ const stateFiles = readdirSync(stateDir)
348
+ .filter(f => f.endsWith('.json'))
349
+ .map(f => join(stateDir, f));
350
+
351
+ for (const file of stateFiles) {
352
+ try {
353
+ JSON.parse(readFileSync(file, 'utf8'));
354
+ } catch {
355
+ issues.push(`Corrupted state file: ${basename(file)}`);
356
+ }
357
+ }
358
+ }
359
+
360
+ } catch (error) {
361
+ issues.push(`Session validation failed: ${error.message}`);
362
+ }
363
+
364
+ return {
365
+ valid: issues.length === 0,
366
+ issues,
367
+ repairs,
368
+ canRecover: issues.length > 0 && issues.every(issue =>
369
+ issue.includes('stale lock') || issue.includes('orphaned')
370
+ )
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Create checkpoint for current state
376
+ */
377
+ export function createCheckpoint(description, metadata = {}, workspace = process.cwd()) {
378
+ const checkpointDir = join(workspace, '.cortex', 'checkpoints');
379
+ if (!existsSync(checkpointDir)) {
380
+ mkdirSync(checkpointDir, { recursive: true });
381
+ }
382
+
383
+ const checkpoint = {
384
+ id: `checkpoint-${Date.now()}`,
385
+ description,
386
+ timestamp: new Date().toISOString(),
387
+ session: loadSession(workspace),
388
+ plans: getPlans(null, workspace),
389
+ metadata
390
+ };
391
+
392
+ const checkpointPath = join(checkpointDir, `${checkpoint.id}.json`);
393
+ atomicWriteJSON(checkpointPath, checkpoint);
394
+
395
+ return checkpoint;
396
+ }
397
+
398
+ /**
399
+ * Setup graceful shutdown handlers
400
+ */
401
+ export function setupGracefulShutdown(currentPlanId = null) {
402
+ const handleShutdown = (signal) => {
403
+ console.log(`\n🛑 Received ${signal}, saving state...`);
404
+
405
+ if (currentPlanId) {
406
+ markPlanInterrupted(currentPlanId);
407
+ }
408
+
409
+ // Create emergency checkpoint
410
+ try {
411
+ createCheckpoint(`Emergency checkpoint on ${signal}`, { signal, emergency: true });
412
+ console.log('✅ State saved successfully');
413
+ } catch (error) {
414
+ console.warn('⚠️ Failed to save state:', error.message);
415
+ }
416
+
417
+ process.exit(0);
418
+ };
419
+
420
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
421
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
422
+
423
+ // Handle uncaught exceptions
424
+ process.on('uncaughtException', (error) => {
425
+ console.error('\n💥 Uncaught exception:', error);
426
+
427
+ if (currentPlanId) {
428
+ markPlanInterrupted(currentPlanId);
429
+ }
430
+
431
+ try {
432
+ createCheckpoint(`Emergency checkpoint on uncaught exception`, {
433
+ error: error.message,
434
+ stack: error.stack,
435
+ emergency: true
436
+ });
437
+ console.log('✅ Emergency state saved');
438
+ } catch {
439
+ console.warn('⚠️ Failed to save emergency state');
440
+ }
441
+
442
+ process.exit(1);
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Recovery status summary
448
+ */
449
+ export function getRecoveryStatus(workspace = process.cwd()) {
450
+ const interrupted = findInterruptedPlans(workspace);
451
+ const integrity = validateSessionIntegrity(workspace);
452
+
453
+ return {
454
+ hasInterruptedWork: interrupted.length > 0,
455
+ interruptedCount: interrupted.length,
456
+ sessionIntegrity: integrity.valid,
457
+ issues: integrity.issues,
458
+ lastInterrupted: interrupted[0]?.updated || null,
459
+ canAutoRecover: integrity.canRecover && interrupted.length === 0
460
+ };
461
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * session.mjs — Session persistence and management
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { atomicAppendJSONL, atomicWriteJSON } from './atomic.mjs';
8
+
9
+ /**
10
+ * Get the session directory for the current working directory
11
+ */
12
+ function getSessionDir(cwd = process.cwd()) {
13
+ return join(cwd, '.cortex', 'sessions');
14
+ }
15
+
16
+ /**
17
+ * Get the current session file path
18
+ */
19
+ function getCurrentSessionPath(cwd = process.cwd()) {
20
+ const sessionDir = getSessionDir(cwd);
21
+ return join(sessionDir, 'current.jsonl');
22
+ }
23
+
24
+ /**
25
+ * Ensure session directory exists
26
+ */
27
+ function ensureSessionDir(cwd = process.cwd()) {
28
+ const sessionDir = getSessionDir(cwd);
29
+ if (!existsSync(sessionDir)) {
30
+ mkdirSync(sessionDir, { recursive: true });
31
+ }
32
+ return sessionDir;
33
+ }
34
+
35
+ /**
36
+ * Add a message to the current session
37
+ */
38
+ export function addMessage(role, content, metadata = {}) {
39
+ ensureSessionDir();
40
+ const sessionPath = getCurrentSessionPath();
41
+
42
+ const message = {
43
+ timestamp: new Date().toISOString(),
44
+ role, // 'user', 'assistant', or 'system'
45
+ content,
46
+ ...metadata
47
+ };
48
+
49
+ atomicAppendJSONL(sessionPath, message);
50
+ }
51
+
52
+ /**
53
+ * Load the current session messages
54
+ */
55
+ export function loadSession(cwd = process.cwd()) {
56
+ const sessionPath = getCurrentSessionPath(cwd);
57
+
58
+ if (!existsSync(sessionPath)) {
59
+ return [];
60
+ }
61
+
62
+ try {
63
+ const content = readFileSync(sessionPath, 'utf8');
64
+ return content
65
+ .trim()
66
+ .split('\n')
67
+ .filter(line => line.trim())
68
+ .map(line => JSON.parse(line));
69
+ } catch (err) {
70
+ console.warn(`Failed to load session: ${err.message}`);
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Archive the current session and start a new one
77
+ */
78
+ export function archiveSession(cwd = process.cwd()) {
79
+ const sessionDir = ensureSessionDir(cwd);
80
+ const currentPath = getCurrentSessionPath(cwd);
81
+
82
+ if (!existsSync(currentPath)) {
83
+ return null; // No current session to archive
84
+ }
85
+
86
+ // Create archive filename with timestamp
87
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
88
+ const archiveDir = join(sessionDir, 'archive');
89
+ if (!existsSync(archiveDir)) {
90
+ mkdirSync(archiveDir, { recursive: true });
91
+ }
92
+
93
+ const archivePath = join(archiveDir, `session-${timestamp}.jsonl`);
94
+
95
+ try {
96
+ // Move current session to archive
97
+ const content = readFileSync(currentPath, 'utf8');
98
+ writeFileSync(archivePath, content);
99
+
100
+ // Remove current session file
101
+ writeFileSync(currentPath, '');
102
+
103
+ return archivePath;
104
+ } catch (err) {
105
+ console.warn(`Failed to archive session: ${err.message}`);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get session summary for display
112
+ */
113
+ export function getSessionSummary(cwd = process.cwd()) {
114
+ const messages = loadSession(cwd);
115
+
116
+ if (messages.length === 0) {
117
+ return { messageCount: 0, firstMessage: null, lastMessage: null };
118
+ }
119
+
120
+ const userMessages = messages.filter(m => m.role === 'user');
121
+ const assistantMessages = messages.filter(m => m.role === 'assistant');
122
+
123
+ return {
124
+ messageCount: messages.length,
125
+ userMessageCount: userMessages.length,
126
+ assistantMessageCount: assistantMessages.length,
127
+ firstMessage: messages[0],
128
+ lastMessage: messages[messages.length - 1],
129
+ duration: messages.length > 1 ?
130
+ new Date(messages[messages.length - 1].timestamp).getTime() -
131
+ new Date(messages[0].timestamp).getTime() : 0
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Add a handoff event to the session for transparency
137
+ */
138
+ export function addHandoff(operation, fromTier, toTier, reason, metadata = {}) {
139
+ addMessage('system', `HANDOFF: ${operation} from ${fromTier} to ${toTier}`, {
140
+ type: 'handoff',
141
+ operation, // 'delegate', 'escalate', 'bounce'
142
+ fromTier,
143
+ toTier,
144
+ reason,
145
+ ...metadata
146
+ });
147
+ }