sidecar-cli 0.1.1-beta.2 → 0.1.1-rc.1

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/README.md CHANGED
@@ -21,6 +21,12 @@ Install globally (stable):
21
21
  npm install -g sidecar-cli
22
22
  ```
23
23
 
24
+ Note on npm deprecation warnings:
25
+
26
+ - You may see a transitive warning for `prebuild-install@7.1.3` during install.
27
+ - This currently comes from `better-sqlite3` (Sidecar's SQLite dependency), not from Sidecar code directly.
28
+ - Sidecar still installs/works normally; we will update when upstream dependency chain moves off it.
29
+
24
30
  Install beta:
25
31
 
26
32
  ```bash
@@ -103,8 +109,11 @@ Global:
103
109
 
104
110
  - `sidecar init [--force] [--name <project-name>] [--json]`
105
111
  - `sidecar status [--json]`
112
+ - `sidecar preferences show [--json]`
106
113
  - `sidecar ui [--no-open] [--port <port>] [--install-only] [--project <path>] [--reinstall]`
107
114
  - `sidecar capabilities --json`
115
+ - `sidecar event add ... [--json]`
116
+ - `sidecar export [--format json|jsonl] [--output <path>]`
108
117
  - `sidecar help`
109
118
 
110
119
  Context and summary:
@@ -256,6 +265,15 @@ Initial UI screens:
256
265
  - Decisions: decision records with summary and timestamps
257
266
  - Preferences: `.sidecar/preferences.json` and `.sidecar/summary.md`
258
267
 
268
+ UI write support (v1):
269
+
270
+ - Add notes from Overview
271
+ - Add open tasks from Overview
272
+ - Edit `.sidecar/preferences.json` from Preferences
273
+ - `output.humanTime` controls timestamp style in human-readable CLI output:
274
+ - `true`: friendly local times (for example `3/18/2026, 11:51 AM`)
275
+ - `false`: raw ISO-style timestamps
276
+
259
277
  ## JSON output
260
278
 
261
279
  Most commands support `--json` and return structured output:
@@ -267,6 +285,68 @@ Most commands support `--json` and return structured output:
267
285
 
268
286
  This makes Sidecar easy to automate from scripts and AI agents.
269
287
 
288
+ ## Integration API
289
+
290
+ Sidecar CLI is the first integration API for scripts, agents, and local tooling.
291
+
292
+ Standard JSON envelope:
293
+
294
+ ```json
295
+ {
296
+ "ok": true,
297
+ "version": "1.0",
298
+ "command": "task add",
299
+ "data": {},
300
+ "errors": []
301
+ }
302
+ ```
303
+
304
+ Failure envelope:
305
+
306
+ ```json
307
+ {
308
+ "ok": false,
309
+ "version": "1.0",
310
+ "command": "task add",
311
+ "data": null,
312
+ "errors": ["..."]
313
+ }
314
+ ```
315
+
316
+ Generic event ingest:
317
+
318
+ ```bash
319
+ sidecar event add --type decision --title "Use SQLite" --summary "Simple local storage for v1" --created-by agent --source cli --json
320
+ sidecar event add --json-input '{"type":"note","summary":"Captured context","created_by":"agent"}' --json
321
+ cat event.json | sidecar event add --stdin --json
322
+ ```
323
+
324
+ Capabilities metadata:
325
+
326
+ ```bash
327
+ sidecar capabilities --json
328
+ ```
329
+
330
+ Includes:
331
+
332
+ - `cli_version`
333
+ - `json_contract_version`
334
+ - `features`
335
+ - command and option metadata
336
+
337
+ Export project memory:
338
+
339
+ ```bash
340
+ sidecar export --format json
341
+ sidecar export --format json --output ./exports/sidecar.json
342
+ sidecar export --format jsonl > sidecar-events.jsonl
343
+ ```
344
+
345
+ JSONL note:
346
+
347
+ - JSONL export currently emits events only, one JSON object per line.
348
+ - each JSONL line includes `"version": "1.0"` and `"record_type": "event"`.
349
+
270
350
  ## Release and distribution
271
351
 
272
352
  See [RELEASE.md](./RELEASE.md) for publishing/release details.
package/dist/cli.js CHANGED
@@ -21,11 +21,14 @@ import { addArtifact, listArtifacts } from './services/artifact-service.js';
21
21
  import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
22
22
  import { addTask, listTasks, markTaskDone } from './services/task-service.js';
23
23
  import { currentSession, endSession, startSession, verifySessionHygiene } from './services/session-service.js';
24
+ import { eventIngestSchema, ingestEvent } from './services/event-ingest-service.js';
25
+ import { buildExportJson, buildExportJsonlEvents, writeOutputFile } from './services/export-service.js';
24
26
  const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
25
27
  const actorSchema = z.enum(['human', 'agent']);
26
28
  const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
27
29
  const artifactKindSchema = z.enum(['file', 'doc', 'screenshot', 'other']);
28
30
  const taskStatusSchema = z.enum(['open', 'done', 'all']);
31
+ const exportFormatSchema = z.enum(['json', 'jsonl']);
29
32
  const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
30
33
  function fail(message) {
31
34
  throw new SidecarError(message);
@@ -167,6 +170,13 @@ function renderContextMarkdown(data) {
167
170
  }
168
171
  return lines.join('\n');
169
172
  }
173
+ async function readStdinText() {
174
+ const chunks = [];
175
+ for await (const chunk of process.stdin) {
176
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
177
+ }
178
+ return Buffer.concat(chunks).toString('utf8');
179
+ }
170
180
  function resolveProjectRoot(projectPath) {
171
181
  const basePath = projectPath ? path.resolve(projectPath) : process.cwd();
172
182
  const root = findSidecarRoot(basePath);
@@ -363,7 +373,7 @@ program
363
373
  };
364
374
  const recent = db.prepare(`SELECT type, title, created_at FROM events WHERE project_id = ? ORDER BY created_at DESC LIMIT 5`).all(projectId);
365
375
  db.close();
366
- const data = { initialized: true, project, counts, recent };
376
+ const data = { project, counts, recent_events: recent };
367
377
  respondSuccess(command, Boolean(opts.json), data, [
368
378
  `Project: ${project.name}`,
369
379
  `Root: ${project.root_path}`,
@@ -379,6 +389,24 @@ program
379
389
  handleCommandError(command, Boolean(opts.json), normalized);
380
390
  }
381
391
  });
392
+ const preferences = program.command('preferences').description('Preferences commands');
393
+ preferences
394
+ .command('show')
395
+ .description('Show project preferences')
396
+ .option('--json', 'Print machine-readable JSON output')
397
+ .addHelpText('after', '\nExamples:\n $ sidecar preferences show\n $ sidecar preferences show --json')
398
+ .action((opts) => {
399
+ const command = 'preferences show';
400
+ try {
401
+ const { rootPath } = requireInitialized();
402
+ const prefsPath = getSidecarPaths(rootPath).preferencesPath;
403
+ const preferencesData = fs.existsSync(prefsPath) ? JSON.parse(fs.readFileSync(prefsPath, 'utf8')) : {};
404
+ respondSuccess(command, Boolean(opts.json), { project: { root_path: rootPath }, preferences: preferencesData, path: prefsPath }, [`Preferences path: ${prefsPath}`, stringifyJson(preferencesData)]);
405
+ }
406
+ catch (err) {
407
+ handleCommandError(command, Boolean(opts.json), err);
408
+ }
409
+ });
382
410
  program
383
411
  .command('capabilities')
384
412
  .description('Output a machine-readable manifest of Sidecar commands')
@@ -392,6 +420,120 @@ program
392
420
  else
393
421
  console.log(stringifyJson(manifest));
394
422
  });
423
+ const event = program.command('event').description('Generic event ingest commands');
424
+ event
425
+ .command('add')
426
+ .description('Add a validated generic Sidecar event')
427
+ .option('--type <type>', 'note|decision|worklog|task_created|task_completed|summary_generated')
428
+ .option('--title <title>', 'Event title')
429
+ .option('--summary <summary>', 'Event summary')
430
+ .option('--details-json <json>', 'JSON object for details_json')
431
+ .option('--created-by <by>', 'human|agent|system')
432
+ .option('--source <source>', 'cli|imported|generated')
433
+ .option('--session-id <id>', 'Optional session id', (v) => Number.parseInt(v, 10))
434
+ .option('--json-input <json>', 'Raw JSON event payload')
435
+ .option('--stdin', 'Read JSON event payload from stdin')
436
+ .option('--json', 'Print machine-readable JSON output')
437
+ .addHelpText('after', '\nExamples:\n $ sidecar event add --type note --summary "Captured context"\n $ sidecar event add --json-input \'{"type":"decision","title":"Use SQLite","summary":"Local-first"}\' --json\n $ cat event.json | sidecar event add --stdin --json')
438
+ .action(async (opts) => {
439
+ const command = 'event add';
440
+ try {
441
+ const payloadSources = [Boolean(opts.jsonInput), Boolean(opts.stdin), Boolean(opts.type || opts.title || opts.summary || opts.detailsJson || opts.createdBy || opts.source || opts.sessionId)];
442
+ if (payloadSources.filter(Boolean).length !== 1) {
443
+ fail('Provide exactly one payload source: structured flags OR --json-input OR --stdin');
444
+ }
445
+ let payloadRaw;
446
+ if (opts.jsonInput) {
447
+ payloadRaw = JSON.parse(opts.jsonInput);
448
+ }
449
+ else if (opts.stdin) {
450
+ const raw = (await readStdinText()).trim();
451
+ if (!raw)
452
+ fail('STDIN payload is empty');
453
+ payloadRaw = JSON.parse(raw);
454
+ }
455
+ else {
456
+ payloadRaw = {
457
+ type: opts.type,
458
+ title: opts.title,
459
+ summary: opts.summary,
460
+ details_json: opts.detailsJson ? JSON.parse(opts.detailsJson) : undefined,
461
+ created_by: opts.createdBy,
462
+ source: opts.source,
463
+ session_id: Number.isInteger(opts.sessionId) ? opts.sessionId : undefined,
464
+ };
465
+ }
466
+ const payload = eventIngestSchema.parse(payloadRaw);
467
+ const { db, projectId } = requireInitialized();
468
+ const created = ingestEvent(db, { project_id: projectId, payload });
469
+ db.close();
470
+ respondSuccess(command, Boolean(opts.json), { event: { ...created, created_at: nowIso() } }, [`Recorded ${created.type} event #${created.id}.`]);
471
+ }
472
+ catch (err) {
473
+ handleCommandError(command, Boolean(opts.json), err);
474
+ }
475
+ });
476
+ program
477
+ .command('export')
478
+ .description('Export project memory in JSON or JSONL')
479
+ .option('--format <format>', 'json|jsonl', 'json')
480
+ .option('--limit <n>', 'Limit exported events', (v) => Number.parseInt(v, 10))
481
+ .option('--type <event-type>', 'Filter exported events by type')
482
+ .option('--since <iso-date>', 'Filter events created_at >= since')
483
+ .option('--until <iso-date>', 'Filter events created_at <= until')
484
+ .option('--output <path>', 'Write export to file path instead of stdout')
485
+ .option('--json', 'Wrap command metadata in JSON envelope when writing to file')
486
+ .addHelpText('after', '\nExamples:\n $ sidecar export --format json\n $ sidecar export --format jsonl --output sidecar-events.jsonl\n $ sidecar export --type decision --since 2026-01-01T00:00:00Z')
487
+ .action((opts) => {
488
+ const command = 'export';
489
+ try {
490
+ const format = exportFormatSchema.parse(opts.format);
491
+ if (opts.since && Number.isNaN(Date.parse(opts.since)))
492
+ fail('--since must be a valid ISO date');
493
+ if (opts.until && Number.isNaN(Date.parse(opts.until)))
494
+ fail('--until must be a valid ISO date');
495
+ const { db, projectId, rootPath } = requireInitialized();
496
+ if (format === 'json') {
497
+ const payload = buildExportJson(db, {
498
+ projectId,
499
+ rootPath,
500
+ limit: opts.limit,
501
+ type: opts.type,
502
+ since: opts.since,
503
+ until: opts.until,
504
+ });
505
+ db.close();
506
+ const rendered = stringifyJson(payload);
507
+ if (opts.output) {
508
+ const filePath = writeOutputFile(opts.output, `${rendered}\n`);
509
+ respondSuccess(command, Boolean(opts.json), { format, output_path: filePath }, [`Export written: ${filePath}`]);
510
+ }
511
+ else {
512
+ console.log(rendered);
513
+ }
514
+ return;
515
+ }
516
+ const lines = buildExportJsonlEvents(db, {
517
+ projectId,
518
+ limit: opts.limit,
519
+ type: opts.type,
520
+ since: opts.since,
521
+ until: opts.until,
522
+ });
523
+ db.close();
524
+ const rendered = `${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`;
525
+ if (opts.output) {
526
+ const filePath = writeOutputFile(opts.output, rendered);
527
+ respondSuccess(command, Boolean(opts.json), { format, output_path: filePath, records: lines.length }, [`Export written: ${filePath}`]);
528
+ }
529
+ else {
530
+ process.stdout.write(rendered);
531
+ }
532
+ }
533
+ catch (err) {
534
+ handleCommandError(command, Boolean(opts.json), err);
535
+ }
536
+ });
395
537
  program
396
538
  .command('context')
397
539
  .description('Generate a compact context snapshot for a work session')
@@ -436,7 +578,7 @@ summary
436
578
  const { db, projectId, rootPath } = requireInitialized();
437
579
  const out = refreshSummaryFile(db, rootPath, projectId, Math.max(1, opts.limit));
438
580
  db.close();
439
- respondSuccess(command, Boolean(opts.json), { ...out, timestamp: nowIso() }, ['Summary refreshed.', `Path: ${out.path}`]);
581
+ respondSuccess(command, Boolean(opts.json), { summary: { path: out.path, generated_at: out.generatedAt } }, ['Summary refreshed.', `Path: ${out.path}`]);
440
582
  }
441
583
  catch (err) {
442
584
  handleCommandError(command, Boolean(opts.json), err);
@@ -456,7 +598,7 @@ program
456
598
  const rows = listRecentEvents(db, { projectId, type: opts.type, limit: Math.max(1, opts.limit) });
457
599
  db.close();
458
600
  if (opts.json)
459
- printJsonEnvelope(jsonSuccess(command, rows));
601
+ printJsonEnvelope(jsonSuccess(command, { events: rows }));
460
602
  else {
461
603
  if (rows.length === 0) {
462
604
  console.log('No events found.');
@@ -488,7 +630,7 @@ program
488
630
  const sessionId = maybeSessionId(db, projectId, opts.session);
489
631
  const eventId = addNote(db, { projectId, text, title: opts.title, by, sessionId });
490
632
  db.close();
491
- respondSuccess(command, Boolean(opts.json), { eventId, timestamp: nowIso() }, [`Recorded note event #${eventId}.`]);
633
+ respondSuccess(command, Boolean(opts.json), { event: { id: eventId, type: 'note', title: opts.title?.trim() || 'Note', summary: text, created_by: by, session_id: sessionId, created_at: nowIso() } }, [`Recorded note event #${eventId}.`]);
492
634
  }
493
635
  catch (err) {
494
636
  handleCommandError(command, Boolean(opts.json), err);
@@ -513,7 +655,7 @@ decision
513
655
  const sessionId = maybeSessionId(db, projectId, opts.session);
514
656
  const eventId = addDecision(db, { projectId, title: opts.title, summary: opts.summary, details: opts.details, by, sessionId });
515
657
  db.close();
516
- respondSuccess(command, Boolean(opts.json), { eventId, timestamp: nowIso() }, [`Recorded decision event #${eventId}.`]);
658
+ respondSuccess(command, Boolean(opts.json), { event: { id: eventId, type: 'decision', title: opts.title, summary: opts.summary, created_by: by, session_id: sessionId, created_at: nowIso() } }, [`Recorded decision event #${eventId}.`]);
517
659
  }
518
660
  catch (err) {
519
661
  handleCommandError(command, Boolean(opts.json), err);
@@ -552,7 +694,7 @@ worklog
552
694
  addArtifact(db, { projectId, path: filePath, kind: 'file' });
553
695
  }
554
696
  db.close();
555
- respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [
697
+ respondSuccess(command, Boolean(opts.json), { event: { id: result.eventId, type: 'worklog', summary: opts.done, created_by: by, session_id: sessionId, created_at: nowIso() }, artifacts: result.files.map((p) => ({ path: p, kind: 'file' })) }, [
556
698
  `Recorded worklog event #${result.eventId}.`,
557
699
  `Artifacts linked: ${result.files.length}`,
558
700
  ]);
@@ -578,7 +720,7 @@ task
578
720
  const { db, projectId } = requireInitialized();
579
721
  const result = addTask(db, { projectId, title, description: opts.description, priority, by });
580
722
  db.close();
581
- respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [`Added task #${result.taskId}.`]);
723
+ respondSuccess(command, Boolean(opts.json), { task: { id: result.taskId, title, description: opts.description ?? null, status: 'open', priority }, event: { id: result.eventId, type: 'task_created' } }, [`Added task #${result.taskId}.`]);
582
724
  }
583
725
  catch (err) {
584
726
  handleCommandError(command, Boolean(opts.json), err);
@@ -602,7 +744,7 @@ task
602
744
  db.close();
603
745
  if (!result.ok)
604
746
  fail(result.reason);
605
- respondSuccess(command, Boolean(opts.json), { taskId, eventId: result.eventId, timestamp: nowIso() }, [`Completed task #${taskId}.`]);
747
+ respondSuccess(command, Boolean(opts.json), { task: { id: taskId, status: 'done' }, event: { id: result.eventId, type: 'task_completed' } }, [`Completed task #${taskId}.`]);
606
748
  }
607
749
  catch (err) {
608
750
  handleCommandError(command, Boolean(opts.json), err);
@@ -624,7 +766,7 @@ task
624
766
  db.close();
625
767
  if (opts.format === 'json' || opts.json) {
626
768
  if (opts.json)
627
- printJsonEnvelope(jsonSuccess(command, rows));
769
+ printJsonEnvelope(jsonSuccess(command, { status, tasks: rows }));
628
770
  else
629
771
  console.log(stringifyJson(rows));
630
772
  return;
@@ -666,7 +808,7 @@ session
666
808
  db.close();
667
809
  if (!result.ok)
668
810
  fail(result.reason);
669
- respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [`Started session #${result.sessionId}.`]);
811
+ respondSuccess(command, Boolean(opts.json), { session: { id: result.sessionId, actor_type: actor, actor_name: opts.name ?? null, started_at: nowIso() } }, [`Started session #${result.sessionId}.`]);
670
812
  }
671
813
  catch (err) {
672
814
  handleCommandError(command, Boolean(opts.json), err);
@@ -686,7 +828,7 @@ session
686
828
  db.close();
687
829
  if (!result.ok)
688
830
  fail(result.reason);
689
- respondSuccess(command, Boolean(opts.json), { ...result, timestamp: nowIso() }, [`Ended session #${result.sessionId}.`]);
831
+ respondSuccess(command, Boolean(opts.json), { session: { id: result.sessionId, ended_at: nowIso(), summary: opts.summary ?? null } }, [`Ended session #${result.sessionId}.`]);
690
832
  }
691
833
  catch (err) {
692
834
  handleCommandError(command, Boolean(opts.json), err);
@@ -704,7 +846,7 @@ session
704
846
  const current = currentSession(db, projectId);
705
847
  db.close();
706
848
  if (opts.json)
707
- printJsonEnvelope(jsonSuccess(command, { current: current ?? null }));
849
+ printJsonEnvelope(jsonSuccess(command, { session: current ?? null }));
708
850
  else if (!current) {
709
851
  console.log('No active session.');
710
852
  }
@@ -768,7 +910,7 @@ artifact
768
910
  const { db, projectId } = requireInitialized();
769
911
  const artifactId = addArtifact(db, { projectId, path: artifactPath, kind, note: opts.note });
770
912
  db.close();
771
- respondSuccess(command, Boolean(opts.json), { artifactId, timestamp: nowIso() }, [`Added artifact #${artifactId}.`]);
913
+ respondSuccess(command, Boolean(opts.json), { artifact: { id: artifactId, path: artifactPath, kind, note: opts.note ?? null, created_at: nowIso() } }, [`Added artifact #${artifactId}.`]);
772
914
  }
773
915
  catch (err) {
774
916
  handleCommandError(command, Boolean(opts.json), err);
@@ -786,7 +928,7 @@ artifact
786
928
  const rows = listArtifacts(db, projectId);
787
929
  db.close();
788
930
  if (opts.json)
789
- printJsonEnvelope(jsonSuccess(command, rows));
931
+ printJsonEnvelope(jsonSuccess(command, { artifacts: rows }));
790
932
  else {
791
933
  if (rows.length === 0) {
792
934
  console.log('No artifacts found.');
@@ -1,7 +1,9 @@
1
1
  import { stringifyJson } from './format.js';
2
+ export const JSON_CONTRACT_VERSION = '1.0';
2
3
  export function jsonSuccess(command, data) {
3
4
  return {
4
5
  ok: true,
6
+ version: JSON_CONTRACT_VERSION,
5
7
  command,
6
8
  data,
7
9
  errors: [],
@@ -10,6 +12,7 @@ export function jsonSuccess(command, data) {
10
12
  export function jsonFailure(command, message) {
11
13
  return {
12
14
  ok: false,
15
+ version: JSON_CONTRACT_VERSION,
13
16
  command,
14
17
  data: null,
15
18
  errors: [message],
@@ -1,10 +1,17 @@
1
+ import { JSON_CONTRACT_VERSION } from '../lib/output.js';
1
2
  export function getCapabilitiesManifest(version) {
2
3
  return {
3
- schema_version: 1,
4
- cli: {
5
- name: 'sidecar',
6
- version,
7
- },
4
+ cli_name: 'sidecar',
5
+ cli_version: version,
6
+ json_contract_version: JSON_CONTRACT_VERSION,
7
+ features: [
8
+ 'json_envelope_v1',
9
+ 'capabilities_manifest',
10
+ 'event_ingest',
11
+ 'export_json',
12
+ 'export_jsonl_events',
13
+ 'optional_local_ui',
14
+ ],
8
15
  commands: [
9
16
  {
10
17
  name: 'init',
@@ -20,6 +27,22 @@ export function getCapabilitiesManifest(version) {
20
27
  arguments: [],
21
28
  options: ['--json'],
22
29
  },
30
+ {
31
+ name: 'preferences',
32
+ description: 'Preferences operations',
33
+ json_output: true,
34
+ arguments: [],
35
+ options: [],
36
+ subcommands: [
37
+ {
38
+ name: 'show',
39
+ description: 'Show project preferences',
40
+ json_output: true,
41
+ arguments: [],
42
+ options: ['--json'],
43
+ },
44
+ ],
45
+ },
23
46
  {
24
47
  name: 'ui',
25
48
  description: 'Launch optional local Sidecar UI (lazy-installed on first run)',
@@ -41,6 +64,40 @@ export function getCapabilitiesManifest(version) {
41
64
  arguments: [],
42
65
  options: ['--limit <n>', '--format text|markdown|json', '--json'],
43
66
  },
67
+ {
68
+ name: 'event',
69
+ description: 'Generic event ingest operations',
70
+ json_output: true,
71
+ arguments: [],
72
+ options: [],
73
+ subcommands: [
74
+ {
75
+ name: 'add',
76
+ description: 'Add a validated generic event payload',
77
+ json_output: true,
78
+ arguments: [],
79
+ options: [
80
+ '--type <type>',
81
+ '--title <title>',
82
+ '--summary <summary>',
83
+ '--details-json <json>',
84
+ '--created-by <by>',
85
+ '--source <source>',
86
+ '--session-id <id>',
87
+ '--json-input <json>',
88
+ '--stdin',
89
+ '--json',
90
+ ],
91
+ },
92
+ ],
93
+ },
94
+ {
95
+ name: 'export',
96
+ description: 'Export project memory in JSON or JSONL',
97
+ json_output: true,
98
+ arguments: [],
99
+ options: ['--format json|jsonl', '--limit <n>', '--type <event-type>', '--since <iso-date>', '--until <iso-date>', '--output <path>', '--json'],
100
+ },
44
101
  {
45
102
  name: 'summary',
46
103
  description: 'Summary operations',
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { createEvent } from './event-service.js';
3
+ const eventTypeSchema = z.enum([
4
+ 'note',
5
+ 'decision',
6
+ 'worklog',
7
+ 'task_created',
8
+ 'task_completed',
9
+ 'summary_generated',
10
+ ]);
11
+ const createdBySchema = z.enum(['human', 'agent', 'system']).default('system');
12
+ const sourceSchema = z.enum(['cli', 'imported', 'generated']).default('cli');
13
+ export const eventIngestSchema = z
14
+ .object({
15
+ type: eventTypeSchema,
16
+ title: z.string().trim().min(1).optional(),
17
+ summary: z.string().trim().min(1).optional(),
18
+ details_json: z.record(z.string(), z.unknown()).optional(),
19
+ created_by: createdBySchema.optional(),
20
+ source: sourceSchema.optional(),
21
+ session_id: z.number().int().positive().nullable().optional(),
22
+ })
23
+ .superRefine((payload, ctx) => {
24
+ if (payload.type === 'decision') {
25
+ if (!payload.title)
26
+ ctx.addIssue({ code: 'custom', path: ['title'], message: 'title is required for decision events' });
27
+ if (!payload.summary)
28
+ ctx.addIssue({ code: 'custom', path: ['summary'], message: 'summary is required for decision events' });
29
+ return;
30
+ }
31
+ if (payload.type === 'summary_generated')
32
+ return;
33
+ if (!payload.summary) {
34
+ ctx.addIssue({ code: 'custom', path: ['summary'], message: `summary is required for ${payload.type} events` });
35
+ }
36
+ });
37
+ export function ingestEvent(db, input) {
38
+ const payload = eventIngestSchema.parse(input.payload);
39
+ const title = payload.title ??
40
+ (payload.type === 'note'
41
+ ? 'Note'
42
+ : payload.type === 'worklog'
43
+ ? 'Worklog entry'
44
+ : payload.type === 'task_created'
45
+ ? 'Task created'
46
+ : payload.type === 'task_completed'
47
+ ? 'Task completed'
48
+ : payload.type === 'summary_generated'
49
+ ? 'Summary refreshed'
50
+ : 'Event');
51
+ const summary = payload.summary ?? '';
52
+ const eventId = createEvent(db, {
53
+ projectId: input.project_id,
54
+ type: payload.type,
55
+ title,
56
+ summary,
57
+ details: payload.details_json ?? {},
58
+ createdBy: payload.created_by ?? 'system',
59
+ source: payload.source ?? 'cli',
60
+ sessionId: payload.session_id ?? null,
61
+ });
62
+ return {
63
+ id: eventId,
64
+ type: payload.type,
65
+ title,
66
+ summary,
67
+ details_json: payload.details_json ?? {},
68
+ created_by: payload.created_by ?? 'system',
69
+ source: payload.source ?? 'cli',
70
+ session_id: payload.session_id ?? null,
71
+ };
72
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { JSON_CONTRACT_VERSION } from '../lib/output.js';
4
+ export function buildExportJson(db, input) {
5
+ const project = db.prepare('SELECT id, name, root_path, created_at, updated_at FROM projects WHERE id = ?').get(input.projectId);
6
+ const preferencesPath = path.join(input.rootPath, '.sidecar', 'preferences.json');
7
+ const preferences = fs.existsSync(preferencesPath)
8
+ ? JSON.parse(fs.readFileSync(preferencesPath, 'utf8'))
9
+ : null;
10
+ const sessions = db
11
+ .prepare('SELECT id, project_id, started_at, ended_at, actor_type, actor_name, summary FROM sessions WHERE project_id = ? ORDER BY started_at DESC')
12
+ .all(input.projectId);
13
+ const tasks = db
14
+ .prepare(`SELECT id, project_id, title, description, status, priority, created_at, updated_at, closed_at, origin_event_id FROM tasks WHERE project_id = ? ORDER BY CASE WHEN status = 'open' THEN 0 ELSE 1 END, updated_at DESC`)
15
+ .all(input.projectId);
16
+ const artifacts = db
17
+ .prepare('SELECT id, project_id, path, kind, note, created_at FROM artifacts WHERE project_id = ? ORDER BY created_at DESC')
18
+ .all(input.projectId);
19
+ const eventWhere = ['project_id = ?'];
20
+ const eventArgs = [input.projectId];
21
+ if (input.type) {
22
+ eventWhere.push('type = ?');
23
+ eventArgs.push(input.type);
24
+ }
25
+ if (input.since) {
26
+ eventWhere.push('created_at >= ?');
27
+ eventArgs.push(input.since);
28
+ }
29
+ if (input.until) {
30
+ eventWhere.push('created_at <= ?');
31
+ eventArgs.push(input.until);
32
+ }
33
+ const limitClause = input.limit && input.limit > 0 ? ` LIMIT ${Math.floor(input.limit)}` : '';
34
+ const events = db
35
+ .prepare(`SELECT id, project_id, type, title, summary, details_json, created_at, created_by, source, session_id
36
+ FROM events
37
+ WHERE ${eventWhere.join(' AND ')}
38
+ ORDER BY created_at DESC${limitClause}`)
39
+ .all(...eventArgs);
40
+ return {
41
+ version: JSON_CONTRACT_VERSION,
42
+ project,
43
+ preferences: preferences,
44
+ sessions: sessions,
45
+ tasks: tasks,
46
+ artifacts: artifacts,
47
+ events,
48
+ };
49
+ }
50
+ export function buildExportJsonlEvents(db, input) {
51
+ const where = ['project_id = ?'];
52
+ const args = [input.projectId];
53
+ if (input.type) {
54
+ where.push('type = ?');
55
+ args.push(input.type);
56
+ }
57
+ if (input.since) {
58
+ where.push('created_at >= ?');
59
+ args.push(input.since);
60
+ }
61
+ if (input.until) {
62
+ where.push('created_at <= ?');
63
+ args.push(input.until);
64
+ }
65
+ const limitClause = input.limit && input.limit > 0 ? ` LIMIT ${Math.floor(input.limit)}` : '';
66
+ const rows = db
67
+ .prepare(`SELECT id, project_id, type, title, summary, details_json, created_at, created_by, source, session_id
68
+ FROM events
69
+ WHERE ${where.join(' AND ')}
70
+ ORDER BY created_at DESC${limitClause}`)
71
+ .all(...args);
72
+ return rows.map((row) => JSON.stringify({ version: JSON_CONTRACT_VERSION, record_type: 'event', project_id: row.project_id, data: row }));
73
+ }
74
+ export function writeOutputFile(outputPath, content) {
75
+ const abs = path.resolve(outputPath);
76
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
77
+ fs.writeFileSync(abs, content);
78
+ return abs;
79
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidecar-cli",
3
- "version": "0.1.1-beta.2",
3
+ "version": "0.1.1-rc.1",
4
4
  "description": "Local-first project memory and recording tool",
5
5
  "scripts": {
6
6
  "build": "npm run clean && tsc -p tsconfig.json",