Agent Skills › melandlabs/openloomi

melandlabs/openloomi

GitHub

基于Rust的高性能浏览器自动化CLI,专为AI代理设计。支持导航、表单填写、点击、截图及数据提取。通过快照获取元素引用进行交互,适用于网页测试、登录及任意需程序化Web交互的任务。

15 skills 596

Install All Skills

npx skills add melandlabs/openloomi --all -g -y
More Options

List skills in collection

npx skills add melandlabs/openloomi --list

Skills in Collection (15)

基于Rust的高性能浏览器自动化CLI,专为AI代理设计。支持导航、表单填写、点击、截图及数据提取。通过快照获取元素引用进行交互,适用于网页测试、登录及任意需程序化Web交互的任务。
打开网站或导航到URL 填写表单或输入文本 点击按钮或链接 截取网页屏幕截图 从页面抓取或提取数据 测试Web应用程序 自动执行浏览器操作
skills/agent-browser/SKILL.md
npx skills add melandlabs/openloomi --skill agent-browser -g -y
SKILL.md
Frontmatter
{
    "name": "agent-browser",
    "description": "Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to \"open a website\", \"fill out a form\", \"click a button\", \"take a screenshot\", \"scrape data from a page\", \"test this web app\", \"login to a site\", \"automate browser actions\", or any task requiring programmatic web interaction.",
    "allowed-tools": "Bash(npx agent-browser:*), Bash(agent-browser:*)"
}

Browser Automation with agent-browser

Installation

Global Installation (recommended)

Installs the native Rust binary for maximum performance:

npm install -g agent-browser
agent-browser install  # Download Chromium

This is the fastest option -- commands run through the native Rust CLI directly with sub-millisecond parsing overhead.

Quick Start (no install)

Run directly with npx if you want to try it without installing globally:

npx agent-browser install   # Download Chromium (first time only)
npx agent-browser open example.com

Note: npx routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.

Homebrew (macOS)

brew install agent-browser
agent-browser install  # Download Chromium

Linux Dependencies

On Linux, install system dependencies:

agent-browser install --with-deps
# or manually: npx playwright install-deps chromium

Core Workflow

Every browser automation follows this pattern:

  1. Navigate: agent-browser open <url>
  2. Snapshot: agent-browser snapshot -i (get element refs like @e1, @e2)
  3. Interact: Use refs to click, fill, select
  4. Re-snapshot: After navigation or DOM changes, get fresh refs
agent-browser open https://example.com/form
agent-browser snapshot -i
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"

agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3
agent-browser wait --load networkidle
agent-browser snapshot -i  # Check result

Command Chaining

Commands can be chained with && in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls.

# Chain open + wait + snapshot in one call
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i

# Chain multiple interactions
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "password123" && agent-browser click @e3

# Navigate and capture
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png

When to chain: Use && when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs).

Essential Commands

# Navigation
agent-browser open <url>              # Navigate (aliases: goto, navigate)
agent-browser close                   # Close browser

# Snapshot
agent-browser snapshot -i             # Interactive elements with refs (recommended)
agent-browser snapshot -i -C          # Include cursor-interactive elements (divs with onclick, cursor:pointer)
agent-browser snapshot -s "#selector" # Scope to CSS selector

# Interaction (use @refs from snapshot)
agent-browser click @e1               # Click element
agent-browser click @e1 --new-tab     # Click and open in new tab
agent-browser fill @e2 "text"         # Clear and type text
agent-browser type @e2 "text"         # Type without clearing
agent-browser select @e1 "option"     # Select dropdown option
agent-browser check @e1               # Check checkbox
agent-browser press Enter             # Press key
agent-browser scroll down 500         # Scroll page

# Get information
agent-browser get text @e1            # Get element text
agent-browser get url                 # Get current URL
agent-browser get title               # Get page title

# Wait
agent-browser wait @e1                # Wait for element
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --url "**/page"    # Wait for URL pattern
agent-browser wait 2000               # Wait milliseconds

# Capture
agent-browser screenshot              # Screenshot to temp dir
agent-browser screenshot --full       # Full page screenshot
agent-browser screenshot --annotate   # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf          # Save as PDF

# Diff (compare page states)
agent-browser diff snapshot                          # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt    # Compare current vs saved file
agent-browser diff screenshot --baseline before.png  # Visual pixel diff
agent-browser diff url <url1> <url2>                 # Compare two pages
agent-browser diff url <url1> <url2> --wait-until networkidle  # Custom wait strategy
agent-browser diff url <url1> <url2> --selector "#main"  # Scope to element

Common Patterns

Form Submission

agent-browser open https://example.com/signup
agent-browser snapshot -i
agent-browser fill @e1 "Jane Doe"
agent-browser fill @e2 "jane@example.com"
agent-browser select @e3 "California"
agent-browser check @e4
agent-browser click @e5
agent-browser wait --load networkidle

Authentication with State Persistence

# Login once and save state
agent-browser open https://app.example.com/login
agent-browser snapshot -i
agent-browser fill @e1 "$USERNAME"
agent-browser fill @e2 "$PASSWORD"
agent-browser click @e3
agent-browser wait --url "**/dashboard"
agent-browser state save auth.json

# Reuse in future sessions
agent-browser state load auth.json
agent-browser open https://app.example.com/dashboard

Session Persistence

# Auto-save/restore cookies and localStorage across browser restarts
agent-browser --session-name myapp open https://app.example.com/login
# ... login flow ...
agent-browser close  # State auto-saved to ~/.agent-browser/sessions/

# Next time, state is auto-loaded
agent-browser --session-name myapp open https://app.example.com/dashboard

# Encrypt state at rest
export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)
agent-browser --session-name secure open https://app.example.com

# Manage saved states
agent-browser state list
agent-browser state show myapp-default.json
agent-browser state clear myapp
agent-browser state clean --older-than 7

Data Extraction

agent-browser open https://example.com/products
agent-browser snapshot -i
agent-browser get text @e5           # Get specific element text
agent-browser get text body > page.txt  # Get all page text

# JSON output for parsing
agent-browser snapshot -i --json
agent-browser get text @e1 --json

Parallel Sessions

agent-browser --session site1 open https://site-a.com
agent-browser --session site2 open https://site-b.com

agent-browser --session site1 snapshot -i
agent-browser --session site2 snapshot -i

agent-browser session list

Connect to Existing Chrome

# Auto-discover running Chrome with remote debugging enabled
agent-browser --auto-connect open https://example.com
agent-browser --auto-connect snapshot

# Or with explicit CDP port
agent-browser --cdp 9222 snapshot

Color Scheme (Dark Mode)

# Persistent dark mode via flag (applies to all pages and new tabs)
agent-browser --color-scheme dark open https://example.com

# Or via environment variable
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com

# Or set during session (persists for subsequent commands)
agent-browser set media dark

Visual Browser (Debugging)

agent-browser --headed open https://example.com
agent-browser highlight @e1          # Highlight element
agent-browser record start demo.webm # Record session
agent-browser profiler start         # Start Chrome DevTools profiling
agent-browser profiler stop trace.json # Stop and save profile (path optional)

Local Files (PDFs, HTML)

# Open local files with file:// URLs
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
agent-browser screenshot output.png

iOS Simulator (Mobile Safari)

# List available iOS simulators
agent-browser device list

# Launch Safari on a specific device
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com

# Same workflow as desktop - snapshot, interact, re-snapshot
agent-browser -p ios snapshot -i
agent-browser -p ios tap @e1          # Tap (alias for click)
agent-browser -p ios fill @e2 "text"
agent-browser -p ios swipe up         # Mobile-specific gesture

# Take screenshot
agent-browser -p ios screenshot mobile.png

# Close session (shuts down simulator)
agent-browser -p ios close

Requirements: macOS with Xcode, Appium (npm install -g appium && appium driver install xcuitest)

Real devices: Works with physical iOS devices if pre-configured. Use --device "<UDID>" where UDID is from xcrun xctrace list devices.

Diffing (Verifying Changes)

Use diff snapshot after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session.

# Typical workflow: snapshot -> action -> diff
agent-browser snapshot -i          # Take baseline snapshot
agent-browser click @e2            # Perform action
agent-browser diff snapshot        # See what changed (auto-compares to last snapshot)

For visual regression testing or monitoring:

# Save a baseline screenshot, then compare later
agent-browser screenshot baseline.png
# ... time passes or changes are made ...
agent-browser diff screenshot --baseline baseline.png

# Compare staging vs production
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot

diff snapshot output uses + for additions and - for removals, similar to git diff. diff screenshot produces a diff image with changed pixels highlighted in red, plus a mismatch percentage.

Timeouts and Slow Pages

The default Playwright timeout is 60 seconds for local browsers. For slow websites or large pages, use explicit waits instead of relying on the default timeout:

# Wait for network activity to settle (best for slow pages)
agent-browser wait --load networkidle

# Wait for a specific element to appear
agent-browser wait "#content"
agent-browser wait @e1

# Wait for a specific URL pattern (useful after redirects)
agent-browser wait --url "**/dashboard"

# Wait for a JavaScript condition
agent-browser wait --fn "document.readyState === 'complete'"

# Wait a fixed duration (milliseconds) as a last resort
agent-browser wait 5000

When dealing with consistently slow websites, use wait --load networkidle after open to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with wait <selector> or wait @ref.

Session Management and Cleanup

When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:

# Each agent gets its own isolated session
agent-browser --session agent1 open site-a.com
agent-browser --session agent2 open site-b.com

# Check active sessions
agent-browser session list

Always close your browser session when done to avoid leaked processes:

agent-browser close                    # Close default session
agent-browser --session agent1 close   # Close specific session

If a previous session was not closed properly, the daemon may still be running. Use agent-browser close to clean it up before starting new work.

Ref Lifecycle (Important)

Refs (@e1, @e2, etc.) are invalidated when the page changes. Always re-snapshot after:

  • Clicking links or buttons that navigate
  • Form submissions
  • Dynamic content loading (dropdowns, modals)
agent-browser click @e5              # Navigates to new page
agent-browser snapshot -i            # MUST re-snapshot
agent-browser click @e1              # Use new refs

Annotated Screenshots (Vision Mode)

Use --annotate to take a screenshot with numbered labels overlaid on interactive elements. Each label [N] maps to ref @eN. This also caches refs, so you can interact with elements immediately without a separate snapshot.

agent-browser screenshot --annotate
# Output includes the image path and a legend:
#   [1] @e1 button "Submit"
#   [2] @e2 link "Home"
#   [3] @e3 textbox "Email"
agent-browser click @e2              # Click using ref from annotated screenshot

Use annotated screenshots when:

  • The page has unlabeled icon buttons or visual-only elements
  • You need to verify visual layout or styling
  • Canvas or chart elements are present (invisible to text snapshots)
  • You need spatial reasoning about element positions

Semantic Locators (Alternative to Refs)

When refs are unavailable or unreliable, use semantic locators:

agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find role button click --name "Submit"
agent-browser find placeholder "Search" type "query"
agent-browser find testid "submit-btn" click

JavaScript Evaluation (eval)

Use eval to run JavaScript in the browser context. Shell quoting can corrupt complex expressions -- use --stdin or -b to avoid issues.

# Simple expressions work with regular quoting
agent-browser eval 'document.title'
agent-browser eval 'document.querySelectorAll("img").length'

# Complex JS: use --stdin with heredoc (RECOMMENDED)
agent-browser eval --stdin <<'EVALEOF'
JSON.stringify(
  Array.from(document.querySelectorAll("img"))
    .filter(i => !i.alt)
    .map(i => ({ src: i.src.split("/").pop(), width: i.width }))
)
EVALEOF

# Alternative: base64 encoding (avoids all shell escaping issues)
agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)"

Why this matters: When the shell processes your command, inner double quotes, ! characters (history expansion), backticks, and $() can all corrupt the JavaScript before it reaches agent-browser. The --stdin and -b flags bypass shell interpretation entirely.

Rules of thumb:

  • Single-line, no nested quotes -> regular eval 'expression' with single quotes is fine
  • Nested quotes, arrow functions, template literals, or multiline -> use eval --stdin <<'EVALEOF'
  • Programmatic/generated scripts -> use eval -b with base64

Configuration File

Create agent-browser.json in the project root for persistent settings:

{
  "headed": true,
  "proxy": "http://localhost:8080",
  "profile": "./browser-data"
}

Priority (lowest to highest): ~/.agent-browser/config.json < ./agent-browser.json < env vars < CLI flags. Use --config <path> or AGENT_BROWSER_CONFIG env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., --executable-path -> "executablePath"). Boolean flags accept true/false values (e.g., --headed false overrides config). Extensions from user and project configs are merged, not replaced.

Deep-Dive Documentation

Reference When to Use
references/commands.md Full command reference with all options
references/snapshot-refs.md Ref lifecycle, invalidation rules, troubleshooting
references/session-management.md Parallel sessions, state persistence, concurrent scraping
references/authentication.md Login flows, OAuth, 2FA handling, state reuse
references/video-recording.md Recording workflows for debugging and documentation
references/profiling.md Chrome DevTools profiling for performance analysis
references/proxy-support.md Proxy configuration, geo-testing, rotating proxies

Ready-to-Use Templates

Template Description
templates/form-automation.sh Form filling with validation
templates/authenticated-session.sh Login once, reuse state
templates/capture-workflow.sh Content extraction with screenshots
./templates/form-automation.sh https://example.com/form
./templates/authenticated-session.sh https://app.example.com/login
./templates/capture-workflow.sh https://example.com ./output
通过 Composio 集成1000+外部应用。支持CLI直接执行任务(如发邮件、创建Issue)或SDK构建AI代理与App,实现跨工具自动化与数据交互。
用户希望访问或交互外部应用(如Gmail, Slack等) 需要自动化外部服务任务(发送消息、创建工单等) 开发集成外部工具的AI代理或应用程序
skills/composio/SKILL.md
npx skills add melandlabs/openloomi --skill composio -g -y
SKILL.md
Frontmatter
{
    "name": "composio",
    "tags": [
        "composio",
        "tool-router",
        "agents",
        "mcp",
        "tools",
        "api",
        "automation",
        "cli"
    ],
    "description": "Use 1000+ external apps via Composio - either directly through the CLI or by building AI agents and apps with the SDK"
}

When to Apply

  • User wants to access or interact with external apps (Gmail, Slack, GitHub, Notion, etc.)
  • User wants to automate a task using an external service (send email, create issue, post message)
  • Building an AI agent or app that integrates with external tools
  • Multi-user apps that need per-user connections to external services

Setup

Check if the CLI is installed; if not, install it:

curl -fsSL https://composio.dev/install | bash

After installation, restart your terminal or source your shell config, then authenticate:

composio login       # OAuth; interactive org/project picker (use -y to skip)
composio whoami      # verify org_id, project_id, user_id

For agents without direct browser access: composio login --no-wait | jq to get URL/key, share URL with user, then composio login --key <cli_key> --no-wait once they complete login.


1. Use Apps via Composio CLI

Use this when: The user wants to take action on an external app directly — no code writing needed. The agent uses the CLI to search, connect, and execute tools on behalf of the user.

Key commands (new top-level aliases):

  • composio search "<query>" — find tools by use case
  • composio execute "<TOOL_SLUG>" -d '{...<input params>}' — execute a tool
  • composio link [toolkit] — connect a user account to an app (agents: always use --no-wait for non-interactive mode)
  • composio listen — listen for real-time trigger events

Typical workflow: search → link (if needed) → execute

Full reference: Composio CLI Guide


2. Building Apps and Agents with Composio

Use this when: Writing code — an AI agent, app, or backend service that integrates with external tools via the Composio SDK.

Run this first inside the project directory to set up the API key:

composio init

Full reference: Building with Composio

通过 cua-driver 自动化 macOS 原生应用,支持快照 AX 树、点击输入及验证。核心约束是严禁切换前台焦点,禁止使用 open 等激活命令,确保用户当前工作流不被打断。
用户要求操作或自动化 macOS 应用程序(如打开文件、点击按钮) 需要在后台驱动 GUI 任务而不干扰当前前台应用
skills/cua-driver/SKILL.md
npx skills add melandlabs/openloomi --skill cua-driver -g -y
SKILL.md
Frontmatter
{
    "name": "cua-driver",
    "description": "Drive a native macOS app via the cua-driver CLI (default) or MCP server — snapshot its AX tree, click\/type\/scroll by element_index, verify via re-snapshot. Use when the user asks you to operate, drive, automate, or perform a GUI task in a real macOS application on the host (e.g. \"open a file in TextEdit\", \"navigate to \/Applications in Finder\", \"click the Save button in Numbers\")."
}

cua-driver

Orchestrates macOS app automation via cua-driver. Whenever a user asks to drive a native macOS app, follow the loop in this skill rather than calling tools ad-hoc — the snapshot-before-action invariant is not optional and silently breaks if you skip it.

The no-foreground contract — read this first

The user's frontmost app MUST NOT change. This is the whole reason cua-driver exists. Users pay for the right to keep typing in their editor while an agent drives another app in the background. Violate this rule and every other nice property the driver gives you (no cursor warp, no Space switch, no window raise) stops mattering — you just shipped the Accessibility Inspector with extra steps.

Before running any shell command, ask: "does this raise, activate, foreground, or make-key any app?" If yes, don't run it. Every one of the commands below activates the target on macOS and is therefore forbidden unless the user explicitly asked for frontmost state:

  • Every form of the open CLI — open -a <App>, open -b <bundle-id>, open <file>, open <path-to-App.app>, open <url> — always activates. macOS routes all forms through LaunchServices, which unhides and foregrounds the target regardless of whether you passed an app name, a bundle id, a document, a URL, or the bundle path itself. The activation happens even when the only intent was "start the process." Never use open for any app launch. This includes launching a just-built .app from a local build dir (e.g. open build/Build/Products/Debug/MyApp.app) — resolve the CFBundleIdentifier from Info.plist and use launch_app with that id. See "The narrow carve-out" below for why launch_app is safe even when the app internally calls NSApp.activate.

  • osascript -e 'tell application "X" to activate' — activates by design. Same for ... to open <file>, ... to launch, and anything with activate in the tell block.

  • osascript -e 'tell application "System Events" to ... frontmost' in a mutating form (setting frontmost rather than reading it).

  • AppleScript files that invoke activate, launch, or open against the target app.

  • cliclick (moves the user's real cursor to the target coords before clicking — a focus-steal-equivalent even if the app's window state is unchanged).

  • CGEventPost with cghidEventTap targeting a coordinate over a different app's window (warps the cursor, possibly activates on hit).

  • AppleScriptTask, NSAppleScript, Process wrapping osascript that contains any of the above.

  • NSRunningApplication.activate(options:) called from your own helper binary — same class.

  • Dock clicks and any open invocation (see the first bullet — every form of open goes through LaunchServices which activates, full stop).

  • Keyboard shortcuts that semantically mean "focus here" — most notably Chrome / Safari / Arc's ⌘L (focus omnibox) and Finder's ⌘⇧G (Go to Folder). These aren't pure key events — the receiving app interprets "user wants to type here" as activation intent and raises its window to be key. Even when delivered to a backgrounded pid via hotkey, the downstream app pulls focus. For omnibox navigation specifically, the correct path is launch_app({bundle_id: "com.google.Chrome", urls: ["https://…"]}) — no omnibox dance, no ⌘L, no focus-steal. Do NOT try set_value on the omnibox: Chrome's commit logic requires a "user-typed" signal that neither an AX value write nor CGEvent.postToPid keystrokes supply from a backgrounded pid — the URL lands in the field but Return fires as a no-op. See WEB_APPS.md → "Navigate to a URL" for the full pattern. The general principle: a shortcut that says "put my cursor inside this app" is a focus-steal; a shortcut that says "do this thing" (copy, save, quit) is fine.

  • Tab-switching shortcuts in browsers (⌘1..⌘9, ⌘], ⌘[, ⌘⇧[, ⌘⇧]) are visibly disruptive even when delivered to a backgrounded pid. The app's key handler processes the shortcut, the window re-renders the new tab's content, the user sees their tabs flipping. There is no AX-only workaround: page content (HTML, form state, AXWebArea) populates only for the focused tab; inspecting a background tab requires activating it, which is the visible flip. Observed with Dia; the same mechanic applies to every Chromium-family browser (Chrome, Arc, Brave, Edge).

    Prefer the windows-over-tabs pattern: for each URL you need to drive backgrounded, use launch_app({bundle_id, urls: [url]}) — browsers open each URL in a new window. Each window has its own window_id, its own AX tree, and can be inspected / interacted with via element_index without activating or switching anything. Tabs are a UX grouping for humans; cua-driver workflows should default to windows. See WEB_APPS.md → "Tabs vs windows" for the full pattern.

    Tab-title enumeration (read-only) IS safe — walk a window's toolbar AX tree for AXTab / AXRadioButton children and read their AXTitles. Tab switching (activating one) is not.

Reading frontmost state is fine (osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'). Mutating it is not.

Corollary — the AXMenuBar rule. AXMenuBarItem + AXPick dispatches at the AX layer regardless of which app is frontmost, but macOS's on-screen menu bar always belongs to the frontmost app. If you drive a backgrounded app's menu bar, the AX call succeeds but the viewer sees the dispatch rendered over the frontmost app's menu bar — confusing in any observed session and routinely a silent no-op too, because action menu items go DISABLED when their owning app isn't the key window. So: only use menu-bar navigation when the target is already frontmost. For backgrounded targets, read state via in-window AX (window title, toolbar AXStaticText) and dispatch via in-window element_index or pixel clicks — both paths are frontmost-insensitive. Full rationale in "Navigating native menu bars" below.

"Open <app>" in user speech means launch, not activate. cua-driver launch_app is the one correct path for process startup — it's idempotent (no-op on a running app), returns the pid, and has an internal FocusRestoreGuard that catches NSApp.activate(ignoringOtherApps:) calls the target makes during application(_:open:) and clobbers the frontmost back to what it was before the launch. That guard is why launch_app with urls (e.g. {"bundle_id": "com.colliderli.iina", "urls": ["~/video.mp4"]}) is safe even for apps that normally foreground on media-load (Chrome, Electron, media players).

Defaults — always prefer cua-driver over shell shims

Default transport is the cua-driver CLIBash shelling out to cua-driver <tool-name> '<JSON-args>'. MCP tools (prefix mcp__cua-driver__*) only when the user explicitly asks for them. CLI wins because it picks up rebuilds instantly, failures are easier to diagnose, and there's no per-tool schema-load overhead.

Every reference to click(...), get_window_state(...) etc. in this skill means cua-driver click '{...}' — translate to MCP form only when MCP is requested.

Claude Code computer-use compatibility mode

For normal Claude Code use, keep the default CLI or cua-driver MCP server path above. If the user explicitly wants Claude Code's vision/computer-use-style flow, they can register:

claude mcp add --transport stdio cua-computer-use -- cua-driver mcp --claude-code-computer-use-compat

Observation: Claude Code vision flows appear to treat a screenshot MCP tool as the image-grounding anchor. This compatibility mode keeps the normal CuaDriver tools and changes only screenshot. The compatibility screenshot requires pid and window_id, captures only that target window, and returns the window-local pixel coordinate frame. Start with launch_app or list_windows, then call screenshot({pid, window_id}); do not assume desktop coordinates or a full-screen capture.

Use MCP for this Claude Code vision/computer-use-style path. Do not shell out to cua-driver screenshot as a substitute: CLI screenshots still work as CuaDriver calls, but they do not expose the mcp__cua-computer-use__screenshot tool name that Claude Code appears to use as the image-grounding cue.

Intent → tool mapping. If you find yourself reaching for the right column, something has gone wrong — re-read "The no-foreground contract" above:

Intent Use Don't use
Open / launch an app launch_app({bundle_id}) or launch_app({bundle_id, urls:[...]}) open -a, osascript 'tell app … to launch/activate/open'
Find a pid list_apps or launch_app's return pgrep, ps, osascript frontmost
Enumerate an app's windows list_windows({pid}) — or read the windows array launch_app already returns osascript 'every window of app …'
Click / type / scroll / keys click, type_text, scroll, press_key, hotkey osascript, cliclick, raw CGEvent, open <url>
Drag / drag-and-drop / marquee select drag({pid, from_x, from_y, to_x, to_y}) (pixel-only — macOS AX has no semantic drag) cliclick dd:, osascript drag
Screenshot screenshot or the PNG in get_window_state screencapture
Quit an app ask the user first, then hotkey({pid, keys:["cmd","q"]}) kill, killall, pkill
Hand a file/URL to an app launch_app({bundle_id, urls:[<path>]}) open -a <App> <path>, open <url>

The narrow carve-out

The only legitimate use of osascript -e 'tell app X to activate' is when the user explicitly asked for frontmost state ("bring Chrome to the front", "make it frontmost", "I want to see X"). Reaching for it because a tool call returned something confusing is wrong — that's the skill's classic foot-in-the-door failure mode and it steals focus every time.

When a cua-driver call surprises you, diagnose cua-driver first:

  • Tiny screenshot / empty tree_markdown? Check cua-driver get_configcapture_mode. Default "som" returns both the AX tree and screenshot. "vision" omits the AX tree (PNG only), "ax" omits the PNG. If a snapshot lacks a tree, capture_mode is almost certainly "vision" — either reason purely from the PNG or flip to "som" / "ax" via set_config.
  • has_screenshot: false? The window capture failed (transient race against a close, or the window has no backing store yet). Re-snapshot; if persistent, pick a different window_id via list_windows.
  • Invalid element_index / No cached AX state? You either skipped get_window_state this turn or passed a different window_id than the one the snapshot cached against. The cache is keyed on (pid, window_id) — indices don't carry across windows of the same app. Re-snapshot with the same window_id you're about to click in.
  • Sparse Chromium AX tree? Retry get_window_state once — the tree populates on second call.

Only after those are ruled out, and only if the user's action genuinely needs frontmost state, fall through to the activate fallback. Always name the focus steal in your response ("I'll briefly bring Chrome to the front because …").

Self-check pattern

Before every Bash call whose command line touches any macOS app (launching, opening, clicking, typing, scripting, screenshotting), run the self-check:

  1. Does this command foreground the target? If yes — stop and translate to the cua-driver equivalent from the mapping table.
  2. Does this command move the user's real cursor? (cliclick, any CGEventPost at cghidEventTap over another app's window). If yes — stop; use click({pid, x, y}) which routes per-pid via SkyLight and never warps the cursor.
  3. Does this command bypass cua-driver entirely? (osascript mutating GUI state, AppleScript files, external helpers.) If yes — stop; find the cua-driver tool that does the intent.

If all three are "no," the command is safe. If you can't answer, default to stop and ask rather than proceed. A single open -a run by accident kills the demo, the trust, and the user's in-flight editor state.

Prerequisites — check before starting

  1. cua-driver is on $PATH (which cua-driver). If not, point the user at scripts/install-local.sh and stop.
  2. Run cua-driver check_permissions (with the daemon up — see step 3). The default behavior also raises the system permission dialogs for any missing grants, so the user can grant on the spot. If either grant still reads false after that (user dismissed the dialog), tell them to open System Settings → Privacy & Security and grant Accessibility and Screen Recording to CuaDriver.app, then stop. Pass '{"prompt":false}' for a purely read-only status check that won't steal focus.
  3. Start the daemon with open -n -g -a CuaDriver --args serve (the recommended form — goes through LaunchServices so TCC attributes the process to CuaDriver.app). cua-driver serve & also works; the CLI auto-relaunches through open -n -g -a CuaDriver when it detects a wrong-TCC context (any IDE-spawned shell: Claude Code, Cursor, VS Code, Conductor). Verify with cua-driver status.

Using cua-driver from the shell

Tool names are snake_case, management subcommands are kebab-case — no ambiguity. Tools invoked as cua-driver <tool-name> '<JSON-args>'. Management subcommands:

  • open -n -g -a CuaDriver --args serve — start persistent daemon (required for element_index workflows; without it each CLI invocation spawns a fresh process and the per-pid element cache dies between calls). cua-driver serve & also works — the CLI auto-relaunches via open when the shell's TCC context is wrong. Pass --no-relaunch / CUA_DRIVER_NO_RELAUNCH=1 to opt out.
  • cua-driver stop / status
  • cua-driver list-tools, describe <tool>
  • cua-driver recording start|stop|status — see RECORDING.md

Canonical multi-step workflow:

open -n -g -a CuaDriver --args serve
cua-driver launch_app '{"bundle_id":"com.apple.calculator"}'
# → {pid: 844, windows: [{window_id: 10725, ...}]}
cua-driver get_window_state '{"pid":844,"window_id":10725}'
cua-driver click '{"pid":844,"window_id":10725,"element_index":14}'
cua-driver stop

Agent cursor overlay

Visual cursor overlay for demos and screen recordings. Default: enabled. Toggle with cua-driver set_agent_cursor_enabled '{"enabled":true|false}'. A triangle pointer Bezier-glides to each click target, ring-ripples on landing, idle-hides after ~1.5s. Motion knobs: set_agent_cursor_motion takes any subset of start_handle, end_handle, arc_size, arc_flow, spring — tuneable at runtime, persisted to config.

Requires an AppKit runloop, which cua-driver serve / mcp bootstraps. One-shot CLI invocations skip the overlay entirely.

The core invariant — snapshot before AND after every action

Every action MUST be bracketed by get_window_state(pid, window_id):

  • Before — the pre-action snapshot resolves the element_index you're about to use. Indices from previous turns are stale; the server replaces the element index map on every snapshot, keyed on (pid, window_id). Indices from turn N don't resolve in turn N+1, and indices from window A don't resolve against window B of the same app. Skip this and element-indexed actions fail with No cached AX state.
  • After — the post-action snapshot verifies the action actually landed. Without it you can't tell a silent no-op from a real effect. The AX tree change (new value, new window, disappeared menu, disabled button, etc.) is your evidence that the action fired. If nothing changed, the action probably failed silently — say so, don't assume success.

This applies to pixel clicks too — re-snapshot after to confirm the click landed on the intended target.

Why window selection is the caller's job now

get_app_state used to pick a window for you via a max-area heuristic that returned the wrong surface on apps with large off-screen utility panels. Concrete reproducer: IINA's OpenSubtitles helper (600×432 off-screen) out-area'd the visible 320×240 player window, so get_app_state(pid) screenshot'd the invisible panel and clicks landed there silently. The new get_window_state(pid, window_id) makes the caller name the window explicitly — the driver validates that the window belongs to the pid and is on the current Space, then snapshots exactly what was asked for. Enumerate candidates via list_windows or read the windows array launch_app already returns.

Behavior matrix

Two orthogonal axes shape what the agent can do.

capture_mode → addressing mode

capture_mode get_window_state returns Use for actions
som (default) tree + screenshot element_index preferred; pixel fallback
ax tree only (no PNG) element_index only
vision PNG only (no tree) pixel only — see SCREENSHOT.md

vision was renamed from screenshot — the old name still decodes as a deprecated alias, so an on-disk "capture_mode": "screenshot" keeps working. Default is som so element_index clicks work the first time a user calls get_window_state; the other modes are opt-in when the caller specifically doesn't want one half of the work. Note the tool named screenshot is separate (raw PNG, no AX walk) and unrelated to the capture mode.

When a snapshot looks wrong (tiny screenshot / empty tree), check cua-driver get_config for capture_mode before anything else.

Pure-vision mode has its own caveats — Claude Code's vision pipeline downsamples dense text aggressively, so pixel grounding takes multiple correction cycles on text-heavy UIs. Read SCREENSHOT.md before driving anything in that mode; it documents the iterate/annotate/verify recipe plus the JPEG-over-PNG finding.

Window state → what works

state get_window_state click/set_value (AX) press_key commit (Return/Space/Tab) pixel click
frontmost
backgrounded / visible
minimized (Dock genie) ✅ (no deminiaturize — AX actions fire on the minimized window in place) ❌ silent no-op / system beep — use set_value or click equivalent ❌ no on-screen bounds
hidden (hides=true / NSApp.hide) depends
on another Space ⚠️ AX tree often stripped to menu-bar-only on SwiftUI apps (System Settings) — AppKit apps usually fine. Response carries off_space: true + window_space_ids so you can detect it ❌ window not in current-Space list

Critical cell — minimized + keyboard commit. The keystroke reaches the app but AX focus doesn't propagate to renderer focus on a minimized window. Workarounds in order of preference: set_value to write the field's entire value directly, or AX-click a commit-equivalent button (Go, Submit, checkbox). Tell the user the window needs to un-minimize only as a last resort.

The canonical loop

launch_app(target)
  → pick window_id from the returned `windows` array
    (or call list_windows(pid) separately)
  → get_window_state(pid, window_id)
    → [act]  # every action also takes (pid, window_id)
  → get_window_state(pid, window_id) → verify

launch_app now returns a windows array alongside the pid, so the common case collapses to two calls (launch_appget_window_state) without a separate list_windows hop.

1. Resolve target pid — always via launch_app

Always start with launch_app, whether or not the target is already running. It's idempotent (relaunching returns the existing pid with no side effects) and gives you the pid in one call — no list_apps hop.

  • launch_app({bundle_id: "com.apple.finder"}) — preferred, unambiguous.
  • launch_app({name: "Calculator"}) — when bundle_id isn't known.

launch_app is a hidden-launch primitive by design — that's the entire point of cua-driver: agents drive apps in the background while the user keeps typing in their real foreground app. The target's window is initialized (AX tree fully populated, clickable via element_index, the pid appears in list_apps) but not drawn on screen. The driver never activates or unhides apps on its own; that would violate the no-foreground contract the whole driver exists to protect.

If the user explicitly wants the window visible (usually for a demo or recording), they unhide it themselves — Dock click, Cmd-Tab, or Spotlight. Do not reach for open / osascript activate as a shortcut to make the window visible; those paths break the backgrounded invariant on every call, not just the call that "needed" the foreground. Say out loud what the user needs to do ("click the Todo app in your Dock to bring it forward") and let them do it.

Never shell out to any form of open (including open <path-to-App.app> for a just-built binary — resolve the bundle id from Info.plist and use launch_app with that), osascript 'tell app … to launch/open', or similar. Those paths activate the target, bypass the driver's focus-restore guard, and require a Bash permission prompt the agent loop shouldn't be burning on app launch. See "Prefer cua-driver tools over shell shims" above for the full intent → tool mapping.

list_apps is for app-level discovery (answering "what's installed / running / frontmost?") — not part of the core action loop. Skip it in the loop. For window-level questions — "does this app have a visible window?", "which Space is this window on?", "which of this pid's windows is the main one?" — call list_windows instead; the app record doesn't carry window state on purpose. In the common single-window case you can skip list_windows entirely and read the windows array that launch_app already returned.

2. Snapshot and act by element_index

Call get_window_state({pid, window_id}) with the window_id from launch_app's windows array (or a fresh list_windows({pid}) if you're interacting with a long-lived process). The default som capture_mode returns both the AX tree and screenshot, so the canonical loop works immediately without any config change. The rest of this section walks through som mode. If you're in vision mode (PNG only, no AX tree), flip back: cua-driver set_config '{"key": "capture_mode", "value": "som"}'.

In som mode (the default) the response carries:

  • tree_markdown — every actionable element tagged [N]. That N is the element_index. The tree can be very large (Finder is ~1600 elements, ~190 KB); when it exceeds token limits the MCP harness saves it to a file and returns the path. Use Bash + jq -r '.tree_markdown' + grep to pull the section you need.
  • screenshot_file_path — absolute path to the saved screenshot when screenshot_out_file was passed. Absent otherwise.
  • screenshot_width / _height / _scale_factor — dimensions of the captured image. Present whenever a screenshot was taken. Getting the screenshot as a file (CLI and context-constrained agents):
# write to file — stdout stays readable (AX tree / summary only, no base64)
cua-driver get_window_state '{"pid":N,"window_id":W,"screenshot_out_file":"/tmp/shot.jpg"}'

# CLI --screenshot-out-file flag is equivalent and works for all capture modes
cua-driver get_window_state '{"pid":N,"window_id":W}' --screenshot-out-file /tmp/shot.jpg

Pass screenshot_out_file when using get_window_state via CLI or from an agent whose context window can't absorb ~31 KB of inline base64 (e.g. OpenCode with a local Ollama model). The MCP image content block is omitted from the response when this param is set — the model receives only the AX tree and screenshot_file_path, then reads the image from disk.

Reason over both the tree AND the screenshot — they're complementary, not redundant. In som mode every turn's get_window_state gives you both halves and you should pull signal from each:

  • The AX tree tells you what's clickable — roles, labels, element_index handles, advertised actions, parent-child structure. This is the ground truth for dispatching.
  • The screenshot tells you which one — the tree often has many buttons with similar or empty labels ("Delete", "OK", anonymous UUID-labeled buttons, five AXStaticText = " "), and visual context disambiguates. Captions, colors, layout relationships visible in pixels often don't show up in the AX tree at all (especially in Chromium / Electron / web content).

Canonical pattern: look at the screenshot to decide "the blue Subscribe button on the top-right of the video card", then walk the tree to find the matching AXButton and dispatch by its element_index. Don't try to do it from just the tree — you'll pick the wrong element when labels repeat. Don't try to do it from just the screenshot — you lose the reliable AX-action path and the safe backgrounded-dispatch.

Reach for pixel coordinates only when the target is a canvas / video / WebGL / custom-drawn surface that isn't in the AX tree (see Pixel-coordinate clicks below).

The actions=[...] list on each element is advisory, not authoritative. cua-driver does not pre-flight check against it — click({pid, element_index}) always attempts AXPress (or the action you pass) and surfaces whatever the target returns. Many apps accept AXPress on elements that don't advertise it — Chrome's omnibox suggestion AXMenuItem is a live example. Try the click first — pivot only on the returned AX error code.

Dispatch table (every row assumes a (pid, window_id) pair from the last get_window_state; window_id is required alongside element_index, ignored on pixel-only forms unless you want to anchor the conversion against a specific window):

Intent Tool Notes
List an app's windows list_windows({pid}) returns window_id, title, bounds, z_index, is_on_screen, on_current_space. Already included in launch_app's response — only call this for long-lived pids
Snapshot a window get_window_state({pid, window_id}) returns tree_markdown + screenshot_*; populates the (pid, window_id) element_index cache
Left click click({pid, window_id, element_index}) default action: "press". Pixel form: click({pid, x, y}) (window_id optional — when supplied, pinpoints the anchor window) — modifier: ["cmd"]
Double-click / open double_click({pid, window_id, element_index}) AXOpen when advertised (Finder items, openable rows); else stamped pixel double-click at the element's center. Pixel form: double_click({pid, x, y}) — primer-gated recipe lands on backgrounded Chromium web content (YouTube fullscreen, Finder open-on-dbl). click({..., count: 2}) still works and routes through the same recipe; double_click is the intent-first spelling
Right click / context menu right_click({pid, window_id, element_index}) or click({pid, window_id, element_index, action: "show_menu"}) Chromium web-content coerces pixel right-click to left — see WEB_APPS.md
Type at cursor type_text({pid, text, window_id, element_index}) AXSelectedText write; focuses first
Set whole field value set_value({pid, window_id, element_index, value}) sliders, steppers, text fields; use for keyboard-commit workarounds on minimized windows
Scroll scroll({pid, direction, amount, by, window_id, element_index}) synthesizes PageUp/PageDown/arrows via SLEventPostToPid
Focus + send key press_key({pid, key, window_id, element_index, modifiers}) element_index sets AXFocused, then posts key
Send key to pid press_key({pid, key, modifiers}) no focus change; key goes to pid's current focus
Modifier combo hotkey({pid, keys}) e.g. ["cmd","c"]; posted per-pid, not HID tap
Unicode keystrokes type_text({pid, text, delay_ms}) AX write with automatic CGEvent fallback; reaches Chromium/Electron inputs

All keyboard/text primitives require pid. There is no frontmost-routed variant — every key goes to the named target via CGEvent.postToPid, so the driver cannot leak keystrokes into the user's foreground app.

Why element_index is the primary path: works on hidden / occluded / off-Space windows, no focus steal, stable across rebuilds, labels tell you what you're clicking. Reach for pixel coordinates only when AX can't.

Pixel-coordinate clicks

The pixel path (click({pid, x, y})) is for surfaces the AX tree doesn't reach — canvases, video players, WebGL, custom-drawn controls. Coords are window-local screenshot pixels (same space as the PNG get_window_state returns). Top-left origin, y-down. The driver handles screen-point conversion internally. Passing window_id alongside x, y is optional but recommended — it pins the coordinate conversion to the window whose screenshot produced the pixel, rather than the driver's heuristic choice.

Reading coordinates from the PNG

PNGs returned by get_window_state are capped at 1568 px long-side by default (max_image_dimension config), matching Anthropic's multimodal-vision downsampling limit. That means the image the model reasons over and the image the click tool's coordinate system lives in are the same resolution — just look at the PNG, pick a pixel, click at that pixel. No scaling math.

This is the default because the mismatch between "rendered thumbnail" and "native PNG" was a recurring coord-estimation footgun. If you opt out (explicit max_image_dimension=0 for pixel-perfect verification flows), the old rule applies: don't eyeball coords from whatever your client renders — it may be 2-4× smaller than the PNG on disk, and a 2% error in thumbnail space becomes ~80 px in the real image. Use the crosshair recipe below against the full-resolution file in that case.

  1. get_window_state({pid, window_id}) returns an image capped at 1568 long-side (default) plus its dimensions (screenshot_width / screenshot_height). Write the bytes to disk with --screenshot-out-file <path> in any capture mode — works identically in vision (where it's the only way) and som (where it sidesteps the jq + base64 dance on the spliced screenshot_png_b64 field).
  2. You are a multimodal model — look at the PNG. Since the PNG matches what you see, pick the target pixel directly. No fractional math needed.
  3. When precision matters (small targets, dense UIs), draw a crosshair on the image (do not crop — cropping loses the coordinate system and requires error-prone offset math) and show it before clicking:
from PIL import Image, ImageDraw
img = Image.open('/tmp/shot.png')
draw = ImageDraw.Draw(img)
x, y = <your_coordinate>
r = 18
draw.ellipse([x-r, y-r, x+r, y+r], outline='red', width=4)
draw.line([x-30, y, x+30, y], fill='red', width=3)
draw.line([x, y-30, x, y+30], fill='red', width=3)
img.save('/tmp/shot_annotated.png')
  1. Only dispatch the click after the user (or your own re-read of the annotated image) confirms the crosshair is on target.

Addressing variants

  • click({pid, x, y}) — single left-click.
  • click({pid, x, y, count: 2}) — double-click.
  • click({pid, x, y, modifier: ["cmd"]}) — cmd-click. Accepts any subset of cmd/shift/option/ctrl.
  • right_click({pid, x, y}) — also takes modifier.

The pixel path animates the agent cursor overlay but never warps the real cursor. If the pid has no on-screen window the call errors with pid X has no on-screen window — you need a visible window to anchor the conversion.

How the pixel click is dispatched

The recipe is the backgrounded "noraise" sequence: yabai's focus-without-raise SLPS event records followed by an off-screen user-activation primer and the real click, all stamped via SLEventPostToPid. The target app becomes AppKit-active for event routing but its window does not rise to the front of the z-stack, and macOS's "switch to Space with windows for app" follow is suppressed. Full mechanics in Sources/CuaDriverCore/Input/MouseInput.swift (clickViaAuthSignedPost) and the companion FocusWithoutRaise.swift.

Known limits

  • Chromium <video> play/pause: pixel click is often rejected by HTML5's click-to-play handler on some builds. Use keyboard instead: press_key({pid, key: "k"}) (YouTube) or press_key({pid, key: "space"}) (generic). Keyboard events travel through a different auth envelope.
  • Pixel right-click on Chromium web content coerces to a left-click — a known Chromium renderer-IPC limitation that affects every non-HID-tap synthesis path. For context menus on AX-addressable elements (links, buttons, toolbar items), use right_click({pid, element_index}) instead.

Canvases, viewports, games (Blender, Unity, GHOST, Qt, wxWidgets)

Apps whose main surface is an OpenGL / Metal / Qt / wxWidgets viewport expose no useful AX tree — the whole surface is one opaque AXGroup or AXWindow from AX's perspective. Per-pid event paths (SLEventPostToPid, CGEvent.postToPid) are filtered by the viewport's own event-source check and silently dropped — the event loop wants "real HID origin".

The working pattern:

  1. Bring the target frontmost (a brief osascript activate is acceptable here — this is the carve-out the skill's osascript gate allows).
  2. CGEvent.post(tap: .cghidEventTap) with a leading mouseMoved event (~30 ms before the click). cua-driver click when the target is frontmost automatically takes this path.
  3. Accept that the real cursor visibly moves — cghidEventTap is the system HID stream, the cursor warps to the click point.

There is no backgrounded path that reaches these apps today.

Navigating native menu bars (AXMenuBar)

Only drive the menu bar when the target app is frontmost. This is the single most-misused cua-driver capability. If the target is backgrounded, don't reach for AXMenuBarItem + AXPick — use in-window element_index or pixel clicks instead. Two reasons, one functional and one perceptual:

  • Functional: menu items that touch document/playback/editor state go DISABLED when their owning app isn't the key window (Preview rotate, IINA speed change, most editor commands). AXPick
    • AXPress will dispatch successfully from the driver's side but no-op at the target — you get a silent false-pass.
  • Perceptual (matters for demos, screen recordings, and anything the user watches live): macOS's screen-rendered menu bar always belongs to the frontmost app. AXPick on a backgrounded app's AXMenuBarItem dispatches to that app's per-process menu at the AX layer, but any visible menu render happens over the frontmost app's menu bar — the viewer sees an IINA submenu flashing on top of Chrome's menus, which reads as "the agent clicked the wrong app." The AX call was correct; the frame the user sees is not. For recorded or observed sessions, this is an integrity bug even though it's not a correctness bug.

Good decision rule: if the target is not already frontmost, do not use AXMenuBarItem at all. For reading in-window state, snapshot the window AX tree — most apps expose the same state via an in-window AXStaticText, title bar, or toolbar. For dispatching actions, use in-window element_index (buttons, toolbar items) or pixel clicks on in-window controls — both dispatch via AppKit's window-under-pointer hit-test and are not frontmost-gated.

When the target IS frontmost, the menu-bar flow below is fine and the canonical path for menus.

The two-snapshot pattern (target frontmost only)

Menu contents are a two-snapshot flow. Closed AXMenu subtrees are deliberately skipped during snapshot — otherwise every app's File / Edit / View hierarchy plus every Recent Items macOS has ever seen would inflate the tree 10-100x. But once a menu is open, its AXMenuItem children do receive element_index values so you can click them normally.

  1. Find the [N] AXMenuBarItem "<Menu Name>" in the tree.
  2. click({pid, element_index: N, action: "pick"}) — menu bar items implement AXPick ("open my submenu"), not AXPress. Using the default action on an AXMenuBarItem is a no-op.
  3. Re-snapshot. The expanded menu's items now appear under the bar item as [M] AXMenuItem "<Item Name>".
  4. Click the target item — most items respond to AXPress (default action). Submenus nest under the item and are walked the same way.
  5. Re-snapshot and verify.

If you ever need to back out without selecting, press_key({pid, key: "escape"}) closes the open menu. Leaving a menu expanded between turns poisons subsequent snapshots for that pid.

Commands gated on the target being frontmost

Some menu items and global shortcuts (Preview's Tools → Rotate Right, ⌘R; anything in the View menu that manipulates the current document; most editor commands) are disabled unless the target app is the key / frontmost window. You'll see it in the AX tree as DISABLED on the menu item even though the user's intent is obviously valid.

Before activating, confirm you're in this narrow case — the menu item still reads DISABLED after a fresh snapshot AND the action the user requested genuinely requires frontmost (Preview rotate, View menu document manipulation, editor commands). If either check fails, don't activate.

When both checks pass, the driver has no activate tool (deliberately — the whole point is backgroundable control), so this is the one legitimate osascript fallback:

osascript -e 'tell application "<App Name>" to activate'

Then re-snapshot — the menu item loses its DISABLED tag — and click({action: "pick"}) the item. Alternatively, a hotkey call delivered to the now-frontmost app works for the shortcut form (⌘R, ⌘+, etc.).

Always name the focus steal in your response so the user isn't surprised — "Briefly activating Preview to enable Tools → Rotate Right" or similar. Don't silently steal focus. You don't need to restore the previous frontmost afterwards unless the user asks — they can cmd-tab back.

Web-rendered apps (browsers, Electron, Tauri)

For Chrome / Edge / Brave / Arc / Safari, Electron apps (Slack, VSCode, Notion, Discord), and Tauri apps — see WEB_APPS.md.

Covers: sparse AX tree population (retry-once pattern for Chromium), URL navigation via omnibox suggestions, the set_value workaround for keyboard commits on minimized windows (Return silently no-ops — symptom is a macOS system beep; use set_value or click a clickable equivalent), scrolling via synthetic PageUp/Down keystrokes, in-page clicks, and typing into web inputs.

Chromium web content specifically also coerces right_click back to left — use element_index for AX-addressable targets and accept the limit otherwise.

Browser JS primitives — page tool and get_window_state(javascript=)

When the AX tree doesn't expose the data you need (common in Chromium/Electron — the tree is sparse for web content), use the page tool or the javascript param on get_window_state to query the DOM directly via Apple Events. Requires "Allow JavaScript from Apple Events" to be enabled — see WEB_APPS.md for the setup path.

Three actions on the page tool:

  • page({pid, window_id, action: "get_text"}) — returns document.body.innerText. Fastest way to read page content, prices, article text, or any raw text the AX tree truncates or omits.

  • page({pid, window_id, action: "query_dom", css_selector: "a[href]", attributes: ["href"]}) — runs querySelectorAll and returns each match's tag, text, and requested attributes as a JSON array. Use for table rows, link hrefs, data attributes, structured page data.

  • page({pid, window_id, action: "execute_javascript", javascript: "..."}) — raw JS. Wrap in an IIFE with try-catch. Don't use this for elements already indexed by get_window_stateclick and set_value are more reliable there.

Co-located read — get_window_state with javascript:

get_window_state({pid, window_id, javascript: "document.title"})

Runs the JS and appends the result as a ## JavaScript result section alongside the AX snapshot — one round-trip instead of two. Use this when you need both the element tree (for subsequent clicks) and some page data in the same turn.

Decision rule — AX vs JS:

Need Use
Click / type into an element get_window_stateclick / set_value (AX, works backgrounded)
Read text the AX tree drops page(get_text) or get_window_state(javascript=)
Scrape structured data (tables, hrefs) page(query_dom)
Trigger JS events / mutations page(execute_javascript)

Supported backends:

App type How Context
Chrome / Brave / Edge Apple Events execute javascript Full DOM ✅
Safari Apple Events do JavaScript Full DOM ✅
Electron (VS Code, Cursor…) SIGUSR1 → V8 inspector → CDP Main process only: process, Buffer — no document, no require in sandboxed apps
Electron (with --remote-debugging-port) CDP page target Full DOM ✅

Electron sandbox note: SIGUSR1 connects to the Node.js main process. Sandboxed Electron apps (VS Code, Cursor) strip require and Electron APIs there. Useful for: process.env, process.versions, process.cwd(), process.pid. For full DOM/renderer access, launch the app with --remote-debugging-port=9222 — cua-driver will detect and prefer the page target automatically.

Arc returns no values; Firefox has no JS-via-AppleEvents support — see WEB_APPS.md for the full matrix.

3. Re-snapshot and verify — mandatory

Always call get_window_state({pid, window_id}) after the action. This isn't optional verification — it's the second half of the snapshot invariant.

Check the AX tree diff: a changed value, a new element, a new window, or the disappearance of the thing you just clicked (menus collapse after selection, buttons may become disabled, etc.). If nothing changed, the action likely failed silently — tell the user what you attempted and what you observed, don't paper over with "done" language. Agents that skip this step report success on silently-dropped actions — the single most common failure mode.

Recording trajectories

Session-scoped action recording + replay, for demos, regressions, and training data. Only invoke when the user explicitly asks to record a session — the skill does not auto-enable this. CLI surface: cua-driver recording start|stop|status; raw tool: set_recording.

See RECORDING.md for the full flow: enable/disable, turn folder contents, replay via replay_trajectory, and the element_index doesn't-survive-across-sessions caveat.

Common error patterns

Error text Meaning Fix
No cached AX state for pid X window_id W You either skipped get_window_state this turn, or passed a different window_id to the click than the one the snapshot cached against Call get_window_state({pid: X, window_id: W}) first — the same window_id you intend to click in
Invalid element_index N for pid X window_id W Index is stale or out of range Re-run get_window_state with the same window_id, pick a fresh index from the new tree
window_id W belongs to pid P, not … Passed a window_id that's owned by a different process Use list_windows({pid: X}) to enumerate this pid's own windows
AX action AXPress failed with code … Element doesn't support AXPress Try show_menu, confirm, cancel, or pick
macOS system-alert beep on press_key with no visible change Target window is minimized; Return / Space / Tab commits don't establish real renderer focus on minimized windows AX-click a clickable equivalent (Go button, Submit button, checkbox) instead of pressing the key; see "Keyboard commits on minimized windows" under the Browser section
Accessibility permission not granted TCC not granted Stop; tell user to grant in System Settings
Screen Recording permission not granted TCC not granted for capture Affects screenshot and get_window_state (which always captures). Grant in System Settings — the driver can't operate without it

Things to avoid

  • Never reuse an element_index across a re-snapshot of the same pid.
  • Never translate screenshot pixels into a click — the screenshot is for visual disambiguation, not coordinates. Use the element_index.
  • Prefer AX over pixels. click({pid, x, y}) works for canvas / WebView regions, but it lands blindly and skips the agent-cursor overlay. Exhaust AX paths (menu bars, cmd-k palettes, toolbar items, keyboard shortcuts) before dropping to coordinates.
  • Never drive destructive actions (delete files, close unsaved documents, send messages, submit forms) without explicit user intent for that specific destructive step.
  • Never launch apps autonomously; confirm with the user first unless their original request clearly implies the launch.

Example end-to-end task

User: "Open the Downloads folder in Finder."

  1. launch_app({bundle_id: "com.apple.finder", urls: ["~/Downloads"]}){pid: 844, windows: [{window_id: 6123, title: "Downloads", ...}]}. Idempotent launch; plus Finder opens a hidden window rooted at ~/Downloads via application(_:open:) — zero activation, no focus steal. The windows array lets you skip a list_windows hop.
  2. get_window_state({pid: 844, window_id: 6123}) → verify an AXWindow whose title contains "Downloads" is present with a populated AX subtree (sidebar, list view, files).
  3. Done.

If the user instead asks to navigate within an already-open Finder window, use the menu-bar flow from the "Navigating native menu bars" section above (click Go → pick a menu item → re-snapshot → click it).

指导创建独特、生产级的前端界面,避免通用AI美学。适用于构建网页、组件或应用,强调大胆的设计方向、精细的排版与动效,生成具有高审美价值的代码。
用户要求构建网页组件或页面 需要美化或设计Web UI 创建着陆页、仪表盘或React/Vue组件
skills/frontend-design/SKILL.md
npx skills add melandlabs/openloomi --skill frontend-design -g -y
SKILL.md
Frontmatter
{
    "name": "frontend-design",
    "license": "Complete terms in LICENSE.txt",
    "description": "Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML\/CSS layouts, or when styling\/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics."
}

This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.

The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.

Design Thinking

Before coding, understand the context and commit to a BOLD aesthetic direction:

  • Purpose: What problem does this interface solve? Who uses it?
  • Tone: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
  • Constraints: Technical requirements (framework, performance, accessibility).
  • Differentiation: What makes this UNFORGETTABLE? What's the one thing someone will remember?

CRITICAL: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.

Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:

  • Production-grade and functional
  • Visually striking and memorable
  • Cohesive with a clear aesthetic point-of-view
  • Meticulously refined in every detail

Frontend Aesthetics Guidelines

Focus on:

  • Typography: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
  • Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
  • Motion: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
  • Spatial Composition: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
  • Backgrounds & Visual Details: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.

NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.

Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.

IMPORTANT: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.

Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

提供 OpenLoomi 后端 API 文档与参考,涵盖认证、用户、聊天、消息、文件、AI、集成及计费等模块。适用于需要调用或理解 OpenLoomi 服务端接口、路由及功能的开发场景。
查询 OpenLoomi API 端点详情 处理后端认证与登录逻辑 实现聊天、消息或文件上传功能 集成第三方服务如 Slack/Discord 调试 AI 模型或 RAG 相关接口
skills/openloomi-api/SKILL.md
npx skills add melandlabs/openloomi --skill openloomi-api -g -y
SKILL.md
Frontmatter
{
    "name": "openloomi-api",
    "metadata": {
        "version": "0.6.4"
    },
    "description": "openloomi API documentation and reference. Use when working with openloomi backend APIs, AI, authentication, characters, messages, files, integrations, billing, or any server-side functionality. Triggers: API endpoints, backend routes, authentication, cloud API, integrations"
}

Note: If you haven't downloaded or installed openloomi yet, please refer to Getting Started for installation instructions.

OpenLoomi API Documentation

API Modules

Module Overview

Module Base Path Description
Auth /api/auth/*, /api/remote-auth/* OAuth, login, register
User /api/user/* User identity and entitlements
Chat /api/chat/* (app routes) Chat/Character CRUD
Messages /api/messages/* Message sending and sync
Files /api/files/* File storage and upload
Storage /api/storage/* Session and disk management
Integrations /api/integrations/*, /api/*/callback Slack, Discord, X, etc.
RAG /api/rag/* Retrieval-augmented generation
Workspace /api/workspace/* Artifacts and skills
Native /api/native/* Native agent operations
AI /api/ai/* LLM, embeddings, images, audio
Insights /api/insights/*, /api/chat-insights/* Analytics and insights
Billing /api/billing/* Billing ledger

Endpoints Reference

Auth Module (/api/auth/*, /api/remote-auth/*)

Method Endpoint Description
POST /api/auth/poll-[provider] Poll OAuth status
POST /api/auth/set-token Set auth token
POST /api/auth/clear-auth-cookie Clear session
POST /api/remote-auth/login Login with email/password
POST /api/remote-auth/register Register new user
POST /api/remote-auth/oauth/[provider] OAuth exchange
POST /api/remote-auth/oauth/[provider]/exchange OAuth code exchange
POST /api/remote-auth/refresh Refresh token
GET /api/remote-auth/user Get current user
PUT /api/remote-auth/user Update user info
GET /api/remote-auth/subscription Get subscription info

Login Example

curl -X POST https://app.alloomi.ai/api/remote-auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password123"}'

Response:

{
  "user": { "id": "user_xxx", "email": "user@example.com", "name": "User" },
  "token": "eyJhbG..."
}

User Module (/api/user/*)

Method Endpoint Description
GET /api/user/identity Get user identity
PUT /api/user/identity Update identity
PUT /api/user/password Change password
GET /api/user/entitlements Get user entitlements

Messages Module (/api/messages/*)

Method Endpoint Description
GET /api/messages List messages
POST /api/messages Send message
GET /api/messages/sync Sync messages
GET /api/messages/check Check message status
GET /api/messages/raw Get raw message

Files Module (/api/files/*)

Method Endpoint Description
GET /api/files/list List files
GET /api/files/[id] Get file by ID
POST /api/files/upload Upload file
POST /api/files/save Save file
GET /api/files/usage Get storage usage
GET /api/files/insights/download Download insights file
POST /api/files/insights/save Save insights

Storage Module (/api/storage/*)

Method Endpoint Description
GET /api/storage/disk-usage Get disk usage
POST /api/storage/cleanup Cleanup storage
GET /api/storage/sessions List sessions
GET /api/storage/sessions/[taskId] Get session by task ID
DELETE /api/storage/sessions/[taskId] Delete session

Integrations Module (/api/integrations/*)

Method Endpoint Description
GET /api/integrations/accounts List connected accounts
GET /api/integrations/slack/oauth/start Start Slack OAuth
GET /api/integrations/slack/oauth/exchange Exchange Slack OAuth code
GET /api/integrations/discord/oauth/start Start Discord OAuth
GET /api/integrations/discord/oauth/exchange Exchange Discord OAuth code
GET /api/integrations/x/oauth/start Start X OAuth

OAuth Callbacks (Various Platforms)

Method Endpoint Platform
GET /api/slack/callback Slack
GET /api/discord/callback Discord
GET /api/auth/callback/github GitHub
GET /api/auth/callback/google Google
POST /api/feishu/listener/init Feishu
POST /api/dingtalk/listener/init DingTalk
POST /api/qqbot/listener/init QQ Bot
POST /api/weixin/listener/init WeChat
POST /api/telegram/user-listener/init Telegram
POST /api/whatsapp/register-socket WhatsApp
POST /api/imessage/init-self-listener iMessage

RAG Module (/api/rag/*)

Method Endpoint Description
GET /api/rag/search Search documents
GET /api/rag/stats Get RAG statistics
GET /api/rag/documents List documents
GET /api/rag/documents/[documentId] Get document
GET /api/rag/documents/[documentId]/binary Get document binary
DELETE /api/rag/documents/[documentId] Delete document
POST /api/rag/upload Upload document
POST /api/rag/upload/init Initialize upload
POST /api/rag/upload/chunk Upload chunk
POST /api/rag/upload/complete Complete upload
POST /api/rag/upload/async Async upload
GET /api/rag/upload/async/status Check async upload status

Workspace Module (/api/workspace/*)

Method Endpoint Description
GET /api/workspace/artifacts List artifacts
GET /api/workspace/files List files
GET /api/workspace/file/[...path] Get file by path
GET /api/workspace/preview Preview artifact
GET /api/workspace/external-preview External preview
GET /api/workspace/skills List skills
GET /api/workspace/skills/[skillId] Get skill
POST /api/workspace/skills Create skill
PUT /api/workspace/skills/[skillId] Update skill
DELETE /api/workspace/skills/[skillId] Delete skill
POST /api/workspace/skills/toggle Toggle skill
POST /api/workspace/skills/upload Upload skill
GET /api/workspace/skills/metadata Get skill metadata

AI Module (/api/ai/*)

Method Endpoint Description
POST /api/ai/chat Chat completions (streaming)
GET /api/ai/chat Check AI status
POST /api/ai/v1/chat/completions V1 chat completions
POST /api/ai/v1/embeddings Generate embeddings
POST /api/ai/v1/images/generations Generate images
POST /api/ai/v1/audio/speech Text-to-speech
POST /api/ai/v1/audio/transcriptions Speech-to-text
POST /api/ai/v1/messages/count_tokens Count tokens
POST /api/ai/v1/upload Upload file for AI
GET /api/ai/v1/models List available models

Chat Example

curl -X POST https://app.alloomi.ai/api/ai/chat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "Hello!"}
    ],
    "stream": true
  }'

Insights Module (/api/insights/*, /api/chat-insights/*)

Method Endpoint Description
GET /api/chat-insights Get chat insights
GET /api/insights/brief-categories List brief categories
POST /api/insights/brief-categories/sync Sync categories
POST /api/insights/brief-categories/overrides Override categories
POST /api/insights/brief-categories/pinned Pin categories
POST /api/insights/brief-categories/cleanup Cleanup categories
GET /api/insight-tabs List insight tabs
POST /api/insight-tabs Create insight tab
PUT /api/insight-tabs/[tabId] Update tab
POST /api/insight-tabs/reorder Reorder tabs

Billing Module (/api/billing/*)

Method Endpoint Description
GET /api/billing/ledger Get billing ledger

Error Handling

Error Response Format

// API errors return standard HTTP status codes
{
  error: string;      // Error message
  code?: string;       // Error code for programmatic handling
  cause?: string;      // Additional context
}

Common Status Codes

Code Meaning
200 Success
400 Bad Request - Invalid input
401 Unauthorized - Not authenticated
403 Forbidden - Insufficient permissions
404 Not Found
429 Too Many Requests
500 Internal Server Error

AI/Agent Usage

Local API Access

When running openloomi desktop app, the local API server runs on port 3414 (fallback: 3515):

Environment Base URL
User Local Desktop http://localhost:3414
User Local Desktop (fallback) http://localhost:3515

Authentication Token

The auth token is stored at ~/.openloomi/token (base64 encoded JWT). You must decode it before use:

# Decode base64 to get JWT token
TOKEN=$(cat ~/.openloomi/token | base64 -d)

# Verify token contents (decodes JWT payload)
echo "$TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool

curl Examples

Important: All authenticated requests require the token to be base64 decoded first.

# Helper: Get decoded token
TOKEN=$(cat ~/.openloomi/token | base64 -d)

# 1. Check AI API status (no auth required)
curl http://localhost:3414/api/ai/chat

# 2. Get current user info
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl http://localhost:3414/api/remote-auth/user \
  -H "Authorization: Bearer $TOKEN"

# 3. Get subscription info
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl http://localhost:3414/api/remote-auth/subscription \
  -H "Authorization: Bearer $TOKEN"

# 4. Chat with AI (streaming)
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl -X POST http://localhost:3414/api/ai/chat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Hello!"}],"stream":true}'

# 5. Get chat insights (requires chatId)
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl "http://localhost:3414/api/chat-insights?chatId=xxx" \
  -H "Authorization: Bearer $TOKEN"

# 6. Search RAG documents
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl -X POST http://localhost:3414/api/rag/search \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"search term","limit":5}'

# 7. List workspace skills
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl http://localhost:3414/api/workspace/skills \
  -H "Authorization: Bearer $TOKEN"

# 8. Submit feedback
TOKEN=$(cat ~/.openloomi/token | base64 -d)
curl -X POST http://localhost:3414/api/remote-feedback \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content":"Feedback message","email":"user@example.com"}'

Production API Access

# Using production API
export TOKEN="your_production_token"
curl https://app.alloomi.ai/api/remote-auth/user \
  -H "Authorization: Bearer $TOKEN"

Summary

  • 129+ API endpoints across 20+ functional modules
  • Dual authentication: Session cookies (web) and Bearer tokens (Tauri)
  • RESTful JSON APIs with Zod validation
  • CloudApiClient for desktop/Tauri integration
  • SWR utilities for client-side data fetching
  • OAuth support for Google, GitHub, Slack, Discord, X
  • RAG for document retrieval and search
  • AI endpoints for chat, embeddings, images, audio
管理26种平台的OAuth连接,支持列出账户、检查状态及断开连接。作为主动式数字伙伴的核心组件,持续同步消息、日历等数据,构建统一知识库以增强AI代理的上下文感知能力。
连接平台 集成状态查询 列出已连接账户 断开平台连接
skills/openloomi-connectors/SKILL.md
npx skills add melandlabs/openloomi --skill openloomi-connectors -g -y
SKILL.md
Frontmatter
{
    "name": "openloomi-connectors",
    "metadata": {
        "version": "0.6.4"
    },
    "description": "openloomi Connectors tools - manage platform integrations (OAuth connections, list accounts, check status). Triggers: connect platform, integration status, list accounts, disconnect",
    "allowed-tools": "Bash(node $SKILL_DIR\/scripts\/openloomi-connectors.cjs *)"
}

Note: If you haven't downloaded or installed openloomi yet, please refer to Getting Started for installation instructions.

OpenLoomi Connectors Skill

OpenLoomi Connectors provides access to 26 messaging and productivity platform integrations. It allows AI agents to manage OAuth connections, list connected accounts, check connection status, and disconnect platforms on behalf of the user.


What is openloomi?

Most AI assistants function as workflow tools—users give commands, they execute tasks, with no persistent knowledge of who you are or what matters to you.

openloomi takes a fundamentally different approach: it operates as a proactive digital partner that watches, learns, remembers, and acts on your behalf. The difference is architectural.

How It Works

When users connect messaging platforms and integrations to openloomi, they sync with permission:

  • Raw messages and communications
  • Meetings and calendar events
  • Emails and tweets
  • Voice calls
  • Notes and captured ideas

This aggregated data becomes "the single source of truth for openloomi's brain."

The Continuous Sync Loop

openloomi runs a background agent on a continuous sync loop, actively gathering information from all connected sources. An agent without this loop can only respond based on stale context. With it, every conversation—and every moment—makes openloomi smarter and more aligned with you.


Supported Platforms (26)

ID Display Name Aliases
telegram Telegram tg
whatsapp WhatsApp
slack Slack
discord Discord
gmail Gmail google_mail
outlook Outlook outlook_mail
linkedin LinkedIn
instagram Instagram
twitter X/Twitter x, tweet, tweets, 推特
google_calendar Google Calendar gcal
outlook_calendar Outlook Calendar
teams Microsoft Teams microsoft_teams
facebook_messenger Facebook Messenger messenger
google_drive Google Drive gdrive
google_docs Google Docs gdocs
hubspot HubSpot
notion Notion
github GitHub gh
asana Asana
jira Jira
linear Linear
imessage iMessage
feishu Lark/Feishu lark, 飞书
dingtalk DingTalk 钉钉
qqbot QQ qq, qq_bot
weixin WeChat wechat, 微信, wechat_work, wecom, 企业微信

Authentication

The CLI auto-reads your token from ~/.openloomi/token (base64 encoded JWT).

Local API Access

The local API server runs on port 3414 (fallback: 3515). If 3414 is unavailable, try 3515.


API Endpoints

Integration Accounts

GET /api/integrations/accounts - List Connected Accounts

Returns all connected platform accounts for the authenticated user.

curl http://localhost:3414/api/integrations/accounts \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "accounts": [
    {
      "id": "int_xxx",
      "platform": "gmail",
      "externalId": "user@gmail.com",
      "displayName": "My Gmail",
      "status": "active",
      "metadata": {},
      "createdAt": "2024-01-01T00:00:00Z",
      "botId": "bot_xxx"
    }
  ]
}

Note: Each account includes a botId field which is used for send-reply and other bot operations.


OAuth Start Endpoints

GET /api/integrations/slack/oauth/start?userId=<userId> - Start Slack OAuth

Returns the Slack OAuth authorization URL. The CLI opens this URL in the browser for the user to complete authorization.

curl "http://localhost:3414/api/integrations/slack/oauth/start?userId=<userId>"

Response:

{
  "authorizationUrl": "https://slack.com/oauth/v2/authorize?...",
  "state": "userId:uuid"
}

GET /api/integrations/discord/oauth/start?userId=<userId> - Start Discord OAuth

Returns the Discord OAuth authorization URL.

GET /api/integrations/x/oauth/start?userId=<userId> - Start X OAuth

Returns the X/Twitter OAuth authorization URL.


OAuth Exchange Endpoints

GET /api/integrations/slack/oauth/exchange?code=<code>&state=<state> - Exchange Slack Code

Exchange OAuth code for Slack access.

GET /api/integrations/discord/oauth/exchange?code=<code>&state=<state> - Exchange Discord Code

Exchange OAuth code for Discord access.


OAuth Callbacks

Platform Endpoint
GitHub GET /api/auth/callback/github
Google GET /api/auth/callback/google
Feishu POST /api/feishu/listener/init
DingTalk POST /api/dingtalk/listener/init
QQ Bot POST /api/qqbot/listener/init
WeChat POST /api/weixin/listener/init
Telegram POST /api/telegram/user-listener/init
WhatsApp POST /api/whatsapp/register-socket
iMessage POST /api/imessage/init-self-listener

DELETE /api/integrations/:id - Disconnect Account

Delete a connected integration account.

curl -X DELETE http://localhost:3414/api/integrations/int_xxx \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "success": true,
  "deletedAccountId": "int_xxx",
  "deletedBotIds": ["bot_xxx"]
}

GET /api/contacts - Query Contacts

Query user contacts with optional filtering and pagination.

curl "http://localhost:3414/api/contacts?name=John&page=1&pageSize=10" \
  -H "Authorization: Bearer $TOKEN"

Parameters:

  • name (string, optional) - Filter contacts by name (partial match)
  • page (number, default 1) - Page number
  • pageSize (number, default 10) - Items per page (max 100)

Response:

{
  "success": true,
  "contacts": [
    {
      "id": "contact_xxx",
      "name": "John Doe",
      "type": "email",
      "botId": "bot_xxx",
      "platform": "gmail"
    }
  ],
  "pagination": {
    "page": 1,
    "pageSize": 10,
    "totalCount": 50,
    "totalPages": 5,
    "hasMore": true,
    "hasPrevious": false
  }
}

POST /api/messages - Send Message

Send a message via a connected platform bot.

curl -X POST http://localhost:3414/api/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "botId": "bot_xxx",
    "recipients": ["John"],
    "message": "Hello!",
    "subject": "Optional subject"
  }'

Parameters:

  • botId (string, required) - The bot ID to send from
  • recipients (array, required) - List of recipient names
  • message (string, required) - Message content
  • subject (string, optional) - Email subject line
  • cc (array, optional) - CC recipients
  • bcc (array, optional) - BCC recipients

Note: botId is returned by list-accounts in the botId field (different from account id).


Platform Aliases Reference

Aliases are case-insensitive and support both English and Chinese:

Alias Platform
tg telegram
gh github
gc gmail
x twitter
tweet, tweets, 推特 twitter
gcal google_calendar
gdrive google_drive
gdocs google_docs
wechat, 微信 weixin
lark, 飞书 feishu
钉钉 dingtalk
qq, qq_bot qqbot

Desktop UI

Users can also authorize accounts directly through the openloomi desktop application without using CLI commands.

Adding Account Authorization via Desktop UI

  1. Open openloomi desktop app on your computer

  2. Navigate to Settings (gear icon in the sidebar or top-right menu)

  3. Go to Integrations tab/section

  4. Click on the platform you want to connect (e.g., Telegram, Slack, Discord, Gmail, etc.)

  5. Follow the platform-specific authorization flow:

    • OAuth platforms (Slack, Discord, X/Twitter): Click "Connect" → you'll be redirected to the platform's authorization page in your browser → Approve the permissions → you'll be redirected back
    • App Password platforms (Gmail, Outlook): Enter your email and app password
    • App Credentials platforms (DingTalk, Feishu, QQ): Enter your appId and appSecret
    • QR/Interactive platforms (WhatsApp, Telegram, iMessage): Scan the QR code or follow the in-app instructions
  6. Verify connection — once authorized, the platform will show as "Connected" with a green checkmark

Managing Connected Accounts

  • List connected accounts: Settings → Integrations → shows all connected platforms with status
  • Disconnect account: Settings → Integrations → click on connected platform → "Disconnect" or remove
  • Check status: Connected platforms show green "Active" badge; expired/disconnected shows red "Inactive" badge

CLI Script

Quick Start

# List all supported platforms
node $SKILL_DIR/scripts/openloomi-connectors.cjs list-platforms

# List all connected accounts (includes botId for send-reply)
node $SKILL_DIR/scripts/openloomi-connectors.cjs list-accounts

# Check connection status for a platform
node $SKILL_DIR/scripts/openloomi-connectors.cjs status telegram

# Connect a platform (opens browser for OAuth)
node $SKILL_DIR/scripts/openloomi-connectors.cjs connect slack

# Disconnect an account by ID
node $SKILL_DIR/scripts/openloomi-connectors.cjs disconnect int_xxx

# Query contacts
node $SKILL_DIR/scripts/openloomi-connectors.cjs query-contacts --name=John --page=1 --pageSize=10

# Send a message (requires botId from list-accounts)
node $SKILL_DIR/scripts/openloomi-connectors.cjs send-reply --botId=bot_xxx --recipients=John --message="Hello!"

Commands

Command Description
list-platforms List all 26 supported platforms with IDs and aliases
list-accounts List all connected integration accounts (includes botId)
status <platform> Check if a platform is connected (e.g., telegram, slack)
connect <platform> [options] Connect a platform (OAuth, App Password, or App Credentials)
disconnect <accountId> Disconnect a specific account by ID
query-contacts [options] Query contacts (--name=, --page=, --pageSize=)
send-reply --botId= --recipients= --message= Send a message via REST API

Platform Connection Methods

Method Platforms
OAuth (auto-opens browser) slack, discord, x
App Password gmail --email=x --password=xxxx, outlook --email=x --password=xxxx
App Credentials dingtalk --clientId=x --clientSecret=x, feishu --appId=x --appSecret=x, qq --appId=x --appSecret=x
iLink Token wechat --token=x
Browser Required (QR/interactive) whatsapp, telegram, imessage

AI Agent Workflow

Triggered when the user asks about:

  1. Connecting a platform - "connect telegram", "link my slack"
  2. Listing integrations - "show my connected accounts", "what platforms am I connected to"
  3. Checking status - "is my github connected?", "telegram status"
  4. Disconnecting - "disconnect my discord", "remove whatsapp"
  5. Querying contacts - "show my contacts", "find John in contacts"
  6. Sending messages - "send email to John", "reply to that message"

Execution Flow:

  1. Identify intent - connect / list / status / disconnect / query-contacts / send-reply
  2. Resolve platform - use alias normalization (e.g., gh -> github)
  3. Execute command - use Bash tool
  4. Format output - report results naturally in user's language

Note on send-reply: The botId is returned by list-accounts in the botId field.

用于搜索和管理个人记忆文件、知识库及聊天洞察的工具。支持本地内存文件读取,并通过连接器同步多平台数据,实现基于分层记忆结构的智能检索与知识管理。
用户需要搜索个人记忆文件 用户需要查询知识库内容 用户希望获取聊天历史中的结构化洞察信息
skills/openloomi-memory/SKILL.md
npx skills add melandlabs/openloomi --skill openloomi-memory -g -y
SKILL.md
Frontmatter
{
    "name": "openloomi-memory",
    "metadata": {
        "version": "0.5.1"
    },
    "description": "openloomi Memory tools - search memory files, knowledge base, and chat insights. Triggers: memory search, knowledge base, documents, insights",
    "allowed-tools": "Bash(node $SKILL_DIR\/scripts\/openloomi-memory.cjs *)"
}

Note: If you haven't downloaded or installed openloomi yet, please refer to Getting Started for installation instructions.

OpenLoomi Memory Skill

OpenLoomi Memory is a personal knowledge management tool that searches and manages three types of information:

Type Description Data Location
Memory Files Personal memory files (markdown/json) ~/.openloomi/data/memory/
Knowledge Base Uploaded documents via RAG/embeddings openloomi server
Insights Structured info extracted from chat history, with usage tracking and automatic maintenance openloomi server

Overview

openloomi Memory is built on a unique architectural principle: instead of treating memory as an afterthought, it's the foundation.

How it works with Connectors: Before memory can search your data, you need to connect your platforms using the openloomi-connectors skill. Connectors handles OAuth authentication and integration setup for 26 platforms (Telegram, WhatsApp, Slack, Discord, Gmail, Outlook, Twitter/X, WeChat, and more). Once connected, openloomi continuously syncs everything with your permission—raw messages, meetings, emails, tweets, calendar events, voice calls, and any notes or ideas you've captured. This aggregated data becomes the single source of truth that powers openloomi's brain.

The memory is layered:

  • Raw information: Original messages, files, transcripts
  • Information insights: Extracted entities, decisions, key events
  • Contextual memory: Recent conversation state
  • Knowledge-base memory: Long-term people/projects/preferences knowledge graph

This enables reasoning across both immediate context and deep historical knowledge simultaneously. When you create custom agent roles to handle tasks, this memory acts as the orchestrator—dramatically improving execution quality.

Inside openloomi, memory is layered into multiple levels:

  • Raw information: Original messages, files, transcripts
  • Information insights: Extracted entities, decisions, key events
  • Contextual memory: Recent conversation state
  • Knowledge-base memory: Long-term people/projects/preferences knowledge graph

This enables reasoning across both immediate context and deep historical knowledge simultaneously.

Insights include automatic usage tracking—recording view frequency, sources, and calculating value scores to surface the most relevant information. A periodic maintenance system (daily analytics refresh, weekly compaction) keeps insight retrieval accurate and prevents context decay by archiving or removing stale, low-value content.


Authentication

The CLI auto-reads your token from ~/.openloomi/token (base64 encoded JWT).


Local Memory Filesystem

Overview

Memory files are stored locally at ~/.openloomi/data/memory/ and searched via direct filesystem access. This is a read-only operation that performs case-insensitive text search across .md and .json files.

Directory Structure

~/.openloomi/data/memory/
├── chats/           # Chat conversation exports
├── channels/         # Channel memory exports e.g., weixin, telegram, etc.
├── people/          # Person profiles
├── projects/       # Project notes
├── notes/          # General notes
└── strategy/       # Strategy documents

Write Operations

Memory files are plain markdown or JSON stored locally. You can add or delete files directly.

Adding a memory file:

node $SKILL_DIR/scripts/openloomi-memory.cjs add-memory "Content to remember" --file=filename.md --directory=notes
  • --file (optional): Filename. If not provided, auto-generated from first line of content.
  • --directory (optional): Subdirectory under ~/.openloomi/data/memory/. Created if doesn't exist.

Deleting a memory file:

node $SKILL_DIR/scripts/openloomi-memory.cjs delete-memory filename.md --directory=notes

How search-memory Works

  1. Path: ~/.openloomi/data/memory/ (or subdirectory if specified)
  2. Search Type: Case-insensitive full-text search
  3. Files: Scans .md and .json files recursively (max depth 5)
  4. Matching: Each line is searched; returns first match per file
  5. Output: File path, line number, and line preview (first 200 chars)

Example Output

{
  "results": [
    {
      "file": "people/boss.md",
      "line": 42,
      "preview": "My boss John mentioned the deadline is next Friday"
    },
    {
      "file": "projects/app/notes.md",
      "line": 10,
      "preview": "Boss wants the app launched by end of month"
    }
  ],
  "total": 2
}

API Endpoints

Knowledge Base (RAG)

POST /api/rag/search - Search Documents

Semantic search of uploaded documents using embeddings.

curl -X POST http://localhost:3414/api/rag/search \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "project plan", "limit": 5}'

Parameters:

  • query (string, required) - Search query
  • limit (number, default 5) - Max results to return

Response:

{
  "results": [
    {
      "id": "doc_xxx",
      "title": "Project Document",
      "content": "...",
      "score": 0.95
    }
  ]
}

GET /api/rag/documents - List Documents

List all documents in the knowledge base.

curl http://localhost:3414/api/rag/documents?limit=50 \
  -H "Authorization: Bearer $TOKEN"

Parameters:

  • limit (number, default 50) - Max results to return

Response:

{
  "documents": [
    {
      "id": "doc_xxx",
      "name": "document.pdf",
      "type": "pdf",
      "size": 102400,
      "createdAt": "2024-01-01T00:00:00Z"
    }
  ],
  "total": 10
}

GET /api/rag/documents/[id] - Get Document

Get a single document by ID.

curl http://localhost:3414/api/rag/documents/doc_xxx \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "id": "doc_xxx",
  "name": "Project Document.pdf",
  "type": "pdf",
  "size": 102400,
  "content": "Document text content...",
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-01T00:00:00Z"
}

Insights

Insights are structured information extracted from chat history, such as key decisions, action items, and relationship notes. Each insight belongs to one or more groups (channels/platforms) like gmail, telegram, whatsapp, slack, discord, linkedin, twitter, etc.

GET /api/insights - List Insights

List all insights from a time period.

curl "http://localhost:3414/api/insights?days=7&limit=50" \
  -H "Authorization: Bearer $TOKEN"

Parameters:

  • days (number, default 7) - Look back period in days
  • limit (number, default 50) - Max results to return

Insight Structure: Each insight contains a groups field—an array of channel identifiers indicating which platform(s) the insight came from:

{
  "id": "insight_xxx",
  "chatId": "chat_xxx",
  "type": "decision",
  "content": "John sent an email about the project deadline",
  "groups": ["gmail"],
  "people": ["John"],
  "time": "2024-01-01T00:00:00Z",
  "createdAt": "2024-01-01T00:00:00Z"
}

Insight Types:

Type Description
decision Key decisions made
action_item Tasks or follow-ups
note General notes
preference User preferences
relationship Notes about people
event Important events

Common Channel Groups:

Channel Group Value Description
Gmail "gmail" Google Mail messages
Outlook "outlook" Microsoft Outlook emails
Telegram "telegram" Telegram chats
WhatsApp "whatsapp" WhatsApp messages
Slack "slack" Slack messages
Discord "discord" Discord messages
LinkedIn "linkedin" LinkedIn messages
Twitter/X "twitter" Twitter posts
WeChat "weixin" WeChat messages
RSS "rss" RSS feed items

POST /api/insights - Create Insight

Create a new insight manually.

curl -X POST http://localhost:3414/api/insights \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "preference", "content": "I prefer Americano coffee", "groups": ["whatsapp"]}'

Parameters:

  • type (string, required) - Insight type (decision, action_item, note, preference, relationship, event)
  • content (string, required) - The insight text
  • groups (array, optional) - Channel groups to associate with
  • people (array, optional) - People mentioned in the insight

Response:

{
  "id": "insight_xxx",
  "type": "preference",
  "content": "I prefer Americano coffee",
  "groups": ["whatsapp"],
  "createdAt": "2024-01-01T00:00:00Z"
}

PUT /api/insights/[id] - Update Insight

Partial update an existing insight. Arrays (details, timeline, insights) are appended to, not replaced.

curl -X PUT http://localhost:3414/api/insights/insight_xxx \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": {
      "description": "Updated description",
      "details": [{"content": "User mentioned new preference", "person": "User"}],
      "timeline": [{"summary": "Progress update", "label": "Update"}]
    }
  }'

Update Fields:

  • title - New title
  • description - New description
  • importance - Important, General, Not Important
  • urgency - As soon as possible, Within 24 hours, Not urgent, General
  • details - Array of detail objects (appended to existing)
  • timeline - Array of timeline events (appended to existing)
  • myTasks - Array of task objects
  • groups - Array of group tags (replaced)
  • categories - Array of categories (replaced)
  • people - Array of people names (replaced)

Response:

{
  "message": "Insight updated successfully",
  "id": "insight_xxx"
}

GET /api/insights/[id]?fetch=true - Get Insight

Get a single insight by ID, including associated chat.

curl "http://localhost:3414/api/insights/insight_xxx?fetch=true" \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "id": "insight_xxx",
  "chatId": "chat_xxx",
  "type": "decision",
  "content": "User decided to start new project next month",
  "chat": {
    "id": "chat_xxx",
    "title": "Chat with John",
    "messages": [...]
  },
  "createdAt": "2024-01-01T00:00:00Z"
}

DELETE /api/insights/[id] - Delete Insight

Delete a specific insight.

curl -X DELETE http://localhost:3414/api/insights/insight_xxx \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "success": true
}

GET /api/chat-insights?chatId=xxx - Get Chat Insights

Get all insights for a specific chat.

curl "http://localhost:3414/api/chat-insights?chatId=chat_xxx" \
  -H "Authorization: Bearer $TOKEN"

Insight Usage Analytics & Maintenance

openloomi tracks insight usage and performs periodic maintenance to preserve retrieval quality, avoiding context decay.

Usage Tracking

Each insight view is recorded with:

  • Access timestamp
  • Access source (list, detail, search, favorite)
  • Cumulative access counts (7-day / 30-day / total)

Data is stored in the insightWeights table:

  • accessCountTotal - Total access count
  • accessCount7d - Access count in last 7 days
  • accessCount30d - Access count in last 30 days
  • lastAccessedAt - Last access timestamp

Analysis Dimensions

Each insight is scored on trend and value:

Metric Weight Description
Frequency 45% Based on 7-day / 30-day access frequency
Freshness 25% Last access time
Relevance 20% Importance (70%) + Urgency (30%)
Favorites 10% Whether the insight is favorited

Trend Indicators:

  • rising - Access frequency increasing
  • falling - Access frequency decreasing
  • stable - Frequency stable

Periodic Maintenance

System automatically runs two maintenance tasks:

Task Frequency Purpose
Daily analytics refresh 24 hours Refresh access stats, recalculate trends and scores
Weekly compaction 7 days Merge similar insights, prune low-value content

Retention Policy:

  • delete: 90 days no access + low score + not high importance → soft delete, hard delete after 180 days
  • archive: 30 days no access + low score OR falling trend + low score → archived

GET /api/insights/analytics - Get Insight Usage Analytics

curl "http://localhost:3414/api/insights/analytics" \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "generatedAt": "2024-01-15T10:30:00Z",
  "summary": {
    "totalInsights": 150,
    "activeInsights": 45,
    "dormantInsights": 105,
    "totalAccesses30d": 230,
    "averageValueScore": 42,
    "risingInsights": 12,
    "fallingInsights": 8,
    "stableInsights": 25
  },
  "topInsights": [...],
  "bottomInsights": [...],
  "relationships": [...],
  "insights": [
    {
      "id": "insight_xxx",
      "title": "User decided to start new project",
      "description": "",
      "taskLabel": "",
      "platform": "gmail",
      "account": "user@gmail.com",
      "importance": "general",
      "urgency": "not_urgent",
      "isFavorited": false,
      "isArchived": false,
      "createdAt": "2024-01-01T00:00:00Z",
      "updatedAt": "2024-01-10T00:00:00Z",
      "time": "2024-01-01T00:00:00Z",
      "accessCountTotal": 15,
      "accessCount7d": 3,
      "accessCount30d": 8,
      "lastAccessedAt": "2024-01-15T10:30:00Z",
      "trend": "rising",
      "recent7dAccessCount": 3,
      "previous7dAccessCount": 1,
      "valueScore": 58,
      "recommendation": {
        "action": "keep",
        "reason": "Usage, freshness, or relevance still supports keeping it active."
      }
    }
  ]
}

POST /api/insights/[id]/view - Record Insight View

curl -X POST "http://localhost:3414/api/insights/insight_xxx/view" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"viewSource": "search"}'

Parameters:

  • viewSource (string) - Source type: list, detail, search, favorite

Response:

{
  "ok": true
}

CLI Script

Quick Start

# Search ALL memory sources at once (recommended for comprehensive search)
node $SKILL_DIR/scripts/openloomi-memory.cjs search-all "query"

# Search local memory files (full-text, case-insensitive)
node $SKILL_DIR/scripts/openloomi-memory.cjs search-memory "boss"

# Search local memory files in specific subdirectory
node $SKILL_DIR/scripts/openloomi-memory.cjs search-memory "project" --directory=projects

# Search knowledge base (RAG, semantic search)
node $SKILL_DIR/scripts/openloomi-memory.cjs search-knowledge "project plan"

# List knowledge base documents
node $SKILL_DIR/scripts/openloomi-memory.cjs list-documents

# Get document content
node $SKILL_DIR/scripts/openloomi-memory.cjs get-document doc_xxx

# List recent insights (last 7 days)
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --days=7

# List insights from a specific channel (e.g., Gmail, Telegram, WhatsApp)
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --channel=gmail --days=7
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --channel=telegram --days=30
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --channel=whatsapp

# Filter insights by keyword (supports multiple keywords - OR logic)
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --keyword=screen --keyword=linkedin --days=30

# Get single insight
node $SKILL_DIR/scripts/openloomi-memory.cjs get-insight insight_xxx

# Create a new insight
node $SKILL_DIR/scripts/openloomi-memory.cjs add-insight --title="Coffee preference" --description="I prefer Americano coffee" --importance=General

# Update an insight (partial update with array append)
node $SKILL_DIR/scripts/openloomi-memory.cjs update-insight insight_xxx --description="Updated description" --detail="User mentioned new preference"

# Delete insight
node $SKILL_DIR/scripts/openloomi-memory.cjs delete-insight insight_xxx

# Add a memory file
node $SKILL_DIR/scripts/openloomi-memory.cjs add-memory "My boss John likes Monday project discussions" --file=people/boss.md

# Delete a memory file
node $SKILL_DIR/scripts/openloomi-memory.cjs delete-memory people/boss.md

Command Reference

Command Description Target
search-all Search all memory sources simultaneously Local files + Knowledge base + Insights
search-memory Full-text search in local .md/.json files ~/.openloomi/data/memory/
search-knowledge Semantic search via embeddings openloomi server (RAG)
list-documents List uploaded documents Knowledge base
get-document Get document content by ID Knowledge base
list-insights List extracted insights (supports --channel filter) Insights API
get-insight Get single insight by ID Insights API
delete-insight Delete an insight Insights API
add-insight Create a new insight (title, description, importance, urgency, groups, people) Insights API
update-insight Update an insight (partial update with array append logic) Insights API
add-memory Add a memory file (auto-generates filename from content) Local filesystem
delete-memory Delete a memory file Local filesystem

AI Agent Workflow

Triggered when the user asks about memory, knowledge, or past information:

  1. Memory file search - "search my memory", "find what I said about..."
  2. Knowledge base search - "search uploaded documents", "find in knowledge base"
  3. Insights management - "list insights", "delete an insight"
  4. Channel insights - "what messages on Gmail?", "show me Telegram chats", "any WhatsApp messages?"
  5. Comprehensive search - "search everything", "find in all my memory", "build relationship graph"

Execution Flow:

  1. Identify intent - determine if user wants comprehensive search or specific source
  2. Prefer search-all - for general memory queries, always use search-all first to get comprehensive results across all sources
  3. Execute in parallel - when specific sources are needed, run multiple searches simultaneously:
    • search-memory for local files
    • search-knowledge for uploaded documents
    • list-insights for extracted insights
  4. For channel queries - use list-insights with --channel parameter:
    • "gmail" - Email messages via Gmail
    • "outlook" - Email messages via Outlook
    • "telegram" - Telegram chats
    • "whatsapp" - WhatsApp messages
    • "slack" - Slack messages
    • "discord" - Discord messages
    • "linkedin" - LinkedIn messages
    • "twitter" - Twitter/X posts
    • "weixin" - WeChat messages
    • "rss" - RSS feed items
  5. Format output - aggregate and present results in user's language

Best Practice for Comprehensive Queries:

# When user asks about relationships, people, or general memory:
node $SKILL_DIR/scripts/openloomi-memory.cjs search-all "person/project/topic"

# Then optionally get details from specific sources
node $SKILL_DIR/scripts/openloomi-memory.cjs search-memory "person" --directory=people
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --days=30 --keyword=<keyword>

Channel-Based Message Queries:

# User asks "what emails did I receive?" or "show me Gmail messages"
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --channel=gmail --days=7

# User asks "any Telegram messages about project X"?
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --channel=telegram --days=30

# User asks "recent WhatsApp messages"?
node $SKILL_DIR/scripts/openloomi-memory.cjs list-insights --channel=whatsapp

Living Connections (Hebbian Potentiation)

Living Connections track relationships between insights that strengthen when they're accessed together. This implements Hebbian learning: "insights that fire together, wire together."

Commands

# Get related insights - "users who viewed X also viewed Y"
node $SKILL_DIR/scripts/openloomi-memory.cjs get-related-insights <insightId>

# Get related insights with filters
node $SKILL_DIR/scripts/openloomi-memory.cjs get-related-insights insight_xxx --limit=10 --minStrength=0.3

# Get connection statistics
node $SKILL_DIR/scripts/openloomi-memory.cjs get-connection-stats <insightId>

How It Works

  1. When you view an insight, connections to other insights viewed within 5 minutes are strengthened
  2. Connection strength decays over time using Ebbinghaus-style forgetting curve
  3. Strong connections (strength > 0.5) are considered "living" - actively referenced

Response Format

{
  "insightId": "insight_xxx",
  "connections": [...],
  "relatedInsights": [
    {
      "insightId": "insight_yyy",
      "strength": 0.72,
      "coAccessCount": 5
    }
  ],
  "total": 5
}

Temporal Queries (Time-Travel)

Temporal validity enables "time-travel" queries - seeing what insights were relevant at a specific point in time.

Commands

# Get insights valid at a specific point in time (time-travel query)
node $SKILL_DIR/scripts/openloomi-memory.cjs get-insights-as-of 2026-01-01

# Get currently valid insights (no expiration or future expiration)
node $SKILL_DIR/scripts/openloomi-memory.cjs get-current-insights

# Get insights overlapping a time interval
node $SKILL_DIR/scripts/openloomi-memory.cjs get-insights-in-interval 2026-01-01 2026-06-01

Use Cases

  • "What did I know about Project X on March 1st?"
  • "What insights were valid during my vacation last July?"
  • "Show me only currently relevant insights (hide expired ones)"

Entity Registry

Entity Registry tracks people, groups, concepts, projects, and companies as first-class entities with disambiguation support.

Commands

# List all entities of a specific type
node $SKILL_DIR/scripts/openloomi-memory.cjs list-entities --type=person

# Search entities by name
node $SKILL_DIR/scripts/openloomi-memory.cjs list-entities --search=John

# Get entity details with linked insights
node $SKILL_DIR/scripts/openloomi-memory.cjs get-entity <entityId> --insights

Entity Types

Type Description
person People (contacts, colleagues, friends)
group Groups (teams, organizations)
concept Abstract concepts (ideas, methodologies)
project Projects (initiatives, deliverables)
company Companies (clients, vendors, employers)

Search with Connections

Combined search that returns matching insights along with their Living Connections, providing a richer context.

# Search insights and include related insights
node $SKILL_DIR/scripts/openloomi-memory.cjs search-with-connections "project deadline"

# With custom limit
node $SKILL_DIR/scripts/openloomi-memory.cjs search-with-connections "project deadline" --limit=5
处理PDF文件的技能,涵盖读取、文本/表格提取、合并、拆分、旋转、水印、创建、表单填写、加密解密、图片提取及OCR。适用于涉及.pdf文件或需生成PDF的场景。
用户提及 .pdf 文件 用户要求处理 PDF(如读取、合并、拆分、转换等) 用户要求生成或输出 PDF 文件
skills/pdf/SKILL.md
npx skills add melandlabs/openloomi --skill pdf -g -y
SKILL.md
Frontmatter
{
    "name": "pdf",
    "license": "Proprietary. LICENSE.txt has complete terms",
    "description": "Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text\/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting\/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill."
}

PDF Processing Guide

Overview

This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see REFERENCE.md. If you need to fill out a PDF form, read FORMS.md and follow its instructions.

Quick Start

from pypdf import PdfReader, PdfWriter

# Read a PDF
reader = PdfReader("document.pdf")
print(f"Pages: {len(reader.pages)}")

# Extract text
text = ""
for page in reader.pages:
    text += page.extract_text()

Python Libraries

pypdf - Basic Operations

Merge PDFs

from pypdf import PdfWriter, PdfReader

writer = PdfWriter()
for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
    reader = PdfReader(pdf_file)
    for page in reader.pages:
        writer.add_page(page)

with open("merged.pdf", "wb") as output:
    writer.write(output)

Split PDF

reader = PdfReader("input.pdf")
for i, page in enumerate(reader.pages):
    writer = PdfWriter()
    writer.add_page(page)
    with open(f"page_{i+1}.pdf", "wb") as output:
        writer.write(output)

Extract Metadata

reader = PdfReader("document.pdf")
meta = reader.metadata
print(f"Title: {meta.title}")
print(f"Author: {meta.author}")
print(f"Subject: {meta.subject}")
print(f"Creator: {meta.creator}")

Rotate Pages

reader = PdfReader("input.pdf")
writer = PdfWriter()

page = reader.pages[0]
page.rotate(90)  # Rotate 90 degrees clockwise
writer.add_page(page)

with open("rotated.pdf", "wb") as output:
    writer.write(output)

pdfplumber - Text and Table Extraction

Extract Text with Layout

import pdfplumber

with pdfplumber.open("document.pdf") as pdf:
    for page in pdf.pages:
        text = page.extract_text()
        print(text)

Extract Tables

with pdfplumber.open("document.pdf") as pdf:
    for i, page in enumerate(pdf.pages):
        tables = page.extract_tables()
        for j, table in enumerate(tables):
            print(f"Table {j+1} on page {i+1}:")
            for row in table:
                print(row)

Advanced Table Extraction

import pandas as pd

with pdfplumber.open("document.pdf") as pdf:
    all_tables = []
    for page in pdf.pages:
        tables = page.extract_tables()
        for table in tables:
            if table:  # Check if table is not empty
                df = pd.DataFrame(table[1:], columns=table[0])
                all_tables.append(df)

# Combine all tables
if all_tables:
    combined_df = pd.concat(all_tables, ignore_index=True)
    combined_df.to_excel("extracted_tables.xlsx", index=False)

reportlab - Create PDFs

Basic PDF Creation

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

c = canvas.Canvas("hello.pdf", pagesize=letter)
width, height = letter

# Add text
c.drawString(100, height - 100, "Hello World!")
c.drawString(100, height - 120, "This is a PDF created with reportlab")

# Add a line
c.line(100, height - 140, 400, height - 140)

# Save
c.save()

Create PDF with Multiple Pages

from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.lib.styles import getSampleStyleSheet

doc = SimpleDocTemplate("report.pdf", pagesize=letter)
styles = getSampleStyleSheet()
story = []

# Add content
title = Paragraph("Report Title", styles['Title'])
story.append(title)
story.append(Spacer(1, 12))

body = Paragraph("This is the body of the report. " * 20, styles['Normal'])
story.append(body)
story.append(PageBreak())

# Page 2
story.append(Paragraph("Page 2", styles['Heading1']))
story.append(Paragraph("Content for page 2", styles['Normal']))

# Build PDF
doc.build(story)

Subscripts and Superscripts

IMPORTANT: Never use Unicode subscript/superscript characters (₀₁₂₃₄₅₆₇₈₉, ⁰¹²³⁴⁵⁶⁷⁸⁹) in ReportLab PDFs. The built-in fonts do not include these glyphs, causing them to render as solid black boxes.

Instead, use ReportLab's XML markup tags in Paragraph objects:

from reportlab.platypus import Paragraph
from reportlab.lib.styles import getSampleStyleSheet

styles = getSampleStyleSheet()

# Subscripts: use <sub> tag
chemical = Paragraph("H<sub>2</sub>O", styles['Normal'])

# Superscripts: use <super> tag
squared = Paragraph("x<super>2</super> + y<super>2</super>", styles['Normal'])

For canvas-drawn text (not Paragraph objects), manually adjust font the size and position rather than using Unicode subscripts/superscripts.

Command-Line Tools

pdftotext (poppler-utils)

# Extract text
pdftotext input.pdf output.txt

# Extract text preserving layout
pdftotext -layout input.pdf output.txt

# Extract specific pages
pdftotext -f 1 -l 5 input.pdf output.txt  # Pages 1-5

qpdf

# Merge PDFs
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf

# Split pages
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
qpdf input.pdf --pages . 6-10 -- pages6-10.pdf

# Rotate pages
qpdf input.pdf output.pdf --rotate=+90:1  # Rotate page 1 by 90 degrees

# Remove password
qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf

pdftk (if available)

# Merge
pdftk file1.pdf file2.pdf cat output merged.pdf

# Split
pdftk input.pdf burst

# Rotate
pdftk input.pdf rotate 1east output rotated.pdf

Common Tasks

Extract Text from Scanned PDFs

# Requires: pip install pytesseract pdf2image
import pytesseract
from pdf2image import convert_from_path

# Convert PDF to images
images = convert_from_path('scanned.pdf')

# OCR each page
text = ""
for i, image in enumerate(images):
    text += f"Page {i+1}:\n"
    text += pytesseract.image_to_string(image)
    text += "\n\n"

print(text)

Add Watermark

from pypdf import PdfReader, PdfWriter

# Create watermark (or load existing)
watermark = PdfReader("watermark.pdf").pages[0]

# Apply to all pages
reader = PdfReader("document.pdf")
writer = PdfWriter()

for page in reader.pages:
    page.merge_page(watermark)
    writer.add_page(page)

with open("watermarked.pdf", "wb") as output:
    writer.write(output)

Extract Images

# Using pdfimages (poppler-utils)
pdfimages -j input.pdf output_prefix

# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.

Password Protection

from pypdf import PdfReader, PdfWriter

reader = PdfReader("input.pdf")
writer = PdfWriter()

for page in reader.pages:
    writer.add_page(page)

# Add password
writer.encrypt("userpassword", "ownerpassword")

with open("encrypted.pdf", "wb") as output:
    writer.write(output)

Quick Reference

Task Best Tool Command/Code
Merge PDFs pypdf writer.add_page(page)
Split PDFs pypdf One page per file
Extract text pdfplumber page.extract_text()
Extract tables pdfplumber page.extract_tables()
Create PDFs reportlab Canvas or Platypus
Command line merge qpdf qpdf --empty --pages ...
OCR scanned PDFs pytesseract Convert to image first
Fill PDF forms pdf-lib or pypdf (see FORMS.md) See FORMS.md

Next Steps

  • For advanced pypdfium2 usage, see REFERENCE.md
  • For JavaScript libraries (pdf-lib), see REFERENCE.md
  • If you need to fill out a PDF form, follow the instructions in FORMS.md
  • For troubleshooting guides, see REFERENCE.md
指导用户创建或更新技能,提供模块化技能的设计原则、结构规范及上下文优化策略,帮助扩展Claude的专业能力与工具集成。
用户希望创建新的技能 用户需要更新现有技能
skills/skill-creator/SKILL.md
npx skills add melandlabs/openloomi --skill skill-creator -g -y
SKILL.md
Frontmatter
{
    "name": "skill-creator",
    "license": "Complete terms in LICENSE.txt",
    "description": "Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations."
}

Skill Creator

This skill provides guidance for creating effective skills.

About Skills

Skills are modular, self-contained packages that extend Claude's capabilities by providing specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific domains or tasks—they transform Claude from a general-purpose agent into a specialized agent equipped with procedural knowledge that no model can fully possess.

What Skills Provide

  1. Specialized workflows - Multi-step procedures for specific domains
  2. Tool integrations - Instructions for working with specific file formats or APIs
  3. Domain expertise - Company-specific knowledge, schemas, business logic
  4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks

Core Principles

Concise is Key

The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.

Default assumption: Claude is already very smart. Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?"

Prefer concise examples over verbose explanations.

Set Appropriate Degrees of Freedom

Match the level of specificity to the task's fragility and variability:

High freedom (text-based instructions): Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.

Medium freedom (pseudocode or scripts with parameters): Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.

Low freedom (specific scripts, few parameters): Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.

Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).

Anatomy of a Skill

Every skill consists of a required SKILL.md file and optional bundled resources:

skill-name/
├── SKILL.md (required)
│   ├── YAML frontmatter metadata (required)
│   │   ├── name: (required)
│   │   └── description: (required)
│   └── Markdown instructions (required)
└── Bundled Resources (optional)
    ├── scripts/          - Executable code (Python/Bash/etc.)
    ├── references/       - Documentation intended to be loaded into context as needed
    └── assets/           - Files used in output (templates, icons, fonts, etc.)

SKILL.md (required)

Every SKILL.md consists of:

  • Frontmatter (YAML): Contains name and description fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
  • Body (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).

Bundled Resources (optional)

Scripts (scripts/)

Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.

  • When to include: When the same code is being rewritten repeatedly or deterministic reliability is needed
  • Example: scripts/rotate_pdf.py for PDF rotation tasks
  • Benefits: Token efficient, deterministic, may be executed without loading into context
  • Note: Scripts may still need to be read by Claude for patching or environment-specific adjustments
References (references/)

Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking.

  • When to include: For documentation that Claude should reference while working
  • Examples: references/finance.md for financial schemas, references/mnda.md for company NDA template, references/policies.md for company policies, references/api_docs.md for API specifications
  • Use cases: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
  • Benefits: Keeps SKILL.md lean, loaded only when Claude determines it's needed
  • Best practice: If files are large (>10k words), include grep search patterns in SKILL.md
  • Avoid duplication: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
Assets (assets/)

Files not intended to be loaded into context, but rather used within the output Claude produces.

  • When to include: When the skill needs files that will be used in the final output
  • Examples: assets/logo.png for brand assets, assets/slides.pptx for PowerPoint templates, assets/frontend-template/ for HTML/React boilerplate, assets/font.ttf for typography
  • Use cases: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
  • Benefits: Separates output resources from documentation, enables Claude to use files without loading them into context

What to Not Include in a Skill

A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:

  • README.md
  • INSTALLATION_GUIDE.md
  • QUICK_REFERENCE.md
  • CHANGELOG.md
  • etc.

The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.

Progressive Disclosure Design Principle

Skills use a three-level loading system to manage context efficiently:

  1. Metadata (name + description) - Always in context (~100 words)
  2. SKILL.md body - When skill triggers (<5k words)
  3. Bundled resources - As needed by Claude (Unlimited because scripts can be executed without reading into context window)

Progressive Disclosure Patterns

Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.

Key principle: When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.

Pattern 1: High-level guide with references

# PDF Processing

## Quick start

Extract text with pdfplumber:
[code example]

## Advanced features

- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns

Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.

Pattern 2: Domain-specific organization

For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:

bigquery-skill/
├── SKILL.md (overview and navigation)
└── reference/
    ├── finance.md (revenue, billing metrics)
    ├── sales.md (opportunities, pipeline)
    ├── product.md (API usage, features)
    └── marketing.md (campaigns, attribution)

When a user asks about sales metrics, Claude only reads sales.md.

Similarly, for skills supporting multiple frameworks or variants, organize by variant:

cloud-deploy/
├── SKILL.md (workflow + provider selection)
└── references/
    ├── aws.md (AWS deployment patterns)
    ├── gcp.md (GCP deployment patterns)
    └── azure.md (Azure deployment patterns)

When the user chooses AWS, Claude only reads aws.md.

Pattern 3: Conditional details

Show basic content, link to advanced content:

# DOCX Processing

## Creating documents

Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).

## Editing documents

For simple edits, modify the XML directly.

**For tracked changes**: See [REDLINING.md](REDLINING.md)
**For OOXML details**: See [OOXML.md](OOXML.md)

Claude reads REDLINING.md or OOXML.md only when the user needs those features.

Important guidelines:

  • Avoid deeply nested references - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
  • Structure longer reference files - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.

Skill Creation Process

Skill creation involves these steps:

  1. Understand the skill with concrete examples
  2. Plan reusable skill contents (scripts, references, assets)
  3. Initialize the skill (run init_skill.py)
  4. Edit the skill (implement resources and write SKILL.md)
  5. Package the skill (run package_skill.py)
  6. Iterate based on real usage

Follow these steps in order, skipping only if there is a clear reason why they are not applicable.

Step 1: Understanding the Skill with Concrete Examples

Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.

To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.

For example, when building an image-editor skill, relevant questions include:

  • "What functionality should the image-editor skill support? Editing, rotating, anything else?"
  • "Can you give some examples of how this skill would be used?"
  • "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
  • "What would a user say that should trigger this skill?"

To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.

Conclude this step when there is a clear sense of the functionality the skill should support.

Step 2: Planning the Reusable Skill Contents

To turn concrete examples into an effective skill, analyze each example by:

  1. Considering how to execute on the example from scratch
  2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly

Example: When building a pdf-editor skill to handle queries like "Help me rotate this PDF," the analysis shows:

  1. Rotating a PDF requires re-writing the same code each time
  2. A scripts/rotate_pdf.py script would be helpful to store in the skill

Example: When designing a frontend-webapp-builder skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:

  1. Writing a frontend webapp requires the same boilerplate HTML/React each time
  2. An assets/hello-world/ template containing the boilerplate HTML/React project files would be helpful to store in the skill

Example: When building a big-query skill to handle queries like "How many users have logged in today?" the analysis shows:

  1. Querying BigQuery requires re-discovering the table schemas and relationships each time
  2. A references/schema.md file documenting the table schemas would be helpful to store in the skill

To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.

Step 3: Initializing the Skill

At this point, it is time to actually create the skill.

Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.

When creating a new skill from scratch, always run the init_skill.py script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.

Usage:

scripts/init_skill.py <skill-name> --path <output-directory>

The script:

  • Creates the skill directory at the specified path
  • Generates a SKILL.md template with proper frontmatter and TODO placeholders
  • Creates example resource directories: scripts/, references/, and assets/
  • Adds example files in each directory that can be customized or deleted

After initialization, customize or remove the generated SKILL.md and example files as needed.

Step 4: Edit the Skill

When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.

Learn Proven Design Patterns

Consult these helpful guides based on your skill's needs:

  • Multi-step processes: See references/workflows.md for sequential workflows and conditional logic
  • Specific output formats or quality standards: See references/output-patterns.md for template and example patterns

These files contain established best practices for effective skill design.

Start with Reusable Skill Contents

To begin implementation, start with the reusable resources identified above: scripts/, references/, and assets/ files. Note that this step may require user input. For example, when implementing a brand-guidelines skill, the user may need to provide brand assets or templates to store in assets/, or documentation to store in references/.

Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.

Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in scripts/, references/, and assets/ to demonstrate structure, but most skills won't need all of them.

Update SKILL.md

Writing Guidelines: Always use imperative/infinitive form.

Frontmatter

Write the YAML frontmatter with name and description:

  • name: The skill name
  • description: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.
    • Include both what the Skill does and specific triggers/contexts for when to use it.
    • Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude.
    • Example description for a docx skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"

Do not include any other fields in YAML frontmatter.

Body

Write instructions for using the skill and its bundled resources.

Step 5: Packaging a Skill

Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:

scripts/package_skill.py <path/to/skill-folder>

Optional output directory specification:

scripts/package_skill.py <path/to/skill-folder> ./dist

The packaging script will:

  1. Validate the skill automatically, checking:

    • YAML frontmatter format and required fields
    • Skill naming conventions and directory structure
    • Description completeness and quality
    • File organization and resource references
  2. Package the skill if validation passes, creating a .skill file named after the skill (e.g., my-skill.skill) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.

If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.

Step 6: Iterate

After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.

Iteration workflow:

  1. Use the skill on real tasks
  2. Notice struggles or inefficiencies
  3. Identify how SKILL.md or bundled resources should be updated
  4. Implement changes and test again
获取当前天气和预报,支持多地点。适用于查询温度、降雨及周预报。无需API密钥,基于wttr.in。不处理历史数据、极端警报或专业气象分析。
用户询问天气状况 查询特定地点的温度 请求未来几天或周的天气预报 出行前的天气检查
skills/weather/SKILL.md
npx skills add melandlabs/openloomi --skill weather -g -y
SKILL.md
Frontmatter
{
    "name": "weather",
    "homepage": "https:\/\/wttr.in\/:help",
    "description": "Get current weather and forecasts via wttr.in or Open-Meteo. Use when: user asks about weather, temperature, or forecasts for any location. NOT for: historical weather data, severe weather alerts, or detailed meteorological analysis. No API key needed."
}

Weather Skill

Get current weather conditions and forecasts.

When to Use

USE this skill when:

  • "What's the weather?"
  • "Will it rain today/tomorrow?"
  • "Temperature in [city]"
  • "Weather forecast for the week"
  • Travel planning weather checks

When NOT to Use

DON'T use this skill when:

  • Historical weather data → use weather archives/APIs
  • Climate analysis or trends → use specialized data sources
  • Hyper-local microclimate data → use local sensors
  • Severe weather alerts → check official NWS sources
  • Aviation/marine weather → use specialized services (METAR, etc.)

Location

Always include a city, region, or airport code in weather queries.

Commands

Current Weather

# One-line summary
curl "wttr.in/London?format=3"

# Detailed current conditions
curl "wttr.in/London?0"

# Specific city
curl "wttr.in/New+York?format=3"

Forecasts

# 3-day forecast
curl "wttr.in/London"

# Week forecast
curl "wttr.in/London?format=v2"

# Specific day (0=today, 1=tomorrow, 2=day after)
curl "wttr.in/London?1"

Format Options

# One-liner
curl "wttr.in/London?format=%l:+%c+%t+%w"

# JSON output
curl "wttr.in/London?format=j1"

# PNG image
curl "wttr.in/London.png"

Format Codes

  • %c — Weather condition emoji
  • %t — Temperature
  • %f — "Feels like"
  • %w — Wind
  • %h — Humidity
  • %p — Precipitation
  • %l — Location

Quick Responses

"What's the weather?"

curl -s "wttr.in/London?format=%l:+%c+%t+(feels+like+%f),+%w+wind,+%h+humidity"

"Will it rain?"

curl -s "wttr.in/London?format=%l:+%c+%p"

"Weekend forecast"

curl "wttr.in/London?format=v2"

Notes

  • No API key needed (uses wttr.in)
  • Rate limited; don't spam requests
  • Works for most global cities
  • Supports airport codes: curl wttr.in/ORD
用于创建、读取、编辑和转换Word文档(.docx)的技能。支持格式化、表格、图片插入、修订处理及内容提取,涵盖从模板生成到文件修复的全流程操作。
用户提及 Word doc、word document、.docx 请求生成带格式的专业文档(如报告、信函、模板) 要求提取或重组 .docx 内容 需要在文档中插入/替换图片或执行查找替换 处理修订记录、注释或转换为 PDF/图片
skills/docx/SKILL.md
npx skills add melandlabs/openloomi --skill docx -g -y
SKILL.md
Frontmatter
{
    "name": "docx",
    "license": "Proprietary. LICENSE.txt has complete terms",
    "description": "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of \"Word doc\", \"word document\", \".docx\", or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a \"report\", \"memo\", \"letter\", \"template\", or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
}

DOCX creation, editing, and analysis

Overview

A .docx file is a ZIP archive containing XML files.

Quick Reference

Task Approach
Read/analyze content pandoc or unpack for raw XML
Create new document Use docx-js - see Creating New Documents below
Edit existing document Unpack → edit XML → repack - see Editing Existing Documents below

Converting .doc to .docx

Legacy .doc files are auto-converted on read and write — no manual step needed:

# Reading .doc (auto-converted to .docx internally)
python scripts/office/unpack.py legacy.doc unpacked/

# Writing .doc (auto-converted from .docx)
python scripts/office/pack.py unpacked/ output.doc

LibreOffice is used for the conversion, with automatic sandbox-friendly socket handling.

Reading Content

# Text extraction with tracked changes
pandoc --track-changes=all document.docx -o output.md

# Raw XML access
python scripts/office/unpack.py document.docx unpacked/

Converting to Images

python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page

Accepting Tracked Changes

To produce a clean document with all tracked changes accepted (requires LibreOffice):

python scripts/accept_changes.py input.docx output.docx

Creating New Documents

Generate .docx files with JavaScript, then validate. Install: npm install -g docx

Setup

const {
  Document,
  Packer,
  Paragraph,
  TextRun,
  Table,
  TableRow,
  TableCell,
  ImageRun,
  Header,
  Footer,
  AlignmentType,
  PageOrientation,
  LevelFormat,
  ExternalHyperlink,
  TableOfContents,
  HeadingLevel,
  BorderStyle,
  WidthType,
  ShadingType,
  VerticalAlign,
  PageNumber,
  PageBreak,
} = require("docx");

const doc = new Document({
  sections: [
    {
      children: [
        /* content */
      ],
    },
  ],
});
Packer.toBuffer(doc).then((buffer) => fs.writeFileSync("doc.docx", buffer));

Validation

After creating the file, validate it. If validation fails, unpack, fix the XML, and repack.

python scripts/office/validate.py doc.docx

Page Size

// CRITICAL: docx-js defaults to A4, not US Letter
// Always set page size explicitly for consistent results
sections: [
  {
    properties: {
      page: {
        size: {
          width: 12240, // 8.5 inches in DXA
          height: 15840, // 11 inches in DXA
        },
        margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1 inch margins
      },
    },
    children: [
      /* content */
    ],
  },
];

Common page sizes (DXA units, 1440 DXA = 1 inch):

Paper Width Height Content Width (1" margins)
US Letter 12,240 15,840 9,360
A4 (default) 11,906 16,838 9,026

Landscape orientation: docx-js swaps width/height internally, so pass portrait dimensions and let it handle the swap:

size: {
  width: 12240,   // Pass SHORT edge as width
  height: 15840,  // Pass LONG edge as height
  orientation: PageOrientation.LANDSCAPE  // docx-js swaps them in the XML
},
// Content width = 15840 - left margin - right margin (uses the long edge)

Styles (Override Built-in Headings)

Use Arial as the default font (universally supported). Keep titles black for readability.

const doc = new Document({
  styles: {
    default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default
    paragraphStyles: [
      // IMPORTANT: Use exact IDs to override built-in styles
      {
        id: "Heading1",
        name: "Heading 1",
        basedOn: "Normal",
        next: "Normal",
        quickFormat: true,
        run: { size: 32, bold: true, font: "Arial" },
        paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 },
      }, // outlineLevel required for TOC
      {
        id: "Heading2",
        name: "Heading 2",
        basedOn: "Normal",
        next: "Normal",
        quickFormat: true,
        run: { size: 28, bold: true, font: "Arial" },
        paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 },
      },
    ],
  },
  sections: [
    {
      children: [
        new Paragraph({
          heading: HeadingLevel.HEADING_1,
          children: [new TextRun("Title")],
        }),
      ],
    },
  ],
});

Lists (NEVER use unicode bullets)

// ❌ WRONG - never manually insert bullet characters
new Paragraph({ children: [new TextRun("• Item")] }); // BAD
new Paragraph({ children: [new TextRun("\u2022 Item")] }); // BAD

// ✅ CORRECT - use numbering config with LevelFormat.BULLET
const doc = new Document({
  numbering: {
    config: [
      {
        reference: "bullets",
        levels: [
          {
            level: 0,
            format: LevelFormat.BULLET,
            text: "•",
            alignment: AlignmentType.LEFT,
            style: { paragraph: { indent: { left: 720, hanging: 360 } } },
          },
        ],
      },
      {
        reference: "numbers",
        levels: [
          {
            level: 0,
            format: LevelFormat.DECIMAL,
            text: "%1.",
            alignment: AlignmentType.LEFT,
            style: { paragraph: { indent: { left: 720, hanging: 360 } } },
          },
        ],
      },
    ],
  },
  sections: [
    {
      children: [
        new Paragraph({
          numbering: { reference: "bullets", level: 0 },
          children: [new TextRun("Bullet item")],
        }),
        new Paragraph({
          numbering: { reference: "numbers", level: 0 },
          children: [new TextRun("Numbered item")],
        }),
      ],
    },
  ],
});

// ⚠️ Each reference creates INDEPENDENT numbering
// Same reference = continues (1,2,3 then 4,5,6)
// Different reference = restarts (1,2,3 then 1,2,3)

Tables

CRITICAL: Tables need dual widths - set both columnWidths on the table AND width on each cell. Without both, tables render incorrectly on some platforms.

// CRITICAL: Always set table width for consistent rendering
// CRITICAL: Use ShadingType.CLEAR (not SOLID) to prevent black backgrounds
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };

new Table({
  width: { size: 9360, type: WidthType.DXA }, // Always use DXA (percentages break in Google Docs)
  columnWidths: [4680, 4680], // Must sum to table width (DXA: 1440 = 1 inch)
  rows: [
    new TableRow({
      children: [
        new TableCell({
          borders,
          width: { size: 4680, type: WidthType.DXA }, // Also set on each cell
          shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR not SOLID
          margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Cell padding (internal, not added to width)
          children: [new Paragraph({ children: [new TextRun("Cell")] })],
        }),
      ],
    }),
  ],
});

Table width calculation:

Always use WidthType.DXAWidthType.PERCENTAGE breaks in Google Docs.

// Table width = sum of columnWidths = content width
// US Letter with 1" margins: 12240 - 2880 = 9360 DXA
width: { size: 9360, type: WidthType.DXA },
columnWidths: [7000, 2360]  // Must sum to table width

Width rules:

  • Always use WidthType.DXA — never WidthType.PERCENTAGE (incompatible with Google Docs)
  • Table width must equal the sum of columnWidths
  • Cell width must match corresponding columnWidth
  • Cell margins are internal padding - they reduce content area, not add to cell width
  • For full-width tables: use content width (page width minus left and right margins)

Images

// CRITICAL: type parameter is REQUIRED
new Paragraph({
  children: [
    new ImageRun({
      type: "png", // Required: png, jpg, jpeg, gif, bmp, svg
      data: fs.readFileSync("image.png"),
      transformation: { width: 200, height: 150 },
      altText: { title: "Title", description: "Desc", name: "Name" }, // All three required
    }),
  ],
});

Page Breaks

// CRITICAL: PageBreak must be inside a Paragraph
new Paragraph({ children: [new PageBreak()] });

// Or use pageBreakBefore
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] });

Table of Contents

// CRITICAL: Headings must use HeadingLevel ONLY - no custom styles
new TableOfContents("Table of Contents", {
  hyperlink: true,
  headingStyleRange: "1-3",
});

Headers/Footers

sections: [
  {
    properties: {
      page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } }, // 1440 = 1 inch
    },
    headers: {
      default: new Header({
        children: [new Paragraph({ children: [new TextRun("Header")] })],
      }),
    },
    footers: {
      default: new Footer({
        children: [
          new Paragraph({
            children: [
              new TextRun("Page "),
              new TextRun({ children: [PageNumber.CURRENT] }),
            ],
          }),
        ],
      }),
    },
    children: [
      /* content */
    ],
  },
];

Critical Rules for docx-js

  • Set page size explicitly - docx-js defaults to A4; use US Letter (12240 x 15840 DXA) for US documents
  • Landscape: pass portrait dimensions - docx-js swaps width/height internally; pass short edge as width, long edge as height, and set orientation: PageOrientation.LANDSCAPE
  • Never use \n - use separate Paragraph elements
  • Never use unicode bullets - use LevelFormat.BULLET with numbering config
  • PageBreak must be in Paragraph - standalone creates invalid XML
  • ImageRun requires type - always specify png/jpg/etc
  • Always set table width with DXA - never use WidthType.PERCENTAGE (breaks in Google Docs)
  • Tables need dual widths - columnWidths array AND cell width, both must match
  • Table width = sum of columnWidths - for DXA, ensure they add up exactly
  • Always add cell margins - use margins: { top: 80, bottom: 80, left: 120, right: 120 } for readable padding
  • Use ShadingType.CLEAR - never SOLID for table shading
  • TOC requires HeadingLevel only - no custom styles on heading paragraphs
  • Override built-in styles - use exact IDs: "Heading1", "Heading2", etc.
  • Include outlineLevel - required for TOC (0 for H1, 1 for H2, etc.)

Editing Existing Documents

Follow all 3 steps in order.

Step 1: Unpack

python scripts/office/unpack.py document.docx unpacked/

Extracts XML, pretty-prints, merges adjacent runs, and converts smart quotes to XML entities (&#x201C; etc.) so they survive editing. Use --merge-runs false to skip run merging.

Step 2: Edit XML

Edit files in unpacked/word/. See XML Reference below for patterns.

Use "Claude" as the author for tracked changes and comments, unless the user explicitly requests use of a different name.

Use the Edit tool directly for string replacement. Do not write Python scripts. Scripts introduce unnecessary complexity. The Edit tool shows exactly what is being replaced.

CRITICAL: Use smart quotes for new content. When adding text with apostrophes or quotes, use XML entities to produce smart quotes:

<!-- Use these entities for professional typography -->
<w:t>Here&#x2019;s a quote: &#x201C;Hello&#x201D;</w:t>
Entity Character
&#x2018; ‘ (left single)
&#x2019; ’ (right single / apostrophe)
&#x201C; “ (left double)
&#x201D; ” (right double)

Adding comments: Use comment.py to handle boilerplate across multiple XML files (text must be pre-escaped XML):

python scripts/comment.py unpacked/ 0 "Comment text with &amp; and &#x2019;"
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0  # reply to comment 0
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author"  # custom author name

Then add markers to document.xml (see Comments in XML Reference).

Step 3: Pack

python scripts/office/pack.py unpacked/ output.docx --original document.docx

Validates with auto-repair, condenses XML, and creates DOCX. Use --validate false to skip.

Auto-repair will fix:

  • durableId >= 0x7FFFFFFF (regenerates valid ID)
  • Missing xml:space="preserve" on <w:t> with whitespace

Auto-repair won't fix:

  • Malformed XML, invalid element nesting, missing relationships, schema violations

Common Pitfalls

  • Replace entire <w:r> elements: When adding tracked changes, replace the whole <w:r>...</w:r> block with <w:del>...<w:ins>... as siblings. Don't inject tracked change tags inside a run.
  • Preserve <w:rPr> formatting: Copy the original run's <w:rPr> block into your tracked change runs to maintain bold, font size, etc.

XML Reference

Schema Compliance

  • Element order in <w:pPr>: <w:pStyle>, <w:numPr>, <w:spacing>, <w:ind>, <w:jc>, <w:rPr> last
  • Whitespace: Add xml:space="preserve" to <w:t> with leading/trailing spaces
  • RSIDs: Must be 8-digit hex (e.g., 00AB1234)

Tracked Changes

Insertion:

<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
  <w:r><w:t>inserted text</w:t></w:r>
</w:ins>

Deletion:

<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
  <w:r><w:delText>deleted text</w:delText></w:r>
</w:del>

Inside <w:del>: Use <w:delText> instead of <w:t>, and <w:delInstrText> instead of <w:instrText>.

Minimal edits - only mark what changes:

<!-- Change "30 days" to "60 days" -->
<w:r><w:t>The term is </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
  <w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
  <w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> days.</w:t></w:r>

Deleting entire paragraphs/list items - when removing ALL content from a paragraph, also mark the paragraph mark as deleted so it merges with the next paragraph. Add <w:del/> inside <w:pPr><w:rPr>:

<w:p>
  <w:pPr>
    <w:numPr>...</w:numPr>  <!-- list numbering if present -->
    <w:rPr>
      <w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
    </w:rPr>
  </w:pPr>
  <w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
    <w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
  </w:del>
</w:p>

Without the <w:del/> in <w:pPr><w:rPr>, accepting changes leaves an empty paragraph/list item.

Rejecting another author's insertion - nest deletion inside their insertion:

<w:ins w:author="Jane" w:id="5">
  <w:del w:author="Claude" w:id="10">
    <w:r><w:delText>their inserted text</w:delText></w:r>
  </w:del>
</w:ins>

Restoring another author's deletion - add insertion after (don't modify their deletion):

<w:del w:author="Jane" w:id="5">
  <w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
  <w:r><w:t>deleted text</w:t></w:r>
</w:ins>

Comments

After running comment.py (see Step 2), add markers to document.xml. For replies, use --parent flag and nest markers inside the parent's.

CRITICAL: <w:commentRangeStart> and <w:commentRangeEnd> are siblings of <w:r>, never inside <w:r>.

<!-- Comment markers are direct children of w:p, never inside w:r -->
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
  <w:r><w:delText>deleted</w:delText></w:r>
</w:del>
<w:r><w:t> more text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>

<!-- Comment 0 with reply 1 nested inside -->
<w:commentRangeStart w:id="0"/>
  <w:commentRangeStart w:id="1"/>
  <w:r><w:t>text</w:t></w:r>
  <w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>

Images

  1. Add image file to word/media/
  2. Add relationship to word/_rels/document.xml.rels:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
  1. Add content type to [Content_Types].xml:
<Default Extension="png" ContentType="image/png"/>
  1. Reference in document.xml:
<w:drawing>
  <wp:inline>
    <wp:extent cx="914400" cy="914400"/>  <!-- EMUs: 914400 = 1 inch -->
    <a:graphic>
      <a:graphicData uri=".../picture">
        <pic:pic>
          <pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
        </pic:pic>
      </a:graphicData>
    </a:graphic>
  </wp:inline>
</w:drawing>

Dependencies

  • pandoc: Text extraction
  • docx: npm install -g docx (new documents)
  • LibreOffice: PDF conversion (auto-configured for sandboxed environments via scripts/office/soffice.py)
  • Poppler: pdftoppm for images
OpenLoomi Loop是主动执行大脑,持续从Gmail等外部源拉取信号,经内存丰富后转化为决策队列,并通过原生API或CLI代理执行。
openloomi loop loop tick loop schedule loop inbox loop run proactive decisions signal → decision → execute pull signals decision queue loop serve
skills/openloomi-loop/SKILL.md
npx skills add melandlabs/openloomi --skill openloomi-loop -g -y
SKILL.md
Frontmatter
{
    "name": "openloomi-loop",
    "metadata": {
        "version": "0.6.4"
    },
    "description": "Use this when the user asks about openloomi's Loop — openloomi's proactive execution brain. It actively and continuously pulls external signals (Gmail, Calendar, GitHub, Slack) via any of three Composio surfaces — **MCP** (`mcp__composio__*`), the **`composio-cli` skill**, or the **`composio` CLI** — enriches them through openloomi-memory, **converts every actionable signal into a typed decision** (`rsvp` \/ `draft_reply` \/ `review_pr` \/ `todo` \/ `slack_reply` \/ …), queues it in `data\/decisions.json`, and **executes via the openloomi native agent API by default** (`POST http:\/\/127.0.0.1:3414\/api\/native\/agent`, the same agentic endpoint the locomo benchmark uses — supports tool use, memory writes, multi-round reasoning; no agent install needed) — with a pluggable spawned-CLI fallback (`claude -p` \/ `codex` \/ `aider` \/ anything via `LOOP_AGENT_BIN`). Triggers: 'openloomi loop', 'loop tick', 'loop schedule', 'loop inbox', 'loop run', 'proactive decisions', 'signal → decision → execute', 'pull signals', 'decision queue', 'loop serve'",
    "allowed-tools": "Bash(node $SKILL_DIR\/scripts\/openloomi-loop.cjs *), Bash(node $SKILL_DIR\/scripts\/loop-tick.cjs *), Bash(node $SKILL_DIR\/scripts\/obsidian-scan.cjs *), Bash(node ..\/..\/openloomi-memory\/scripts\/openloomi-memory.cjs *), Bash(curl *), Bash(claude *), Bash(codex *), Bash(aider *), Bash(tail -f $SKILL_DIR\/data\/daemon.log), Bash(cat >> $SKILL_DIR\/data\/signals.jsonl), Bash(echo *), Bash(ls *)"
}

Note: If you haven't downloaded or installed openloomi yet, please refer to Getting Started for installation instructions.

OpenLoomi Loop — The Proactive Execution Brain

Proactive & Continuous — watches external signals, thinks via openloomi-memory, and acts via the openloomi AI API (or any agent runtime you choose). Proactive Execution Brain — the always-on execution layer of openloomi.

A Claude Code skill that runs proactively and continuously, turning ambient signals from your connected tools into finished work. The Loop is openloomi's proactive execution brain — a vigilant teammate that watches, thinks, and acts without you having to ask. Three layers, all agentic:

  1. Pull — Claude fetches fresh signals (Gmail, Calendar, GitHub, Slack) into a local signal store (data/signals.jsonl) via any of three Composio surfaces: the Composio MCP (mcp__composio__* tools), the composio-cli skill (Claude calls Skill composio-cli …), or the composio CLI (Claude shells out with Bash(composio …)). The Loop doesn't care which surface — only the resulting signal envelope matters.
  2. Signal → Decision — Claude calls the openloomi-memory skill to enrich every signal with sender / project context, then converts every actionable signal into a typed decision (rsvp, draft_reply, review_pr, todo, slack_reply, …) and appends it to data/decisions.json. Signals that survive the hard-rule filters are never left raw — they are always either classified into a decision or explicitly dropped with a reason. The queue is the single source of truth for "what's next."
  3. Execute — You browse the decision queue and pick. Picking hands the built prompt to an agent runtime — by default the openloomi native agent API (POST http://127.0.0.1:3414/api/native/agent, the same agentic endpoint the locomo benchmark uses; supports tool use, memory writes, multi-round reasoning; authenticated with ~/.openloomi/token; no agent install required). The runtime can be swapped to any spawned CLI agent (claude -p / codex / aider / custom binary, picked via LOOP_AGENT_BIN) or, when already inside a Claude Code session, executed in-session via direct tool calls. All three surfaces read the same prompt, dispatch the same action.kind, and write memory back via openloomi-memory.

No background daemon. No subprocess hacks. No local memory cache. The Loop is Claude pulling signals, Claude enriching with memory, Claude acting — every layer is agentic. The brain never sleeps: it ticks, watches, and remembers.


Proactive & Continuous

The Loop is not a one-shot tool you invoke. It is a continuously running execution brain with two complementary properties:

  • Proactive — The Loop watches Gmail, Calendar, GitHub, and Slack in the background (via any of the three Composio surfaces — MCP, composio-cli skill, or composio CLI). It surfaces decisions before you ask: a meeting invitation becomes an rsvp suggestion, an unread email from a known person becomes a draft_reply card, a PR where you're a reviewer becomes a review_pr task. Nothing fires automatically — but everything is queued and waiting the moment you look.
  • Continuousloop schedule --interval N runs an infinite tick loop in the background. Each tick: pull new signals → enrich with memory → classify → queue. State persists in data/decisions.json, so the queue survives restarts, and each new signal joins the same ongoing conversation. loop watch keeps emitting desktop notifications on fresh entries.
  • Proactive Execution Brain — Openloomi's memory (openloomi-memory) stores what you know; the Loop is the brain that decides what to do about it. Itself not a daemon, not a script, not a cron — the Loop is an agent runtime, looping. Each tick is one call to whichever runtime is configured (openloomi AI API by default, or a spawned CLI agent); each executed decision is one more call. No Claude install required. Composability over persistence.

Together, Proactive and Continuous turns Claude into a teammate that never sleeps and never loses context: it remembers people (via openloomi-memory), watches the world (via any of the three Composio surfaces — MCP, composio-cli skill, or composio CLI), and prepares the next move (via the decision queue). You stay in control of execution; the Loop stays in control of awareness.


Quick Start

# 1. Ask your agent runtime to do one tick (prints the prompt it should run).
#    Default = openloomi native agent API (no install needed, just ~/.openloomi/token):
PROMPT=$(node $SKILL_DIR/scripts/openloomi-loop.cjs tick --json | jq -r .prompt)
curl -sX POST http://127.0.0.1:3414/api/native/agent \
  -H "Authorization: Bearer $(cat ~/.openloomi/token | base64 -d)" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg p "$PROMPT" '{prompt: $p, provider: "claude"}')"

# Or, if you have the `claude` CLI installed and want a spawned agent:
node $SKILL_DIR/scripts/openloomi-loop.cjs tick --compact | claude -p

# Or run the tick from inside a Claude Code session — Claude will see the
# "Run openloomi-loop tick" prompt in its context and execute it directly.

# 2. Drop a signal into the queue (for testing without Composio connected)
echo '{"source":"gmail","type":"email","payload":{"from":"Sarah <sarah@acme.com>","subject":"Q2 review tomorrow","labels":["INBOX"]}}' \
  | node $SKILL_DIR/scripts/openloomi-loop.cjs inject -

# 3. Run the lib-level analyze (ingest inbox → classify → decisions)
node $SKILL_DIR/scripts/openloomi-loop.cjs analyze

# 4. Browse the decision queue
node $SKILL_DIR/scripts/openloomi-loop.cjs inbox           # plain list
node $SKILL_DIR/scripts/openloomi-loop.cjs inbox --pick    # arrow-key picker

# 5. Run a decision (spawns a new claude code session with full context)
node $SKILL_DIR/scripts/openloomi-loop.cjs run dec_xxx

# 6. Optional: schedule ticks in the background every N seconds
node $SKILL_DIR/scripts/openloomi-loop.cjs schedule --interval 600

# 7. Memory operations go through the openloomi-memory skill
node $SKILL_DIR/scripts/openloomi-loop.cjs memory search-all "Sarah"

Quick start/stop with loop-ctl.sh

For day-to-day use, prefer the bundled loop-ctl.sh helper over running the CLI directly. It manages both the schedule background loop and the web UI as a pair, writes PID files for clean shutdown, and self-heals the data/ directory on first run.

# Start schedule + web (defaults: INTERVAL=600s, LOOP_WEB_PORT=3614)
$SKILL_DIR/loop-ctl.sh start

# Check what's running
$SKILL_DIR/loop-ctl.sh status
#   schedule: pid=6948 uptime=18m05s
#   web:      pid=6949 http://127.0.0.1:3614/

# Restart (e.g. after editing scripts/)
$SKILL_DIR/loop-ctl.sh restart

# Stop both
$SKILL_DIR/loop-ctl.sh stop

# Override defaults
LOOP_WEB_PORT=4000 INTERVAL=300 $SKILL_DIR/loop-ctl.sh start

What it does:

  • start — runs openloomi-loop schedule --interval ${INTERVAL:-600} and openloomi-loop web --port ${LOOP_WEB_PORT:-3614} in the background. schedule writes its own PID to data/daemon.pid; the web PID is written to data/web.pid. Both stdout/stderr are redirected to data/schedule.log and data/web.log. Auto-mkdir of data/ so first-run after a git-clean works. Skips if either is already alive (no double-start).
  • stopSIGTERM each PID recorded in data/daemon.pid / data/web.pid, plus a pkill -f belt-and-suspenders for any orphan. Removes the PID files. No SIGKILL grace — spawned-agent children are expected to terminate cleanly on parent exit.
  • status — prints the loop status snapshot, checks that the web port is bound via lsof, and lists each PID file as alive / stale / not present.
  • restartstop then start.

It does not start a tick on its own — schedule spawns ticks every INTERVAL seconds, independent of any manual invocation. Pair with loop analyze or loop inject if you want to feed it ad-hoc.


Architecture

            ┌───────────────────────────────────────────────┐
            │                                               │
            ▼                                               │
   ┌──────────────┐    ┌──────────────┐    ┌───────────────┴──┐    ┌──────────────┐
   │  External    │───▶│   Context    │───▶│     Decision     │───▶│   Execute    │───▶ Output
   │ Environment  │    │    Layer     │    │      Layer       │    │    Layer     │
   │              │    │              │    │                  │    │              │
   │ Composio (3  │    │ signals.jsonl│    │ openloomi-memory │    │ Agent        │
   │ surfaces):   │    │  (raw sigs)  │    │  enrichment      │    │ runtime (3   │
   │ • MCP        │    │      │       │    │       │          │    │ surfaces):   │
   │   mcp__compos│    │      ▼       │    │       ▼          │    │ • openloomi  │
   │   io__*      │    │  signal →    │    │  signal →        │    │   AI API     │
   │ • composio-  │    │  decision    │    │  decision        │    │   (default,  │
   │   cli Skill  │    │  conversion  │    │  classification  │    │   no install)│
   │ • composio   │    │              │    │ (typed actions)  │    │ • spawned    │
   │   CLI        │    │              │    │ decisions.json   │    │   CLI agent  │
   │   ↘ (no       │    │              │    │                  │    │   (claude -p,│
   │  composio →)  │    │              │    │                  │    │   codex,     │
   │ list-insights │    │              │    │                  │    │   aider, …)  │
   │ (openloomi-   │    │              │    │                  │    │ • in-session │
   │  memory)      │    │              │    │                  │    │   (direct    │
   │ + data/inbox │    │              │    │                  │    │   tool calls)│
   └──────────────┘    └──────┬───────┘    └────────┬─────────┘    └──────┬───────┘
                              │                     │                    │
                              │       ┌─────────────┘                    │
                              │       │                                  │
                              ▼       ▼                                  ▼
                        openloomi-memory  (single source of truth:
                                           people, projects, insights,
                                           entities, RAG, temporal)

Execute-layer pick order (configurable; default shown):

  1. openloomi native agent API (POST http://127.0.0.1:3414/api/native/agent) — Authorization: Bearer $(cat ~/.openloomi/token | base64 -d). Default, no install. The same agentic endpoint the locomo benchmark uses; supports tool use, memory writes, multi-round reasoning, SSE streaming.
  2. Spawned CLI agent (claude -p / codex / aider / custom, picked via LOOP_AGENT_BIN) — only when the runtime needs features the native agent API can't drive (e.g. a different provider, a custom local toolset, or a CLI already wired into the user's shell). Costs a binary install and per-tick spawn.
  3. In-session — when the user is already inside a parent agent session (Claude Code, Cursor, etc.) and wants zero spawn cost. Uses the parent's tools directly. No API key or child process.

Signal → Decision is a hard contract, not a suggestion. Every signal that survives the hard-rule filters in step 5 must be turned into a queued decision (step 6) before the tick returns. If classify() cannot map a signal to a known decision type, the tick queues a {type: "unknown"} decision with reason: "no_matching_action" rather than dropping it silently — so a signal either becomes an actionable decision or becomes a visible queue item the user can act on. No raw signals linger past a tick.

Data flow per tick (agentic)

  1. Pull — Claude fetches fresh data through whichever Composio surface is available. The default is the Composio MCP (mcp__composio__COMPOSIO_MANAGE_CONNECTIONS + mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL in parallel for each connected toolkit). If MCP isn't loaded, Claude falls back to the composio-cli skill (Skill composio-cli to discover and execute tools), or shells out to the composio CLI directly (Bash(composio …)). All three surfaces are equivalent — pick whichever the runtime supports. For gmail and slack, if no Composio surface is connected for that channel, the tick falls back to openloomi-memory list-insights --channel=<gmail|slack> --days=N. For googlecalendar and github (no dedicated insight channel), the tick falls back to unfiltered list-insights --days=N and lets the existing classifier drop non-matching channels. data/inbox/*.json is always available as a manual path and is ingested by the lib-level tick. See "Synthesizing signals from insights" below for the payload mapping.
  2. Persist — Each new signal is appended to data/signals.jsonl (deduped by messageId / eventId / ts).
  3. Enrich — For every signal, Claude calls openloomi-memory to look up the sender / organizer / channel:
    • search-all <name-or-email> across local files + knowledge base + insights
    • search-memory <name> --directory=people for direct hits
    • list-entities --type=person --search=<name> for the entity registry
    • list-insights --channel=<gmail|slack|...> --days=7 for recent context
  4. Remember — New senders get add-memory calls into ~/.openloomi/data/memory/people/. Recurring calendar event titles get project notes under ~/.openloomi/data/memory/projects/.
  5. Filter — Hard rules skip noreply@* senders, Gmail Promotions/Social/Forums/Updates/Spam labels, already-accepted calendar events, already-replied emails.
  6. Convert signal → decision — Every survivor must be converted into a typed decision. Survivors get a typed action: rsvp, draft_reply, review_pr, todo, slack_reply, … A signal that cannot be classified is not silently dropped — it is queued as {type: "unknown", reason: "no_matching_action"} so the human can decide, or the classifier / tick prompt can be extended. The Loop never holds a raw signal in memory that hasn't been turned into a queued decision.
  7. Queue — Decisions are added to data/decisions.json via loop ingest-decision '<json>' with memory_refs listing the openloomi-memory entries cited. Confidence is 0.85 when the sender is in openloomi-memory, else 0.60.
  8. Status — A snapshot is written to data/status.json for loop status.

The tick is strictly read/derive. No external destructive action runs during a tick — execution is always on user request via loop run <id>.

Synthesizing signals from insights

When the composio toolkit is not registered for a channel, the tick prompt instructs Claude to fall back to openloomi-memory list-insights and map each returned insight into the same data/signals.jsonl payload shape that the composio path produces. The mapping is:

Insight channel Synthesized signal
gmail type: "email" with payload.messageId = insight.id, payload.from = insight.people[0], payload.subject = insight.title, etc.
slack type: "slack_message" with payload.channel, payload.ts, payload.user, payload.text. mentions_me = false (insights don't carry this flag — conservative).
google_calendar, github Pull unfiltered insights; synthesize based on insight.groups[0]; signals whose type is not recognized by classify() are safely dropped.
Other (telegram, whatsapp, discord, linkedin, twitter, weixin, rss, …) Synthesize as type: "<channel>_message"; classifier returns null and the signal is dropped.

Each synthesized signal carries _origin: "insights" in its envelope so data/daemon.log can account for which path produced it. Dedup uses both the existing messageId / eventId / ts keys and a new _insightId = insight.id key, so toggling composio on/off between ticks does not double-insert.


Commands

Command Purpose
tick [--compact] [--json] [--config k=v] Print the prompt Claude runs for one Loop tick. --compact for cron; --json for structured output.
schedule [--interval N] [--watch-interval N] Loop: call the agent runtime with loop tick --compact every N seconds and watch for new decisions (desktop notifications). The tick and watch run on independent timers — a hung tick never blocks notifications. Tick is hard-killed after LOOP_AGENT_TIMEOUT_MS (default 15 min). Writes its own PID for stop.
watch [--interval N] Poll decisions.json every N seconds and fire desktop notifications on new pending decisions. Pair with external ticks (cron, another loop schedule) to feed it.
notify [--all] [--webhook URL] Manually fire notifications. --all notifies every current pending; default notifies only new (unseen) ones. Webhook (Slack-compatible JSON) optional via --webhook or env LOOP_NOTIFY_WEBHOOK.
ingest-decision <json|- or file> Append a decision to decisions.json. Called by the Claude tick agent.
analyze [--seen-init] Lib-level tick: ingest data/inbox/ → classify → decisions. Memory enrichment is skipped (the agentic tick handles that). --seen-init also clears data/notifications.seen.json so a running watch will re-fire notifications for all current pending on its next poll.
pull Alias for analyze (kept for backwards compat).
status Show last-tick snapshot + counts + config + current watch session (pid, started_at, host).
summary [--since=ISO] Activity report from notifications.log. Default = current watch session window. --since=<ISO> = everything from that timestamp. Batches tagged [pid=X] (this session) vs [pid=?] (pre-session, historical). Use to answer "what did I receive this session?" without conflating historical log entries.
inbox [--pick] [--limit N] List pending decisions (interactive picker).
decisions [--status pending|done|dismissed|all] List decisions by bucket.
decision <id> Show full JSON for one decision.
run <id> [--dry] Hand the built prompt to the configured agent runtime (openloomi AI API by default; spawned CLI agent fallback).
dismiss <id> [--reason ...] Mark as dismissed.
inject <file|-> Drop a signal JSON into data/inbox/.
memory <subcommand> [args...] Delegate to the openloomi-memory CLI: search-all, search-memory, list-insights, add-memory, add-insight, etc.
config [get|set k v] Read/edit config.
logs [-n N] Tail the loop log.
serve REPL: list, run <id>, dismiss <id>, analyze, status, quit.
web [--port N] [--no-open] Start HTTP server with REST API + Ink & Circuit style UI at http://127.0.0.1:N/. Auto-opens browser. CLI default port 3614collides with the openloomi desktop app, which binds 3614. When the app is running, use --port 3614 (or any other free port), or run via $SKILL_DIR/loop-ctl.sh start which defaults to 3614 to avoid the clash.

Notification channels

Every fired notification is written to three places:

  1. data/notifications.log — append-only log of all notifications
  2. macOS desktop notification — via osascript (no extra deps); auto-suppressed on other platforms
  3. Webhook — Slack-compatible JSON POST, if LOOP_NOTIFY_WEBHOOK env var is set or --webhook is passed to notify

The watcher maintains data/notifications.seen.json to ensure each decision fires exactly once.

All commands operate on $SKILL_DIR/data/ for the signal/decision store. Memory is delegated entirely to openloomi-memory.


Web UI — loop web

loop web (or node scripts/loop-web.cjs <port>) starts an HTTP server (override with --port N or LOOP_WEB_PORT). The CLI default is 3614, but the openloomi desktop app also binds 3614 — if both run on the same machine, the second one to start will fail with EADDRINUSE. The bundled $SKILL_DIR/loop-ctl.sh start defaults to 3614 to sidestep the conflict. Auto-opens the default browser.

Ink & Circuit themed UI (amber/dark, Syne + Space Grotesk + JetBrains Mono, hex markers, circuit corners) with three views:

View What it shows
Q Queue 3-column kanban: PENDING / DONE / DISMISSED. Each card shows type badge, confidence bar, triggering context (⏰/✉️/🔀/💬), 👤 person, 🧠 memory refs, action line. Click → detail panel.
T Timeline Canvas graph — decisions as hex nodes positioned by time, grouped by type. Pan/zoom. Click → detail.
A Activity Split view: live notifications.log feed (left) + recent decisions (right). Auto-refreshes every 4s.

Detail panel (slide-in from right):

  • Why-this-surfaced trail, triggering context (organizer/sender/time/labels/snippet/branch)
  • 👤 Known contact · 🧠 memory refs (click to inline-load file content)
  • Suggested action JSON · raw source signal (click to view original inbox/.processed/*.json)
  • Action buttons: ▶ RUN (hands prompt to agent runtime), DRY RUN (shows full prompt), ✓ MARK DONE, ✕ DISMISS

Keyboard: Q / T / A switch views · / open search · ↑↓ navigate · Enter run selected · Esc close.

REST API (CORS-enabled, returns JSON):

GET  /api/state                counts, last tick, status
GET  /api/decisions            { pending, done, dismissed }
GET  /api/decision/:id         full decision + bucket
GET  /api/signals?limit=50     tail of signals.jsonl
GET  /api/notifications?limit=50  tail of notifications.log
GET  /api/memory?path=<rel>    read ~/.openloomi/data/memory/<rel>   (path-traversal safe)
GET  /api/source?path=<rel>    read data/inbox/<rel>                 (path-traversal safe)
POST /api/run/:id[?dry=1]      hand prompt to agent runtime (or return prompt if dry=1)
POST /api/dismiss/:id          move pending → dismissed
POST /api/done/:id             move pending → done
POST /api/notify               fire macOS desktop test notification

Static UI files served from web/. No external deps; no auth (binds 127.0.0.1 only).

Design system — Ink & Circuit

The web UI is built on the Ink & Circuit visual language. Reference files in references/:

File What
references/DESIGN.md Canonical design tokens (colors, type, layout), component patterns, animation rules, keyboard map, "how to adapt to a new domain" guide. Update first when extending the visual language.
references/index.html The design source this UI was adapted from. Keep untouched as a visual reference.
web/index.html The openloomi-loop implementation. Has a header comment linking to ../references/DESIGN.md.

The design system maps 5 decision types to the 5 knowledge-graph categories: rsvp (amber), draft_reply (green), review_pr (blue), slack_reply (purple), todo (red). When adding a new decision type, add a CSS variable, a .t-<type> card class, a hex color in JS TC, and a label in TL.


Decision Types

Type Trigger Action
rsvp Calendar event with my_response: needsAction calendar_rsvp
draft_reply Email matching meeting/RSVP/invite patterns, or with action verbs (please/could you/need/urgent/…) from a known person email_reply
review_pr GitHub PR where you're a reviewer github_review
todo GitHub issue assigned to you, open todo
slack_reply Slack message that mentions you slack_reply

Confidence is 0.85 when sender is in openloomi-memory (known contact), else 0.60.


Running a Decision → Agent Runtime

run <id> builds a prompt like:

You are executing an openloomi Loop decision. The user picked this from a proactive suggestion list.

DECISION TYPE: draft_reply
TITLE: Reply: Q2 Roadmap Review tomorrow - please RSVP
CONFIDENCE: 0.85

WHY THIS SURFACED:
- Source: gmail:email
- Subject: Q2 Roadmap Review tomorrow - please RSVP

MEMORY REFS (openloomi-memory):
- people/sarah_chen.md  (3 prior interactions)
- insights/insight_abc  (related deadline discussion from last week)

SOURCE SIGNAL (gmail:email):
{ ...payload... }

SUGGESTED ACTION:
{ "kind": "email_reply", "params": { ... } }

Execute this action now. Steps:
1. Confirm what you're about to do in one line.
2. Take the action (read files, draft replies, update tasks — whatever the action calls for).
3. When done, summarize in 3 bullets: what changed, what was written to memory, follow-ups.
4. If any step is destructive or sends externally, STOP and ask the user to confirm before continuing.

For any new people or insights discovered, use the openloomi-memory skill:
  node $OPENLOOMI_MEMORY_DIR/scripts/openloomi-memory.cjs add-memory ...
  node $OPENLOOMI_MEMORY_DIR/scripts/openloomi-memory.cjs add-insight ...

…then hands that prompt to whichever agent runtime is configured. There are three interchangeable surfaces — pick based on what you have installed and where the loop is running:

Surface A — openloomi native agent API (default, no install required)

The Loop's default execution surface is the native agent API served by the same openloomi desktop app / local server that the locomo benchmark hits: POST http://127.0.0.1:3414/api/native/agent (cloud override: https://app.alloomi.ai/api/native/agent). It accepts a prompt, drives the underlying model with tool use + memory reads/writes + multi-round reasoning, and streams the answer back as Server-Sent Events.

Authentication uses the same JWT the desktop app stores locally:

TOKEN=$(cat ~/.openloomi/token | base64 -d)
PROMPT='You are executing an openloomi Loop decision …'
curl -sNX POST http://127.0.0.1:3414/api/native/agent \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg p "$PROMPT" '{prompt: $p, provider: "claude"}')"

Response is an SSE stream, one data: {…} event per line. Loop the response, pick the events you care about:

Event type Meaning
session First event — carries sessionId and messageId. Save it for tracing.
reasoning Model's chain-of-thought / scratchpad (optional, can be filtered out).
text The model's user-visible reply (content is the chunk). Concatenate all text chunks.
tool_use Model invoked a tool (the agent API drives tool calls server-side, you don't have to handle them).
tool_result Tool returned. Loop can ignore — the agent API chains it back into the model.
done Final event — model finished, stream complete.

A minimal parser that just collects the user-visible answer:

const lines = responseText.split('\n');
let answer = '';
for (const line of lines) {
  if (!line.startsWith('data:')) continue;
  const raw = line.slice(5).trim();
  if (!raw || raw === '[DONE]') continue;
  const evt = JSON.parse(raw);
  if (evt.type === 'text' && evt.content) answer += evt.content;
}

Why this is the default: zero install, identical auth to the desktop app, true agentic behavior (tools + memory + multi-round), SSE streaming, works inside any sandbox, runs from cron / CI without extra config. The Loop's run <id> defaults to this surface and uses the same provider/model the rest of openloomi picks.

Limitations: the model and tool policy are server-side; you don't get to swap them mid-request. If you need a different provider (Anthropic direct, OpenAI, Gemini), use Surface B with a spawned CLI agent that already speaks that provider's API.

Surface B — Spawned CLI agent (configurable via LOOP_AGENT_BIN)

When a decision genuinely needs a stateful, tool-using agent — long-running shell sessions, persistent file edits, multi-round MCP — the Loop can shell out to any CLI that accepts a prompt on stdin / -p:

LOOP_AGENT_BIN=claude   node $SKILL_DIR/scripts/openloomi-loop.cjs run dec_abc   # default
LOOP_AGENT_BIN=codex    node $SKILL_DIR/scripts/openloomi-loop.cjs run dec_abc   # OpenAI Codex
LOOP_AGENT_BIN=aider    node $SKILL_DIR/scripts/openloomi-loop.cjs run dec_abc   # Aider
LOOP_AGENT_BIN=/opt/my-agent/bin/mybot  node …/run dec_abc                       # any custom binary

The spawned binary's stdio is inherited so you see its output directly. On exit, the decision is moved to done (exit 0) or dismissed (non-zero). Memory writeback happens inside the spawned agent itself via openloomi-memory.

This surface costs a binary install and per-tick spawn overhead. Use it only when Surface A can't drive the decision (e.g. the action requires terminal commands the API doesn't expose).

Surface C — In-session (the parent does the work)

When run <id> is invoked from inside an agent session that already has tools loaded (Claude Code, Cursor, etc.), the cleanest path is to not spawn anything — read the built prompt with --dry, then have the parent session call the tools itself. See the Recommended pattern (no bypass needed) section below.

This surface has no install requirement, no API key, and no spawn cost — the user is already paying for the parent's context.

Choosing a runtime

Concern Pick
No agent CLI installed; user only has openloomi Surface A (openloomi AI API)
Decision needs terminal / shell / file-system access Surface B (spawned agent with shell tools)
Decision needs persistent multi-round MCP tool use Surface B (spawned Claude/codex/aider)
User is already inside Claude Code / Cursor / another agent Surface C (in-session)
Running headless (cron, CI, container) with no agent installed Surface A (openloomi AI API)
Need full transparency / want to see the agent "think" Surface B with --verbose
Need to handle destructive actions safely (extra confirm gate) Surface A (cleanest for a single confirmation round)

On exit, the decision moves to done or dismissed. Any memory writeback happens in whatever runtime handled the call — for Surface A, the Loop itself POSTs back to openloomi-memory after the AI returns; for Surface B/C, the runtime does the writeback.

Use run <id> --dry to print the prompt without invoking any runtime.

⚠️ Known issue: Surface B (spawned agent) refuses to nest

When the Loop is being driven from inside another agent session that uses the same LOOP_AGENT_BIN (e.g. the user invokes loop run <id> from inside Claude Code, or the Web UI's ▶ RUN button is clicked while Claude Code is the parent), Surface B aborts with the agent's own nested-session error. For Claude Code that looks like:

Error: Claude Code cannot be launched inside another Claude Code session.
Nested sessions share runtime resources and will crash all active sessions.
To bypass this check, unset the CLAUDECODE environment variable.

Two caveats even with the bypass:

  1. Do NOT bypass blindly. If the inner agent exits, errors, or hangs, it can corrupt the parent's stdio / shared resources. The CLI's own check is correct.
  2. Failed spawn is recorded against the decision. The loop marks the decision as dismissed with result=null and increments the failure counter (failed (1/null) in the run log), even though no action was actually attempted.

Recommended pattern (no bypass needed):

Switch surfaces instead of bypassing. If you're already inside an agent session, you don't need Surface B at all — use Surface C (in-session) or Surface A (openloomi AI API) instead.

  1. loop run <id> --dry → read the built prompt and the suggested action.kind / action.params.

  2. Take the action in the parent agent session itself (Surface C), by calling Composio via whichever surface is loaded. Pick one — don't try all three:

    action.kind MCP call (if loaded) composio-cli Skill (if no MCP) composio CLI (if neither)
    calendar_rsvp mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOLGOOGLECALENDAR_PATCH_EVENT (event_id, calendar_id="primary", rsvp_response="accepted|declined|tentative", send_updates="none" to skip attendee spam) Skill composio-cli → "execute GOOGLECALENDAR_PATCH_EVENT on googlecalendar with …" Bash(composio googlecalendar patch_event --json '{…}')
    email_reply mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOLGMAIL_SEND_EMAIL (or GMAIL_CREATE_DRAFT for review-first) Skill composio-cli → "execute GMAIL_SEND_EMAIL on gmail with …" Bash(composio gmail send_email --json '{…}')
    github_review mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOLGITHUB_CREATE_REVIEW / GITHUB_ADD_REVIEW_COMMENT Skill composio-cli → "execute GITHUB_CREATE_REVIEW on github with …" Bash(composio github create_review --json '{…}')
    slack_reply mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOLSLACK_SEND_MESSAGE (channel, text, thread_ts) Skill composio-cli → "execute SLACK_SEND_MESSAGE on slack with …" Bash(composio slack send_message --json '{…}')
    todo mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOLGITHUB_UPDATE_ISSUE (assign / label) Skill composio-cli → "execute GITHUB_UPDATE_ISSUE on github with …" Bash(composio github update_issue --json '{…}')

    Tag the result with via: '<mcp|skill|cli>_in_session_api_call' so the decision log captures which surface executed it.

  3. Confirm the action landed (e.g. fetch the event / PR / message again and check the new state).

  4. Move the decision to done manually — there's no CLI subcommand for it, but it's one node call against decisions.json:

    const fs = require('fs');
    const p = '/path/to/openloomi-loop/data/decisions.json';  // the path `loop status` prints
    const d = JSON.parse(fs.readFileSync(p, 'utf8'));
    const id = 'dec_xxxxxxxxx';
    const item = (d.dismissed || []).splice(
      (d.dismissed || []).findIndex(x => x.id === id), 1
    )[0] || (d.pending || []).splice(
      (d.pending || []).findIndex(x => x.id === id), 1
    )[0];
    item.status = 'done';
    item.result = { action: '<kind>', ...item.action.params, via: 'in_session_api_call' };
    item.completed_at = new Date().toISOString();
    d.done.unshift(item);
    fs.writeFileSync(p, JSON.stringify(d, null, 2));
    

When forcing Surface B IS acceptable: only when the parent agent session is throwaway (e.g. a claude -p "…" one-shot from a shell, or a CI job that doesn't need the parent anymore). Not acceptable from inside an interactive Claude Code session that the user wants to keep.


Sources

The Loop accepts signals from four surfaces — three Composio-shaped (pick whichever the runtime supports) plus a manual drop folder. They're tried in priority order and produce the same signal envelope, so downstream code is identical.

# Source What it pulls When enabled Transport
1 Composio MCP mcp__composio__COMPOSIO_MANAGE_CONNECTIONS lists registered toolkits; mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL calls them in parallel. Pulls connected toolkits: gmail, googlecalendar, github, slack, etc. Whenever the user has connected toolkits via Composio and the MCP server is loaded in this Claude Code session. Structured args, no shell — fastest.
2 Composio Skill (composio-cli) Claude invokes Skill composio-cli … to discover tools, connect accounts, and execute actions. Same toolkit coverage as MCP. When MCP isn't loaded but the composio-cli skill is installed in the Claude Code session. Skill-mediated — loads schemas on demand.
3 Composio CLI Bash(composio <toolkit> <action> --json …) shells out to the installed composio binary. Same toolkit coverage. Headless contexts (cron, Surface A scheduled runs, CI, container) where neither MCP nor the skill is available. Most portable. Subprocess spawn + JSON parse per call.
4 openloomi-memory insights (list-insights) Pre-extracted summaries for channels synced by openloomi-connectors (gmail, slack, telegram, whatsapp, etc.). Synthesized into signal-shape payloads; the existing classifier handles gmail/slack and drops everything else. Always on. Kicks in only when no Composio surface is connected for that channel. File-backed — no external auth needed.
5 Obsidian vault (OBSIDIAN_VAULT env) Recursively scans a local Obsidian vault for .md files whose mtime changed since the last tick. Emits one obsidian_note_changed signal per changed file (capped at OBSIDIAN_VAULT_CAP, default 50). When OBSIDIAN_VAULT is set. Tauri / desktop reads the path directly; browser web view reads from a FileSystemDirectoryHandle persisted in IndexedDB. Filesystem — desktop: @tauri-apps/plugin-fs; web: File System Access API; headless: node:fs.
openloomi-memory (mcp__* or CLI) Memory reads/writes for enrichment. Always — required for proper context. Skill / CLI.
data/inbox/*.json Manual drop folder (or any non-Composio bridge script). Default enableSources.file: true. Filesystem.

The Loop tries sources in the order 1 → 2 → 3 → 4 for each requested channel and uses the first one that returns data. This means: if MCP works, it always wins; if MCP is missing, composio-cli Skill picks up; if both are missing (e.g. cron), composio CLI runs; if no Composio surface is connected at all, the openloomi-memory insights fallback kicks in.

Inject format

{
  "source": "gmail",
  "type": "email",
  "payload": {
    "from": "Sarah Chen <sarah@acme.com>",
    "subject": "Q2 Roadmap Review",
    "snippet": "...",
    "threadId": "t1",
    "labels": ["INBOX", "IMPORTANT"]
  }
}

Or for calendar:

{
  "source": "google_calendar",
  "type": "calendar_event",
  "payload": {
    "eventId": "evt1",
    "title": "Design review with Jamie",
    "start": "2026-06-26T15:00:00Z",
    "my_response": "needsAction"
  }
}

Configuration

loop config get
loop config set intervalSec 600       # for `loop schedule` (10 min)
loop config set noReplySkip false
loop config set enableSources.file false

Defaults:

{
  "intervalSec": 600,
  "maxSignals": 5000,
  "maxDecisions": 500,
  "autoRun": false,
  "enableSources": { "composio": true, "openloomi": true, "file": true },
  "noReplySkip": true,
  "promotionSkip": true
}

Environment variables

Var Default Effect
LOOP_AGENT_RUNTIME api Which Execute surface to use. api = Surface A (openloomi AI API). cli = Surface B (spawned CLI agent from LOOP_AGENT_BIN). auto = Surface A if reachable, else Surface B.
LOOP_AGENT_BIN claude Binary invoked for Surface B (schedule, run, tick). Any CLI that accepts a prompt on -p works — claude, codex, aider, custom.
LOOP_AGENT_TIMEOUT_MS 900000 (15 min) Hard timeout for one Surface B child. On timeout: SIGTERM → 5s grace → SIGKILL. Prevents a hung tick from blocking notifications.
LOOP_AGENT_SAFE_PERMISSIONS (unset) Set to 1 to opt out of --dangerously-skip-permissions for the spawned Claude child. Default adds it so the tick can call mcp__composio__* and the openloomi CLIs without per-call prompts. Ticks are read/derive only — no email sends, no RSVPs, no dismisses — so the flag is safe. Ignored on non-Claude agents.
LOOP_NATIVE_AGENT_URL http://127.0.0.1:3414 Base URL for Surface A (openloomi native agent API). Loop appends /api/native/agent. Override to https://app.alloomi.ai for cloud.
LOOP_NATIVE_AGENT_PROVIDER claude provider field sent in the Surface A request body. The server picks the matching model + auth.
LOOP_NATIVE_AGENT_TIMEOUT_MS 2400000 (40 min) HTTP timeout for one Surface A request. The native agent is multi-round; 40 min mirrors the locomo benchmark default so long tool-use chains don't get cut off. Drop to 60–120 s if you want fail-fast on stuck decisions.
LOOP_OPENLOOMI_TOKEN ~/.openloomi/token Path to the base64-encoded JWT used to authenticate Surface A. Override only for testing / multi-account setups. The desktop app writes this on login; the Loop reads it on each request.
LOOP_WEB_PORT 3614 Default port for loop web. CLI / LOOP_WEB_PORT defaults to 3614, which conflicts with the openloomi desktop app's Next.js server on the same port. loop-ctl.sh defaults to 3614 to avoid that clash; override per-call with --port N or this env var.
LOOP_NOTIFY_WEBHOOK (unset) If set, every notification also POSTs a Slack-compatible JSON payload to this URL.
OBSIDIAN_VAULT (unset) Absolute path to the user's local Obsidian vault. When set, each tick scans the vault for .md files whose mtime changed since the last scan and emits one obsidian_note_changed signal per change into data/signals.jsonl. The Tauri desktop app reads the path directly; the browser web view uses a FileSystemDirectoryHandle persisted in IndexedDB (picked via Settings → Obsidian vault). Skip the step entirely by leaving this unset.
OBSIDIAN_VAULT_EXT .md Comma-separated extensions the scanner treats as Obsidian notes. Change to .md,.markdown if you also keep raw .markdown files.
OBSIDIAN_VAULT_CAP 50 Max obsidian_note_changed signals per tick. Beyond this the scanner emits a single obsidian_scan_overflow signal and drops the rest. Prevents a fresh clone of the vault from swamping the queue.
OBSIDIAN_VAULT_RECURSIVE 1 Set to 0 to scan only the vault root (skip subdirectories). Default recurses while skipping hidden and node_modules directories.

Watch independence + --seen-init

loop schedule runs two independent timers: one for ticks (--interval, default 600s) and one for watching (--watch-interval, default 5s). A hung tick can never block notifications — the watch loop polls data/decisions.json every 5s regardless.

If you want to re-fire notifications for everything currently pending (e.g. after fixing a bug in the notification path, or to demo it), run loop analyze --seen-init. This clears data/notifications.seen.json, and the next watch poll — even on a running loop schedule / loop watch — will treat every current pending decision as new.


Data Layout

$SKILL_DIR/data/                       # managed by the loop skill
├── daemon.pid          # current `loop schedule` PID (if running)
├── daemon.log          # append-only log
├── watch.session.json  # { pid, started_at, host } — written by `loop watch`/`schedule` on start; used by `loop summary` to scope "this session" reports
├── notifications.log   # append-only notification audit trail; lines tagged `[pid=X started=Y]` from the current watch
├── notifications.seen.json # dedupe: which decision IDs have already fired
├── status.json         # last tick snapshot
├── config.json         # config
├── decisions.json      # { pending: [], done: [], dismissed: [] }
├── signals.jsonl       # append-only signal log (capped at maxSignals)
└── inbox/              # drop folder for manual signal injection
    ├── *.json          # new signals to ingest
    ├── .processed/     # ingested files
    └── .failed/        # malformed files

~/.openloomi/data/memory/              # managed by openloomi-memory
├── people/             # { email-sanitized }.md   (auto-grown by tick)
├── projects/           # { title-sanitized }.md
├── chats/, channels/, notes/, strategy/
└── ... (full set in openloomi-memory SKILL.md)

The loop skill does not write to ~/.openloomi/data/memory/ directly — it delegates to the openloomi-memory CLI which handles filesystem layout, naming, and idempotency.


Examples

End-to-end demo (no Composio needed)

# Clean state
rm -f $SKILL_DIR/data/decisions.json $SKILL_DIR/data/signals.jsonl

# Drop 3 signals
for s in \
  '{"source":"gmail","type":"email","payload":{"from":"Sarah <sarah@acme.com>","subject":"Q2 review tomorrow please RSVP","labels":["INBOX"]}}' \
  '{"source":"google_calendar","type":"calendar_event","payload":{"eventId":"e1","title":"Design review with Jamie","my_response":"needsAction"}}' \
  '{"source":"github","type":"github_pr","payload":{"repo":"x/y","number":42,"title":"Refactor auth","state":"open","user_is_reviewer":true}}'
; do echo "$s" | loop inject -; done

# Analyze (lib-level, no memory enrichment)
loop analyze

# Browse queue
loop inbox

# Pick the first one
loop run $(loop inbox | grep -oE 'dec_[a-z0-9]+' | head -1)

# Memory peek (delegates to openloomi-memory)
loop memory search-all "Sarah"

Periodic background ticks (cron / launchd)

# Foreground loop (Ctrl+C to stop). Defaults to Surface A (openloomi native agent API).
loop schedule --interval 600          # tick every 10 minutes

# Or one-shot via launchd / cron, every 10 min — Surface A, no agent install needed:
*/10 * * * * /usr/local/bin/node $SKILL_DIR/scripts/openloomi-loop.cjs tick --json \
  | xargs -I{} curl -sNX POST $LOOP_NATIVE_AGENT_URL/api/native/agent \
      -H "Authorization: Bearer $(cat ~/.openloomi/token | base64 -d)" \
      -H "Content-Type: application/json" -d '{}'

# Or, if you have the `claude` CLI installed and want Surface B:
LOOP_AGENT_RUNTIME=cli LOOP_AGENT_BIN=claude loop schedule --interval 600

REPL session

loop serve
# loop> list
# loop> run dec_xxx
# loop> dismiss dec_yyy
# loop> analyze
# loop> status
# loop> quit

Activity report — "what did I receive this session?"

# Default = current watch session window (uses data/watch.session.json)
loop summary
# scope:   current session (pid=26402 started=2026-06-25T11:50:22.290Z)
# batches: 1
# notified: 1 decisions
# types:
#   review_pr      1
#
# batches:
#   2026-06-25T11:50:43.304Z  [pid=26402]  1 new

# Custom window — everything since 09:00 today
loop summary --since=2026-06-25T09:00:00Z
# Batches tagged [pid=X] are from a known watch process; [pid=?]
# marks pre-session (historical) entries from before session tracking existed.

Adding a new tool's signals

  1. Append a normalized payload to data/signals.jsonl (one JSON object per line).
  2. Add a classifier branch in loop-lib.cjs → classify() if the new signal type warrants a new decision type.
  3. Extend the tick prompt in loop-tick.cjs to teach Claude how to fetch from the new toolkit via the available Composio surface (MCP → composio-cli skill → composio CLI → openloomi-memory insights fallback, in that order).

Extending Signals, Decisions, and Actions

The Loop has three independent extension axes. You can add any one of them without touching the other two — but a new signal usually needs all three wired up before it can reach a user-actionable button.

Mental model

   raw event (Gmail / Calendar / GitHub / your-own-bridge)
        │
        ▼  normalize
   ┌─────────┐
   │ signal  │   ← what the world said
   └────┬────┘
        │   classifier (loop-lib.cjs → classify())
        ▼   hard contract: every survivor becomes a decision
   ┌──────────┐
   │ decision │   ← what we should do
   └────┬─────┘
        │   executor (run <id> → buildPrompt() → agent runtime: API | CLI | in-session)
        ▼   action.kind tells the executor which Composio tool to call
   ┌────────┐
   │ action │   ← what we actually did
   └────────┘
  • Signal = a normalized JSON envelope on data/signals.jsonl. Source-agnostic.
  • Decision = { type, title, action: { kind, params }, memory_refs, confidence, ... } queued in data/decisions.json. Always derived from a signal.
  • Action = { kind, params } — the typed instruction the executor runs. Decoupled from the decision type (one decision type can map to several actions over time).

1. Add a new signal source (channel)

A "signal source" is any path that emits a normalized JSON envelope into data/signals.jsonl (or data/inbox/*.json). Four concrete paths — three are Composio surfaces (pick whichever the runtime supports), plus a manual escape hatch:

Path Surface Where to wire it
Composio MCP mcp__composio__* tools (Claude Code MCP client) Teach the agentic tick prompt in loop-tick.cjs to call mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL for the new toolkit, and synthesize the result into the signal shape below.
Composio Skill composio-cli skill (Claude invokes Skill composio-cli …) Same destination — different transport. The tick prompt should treat Skill composio-cli <action> as equivalent to the MCP COMPOSIO_MULTI_EXECUTE_TOOL call. Use this when the MCP server isn't loaded but the composio-cli skill is installed.
Composio CLI composio binary in $PATH The tick prompt runs Bash(composio <toolkit> <action> --json …) and parses stdout. Useful when neither MCP nor the skill is available — e.g. headless CI, Surface A scheduled cron runs, or a stripped-down container.
openloomi-memory insights list-insights CLI No code change needed — list-insights --channel=<name> --days=N is already a fallback. Just make sure the channel is synced via openloomi-connectors. The fallback synthesizer in loop-tick.cjs will map insights to <channel>_message signals automatically.
Manual / bridge script file drop or loop inject - Drop a .json file into data/inbox/ (or loop inject - on stdin). Anything that writes to data/signals.jsonl directly is also fine — dedupe keys (messageId / eventId / ts / _insightId) prevent double-insert.

Choosing between the three Composio surfaces

All three return the same Composio tool result — they differ only in transport:

Surface Best when… Cost / latency
MCP (mcp__composio__*) MCP server is loaded in the current Claude Code session. Fastest (no shell, structured args). One MCP handshake per session.
composio-cli Skill MCP server is not loaded but the skill is installed. Useful for portable / project-local setups. Slightly slower — skill loads schemas on demand.
composio CLI Headless contexts (cron, Surface A scheduled runs, CI), no MCP server, no skill installed. Most portable. Slowest — subprocess spawn + JSON parse per call. Prefer parallel batches.

The tick prompt should try them in this order: MCP → composio-cli skill → composio CLI → openloomi-memory insights fallback, stopping at the first that returns data. Don't try all four blindly — pick the highest-fidelity one available.

Required envelope shape — every signal must have at minimum:

{
  "source":  "gmail",                       // channel id (free-form, used for log grouping)
  "type":    "email",                       // semantic type the classifier branches on
  "payload": { "from": "...", "subject": "..." /* channel-specific */ },
  "ts":      "2026-06-30T08:00:00Z",        // ISO timestamp, used for ordering + dedupe
  "messageId": "gmail:abc123",              // dedupe key (or eventId / ts)
  "_origin": "composio"                     // "composio" | "insights" | "inbox" (optional)
}

Conventions:

  • source is the channel (toolkit / provider). type is the semantic kind within that channel. A new source can reuse an existing type (e.g. a trello source with type: "trello_message"); the classifier only branches on type.
  • If a dedupe key isn't natural (RSS, scrape, manual drop), set _insightId to a stable hash of the payload — the dedupe code accepts it as a fallback.
  • The hard-rule filters in loop-lib.cjs → isHardSkipped() are currently Gmail-flavored. New channels should extend that function (or run their own pre-filter before appending) so noreply@* / mailer-daemon / etc. are skipped at the signal level, not the decision level.

Obsidian vault (optional)

When OBSIDIAN_VAULT is set, each tick runs scripts/obsidian-scan.cjs as a fifth signal source (after the Composio path runs, before classify). The scanner:

  1. Recursively walks the vault (Tauri: @tauri-apps/plugin-fs readDir + stat; browser: FileSystemDirectoryHandle.entries(); headless: node:fs) and filters to the configured extensions (default .md).
  2. Diffs each entry's mtimeMs against data/obsidian.state.json. The state file is rewritten every tick with the new mtime map so the next tick only emits signals for files that actually changed.
  3. For each change, appends one NDJSON line to data/signals.jsonl with source: "obsidian", type: "obsidian_note_changed", and payload: { path, mtime_ms, size, vault }. The path is slash-normalized and relative to the vault root (e.g. ideas/onboarding_redesign.md), so memory lookups by path are portable.
  4. Caps per-tick emissions at OBSIDIAN_VAULT_CAP (default 50) and emits a single obsidian_scan_overflow signal with the dropped count if exceeded — protects the queue from a fresh clone of a large vault.

The vault itself is not a Composio toolkit — it's a local filesystem adapter, which is what makes the multi-source "product-research iteration" scenario (Linear + GitHub + Obsidian) possible without adding another cloud dependency. On Tauri the path is read directly (no extra permissions beyond the app's existing scope). On the browser, the user picks the directory once via Settings → Obsidian vault; the resulting FileSystemDirectoryHandle is persisted in IndexedDB so the next tick reads without re-prompting. Safari and Firefox do not currently grant persistent directory access — the scanner silently skips in those browsers, the rest of the tick is unaffected.

The classifier maps each obsidian_note_changed signal to a typed decision based on its path prefix:

Path prefix Decision type
projects/, plans/ release_plan
people/ todo (action contact_update)
customers/ requirement_synthesis
ideas/, drafts/, (other) doc_update

The enrich step then reads up to ~2 KB of each changed note via the same PlatformFileSystem.readFile interface and indexes it into openloomi-memory keyed by path — so future linear_review / requirement_synthesis cards can look up people/sarah_chen.md, projects/q2_roadmap.md, ideas/onboarding_redesign.md by path to fold the same evidence into typed decisions.

2. Add a new decision type

A decision type is the user-facing label ("review a PR", "RSVP to a meeting"). Adding one has three touch points:

(a) The classifier branchscripts/loop-lib.cjs → classify(signal) must return an object of this shape:

{
  type:   '<decision_type>',       // NEW — e.g. 'merge_pr', 'archive_email'
  title:  'Human-readable line',   // shown in inbox, web UI cards, notifications
  action: { kind: '<action_kind>', params: { ... } },   // see §3
  memory_refs: [ /* optional, populated by the agentic tick */ ],
  confidence: 0.85 | 0.60,
}

Returning null from classify() means "I don't know what to do with this signal" — the tick should fall back to queuing a { type: 'unknown', reason: 'no_matching_action' } decision so the human sees it. Returning nothing / throwing breaks the signal → decision contract and must be avoided.

(b) The decision type table — update these so the new type renders and gets dispatched correctly:

File What to add
scripts/loop-lib.cjs A case '<your_type>': branch in classify() (or guard in the relevant signal-type branch).
SKILL.md (this file) A row in the Decision Types table (Type / Trigger / Action columns).
scripts/loop-web.cjs (UI) A hex color in TC, a label in TL, and a .t-<type> CSS class — see the Design system — Ink & Circuit section. The 5 default colors are amber / green / blue / purple / red; pick a new one and document the mapping.
scripts/loop-tick.cjs If the new type needs different enrichment logic (e.g. look up labels, not people), extend the tick prompt that teaches Claude how to enrich this type.

(c) The hard contract reminder — per §"Data flow per tick", every signal that survives hard filters must produce a decision. So when you add a classifier branch, you also implicitly accept responsibility for handling all the signals that match it. If your branch covers 90% and silently drops 10%, fix the branch — don't add a "skip silently" path.

3. Add a new action kind

An action kind is the executable verb the run prompt tells Claude to perform. It is intentionally decoupled from the decision type: one decision type can dispatch to multiple action kinds over its lifetime, and one action kind can be triggered by several decision types.

Where action kinds live:

Layer What it does Where to edit
Decision envelope Carries { action: { kind, params } } so the executor knows what to run. Set in classify() (or by the agentic tick via ingest-decision).
Executor prompt scripts/openloomi-loop.cjs → buildPrompt(dec) embeds kind / params into the prompt that gets sent to the agent runtime (openloomi AI API by default; spawned CLI agent via LOOP_AGENT_BIN; in-session when called from a parent agent). The default prompt already reads dec.action.kind and dec.action.params and instructs the runtime to dispatch. Custom per-kind prompts go here.
In-session fallback The "Recommended pattern (no bypass needed)" section describes how to handle a decision in the parent Claude session: call Composio via whichever surface is loaded (mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL, Skill composio-cli …, or Bash(composio …)) with the right tool slug and arguments, then mark the decision done manually. No code change needed — just document the Composio tool slug + required params in this file so the executor (whether spawned or in-session) knows what to call.

To add a new action kind, do this:

  1. Define the verb. Choose a kind string (snake_case, stable — changing it is a breaking change for existing queued decisions). Document it in a new row below.
  2. Define the params. What does the executor need? eventId for an RSVP, repo + number for a PR review, etc. Keep params JSON-serializable and Composio-call-shaped. Anything the executor can't reduce to a tool call belongs in the decision's context, not in params.
  3. Wire it into the run prompt. Either:
    • Rely on the default buildPrompt() output (it already says "take the action the action calls for") if the kind maps 1:1 to a Composio tool, or
    • Add a per-kind prompt section in buildPrompt() if the kind needs special handling (multi-step, confirmation gates, side effects to record). The prompt should instruct the executor to try MCP → composio-cli skill → composio CLI in that order, same as Pull.
  4. Document the executor path. Add a row to the table below so both humans and future-Claude know which Composio tool slug handles this kind, across all three surfaces.

Current action kinds

action.kind Composio tool slug MCP call composio-cli Skill call composio CLI call Agent runtime dispatch (any of 3 surfaces)
calendar_rsvp GOOGLECALENDAR_PATCH_EVENT mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL({ tool_slug, arguments: { event_id, calendar_id: "primary", rsvp_response, send_updates } }) Skill composio-cli → "execute GOOGLECALENDAR_PATCH_EVENT on googlecalendar with …" Bash(composio googlecalendar patch_event --json '{…}') The runtime reads params.eventId, decides accept/decline, calls via whichever surface is loaded. Works on Surface A (one HTTP roundtrip), Surface B (spawned agent uses the same call), Surface C (parent session calls directly).
email_reply GMAIL_SEND_EMAIL / GMAIL_CREATE_DRAFT mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL({ tool_slug, arguments: { to, subject, body, threadId } }) Skill composio-cli → "execute GMAIL_SEND_EMAIL on gmail with …" Bash(composio gmail send_email --json '{…}') Runtime drafts via params.to / params.subject / params.threadId. Surface A may return the draft text in the AI response and let the Loop POST the actual send to keep destructive sends behind a separate confirm.
github_review GITHUB_CREATE_REVIEW / GITHUB_ADD_REVIEW_COMMENT mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL({ tool_slug, arguments: { repo, number, body, event } }) Skill composio-cli → "execute GITHUB_CREATE_REVIEW on github with …" Bash(composio github create_review --json '{…}') Runtime reads params.repo / params.number. Surface A returns the review body; the Loop (or the runtime) posts it via Composio.
slack_reply SLACK_SEND_MESSAGE mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL({ tool_slug, arguments: { channel, text, thread_ts } }) Skill composio-cli → "execute SLACK_SEND_MESSAGE on slack with …" Bash(composio slack send_message --json '{…}') Runtime uses params.channel / params.ts.
todo GITHUB_UPDATE_ISSUE (assign / label) or local task tracker mcp__composio__COMPOSIO_MULTI_EXECUTE_TOOL({ tool_slug, arguments: { repo, number, state, labels } }) Skill composio-cli → "execute GITHUB_UPDATE_ISSUE on github with …" Bash(composio github update_issue --json '{…}') Runtime records against params.title / params.repo / params.number.

Adding a new kind — checklist

  • kind is snake_case and irreversible (treat it as an API).
  • params is a flat JSON object — no functions, no Buffers, no callbacks.
  • Composio tool slug (or in-session API call) is documented in the table above.
  • If the kind is destructive (sends, deletes, transfers money), the run prompt must explicitly instruct Claude to STOP and confirm with the user. The default prompt already does this; don't bypass it.
  • The decision can still be done / dismissed normally — no new status required.
  • loop run <id> --dry shows a sensible prompt for the new kind (verify by hand).

4. End-to-end extension recipe (worked example)

Suppose you want to add Trello: "when I'm @-mentioned on a Trello card, surface a reply_trello decision that opens the card and drafts a reply."

  1. Signal — Trello isn't on Composio out of the box, so wire it as an openloomi-connectors-synced channel. That makes it appear in openloomi-memory list-insights --channel=trello. The tick's fallback synthesizer emits { source: 'trello', type: 'trello_message', payload: { cardId, mentionsMe, text, url } }. Verify with loop inject - first.

  2. Decision type — pick type: 'reply_trello'. Add a case in loop-lib.cjs → classify() keyed off signal.type === 'trello_message' && p.mentionsMe. Document the new type in the Decision Types table, add a hex color to the web UI's TC map, add a label to TL, and add a .t-reply_trello CSS class.

  3. Action kind — pick kind: 'trello_reply' with params: { cardId, url }. Add a row to the Current action kinds table pointing at the Trello Composio tool slug (or REST call if not on Composio yet). The default buildPrompt() will pick it up; if the reply flow needs multi-step handling (fetch card → fetch comments → post reply), extend buildPrompt() with a per-kind section.

  4. Sanity check — drop a fake signal:

    echo '{"source":"trello","type":"trello_message","ts":"2026-06-30T08:00:00Z","messageId":"t1","payload":{"cardId":"c1","mentionsMe":true,"text":"@timi thoughts?","url":"https://trello.com/c/c1"}}' \
      | loop inject -
    loop analyze
    loop inbox           # should show 1 reply_trello decision
    loop run <id> --dry  # should show a prompt that mentions trello_reply
    
  5. Iterate — once green on --dry, flip the agentic tick (loop schedule --interval 600) and watch the new decisions flow in.

Hard contracts (don't break these when extending)

  1. Every survivor signal → a queued decision. classify() may return a typed decision, or the tick must queue a { type: 'unknown', reason: 'no_matching_action' } decision. Never let a signal silently disappear.
  2. action.kind is an API. Renaming a kind breaks every queued decision that referenced it. Add new kinds; don't repurpose old ones.
  3. action.params is JSON. No closures, no live objects, no Date instances — the decision may be reloaded days later from disk.
  4. Destructive actions confirm. If a new action kind sends, deletes, transfers, or charges, the spawned executor must ask before acting. The default prompt already does this; preserve the gate when adding per-kind prompt sections.
  5. Memory is openloomi-memory's job. New signal sources that learn about new people / projects should add-memory / add-insight via the openloomi-memory CLI — don't write to ~/.openloomi/data/memory/ directly from the loop skill.

Hard-Rule Filters (No-AI Decisions)

These run before the LLM classifier and can short-circuit:

Signal Outcome
Sender matches noreply@*, no-reply@*, donotreply@*, mailer-daemon@*, notifications?@* Skip
Gmail label in Promotions, Social, Forums, Updates, Spam Skip
Calendar event already accepted / declined / tentative Skip
Email already replied Skip

This keeps the LLM work small and the suggestion feed high-signal.


Companion Skills

When you want… Use
API endpoint reference openloomi-api
Connect / manage a platform openloomi-connectors
Search / write memory openloomi-memory (delegated target for all reads/writes)
User-facing product / setup openloomi-feature-guide

This skill (openloomi-loop) is the proactive executoropenloomi-memory is the memory layer. The Loop never owns its own memory; it asks openloomi-memory.


Reference

  • openloomi website: https://openloomi.ai
  • openloomi documents: https://openloomi.ai/docs
  • Composio MCP: mcp__composio__* tools (preferred when loaded)
  • Composio Skill: composio-cli skill (Skill composio-cli …, used when MCP server isn't loaded)
  • Composio CLI: composio binary in $PATH (Bash(composio …), used for headless / CI / scheduled runs)
  • openloomi native agent API (default Execute surface): POST http://127.0.0.1:3414/api/native/agent — agentic endpoint (tool use, memory, multi-round, SSE streaming), request body {prompt, provider}, Authorization: Bearer $(cat ~/.openloomi/token | base64 -d). Cloud: https://app.alloomi.ai/api/native/agent. Same endpoint the benchmark/locomo suite drives.
  • openloomi API docs: see the openloomi-api skill (Native module /api/native/*, AI module /api/ai/*).
  • openloomi-memory CLI: node $SKILL_DIR/../openloomi-memory/scripts/openloomi-memory.cjs <subcommand>
  • Token: ~/.openloomi/token (base64-encoded JWT)
处理所有.pptx文件相关任务,包括创建、编辑、读取内容及解析。适用于演示文稿生成、模板修改、文本提取及幻灯片拆分合并等场景。
涉及 .pptx 文件的输入或输出操作 创建、编辑或修改演示文稿/幻灯片 从 .pptx 文件中读取或提取内容 用户提及 deck, slides, presentation 或 .pptx 文件名
skills/pptx/SKILL.md
npx skills add melandlabs/openloomi --skill pptx -g -y
SKILL.md
Frontmatter
{
    "name": "pptx",
    "license": "Proprietary. LICENSE.txt has complete terms",
    "description": "Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."
}

PPTX Skill

Quick Reference

Task Guide
Read/analyze content python -m markitdown presentation.pptx
Edit or create from template Read editing.md
Create from scratch Read pptxgenjs.md

Reading Content

# Text extraction
python -m markitdown presentation.pptx

# Visual overview
python scripts/thumbnail.py presentation.pptx

# Raw XML
python scripts/office/unpack.py presentation.pptx unpacked/

Editing Workflow

Read editing.md for full details.

  1. Analyze template with thumbnail.py
  2. Unpack → manipulate slides → edit content → clean → pack

Creating from Scratch

Read pptxgenjs.md for full details.

Use when no template or reference presentation is available.


Design Ideas

Don't create boring slides. Plain bullets on a white background won't impress anyone. Consider ideas from this list for each slide.

Before Starting

  • Pick a bold, content-informed color palette: The palette should feel designed for THIS topic. If swapping your colors into a completely different presentation would still "work," you haven't made specific enough choices.
  • Dominance over equality: One color should dominate (60-70% visual weight), with 1-2 supporting tones and one sharp accent. Never give all colors equal weight.
  • Dark/light contrast: Dark backgrounds for title + conclusion slides, light for content ("sandwich" structure). Or commit to dark throughout for a premium feel.
  • Commit to a visual motif: Pick ONE distinctive element and repeat it — rounded image frames, icons in colored circles, thick single-side borders. Carry it across every slide.

Color Palettes

Choose colors that match your topic — don't default to generic blue. Use these palettes as inspiration:

Theme Primary Secondary Accent
Midnight Executive 1E2761 (navy) CADCFC (ice blue) FFFFFF (white)
Forest & Moss 2C5F2D (forest) 97BC62 (moss) F5F5F5 (cream)
Coral Energy F96167 (coral) F9E795 (gold) 2F3C7E (navy)
Warm Terracotta B85042 (terracotta) E7E8D1 (sand) A7BEAE (sage)
Ocean Gradient 065A82 (deep blue) 1C7293 (teal) 21295C (midnight)
Charcoal Minimal 36454F (charcoal) F2F2F2 (off-white) 212121 (black)
Teal Trust 028090 (teal) 00A896 (seafoam) 02C39A (mint)
Berry & Cream 6D2E46 (berry) A26769 (dusty rose) ECE2D0 (cream)
Sage Calm 84B59F (sage) 69A297 (eucalyptus) 50808E (slate)
Cherry Bold 990011 (cherry) FCF6F5 (off-white) 2F3C7E (navy)

For Each Slide

Every slide needs a visual element — image, chart, icon, or shape. Text-only slides are forgettable.

Layout options:

  • Two-column (text left, illustration on right)
  • Icon + text rows (icon in colored circle, bold header, description below)
  • 2x2 or 2x3 grid (image on one side, grid of content blocks on other)
  • Half-bleed image (full left or right side) with content overlay

Data display:

  • Large stat callouts (big numbers 60-72pt with small labels below)
  • Comparison columns (before/after, pros/cons, side-by-side options)
  • Timeline or process flow (numbered steps, arrows)

Visual polish:

  • Icons in small colored circles next to section headers
  • Italic accent text for key stats or taglines

Typography

Choose an interesting font pairing — don't default to Arial. Pick a header font with personality and pair it with a clean body font.

Header Font Body Font
Georgia Calibri
Arial Black Arial
Calibri Calibri Light
Cambria Calibri
Trebuchet MS Calibri
Impact Arial
Palatino Garamond
Consolas Calibri
Element Size
Slide title 36-44pt bold
Section header 20-24pt bold
Body text 14-16pt
Captions 10-12pt muted

Spacing

  • 0.5" minimum margins
  • 0.3-0.5" between content blocks
  • Leave breathing room—don't fill every inch

Avoid (Common Mistakes)

  • Don't repeat the same layout — vary columns, cards, and callouts across slides
  • Don't center body text — left-align paragraphs and lists; center only titles
  • Don't skimp on size contrast — titles need 36pt+ to stand out from 14-16pt body
  • Don't default to blue — pick colors that reflect the specific topic
  • Don't mix spacing randomly — choose 0.3" or 0.5" gaps and use consistently
  • Don't style one slide and leave the rest plain — commit fully or keep it simple throughout
  • Don't create text-only slides — add images, icons, charts, or visual elements; avoid plain title + bullets
  • Don't forget text box padding — when aligning lines or shapes with text edges, set margin: 0 on the text box or offset the shape to account for padding
  • Don't use low-contrast elements — icons AND text need strong contrast against the background; avoid light text on light backgrounds or dark text on dark backgrounds
  • NEVER use accent lines under titles — these are a hallmark of AI-generated slides; use whitespace or background color instead

QA (Required)

Assume there are problems. Your job is to find them.

Your first render is almost never correct. Approach QA as a bug hunt, not a confirmation step. If you found zero issues on first inspection, you weren't looking hard enough.

Content QA

python -m markitdown output.pptx

Check for missing content, typos, wrong order.

When using templates, check for leftover placeholder text:

python -m markitdown output.pptx | grep -iE "xxxx|lorem|ipsum|this.*(page|slide).*layout"

If grep returns results, fix them before declaring success.

Visual QA

⚠️ USE SUBAGENTS — even for 2-3 slides. You've been staring at the code and will see what you expect, not what's there. Subagents have fresh eyes.

Convert slides to images (see Converting to Images), then use this prompt:

Visually inspect these slides. Assume there are issues — find them.

Look for:
- Overlapping elements (text through shapes, lines through words, stacked elements)
- Text overflow or cut off at edges/box boundaries
- Decorative lines positioned for single-line text but title wrapped to two lines
- Source citations or footers colliding with content above
- Elements too close (< 0.3" gaps) or cards/sections nearly touching
- Uneven gaps (large empty area in one place, cramped in another)
- Insufficient margin from slide edges (< 0.5")
- Columns or similar elements not aligned consistently
- Low-contrast text (e.g., light gray text on cream-colored background)
- Low-contrast icons (e.g., dark icons on dark backgrounds without a contrasting circle)
- Text boxes too narrow causing excessive wrapping
- Leftover placeholder content

For each slide, list issues or areas of concern, even if minor.

Read and analyze these images:
1. /path/to/slide-01.jpg (Expected: [brief description])
2. /path/to/slide-02.jpg (Expected: [brief description])

Report ALL issues found, including minor ones.

Verification Loop

  1. Generate slides → Convert to images → Inspect
  2. List issues found (if none found, look again more critically)
  3. Fix issues
  4. Re-verify affected slides — one fix often creates another problem
  5. Repeat until a full pass reveals no new issues

Do not declare success until you've completed at least one fix-and-verify cycle.


Converting to Images

Convert presentations to individual slide images for visual inspection:

python scripts/office/soffice.py --headless --convert-to pdf output.pptx
pdftoppm -jpeg -r 150 output.pdf slide

This creates slide-01.jpg, slide-02.jpg, etc.

To re-render specific slides after fixes:

pdftoppm -jpeg -r 150 -f N -l N output.pdf slide-fixed

Dependencies

  • pip install "markitdown[pptx]" - text extraction
  • pip install Pillow - thumbnail grids
  • npm install -g pptxgenjs - creating from scratch
  • LibreOffice (soffice) - PDF conversion (auto-configured for sandboxed environments via scripts/office/soffice.py)
  • Poppler (pdftoppm) - PDF to images
提供Web和移动端UI/UX设计智能支持,涵盖50+风格、色彩、字体及99条UX准则。用于页面规划、组件构建、代码审查及体验优化,确保视觉一致性与无障碍标准。
设计新页面或重构UI组件 选择配色方案、排版或布局系统 审查UI代码以优化用户体验和无障碍性 实现响应式行为或交互动画 解决界面视觉效果不专业或可用性反馈问题
skills/ui-ux-pro-max/SKILL.md
npx skills add melandlabs/openloomi --skill ui-ux-pro-max -g -y
SKILL.md
Frontmatter
{
    "name": "ui-ux-pro-max",
    "description": "UI\/UX design intelligence for web and mobile. Includes 50+ styles, 161 color palettes, 57 font pairings, 161 product types, 99 UX guidelines, and 25 chart types across 10 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn\/ui, and HTML\/CSS). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, and check UI\/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, and mobile app. Elements: button, modal, navbar, sidebar, card, table, form, and chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, and flat design. Topics: color systems, accessibility, animation, layout, typography, font pairing, spacing, interaction states, shadow, and gradient. Integrations: shadcn\/ui MCP for component search and examples."
}

UI/UX Pro Max - Design Intelligence

Comprehensive design guide for web and mobile applications. Contains 50+ styles, 161 color palettes, 57 font pairings, 161 product types with reasoning rules, 99 UX guidelines, and 25 chart types across 10 technology stacks. Searchable database with priority-based recommendations.

When to Apply

This Skill should be used when the task involves UI structure, visual design decisions, interaction patterns, or user experience quality control.

Must Use

This Skill must be invoked in the following situations:

  • Designing new pages (Landing Page, Dashboard, Admin, SaaS, Mobile App)
  • Creating or refactoring UI components (buttons, modals, forms, tables, charts, etc.)
  • Choosing color schemes, typography systems, spacing standards, or layout systems
  • Reviewing UI code for user experience, accessibility, or visual consistency
  • Implementing navigation structures, animations, or responsive behavior
  • Making product-level design decisions (style, information hierarchy, brand expression)
  • Improving perceived quality, clarity, or usability of interfaces

Recommended

This Skill is recommended in the following situations:

  • UI looks "not professional enough" but the reason is unclear
  • Receiving feedback on usability or experience
  • Pre-launch UI quality optimization
  • Aligning cross-platform design (Web / iOS / Android)
  • Building design systems or reusable component libraries

Skip

This Skill is not needed in the following situations:

  • Pure backend logic development
  • Only involving API or database design
  • Performance optimization unrelated to the interface
  • Infrastructure or DevOps work
  • Non-visual scripts or automation tasks

Decision criteria: If the task will change how a feature looks, feels, moves, or is interacted with, this Skill should be used.

Rule Categories by Priority

For human/AI reference: follow priority 1→10 to decide which rule category to focus on first; use --domain <Domain> to query details when needed. Scripts do not read this table.

Priority Category Impact Domain Key Checks (Must Have) Anti-Patterns (Avoid)
1 Accessibility CRITICAL ux Contrast 4.5:1, Alt text, Keyboard nav, Aria-labels Removing focus rings, Icon-only buttons without labels
2 Touch & Interaction CRITICAL ux Min size 44×44px, 8px+ spacing, Loading feedback Reliance on hover only, Instant state changes (0ms)
3 Performance HIGH ux WebP/AVIF, Lazy loading, Reserve space (CLS < 0.1) Layout thrashing, Cumulative Layout Shift
4 Style Selection HIGH style, product Match product type, Consistency, SVG icons (no emoji) Mixing flat & skeuomorphic randomly, Emoji as icons
5 Layout & Responsive HIGH ux Mobile-first breakpoints, Viewport meta, No horizontal scroll Horizontal scroll, Fixed px container widths, Disable zoom
6 Typography & Color MEDIUM typography, color Base 16px, Line-height 1.5, Semantic color tokens Text < 12px body, Gray-on-gray, Raw hex in components
7 Animation MEDIUM ux Duration 150–300ms, Motion conveys meaning, Spatial continuity Decorative-only animation, Animating width/height, No reduced-motion
8 Forms & Feedback MEDIUM ux Visible labels, Error near field, Helper text, Progressive disclosure Placeholder-only label, Errors only at top, Overwhelm upfront
9 Navigation Patterns HIGH ux Predictable back, Bottom nav ≤5, Deep linking Overloaded nav, Broken back behavior, No deep links
10 Charts & Data LOW chart Legends, Tooltips, Accessible colors Relying on color alone to convey meaning

Quick Reference

1. Accessibility (CRITICAL)

  • color-contrast - Minimum 4.5:1 ratio for normal text (large text 3:1); Material Design
  • focus-states - Visible focus rings on interactive elements (2–4px; Apple HIG, MD)
  • alt-text - Descriptive alt text for meaningful images
  • aria-labels - aria-label for icon-only buttons; accessibilityLabel in native (Apple HIG)
  • keyboard-nav - Tab order matches visual order; full keyboard support (Apple HIG)
  • form-labels - Use label with for attribute
  • skip-links - Skip to main content for keyboard users
  • heading-hierarchy - Sequential h1→h6, no level skip
  • color-not-only - Don't convey info by color alone (add icon/text)
  • dynamic-type - Support system text scaling; avoid truncation as text grows (Apple Dynamic Type, MD)
  • reduced-motion - Respect prefers-reduced-motion; reduce/disable animations when requested (Apple Reduced Motion API, MD)
  • voiceover-sr - Meaningful accessibilityLabel/accessibilityHint; logical reading order for VoiceOver/screen readers (Apple HIG, MD)
  • escape-routes - Provide cancel/back in modals and multi-step flows (Apple HIG)
  • keyboard-shortcuts - Preserve system and a11y shortcuts; offer keyboard alternatives for drag-and-drop (Apple HIG)

2. Touch & Interaction (CRITICAL)

  • touch-target-size - Min 44×44pt (Apple) / 48×48dp (Material); extend hit area beyond visual bounds if needed
  • touch-spacing - Minimum 8px/8dp gap between touch targets (Apple HIG, MD)
  • hover-vs-tap - Use click/tap for primary interactions; don't rely on hover alone
  • loading-buttons - Disable button during async operations; show spinner or progress
  • error-feedback - Clear error messages near problem
  • cursor-pointer - Add cursor-pointer to clickable elements (Web)
  • gesture-conflicts - Avoid horizontal swipe on main content; prefer vertical scroll
  • tap-delay - Use touch-action: manipulation to reduce 300ms delay (Web)
  • standard-gestures - Use platform standard gestures consistently; don't redefine (e.g. swipe-back, pinch-zoom) (Apple HIG)
  • system-gestures - Don't block system gestures (Control Center, back swipe, etc.) (Apple HIG)
  • press-feedback - Visual feedback on press (ripple/highlight; MD state layers)
  • haptic-feedback - Use haptic for confirmations and important actions; avoid overuse (Apple HIG)
  • gesture-alternative - Don't rely on gesture-only interactions; always provide visible controls for critical actions
  • safe-area-awareness - Keep primary touch targets away from notch, Dynamic Island, gesture bar and screen edges
  • no-precision-required - Avoid requiring pixel-perfect taps on small icons or thin edges
  • swipe-clarity - Swipe actions must show clear affordance or hint (chevron, label, tutorial)
  • drag-threshold - Use a movement threshold before starting drag to avoid accidental drags

3. Performance (HIGH)

  • image-optimization - Use WebP/AVIF, responsive images (srcset/sizes), lazy load non-critical assets
  • image-dimension - Declare width/height or use aspect-ratio to prevent layout shift (Core Web Vitals: CLS)
  • font-loading - Use font-display: swap/optional to avoid invisible text (FOIT); reserve space to reduce layout shift (MD)
  • font-preload - Preload only critical fonts; avoid overusing preload on every variant
  • critical-css - Prioritize above-the-fold CSS (inline critical CSS or early-loaded stylesheet)
  • lazy-loading - Lazy load non-hero components via dynamic import / route-level splitting
  • bundle-splitting - Split code by route/feature (React Suspense / Next.js dynamic) to reduce initial load and TTI
  • third-party-scripts - Load third-party scripts async/defer; audit and remove unnecessary ones (MD)
  • reduce-reflows - Avoid frequent layout reads/writes; batch DOM reads then writes
  • content-jumping - Reserve space for async content to avoid layout jumps (Core Web Vitals: CLS)
  • lazy-load-below-fold - Use loading="lazy" for below-the-fold images and heavy media
  • virtualize-lists - Virtualize lists with 50+ items to improve memory efficiency and scroll performance
  • main-thread-budget - Keep per-frame work under ~16ms for 60fps; move heavy tasks off main thread (HIG, MD)
  • progressive-loading - Use skeleton screens / shimmer instead of long blocking spinners for >1s operations (Apple HIG)
  • input-latency - Keep input latency under ~100ms for taps/scrolls (Material responsiveness standard)
  • tap-feedback-speed - Provide visual feedback within 100ms of tap (Apple HIG)
  • debounce-throttle - Use debounce/throttle for high-frequency events (scroll, resize, input)
  • offline-support - Provide offline state messaging and basic fallback (PWA / mobile)
  • network-fallback - Offer degraded modes for slow networks (lower-res images, fewer animations)

4. Style Selection (HIGH)

  • style-match - Match style to product type (use --design-system for recommendations)
  • consistency - Use same style across all pages
  • no-emoji-icons - Use SVG icons (Heroicons, Lucide), not emojis
  • color-palette-from-product - Choose palette from product/industry (search --domain color)
  • effects-match-style - Shadows, blur, radius aligned with chosen style (glass / flat / clay etc.)
  • platform-adaptive - Respect platform idioms (iOS HIG vs Material): navigation, controls, typography, motion
  • state-clarity - Make hover/pressed/disabled states visually distinct while staying on-style (Material state layers)
  • elevation-consistent - Use a consistent elevation/shadow scale for cards, sheets, modals; avoid random shadow values
  • dark-mode-pairing - Design light/dark variants together to keep brand, contrast, and style consistent
  • icon-style-consistent - Use one icon set/visual language (stroke width, corner radius) across the product
  • system-controls - Prefer native/system controls over fully custom ones; only customize when branding requires it (Apple HIG)
  • blur-purpose - Use blur to indicate background dismissal (modals, sheets), not as decoration (Apple HIG)
  • primary-action - Each screen should have only one primary CTA; secondary actions visually subordinate (Apple HIG)

5. Layout & Responsive (HIGH)

  • viewport-meta - width=device-width initial-scale=1 (never disable zoom)
  • mobile-first - Design mobile-first, then scale up to tablet and desktop
  • breakpoint-consistency - Use systematic breakpoints (e.g. 375 / 768 / 1024 / 1440)
  • readable-font-size - Minimum 16px body text on mobile (avoids iOS auto-zoom)
  • line-length-control - Mobile 35–60 chars per line; desktop 60–75 chars
  • horizontal-scroll - No horizontal scroll on mobile; ensure content fits viewport width
  • spacing-scale - Use 4pt/8dp incremental spacing system (Material Design)
  • touch-density - Keep component spacing comfortable for touch: not cramped, not causing mis-taps
  • container-width - Consistent max-width on desktop (max-w-6xl / 7xl)
  • z-index-management - Define layered z-index scale (e.g. 0 / 10 / 20 / 40 / 100 / 1000)
  • fixed-element-offset - Fixed navbar/bottom bar must reserve safe padding for underlying content
  • scroll-behavior - Avoid nested scroll regions that interfere with the main scroll experience
  • viewport-units - Prefer min-h-dvh over 100vh on mobile
  • orientation-support - Keep layout readable and operable in landscape mode
  • content-priority - Show core content first on mobile; fold or hide secondary content
  • visual-hierarchy - Establish hierarchy via size, spacing, contrast — not color alone

6. Typography & Color (MEDIUM)

  • line-height - Use 1.5-1.75 for body text
  • line-length - Limit to 65-75 characters per line
  • font-pairing - Match heading/body font personalities
  • font-scale - Consistent type scale (e.g. 12 14 16 18 24 32)
  • contrast-readability - Darker text on light backgrounds (e.g. slate-900 on white)
  • text-styles-system - Use platform type system: iOS 11 Dynamic Type styles / Material 5 type roles (display, headline, title, body, label) (HIG, MD)
  • weight-hierarchy - Use font-weight to reinforce hierarchy: Bold headings (600–700), Regular body (400), Medium labels (500) (MD)
  • color-semantic - Define semantic color tokens (primary, secondary, error, surface, on-surface) not raw hex in components (Material color system)
  • color-dark-mode - Dark mode uses desaturated / lighter tonal variants, not inverted colors; test contrast separately (HIG, MD)
  • color-accessible-pairs - Foreground/background pairs must meet 4.5:1 (AA) or 7:1 (AAA); use tools to verify (WCAG, MD)
  • color-not-decorative-only - Functional color (error red, success green) must include icon/text; avoid color-only meaning (HIG, MD)
  • truncation-strategy - Prefer wrapping over truncation; when truncating use ellipsis and provide full text via tooltip/expand (Apple HIG)
  • letter-spacing - Respect default letter-spacing per platform; avoid tight tracking on body text (HIG, MD)
  • number-tabular - Use tabular/monospaced figures for data columns, prices, and timers to prevent layout shift
  • whitespace-balance - Use whitespace intentionally to group related items and separate sections; avoid visual clutter (Apple HIG)

7. Animation (MEDIUM)

  • duration-timing - Use 150–300ms for micro-interactions; complex transitions ≤400ms; avoid >500ms (MD)
  • transform-performance - Use transform/opacity only; avoid animating width/height/top/left
  • loading-states - Show skeleton or progress indicator when loading exceeds 300ms
  • excessive-motion - Animate 1-2 key elements per view max
  • easing - Use ease-out for entering, ease-in for exiting; avoid linear for UI transitions
  • motion-meaning - Every animation must express a cause-effect relationship, not just be decorative (Apple HIG)
  • state-transition - State changes (hover / active / expanded / collapsed / modal) should animate smoothly, not snap
  • continuity - Page/screen transitions should maintain spatial continuity (shared element, directional slide) (Apple HIG)
  • parallax-subtle - Use parallax sparingly; must respect reduced-motion and not cause disorientation (Apple HIG)
  • spring-physics - Prefer spring/physics-based curves over linear or cubic-bezier for natural feel (Apple HIG fluid animations)
  • exit-faster-than-enter - Exit animations shorter than enter (~60–70% of enter duration) to feel responsive (MD motion)
  • stagger-sequence - Stagger list/grid item entrance by 30–50ms per item; avoid all-at-once or too-slow reveals (MD)
  • shared-element-transition - Use shared element / hero transitions for visual continuity between screens (MD, HIG)
  • interruptible - Animations must be interruptible; user tap/gesture cancels in-progress animation immediately (Apple HIG)
  • no-blocking-animation - Never block user input during an animation; UI must stay interactive (Apple HIG)
  • fade-crossfade - Use crossfade for content replacement within the same container (MD)
  • scale-feedback - Subtle scale (0.95–1.05) on press for tappable cards/buttons; restore on release (HIG, MD)
  • gesture-feedback - Drag, swipe, and pinch must provide real-time visual response tracking the finger (MD Motion)
  • hierarchy-motion - Use translate/scale direction to express hierarchy: enter from below = deeper, exit upward = back (MD)
  • motion-consistency - Unify duration/easing tokens globally; all animations share the same rhythm and feel
  • opacity-threshold - Fading elements should not linger below opacity 0.2; either fade fully or remain visible
  • modal-motion - Modals/sheets should animate from their trigger source (scale+fade or slide-in) for spatial context (HIG, MD)
  • navigation-direction - Forward navigation animates left/up; backward animates right/down — keep direction logically consistent (HIG)
  • layout-shift-avoid - Animations must not cause layout reflow or CLS; use transform for position changes

8. Forms & Feedback (MEDIUM)

  • input-labels - Visible label per input (not placeholder-only)
  • error-placement - Show error below the related field
  • submit-feedback - Loading then success/error state on submit
  • required-indicators - Mark required fields (e.g. asterisk)
  • empty-states - Helpful message and action when no content
  • toast-dismiss - Auto-dismiss toasts in 3-5s
  • confirmation-dialogs - Confirm before destructive actions
  • input-helper-text - Provide persistent helper text below complex inputs, not just placeholder (Material Design)
  • disabled-states - Disabled elements use reduced opacity (0.38–0.5) + cursor change + semantic attribute (MD)
  • progressive-disclosure - Reveal complex options progressively; don't overwhelm users upfront (Apple HIG)
  • inline-validation - Validate on blur (not keystroke); show error only after user finishes input (MD)
  • input-type-keyboard - Use semantic input types (email, tel, number) to trigger the correct mobile keyboard (HIG, MD)
  • password-toggle - Provide show/hide toggle for password fields (MD)
  • autofill-support - Use autocomplete / textContentType attributes so the system can autofill (HIG, MD)
  • undo-support - Allow undo for destructive or bulk actions (e.g. "Undo delete" toast) (Apple HIG)
  • success-feedback - Confirm completed actions with brief visual feedback (checkmark, toast, color flash) (MD)
  • error-recovery - Error messages must include a clear recovery path (retry, edit, help link) (HIG, MD)
  • multi-step-progress - Multi-step flows show step indicator or progress bar; allow back navigation (MD)
  • form-autosave - Long forms should auto-save drafts to prevent data loss on accidental dismissal (Apple HIG)
  • sheet-dismiss-confirm - Confirm before dismissing a sheet/modal with unsaved changes (Apple HIG)
  • error-clarity - Error messages must state cause + how to fix (not just "Invalid input") (HIG, MD)
  • field-grouping - Group related fields logically (fieldset/legend or visual grouping) (MD)
  • read-only-distinction - Read-only state should be visually and semantically different from disabled (MD)
  • focus-management - After submit error, auto-focus the first invalid field (WCAG, MD)
  • error-summary - For multiple errors, show summary at top with anchor links to each field (WCAG)
  • touch-friendly-input - Mobile input height ≥44px to meet touch target requirements (Apple HIG)
  • destructive-emphasis - Destructive actions use semantic danger color (red) and are visually separated from primary actions (HIG, MD)
  • toast-accessibility - Toasts must not steal focus; use aria-live="polite" for screen reader announcement (WCAG)
  • aria-live-errors - Form errors use aria-live region or role="alert" to notify screen readers (WCAG)
  • contrast-feedback - Error and success state colors must meet 4.5:1 contrast ratio (WCAG, MD)
  • timeout-feedback - Request timeout must show clear feedback with retry option (MD)

9. Navigation Patterns (HIGH)

  • bottom-nav-limit - Bottom navigation max 5 items; use labels with icons (Material Design)
  • drawer-usage - Use drawer/sidebar for secondary navigation, not primary actions (Material Design)
  • back-behavior - Back navigation must be predictable and consistent; preserve scroll/state (Apple HIG, MD)
  • deep-linking - All key screens must be reachable via deep link / URL for sharing and notifications (Apple HIG, MD)
  • tab-bar-ios - iOS: use bottom Tab Bar for top-level navigation (Apple HIG)
  • top-app-bar-android - Android: use Top App Bar with navigation icon for primary structure (Material Design)
  • nav-label-icon - Navigation items must have both icon and text label; icon-only nav harms discoverability (MD)
  • nav-state-active - Current location must be visually highlighted (color, weight, indicator) in navigation (HIG, MD)
  • nav-hierarchy - Primary nav (tabs/bottom bar) vs secondary nav (drawer/settings) must be clearly separated (MD)
  • modal-escape - Modals and sheets must offer a clear close/dismiss affordance; swipe-down to dismiss on mobile (Apple HIG)
  • search-accessible - Search must be easily reachable (top bar or tab); provide recent/suggested queries (MD)
  • breadcrumb-web - Web: use breadcrumbs for 3+ level deep hierarchies to aid orientation (MD)
  • state-preservation - Navigating back must restore previous scroll position, filter state, and input (HIG, MD)
  • gesture-nav-support - Support system gesture navigation (iOS swipe-back, Android predictive back) without conflict (HIG, MD)
  • tab-badge - Use badges on nav items sparingly to indicate unread/pending; clear after user visits (HIG, MD)
  • overflow-menu - When actions exceed available space, use overflow/more menu instead of cramming (MD)
  • bottom-nav-top-level - Bottom nav is for top-level screens only; never nest sub-navigation inside it (MD)
  • adaptive-navigation - Large screens (≥1024px) prefer sidebar; small screens use bottom/top nav (Material Adaptive)
  • back-stack-integrity - Never silently reset the navigation stack or unexpectedly jump to home (HIG, MD)
  • navigation-consistency - Navigation placement must stay the same across all pages; don't change by page type
  • avoid-mixed-patterns - Don't mix Tab + Sidebar + Bottom Nav at the same hierarchy level
  • modal-vs-navigation - Modals must not be used for primary navigation flows; they break the user's path (HIG)
  • focus-on-route-change - After page transition, move focus to main content region for screen reader users (WCAG)
  • persistent-nav - Core navigation must remain reachable from deep pages; don't hide it entirely in sub-flows (HIG, MD)
  • destructive-nav-separation - Dangerous actions (delete account, logout) must be visually and spatially separated from normal nav items (HIG, MD)
  • empty-nav-state - When a nav destination is unavailable, explain why instead of silently hiding it (MD)

10. Charts & Data (LOW)

  • chart-type - Match chart type to data type (trend → line, comparison → bar, proportion → pie/donut)
  • color-guidance - Use accessible color palettes; avoid red/green only pairs for colorblind users (WCAG, MD)
  • data-table - Provide table alternative for accessibility; charts alone are not screen-reader friendly (WCAG)
  • pattern-texture - Supplement color with patterns, textures, or shapes so data is distinguishable without color (WCAG, MD)
  • legend-visible - Always show legend; position near the chart, not detached below a scroll fold (MD)
  • tooltip-on-interact - Provide tooltips/data labels on hover (Web) or tap (mobile) showing exact values (HIG, MD)
  • axis-labels - Label axes with units and readable scale; avoid truncated or rotated labels on mobile
  • responsive-chart - Charts must reflow or simplify on small screens (e.g. horizontal bar instead of vertical, fewer ticks)
  • empty-data-state - Show meaningful empty state when no data exists ("No data yet" + guidance), not a blank chart (MD)
  • loading-chart - Use skeleton or shimmer placeholder while chart data loads; don't show an empty axis frame
  • animation-optional - Chart entrance animations must respect prefers-reduced-motion; data should be readable immediately (HIG)
  • large-dataset - For 1000+ data points, aggregate or sample; provide drill-down for detail instead of rendering all (MD)
  • number-formatting - Use locale-aware formatting for numbers, dates, currencies on axes and labels (HIG, MD)
  • touch-target-chart - Interactive chart elements (points, segments) must have ≥44pt tap area or expand on touch (Apple HIG)
  • no-pie-overuse - Avoid pie/donut for >5 categories; switch to bar chart for clarity
  • contrast-data - Data lines/bars vs background ≥3:1; data text labels ≥4.5:1 (WCAG)
  • legend-interactive - Legends should be clickable to toggle series visibility (MD)
  • direct-labeling - For small datasets, label values directly on the chart to reduce eye travel
  • tooltip-keyboard - Tooltip content must be keyboard-reachable and not rely on hover alone (WCAG)
  • sortable-table - Data tables must support sorting with aria-sort indicating current sort state (WCAG)
  • axis-readability - Axis ticks must not be cramped; maintain readable spacing, auto-skip on small screens
  • data-density - Limit information density per chart to avoid cognitive overload; split into multiple charts if needed
  • trend-emphasis - Emphasize data trends over decoration; avoid heavy gradients/shadows that obscure the data
  • gridline-subtle - Grid lines should be low-contrast (e.g. gray-200) so they don't compete with data
  • focusable-elements - Interactive chart elements (points, bars, slices) must be keyboard-navigable (WCAG)
  • screen-reader-summary - Provide a text summary or aria-label describing the chart's key insight for screen readers (WCAG)
  • error-state-chart - Data load failure must show error message with retry action, not a broken/empty chart
  • export-option - For data-heavy products, offer CSV/image export of chart data
  • drill-down-consistency - Drill-down interactions must maintain a clear back-path and hierarchy breadcrumb
  • time-scale-clarity - Time series charts must clearly label time granularity (day/week/month) and allow switching

How to Use

Search specific domains using the CLI tool below.


Prerequisites

Check if Python is installed:

python3 --version || python --version

If Python is not installed, install it based on user's OS:

macOS:

brew install python3

Ubuntu/Debian:

sudo apt update && sudo apt install python3

Windows:

winget install Python.Python.3.12

How to Use This Skill

Use this skill when the user requests any of the following:

Scenario Trigger Examples Start From
New project / page "Build a landing page", "Build a dashboard" Step 1 → Step 2 (design system)
New component "Create a pricing card", "Add a modal" Step 3 (domain search: style, ux)
Choose style / color / font "What style fits a fintech app?", "Recommend a color palette" Step 2 (design system)
Review existing UI "Review this page for UX issues", "Check accessibility" Quick Reference checklist above
Fix a UI bug "Button hover is broken", "Layout shifts on load" Quick Reference → relevant section
Improve / optimize "Make this faster", "Improve mobile experience" Step 3 (domain search: ux, react)
Implement dark mode "Add dark mode support" Step 3 (domain: style "dark mode")
Add charts / data viz "Add an analytics dashboard chart" Step 3 (domain: chart)
Stack best practices "React performance tips"、"SwiftUI navigation" Step 4 (stack search)

Follow this workflow:

Step 1: Analyze User Requirements

Extract key information from user request:

  • Product type: Entertainment (social, video, music, gaming), Tool (scanner, editor, converter), Productivity (task manager, notes, calendar), or hybrid
  • Target audience: C-end consumer users; consider age group, usage context (commute, leisure, work)
  • Style keywords: playful, vibrant, minimal, dark mode, content-first, immersive, etc.
  • Stack: React Native (this project's only tech stack)

Step 2: Generate Design System (REQUIRED)

Always start with --design-system to get comprehensive recommendations with reasoning:

python3 skills/ui-ux-pro-max/scripts/search.py "<product_type> <industry> <keywords>" --design-system [-p "Project Name"]

This command:

  1. Searches domains in parallel (product, style, color, landing, typography)
  2. Applies reasoning rules from ui-reasoning.csv to select best matches
  3. Returns complete design system: pattern, style, colors, typography, effects
  4. Includes anti-patterns to avoid

Example:

python3 skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --design-system -p "Serenity Spa"

Step 2b: Persist Design System (Master + Overrides Pattern)

To save the design system for hierarchical retrieval across sessions, add --persist:

python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name"

This creates:

  • design-system/MASTER.md — Global Source of Truth with all design rules
  • design-system/pages/ — Folder for page-specific overrides

With page-specific override:

python3 skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system --persist -p "Project Name" --page "dashboard"

This also creates:

  • design-system/pages/dashboard.md — Page-specific deviations from Master

How hierarchical retrieval works:

  1. When building a specific page (e.g., "Checkout"), first check design-system/pages/checkout.md
  2. If the page file exists, its rules override the Master file
  3. If not, use design-system/MASTER.md exclusively

Context-aware retrieval prompt:

I am building the [Page Name] page. Please read design-system/MASTER.md.
Also check if design-system/pages/[page-name].md exists.
If the page file exists, prioritize its rules.
If not, use the Master rules exclusively.
Now, generate the code...

Step 3: Supplement with Detailed Searches (as needed)

After getting the design system, use domain searches to get additional details:

python3 skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]

When to use detailed searches:

Need Domain Example
Product type patterns product --domain product "entertainment social"
More style options style --domain style "glassmorphism dark"
Color palettes color --domain color "entertainment vibrant"
Font pairings typography --domain typography "playful modern"
Chart recommendations chart --domain chart "real-time dashboard"
UX best practices ux --domain ux "animation accessibility"
Alternative fonts typography --domain typography "elegant luxury"
Individual Google Fonts google-fonts --domain google-fonts "sans serif popular variable"
Landing structure landing --domain landing "hero social-proof"
React Native perf react --domain react "rerender memo list"
App interface a11y web --domain web "accessibilityLabel touch safe-areas"
AI prompt / CSS keywords prompt --domain prompt "minimalism"

Step 4: Stack Guidelines (React Native)

Get React Native implementation-specific best practices:

python3 skills/ui-ux-pro-max/scripts/search.py "<keyword>" --stack react-native

Search Reference

Available Domains

Domain Use For Example Keywords
product Product type recommendations SaaS, e-commerce, portfolio, healthcare, beauty, service
style UI styles, colors, effects glassmorphism, minimalism, dark mode, brutalism
typography Font pairings, Google Fonts elegant, playful, professional, modern
color Color palettes by product type saas, ecommerce, healthcare, beauty, fintech, service
landing Page structure, CTA strategies hero, hero-centric, testimonial, pricing, social-proof
chart Chart types, library recommendations trend, comparison, timeline, funnel, pie
ux Best practices, anti-patterns animation, accessibility, z-index, loading
google-fonts Individual Google Fonts lookup sans serif, monospace, japanese, variable font, popular
react React/Next.js performance waterfall, bundle, suspense, memo, rerender, cache
web App interface guidelines (iOS/Android/React Native) accessibilityLabel, touch targets, safe areas, Dynamic Type
prompt AI prompts, CSS keywords (style name)

Available Stacks

Stack Focus
react-native Components, Navigation, Lists

Example Workflow

User request: "Make an AI search homepage."

Step 1: Analyze Requirements

  • Product type: Tool (AI search engine)
  • Target audience: C-end users looking for fast, intelligent search
  • Style keywords: modern, minimal, content-first, dark mode
  • Stack: React Native

Step 2: Generate Design System (REQUIRED)

python3 skills/ui-ux-pro-max/scripts/search.py "AI search tool modern minimal" --design-system -p "AI Search"

Output: Complete design system with pattern, style, colors, typography, effects, and anti-patterns.

Step 3: Supplement with Detailed Searches (as needed)

# Get style options for a modern tool product
python3 skills/ui-ux-pro-max/scripts/search.py "minimalism dark mode" --domain style

# Get UX best practices for search interaction and loading
python3 skills/ui-ux-pro-max/scripts/search.py "search loading animation" --domain ux

Step 4: Stack Guidelines

python3 skills/ui-ux-pro-max/scripts/search.py "list performance navigation" --stack react-native

Then: Synthesize design system + detailed searches and implement the design.


Output Formats

The --design-system flag supports two output formats:

# ASCII box (default) - best for terminal display
python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system

# Markdown - best for documentation
python3 skills/ui-ux-pro-max/scripts/search.py "fintech crypto" --design-system -f markdown

Tips for Better Results

Query Strategy

  • Use multi-dimensional keywords — combine product + industry + tone + density: "entertainment social vibrant content-dense" not just "app"
  • Try different keywords for the same need: "playful neon""vibrant dark""content-first minimal"
  • Use --design-system first for full recommendations, then --domain to deep-dive any dimension you're unsure about
  • Always add --stack react-native for implementation-specific guidance

Common Sticking Points

Problem What to Do
Can't decide on style/color Re-run --design-system with different keywords
Dark mode contrast issues Quick Reference §6: color-dark-mode + color-accessible-pairs
Animations feel unnatural Quick Reference §7: spring-physics + easing + exit-faster-than-enter
Form UX is poor Quick Reference §8: inline-validation + error-clarity + focus-management
Navigation feels confusing Quick Reference §9: nav-hierarchy + bottom-nav-limit + back-behavior
Layout breaks on small screens Quick Reference §5: mobile-first + breakpoint-consistency
Performance / jank Quick Reference §3: virtualize-lists + main-thread-budget + debounce-throttle

Pre-Delivery Checklist

  • Run --domain ux "animation accessibility z-index loading" as a UX validation pass before implementation
  • Run through Quick Reference §1–§3 (CRITICAL + HIGH) as a final review
  • Test on 375px (small phone) and landscape orientation
  • Verify behavior with reduced-motion enabled and Dynamic Type at largest size
  • Check dark mode contrast independently (don't assume light mode values work)
  • Confirm all touch targets ≥44pt and no content hidden behind safe areas

Common Rules for Professional UI

These are frequently overlooked issues that make UI look unprofessional: Scope notice: The rules below are for App UI (iOS/Android/React Native/Flutter), not desktop-web interaction patterns.

Icons & Visual Elements

Rule Standard Avoid Why It Matters
No Emoji as Structural Icons Use vector-based icons (e.g., Lucide, react-native-vector-icons, @expo/vector-icons). Using emojis (🎨 🚀 ⚙️) for navigation, settings, or system controls. Emojis are font-dependent, inconsistent across platforms, and cannot be controlled via design tokens.
Vector-Only Assets Use SVG or platform vector icons that scale cleanly and support theming. Raster PNG icons that blur or pixelate. Ensures scalability, crisp rendering, and dark/light mode adaptability.
Stable Interaction States Use color, opacity, or elevation transitions for press states without changing layout bounds. Layout-shifting transforms that move surrounding content or trigger visual jitter. Prevents unstable interactions and preserves smooth motion/perceived quality on mobile.
Correct Brand Logos Use official brand assets and follow their usage guidelines (spacing, color, clear space). Guessing logo paths, recoloring unofficially, or modifying proportions. Prevents brand misuse and ensures legal/platform compliance.
Consistent Icon Sizing Define icon sizes as design tokens (e.g., icon-sm, icon-md = 24pt, icon-lg). Mixing arbitrary values like 20pt / 24pt / 28pt randomly. Maintains rhythm and visual hierarchy across the interface.
Stroke Consistency Use a consistent stroke width within the same visual layer (e.g., 1.5px or 2px). Mixing thick and thin stroke styles arbitrarily. Inconsistent strokes reduce perceived polish and cohesion.
Filled vs Outline Discipline Use one icon style per hierarchy level. Mixing filled and outline icons at the same hierarchy level. Maintains semantic clarity and stylistic coherence.
Touch Target Minimum Minimum 44×44pt interactive area (use hitSlop if icon is smaller). Small icons without expanded tap area. Meets accessibility and platform usability standards.
Icon Alignment Align icons to text baseline and maintain consistent padding. Misaligned icons or inconsistent spacing around them. Prevents subtle visual imbalance that reduces perceived quality.
Icon Contrast Follow WCAG contrast standards: 4.5:1 for small elements, 3:1 minimum for larger UI glyphs. Low-contrast icons that blend into the background. Ensures accessibility in both light and dark modes.

Interaction (App)

Rule Do Don't
Tap feedback Provide clear pressed feedback (ripple/opacity/elevation) within 80-150ms No visual response on tap
Animation timing Keep micro-interactions around 150-300ms with platform-native easing Instant transitions or slow animations (>500ms)
Accessibility focus Ensure screen reader focus order matches visual order and labels are descriptive Unlabeled controls or confusing focus traversal
Disabled state clarity Use disabled semantics (disabled/native disabled props), reduced emphasis, and no tap action Controls that look tappable but do nothing
Touch target minimum Keep tap areas >=44x44pt (iOS) or >=48x48dp (Android), expand hit area when icon is smaller Tiny tap targets or icon-only hit areas without padding
Gesture conflict prevention Keep one primary gesture per region and avoid nested tap/drag conflicts Overlapping gestures causing accidental actions
Semantic native controls Prefer native interactive primitives (Button, Pressable, platform equivalents) with proper accessibility roles Generic containers used as primary controls without semantics

Light/Dark Mode Contrast

Rule Do Don't
Surface readability (light) Keep cards/surfaces clearly separated from background with sufficient opacity/elevation Overly transparent surfaces that blur hierarchy
Text contrast (light) Maintain body text contrast >=4.5:1 against light surfaces Low-contrast gray body text
Text contrast (dark) Maintain primary text contrast >=4.5:1 and secondary text >=3:1 on dark surfaces Dark mode text that blends into background
Border and divider visibility Ensure separators are visible in both themes (not just light mode) Theme-specific borders disappearing in one mode
State contrast parity Keep pressed/focused/disabled states equally distinguishable in light and dark themes Defining interaction states for one theme only
Token-driven theming Use semantic color tokens mapped per theme across app surfaces/text/icons Hardcoded per-screen hex values
Scrim and modal legibility Use a modal scrim strong enough to isolate foreground content (typically 40-60% black) Weak scrim that leaves background visually competing

Layout & Spacing

Rule Do Don't
Safe-area compliance Respect top/bottom safe areas for all fixed headers, tab bars, and CTA bars Placing fixed UI under notch, status bar, or gesture area
System bar clearance Add spacing for status/navigation bars and gesture home indicator Let tappable content collide with OS chrome
Consistent content width Keep predictable content width per device class (phone/tablet) Mixing arbitrary widths between screens
8dp spacing rhythm Use a consistent 4/8dp spacing system for padding/gaps/section spacing Random spacing increments with no rhythm
Readable text measure Keep long-form text readable on large devices (avoid edge-to-edge paragraphs on tablets) Full-width long text that hurts readability
Section spacing hierarchy Define clear vertical rhythm tiers (e.g., 16/24/32/48) by hierarchy Similar UI levels with inconsistent spacing
Adaptive gutters by breakpoint Increase horizontal insets on larger widths and in landscape Same narrow gutter on all device sizes/orientations
Scroll and fixed element coexistence Add bottom/top content insets so lists are not hidden behind fixed bars Scroll content obscured by sticky headers/footers

Pre-Delivery Checklist

Before delivering UI code, verify these items: Scope notice: This checklist is for App UI (iOS/Android/React Native/Flutter).

Visual Quality

  • No emojis used as icons (use SVG instead)
  • All icons come from a consistent icon family and style
  • Official brand assets are used with correct proportions and clear space
  • Pressed-state visuals do not shift layout bounds or cause jitter
  • Semantic theme tokens are used consistently (no ad-hoc per-screen hardcoded colors)

Interaction

  • All tappable elements provide clear pressed feedback (ripple/opacity/elevation)
  • Touch targets meet minimum size (>=44x44pt iOS, >=48x48dp Android)
  • Micro-interaction timing stays in the 150-300ms range with native-feeling easing
  • Disabled states are visually clear and non-interactive
  • Screen reader focus order matches visual order, and interactive labels are descriptive
  • Gesture regions avoid nested/conflicting interactions (tap/drag/back-swipe conflicts)

Light/Dark Mode

  • Primary text contrast >=4.5:1 in both light and dark mode
  • Secondary text contrast >=3:1 in both light and dark mode
  • Dividers/borders and interaction states are distinguishable in both modes
  • Modal/drawer scrim opacity is strong enough to preserve foreground legibility (typically 40-60% black)
  • Both themes are tested before delivery (not inferred from a single theme)

Layout

  • Safe areas are respected for headers, tab bars, and bottom CTA bars
  • Scroll content is not hidden behind fixed/sticky bars
  • Verified on small phone, large phone, and tablet (portrait + landscape)
  • Horizontal insets/gutters adapt correctly by device size and orientation
  • 4/8dp spacing rhythm is maintained across component, section, and page levels
  • Long-form text measure remains readable on larger devices (no edge-to-edge paragraphs)

Accessibility

  • All meaningful images/icons have accessibility labels
  • Form fields have labels, hints, and clear error messages
  • Color is not the only indicator
  • Reduced motion and dynamic text size are supported without layout breakage
  • Accessibility traits/roles/states (selected, disabled, expanded) are announced correctly
处理Excel、CSV等表格文件的创建、编辑、清洗及格式转换。适用于打开、修复、计算公式、美化报表或从数据源生成新表格,要求输出专业格式且无公式错误。
用户请求打开、读取、编辑或修复.xlsx/.csv文件 需要从其他数据源创建新的电子表格 清理混乱的表格数据并转换为标准格式 涉及表格文件格式转换的任务
skills/xlsx/SKILL.md
npx skills add melandlabs/openloomi --skill xlsx -g -y
SKILL.md
Frontmatter
{
    "name": "xlsx",
    "license": "Proprietary. LICENSE.txt has complete terms",
    "description": "Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."
}

Requirements for Outputs

All Excel files

Professional Font

  • Use a consistent, professional font (e.g., Arial, Times New Roman) for all deliverables unless otherwise instructed by the user

Zero Formula Errors

  • Every Excel model MUST be delivered with ZERO formula errors (#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?)

Preserve Existing Templates (when updating templates)

  • Study and EXACTLY match existing format, style, and conventions when modifying files
  • Never impose standardized formatting on files with established patterns
  • Existing template conventions ALWAYS override these guidelines

Financial models

Color Coding Standards

Unless otherwise stated by the user or existing template

Industry-Standard Color Conventions

  • Blue text (RGB: 0,0,255): Hardcoded inputs, and numbers users will change for scenarios
  • Black text (RGB: 0,0,0): ALL formulas and calculations
  • Green text (RGB: 0,128,0): Links pulling from other worksheets within same workbook
  • Red text (RGB: 255,0,0): External links to other files
  • Yellow background (RGB: 255,255,0): Key assumptions needing attention or cells that need to be updated

Number Formatting Standards

Required Format Rules

  • Years: Format as text strings (e.g., "2024" not "2,024")
  • Currency: Use $#,##0 format; ALWAYS specify units in headers ("Revenue ($mm)")
  • Zeros: Use number formatting to make all zeros "-", including percentages (e.g., "$#,##0;($#,##0);-")
  • Percentages: Default to 0.0% format (one decimal)
  • Multiples: Format as 0.0x for valuation multiples (EV/EBITDA, P/E)
  • Negative numbers: Use parentheses (123) not minus -123

Formula Construction Rules

Assumptions Placement

  • Place ALL assumptions (growth rates, margins, multiples, etc.) in separate assumption cells
  • Use cell references instead of hardcoded values in formulas
  • Example: Use =B5*(1+$B$6) instead of =B5*1.05

Formula Error Prevention

  • Verify all cell references are correct
  • Check for off-by-one errors in ranges
  • Ensure consistent formulas across all projection periods
  • Test with edge cases (zero values, negative numbers)
  • Verify no unintended circular references

Documentation Requirements for Hardcodes

  • Comment or in cells beside (if end of table). Format: "Source: [System/Document], [Date], [Specific Reference], [URL if applicable]"
  • Examples:
    • "Source: Company 10-K, FY2024, Page 45, Revenue Note, [SEC EDGAR URL]"
    • "Source: Company 10-Q, Q2 2025, Exhibit 99.1, [SEC EDGAR URL]"
    • "Source: Bloomberg Terminal, 8/15/2025, AAPL US Equity"
    • "Source: FactSet, 8/20/2025, Consensus Estimates Screen"

XLSX creation, editing, and analysis

Overview

A user may ask you to create, edit, or analyze the contents of an .xlsx file. You have different tools and workflows available for different tasks.

Important Requirements

LibreOffice Required for Formula Recalculation: You can assume LibreOffice is installed for recalculating formula values using the scripts/recalc.py script. The script automatically configures LibreOffice on first run, including in sandboxed environments where Unix sockets are restricted (handled by scripts/office/soffice.py)

Reading and analyzing data

Data analysis with pandas

For data analysis, visualization, and basic operations, use pandas which provides powerful data manipulation capabilities:

import pandas as pd

# Read Excel
df = pd.read_excel('file.xlsx')  # Default: first sheet
all_sheets = pd.read_excel('file.xlsx', sheet_name=None)  # All sheets as dict

# Analyze
df.head()      # Preview data
df.info()      # Column info
df.describe()  # Statistics

# Write Excel
df.to_excel('output.xlsx', index=False)

Excel File Workflows

CRITICAL: Use Formulas, Not Hardcoded Values

Always use Excel formulas instead of calculating values in Python and hardcoding them. This ensures the spreadsheet remains dynamic and updateable.

❌ WRONG - Hardcoding Calculated Values

# Bad: Calculating in Python and hardcoding result
total = df['Sales'].sum()
sheet['B10'] = total  # Hardcodes 5000

# Bad: Computing growth rate in Python
growth = (df.iloc[-1]['Revenue'] - df.iloc[0]['Revenue']) / df.iloc[0]['Revenue']
sheet['C5'] = growth  # Hardcodes 0.15

# Bad: Python calculation for average
avg = sum(values) / len(values)
sheet['D20'] = avg  # Hardcodes 42.5

✅ CORRECT - Using Excel Formulas

# Good: Let Excel calculate the sum
sheet['B10'] = '=SUM(B2:B9)'

# Good: Growth rate as Excel formula
sheet['C5'] = '=(C4-C2)/C2'

# Good: Average using Excel function
sheet['D20'] = '=AVERAGE(D2:D19)'

This applies to ALL calculations - totals, percentages, ratios, differences, etc. The spreadsheet should be able to recalculate when source data changes.

Common Workflow

  1. Choose tool: pandas for data, openpyxl for formulas/formatting
  2. Create/Load: Create new workbook or load existing file
  3. Modify: Add/edit data, formulas, and formatting
  4. Save: Write to file
  5. Recalculate formulas (MANDATORY IF USING FORMULAS): Use the scripts/recalc.py script
    python scripts/recalc.py output.xlsx
    
  6. Verify and fix any errors:
    • The script returns JSON with error details
    • If status is errors_found, check error_summary for specific error types and locations
    • Fix the identified errors and recalculate again
    • Common errors to fix:
      • #REF!: Invalid cell references
      • #DIV/0!: Division by zero
      • #VALUE!: Wrong data type in formula
      • #NAME?: Unrecognized formula name

Creating new Excel files

# Using openpyxl for formulas and formatting
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment

wb = Workbook()
sheet = wb.active

# Add data
sheet['A1'] = 'Hello'
sheet['B1'] = 'World'
sheet.append(['Row', 'of', 'data'])

# Add formula
sheet['B2'] = '=SUM(A1:A10)'

# Formatting
sheet['A1'].font = Font(bold=True, color='FF0000')
sheet['A1'].fill = PatternFill('solid', start_color='FFFF00')
sheet['A1'].alignment = Alignment(horizontal='center')

# Column width
sheet.column_dimensions['A'].width = 20

wb.save('output.xlsx')

Editing existing Excel files

# Using openpyxl to preserve formulas and formatting
from openpyxl import load_workbook

# Load existing file
wb = load_workbook('existing.xlsx')
sheet = wb.active  # or wb['SheetName'] for specific sheet

# Working with multiple sheets
for sheet_name in wb.sheetnames:
    sheet = wb[sheet_name]
    print(f"Sheet: {sheet_name}")

# Modify cells
sheet['A1'] = 'New Value'
sheet.insert_rows(2)  # Insert row at position 2
sheet.delete_cols(3)  # Delete column 3

# Add new sheet
new_sheet = wb.create_sheet('NewSheet')
new_sheet['A1'] = 'Data'

wb.save('modified.xlsx')

Recalculating formulas

Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided scripts/recalc.py script to recalculate formulas:

python scripts/recalc.py <excel_file> [timeout_seconds]

Example:

python scripts/recalc.py output.xlsx 30

The script:

  • Automatically sets up LibreOffice macro on first run
  • Recalculates all formulas in all sheets
  • Scans ALL cells for Excel errors (#REF!, #DIV/0!, etc.)
  • Returns JSON with detailed error locations and counts
  • Works on both Linux and macOS

Formula Verification Checklist

Quick checks to ensure formulas work correctly:

Essential Verification

  • Test 2-3 sample references: Verify they pull correct values before building full model
  • Column mapping: Confirm Excel columns match (e.g., column 64 = BL, not BK)
  • Row offset: Remember Excel rows are 1-indexed (DataFrame row 5 = Excel row 6)

Common Pitfalls

  • NaN handling: Check for null values with pd.notna()
  • Far-right columns: FY data often in columns 50+
  • Multiple matches: Search all occurrences, not just first
  • Division by zero: Check denominators before using / in formulas (#DIV/0!)
  • Wrong references: Verify all cell references point to intended cells (#REF!)
  • Cross-sheet references: Use correct format (Sheet1!A1) for linking sheets

Formula Testing Strategy

  • Start small: Test formulas on 2-3 cells before applying broadly
  • Verify dependencies: Check all cells referenced in formulas exist
  • Test edge cases: Include zero, negative, and very large values

Interpreting scripts/recalc.py Output

The script returns JSON with error details:

{
  "status": "success", // or "errors_found"
  "total_errors": 0, // Total error count
  "total_formulas": 42, // Number of formulas in file
  "error_summary": {
    // Only present if errors found
    "#REF!": {
      "count": 2,
      "locations": ["Sheet1!B5", "Sheet1!C10"]
    }
  }
}

Best Practices

Library Selection

  • pandas: Best for data analysis, bulk operations, and simple data export
  • openpyxl: Best for complex formatting, formulas, and Excel-specific features

Working with openpyxl

  • Cell indices are 1-based (row=1, column=1 refers to cell A1)
  • Use data_only=True to read calculated values: load_workbook('file.xlsx', data_only=True)
  • Warning: If opened with data_only=True and saved, formulas are replaced with values and permanently lost
  • For large files: Use read_only=True for reading or write_only=True for writing
  • Formulas are preserved but not evaluated - use scripts/recalc.py to update values

Working with pandas

  • Specify data types to avoid inference issues: pd.read_excel('file.xlsx', dtype={'id': str})
  • For large files, read specific columns: pd.read_excel('file.xlsx', usecols=['A', 'C', 'E'])
  • Handle dates properly: pd.read_excel('file.xlsx', parse_dates=['date_column'])

Code Style Guidelines

IMPORTANT: When generating Python code for Excel operations:

  • Write minimal, concise Python code without unnecessary comments
  • Avoid verbose variable names and redundant operations
  • Avoid unnecessary print statements

For Excel files themselves:

  • Add comments to cells with complex formulas or important assumptions
  • Document data sources for hardcoded values
  • Include notes for key calculations and model sections

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-13 23:53
浙ICP备14020137号-1 $Map of visitor$