vibeflow-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.
Files changed (2) hide show
  1. package/index.js +295 -118
  2. package/package.json +4 -4
package/index.js CHANGED
@@ -35,9 +35,10 @@ const getDataDir = () => {
35
35
  return path.join(xdg, "vibeflow-cli");
36
36
  };
37
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");
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
+ const DEFAULT_IDLE_MINUTES = 240;
41
42
 
42
43
  const ensureDir = () => {
43
44
  fs.mkdirSync(DATA_DIR, { recursive: true });
@@ -69,15 +70,20 @@ const saveSessions = (sessions) => {
69
70
  saveJson(SESSIONS_FILE, sessions);
70
71
  };
71
72
 
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
- };
73
+ const loadState = () => {
74
+ const data = loadJson(STATE_FILE, {
75
+ activeByRepo: {},
76
+ idleTimeoutMinutes: DEFAULT_IDLE_MINUTES
77
+ });
78
+ if (!data || typeof data !== "object") {
79
+ return { activeByRepo: {}, idleTimeoutMinutes: DEFAULT_IDLE_MINUTES };
80
+ }
81
+ return {
82
+ activeByRepo: data.activeByRepo || {},
83
+ idleTimeoutMinutes:
84
+ typeof data.idleTimeoutMinutes === "number" ? data.idleTimeoutMinutes : DEFAULT_IDLE_MINUTES
85
+ };
86
+ };
81
87
 
82
88
  const saveState = (state) => {
83
89
  saveJson(STATE_FILE, state);
@@ -173,24 +179,26 @@ const getLastSessionForRepo = (sessions, repoKey) => {
173
179
  return filtered[0];
174
180
  };
175
181
 
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
182
+ const usage = () => {
183
+ console.log(`
184
+ VibeFlow CLI
185
+
186
+ Usage:
187
+ vf start [path] Start a session
188
+ vf intent "text" Set intent for current repo session
189
+ vf park "note" Park a thought
190
+ vf status Show current session status
185
191
  vf status --watch Live session timer
186
192
  vf timer Live session timer (alias)
187
- vf resume [path] Show last session summary
193
+ vf idle [value] Get/set idle timeout (minutes or Nh)
194
+ vf touch Refresh activity timestamp
195
+ vf resume [path] Show last session summary
188
196
  vf history [path] List recent sessions
189
197
  vf end End current session
190
198
  vf receipt [id] Print receipt (defaults to last session)
191
- vf help Show this help
192
- `);
193
- };
199
+ vf help Show this help
200
+ `);
201
+ };
194
202
 
195
203
  const printSessionSummary = (session, active = false) => {
196
204
  const durationMs = session.endedAt
@@ -206,40 +214,76 @@ const printSessionSummary = (session, active = false) => {
206
214
  }
207
215
  console.log(`Duration: ${formatDuration(durationMs)}`);
208
216
  console.log(`Intent: ${session.intent?.text || "Not set"}`);
209
- console.log(`Parked: ${session.parkedThoughts.length}`);
210
- };
217
+ console.log(`Parked: ${(session.parkedThoughts || []).length}`);
218
+ };
211
219
 
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
+ const getIdleTimeoutMs = (state) => {
221
+ const minutes =
222
+ typeof state.idleTimeoutMinutes === "number" ? state.idleTimeoutMinutes : DEFAULT_IDLE_MINUTES;
223
+ if (minutes <= 0) {
224
+ return 0;
225
+ }
226
+ return minutes * 60 * 1000;
227
+ };
228
+
229
+ const normalizeSession = (session) => {
230
+ if (!session.lastActivityAt) {
231
+ session.lastActivityAt = session.startedAt;
232
+ }
233
+ if (!session.parkedThoughts) {
234
+ session.parkedThoughts = [];
235
+ }
236
+ return session;
237
+ };
238
+
239
+ const ensureActiveSession = (sessions, state, repoKey) => {
240
+ const activeId = state.activeByRepo[repoKey];
241
+ if (!activeId) {
242
+ return { session: null, autoEnded: false, endedSession: null };
243
+ }
244
+ const session = findSessionById(sessions, activeId);
245
+ if (!session) {
246
+ return { session: null, autoEnded: false, endedSession: null };
247
+ }
248
+ normalizeSession(session);
249
+ const timeoutMs = getIdleTimeoutMs(state);
250
+ if (timeoutMs > 0) {
251
+ const last = Date.parse(session.lastActivityAt || session.startedAt);
252
+ if (Date.now() - last > timeoutMs) {
253
+ session.endedAt = nowIso();
254
+ session.endedReason = "idle";
255
+ saveSessions(sessions);
256
+ delete state.activeByRepo[repoKey];
257
+ saveState(state);
258
+ return { session: null, autoEnded: true, endedSession: session };
259
+ }
260
+ }
261
+ return { session, autoEnded: false, endedSession: null };
262
+ };
220
263
 
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();
264
+ const startSession = (targetPath, watch) => {
265
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
266
+ const repoKey = getRepoKey(cwd);
267
+ const repoName = path.basename(repoKey);
268
+ const sessions = loadSessions();
226
269
  const state = loadState();
227
270
 
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
- }
271
+ const existing = ensureActiveSession(sessions, state, repoKey);
272
+ if (existing.session && !existing.session.endedAt) {
273
+ console.log(c.yellow("Session already active:"));
274
+ printSessionSummary(existing.session, true);
275
+ return;
276
+ }
234
277
 
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
- };
278
+ const session = {
279
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
280
+ repoKey,
281
+ repoName,
282
+ cwd,
283
+ startedAt: nowIso(),
284
+ lastActivityAt: nowIso(),
285
+ parkedThoughts: []
286
+ };
243
287
 
244
288
  sessions.push(session);
245
289
  state.activeByRepo[repoKey] = session.id;
@@ -258,7 +302,11 @@ const startSession = (targetPath) => {
258
302
  console.log(c.dim("Terminal IDE - Intent - Context - Flow\n"));
259
303
  console.log(c.green("Session started."));
260
304
  printSessionSummary(session, true);
261
- renderLiveTimer(session);
305
+ if (watch) {
306
+ renderLiveTimer(session);
307
+ return;
308
+ }
309
+ console.log(c.gray("Live timer: run `vf timer` when you want a live view."));
262
310
  };
263
311
 
264
312
  const setIntent = (text) => {
@@ -267,18 +315,24 @@ const setIntent = (text) => {
267
315
  console.error("Intent text is required.");
268
316
  return;
269
317
  }
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
- };
318
+ const repoKey = getRepoKey(process.cwd());
319
+ const sessions = loadSessions();
320
+ const state = loadState();
321
+ const active = ensureActiveSession(sessions, state, repoKey);
322
+ if (!active.session) {
323
+ if (active.autoEnded) {
324
+ console.error("Session auto-ended due to inactivity. Run `vf start` to begin a new one.");
325
+ printSessionSummary(active.endedSession, false);
326
+ return;
327
+ }
328
+ console.error("No active session. Run `vf start` first.");
329
+ return;
330
+ }
331
+ active.session.intent = { text: value, setAt: nowIso() };
332
+ active.session.lastActivityAt = nowIso();
333
+ saveSessions(sessions);
334
+ console.log("Intent saved.");
335
+ };
282
336
 
283
337
  const parkThought = (text) => {
284
338
  const value = text.trim();
@@ -286,19 +340,35 @@ const parkThought = (text) => {
286
340
  console.error("Parked thought text is required.");
287
341
  return;
288
342
  }
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
- };
343
+ const repoKey = getRepoKey(process.cwd());
344
+ const sessions = loadSessions();
345
+ const state = loadState();
346
+ const active = ensureActiveSession(sessions, state, repoKey);
347
+ if (!active.session) {
348
+ if (active.autoEnded) {
349
+ console.error("Session auto-ended due to inactivity. Run `vf start` to begin a new one.");
350
+ printSessionSummary(active.endedSession, false);
351
+ return;
352
+ }
353
+ console.error("No active session. Run `vf start` first.");
354
+ return;
355
+ }
356
+ active.session.parkedThoughts.push({ text: value, createdAt: nowIso() });
357
+ active.session.lastActivityAt = nowIso();
358
+ saveSessions(sessions);
359
+ console.log("Thought parked.");
360
+ };
301
361
 
362
+ const printBigTimeOnce = (elapsed) => {
363
+ const lines = renderBigTime(elapsed);
364
+ const tinted = lines.map((line, idx) => {
365
+ if (idx < 2) return c.cyan(line);
366
+ if (idx < 4) return c.blue(line);
367
+ return c.magenta(line);
368
+ });
369
+ console.log(tinted.join("\n"));
370
+ };
371
+
302
372
  const renderLiveTimer = (session) => {
303
373
  console.log(c.cyan("== VibeFlow Timer =="));
304
374
  console.log(c.gray("Ctrl+C to stop\n"));
@@ -330,35 +400,47 @@ const status = (watch = false) => {
330
400
  const repoKey = getRepoKey(process.cwd());
331
401
  const sessions = loadSessions();
332
402
  const state = loadState();
333
- const session = ensureActiveSession(sessions, state, repoKey);
334
- if (session) {
403
+ const active = ensureActiveSession(sessions, state, repoKey);
404
+ if (active.session) {
335
405
  if (watch) {
336
- renderLiveTimer(session);
406
+ active.session.lastActivityAt = nowIso();
407
+ saveSessions(sessions);
408
+ renderLiveTimer(active.session);
337
409
  return;
338
410
  }
339
- printSessionSummary(session, true);
411
+ printSessionSummary(active.session, true);
340
412
  return;
341
413
  }
342
- const last = getLastSessionForRepo(sessions, repoKey);
343
- if (!last) {
344
- console.log("No sessions found for this repo.");
414
+ if (active.autoEnded && active.endedSession) {
415
+ console.log(c.yellow("Session auto-ended due to inactivity."));
416
+ printSessionSummary(active.endedSession, false);
417
+ return;
418
+ }
419
+ const last = getLastSessionForRepo(sessions, repoKey);
420
+ if (!last) {
421
+ console.log("No sessions found for this repo.");
345
422
  return;
346
423
  }
347
424
  console.log("No active session. Last session:");
348
425
  printSessionSummary(last, false);
349
426
  };
350
427
 
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
- }
428
+ const resume = (targetPath) => {
429
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
430
+ const repoKey = getRepoKey(cwd);
431
+ const sessions = loadSessions();
432
+ const state = loadState();
433
+ const active = ensureActiveSession(sessions, state, repoKey);
434
+ if (active.session) {
435
+ console.log("Active session:");
436
+ printSessionSummary(active.session, true);
437
+ return;
438
+ }
439
+ if (active.autoEnded && active.endedSession) {
440
+ console.log("Last session (auto-ended due to inactivity):");
441
+ printSessionSummary(active.endedSession, false);
442
+ return;
443
+ }
362
444
  const last = getLastSessionForRepo(sessions, repoKey);
363
445
  if (!last) {
364
446
  console.log("No sessions found for this repo.");
@@ -396,17 +478,22 @@ const end = () => {
396
478
  const repoKey = getRepoKey(process.cwd());
397
479
  const sessions = loadSessions();
398
480
  const state = loadState();
399
- const session = ensureActiveSession(sessions, state, repoKey);
400
- if (!session) {
481
+ const active = ensureActiveSession(sessions, state, repoKey);
482
+ if (!active.session) {
483
+ if (active.autoEnded) {
484
+ console.error("Session already auto-ended due to inactivity.");
485
+ printSessionSummary(active.endedSession, false);
486
+ return;
487
+ }
401
488
  console.error("No active session to end.");
402
489
  return;
403
490
  }
404
- session.endedAt = nowIso();
491
+ active.session.endedAt = nowIso();
405
492
  saveSessions(sessions);
406
493
  delete state.activeByRepo[repoKey];
407
494
  saveState(state);
408
495
  console.log("Session ended.");
409
- printSessionSummary(session, false);
496
+ printSessionSummary(active.session, false);
410
497
  process.exit(0);
411
498
  };
412
499
 
@@ -433,15 +520,99 @@ const receipt = (id) => {
433
520
  const durationMs = session.endedAt
434
521
  ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
435
522
  : 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
- };
523
+ console.log(`Duration: ${formatDuration(durationMs)}`);
524
+ console.log(`Intent: ${session.intent?.text || "Not set"}`);
525
+ console.log(`Parked: ${(session.parkedThoughts || []).length}`);
526
+ };
527
+
528
+ const parseIdleMinutes = (value) => {
529
+ if (!value) {
530
+ return null;
531
+ }
532
+ const cleaned = value.trim().toLowerCase();
533
+ if (cleaned === "off" || cleaned === "disable" || cleaned === "0") {
534
+ return 0;
535
+ }
536
+ const match = cleaned.match(/^(\d+)(h|m)?$/);
537
+ if (!match) {
538
+ return null;
539
+ }
540
+ const amount = Number(match[1]);
541
+ if (!Number.isFinite(amount) || amount <= 0) {
542
+ return null;
543
+ }
544
+ if (match[2] === "h") {
545
+ return amount * 60;
546
+ }
547
+ return amount;
548
+ };
549
+
550
+ const setIdle = (value) => {
551
+ const state = loadState();
552
+ if (!value) {
553
+ const current = getIdleTimeoutMs(state);
554
+ if (current === 0) {
555
+ console.log("Idle timeout: disabled");
556
+ return;
557
+ }
558
+ console.log(`Idle timeout: ${state.idleTimeoutMinutes} minutes`);
559
+ return;
560
+ }
561
+ const minutes = parseIdleMinutes(value);
562
+ if (minutes === null) {
563
+ console.error("Invalid idle timeout. Use minutes (e.g. 90) or hours (e.g. 4h) or 'off'.");
564
+ return;
565
+ }
566
+ state.idleTimeoutMinutes = minutes;
567
+ saveState(state);
568
+ if (minutes === 0) {
569
+ console.log("Idle timeout disabled.");
570
+ return;
571
+ }
572
+ console.log(`Idle timeout set to ${minutes} minutes.`);
573
+ };
574
+
575
+ const touch = () => {
576
+ const repoKey = getRepoKey(process.cwd());
577
+ const sessions = loadSessions();
578
+ const state = loadState();
579
+ const active = ensureActiveSession(sessions, state, repoKey);
580
+ if (!active.session) {
581
+ if (active.autoEnded) {
582
+ console.error("Session auto-ended due to inactivity.");
583
+ printSessionSummary(active.endedSession, false);
584
+ return;
585
+ }
586
+ console.error("No active session. Run `vf start` first.");
587
+ return;
588
+ }
589
+ active.session.lastActivityAt = nowIso();
590
+ saveSessions(sessions);
591
+ console.log("Session activity refreshed.");
592
+ };
440
593
 
441
- switch (command) {
442
- case "start":
443
- startSession(args[1]);
444
- break;
594
+ const parseStartArgs = (values) => {
595
+ let watch = false;
596
+ let targetPath = null;
597
+ for (const arg of values) {
598
+ if (arg === "--watch" || arg === "--timer") {
599
+ watch = true;
600
+ continue;
601
+ }
602
+ if (!targetPath) {
603
+ targetPath = arg;
604
+ }
605
+ }
606
+ return { watch, targetPath };
607
+ };
608
+
609
+ switch (command) {
610
+ case "start":
611
+ {
612
+ const parsed = parseStartArgs(args.slice(1));
613
+ startSession(parsed.targetPath, parsed.watch);
614
+ }
615
+ break;
445
616
  case "intent":
446
617
  setIntent(args.slice(1).join(" "));
447
618
  break;
@@ -454,15 +625,21 @@ switch (command) {
454
625
  case "timer":
455
626
  timer();
456
627
  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;
628
+ case "resume":
629
+ resume(args[1]);
630
+ break;
631
+ case "history":
632
+ history(args[1]);
633
+ break;
634
+ case "idle":
635
+ setIdle(args[1]);
636
+ break;
637
+ case "touch":
638
+ touch();
639
+ break;
640
+ case "end":
641
+ end();
642
+ break;
466
643
  case "receipt":
467
644
  receipt(args[1]);
468
645
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
- {
1
+ {
2
2
  "name": "vibeflow-cli",
3
- "version": "0.1.1",
4
- "description": "VibeFlow CLI intent, context, and session memory for any repo.",
3
+ "version": "0.1.2",
4
+ "description": "VibeFlow CLI - intent, context, and session memory for any repo.",
5
5
  "author": "Dev Kuns (https://github.com/atobouh)",
6
6
  "license": "MIT",
7
7
  "type": "commonjs",
@@ -13,4 +13,4 @@
13
13
  "README.md",
14
14
  "package.json"
15
15
  ]
16
- }
16
+ }