@fuzeelogik/myflo 1.0.0-rc.4
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/README.md +103 -0
- package/bin/flo.js +8 -0
- package/lib/activity.js +243 -0
- package/lib/agents-cmd.js +173 -0
- package/lib/agents-store.js +153 -0
- package/lib/completions.js +120 -0
- package/lib/doctor.js +101 -0
- package/lib/edit-cmd.js +136 -0
- package/lib/export.js +182 -0
- package/lib/guidance-audit.js +215 -0
- package/lib/help.js +49 -0
- package/lib/hook-cmd.js +129 -0
- package/lib/inbox-install.js +111 -0
- package/lib/inbox-registry.js +122 -0
- package/lib/inbox.js +320 -0
- package/lib/log-cmd.js +82 -0
- package/lib/main.js +97 -0
- package/lib/mcp-server.js +459 -0
- package/lib/memory-backend-agentdb.js +240 -0
- package/lib/memory-cmd.js +148 -0
- package/lib/memory-store.js +258 -0
- package/lib/messages.js +119 -0
- package/lib/migrate.js +88 -0
- package/lib/notes-cmd.js +110 -0
- package/lib/replace-ruflo.js +133 -0
- package/lib/sessions.js +82 -0
- package/lib/setup.js +93 -0
- package/lib/swarm.js +236 -0
- package/lib/tasks-cmd.js +160 -0
- package/lib/tasks-store.js +152 -0
- package/lib/terminal-attach.js +281 -0
- package/lib/transcribe-cmd.js +75 -0
- package/lib/transcribe.js +104 -0
- package/lib/transcripts.js +95 -0
- package/package.json +45 -0
- package/tests/smoke.sh +392 -0
package/lib/inbox.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { readdir, readFile, stat, rename, mkdir, appendFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join, basename, extname, resolve } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { transcribeAndSaveSidecar } from './transcribe.js';
|
|
7
|
+
import { addInbox, removeInbox, listInboxes } from './inbox-registry.js';
|
|
8
|
+
import { installLaunchAgent, uninstallLaunchAgent, listInstalledAgents } from './inbox-install.js';
|
|
9
|
+
import { storeEntry } from './memory-store.js';
|
|
10
|
+
import { writeFile, copyFile } from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
const SETTLE_MS = 2000;
|
|
13
|
+
const HANDLED_DIR = '.processed';
|
|
14
|
+
const FAILED_DIR = '.failed';
|
|
15
|
+
const LOG_FILE = 'inbox.log';
|
|
16
|
+
|
|
17
|
+
const AUDIO_EXTS = new Set(['.m4a', '.wav', '.mp3', '.aiff', '.flac']);
|
|
18
|
+
|
|
19
|
+
export async function inboxCommand(args) {
|
|
20
|
+
const [sub = 'help', ...rest] = args;
|
|
21
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
|
|
22
|
+
if (sub === 'watch') return watchCommand(rest);
|
|
23
|
+
if (sub === 'status') return statusCommand(rest);
|
|
24
|
+
if (sub === 'add') return addCommand(rest);
|
|
25
|
+
if (sub === 'list') return listCommand(rest);
|
|
26
|
+
if (sub === 'remove') return removeCommand(rest);
|
|
27
|
+
if (sub === 'install') return installCommand(rest);
|
|
28
|
+
if (sub === 'uninstall') return uninstallCommand(rest);
|
|
29
|
+
console.error(`flo inbox: unknown subcommand '${sub}'`);
|
|
30
|
+
console.error(`Available: watch, status, add, list, remove, install, uninstall, help`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printHelp() {
|
|
35
|
+
console.log(`flo inbox — folder watcher + registry + launchd installer
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
flo inbox watch <dir> [--once]
|
|
39
|
+
flo inbox status [--dir <dir>]
|
|
40
|
+
flo inbox add <dir> [--slug <name>] Register an inbox in ~/.flo/inboxes.json
|
|
41
|
+
flo inbox list [--json] List registered inboxes
|
|
42
|
+
flo inbox remove <slug> Remove from registry (does not delete files)
|
|
43
|
+
flo inbox install <slug> [--interval N] macOS: install a launchd agent that runs
|
|
44
|
+
'flo inbox watch <dir> --once' every N seconds
|
|
45
|
+
(default 30). Idempotent.
|
|
46
|
+
flo inbox uninstall <slug> macOS: remove the launchd agent
|
|
47
|
+
|
|
48
|
+
File handling (extension-based):
|
|
49
|
+
.md Parse frontmatter (to:/from:/subject:), log routing intent.
|
|
50
|
+
.m4a .wav .mp3 Local transcription via whisper/mlx-whisper, sidecar .txt written.
|
|
51
|
+
* Logged as unhandled.
|
|
52
|
+
|
|
53
|
+
After handling, files move to <dir>/${HANDLED_DIR}/ (success) or <dir>/${FAILED_DIR}/.
|
|
54
|
+
All activity appended to <dir>/${LOG_FILE}.
|
|
55
|
+
|
|
56
|
+
Settle detection: 2000ms quiet window before processing.
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function watchCommand(args) {
|
|
61
|
+
const parsed = parseArgs(args);
|
|
62
|
+
if (!parsed.dir) {
|
|
63
|
+
console.error(`flo inbox watch: missing <dir>`);
|
|
64
|
+
console.error(`Usage: flo inbox watch <dir>`);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
const dir = resolve(parsed.dir.replace(/^~/, homedir()));
|
|
68
|
+
if (!existsSync(dir)) {
|
|
69
|
+
await mkdir(dir, { recursive: true });
|
|
70
|
+
process.stderr.write(`flo inbox: created ${dir}\n`);
|
|
71
|
+
}
|
|
72
|
+
await mkdir(join(dir, HANDLED_DIR), { recursive: true });
|
|
73
|
+
await mkdir(join(dir, FAILED_DIR), { recursive: true });
|
|
74
|
+
|
|
75
|
+
if (parsed.once) {
|
|
76
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
77
|
+
for (const e of entries) {
|
|
78
|
+
if (e.isFile() && !e.name.startsWith('.') && e.name !== LOG_FILE) {
|
|
79
|
+
await handle(dir, e.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.stderr.write(`flo inbox: watching ${dir} (settle=${SETTLE_MS}ms). Ctrl-C to stop.\n`);
|
|
86
|
+
const pending = new Map();
|
|
87
|
+
watch(dir, async (event, filename) => {
|
|
88
|
+
if (!filename) return;
|
|
89
|
+
if (filename.startsWith('.')) return;
|
|
90
|
+
const path = join(dir, filename);
|
|
91
|
+
let st;
|
|
92
|
+
try { st = await stat(path); } catch { return; }
|
|
93
|
+
if (!st.isFile()) return;
|
|
94
|
+
if (pending.has(filename)) clearTimeout(pending.get(filename));
|
|
95
|
+
pending.set(filename, setTimeout(async () => {
|
|
96
|
+
pending.delete(filename);
|
|
97
|
+
await handle(dir, filename);
|
|
98
|
+
}, SETTLE_MS));
|
|
99
|
+
});
|
|
100
|
+
// Also scan existing files at startup
|
|
101
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (e.isFile() && !e.name.startsWith('.') && e.name !== LOG_FILE) {
|
|
104
|
+
await handle(dir, e.name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Stay alive
|
|
108
|
+
await new Promise(() => {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function statusCommand(args) {
|
|
112
|
+
const parsed = parseArgs(args);
|
|
113
|
+
const dir = resolve((parsed.dir || './inbox').replace(/^~/, homedir()));
|
|
114
|
+
if (!existsSync(dir)) {
|
|
115
|
+
console.log(`flo inbox: no inbox at ${dir}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const pending = (await readdir(dir, { withFileTypes: true })).filter(e => e.isFile() && !e.name.startsWith('.') && e.name !== LOG_FILE);
|
|
119
|
+
const processed = existsSync(join(dir, HANDLED_DIR))
|
|
120
|
+
? (await readdir(join(dir, HANDLED_DIR))).length : 0;
|
|
121
|
+
const failed = existsSync(join(dir, FAILED_DIR))
|
|
122
|
+
? (await readdir(join(dir, FAILED_DIR))).length : 0;
|
|
123
|
+
console.log(`flo inbox status: ${dir}`);
|
|
124
|
+
console.log(` pending: ${pending.length}`);
|
|
125
|
+
console.log(` processed: ${processed}`);
|
|
126
|
+
console.log(` failed: ${failed}`);
|
|
127
|
+
if (pending.length) {
|
|
128
|
+
console.log(`\nPending files:`);
|
|
129
|
+
for (const e of pending.slice(0, 20)) console.log(` - ${e.name}`);
|
|
130
|
+
if (pending.length > 20) console.log(` …and ${pending.length - 20} more`);
|
|
131
|
+
}
|
|
132
|
+
const logPath = join(dir, LOG_FILE);
|
|
133
|
+
if (existsSync(logPath)) {
|
|
134
|
+
const raw = await readFile(logPath, 'utf8');
|
|
135
|
+
const tail = raw.trim().split('\n').slice(-5);
|
|
136
|
+
console.log(`\nRecent log entries:`);
|
|
137
|
+
for (const line of tail) console.log(` ${line}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function handle(dir, filename) {
|
|
142
|
+
const path = join(dir, filename);
|
|
143
|
+
const ext = extname(filename).toLowerCase();
|
|
144
|
+
const log = async (msg) => appendFile(join(dir, LOG_FILE), `${new Date().toISOString()} ${msg}\n`).catch(() => {});
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
let action = '';
|
|
148
|
+
if (ext === '.md') {
|
|
149
|
+
const raw = await readFile(path, 'utf8');
|
|
150
|
+
const fm = parseFrontmatter(raw);
|
|
151
|
+
const body = raw.replace(/^---[\s\S]*?---\r?\n?/, '');
|
|
152
|
+
const bridge = await bridgeMarkdownMessage({ filename, fm, body });
|
|
153
|
+
action = `md to=${fm.to || '?'} from=${fm.from || '?'} subject=${fm.subject || '?'}`;
|
|
154
|
+
if (bridge.mailbox) action += ` mailbox=${bridge.mailbox}`;
|
|
155
|
+
if (bridge.memoryId) action += ` memory=${bridge.memoryId}`;
|
|
156
|
+
} else if (AUDIO_EXTS.has(ext)) {
|
|
157
|
+
const size = (await stat(path)).size;
|
|
158
|
+
process.stderr.write(`flo inbox: transcribing ${basename(path)} (${size}b)…\n`);
|
|
159
|
+
const result = await transcribeAndSaveSidecar(path);
|
|
160
|
+
if (result.text) {
|
|
161
|
+
action = `audio transcribed tool=${result.tool} chars=${result.text.length} sidecar=${basename(result.sidecar)}`;
|
|
162
|
+
} else {
|
|
163
|
+
action = `audio transcribe-failed tool=${result.tool || 'none'} error=${result.error}`;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
action = `unhandled ext=${ext} file=${basename(path)}`;
|
|
167
|
+
}
|
|
168
|
+
await log(`handled ${filename}: ${action}`);
|
|
169
|
+
await rename(path, join(dir, HANDLED_DIR, filename));
|
|
170
|
+
process.stderr.write(`flo inbox: ${filename} → ${action}\n`);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
await log(`failed ${filename}: ${err.message}`);
|
|
173
|
+
try { await rename(path, join(dir, FAILED_DIR, filename)); } catch {}
|
|
174
|
+
process.stderr.write(`flo inbox: ${filename} FAILED: ${err.message}\n`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseFrontmatter(content) {
|
|
179
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
180
|
+
if (!m) return {};
|
|
181
|
+
const fm = {};
|
|
182
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
183
|
+
const kv = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
|
|
184
|
+
if (!kv) continue;
|
|
185
|
+
fm[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
186
|
+
}
|
|
187
|
+
return fm;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function safeRecipient(s) {
|
|
191
|
+
return String(s || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 40);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function bridgeMarkdownMessage({ filename, fm, body }) {
|
|
195
|
+
const out = { mailbox: null, memoryId: null };
|
|
196
|
+
const recipient = safeRecipient(fm.to);
|
|
197
|
+
if (!recipient) return out; // No 'to:' → just log, no routing
|
|
198
|
+
try {
|
|
199
|
+
const { homedir } = await import('node:os');
|
|
200
|
+
const { join } = await import('node:path');
|
|
201
|
+
const { mkdir } = await import('node:fs/promises');
|
|
202
|
+
const floHome = process.env.FLO_HOME || join(homedir(), '.flo');
|
|
203
|
+
const mailboxDir = join(floHome, 'messages', recipient);
|
|
204
|
+
await mkdir(mailboxDir, { recursive: true });
|
|
205
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
206
|
+
const from = safeRecipient(fm.from) || 'unknown';
|
|
207
|
+
const mailboxPath = join(mailboxDir, `${ts}-${from}-${filename}`);
|
|
208
|
+
await writeFile(mailboxPath, `---\nto: ${recipient}\nfrom: ${from}\nsubject: ${fm.subject || ''}\nreceivedAt: ${new Date().toISOString()}\nsource: ${filename}\n---\n${body}`, 'utf8');
|
|
209
|
+
out.mailbox = mailboxPath;
|
|
210
|
+
} catch { /* non-critical */ }
|
|
211
|
+
try {
|
|
212
|
+
const entry = await storeEntry({
|
|
213
|
+
namespace: 'inbox',
|
|
214
|
+
key: `msg:${recipient}:${Date.now()}`,
|
|
215
|
+
value: body.trim(),
|
|
216
|
+
tags: [`to:${recipient}`, fm.from ? `from:${safeRecipient(fm.from)}` : 'from:unknown'],
|
|
217
|
+
metadata: { to: recipient, from: fm.from || null, subject: fm.subject || null, source: filename },
|
|
218
|
+
});
|
|
219
|
+
out.memoryId = entry.id;
|
|
220
|
+
} catch { /* non-critical */ }
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseArgs(args) {
|
|
225
|
+
const out = { dir: null, once: false };
|
|
226
|
+
for (let i = 0; i < args.length; i++) {
|
|
227
|
+
const a = args[i];
|
|
228
|
+
if (a === '--dir') out.dir = args[++i];
|
|
229
|
+
else if (a === '--once') out.once = true;
|
|
230
|
+
else if (!a.startsWith('--') && !out.dir) out.dir = a;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function addCommand(args) {
|
|
236
|
+
let dir = null, slug = null;
|
|
237
|
+
for (let i = 0; i < args.length; i++) {
|
|
238
|
+
if (args[i] === '--slug') slug = args[++i];
|
|
239
|
+
else if (!args[i].startsWith('--') && !dir) dir = args[i];
|
|
240
|
+
}
|
|
241
|
+
if (!dir) {
|
|
242
|
+
console.error(`flo inbox add: missing <dir>`);
|
|
243
|
+
console.error(`Usage: flo inbox add <dir> [--slug <name>]`);
|
|
244
|
+
process.exit(2);
|
|
245
|
+
}
|
|
246
|
+
const entry = await addInbox({ dir, slug });
|
|
247
|
+
console.log(`flo inbox add: registered '${entry.slug}' → ${entry.dir}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function listCommand(args) {
|
|
251
|
+
const json = args.includes('--json');
|
|
252
|
+
const entries = await listInboxes();
|
|
253
|
+
if (json) {
|
|
254
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!entries.length) {
|
|
258
|
+
console.log(`flo inbox: no inboxes registered. Use 'flo inbox add <dir>' to add one.`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
console.log(`slug pending processed failed dir`);
|
|
262
|
+
console.log(`-------------------- ------- --------- ------ ---`);
|
|
263
|
+
for (const e of entries) {
|
|
264
|
+
const slug = (e.slug || '?').padEnd(20).slice(0, 20);
|
|
265
|
+
const pending = String(e.pending ?? 0).padStart(7);
|
|
266
|
+
const processed = String(e.processed ?? 0).padStart(9);
|
|
267
|
+
const failed = String(e.failed ?? 0).padStart(6);
|
|
268
|
+
const exists = e.exists === false ? ' (missing)' : '';
|
|
269
|
+
console.log(`${slug} ${pending} ${processed} ${failed} ${e.dir}${exists}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function removeCommand(args) {
|
|
274
|
+
const slug = args[0];
|
|
275
|
+
if (!slug) {
|
|
276
|
+
console.error(`flo inbox remove: missing <slug>`);
|
|
277
|
+
console.error(`Usage: flo inbox remove <slug>`);
|
|
278
|
+
process.exit(2);
|
|
279
|
+
}
|
|
280
|
+
const removed = await removeInbox(slug);
|
|
281
|
+
if (removed) console.log(`flo inbox remove: removed '${slug}'`);
|
|
282
|
+
else { console.error(`flo inbox remove: no inbox with slug '${slug}'`); process.exit(1); }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function installCommand(args) {
|
|
286
|
+
if (args.includes('--list')) {
|
|
287
|
+
const list = await listInstalledAgents();
|
|
288
|
+
if (!list.length) { console.log(`flo inbox install: no agents installed`); return; }
|
|
289
|
+
for (const a of list) console.log(` ${a.slug}\t${a.plistPath}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let slug = null, interval = 30;
|
|
293
|
+
for (let i = 0; i < args.length; i++) {
|
|
294
|
+
if (args[i] === '--interval') interval = Number(args[++i]);
|
|
295
|
+
else if (!args[i].startsWith('--') && !slug) slug = args[i];
|
|
296
|
+
}
|
|
297
|
+
if (!slug) {
|
|
298
|
+
console.error(`flo inbox install: missing <slug>`);
|
|
299
|
+
console.error(`Usage: flo inbox install <slug> [--interval <seconds>]`);
|
|
300
|
+
console.error(` flo inbox install --list`);
|
|
301
|
+
process.exit(2);
|
|
302
|
+
}
|
|
303
|
+
const result = await installLaunchAgent({ slug, interval });
|
|
304
|
+
console.log(`flo inbox install: installed '${result.label}'`);
|
|
305
|
+
console.log(` plist: ${result.plistPath}`);
|
|
306
|
+
console.log(` watches: ${result.dir} every ${result.interval}s`);
|
|
307
|
+
console.log(` load with: launchctl bootstrap gui/$(id -u) ${result.plistPath}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function uninstallCommand(args) {
|
|
311
|
+
const slug = args[0];
|
|
312
|
+
if (!slug) {
|
|
313
|
+
console.error(`flo inbox uninstall: missing <slug>`);
|
|
314
|
+
console.error(`Usage: flo inbox uninstall <slug>`);
|
|
315
|
+
process.exit(2);
|
|
316
|
+
}
|
|
317
|
+
const result = await uninstallLaunchAgent({ slug });
|
|
318
|
+
if (result.removed) console.log(`flo inbox uninstall: removed ${result.plistPath}`);
|
|
319
|
+
else { console.error(`flo inbox uninstall: nothing to remove for '${slug}'`); process.exit(1); }
|
|
320
|
+
}
|
package/lib/log-cmd.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `flo log` — continuous activity tailer. Polls every N seconds and prints
|
|
2
|
+
// any events newer than the last seen timestamp.
|
|
3
|
+
|
|
4
|
+
// activity.js is on PR #35; if it's not present on this branch, `flo log`
|
|
5
|
+
// reports that gracefully instead of failing the whole CLI.
|
|
6
|
+
let collectActivity;
|
|
7
|
+
try {
|
|
8
|
+
({ collectActivity } = await import('./activity.js'));
|
|
9
|
+
} catch { /* activity module not available */ }
|
|
10
|
+
|
|
11
|
+
const POLL_MS = 2000;
|
|
12
|
+
const TYPE_GLYPH = {
|
|
13
|
+
task: 'T', note: 'N', memory: 'M', inbox: 'I',
|
|
14
|
+
transcript: 'A', terminal: '$', checkpoint: 'C',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function logCommand(args) {
|
|
18
|
+
const opts = parseFlags(args);
|
|
19
|
+
if (opts.help) return printHelp();
|
|
20
|
+
|
|
21
|
+
if (!collectActivity) {
|
|
22
|
+
console.error(`flo log: requires the activity module (not present on this branch).`);
|
|
23
|
+
console.error(`This will work after PR #35 ('flo activity') merges to main.`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const intervalMs = (opts.interval || POLL_MS / 1000) * 1000;
|
|
28
|
+
process.stderr.write(`flo log: tailing activity (every ${intervalMs}ms; Ctrl-C to stop)\n`);
|
|
29
|
+
|
|
30
|
+
// Seed with the most recent event so we don't dump history at startup
|
|
31
|
+
let cursor = Date.now();
|
|
32
|
+
const initial = await collectActivity({ sinceMs: cursor - 60_000 });
|
|
33
|
+
if (initial.length > 0) cursor = initial[0].ts;
|
|
34
|
+
|
|
35
|
+
let stopped = false;
|
|
36
|
+
process.on('SIGINT', () => { stopped = true; });
|
|
37
|
+
process.on('SIGTERM', () => { stopped = true; });
|
|
38
|
+
|
|
39
|
+
while (!stopped) {
|
|
40
|
+
try {
|
|
41
|
+
const events = await collectActivity({ sinceMs: cursor + 1, type: opts.type });
|
|
42
|
+
if (events.length > 0) {
|
|
43
|
+
// collectActivity returns newest-first; print oldest-first so tail reads naturally
|
|
44
|
+
const sorted = [...events].sort((a, b) => a.ts - b.ts);
|
|
45
|
+
for (const e of sorted) {
|
|
46
|
+
const time = e.timestamp.replace('T', ' ').slice(0, 19);
|
|
47
|
+
const glyph = TYPE_GLYPH[e.type] || '?';
|
|
48
|
+
console.log(`${time} ${glyph} ${e.type.padEnd(10)} ${e.snippet}`);
|
|
49
|
+
}
|
|
50
|
+
cursor = events[0].ts;
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
process.stderr.write(`flo log: error — ${err.message}\n`);
|
|
54
|
+
}
|
|
55
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printHelp() {
|
|
60
|
+
console.log(`flo log — tail activity events continuously
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
flo log [--type <type>] [--interval <seconds>]
|
|
64
|
+
|
|
65
|
+
--type <t> Filter (task / note / memory / inbox / transcript / terminal / checkpoint)
|
|
66
|
+
--interval <s> Poll interval (default: 2 seconds)
|
|
67
|
+
-h, --help Show this help
|
|
68
|
+
|
|
69
|
+
Press Ctrl-C to stop. Seeds with current time so old events don't dump on start.
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseFlags(args) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (let i = 0; i < args.length; i++) {
|
|
76
|
+
const a = args[i];
|
|
77
|
+
if (a === '--help' || a === '-h') out.help = true;
|
|
78
|
+
else if (a === '--type') out.type = args[++i];
|
|
79
|
+
else if (a === '--interval') out.interval = Number(args[++i]);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
package/lib/main.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { printHelp, printVersion } from './help.js';
|
|
2
|
+
import { guidanceAudit } from './guidance-audit.js';
|
|
3
|
+
import { migrate } from './migrate.js';
|
|
4
|
+
import { sessionsList } from './sessions.js';
|
|
5
|
+
import { inboxCommand } from './inbox.js';
|
|
6
|
+
import { doctor } from './doctor.js';
|
|
7
|
+
import { mcpServe } from './mcp-server.js';
|
|
8
|
+
import { transcribeCommand } from './transcribe-cmd.js';
|
|
9
|
+
import { swarmCommand } from './swarm.js';
|
|
10
|
+
import { agentsCommand } from './agents-cmd.js';
|
|
11
|
+
import { hookCommand } from './hook-cmd.js';
|
|
12
|
+
import { replaceCommand } from './replace-ruflo.js';
|
|
13
|
+
import { memoryCommand } from './memory-cmd.js';
|
|
14
|
+
import { messagesCommand } from './messages.js';
|
|
15
|
+
import { transcriptsCommand } from './transcripts.js';
|
|
16
|
+
import { tasksCommand } from './tasks-cmd.js';
|
|
17
|
+
import { notesCommand } from './notes-cmd.js';
|
|
18
|
+
import { activityCommand } from './activity.js';
|
|
19
|
+
import { sessionCommand } from './terminal-attach.js';
|
|
20
|
+
import { setupCommand } from './setup.js';
|
|
21
|
+
import { exportCommand, importCommand } from './export.js';
|
|
22
|
+
import { logCommand } from './log-cmd.js';
|
|
23
|
+
import { completionsCommand } from './completions.js';
|
|
24
|
+
import { editCommand } from './edit-cmd.js';
|
|
25
|
+
|
|
26
|
+
const COMMANDS = {
|
|
27
|
+
help: () => printHelp(),
|
|
28
|
+
'--help': () => printHelp(),
|
|
29
|
+
'-h': () => printHelp(),
|
|
30
|
+
version: () => printVersion(),
|
|
31
|
+
'--version': () => printVersion(),
|
|
32
|
+
'-v': () => printVersion(),
|
|
33
|
+
guidance: guidanceDispatch,
|
|
34
|
+
migrate: (args) => migrate(args),
|
|
35
|
+
sessions: sessionsDispatch,
|
|
36
|
+
inbox: inboxCommand,
|
|
37
|
+
doctor: (args) => doctor(args),
|
|
38
|
+
mcp: mcpDispatch,
|
|
39
|
+
transcribe: transcribeCommand,
|
|
40
|
+
swarm: swarmCommand,
|
|
41
|
+
memory: memoryCommand,
|
|
42
|
+
messages: messagesCommand,
|
|
43
|
+
transcripts: transcriptsCommand,
|
|
44
|
+
tasks: tasksCommand,
|
|
45
|
+
notes: notesCommand,
|
|
46
|
+
activity: activityCommand,
|
|
47
|
+
session: sessionCommand,
|
|
48
|
+
setup: setupCommand,
|
|
49
|
+
export: exportCommand,
|
|
50
|
+
import: importCommand,
|
|
51
|
+
log: logCommand,
|
|
52
|
+
completions: completionsCommand,
|
|
53
|
+
edit: editCommand,
|
|
54
|
+
agents: agentsCommand,
|
|
55
|
+
hook: hookCommand,
|
|
56
|
+
replace: replaceCommand,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export async function run(argv) {
|
|
60
|
+
const [cmd, ...rest] = argv;
|
|
61
|
+
if (!cmd) {
|
|
62
|
+
printHelp();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const handler = COMMANDS[cmd];
|
|
66
|
+
if (!handler) {
|
|
67
|
+
console.error(`flo: unknown command '${cmd}'`);
|
|
68
|
+
console.error(`Run 'flo help' to see available commands.`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
await handler(rest);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function guidanceDispatch(args) {
|
|
75
|
+
const [sub, ...rest] = args;
|
|
76
|
+
if (sub === 'audit') return guidanceAudit(rest);
|
|
77
|
+
console.error(`flo guidance: unknown subcommand '${sub || '(none)'}'`);
|
|
78
|
+
console.error(`Available: audit`);
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function sessionsDispatch(args) {
|
|
83
|
+
const [sub = 'list', ...rest] = args;
|
|
84
|
+
if (sub === 'list') return sessionsList(rest);
|
|
85
|
+
console.error(`flo sessions: unknown subcommand '${sub}'`);
|
|
86
|
+
console.error(`Available: list`);
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function mcpDispatch(args) {
|
|
91
|
+
const [sub] = args;
|
|
92
|
+
if (sub === 'start') return mcpServe();
|
|
93
|
+
console.error(`flo mcp: unknown subcommand '${sub || '(none)'}'`);
|
|
94
|
+
console.error(`Available: start`);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
97
|
+
|