codedash-app 4.2.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +66 -0
- package/package.json +3 -3
- package/src/frontend/app.js +7 -0
- package/src/handoff.js +117 -0
- package/src/server.js +18 -0
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ const { loadSessions, searchFullText, getSessionPreview, computeSessionCost } =
|
|
|
4
4
|
const { startServer } = require('../src/server');
|
|
5
5
|
const { exportArchive, importArchive } = require('../src/migrate');
|
|
6
6
|
const { convertSession } = require('../src/convert');
|
|
7
|
+
const { generateHandoff, quickHandoff } = require('../src/handoff');
|
|
7
8
|
|
|
8
9
|
const DEFAULT_PORT = 3847;
|
|
9
10
|
const args = process.argv.slice(2);
|
|
@@ -131,6 +132,70 @@ switch (command) {
|
|
|
131
132
|
break;
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
case 'handoff':
|
|
136
|
+
case 'continue': {
|
|
137
|
+
const sid = args[1];
|
|
138
|
+
const target = args[2] || 'any';
|
|
139
|
+
const verbFlag = args.find(a => a.startsWith('--verbosity='));
|
|
140
|
+
const verbosity = verbFlag ? verbFlag.split('=')[1] : 'standard';
|
|
141
|
+
const outFlag = args.find(a => a.startsWith('--out='));
|
|
142
|
+
|
|
143
|
+
if (!sid) {
|
|
144
|
+
console.log(`
|
|
145
|
+
\x1b[36m\x1b[1mHandoff session to another agent\x1b[0m
|
|
146
|
+
|
|
147
|
+
Usage: codedash handoff <session-id> [target] [options]
|
|
148
|
+
|
|
149
|
+
Generates a context document for continuing a session in another tool.
|
|
150
|
+
|
|
151
|
+
Targets: claude, codex, opencode, any (default)
|
|
152
|
+
Options:
|
|
153
|
+
--verbosity=minimal|standard|verbose|full
|
|
154
|
+
--out=file.md (save to file instead of stdout)
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
codedash handoff 13ae5748 Print handoff doc
|
|
158
|
+
codedash handoff 13ae5748 codex For Codex specifically
|
|
159
|
+
codedash handoff 13ae5748 --verbosity=full Include more context
|
|
160
|
+
codedash handoff 13ae5748 --out=handoff.md Save to file
|
|
161
|
+
|
|
162
|
+
Quick handoff (latest session):
|
|
163
|
+
codedash handoff claude codex Latest Claude → Codex
|
|
164
|
+
`);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if sid is a tool name (quick handoff)
|
|
169
|
+
let result;
|
|
170
|
+
if (['claude', 'codex', 'opencode'].includes(sid)) {
|
|
171
|
+
result = quickHandoff(sid, target, { verbosity });
|
|
172
|
+
} else {
|
|
173
|
+
const allH = loadSessions();
|
|
174
|
+
const match = allH.find(s => s.id === sid || s.id.startsWith(sid));
|
|
175
|
+
if (!match) {
|
|
176
|
+
console.error(` Session not found: ${sid}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
result = generateHandoff(match.id, match.project, { verbosity, target });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!result.ok) {
|
|
183
|
+
console.error(` \x1b[31mError:\x1b[0m ${result.error}\n`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (outFlag) {
|
|
188
|
+
const outPath = outFlag.split('=')[1];
|
|
189
|
+
require('fs').writeFileSync(outPath, result.markdown);
|
|
190
|
+
console.log(`\n \x1b[32mHandoff saved to ${outPath}\x1b[0m`);
|
|
191
|
+
console.log(` Source: ${result.session.tool} (${result.session.id.slice(0, 12)})`);
|
|
192
|
+
console.log(` Messages: ${result.session.messages}\n`);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(result.markdown);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
134
199
|
case 'convert': {
|
|
135
200
|
const sid = args[1];
|
|
136
201
|
const target = args[2]; // 'claude' or 'codex'
|
|
@@ -253,6 +318,7 @@ switch (command) {
|
|
|
253
318
|
codedash show <session-id> Show session details + messages
|
|
254
319
|
codedash list [limit] List sessions in terminal
|
|
255
320
|
codedash stats Show session statistics
|
|
321
|
+
codedash handoff <id> [target] Generate handoff document
|
|
256
322
|
codedash convert <id> <format> Convert session (claude/codex)
|
|
257
323
|
codedash export [file.tar.gz] Export all sessions to archive
|
|
258
324
|
codedash import <file.tar.gz> Import sessions from archive
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codedash-app",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Dashboard + CLI for Claude Code, Codex & OpenCode sessions. View, search, resume, convert, handoff between agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codedash": "./bin/cli.js"
|
|
7
7
|
},
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
"author": "Valerii Kovalskii",
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=18"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/frontend/app.js
CHANGED
|
@@ -1091,6 +1091,7 @@ async function openDetail(s) {
|
|
|
1091
1091
|
infoHtml += '<button class="launch-btn btn-secondary" onclick="exportMd(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Export MD</button>';
|
|
1092
1092
|
var convertTarget = s.tool === 'codex' ? 'claude' : 'codex';
|
|
1093
1093
|
infoHtml += '<button class="launch-btn btn-secondary" onclick="convertTo(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\',\'' + convertTarget + '\')">Convert to ' + convertTarget + '</button>';
|
|
1094
|
+
infoHtml += '<button class="launch-btn btn-secondary" onclick="downloadHandoff(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Handoff</button>';
|
|
1094
1095
|
}
|
|
1095
1096
|
infoHtml += '<button class="star-btn detail-star' + (isStarred ? ' active' : '') + '" onclick="toggleStar(\'' + s.id + '\')">★ ' + (isStarred ? 'Starred' : 'Star') + '</button>';
|
|
1096
1097
|
infoHtml += '<button class="launch-btn btn-delete" onclick="showDeleteConfirm(\'' + s.id + '\',\'' + escHtml(s.project || '') + '\')">Delete</button>';
|
|
@@ -1813,6 +1814,12 @@ async function convertTo(sessionId, project, targetFormat) {
|
|
|
1813
1814
|
}
|
|
1814
1815
|
}
|
|
1815
1816
|
|
|
1817
|
+
// ── Handoff ───────────────────────────────────────────────────
|
|
1818
|
+
|
|
1819
|
+
function downloadHandoff(sessionId, project) {
|
|
1820
|
+
window.open('/api/handoff/' + sessionId + '?project=' + encodeURIComponent(project) + '&verbosity=standard');
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1816
1823
|
// ── Install agents ────────────────────────────────────────────
|
|
1817
1824
|
|
|
1818
1825
|
var AGENT_INSTALL = {
|
package/src/handoff.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadSessions, loadSessionDetail, getSessionPreview, findSessionFile, computeSessionCost } = require('./data');
|
|
4
|
+
|
|
5
|
+
// ── Handoff document generator ────────────────────────────
|
|
6
|
+
|
|
7
|
+
const VERBOSITY = {
|
|
8
|
+
minimal: 3,
|
|
9
|
+
standard: 10,
|
|
10
|
+
verbose: 20,
|
|
11
|
+
full: 50,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function generateHandoff(sessionId, project, options) {
|
|
15
|
+
options = options || {};
|
|
16
|
+
const verbosity = options.verbosity || 'standard';
|
|
17
|
+
const target = options.target || 'any';
|
|
18
|
+
const msgLimit = VERBOSITY[verbosity] || 10;
|
|
19
|
+
|
|
20
|
+
// Find session
|
|
21
|
+
const sessions = loadSessions();
|
|
22
|
+
const session = sessions.find(s => s.id === sessionId || s.id.startsWith(sessionId));
|
|
23
|
+
if (!session) return { ok: false, error: 'Session not found' };
|
|
24
|
+
|
|
25
|
+
// Load messages
|
|
26
|
+
const detail = loadSessionDetail(session.id, session.project || project);
|
|
27
|
+
const messages = (detail.messages || []).slice(-msgLimit);
|
|
28
|
+
const cost = computeSessionCost(session.id, session.project || project);
|
|
29
|
+
|
|
30
|
+
// Build handoff document
|
|
31
|
+
const lines = [];
|
|
32
|
+
lines.push('# Session Handoff');
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push(`> Transferred from **${session.tool}** session \`${session.id}\``);
|
|
35
|
+
lines.push(`> Project: \`${session.project_short || session.project || 'unknown'}\``);
|
|
36
|
+
lines.push(`> Started: ${session.first_time} | Last active: ${session.last_time}`);
|
|
37
|
+
lines.push(`> Messages: ${session.detail_messages || session.messages} | Cost: $${cost.cost.toFixed(2)} (${cost.model || 'unknown'})`);
|
|
38
|
+
lines.push('');
|
|
39
|
+
|
|
40
|
+
// Summary of what was being worked on
|
|
41
|
+
if (messages.length > 0) {
|
|
42
|
+
// First user message = original task
|
|
43
|
+
const firstUser = messages.find(m => m.role === 'user');
|
|
44
|
+
if (firstUser) {
|
|
45
|
+
lines.push('## Original Task');
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push(firstUser.content.slice(0, 500));
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Last assistant message = current state
|
|
52
|
+
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
|
|
53
|
+
if (lastAssistant) {
|
|
54
|
+
lines.push('## Current State (last assistant response)');
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push(lastAssistant.content.slice(0, 1000));
|
|
57
|
+
lines.push('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Last user message = latest request
|
|
61
|
+
const lastUser = [...messages].reverse().find(m => m.role === 'user');
|
|
62
|
+
if (lastUser && lastUser !== firstUser) {
|
|
63
|
+
lines.push('## Latest Request');
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(lastUser.content.slice(0, 500));
|
|
66
|
+
lines.push('');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Full recent conversation
|
|
71
|
+
lines.push('## Recent Conversation');
|
|
72
|
+
lines.push('');
|
|
73
|
+
for (const m of messages) {
|
|
74
|
+
const role = m.role === 'user' ? 'User' : 'Assistant';
|
|
75
|
+
lines.push(`### ${role}`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(m.content.slice(0, verbosity === 'full' ? 3000 : 1000));
|
|
78
|
+
lines.push('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Instructions for target agent
|
|
82
|
+
lines.push('## Instructions for New Agent');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('This is a handoff from a previous coding session. Please:');
|
|
85
|
+
lines.push('1. Read the context above to understand what was being worked on');
|
|
86
|
+
lines.push('2. Continue from where the previous agent left off');
|
|
87
|
+
lines.push('3. Do not repeat work that was already completed');
|
|
88
|
+
if (session.project) {
|
|
89
|
+
lines.push(`4. The project directory is: \`${session.project}\``);
|
|
90
|
+
}
|
|
91
|
+
lines.push('');
|
|
92
|
+
|
|
93
|
+
const markdown = lines.join('\n');
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
markdown: markdown,
|
|
98
|
+
session: {
|
|
99
|
+
id: session.id,
|
|
100
|
+
tool: session.tool,
|
|
101
|
+
project: session.project_short || session.project,
|
|
102
|
+
messages: messages.length,
|
|
103
|
+
},
|
|
104
|
+
target: target,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Quick handoff: find latest session and generate ───────
|
|
109
|
+
|
|
110
|
+
function quickHandoff(sourceTool, target, options) {
|
|
111
|
+
const sessions = loadSessions();
|
|
112
|
+
const source = sessions.find(s => s.tool === sourceTool);
|
|
113
|
+
if (!source) return { ok: false, error: `No ${sourceTool} sessions found` };
|
|
114
|
+
return generateHandoff(source.id, source.project, { ...options, target });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { generateHandoff, quickHandoff, VERBOSITY };
|
package/src/server.js
CHANGED
|
@@ -6,6 +6,7 @@ const { exec } = require('child_process');
|
|
|
6
6
|
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data');
|
|
7
7
|
const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals');
|
|
8
8
|
const { convertSession } = require('./convert');
|
|
9
|
+
const { generateHandoff } = require('./handoff');
|
|
9
10
|
const { CHANGELOG } = require('./changelog');
|
|
10
11
|
const { getHTML } = require('./html');
|
|
11
12
|
|
|
@@ -112,6 +113,23 @@ function startServer(port, openBrowser = true) {
|
|
|
112
113
|
json(res, active);
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// ── Handoff document ───────────────────
|
|
117
|
+
else if (req.method === 'GET' && pathname.startsWith('/api/handoff/')) {
|
|
118
|
+
const sessionId = pathname.split('/').pop();
|
|
119
|
+
const project = parsed.searchParams.get('project') || '';
|
|
120
|
+
const verbosity = parsed.searchParams.get('verbosity') || 'standard';
|
|
121
|
+
const result = generateHandoff(sessionId, project, { verbosity });
|
|
122
|
+
if (result.ok) {
|
|
123
|
+
res.writeHead(200, {
|
|
124
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
125
|
+
'Content-Disposition': `attachment; filename="handoff-${sessionId.slice(0, 8)}.md"`,
|
|
126
|
+
});
|
|
127
|
+
res.end(result.markdown);
|
|
128
|
+
} else {
|
|
129
|
+
json(res, result, 404);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
// ── Convert session ─────────────────────
|
|
116
134
|
else if (req.method === 'POST' && pathname === '/api/convert') {
|
|
117
135
|
readBody(req, body => {
|