sidecar-cli 0.1.0-beta.4

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/cli.js ADDED
@@ -0,0 +1,727 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { Command } from 'commander';
5
+ import Database from 'better-sqlite3';
6
+ import { z } from 'zod';
7
+ import { initializeSchema } from './db/schema.js';
8
+ import { getSidecarPaths } from './lib/paths.js';
9
+ import { nowIso, humanTime, stringifyJson } from './lib/format.js';
10
+ import { SidecarError } from './lib/errors.js';
11
+ import { jsonFailure, jsonSuccess, printJsonEnvelope } from './lib/output.js';
12
+ import { bannerDisabled, renderBanner } from './lib/banner.js';
13
+ import { requireInitialized } from './db/client.js';
14
+ import { renderAgentsMarkdown } from './templates/agents.js';
15
+ import { refreshSummaryFile } from './services/summary-service.js';
16
+ import { buildContext } from './services/context-service.js';
17
+ import { getCapabilitiesManifest } from './services/capabilities-service.js';
18
+ import { addArtifact, listArtifacts } from './services/artifact-service.js';
19
+ import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
20
+ import { addTask, listTasks, markTaskDone } from './services/task-service.js';
21
+ import { currentSession, endSession, startSession, verifySessionHygiene } from './services/session-service.js';
22
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
23
+ const actorSchema = z.enum(['human', 'agent']);
24
+ const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
25
+ const artifactKindSchema = z.enum(['file', 'doc', 'screenshot', 'other']);
26
+ const taskStatusSchema = z.enum(['open', 'done', 'all']);
27
+ const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
28
+ function fail(message) {
29
+ throw new SidecarError(message);
30
+ }
31
+ function maybeSessionId(db, projectId, explicit) {
32
+ if (explicit) {
33
+ const parsed = Number.parseInt(explicit, 10);
34
+ if (!Number.isInteger(parsed) || parsed <= 0) {
35
+ throw new SidecarError('Session id must be a positive integer');
36
+ }
37
+ return parsed;
38
+ }
39
+ return getActiveSessionId(db, projectId);
40
+ }
41
+ function handleCommandError(command, asJson, err) {
42
+ const message = err instanceof Error ? err.message : String(err);
43
+ if (asJson) {
44
+ printJsonEnvelope(jsonFailure(command, message));
45
+ }
46
+ else {
47
+ console.error(message);
48
+ }
49
+ process.exit(err instanceof SidecarError ? err.exitCode : 1);
50
+ }
51
+ function respondSuccess(command, asJson, data, lines = []) {
52
+ if (asJson) {
53
+ printJsonEnvelope(jsonSuccess(command, data));
54
+ return;
55
+ }
56
+ for (const line of lines) {
57
+ console.log(line);
58
+ }
59
+ }
60
+ function renderContextText(data) {
61
+ const lines = [];
62
+ lines.push(`Project: ${data.projectName}`);
63
+ lines.push(`Path: ${data.projectPath}`);
64
+ lines.push(`Generated: ${data.generatedAt}`);
65
+ lines.push(`Active session: ${data.activeSession
66
+ ? `#${data.activeSession.id} (${data.activeSession.actor_type}${data.activeSession.actor_name ? `: ${data.activeSession.actor_name}` : ''})`
67
+ : 'none'}`);
68
+ lines.push('');
69
+ lines.push('Recent decisions');
70
+ if (data.recentDecisions.length === 0)
71
+ lines.push('- none');
72
+ for (const item of data.recentDecisions) {
73
+ lines.push(`- ${humanTime(item.created_at)} | ${item.title}: ${item.summary}`);
74
+ }
75
+ lines.push('');
76
+ lines.push('Recent worklogs');
77
+ if (data.recentWorklogs.length === 0)
78
+ lines.push('- none');
79
+ for (const item of data.recentWorklogs) {
80
+ lines.push(`- ${humanTime(item.created_at)} | ${item.title}: ${item.summary}`);
81
+ }
82
+ lines.push('');
83
+ lines.push('Open tasks');
84
+ if (data.openTasks.length === 0)
85
+ lines.push('- none');
86
+ for (const task of data.openTasks) {
87
+ lines.push(`- #${task.id} [${task.priority ?? 'n/a'}] ${task.title}`);
88
+ }
89
+ lines.push('');
90
+ lines.push('Recent notes');
91
+ if (data.notableNotes.length === 0)
92
+ lines.push('- none');
93
+ for (const item of data.notableNotes) {
94
+ lines.push(`- ${humanTime(item.created_at)} | ${item.title}: ${item.summary}`);
95
+ }
96
+ return lines.join('\n');
97
+ }
98
+ function renderContextMarkdown(data) {
99
+ const lines = [];
100
+ lines.push('# Sidecar Context');
101
+ lines.push(`Project: ${data.projectName}`);
102
+ lines.push(`Path: ${data.projectPath}`);
103
+ lines.push(`Generated: ${data.generatedAt}`);
104
+ lines.push('');
105
+ lines.push('## Active Session');
106
+ if (!data.activeSession) {
107
+ lines.push('- None');
108
+ }
109
+ else {
110
+ lines.push(`- #${data.activeSession.id} (${data.activeSession.actor_type}${data.activeSession.actor_name ? `: ${data.activeSession.actor_name}` : ''}) started ${data.activeSession.started_at}`);
111
+ }
112
+ lines.push('');
113
+ lines.push('## Recent Decisions');
114
+ if (data.recentDecisions.length === 0)
115
+ lines.push('- None');
116
+ for (const item of data.recentDecisions) {
117
+ lines.push(`- ${item.created_at} | **${item.title}**: ${item.summary}`);
118
+ }
119
+ lines.push('');
120
+ lines.push('## Recent Worklogs');
121
+ if (data.recentWorklogs.length === 0)
122
+ lines.push('- None');
123
+ for (const item of data.recentWorklogs) {
124
+ lines.push(`- ${item.created_at} | **${item.title}**: ${item.summary}`);
125
+ }
126
+ lines.push('');
127
+ lines.push('## Open Tasks');
128
+ if (data.openTasks.length === 0)
129
+ lines.push('- None');
130
+ for (const task of data.openTasks) {
131
+ lines.push(`- [ ] #${task.id} (${task.priority ?? 'n/a'}) ${task.title}`);
132
+ }
133
+ lines.push('');
134
+ lines.push('## Recent Notes');
135
+ if (data.notableNotes.length === 0)
136
+ lines.push('- None');
137
+ for (const item of data.notableNotes) {
138
+ lines.push(`- ${item.created_at} | **${item.title}**: ${item.summary}`);
139
+ }
140
+ lines.push('');
141
+ lines.push('## Recent Artifacts');
142
+ if (data.recentArtifacts.length === 0)
143
+ lines.push('- None');
144
+ for (const art of data.recentArtifacts) {
145
+ lines.push(`- ${art.kind}: ${art.path}${art.note ? ` - ${art.note}` : ''}`);
146
+ }
147
+ return lines.join('\n');
148
+ }
149
+ const program = new Command();
150
+ program.name('sidecar').description('Local-first project memory and recording CLI').version(pkg.version);
151
+ program.option('--no-banner', 'Disable Sidecar banner output');
152
+ program
153
+ .command('init')
154
+ .description('Initialize Sidecar in the current directory')
155
+ .option('--force', 'Overwrite Sidecar files if they already exist')
156
+ .option('--name <project-name>', 'Project name (defaults to current directory name)')
157
+ .option('--json', 'Print machine-readable JSON output')
158
+ .addHelpText('after', '\nExamples:\n $ sidecar init\n $ sidecar init --name "My Project"\n $ sidecar init --force --json')
159
+ .action((opts) => {
160
+ const command = 'init';
161
+ try {
162
+ const rootPath = process.cwd();
163
+ const sidecar = getSidecarPaths(rootPath);
164
+ const projectName = opts.name?.trim() || path.basename(rootPath);
165
+ if (fs.existsSync(sidecar.sidecarPath) && !opts.force) {
166
+ fail('Sidecar is already initialized in this project. Re-run with --force to recreate .sidecar files.');
167
+ }
168
+ const files = [
169
+ sidecar.dbPath,
170
+ sidecar.configPath,
171
+ sidecar.preferencesPath,
172
+ sidecar.agentsPath,
173
+ sidecar.summaryPath,
174
+ ];
175
+ fs.mkdirSync(sidecar.sidecarPath, { recursive: true });
176
+ if (opts.force) {
177
+ for (const file of [
178
+ sidecar.dbPath,
179
+ `${sidecar.dbPath}-wal`,
180
+ `${sidecar.dbPath}-shm`,
181
+ sidecar.configPath,
182
+ sidecar.preferencesPath,
183
+ sidecar.agentsPath,
184
+ sidecar.summaryPath,
185
+ ]) {
186
+ if (fs.existsSync(file))
187
+ fs.rmSync(file);
188
+ }
189
+ }
190
+ const db = new Database(sidecar.dbPath);
191
+ initializeSchema(db);
192
+ const ts = nowIso();
193
+ db.prepare(`DELETE FROM projects`).run();
194
+ db.prepare(`INSERT INTO projects (name, root_path, created_at, updated_at) VALUES (?, ?, ?, ?)`).run(projectName, rootPath, ts, ts);
195
+ db.close();
196
+ const config = {
197
+ schemaVersion: 1,
198
+ project: { name: projectName, rootPath, createdAt: ts },
199
+ defaults: { summary: { recentLimit: 10 } },
200
+ settings: {},
201
+ };
202
+ fs.writeFileSync(sidecar.configPath, stringifyJson(config));
203
+ fs.writeFileSync(sidecar.preferencesPath, stringifyJson({
204
+ summary: { format: 'markdown', recentLimit: 8 },
205
+ output: { humanTime: true },
206
+ }));
207
+ fs.writeFileSync(sidecar.agentsPath, renderAgentsMarkdown(projectName));
208
+ const db2 = new Database(sidecar.dbPath);
209
+ const refreshed = refreshSummaryFile(db2, rootPath, 1, 10);
210
+ db2.close();
211
+ const data = {
212
+ rootPath,
213
+ sidecarPath: sidecar.sidecarPath,
214
+ projectName,
215
+ filesCreated: files,
216
+ summaryGeneratedAt: refreshed.generatedAt,
217
+ timestamp: nowIso(),
218
+ };
219
+ const shouldShowBanner = !opts.json && !bannerDisabled();
220
+ if (shouldShowBanner) {
221
+ console.log(renderBanner());
222
+ console.log('');
223
+ }
224
+ respondSuccess(command, Boolean(opts.json), data, [
225
+ `Initialized Sidecar for project: ${projectName}`,
226
+ 'Sidecar provides local project memory for decisions, work logs, tasks, and summaries.',
227
+ 'Created:',
228
+ ...data.filesCreated.map((f) => `- ${f}`),
229
+ '',
230
+ 'Next step:',
231
+ 'sidecar context',
232
+ ]);
233
+ }
234
+ catch (err) {
235
+ handleCommandError(command, Boolean(opts.json), err);
236
+ }
237
+ });
238
+ program
239
+ .command('status')
240
+ .description('Show Sidecar status and recent project activity')
241
+ .option('--json', 'Print machine-readable JSON output')
242
+ .addHelpText('after', '\nExamples:\n $ sidecar status\n $ sidecar status --json')
243
+ .action((opts) => {
244
+ const command = 'status';
245
+ try {
246
+ const { db, projectId } = requireInitialized();
247
+ const project = db.prepare(`SELECT name, root_path, created_at FROM projects WHERE id = ?`).get(projectId);
248
+ const counts = {
249
+ events: db.prepare(`SELECT COUNT(*) as count FROM events WHERE project_id = ?`).get(projectId).count,
250
+ tasks: db.prepare(`SELECT COUNT(*) as count FROM tasks WHERE project_id = ?`).get(projectId).count,
251
+ sessions: db.prepare(`SELECT COUNT(*) as count FROM sessions WHERE project_id = ?`).get(projectId).count,
252
+ };
253
+ const recent = db.prepare(`SELECT type, title, created_at FROM events WHERE project_id = ? ORDER BY created_at DESC LIMIT 5`).all(projectId);
254
+ db.close();
255
+ const data = { initialized: true, project, counts, recent };
256
+ respondSuccess(command, Boolean(opts.json), data, [
257
+ `Project: ${project.name}`,
258
+ `Root: ${project.root_path}`,
259
+ `Counts: events=${counts.events}, tasks=${counts.tasks}, sessions=${counts.sessions}`,
260
+ 'Recent activity:',
261
+ ...recent.map((r) => `- ${humanTime(r.created_at)} | ${r.type} | ${r.title}`),
262
+ ]);
263
+ }
264
+ catch (err) {
265
+ const normalized = err instanceof SidecarError && err.code === 'NOT_INITIALIZED'
266
+ ? new SidecarError(NOT_INITIALIZED_MSG, err.code, err.exitCode)
267
+ : err;
268
+ handleCommandError(command, Boolean(opts.json), normalized);
269
+ }
270
+ });
271
+ program
272
+ .command('capabilities')
273
+ .description('Output a machine-readable manifest of Sidecar commands')
274
+ .option('--json', 'Print machine-readable JSON output')
275
+ .addHelpText('after', '\nExamples:\n $ sidecar capabilities --json')
276
+ .action((opts) => {
277
+ const command = 'capabilities';
278
+ const manifest = getCapabilitiesManifest(pkg.version);
279
+ if (opts.json)
280
+ printJsonEnvelope(jsonSuccess(command, manifest));
281
+ else
282
+ console.log(stringifyJson(manifest));
283
+ });
284
+ program
285
+ .command('context')
286
+ .description('Generate a compact context snapshot for a work session')
287
+ .option('--limit <n>', 'Item limit per section', (v) => Number.parseInt(v, 10), 8)
288
+ .option('--format <format>', 'text|markdown|json', 'text')
289
+ .option('--json', 'Wrap output in standard JSON envelope')
290
+ .addHelpText('after', '\nExamples:\n $ sidecar context\n $ sidecar context --format markdown\n $ sidecar context --format json --json')
291
+ .action((opts) => {
292
+ const command = 'context';
293
+ try {
294
+ const { db, projectId } = requireInitialized();
295
+ const limit = Math.max(1, opts.limit);
296
+ const data = buildContext(db, { projectId, limit });
297
+ db.close();
298
+ if (opts.format === 'json') {
299
+ if (opts.json)
300
+ printJsonEnvelope(jsonSuccess(command, data));
301
+ else
302
+ console.log(stringifyJson(data));
303
+ return;
304
+ }
305
+ const rendered = opts.format === 'markdown' ? renderContextMarkdown(data) : renderContextText(data);
306
+ if (opts.json)
307
+ printJsonEnvelope(jsonSuccess(command, { format: opts.format, content: rendered }));
308
+ else
309
+ console.log(rendered);
310
+ }
311
+ catch (err) {
312
+ handleCommandError(command, Boolean(opts.json), err);
313
+ }
314
+ });
315
+ const summary = program.command('summary').description('Summary operations');
316
+ summary
317
+ .command('refresh')
318
+ .description('Regenerate .sidecar/summary.md from local records')
319
+ .option('--limit <n>', 'Item limit per section', (v) => Number.parseInt(v, 10), 8)
320
+ .option('--json', 'Print machine-readable JSON output')
321
+ .addHelpText('after', '\nExamples:\n $ sidecar summary refresh\n $ sidecar summary refresh --limit 5 --json')
322
+ .action((opts) => {
323
+ const command = 'summary refresh';
324
+ try {
325
+ const { db, projectId, rootPath } = requireInitialized();
326
+ const out = refreshSummaryFile(db, rootPath, projectId, Math.max(1, opts.limit));
327
+ db.close();
328
+ respondSuccess(command, Boolean(opts.json), { ...out, timestamp: nowIso() }, ['Summary refreshed.', `Path: ${out.path}`]);
329
+ }
330
+ catch (err) {
331
+ handleCommandError(command, Boolean(opts.json), err);
332
+ }
333
+ });
334
+ program
335
+ .command('recent')
336
+ .description('Show recent events in timeline order')
337
+ .option('--type <event-type>', 'Filter by event type')
338
+ .option('--limit <n>', 'Number of rows', (v) => Number.parseInt(v, 10), 20)
339
+ .option('--json', 'Print machine-readable JSON output')
340
+ .addHelpText('after', '\nExamples:\n $ sidecar recent\n $ sidecar recent --type decision --limit 10')
341
+ .action((opts) => {
342
+ const command = 'recent';
343
+ try {
344
+ const { db, projectId } = requireInitialized();
345
+ const rows = listRecentEvents(db, { projectId, type: opts.type, limit: Math.max(1, opts.limit) });
346
+ db.close();
347
+ if (opts.json)
348
+ printJsonEnvelope(jsonSuccess(command, rows));
349
+ else {
350
+ if (rows.length === 0) {
351
+ console.log('No events found.');
352
+ return;
353
+ }
354
+ for (const row of rows) {
355
+ console.log(`#${row.id} ${humanTime(row.created_at)} | ${row.type} | ${row.title}`);
356
+ console.log(` ${row.summary}`);
357
+ }
358
+ }
359
+ }
360
+ catch (err) {
361
+ handleCommandError(command, Boolean(opts.json), err);
362
+ }
363
+ });
364
+ program
365
+ .command('note <text>')
366
+ .description('Record a freeform note event')
367
+ .option('--title <title>', 'Optional title')
368
+ .option('--by <actor>', 'human|agent', 'human')
369
+ .option('--session <session-id>', 'Session id override')
370
+ .option('--json', 'Print machine-readable JSON output')
371
+ .addHelpText('after', '\nExamples:\n $ sidecar note "Need to revisit parser edge cases"\n $ sidecar note "UI flaky" --title "Test note" --by agent')
372
+ .action((text, opts) => {
373
+ const command = 'note';
374
+ try {
375
+ const by = actorSchema.parse(opts.by);
376
+ const { db, projectId } = requireInitialized();
377
+ const sessionId = maybeSessionId(db, projectId, opts.session);
378
+ const eventId = addNote(db, { projectId, text, title: opts.title, by, sessionId });
379
+ db.close();
380
+ respondSuccess(command, Boolean(opts.json), { eventId, timestamp: nowIso() }, [`Recorded note event #${eventId}.`]);
381
+ }
382
+ catch (err) {
383
+ handleCommandError(command, Boolean(opts.json), err);
384
+ }
385
+ });
386
+ const decision = program.command('decision').description('Decision commands');
387
+ decision
388
+ .command('record')
389
+ .description('Record a project decision')
390
+ .requiredOption('--title <title>', 'Decision title')
391
+ .requiredOption('--summary <summary>', 'Decision summary')
392
+ .option('--details <details>', 'Optional details')
393
+ .option('--by <actor>', 'human|agent', 'human')
394
+ .option('--session <session-id>', 'Session id override')
395
+ .option('--json', 'Print machine-readable JSON output')
396
+ .addHelpText('after', '\nExamples:\n $ sidecar decision record --title "Use SQLite" --summary "Local-first storage"\n $ sidecar decision record --title "Auth strategy" --summary "No auth in v1" --by agent')
397
+ .action((opts) => {
398
+ const command = 'decision record';
399
+ try {
400
+ const by = actorSchema.parse(opts.by);
401
+ const { db, projectId } = requireInitialized();
402
+ const sessionId = maybeSessionId(db, projectId, opts.session);
403
+ const eventId = addDecision(db, { projectId, title: opts.title, summary: opts.summary, details: opts.details, by, sessionId });
404
+ db.close();
405
+ respondSuccess(command, Boolean(opts.json), { eventId, timestamp: nowIso() }, [`Recorded decision event #${eventId}.`]);
406
+ }
407
+ catch (err) {
408
+ handleCommandError(command, Boolean(opts.json), err);
409
+ }
410
+ });
411
+ const worklog = program.command('worklog').description('Worklog commands');
412
+ worklog
413
+ .command('record')
414
+ .description('Record completed work and related metadata')
415
+ .option('--goal <goal>', 'Goal worked on')
416
+ .requiredOption('--done <done-summary>', 'What was completed')
417
+ .option('--files <comma-separated-paths>', 'Changed files')
418
+ .option('--risks <text>', 'Risks')
419
+ .option('--next <text>', 'Next step')
420
+ .option('--by <actor>', 'human|agent', 'human')
421
+ .option('--session <session-id>', 'Session id override')
422
+ .option('--json', 'Print machine-readable JSON output')
423
+ .addHelpText('after', '\nExamples:\n $ sidecar worklog record --done "Implemented context output"\n $ sidecar worklog record --goal "Refactor" --done "Moved formatter logic" --files src/cli.ts,src/lib/format.ts --by agent')
424
+ .action((opts) => {
425
+ const command = 'worklog record';
426
+ try {
427
+ const by = actorSchema.parse(opts.by);
428
+ const { db, projectId } = requireInitialized();
429
+ const sessionId = maybeSessionId(db, projectId, opts.session);
430
+ const result = addWorklog(db, {
431
+ projectId,
432
+ goal: opts.goal,
433
+ done: opts.done,
434
+ files: opts.files,
435
+ risks: opts.risks,
436
+ next: opts.next,
437
+ by,
438
+ sessionId,
439
+ });
440
+ for (const filePath of result.files) {
441
+ addArtifact(db, { projectId, path: filePath, kind: 'file' });
442
+ }
443
+ db.close();
444
+ respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [
445
+ `Recorded worklog event #${result.eventId}.`,
446
+ `Artifacts linked: ${result.files.length}`,
447
+ ]);
448
+ }
449
+ catch (err) {
450
+ handleCommandError(command, Boolean(opts.json), err);
451
+ }
452
+ });
453
+ const task = program.command('task').description('Task commands');
454
+ task
455
+ .command('add <title>')
456
+ .description('Create an open task')
457
+ .option('--description <text>', 'Description')
458
+ .option('--priority <priority>', 'low|medium|high', 'medium')
459
+ .option('--by <actor>', 'human|agent', 'human')
460
+ .option('--json', 'Print machine-readable JSON output')
461
+ .addHelpText('after', '\nExamples:\n $ sidecar task add "Ship v0.1"\n $ sidecar task add "Add tests" --priority high --by agent')
462
+ .action((title, opts) => {
463
+ const command = 'task add';
464
+ try {
465
+ const priority = taskPrioritySchema.parse(opts.priority);
466
+ const by = actorSchema.parse(opts.by);
467
+ const { db, projectId } = requireInitialized();
468
+ const result = addTask(db, { projectId, title, description: opts.description, priority, by });
469
+ db.close();
470
+ respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [`Added task #${result.taskId}.`]);
471
+ }
472
+ catch (err) {
473
+ handleCommandError(command, Boolean(opts.json), err);
474
+ }
475
+ });
476
+ task
477
+ .command('done <task-id>')
478
+ .description('Mark a task as done')
479
+ .option('--by <actor>', 'human|agent', 'human')
480
+ .option('--json', 'Print machine-readable JSON output')
481
+ .addHelpText('after', '\nExamples:\n $ sidecar task done 3\n $ sidecar task done 3 --json')
482
+ .action((taskIdText, opts) => {
483
+ const command = 'task done';
484
+ try {
485
+ const taskId = Number.parseInt(taskIdText, 10);
486
+ if (!Number.isInteger(taskId) || taskId <= 0)
487
+ fail('Task id must be a positive integer');
488
+ const by = actorSchema.parse(opts.by);
489
+ const { db, projectId } = requireInitialized();
490
+ const result = markTaskDone(db, { projectId, taskId, by });
491
+ db.close();
492
+ if (!result.ok)
493
+ fail(result.reason);
494
+ respondSuccess(command, Boolean(opts.json), { taskId, eventId: result.eventId, timestamp: nowIso() }, [`Completed task #${taskId}.`]);
495
+ }
496
+ catch (err) {
497
+ handleCommandError(command, Boolean(opts.json), err);
498
+ }
499
+ });
500
+ task
501
+ .command('list')
502
+ .description('List tasks by status')
503
+ .option('--status <status>', 'open|done|all', 'open')
504
+ .option('--format <format>', 'table|json', 'table')
505
+ .option('--json', 'Print machine-readable JSON output')
506
+ .addHelpText('after', '\nExamples:\n $ sidecar task list\n $ sidecar task list --status all --format json')
507
+ .action((opts) => {
508
+ const command = 'task list';
509
+ try {
510
+ const status = taskStatusSchema.parse(opts.status);
511
+ const { db, projectId } = requireInitialized();
512
+ const rows = listTasks(db, { projectId, status });
513
+ db.close();
514
+ if (opts.format === 'json' || opts.json) {
515
+ if (opts.json)
516
+ printJsonEnvelope(jsonSuccess(command, rows));
517
+ else
518
+ console.log(stringifyJson(rows));
519
+ return;
520
+ }
521
+ const taskRows = rows;
522
+ if (taskRows.length === 0) {
523
+ console.log('No tasks found.');
524
+ return;
525
+ }
526
+ const idWidth = Math.max(2, ...taskRows.map((r) => String(r.id).length));
527
+ const statusWidth = Math.max(6, ...taskRows.map((r) => r.status.length));
528
+ const priorityWidth = Math.max(8, ...taskRows.map((r) => (r.priority ?? 'n/a').length));
529
+ console.log(`${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'PRIORITY'.padEnd(priorityWidth)} TITLE`);
530
+ for (const row of taskRows) {
531
+ const id = String(row.id).padEnd(idWidth);
532
+ const statusLabel = row.status.padEnd(statusWidth);
533
+ const prio = (row.priority ?? 'n/a').padEnd(priorityWidth);
534
+ console.log(`${id} ${statusLabel} ${prio} ${row.title}`);
535
+ }
536
+ }
537
+ catch (err) {
538
+ handleCommandError(command, Boolean(opts.json), err);
539
+ }
540
+ });
541
+ const session = program.command('session').description('Session commands');
542
+ session
543
+ .command('start')
544
+ .description('Start a new work session')
545
+ .option('--actor <actor>', 'human|agent', 'human')
546
+ .option('--name <actor-name>', 'Actor name')
547
+ .option('--json', 'Print machine-readable JSON output')
548
+ .addHelpText('after', '\nExamples:\n $ sidecar session start --actor agent --name codex')
549
+ .action((opts) => {
550
+ const command = 'session start';
551
+ try {
552
+ const actor = actorSchema.parse(opts.actor);
553
+ const { db, projectId } = requireInitialized();
554
+ const result = startSession(db, { projectId, actor, name: opts.name });
555
+ db.close();
556
+ if (!result.ok)
557
+ fail(result.reason);
558
+ respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [`Started session #${result.sessionId}.`]);
559
+ }
560
+ catch (err) {
561
+ handleCommandError(command, Boolean(opts.json), err);
562
+ }
563
+ });
564
+ session
565
+ .command('end')
566
+ .description('End the current active session')
567
+ .option('--summary <text>', 'Session summary')
568
+ .option('--json', 'Print machine-readable JSON output')
569
+ .addHelpText('after', '\nExamples:\n $ sidecar session end --summary "Completed migration"')
570
+ .action((opts) => {
571
+ const command = 'session end';
572
+ try {
573
+ const { db, projectId } = requireInitialized();
574
+ const result = endSession(db, { projectId, summary: opts.summary });
575
+ db.close();
576
+ if (!result.ok)
577
+ fail(result.reason);
578
+ respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [`Ended session #${result.sessionId}.`]);
579
+ }
580
+ catch (err) {
581
+ handleCommandError(command, Boolean(opts.json), err);
582
+ }
583
+ });
584
+ session
585
+ .command('current')
586
+ .description('Show the current active session')
587
+ .option('--json', 'Print machine-readable JSON output')
588
+ .addHelpText('after', '\nExamples:\n $ sidecar session current\n $ sidecar session current --json')
589
+ .action((opts) => {
590
+ const command = 'session current';
591
+ try {
592
+ const { db, projectId } = requireInitialized();
593
+ const current = currentSession(db, projectId);
594
+ db.close();
595
+ if (opts.json)
596
+ printJsonEnvelope(jsonSuccess(command, { current: current ?? null }));
597
+ else if (!current) {
598
+ console.log('No active session.');
599
+ }
600
+ else {
601
+ const session = current;
602
+ console.log(`Session #${session.id}`);
603
+ console.log(`Actor: ${session.actor_type}${session.actor_name ? ` (${session.actor_name})` : ''}`);
604
+ console.log(`Started: ${humanTime(session.started_at)}`);
605
+ }
606
+ }
607
+ catch (err) {
608
+ handleCommandError(command, Boolean(opts.json), err);
609
+ }
610
+ });
611
+ session
612
+ .command('verify')
613
+ .description('Run lightweight hygiene checks for the current project')
614
+ .option('--json', 'Print machine-readable JSON output')
615
+ .addHelpText('after', '\nExamples:\n $ sidecar session verify\n $ sidecar session verify --json')
616
+ .action((opts) => {
617
+ const command = 'session verify';
618
+ try {
619
+ const { db, projectId } = requireInitialized();
620
+ const summaryRecent = Boolean(db
621
+ .prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'summary_generated' AND created_at >= datetime('now', '-3 day') LIMIT 1`)
622
+ .get(projectId));
623
+ const warnings = verifySessionHygiene(db, projectId, summaryRecent);
624
+ db.close();
625
+ if (opts.json)
626
+ printJsonEnvelope(jsonSuccess(command, { warnings, healthy: warnings.length === 0 }));
627
+ else {
628
+ if (warnings.length === 0) {
629
+ console.log('Session hygiene looks good.');
630
+ return;
631
+ }
632
+ console.log('Session hygiene warnings:');
633
+ for (const w of warnings)
634
+ console.log(`- ${w}`);
635
+ }
636
+ }
637
+ catch (err) {
638
+ handleCommandError(command, Boolean(opts.json), err);
639
+ }
640
+ });
641
+ program
642
+ .command('doctor')
643
+ .description('Alias for `sidecar session verify`')
644
+ .option('--json', 'Print machine-readable JSON output')
645
+ .addHelpText('after', '\nExamples:\n $ sidecar doctor')
646
+ .action((opts) => {
647
+ const command = 'doctor';
648
+ try {
649
+ const { db, projectId } = requireInitialized();
650
+ const summaryRecent = Boolean(db
651
+ .prepare(`SELECT id FROM events WHERE project_id = ? AND type = 'summary_generated' AND created_at >= datetime('now', '-3 day') LIMIT 1`)
652
+ .get(projectId));
653
+ const warnings = verifySessionHygiene(db, projectId, summaryRecent);
654
+ db.close();
655
+ if (opts.json)
656
+ printJsonEnvelope(jsonSuccess(command, { warnings, healthy: warnings.length === 0 }));
657
+ else {
658
+ if (warnings.length === 0) {
659
+ console.log('Session hygiene looks good.');
660
+ return;
661
+ }
662
+ console.log('Session hygiene warnings:');
663
+ for (const w of warnings)
664
+ console.log(`- ${w}`);
665
+ }
666
+ }
667
+ catch (err) {
668
+ handleCommandError(command, Boolean(opts.json), err);
669
+ }
670
+ });
671
+ const artifact = program.command('artifact').description('Artifact commands');
672
+ artifact
673
+ .command('add <path>')
674
+ .description('Attach an artifact reference')
675
+ .option('--kind <kind>', 'file|doc|screenshot|other', 'file')
676
+ .option('--note <text>', 'Optional note')
677
+ .option('--json', 'Print machine-readable JSON output')
678
+ .addHelpText('after', '\nExamples:\n $ sidecar artifact add README.md --kind doc --note "Product spec"')
679
+ .action((artifactPath, opts) => {
680
+ const command = 'artifact add';
681
+ try {
682
+ const kind = artifactKindSchema.parse(opts.kind);
683
+ const { db, projectId } = requireInitialized();
684
+ const artifactId = addArtifact(db, { projectId, path: artifactPath, kind, note: opts.note });
685
+ db.close();
686
+ respondSuccess(command, Boolean(opts.json), { artifactId, timestamp: nowIso() }, [`Added artifact #${artifactId}.`]);
687
+ }
688
+ catch (err) {
689
+ handleCommandError(command, Boolean(opts.json), err);
690
+ }
691
+ });
692
+ artifact
693
+ .command('list')
694
+ .description('List recent artifact references')
695
+ .option('--json', 'Print machine-readable JSON output')
696
+ .addHelpText('after', '\nExamples:\n $ sidecar artifact list\n $ sidecar artifact list --json')
697
+ .action((opts) => {
698
+ const command = 'artifact list';
699
+ try {
700
+ const { db, projectId } = requireInitialized();
701
+ const rows = listArtifacts(db, projectId);
702
+ db.close();
703
+ if (opts.json)
704
+ printJsonEnvelope(jsonSuccess(command, rows));
705
+ else {
706
+ if (rows.length === 0) {
707
+ console.log('No artifacts found.');
708
+ return;
709
+ }
710
+ for (const row of rows) {
711
+ console.log(`#${row.id} ${row.kind} ${row.path}${row.note ? ` - ${row.note}` : ''}`);
712
+ }
713
+ }
714
+ }
715
+ catch (err) {
716
+ handleCommandError(command, Boolean(opts.json), err);
717
+ }
718
+ });
719
+ if (process.argv.length === 2) {
720
+ if (!bannerDisabled()) {
721
+ console.log(renderBanner());
722
+ console.log('');
723
+ }
724
+ program.outputHelp();
725
+ process.exit(0);
726
+ }
727
+ program.parse(process.argv);