@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Audio transcription dispatcher.
|
|
2
|
+
// Detects available local tools at runtime and picks the best one.
|
|
3
|
+
// Preference order:
|
|
4
|
+
// 1. mlx-whisper (M-series Apple Silicon optimized; ~5x faster on M-chips)
|
|
5
|
+
// 2. whisper (OpenAI reference CLI; pip install openai-whisper)
|
|
6
|
+
// 3. whisper-cpp (cross-platform C impl)
|
|
7
|
+
// No cloud calls. Local-only, defensive posture.
|
|
8
|
+
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { mkdtemp, readFile, copyFile } from 'node:fs/promises';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { basename, join, dirname } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
|
|
18
|
+
const TOOLS = [
|
|
19
|
+
{
|
|
20
|
+
name: 'mlx-whisper',
|
|
21
|
+
binary: 'mlx-whisper',
|
|
22
|
+
args: (audioPath, outDir, model) => [audioPath, '--model', `mlx-community/whisper-${model}-mlx`, '--output-dir', outDir, '--output-format', 'txt'],
|
|
23
|
+
txtFilename: (audioPath) => basename(audioPath).replace(/\.[^.]+$/, '.txt'),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'whisper',
|
|
27
|
+
binary: 'whisper',
|
|
28
|
+
args: (audioPath, outDir, model) => [audioPath, '--model', model, '--output_dir', outDir, '--output_format', 'txt', '--verbose', 'False'],
|
|
29
|
+
txtFilename: (audioPath) => basename(audioPath).replace(/\.[^.]+$/, '.txt'),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'whisper-cpp',
|
|
33
|
+
binary: 'whisper-cpp',
|
|
34
|
+
args: (audioPath, outDir) => ['-f', audioPath, '-otxt', '-of', join(outDir, basename(audioPath).replace(/\.[^.]+$/, ''))],
|
|
35
|
+
txtFilename: (audioPath) => basename(audioPath).replace(/\.[^.]+$/, '.txt'),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect the first installed transcription tool.
|
|
41
|
+
* Returns the tool spec or null.
|
|
42
|
+
*/
|
|
43
|
+
export async function detectTool() {
|
|
44
|
+
for (const tool of TOOLS) {
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await execFileAsync('which', [tool.binary]);
|
|
47
|
+
if (stdout.trim()) return tool;
|
|
48
|
+
} catch { /* not installed */ }
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Transcribe an audio file. Returns { text, tool, error }.
|
|
55
|
+
* Never throws — failures are reported in the result so the inbox can log+continue.
|
|
56
|
+
*/
|
|
57
|
+
export async function transcribe(audioPath, opts = {}) {
|
|
58
|
+
const model = opts.model || process.env.FLO_WHISPER_MODEL || 'base';
|
|
59
|
+
const tool = opts.tool || await detectTool();
|
|
60
|
+
if (!tool) {
|
|
61
|
+
return {
|
|
62
|
+
text: null,
|
|
63
|
+
tool: null,
|
|
64
|
+
error: 'no transcription tool found; install one of: mlx-whisper, openai-whisper, whisper-cpp',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (!existsSync(audioPath)) {
|
|
68
|
+
return { text: null, tool: tool.name, error: `audio file not found: ${audioPath}` };
|
|
69
|
+
}
|
|
70
|
+
const outDir = await mkdtemp(join(tmpdir(), 'flo-transcribe-'));
|
|
71
|
+
try {
|
|
72
|
+
const args = tool.args(audioPath, outDir, model);
|
|
73
|
+
await execFileAsync(tool.binary, args, {
|
|
74
|
+
timeout: opts.timeoutMs || 5 * 60_000,
|
|
75
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
const txtPath = join(outDir, tool.txtFilename(audioPath));
|
|
78
|
+
if (!existsSync(txtPath)) {
|
|
79
|
+
return { text: null, tool: tool.name, error: `transcript file not produced at ${txtPath}` };
|
|
80
|
+
}
|
|
81
|
+
const text = await readFile(txtPath, 'utf8');
|
|
82
|
+
return { text: text.trim(), tool: tool.name, error: null };
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
text: null,
|
|
86
|
+
tool: tool.name,
|
|
87
|
+
error: err.message || String(err),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convenience: transcribe + save a sidecar .txt next to the original audio file.
|
|
94
|
+
*/
|
|
95
|
+
export async function transcribeAndSaveSidecar(audioPath, opts = {}) {
|
|
96
|
+
const result = await transcribe(audioPath, opts);
|
|
97
|
+
if (result.text) {
|
|
98
|
+
const sidecar = audioPath + '.txt';
|
|
99
|
+
const { writeFile } = await import('node:fs/promises');
|
|
100
|
+
await writeFile(sidecar, result.text + '\n', 'utf8');
|
|
101
|
+
result.sidecar = sidecar;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// `flo transcripts list` — surface sidecar .txt files produced by audio
|
|
2
|
+
// transcription (inbox watcher or standalone `flo transcribe --save`).
|
|
3
|
+
|
|
4
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { join, basename, extname } from 'node:path';
|
|
7
|
+
import { listInboxes } from './inbox-registry.js';
|
|
8
|
+
|
|
9
|
+
const AUDIO_EXTS = new Set(['.m4a', '.wav', '.mp3', '.aiff', '.flac']);
|
|
10
|
+
|
|
11
|
+
export async function transcriptsCommand(args) {
|
|
12
|
+
const [sub = 'list', ...rest] = args;
|
|
13
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
|
|
14
|
+
if (sub === 'list') return listCommand(rest);
|
|
15
|
+
console.error(`flo transcripts: unknown subcommand '${sub}'`);
|
|
16
|
+
console.error(`Available: list, help`);
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printHelp() {
|
|
21
|
+
console.log(`flo transcripts — list sidecar transcripts from registered inboxes
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
flo transcripts list [--json] [--limit N]
|
|
25
|
+
|
|
26
|
+
Scans every registered inbox's .processed/ folder for audio files whose
|
|
27
|
+
sidecar .txt was written next to them by the inbox watcher.
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function listCommand(args) {
|
|
32
|
+
let json = false, limit = 50;
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === '--json') json = true;
|
|
35
|
+
else if (args[i] === '--limit') limit = Number(args[++i]);
|
|
36
|
+
}
|
|
37
|
+
const transcripts = await collectTranscripts(limit);
|
|
38
|
+
if (json) { console.log(JSON.stringify(transcripts, null, 2)); return; }
|
|
39
|
+
if (!transcripts.length) {
|
|
40
|
+
console.log(`flo transcripts: none found. Audio drops in registered inboxes will produce transcripts here.`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(`when inbox file chars snippet`);
|
|
44
|
+
console.log(`------------------- ---------------- -------------------------------- ----- ----------------`);
|
|
45
|
+
for (const t of transcripts) {
|
|
46
|
+
const when = new Date(t.mtime).toISOString().slice(0, 19).replace('T', ' ');
|
|
47
|
+
const inbox = (t.inboxSlug || '?').padEnd(16).slice(0, 16);
|
|
48
|
+
const file = (t.audioFilename || '?').padEnd(32).slice(0, 32);
|
|
49
|
+
const chars = String(t.chars).padStart(5);
|
|
50
|
+
const snippet = (t.snippet || '').slice(0, 60);
|
|
51
|
+
console.log(`${when} ${inbox} ${file} ${chars} ${snippet}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function collectTranscripts(limit = 200) {
|
|
56
|
+
const inboxes = await listInboxes();
|
|
57
|
+
const transcripts = [];
|
|
58
|
+
for (const inbox of inboxes) {
|
|
59
|
+
if (!inbox.exists) continue;
|
|
60
|
+
const dirs = [join(inbox.dir, '.processed'), inbox.dir];
|
|
61
|
+
for (const dir of dirs) {
|
|
62
|
+
if (!existsSync(dir)) continue;
|
|
63
|
+
let entries;
|
|
64
|
+
try { entries = await readdir(dir); } catch { continue; }
|
|
65
|
+
for (const name of entries) {
|
|
66
|
+
const ext = extname(name).toLowerCase();
|
|
67
|
+
if (!AUDIO_EXTS.has(ext)) continue;
|
|
68
|
+
const audioPath = join(dir, name);
|
|
69
|
+
const sidecar = audioPath + '.txt';
|
|
70
|
+
if (!existsSync(sidecar)) continue;
|
|
71
|
+
try {
|
|
72
|
+
const [audioStat, txt] = await Promise.all([
|
|
73
|
+
stat(audioPath),
|
|
74
|
+
readFile(sidecar, 'utf8'),
|
|
75
|
+
]);
|
|
76
|
+
const sidecarStat = await stat(sidecar);
|
|
77
|
+
transcripts.push({
|
|
78
|
+
inboxSlug: inbox.slug,
|
|
79
|
+
inboxDir: inbox.dir,
|
|
80
|
+
audioPath,
|
|
81
|
+
audioFilename: basename(audioPath),
|
|
82
|
+
sidecarPath: sidecar,
|
|
83
|
+
audioBytes: audioStat.size,
|
|
84
|
+
mtime: sidecarStat.mtimeMs,
|
|
85
|
+
chars: txt.length,
|
|
86
|
+
snippet: txt.trim().slice(0, 160).replace(/\s+/g, ' '),
|
|
87
|
+
fullText: txt,
|
|
88
|
+
});
|
|
89
|
+
} catch { /* skip */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
transcripts.sort((a, b) => b.mtime - a.mtime);
|
|
94
|
+
return transcripts.slice(0, limit);
|
|
95
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fuzeelogik/myflo",
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
|
+
"description": "myflo — local-first developer workbench. CLI + MCP server + Next.js dashboard. Forked from ruflo for the runtime substrate; flo CLI on top is ours.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flo": "./bin/flo.js",
|
|
8
|
+
"myflo": "./bin/flo.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/**",
|
|
12
|
+
"lib/**",
|
|
13
|
+
"tests/**",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"optionalDependencies": {
|
|
18
|
+
"@myflo/memory": "workspace:*"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/therealsiege/myflo.git",
|
|
26
|
+
"directory": "apps/cli"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"myflo",
|
|
30
|
+
"flo",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"mcp",
|
|
33
|
+
"local-first",
|
|
34
|
+
"developer-tools",
|
|
35
|
+
"agentdb",
|
|
36
|
+
"memory",
|
|
37
|
+
"tasks",
|
|
38
|
+
"transcription"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"tag": "latest"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/tests/smoke.sh
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# End-to-end smoke test for the flo CLI.
|
|
3
|
+
# Exercises every command. Exits non-zero on first failure.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
8
|
+
FLO="node ${REPO_ROOT}/apps/cli/bin/flo.js"
|
|
9
|
+
TMP="$(mktemp -d)"
|
|
10
|
+
PASS=0
|
|
11
|
+
FAIL=0
|
|
12
|
+
|
|
13
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
14
|
+
|
|
15
|
+
check() {
|
|
16
|
+
local name="$1"; shift
|
|
17
|
+
if "$@" >/dev/null 2>&1; then
|
|
18
|
+
echo " PASS $name"
|
|
19
|
+
PASS=$((PASS+1))
|
|
20
|
+
else
|
|
21
|
+
echo " FAIL $name"
|
|
22
|
+
FAIL=$((FAIL+1))
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
echo "flo smoke test"
|
|
27
|
+
echo "--------------"
|
|
28
|
+
|
|
29
|
+
check "help" $FLO help
|
|
30
|
+
check "version" $FLO version
|
|
31
|
+
|
|
32
|
+
# Doctor may exit non-zero if MCP config absent — that's fine; just check it runs.
|
|
33
|
+
$FLO doctor >/dev/null 2>&1 || true
|
|
34
|
+
echo " PASS doctor (ran)"
|
|
35
|
+
PASS=$((PASS+1))
|
|
36
|
+
|
|
37
|
+
check "sessions list (text)" $FLO sessions list --limit 5
|
|
38
|
+
check "sessions list (json)" bash -c "$FLO sessions list --json | python3 -c 'import sys,json; json.load(sys.stdin)'"
|
|
39
|
+
|
|
40
|
+
check "guidance audit (json)" bash -c "$FLO guidance audit --json --quiet | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d[\"total\"] >= 0'"
|
|
41
|
+
|
|
42
|
+
# Inbox: drop a markdown file, run --once, confirm it moved to .processed
|
|
43
|
+
mkdir -p "$TMP/inbox-test"
|
|
44
|
+
cat > "$TMP/inbox-test/hello.md" <<EOF
|
|
45
|
+
---
|
|
46
|
+
to: tester
|
|
47
|
+
from: smoke
|
|
48
|
+
subject: ping
|
|
49
|
+
---
|
|
50
|
+
hi
|
|
51
|
+
EOF
|
|
52
|
+
$FLO inbox watch "$TMP/inbox-test" --once >/dev/null 2>&1
|
|
53
|
+
if [ -f "$TMP/inbox-test/.processed/hello.md" ]; then
|
|
54
|
+
echo " PASS inbox once-mode (file moved to .processed)"
|
|
55
|
+
PASS=$((PASS+1))
|
|
56
|
+
else
|
|
57
|
+
echo " FAIL inbox once-mode (file did not move)"
|
|
58
|
+
FAIL=$((FAIL+1))
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Migrate dry-run (must not write)
|
|
62
|
+
check "migrate --dry-run" bash -c "$FLO migrate --dry-run --mcp-path $TMP/no-such-mcp.json"
|
|
63
|
+
if [ -f "$TMP/no-such-mcp.json" ]; then
|
|
64
|
+
echo " FAIL migrate --dry-run wrote a file"
|
|
65
|
+
FAIL=$((FAIL+1))
|
|
66
|
+
else
|
|
67
|
+
echo " PASS migrate --dry-run did not write"
|
|
68
|
+
PASS=$((PASS+1))
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# MCP handshake: send initialize + tools/list, expect expanded toolset
|
|
72
|
+
RESP=$(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n' | $FLO mcp start 2>/dev/null)
|
|
73
|
+
if echo "$RESP" | grep -q '"protocolVersion":"2024-11-05"' \
|
|
74
|
+
&& echo "$RESP" | grep -q 'flo_sessions_list' \
|
|
75
|
+
&& echo "$RESP" | grep -q 'flo_memory_store' \
|
|
76
|
+
&& echo "$RESP" | grep -q 'flo_inbox_list' \
|
|
77
|
+
&& echo "$RESP" | grep -q 'flo_transcribe'; then
|
|
78
|
+
echo " PASS mcp start (initialize + tools/list, expanded)"
|
|
79
|
+
PASS=$((PASS+1))
|
|
80
|
+
else
|
|
81
|
+
echo " FAIL mcp start (unexpected response or missing tools)"
|
|
82
|
+
FAIL=$((FAIL+1))
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# MCP call: round-trip a tool to exercise the dispatch path
|
|
86
|
+
MCP_CALL=$(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"flo_memory_namespaces","arguments":{}}}\n' | $FLO mcp start 2>/dev/null)
|
|
87
|
+
if echo "$MCP_CALL" | grep -q '"content"'; then
|
|
88
|
+
echo " PASS mcp tools/call (flo_memory_namespaces)"
|
|
89
|
+
PASS=$((PASS+1))
|
|
90
|
+
else
|
|
91
|
+
echo " FAIL mcp tools/call did not return content"
|
|
92
|
+
FAIL=$((FAIL+1))
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Transcribe: detect should always run; we don't assert which tool, only that the command exits cleanly with --json
|
|
96
|
+
if $FLO transcribe --detect --json >/dev/null 2>&1; then
|
|
97
|
+
echo " PASS transcribe --detect"
|
|
98
|
+
PASS=$((PASS+1))
|
|
99
|
+
else
|
|
100
|
+
echo " SKIP transcribe --detect (no tool installed — install whisper/mlx-whisper to enable)"
|
|
101
|
+
# Not counted as a failure: transcribe is local-tool-dependent.
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# Swarm: if .swarm/ exists, expect available=true; otherwise expect available=false
|
|
105
|
+
if [ -d "${REPO_ROOT}/.swarm" ]; then
|
|
106
|
+
if $FLO swarm status --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get("available") is True' 2>/dev/null; then
|
|
107
|
+
echo " PASS swarm status (available)"
|
|
108
|
+
PASS=$((PASS+1))
|
|
109
|
+
else
|
|
110
|
+
echo " FAIL swarm status with .swarm/ present"
|
|
111
|
+
FAIL=$((FAIL+1))
|
|
112
|
+
fi
|
|
113
|
+
else
|
|
114
|
+
echo " SKIP swarm status (no .swarm/ dir)"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Memory store: store → list → search → namespaces → bridge from inbox md drop
|
|
118
|
+
export FLO_HOME="$TMP/flo-home"
|
|
119
|
+
if $FLO memory store --value "JWT auth pattern with 1hr refresh" --key smoke-pattern-auth --namespace smoke-patterns --tags auth,security >/dev/null 2>&1; then
|
|
120
|
+
echo " PASS memory store"
|
|
121
|
+
PASS=$((PASS+1))
|
|
122
|
+
else
|
|
123
|
+
echo " FAIL memory store"
|
|
124
|
+
FAIL=$((FAIL+1))
|
|
125
|
+
fi
|
|
126
|
+
if $FLO memory list --namespace smoke-patterns --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert len(d)>=1' 2>/dev/null; then
|
|
127
|
+
echo " PASS memory list (json)"
|
|
128
|
+
PASS=$((PASS+1))
|
|
129
|
+
else
|
|
130
|
+
echo " FAIL memory list (json)"
|
|
131
|
+
FAIL=$((FAIL+1))
|
|
132
|
+
fi
|
|
133
|
+
if $FLO memory search "JWT" --namespace smoke-patterns --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any("JWT" in e["value"] for e in d)' 2>/dev/null; then
|
|
134
|
+
echo " PASS memory search (substring)"
|
|
135
|
+
PASS=$((PASS+1))
|
|
136
|
+
else
|
|
137
|
+
echo " FAIL memory search (substring)"
|
|
138
|
+
FAIL=$((FAIL+1))
|
|
139
|
+
fi
|
|
140
|
+
if $FLO memory namespaces --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(n["namespace"]=="smoke-patterns" for n in d)' 2>/dev/null; then
|
|
141
|
+
echo " PASS memory namespaces"
|
|
142
|
+
PASS=$((PASS+1))
|
|
143
|
+
else
|
|
144
|
+
echo " FAIL memory namespaces"
|
|
145
|
+
FAIL=$((FAIL+1))
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Inbox bridge: drop .md with frontmatter → mailbox + memory:inbox entry
|
|
149
|
+
mkdir -p "$TMP/bridge-inbox"
|
|
150
|
+
cat > "$TMP/bridge-inbox/msg.md" <<EOF
|
|
151
|
+
---
|
|
152
|
+
to: architect
|
|
153
|
+
from: tester
|
|
154
|
+
subject: bridge smoke
|
|
155
|
+
---
|
|
156
|
+
Smoke body content.
|
|
157
|
+
EOF
|
|
158
|
+
$FLO inbox watch "$TMP/bridge-inbox" --once >/dev/null 2>&1
|
|
159
|
+
if $FLO messages list architect --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert len(d)>=1' 2>/dev/null; then
|
|
160
|
+
echo " PASS inbox bridge (mailbox)"
|
|
161
|
+
PASS=$((PASS+1))
|
|
162
|
+
else
|
|
163
|
+
echo " FAIL inbox bridge (mailbox)"
|
|
164
|
+
FAIL=$((FAIL+1))
|
|
165
|
+
fi
|
|
166
|
+
if $FLO memory list --namespace inbox --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(e.get("metadata",{}).get("to")=="architect" for e in d)' 2>/dev/null; then
|
|
167
|
+
echo " PASS inbox bridge (memory entry)"
|
|
168
|
+
PASS=$((PASS+1))
|
|
169
|
+
else
|
|
170
|
+
echo " FAIL inbox bridge (memory entry)"
|
|
171
|
+
FAIL=$((FAIL+1))
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# Inbox registry: add → list → install (macOS only) → uninstall → remove
|
|
175
|
+
mkdir -p "$TMP/inbox-reg-test"
|
|
176
|
+
if $FLO inbox add "$TMP/inbox-reg-test" --slug smoke-reg >/dev/null 2>&1; then
|
|
177
|
+
echo " PASS inbox add"
|
|
178
|
+
PASS=$((PASS+1))
|
|
179
|
+
else
|
|
180
|
+
echo " FAIL inbox add"
|
|
181
|
+
FAIL=$((FAIL+1))
|
|
182
|
+
fi
|
|
183
|
+
if $FLO inbox list --json 2>/dev/null | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(i["slug"]=="smoke-reg" for i in d)' 2>/dev/null; then
|
|
184
|
+
echo " PASS inbox list (json)"
|
|
185
|
+
PASS=$((PASS+1))
|
|
186
|
+
else
|
|
187
|
+
echo " FAIL inbox list (json)"
|
|
188
|
+
FAIL=$((FAIL+1))
|
|
189
|
+
fi
|
|
190
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
191
|
+
if $FLO inbox install smoke-reg --interval 60 >/dev/null 2>&1; then
|
|
192
|
+
if [ -f "$HOME/Library/LaunchAgents/io.myflo.inbox.smoke-reg.plist" ]; then
|
|
193
|
+
echo " PASS inbox install (plist created)"
|
|
194
|
+
PASS=$((PASS+1))
|
|
195
|
+
$FLO inbox uninstall smoke-reg >/dev/null 2>&1
|
|
196
|
+
if [ ! -f "$HOME/Library/LaunchAgents/io.myflo.inbox.smoke-reg.plist" ]; then
|
|
197
|
+
echo " PASS inbox uninstall (plist removed)"
|
|
198
|
+
PASS=$((PASS+1))
|
|
199
|
+
else
|
|
200
|
+
echo " FAIL inbox uninstall did not remove plist"
|
|
201
|
+
FAIL=$((FAIL+1))
|
|
202
|
+
fi
|
|
203
|
+
else
|
|
204
|
+
echo " FAIL inbox install did not create plist"
|
|
205
|
+
FAIL=$((FAIL+1))
|
|
206
|
+
fi
|
|
207
|
+
else
|
|
208
|
+
echo " FAIL inbox install command"
|
|
209
|
+
FAIL=$((FAIL+1))
|
|
210
|
+
fi
|
|
211
|
+
else
|
|
212
|
+
echo " SKIP inbox install (macOS-only)"
|
|
213
|
+
fi
|
|
214
|
+
$FLO inbox remove smoke-reg >/dev/null 2>&1 || true
|
|
215
|
+
|
|
216
|
+
# Transcripts: returns JSON array (empty is OK)
|
|
217
|
+
if $FLO transcripts list --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert isinstance(d, list)' 2>/dev/null; then
|
|
218
|
+
echo " PASS transcripts list (json shape)"
|
|
219
|
+
PASS=$((PASS+1))
|
|
220
|
+
else
|
|
221
|
+
echo " FAIL transcripts list (json shape)"
|
|
222
|
+
FAIL=$((FAIL+1))
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Tasks: create → list → update → complete → counts → delete (event log round-trip)
|
|
226
|
+
TASK_ID=$($FLO tasks create "smoke test task" --tags smoke,test --json 2>/dev/null \
|
|
227
|
+
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])' 2>/dev/null)
|
|
228
|
+
if [ -n "$TASK_ID" ]; then
|
|
229
|
+
echo " PASS tasks create (id=$TASK_ID)"
|
|
230
|
+
PASS=$((PASS+1))
|
|
231
|
+
else
|
|
232
|
+
echo " FAIL tasks create"
|
|
233
|
+
FAIL=$((FAIL+1))
|
|
234
|
+
fi
|
|
235
|
+
if $FLO tasks list --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(t["status"]=="pending" for t in d)' 2>/dev/null; then
|
|
236
|
+
echo " PASS tasks list (pending)"
|
|
237
|
+
PASS=$((PASS+1))
|
|
238
|
+
else
|
|
239
|
+
echo " FAIL tasks list"
|
|
240
|
+
FAIL=$((FAIL+1))
|
|
241
|
+
fi
|
|
242
|
+
if [ -n "$TASK_ID" ] && $FLO tasks update "$TASK_ID" --status in_progress >/dev/null 2>&1; then
|
|
243
|
+
if $FLO tasks list --status in_progress --json | python3 -c "import sys,json; d=json.load(sys.stdin); assert any(t['id']=='$TASK_ID' for t in d)" 2>/dev/null; then
|
|
244
|
+
echo " PASS tasks update (status transition)"
|
|
245
|
+
PASS=$((PASS+1))
|
|
246
|
+
else
|
|
247
|
+
echo " FAIL tasks update (didn't show in in_progress list)"
|
|
248
|
+
FAIL=$((FAIL+1))
|
|
249
|
+
fi
|
|
250
|
+
else
|
|
251
|
+
echo " FAIL tasks update command"
|
|
252
|
+
FAIL=$((FAIL+1))
|
|
253
|
+
fi
|
|
254
|
+
if [ -n "$TASK_ID" ] && $FLO tasks complete "$TASK_ID" >/dev/null 2>&1; then
|
|
255
|
+
if $FLO tasks counts --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d["completed"]>=1' 2>/dev/null; then
|
|
256
|
+
echo " PASS tasks complete + counts"
|
|
257
|
+
PASS=$((PASS+1))
|
|
258
|
+
else
|
|
259
|
+
echo " FAIL tasks counts"
|
|
260
|
+
FAIL=$((FAIL+1))
|
|
261
|
+
fi
|
|
262
|
+
else
|
|
263
|
+
echo " FAIL tasks complete command"
|
|
264
|
+
FAIL=$((FAIL+1))
|
|
265
|
+
fi
|
|
266
|
+
[ -n "$TASK_ID" ] && $FLO tasks delete "$TASK_ID" >/dev/null 2>&1 || true
|
|
267
|
+
# Terminal-attach: add/list/remove
|
|
268
|
+
if $FLO session terminal-add smoke-term --cwd "$TMP" --app ghostty --title "smoke" >/dev/null 2>&1; then
|
|
269
|
+
echo " PASS session terminal-add"
|
|
270
|
+
PASS=$((PASS+1))
|
|
271
|
+
else
|
|
272
|
+
echo " FAIL session terminal-add"
|
|
273
|
+
FAIL=$((FAIL+1))
|
|
274
|
+
fi
|
|
275
|
+
if $FLO session terminal-list --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(t["slug"]=="smoke-term" for t in d)' 2>/dev/null; then
|
|
276
|
+
echo " PASS session terminal-list (json)"
|
|
277
|
+
PASS=$((PASS+1))
|
|
278
|
+
else
|
|
279
|
+
echo " FAIL session terminal-list (json)"
|
|
280
|
+
FAIL=$((FAIL+1))
|
|
281
|
+
fi
|
|
282
|
+
if $FLO session terminal-remove smoke-term >/dev/null 2>&1; then
|
|
283
|
+
echo " PASS session terminal-remove"
|
|
284
|
+
PASS=$((PASS+1))
|
|
285
|
+
else
|
|
286
|
+
echo " FAIL session terminal-remove"
|
|
287
|
+
FAIL=$((FAIL+1))
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# BM25 ranking: discriminating query "refresh oauth2" should hit JWT entry alone
|
|
291
|
+
$FLO memory store --value "JWT auth with refresh tokens and OAuth2" --namespace bm25-test >/dev/null 2>&1
|
|
292
|
+
$FLO memory store --value "Stripe payment integration handles webhooks" --namespace bm25-test >/dev/null 2>&1
|
|
293
|
+
$FLO memory store --value "Auth tokens signed with HS256 keys" --namespace bm25-test >/dev/null 2>&1
|
|
294
|
+
TOP_SCORE_VAL=$($FLO memory search "refresh oauth2" --namespace bm25-test --json 2>/dev/null \
|
|
295
|
+
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d[0]["value"] if d else "")' 2>/dev/null)
|
|
296
|
+
if echo "$TOP_SCORE_VAL" | grep -q "JWT auth"; then
|
|
297
|
+
echo " PASS memory search BM25 ranking (JWT entry top)"
|
|
298
|
+
PASS=$((PASS+1))
|
|
299
|
+
else
|
|
300
|
+
echo " FAIL memory search BM25 ranking (got: $TOP_SCORE_VAL)"
|
|
301
|
+
FAIL=$((FAIL+1))
|
|
302
|
+
fi
|
|
303
|
+
WEBHOOK_TOP=$($FLO memory search "webhook" --namespace bm25-test --json 2>/dev/null \
|
|
304
|
+
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d[0]["value"] if d else "")' 2>/dev/null)
|
|
305
|
+
if echo "$WEBHOOK_TOP" | grep -q "Stripe"; then
|
|
306
|
+
echo " PASS memory search BM25 ranking (webhook → Stripe entry)"
|
|
307
|
+
PASS=$((PASS+1))
|
|
308
|
+
else
|
|
309
|
+
echo " FAIL memory search BM25 ranking (webhook expected Stripe, got: $WEBHOOK_TOP)"
|
|
310
|
+
FAIL=$((FAIL+1))
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
unset FLO_HOME
|
|
314
|
+
|
|
315
|
+
# AgentDB backend: same operations should work with FLO_MEMORY_BACKEND=agentdb
|
|
316
|
+
export FLO_HOME="$TMP/flo-agentdb"
|
|
317
|
+
export FLO_MEMORY_BACKEND=agentdb
|
|
318
|
+
if $FLO memory store --value "JWT auth via AgentDB" --namespace agentdb-test --tags auth >/dev/null 2>&1; then
|
|
319
|
+
echo " PASS memory store (agentdb backend)"
|
|
320
|
+
PASS=$((PASS+1))
|
|
321
|
+
else
|
|
322
|
+
echo " FAIL memory store (agentdb backend)"
|
|
323
|
+
FAIL=$((FAIL+1))
|
|
324
|
+
fi
|
|
325
|
+
$FLO memory store --value "Stripe webhook handler" --namespace agentdb-test >/dev/null 2>&1 || true
|
|
326
|
+
$FLO memory store --value "HS256 keys" --namespace agentdb-test >/dev/null 2>&1 || true
|
|
327
|
+
if $FLO memory list --namespace agentdb-test --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert len(d)>=2' 2>/dev/null; then
|
|
328
|
+
echo " PASS memory list (agentdb backend)"
|
|
329
|
+
PASS=$((PASS+1))
|
|
330
|
+
else
|
|
331
|
+
echo " FAIL memory list (agentdb backend)"
|
|
332
|
+
FAIL=$((FAIL+1))
|
|
333
|
+
fi
|
|
334
|
+
if $FLO memory search "JWT" --namespace agentdb-test --json 2>/dev/null \
|
|
335
|
+
| python3 -c 'import sys,json; d=json.load(sys.stdin); assert any("JWT" in e["value"] for e in d)' 2>/dev/null; then
|
|
336
|
+
echo " PASS memory search FTS5 (agentdb backend)"
|
|
337
|
+
PASS=$((PASS+1))
|
|
338
|
+
else
|
|
339
|
+
echo " FAIL memory search FTS5 (agentdb backend)"
|
|
340
|
+
FAIL=$((FAIL+1))
|
|
341
|
+
fi
|
|
342
|
+
if $FLO memory namespaces --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(n["namespace"]=="agentdb-test" for n in d)' 2>/dev/null; then
|
|
343
|
+
echo " PASS memory namespaces (agentdb backend)"
|
|
344
|
+
PASS=$((PASS+1))
|
|
345
|
+
else
|
|
346
|
+
echo " FAIL memory namespaces (agentdb backend)"
|
|
347
|
+
FAIL=$((FAIL+1))
|
|
348
|
+
fi
|
|
349
|
+
unset FLO_HOME FLO_MEMORY_BACKEND
|
|
350
|
+
|
|
351
|
+
# Agents: spawn / list / update / health
|
|
352
|
+
export FLO_HOME="$TMP/flo-agents"
|
|
353
|
+
AGENT_ID=$($FLO agents spawn coder --name builder --tags impl --json 2>/dev/null \
|
|
354
|
+
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])' 2>/dev/null)
|
|
355
|
+
if [ -n "$AGENT_ID" ]; then echo " PASS agents spawn (id=$AGENT_ID)"; PASS=$((PASS+1))
|
|
356
|
+
else echo " FAIL agents spawn"; FAIL=$((FAIL+1)); fi
|
|
357
|
+
if $FLO agents list --json | python3 -c "import sys,json; d=json.load(sys.stdin); assert any(a['id']=='$AGENT_ID' for a in d)" 2>/dev/null; then
|
|
358
|
+
echo " PASS agents list"; PASS=$((PASS+1))
|
|
359
|
+
else echo " FAIL agents list"; FAIL=$((FAIL+1)); fi
|
|
360
|
+
if [ -n "$AGENT_ID" ] && $FLO agents update "$AGENT_ID" --status busy >/dev/null 2>&1; then
|
|
361
|
+
if $FLO agents list --status busy --json | python3 -c "import sys,json; d=json.load(sys.stdin); assert any(a['id']=='$AGENT_ID' for a in d)" 2>/dev/null; then
|
|
362
|
+
echo " PASS agents update (status transition)"; PASS=$((PASS+1))
|
|
363
|
+
else echo " FAIL agents update visibility"; FAIL=$((FAIL+1)); fi
|
|
364
|
+
else echo " FAIL agents update command"; FAIL=$((FAIL+1)); fi
|
|
365
|
+
if $FLO agents health --json | python3 -c 'import sys,json; d=json.load(sys.stdin); assert any(h["health"]=="healthy" for h in d)' 2>/dev/null; then
|
|
366
|
+
echo " PASS agents health"; PASS=$((PASS+1))
|
|
367
|
+
else echo " FAIL agents health"; FAIL=$((FAIL+1)); fi
|
|
368
|
+
unset FLO_HOME
|
|
369
|
+
|
|
370
|
+
# Swarm vote: record + tally in an isolated temp swarm dir
|
|
371
|
+
mkdir -p "$TMP/swarm-vote-test"
|
|
372
|
+
(cd "$TMP/swarm-vote-test" && $FLO swarm vote use-flo --voter alice --vote yes >/dev/null 2>&1 \
|
|
373
|
+
&& $FLO swarm vote use-flo --voter bob --vote yes --weight 2 >/dev/null 2>&1 \
|
|
374
|
+
&& $FLO swarm vote use-flo --voter carol --vote no >/dev/null 2>&1)
|
|
375
|
+
TALLY=$(cd "$TMP/swarm-vote-test" && $FLO swarm tally use-flo --json 2>/dev/null)
|
|
376
|
+
if echo "$TALLY" | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d["totalVoters"]==3 and d["tally"]["yes"]==3 and d["tally"]["no"]==1' 2>/dev/null; then
|
|
377
|
+
echo " PASS swarm vote + tally"; PASS=$((PASS+1))
|
|
378
|
+
else echo " FAIL swarm vote + tally (got $TALLY)"; FAIL=$((FAIL+1)); fi
|
|
379
|
+
|
|
380
|
+
# MCP tools/list should now report 22 tools (16 + 6 new)
|
|
381
|
+
TOOLCOUNT=$(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n' \
|
|
382
|
+
| $FLO mcp start 2>/dev/null | python3 -c 'import sys,json
|
|
383
|
+
for line in sys.stdin:
|
|
384
|
+
if not line.strip(): continue
|
|
385
|
+
m = json.loads(line)
|
|
386
|
+
if m.get("id") == 2: print(len(m["result"]["tools"]))' 2>/dev/null)
|
|
387
|
+
if [ "$TOOLCOUNT" = "22" ]; then echo " PASS mcp tools/list count (22)"; PASS=$((PASS+1))
|
|
388
|
+
else echo " FAIL mcp tools/list count (got $TOOLCOUNT)"; FAIL=$((FAIL+1)); fi
|
|
389
|
+
|
|
390
|
+
echo "--------------"
|
|
391
|
+
echo "$PASS passed, $FAIL failed."
|
|
392
|
+
if [ "$FAIL" -gt 0 ]; then exit 1; fi
|