shmakk 1.2.3 → 1.2.5

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 (52) hide show
  1. package/.env.example +11 -0
  2. package/README.md +75 -1
  3. package/docs/index.html +154 -16
  4. package/docs/mcp.md +78 -0
  5. package/docs/ssh.md +82 -0
  6. package/docs/vibedit-analysis.md +375 -0
  7. package/docs/vim.md +110 -0
  8. package/docs/voice.md +4 -0
  9. package/package.json +9 -5
  10. package/scripts/test-vibedit.js +45 -0
  11. package/scripts/vibedit-demo.sh +52 -0
  12. package/skills/shmakk-skill-creator.md +269 -0
  13. package/src/_check.js +7 -0
  14. package/src/_check_schema.js +5 -0
  15. package/src/_cleanup.js +18 -0
  16. package/src/_fix.js +9 -0
  17. package/src/_test_import.js +15 -0
  18. package/src/agent.js +11 -4
  19. package/src/browser-daemon.js +209 -0
  20. package/src/browser.js +10 -0
  21. package/src/cli/browserDaemon.js +60 -0
  22. package/src/cli/connectBrowser.js +137 -0
  23. package/src/cli.js +235 -8
  24. package/src/completions.js +8 -0
  25. package/src/control.js +273 -1
  26. package/src/core/browserConnector.js +523 -0
  27. package/src/correction.js +6 -0
  28. package/src/electron.js +305 -0
  29. package/src/endpoints.js +74 -9
  30. package/src/index.js +24 -1
  31. package/src/llm.js +501 -61
  32. package/src/mobile.js +307 -0
  33. package/src/notify.js +51 -3
  34. package/src/orchestrator.js +35 -1
  35. package/src/pty.js +11 -6
  36. package/src/review.js +45 -11
  37. package/src/self-commands.js +153 -0
  38. package/src/session-convert.js +508 -0
  39. package/src/session-search.js +31 -0
  40. package/src/session.js +392 -46
  41. package/src/skills/browserActions.ts +984 -0
  42. package/src/skills.js +451 -24
  43. package/src/system-prompt.js +31 -25
  44. package/src/tools.js +81 -0
  45. package/src/vibedit/control.js +534 -0
  46. package/src/vibedit/electron.js +108 -0
  47. package/src/vibedit/files.js +171 -0
  48. package/src/vibedit/index.js +298 -0
  49. package/src/vibedit/overlay.js +1482 -0
  50. package/src/vibedit/prompts.js +245 -0
  51. package/src/vibedit/state.js +32 -0
  52. package/src/vim.js +410 -0
@@ -370,6 +370,64 @@ const SELF_COMMANDS = [
370
370
  action: 'disable-debug',
371
371
  },
372
372
 
373
+ // ── Voice modes ──
374
+ {
375
+ patterns: [
376
+ /^(?:enable|turn\s+on|start)\s+stt$/i,
377
+ /^stt\s+on$/i,
378
+ /^(?:enable|turn\s+on|start)\s+speech[-\s]?to[-\s]?text$/i,
379
+ /^(?:enable|turn\s+on|start)\s+voice(?:\s+input)?$/i,
380
+ /^voice(?:\s+input)?\s+on$/i,
381
+ ],
382
+ action: 'enable-stt',
383
+ },
384
+ {
385
+ patterns: [
386
+ /^(?:disable|turn\s+off|stop)\s+stt$/i,
387
+ /^stt\s+off$/i,
388
+ /^(?:disable|turn\s+off|stop)\s+speech[-\s]?to[-\s]?text$/i,
389
+ /^(?:disable|turn\s+off|stop)\s+voice(?:\s+input)?$/i,
390
+ /^voice(?:\s+input)?\s+off$/i,
391
+ ],
392
+ action: 'disable-stt',
393
+ },
394
+ {
395
+ patterns: [
396
+ /^(?:enable|turn\s+on|start)\s+tts$/i,
397
+ /^tts\s+on$/i,
398
+ /^(?:enable|turn\s+on|start)\s+text[-\s]?to[-\s]?speech$/i,
399
+ /^(?:enable|turn\s+on|start)\s+spoken\s+responses?$/i,
400
+ ],
401
+ action: 'enable-tts',
402
+ },
403
+ {
404
+ patterns: [
405
+ /^(?:disable|turn\s+off|stop)\s+tts$/i,
406
+ /^tts\s+off$/i,
407
+ /^(?:disable|turn\s+off|stop)\s+text[-\s]?to[-\s]?speech$/i,
408
+ /^(?:disable|turn\s+off|stop)\s+spoken\s+responses?$/i,
409
+ ],
410
+ action: 'disable-tts',
411
+ },
412
+ {
413
+ patterns: [
414
+ /^(?:enable|turn\s+on|start)\s+sts$/i,
415
+ /^sts\s+on$/i,
416
+ /^(?:enable|turn\s+on|start)\s+speech[-\s]?to[-\s]?speech$/i,
417
+ /^(?:enable|turn\s+on|start)\s+always[-\s]?on\s+voice$/i,
418
+ ],
419
+ action: 'enable-sts',
420
+ },
421
+ {
422
+ patterns: [
423
+ /^(?:disable|turn\s+off|stop)\s+sts$/i,
424
+ /^sts\s+off$/i,
425
+ /^(?:disable|turn\s+off|stop)\s+speech[-\s]?to[-\s]?speech$/i,
426
+ /^(?:disable|turn\s+off|stop)\s+always[-\s]?on\s+voice$/i,
427
+ ],
428
+ action: 'disable-sts',
429
+ },
430
+
373
431
  // ── Profile ──
374
432
  {
375
433
  patterns: [
@@ -393,6 +451,18 @@ const SELF_COMMANDS = [
393
451
  confirm: true,
394
452
  },
395
453
 
454
+ // ── Workspace consolidation ──
455
+ {
456
+ patterns: [
457
+ /^consolidate\s+workspace$/i,
458
+ /^merge\s+workspace$/i,
459
+ /^merge\s+\.shmakk$/i,
460
+ /^consolidate\s+\.shmakk$/i,
461
+ ],
462
+ action: 'consolidate-workspace',
463
+ confirm: true,
464
+ },
465
+
396
466
  // ── Edit review ──
397
467
  {
398
468
  patterns: [
@@ -415,6 +485,31 @@ const SELF_COMMANDS = [
415
485
  action: 'sidebar-query',
416
486
  needsArg: true,
417
487
  },
488
+
489
+ // ── Vibedit ──
490
+ // Visual editing workflow: screenshot running app → vision LLM analysis
491
+ // → structured functional description → PM team delegation.
492
+ // Accepts a natural language request (e.g. "make the header blue").
493
+ {
494
+ patterns: [
495
+ /^\/?vibedit\s+(.+)$/i,
496
+ /^shmakk\s+vibedit\s+(.+)$/i,
497
+ ],
498
+ action: 'vibedit',
499
+ needsArg: true,
500
+ },
501
+
502
+ // ── Vibedit Electron ──
503
+ // Visual editing overlay connected to a live Electron desktop app via CDP.
504
+ {
505
+ patterns: [
506
+ /^\/?vibedit[\s-]electron\s*(.*)$/i,
507
+ /^shmakk\s+vibedit[\s-]electron\s*(.*)$/i,
508
+ /^\/?ve\s*(.*)$/i,
509
+ ],
510
+ action: 'vibedit-electron',
511
+ needsArg: true,
512
+ },
418
513
  ];
419
514
 
420
515
  // Self-command prefixes accepted by the shell:
@@ -583,6 +678,7 @@ function executeSelfCommand(match, write, ctx = {}) {
583
678
  case 'mcp-status': ctl.mcpStatus(); break;
584
679
  case 'compact': ctl.compactContext(); break;
585
680
  case 'reset': ctl.resetConversation(); break;
681
+ case 'consolidate-workspace': ctl.consolidateWorkspace(); break;
586
682
 
587
683
  case 'show-rules': {
588
684
  const { loadRules, rulesStatus } = require('./rules');
@@ -907,6 +1003,63 @@ function executeSelfCommand(match, write, ctx = {}) {
907
1003
  break;
908
1004
  }
909
1005
 
1006
+ // ── Voice modes ──
1007
+ case 'enable-stt': {
1008
+ if (ctx.setVoiceMode) ctx.setVoiceMode('stt', true);
1009
+ else {
1010
+ if (ctx.opts) {
1011
+ ctx.opts.stt = true;
1012
+ ctx.opts.tts = false;
1013
+ ctx.opts.sts = false;
1014
+ ctx.opts.voice = true;
1015
+ }
1016
+ }
1017
+ write('[shmakk] STT enabled — press Ctrl+O for voice input\r\n');
1018
+ break;
1019
+ }
1020
+ case 'disable-stt': {
1021
+ if (ctx.setVoiceMode) ctx.setVoiceMode('stt', false);
1022
+ else {
1023
+ if (ctx.opts) { ctx.opts.stt = false; ctx.opts.voice = !!ctx.opts.sts; }
1024
+ }
1025
+ write('[shmakk] STT disabled\r\n');
1026
+ break;
1027
+ }
1028
+ case 'enable-tts': {
1029
+ if (ctx.setVoiceMode) ctx.setVoiceMode('tts', true);
1030
+ else if (ctx.opts) {
1031
+ ctx.opts.stt = false;
1032
+ ctx.opts.tts = true;
1033
+ ctx.opts.sts = false;
1034
+ ctx.opts.voice = false;
1035
+ }
1036
+ write('[shmakk] TTS enabled — agent replies will be spoken\r\n');
1037
+ break;
1038
+ }
1039
+ case 'disable-tts': {
1040
+ if (ctx.setVoiceMode) ctx.setVoiceMode('tts', false);
1041
+ else if (ctx.opts) ctx.opts.tts = false;
1042
+ write('[shmakk] TTS disabled\r\n');
1043
+ break;
1044
+ }
1045
+ case 'enable-sts': {
1046
+ if (ctx.setVoiceMode) ctx.setVoiceMode('sts', true);
1047
+ else if (ctx.opts) {
1048
+ ctx.opts.stt = false;
1049
+ ctx.opts.tts = false;
1050
+ ctx.opts.sts = true;
1051
+ ctx.opts.voice = true;
1052
+ }
1053
+ write('[shmakk] STS enabled — always-on speech-to-speech started\r\n');
1054
+ break;
1055
+ }
1056
+ case 'disable-sts': {
1057
+ if (ctx.setVoiceMode) ctx.setVoiceMode('sts', false);
1058
+ else if (ctx.opts) { ctx.opts.sts = false; ctx.opts.stt = false; ctx.opts.tts = false; ctx.opts.voice = false; }
1059
+ write('[shmakk] STS disabled\r\n');
1060
+ break;
1061
+ }
1062
+
910
1063
  // ── Profile ──
911
1064
  case 'set-profile': {
912
1065
  const validProfiles = ['tiny', 'balanced', 'deep', 'builder', 'large-app'];
@@ -0,0 +1,508 @@
1
+ // Convert sessions between Claude Code format and shmakk format.
2
+ //
3
+ // claude2shmakk: reads a Claude session directory (containing audit.jsonl
4
+ // and the parent {sessionId}.json metadata) and imports it into shmakk's
5
+ // SQLite session database and audit log.
6
+ //
7
+ // shmakk2claude: reads a shmakk session from its SQLite database and
8
+ // exports it as a Claude-compatible session directory with audit.jsonl
9
+ // and session JSON.
10
+ //
11
+ // Usage:
12
+ // node src/session-convert.js claude2shmakk <claude-session-dir> [shmakk-session-id]
13
+ // node src/session-convert.js shmakk2claude <shmakk-session-id> <output-dir>
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const crypto = require('crypto');
19
+ const readline = require('readline');
20
+
21
+ // ── Helpers ───────────────────────────────────────────────────────────────
22
+
23
+ function makeShmakkSessionId() {
24
+ const date = new Date().toISOString().slice(0, 10);
25
+ const rand = crypto.randomBytes(4).toString('hex');
26
+ return `${date}-${rand}`;
27
+ }
28
+
29
+ function ensureDir(dir) {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ }
32
+
33
+ function uid() {
34
+ return crypto.randomUUID();
35
+ }
36
+
37
+ // ── Claude audit.jsonl parsing ────────────────────────────────────────────
38
+
39
+ // Parse a Claude audit.jsonl file. Returns an array of normalized turns
40
+ // where each turn has { role, content, tool_calls, tool_results }.
41
+ // Multiple consecutive assistant content pieces are merged into one turn.
42
+ function parseClaudeAudit(jsonlPath) {
43
+ const raw = [];
44
+ const rl = readline.createInterface({
45
+ input: fs.createReadStream(jsonlPath),
46
+ crlfDelay: Infinity,
47
+ });
48
+
49
+ return new Promise((resolve, reject) => {
50
+ rl.on('line', (line) => {
51
+ try { raw.push(JSON.parse(line)); } catch {}
52
+ });
53
+ rl.on('close', () => resolve(normalizeClaudeLines(raw)));
54
+ rl.on('error', reject);
55
+ });
56
+ }
57
+
58
+ function normalizeClaudeLines(raw) {
59
+ const turns = [];
60
+ let pendingAssistant = null;
61
+
62
+ // CLI system messages that we don't want as user turns
63
+ const SKIP_USER_PREFIXES = [
64
+ '<command-message>',
65
+ '<command-name>',
66
+ '<system-message>',
67
+ '<bash-input>',
68
+ '<bash-stdout>',
69
+ '<bash-stderr>',
70
+ ];
71
+
72
+ function isSkippableUserContent(content) {
73
+ if (typeof content !== 'string') return false;
74
+ for (const prefix of SKIP_USER_PREFIXES) {
75
+ if (content.startsWith(prefix)) return true;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ function flushAssistant() {
81
+ if (!pendingAssistant) return;
82
+ const t = pendingAssistant;
83
+ const hasText = t.parts.some(p => p.type === 'text' && p.text.trim());
84
+ const hasToolCalls = t.parts.some(p => p.type === 'tool_use');
85
+
86
+ // Drop empty assistant blocks and thinking-only blocks
87
+ if (!hasText && !hasToolCalls) {
88
+ pendingAssistant = null;
89
+ return;
90
+ }
91
+
92
+ const turn = {
93
+ role: 'assistant',
94
+ content: t.parts.filter(p => p.type === 'text').map(p => p.text).join('') || null,
95
+ reasoning: t.parts.filter(p => p.type === 'thinking').map(p => p.thinking).join('\n') || null,
96
+ tool_calls: t.parts.filter(p => p.type === 'tool_use').map(p => ({
97
+ id: p.id,
98
+ name: p.name,
99
+ input: p.input,
100
+ })),
101
+ };
102
+ if (!turn.content && !turn.tool_calls.length) {
103
+ pendingAssistant = null;
104
+ return;
105
+ }
106
+ turns.push(turn);
107
+ pendingAssistant = null;
108
+ }
109
+
110
+ for (const line of raw) {
111
+ const msg = line.message;
112
+ if (!msg) continue;
113
+
114
+ if (line.type === 'user' && msg.role === 'user') {
115
+ // Could be text or tool_result
116
+ const content = msg.content;
117
+ if (typeof content === 'string') {
118
+ if (!isSkippableUserContent(content)) {
119
+ flushAssistant();
120
+ turns.push({ role: 'user', content, tool_calls: [], tool_results: [] });
121
+ }
122
+ } else if (Array.isArray(content)) {
123
+ const toolResults = [];
124
+ const userTexts = [];
125
+ for (const part of content) {
126
+ if (part.type === 'tool_result') {
127
+ toolResults.push({
128
+ tool_call_id: part.tool_use_id,
129
+ content: part.content,
130
+ is_error: part.is_error || false,
131
+ });
132
+ } else if (part.type === 'text') {
133
+ userTexts.push(part.text);
134
+ } else if (typeof part === 'string') {
135
+ userTexts.push(part);
136
+ }
137
+ }
138
+ flushAssistant();
139
+
140
+ if (toolResults.length > 0) {
141
+ for (const tr of toolResults) {
142
+ turns.push({
143
+ role: 'tool',
144
+ tool_call_id: tr.tool_call_id,
145
+ content: tr.content,
146
+ is_error: tr.is_error,
147
+ });
148
+ }
149
+ }
150
+ if (userTexts.length > 0) {
151
+ turns.push({ role: 'user', content: userTexts.join('\n'), tool_calls: [], tool_results: [] });
152
+ }
153
+ }
154
+ } else if (line.type === 'assistant') {
155
+ // Accumulate parts into pendingAssistant
156
+ if (!pendingAssistant) {
157
+ pendingAssistant = { model: msg.model, parts: [] };
158
+ }
159
+ for (const part of msg.content) {
160
+ pendingAssistant.parts.push(part);
161
+ }
162
+ }
163
+ // Skip system, rate_limit_event, etc.
164
+ }
165
+ flushAssistant();
166
+
167
+ return turns;
168
+ }
169
+
170
+ // ── Read Claude session metadata ──────────────────────────────────────────
171
+
172
+ function readClaudeSessionMeta(sessionDir) {
173
+ const dirName = path.basename(sessionDir); // e.g. "local_2a11b98e-..."
174
+ const parentDir = path.dirname(sessionDir);
175
+ const jsonPath = path.join(parentDir, `${dirName}.json`);
176
+
177
+ if (!fs.existsSync(jsonPath)) return null;
178
+
179
+ try {
180
+ const meta = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
181
+ return {
182
+ sessionId: meta.sessionId,
183
+ title: meta.title || 'Untitled',
184
+ cwd: meta.cwd || process.cwd(),
185
+ workspaces: (meta.userSelectedFolders || []).slice(),
186
+ model: meta.model || 'unknown',
187
+ createdAt: meta.createdAt || Date.now(),
188
+ lastActivityAt: meta.lastActivityAt || Date.now(),
189
+ };
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ // ── claude2shmakk ─────────────────────────────────────────────────────────
196
+
197
+ async function claude2shmakk(claudeSessionDir, shmakkSessionId) {
198
+ const sessionDir = path.resolve(claudeSessionDir);
199
+
200
+ // Validate input
201
+ const auditPath = path.join(sessionDir, 'audit.jsonl');
202
+ if (!fs.existsSync(auditPath)) {
203
+ // Try audit.json2 as fallback
204
+ const altPath = path.join(sessionDir, 'audit.json2');
205
+ if (fs.existsSync(altPath)) {
206
+ // audit.json2 is same format, just rename reference
207
+ } else {
208
+ console.error(`No audit.jsonl found in ${sessionDir}`);
209
+ process.exit(1);
210
+ }
211
+ }
212
+
213
+ const actualAuditPath = fs.existsSync(auditPath) ? auditPath : path.join(sessionDir, 'audit.json2');
214
+
215
+ // Read metadata
216
+ const meta = readClaudeSessionMeta(sessionDir);
217
+ const workspace = (meta && meta.workspaces && meta.workspaces.length > 0) ? meta.workspaces[0] : (meta ? meta.cwd : claudeSessionDir);
218
+
219
+ // Parse audit
220
+ console.error(`Parsing ${actualAuditPath}...`);
221
+ const turns = await parseClaudeAudit(actualAuditPath);
222
+ console.error(`Found ${turns.length} turns.`);
223
+
224
+ // Generate session ID
225
+ const sessionId = shmakkSessionId || makeShmakkSessionId();
226
+
227
+ // Load shmakk modules
228
+ const sessionSearch = require('./session-search');
229
+ const audit = require('./audit');
230
+
231
+ if (!sessionSearch.isAvailable()) {
232
+ console.error('better-sqlite3 not available. Cannot write session.');
233
+ console.error('Install with: npm install better-sqlite3');
234
+ process.exit(1);
235
+ }
236
+
237
+ // Use public API for session management
238
+ const startedAt = meta ? meta.createdAt : Date.now();
239
+ const endTs = meta ? meta.lastActivityAt : Date.now() + 100;
240
+
241
+ sessionSearch.recordSessionStart({ sessionId, workspace, pid: process.pid });
242
+
243
+ // Use the same DB connection from session-search instead of opening a new one
244
+ const db = sessionSearch.getDB();
245
+ db.prepare('UPDATE sessions SET started_at = ? WHERE id = ?').run(startedAt, sessionId);
246
+
247
+ // Write turns
248
+ const insertTurn = db.prepare(
249
+ 'INSERT INTO turns (session_id, ts, role, content) VALUES (?, ?, ?, ?)'
250
+ );
251
+
252
+ let turnTs = startedAt;
253
+ for (const turn of turns) {
254
+ turnTs += 100; // 100ms between turns to preserve ordering
255
+
256
+ if (turn.role === 'assistant') {
257
+ const text = turn.content || '';
258
+ if (text.trim()) {
259
+ insertTurn.run(sessionId, turnTs, 'assistant', text.slice(0, 50000));
260
+ }
261
+ if (turn.tool_calls && turn.tool_calls.length > 0) {
262
+ for (const tc of turn.tool_calls) {
263
+ const tcText = `[tool_use: ${tc.name}] ${JSON.stringify(tc.input)}`;
264
+ insertTurn.run(sessionId, turnTs + 1, 'assistant', tcText.slice(0, 50000));
265
+ }
266
+ }
267
+ } else if (turn.role === 'user') {
268
+ insertTurn.run(sessionId, turnTs, 'user', (turn.content || '').slice(0, 50000));
269
+ } else if (turn.role === 'tool') {
270
+ const toolText = `[tool_result: ${turn.is_error ? 'ERROR ' : ''}${turn.content || ''}]`;
271
+ insertTurn.run(sessionId, turnTs, 'tool', toolText.slice(0, 50000));
272
+ }
273
+ }
274
+
275
+ // Finalize session
276
+ sessionSearch.recordSessionEnd({
277
+ sessionId,
278
+ summary: meta ? `Imported from Claude: ${meta.title}` : 'Imported from Claude',
279
+ });
280
+ db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(endTs, sessionId);
281
+
282
+ // Write to audit log
283
+ audit.append({ kind: 'session-start', workspace, pinnedWorkspace: null, review: false, pid: process.pid, import: { from: 'claude', source: claudeSessionDir } });
284
+ audit.append({ kind: 'session-end', exitCode: 0, import: true });
285
+
286
+ // Don't close the shared DB connection — session-search owns it
287
+
288
+ console.log(`Imported Claude session -> shmakk session ${sessionId}`);
289
+ console.log(` Title: ${meta ? meta.title : 'Unknown'}`);
290
+ console.log(` Turns: ${turns.length}`);
291
+ console.log(` Workspace: ${workspace}`);
292
+ console.log(` Model: ${meta ? meta.model : 'unknown'}`);
293
+
294
+ return sessionId;
295
+ }
296
+
297
+ // ── shmakk2claude ─────────────────────────────────────────────────────────
298
+
299
+ async function shmakk2claude(shmakkSessionId, outputDir) {
300
+ const outDir = path.resolve(outputDir);
301
+ ensureDir(outDir);
302
+
303
+ // Load shmakk DB
304
+ let D;
305
+ try {
306
+ D = require('better-sqlite3');
307
+ } catch {
308
+ console.error('better-sqlite3 not available.');
309
+ process.exit(1);
310
+ }
311
+
312
+ const dbPath_ = path.join(os.homedir(), '.config', 'shmakk', 'sessions.db');
313
+ if (!fs.existsSync(dbPath_)) {
314
+ console.error('No shmakk session database found.');
315
+ process.exit(1);
316
+ }
317
+
318
+ const db = new D(dbPath_, { readonly: true });
319
+ db.pragma('journal_mode = WAL');
320
+
321
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(shmakkSessionId);
322
+ if (!session) {
323
+ console.error(`Session ${shmakkSessionId} not found.`);
324
+ db.close();
325
+ process.exit(1);
326
+ }
327
+
328
+ const turns = db.prepare(
329
+ 'SELECT * FROM turns WHERE session_id = ? ORDER BY ts, id'
330
+ ).all(shmakkSessionId);
331
+
332
+ db.close();
333
+
334
+ // Generate Claude session ID
335
+ const claudeSessionId = `local_${uid()}`;
336
+ const cliSessionId = uid();
337
+
338
+ // Create audit.jsonl
339
+ const auditLines = [];
340
+ const now = new Date().toISOString();
341
+
342
+ // System init
343
+ auditLines.push(JSON.stringify({
344
+ type: 'system',
345
+ subtype: 'init',
346
+ cwd: outDir,
347
+ session_id: cliSessionId,
348
+ tools: [], // shmakk tools not mapped
349
+ _audit_timestamp: now,
350
+ }));
351
+
352
+ // Convert turns
353
+ for (const turn of turns) {
354
+ const ts = new Date(turn.ts).toISOString();
355
+ const msgUuid = uid();
356
+
357
+ if (turn.role === 'user') {
358
+ auditLines.push(JSON.stringify({
359
+ type: 'user',
360
+ uuid: msgUuid,
361
+ session_id: cliSessionId,
362
+ parent_tool_use_id: null,
363
+ message: { role: 'user', content: turn.content },
364
+ _audit_timestamp: ts,
365
+ }));
366
+ } else if (turn.role === 'assistant') {
367
+ // Check if this is a tool_use representation
368
+ const toolMatch = turn.content.match(/^\[tool_use:\s*([^\]]+)\]\s*(.*)$/s);
369
+ if (toolMatch) {
370
+ let input = {};
371
+ try { input = JSON.parse(toolMatch[2].trim()); } catch { input = { raw: toolMatch[2].trim() }; }
372
+ auditLines.push(JSON.stringify({
373
+ type: 'assistant',
374
+ message: {
375
+ model: 'unknown',
376
+ id: `msg_${uid()}`,
377
+ type: 'message',
378
+ role: 'assistant',
379
+ content: [{ type: 'tool_use', id: `toolu_${uid()}`, name: toolMatch[1].trim(), input }],
380
+ stop_reason: 'tool_use',
381
+ usage: { input_tokens: 0, output_tokens: 0 },
382
+ },
383
+ parent_tool_use_id: null,
384
+ session_id: cliSessionId,
385
+ uuid: uid(),
386
+ _audit_timestamp: ts,
387
+ }));
388
+ } else {
389
+ auditLines.push(JSON.stringify({
390
+ type: 'assistant',
391
+ message: {
392
+ model: 'unknown',
393
+ id: `msg_${uid()}`,
394
+ type: 'message',
395
+ role: 'assistant',
396
+ content: [{ type: 'text', text: turn.content }],
397
+ stop_reason: 'end_turn',
398
+ usage: { input_tokens: 0, output_tokens: 0 },
399
+ },
400
+ parent_tool_use_id: null,
401
+ session_id: cliSessionId,
402
+ uuid: uid(),
403
+ _audit_timestamp: ts,
404
+ }));
405
+ }
406
+ } else if (turn.role === 'tool') {
407
+ const errMatch = turn.content.match(/^\[tool_result:\s*(ERROR\s*)?(.*?)\]$/s);
408
+ const isError = !!errMatch && !!errMatch[1];
409
+ const content = errMatch ? errMatch[2] : turn.content;
410
+
411
+ auditLines.push(JSON.stringify({
412
+ type: 'user',
413
+ message: {
414
+ role: 'user',
415
+ content: [{
416
+ type: 'tool_result',
417
+ content: content,
418
+ is_error: isError,
419
+ tool_use_id: null, // can't recover exact ID
420
+ }],
421
+ },
422
+ parent_tool_use_id: null,
423
+ session_id: cliSessionId,
424
+ uuid: uid(),
425
+ _audit_timestamp: ts,
426
+ }));
427
+ }
428
+ }
429
+
430
+ // Write audit.jsonl
431
+ fs.writeFileSync(path.join(outDir, 'audit.jsonl'), auditLines.join('\n') + '\n');
432
+
433
+ // Write session metadata JSON
434
+ const sessionMeta = {
435
+ sessionId: claudeSessionId,
436
+ processName: `shmakk-import-${shmakkSessionId.slice(0, 8)}`,
437
+ cliSessionId: cliSessionId,
438
+ cwd: outDir,
439
+ userSelectedFolders: session.workspace ? [session.workspace] : [],
440
+ createdAt: session.started_at,
441
+ lastActivityAt: session.ended_at || Date.now(),
442
+ model: 'unknown',
443
+ isArchived: false,
444
+ title: session.summary || `Imported from shmakk: ${shmakkSessionId}`,
445
+ hostLoopMode: false,
446
+ webFetchAllowedUrls: [],
447
+ slashCommands: [],
448
+ enabledMcpTools: {},
449
+ isAgentCompleted: true,
450
+ accountName: 'shmakk-import',
451
+ emailAddress: '',
452
+ };
453
+
454
+ fs.writeFileSync(
455
+ path.join(outDir, `${claudeSessionId}.json`),
456
+ JSON.stringify(sessionMeta, null, 2)
457
+ );
458
+
459
+ // Create session subdirectory
460
+ const subDir = path.join(outDir, claudeSessionId);
461
+ ensureDir(subDir);
462
+ fs.writeFileSync(path.join(subDir, 'context.txt'), '');
463
+
464
+ console.log(`Exported shmakk session ${shmakkSessionId} -> Claude session`);
465
+ console.log(` Output: ${outDir}`);
466
+ console.log(` Claude session ID: ${claudeSessionId}`);
467
+ console.log(` Turns: ${turns.length}`);
468
+ console.log(` Title: ${sessionMeta.title}`);
469
+ }
470
+
471
+ // ── CLI ───────────────────────────────────────────────────────────────────
472
+
473
+ async function main() {
474
+ const args = process.argv.slice(2);
475
+ const direction = args[0];
476
+ const source = args[1];
477
+ const target = args[2];
478
+
479
+ if (!direction || !source) {
480
+ console.error('Usage:');
481
+ console.error(' node src/session-convert.js claude2shmakk <claude-session-dir> [shmakk-session-id]');
482
+ console.error(' node src/session-convert.js shmakk2claude <shmakk-session-id> <output-dir>');
483
+ process.exit(1);
484
+ }
485
+
486
+ if (direction === 'claude2shmakk') {
487
+ await claude2shmakk(source, target);
488
+ } else if (direction === 'shmakk2claude') {
489
+ if (!target) {
490
+ console.error('Output directory required for shmakk2claude');
491
+ process.exit(1);
492
+ }
493
+ await shmakk2claude(source, target);
494
+ } else {
495
+ console.error(`Unknown direction: ${direction}. Use claude2shmakk or shmakk2claude.`);
496
+ process.exit(1);
497
+ }
498
+ }
499
+
500
+ if (require.main === module) {
501
+ main().catch((e) => {
502
+ console.error('ERROR:', e.message || e);
503
+ if (process.env.SHMAKK_DEBUG) console.error(e.stack);
504
+ process.exit(1);
505
+ });
506
+ }
507
+
508
+ module.exports = { claude2shmakk, shmakk2claude, parseClaudeAudit };