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.
- package/index.js +625 -160
- 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, {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
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
|
|
188
|
-
vf
|
|
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
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
274
|
-
if (!session) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
293
|
-
if (!session) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
334
|
-
if (session) {
|
|
588
|
+
const active = ensureActiveSession(sessions, state, repoKey);
|
|
589
|
+
if (active.session) {
|
|
335
590
|
if (watch) {
|
|
336
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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 "
|
|
464
|
-
|
|
465
|
-
break;
|
|
466
|
-
case "
|
|
467
|
-
|
|
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.
|
|
4
|
-
"description": "VibeFlow CLI
|
|
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",
|