vibeflow-cli 0.1.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.
Files changed (3) hide show
  1. package/README.md +38 -0
  2. package/index.js +479 -0
  3. package/package.json +16 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # VibeFlow CLI
2
+
3
+ Terminal-first intent + context tracking.
4
+
5
+ ## Install (from this repo)
6
+ ```bash
7
+ cd cli
8
+ npm install -g .
9
+ ```
10
+
11
+ ## Versioning
12
+ Before publishing, bump the version:
13
+ ```bash
14
+ npm version patch
15
+ ```
16
+
17
+ ## Publish
18
+ ```bash
19
+ npm publish --access public
20
+ ```
21
+
22
+ ## Commands
23
+ ```bash
24
+ vf start [path]
25
+ vf intent "text"
26
+ vf park "note"
27
+ vf status
28
+ vf status --watch
29
+ vf timer
30
+ vf resume [path]
31
+ vf history [path]
32
+ vf end
33
+ vf receipt [id]
34
+ vf help
35
+ ```
36
+
37
+ ## Data location
38
+ Stored locally in your user config directory. Override with `VF_DATA_DIR`.
package/index.js ADDED
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const args = process.argv.slice(2);
8
+ const command = args[0];
9
+
10
+ const isTty = process.stdout.isTTY;
11
+ const color = (code, text) => (isTty ? `\u001b[${code}m${text}\u001b[0m` : text);
12
+ const c = {
13
+ dim: (t) => color("2", t),
14
+ cyan: (t) => color("36", t),
15
+ magenta: (t) => color("35", t),
16
+ blue: (t) => color("34", t),
17
+ green: (t) => color("32", t),
18
+ yellow: (t) => color("33", t),
19
+ gray: (t) => color("90", t)
20
+ };
21
+
22
+ const nowIso = () => new Date().toISOString();
23
+
24
+ const getDataDir = () => {
25
+ if (process.env.VF_DATA_DIR) {
26
+ return process.env.VF_DATA_DIR;
27
+ }
28
+ if (process.platform === "win32") {
29
+ return path.join(process.env.APPDATA || os.homedir(), "vibeflow-cli");
30
+ }
31
+ if (process.platform === "darwin") {
32
+ return path.join(os.homedir(), "Library", "Application Support", "vibeflow-cli");
33
+ }
34
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
35
+ return path.join(xdg, "vibeflow-cli");
36
+ };
37
+
38
+ const DATA_DIR = getDataDir();
39
+ const SESSIONS_FILE = path.join(DATA_DIR, "sessions.json");
40
+ const STATE_FILE = path.join(DATA_DIR, "state.json");
41
+
42
+ const ensureDir = () => {
43
+ fs.mkdirSync(DATA_DIR, { recursive: true });
44
+ };
45
+
46
+ const loadJson = (filePath, fallback) => {
47
+ try {
48
+ if (!fs.existsSync(filePath)) {
49
+ return fallback;
50
+ }
51
+ const raw = fs.readFileSync(filePath, "utf8");
52
+ return JSON.parse(raw);
53
+ } catch {
54
+ return fallback;
55
+ }
56
+ };
57
+
58
+ const saveJson = (filePath, value) => {
59
+ ensureDir();
60
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
61
+ };
62
+
63
+ const loadSessions = () => {
64
+ const data = loadJson(SESSIONS_FILE, []);
65
+ return Array.isArray(data) ? data : [];
66
+ };
67
+
68
+ const saveSessions = (sessions) => {
69
+ saveJson(SESSIONS_FILE, sessions);
70
+ };
71
+
72
+ const loadState = () => {
73
+ const data = loadJson(STATE_FILE, { activeByRepo: {} });
74
+ if (!data || typeof data !== "object") {
75
+ return { activeByRepo: {} };
76
+ }
77
+ return {
78
+ activeByRepo: data.activeByRepo || {}
79
+ };
80
+ };
81
+
82
+ const saveState = (state) => {
83
+ saveJson(STATE_FILE, state);
84
+ };
85
+
86
+ const findGitRoot = (startDir) => {
87
+ let current = path.resolve(startDir);
88
+ while (true) {
89
+ if (fs.existsSync(path.join(current, ".git"))) {
90
+ return current;
91
+ }
92
+ const parent = path.dirname(current);
93
+ if (parent === current) {
94
+ return null;
95
+ }
96
+ current = parent;
97
+ }
98
+ };
99
+
100
+ const getRepoKey = (cwd) => {
101
+ const root = findGitRoot(cwd);
102
+ return root || path.resolve(cwd);
103
+ };
104
+
105
+ const formatDuration = (ms) => {
106
+ const total = Math.max(0, Math.floor(ms / 1000));
107
+ const hours = Math.floor(total / 3600);
108
+ const minutes = Math.floor((total % 3600) / 60);
109
+ const seconds = total % 60;
110
+ if (hours > 0) {
111
+ return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
112
+ }
113
+ if (minutes > 0) {
114
+ return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
115
+ }
116
+ return `${seconds}s`;
117
+ };
118
+
119
+ const formatClock = (ms) => {
120
+ const total = Math.max(0, Math.floor(ms / 1000));
121
+ const hours = Math.floor(total / 3600);
122
+ const minutes = Math.floor((total % 3600) / 60);
123
+ const seconds = total % 60;
124
+ if (hours > 0) {
125
+ return `${hours.toString().padStart(2, "0")}:${minutes
126
+ .toString()
127
+ .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
128
+ }
129
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
130
+ };
131
+
132
+ const BIG_DIGITS = {
133
+ "0": [" ___ ", "| |", "| |", "| |", "|___|"],
134
+ "1": [" | ", " | ", " | ", " | ", " | "],
135
+ "2": [" ___ ", " |", " ___|", "| ", "|___ "],
136
+ "3": [" ___ ", " |", " ___|", " |", " ___|"],
137
+ "4": ["| |", "| |", "|___|", " |", " |"],
138
+ "5": [" ___ ", "| ", "|___ ", " |", " ___|"],
139
+ "6": [" ___ ", "| ", "|___ ", "| |", "|___|"],
140
+ "7": [" ___ ", " |", " |", " |", " |"],
141
+ "8": [" ___ ", "| |", "|___|", "| |", "|___|"],
142
+ "9": [" ___ ", "| |", "|___|", " |", " ___|"],
143
+ ":": [" ", " : ", " ", " : ", " "]
144
+ };
145
+
146
+ const renderBigTime = (text) => {
147
+ const lines = ["", "", "", "", ""];
148
+ for (const ch of text) {
149
+ const glyph = BIG_DIGITS[ch] || BIG_DIGITS["0"];
150
+ for (let i = 0; i < lines.length; i += 1) {
151
+ lines[i] += `${glyph[i]} `;
152
+ }
153
+ }
154
+ return lines.map((line) => line.trimEnd());
155
+ };
156
+
157
+ const formatTime = (iso) => {
158
+ try {
159
+ return new Date(iso).toLocaleString();
160
+ } catch {
161
+ return iso;
162
+ }
163
+ };
164
+
165
+ const findSessionById = (sessions, id) => sessions.find((s) => s.id === id);
166
+
167
+ const getLastSessionForRepo = (sessions, repoKey) => {
168
+ const filtered = sessions.filter((s) => s.repoKey === repoKey);
169
+ if (filtered.length === 0) {
170
+ return null;
171
+ }
172
+ filtered.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
173
+ return filtered[0];
174
+ };
175
+
176
+ const usage = () => {
177
+ console.log(`
178
+ VibeFlow CLI
179
+
180
+ Usage:
181
+ vf start [path] Start a session
182
+ vf intent "text" Set intent for current repo session
183
+ vf park "note" Park a thought
184
+ vf status Show current session status
185
+ vf status --watch Live session timer
186
+ vf timer Live session timer (alias)
187
+ vf resume [path] Show last session summary
188
+ vf history [path] List recent sessions
189
+ vf end End current session
190
+ vf receipt [id] Print receipt (defaults to last session)
191
+ vf help Show this help
192
+ `);
193
+ };
194
+
195
+ const printSessionSummary = (session, active = false) => {
196
+ const durationMs = session.endedAt
197
+ ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
198
+ : Date.now() - Date.parse(session.startedAt);
199
+ const state = active ? "Active" : session.endedAt ? "Ended" : "In progress";
200
+ console.log(`Repo: ${session.repoName || path.basename(session.repoKey)}`);
201
+ console.log(`Session ID: ${session.id}`);
202
+ console.log(`Status: ${state}`);
203
+ console.log(`Started: ${formatTime(session.startedAt)}`);
204
+ if (session.endedAt) {
205
+ console.log(`Ended: ${formatTime(session.endedAt)}`);
206
+ }
207
+ console.log(`Duration: ${formatDuration(durationMs)}`);
208
+ console.log(`Intent: ${session.intent?.text || "Not set"}`);
209
+ console.log(`Parked: ${session.parkedThoughts.length}`);
210
+ };
211
+
212
+ const ensureActiveSession = (sessions, state, repoKey) => {
213
+ const activeId = state.activeByRepo[repoKey];
214
+ if (!activeId) {
215
+ return null;
216
+ }
217
+ const session = findSessionById(sessions, activeId);
218
+ return session || null;
219
+ };
220
+
221
+ const startSession = (targetPath) => {
222
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
223
+ const repoKey = getRepoKey(cwd);
224
+ const repoName = path.basename(repoKey);
225
+ const sessions = loadSessions();
226
+ const state = loadState();
227
+
228
+ const existing = ensureActiveSession(sessions, state, repoKey);
229
+ if (existing && !existing.endedAt) {
230
+ console.log(c.yellow("Session already active:"));
231
+ printSessionSummary(existing, true);
232
+ return;
233
+ }
234
+
235
+ const session = {
236
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
237
+ repoKey,
238
+ repoName,
239
+ cwd,
240
+ startedAt: nowIso(),
241
+ parkedThoughts: []
242
+ };
243
+
244
+ sessions.push(session);
245
+ state.activeByRepo[repoKey] = session.id;
246
+ saveSessions(sessions);
247
+ saveState(state);
248
+
249
+ const banner = [
250
+ c.cyan("__ __ _ ______ _ "),
251
+ c.cyan("\\ \\ / /(_) | | ____| | "),
252
+ c.blue(" \\ \\ / / _| |__ ___ | |__ | | _____ __"),
253
+ c.blue(" \\ \\/ / | | '_ \\ / _ \\| __| | |/ _ \\ \\ /\\ / /"),
254
+ c.magenta(" \\ / | | |_) | __/| | | | (_) \\ V V / "),
255
+ c.magenta(" \\/ |_|_.__/ \\___||_| |_|\\___/ \\_/\\_/ ")
256
+ ];
257
+ console.log(banner.join("\n"));
258
+ console.log(c.dim("Terminal IDE - Intent - Context - Flow\n"));
259
+ console.log(c.green("Session started."));
260
+ printSessionSummary(session, true);
261
+ renderLiveTimer(session);
262
+ };
263
+
264
+ const setIntent = (text) => {
265
+ const value = text.trim();
266
+ if (!value) {
267
+ console.error("Intent text is required.");
268
+ return;
269
+ }
270
+ const repoKey = getRepoKey(process.cwd());
271
+ const sessions = loadSessions();
272
+ const state = loadState();
273
+ const session = ensureActiveSession(sessions, state, repoKey);
274
+ if (!session) {
275
+ console.error("No active session. Run `vf start` first.");
276
+ return;
277
+ }
278
+ session.intent = { text: value, setAt: nowIso() };
279
+ saveSessions(sessions);
280
+ console.log("Intent saved.");
281
+ };
282
+
283
+ const parkThought = (text) => {
284
+ const value = text.trim();
285
+ if (!value) {
286
+ console.error("Parked thought text is required.");
287
+ return;
288
+ }
289
+ const repoKey = getRepoKey(process.cwd());
290
+ const sessions = loadSessions();
291
+ const state = loadState();
292
+ const session = ensureActiveSession(sessions, state, repoKey);
293
+ if (!session) {
294
+ console.error("No active session. Run `vf start` first.");
295
+ return;
296
+ }
297
+ session.parkedThoughts.push({ text: value, createdAt: nowIso() });
298
+ saveSessions(sessions);
299
+ console.log("Thought parked.");
300
+ };
301
+
302
+ const renderLiveTimer = (session) => {
303
+ console.log(c.cyan("== VibeFlow Timer =="));
304
+ console.log(c.gray("Ctrl+C to stop\n"));
305
+ const startMs = Date.parse(session.startedAt);
306
+ let painted = false;
307
+ const interval = setInterval(() => {
308
+ const elapsed = formatClock(Date.now() - startMs);
309
+ const lines = renderBigTime(elapsed);
310
+ if (painted) {
311
+ process.stdout.write(`\u001b[${lines.length}A`);
312
+ }
313
+ const tinted = lines.map((line, idx) => {
314
+ if (idx < 2) return c.cyan(line);
315
+ if (idx < 4) return c.blue(line);
316
+ return c.magenta(line);
317
+ });
318
+ process.stdout.write(tinted.map((line) => `\u001b[2K${line}`).join("\n") + "\n");
319
+ painted = true;
320
+ }, 1000);
321
+ const cleanup = () => {
322
+ clearInterval(interval);
323
+ process.stdout.write("\n");
324
+ process.exit(0);
325
+ };
326
+ process.on("SIGINT", cleanup);
327
+ };
328
+
329
+ const status = (watch = false) => {
330
+ const repoKey = getRepoKey(process.cwd());
331
+ const sessions = loadSessions();
332
+ const state = loadState();
333
+ const session = ensureActiveSession(sessions, state, repoKey);
334
+ if (session) {
335
+ if (watch) {
336
+ renderLiveTimer(session);
337
+ return;
338
+ }
339
+ printSessionSummary(session, true);
340
+ return;
341
+ }
342
+ const last = getLastSessionForRepo(sessions, repoKey);
343
+ if (!last) {
344
+ console.log("No sessions found for this repo.");
345
+ return;
346
+ }
347
+ console.log("No active session. Last session:");
348
+ printSessionSummary(last, false);
349
+ };
350
+
351
+ const resume = (targetPath) => {
352
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
353
+ const repoKey = getRepoKey(cwd);
354
+ const sessions = loadSessions();
355
+ const state = loadState();
356
+ const active = ensureActiveSession(sessions, state, repoKey);
357
+ if (active) {
358
+ console.log("Active session:");
359
+ printSessionSummary(active, true);
360
+ return;
361
+ }
362
+ const last = getLastSessionForRepo(sessions, repoKey);
363
+ if (!last) {
364
+ console.log("No sessions found for this repo.");
365
+ return;
366
+ }
367
+ console.log("Last session:");
368
+ printSessionSummary(last, false);
369
+ };
370
+
371
+ const history = (targetPath) => {
372
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
373
+ const repoKey = getRepoKey(cwd);
374
+ const sessions = loadSessions().filter((s) => s.repoKey === repoKey);
375
+ if (sessions.length === 0) {
376
+ console.log("No sessions found for this repo.");
377
+ return;
378
+ }
379
+ sessions.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
380
+ console.log(`Recent sessions (${Math.min(5, sessions.length)}):`);
381
+ for (const session of sessions.slice(0, 5)) {
382
+ const durationMs = session.endedAt
383
+ ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
384
+ : Date.now() - Date.parse(session.startedAt);
385
+ console.log(
386
+ `- ${session.id} - ${formatTime(session.startedAt)} - ${formatDuration(durationMs)} - ${
387
+ session.intent?.text || "No intent"
388
+ }`
389
+ );
390
+ }
391
+ };
392
+
393
+ const timer = () => status(true);
394
+
395
+ const end = () => {
396
+ const repoKey = getRepoKey(process.cwd());
397
+ const sessions = loadSessions();
398
+ const state = loadState();
399
+ const session = ensureActiveSession(sessions, state, repoKey);
400
+ if (!session) {
401
+ console.error("No active session to end.");
402
+ return;
403
+ }
404
+ session.endedAt = nowIso();
405
+ saveSessions(sessions);
406
+ delete state.activeByRepo[repoKey];
407
+ saveState(state);
408
+ console.log("Session ended.");
409
+ printSessionSummary(session, false);
410
+ process.exit(0);
411
+ };
412
+
413
+ const receipt = (id) => {
414
+ const sessions = loadSessions();
415
+ const repoKey = getRepoKey(process.cwd());
416
+ const targetId = id || getLastSessionForRepo(sessions, repoKey)?.id;
417
+ if (!targetId) {
418
+ console.error("No session found to print a receipt.");
419
+ return;
420
+ }
421
+ const session = findSessionById(sessions, targetId);
422
+ if (!session) {
423
+ console.error("Session not found.");
424
+ return;
425
+ }
426
+ console.log("VibeFlow Session Receipt");
427
+ console.log(`Session ID: ${session.id}`);
428
+ console.log(`Repo: ${session.repoName || path.basename(session.repoKey)}`);
429
+ console.log(`Started: ${formatTime(session.startedAt)}`);
430
+ if (session.endedAt) {
431
+ console.log(`Ended: ${formatTime(session.endedAt)}`);
432
+ }
433
+ const durationMs = session.endedAt
434
+ ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
435
+ : Date.now() - Date.parse(session.startedAt);
436
+ console.log(`Duration: ${formatDuration(durationMs)}`);
437
+ console.log(`Intent: ${session.intent?.text || "Not set"}`);
438
+ console.log(`Parked: ${session.parkedThoughts.length}`);
439
+ };
440
+
441
+ switch (command) {
442
+ case "start":
443
+ startSession(args[1]);
444
+ break;
445
+ case "intent":
446
+ setIntent(args.slice(1).join(" "));
447
+ break;
448
+ case "park":
449
+ parkThought(args.slice(1).join(" "));
450
+ break;
451
+ case "status":
452
+ status(args.includes("--watch"));
453
+ break;
454
+ case "timer":
455
+ timer();
456
+ break;
457
+ case "resume":
458
+ resume(args[1]);
459
+ break;
460
+ case "history":
461
+ history(args[1]);
462
+ break;
463
+ case "end":
464
+ end();
465
+ break;
466
+ case "receipt":
467
+ receipt(args[1]);
468
+ break;
469
+ case "help":
470
+ case "-h":
471
+ case "--help":
472
+ case undefined:
473
+ usage();
474
+ break;
475
+ default:
476
+ console.error(`Unknown command: ${command}`);
477
+ usage();
478
+ process.exit(1);
479
+ }
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "vibeflow-cli",
3
+ "version": "0.1.1",
4
+ "description": "VibeFlow CLI — intent, context, and session memory for any repo.",
5
+ "author": "Dev Kuns (https://github.com/atobouh)",
6
+ "license": "MIT",
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "vf": "index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "README.md",
14
+ "package.json"
15
+ ]
16
+ }