vibeflow-cli 0.1.1 → 0.1.3

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 +625 -160
  2. package/package.json +2 -2
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,49 @@ 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
+ pendingTimeEchoesByRepo: {},
78
+ deliveredTimeEchoesByRepo: {},
79
+ repoParkedThoughtsByRepo: {}
80
+ });
81
+ if (!data || typeof data !== "object") {
82
+ return {
83
+ activeByRepo: {},
84
+ idleTimeoutMinutes: DEFAULT_IDLE_MINUTES,
85
+ pendingTimeEchoesByRepo: {},
86
+ deliveredTimeEchoesByRepo: {},
87
+ repoParkedThoughtsByRepo: {}
88
+ };
89
+ }
90
+ const pending = data.pendingTimeEchoesByRepo || {};
91
+ const delivered = data.deliveredTimeEchoesByRepo || {};
92
+ const parked = data.repoParkedThoughtsByRepo || {};
93
+ const normalizeEchoMap = (map) =>
94
+ Object.fromEntries(
95
+ Object.entries(map).map(([key, list]) => [
96
+ key,
97
+ Array.isArray(list) ? list.map(normalizeTimeEcho) : []
98
+ ])
99
+ );
100
+ const normalizeThoughtMap = (map) =>
101
+ Object.fromEntries(
102
+ Object.entries(map).map(([key, list]) => [
103
+ key,
104
+ Array.isArray(list) ? list.map(normalizeParkedThought) : []
105
+ ])
106
+ );
107
+ return {
108
+ activeByRepo: data.activeByRepo || {},
109
+ idleTimeoutMinutes:
110
+ typeof data.idleTimeoutMinutes === "number" ? data.idleTimeoutMinutes : DEFAULT_IDLE_MINUTES,
111
+ pendingTimeEchoesByRepo: normalizeEchoMap(pending),
112
+ deliveredTimeEchoesByRepo: normalizeEchoMap(delivered),
113
+ repoParkedThoughtsByRepo: normalizeThoughtMap(parked)
114
+ };
115
+ };
81
116
 
82
117
  const saveState = (state) => {
83
118
  saveJson(STATE_FILE, state);
@@ -154,15 +189,32 @@ const renderBigTime = (text) => {
154
189
  return lines.map((line) => line.trimEnd());
155
190
  };
156
191
 
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);
192
+ const formatTime = (iso) => {
193
+ try {
194
+ return new Date(iso).toLocaleString();
195
+ } catch {
196
+ return iso;
197
+ }
198
+ };
199
+
200
+ const createId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
201
+
202
+ const normalizeParkedThought = (thought) => ({
203
+ id: thought?.id || createId(),
204
+ text: thought?.text || "",
205
+ createdAt: thought?.createdAt || nowIso()
206
+ });
207
+
208
+ const normalizeTimeEcho = (echo) => ({
209
+ id: echo?.id || createId(),
210
+ text: echo?.text || "",
211
+ createdAt: echo?.createdAt || nowIso(),
212
+ deliverAt: echo?.deliverAt || nowIso(),
213
+ deliveredAt: echo?.deliveredAt,
214
+ sourceSessionId: echo?.sourceSessionId
215
+ });
216
+
217
+ const findSessionById = (sessions, id) => sessions.find((s) => s.id === id);
166
218
 
167
219
  const getLastSessionForRepo = (sessions, repoKey) => {
168
220
  const filtered = sessions.filter((s) => s.repoKey === repoKey);
@@ -173,73 +225,236 @@ const getLastSessionForRepo = (sessions, repoKey) => {
173
225
  return filtered[0];
174
226
  };
175
227
 
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
228
+ const usage = () => {
229
+ console.log(`
230
+ VibeFlow CLI
231
+
232
+ Usage:
233
+ vf start [path] Start a session
234
+ vf intent "text" Set intent for current repo session
235
+ vf park "note" Park a thought
236
+ vf echo "message" Queue a time echo for next session
237
+ vf echo list List queued/delivered echoes
238
+ vf echo park <id> Park a delivered echo
239
+ vf echo discard <id> Discard a delivered echo
240
+ vf status Show current session status
185
241
  vf status --watch Live session timer
186
242
  vf timer Live session timer (alias)
187
- vf resume [path] Show last session summary
188
- vf history [path] List recent sessions
243
+ vf idle [value] Get/set idle timeout (minutes or Nh)
244
+ vf touch Refresh activity timestamp
245
+ vf resume [path] Show last session summary
246
+ vf history [path] List recent sessions
189
247
  vf end End current session
190
248
  vf receipt [id] Print receipt (defaults to last session)
191
- vf help Show this help
192
- `);
193
- };
249
+ vf help Show this help
250
+ `);
251
+ };
194
252
 
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";
253
+ const printSessionSummary = (session, active = false) => {
254
+ const durationMs = session.endedAt
255
+ ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
256
+ : Date.now() - Date.parse(session.startedAt);
257
+ const state = active ? "Active" : session.endedAt ? "Ended" : "In progress";
200
258
  console.log(`Repo: ${session.repoName || path.basename(session.repoKey)}`);
201
259
  console.log(`Session ID: ${session.id}`);
202
260
  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
- };
261
+ console.log(`Started: ${formatTime(session.startedAt)}`);
262
+ if (session.endedAt) {
263
+ console.log(`Ended: ${formatTime(session.endedAt)}`);
264
+ }
265
+ console.log(`Duration: ${formatDuration(durationMs)}`);
266
+ console.log(`Intent: ${session.intent?.text || "Not set"}`);
267
+ console.log(`Parked: ${(session.parkedThoughts || []).length}`);
268
+ console.log(`Time Echoes: ${(session.timeEchoes || []).length}`);
269
+ };
211
270
 
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
- };
271
+ const getIdleTimeoutMs = (state) => {
272
+ const minutes =
273
+ typeof state.idleTimeoutMinutes === "number" ? state.idleTimeoutMinutes : DEFAULT_IDLE_MINUTES;
274
+ if (minutes <= 0) {
275
+ return 0;
276
+ }
277
+ return minutes * 60 * 1000;
278
+ };
279
+
280
+ const normalizeSession = (session) => {
281
+ if (!session.lastActivityAt) {
282
+ session.lastActivityAt = session.startedAt;
283
+ }
284
+ if (!session.parkedThoughts) {
285
+ session.parkedThoughts = [];
286
+ }
287
+ session.parkedThoughts = session.parkedThoughts.map(normalizeParkedThought);
288
+ if (!session.timeEchoes) {
289
+ session.timeEchoes = session.timeEcho ? [normalizeTimeEcho(session.timeEcho)] : [];
290
+ } else {
291
+ session.timeEchoes = session.timeEchoes.map(normalizeTimeEcho);
292
+ }
293
+ return session;
294
+ };
295
+
296
+ const ensureGitignore = (repoKey) => {
297
+ const gitDir = path.join(repoKey, ".git");
298
+ if (!fs.existsSync(gitDir)) {
299
+ return;
300
+ }
301
+ const gitignorePath = path.join(repoKey, ".gitignore");
302
+ const entry = ".vibeflow/";
303
+ if (!fs.existsSync(gitignorePath)) {
304
+ fs.writeFileSync(gitignorePath, `${entry}\n`, "utf8");
305
+ return;
306
+ }
307
+ const raw = fs.readFileSync(gitignorePath, "utf8");
308
+ const lines = raw.split(/\r?\n/);
309
+ if (lines.some((line) => line.trim() === entry || line.trim() === ".vibeflow")) {
310
+ return;
311
+ }
312
+ lines.push(entry);
313
+ fs.writeFileSync(gitignorePath, `${lines.join("\n").replace(/\n+$/, "")}\n`, "utf8");
314
+ };
315
+
316
+ const buildTracePayload = (session) => {
317
+ const durationMs = session.endedAt
318
+ ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
319
+ : Date.now() - Date.parse(session.startedAt);
320
+ return {
321
+ version: 1,
322
+ generatedAt: nowIso(),
323
+ sessionId: session.id,
324
+ repoKey: session.repoKey,
325
+ repoName: session.repoName,
326
+ startedAt: session.startedAt,
327
+ endedAt: session.endedAt || null,
328
+ durationMs,
329
+ intent: session.intent || null,
330
+ parkedThoughts: session.parkedThoughts || [],
331
+ timeEchoes: session.timeEchoes || []
332
+ };
333
+ };
334
+
335
+ const writeTrace = (session) => {
336
+ try {
337
+ const dir = path.join(session.repoKey, ".vibeflow");
338
+ fs.mkdirSync(dir, { recursive: true });
339
+ const payload = buildTracePayload(session);
340
+ fs.writeFileSync(path.join(dir, "trace.json"), JSON.stringify(payload, null, 2), "utf8");
341
+ ensureGitignore(session.repoKey);
342
+ } catch {
343
+ // ignore trace failures
344
+ }
345
+ };
346
+
347
+ const readTrace = (repoKey) => {
348
+ try {
349
+ const filePath = path.join(repoKey, ".vibeflow", "trace.json");
350
+ if (!fs.existsSync(filePath)) {
351
+ return null;
352
+ }
353
+ const raw = fs.readFileSync(filePath, "utf8");
354
+ return JSON.parse(raw);
355
+ } catch {
356
+ return null;
357
+ }
358
+ };
359
+
360
+ const queueTimeEchoes = (session, state) => {
361
+ if (!session.timeEchoes || session.timeEchoes.length === 0) {
362
+ return;
363
+ }
364
+ const existing = state.pendingTimeEchoesByRepo[session.repoKey] || [];
365
+ state.pendingTimeEchoesByRepo[session.repoKey] = existing.concat(session.timeEchoes);
366
+ };
367
+
368
+ const deliverPendingEchoes = (repoKey, state) => {
369
+ const pending = state.pendingTimeEchoesByRepo[repoKey] || [];
370
+ if (pending.length === 0) {
371
+ return [];
372
+ }
373
+ delete state.pendingTimeEchoesByRepo[repoKey];
374
+ state.deliveredTimeEchoesByRepo[repoKey] = pending;
375
+ return pending;
376
+ };
377
+
378
+ const clearDeliveredEchoes = (repoKey, state) => {
379
+ if (state.deliveredTimeEchoesByRepo[repoKey]) {
380
+ delete state.deliveredTimeEchoesByRepo[repoKey];
381
+ }
382
+ };
383
+
384
+ const showEchoes = (echoes) => {
385
+ if (!echoes || echoes.length === 0) {
386
+ return;
387
+ }
388
+ console.log(c.magenta("Time Echoes:"));
389
+ echoes.forEach((echo) => {
390
+ console.log(`- ${echo.id}: ${echo.text}`);
391
+ });
392
+ console.log(c.gray("Use `vf echo park <id>` to park or `vf echo discard <id>` to delete."));
393
+ };
394
+
395
+ const showTraceSummary = (trace) => {
396
+ if (!trace) {
397
+ return;
398
+ }
399
+ console.log(c.blue("VibeTrace found"));
400
+ console.log(`Last intent: ${trace.intent?.text || "Not set"}`);
401
+ if (trace.durationMs) {
402
+ console.log(`Last session duration: ${formatDuration(trace.durationMs)}`);
403
+ }
404
+ console.log(`Parked thoughts: ${(trace.parkedThoughts || []).length}`);
405
+ console.log(`Time echoes: ${(trace.timeEchoes || []).length}`);
406
+ };
407
+
408
+ const ensureActiveSession = (sessions, state, repoKey) => {
409
+ const activeId = state.activeByRepo[repoKey];
410
+ if (!activeId) {
411
+ return { session: null, autoEnded: false, endedSession: null };
412
+ }
413
+ const session = findSessionById(sessions, activeId);
414
+ if (!session) {
415
+ return { session: null, autoEnded: false, endedSession: null };
416
+ }
417
+ normalizeSession(session);
418
+ const timeoutMs = getIdleTimeoutMs(state);
419
+ if (timeoutMs > 0) {
420
+ const last = Date.parse(session.lastActivityAt || session.startedAt);
421
+ if (Date.now() - last > timeoutMs) {
422
+ session.endedAt = nowIso();
423
+ session.endedReason = "idle";
424
+ queueTimeEchoes(session, state);
425
+ writeTrace(session);
426
+ saveSessions(sessions);
427
+ delete state.activeByRepo[repoKey];
428
+ saveState(state);
429
+ return { session: null, autoEnded: true, endedSession: session };
430
+ }
431
+ }
432
+ return { session, autoEnded: false, endedSession: null };
433
+ };
220
434
 
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
- }
435
+ const startSession = (targetPath, watch) => {
436
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
437
+ const repoKey = getRepoKey(cwd);
438
+ const repoName = path.basename(repoKey);
439
+ const sessions = loadSessions();
440
+ const state = loadState();
441
+
442
+ const existing = ensureActiveSession(sessions, state, repoKey);
443
+ if (existing.session && !existing.session.endedAt) {
444
+ console.log(c.yellow("Session already active:"));
445
+ printSessionSummary(existing.session, true);
446
+ return;
447
+ }
234
448
 
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
- };
449
+ const session = {
450
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
451
+ repoKey,
452
+ repoName,
453
+ cwd,
454
+ startedAt: nowIso(),
455
+ lastActivityAt: nowIso(),
456
+ parkedThoughts: []
457
+ };
243
458
 
244
459
  sessions.push(session);
245
460
  state.activeByRepo[repoKey] = session.id;
@@ -258,7 +473,19 @@ const startSession = (targetPath) => {
258
473
  console.log(c.dim("Terminal IDE - Intent - Context - Flow\n"));
259
474
  console.log(c.green("Session started."));
260
475
  printSessionSummary(session, true);
261
- renderLiveTimer(session);
476
+ const trace = readTrace(repoKey);
477
+ showTraceSummary(trace);
478
+ clearDeliveredEchoes(repoKey, state);
479
+ const echoes = deliverPendingEchoes(repoKey, state);
480
+ if (echoes.length > 0) {
481
+ showEchoes(echoes);
482
+ saveState(state);
483
+ }
484
+ if (watch) {
485
+ renderLiveTimer(session);
486
+ return;
487
+ }
488
+ console.log(c.gray("Live timer: run `vf timer` when you want a live view."));
262
489
  };
263
490
 
264
491
  const setIntent = (text) => {
@@ -267,38 +494,66 @@ const setIntent = (text) => {
267
494
  console.error("Intent text is required.");
268
495
  return;
269
496
  }
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
- };
497
+ const repoKey = getRepoKey(process.cwd());
498
+ const sessions = loadSessions();
499
+ const state = loadState();
500
+ const active = ensureActiveSession(sessions, state, repoKey);
501
+ if (!active.session) {
502
+ if (active.autoEnded) {
503
+ console.error("Session auto-ended due to inactivity. Run `vf start` to begin a new one.");
504
+ printSessionSummary(active.endedSession, false);
505
+ return;
506
+ }
507
+ console.error("No active session. Run `vf start` first.");
508
+ return;
509
+ }
510
+ active.session.intent = { text: value, setAt: nowIso() };
511
+ active.session.lastActivityAt = nowIso();
512
+ saveSessions(sessions);
513
+ console.log("Intent saved.");
514
+ };
282
515
 
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
- };
516
+ const parkThought = (text) => {
517
+ const value = text.trim();
518
+ if (!value) {
519
+ console.error("Parked thought text is required.");
520
+ return;
521
+ }
522
+ const repoKey = getRepoKey(process.cwd());
523
+ const sessions = loadSessions();
524
+ const state = loadState();
525
+ const active = ensureActiveSession(sessions, state, repoKey);
526
+ if (!active.session) {
527
+ if (active.autoEnded) {
528
+ console.error("Session auto-ended due to inactivity. Run `vf start` to begin a new one.");
529
+ printSessionSummary(active.endedSession, false);
530
+ return;
531
+ }
532
+ console.error("No active session. Run `vf start` first.");
533
+ return;
534
+ }
535
+ const thought = normalizeParkedThought({ text: value, createdAt: nowIso() });
536
+ active.session.parkedThoughts.push(thought);
537
+ if (!state.repoParkedThoughtsByRepo[repoKey]) {
538
+ state.repoParkedThoughtsByRepo[repoKey] = [];
539
+ }
540
+ state.repoParkedThoughtsByRepo[repoKey].push(thought);
541
+ active.session.lastActivityAt = nowIso();
542
+ saveSessions(sessions);
543
+ saveState(state);
544
+ console.log("Thought parked.");
545
+ };
301
546
 
547
+ const printBigTimeOnce = (elapsed) => {
548
+ const lines = renderBigTime(elapsed);
549
+ const tinted = lines.map((line, idx) => {
550
+ if (idx < 2) return c.cyan(line);
551
+ if (idx < 4) return c.blue(line);
552
+ return c.magenta(line);
553
+ });
554
+ console.log(tinted.join("\n"));
555
+ };
556
+
302
557
  const renderLiveTimer = (session) => {
303
558
  console.log(c.cyan("== VibeFlow Timer =="));
304
559
  console.log(c.gray("Ctrl+C to stop\n"));
@@ -330,43 +585,61 @@ const status = (watch = false) => {
330
585
  const repoKey = getRepoKey(process.cwd());
331
586
  const sessions = loadSessions();
332
587
  const state = loadState();
333
- const session = ensureActiveSession(sessions, state, repoKey);
334
- if (session) {
588
+ const active = ensureActiveSession(sessions, state, repoKey);
589
+ if (active.session) {
335
590
  if (watch) {
336
- renderLiveTimer(session);
591
+ active.session.lastActivityAt = nowIso();
592
+ saveSessions(sessions);
593
+ renderLiveTimer(active.session);
337
594
  return;
338
595
  }
339
- printSessionSummary(session, true);
596
+ printSessionSummary(active.session, true);
340
597
  return;
341
598
  }
342
- const last = getLastSessionForRepo(sessions, repoKey);
343
- if (!last) {
344
- console.log("No sessions found for this repo.");
599
+ if (active.autoEnded && active.endedSession) {
600
+ console.log(c.yellow("Session auto-ended due to inactivity."));
601
+ printSessionSummary(active.endedSession, false);
602
+ return;
603
+ }
604
+ const last = getLastSessionForRepo(sessions, repoKey);
605
+ if (!last) {
606
+ console.log("No sessions found for this repo.");
345
607
  return;
346
608
  }
347
609
  console.log("No active session. Last session:");
348
610
  printSessionSummary(last, false);
349
611
  };
350
612
 
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
- };
613
+ const resume = (targetPath) => {
614
+ const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
615
+ const repoKey = getRepoKey(cwd);
616
+ const sessions = loadSessions();
617
+ const state = loadState();
618
+ const active = ensureActiveSession(sessions, state, repoKey);
619
+ if (active.session) {
620
+ console.log("Active session:");
621
+ printSessionSummary(active.session, true);
622
+ } else if (active.autoEnded && active.endedSession) {
623
+ console.log("Last session (auto-ended due to inactivity):");
624
+ printSessionSummary(active.endedSession, false);
625
+ } else {
626
+ const last = getLastSessionForRepo(sessions, repoKey);
627
+ if (!last) {
628
+ console.log("No sessions found for this repo.");
629
+ } else {
630
+ console.log("Last session:");
631
+ printSessionSummary(last, false);
632
+ }
633
+ }
634
+ const trace = readTrace(repoKey);
635
+ showTraceSummary(trace);
636
+ clearDeliveredEchoes(repoKey, state);
637
+ const echoes = deliverPendingEchoes(repoKey, state);
638
+ if (echoes.length > 0) {
639
+ showEchoes(echoes);
640
+ saveState(state);
641
+ }
642
+ };
370
643
 
371
644
  const history = (targetPath) => {
372
645
  const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
@@ -391,25 +664,33 @@ const history = (targetPath) => {
391
664
  };
392
665
 
393
666
  const timer = () => status(true);
394
-
667
+
395
668
  const end = () => {
396
669
  const repoKey = getRepoKey(process.cwd());
397
670
  const sessions = loadSessions();
398
671
  const state = loadState();
399
- const session = ensureActiveSession(sessions, state, repoKey);
400
- if (!session) {
672
+ const active = ensureActiveSession(sessions, state, repoKey);
673
+ if (!active.session) {
674
+ if (active.autoEnded) {
675
+ console.error("Session already auto-ended due to inactivity.");
676
+ printSessionSummary(active.endedSession, false);
677
+ return;
678
+ }
401
679
  console.error("No active session to end.");
402
680
  return;
403
681
  }
404
- session.endedAt = nowIso();
682
+ active.session.endedAt = nowIso();
683
+ active.session.endedReason = "ended";
684
+ queueTimeEchoes(active.session, state);
685
+ writeTrace(active.session);
405
686
  saveSessions(sessions);
406
687
  delete state.activeByRepo[repoKey];
407
688
  saveState(state);
408
689
  console.log("Session ended.");
409
- printSessionSummary(session, false);
690
+ printSessionSummary(active.session, false);
410
691
  process.exit(0);
411
692
  };
412
-
693
+
413
694
  const receipt = (id) => {
414
695
  const sessions = loadSessions();
415
696
  const repoKey = getRepoKey(process.cwd());
@@ -433,15 +714,190 @@ const receipt = (id) => {
433
714
  const durationMs = session.endedAt
434
715
  ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
435
716
  : 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
- };
717
+ console.log(`Duration: ${formatDuration(durationMs)}`);
718
+ console.log(`Intent: ${session.intent?.text || "Not set"}`);
719
+ console.log(`Parked: ${(session.parkedThoughts || []).length}`);
720
+ console.log(`Time Echoes: ${(session.timeEchoes || []).length}`);
721
+ };
722
+
723
+ const echoCommand = (argsList) => {
724
+ const repoKey = getRepoKey(process.cwd());
725
+ const sessions = loadSessions();
726
+ const state = loadState();
727
+ const sub = argsList[0];
728
+ if (!sub || sub === "list") {
729
+ const pending = state.pendingTimeEchoesByRepo[repoKey] || [];
730
+ const delivered = state.deliveredTimeEchoesByRepo[repoKey] || [];
731
+ if (pending.length === 0 && delivered.length === 0) {
732
+ console.log("No time echoes for this repo.");
733
+ return;
734
+ }
735
+ if (pending.length > 0) {
736
+ console.log(c.blue("Queued (next session):"));
737
+ pending.forEach((echo) => {
738
+ console.log(`- ${echo.id}: ${echo.text}`);
739
+ });
740
+ }
741
+ if (delivered.length > 0) {
742
+ console.log(c.magenta("Delivered (waiting for action):"));
743
+ delivered.forEach((echo) => {
744
+ console.log(`- ${echo.id}: ${echo.text}`);
745
+ });
746
+ console.log(c.gray("Use `vf echo park <id>` or `vf echo discard <id>`."));
747
+ }
748
+ return;
749
+ }
750
+ if (sub === "park" || sub === "discard") {
751
+ const id = argsList[1];
752
+ if (!id) {
753
+ console.error("Echo id is required.");
754
+ return;
755
+ }
756
+ const delivered = state.deliveredTimeEchoesByRepo[repoKey] || [];
757
+ const target = delivered.find((echo) => echo.id === id);
758
+ if (!target) {
759
+ console.error("Echo not found or already handled.");
760
+ return;
761
+ }
762
+ state.deliveredTimeEchoesByRepo[repoKey] = delivered.filter((echo) => echo.id !== id);
763
+ if (sub === "park") {
764
+ if (!state.repoParkedThoughtsByRepo[repoKey]) {
765
+ state.repoParkedThoughtsByRepo[repoKey] = [];
766
+ }
767
+ const thought = normalizeParkedThought({ text: target.text, createdAt: nowIso() });
768
+ state.repoParkedThoughtsByRepo[repoKey].push(thought);
769
+ const active = ensureActiveSession(sessions, state, repoKey);
770
+ if (active.session) {
771
+ active.session.parkedThoughts.push(thought);
772
+ active.session.lastActivityAt = nowIso();
773
+ saveSessions(sessions);
774
+ }
775
+ saveState(state);
776
+ console.log("Echo parked as a thought.");
777
+ return;
778
+ }
779
+ saveState(state);
780
+ console.log("Echo discarded.");
781
+ return;
782
+ }
783
+ const text = argsList.join(" ").trim();
784
+ if (!text) {
785
+ console.error("Echo message is required.");
786
+ return;
787
+ }
788
+ const active = ensureActiveSession(sessions, state, repoKey);
789
+ if (!active.session) {
790
+ if (active.autoEnded) {
791
+ console.error("Session auto-ended due to inactivity. Run `vf start` to begin a new one.");
792
+ printSessionSummary(active.endedSession, false);
793
+ return;
794
+ }
795
+ console.error("No active session. Run `vf start` first.");
796
+ return;
797
+ }
798
+ const echo = normalizeTimeEcho({
799
+ text,
800
+ createdAt: nowIso(),
801
+ deliverAt: nowIso(),
802
+ sourceSessionId: active.session.id
803
+ });
804
+ if (!active.session.timeEchoes) {
805
+ active.session.timeEchoes = [];
806
+ }
807
+ active.session.timeEchoes.push(echo);
808
+ active.session.lastActivityAt = nowIso();
809
+ saveSessions(sessions);
810
+ console.log("Time Echo queued for next session.");
811
+ };
812
+
813
+ const parseIdleMinutes = (value) => {
814
+ if (!value) {
815
+ return null;
816
+ }
817
+ const cleaned = value.trim().toLowerCase();
818
+ if (cleaned === "off" || cleaned === "disable" || cleaned === "0") {
819
+ return 0;
820
+ }
821
+ const match = cleaned.match(/^(\d+)(h|m)?$/);
822
+ if (!match) {
823
+ return null;
824
+ }
825
+ const amount = Number(match[1]);
826
+ if (!Number.isFinite(amount) || amount <= 0) {
827
+ return null;
828
+ }
829
+ if (match[2] === "h") {
830
+ return amount * 60;
831
+ }
832
+ return amount;
833
+ };
834
+
835
+ const setIdle = (value) => {
836
+ const state = loadState();
837
+ if (!value) {
838
+ const current = getIdleTimeoutMs(state);
839
+ if (current === 0) {
840
+ console.log("Idle timeout: disabled");
841
+ return;
842
+ }
843
+ console.log(`Idle timeout: ${state.idleTimeoutMinutes} minutes`);
844
+ return;
845
+ }
846
+ const minutes = parseIdleMinutes(value);
847
+ if (minutes === null) {
848
+ console.error("Invalid idle timeout. Use minutes (e.g. 90) or hours (e.g. 4h) or 'off'.");
849
+ return;
850
+ }
851
+ state.idleTimeoutMinutes = minutes;
852
+ saveState(state);
853
+ if (minutes === 0) {
854
+ console.log("Idle timeout disabled.");
855
+ return;
856
+ }
857
+ console.log(`Idle timeout set to ${minutes} minutes.`);
858
+ };
859
+
860
+ const touch = () => {
861
+ const repoKey = getRepoKey(process.cwd());
862
+ const sessions = loadSessions();
863
+ const state = loadState();
864
+ const active = ensureActiveSession(sessions, state, repoKey);
865
+ if (!active.session) {
866
+ if (active.autoEnded) {
867
+ console.error("Session auto-ended due to inactivity.");
868
+ printSessionSummary(active.endedSession, false);
869
+ return;
870
+ }
871
+ console.error("No active session. Run `vf start` first.");
872
+ return;
873
+ }
874
+ active.session.lastActivityAt = nowIso();
875
+ saveSessions(sessions);
876
+ console.log("Session activity refreshed.");
877
+ };
440
878
 
441
- switch (command) {
442
- case "start":
443
- startSession(args[1]);
444
- break;
879
+ const parseStartArgs = (values) => {
880
+ let watch = false;
881
+ let targetPath = null;
882
+ for (const arg of values) {
883
+ if (arg === "--watch" || arg === "--timer") {
884
+ watch = true;
885
+ continue;
886
+ }
887
+ if (!targetPath) {
888
+ targetPath = arg;
889
+ }
890
+ }
891
+ return { watch, targetPath };
892
+ };
893
+
894
+ switch (command) {
895
+ case "start":
896
+ {
897
+ const parsed = parseStartArgs(args.slice(1));
898
+ startSession(parsed.targetPath, parsed.watch);
899
+ }
900
+ break;
445
901
  case "intent":
446
902
  setIntent(args.slice(1).join(" "));
447
903
  break;
@@ -454,18 +910,27 @@ switch (command) {
454
910
  case "timer":
455
911
  timer();
456
912
  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;
913
+ case "resume":
914
+ resume(args[1]);
915
+ break;
916
+ case "history":
917
+ history(args[1]);
918
+ break;
919
+ case "idle":
920
+ setIdle(args[1]);
921
+ break;
922
+ case "touch":
923
+ touch();
924
+ break;
925
+ case "end":
926
+ end();
927
+ break;
928
+ case "receipt":
929
+ receipt(args[1]);
930
+ break;
931
+ case "echo":
932
+ echoCommand(args.slice(1));
933
+ break;
469
934
  case "help":
470
935
  case "-h":
471
936
  case "--help":
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.3",
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",