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.
- package/.env.example +11 -0
- package/README.md +75 -1
- package/docs/index.html +154 -16
- package/docs/mcp.md +78 -0
- package/docs/ssh.md +82 -0
- package/docs/vibedit-analysis.md +375 -0
- package/docs/vim.md +110 -0
- package/docs/voice.md +4 -0
- package/package.json +9 -5
- package/scripts/test-vibedit.js +45 -0
- package/scripts/vibedit-demo.sh +52 -0
- package/skills/shmakk-skill-creator.md +269 -0
- package/src/_check.js +7 -0
- package/src/_check_schema.js +5 -0
- package/src/_cleanup.js +18 -0
- package/src/_fix.js +9 -0
- package/src/_test_import.js +15 -0
- package/src/agent.js +11 -4
- package/src/browser-daemon.js +209 -0
- package/src/browser.js +10 -0
- package/src/cli/browserDaemon.js +60 -0
- package/src/cli/connectBrowser.js +137 -0
- package/src/cli.js +235 -8
- package/src/completions.js +8 -0
- package/src/control.js +273 -1
- package/src/core/browserConnector.js +523 -0
- package/src/correction.js +6 -0
- package/src/electron.js +305 -0
- package/src/endpoints.js +74 -9
- package/src/index.js +24 -1
- package/src/llm.js +501 -61
- package/src/mobile.js +307 -0
- package/src/notify.js +51 -3
- package/src/orchestrator.js +35 -1
- package/src/pty.js +11 -6
- package/src/review.js +45 -11
- package/src/self-commands.js +153 -0
- package/src/session-convert.js +508 -0
- package/src/session-search.js +31 -0
- package/src/session.js +392 -46
- package/src/skills/browserActions.ts +984 -0
- package/src/skills.js +451 -24
- package/src/system-prompt.js +31 -25
- package/src/tools.js +81 -0
- package/src/vibedit/control.js +534 -0
- package/src/vibedit/electron.js +108 -0
- package/src/vibedit/files.js +171 -0
- package/src/vibedit/index.js +298 -0
- package/src/vibedit/overlay.js +1482 -0
- package/src/vibedit/prompts.js +245 -0
- package/src/vibedit/state.js +32 -0
- package/src/vim.js +410 -0
package/src/self-commands.js
CHANGED
|
@@ -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 };
|