This application implements an Electron-based headless agent system with an ultra-thin overlay architecture. The design prioritizes minimal resource usage, non-intrusive UI, and extensible agent integration.
- Minimal Footprint: Single main process, lightweight renderers, no heavy frameworks
- Non-Intrusive: Transparent overlay, edge-docked chat, never blocks user workspace
- Performance-First: Click-through by default, minimal background processing
- Secure: Context isolation, no Node integration in renderers, CSP headers
- Extensible: Clean IPC message schema with multi-provider AI service and agent orchestration
The repo's custom-agent layer uses a trigger-based coordinator-worker model under .github/agents.
- Supervisor owns task routing and delegates only.
- Researcher gathers workspace or documentation context when the target area is still unclear.
- Architect validates reuse opportunities, design boundaries, and consistency before changes are made.
- Builder performs implementation once the plan and files are concrete.
- Verifier performs independent validation immediately after changes.
- Diagnostician isolates root cause when verification fails or the failure mode is ambiguous.
- Vision Operator analyzes screenshots, overlay behavior, accessibility state, and browser-visible results.
- Use Researcher when the code location, supporting docs, or high-volume context is unclear.
- Use Architect when design reuse, structural consistency, or boundary choices matter.
- Use Builder only after the task is specific enough to implement safely.
- Use Verifier after every code change.
- Use Diagnostician when the verifier finds a regression or the root cause is not yet known.
- Use Vision Operator when UI state, screenshots, overlay behavior, or browser-visible results matter.
The orchestration layer is reinforced by hook policies under .github/hooks:
PreToolUseblocks disallowed tool classes by role.SubagentStopchecks each role's final response for required evidence sections before allowing completion.PostToolUserecords an audit trail.
The practical effect is that routing is not just descriptive. Read-only roles are restricted from mutating files, and worker outputs must carry enough evidence to pass stop-hook quality gates.
See docs/AGENT_ORCHESTRATION.md for the detailed routing and role contract.
The runtime still exposes a single public entrypoint at src/main/ai-service.js, but the implementation is being decomposed into smaller internal modules behind that facade.
system-prompt.js: platform-aware prompt text and action instructions.message-builder.js: prompt assembly, history injection, inspect context, live UI context, semantic DOM context, and provider-specific vision formatting.commands.js: slash-command handling for/provider,/model,/status,/login,/capture,/vision, and/clear.providers/registry.js: provider selection state and API-key storage.providers/copilot/model-registry.js: Copilot model metadata, preference persistence, and dynamic discovery.providers/orchestration.js: fallback chain selection and provider dispatch for initial response, continuation, and regeneration flows.browser-session-state.js,conversation-history.js,visual-context.js, andui-context.js: runtime state holders previously embedded in the monolith.
src/main/ai-service.jsremains the only supported public entrypoint during the migration.- Extracted modules are composed from the facade instead of being consumed directly by app code.
- Source-sensitive regression markers remain in the facade because some tests still inspect literal strings in that file.
The modularization work is gated by focused characterization tests in addition to broader smoke coverage:
scripts/test-ai-service-contract.jsscripts/test-ai-service-commands.jsscripts/test-ai-service-provider-orchestration.js- existing
scripts/test-v006-features.jsandscripts/test-bug-fixes.js
This allows internal seams to move without changing the external contract seen by the CLI, Electron runtime, or agent adapters.
The cognitive layer sits above the AI service and provides learning, memory, tool generation, and context management. All state is persisted under ~/.liku/.
~/.liku/
├── memory/
│ └── notes.json # Agentic memory (A-MEM)
├── skills/
│ ├── index.json # Skill metadata + usage stats
│ └── *.md # Skill definitions
├── tools/
│ ├── registry.json # Tool metadata + approval status
│ ├── dynamic/ # Approved/executable tool scripts
│ └── proposed/ # Quarantined proposals (not executable)
├── telemetry/
│ └── logs/ # Structured JSONL telemetry
└── preferences.json # User preferences (migrated from ~/.liku-cli/)
CRUD store for structured notes with Zettelkasten-style linking. Each note has type, keywords, tags, and links attributes. getRelevantNotes(query, limit) selects notes by keyword overlap score and injects up to 2000 BPE tokens into the system prompt as ## Working Memory.
Loads skill files from ~/.liku/skills/, selects the top 3 matching skills by combined scoring, and injects up to 1500 BPE tokens as ## Relevant Skills. Stale index entries (pointing to deleted files) are pruned on every loadIndex() call.
Tiered scoring (N1-T2):
- Tier 1: Word-boundary keyword matching (+2/keyword, +1/tag, +0.5 recency).
- Tier 2: TF-IDF cosine similarity (pure JS, zero deps).
tokenize()→termFrequency()→inverseDocFrequency()→tfidfVector()→cosineSimilarity(). TF-IDF score scaled ×5 and added to keyword score. - Combined:
finalScore = keywordScore + (tfidfSimilarity × 5)
telemetry-writer.js: Structured JSONL logger with rotation at 10MB. Schema:{ task, phase, outcome, context, timestamp }.reflection-trigger.js: Fires reflection when consecutive failures ≥ 3 or session failures ≥ 5. Bounded atMAX_REFLECTION_ITERATIONS = 2. Session failure count decays by 1 on success. Supports cross-model reflection — whenreflectionModelOverrideis set (via/rmodel), reflection passes route to a reasoning model (e.g., o3-mini) instead of the default chat model.
tool-validator.js: Static analysis — rejects code matching 16 banned patterns (require(,process.,fs., etc.) and scripts over 10KB.tool-registry.js: CRUD for tool metadata. Proposal flow:proposeTool()→ quarantine inproposed/→promoteTool()moves todynamic/→ executable.rejectTool()deletes and logs negative reward.sandbox.js: Forkssandbox-worker.jsas a separate Node.js process viachild_process.fork(). Worker env stripped to{ NODE_ENV: 'sandbox', PATH }. Parent sets 5.5s timeout withSIGKILL. Returns a Promise.sandbox-worker.js: Receives tool code via IPC, executes invm.createContextwith allowlisted globals (JSON,Math,Date,Array,Object,String,Number,Boolean,RegExp,Map,Set,Promise). Args areObject.freeze-d. Results sent back via IPC.hook-runner.js: Invokes.github/hooks/security scripts (PreToolUse/PostToolUse). Fails closed on errors.
BPE tokenizer using js-tiktoken (cl100k_base encoding, compatible with GPT-4o/o1). Exports countTokens(text) → number and truncateToTokenBudget(text, maxTokens) → string. Lazy-loaded singleton encoder.
Assembles the message array for API calls. Accepts explicit skillsContext and memoryContext parameters (injected as ## Relevant Skills and ## Working Memory system messages). This makes context injection testable and decoupled from global state.
Extracts procedural memory from successful multi-step action sequences (≥ 3 steps). Extracted AWM notes are auto-registered as skills via skillRouter.addSkill(), gated by the PreToolUse hook.
saveSessionNote() in ai-service.js fires on chat exit. Extracts user messages from recent conversation history, computes top keywords via frequency analysis (with stop word removal), and writes an episodic memory note via memoryStore.addNote(). On next session, getRelevantNotes() picks up matching session context automatically.
liku analytics [--days N] [--raw] [--json] reads telemetry JSONL for the requested date range and displays success rates, top tasks, phase breakdown, and common failure reasons.
User Input → ai-service.js
├── memory-store.getRelevantNotes() → memoryContext
├── skill-router.getRelevantSkills() → skillsContext
├── message-builder.buildMessages({ skillsContext, memoryContext })
├── Provider sends request → AI response
├── system-automation.executeAction()
│ ├── hook-runner.runPreToolUse()
│ ├── sandbox.executeDynamicTool() [if dynamic tool]
│ └── hook-runner.runPostToolUse()
├── telemetry-writer.writeTelemetry()
├── reflection-trigger.shouldReflect() → optional reflection loop
└── AWM extraction (if ≥3 successful steps)
┌─────────────────────────────────────────────────────────────────┐
│ Main Process │
│ ┌────────────┐ ┌──────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ Overlay │ │ Chat │ │ Tray │ │ Global │ │
│ │ Manager │ │ Manager │ │ Icon │ │ Hotkeys │ │
│ └─────┬──────┘ └────┬─────┘ └─────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ ┌─────┴──────────────┴──────────────┴────────────────┴──────┐ │
│ │ IPC Router │ │
│ └─────┬────────────────────────────────────────────┬────────┘ │
└────────┼────────────────────────────────────────────┼───────────┘
│ │
┌────┴────────┐ ┌───────┴────────┐
│ Overlay │ │ Chat │
│ Renderer │ │ Renderer │
│ │ │ │
│ ┌─────────┐ │ │ ┌────────────┐ │
│ │ Dots │ │ │ │ History │ │
│ │ Grid │ │ │ │ │ │
│ └─────────┘ │ │ └────────────┘ │
│ ┌─────────┐ │ │ ┌────────────┐ │
│ │ Mode │ │ │ │ Input │ │
│ │Indicator│ │ │ │ │ │
│ └─────────┘ │ │ └────────────┘ │
└─────────────┘ │ ┌────────────┐ │
│ │ Controls │ │
│ └────────────┘ │
└────────────────┘
Responsibilities:
- Window lifecycle management
- IPC message routing
- Global state management
- System integration (tray, hotkeys)
Key Functions:
createOverlayWindow(): Creates transparent, always-on-top overlaycreateChatWindow(): Creates edge-docked chat interfacecreateTray(): Sets up system tray icon and menuregisterShortcuts(): Registers global hotkeyssetupIPC(): Configures IPC message handlerssetOverlayMode(): Switches between passive/selection modestoggleChat(): Shows/hides chat windowtoggleOverlay(): Shows/hides overlay
State:
{
overlayMode: 'passive' | 'selection',
isChatVisible: boolean,
overlayWindow: BrowserWindow,
chatWindow: BrowserWindow,
tray: Tray
}Responsibilities:
- Render dot grid
- Handle dot interactions
- Display mode indicator
- Communicate selections to main process
Files:
index.html: UI structure and stylespreload.js: Secure IPC bridge
State:
{
currentMode: 'passive' | 'selection',
gridType: 'coarse' | 'fine',
dots: Array<{id, x, y, label}>
}Key Functions:
generateCoarseGrid(): Creates ~100px spacing gridgenerateFineGrid(): Creates ~25px spacing gridrenderDots(): Renders interactive dotsselectDot(): Handles dot click eventsupdateModeDisplay(): Updates UI based on mode
Responsibilities:
- Display chat history
- Handle user input
- Show mode controls
- Receive agent responses
Files:
index.html: UI structure and stylespreload.js: Secure IPC bridge
State:
{
currentMode: 'passive' | 'selection',
messages: Array<{text, type, timestamp}>
}Key Functions:
addMessage(): Adds message to historysendMessage(): Sends user message to mainsetMode(): Changes overlay modeupdateModeDisplay(): Updates mode button states
{
id: string, // e.g., 'dot-100-200'
x: number, // Screen X coordinate
y: number, // Screen Y coordinate
label: string, // e.g., 'A2'
timestamp: number // Unix timestamp in ms
}'passive' | 'selection'string // User message text{
text: string, // Response text
timestamp: number // Unix timestamp in ms
}'passive' | 'selection'// Response:
{
overlayMode: 'passive' | 'selection',
isChatVisible: boolean
}{
// Frameless and transparent
frame: false,
transparent: true,
// Always on top
alwaysOnTop: true,
level: 'screen-saver', // macOS only
// Full screen
fullscreen: true,
// Non-interactive by default
focusable: false,
skipTaskbar: true,
// Security
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: 'overlay/preload.js'
}
}{
// Standard window with frame
frame: true,
transparent: false,
// Positioned at bottom-right
x: width - chatWidth - margin,
y: height - chatHeight - margin,
// Resizable but not always on top
resizable: true,
alwaysOnTop: false,
// Hidden by default
show: false,
// Security
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: 'chat/preload.js'
}
}- Purpose: Allow normal application interaction
- Behavior:
- Overlay fully click-through via
setIgnoreMouseEvents(true) - No dots rendered
- Mode indicator hidden
- CPU usage minimal (no event processing)
- Overlay fully click-through via
- Purpose: Enable screen element selection
- Behavior:
- Overlay captures mouse events via
setIgnoreMouseEvents(false) - Dots rendered with CSS
pointer-events: auto - Mode indicator visible
- Click events captured and routed via IPC
- Automatically reverts to passive after selection
- Overlay captures mouse events via
All renderer processes use context isolation to prevent prototype pollution attacks.
Secure bridge between main and renderer processes:
contextBridge.exposeInMainWorld('electronAPI', {
// Only expose necessary methods
selectDot: (data) => ipcRenderer.send('dot-selected', data),
onModeChanged: (cb) => ipcRenderer.on('mode-changed', cb)
});All HTML files include CSP headers:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline';">All resources loaded locally, no CDN or external dependencies.
- Target: < 300MB steady-state
- Baseline: ~150MB for Electron + Chromium
- Overlay: ~20-30MB (minimal DOM, vanilla JS)
- Chat: ~30-40MB (simple UI, limited history)
- Idle (passive mode): < 0.5%
- Selection mode: < 2%
- During interaction: < 5%
- Target: < 3 seconds to functional
- Breakdown:
- Electron init: ~1s
- Window creation: ~1s
- Renderer load: ~0.5s
New providers can be added by implementing the provider interface in src/main/ai-service/providers/ and registering in the provider registry. The orchestration layer handles fallback chains and dispatch.
New CLI commands are added as modules in src/cli/commands/ and registered in the COMMANDS table in src/cli/liku.js.
New orchestration roles can be added as agent definition files in .github/agents/ with corresponding hook policies in .github/hooks/.
- Window level:
'screen-saver'to float above fullscreen - Dock: Hidden via
app.dock.hide() - Tray: NSStatusBar with popover behavior
- Permissions: Requires accessibility + screen recording
- Window level: Standard
alwaysOnTop - Taskbar: Overlay hidden via
skipTaskbar - Tray: System tray with balloon tooltips
- Permissions: No special permissions required
- Check window level setting
- Verify
alwaysOnTopis true - Test with
overlayWindow.show() - Check GPU acceleration settings
- Verify
setIgnoreMouseEvents(true, {forward: true}) - Check CSS
pointer-eventson elements - Test in different applications
- Check for conflicting event handlers
- Verify
chatWindow.show()is called - Check window position (may be off-screen)
- Verify not hidden behind other windows
- Check
skipTaskbarsetting
- Verify preload script loaded
- Check
contextBridgeexposure - Enable IPC logging in DevTools
- Verify correct channel names
- Check provider authentication (
/loginor environment variables) - Verify model availability with
/status - Check capability routing with
/model - Review conversation state with
/status
- Use context isolation
- Disable node integration in renderers
- Minimize renderer dependencies
- Implement proper cleanup on window close
- Use debouncing for frequent events
- Test on both platforms
- Enable node integration in production
- Load remote content without validation
- Create/destroy windows repeatedly
- Poll continuously in background
- Ignore security warnings
- Assume platform consistency