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 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.2.1",
4
- "description": "Termius-style browser dashboard for Claude Code sessions. View, search, resume, and delete sessions with a dark-themed UI.",
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": ">=16"
34
+ "node": ">=18"
35
35
  }
36
36
  }
@@ -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 + '\')">&#9733; ' + (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 => {