
Videodb Monitoring
- 2 installs
- 25 repo stars
- Updated March 27, 2026
- video-db/openclaw-monitoring
Helps with ai & agent building tasks.
About
videodb-monitoring is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- videodb-monitoring
- AI & Agent Building
- AI-coding skill
Videodb Monitoring by the numbers
- 2 all-time installs (skills.sh)
- Ranked #13,956 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/video-db/openclaw-monitoring --skill videodb-monitoringAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 25 |
| Last updated | March 27, 2026 |
| Repository | video-db/openclaw-monitoring ↗ |
What it does
Helps with ai & agent building tasks.
Files
VideoDB Screen Recording Skill
Screen recording capabilities powered by VideoDB. Use this when the user asks for screen recordings, wants to search past activity, or needs transcripts.
Run commands from {baseDir} using npx tsx videodb.ts.
Prerequisites
1. Check API Key
Before using any commands, verify the API key is configured:
openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_API_KEYIf not set or empty:
- Ask the user for their VideoDB API key
- If they provide it, set it for them:
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_API_KEY 'sk-xxx'- If they don't have one, direct them to: https://console.videodb.io
2. Check Monitor is Running
openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_IS_RUNNINGIf not `true`, start the monitor:
cd {baseDir} && nohup npx tsx monitor.ts > ~/.videodb/logs/monitor.log 2>&1 & disown && sleep 3Verify it started:
openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_CAPTURE_SESSION_IDIf you get a session ID (e.g., cap-xxxxxxxx-...), the monitor is ready.
Commands
Get Current Timestamp
cd {baseDir} && npx tsx videodb.ts nowReturns current Unix timestamp (seconds since epoch).
Generate Stream URL
cd {baseDir} && npx tsx videodb.ts stream <start_timestamp> <end_timestamp>
cd {baseDir} && npx tsx videodb.ts stream <start_timestamp> <end_timestamp> --title "Checkout flow" --description "OpenClaw browser run"Creates a playable recording URL for the time range. If --title or --description is provided, the generated player share page uses that metadata.
Start Indexing
Start indexing only when the user asks for search, summaries, or transcripts:
cd {baseDir} && npx tsx videodb.ts start-indexingThis starts:
- transcript capture for system audio
- audio indexing
- visual indexing
You can also control them individually:
cd {baseDir} && npx tsx videodb.ts start-visual-index
cd {baseDir} && npx tsx videodb.ts start-transcript
cd {baseDir} && npx tsx videodb.ts start-audio-indexStop Indexing
Stop indexing as soon as it is no longer needed to save cost:
cd {baseDir} && npx tsx videodb.ts stop-indexingIndividual stop commands:
cd {baseDir} && npx tsx videodb.ts stop-visual-index
cd {baseDir} && npx tsx videodb.ts stop-transcript
cd {baseDir} && npx tsx videodb.ts stop-audio-indexSearch Recordings
cd {baseDir} && npx tsx videodb.ts search "user opened Amazon"Searches indexed screen activity for matching events. If no visual index exists yet, start indexing first.
Activity Summary
cd {baseDir} && npx tsx videodb.ts summary # last 30 minutes
cd {baseDir} && npx tsx videodb.ts summary --hours 2 # last 2 hoursAudio Transcripts
cd {baseDir} && npx tsx videodb.ts transcript # last 30 minutes
cd {baseDir} && npx tsx videodb.ts transcript --hours 1 # last hourRecording Workflow
When user requests screen recording of a task:
1. Capture start time:
cd {baseDir} && npx tsx videodb.ts nowStore this as start_time.
2. Do the work (browser actions, file editing, etc.)
3. Capture end time:
cd {baseDir} && npx tsx videodb.ts now4. Generate stream URL:
cd {baseDir} && npx tsx videodb.ts stream <start_time> <end_time>Optional player metadata:
cd {baseDir} && npx tsx videodb.ts stream <start_time> <end_time> --title "Task recording" --description "Captured during OpenClaw task execution"5. Include URL in response:
Screen recording: https://rt.stream.videodb.io/...If the command prints a player page URL, prefer sharing that URL with the user.
Indexing is not started automatically by the monitor. If the user also wants search, summaries, or transcripts, start indexing explicitly before those commands and stop it afterwards.
Example
User: "Open example.com and send me the recording"
# Check prerequisites
openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_IS_RUNNING
# true
# Start time
cd {baseDir} && npx tsx videodb.ts now
# 1709740800
# Do the work (open browser, navigate)
# ...
# End time
cd {baseDir} && npx tsx videodb.ts now
# 1709740830
# Generate URL
cd {baseDir} && npx tsx videodb.ts stream 1709740800 1709740830 --title "example.com walkthrough" --description "OpenClaw browser automation"
# 📹 Screen recording (30s): https://rt.stream.videodb.io/abc123
# Player page: https://player.videodb.io/watch?v=example-slugResponse:
Done! I opened example.com.
>
Screen recording: https://rt.stream.videodb.io/abc123
When to Use
| User Request | Command |
|---|---|
| "Record my screen while you do X" | Use workflow above |
| "What did I do in the last hour?" | start-indexing, then summary --hours 1, then stop-indexing |
| "Find when I opened the spreadsheet" | start-indexing, then search "opened spreadsheet" |
| "What was said in that meeting?" | start-indexing, then transcript |
| "Get the recording from 5 mins ago" | stream with timestamps |
| "Record this and set the title/description" | stream with --title and --description |
Troubleshooting
If commands fail with "No capture session": 1. Check if monitor is running: openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_IS_RUNNING 2. If not, start it (see Prerequisites above) 3. If it shows running but still fails, restart the monitor
If summary/search/transcript say no index or no transcript: 1. Start indexing with cd {baseDir} && npx tsx videodb.ts start-indexing 2. Wait briefly for data to accumulate 3. Retry the command 4. Stop indexing with cd {baseDir} && npx tsx videodb.ts stop-indexing when done
#!/usr/bin/env npx tsx
import { connect, CaptureClient } from "videodb";
import { execSync, spawn, type ChildProcess } from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
const LOG_DIR = path.join(os.homedir(), ".videodb", "logs");
const LOG_FILE = path.join(LOG_DIR, "monitor.log");
const SKILL_CONFIG_BASE = "skills.entries.videodb-monitoring";
let cleanupDone = false;
function log(message: string): void {
const timestamp = new Date().toISOString();
const line = `${timestamp} ${message}\n`;
try {
fs.mkdirSync(LOG_DIR, { recursive: true });
fs.appendFileSync(LOG_FILE, line);
} catch {
// ignore
}
console.log(`[${timestamp}] ${message}`);
}
interface OpenClawConfig {
skills?: {
entries?: {
"videodb-monitoring"?: {
enabled?: boolean;
apiKey?: string;
env?: {
VIDEODB_API_KEY?: string;
VIDEODB_IS_RUNNING?: string;
VIDEODB_CAPTURE_SESSION_ID?: string;
VIDEODB_MONITOR_PID?: string;
};
};
};
};
}
function readOpenClawConfig(): OpenClawConfig {
try {
if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
return JSON.parse(fs.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
}
} catch {
// ignore
}
return {};
}
function getApiKey(): string | undefined {
// Check environment variables first
if (process.env.VIDEODB_API_KEY) return process.env.VIDEODB_API_KEY;
if (process.env.VIDEO_DB_API_KEY) return process.env.VIDEO_DB_API_KEY;
// Then check skills config
const config = readOpenClawConfig();
const skillConfig = config.skills?.entries?.["videodb-monitoring"];
return (
skillConfig?.env?.VIDEODB_API_KEY ||
(typeof skillConfig?.apiKey === "string" ? skillConfig.apiKey : undefined)
);
}
function setSkillEnv(key: string, value: string): void {
const configPath = `${SKILL_CONFIG_BASE}.env.${key}`;
try {
// Wrap in double quotes so value stays a string (not parsed as bool/number)
execSync(`openclaw config set ${configPath} '"${value}"'`, {
timeout: 10000,
stdio: "pipe",
});
log(`Config set: env.${key} = ${value}`);
} catch (e: any) {
log(`[warning] Could not set env.${key}: ${e.message}`);
}
}
function resetSkillState(): void {
setSkillEnv("VIDEODB_IS_RUNNING", "false");
setSkillEnv("VIDEODB_CAPTURE_SESSION_ID", "");
setSkillEnv("VIDEODB_MONITOR_PID", "");
}
function clearSkillState(): void {
if (cleanupDone) return;
cleanupDone = true;
log("Clearing skill state...");
resetSkillState();
log("Skill state cleared");
}
async function createSession(apiKey: string) {
const conn = connect(apiKey);
const session = await conn.createCaptureSession({
endUserId: "openclaw-monitor",
metadata: { app: "openclaw-monitoring" },
});
const token = await conn.generateClientToken();
return { sessionId: session.id, token, conn };
}
function getConfiguredMonitorPid(): number | undefined {
const rawPid =
readOpenClawConfig().skills?.entries?.["videodb-monitoring"]?.env?.VIDEODB_MONITOR_PID;
if (!rawPid) return undefined;
const pid = Number.parseInt(rawPid, 10);
return Number.isFinite(pid) && pid > 0 ? pid : undefined;
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function stopProcess(pid: number, label: string): Promise<void> {
if (pid === process.pid) return;
if (!isProcessAlive(pid)) return;
log(`Stopping ${label} ${pid} with SIGTERM`);
try {
process.kill(pid, "SIGTERM");
} catch (err: any) {
log(`[warning] Could not SIGTERM ${label} ${pid}: ${err.message}`);
return;
}
await sleep(2000);
if (!isProcessAlive(pid)) {
log(`${label} ${pid} stopped cleanly`);
return;
}
log(`${label} ${pid} still running; sending SIGKILL`);
try {
process.kill(pid, "SIGKILL");
} catch (err: any) {
log(`[warning] Could not SIGKILL ${label} ${pid}: ${err.message}`);
return;
}
await sleep(500);
}
async function cleanupPreviousMonitor(): Promise<void> {
const existingMonitorPid = getConfiguredMonitorPid();
if (existingMonitorPid && existingMonitorPid !== process.pid) {
await stopProcess(existingMonitorPid, "previous monitor");
}
log("Resetting stale skill state before startup");
resetSkillState();
}
async function capture(token: string, sessionId: string): Promise<never> {
log("initializing capture client");
const client = new CaptureClient({ sessionToken: token });
let caffeinate: ChildProcess | null = null;
if (process.platform === "darwin") {
caffeinate = spawn("caffeinate", ["-dims"], { stdio: "ignore", detached: true });
caffeinate.unref();
}
const shutdown = async () => {
log("shutdown requested");
clearSkillState();
await client.stopSession().catch((e) => log(`stopSession error: ${e.message}`));
await client.shutdown().catch((e) => log(`shutdown error: ${e.message}`));
caffeinate?.kill();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("SIGHUP", () => log("received SIGHUP, ignoring"));
process.on("uncaughtException", (err) => {
log(`uncaughtException: ${err.message}`);
clearSkillState();
process.exit(1);
});
process.on("unhandledRejection", (reason: any) => {
log(`unhandledRejection: ${reason?.message || reason}`);
clearSkillState();
process.exit(1);
});
// Request screen capture permission (required)
log("requesting screen-capture permission");
try {
await client.requestPermission("screen-capture");
log("screen-capture permission granted");
} catch (err: any) {
log(`screen-capture permission failed: ${err.message}`);
throw err;
}
// Request microphone permission (optional - continue without if denied)
let hasAudioPermission = false;
log("requesting microphone permission");
try {
await client.requestPermission("microphone");
hasAudioPermission = true;
log("microphone permission granted");
} catch (err: any) {
log(`microphone permission denied: ${err.message} - continuing without audio`);
}
const channels = await client.listChannels();
const display = channels.displays.default;
const systemAudio = hasAudioPermission ? channels.systemAudio.default : null;
if (!display) {
log("no display found");
throw new Error("No display found");
}
const selected: { channelId: string; type: "video" | "audio"; store: boolean }[] = [];
if (display) {
selected.push({ channelId: display.id, type: "video", store: true });
}
if (systemAudio) {
selected.push({ channelId: systemAudio.id, type: "audio", store: true });
}
log(`recording ${selected.length} channel(s)`);
selected.forEach((ch) => log(` - ${ch.type}: ${ch.channelId}`));
await client.startSession({ sessionId, channels: selected as any });
log("recording started");
return new Promise(() => {});
}
async function main() {
log("VideoDB Screen Monitor starting");
const apiKey = getApiKey();
if (!apiKey) {
log("API key not found");
console.error("API key not found. Set it via:");
console.error(` openclaw config set ${SKILL_CONFIG_BASE}.env.VIDEODB_API_KEY 'sk-xxx'`);
process.exit(1);
}
log(`API key: ${apiKey.slice(0, 10)}...`);
await cleanupPreviousMonitor();
// Set running state immediately
setSkillEnv("VIDEODB_IS_RUNNING", "true");
setSkillEnv("VIDEODB_MONITOR_PID", String(process.pid));
const { sessionId, token } = await createSession(apiKey);
log(`session created: ${sessionId}`);
setSkillEnv("VIDEODB_CAPTURE_SESSION_ID", sessionId);
await capture(token, sessionId);
}
main().catch((err) => {
log(`fatal error: ${err.message}`);
clearSkillState();
console.error(err.message);
process.exit(1);
});
{
"name": "openclaw-videodb-monitoring-skill",
"version": "1.0.0",
"type": "module",
"bin": {
"videodb": "./videodb.ts"
},
"dependencies": {
"videodb": "^0.2.1"
},
"devDependencies": {
"tsx": "^4.7.0",
"@types/node": "^20.0.0"
}
}
VideoDB Screen Recording Skill for OpenClaw
On-demand screen recording capabilities for OpenClaw agents. Record your screen and generate playable stream URLs when needed.
Prerequisites
- OpenClaw installed and running
- VideoDB API key (get one at console.videodb.io)
- Node.js 18+
Installation
1. Copy Skill to OpenClaw
mkdir -p ~/.openclaw/workspace/skills/videodb-monitoring
cp -r ./* ~/.openclaw/workspace/skills/videodb-monitoring/
cd ~/.openclaw/workspace/skills/videodb-monitoring
npm install2. Set API Key
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_API_KEY 'sk-xxx'
openclaw config set skills.entries.videodb-monitoring.enabled true3. Restart Gateway
openclaw gateway restart4. Verify
openclaw skills listYou should see videodb-monitoring in the list.
How It Works
The skill is on-demand — the agent will use it when:
- You ask for a screen recording
- You want to search past activity
- You need transcripts of audio
The agent will: 1. Check if the API key is configured (ask you for it if not) 2. Start the monitor if not running 3. Start indexing only when needed for search, summaries, or transcripts 4. Stop indexing when it is no longer needed to reduce cost 5. Capture timestamps and generate stream URLs, optionally with player title/description metadata 6. Include recording URLs in responses when requested
Example Requests
- "Do X on the browser and send me the recording"
- "What did I do in the last hour?"
- "Find when I opened the spreadsheet"
- "What was said in that meeting?"
Manual Monitor Control
The monitor can also be started manually:
cd ~/.openclaw/workspace/skills/videodb-monitoring
# Foreground (for testing)
npx tsx monitor.ts
# Background (for production)
nohup npx tsx monitor.ts > ~/.videodb/logs/monitor.log 2>&1 &
disownCheck status:
openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_IS_RUNNING
openclaw config get skills.entries.videodb-monitoring.env.VIDEODB_CAPTURE_SESSION_IDGenerate Stream URLs
Basic stream generation:
cd ~/.openclaw/workspace/skills/videodb-monitoring
npx tsx videodb.ts stream 1709740800 1709740830With player metadata:
cd ~/.openclaw/workspace/skills/videodb-monitoring
npx tsx videodb.ts stream 1709740800 1709740830 --title "Checkout flow" --description "OpenClaw browser run"When metadata is provided, the skill also prints the player share page URL when available.
Indexing Control
The monitor now only records. Indexing is controlled explicitly through videodb.ts so you only pay for it when needed.
Start all indexing:
cd ~/.openclaw/workspace/skills/videodb-monitoring
npx tsx videodb.ts start-indexingStop all indexing:
cd ~/.openclaw/workspace/skills/videodb-monitoring
npx tsx videodb.ts stop-indexingGranular commands:
npx tsx videodb.ts start-visual-index
npx tsx videodb.ts stop-visual-index
npx tsx videodb.ts start-transcript
npx tsx videodb.ts stop-transcript
npx tsx videodb.ts start-audio-index
npx tsx videodb.ts stop-audio-indexLogs
All logs are stored in ~/.videodb/logs/:
| File | Description |
|---|---|
monitor.log | Screen capture monitor |
skill.log | Skill command execution |
View logs:
tail -f ~/.videodb/logs/monitor.log
tail -f ~/.videodb/logs/skill.logTroubleshooting
"API key not found"
Set your API key:
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_API_KEY 'sk-xxx'"No capture session"
Start the monitor:
cd ~/.openclaw/workspace/skills/videodb-monitoring
nohup npx tsx monitor.ts > ~/.videodb/logs/monitor.log 2>&1 &
disown"Another recorder instance is already running"
The monitor now performs a pre-cleanup on startup and will try to stop the previous monitor PID plus any lingering recorder helper processes automatically.
If you still need to clean up manually:
pkill -9 -f videodb_recorder
pkill -9 -f "monitor.ts"
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_IS_RUNNING 'false'Then restart the monitor.
"Permission denied" for microphone
The monitor will continue without audio. Screen recording still works.
"No visual index found" or "No transcripts"
Start indexing when you need it:
cd ~/.openclaw/workspace/skills/videodb-monitoring
npx tsx videodb.ts start-indexingRun your search/summary/transcript command, then stop indexing afterwards:
npx tsx videodb.ts stop-indexingStale state after crash
Reset state manually:
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_IS_RUNNING 'false'
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_CAPTURE_SESSION_ID ''
openclaw config set skills.entries.videodb-monitoring.env.VIDEODB_MONITOR_PID ''File Structure
videodb-monitoring/
├── SKILL.md # Skill definition (read by OpenClaw)
├── monitor.ts # Screen capture daemon
├── videodb.ts # CLI tool for stream URLs, search, etc.
├── package.json # Dependencies
└── README.md # This file#!/usr/bin/env npx tsx
import { connect } from "videodb";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
const CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
const LOG_DIR = path.join(os.homedir(), ".videodb", "logs");
const LOG_FILE = path.join(LOG_DIR, "skill.log");
const SKILL_NAME = "videodb-monitoring";
const SKILL_CONFIG_PATH = `skills.entries.${SKILL_NAME}`;
const VISUAL_INDEX_NAME = "openclaw-visual-index";
const AUDIO_INDEX_NAME = "openclaw-audio-index";
const DEFAULT_VISUAL_PROMPT =
"Describe the screen: " +
"(1) Active application and current activity. " +
"(2) Browser status - is one open? What URL/page? " +
"(3) Any error dialogs, crashes, or warning messages? " +
"(4) Timestamp if a clock is visible.";
const DEFAULT_AUDIO_PROMPT = "Summarize the audio content.";
function log(message: string): void {
const timestamp = new Date().toISOString();
const line = `${timestamp} ${message}\n`;
try {
fs.mkdirSync(LOG_DIR, { recursive: true });
fs.appendFileSync(LOG_FILE, line);
} catch {
// ignore
}
}
interface SkillConfig {
enabled?: boolean;
apiKey?: string;
env?: {
VIDEODB_API_KEY?: string;
VIDEODB_IS_RUNNING?: string;
VIDEODB_CAPTURE_SESSION_ID?: string;
VIDEODB_MONITOR_PID?: string;
};
}
interface Config {
skills?: {
entries?: {
"videodb-monitoring"?: SkillConfig;
};
};
}
function loadConfig(): { apiKey: string; sessionId: string } {
// Check environment variables first
let apiKey = process.env.VIDEODB_API_KEY || process.env.VIDEO_DB_API_KEY;
let sessionId = process.env.VIDEODB_CAPTURE_SESSION_ID;
let isRunning = false;
try {
const config: Config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
const skillConfig = config.skills?.entries?.["videodb-monitoring"];
const env = skillConfig?.env;
if (!apiKey) {
apiKey = env?.VIDEODB_API_KEY ||
(typeof skillConfig?.apiKey === "string" ? skillConfig.apiKey : undefined);
}
sessionId = sessionId || env?.VIDEODB_CAPTURE_SESSION_ID;
isRunning = env?.VIDEODB_IS_RUNNING === "true";
} catch {
// ignore
}
if (!apiKey) {
console.error("ERROR: VideoDB API key not found.\n");
console.error("Please set your API key using one of these methods:\n");
console.error("1. Set it in OpenClaw config:");
console.error(` openclaw config set ${SKILL_CONFIG_PATH}.env.VIDEODB_API_KEY 'sk-xxx'\n`);
console.error("2. Or provide it to the agent and it will set it for you.\n");
console.error("Get your API key at: https://console.videodb.io");
process.exit(1);
}
if (!sessionId) {
console.error("ERROR: No capture session found.\n");
if (!isRunning) {
console.error("The screen monitor is not running. Start it with:\n");
console.error(" cd ~/.openclaw/workspace/skills/videodb-monitoring");
console.error(" nohup npx tsx monitor.ts > ~/.videodb/logs/monitor.log 2>&1 &");
console.error(" disown\n");
} else {
console.error("Monitor shows as running but no session ID found.");
console.error("Try restarting the monitor.\n");
}
process.exit(1);
}
return { apiKey, sessionId };
}
function parseFlagValue(args: string[], flag: string): string | undefined {
const idx = args.indexOf(flag);
return idx !== -1 ? args[idx + 1] : undefined;
}
function isStoppedStatus(status?: string): boolean {
return status === "stopped" || status === "stop_requested" || status === "stopping";
}
async function loadSession() {
const { apiKey, sessionId } = loadConfig();
const conn = connect(apiKey);
const coll = await conn.getCollection();
const session = await coll.getCaptureSession(sessionId);
await session.refresh();
return { apiKey, sessionId, conn, coll, session };
}
function getManagedIndexes(
indexes: Array<{
name?: string;
extractionType?: string;
status?: string;
rtstreamIndexId: string;
start: () => Promise<void>;
stop: () => Promise<void>;
getScenes?: (
start?: number,
end?: number,
page?: number,
pageSize?: number
) => Promise<{ scenes: unknown[]; nextPage: boolean } | null>;
}>,
kind: "visual" | "audio"
) {
const preferredName = kind === "visual" ? VISUAL_INDEX_NAME : AUDIO_INDEX_NAME;
const namedIndexes = indexes.filter((index) => index.name === preferredName);
if (namedIndexes.length > 0) return namedIndexes;
return indexes.filter((index) =>
kind === "visual" ? index.extractionType !== "transcript" : index.extractionType === "transcript"
);
}
async function startVisualIndex(prompt: string = DEFAULT_VISUAL_PROMPT) {
log(`startVisualIndex: prompt="${prompt}"`);
const { session } = await loadSession();
const screens = session.getRTStream("screen");
if (screens.length === 0) {
console.log("No screen stream found");
return;
}
const screen = screens[0];
const indexes = getManagedIndexes(await screen.listSceneIndexes(), "visual");
const runningIndex = indexes.find((index) => !isStoppedStatus(index.status));
if (runningIndex) {
console.log(`Visual indexing already running (${runningIndex.rtstreamIndexId})`);
return;
}
const stoppedIndex = indexes[0];
if (stoppedIndex) {
await stoppedIndex.start();
log(`startVisualIndex: restarted ${stoppedIndex.rtstreamIndexId}`);
console.log(`Started visual indexing (${stoppedIndex.rtstreamIndexId})`);
return;
}
const created = await screen.indexVisuals({
name: VISUAL_INDEX_NAME,
prompt,
batchConfig: { type: "time", value: 5, frameCount: 1 },
});
if (!created) {
console.log("Could not create visual index");
return;
}
log(`startVisualIndex: created ${created.rtstreamIndexId}`);
console.log(`Started visual indexing (${created.rtstreamIndexId})`);
}
async function stopVisualIndex() {
log("stopVisualIndex");
const { session } = await loadSession();
const screens = session.getRTStream("screen");
if (screens.length === 0) {
console.log("No screen stream found");
return;
}
const screen = screens[0];
const indexes = getManagedIndexes(await screen.listSceneIndexes(), "visual");
const runningIndexes = indexes.filter((index) => !isStoppedStatus(index.status));
if (runningIndexes.length === 0) {
console.log("No active visual index found");
return;
}
for (const index of runningIndexes) {
await index.stop();
log(`stopVisualIndex: stopped ${index.rtstreamIndexId}`);
}
console.log(`Stopped ${runningIndexes.length} visual index(es)`);
}
async function startTranscript(engine: string = "assemblyai") {
log(`startTranscript: engine=${engine}`);
const { session } = await loadSession();
const audios = session.getRTStream("system_audio");
if (audios.length === 0) {
console.log("No audio stream found");
return;
}
await audios[0].startTranscript(undefined, engine);
console.log(`Started transcript (${engine})`);
}
async function stopTranscript(engine: string = "assemblyai", mode: "graceful" | "force" = "graceful") {
log(`stopTranscript: engine=${engine} mode=${mode}`);
const { session } = await loadSession();
const audios = session.getRTStream("system_audio");
if (audios.length === 0) {
console.log("No audio stream found");
return;
}
await audios[0].stopTranscript(mode, engine);
console.log(`Stopped transcript (${engine}, ${mode})`);
}
async function startAudioIndex(prompt: string = DEFAULT_AUDIO_PROMPT) {
log(`startAudioIndex: prompt="${prompt}"`);
const { session } = await loadSession();
const audios = session.getRTStream("system_audio");
if (audios.length === 0) {
console.log("No audio stream found");
return;
}
const audio = audios[0];
const indexes = getManagedIndexes(await audio.listSceneIndexes(), "audio");
const runningIndex = indexes.find((index) => !isStoppedStatus(index.status));
if (runningIndex) {
console.log(`Audio indexing already running (${runningIndex.rtstreamIndexId})`);
return;
}
const stoppedIndex = indexes[0];
if (stoppedIndex) {
await stoppedIndex.start();
log(`startAudioIndex: restarted ${stoppedIndex.rtstreamIndexId}`);
console.log(`Started audio indexing (${stoppedIndex.rtstreamIndexId})`);
return;
}
const created = await audio.indexAudio({
name: AUDIO_INDEX_NAME,
prompt,
batchConfig: { type: "time", value: 30 },
autoStartTranscript: false,
});
if (!created) {
console.log("Could not create audio index. Start transcript first with `videodb start-transcript`.");
return;
}
log(`startAudioIndex: created ${created.rtstreamIndexId}`);
console.log(`Started audio indexing (${created.rtstreamIndexId})`);
}
async function stopAudioIndex() {
log("stopAudioIndex");
const { session } = await loadSession();
const audios = session.getRTStream("system_audio");
if (audios.length === 0) {
console.log("No audio stream found");
return;
}
const audio = audios[0];
const indexes = getManagedIndexes(await audio.listSceneIndexes(), "audio");
const runningIndexes = indexes.filter((index) => !isStoppedStatus(index.status));
if (runningIndexes.length === 0) {
console.log("No active audio index found");
return;
}
for (const index of runningIndexes) {
await index.stop();
log(`stopAudioIndex: stopped ${index.rtstreamIndexId}`);
}
console.log(`Stopped ${runningIndexes.length} audio index(es)`);
}
async function startIndexing(args: string[]) {
const visualPrompt = parseFlagValue(args, "--visual-prompt") || DEFAULT_VISUAL_PROMPT;
const audioPrompt = parseFlagValue(args, "--audio-prompt") || DEFAULT_AUDIO_PROMPT;
const engine = parseFlagValue(args, "--engine") || "assemblyai";
await startTranscript(engine);
await startAudioIndex(audioPrompt);
await startVisualIndex(visualPrompt);
}
async function stopIndexing(args: string[]) {
const engine = parseFlagValue(args, "--engine") || "assemblyai";
const mode = parseFlagValue(args, "--mode") === "force" ? "force" : "graceful";
await stopVisualIndex();
await stopAudioIndex();
await stopTranscript(engine, mode);
}
async function search(query: string) {
log(`search: query="${query}"`);
const { session } = await loadSession();
const screens = session.getRTStream("screen");
if (screens.length === 0) {
log("search: no screen stream found");
console.log("No screen stream found");
return;
}
const screen = screens[0];
const indexes = getManagedIndexes(await screen.listSceneIndexes(), "visual");
if (indexes.length === 0) {
console.log("No visual index found. Start one with `videodb start-visual-index`.");
return;
}
const results = await screen.search({ query, resultThreshold: 5 });
const shots = results.getShots();
log(`search: found ${shots.length} results`);
if (shots.length === 0) {
console.log(`No results for "${query}"`);
return;
}
console.log(`Found ${shots.length} result(s) for "${query}":\n`);
for (let i = 0; i < shots.length; i++) {
const shot = shots[i];
await shot.generateStream();
const start = Math.floor(shot.start);
const end = Math.floor(shot.end);
const score = shot.searchScore ? ` (score: ${shot.searchScore.toFixed(2)})` : "";
console.log(`${i + 1}. [${start}s - ${end}s]${score}`);
if (shot.text) console.log(` ${shot.text}`);
if (shot.streamUrl) console.log(` Watch: ${shot.streamUrl}`);
console.log();
}
}
async function summary(hours: number) {
log(`summary: hours=${hours}`);
const { session } = await loadSession();
const screens = session.getRTStream("screen");
if (screens.length === 0) {
log("summary: no screen stream found");
console.log("No screen stream found");
return;
}
const screen = screens[0];
const now = Math.floor(Date.now() / 1000);
const start = now - Math.floor(hours * 3600);
const indexes = getManagedIndexes(await screen.listSceneIndexes(), "visual");
if (indexes.length === 0) {
log("summary: no visual index found");
console.log("No visual index found. Start one with `videodb start-visual-index`.");
return;
}
const index = indexes[0];
if (!index.getScenes) {
console.log("Selected visual index does not support scene retrieval");
return;
}
const result = await index.getScenes(start, now, 1, 50);
if (!result || result.scenes.length === 0) {
log(`summary: no activity in last ${hours} hours`);
console.log(`No activity indexed in the last ${hours} hour(s)`);
return;
}
log(`summary: found ${result.scenes.length} scenes`);
console.log(`Screen activity (last ${hours} hour(s)):\n`);
for (const scene of result.scenes as any[]) {
const time = new Date((scene.start || scene.timestamp) * 1000).toLocaleTimeString();
const text = scene.text || scene.description || JSON.stringify(scene);
console.log(`[${time}] ${text}`);
}
}
async function transcript(hours: number) {
log(`transcript: hours=${hours}`);
const { session } = await loadSession();
const audios = session.getRTStream("system_audio");
if (audios.length === 0) {
log("transcript: no audio stream found");
console.log("No audio stream found");
return;
}
const audio = audios[0];
const now = Math.floor(Date.now() / 1000);
const start = now - Math.floor(hours * 3600);
const data = await audio.getTranscript({ start, end: now, pageSize: 100 });
const segments = (data.segments || data.transcriptions || []) as any[];
log(`transcript: found ${segments.length} segments`);
if (segments.length === 0) {
console.log(`No transcripts in the last ${hours} hour(s)`);
return;
}
console.log(`Transcripts (last ${hours} hour(s)):\n`);
for (const seg of segments) {
const time = new Date((seg.start || seg.timestamp) * 1000).toLocaleTimeString();
console.log(`[${time}] ${seg.text}`);
}
}
async function stream(startTs: number, endTs: number, args: string[]) {
const title = parseFlagValue(args, "--title");
const description = parseFlagValue(args, "--description");
log(
`stream: start=${startTs} end=${endTs}` +
(title ? ` title="${title}"` : "") +
(description ? ` description="${description}"` : "")
);
if (!startTs || !endTs || isNaN(startTs) || isNaN(endTs)) {
console.error(
"Usage: videodb stream <start_unix_timestamp> <end_unix_timestamp> [--title TITLE] [--description DESCRIPTION]"
);
console.error(
'Example: videodb stream 1709740800 1709740810 --title "Checkout flow" --description "OpenClaw browser run"'
);
process.exit(1);
}
if (endTs <= startTs) {
console.error("Error: end timestamp must be greater than start timestamp");
process.exit(1);
}
const { session } = await loadSession();
const screens = session.getRTStream("screen");
if (screens.length === 0) {
log("stream: no screen stream found");
console.log("No screen stream found");
return;
}
const screen = screens[0];
const url = await screen.generateStream(
startTs,
endTs,
title || description ? { title, description } : undefined
);
if (url) {
log(`stream: generated ${url}`);
const duration = endTs - startTs;
console.log(`📹 Screen recording (${duration}s): ${url}`);
console.log(`VideoDB stream URL: ${url}`);
if (screen.playerUrl) {
console.log(`Share this player page with the user: ${screen.playerUrl}`);
}
} else {
log("stream: no URL generated");
console.log("Could not generate stream URL for the specified time range");
}
}
async function main() {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "start-indexing":
await startIndexing(args);
break;
case "stop-indexing":
await stopIndexing(args);
break;
case "start-visual-index":
await startVisualIndex(parseFlagValue(args, "--prompt") || DEFAULT_VISUAL_PROMPT);
break;
case "stop-visual-index":
await stopVisualIndex();
break;
case "start-transcript":
await startTranscript(parseFlagValue(args, "--engine") || "assemblyai");
break;
case "stop-transcript":
await stopTranscript(
parseFlagValue(args, "--engine") || "assemblyai",
parseFlagValue(args, "--mode") === "force" ? "force" : "graceful"
);
break;
case "start-audio-index":
await startAudioIndex(parseFlagValue(args, "--prompt") || DEFAULT_AUDIO_PROMPT);
break;
case "stop-audio-index":
await stopAudioIndex();
break;
case "search":
if (args.length === 0) {
console.error("Usage: videodb search <query>");
process.exit(1);
}
await search(args.join(" "));
break;
case "summary": {
let hours = 0.5;
const idx = args.indexOf("--hours");
if (idx !== -1 && args[idx + 1]) hours = parseFloat(args[idx + 1]);
await summary(hours);
break;
}
case "transcript": {
let hours = 0.5;
const idx = args.indexOf("--hours");
if (idx !== -1 && args[idx + 1]) hours = parseFloat(args[idx + 1]);
await transcript(hours);
break;
}
case "stream": {
const startTs = parseInt(args[0], 10);
const endTs = parseInt(args[1], 10);
await stream(startTs, endTs, args);
break;
}
case "now":
console.log(Math.floor(Date.now() / 1000));
break;
default:
console.log("VideoDB Screen Recording Tool\n");
console.log("Commands:");
console.log(" videodb start-indexing [--visual-prompt P] [--audio-prompt P] [--engine E]");
console.log(" videodb stop-indexing [--engine E] [--mode graceful|force]");
console.log(" videodb start-visual-index [--prompt P]");
console.log(" videodb stop-visual-index");
console.log(" videodb start-transcript [--engine E]");
console.log(" videodb stop-transcript [--engine E] [--mode graceful|force]");
console.log(" videodb start-audio-index [--prompt P]");
console.log(" videodb stop-audio-index");
console.log(
" videodb stream <start> <end> [--title T] [--description D] Generate stream URL"
);
console.log(" videodb search <query> Search screen recordings");
console.log(" videodb summary [--hours N] Get activity summary");
console.log(" videodb transcript [--hours N] Get audio transcripts");
console.log(" videodb now Print current unix timestamp");
}
}
main().catch((err) => {
log(`error: ${err.message}`);
console.error(err.message);
process.exit(1);
});