heyiam 0.1.7 → 0.1.8
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/dist/analyzer.d.ts +3 -3
- package/dist/archive.d.ts +14 -0
- package/dist/archive.js +125 -0
- package/dist/archive.js.map +1 -0
- package/dist/auth.d.ts +0 -6
- package/dist/auth.js +2 -4
- package/dist/auth.js.map +1 -1
- package/dist/autostart.d.ts +19 -0
- package/dist/autostart.js +103 -0
- package/dist/autostart.js.map +1 -0
- package/dist/bridge.d.ts +0 -2
- package/dist/bridge.js +33 -4
- package/dist/bridge.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/context-export.d.ts +22 -0
- package/dist/context-export.js +230 -0
- package/dist/context-export.js.map +1 -0
- package/dist/daemon-install.d.ts +23 -0
- package/dist/daemon-install.js +155 -0
- package/dist/daemon-install.js.map +1 -0
- package/dist/db.d.ts +118 -0
- package/dist/db.js +444 -0
- package/dist/db.js.map +1 -0
- package/dist/export.d.ts +30 -0
- package/dist/export.js +377 -0
- package/dist/export.js.map +1 -0
- package/dist/format-utils.d.ts +6 -0
- package/dist/format-utils.js +15 -0
- package/dist/format-utils.js.map +1 -0
- package/dist/index.js +474 -117
- package/dist/index.js.map +1 -1
- package/dist/llm/project-enhance.js +1 -1
- package/dist/parsers/claude.js +73 -0
- package/dist/parsers/claude.js.map +1 -1
- package/dist/parsers/codex.js +1 -1
- package/dist/parsers/codex.js.map +1 -1
- package/dist/parsers/cursor.d.ts +2 -0
- package/dist/parsers/cursor.js +14 -26
- package/dist/parsers/cursor.js.map +1 -1
- package/dist/parsers/gemini.d.ts +3 -2
- package/dist/parsers/gemini.js +198 -21
- package/dist/parsers/gemini.js.map +1 -1
- package/dist/parsers/index.d.ts +1 -1
- package/dist/parsers/index.js +23 -7
- package/dist/parsers/index.js.map +1 -1
- package/dist/parsers/types.d.ts +27 -1
- package/dist/render/build-render-data.d.ts +59 -0
- package/dist/render/build-render-data.js +101 -0
- package/dist/render/build-render-data.js.map +1 -0
- package/dist/render/components/PortfolioPage.d.ts +4 -0
- package/dist/render/components/PortfolioPage.js +16 -0
- package/dist/render/components/PortfolioPage.js.map +1 -0
- package/dist/render/components/ProjectPage.d.ts +4 -0
- package/dist/render/components/ProjectPage.js +101 -0
- package/dist/render/components/ProjectPage.js.map +1 -0
- package/dist/render/components/SessionPage.d.ts +4 -0
- package/dist/render/components/SessionPage.js +29 -0
- package/dist/render/components/SessionPage.js.map +1 -0
- package/dist/render/index.d.ts +37 -0
- package/dist/render/index.js +140 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/types.d.ts +121 -0
- package/dist/render/types.js +9 -0
- package/dist/render/types.js.map +1 -0
- package/dist/routes/archive.d.ts +3 -0
- package/dist/routes/archive.js +56 -0
- package/dist/routes/archive.js.map +1 -0
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.js +116 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/context.d.ts +61 -0
- package/dist/routes/context.js +356 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/dashboard.d.ts +3 -0
- package/dist/routes/dashboard.js +103 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/enhance.d.ts +3 -0
- package/dist/routes/enhance.js +305 -0
- package/dist/routes/enhance.js.map +1 -0
- package/dist/routes/export.d.ts +3 -0
- package/dist/routes/export.js +145 -0
- package/dist/routes/export.js.map +1 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.js +13 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/preview.d.ts +3 -0
- package/dist/routes/preview.js +191 -0
- package/dist/routes/preview.js.map +1 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.js +356 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/publish.d.ts +3 -0
- package/dist/routes/publish.js +466 -0
- package/dist/routes/publish.js.map +1 -0
- package/dist/routes/search.d.ts +3 -0
- package/dist/routes/search.js +110 -0
- package/dist/routes/search.js.map +1 -0
- package/dist/routes/sessions.d.ts +3 -0
- package/dist/routes/sessions.js +103 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/settings.d.ts +3 -0
- package/dist/routes/settings.js +30 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/screenshot.d.ts +5 -2
- package/dist/screenshot.js +187 -13
- package/dist/screenshot.js.map +1 -1
- package/dist/search.d.ts +30 -0
- package/dist/search.js +153 -0
- package/dist/search.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +55 -1318
- package/dist/server.js.map +1 -1
- package/dist/settings.d.ts +23 -6
- package/dist/settings.js +36 -12
- package/dist/settings.js.map +1 -1
- package/dist/source-audit.d.ts +29 -0
- package/dist/source-audit.js +203 -0
- package/dist/source-audit.js.map +1 -0
- package/dist/sync.d.ts +74 -0
- package/dist/sync.js +358 -0
- package/dist/sync.js.map +1 -0
- package/dist/transcript.d.ts +68 -0
- package/dist/transcript.js +268 -0
- package/dist/transcript.js.map +1 -0
- package/package.json +5 -1
- package/app/dist/assets/html2canvas-Cwn_rrOw.js +0 -5
- package/app/dist/assets/index-CEQyTkgN.js +0 -14
- package/app/dist/assets/index-DLh5xRE8.css +0 -1
- package/app/dist/favicon.svg +0 -5
- package/app/dist/icons.svg +0 -24
- package/app/dist/index.html +0 -20
- package/dist/machine-key.d.ts +0 -10
- package/dist/machine-key.js +0 -51
- package/dist/machine-key.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { startServer } from './server.js';
|
|
4
4
|
import open from 'open';
|
|
5
|
-
import { checkAuthStatus, deviceAuthFlow, getAuthToken, deleteAuthToken, buildPublishPayload } from './auth.js';
|
|
6
|
-
import { loadOrCreateKeyPair, signPayload, getFingerprint } from './machine-key.js';
|
|
7
5
|
import { getAnthropicApiKey } from './settings.js';
|
|
6
|
+
import { listSessions, parseSession } from './parsers/index.js';
|
|
7
|
+
import { bridgeToAnalyzer } from './bridge.js';
|
|
8
|
+
import { archiveSessionFiles } from './archive.js';
|
|
9
|
+
import { analyzeSession } from './analyzer.js';
|
|
10
|
+
import { openDatabase, getSessionRow, getContextSummary } from './db.js';
|
|
11
|
+
import { searchSessions, decodeProjectName } from './search.js';
|
|
12
|
+
import { exportSessionContext } from './context-export.js';
|
|
13
|
+
import { SOURCE_DISPLAY_NAMES } from './parsers/types.js';
|
|
14
|
+
import { syncSessionIndex, fullReindex } from './sync.js';
|
|
15
|
+
import { formatLoc } from './format-utils.js';
|
|
8
16
|
const program = new Command();
|
|
9
17
|
program
|
|
10
18
|
.name('heyiam')
|
|
11
19
|
.description('Turn AI coding sessions into portfolio case studies')
|
|
12
|
-
.version('0.1.
|
|
20
|
+
.version('0.1.7');
|
|
13
21
|
program
|
|
14
22
|
.command('open')
|
|
15
23
|
.description('Start the local server and open the browser')
|
|
@@ -17,7 +25,6 @@ program
|
|
|
17
25
|
.option('--no-open', 'Start server without opening browser')
|
|
18
26
|
.action(async (opts) => {
|
|
19
27
|
const port = parseInt(opts.port, 10);
|
|
20
|
-
// Check for API key before starting
|
|
21
28
|
const apiKey = getAnthropicApiKey();
|
|
22
29
|
if (!apiKey) {
|
|
23
30
|
console.log('\n⚠ No Anthropic API key found.');
|
|
@@ -32,9 +39,8 @@ program
|
|
|
32
39
|
}
|
|
33
40
|
console.log('Press Ctrl+C to stop\n');
|
|
34
41
|
if (opts.open) {
|
|
35
|
-
open(url).catch(() => { });
|
|
42
|
+
open(url).catch(() => { });
|
|
36
43
|
}
|
|
37
|
-
// Keep the process alive until Ctrl+C
|
|
38
44
|
const shutdown = () => {
|
|
39
45
|
console.log('\nShutting down...');
|
|
40
46
|
server.close(() => process.exit(0));
|
|
@@ -42,122 +48,20 @@ program
|
|
|
42
48
|
};
|
|
43
49
|
process.on('SIGINT', shutdown);
|
|
44
50
|
process.on('SIGTERM', shutdown);
|
|
45
|
-
// Keep event loop alive explicitly
|
|
46
|
-
const keepAlive = setInterval(() => { }, 60_000);
|
|
47
|
-
keepAlive.unref = undefined; // prevent unref
|
|
48
|
-
});
|
|
49
|
-
program
|
|
50
|
-
.command('open-time')
|
|
51
|
-
.description('Start the server and open the time breakdown page')
|
|
52
|
-
.option('-p, --port <number>', 'Port to run on', '17845')
|
|
53
|
-
.action(async (opts) => {
|
|
54
|
-
const port = parseInt(opts.port, 10);
|
|
55
|
-
await startServer(port);
|
|
56
|
-
const url = `http://localhost:${port}/time`;
|
|
57
|
-
console.log(`\nheyiam running at http://localhost:${port}`);
|
|
58
|
-
console.log(`Opening ${url}\n`);
|
|
59
|
-
console.log('Press Ctrl+C to stop\n');
|
|
60
|
-
open(url).catch(() => { });
|
|
61
|
-
const shutdown = () => {
|
|
62
|
-
console.log('\nShutting down...');
|
|
63
|
-
process.exit(0);
|
|
64
|
-
};
|
|
65
|
-
process.on('SIGINT', shutdown);
|
|
66
|
-
process.on('SIGTERM', shutdown);
|
|
67
51
|
setInterval(() => { }, 60_000);
|
|
68
52
|
});
|
|
69
|
-
import { API_URL } from './config.js';
|
|
70
|
-
const API_BASE = API_URL;
|
|
71
|
-
program
|
|
72
|
-
.command('login')
|
|
73
|
-
.description('Authenticate with heyi.am')
|
|
74
|
-
.option('--api-url <url>', 'API base URL', API_BASE)
|
|
75
|
-
.action(async (opts) => {
|
|
76
|
-
const status = await checkAuthStatus(opts.apiUrl);
|
|
77
|
-
if (status.authenticated) {
|
|
78
|
-
console.log(`Already logged in as ${status.username}`);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
console.log('Starting device authorization...');
|
|
82
|
-
try {
|
|
83
|
-
const auth = await deviceAuthFlow(opts.apiUrl, undefined, {
|
|
84
|
-
openBrowser: (url) => open(url).then(() => { }),
|
|
85
|
-
onUserCode: (code, uri) => {
|
|
86
|
-
console.log(`\nOpen ${uri} and enter code: ${code}\n`);
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
console.log(`Logged in as ${auth.username}`);
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
console.error(`Login failed: ${err.message}`);
|
|
93
|
-
process.exitCode = 1;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
program
|
|
97
|
-
.command('logout')
|
|
98
|
-
.description('Remove saved authentication credentials')
|
|
99
|
-
.action(() => {
|
|
100
|
-
deleteAuthToken();
|
|
101
|
-
console.log('Logged out. Run `heyiam login` to re-authenticate.');
|
|
102
|
-
});
|
|
103
|
-
program
|
|
104
|
-
.command('publish')
|
|
105
|
-
.description('Publish a session to heyi.am')
|
|
106
|
-
.option('--api-url <url>', 'API base URL', API_BASE)
|
|
107
|
-
.action(async (opts) => {
|
|
108
|
-
const auth = getAuthToken();
|
|
109
|
-
if (!auth) {
|
|
110
|
-
console.error('Not logged in. Run `heyiam login` first.');
|
|
111
|
-
process.exitCode = 1;
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const keyPair = loadOrCreateKeyPair();
|
|
115
|
-
const fingerprint = getFingerprint(keyPair.publicKey);
|
|
116
|
-
console.log(`Using machine key: ${fingerprint}`);
|
|
117
|
-
// TODO: session data will come from the parser pipeline once Task 8.3+ are complete
|
|
118
|
-
const sessionData = { placeholder: true };
|
|
119
|
-
const payloadStr = JSON.stringify(sessionData);
|
|
120
|
-
const signature = signPayload(payloadStr, keyPair.privateKey);
|
|
121
|
-
const body = buildPublishPayload(sessionData, signature, keyPair.publicKey);
|
|
122
|
-
try {
|
|
123
|
-
const res = await fetch(`${opts.apiUrl}/api/shares`, {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: {
|
|
126
|
-
'Content-Type': 'application/json',
|
|
127
|
-
Authorization: `Bearer ${auth.token}`,
|
|
128
|
-
},
|
|
129
|
-
body: JSON.stringify(body),
|
|
130
|
-
});
|
|
131
|
-
if (!res.ok) {
|
|
132
|
-
const err = await res.text();
|
|
133
|
-
console.error(`Publish failed (${res.status}): ${err}`);
|
|
134
|
-
process.exitCode = 1;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const result = (await res.json());
|
|
138
|
-
console.log(`Published! ${result.url ?? ''}`);
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
console.error(`Publish failed: ${err.message}`);
|
|
142
|
-
process.exitCode = 1;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
import { listSessions, parseSession } from './parsers/index.js';
|
|
146
|
-
import { bridgeToAnalyzer } from './bridge.js';
|
|
147
|
-
import { analyzeSession } from './analyzer.js';
|
|
148
53
|
program
|
|
149
54
|
.command('time')
|
|
150
55
|
.description('Show your time vs agent time per project')
|
|
151
56
|
.action(async () => {
|
|
152
57
|
const allSessions = await listSessions();
|
|
153
|
-
|
|
58
|
+
await archiveSessionFiles(allSessions);
|
|
154
59
|
const byDir = new Map();
|
|
155
60
|
for (const s of allSessions) {
|
|
156
61
|
const existing = byDir.get(s.projectDir) ?? [];
|
|
157
62
|
existing.push(s);
|
|
158
63
|
byDir.set(s.projectDir, existing);
|
|
159
64
|
}
|
|
160
|
-
// Derive human-readable name: "-Users-ben-Dev-heyi-am" → "heyi-am"
|
|
161
65
|
const displayName = (dir) => {
|
|
162
66
|
const devIdx = dir.indexOf('-Dev-');
|
|
163
67
|
if (devIdx !== -1)
|
|
@@ -169,7 +73,6 @@ program
|
|
|
169
73
|
for (const [dirName, sessions] of byDir) {
|
|
170
74
|
let yourMinutes = 0;
|
|
171
75
|
let agentMinutes = 0;
|
|
172
|
-
// Only count parent sessions (not subagents)
|
|
173
76
|
const parents = sessions.filter(s => !s.isSubagent);
|
|
174
77
|
for (const meta of parents) {
|
|
175
78
|
try {
|
|
@@ -178,12 +81,11 @@ program
|
|
|
178
81
|
const session = analyzeSession(analysis);
|
|
179
82
|
const dur = session.durationMinutes ?? 0;
|
|
180
83
|
yourMinutes += dur;
|
|
181
|
-
agentMinutes += dur;
|
|
84
|
+
agentMinutes += dur;
|
|
182
85
|
}
|
|
183
86
|
catch {
|
|
184
87
|
// Skip sessions that fail to parse
|
|
185
88
|
}
|
|
186
|
-
// Add child/subagent time
|
|
187
89
|
for (const child of meta.children ?? []) {
|
|
188
90
|
try {
|
|
189
91
|
const parsed = await parseSession(child.path);
|
|
@@ -200,7 +102,6 @@ program
|
|
|
200
102
|
projects.push({ name: displayName(dirName), yourMinutes, agentMinutes, sessions: parents.length });
|
|
201
103
|
}
|
|
202
104
|
}
|
|
203
|
-
// Sort by agent time descending
|
|
204
105
|
projects.sort((a, b) => b.agentMinutes - a.agentMinutes);
|
|
205
106
|
const fmtTime = (mins) => {
|
|
206
107
|
if (mins >= 60) {
|
|
@@ -209,7 +110,6 @@ program
|
|
|
209
110
|
}
|
|
210
111
|
return `${Math.round(mins)}m`;
|
|
211
112
|
};
|
|
212
|
-
// Print table
|
|
213
113
|
const totalYou = projects.reduce((s, p) => s + p.yourMinutes, 0);
|
|
214
114
|
const totalAgent = projects.reduce((s, p) => s + p.agentMinutes, 0);
|
|
215
115
|
console.log('');
|
|
@@ -226,16 +126,473 @@ program
|
|
|
226
126
|
console.log(' Detailed view: heyiam open, then visit /time');
|
|
227
127
|
console.log('');
|
|
228
128
|
});
|
|
129
|
+
// ── Search command ────────────────────────────────────────────
|
|
130
|
+
program
|
|
131
|
+
.command('search [query]')
|
|
132
|
+
.description('Search across all AI sessions')
|
|
133
|
+
.option('--project <name>', 'Filter by project name')
|
|
134
|
+
.option('--source <source>', 'Filter by source (claude, cursor, codex, gemini)')
|
|
135
|
+
.option('--after <date>', 'Sessions after this date (ISO)')
|
|
136
|
+
.option('--before <date>', 'Sessions before this date (ISO)')
|
|
137
|
+
.option('--skill <skill>', 'Filter by skill name')
|
|
138
|
+
.option('--file <path>', 'Filter by file path')
|
|
139
|
+
.option('--min-duration <minutes>', 'Minimum session duration in minutes', parseInt)
|
|
140
|
+
.action(async (query, opts) => {
|
|
141
|
+
const db = openDatabase();
|
|
142
|
+
// Archive on every CLI command — don't lose sessions between opens
|
|
143
|
+
const searchSessions2 = await listSessions();
|
|
144
|
+
await archiveSessionFiles(searchSessions2);
|
|
145
|
+
await syncSessionIndex(db, undefined, (p) => {
|
|
146
|
+
if (p.phase === 'indexing' && p.current === 1 && (p.total ?? 0) > 0) {
|
|
147
|
+
console.log(` Syncing index (${p.total} sessions)...`);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
const filters = {
|
|
151
|
+
project: opts.project,
|
|
152
|
+
source: opts.source,
|
|
153
|
+
after: opts.after,
|
|
154
|
+
before: opts.before,
|
|
155
|
+
skill: opts.skill,
|
|
156
|
+
file: opts.file,
|
|
157
|
+
minDuration: opts.minDuration,
|
|
158
|
+
};
|
|
159
|
+
// Remove undefined values
|
|
160
|
+
for (const key of Object.keys(filters)) {
|
|
161
|
+
if (filters[key] === undefined)
|
|
162
|
+
delete filters[key];
|
|
163
|
+
}
|
|
164
|
+
const results = searchSessions(db, query, Object.keys(filters).length > 0 ? filters : undefined);
|
|
165
|
+
if (results.length === 0) {
|
|
166
|
+
console.log('\n No results found.\n');
|
|
167
|
+
db.close();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
171
|
+
for (const r of results) {
|
|
172
|
+
const sourceName = SOURCE_DISPLAY_NAMES[r.source] ?? r.source;
|
|
173
|
+
const date = formatDateShort(r.date);
|
|
174
|
+
const dur = r.durationMinutes > 0 ? `${r.durationMinutes}m` : '';
|
|
175
|
+
const loc = r.linesOfCode > 0 ? formatLoc(r.linesOfCode) + ' LOC' : '';
|
|
176
|
+
const projectName = r.projectName.split('/').pop() ?? r.projectName;
|
|
177
|
+
console.log(` ${projectName} / ${r.title || 'Untitled'}`);
|
|
178
|
+
console.log(` ${sourceName} · ${date}${dur ? ' · ' + dur : ''}${r.turns ? ' · ' + r.turns + ' turns' : ''}${loc ? ' · ' + loc : ''}`);
|
|
179
|
+
if (r.snippet) {
|
|
180
|
+
const clean = r.snippet.replace(/<\/?mark>/g, '').replace(/\.\.\./g, '\u2026').trim();
|
|
181
|
+
if (clean)
|
|
182
|
+
console.log(` ${clean}`);
|
|
183
|
+
}
|
|
184
|
+
console.log('');
|
|
185
|
+
}
|
|
186
|
+
console.log(`${results.length} result${results.length === 1 ? '' : 's'} found\n`);
|
|
187
|
+
db.close();
|
|
188
|
+
});
|
|
189
|
+
// ── Context command ──────────────────────────────────────────
|
|
190
|
+
program
|
|
191
|
+
.command('context <sessionId>')
|
|
192
|
+
.description('Export a session as compressed context for AI consumption')
|
|
193
|
+
.option('--full', 'Include all turns (large output)')
|
|
194
|
+
.option('--compact', 'Metadata + execution path only (smallest)')
|
|
195
|
+
.option('--clipboard', 'Copy to clipboard instead of stdout')
|
|
196
|
+
.action(async (sessionId, opts) => {
|
|
197
|
+
const db = openDatabase();
|
|
198
|
+
// Archive on every CLI command
|
|
199
|
+
const ctxSessions = await listSessions();
|
|
200
|
+
await archiveSessionFiles(ctxSessions);
|
|
201
|
+
await syncSessionIndex(db);
|
|
202
|
+
const row = getSessionRow(db, sessionId);
|
|
203
|
+
if (!row) {
|
|
204
|
+
console.error(`\n Session not found: ${sessionId}\n`);
|
|
205
|
+
db.close();
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
const tier = opts.compact ? 'compact' : opts.full ? 'full' : 'summary';
|
|
209
|
+
let result;
|
|
210
|
+
// Try to load the source file; fall back to stored context summary if unavailable
|
|
211
|
+
let sourceAvailable = false;
|
|
212
|
+
if (row.file_path) {
|
|
213
|
+
try {
|
|
214
|
+
const parsed = await parseSession(row.file_path);
|
|
215
|
+
const projectName = decodeProjectName(row.project_dir).split('/').pop() ?? row.project_dir;
|
|
216
|
+
const analysis = bridgeToAnalyzer(parsed, { sessionId: row.id, projectName });
|
|
217
|
+
const session = analyzeSession(analysis);
|
|
218
|
+
result = exportSessionContext(session, analysis.turns, { tier });
|
|
219
|
+
sourceAvailable = true;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Source file is gone or unreadable — fall through to stored summary
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!sourceAvailable) {
|
|
226
|
+
const stored = getContextSummary(db, sessionId);
|
|
227
|
+
if (stored) {
|
|
228
|
+
// Stored summary is always compact tier — inform user if they requested a richer tier
|
|
229
|
+
const { estimateTokens } = await import('./context-export.js');
|
|
230
|
+
if (tier !== 'compact') {
|
|
231
|
+
console.error(`\n Source file unavailable — using stored compact summary (${tier} tier requires source data).\n`);
|
|
232
|
+
}
|
|
233
|
+
result = { content: stored, tokens: estimateTokens(stored), tier: 'compact' };
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.error(`\n Session source file is unavailable and no stored summary exists.\n Re-index while the source file exists to store a summary.\n`);
|
|
237
|
+
db.close();
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (opts.clipboard) {
|
|
242
|
+
try {
|
|
243
|
+
const { default: clipboardy } = await import('clipboardy');
|
|
244
|
+
await clipboardy.write(result.content);
|
|
245
|
+
console.log(`\n Session context copied (${result.tokens.toLocaleString()} tokens, ${result.tier} format)\n`);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Fallback: try pbcopy on macOS using spawn (no shell injection)
|
|
249
|
+
try {
|
|
250
|
+
const { spawn } = await import('node:child_process');
|
|
251
|
+
await new Promise((resolve, reject) => {
|
|
252
|
+
const proc = spawn('pbcopy', [], { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
253
|
+
proc.stdin.write(result.content);
|
|
254
|
+
proc.stdin.end();
|
|
255
|
+
proc.on('close', (code) => code === 0 ? resolve() : reject(new Error('pbcopy failed')));
|
|
256
|
+
proc.on('error', reject);
|
|
257
|
+
});
|
|
258
|
+
console.log(`\n Session context copied (${result.tokens.toLocaleString()} tokens, ${result.tier} format)\n`);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
console.error('\n Could not copy to clipboard. Install clipboardy or use stdout.\n');
|
|
262
|
+
process.stdout.write(result.content);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
process.stdout.write(result.content + '\n');
|
|
268
|
+
}
|
|
269
|
+
db.close();
|
|
270
|
+
});
|
|
271
|
+
// ── Reindex command ──────────────────────────────────────────
|
|
272
|
+
program
|
|
273
|
+
.command('reindex')
|
|
274
|
+
.description('Rebuild the SQLite search index from scratch')
|
|
275
|
+
.action(async () => {
|
|
276
|
+
const db = openDatabase();
|
|
277
|
+
console.log('\n Clearing existing index...');
|
|
278
|
+
const result = await fullReindex(db, undefined, (p) => {
|
|
279
|
+
if (p.phase === 'indexing' && p.current && p.total) {
|
|
280
|
+
if (p.current === 1)
|
|
281
|
+
console.log(` Indexing ${p.total} sessions...`);
|
|
282
|
+
if (p.current % 50 === 0 || p.current === p.total) {
|
|
283
|
+
const pct = Math.round((p.current / p.total) * 100);
|
|
284
|
+
process.stdout.write(`\r Progress: ${p.current}/${p.total} (${pct}%)`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
console.log('');
|
|
289
|
+
console.log(` Done. ${result.indexed} sessions indexed${result.errors > 0 ? `, ${result.errors} errors` : ''}.\n`);
|
|
290
|
+
db.close();
|
|
291
|
+
});
|
|
292
|
+
// ── Archive command (standalone) ─────────────────────────────
|
|
293
|
+
program
|
|
294
|
+
.command('archive')
|
|
295
|
+
.description('Discover and archive sessions from all sources')
|
|
296
|
+
.action(async () => {
|
|
297
|
+
console.log('\n Discovering sessions...');
|
|
298
|
+
const allSessions = await listSessions();
|
|
299
|
+
// Count by source
|
|
300
|
+
const bySource = new Map();
|
|
301
|
+
for (const s of allSessions) {
|
|
302
|
+
bySource.set(s.source, (bySource.get(s.source) || 0) + 1);
|
|
303
|
+
}
|
|
304
|
+
const result = await archiveSessionFiles(allSessions);
|
|
305
|
+
const total = result.archived + result.alreadyArchived;
|
|
306
|
+
console.log(` Archived ${result.archived} new (${total} total across ${bySource.size} sources)`);
|
|
307
|
+
for (const [source, count] of bySource) {
|
|
308
|
+
const name = SOURCE_DISPLAY_NAMES[source] ?? source;
|
|
309
|
+
console.log(` ${name.padEnd(15)} ${count} sessions`);
|
|
310
|
+
}
|
|
311
|
+
if (result.cursorExported > 0) {
|
|
312
|
+
console.log(` Exported ${result.cursorExported} Cursor sessions as JSONL`);
|
|
313
|
+
}
|
|
314
|
+
if (result.failed > 0) {
|
|
315
|
+
console.log(` ${result.failed} failed`);
|
|
316
|
+
}
|
|
317
|
+
console.log('');
|
|
318
|
+
});
|
|
319
|
+
// ── Sync command (standalone) ───────────────────────────────
|
|
320
|
+
program
|
|
321
|
+
.command('sync')
|
|
322
|
+
.description('Index sessions into SQLite search database')
|
|
323
|
+
.action(async () => {
|
|
324
|
+
const db = openDatabase();
|
|
325
|
+
console.log('\n Syncing index...');
|
|
326
|
+
// Also archive while we're at it
|
|
327
|
+
const allSessions = await listSessions();
|
|
328
|
+
await archiveSessionFiles(allSessions);
|
|
329
|
+
const result = await syncSessionIndex(db, undefined, (p) => {
|
|
330
|
+
if (p.phase === 'indexing' && p.current && p.total) {
|
|
331
|
+
if (p.current % 50 === 0 || p.current === p.total) {
|
|
332
|
+
process.stdout.write(`\r Indexing: ${p.current}/${p.total}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
const { countPreservedSessions } = await import('./db.js');
|
|
337
|
+
const preserved = countPreservedSessions(db);
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(` Indexed ${result.indexed} sessions (${result.indexed + result.skipped} total, ${preserved} preserved)`);
|
|
340
|
+
console.log('');
|
|
341
|
+
db.close();
|
|
342
|
+
});
|
|
343
|
+
// ── Status command ──────────────────────────────────────────
|
|
344
|
+
program
|
|
345
|
+
.command('status')
|
|
346
|
+
.description('Show archive health, session counts, and daemon status')
|
|
347
|
+
.action(async () => {
|
|
348
|
+
const db = openDatabase();
|
|
349
|
+
await syncSessionIndex(db);
|
|
350
|
+
const { getSessionCount, countPreservedSessions } = await import('./db.js');
|
|
351
|
+
const { getAllProjectStats } = await import('./db.js');
|
|
352
|
+
const totalSessions = getSessionCount(db);
|
|
353
|
+
const preserved = countPreservedSessions(db);
|
|
354
|
+
const projects = getAllProjectStats(db);
|
|
355
|
+
// Source breakdown
|
|
356
|
+
const bySource = new Map();
|
|
357
|
+
const rows = db.prepare('SELECT source, COUNT(*) as c FROM sessions GROUP BY source').all();
|
|
358
|
+
for (const row of rows)
|
|
359
|
+
bySource.set(row.source, row.c);
|
|
360
|
+
// Daemon status
|
|
361
|
+
const { existsSync, readFileSync } = await import('node:fs');
|
|
362
|
+
const { join } = await import('node:path');
|
|
363
|
+
const { homedir } = await import('node:os');
|
|
364
|
+
const pidFile = join(homedir(), '.config', 'heyiam', 'daemon', 'daemon.pid');
|
|
365
|
+
let daemonRunning = false;
|
|
366
|
+
if (existsSync(pidFile)) {
|
|
367
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
368
|
+
try {
|
|
369
|
+
process.kill(pid, 0);
|
|
370
|
+
daemonRunning = true;
|
|
371
|
+
}
|
|
372
|
+
catch { /* not running */ }
|
|
373
|
+
}
|
|
374
|
+
// Status file
|
|
375
|
+
const statusFile = join(homedir(), '.config', 'heyiam', 'daemon', 'status.json');
|
|
376
|
+
let lastSync = 'never';
|
|
377
|
+
if (existsSync(statusFile)) {
|
|
378
|
+
try {
|
|
379
|
+
const status = JSON.parse(readFileSync(statusFile, 'utf-8'));
|
|
380
|
+
if (status.lastSync) {
|
|
381
|
+
const ago = Math.round((Date.now() - new Date(status.lastSync).getTime()) / 60000);
|
|
382
|
+
lastSync = ago < 1 ? 'just now' : `${ago}m ago`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch { /* ignore */ }
|
|
386
|
+
}
|
|
387
|
+
console.log('');
|
|
388
|
+
console.log(' heyi.am status');
|
|
389
|
+
console.log(' ' + '─'.repeat(50));
|
|
390
|
+
console.log(` Sessions: ${totalSessions} indexed`);
|
|
391
|
+
console.log(` Preserved: ${preserved} (source file deleted, DB has content)`);
|
|
392
|
+
console.log(` Projects: ${projects.length}`);
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(' Sources:');
|
|
395
|
+
for (const [source, count] of bySource) {
|
|
396
|
+
const name = SOURCE_DISPLAY_NAMES[source] ?? source;
|
|
397
|
+
console.log(` ${name.padEnd(15)} ${count} sessions`);
|
|
398
|
+
}
|
|
399
|
+
console.log('');
|
|
400
|
+
console.log(` Daemon: ${daemonRunning ? '● running' : '○ stopped'}`);
|
|
401
|
+
console.log(` Last sync: ${lastSync}`);
|
|
402
|
+
console.log('');
|
|
403
|
+
db.close();
|
|
404
|
+
});
|
|
405
|
+
// ── Daemon management ───────────────────────────────────────
|
|
406
|
+
const daemon = program
|
|
407
|
+
.command('daemon')
|
|
408
|
+
.description('Manage the background archiving daemon');
|
|
409
|
+
daemon
|
|
410
|
+
.command('start')
|
|
411
|
+
.description('Start the background tray daemon')
|
|
412
|
+
.action(async () => {
|
|
413
|
+
const { existsSync } = await import('node:fs');
|
|
414
|
+
const { join } = await import('node:path');
|
|
415
|
+
const { homedir } = await import('node:os');
|
|
416
|
+
const { spawn } = await import('node:child_process');
|
|
417
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
418
|
+
const daemonDir = join(homedir(), '.config', 'heyiam', 'daemon');
|
|
419
|
+
const binaryPath = join(daemonDir, 'heyiam-tray');
|
|
420
|
+
const pidFile = join(daemonDir, 'daemon.pid');
|
|
421
|
+
if (!existsSync(binaryPath)) {
|
|
422
|
+
console.log('\n Daemon not installed. Run: heyiam daemon install\n');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Check if already running
|
|
426
|
+
if (existsSync(pidFile)) {
|
|
427
|
+
const pid = parseInt(await import('node:fs').then(fs => fs.readFileSync(pidFile, 'utf-8').trim()), 10);
|
|
428
|
+
try {
|
|
429
|
+
process.kill(pid, 0);
|
|
430
|
+
console.log('\n Daemon is already running.\n');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
catch { /* stale pid */ }
|
|
434
|
+
}
|
|
435
|
+
const child = spawn(binaryPath, [], {
|
|
436
|
+
detached: true,
|
|
437
|
+
stdio: 'ignore',
|
|
438
|
+
});
|
|
439
|
+
child.unref();
|
|
440
|
+
mkdirSync(daemonDir, { recursive: true });
|
|
441
|
+
writeFileSync(pidFile, String(child.pid));
|
|
442
|
+
console.log(`\n Daemon started (PID ${child.pid})\n`);
|
|
443
|
+
});
|
|
444
|
+
daemon
|
|
445
|
+
.command('stop')
|
|
446
|
+
.description('Stop the background tray daemon')
|
|
447
|
+
.action(async () => {
|
|
448
|
+
const { existsSync, readFileSync, unlinkSync } = await import('node:fs');
|
|
449
|
+
const { join } = await import('node:path');
|
|
450
|
+
const { homedir } = await import('node:os');
|
|
451
|
+
const pidFile = join(homedir(), '.config', 'heyiam', 'daemon', 'daemon.pid');
|
|
452
|
+
if (!existsSync(pidFile)) {
|
|
453
|
+
console.log('\n Daemon is not running.\n');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
457
|
+
try {
|
|
458
|
+
process.kill(pid, 'SIGTERM');
|
|
459
|
+
unlinkSync(pidFile);
|
|
460
|
+
console.log('\n Daemon stopped.\n');
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
unlinkSync(pidFile);
|
|
464
|
+
console.log('\n Daemon was not running (stale PID file removed).\n');
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
daemon
|
|
468
|
+
.command('status')
|
|
469
|
+
.description('Show daemon status')
|
|
470
|
+
.action(async () => {
|
|
471
|
+
const { existsSync, readFileSync } = await import('node:fs');
|
|
472
|
+
const { join } = await import('node:path');
|
|
473
|
+
const { homedir } = await import('node:os');
|
|
474
|
+
const daemonDir = join(homedir(), '.config', 'heyiam', 'daemon');
|
|
475
|
+
const pidFile = join(daemonDir, 'daemon.pid');
|
|
476
|
+
const statusFile = join(daemonDir, 'status.json');
|
|
477
|
+
const binaryPath = join(daemonDir, 'heyiam-tray');
|
|
478
|
+
const installed = existsSync(binaryPath);
|
|
479
|
+
let running = false;
|
|
480
|
+
let pid = null;
|
|
481
|
+
if (existsSync(pidFile)) {
|
|
482
|
+
pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
483
|
+
try {
|
|
484
|
+
process.kill(pid, 0);
|
|
485
|
+
running = true;
|
|
486
|
+
}
|
|
487
|
+
catch { /* not running */ }
|
|
488
|
+
}
|
|
489
|
+
console.log('');
|
|
490
|
+
console.log(` Installed: ${installed ? 'yes' : 'no'}`);
|
|
491
|
+
console.log(` Running: ${running ? `yes (PID ${pid})` : 'no'}`);
|
|
492
|
+
if (existsSync(statusFile)) {
|
|
493
|
+
try {
|
|
494
|
+
const status = JSON.parse(readFileSync(statusFile, 'utf-8'));
|
|
495
|
+
if (status.lastSync) {
|
|
496
|
+
const ago = Math.round((Date.now() - new Date(status.lastSync).getTime()) / 60000);
|
|
497
|
+
console.log(` Last sync: ${ago < 1 ? 'just now' : `${ago}m ago`}`);
|
|
498
|
+
}
|
|
499
|
+
if (status.sessionCount)
|
|
500
|
+
console.log(` Sessions: ${status.sessionCount}`);
|
|
501
|
+
if (status.warnings?.length > 0) {
|
|
502
|
+
console.log(` Warnings: ${status.warnings.join(', ')}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch { /* ignore */ }
|
|
506
|
+
}
|
|
507
|
+
console.log('');
|
|
508
|
+
});
|
|
509
|
+
daemon
|
|
510
|
+
.command('install')
|
|
511
|
+
.description('Download and install the background tray daemon')
|
|
512
|
+
.option('--force', 'Reinstall even if already installed')
|
|
513
|
+
.action(async (opts) => {
|
|
514
|
+
const { installDaemon, getDaemonBinaryPath } = await import('./daemon-install.js');
|
|
515
|
+
const { existsSync } = await import('node:fs');
|
|
516
|
+
const binaryPath = getDaemonBinaryPath();
|
|
517
|
+
if (existsSync(binaryPath) && !opts.force) {
|
|
518
|
+
console.log('\n Daemon is already installed.');
|
|
519
|
+
console.log(' To reinstall, run: heyiam daemon install --force\n');
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const result = await installDaemon((msg) => console.log(msg));
|
|
524
|
+
console.log(`\n Daemon installed (${result.version})`);
|
|
525
|
+
console.log(` Binary: ${result.binaryPath}`);
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
529
|
+
console.error(`\n Failed to install daemon: ${message}\n`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
// Auto-start registration prompt
|
|
533
|
+
const { askYesNo, registerAutostart } = await import('./autostart.js');
|
|
534
|
+
const wantAutostart = await askYesNo(' Start daemon automatically on login? (y/n) ');
|
|
535
|
+
if (wantAutostart) {
|
|
536
|
+
const result = registerAutostart();
|
|
537
|
+
if (result.registered) {
|
|
538
|
+
console.log(`\n Auto-start registered via ${result.method}.`);
|
|
539
|
+
console.log(' The daemon will start automatically on your next login.\n');
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
console.log('\n Auto-start is not supported on this platform yet.');
|
|
543
|
+
console.log(' You can start the daemon manually with: heyiam daemon start\n');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
console.log('\n Skipped auto-start. You can start manually with: heyiam daemon start\n');
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
daemon
|
|
551
|
+
.command('uninstall')
|
|
552
|
+
.description('Remove the background tray daemon')
|
|
553
|
+
.action(async () => {
|
|
554
|
+
const { existsSync, unlinkSync } = await import('node:fs');
|
|
555
|
+
const { join } = await import('node:path');
|
|
556
|
+
const { homedir } = await import('node:os');
|
|
557
|
+
// Stop first
|
|
558
|
+
const pidFile = join(homedir(), '.config', 'heyiam', 'daemon', 'daemon.pid');
|
|
559
|
+
if (existsSync(pidFile)) {
|
|
560
|
+
const pid = parseInt((await import('node:fs')).readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
561
|
+
try {
|
|
562
|
+
process.kill(pid, 'SIGTERM');
|
|
563
|
+
}
|
|
564
|
+
catch { /* already dead */ }
|
|
565
|
+
unlinkSync(pidFile);
|
|
566
|
+
}
|
|
567
|
+
// Remove binary
|
|
568
|
+
const binaryPath = join(homedir(), '.config', 'heyiam', 'daemon', 'heyiam-tray');
|
|
569
|
+
if (existsSync(binaryPath))
|
|
570
|
+
unlinkSync(binaryPath);
|
|
571
|
+
// Remove auto-start registration (macOS launchd, Linux XDG)
|
|
572
|
+
const { unregisterAutostart } = await import('./autostart.js');
|
|
573
|
+
const autostart = unregisterAutostart();
|
|
574
|
+
if (autostart.removed) {
|
|
575
|
+
console.log(' Auto-start registration removed.');
|
|
576
|
+
}
|
|
577
|
+
console.log('\n Daemon uninstalled.\n');
|
|
578
|
+
});
|
|
579
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
580
|
+
function formatDateShort(iso) {
|
|
581
|
+
const d = new Date(iso);
|
|
582
|
+
if (isNaN(d.getTime()))
|
|
583
|
+
return iso;
|
|
584
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
585
|
+
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
586
|
+
}
|
|
229
587
|
export { program };
|
|
230
|
-
// Only run if this is the entry point (not imported for testing)
|
|
588
|
+
// Only run if this is the entry point (not imported for testing)
|
|
231
589
|
import { realpathSync } from 'node:fs';
|
|
232
590
|
const resolvedArgv = process.argv[1] ? realpathSync(process.argv[1]) : '';
|
|
233
591
|
const isDirectRun = resolvedArgv.endsWith('/dist/index.js') ||
|
|
234
592
|
resolvedArgv.endsWith('/src/index.ts');
|
|
235
593
|
if (isDirectRun) {
|
|
236
|
-
// If no command given (just `heyiam`), default to `open`
|
|
237
594
|
const args = process.argv.slice(2);
|
|
238
|
-
const knownCommands = ['open', '
|
|
595
|
+
const knownCommands = ['open', 'time', 'search', 'context', 'reindex', 'archive', 'sync', 'status', 'daemon'];
|
|
239
596
|
if (args.length === 0 || !knownCommands.includes(args[0])) {
|
|
240
597
|
process.argv.splice(2, 0, 'open');
|
|
241
598
|
}
|