@vale-dinap/lucidly 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lucidly start the viewer; opens the latest session of this project
4
+ * lucidly <session-id|file> open a specific Claude Code session (id prefix or .jsonl path)
5
+ * agent | lucidly view a piped stream (Claude stream-json or plain text)
6
+ * lucidly --load run.lucidly.json
7
+ * lucidly list [--all] list recent sessions
8
+ * lucidly outline <id|file> print a text outline of a session
9
+ * lucidly actions <id|file> list every action in order (commands, edits, reads...)
10
+ * lucidly commands <id|file> list executed shell commands
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ import { readFile } from 'node:fs/promises';
14
+ import { resolve } from 'node:path';
15
+ import { Engine } from "./core/engine.js";
16
+ import { actionList, outline } from "./outline.js";
17
+ import { LucidlyServer } from "./server/server.js";
18
+ import { RunSession } from "./server/session.js";
19
+ import { ClaudeSessionSource, findSession, listSessions } from "./sources/claude-sessions.js";
20
+ import { StreamSource } from "./sources/stdin.js";
21
+ function parseArgs(argv) {
22
+ const a = {
23
+ command: 'serve',
24
+ port: Number(process.env.LUCIDLY_PORT ?? 4545),
25
+ open: true,
26
+ all: false,
27
+ cwd: process.cwd(),
28
+ format: 'auto',
29
+ stdin: !process.stdin.isTTY,
30
+ activities: false,
31
+ kinds: [],
32
+ errors: false,
33
+ full: false,
34
+ };
35
+ const rest = [];
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const x = argv[i];
38
+ const next = () => {
39
+ const v = argv[++i];
40
+ if (v === undefined)
41
+ throw new Error(`${x} needs a value`);
42
+ return v;
43
+ };
44
+ if (x === '--port' || x === '-p')
45
+ a.port = Number(next());
46
+ else if (x === '--no-open')
47
+ a.open = false;
48
+ else if (x === '--all')
49
+ a.all = true;
50
+ else if (x === '--load')
51
+ a.load = next();
52
+ else if (x === '--cwd')
53
+ a.cwd = resolve(next());
54
+ else if (x === '--text')
55
+ a.format = 'text';
56
+ else if (x === '--json')
57
+ a.format = 'json';
58
+ else if (x === '--no-stdin')
59
+ a.stdin = false;
60
+ else if (x === '--goals')
61
+ a.goals = Number(next());
62
+ else if (x === '--activities')
63
+ a.activities = true;
64
+ else if (x === '--kind')
65
+ a.kinds.push(...next().split(','));
66
+ else if (x === '--errors')
67
+ a.errors = true;
68
+ else if (x === '--full')
69
+ a.full = true;
70
+ else if (x === '--grep')
71
+ a.grep = next();
72
+ else if (x === '--help' || x === '-h')
73
+ a.command = 'help';
74
+ else if (x.startsWith('-'))
75
+ throw new Error(`Unknown option ${x}`);
76
+ else
77
+ rest.push(x);
78
+ }
79
+ if (['list', 'outline', 'actions', 'commands', 'help'].includes(rest[0]))
80
+ a.command = rest.shift();
81
+ a.target = rest[0];
82
+ return a;
83
+ }
84
+ const HELP = `Lucidly — see what your AI agent is doing, at a glance.
85
+
86
+ Usage:
87
+ lucidly [session] Start the viewer (default: latest session of this folder)
88
+ <agent> | lucidly View a piped stream (Claude stream-json or plain text)
89
+ lucidly --load <file> Open a saved .lucidly.json run
90
+ lucidly list [--all] List recent Claude Code sessions
91
+ lucidly outline <session> Print a text outline of a session
92
+ lucidly actions <session> List every action in order (for debugging)
93
+ lucidly commands <session> List the shell commands that were executed
94
+
95
+ Options:
96
+ -p, --port <n> Port (default 4545, or next free)
97
+ --no-open Don't open the browser
98
+ --cwd <dir> Project folder whose sessions to show (default: current folder)
99
+ --all List sessions from all projects
100
+ --text | --json Force the format of piped input
101
+ --goals <n> outline: only the last n goals
102
+ --activities outline: include individual actions
103
+ --kind <k,...> actions: only these kinds (command, edit, write, read, search, web, agent, ask, other)
104
+ --errors actions: only failed ones
105
+ --grep <text> actions: only those mentioning text
106
+ --full actions: print full multi-line commands/inputs
107
+ `;
108
+ function openBrowser(url) {
109
+ const cmd = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
110
+ const args = process.platform === 'win32' ? ['/c', 'start', '""', url] : [url];
111
+ try {
112
+ spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).unref();
113
+ }
114
+ catch {
115
+ /* the URL is printed anyway */
116
+ }
117
+ }
118
+ function ago(ms) {
119
+ const s = Math.round((Date.now() - ms) / 1000);
120
+ if (s < 60)
121
+ return `${s}s ago`;
122
+ if (s < 3600)
123
+ return `${Math.round(s / 60)}m ago`;
124
+ if (s < 86400)
125
+ return `${Math.round(s / 3600)}h ago`;
126
+ return `${Math.round(s / 86400)}d ago`;
127
+ }
128
+ async function resolveSession(target) {
129
+ const path = target.endsWith('.jsonl') ? resolve(target) : await findSession(target);
130
+ if (!path)
131
+ throw new Error(`No Claude Code session matches "${target}"`);
132
+ return path;
133
+ }
134
+ /** Build the full picture of a session or saved run, without serving it. */
135
+ async function loadEngine(target) {
136
+ if (target.endsWith('.lucidly.json'))
137
+ return RunSession.fromSave('x', JSON.parse(await readFile(target, 'utf8'))).engine;
138
+ const e = new Engine();
139
+ const src = new ClaudeSessionSource(await resolveSession(target), false);
140
+ await src.start((evs) => e.applyAll(evs));
141
+ return e;
142
+ }
143
+ async function main() {
144
+ const args = parseArgs(process.argv.slice(2));
145
+ if (args.command === 'help') {
146
+ process.stdout.write(HELP);
147
+ return;
148
+ }
149
+ if (args.command === 'list') {
150
+ const sessions = await listSessions({ cwd: args.all ? undefined : args.cwd, limit: 40 });
151
+ if (!sessions.length)
152
+ console.log(args.all ? 'No Claude Code sessions found.' : 'No sessions for this folder. Try: lucidly list --all');
153
+ for (const s of sessions) {
154
+ console.log(`${s.id.slice(0, 8)} ${ago(s.mtime).padEnd(8)} ${(s.title ?? s.firstPrompt ?? '').slice(0, 70)}${args.all ? ` [${s.cwd ?? s.project}]` : ''}`);
155
+ }
156
+ return;
157
+ }
158
+ if (args.command === 'actions' || args.command === 'commands') {
159
+ if (!args.target)
160
+ throw new Error(`${args.command} needs a session id or file`);
161
+ const engine = await loadEngine(args.target);
162
+ const kinds = args.command === 'commands' ? ['command'] : args.kinds;
163
+ console.log(actionList(engine, { kinds, errorsOnly: args.errors, goals: args.goals, full: args.full, grep: args.grep }));
164
+ return;
165
+ }
166
+ if (args.command === 'outline') {
167
+ if (!args.target)
168
+ throw new Error('outline needs a session id or file');
169
+ const e = new Engine();
170
+ if (args.target.endsWith('.lucidly.json')) {
171
+ const run = RunSession.fromSave('x', JSON.parse(await readFile(args.target, 'utf8')));
172
+ console.log(outline(run.engine, { goals: args.goals, activities: args.activities }));
173
+ return;
174
+ }
175
+ const src = new ClaudeSessionSource(await resolveSession(args.target), false);
176
+ await src.start((evs) => e.applyAll(evs));
177
+ console.log(outline(e, { goals: args.goals, activities: args.activities }));
178
+ return;
179
+ }
180
+ const server = new LucidlyServer({ port: args.port, cwd: args.cwd });
181
+ await server.listen();
182
+ let first;
183
+ if (args.load) {
184
+ const data = JSON.parse(await readFile(args.load, 'utf8'));
185
+ first = server.addRun((id) => RunSession.fromSave(id, data));
186
+ }
187
+ if (args.stdin && !args.target) {
188
+ first = await server.open(new StreamSource(process.stdin, 'stdin', args.format));
189
+ }
190
+ if (args.target) {
191
+ first = await server.open(new ClaudeSessionSource(await resolveSession(args.target)));
192
+ }
193
+ else if (!first) {
194
+ const [latest] = await listSessions({ cwd: args.cwd, limit: 1 });
195
+ if (latest)
196
+ first = await server.open(new ClaudeSessionSource(latest.path));
197
+ }
198
+ const url = server.url + (first ? `#run=${first.id}` : '');
199
+ console.error(`Lucidly is running at ${url}`);
200
+ if (first)
201
+ console.error(`Showing: ${first.engine.run.title ?? first.label}`);
202
+ console.error('Press Ctrl+C to stop.');
203
+ if (args.open)
204
+ openBrowser(url);
205
+ const stop = async () => {
206
+ await server.close();
207
+ process.exit(0);
208
+ };
209
+ process.on('SIGINT', stop);
210
+ process.on('SIGTERM', stop);
211
+ }
212
+ main().catch((e) => {
213
+ console.error(`lucidly: ${e?.message ?? e}`);
214
+ process.exit(1);
215
+ });