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.
Files changed (136) hide show
  1. package/dist/analyzer.d.ts +3 -3
  2. package/dist/archive.d.ts +14 -0
  3. package/dist/archive.js +125 -0
  4. package/dist/archive.js.map +1 -0
  5. package/dist/auth.d.ts +0 -6
  6. package/dist/auth.js +2 -4
  7. package/dist/auth.js.map +1 -1
  8. package/dist/autostart.d.ts +19 -0
  9. package/dist/autostart.js +103 -0
  10. package/dist/autostart.js.map +1 -0
  11. package/dist/bridge.d.ts +0 -2
  12. package/dist/bridge.js +33 -4
  13. package/dist/bridge.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.js +2 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/context-export.d.ts +22 -0
  18. package/dist/context-export.js +230 -0
  19. package/dist/context-export.js.map +1 -0
  20. package/dist/daemon-install.d.ts +23 -0
  21. package/dist/daemon-install.js +155 -0
  22. package/dist/daemon-install.js.map +1 -0
  23. package/dist/db.d.ts +118 -0
  24. package/dist/db.js +444 -0
  25. package/dist/db.js.map +1 -0
  26. package/dist/export.d.ts +30 -0
  27. package/dist/export.js +377 -0
  28. package/dist/export.js.map +1 -0
  29. package/dist/format-utils.d.ts +6 -0
  30. package/dist/format-utils.js +15 -0
  31. package/dist/format-utils.js.map +1 -0
  32. package/dist/index.js +474 -117
  33. package/dist/index.js.map +1 -1
  34. package/dist/llm/project-enhance.js +1 -1
  35. package/dist/parsers/claude.js +73 -0
  36. package/dist/parsers/claude.js.map +1 -1
  37. package/dist/parsers/codex.js +1 -1
  38. package/dist/parsers/codex.js.map +1 -1
  39. package/dist/parsers/cursor.d.ts +2 -0
  40. package/dist/parsers/cursor.js +14 -26
  41. package/dist/parsers/cursor.js.map +1 -1
  42. package/dist/parsers/gemini.d.ts +3 -2
  43. package/dist/parsers/gemini.js +198 -21
  44. package/dist/parsers/gemini.js.map +1 -1
  45. package/dist/parsers/index.d.ts +1 -1
  46. package/dist/parsers/index.js +23 -7
  47. package/dist/parsers/index.js.map +1 -1
  48. package/dist/parsers/types.d.ts +27 -1
  49. package/dist/render/build-render-data.d.ts +59 -0
  50. package/dist/render/build-render-data.js +101 -0
  51. package/dist/render/build-render-data.js.map +1 -0
  52. package/dist/render/components/PortfolioPage.d.ts +4 -0
  53. package/dist/render/components/PortfolioPage.js +16 -0
  54. package/dist/render/components/PortfolioPage.js.map +1 -0
  55. package/dist/render/components/ProjectPage.d.ts +4 -0
  56. package/dist/render/components/ProjectPage.js +101 -0
  57. package/dist/render/components/ProjectPage.js.map +1 -0
  58. package/dist/render/components/SessionPage.d.ts +4 -0
  59. package/dist/render/components/SessionPage.js +29 -0
  60. package/dist/render/components/SessionPage.js.map +1 -0
  61. package/dist/render/index.d.ts +37 -0
  62. package/dist/render/index.js +140 -0
  63. package/dist/render/index.js.map +1 -0
  64. package/dist/render/types.d.ts +121 -0
  65. package/dist/render/types.js +9 -0
  66. package/dist/render/types.js.map +1 -0
  67. package/dist/routes/archive.d.ts +3 -0
  68. package/dist/routes/archive.js +56 -0
  69. package/dist/routes/archive.js.map +1 -0
  70. package/dist/routes/auth.d.ts +3 -0
  71. package/dist/routes/auth.js +116 -0
  72. package/dist/routes/auth.js.map +1 -0
  73. package/dist/routes/context.d.ts +61 -0
  74. package/dist/routes/context.js +356 -0
  75. package/dist/routes/context.js.map +1 -0
  76. package/dist/routes/dashboard.d.ts +3 -0
  77. package/dist/routes/dashboard.js +103 -0
  78. package/dist/routes/dashboard.js.map +1 -0
  79. package/dist/routes/enhance.d.ts +3 -0
  80. package/dist/routes/enhance.js +305 -0
  81. package/dist/routes/enhance.js.map +1 -0
  82. package/dist/routes/export.d.ts +3 -0
  83. package/dist/routes/export.js +145 -0
  84. package/dist/routes/export.js.map +1 -0
  85. package/dist/routes/index.d.ts +12 -0
  86. package/dist/routes/index.js +13 -0
  87. package/dist/routes/index.js.map +1 -0
  88. package/dist/routes/preview.d.ts +3 -0
  89. package/dist/routes/preview.js +191 -0
  90. package/dist/routes/preview.js.map +1 -0
  91. package/dist/routes/projects.d.ts +3 -0
  92. package/dist/routes/projects.js +356 -0
  93. package/dist/routes/projects.js.map +1 -0
  94. package/dist/routes/publish.d.ts +3 -0
  95. package/dist/routes/publish.js +466 -0
  96. package/dist/routes/publish.js.map +1 -0
  97. package/dist/routes/search.d.ts +3 -0
  98. package/dist/routes/search.js +110 -0
  99. package/dist/routes/search.js.map +1 -0
  100. package/dist/routes/sessions.d.ts +3 -0
  101. package/dist/routes/sessions.js +103 -0
  102. package/dist/routes/sessions.js.map +1 -0
  103. package/dist/routes/settings.d.ts +3 -0
  104. package/dist/routes/settings.js +30 -0
  105. package/dist/routes/settings.js.map +1 -0
  106. package/dist/screenshot.d.ts +5 -2
  107. package/dist/screenshot.js +187 -13
  108. package/dist/screenshot.js.map +1 -1
  109. package/dist/search.d.ts +30 -0
  110. package/dist/search.js +153 -0
  111. package/dist/search.js.map +1 -0
  112. package/dist/server.d.ts +1 -1
  113. package/dist/server.js +55 -1318
  114. package/dist/server.js.map +1 -1
  115. package/dist/settings.d.ts +23 -6
  116. package/dist/settings.js +36 -12
  117. package/dist/settings.js.map +1 -1
  118. package/dist/source-audit.d.ts +29 -0
  119. package/dist/source-audit.js +203 -0
  120. package/dist/source-audit.js.map +1 -0
  121. package/dist/sync.d.ts +74 -0
  122. package/dist/sync.js +358 -0
  123. package/dist/sync.js.map +1 -0
  124. package/dist/transcript.d.ts +68 -0
  125. package/dist/transcript.js +268 -0
  126. package/dist/transcript.js.map +1 -0
  127. package/package.json +5 -1
  128. package/app/dist/assets/html2canvas-Cwn_rrOw.js +0 -5
  129. package/app/dist/assets/index-CEQyTkgN.js +0 -14
  130. package/app/dist/assets/index-DLh5xRE8.css +0 -1
  131. package/app/dist/favicon.svg +0 -5
  132. package/app/dist/icons.svg +0 -24
  133. package/app/dist/index.html +0 -20
  134. package/dist/machine-key.d.ts +0 -10
  135. package/dist/machine-key.js +0 -51
  136. 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.2');
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(() => { }); // fire-and-forget, don't crash if browser fails
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
- // Group by project directory
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; // primary agent worked the whole session
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', 'open-time', 'login', 'logout', 'publish', 'time'];
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
  }