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 +80 -0
- package/dist/cli.js +156 -14
- package/dist/lib/output.js +3 -0
- package/dist/services/capabilities-service.js +62 -5
- package/dist/services/event-ingest-service.js +72 -0
- package/dist/services/export-service.js +79 -0
- package/dist/types/api.js +1 -0
- package/package.json +1 -1
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 = {
|
|
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), {
|
|
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,
|
|
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,
|
|
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), {
|
|
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), {
|
|
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,
|
|
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), {
|
|
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), {
|
|
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, {
|
|
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,
|
|
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.');
|
package/dist/lib/output.js
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {};
|