vibeflow-cli 0.1.2 → 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 +338 -50
  2. package/package.json +3 -3
package/index.js CHANGED
@@ -73,15 +73,44 @@ const saveSessions = (sessions) => {
73
73
  const loadState = () => {
74
74
  const data = loadJson(STATE_FILE, {
75
75
  activeByRepo: {},
76
- idleTimeoutMinutes: DEFAULT_IDLE_MINUTES
76
+ idleTimeoutMinutes: DEFAULT_IDLE_MINUTES,
77
+ pendingTimeEchoesByRepo: {},
78
+ deliveredTimeEchoesByRepo: {},
79
+ repoParkedThoughtsByRepo: {}
77
80
  });
78
81
  if (!data || typeof data !== "object") {
79
- return { activeByRepo: {}, idleTimeoutMinutes: DEFAULT_IDLE_MINUTES };
80
- }
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
+ );
81
107
  return {
82
108
  activeByRepo: data.activeByRepo || {},
83
109
  idleTimeoutMinutes:
84
- typeof data.idleTimeoutMinutes === "number" ? data.idleTimeoutMinutes : DEFAULT_IDLE_MINUTES
110
+ typeof data.idleTimeoutMinutes === "number" ? data.idleTimeoutMinutes : DEFAULT_IDLE_MINUTES,
111
+ pendingTimeEchoesByRepo: normalizeEchoMap(pending),
112
+ deliveredTimeEchoesByRepo: normalizeEchoMap(delivered),
113
+ repoParkedThoughtsByRepo: normalizeThoughtMap(parked)
85
114
  };
86
115
  };
87
116
 
@@ -160,15 +189,32 @@ const renderBigTime = (text) => {
160
189
  return lines.map((line) => line.trimEnd());
161
190
  };
162
191
 
163
- const formatTime = (iso) => {
164
- try {
165
- return new Date(iso).toLocaleString();
166
- } catch {
167
- return iso;
168
- }
169
- };
170
-
171
- 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);
172
218
 
173
219
  const getLastSessionForRepo = (sessions, repoKey) => {
174
220
  const filtered = sessions.filter((s) => s.repoKey === repoKey);
@@ -187,34 +233,39 @@ Usage:
187
233
  vf start [path] Start a session
188
234
  vf intent "text" Set intent for current repo session
189
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
190
240
  vf status Show current session status
191
241
  vf status --watch Live session timer
192
242
  vf timer Live session timer (alias)
193
243
  vf idle [value] Get/set idle timeout (minutes or Nh)
194
244
  vf touch Refresh activity timestamp
195
245
  vf resume [path] Show last session summary
196
- vf history [path] List recent sessions
246
+ vf history [path] List recent sessions
197
247
  vf end End current session
198
248
  vf receipt [id] Print receipt (defaults to last session)
199
249
  vf help Show this help
200
250
  `);
201
251
  };
202
252
 
203
- const printSessionSummary = (session, active = false) => {
204
- const durationMs = session.endedAt
205
- ? Date.parse(session.endedAt) - Date.parse(session.startedAt)
206
- : Date.now() - Date.parse(session.startedAt);
207
- 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";
208
258
  console.log(`Repo: ${session.repoName || path.basename(session.repoKey)}`);
209
259
  console.log(`Session ID: ${session.id}`);
210
260
  console.log(`Status: ${state}`);
211
- console.log(`Started: ${formatTime(session.startedAt)}`);
212
- if (session.endedAt) {
213
- console.log(`Ended: ${formatTime(session.endedAt)}`);
214
- }
215
- console.log(`Duration: ${formatDuration(durationMs)}`);
216
- console.log(`Intent: ${session.intent?.text || "Not set"}`);
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"}`);
217
267
  console.log(`Parked: ${(session.parkedThoughts || []).length}`);
268
+ console.log(`Time Echoes: ${(session.timeEchoes || []).length}`);
218
269
  };
219
270
 
220
271
  const getIdleTimeoutMs = (state) => {
@@ -233,9 +284,127 @@ const normalizeSession = (session) => {
233
284
  if (!session.parkedThoughts) {
234
285
  session.parkedThoughts = [];
235
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
+ }
236
293
  return session;
237
294
  };
238
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
+
239
408
  const ensureActiveSession = (sessions, state, repoKey) => {
240
409
  const activeId = state.activeByRepo[repoKey];
241
410
  if (!activeId) {
@@ -252,6 +421,8 @@ const ensureActiveSession = (sessions, state, repoKey) => {
252
421
  if (Date.now() - last > timeoutMs) {
253
422
  session.endedAt = nowIso();
254
423
  session.endedReason = "idle";
424
+ queueTimeEchoes(session, state);
425
+ writeTrace(session);
255
426
  saveSessions(sessions);
256
427
  delete state.activeByRepo[repoKey];
257
428
  saveState(state);
@@ -266,7 +437,7 @@ const startSession = (targetPath, watch) => {
266
437
  const repoKey = getRepoKey(cwd);
267
438
  const repoName = path.basename(repoKey);
268
439
  const sessions = loadSessions();
269
- const state = loadState();
440
+ const state = loadState();
270
441
 
271
442
  const existing = ensureActiveSession(sessions, state, repoKey);
272
443
  if (existing.session && !existing.session.endedAt) {
@@ -302,6 +473,14 @@ const startSession = (targetPath, watch) => {
302
473
  console.log(c.dim("Terminal IDE - Intent - Context - Flow\n"));
303
474
  console.log(c.green("Session started."));
304
475
  printSessionSummary(session, true);
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
+ }
305
484
  if (watch) {
306
485
  renderLiveTimer(session);
307
486
  return;
@@ -334,12 +513,12 @@ const setIntent = (text) => {
334
513
  console.log("Intent saved.");
335
514
  };
336
515
 
337
- const parkThought = (text) => {
338
- const value = text.trim();
339
- if (!value) {
340
- console.error("Parked thought text is required.");
341
- return;
342
- }
516
+ const parkThought = (text) => {
517
+ const value = text.trim();
518
+ if (!value) {
519
+ console.error("Parked thought text is required.");
520
+ return;
521
+ }
343
522
  const repoKey = getRepoKey(process.cwd());
344
523
  const sessions = loadSessions();
345
524
  const state = loadState();
@@ -353,9 +532,15 @@ const parkThought = (text) => {
353
532
  console.error("No active session. Run `vf start` first.");
354
533
  return;
355
534
  }
356
- active.session.parkedThoughts.push({ text: value, createdAt: nowIso() });
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);
357
541
  active.session.lastActivityAt = nowIso();
358
542
  saveSessions(sessions);
543
+ saveState(state);
359
544
  console.log("Thought parked.");
360
545
  };
361
546
 
@@ -434,21 +619,27 @@ const resume = (targetPath) => {
434
619
  if (active.session) {
435
620
  console.log("Active session:");
436
621
  printSessionSummary(active.session, true);
437
- return;
438
- }
439
- if (active.autoEnded && active.endedSession) {
622
+ } else if (active.autoEnded && active.endedSession) {
440
623
  console.log("Last session (auto-ended due to inactivity):");
441
624
  printSessionSummary(active.endedSession, false);
442
- return;
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
+ }
443
633
  }
444
- const last = getLastSessionForRepo(sessions, repoKey);
445
- if (!last) {
446
- console.log("No sessions found for this repo.");
447
- return;
448
- }
449
- console.log("Last session:");
450
- printSessionSummary(last, false);
451
- };
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
+ };
452
643
 
453
644
  const history = (targetPath) => {
454
645
  const cwd = targetPath ? path.resolve(targetPath) : process.cwd();
@@ -473,7 +664,7 @@ const history = (targetPath) => {
473
664
  };
474
665
 
475
666
  const timer = () => status(true);
476
-
667
+
477
668
  const end = () => {
478
669
  const repoKey = getRepoKey(process.cwd());
479
670
  const sessions = loadSessions();
@@ -489,6 +680,9 @@ const end = () => {
489
680
  return;
490
681
  }
491
682
  active.session.endedAt = nowIso();
683
+ active.session.endedReason = "ended";
684
+ queueTimeEchoes(active.session, state);
685
+ writeTrace(active.session);
492
686
  saveSessions(sessions);
493
687
  delete state.activeByRepo[repoKey];
494
688
  saveState(state);
@@ -496,7 +690,7 @@ const end = () => {
496
690
  printSessionSummary(active.session, false);
497
691
  process.exit(0);
498
692
  };
499
-
693
+
500
694
  const receipt = (id) => {
501
695
  const sessions = loadSessions();
502
696
  const repoKey = getRepoKey(process.cwd());
@@ -523,6 +717,97 @@ const receipt = (id) => {
523
717
  console.log(`Duration: ${formatDuration(durationMs)}`);
524
718
  console.log(`Intent: ${session.intent?.text || "Not set"}`);
525
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.");
526
811
  };
527
812
 
528
813
  const parseIdleMinutes = (value) => {
@@ -640,9 +925,12 @@ switch (command) {
640
925
  case "end":
641
926
  end();
642
927
  break;
643
- case "receipt":
644
- receipt(args[1]);
645
- break;
928
+ case "receipt":
929
+ receipt(args[1]);
930
+ break;
931
+ case "echo":
932
+ echoCommand(args.slice(1));
933
+ break;
646
934
  case "help":
647
935
  case "-h":
648
936
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "vibeflow-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
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",
@@ -13,4 +13,4 @@
13
13
  "README.md",
14
14
  "package.json"
15
15
  ]
16
- }
16
+ }