From cc6e18ff2b002990a30488158c79e8d483ecf154 Mon Sep 17 00:00:00 2001 From: Apple Date: Thu, 29 Jan 2026 09:57:08 +0530 Subject: [PATCH] feat: add quick-win features (search, templates, backup, notifications) Tier 4 quick-win features from the product roadmap: - Conversation search & bookmarks: full-text search across JSONL transcripts with filters (agent, channel, since) and bookmark persistence - Response templates: CRUD with variable expansion, per-channel overrides, per-agent filtering - Backup & export: create/restore with SHA256 integrity, export to Markdown/JSON/JSONL with filtering - Push notifications: priority-based routing, quiet hours, smart grouping, listener API for push providers New CLI commands: moltbot search [--agent|--channel|--since|--limit|--json|--bookmarks] moltbot bookmark [--add|--remove|--list] moltbot templates list|add|remove|show|update moltbot backup create|restore|export moltbot notifications list|read|clear|prefs (alias: notif) 51 new tests across 4 test files. Co-Authored-By: Claude Opus 4.5 --- src/cli/program/command-registry.ts | 51 +- src/cli/program/register.backup.ts | 104 ++++ src/cli/program/register.notifications.ts | 114 +++++ .../register.status-health-sessions.ts | 62 +++ src/cli/program/register.templates.ts | 148 ++++++ src/commands/backup.test.ts | 201 ++++++++ src/commands/backup.ts | 477 ++++++++++++++++++ src/commands/notifications.ts | 180 +++++++ src/commands/sessions-search.test.ts | 177 +++++++ src/commands/sessions-search.ts | 324 ++++++++++++ src/commands/templates.test.ts | 225 +++++++++ src/commands/templates.ts | 323 ++++++++++++ src/notifications/manager.test.ts | 209 ++++++++ src/notifications/manager.ts | 303 +++++++++++ src/notifications/types.ts | 78 +++ 15 files changed, 2975 insertions(+), 1 deletion(-) create mode 100644 src/cli/program/register.backup.ts create mode 100644 src/cli/program/register.notifications.ts create mode 100644 src/cli/program/register.templates.ts create mode 100644 src/commands/backup.test.ts create mode 100644 src/commands/backup.ts create mode 100644 src/commands/notifications.ts create mode 100644 src/commands/sessions-search.test.ts create mode 100644 src/commands/sessions-search.ts create mode 100644 src/commands/templates.test.ts create mode 100644 src/commands/templates.ts create mode 100644 src/notifications/manager.test.ts create mode 100644 src/notifications/manager.ts create mode 100644 src/notifications/types.ts diff --git a/src/cli/program/command-registry.ts b/src/cli/program/command-registry.ts index 0b4618ef0..b805888d8 100644 --- a/src/cli/program/command-registry.ts +++ b/src/cli/program/command-registry.ts @@ -3,6 +3,7 @@ import type { Command } from "commander"; import { agentsListCommand } from "../../commands/agents.js"; import { healthCommand } from "../../commands/health.js"; import { sessionsCommand } from "../../commands/sessions.js"; +import { sessionsSearchCommand } from "../../commands/sessions-search.js"; import { statusCommand } from "../../commands/status.js"; import { defaultRuntime } from "../../runtime.js"; import { getFlagValue, getPositiveIntFlagValue, getVerboseFlag, hasFlag } from "../argv.js"; @@ -10,6 +11,8 @@ import { registerBrowserCli } from "../browser-cli.js"; import { registerConfigCli } from "../config-cli.js"; import { registerMemoryCli, runMemoryStatus } from "../memory-cli.js"; import { registerAgentCommands } from "./register.agent.js"; +import { registerBackupCommands } from "./register.backup.js"; +import { registerNotificationsCommands } from "./register.notifications.js"; import { registerConfigureCommand } from "./register.configure.js"; import { registerMaintenanceCommands } from "./register.maintenance.js"; import { registerMessageCommands } from "./register.message.js"; @@ -17,6 +20,7 @@ import { registerOnboardCommand } from "./register.onboard.js"; import { registerSetupCommand } from "./register.setup.js"; import { registerStatusHealthSessionsCommands } from "./register.status-health-sessions.js"; import { registerSubCliCommands } from "./register.subclis.js"; +import { registerTemplatesCommands } from "./register.templates.js"; import type { ProgramContext } from "./context.js"; type CommandRegisterParams = { @@ -89,6 +93,39 @@ const routeAgentsList: RouteSpec = { }, }; +const routeSearch: RouteSpec = { + match: (path) => path[0] === "search", + run: async (argv) => { + const json = hasFlag(argv, "--json"); + const agent = getFlagValue(argv, "--agent"); + if (agent === null) return false; + const channel = getFlagValue(argv, "--channel"); + if (channel === null) return false; + const since = getFlagValue(argv, "--since"); + if (since === null) return false; + const limitRaw = getFlagValue(argv, "--limit"); + if (limitRaw === null) return false; + const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 50; + const bookmarks = hasFlag(argv, "--bookmarks"); + // Extract positional query argument (first non-flag arg after "search") + const searchIdx = argv.indexOf("search"); + let query = ""; + if (searchIdx >= 0) { + for (let i = searchIdx + 1; i < argv.length; i++) { + if (!argv[i].startsWith("-")) { + query = argv[i]; + break; + } + } + } + await sessionsSearchCommand( + { query, agent, channel, since, limit, json, bookmarks }, + defaultRuntime, + ); + return true; + }, +}; + const routeMemoryStatus: RouteSpec = { match: (path) => path[0] === "memory" && path[1] === "status", run: async (argv) => { @@ -146,12 +183,24 @@ export const commandRegistry: CommandRegistration[] = [ { id: "status-health-sessions", register: ({ program }) => registerStatusHealthSessionsCommands(program), - routes: [routeHealth, routeStatus, routeSessions], + routes: [routeHealth, routeStatus, routeSessions, routeSearch], }, { id: "browser", register: ({ program }) => registerBrowserCli(program), }, + { + id: "templates", + register: ({ program }) => registerTemplatesCommands(program), + }, + { + id: "backup", + register: ({ program }) => registerBackupCommands(program), + }, + { + id: "notifications", + register: ({ program }) => registerNotificationsCommands(program), + }, ]; export function registerProgramCommands( diff --git a/src/cli/program/register.backup.ts b/src/cli/program/register.backup.ts new file mode 100644 index 000000000..80c215c16 --- /dev/null +++ b/src/cli/program/register.backup.ts @@ -0,0 +1,104 @@ +import type { Command } from "commander"; +import { + backupCreateCommand, + backupExportCommand, + backupRestoreCommand, +} from "../../commands/backup.js"; +import { defaultRuntime } from "../../runtime.js"; +import { theme } from "../../terminal/theme.js"; +import { runCommandWithRuntime } from "../cli-utils.js"; +import { formatHelpExamples } from "../help-format.js"; + +export function registerBackupCommands(program: Command) { + const backup = program + .command("backup") + .description("Backup, restore, and export Moltbot data"); + + backup + .command("create") + .description("Create a full backup of config, sessions, and memory") + .option("--output ", "Output directory (default: ~/moltbot-backup-)") + .option("--include-credentials", "Include API keys and tokens in backup", false) + .option("--json", "Output result as JSON", false) + .addHelpText( + "after", + () => + `\n${theme.heading("Examples:")}\n${formatHelpExamples([ + ["moltbot backup create", "Create backup in home directory."], + ["moltbot backup create --output /tmp/backup", "Specify output directory."], + ["moltbot backup create --include-credentials", "Include API keys."], + ])}`, + ) + .action(async (opts) => { + await runCommandWithRuntime(defaultRuntime, async () => { + await backupCreateCommand( + { + output: opts.output as string | undefined, + includeCredentials: Boolean(opts.includeCredentials), + json: Boolean(opts.json), + }, + defaultRuntime, + ); + }); + }); + + backup + .command("restore") + .description("Restore from a backup directory") + .argument("", "Path to backup directory") + .option("--dry-run", "Show what would be restored without making changes", false) + .option("--json", "Output result as JSON", false) + .addHelpText( + "after", + () => + `\n${theme.heading("Examples:")}\n${formatHelpExamples([ + ["moltbot backup restore ~/moltbot-backup-2024-01-15", "Restore from backup."], + ["moltbot backup restore ~/backup --dry-run", "Preview restore without changes."], + ])}`, + ) + .action(async (inputPath, opts) => { + await runCommandWithRuntime(defaultRuntime, async () => { + await backupRestoreCommand( + { + input: String(inputPath), + dryRun: Boolean(opts.dryRun), + json: Boolean(opts.json), + }, + defaultRuntime, + ); + }); + }); + + backup + .command("export") + .description("Export conversations as Markdown, JSON, or JSONL") + .option("--format ", "Export format: markdown, json, jsonl", "markdown") + .option("--output ", "Output file path") + .option("--agent ", "Export only this agent's conversations") + .option("--session ", "Export only this session") + .option("--since ", "Only export messages after this time (e.g. 1h, 2d, 2024-01-01)") + .addHelpText( + "after", + () => + `\n${theme.heading("Examples:")}\n${formatHelpExamples([ + ["moltbot backup export", "Export all conversations as Markdown."], + ["moltbot backup export --format json", "Export as JSON."], + ["moltbot backup export --agent pi --since 7d", "Last 7 days from pi agent."], + ["moltbot backup export --format jsonl --output ./export.jsonl", "JSONL to file."], + ])}`, + ) + .action(async (opts) => { + await runCommandWithRuntime(defaultRuntime, async () => { + await backupExportCommand( + { + format: opts.format as "markdown" | "json" | "jsonl" | undefined, + output: opts.output as string | undefined, + agent: opts.agent as string | undefined, + session: opts.session as string | undefined, + since: opts.since as string | undefined, + }, + defaultRuntime, + ); + }); + }); +} diff --git a/src/cli/program/register.notifications.ts b/src/cli/program/register.notifications.ts new file mode 100644 index 000000000..aa39ad56f --- /dev/null +++ b/src/cli/program/register.notifications.ts @@ -0,0 +1,114 @@ +import type { Command } from "commander"; +import { + notificationsClearCommand, + notificationsListCommand, + notificationsPrefsCommand, + notificationsReadCommand, +} from "../../commands/notifications.js"; +import { defaultRuntime } from "../../runtime.js"; +import { theme } from "../../terminal/theme.js"; +import { runCommandWithRuntime } from "../cli-utils.js"; +import { formatHelpExamples } from "../help-format.js"; + +export function registerNotificationsCommands(program: Command) { + const notif = program + .command("notifications") + .alias("notif") + .description("Manage push notifications and preferences"); + + notif + .command("list") + .description("List notifications") + .option("--json", "Output as JSON", false) + .option("--unread", "Show only unread notifications", false) + .option("--category ", "Filter by category (message, agent-complete, tool-approval, error, system)") + .option("--channel ", "Filter by channel") + .option("--agent ", "Filter by agent") + .option("--limit ", "Maximum results", "50") + .addHelpText( + "after", + () => + `\n${theme.heading("Examples:")}\n${formatHelpExamples([ + ["moltbot notifications list", "List all notifications."], + ["moltbot notifications list --unread", "Show only unread."], + ["moltbot notifications list --category error", "Show only errors."], + ["moltbot notif list --json", "JSON output."], + ])}`, + ) + .action(async (opts) => { + await runCommandWithRuntime(defaultRuntime, async () => { + await notificationsListCommand( + { + json: Boolean(opts.json), + unread: Boolean(opts.unread), + category: opts.category as string | undefined, + channel: opts.channel as string | undefined, + agent: opts.agent as string | undefined, + limit: opts.limit ? Number.parseInt(String(opts.limit), 10) : 50, + }, + defaultRuntime, + ); + }); + }); + + notif + .command("read") + .description("Mark notifications as read") + .option("--id ", "Mark a specific notification as read") + .option("--all", "Mark all notifications as read", false) + .action(async (opts) => { + await runCommandWithRuntime(defaultRuntime, async () => { + await notificationsReadCommand( + { + id: opts.id as string | undefined, + all: Boolean(opts.all), + }, + defaultRuntime, + ); + }); + }); + + notif + .command("clear") + .description("Clear all notifications") + .action(async () => { + await runCommandWithRuntime(defaultRuntime, async () => { + await notificationsClearCommand(defaultRuntime); + }); + }); + + notif + .command("prefs") + .description("View or update notification preferences") + .option("--json", "Output as JSON", false) + .option("--enabled ", "Enable/disable notifications (true/false)") + .option("--min-priority ", "Minimum priority to deliver (low, normal, high, urgent)") + .option("--group-by ", "Group notifications by: channel, agent, category") + .option("--quiet-start