sidecar-cli 0.1.1 → 0.1.2
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 +132 -1
- package/dist/cli.js +213 -15
- package/dist/lib/output.js +3 -0
- package/dist/lib/ui.js +106 -0
- package/dist/services/capabilities-service.js +69 -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,7 +109,11 @@ Global:
|
|
|
103
109
|
|
|
104
110
|
- `sidecar init [--force] [--name <project-name>] [--json]`
|
|
105
111
|
- `sidecar status [--json]`
|
|
112
|
+
- `sidecar preferences show [--json]`
|
|
113
|
+
- `sidecar ui [--no-open] [--port <port>] [--install-only] [--project <path>] [--reinstall]`
|
|
106
114
|
- `sidecar capabilities --json`
|
|
115
|
+
- `sidecar event add ... [--json]`
|
|
116
|
+
- `sidecar export [--format json|jsonl] [--output <path>]`
|
|
107
117
|
- `sidecar help`
|
|
108
118
|
|
|
109
119
|
Context and summary:
|
|
@@ -168,7 +178,18 @@ Optional local enforcement:
|
|
|
168
178
|
npm run install:hooks
|
|
169
179
|
```
|
|
170
180
|
|
|
171
|
-
This
|
|
181
|
+
This is optional and per-repository clone. `sidecar init` does not install git hooks automatically.
|
|
182
|
+
|
|
183
|
+
This installs a pre-commit guard that checks staged non-doc code changes.
|
|
184
|
+
If staged code changes are present, commit is blocked unless both are recorded since the last commit:
|
|
185
|
+
|
|
186
|
+
- a `worklog` event
|
|
187
|
+
- a `summary refresh` event
|
|
188
|
+
|
|
189
|
+
The guard command is:
|
|
190
|
+
|
|
191
|
+
- `npm run sidecar:reminder -- --staged --enforce`
|
|
192
|
+
|
|
172
193
|
If a pre-commit hook already exists, Sidecar will not overwrite it unless you run:
|
|
173
194
|
|
|
174
195
|
```bash
|
|
@@ -205,6 +226,54 @@ Primary tables:
|
|
|
205
226
|
|
|
206
227
|
No network dependency is required for normal operation.
|
|
207
228
|
|
|
229
|
+
## Optional local UI
|
|
230
|
+
|
|
231
|
+
`sidecar ui` launches a local browser UI for the selected Sidecar project.
|
|
232
|
+
|
|
233
|
+
Lazy-install behavior:
|
|
234
|
+
|
|
235
|
+
1. `sidecar ui` resolves the nearest `.sidecar` project root (or uses `--project`).
|
|
236
|
+
2. Sidecar checks for `@sidecar/ui` in `~/.sidecar/ui`.
|
|
237
|
+
3. If missing/incompatible, Sidecar installs or updates it automatically.
|
|
238
|
+
4. Sidecar starts a local UI server and opens the browser (unless `--no-open`).
|
|
239
|
+
|
|
240
|
+
UI runtime location:
|
|
241
|
+
|
|
242
|
+
- `~/.sidecar/ui`
|
|
243
|
+
- the CLI installs `@sidecar/ui` here (not in your project repo)
|
|
244
|
+
|
|
245
|
+
Version compatibility rule:
|
|
246
|
+
|
|
247
|
+
- CLI and UI must share the same major version.
|
|
248
|
+
- If majors differ, `sidecar ui` auto-reinstalls/updates UI.
|
|
249
|
+
|
|
250
|
+
Common examples:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
sidecar ui
|
|
254
|
+
sidecar ui --no-open --port 4311
|
|
255
|
+
sidecar ui --install-only
|
|
256
|
+
sidecar ui --project ../other-repo
|
|
257
|
+
sidecar ui --reinstall
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Initial UI screens:
|
|
261
|
+
|
|
262
|
+
- Overview: project info, active session, recent decisions/worklogs, open tasks, recent notes
|
|
263
|
+
- Timeline: recent events in chronological order
|
|
264
|
+
- Tasks: open and completed tasks
|
|
265
|
+
- Decisions: decision records with summary and timestamps
|
|
266
|
+
- Preferences: `.sidecar/preferences.json` and `.sidecar/summary.md`
|
|
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
|
+
|
|
208
277
|
## JSON output
|
|
209
278
|
|
|
210
279
|
Most commands support `--json` and return structured output:
|
|
@@ -216,6 +285,68 @@ Most commands support `--json` and return structured output:
|
|
|
216
285
|
|
|
217
286
|
This makes Sidecar easy to automate from scripts and AI agents.
|
|
218
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
|
+
|
|
219
350
|
## Release and distribution
|
|
220
351
|
|
|
221
352
|
See [RELEASE.md](./RELEASE.md) for publishing/release details.
|
package/dist/cli.js
CHANGED
|
@@ -5,12 +5,13 @@ import { Command } from 'commander';
|
|
|
5
5
|
import Database from 'better-sqlite3';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { initializeSchema } from './db/schema.js';
|
|
8
|
-
import { getSidecarPaths } from './lib/paths.js';
|
|
8
|
+
import { findSidecarRoot, getSidecarPaths } from './lib/paths.js';
|
|
9
9
|
import { nowIso, humanTime, stringifyJson } from './lib/format.js';
|
|
10
10
|
import { SidecarError } from './lib/errors.js';
|
|
11
11
|
import { jsonFailure, jsonSuccess, printJsonEnvelope } from './lib/output.js';
|
|
12
12
|
import { bannerDisabled, renderBanner } from './lib/banner.js';
|
|
13
13
|
import { getUpdateNotice } from './lib/update-check.js';
|
|
14
|
+
import { ensureUiInstalled, launchUiServer } from './lib/ui.js';
|
|
14
15
|
import { requireInitialized } from './db/client.js';
|
|
15
16
|
import { renderAgentsMarkdown, renderClaudeMarkdown } from './templates/agents.js';
|
|
16
17
|
import { refreshSummaryFile } from './services/summary-service.js';
|
|
@@ -20,11 +21,14 @@ import { addArtifact, listArtifacts } from './services/artifact-service.js';
|
|
|
20
21
|
import { addDecision, addNote, addWorklog, getActiveSessionId, listRecentEvents } from './services/event-service.js';
|
|
21
22
|
import { addTask, listTasks, markTaskDone } from './services/task-service.js';
|
|
22
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';
|
|
23
26
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
24
27
|
const actorSchema = z.enum(['human', 'agent']);
|
|
25
28
|
const taskPrioritySchema = z.enum(['low', 'medium', 'high']);
|
|
26
29
|
const artifactKindSchema = z.enum(['file', 'doc', 'screenshot', 'other']);
|
|
27
30
|
const taskStatusSchema = z.enum(['open', 'done', 'all']);
|
|
31
|
+
const exportFormatSchema = z.enum(['json', 'jsonl']);
|
|
28
32
|
const NOT_INITIALIZED_MSG = 'Sidecar is not initialized in this directory or any parent directory';
|
|
29
33
|
function fail(message) {
|
|
30
34
|
throw new SidecarError(message);
|
|
@@ -166,6 +170,21 @@ function renderContextMarkdown(data) {
|
|
|
166
170
|
}
|
|
167
171
|
return lines.join('\n');
|
|
168
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
|
+
}
|
|
180
|
+
function resolveProjectRoot(projectPath) {
|
|
181
|
+
const basePath = projectPath ? path.resolve(projectPath) : process.cwd();
|
|
182
|
+
const root = findSidecarRoot(basePath);
|
|
183
|
+
if (!root) {
|
|
184
|
+
throw new SidecarError(NOT_INITIALIZED_MSG);
|
|
185
|
+
}
|
|
186
|
+
return root;
|
|
187
|
+
}
|
|
169
188
|
const program = new Command();
|
|
170
189
|
program.name('sidecar').description('Local-first project memory and recording CLI').version(pkg.version);
|
|
171
190
|
program.option('--no-banner', 'Disable Sidecar banner output');
|
|
@@ -183,6 +202,53 @@ function maybePrintUpdateNotice() {
|
|
|
183
202
|
console.log(`Update available: ${pkg.version} -> ${notice.latestVersion}`);
|
|
184
203
|
console.log(`Run: npm install -g sidecar-cli@${installTag}`);
|
|
185
204
|
}
|
|
205
|
+
program
|
|
206
|
+
.command('ui')
|
|
207
|
+
.description('Launch the optional local Sidecar UI')
|
|
208
|
+
.option('--no-open', 'Do not open the browser automatically')
|
|
209
|
+
.option('--port <port>', 'Port to run the UI on', (v) => Number.parseInt(v, 10), 4310)
|
|
210
|
+
.option('--install-only', 'Install/update UI package but do not launch')
|
|
211
|
+
.option('--project <path>', 'Project path (defaults to nearest Sidecar root)')
|
|
212
|
+
.option('--reinstall', 'Force reinstall UI package')
|
|
213
|
+
.addHelpText('after', '\nExamples:\n $ sidecar ui\n $ sidecar ui --no-open --port 4311\n $ sidecar ui --project ../my-repo --install-only')
|
|
214
|
+
.action((opts) => {
|
|
215
|
+
const command = 'ui';
|
|
216
|
+
try {
|
|
217
|
+
const projectRoot = resolveProjectRoot(opts.project);
|
|
218
|
+
const port = Number(opts.port);
|
|
219
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
220
|
+
fail('Port must be an integer between 1 and 65535');
|
|
221
|
+
}
|
|
222
|
+
if (!bannerDisabled()) {
|
|
223
|
+
console.log(renderBanner());
|
|
224
|
+
console.log('');
|
|
225
|
+
}
|
|
226
|
+
console.log('Launching Sidecar UI');
|
|
227
|
+
console.log(`Project: ${projectRoot}`);
|
|
228
|
+
const { installedVersion } = ensureUiInstalled({
|
|
229
|
+
cliVersion: pkg.version,
|
|
230
|
+
reinstall: Boolean(opts.reinstall),
|
|
231
|
+
onStatus: (line) => console.log(line),
|
|
232
|
+
});
|
|
233
|
+
console.log(`UI version: ${installedVersion}`);
|
|
234
|
+
if (opts.installOnly) {
|
|
235
|
+
console.log('Install-only mode complete.');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const { url } = launchUiServer({
|
|
239
|
+
projectPath: projectRoot,
|
|
240
|
+
port,
|
|
241
|
+
openBrowser: opts.open !== false,
|
|
242
|
+
});
|
|
243
|
+
console.log(`URL: ${url}`);
|
|
244
|
+
if (opts.open === false) {
|
|
245
|
+
console.log('Browser auto-open disabled.');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
handleCommandError(command, false, err);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
186
252
|
program
|
|
187
253
|
.command('init')
|
|
188
254
|
.description('Initialize Sidecar in the current directory')
|
|
@@ -307,7 +373,7 @@ program
|
|
|
307
373
|
};
|
|
308
374
|
const recent = db.prepare(`SELECT type, title, created_at FROM events WHERE project_id = ? ORDER BY created_at DESC LIMIT 5`).all(projectId);
|
|
309
375
|
db.close();
|
|
310
|
-
const data = {
|
|
376
|
+
const data = { project, counts, recent_events: recent };
|
|
311
377
|
respondSuccess(command, Boolean(opts.json), data, [
|
|
312
378
|
`Project: ${project.name}`,
|
|
313
379
|
`Root: ${project.root_path}`,
|
|
@@ -323,6 +389,24 @@ program
|
|
|
323
389
|
handleCommandError(command, Boolean(opts.json), normalized);
|
|
324
390
|
}
|
|
325
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
|
+
});
|
|
326
410
|
program
|
|
327
411
|
.command('capabilities')
|
|
328
412
|
.description('Output a machine-readable manifest of Sidecar commands')
|
|
@@ -336,6 +420,120 @@ program
|
|
|
336
420
|
else
|
|
337
421
|
console.log(stringifyJson(manifest));
|
|
338
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
|
+
});
|
|
339
537
|
program
|
|
340
538
|
.command('context')
|
|
341
539
|
.description('Generate a compact context snapshot for a work session')
|
|
@@ -380,7 +578,7 @@ summary
|
|
|
380
578
|
const { db, projectId, rootPath } = requireInitialized();
|
|
381
579
|
const out = refreshSummaryFile(db, rootPath, projectId, Math.max(1, opts.limit));
|
|
382
580
|
db.close();
|
|
383
|
-
respondSuccess(command, Boolean(opts.json), {
|
|
581
|
+
respondSuccess(command, Boolean(opts.json), { summary: { path: out.path, generated_at: out.generatedAt } }, ['Summary refreshed.', `Path: ${out.path}`]);
|
|
384
582
|
}
|
|
385
583
|
catch (err) {
|
|
386
584
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -400,7 +598,7 @@ program
|
|
|
400
598
|
const rows = listRecentEvents(db, { projectId, type: opts.type, limit: Math.max(1, opts.limit) });
|
|
401
599
|
db.close();
|
|
402
600
|
if (opts.json)
|
|
403
|
-
printJsonEnvelope(jsonSuccess(command, rows));
|
|
601
|
+
printJsonEnvelope(jsonSuccess(command, { events: rows }));
|
|
404
602
|
else {
|
|
405
603
|
if (rows.length === 0) {
|
|
406
604
|
console.log('No events found.');
|
|
@@ -432,7 +630,7 @@ program
|
|
|
432
630
|
const sessionId = maybeSessionId(db, projectId, opts.session);
|
|
433
631
|
const eventId = addNote(db, { projectId, text, title: opts.title, by, sessionId });
|
|
434
632
|
db.close();
|
|
435
|
-
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}.`]);
|
|
436
634
|
}
|
|
437
635
|
catch (err) {
|
|
438
636
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -457,7 +655,7 @@ decision
|
|
|
457
655
|
const sessionId = maybeSessionId(db, projectId, opts.session);
|
|
458
656
|
const eventId = addDecision(db, { projectId, title: opts.title, summary: opts.summary, details: opts.details, by, sessionId });
|
|
459
657
|
db.close();
|
|
460
|
-
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}.`]);
|
|
461
659
|
}
|
|
462
660
|
catch (err) {
|
|
463
661
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -496,7 +694,7 @@ worklog
|
|
|
496
694
|
addArtifact(db, { projectId, path: filePath, kind: 'file' });
|
|
497
695
|
}
|
|
498
696
|
db.close();
|
|
499
|
-
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' })) }, [
|
|
500
698
|
`Recorded worklog event #${result.eventId}.`,
|
|
501
699
|
`Artifacts linked: ${result.files.length}`,
|
|
502
700
|
]);
|
|
@@ -522,7 +720,7 @@ task
|
|
|
522
720
|
const { db, projectId } = requireInitialized();
|
|
523
721
|
const result = addTask(db, { projectId, title, description: opts.description, priority, by });
|
|
524
722
|
db.close();
|
|
525
|
-
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}.`]);
|
|
526
724
|
}
|
|
527
725
|
catch (err) {
|
|
528
726
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -546,7 +744,7 @@ task
|
|
|
546
744
|
db.close();
|
|
547
745
|
if (!result.ok)
|
|
548
746
|
fail(result.reason);
|
|
549
|
-
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}.`]);
|
|
550
748
|
}
|
|
551
749
|
catch (err) {
|
|
552
750
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -568,7 +766,7 @@ task
|
|
|
568
766
|
db.close();
|
|
569
767
|
if (opts.format === 'json' || opts.json) {
|
|
570
768
|
if (opts.json)
|
|
571
|
-
printJsonEnvelope(jsonSuccess(command, rows));
|
|
769
|
+
printJsonEnvelope(jsonSuccess(command, { status, tasks: rows }));
|
|
572
770
|
else
|
|
573
771
|
console.log(stringifyJson(rows));
|
|
574
772
|
return;
|
|
@@ -610,7 +808,7 @@ session
|
|
|
610
808
|
db.close();
|
|
611
809
|
if (!result.ok)
|
|
612
810
|
fail(result.reason);
|
|
613
|
-
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}.`]);
|
|
614
812
|
}
|
|
615
813
|
catch (err) {
|
|
616
814
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -630,7 +828,7 @@ session
|
|
|
630
828
|
db.close();
|
|
631
829
|
if (!result.ok)
|
|
632
830
|
fail(result.reason);
|
|
633
|
-
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}.`]);
|
|
634
832
|
}
|
|
635
833
|
catch (err) {
|
|
636
834
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -648,7 +846,7 @@ session
|
|
|
648
846
|
const current = currentSession(db, projectId);
|
|
649
847
|
db.close();
|
|
650
848
|
if (opts.json)
|
|
651
|
-
printJsonEnvelope(jsonSuccess(command, {
|
|
849
|
+
printJsonEnvelope(jsonSuccess(command, { session: current ?? null }));
|
|
652
850
|
else if (!current) {
|
|
653
851
|
console.log('No active session.');
|
|
654
852
|
}
|
|
@@ -712,7 +910,7 @@ artifact
|
|
|
712
910
|
const { db, projectId } = requireInitialized();
|
|
713
911
|
const artifactId = addArtifact(db, { projectId, path: artifactPath, kind, note: opts.note });
|
|
714
912
|
db.close();
|
|
715
|
-
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}.`]);
|
|
716
914
|
}
|
|
717
915
|
catch (err) {
|
|
718
916
|
handleCommandError(command, Boolean(opts.json), err);
|
|
@@ -730,7 +928,7 @@ artifact
|
|
|
730
928
|
const rows = listArtifacts(db, projectId);
|
|
731
929
|
db.close();
|
|
732
930
|
if (opts.json)
|
|
733
|
-
printJsonEnvelope(jsonSuccess(command, rows));
|
|
931
|
+
printJsonEnvelope(jsonSuccess(command, { artifacts: rows }));
|
|
734
932
|
else {
|
|
735
933
|
if (rows.length === 0) {
|
|
736
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],
|
package/dist/lib/ui.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
5
|
+
import { detectReleaseChannel } from './update-check.js';
|
|
6
|
+
import { SidecarError } from './errors.js';
|
|
7
|
+
const UI_PACKAGE = '@sidecar/ui';
|
|
8
|
+
const UI_RUNTIME_DIR = path.join(os.homedir(), '.sidecar', 'ui');
|
|
9
|
+
function ensureRuntimeDir() {
|
|
10
|
+
fs.mkdirSync(UI_RUNTIME_DIR, { recursive: true });
|
|
11
|
+
const runtimePkgPath = path.join(UI_RUNTIME_DIR, 'package.json');
|
|
12
|
+
if (!fs.existsSync(runtimePkgPath)) {
|
|
13
|
+
fs.writeFileSync(runtimePkgPath, JSON.stringify({ name: 'sidecar-ui-runtime', private: true, version: '0.0.0' }, null, 2));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readInstalledUiVersion() {
|
|
17
|
+
const p = path.join(UI_RUNTIME_DIR, 'node_modules', '@sidecar', 'ui', 'package.json');
|
|
18
|
+
if (!fs.existsSync(p))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
22
|
+
return pkg.version ?? null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function major(version) {
|
|
29
|
+
const m = version.match(/^(\d+)\./);
|
|
30
|
+
if (!m)
|
|
31
|
+
return null;
|
|
32
|
+
return Number.parseInt(m[1], 10);
|
|
33
|
+
}
|
|
34
|
+
function isCompatible(cliVersion, uiVersion) {
|
|
35
|
+
if (!uiVersion)
|
|
36
|
+
return false;
|
|
37
|
+
const cliMajor = major(cliVersion);
|
|
38
|
+
const uiMajor = major(uiVersion);
|
|
39
|
+
return cliMajor !== null && uiMajor !== null && cliMajor === uiMajor;
|
|
40
|
+
}
|
|
41
|
+
function npmInstall(spec) {
|
|
42
|
+
execFileSync('npm', ['install', '--no-audit', '--no-fund', spec], {
|
|
43
|
+
cwd: UI_RUNTIME_DIR,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function getDesiredTag(cliVersion) {
|
|
48
|
+
return detectReleaseChannel(cliVersion);
|
|
49
|
+
}
|
|
50
|
+
export function ensureUiInstalled(options) {
|
|
51
|
+
ensureRuntimeDir();
|
|
52
|
+
const tag = getDesiredTag(options.cliVersion);
|
|
53
|
+
const installed = readInstalledUiVersion();
|
|
54
|
+
const shouldInstall = Boolean(options.reinstall) || !isCompatible(options.cliVersion, installed);
|
|
55
|
+
if (shouldInstall) {
|
|
56
|
+
options.onStatus?.(installed
|
|
57
|
+
? `Updating Sidecar UI (${installed}) for CLI compatibility...`
|
|
58
|
+
: 'Installing Sidecar UI for first use...');
|
|
59
|
+
const spec = `${UI_PACKAGE}@${tag}`;
|
|
60
|
+
try {
|
|
61
|
+
npmInstall(spec);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
const localUiPkg = path.resolve(process.cwd(), 'packages', 'ui');
|
|
65
|
+
if (fs.existsSync(path.join(localUiPkg, 'package.json'))) {
|
|
66
|
+
options.onStatus?.('Falling back to local workspace UI package...');
|
|
67
|
+
npmInstall(localUiPkg);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new SidecarError('Failed to install Sidecar UI package. Check npm access/network and retry `sidecar ui --reinstall`.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const finalVersion = readInstalledUiVersion();
|
|
75
|
+
if (!finalVersion) {
|
|
76
|
+
throw new SidecarError('Sidecar UI appears missing after install. Retry with `sidecar ui --reinstall`.');
|
|
77
|
+
}
|
|
78
|
+
return { installedVersion: finalVersion };
|
|
79
|
+
}
|
|
80
|
+
export function launchUiServer(options) {
|
|
81
|
+
const serverPath = path.join(UI_RUNTIME_DIR, 'node_modules', '@sidecar', 'ui', 'server.js');
|
|
82
|
+
if (!fs.existsSync(serverPath)) {
|
|
83
|
+
throw new SidecarError('Sidecar UI server entry was not found after install.');
|
|
84
|
+
}
|
|
85
|
+
const child = spawn(process.execPath, [serverPath, '--project', options.projectPath, '--port', String(options.port)], {
|
|
86
|
+
stdio: 'inherit',
|
|
87
|
+
env: { ...process.env },
|
|
88
|
+
});
|
|
89
|
+
const url = `http://localhost:${options.port}`;
|
|
90
|
+
if (options.openBrowser) {
|
|
91
|
+
openBrowser(url);
|
|
92
|
+
}
|
|
93
|
+
return { url, child };
|
|
94
|
+
}
|
|
95
|
+
export function openBrowser(url) {
|
|
96
|
+
const platform = process.platform;
|
|
97
|
+
if (platform === 'darwin') {
|
|
98
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (platform === 'win32') {
|
|
102
|
+
spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
106
|
+
}
|
|
@@ -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,29 @@ 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
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'ui',
|
|
48
|
+
description: 'Launch optional local Sidecar UI (lazy-installed on first run)',
|
|
49
|
+
json_output: false,
|
|
50
|
+
arguments: [],
|
|
51
|
+
options: ['--no-open', '--port <port>', '--install-only', '--project <path>', '--reinstall'],
|
|
52
|
+
},
|
|
23
53
|
{
|
|
24
54
|
name: 'capabilities',
|
|
25
55
|
description: 'Show machine-readable CLI manifest',
|
|
@@ -34,6 +64,40 @@ export function getCapabilitiesManifest(version) {
|
|
|
34
64
|
arguments: [],
|
|
35
65
|
options: ['--limit <n>', '--format text|markdown|json', '--json'],
|
|
36
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
|
+
},
|
|
37
101
|
{
|
|
38
102
|
name: 'summary',
|
|
39
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 {};
|