codehere 0.5.0-alpha.3 → 0.5.0-alpha.4
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/dist/index.js +55 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +30 -7
- package/dist/infrastructure/protocols/mcp-server-impl.js +5 -0
- package/dist/infrastructure/protocols/ui-page.js +6 -18
- package/dist/infrastructure/storage/project-thread-migration.js +232 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Command } from 'commander';
|
|
|
14
14
|
import { config } from 'dotenv';
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
+
import { homedir } from 'os';
|
|
17
18
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
18
19
|
import { executeInitCommand } from './presentation/cli/commands/init-command.js';
|
|
19
20
|
import { executeMemoryCommand } from './presentation/cli/commands/memory-command.js';
|
|
@@ -428,6 +429,25 @@ program
|
|
|
428
429
|
}
|
|
429
430
|
};
|
|
430
431
|
try {
|
|
432
|
+
// v0.6.0 M1+M7 — migrate sessions.jsonl → ~/.codehere/projects/<slug>/thread.jsonl
|
|
433
|
+
// on daemon start. Idempotent: skips projects whose thread.jsonl already exists,
|
|
434
|
+
// so this is safe to run on every boot. Opt-out via CODEHERE_DISABLE_THREAD_MIGRATION=1.
|
|
435
|
+
// Failures are non-blocking — back-compat to v0.5 sessions.jsonl is preserved.
|
|
436
|
+
if (process.env.CODEHERE_DISABLE_THREAD_MIGRATION !== '1') {
|
|
437
|
+
try {
|
|
438
|
+
const { migrate } = await import('./infrastructure/storage/project-thread-migration.js');
|
|
439
|
+
const result = migrate({
|
|
440
|
+
sourceJsonl: join(homedir(), '.codehere/sessions.jsonl'),
|
|
441
|
+
targetDir: join(homedir(), '.codehere/projects'),
|
|
442
|
+
});
|
|
443
|
+
if (result.threadsWritten > 0) {
|
|
444
|
+
console.log(colors.dim(` v0.6 migration: created ${result.threadsWritten} project thread(s) from ${result.sessionsProcessed} sessions (${result.durationMs}ms)`));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
console.warn(colors.dim(` v0.6 migration skipped: ${err instanceof Error ? err.message : String(err)}`));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
431
451
|
const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
|
|
432
452
|
const server = startMCPServer({ port, host });
|
|
433
453
|
// Wait for the server to either start listening or fail. The default
|
|
@@ -597,6 +617,41 @@ program
|
|
|
597
617
|
r.end();
|
|
598
618
|
});
|
|
599
619
|
});
|
|
620
|
+
// ── migrate-threads — v0.6.0 schema migration sessions.jsonl → projects/<slug>/thread.jsonl
|
|
621
|
+
program
|
|
622
|
+
.command('migrate-threads', { hidden: true }) // hidden from --help (v0.6.0 internal — auto-runs on daemon start once wired)
|
|
623
|
+
.description('Migrate sessions.jsonl into per-project thread.jsonl files (v0.6.0 schema)')
|
|
624
|
+
.option('--source <path>', 'Source sessions.jsonl', `${homedir()}/.codehere/sessions.jsonl`)
|
|
625
|
+
.option('--target <dir>', 'Target projects dir', `${homedir()}/.codehere/projects`)
|
|
626
|
+
.option('--dry-run', 'Report what migrate WOULD do, write nothing', false)
|
|
627
|
+
.option('--rollback', 'Remove the projects dir; sessions.jsonl untouched', false)
|
|
628
|
+
.option('--keep-temp-dirs', 'Include /tmp + /var/folders cwds (default skips them)', false)
|
|
629
|
+
.action(async (options) => {
|
|
630
|
+
const { migrate, dryRun, rollback } = await import('./infrastructure/storage/project-thread-migration.js');
|
|
631
|
+
if (options.rollback) {
|
|
632
|
+
const r = rollback(options.target);
|
|
633
|
+
console.log(`Rollback: removed ${r.removedDirs} project dir(s) from ${options.target}`);
|
|
634
|
+
console.log(`Source ${options.source} untouched.`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const fn = options.dryRun ? dryRun : migrate;
|
|
638
|
+
const label = options.dryRun ? 'DRY RUN' : 'MIGRATION';
|
|
639
|
+
const result = fn({
|
|
640
|
+
sourceJsonl: options.source,
|
|
641
|
+
targetDir: options.target,
|
|
642
|
+
skipTempDirs: !options.keepTempDirs,
|
|
643
|
+
});
|
|
644
|
+
console.log(`=== ${label} ===`);
|
|
645
|
+
console.log(` source: ${options.source}`);
|
|
646
|
+
console.log(` target: ${options.target}`);
|
|
647
|
+
console.log(` sessions processed: ${result.sessionsProcessed}`);
|
|
648
|
+
console.log(` threads ${options.dryRun ? 'would write' : 'written'}: ${result.threadsWritten}`);
|
|
649
|
+
console.log(` threads skipped (exist): ${result.threadsSkippedExisting}`);
|
|
650
|
+
console.log(` unknown-cwd sessions: ${result.unknownCwdSessions}`);
|
|
651
|
+
console.log(` abandoned sessions: ${result.abandonedSessions}`);
|
|
652
|
+
console.log(` temp-dir sessions skipped: ${result.tempDirSessionsSkipped}`);
|
|
653
|
+
console.log(` duration: ${result.durationMs}ms`);
|
|
654
|
+
});
|
|
600
655
|
// ── scan-tool (called by Claude Code hooks system, not users directly) ───────
|
|
601
656
|
program
|
|
602
657
|
.command('scan-tool', { hidden: true }) // hidden from --help (May 2 cut: internal hook handler, called by Claude Code via stdin)
|
|
@@ -221,6 +221,12 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
221
221
|
}
|
|
222
222
|
// ── stdout: newline-delimited JSON from `--output-format stream-json` ────────
|
|
223
223
|
let stdoutBuf = '';
|
|
224
|
+
// Iter 13 — capture the claude-internal session_id from the init event so
|
|
225
|
+
// appendMessage can use `--resume <session-id>` (reliable) instead of `-c`
|
|
226
|
+
// (which picks "most recent in cwd" and can collide with other concurrent
|
|
227
|
+
// claude sessions OR with conversations containing thinking blocks the
|
|
228
|
+
// headless API can't replay).
|
|
229
|
+
let claudeSessionId;
|
|
224
230
|
child.stdout?.on('data', (chunk) => {
|
|
225
231
|
stdoutBuf += chunk.toString('utf-8');
|
|
226
232
|
let idx;
|
|
@@ -231,6 +237,12 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
231
237
|
continue;
|
|
232
238
|
try {
|
|
233
239
|
const data = JSON.parse(line);
|
|
240
|
+
if (claudeSessionId === undefined &&
|
|
241
|
+
data.type === 'system' &&
|
|
242
|
+
data.subtype === 'init' &&
|
|
243
|
+
typeof data.session_id === 'string') {
|
|
244
|
+
claudeSessionId = data.session_id;
|
|
245
|
+
}
|
|
234
246
|
emit({ type: data.type ?? 'message', timestamp: Date.now(), data });
|
|
235
247
|
}
|
|
236
248
|
catch {
|
|
@@ -319,20 +331,31 @@ export function spawnClaudeCode(prompt, options = {}) {
|
|
|
319
331
|
}, 3000);
|
|
320
332
|
},
|
|
321
333
|
/**
|
|
322
|
-
* Iter 12 — follow-up message to a completed session.
|
|
323
|
-
*
|
|
324
|
-
*
|
|
334
|
+
* Iter 12 (with iter-13 fix) — follow-up message to a completed session.
|
|
335
|
+
*
|
|
336
|
+
* Iter 12 used `claude -p "<text>" -c` which picks "most recent
|
|
337
|
+
* conversation in current directory". Iter 13 dogfood proved this
|
|
338
|
+
* fragile: in a cwd with multiple concurrent claude sessions OR with
|
|
339
|
+
* conversations containing thinking blocks, -c either picks the wrong
|
|
340
|
+
* session or hits an API-replay error ("Invalid signature in thinking
|
|
341
|
+
* block"). Fix: capture the claude session_id from this session's init
|
|
342
|
+
* event (above) and use `--resume <session-id>` for follow-ups —
|
|
343
|
+
* unambiguous and replay-safe.
|
|
325
344
|
*
|
|
326
|
-
* Scope: one-shot Q&A. No streaming
|
|
327
|
-
*
|
|
328
|
-
*
|
|
345
|
+
* Scope: still one-shot Q&A. No streaming. Returns assistant's response
|
|
346
|
+
* text. If we never captured a session_id (init event never fired —
|
|
347
|
+
* shouldn't happen in healthy claude runs), reject explicitly.
|
|
329
348
|
*/
|
|
330
349
|
appendMessage(text) {
|
|
331
350
|
const startedAt = Date.now();
|
|
332
351
|
return new Promise((resolve, reject) => {
|
|
352
|
+
if (!claudeSessionId) {
|
|
353
|
+
reject(new Error('Cannot reply: original claude session_id was never captured (init event missed)'));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
333
356
|
let stdout = '';
|
|
334
357
|
let stderr = '';
|
|
335
|
-
const followup = spawn('claude', ['-p', text, '
|
|
358
|
+
const followup = spawn('claude', ['-p', text, '--resume', claudeSessionId, ...HEADLESS_FLAGS], {
|
|
336
359
|
cwd,
|
|
337
360
|
env: spawnEnv,
|
|
338
361
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -538,6 +538,11 @@ export function startMCPServer(options = {}) {
|
|
|
538
538
|
startedAt: session.startedAt,
|
|
539
539
|
status: session.status,
|
|
540
540
|
agent: session.agent,
|
|
541
|
+
// Iter 13 — include canAppendMessage so the freshly-attached
|
|
542
|
+
// session card can correctly enable/disable reply input on the
|
|
543
|
+
// first render. Without this, ALL just-spawned sessions render
|
|
544
|
+
// with reply disabled even when the adapter supports it.
|
|
545
|
+
canAppendMessage: session.canAppendMessage === true,
|
|
541
546
|
}));
|
|
542
547
|
}
|
|
543
548
|
catch (e) {
|
|
@@ -1212,7 +1212,7 @@ ${SHARED_STYLES}
|
|
|
1212
1212
|
</div>
|
|
1213
1213
|
</div>
|
|
1214
1214
|
</div>
|
|
1215
|
-
|
|
1215
|
+
<!-- intentionally empty: multi-project sidebar arrives in v0.6.x -->
|
|
1216
1216
|
</aside>
|
|
1217
1217
|
|
|
1218
1218
|
<section class="thread-pane">
|
|
@@ -1569,22 +1569,10 @@ ${SHARED_STYLES}
|
|
|
1569
1569
|
localStorage.setItem('codehere:cwd', cwdInput.value.trim());
|
|
1570
1570
|
});
|
|
1571
1571
|
|
|
1572
|
-
// Iter
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
// flow handles spawning the task in that path.
|
|
1577
|
-
const newProjectBtn = document.getElementById('new-project-btn');
|
|
1578
|
-
if (newProjectBtn) {
|
|
1579
|
-
newProjectBtn.addEventListener('click', () => {
|
|
1580
|
-
cwdInput.value = '';
|
|
1581
|
-
localStorage.removeItem('codehere:cwd');
|
|
1582
|
-
// Smooth-scroll the intake form into view (project sidebar is on the left,
|
|
1583
|
-
// intake is on the right; on narrow screens the user might be scrolled).
|
|
1584
|
-
try { cwdInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* old browsers */ }
|
|
1585
|
-
cwdInput.focus();
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1572
|
+
// Iter 13 — sidebar 'add project' button + handler removed (was iter 12 work).
|
|
1573
|
+
// The handler cleared cwd input + scrolled + focused, but the label promised
|
|
1574
|
+
// a new sidebar entry that we do not ship until v0.6.x multi-project work.
|
|
1575
|
+
// Mismatch caused real founder confusion in iter-13 tasting. Removed cleanly.
|
|
1588
1576
|
|
|
1589
1577
|
// Gate A1: starter-prompt chips prefill the textarea + focus it so the user
|
|
1590
1578
|
// can edit or hit ⌘-Enter immediately. No scroll-jumping, no redirect.
|
|
@@ -2230,7 +2218,7 @@ ${SHARED_STYLES}
|
|
|
2230
2218
|
});
|
|
2231
2219
|
const data = await res.json();
|
|
2232
2220
|
if (!res.ok) { alert('Failed: ' + (data.error || res.status)); return; }
|
|
2233
|
-
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor' });
|
|
2221
|
+
attachSession({ sessionId: data.sessionId, prompt, startedAt: data.startedAt, status: data.status, agent: data.agent || agent || 'conductor', canAppendMessage: data.canAppendMessage === true });
|
|
2234
2222
|
promptEl.value = '';
|
|
2235
2223
|
} catch (e) {
|
|
2236
2224
|
alert('Network error: ' + (e && e.message ? e.message : e));
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project thread migration — sessions.jsonl → projects/<slug>/thread.jsonl.
|
|
3
|
+
*
|
|
4
|
+
* v0.6.0 M1+M7 foundation. The new schema groups sessions by their cwd
|
|
5
|
+
* (project) so the persistent project thread shape (one continuous chat per
|
|
6
|
+
* project) has a place to live. The original sessions.jsonl is preserved
|
|
7
|
+
* for one release as back-compat.
|
|
8
|
+
*
|
|
9
|
+
* Built from Spike-1 prototype (agent/spike/v0.6.0-migrate-sessions-to-threads.mjs).
|
|
10
|
+
* Spike validated lossless + reversible on 612 real sessions in 5ms.
|
|
11
|
+
*
|
|
12
|
+
* Production additions over the spike:
|
|
13
|
+
* - Atomic per-project writes (.tmp + rename) so partial failures don't
|
|
14
|
+
* leave half-written thread.jsonl files
|
|
15
|
+
* - Idempotent: skips writing if target thread.jsonl already exists
|
|
16
|
+
* (re-run on daemon start is safe)
|
|
17
|
+
* - Temp-dir filter: skips /tmp/, /var/folders/, /private/var/folders/ by
|
|
18
|
+
* default (configurable) — these are test artifacts, not real projects
|
|
19
|
+
* - Abandoned-session marker: sessions with no completed event get
|
|
20
|
+
* status='abandoned' instead of silent null
|
|
21
|
+
* - Configurable input/output paths so tests can use temp fixtures
|
|
22
|
+
*
|
|
23
|
+
* NOT in scope for this module (handled by callers):
|
|
24
|
+
* - CLI interface (codehere migrate-threads — TBD subcommand)
|
|
25
|
+
* - Auto-run on daemon start
|
|
26
|
+
* - Forward-mirroring (writing to thread.jsonl alongside sessions.jsonl
|
|
27
|
+
* during normal daemon operation)
|
|
28
|
+
*
|
|
29
|
+
* See docs/research/v0.6.0-spike-1-migration.md for the validation history.
|
|
30
|
+
*/
|
|
31
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, renameSync } from 'fs';
|
|
32
|
+
import { join } from 'path';
|
|
33
|
+
import { createHash } from 'crypto';
|
|
34
|
+
/** Path patterns considered ephemeral and skipped from project list. */
|
|
35
|
+
const DEFAULT_TEMP_DIR_PATTERNS = [
|
|
36
|
+
/^\/tmp\//,
|
|
37
|
+
/^\/var\/folders\//,
|
|
38
|
+
/^\/private\/var\/folders\//,
|
|
39
|
+
/^\/private\/tmp\//,
|
|
40
|
+
];
|
|
41
|
+
/** sha1(cwd)[:12] — stable, short, filesystem-safe. */
|
|
42
|
+
function slugForCwd(cwd, unknownLabel) {
|
|
43
|
+
if (!cwd)
|
|
44
|
+
return unknownLabel;
|
|
45
|
+
return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
|
|
46
|
+
}
|
|
47
|
+
function isTempDir(cwd) {
|
|
48
|
+
if (!cwd)
|
|
49
|
+
return false;
|
|
50
|
+
return DEFAULT_TEMP_DIR_PATTERNS.some((re) => re.test(cwd));
|
|
51
|
+
}
|
|
52
|
+
/** Read JSONL events; tolerate malformed lines (count + skip). */
|
|
53
|
+
function readEvents(path) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return { events: [], malformed: 0 };
|
|
56
|
+
const text = readFileSync(path, 'utf-8');
|
|
57
|
+
const lines = text.split('\n').filter((l) => l.trim());
|
|
58
|
+
const events = [];
|
|
59
|
+
let malformed = 0;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
try {
|
|
62
|
+
events.push(JSON.parse(line));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
malformed++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { events, malformed };
|
|
69
|
+
}
|
|
70
|
+
/** Roll up event-log entries into one record per session. */
|
|
71
|
+
function rollupSessions(events) {
|
|
72
|
+
const byId = new Map();
|
|
73
|
+
const completedIds = new Set();
|
|
74
|
+
for (const e of events) {
|
|
75
|
+
const id = (e.id ?? e.sessionId);
|
|
76
|
+
if (!id)
|
|
77
|
+
continue;
|
|
78
|
+
if (e.t === 'completed' || e.completedAt !== undefined)
|
|
79
|
+
completedIds.add(id);
|
|
80
|
+
const existing = byId.get(id) ?? { id, abandoned: true };
|
|
81
|
+
const existingAny = existing;
|
|
82
|
+
for (const k of ['agent', 'prompt', 'cwd', 'startedAt', 'status', 'durationMs', 'completedAt', 'steps', 'artifacts']) {
|
|
83
|
+
if (e[k] !== undefined && existingAny[k] === undefined) {
|
|
84
|
+
existingAny[k] = e[k];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
byId.set(id, existing);
|
|
88
|
+
}
|
|
89
|
+
// Mark abandoned (sessions with no completed event)
|
|
90
|
+
for (const s of byId.values()) {
|
|
91
|
+
s.abandoned = !completedIds.has(s.id);
|
|
92
|
+
if (s.abandoned && !s.status)
|
|
93
|
+
s.status = 'abandoned';
|
|
94
|
+
}
|
|
95
|
+
return [...byId.values()];
|
|
96
|
+
}
|
|
97
|
+
function groupByCwd(sessions, unknownLabel) {
|
|
98
|
+
const groups = new Map();
|
|
99
|
+
for (const s of sessions) {
|
|
100
|
+
const cwd = s.cwd ?? null;
|
|
101
|
+
const slug = slugForCwd(cwd, unknownLabel);
|
|
102
|
+
const group = groups.get(slug) ?? { slug, cwd, sessions: [] };
|
|
103
|
+
group.sessions.push(s);
|
|
104
|
+
groups.set(slug, group);
|
|
105
|
+
}
|
|
106
|
+
return [...groups.values()];
|
|
107
|
+
}
|
|
108
|
+
/** Atomic write: write to .tmp then rename. Caller ensures dir exists. */
|
|
109
|
+
function atomicWriteFile(path, content) {
|
|
110
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
111
|
+
writeFileSync(tmp, content);
|
|
112
|
+
renameSync(tmp, path);
|
|
113
|
+
}
|
|
114
|
+
function buildThreadJsonl(group) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push(JSON.stringify({
|
|
117
|
+
_meta: 'project',
|
|
118
|
+
slug: group.slug,
|
|
119
|
+
cwd: group.cwd,
|
|
120
|
+
sessionCount: group.sessions.length,
|
|
121
|
+
migratedFrom: 'sessions.jsonl',
|
|
122
|
+
migratedAt: Date.now(),
|
|
123
|
+
}));
|
|
124
|
+
const sorted = [...group.sessions].sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
|
|
125
|
+
for (const s of sorted) {
|
|
126
|
+
lines.push(JSON.stringify({
|
|
127
|
+
type: 'task',
|
|
128
|
+
sessionId: s.id,
|
|
129
|
+
agent: s.agent,
|
|
130
|
+
prompt: s.prompt,
|
|
131
|
+
cwd: s.cwd,
|
|
132
|
+
startedAt: s.startedAt,
|
|
133
|
+
status: s.status,
|
|
134
|
+
durationMs: s.durationMs,
|
|
135
|
+
completedAt: s.completedAt,
|
|
136
|
+
steps: s.steps,
|
|
137
|
+
artifacts: s.artifacts,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
return lines.join('\n') + '\n';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run the migration. Idempotent: skips projects whose thread.jsonl already
|
|
144
|
+
* exists (so daemon-startup auto-runs are safe).
|
|
145
|
+
*/
|
|
146
|
+
export function migrate(options) {
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
const skipTempDirs = options.skipTempDirs ?? true;
|
|
149
|
+
const unknownLabel = options.unknownCwdLabel ?? 'unknown';
|
|
150
|
+
const { events } = readEvents(options.sourceJsonl);
|
|
151
|
+
const sessions = rollupSessions(events);
|
|
152
|
+
const groups = groupByCwd(sessions, unknownLabel);
|
|
153
|
+
let threadsWritten = 0;
|
|
154
|
+
let threadsSkippedExisting = 0;
|
|
155
|
+
let unknownCwdSessions = 0;
|
|
156
|
+
let abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
157
|
+
let tempDirSessionsSkipped = 0;
|
|
158
|
+
for (const group of groups) {
|
|
159
|
+
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
160
|
+
tempDirSessionsSkipped += group.sessions.length;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (group.cwd === null)
|
|
164
|
+
unknownCwdSessions += group.sessions.length;
|
|
165
|
+
const dir = join(options.targetDir, group.slug);
|
|
166
|
+
const threadFile = join(dir, 'thread.jsonl');
|
|
167
|
+
if (existsSync(threadFile)) {
|
|
168
|
+
threadsSkippedExisting++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
mkdirSync(dir, { recursive: true });
|
|
172
|
+
atomicWriteFile(threadFile, buildThreadJsonl(group));
|
|
173
|
+
threadsWritten++;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
sessionsProcessed: sessions.length,
|
|
177
|
+
threadsWritten,
|
|
178
|
+
threadsSkippedExisting,
|
|
179
|
+
unknownCwdSessions,
|
|
180
|
+
abandonedSessions,
|
|
181
|
+
tempDirSessionsSkipped,
|
|
182
|
+
durationMs: Date.now() - startedAt,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Dry-run: same as migrate but writes nothing. Returns what WOULD happen. */
|
|
186
|
+
export function dryRun(options) {
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
const skipTempDirs = options.skipTempDirs ?? true;
|
|
189
|
+
const unknownLabel = options.unknownCwdLabel ?? 'unknown';
|
|
190
|
+
const { events } = readEvents(options.sourceJsonl);
|
|
191
|
+
const sessions = rollupSessions(events);
|
|
192
|
+
const groups = groupByCwd(sessions, unknownLabel);
|
|
193
|
+
let threadsWritten = 0;
|
|
194
|
+
let threadsSkippedExisting = 0;
|
|
195
|
+
let unknownCwdSessions = 0;
|
|
196
|
+
let abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
197
|
+
let tempDirSessionsSkipped = 0;
|
|
198
|
+
for (const group of groups) {
|
|
199
|
+
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
200
|
+
tempDirSessionsSkipped += group.sessions.length;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (group.cwd === null)
|
|
204
|
+
unknownCwdSessions += group.sessions.length;
|
|
205
|
+
const threadFile = join(options.targetDir, group.slug, 'thread.jsonl');
|
|
206
|
+
if (existsSync(threadFile))
|
|
207
|
+
threadsSkippedExisting++;
|
|
208
|
+
else
|
|
209
|
+
threadsWritten++;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
sessionsProcessed: sessions.length,
|
|
213
|
+
threadsWritten,
|
|
214
|
+
threadsSkippedExisting,
|
|
215
|
+
unknownCwdSessions,
|
|
216
|
+
abandonedSessions,
|
|
217
|
+
tempDirSessionsSkipped,
|
|
218
|
+
durationMs: Date.now() - startedAt,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Rollback: removes the projects directory. Source sessions.jsonl is never
|
|
223
|
+
* touched. Returns the count of removed project subdirectories.
|
|
224
|
+
*/
|
|
225
|
+
export function rollback(targetDir) {
|
|
226
|
+
if (!existsSync(targetDir))
|
|
227
|
+
return { removedDirs: 0 };
|
|
228
|
+
const subdirs = readdirSync(targetDir);
|
|
229
|
+
rmSync(targetDir, { recursive: true });
|
|
230
|
+
return { removedDirs: subdirs.length };
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=project-thread-migration.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.4",
|
|
4
4
|
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|