myshell-tools 1.0.0 → 2.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.
- package/CHANGELOG.md +44 -69
- package/LICENSE +21 -21
- package/README.md +178 -318
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +130 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/cost.d.ts +36 -0
- package/dist/commands/cost.js +103 -0
- package/dist/commands/cost.js.map +1 -0
- package/dist/commands/doctor.d.ts +36 -0
- package/dist/commands/doctor.js +115 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/login.d.ts +20 -0
- package/dist/commands/login.js +60 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/core/assess.d.ts +25 -0
- package/dist/core/assess.js +142 -0
- package/dist/core/assess.js.map +1 -0
- package/dist/core/classify.d.ts +19 -0
- package/dist/core/classify.js +80 -0
- package/dist/core/classify.js.map +1 -0
- package/dist/core/escalate.d.ts +32 -0
- package/dist/core/escalate.js +57 -0
- package/dist/core/escalate.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/orchestrate.d.ts +42 -0
- package/dist/core/orchestrate.js +439 -0
- package/dist/core/orchestrate.js.map +1 -0
- package/dist/core/policy.d.ts +9 -0
- package/dist/core/policy.js +27 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/prompt.d.ts +26 -0
- package/dist/core/prompt.js +125 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/review.d.ts +46 -0
- package/dist/core/review.js +148 -0
- package/dist/core/review.js.map +1 -0
- package/dist/core/route.d.ts +28 -0
- package/dist/core/route.js +52 -0
- package/dist/core/route.js.map +1 -0
- package/dist/core/types.d.ts +141 -0
- package/dist/core/types.js +14 -0
- package/dist/core/types.js.map +1 -0
- package/dist/infra/atomic.d.ts +53 -0
- package/dist/infra/atomic.js +171 -0
- package/dist/infra/atomic.js.map +1 -0
- package/dist/infra/clock.d.ts +9 -0
- package/dist/infra/clock.js +15 -0
- package/dist/infra/clock.js.map +1 -0
- package/dist/infra/index.d.ts +9 -0
- package/dist/infra/index.js +7 -0
- package/dist/infra/index.js.map +1 -0
- package/dist/infra/ledger.d.ts +49 -0
- package/dist/infra/ledger.js +90 -0
- package/dist/infra/ledger.js.map +1 -0
- package/dist/infra/paths.d.ts +28 -0
- package/dist/infra/paths.js +38 -0
- package/dist/infra/paths.js.map +1 -0
- package/dist/infra/pricing.d.ts +47 -0
- package/dist/infra/pricing.js +151 -0
- package/dist/infra/pricing.js.map +1 -0
- package/dist/infra/session.d.ts +28 -0
- package/dist/infra/session.js +61 -0
- package/dist/infra/session.js.map +1 -0
- package/dist/interface/render.d.ts +27 -0
- package/dist/interface/render.js +134 -0
- package/dist/interface/render.js.map +1 -0
- package/dist/interface/repl.d.ts +23 -0
- package/dist/interface/repl.js +90 -0
- package/dist/interface/repl.js.map +1 -0
- package/dist/interface/run.d.ts +20 -0
- package/dist/interface/run.js +31 -0
- package/dist/interface/run.js.map +1 -0
- package/dist/providers/claude-parse.d.ts +24 -0
- package/dist/providers/claude-parse.js +113 -0
- package/dist/providers/claude-parse.js.map +1 -0
- package/dist/providers/claude.d.ts +45 -0
- package/dist/providers/claude.js +122 -0
- package/dist/providers/claude.js.map +1 -0
- package/dist/providers/codex-parse.d.ts +32 -0
- package/dist/providers/codex-parse.js +145 -0
- package/dist/providers/codex-parse.js.map +1 -0
- package/dist/providers/codex.d.ts +44 -0
- package/dist/providers/codex.js +124 -0
- package/dist/providers/codex.js.map +1 -0
- package/dist/providers/detect.d.ts +49 -0
- package/dist/providers/detect.js +125 -0
- package/dist/providers/detect.js.map +1 -0
- package/dist/providers/errors.d.ts +49 -0
- package/dist/providers/errors.js +189 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/port.d.ts +74 -0
- package/dist/providers/port.js +16 -0
- package/dist/providers/port.js.map +1 -0
- package/dist/providers/registry.d.ts +21 -0
- package/dist/providers/registry.js +34 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/ui/banner.d.ts +19 -0
- package/dist/ui/banner.js +32 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/spinner.d.ts +27 -0
- package/dist/ui/spinner.js +67 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/theme.d.ts +32 -0
- package/dist/ui/theme.js +56 -0
- package/dist/ui/theme.js.map +1 -0
- package/package.json +55 -49
- package/data/orchestrator.json +0 -113
- package/src/auth/recovery.mjs +0 -328
- package/src/auth/refresh.mjs +0 -373
- package/src/chef.mjs +0 -348
- package/src/cli/doctor.mjs +0 -568
- package/src/cli/reset.mjs +0 -447
- package/src/cli/status.mjs +0 -379
- package/src/cli.mjs +0 -429
- package/src/commands/doctor.mjs +0 -375
- package/src/commands/help.mjs +0 -324
- package/src/commands/status.mjs +0 -331
- package/src/monitor/health.mjs +0 -486
- package/src/monitor/performance.mjs +0 -442
- package/src/monitor/report.mjs +0 -535
- package/src/orchestrator/classify.mjs +0 -391
- package/src/orchestrator/confidence.mjs +0 -151
- package/src/orchestrator/handoffs.mjs +0 -231
- package/src/orchestrator/review.mjs +0 -222
- package/src/providers/balance.mjs +0 -201
- package/src/providers/claude.mjs +0 -236
- package/src/providers/codex.mjs +0 -255
- package/src/providers/detect.mjs +0 -185
- package/src/providers/errors.mjs +0 -373
- package/src/providers/select.mjs +0 -162
- package/src/repl-enhanced.mjs +0 -417
- package/src/repl.mjs +0 -321
- package/src/state/archive.mjs +0 -366
- package/src/state/atomic.mjs +0 -116
- package/src/state/cleanup.mjs +0 -440
- package/src/state/recovery.mjs +0 -461
- package/src/state/session.mjs +0 -147
- package/src/ui/errors.mjs +0 -456
- package/src/ui/formatter.mjs +0 -327
- package/src/ui/icons.mjs +0 -318
- package/src/ui/progress.mjs +0 -468
- package/templates/prompts/confidence-format.txt +0 -14
- package/templates/prompts/ic-with-feedback.txt +0 -41
- package/templates/prompts/ic.txt +0 -13
- package/templates/prompts/manager-review.txt +0 -40
- package/templates/prompts/manager.txt +0 -14
- package/templates/prompts/worker.txt +0 -12
package/src/state/recovery.mjs
DELETED
|
@@ -1,461 +0,0 @@
|
|
|
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
|
-
}
|
package/src/state/session.mjs
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
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
|
-
}
|