warp-os 1.1.2 → 1.2.1
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 +85 -0
- package/README.md +6 -4
- package/VERSION +1 -1
- package/agents/warp-annotate.md +394 -0
- package/agents/warp-browse.md +9 -1
- package/agents/warp-build-code.md +9 -1
- package/agents/warp-orchestrator.md +10 -1
- package/agents/warp-plan-architect.md +120 -1
- package/agents/warp-plan-brainstorm.md +93 -2
- package/agents/warp-plan-design.md +97 -4
- package/agents/warp-plan-onboarding.md +9 -1
- package/agents/warp-plan-optimize.md +9 -1
- package/agents/warp-plan-scope.md +67 -1
- package/agents/warp-plan-security.md +576 -35
- package/agents/warp-plan-testdesign.md +9 -1
- package/agents/warp-qa-debug.md +117 -1
- package/agents/warp-qa-test.md +167 -1
- package/agents/warp-release-update.md +290 -4
- package/agents/warp-setup.md +9 -1
- package/agents/warp-upgrade.md +21 -4
- package/bin/hooks/CLAUDE.md +24 -0
- package/bin/hooks/_warp_json.sh +4 -2
- package/bin/hooks/identity-briefing.sh +20 -13
- package/bin/hooks/validate-askuser.sh +41 -0
- package/bin/migrate-sessions.js +284 -173
- package/dist/warp-annotate/SKILL.md +404 -0
- package/dist/warp-browse/SKILL.md +9 -1
- package/dist/warp-build-code/SKILL.md +9 -1
- package/dist/warp-orchestrator/SKILL.md +10 -1
- package/dist/warp-plan-architect/SKILL.md +120 -1
- package/dist/warp-plan-brainstorm/SKILL.md +93 -2
- package/dist/warp-plan-design/SKILL.md +97 -4
- package/dist/warp-plan-onboarding/SKILL.md +9 -1
- package/dist/warp-plan-optimize/SKILL.md +9 -1
- package/dist/warp-plan-scope/SKILL.md +67 -1
- package/dist/warp-plan-security/SKILL.md +578 -35
- package/dist/warp-plan-testdesign/SKILL.md +9 -1
- package/dist/warp-qa-debug/SKILL.md +117 -1
- package/dist/warp-qa-test/SKILL.md +167 -1
- package/dist/warp-release-update/SKILL.md +290 -4
- package/dist/warp-setup/SKILL.md +9 -1
- package/dist/warp-upgrade/SKILL.md +21 -4
- package/package.json +2 -2
- package/shared/project-hooks.json +7 -0
- package/shared/tier1-engineering-constitution.md +9 -1
package/bin/migrate-sessions.js
CHANGED
|
@@ -1,41 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// migrate-sessions.js — Import old .warp/sessions/*.md into claude-mem
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
//
|
|
4
|
+
// Uses bun + direct SQLite inserts (not HTTP API — claude-mem has no public write API).
|
|
5
|
+
// The HTTP API only queues raw tool data through LLM reprocessing, which is wrong for
|
|
6
|
+
// pre-formed historical knowledge. Direct DB insert is the proven approach.
|
|
7
|
+
//
|
|
8
|
+
// Requires: bun (auto-installed by claude-mem)
|
|
9
|
+
// Database: ~/.claude-mem/claude-mem.db
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node bin/migrate-sessions.js [project-dir] Run migration
|
|
13
|
+
// node bin/migrate-sessions.js --clean [dir] Remove all warp-import-* data
|
|
14
|
+
// node bin/migrate-sessions.js --dry-run [dir] Preview without touching DB
|
|
15
|
+
//
|
|
16
|
+
// Idempotent: uses content hashes to avoid duplicates. Safe to re-run.
|
|
17
|
+
// All imported sessions use content_session_id = "warp-import-{id}" for identification.
|
|
5
18
|
|
|
6
19
|
const fs = require('fs');
|
|
7
20
|
const path = require('path');
|
|
8
|
-
const
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { execSync } = require('child_process');
|
|
9
24
|
|
|
10
25
|
const GREEN = '\x1b[32m';
|
|
11
26
|
const YELLOW = '\x1b[33m';
|
|
12
27
|
const CYAN = '\x1b[36m';
|
|
28
|
+
const RED = '\x1b[31m';
|
|
13
29
|
const NC = '\x1b[0m';
|
|
14
30
|
|
|
15
31
|
function ok(msg) { console.log(` ${GREEN}OK${NC} ${msg}`); }
|
|
16
32
|
function warn(msg) { console.log(` ${YELLOW}WARN${NC} ${msg}`); }
|
|
17
33
|
function info(msg) { console.log(` ${CYAN}INFO${NC} ${msg}`); }
|
|
34
|
+
function fail(msg) { console.log(` ${RED}FAIL${NC} ${msg}`); }
|
|
18
35
|
|
|
19
|
-
// ──
|
|
36
|
+
// ── Args ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const cleanMode = args.includes('--clean');
|
|
40
|
+
const dryRun = args.includes('--dry-run');
|
|
41
|
+
const cwd = args.find(a => !a.startsWith('--')) || process.cwd();
|
|
42
|
+
const DB_PATH = path.join(os.homedir(), '.claude-mem', 'claude-mem.db');
|
|
43
|
+
|
|
44
|
+
// ── Check prerequisites ─────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(DB_PATH)) {
|
|
47
|
+
warn('claude-mem database not found at ~/.claude-mem/claude-mem.db');
|
|
48
|
+
info('Install claude-mem first: npx claude-mem install');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let bunPath;
|
|
53
|
+
try {
|
|
54
|
+
bunPath = execSync('which bun 2>/dev/null || where bun 2>nul', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0];
|
|
55
|
+
} catch {
|
|
56
|
+
warn('bun not found. Required for SQLite access.');
|
|
57
|
+
info('Install bun: curl -fsSL https://bun.sh/install | bash');
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Generate the bun script and execute it ──────────────────────────────────
|
|
62
|
+
// We write a temporary .ts file and run it with bun because bun:sqlite
|
|
63
|
+
// is only available in the bun runtime, not Node.js.
|
|
20
64
|
|
|
21
|
-
const cwd = process.argv[2] || process.cwd();
|
|
22
65
|
const sessionsDir = path.join(cwd, '.warp', 'sessions');
|
|
23
66
|
const archiveDir = path.join(sessionsDir, 'archive');
|
|
24
67
|
|
|
25
|
-
if (!fs.existsSync(sessionsDir)) {
|
|
68
|
+
if (!cleanMode && !fs.existsSync(sessionsDir)) {
|
|
26
69
|
console.log('No .warp/sessions/ directory found. Nothing to migrate.');
|
|
27
70
|
process.exit(0);
|
|
28
71
|
}
|
|
29
72
|
|
|
30
73
|
// Gather all session files (active + archived)
|
|
31
74
|
const sessionFiles = [];
|
|
32
|
-
|
|
33
|
-
for (const file of fs.readdirSync(sessionsDir)) {
|
|
34
|
-
|
|
35
|
-
|
|
75
|
+
if (fs.existsSync(sessionsDir)) {
|
|
76
|
+
for (const file of fs.readdirSync(sessionsDir)) {
|
|
77
|
+
if (file.endsWith('.md') && file.startsWith('SESSION_')) {
|
|
78
|
+
sessionFiles.push(path.join(sessionsDir, file));
|
|
79
|
+
}
|
|
36
80
|
}
|
|
37
81
|
}
|
|
38
|
-
|
|
39
82
|
if (fs.existsSync(archiveDir)) {
|
|
40
83
|
for (const file of fs.readdirSync(archiveDir)) {
|
|
41
84
|
if (file.endsWith('.md') && file.startsWith('SESSION_')) {
|
|
@@ -44,75 +87,30 @@ if (fs.existsSync(archiveDir)) {
|
|
|
44
87
|
}
|
|
45
88
|
}
|
|
46
89
|
|
|
47
|
-
if (sessionFiles.length === 0) {
|
|
90
|
+
if (!cleanMode && sessionFiles.length === 0) {
|
|
48
91
|
console.log('No session files found to migrate.');
|
|
49
92
|
process.exit(0);
|
|
50
93
|
}
|
|
51
94
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// ── Check if claude-mem worker is running ────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
function httpPost(urlPath, data) {
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
const body = JSON.stringify(data);
|
|
59
|
-
const req = http.request({
|
|
60
|
-
hostname: '127.0.0.1',
|
|
61
|
-
port: 37777,
|
|
62
|
-
path: urlPath,
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: {
|
|
65
|
-
'Content-Type': 'application/json',
|
|
66
|
-
'Content-Length': Buffer.byteLength(body)
|
|
67
|
-
},
|
|
68
|
-
timeout: 10000
|
|
69
|
-
}, (res) => {
|
|
70
|
-
let data = '';
|
|
71
|
-
res.on('data', chunk => data += chunk);
|
|
72
|
-
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
73
|
-
});
|
|
74
|
-
req.on('error', reject);
|
|
75
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
76
|
-
req.write(body);
|
|
77
|
-
req.end();
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function httpGet(urlPath) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
const req = http.request({
|
|
84
|
-
hostname: '127.0.0.1',
|
|
85
|
-
port: 37777,
|
|
86
|
-
path: urlPath,
|
|
87
|
-
method: 'GET',
|
|
88
|
-
timeout: 5000
|
|
89
|
-
}, (res) => {
|
|
90
|
-
let data = '';
|
|
91
|
-
res.on('data', chunk => data += chunk);
|
|
92
|
-
res.on('end', () => resolve({ status: res.statusCode, body: data }));
|
|
93
|
-
});
|
|
94
|
-
req.on('error', reject);
|
|
95
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
96
|
-
req.end();
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── Parse session file ───────────────────────────────────────────────────────
|
|
101
|
-
|
|
95
|
+
// Parse session files in Node, pass structured data to bun
|
|
102
96
|
function parseSessionFile(filePath) {
|
|
103
97
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
104
98
|
const filename = path.basename(filePath, '.md');
|
|
105
99
|
|
|
106
|
-
// Extract session metadata from filename: SESSION_CC7_20260404
|
|
107
100
|
const match = filename.match(/SESSION_CC(\d+)_(\d{8})/);
|
|
108
|
-
const sessionNum = match ?
|
|
109
|
-
const
|
|
101
|
+
const sessionNum = match ? parseInt(match[1]) : 0;
|
|
102
|
+
const sessionId = match ? `CC${match[1]}` : filename;
|
|
103
|
+
const dateStr = match
|
|
104
|
+
? `${match[2].slice(0,4)}-${match[2].slice(4,6)}-${match[2].slice(6,8)}`
|
|
105
|
+
: new Date().toISOString().slice(0,10);
|
|
106
|
+
|
|
107
|
+
// Parse epoch from date string
|
|
108
|
+
const epoch = new Date(dateStr + 'T12:00:00Z').getTime();
|
|
110
109
|
|
|
111
110
|
// Extract sections
|
|
112
111
|
const sections = {};
|
|
113
112
|
let currentSection = 'header';
|
|
114
113
|
sections[currentSection] = [];
|
|
115
|
-
|
|
116
114
|
for (const line of content.split('\n')) {
|
|
117
115
|
if (line.startsWith('## ')) {
|
|
118
116
|
currentSection = line.replace('## ', '').trim();
|
|
@@ -123,132 +121,245 @@ function parseSessionFile(filePath) {
|
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
// Extract goal from header
|
|
127
124
|
const goalLine = content.match(/Goal:\s+(.+)/);
|
|
128
|
-
const goal = goalLine ? goalLine[1].trim() : `Session ${
|
|
125
|
+
const goal = goalLine ? goalLine[1].trim() : `Session ${sessionId}`;
|
|
126
|
+
|
|
127
|
+
const extractBullets = (key) =>
|
|
128
|
+
(sections[key] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, ''));
|
|
129
|
+
|
|
130
|
+
const keyDecisions = extractBullets('Key Decisions');
|
|
131
|
+
const itemsAchieved = extractBullets('Items Achieved');
|
|
132
|
+
const itemsNotDone = extractBullets('Items Not Done');
|
|
133
|
+
const nextPickup = (sections['Next Pickup Point'] || []).join('\n').trim();
|
|
134
|
+
const filesProduced = extractBullets('Files Produced');
|
|
135
|
+
const filesAmended = extractBullets('Files Amended');
|
|
136
|
+
|
|
137
|
+
// Content hash for idempotency
|
|
138
|
+
const contentHash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
129
139
|
|
|
130
140
|
return {
|
|
131
|
-
filename,
|
|
132
|
-
|
|
133
|
-
dateStr,
|
|
134
|
-
goal,
|
|
135
|
-
content,
|
|
136
|
-
sections,
|
|
137
|
-
keyDecisions: (sections['Key Decisions'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
|
|
138
|
-
itemsAchieved: (sections['Items Achieved'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
|
|
139
|
-
itemsNotDone: (sections['Items Not Done'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
|
|
140
|
-
nextPickup: (sections['Next Pickup Point'] || []).join('\n').trim(),
|
|
141
|
-
filesProduced: (sections['Files Produced'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
|
|
142
|
-
filesAmended: (sections['Files Amended'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
|
|
141
|
+
filename, sessionId, sessionNum, dateStr, epoch, goal, contentHash,
|
|
142
|
+
keyDecisions, itemsAchieved, itemsNotDone, nextPickup, filesProduced, filesAmended
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
const projectName = path.basename(cwd);
|
|
147
|
+
const sessions = sessionFiles.map(f => parseSessionFile(f));
|
|
148
|
+
|
|
149
|
+
if (!cleanMode) {
|
|
150
|
+
console.log(`Found ${sessions.length} session file(s) to migrate.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Write the bun migration script to a temp file
|
|
154
|
+
const bunScript = `
|
|
155
|
+
import { Database } from "bun:sqlite";
|
|
156
|
+
|
|
157
|
+
const DB_PATH = ${JSON.stringify(DB_PATH)};
|
|
158
|
+
const projectName = ${JSON.stringify(projectName)};
|
|
159
|
+
const cleanMode = ${cleanMode};
|
|
160
|
+
const dryRun = ${dryRun};
|
|
161
|
+
const sessions = ${JSON.stringify(sessions)};
|
|
162
|
+
|
|
163
|
+
const db = new Database(DB_PATH);
|
|
164
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
165
|
+
|
|
166
|
+
// Bun-compatible crypto helpers
|
|
167
|
+
function bunHash(input: string): string {
|
|
168
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
169
|
+
hasher.update(input);
|
|
170
|
+
return hasher.digest("hex").slice(0, 16);
|
|
171
|
+
}
|
|
172
|
+
function bunUUID(): string {
|
|
173
|
+
return crypto.randomUUID();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (cleanMode) {
|
|
177
|
+
if (dryRun) {
|
|
178
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'").get();
|
|
179
|
+
const obsCount = db.prepare(\`
|
|
180
|
+
SELECT COUNT(*) as c FROM observations o
|
|
181
|
+
JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
|
|
182
|
+
WHERE s.content_session_id LIKE 'warp-import-%'
|
|
183
|
+
\`).get();
|
|
184
|
+
console.log(\` DRY RUN: Would remove \${count.c} sessions, \${obsCount.c} observations\`);
|
|
185
|
+
} else {
|
|
186
|
+
// Delete observations first (FK constraint)
|
|
187
|
+
const deleted = db.prepare(\`
|
|
188
|
+
DELETE FROM observations WHERE memory_session_id IN (
|
|
189
|
+
SELECT memory_session_id FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'
|
|
190
|
+
)
|
|
191
|
+
\`).run();
|
|
192
|
+
// Delete summaries
|
|
193
|
+
db.prepare(\`
|
|
194
|
+
DELETE FROM session_summaries WHERE memory_session_id IN (
|
|
195
|
+
SELECT memory_session_id FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'
|
|
196
|
+
)
|
|
197
|
+
\`).run();
|
|
198
|
+
// Delete sessions
|
|
199
|
+
const sessDeleted = db.prepare("DELETE FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'").run();
|
|
200
|
+
console.log(\` Cleaned: \${sessDeleted.changes} sessions, \${deleted.changes} observations\`);
|
|
201
|
+
}
|
|
202
|
+
db.close();
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check for existing imports (idempotency)
|
|
207
|
+
const existingImports = db.prepare(
|
|
208
|
+
"SELECT content_session_id FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'"
|
|
209
|
+
).all().map(r => r.content_session_id);
|
|
210
|
+
|
|
211
|
+
let migrated = 0;
|
|
212
|
+
let skipped = 0;
|
|
213
|
+
let failed = 0;
|
|
214
|
+
|
|
215
|
+
for (const session of sessions) {
|
|
216
|
+
const contentSessionId = \`warp-import-\${session.sessionId}\`;
|
|
217
|
+
|
|
218
|
+
// Skip if already imported
|
|
219
|
+
if (existingImports.includes(contentSessionId)) {
|
|
220
|
+
console.log(\` SKIP \${session.filename} (already imported)\`);
|
|
221
|
+
skipped++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (dryRun) {
|
|
226
|
+
const obsCount = session.keyDecisions.length + (session.itemsAchieved.length > 0 ? 1 : 0) +
|
|
227
|
+
(session.itemsNotDone.length > 0 ? 1 : 0) + (session.nextPickup ? 1 : 0);
|
|
228
|
+
console.log(\` DRY \${session.filename} → \${obsCount} observations\`);
|
|
229
|
+
migrated++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
147
232
|
|
|
148
|
-
async function main() {
|
|
149
|
-
// Check worker health
|
|
150
233
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
234
|
+
// Create session
|
|
235
|
+
const memorySessionId = bunUUID();
|
|
236
|
+
const dateIso = new Date(session.epoch).toISOString();
|
|
237
|
+
db.prepare(\`
|
|
238
|
+
INSERT INTO sdk_sessions (content_session_id, memory_session_id, project, status, started_at, started_at_epoch, completed_at, completed_at_epoch)
|
|
239
|
+
VALUES (?, ?, ?, 'completed', ?, ?, ?, ?)
|
|
240
|
+
\`).run(contentSessionId, memorySessionId, projectName, dateIso, session.epoch, dateIso, session.epoch);
|
|
241
|
+
|
|
242
|
+
let obsCount = 0;
|
|
243
|
+
|
|
244
|
+
// Import key decisions as individual "decision" observations
|
|
245
|
+
for (const decision of session.keyDecisions) {
|
|
246
|
+
const hash = bunHash(memorySessionId + decision);
|
|
247
|
+
db.prepare(\`
|
|
248
|
+
INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
|
|
249
|
+
VALUES (?, ?, 'decision', ?, ?, ?, ?, ?)
|
|
250
|
+
\`).run(memorySessionId, projectName,
|
|
251
|
+
decision.length > 80 ? decision.slice(0, 77) + '...' : decision,
|
|
252
|
+
decision,
|
|
253
|
+
hash, dateIso, session.epoch);
|
|
254
|
+
obsCount++;
|
|
155
255
|
}
|
|
156
|
-
} catch {
|
|
157
|
-
warn('claude-mem worker not running. Start it with: npx claude-mem start');
|
|
158
|
-
info('Migration will run on next upgrade after worker is available.');
|
|
159
|
-
process.exit(0);
|
|
160
|
-
}
|
|
161
256
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
257
|
+
// Import items achieved as a "change" observation (grouped)
|
|
258
|
+
if (session.itemsAchieved.length > 0) {
|
|
259
|
+
const narrative = session.itemsAchieved.map(i => \`- \${i}\`).join('\\n');
|
|
260
|
+
const hash = bunHash(memorySessionId + 'achieved' + narrative);
|
|
261
|
+
db.prepare(\`
|
|
262
|
+
INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
|
|
263
|
+
VALUES (?, ?, 'change', ?, ?, ?, ?, ?)
|
|
264
|
+
\`).run(memorySessionId, projectName,
|
|
265
|
+
\`\${session.sessionId}: \${session.itemsAchieved.length} items completed\`,
|
|
266
|
+
\`Session \${session.sessionId} (\${session.dateStr}): \${session.goal}\\n\\nCompleted:\\n\${narrative}\`,
|
|
267
|
+
hash, dateIso, session.epoch);
|
|
268
|
+
obsCount++;
|
|
269
|
+
}
|
|
165
270
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
].filter(Boolean).join('\n\n');
|
|
271
|
+
// Import items not done as a "discovery" observation
|
|
272
|
+
if (session.itemsNotDone.length > 0) {
|
|
273
|
+
const narrative = session.itemsNotDone.map(i => \`- \${i}\`).join('\\n');
|
|
274
|
+
const hash = bunHash(memorySessionId + 'notdone' + narrative);
|
|
275
|
+
db.prepare(\`
|
|
276
|
+
INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
|
|
277
|
+
VALUES (?, ?, 'discovery', ?, ?, ?, ?, ?)
|
|
278
|
+
\`).run(memorySessionId, projectName,
|
|
279
|
+
\`\${session.sessionId}: \${session.itemsNotDone.length} items deferred\`,
|
|
280
|
+
\`Not completed in \${session.sessionId}:\\n\${narrative}\`,
|
|
281
|
+
hash, dateIso, session.epoch);
|
|
282
|
+
obsCount++;
|
|
283
|
+
}
|
|
180
284
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
project
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
tool_output: {
|
|
193
|
-
narrative: narrative,
|
|
194
|
-
keyDecisions: session.keyDecisions,
|
|
195
|
-
itemsAchieved: session.itemsAchieved,
|
|
196
|
-
filesAmended: session.filesAmended,
|
|
197
|
-
nextPickup: session.nextPickup
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (result.status >= 200 && result.status < 300) {
|
|
202
|
-
ok(`${session.filename} → claude-mem (${session.keyDecisions.length} decisions, ${session.itemsAchieved.length} items)`);
|
|
203
|
-
migrated++;
|
|
204
|
-
} else {
|
|
205
|
-
// If the observation API doesn't accept this format, try the raw session init
|
|
206
|
-
const initResult = await httpPost('/api/sessions/init', {
|
|
207
|
-
contentSessionId: `warp-migration-${session.sessionNum}`,
|
|
208
|
-
project: projectName,
|
|
209
|
-
userPrompt: `[Migrated from ${session.filename}] ${session.goal}\n\n${narrative}`
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
if (initResult.status >= 200 && initResult.status < 300) {
|
|
213
|
-
ok(`${session.filename} → claude-mem (via session init)`);
|
|
214
|
-
migrated++;
|
|
215
|
-
} else {
|
|
216
|
-
warn(`${session.filename} — API returned ${result.status}`);
|
|
217
|
-
failed++;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
} catch (err) {
|
|
221
|
-
warn(`${session.filename} — ${err.message}`);
|
|
222
|
-
failed++;
|
|
285
|
+
// Import next pickup as a "discovery" observation
|
|
286
|
+
if (session.nextPickup) {
|
|
287
|
+
const hash = bunHash(memorySessionId + 'pickup' + session.nextPickup);
|
|
288
|
+
db.prepare(\`
|
|
289
|
+
INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
|
|
290
|
+
VALUES (?, ?, 'discovery', ?, ?, ?, ?, ?)
|
|
291
|
+
\`).run(memorySessionId, projectName,
|
|
292
|
+
\`\${session.sessionId}: Next pickup point\`,
|
|
293
|
+
session.nextPickup,
|
|
294
|
+
hash, dateIso, session.epoch);
|
|
295
|
+
obsCount++;
|
|
223
296
|
}
|
|
297
|
+
|
|
298
|
+
// Create session summary
|
|
299
|
+
const summaryRequest = session.goal;
|
|
300
|
+
const summaryLearned = session.keyDecisions.join('; ') || 'No key decisions recorded';
|
|
301
|
+
const summaryCompleted = session.itemsAchieved.join('; ') || 'No items recorded';
|
|
302
|
+
const summaryNextSteps = session.nextPickup || 'No next steps recorded';
|
|
303
|
+
const filesRead = session.filesProduced.join(', ');
|
|
304
|
+
const filesModified = session.filesAmended.join(', ');
|
|
305
|
+
|
|
306
|
+
const summaryDateIso = new Date(session.epoch).toISOString();
|
|
307
|
+
db.prepare(\`
|
|
308
|
+
INSERT INTO session_summaries (memory_session_id, project, request, learned, completed, next_steps, files_read, files_edited, created_at, created_at_epoch)
|
|
309
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
310
|
+
\`).run(memorySessionId, projectName, summaryRequest, summaryLearned, summaryCompleted, summaryNextSteps, filesRead, filesModified, summaryDateIso, session.epoch);
|
|
311
|
+
|
|
312
|
+
console.log(\` OK \${session.filename} → \${obsCount} observations, 1 summary\`);
|
|
313
|
+
migrated++;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.log(\` FAIL \${session.filename} — \${err.message}\`);
|
|
316
|
+
failed++;
|
|
224
317
|
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
db.close();
|
|
321
|
+
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log(\`Migration: \${migrated} imported, \${skipped} skipped, \${failed} failed\`);
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
// Write temp script and execute with bun
|
|
327
|
+
const tmpScript = path.join(os.tmpdir(), `warp-migrate-${Date.now()}.ts`);
|
|
328
|
+
fs.writeFileSync(tmpScript, bunScript);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
execSync(`"${bunPath}" run "${tmpScript}"`, {
|
|
332
|
+
stdio: 'inherit',
|
|
333
|
+
timeout: 120000,
|
|
334
|
+
env: { ...process.env }
|
|
335
|
+
});
|
|
336
|
+
} catch (err) {
|
|
337
|
+
fail(`Migration script failed: ${err.message}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
} finally {
|
|
340
|
+
try { fs.unlinkSync(tmpScript); } catch {}
|
|
341
|
+
}
|
|
225
342
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
343
|
+
// Archive migrated files (only if not dry-run and not clean mode)
|
|
344
|
+
if (!dryRun && !cleanMode && sessions.length > 0) {
|
|
345
|
+
const migratedDir = path.join(sessionsDir, 'migrated-to-claude-mem');
|
|
346
|
+
fs.mkdirSync(migratedDir, { recursive: true });
|
|
230
347
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
348
|
+
let archived = 0;
|
|
349
|
+
for (const file of sessionFiles) {
|
|
350
|
+
const basename = path.basename(file);
|
|
351
|
+
const dest = path.join(migratedDir, basename);
|
|
352
|
+
try {
|
|
353
|
+
if (!fs.existsSync(dest)) {
|
|
235
354
|
fs.copyFileSync(file, dest);
|
|
236
|
-
fs.unlinkSync(file);
|
|
237
|
-
} catch {
|
|
238
|
-
// File might already be in archive subdir, just leave it
|
|
239
355
|
}
|
|
356
|
+
fs.unlinkSync(file);
|
|
357
|
+
archived++;
|
|
358
|
+
} catch {
|
|
359
|
+
// File might already be archived or in a subdir
|
|
240
360
|
}
|
|
241
|
-
ok(`Archived ${migrated} session file(s) → .warp/sessions/migrated-to-claude-mem/`);
|
|
242
361
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.log(`Migration: ${GREEN}${migrated}${NC} imported, ${YELLOW}${failed}${NC} failed`);
|
|
246
|
-
if (failed > 0) {
|
|
247
|
-
info('Failed files left in place. Re-run migration after fixing claude-mem worker.');
|
|
362
|
+
if (archived > 0) {
|
|
363
|
+
ok(`Archived ${archived} session file(s) → .warp/sessions/migrated-to-claude-mem/`);
|
|
248
364
|
}
|
|
249
365
|
}
|
|
250
|
-
|
|
251
|
-
main().catch(err => {
|
|
252
|
-
warn(`Migration error: ${err.message}`);
|
|
253
|
-
process.exit(1);
|
|
254
|
-
});
|