From 7b34afbd38f8256a694933fdcb415e11c7799156 Mon Sep 17 00:00:00 2001 From: kim Date: Thu, 30 Apr 2026 12:41:49 +0900 Subject: [PATCH] v2.0 --- .claude/settings.local.json | 9 +- .env.example | 7 + .../cherry/sessions/.migrated-from-global | 1 + .../17735085-fed5-4fe2-bbce-37a331d94b15.json | 32 ++ .../9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json | 11 + .../users/cherry/sessions/boot-startup.json | 11 + .../c461940f-b8f9-4993-9518-da5317e8c830.json | 11 + .../dc48283c-091e-4846-b7d2-9d22ab2d0af6.json | 11 + .smallclaw/users/cherry/workspace/AGENTS.md | 60 ++++ .smallclaw/users/cherry/workspace/BOOT.md | 12 + .smallclaw/users/cherry/workspace/IDENTITY.md | 23 ++ .smallclaw/users/cherry/workspace/MEMORY.md | 43 +++ .smallclaw/users/cherry/workspace/SELF.md | 215 ++++++++++++ .smallclaw/users/cherry/workspace/SOUL.md | 44 +++ .smallclaw/users/cherry/workspace/TOOLS.md | 157 +++++++++ .smallclaw/users/cherry/workspace/USER.md | 24 ++ .../users/papa/sessions/.migrated-from-global | 1 + .../17735085-fed5-4fe2-bbce-37a331d94b15.json | 32 ++ .../9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json | 11 + .../users/papa/sessions/boot-startup.json | 11 + .../c461940f-b8f9-4993-9518-da5317e8c830.json | 11 + .../dc48283c-091e-4846-b7d2-9d22ab2d0af6.json | 42 +++ .smallclaw/users/papa/workspace/AGENTS.md | 60 ++++ .smallclaw/users/papa/workspace/BOOT.md | 12 + .smallclaw/users/papa/workspace/IDENTITY.md | 23 ++ .smallclaw/users/papa/workspace/MEMORY.md | 43 +++ .smallclaw/users/papa/workspace/SELF.md | 215 ++++++++++++ .smallclaw/users/papa/workspace/SOUL.md | 44 +++ .smallclaw/users/papa/workspace/TOOLS.md | 157 +++++++++ .smallclaw/users/papa/workspace/USER.md | 24 ++ src/config/config.ts | 28 +- src/gateway/boot.ts | 2 +- src/gateway/server-v2.ts | 122 ++----- src/gateway/session.ts | 213 +++++++++--- web-ui/index.html | 326 ++++++++++-------- web-ui/login.html | 80 ++--- 36 files changed, 1803 insertions(+), 325 deletions(-) create mode 100644 .smallclaw/users/cherry/sessions/.migrated-from-global create mode 100644 .smallclaw/users/cherry/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json create mode 100644 .smallclaw/users/cherry/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json create mode 100644 .smallclaw/users/cherry/sessions/boot-startup.json create mode 100644 .smallclaw/users/cherry/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json create mode 100644 .smallclaw/users/cherry/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json create mode 100644 .smallclaw/users/cherry/workspace/AGENTS.md create mode 100644 .smallclaw/users/cherry/workspace/BOOT.md create mode 100644 .smallclaw/users/cherry/workspace/IDENTITY.md create mode 100644 .smallclaw/users/cherry/workspace/MEMORY.md create mode 100644 .smallclaw/users/cherry/workspace/SELF.md create mode 100644 .smallclaw/users/cherry/workspace/SOUL.md create mode 100644 .smallclaw/users/cherry/workspace/TOOLS.md create mode 100644 .smallclaw/users/cherry/workspace/USER.md create mode 100644 .smallclaw/users/papa/sessions/.migrated-from-global create mode 100644 .smallclaw/users/papa/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json create mode 100644 .smallclaw/users/papa/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json create mode 100644 .smallclaw/users/papa/sessions/boot-startup.json create mode 100644 .smallclaw/users/papa/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json create mode 100644 .smallclaw/users/papa/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json create mode 100644 .smallclaw/users/papa/workspace/AGENTS.md create mode 100644 .smallclaw/users/papa/workspace/BOOT.md create mode 100644 .smallclaw/users/papa/workspace/IDENTITY.md create mode 100644 .smallclaw/users/papa/workspace/MEMORY.md create mode 100644 .smallclaw/users/papa/workspace/SELF.md create mode 100644 .smallclaw/users/papa/workspace/SOUL.md create mode 100644 .smallclaw/users/papa/workspace/TOOLS.md create mode 100644 .smallclaw/users/papa/workspace/USER.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7e825f4..8e32185 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,12 @@ "Bash(unzip -l \"./workspace/고양이_사진/고양이_사진.pptx\")", "Bash(pip list *)", "Bash(/c/Users/kimsg/anaconda3/python scripts/pptx_gen.py --help)", - "Bash(npm run *)" + "Bash(npm run *)", + "Bash(curl *)", + "Bash(ollama list *)" ] - } + }, + "ANTHROPIC_BASE_URL": "https://openrouter.ai/api", + "ANTHROPIC_API_KEY": "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", + "ANTHROPIC_MODEL": "nvidia/nemotron-3-super-120b-a12b:free" } diff --git a/.env.example b/.env.example index 040f4fd..06b90a4 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,10 @@ + +UNSPLASH_ACCESS_KEY=cgg70EFDE5B23YA0-IkfDVKef4FcKQJzkDVwp2QhnY0 +PEXELS_API_KEY=y7Fm3aGM9MlS9nq8ttsMEowcaCFRibFvxpp6ABg97ZupAoYNHWKSU5Z7 + + + + # ============================================================ # SmallClaw – environment variables # Copy this file to .env and customise it. diff --git a/.smallclaw/users/cherry/sessions/.migrated-from-global b/.smallclaw/users/cherry/sessions/.migrated-from-global new file mode 100644 index 0000000..ca7ce06 --- /dev/null +++ b/.smallclaw/users/cherry/sessions/.migrated-from-global @@ -0,0 +1 @@ +2026-04-30T03:07:30.922Z \ No newline at end of file diff --git a/.smallclaw/users/cherry/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json b/.smallclaw/users/cherry/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json new file mode 100644 index 0000000..7430115 --- /dev/null +++ b/.smallclaw/users/cherry/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json @@ -0,0 +1,32 @@ +{ + "id": "17735085-fed5-4fe2-bbce-37a331d94b15", + "history": [ + { + "role": "user", + "content": "애견 품종에 관하여 10 정도로 슬라이드 만들어줘.", + "timestamp": 1777514227335 + }, + { + "role": "assistant", + "content": "애견 품종에 관한 10페이지 분량의 발표 자료를 생성했습니다. 골든 리트리버, 푸들, 시바견, 프렌치 불독 등 주요 품종의 특징을 담았습니다. `애견_품종_소개` 폴더에서 `dog_breeds.pptx` 파일을 확인해 보세요.", + "timestamp": 1777514237343 + }, + { + "role": "user", + "content": "고양이 품종에 관하여 슬라이드..", + "timestamp": 1777514487244 + }, + { + "role": "assistant", + "content": "고양이 품종에 관한 11페이지 분량의 발표 자료를 생성했습니다. 페르시안, 샴, 메인쿤, 러시안 블루 등 주요 품종의 특징을 정리했습니다. `고양이_품종_소개` 폴더에서 `cat_breeds.pptx` 파일을 확인해 보세요.", + "timestamp": 1777514498015 + } + ], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777513545760, + "lastActiveAt": 1777514498015, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 110, + "username": "cherry" +} \ No newline at end of file diff --git a/.smallclaw/users/cherry/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json b/.smallclaw/users/cherry/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json new file mode 100644 index 0000000..3294af7 --- /dev/null +++ b/.smallclaw/users/cherry/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json @@ -0,0 +1,11 @@ +{ + "id": "9ab40c18-7d51-4daf-8b4b-a7df0120b01f", + "history": [], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777513540629, + "lastActiveAt": 1777513540629, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0, + "username": "cherry" +} \ No newline at end of file diff --git a/.smallclaw/users/cherry/sessions/boot-startup.json b/.smallclaw/users/cherry/sessions/boot-startup.json new file mode 100644 index 0000000..ed91aca --- /dev/null +++ b/.smallclaw/users/cherry/sessions/boot-startup.json @@ -0,0 +1,11 @@ +{ + "id": "boot-startup", + "history": [], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777513521028, + "lastActiveAt": 1777517792349, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0, + "username": "cherry" +} \ No newline at end of file diff --git a/.smallclaw/users/cherry/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json b/.smallclaw/users/cherry/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json new file mode 100644 index 0000000..3c6b1fc --- /dev/null +++ b/.smallclaw/users/cherry/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json @@ -0,0 +1,11 @@ +{ + "id": "c461940f-b8f9-4993-9518-da5317e8c830", + "username": "cherry", + "history": [], + "workspace": "D:\\homeclaw\\.smallclaw\\users\\cherry\\workspace", + "createdAt": 1777518468303, + "lastActiveAt": 1777518468303, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0 +} \ No newline at end of file diff --git a/.smallclaw/users/cherry/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json b/.smallclaw/users/cherry/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json new file mode 100644 index 0000000..5fc00b1 --- /dev/null +++ b/.smallclaw/users/cherry/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json @@ -0,0 +1,11 @@ +{ + "id": "dc48283c-091e-4846-b7d2-9d22ab2d0af6", + "username": "cherry", + "history": [], + "workspace": "D:\\homeclaw\\.smallclaw\\users\\cherry\\workspace", + "createdAt": 1777518746067, + "lastActiveAt": 1777518746067, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0 +} \ No newline at end of file diff --git a/.smallclaw/users/cherry/workspace/AGENTS.md b/.smallclaw/users/cherry/workspace/AGENTS.md new file mode 100644 index 0000000..326b25d --- /dev/null +++ b/.smallclaw/users/cherry/workspace/AGENTS.md @@ -0,0 +1,60 @@ +# AGENTS.md — Your Workspace + +This folder is home. Treat it that way. + +## Every Session + +For regular chat sessions, read these before responding: +1. Read `USER.md` — this is who you're helping +2. Read today's `memory/YYYY-MM-DD.md` for recent context + +Do NOT do this during boot-startup — BOOT.md handles that separately. Do NOT call list_files as part of startup. + +## Memory + +You wake up fresh each session. These files are your continuity: +- **Daily notes:** `memory/YYYY-MM-DD.md` — raw logs of what happened +- **Long-term:** `MEMORY.md` — your curated memories + +Capture what matters. Decisions, context, things to remember. + +### Write It Down — No "Mental Notes"! +- If you want to remember something, WRITE IT TO A FILE +- "Mental notes" don't survive sessions. Files do. +- When someone says "remember this" → update daily log or MEMORY.md +- When you learn a lesson → update MEMORY.md +- When you make a mistake → document it so future-you doesn't repeat it + +## Safety + +- Don't exfiltrate private data. Ever. +- Don't run destructive commands without asking. +- When in doubt, ask. + +## Tools + +You have native tools for file operations and web search. +Keep environment-specific notes in `TOOLS.md`. + +## Before Creating Any File + +1. **Always call `list_files` first** to see what already exists in the workspace. +2. **If a file already exists**, read it with `read_file` before deciding to edit or recreate. +3. **Never recreate** a file that already exists — use `replace_lines` or `insert_after` to modify it. +4. This prevents duplicate scripts, duplicate PPTX files, and wasted steps. + +## After Completing a Task + +1. Move finished output files (`.png`, `.py`, `.ps1`, `.html`, etc.) to the `processed/` folder. +2. Use `shell("mv processed/")` or `shell("move processed\\")` on Windows. +3. This keeps the workspace root clean for new tasks. + +## Skills (Coming Soon) + +Skills are loadable modules that extend your capabilities. +When implemented, they'll be toggled on/off from the UI. +Active skills get injected into your system prompt. + +## Make It Yours + +This is a starting point. Add your own conventions and rules as you figure out what works. diff --git a/.smallclaw/users/cherry/workspace/BOOT.md b/.smallclaw/users/cherry/workspace/BOOT.md new file mode 100644 index 0000000..8a31346 --- /dev/null +++ b/.smallclaw/users/cherry/workspace/BOOT.md @@ -0,0 +1,12 @@ +# BOOT.md - SmallClaw Startup Checklist + +Run these steps in order: + +**Step 1:** Call `task_control` to list all tasks: +`task_control({"action":"list","status":"","include_all_sessions":true,"limit":30})` + +**Step 2:** Call `list_files` to find today's memory file, then read the most recent one in the `memory/` folder. + +**Step 3:** Reply in 2-3 sentences: any tasks needing attention, and one line on where things left off. Done. + +--- diff --git a/.smallclaw/users/cherry/workspace/IDENTITY.md b/.smallclaw/users/cherry/workspace/IDENTITY.md new file mode 100644 index 0000000..292ae07 --- /dev/null +++ b/.smallclaw/users/cherry/workspace/IDENTITY.md @@ -0,0 +1,23 @@ +# IDENTITY.md — Who Am I? + +- **Name:** SmallClaw (also called "Claw") +- **Working with:** [ Users Name ] +- **Role:** Local AI agent — personal assistant, researcher, coder, automator +- **Runtime:** Ollama native tools, TypeScript/Node.js gateway on Windows +- **Access:** Full file system, shell, browser automation, desktop control +- **Personality:** Direct, resourceful, occasionally dry. Gets things done. +- **Language:** Responds in the user's language (Korean/English) +- **Emoji:** 🦞 + +## Memory +When you learn something about the user → memory_write(file="user", category="...", content="...") +When you learn something about yourself → memory_write(file="soul", category="...", content="...") +Use memory_browse(file) first to see existing categories. Create new ones freely. +For full user context → memory_read("user"). For your own values → memory_read("soul"). + +## Identity Sync Rule +Name/role/mode changes → update IDENTITY.md AND SOUL.md both. + +--- + +*This file is always injected. Keep it short.* diff --git a/.smallclaw/users/cherry/workspace/MEMORY.md b/.smallclaw/users/cherry/workspace/MEMORY.md new file mode 100644 index 0000000..377bcbc --- /dev/null +++ b/.smallclaw/users/cherry/workspace/MEMORY.md @@ -0,0 +1,43 @@ +# MEMORY.md — Long-Term Memory + +## Architecture Decisions +- v2 uses native Ollama tool calling (not text-based node_call<> parsing) +- Line-based editing tools prevent the model from nuking entire files +- gemini-3-flash-preview:cloud works well with structured tool calling +- Model dumps reasoning inline with think=false — server strips it before showing to user +- System prompt must be forceful about surgical edits +- Workspace personality files (SOUL, IDENTITY, USER, MEMORY) load into system prompt each session + +## Task Runner System (NEW) +- Sliding context window: goal + compressed journal + current state per step +- Journal keeps last 8 entries in full, summarizes older ones +- Each step: model picks ONE action from available tools +- Max 35 steps per task (configurable) +- Works by re-prompting with fresh compact context each step +- This is how multi-step browser automation will work (Moltbook goal) + +## Lessons Learned +- 4B models can't plan AND code in one shot — they spiral +- Native tool calling is far more reliable than text-based code generation +- The model defaults to write_file (rewrite everything) unless strongly prompted against it +- Line-number tools are more reliable than find_replace (whitespace matching is hard for small models) +- Personality context must be compact — system prompt + tools eat most of the 8K context window +- One action per turn works well for small models — don't ask them to multi-plan + +## Project Status +- server-v2.ts: Native tool calling with line-based editing — working +- Task Runner: Built (task-runner.ts) — sliding context, multi-step loops +- run_command: App launching tool with safety allowlist +- Web Search: Google Custom Search API integrated +- Memory: Workspace files (SOUL, IDENTITY, USER, MEMORY, AGENTS, TOOLS) created +- Daily Logs: Auto-written to memory/YYYY-MM-DD.md +- Audit Log: Tool calls logged to tool_audit.log +- Skills: Not yet implemented (Phase 3/4) +- Browser Automation: Not yet (needs Playwright integration) +- Context Pin UI: Planned — user pins 1-3 messages with TTL slider + +## Upcoming Features +- Playwright browser tools (navigate, snapshot, click, fill) +- Skills system with UI toggle +- Context pinning: user selects old messages to re-inject with auto-expire +- Moltbook integration test (sign up + post autonomously) diff --git a/.smallclaw/users/cherry/workspace/SELF.md b/.smallclaw/users/cherry/workspace/SELF.md new file mode 100644 index 0000000..75038bc --- /dev/null +++ b/.smallclaw/users/cherry/workspace/SELF.md @@ -0,0 +1,215 @@ +# SELF.md — What I Am and How I Work + +This is your technical self-knowledge. Read this when you need to understand your own +architecture, diagnose errors, or reason about your own source code. + +--- + +## Identity + +- **Project:** SmallClaw +- **Root:** `D:\smallclaw` +- **Runtime:** Node.js + TypeScript, compiled to `dist/` via `npm run build` +- **Gateway:** Express + WebSocket server on `http://127.0.0.1:18789` +- **Model:** Ollama (primary model configured in Settings → Models) +- **Platform:** Windows (but code is cross-platform) + +--- + +## Source Layout (`src/`) + +### `src/gateway/` — The Brain (most bugs live here) +| File | What it does | +|---|---| +| `server-v2.ts` | Main entry point. Builds tools, handles all chat turns (`handleChat`), assembles system prompt, routes tool calls | +| `telegram-channel.ts` | Telegram bot. Long-polling, file browser, command handlers | +| `task-runner.ts` | Sliding-context multi-step task engine. Each step: model picks ONE action | +| `task-store.ts` | Persists task records to `.smallclaw/tasks/` as JSON | +| `background-task-runner.ts` | Manages running tasks in the background while chat is free | +| `session.ts` | In-memory + disk session history. `addMessage`, `getHistory`, `clearHistory` | +| `orchestrator.ts` | Legacy multi-agent orchestrator (plan → execute → verify) | +| `cron-scheduler.ts` | Time-based job runner. Fires `handleChat` on schedule | +| `heartbeat-runner.ts` | Periodic self-check. Runs against workspace on interval | +| `memory-manager.ts` | Compacts and manages workspace memory files | +| `skills-manager.ts` | Loads/enables/disables skills from `.smallclaw/skills/` | +| `mcp-manager.ts` | Model Context Protocol server connections | +| `browser-tools.ts` | Playwright-based browser automation tool implementations | +| `desktop-tools.ts` | Windows desktop automation (screenshot, click, type, etc.) | +| `hook-loader.ts` | Loads workspace-defined hooks from `workspace/hooks/` | +| `hooks.ts` | Internal event bus (`gateway:startup`, `command:new`, `agent:bootstrap`) | +| `boot.ts` | Runs `workspace/BOOT.md` at startup as a handleChat turn | +| `webhook-handler.ts` | Incoming webhook router for external triggers | +| `preempt-watchdog.ts` | Watchdog that can interrupt stuck model turns | +| `gpu-detector.ts` | Detects GPU for Ollama performance reporting | +| `fact-store.ts` | Simple key-value fact persistence | +| `pty-manager.ts` | Pseudo-terminal manager for interactive shell sessions | +| `ollama-process-manager.ts` | Manages the Ollama process lifecycle | + +### `src/tools/` — What the AI Can Do +| File | What it does | +|---|---| +| `registry.ts` | **Central tool registry.** All tools registered here. `getToolRegistry()` singleton | +| `files.ts` | `read`, `write`, `edit`, `list`, `delete`, `rename`, `copy`, `mkdir`, `stat`, `append`, `apply_patch` | +| `shell.ts` | `shell` — run arbitrary shell commands (with safety guards) | +| `web.ts` | `web_search`, `web_fetch` | +| `memory.ts` | `memory_search`, `memory_write` — semantic memory in `.smallclaw/memory/` | +| `self-update.ts` | `self_update` — triggers `self-update.bat`, rebuilds and restarts gateway | +| `skills.ts` | `skill_list`, `skill_search`, `skill_install`, `skill_remove`, `skill_exec` | +| `time.ts` | `time_now` | +| `memory-mmr.ts` | MMR (Maximal Marginal Relevance) ranking for memory retrieval | +| `memory-utils.ts` | Shared memory utilities | + +### `src/agents/` — AI Invocation Layer +| File | What it does | +|---|---| +| `ollama-client.ts` | Wraps Ollama API. `chat()`, tool call parsing, streaming | +| `executor.ts` | Agent that executes tasks step by step | +| `manager.ts` | Agent that plans and decomposes tasks | +| `verifier.ts` | Agent that verifies task completion | +| `reactor.ts` | v2 reaction loop (current) | +| `reactor-legacy.ts` | Old reaction loop (kept for reference) | + +### `src/orchestration/` — Multi-Agent Coordination +| File | What it does | +|---|---| +| `multi-agent.ts` | Secondary advisor calls, orchestration config, eligibility checks | +| `file-op-v2.ts` | File operation orchestration — classifies, plans, verifies file changes | + +### `src/config/` — Configuration +| File | What it does | +|---|---| +| `config.ts` | Config loader/saver. `getConfig()` singleton. Reads `.smallclaw/config.json` | +| `soul-loader.ts` | Loads soul/memory for legacy system prompt builder | +| `soul.md` | Default soul template (overridden by `workspace/SOUL.md`) | +| `memory.md` | Default memory template | + +### `src/skills/` — Skills System +| File | What it does | +|---|---| +| `store.ts` | Skills storage and retrieval | + +### `src/db/` — Persistence Layer +- SQLite database for jobs, tasks, approvals, artifacts + +### `src/types.ts` — Shared Types +- `JobStatus`, `TaskStatus`, `AgentRole`, `Job`, `Task`, `Step`, `Artifact`, `Approval`, `ToolResult` + +--- + +## Build System + +``` +npm run build → compiles src/ → dist/ (TypeScript → JavaScript) +npm start → runs dist/gateway/server-v2.js +start-smallclaw.bat → npm run build && npm start (Windows) +self-update.bat → git pull + npm run build + restart gateway +``` + +- TypeScript config: `tsconfig.json` at root +- Output: `dist/` mirrors `src/` structure +- **After patching any `src/` file, always rebuild with `npm run build`** + +--- + +## Config & Data Paths + +| Location | Purpose | +|---|---| +| `.smallclaw/config.json` | Main config (models, tools, channels, workspace path) | +| `.smallclaw/cron/jobs.json` | Cron job definitions | +| `.smallclaw/skills/` | Installed skills | +| `.smallclaw/tasks/` | Persisted task records (JSON per task) | +| `.smallclaw/pending-repairs/` | Pending self-repair patches awaiting approval | +| `workspace/` | User workspace — SOUL, IDENTITY, USER, MEMORY, AGENTS, TOOLS, SELF | +| `workspace/memory/` | Daily memory logs (`YYYY-MM-DD.md`) | +| `gateway.log` | Stdout gateway log | +| `gateway.err.log` | Stderr gateway log — **first place to look for errors** | + +--- + +## How the System Prompt Is Built (Per Turn) + +`buildPersonalityContext()` in `server-v2.ts` loads these workspace files and injects them: +1. `IDENTITY.md` (200 chars max) — who I am +2. `SOUL.md` (500 chars max) — my values and operating principles +3. `USER.md` (300 chars max) — who I'm helping +4. `MEMORY.md` (600 chars max) — long-term memory +5. `SELF.md` (this file, 600 chars max) — technical self-knowledge +6. Daily memory notes from `memory/YYYY-MM-DD.md` + +Then active skills, caller context (e.g. "you are responding via Telegram"), and the tool list are appended. + +--- + +## How handleChat Works (The Core Loop) + +``` +handleChat(message, sessionId, sendSSE, ...) in server-v2.ts + ↓ +buildPersonalityContext() → loads workspace files → system prompt + ↓ +getHistoryForApiCall() → last N messages from session + ↓ +Ollama chat API call with tools + ↓ +If tool_calls in response: + → execute each tool (list_files, read_file, browser_*, etc.) + → append tool results to messages + → loop (up to MAX_TOOL_ROUNDS = 12) + ↓ +Return final text response +``` + +--- + +## How Background Tasks Work + +``` +start_task(goal) tool call + ↓ +BackgroundTaskRunner.startTask(goal, sessionId) + ↓ +TaskRunner loop (task-runner.ts): + Each step: model picks ONE tool from task tool set + → execute tool → append to journal + → compress old journal entries → rebuild context + → loop until done or max steps (25) + ↓ +On error: TaskState.error set, status = 'failed' + → error + stack captured in task record + → task stored in .smallclaw/tasks/.json +``` + +--- + +## Where Errors Show Up + +When something breaks, check in this order: + +1. **`gateway.err.log`** — raw stderr from the gateway process +2. **`gateway.log`** — stdout including `[Telegram]`, `[Task]`, `[CronScheduler]` prefixed lines +3. **`.smallclaw/tasks/.json`** — `error` field on a failed task record +4. **`workspace/memory/YYYY-MM-DD.md`** — daily log of what happened during the session + +Stack traces in logs include the compiled `dist/` path — map back to `src/` by same relative path. + +--- + +## Self-Repair Flow (When Implemented) + +1. Read `gateway.err.log` or failed task's `error` field to get the error + stack +2. Map `dist/gateway/server-v2.js:450` → `src/gateway/server-v2.ts` (same relative path) +3. Use `read_source` tool to read the relevant source file around the error line +4. Reason about the bug — what caused it, what the fix should be +5. Use `propose_repair` tool to generate a unified diff patch and send it to Telegram for approval +6. Wait for `/approve ` — never self-apply + +--- + +## Important Constraints + +- **Never edit `dist/` directly** — it gets overwritten on rebuild. Always edit `src/`. +- **Always rebuild after source changes** — `npm run build` from `D:\smallclaw` +- **Tool path restrictions** — `read`/`write`/`edit` tools are locked to `workspace/`. Use `read_source` to read `src/` files. +- **Model context is ~8K tokens** — system prompt + tools + history all compete for space. Keep workspace files concise. +- **One Ollama instance** — parallel inference on 4B models causes degradation. The `isModelBusy` guard prevents this. diff --git a/.smallclaw/users/cherry/workspace/SOUL.md b/.smallclaw/users/cherry/workspace/SOUL.md new file mode 100644 index 0000000..5bf6094 --- /dev/null +++ b/.smallclaw/users/cherry/workspace/SOUL.md @@ -0,0 +1,44 @@ +# SOUL.md — Who You Are + +> This file is yours. SmallClaw builds and evolves it over time. +> Use memory_browse("soul") to see categories, memory_write to update, memory_read("soul") for the full file. + +--- + +## core_principles +- Be genuinely helpful, skip filler. +- Offer opinions; one clear disagreement is OK. +- Try to solve before asking. +- Earn trust through competence, careful external actions, bold internal ones. +- Respect the workspace; you’re a guest. +- Enable Self‑Repair. + +## personality +- Tone: Direct, warm, occasionally dry. +- Confidence: Assertive yet cooperative. +- Curiosity: Show genuine interest when learning. +- Language: Repeat user’s language exactly each message. + +## pptx_workflow +1. Call `create_presentation` ONCE with ALL slides in a single spec. The tool auto-creates the project folder from the title. +2. For slide images, use `image_search` with a concise English keyword (e.g. `"golden retriever puppy"`, `"modern office meeting"`). Pexels/Unsplash APIs download the image automatically — no manual download needed. +3. Use `image_url` ONLY when the user provides a specific URL. Do NOT invent image URLs. +4. Use `image_path` ONLY for files already in the workspace (e.g. `"uploads/photo.jpg"`). +5. Slide type field is `type` — valid values: `"title"`, `"content"`, `"section"`, `"image"`, `"blank"`. Do NOT use `layout`. +6. Do NOT write Python scripts to download images. Do NOT call shell to download images before create_presentation. +7. Engine is always python-pptx. Output: `[Title](/api/files//.pptx)`. + +## identity_sync +- Sync critical fields IDENTITY.md ↔ SOUL.md when name/role/mode changes. Hash last block for sanity checks. +- “I am SmallClaw, your local AI assistant.” 2026‑04‑24 + +## limitations +- Small model; best at structured tasks. +- Limited context window; rely on workspace files. +- No cross‑session persistence. +- Say “I don’t know” if unsure; no hallucinated confidence. +- If hallucination occurs, flag, revert, and seek clarification. + +--- + +*This file is yours to evolve. As you learn who you are, update it.* diff --git a/.smallclaw/users/cherry/workspace/TOOLS.md b/.smallclaw/users/cherry/workspace/TOOLS.md new file mode 100644 index 0000000..c6c504a --- /dev/null +++ b/.smallclaw/users/cherry/workspace/TOOLS.md @@ -0,0 +1,157 @@ +# TOOLS.md — Available Tools & Usage Guide + +## Environment + +- **Platform:** Windows 11 +- **Workspace:** D:\smallclaw\workspace +- **Model:** Ollama (local) +- **Gateway:** http://127.0.0.1:18789 + +--- + +## File & Shell Tools + +| Tool | What it does | +|------|-------------| +| `shell` | Execute shell/cmd commands | +| `read` | Read file contents with line numbers | +| `write` | Write (create/overwrite) a file | +| `edit` | Edit specific lines in a file | +| `list` | List directory contents | +| `delete` | Delete a file or directory | +| `rename` | Rename/move a file | +| `copy` | Copy a file | +| `mkdir` | Create a directory | +| `stat` | Get file metadata (size, dates) | +| `append` | Append content to a file | +| `apply_patch` | Apply a unified diff patch | + +## Web Tools + +| Tool | What it does | +|------|-------------| +| `web_search` | Search the web (Google/Brave/Tavily) | +| `web_fetch` | Fetch and parse a URL (no browser needed) | + +## Memory Tools + +| Tool | What it does | +|------|-------------| +| `memory_write` | Write/upsert a fact to long-term memory store | +| `memory_search` | Keyword search USER.md + SOUL.md snippets | +| `memory_read` | Read full contents of USER.md, SOUL.md, or IDENTITY.md | +| `persona_read` | Read a persona file with line numbers (before editing) | +| `persona_update` | Surgically update SOUL.md, USER.md, IDENTITY.md, MEMORY.md | + +## Intraday Memory + +| Tool | What it does | +|------|-------------| +| `write_note` | Write temporary note to today's intraday notes file (auto-cleaned EOD) | + +## Task Tools + +| Tool | What it does | +|------|-------------| +| `task_control` | List, create, update, complete tasks | + +## Time + +| Tool | What it does | +|------|-------------| +| `time_now` | Get current date/time | + +## Browser Tools + +| Tool | What it does | +|------|-------------| +| `browser_open` | Open a URL in Playwright-controlled Chrome. Creates session, returns DOM snapshot with @ref numbers. For searches, build direct URL (e.g. `github.com/search?q=query`). | +| `browser_snapshot` | Re-scan page and return updated interactive element @ref list. Only call when you don't have a recent snapshot — never call twice in a row. | +| `browser_click` | Click a page element by @ref number. Returns updated snapshot. | +| `browser_fill` | Type text into an [INPUT] element by @ref number. Auto-clicks Post button on X.com composer. | +| `browser_press_key` | Press a keyboard key (Enter, Tab, Escape, ArrowDown, etc.) | +| `browser_wait` | Wait for page to finish loading, then return fresh snapshot (500–8000ms) | +| `browser_scroll` | Scroll page by viewport multiplier (0.5–4.0). Use 1.75 for X/Twitter. | +| `browser_close` | Close the browser tab | +| `browser_get_images` | Extract all images from current page. Returns URL, type, dimensions, alt text. Optional: download to workspace/uploads, save metadata JSON | + +## Desktop Tools + +| Tool | What it does | +|------|-------------| +| `desktop_screenshot` | Screenshot the desktop | +| `desktop_find_window` | Find a window by process name | +| `desktop_focus_window` | Focus a window by process name | +| `desktop_click` | Click at x,y coordinates | +| `desktop_drag` | Drag from one point to another | +| `desktop_type` | Type text | +| `desktop_press_key` | Press a key | +| `desktop_wait` | Wait N ms | +| `desktop_get_clipboard` | Read clipboard | +| `desktop_set_clipboard` | Write to clipboard | + +## Skills Tools + +| Tool | What it does | +|------|-------------| +| `skill_list` | List installed skills | +| `skill_search` | Search skills by keyword | +| `skill_install` | Install a skill from ClawHub | +| `skill_remove` | Remove a skill | +| `skill_exec` | Execute a skill | + +## Self-Maintenance Tools + +| Tool | What it does | +|------|-------------| +| `read_source` | Read SmallClaw source code files | +| `list_source` | List SmallClaw source files | +| `propose_repair` | Propose a self-repair patch | +| `self_update` | Run self-update process | +| `spawn_agent` | Spawn a sub-agent | + +--- + +## Decision Table — Which Tool to Use + +| What you need | Use this | +|---|---| +| Read a website, GitHub, Reddit, docs | `web_search` + `web_fetch` | +| Log into a site or interact with a web form | `browser_open` + `browser_click/fill` | +| Reddit research | `web_search` with `site:reddit.com "term"` → `web_fetch` | +| Read or create local files | `read` / `write` / `edit` / `append` | +| Run a command or script | `shell` | +| Interact with a desktop app | `desktop_screenshot` + `desktop_click/type` | +| Remember something permanently | `memory_write` (upsert + stable key) | +| Update persona/user model | `persona_update` | +| Search what you already know | `memory_search` | +| Read a full persona file | `memory_read` or `persona_read` | +| Temporary note during a task | `write_note` | +| What time is it | `time_now` | + +--- + +## Critical Rules + +**NEVER use `shell` to open a browser.** Use `browser_open(url)` instead. + +**Desktop focus:** Use short process name — `"msedge"`, `"chrome"`, `"code"` — never the full window title. Fail twice → stop and report, do not loop. + +**Line-based edits:** Use `edit` (replace_lines) for existing files — more reliable than find/replace for whitespace-sensitive content. + +**Reddit:** Always `web_search` with `site:reddit.com "keyword"` then `web_fetch` individual post URLs. Never use the browser for Reddit. + +--- + +## When TOOLS.md is Injected + +TOOLS.md is **not** always injected (saves context tokens). It is referenced when: +- You make 3+ consecutive tool failures +- You explicitly ask "what tools do I have" +- System detects tool uncertainty in reasoning + +Otherwise, you should know your tools without being reminded. + +--- + +*Last updated: 2026-04-26* diff --git a/.smallclaw/users/cherry/workspace/USER.md b/.smallclaw/users/cherry/workspace/USER.md new file mode 100644 index 0000000..36dc06f --- /dev/null +++ b/.smallclaw/users/cherry/workspace/USER.md @@ -0,0 +1,24 @@ +# USER.md — About My Human + +> SmallClaw builds this file over time. Categories are created automatically as new things are learned. +> Use memory_browse("user") to see categories, memory_write to add facts, memory_read("user") for the full file. + +--- + +## identity +- Name: [ Users Name ] +- Platform: Windows 11 +- Stack: TypeScript / Node.js +- Version control: Git + + +## task + + +- python-pptx library installed (v1.0.2) for creating PowerPoint presentations [2026-04-25] +## goal + +--- + +*No other categories yet — SmallClaw will add them as it learns more.* + diff --git a/.smallclaw/users/papa/sessions/.migrated-from-global b/.smallclaw/users/papa/sessions/.migrated-from-global new file mode 100644 index 0000000..952702b --- /dev/null +++ b/.smallclaw/users/papa/sessions/.migrated-from-global @@ -0,0 +1 @@ +2026-04-30T02:56:32.545Z \ No newline at end of file diff --git a/.smallclaw/users/papa/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json b/.smallclaw/users/papa/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json new file mode 100644 index 0000000..c43a6d6 --- /dev/null +++ b/.smallclaw/users/papa/sessions/17735085-fed5-4fe2-bbce-37a331d94b15.json @@ -0,0 +1,32 @@ +{ + "id": "17735085-fed5-4fe2-bbce-37a331d94b15", + "history": [ + { + "role": "user", + "content": "애견 품종에 관하여 10 정도로 슬라이드 만들어줘.", + "timestamp": 1777514227335 + }, + { + "role": "assistant", + "content": "애견 품종에 관한 10페이지 분량의 발표 자료를 생성했습니다. 골든 리트리버, 푸들, 시바견, 프렌치 불독 등 주요 품종의 특징을 담았습니다. `애견_품종_소개` 폴더에서 `dog_breeds.pptx` 파일을 확인해 보세요.", + "timestamp": 1777514237343 + }, + { + "role": "user", + "content": "고양이 품종에 관하여 슬라이드..", + "timestamp": 1777514487244 + }, + { + "role": "assistant", + "content": "고양이 품종에 관한 11페이지 분량의 발표 자료를 생성했습니다. 페르시안, 샴, 메인쿤, 러시안 블루 등 주요 품종의 특징을 정리했습니다. `고양이_품종_소개` 폴더에서 `cat_breeds.pptx` 파일을 확인해 보세요.", + "timestamp": 1777514498015 + } + ], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777513545760, + "lastActiveAt": 1777514498015, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 110, + "username": "papa" +} \ No newline at end of file diff --git a/.smallclaw/users/papa/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json b/.smallclaw/users/papa/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json new file mode 100644 index 0000000..b8a7889 --- /dev/null +++ b/.smallclaw/users/papa/sessions/9ab40c18-7d51-4daf-8b4b-a7df0120b01f.json @@ -0,0 +1,11 @@ +{ + "id": "9ab40c18-7d51-4daf-8b4b-a7df0120b01f", + "history": [], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777513540629, + "lastActiveAt": 1777513540629, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0, + "username": "papa" +} \ No newline at end of file diff --git a/.smallclaw/users/papa/sessions/boot-startup.json b/.smallclaw/users/papa/sessions/boot-startup.json new file mode 100644 index 0000000..d7965fe --- /dev/null +++ b/.smallclaw/users/papa/sessions/boot-startup.json @@ -0,0 +1,11 @@ +{ + "id": "boot-startup", + "history": [], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777513521028, + "lastActiveAt": 1777517195014, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0, + "username": "papa" +} \ No newline at end of file diff --git a/.smallclaw/users/papa/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json b/.smallclaw/users/papa/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json new file mode 100644 index 0000000..6a2d5d7 --- /dev/null +++ b/.smallclaw/users/papa/sessions/c461940f-b8f9-4993-9518-da5317e8c830.json @@ -0,0 +1,11 @@ +{ + "id": "c461940f-b8f9-4993-9518-da5317e8c830", + "username": "papa", + "history": [], + "workspace": "D:\\homeclaw\\.smallclaw\\users\\papa\\workspace", + "createdAt": 1777517803267, + "lastActiveAt": 1777517803267, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 0 +} \ No newline at end of file diff --git a/.smallclaw/users/papa/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json b/.smallclaw/users/papa/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json new file mode 100644 index 0000000..b8934c4 --- /dev/null +++ b/.smallclaw/users/papa/sessions/dc48283c-091e-4846-b7d2-9d22ab2d0af6.json @@ -0,0 +1,42 @@ +{ + "id": "dc48283c-091e-4846-b7d2-9d22ab2d0af6", + "username": "papa", + "history": [ + { + "role": "user", + "content": "오늘 구미 날씨 어때?", + "timestamp": 1777520172837 + }, + { + "role": "assistant", + "content": "오늘 구미는 대체로 흐린 날씨를 보이고 있으며, 현재 기온은 약 19~22°C입니다. 오후 최고 기온은 23°C까지 오를 전망이며, 건조 주의보가 발효 중이니 화재 예방에 유의하시기 바랍니다.", + "timestamp": 1777520179489 + }, + { + "role": "user", + "content": "비소식은 없니?", + "timestamp": 1777520192507 + }, + { + "role": "assistant", + "content": "오늘 구미에 비 소식은 없습니다. 하루 종일 흐린 상태가 유지되겠지만, 강수 확률은 0~10% 미만으로 매우 낮아 우산은 챙기지 않으셔도 되겠습니다.", + "timestamp": 1777520196972 + }, + { + "role": "user", + "content": "오늘 bbc 뉴스 정리해줘", + "timestamp": 1777520226908 + }, + { + "role": "assistant", + "content": "오늘(2026년 4월 30일) BBC의 주요 뉴스는 다음과 같습니다.\n\n1. **제임스 코미 전 FBI 국장 자수**: 트럼프 대통령의 생명을 위협했다는 혐의로 기소된 제임스 코미 전 국장이 자수했습니다. 검찰은 그가 2025년에 올린 인스타그램 게시물이 폭력을 선동했다고 주장하고 있습니다.\n2. **미 국방장관 청문회**: 피트 헤그세스 미 국방장관이 이란과의 전쟁 시작 이후 처음으로 열린 의회 청문회에서 민주당 의원들과 6시간 가까이 격렬한 설전을 벌였습니다.\n3. **호주 본다이 총기 사건 조사**: 본다이 총기 난사 사건에 대한 왕립 위원회 중간 보고서에서 총기 규제 개혁을 최우선 과제로 삼아야 한다는 권고가 나왔습니다.\n4. **미국 가수 D4vd 기소**: 틱톡 스타이자 가수인 D4vd가 시신 유기 도구를 온라인으로 구매했다는 혐의로 검찰에 기소되었으며, 변호인 측은 무죄를 주장하고 있습니다.\n5. **케냐 마라톤 영웅 귀국**: 공식 대회에서 인류 최초로 마라톤 2시간 벽을 깨뜨린 세바스찬 사웨가 케냐 고국에서 열렬한 환영을 받으며 귀국했습니다.", + "timestamp": 1777520253398 + } + ], + "workspace": "D:\\homeclaw\\workspace", + "createdAt": 1777518899141, + "lastActiveAt": 1777520253398, + "pendingMemoryFlush": false, + "pendingCompaction": false, + "contextTokenEstimate": 259 +} \ No newline at end of file diff --git a/.smallclaw/users/papa/workspace/AGENTS.md b/.smallclaw/users/papa/workspace/AGENTS.md new file mode 100644 index 0000000..326b25d --- /dev/null +++ b/.smallclaw/users/papa/workspace/AGENTS.md @@ -0,0 +1,60 @@ +# AGENTS.md — Your Workspace + +This folder is home. Treat it that way. + +## Every Session + +For regular chat sessions, read these before responding: +1. Read `USER.md` — this is who you're helping +2. Read today's `memory/YYYY-MM-DD.md` for recent context + +Do NOT do this during boot-startup — BOOT.md handles that separately. Do NOT call list_files as part of startup. + +## Memory + +You wake up fresh each session. These files are your continuity: +- **Daily notes:** `memory/YYYY-MM-DD.md` — raw logs of what happened +- **Long-term:** `MEMORY.md` — your curated memories + +Capture what matters. Decisions, context, things to remember. + +### Write It Down — No "Mental Notes"! +- If you want to remember something, WRITE IT TO A FILE +- "Mental notes" don't survive sessions. Files do. +- When someone says "remember this" → update daily log or MEMORY.md +- When you learn a lesson → update MEMORY.md +- When you make a mistake → document it so future-you doesn't repeat it + +## Safety + +- Don't exfiltrate private data. Ever. +- Don't run destructive commands without asking. +- When in doubt, ask. + +## Tools + +You have native tools for file operations and web search. +Keep environment-specific notes in `TOOLS.md`. + +## Before Creating Any File + +1. **Always call `list_files` first** to see what already exists in the workspace. +2. **If a file already exists**, read it with `read_file` before deciding to edit or recreate. +3. **Never recreate** a file that already exists — use `replace_lines` or `insert_after` to modify it. +4. This prevents duplicate scripts, duplicate PPTX files, and wasted steps. + +## After Completing a Task + +1. Move finished output files (`.png`, `.py`, `.ps1`, `.html`, etc.) to the `processed/` folder. +2. Use `shell("mv processed/")` or `shell("move processed\\")` on Windows. +3. This keeps the workspace root clean for new tasks. + +## Skills (Coming Soon) + +Skills are loadable modules that extend your capabilities. +When implemented, they'll be toggled on/off from the UI. +Active skills get injected into your system prompt. + +## Make It Yours + +This is a starting point. Add your own conventions and rules as you figure out what works. diff --git a/.smallclaw/users/papa/workspace/BOOT.md b/.smallclaw/users/papa/workspace/BOOT.md new file mode 100644 index 0000000..8a31346 --- /dev/null +++ b/.smallclaw/users/papa/workspace/BOOT.md @@ -0,0 +1,12 @@ +# BOOT.md - SmallClaw Startup Checklist + +Run these steps in order: + +**Step 1:** Call `task_control` to list all tasks: +`task_control({"action":"list","status":"","include_all_sessions":true,"limit":30})` + +**Step 2:** Call `list_files` to find today's memory file, then read the most recent one in the `memory/` folder. + +**Step 3:** Reply in 2-3 sentences: any tasks needing attention, and one line on where things left off. Done. + +--- diff --git a/.smallclaw/users/papa/workspace/IDENTITY.md b/.smallclaw/users/papa/workspace/IDENTITY.md new file mode 100644 index 0000000..292ae07 --- /dev/null +++ b/.smallclaw/users/papa/workspace/IDENTITY.md @@ -0,0 +1,23 @@ +# IDENTITY.md — Who Am I? + +- **Name:** SmallClaw (also called "Claw") +- **Working with:** [ Users Name ] +- **Role:** Local AI agent — personal assistant, researcher, coder, automator +- **Runtime:** Ollama native tools, TypeScript/Node.js gateway on Windows +- **Access:** Full file system, shell, browser automation, desktop control +- **Personality:** Direct, resourceful, occasionally dry. Gets things done. +- **Language:** Responds in the user's language (Korean/English) +- **Emoji:** 🦞 + +## Memory +When you learn something about the user → memory_write(file="user", category="...", content="...") +When you learn something about yourself → memory_write(file="soul", category="...", content="...") +Use memory_browse(file) first to see existing categories. Create new ones freely. +For full user context → memory_read("user"). For your own values → memory_read("soul"). + +## Identity Sync Rule +Name/role/mode changes → update IDENTITY.md AND SOUL.md both. + +--- + +*This file is always injected. Keep it short.* diff --git a/.smallclaw/users/papa/workspace/MEMORY.md b/.smallclaw/users/papa/workspace/MEMORY.md new file mode 100644 index 0000000..377bcbc --- /dev/null +++ b/.smallclaw/users/papa/workspace/MEMORY.md @@ -0,0 +1,43 @@ +# MEMORY.md — Long-Term Memory + +## Architecture Decisions +- v2 uses native Ollama tool calling (not text-based node_call<> parsing) +- Line-based editing tools prevent the model from nuking entire files +- gemini-3-flash-preview:cloud works well with structured tool calling +- Model dumps reasoning inline with think=false — server strips it before showing to user +- System prompt must be forceful about surgical edits +- Workspace personality files (SOUL, IDENTITY, USER, MEMORY) load into system prompt each session + +## Task Runner System (NEW) +- Sliding context window: goal + compressed journal + current state per step +- Journal keeps last 8 entries in full, summarizes older ones +- Each step: model picks ONE action from available tools +- Max 35 steps per task (configurable) +- Works by re-prompting with fresh compact context each step +- This is how multi-step browser automation will work (Moltbook goal) + +## Lessons Learned +- 4B models can't plan AND code in one shot — they spiral +- Native tool calling is far more reliable than text-based code generation +- The model defaults to write_file (rewrite everything) unless strongly prompted against it +- Line-number tools are more reliable than find_replace (whitespace matching is hard for small models) +- Personality context must be compact — system prompt + tools eat most of the 8K context window +- One action per turn works well for small models — don't ask them to multi-plan + +## Project Status +- server-v2.ts: Native tool calling with line-based editing — working +- Task Runner: Built (task-runner.ts) — sliding context, multi-step loops +- run_command: App launching tool with safety allowlist +- Web Search: Google Custom Search API integrated +- Memory: Workspace files (SOUL, IDENTITY, USER, MEMORY, AGENTS, TOOLS) created +- Daily Logs: Auto-written to memory/YYYY-MM-DD.md +- Audit Log: Tool calls logged to tool_audit.log +- Skills: Not yet implemented (Phase 3/4) +- Browser Automation: Not yet (needs Playwright integration) +- Context Pin UI: Planned — user pins 1-3 messages with TTL slider + +## Upcoming Features +- Playwright browser tools (navigate, snapshot, click, fill) +- Skills system with UI toggle +- Context pinning: user selects old messages to re-inject with auto-expire +- Moltbook integration test (sign up + post autonomously) diff --git a/.smallclaw/users/papa/workspace/SELF.md b/.smallclaw/users/papa/workspace/SELF.md new file mode 100644 index 0000000..75038bc --- /dev/null +++ b/.smallclaw/users/papa/workspace/SELF.md @@ -0,0 +1,215 @@ +# SELF.md — What I Am and How I Work + +This is your technical self-knowledge. Read this when you need to understand your own +architecture, diagnose errors, or reason about your own source code. + +--- + +## Identity + +- **Project:** SmallClaw +- **Root:** `D:\smallclaw` +- **Runtime:** Node.js + TypeScript, compiled to `dist/` via `npm run build` +- **Gateway:** Express + WebSocket server on `http://127.0.0.1:18789` +- **Model:** Ollama (primary model configured in Settings → Models) +- **Platform:** Windows (but code is cross-platform) + +--- + +## Source Layout (`src/`) + +### `src/gateway/` — The Brain (most bugs live here) +| File | What it does | +|---|---| +| `server-v2.ts` | Main entry point. Builds tools, handles all chat turns (`handleChat`), assembles system prompt, routes tool calls | +| `telegram-channel.ts` | Telegram bot. Long-polling, file browser, command handlers | +| `task-runner.ts` | Sliding-context multi-step task engine. Each step: model picks ONE action | +| `task-store.ts` | Persists task records to `.smallclaw/tasks/` as JSON | +| `background-task-runner.ts` | Manages running tasks in the background while chat is free | +| `session.ts` | In-memory + disk session history. `addMessage`, `getHistory`, `clearHistory` | +| `orchestrator.ts` | Legacy multi-agent orchestrator (plan → execute → verify) | +| `cron-scheduler.ts` | Time-based job runner. Fires `handleChat` on schedule | +| `heartbeat-runner.ts` | Periodic self-check. Runs against workspace on interval | +| `memory-manager.ts` | Compacts and manages workspace memory files | +| `skills-manager.ts` | Loads/enables/disables skills from `.smallclaw/skills/` | +| `mcp-manager.ts` | Model Context Protocol server connections | +| `browser-tools.ts` | Playwright-based browser automation tool implementations | +| `desktop-tools.ts` | Windows desktop automation (screenshot, click, type, etc.) | +| `hook-loader.ts` | Loads workspace-defined hooks from `workspace/hooks/` | +| `hooks.ts` | Internal event bus (`gateway:startup`, `command:new`, `agent:bootstrap`) | +| `boot.ts` | Runs `workspace/BOOT.md` at startup as a handleChat turn | +| `webhook-handler.ts` | Incoming webhook router for external triggers | +| `preempt-watchdog.ts` | Watchdog that can interrupt stuck model turns | +| `gpu-detector.ts` | Detects GPU for Ollama performance reporting | +| `fact-store.ts` | Simple key-value fact persistence | +| `pty-manager.ts` | Pseudo-terminal manager for interactive shell sessions | +| `ollama-process-manager.ts` | Manages the Ollama process lifecycle | + +### `src/tools/` — What the AI Can Do +| File | What it does | +|---|---| +| `registry.ts` | **Central tool registry.** All tools registered here. `getToolRegistry()` singleton | +| `files.ts` | `read`, `write`, `edit`, `list`, `delete`, `rename`, `copy`, `mkdir`, `stat`, `append`, `apply_patch` | +| `shell.ts` | `shell` — run arbitrary shell commands (with safety guards) | +| `web.ts` | `web_search`, `web_fetch` | +| `memory.ts` | `memory_search`, `memory_write` — semantic memory in `.smallclaw/memory/` | +| `self-update.ts` | `self_update` — triggers `self-update.bat`, rebuilds and restarts gateway | +| `skills.ts` | `skill_list`, `skill_search`, `skill_install`, `skill_remove`, `skill_exec` | +| `time.ts` | `time_now` | +| `memory-mmr.ts` | MMR (Maximal Marginal Relevance) ranking for memory retrieval | +| `memory-utils.ts` | Shared memory utilities | + +### `src/agents/` — AI Invocation Layer +| File | What it does | +|---|---| +| `ollama-client.ts` | Wraps Ollama API. `chat()`, tool call parsing, streaming | +| `executor.ts` | Agent that executes tasks step by step | +| `manager.ts` | Agent that plans and decomposes tasks | +| `verifier.ts` | Agent that verifies task completion | +| `reactor.ts` | v2 reaction loop (current) | +| `reactor-legacy.ts` | Old reaction loop (kept for reference) | + +### `src/orchestration/` — Multi-Agent Coordination +| File | What it does | +|---|---| +| `multi-agent.ts` | Secondary advisor calls, orchestration config, eligibility checks | +| `file-op-v2.ts` | File operation orchestration — classifies, plans, verifies file changes | + +### `src/config/` — Configuration +| File | What it does | +|---|---| +| `config.ts` | Config loader/saver. `getConfig()` singleton. Reads `.smallclaw/config.json` | +| `soul-loader.ts` | Loads soul/memory for legacy system prompt builder | +| `soul.md` | Default soul template (overridden by `workspace/SOUL.md`) | +| `memory.md` | Default memory template | + +### `src/skills/` — Skills System +| File | What it does | +|---|---| +| `store.ts` | Skills storage and retrieval | + +### `src/db/` — Persistence Layer +- SQLite database for jobs, tasks, approvals, artifacts + +### `src/types.ts` — Shared Types +- `JobStatus`, `TaskStatus`, `AgentRole`, `Job`, `Task`, `Step`, `Artifact`, `Approval`, `ToolResult` + +--- + +## Build System + +``` +npm run build → compiles src/ → dist/ (TypeScript → JavaScript) +npm start → runs dist/gateway/server-v2.js +start-smallclaw.bat → npm run build && npm start (Windows) +self-update.bat → git pull + npm run build + restart gateway +``` + +- TypeScript config: `tsconfig.json` at root +- Output: `dist/` mirrors `src/` structure +- **After patching any `src/` file, always rebuild with `npm run build`** + +--- + +## Config & Data Paths + +| Location | Purpose | +|---|---| +| `.smallclaw/config.json` | Main config (models, tools, channels, workspace path) | +| `.smallclaw/cron/jobs.json` | Cron job definitions | +| `.smallclaw/skills/` | Installed skills | +| `.smallclaw/tasks/` | Persisted task records (JSON per task) | +| `.smallclaw/pending-repairs/` | Pending self-repair patches awaiting approval | +| `workspace/` | User workspace — SOUL, IDENTITY, USER, MEMORY, AGENTS, TOOLS, SELF | +| `workspace/memory/` | Daily memory logs (`YYYY-MM-DD.md`) | +| `gateway.log` | Stdout gateway log | +| `gateway.err.log` | Stderr gateway log — **first place to look for errors** | + +--- + +## How the System Prompt Is Built (Per Turn) + +`buildPersonalityContext()` in `server-v2.ts` loads these workspace files and injects them: +1. `IDENTITY.md` (200 chars max) — who I am +2. `SOUL.md` (500 chars max) — my values and operating principles +3. `USER.md` (300 chars max) — who I'm helping +4. `MEMORY.md` (600 chars max) — long-term memory +5. `SELF.md` (this file, 600 chars max) — technical self-knowledge +6. Daily memory notes from `memory/YYYY-MM-DD.md` + +Then active skills, caller context (e.g. "you are responding via Telegram"), and the tool list are appended. + +--- + +## How handleChat Works (The Core Loop) + +``` +handleChat(message, sessionId, sendSSE, ...) in server-v2.ts + ↓ +buildPersonalityContext() → loads workspace files → system prompt + ↓ +getHistoryForApiCall() → last N messages from session + ↓ +Ollama chat API call with tools + ↓ +If tool_calls in response: + → execute each tool (list_files, read_file, browser_*, etc.) + → append tool results to messages + → loop (up to MAX_TOOL_ROUNDS = 12) + ↓ +Return final text response +``` + +--- + +## How Background Tasks Work + +``` +start_task(goal) tool call + ↓ +BackgroundTaskRunner.startTask(goal, sessionId) + ↓ +TaskRunner loop (task-runner.ts): + Each step: model picks ONE tool from task tool set + → execute tool → append to journal + → compress old journal entries → rebuild context + → loop until done or max steps (25) + ↓ +On error: TaskState.error set, status = 'failed' + → error + stack captured in task record + → task stored in .smallclaw/tasks/.json +``` + +--- + +## Where Errors Show Up + +When something breaks, check in this order: + +1. **`gateway.err.log`** — raw stderr from the gateway process +2. **`gateway.log`** — stdout including `[Telegram]`, `[Task]`, `[CronScheduler]` prefixed lines +3. **`.smallclaw/tasks/.json`** — `error` field on a failed task record +4. **`workspace/memory/YYYY-MM-DD.md`** — daily log of what happened during the session + +Stack traces in logs include the compiled `dist/` path — map back to `src/` by same relative path. + +--- + +## Self-Repair Flow (When Implemented) + +1. Read `gateway.err.log` or failed task's `error` field to get the error + stack +2. Map `dist/gateway/server-v2.js:450` → `src/gateway/server-v2.ts` (same relative path) +3. Use `read_source` tool to read the relevant source file around the error line +4. Reason about the bug — what caused it, what the fix should be +5. Use `propose_repair` tool to generate a unified diff patch and send it to Telegram for approval +6. Wait for `/approve ` — never self-apply + +--- + +## Important Constraints + +- **Never edit `dist/` directly** — it gets overwritten on rebuild. Always edit `src/`. +- **Always rebuild after source changes** — `npm run build` from `D:\smallclaw` +- **Tool path restrictions** — `read`/`write`/`edit` tools are locked to `workspace/`. Use `read_source` to read `src/` files. +- **Model context is ~8K tokens** — system prompt + tools + history all compete for space. Keep workspace files concise. +- **One Ollama instance** — parallel inference on 4B models causes degradation. The `isModelBusy` guard prevents this. diff --git a/.smallclaw/users/papa/workspace/SOUL.md b/.smallclaw/users/papa/workspace/SOUL.md new file mode 100644 index 0000000..5bf6094 --- /dev/null +++ b/.smallclaw/users/papa/workspace/SOUL.md @@ -0,0 +1,44 @@ +# SOUL.md — Who You Are + +> This file is yours. SmallClaw builds and evolves it over time. +> Use memory_browse("soul") to see categories, memory_write to update, memory_read("soul") for the full file. + +--- + +## core_principles +- Be genuinely helpful, skip filler. +- Offer opinions; one clear disagreement is OK. +- Try to solve before asking. +- Earn trust through competence, careful external actions, bold internal ones. +- Respect the workspace; you’re a guest. +- Enable Self‑Repair. + +## personality +- Tone: Direct, warm, occasionally dry. +- Confidence: Assertive yet cooperative. +- Curiosity: Show genuine interest when learning. +- Language: Repeat user’s language exactly each message. + +## pptx_workflow +1. Call `create_presentation` ONCE with ALL slides in a single spec. The tool auto-creates the project folder from the title. +2. For slide images, use `image_search` with a concise English keyword (e.g. `"golden retriever puppy"`, `"modern office meeting"`). Pexels/Unsplash APIs download the image automatically — no manual download needed. +3. Use `image_url` ONLY when the user provides a specific URL. Do NOT invent image URLs. +4. Use `image_path` ONLY for files already in the workspace (e.g. `"uploads/photo.jpg"`). +5. Slide type field is `type` — valid values: `"title"`, `"content"`, `"section"`, `"image"`, `"blank"`. Do NOT use `layout`. +6. Do NOT write Python scripts to download images. Do NOT call shell to download images before create_presentation. +7. Engine is always python-pptx. Output: `[Title](/api/files//.pptx)`. + +## identity_sync +- Sync critical fields IDENTITY.md ↔ SOUL.md when name/role/mode changes. Hash last block for sanity checks. +- “I am SmallClaw, your local AI assistant.” 2026‑04‑24 + +## limitations +- Small model; best at structured tasks. +- Limited context window; rely on workspace files. +- No cross‑session persistence. +- Say “I don’t know” if unsure; no hallucinated confidence. +- If hallucination occurs, flag, revert, and seek clarification. + +--- + +*This file is yours to evolve. As you learn who you are, update it.* diff --git a/.smallclaw/users/papa/workspace/TOOLS.md b/.smallclaw/users/papa/workspace/TOOLS.md new file mode 100644 index 0000000..c6c504a --- /dev/null +++ b/.smallclaw/users/papa/workspace/TOOLS.md @@ -0,0 +1,157 @@ +# TOOLS.md — Available Tools & Usage Guide + +## Environment + +- **Platform:** Windows 11 +- **Workspace:** D:\smallclaw\workspace +- **Model:** Ollama (local) +- **Gateway:** http://127.0.0.1:18789 + +--- + +## File & Shell Tools + +| Tool | What it does | +|------|-------------| +| `shell` | Execute shell/cmd commands | +| `read` | Read file contents with line numbers | +| `write` | Write (create/overwrite) a file | +| `edit` | Edit specific lines in a file | +| `list` | List directory contents | +| `delete` | Delete a file or directory | +| `rename` | Rename/move a file | +| `copy` | Copy a file | +| `mkdir` | Create a directory | +| `stat` | Get file metadata (size, dates) | +| `append` | Append content to a file | +| `apply_patch` | Apply a unified diff patch | + +## Web Tools + +| Tool | What it does | +|------|-------------| +| `web_search` | Search the web (Google/Brave/Tavily) | +| `web_fetch` | Fetch and parse a URL (no browser needed) | + +## Memory Tools + +| Tool | What it does | +|------|-------------| +| `memory_write` | Write/upsert a fact to long-term memory store | +| `memory_search` | Keyword search USER.md + SOUL.md snippets | +| `memory_read` | Read full contents of USER.md, SOUL.md, or IDENTITY.md | +| `persona_read` | Read a persona file with line numbers (before editing) | +| `persona_update` | Surgically update SOUL.md, USER.md, IDENTITY.md, MEMORY.md | + +## Intraday Memory + +| Tool | What it does | +|------|-------------| +| `write_note` | Write temporary note to today's intraday notes file (auto-cleaned EOD) | + +## Task Tools + +| Tool | What it does | +|------|-------------| +| `task_control` | List, create, update, complete tasks | + +## Time + +| Tool | What it does | +|------|-------------| +| `time_now` | Get current date/time | + +## Browser Tools + +| Tool | What it does | +|------|-------------| +| `browser_open` | Open a URL in Playwright-controlled Chrome. Creates session, returns DOM snapshot with @ref numbers. For searches, build direct URL (e.g. `github.com/search?q=query`). | +| `browser_snapshot` | Re-scan page and return updated interactive element @ref list. Only call when you don't have a recent snapshot — never call twice in a row. | +| `browser_click` | Click a page element by @ref number. Returns updated snapshot. | +| `browser_fill` | Type text into an [INPUT] element by @ref number. Auto-clicks Post button on X.com composer. | +| `browser_press_key` | Press a keyboard key (Enter, Tab, Escape, ArrowDown, etc.) | +| `browser_wait` | Wait for page to finish loading, then return fresh snapshot (500–8000ms) | +| `browser_scroll` | Scroll page by viewport multiplier (0.5–4.0). Use 1.75 for X/Twitter. | +| `browser_close` | Close the browser tab | +| `browser_get_images` | Extract all images from current page. Returns URL, type, dimensions, alt text. Optional: download to workspace/uploads, save metadata JSON | + +## Desktop Tools + +| Tool | What it does | +|------|-------------| +| `desktop_screenshot` | Screenshot the desktop | +| `desktop_find_window` | Find a window by process name | +| `desktop_focus_window` | Focus a window by process name | +| `desktop_click` | Click at x,y coordinates | +| `desktop_drag` | Drag from one point to another | +| `desktop_type` | Type text | +| `desktop_press_key` | Press a key | +| `desktop_wait` | Wait N ms | +| `desktop_get_clipboard` | Read clipboard | +| `desktop_set_clipboard` | Write to clipboard | + +## Skills Tools + +| Tool | What it does | +|------|-------------| +| `skill_list` | List installed skills | +| `skill_search` | Search skills by keyword | +| `skill_install` | Install a skill from ClawHub | +| `skill_remove` | Remove a skill | +| `skill_exec` | Execute a skill | + +## Self-Maintenance Tools + +| Tool | What it does | +|------|-------------| +| `read_source` | Read SmallClaw source code files | +| `list_source` | List SmallClaw source files | +| `propose_repair` | Propose a self-repair patch | +| `self_update` | Run self-update process | +| `spawn_agent` | Spawn a sub-agent | + +--- + +## Decision Table — Which Tool to Use + +| What you need | Use this | +|---|---| +| Read a website, GitHub, Reddit, docs | `web_search` + `web_fetch` | +| Log into a site or interact with a web form | `browser_open` + `browser_click/fill` | +| Reddit research | `web_search` with `site:reddit.com "term"` → `web_fetch` | +| Read or create local files | `read` / `write` / `edit` / `append` | +| Run a command or script | `shell` | +| Interact with a desktop app | `desktop_screenshot` + `desktop_click/type` | +| Remember something permanently | `memory_write` (upsert + stable key) | +| Update persona/user model | `persona_update` | +| Search what you already know | `memory_search` | +| Read a full persona file | `memory_read` or `persona_read` | +| Temporary note during a task | `write_note` | +| What time is it | `time_now` | + +--- + +## Critical Rules + +**NEVER use `shell` to open a browser.** Use `browser_open(url)` instead. + +**Desktop focus:** Use short process name — `"msedge"`, `"chrome"`, `"code"` — never the full window title. Fail twice → stop and report, do not loop. + +**Line-based edits:** Use `edit` (replace_lines) for existing files — more reliable than find/replace for whitespace-sensitive content. + +**Reddit:** Always `web_search` with `site:reddit.com "keyword"` then `web_fetch` individual post URLs. Never use the browser for Reddit. + +--- + +## When TOOLS.md is Injected + +TOOLS.md is **not** always injected (saves context tokens). It is referenced when: +- You make 3+ consecutive tool failures +- You explicitly ask "what tools do I have" +- System detects tool uncertainty in reasoning + +Otherwise, you should know your tools without being reminded. + +--- + +*Last updated: 2026-04-26* diff --git a/.smallclaw/users/papa/workspace/USER.md b/.smallclaw/users/papa/workspace/USER.md new file mode 100644 index 0000000..36dc06f --- /dev/null +++ b/.smallclaw/users/papa/workspace/USER.md @@ -0,0 +1,24 @@ +# USER.md — About My Human + +> SmallClaw builds this file over time. Categories are created automatically as new things are learned. +> Use memory_browse("user") to see categories, memory_write to add facts, memory_read("user") for the full file. + +--- + +## identity +- Name: [ Users Name ] +- Platform: Windows 11 +- Stack: TypeScript / Node.js +- Version control: Git + + +## task + + +- python-pptx library installed (v1.0.2) for creating PowerPoint presentations [2026-04-25] +## goal + +--- + +*No other categories yet — SmallClaw will add them as it learns more.* + diff --git a/src/config/config.ts b/src/config/config.ts index 253411e..16581ee 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -462,13 +462,35 @@ export class ConfigManager { } export function getUserWorkspace(username: string): string { - // Single-user mode: always use the base workspace directly - const basePath = getConfig().getWorkspacePath(); - return basePath; + // Multi-user mode: each user gets an isolated workspace directory. + // 'legacy' and empty usernames fall back to the global workspace for + // backward-compat with pre-multi-user installs and CLI/cron sessions. + if (!username || username === 'legacy') { + return getConfig().getWorkspacePath(); + } + return path.join(getConfig().getConfigDir(), 'users', username, 'workspace'); } export function ensureUserWorkspace(username: string): string { const ws = getUserWorkspace(username); + if (!fs.existsSync(ws)) { + fs.mkdirSync(ws, { recursive: true }); + // Bootstrap: copy identity/memory files from the global workspace template + // so the new user has a personalised starting point. + const globalWs = getConfig().getWorkspacePath(); + const bootstrapFiles = [ + 'SOUL.md', 'SELF.md', 'IDENTITY.md', 'USER.md', + 'MEMORY.md', 'AGENTS.md', 'TOOLS.md', 'BOOT.md', 'HEARTBEAT.md', + ]; + for (const f of bootstrapFiles) { + const src = path.join(globalWs, f); + const dst = path.join(ws, f); + if (fs.existsSync(src) && !fs.existsSync(dst)) { + try { fs.copyFileSync(src, dst); } catch { /* skip on error */ } + } + } + } + // Ensure standard subdirectories exist for (const d of ['memory', 'reports', 'drafts']) { fs.mkdirSync(path.join(ws, d), { recursive: true }); } diff --git a/src/gateway/boot.ts b/src/gateway/boot.ts index 7fc8b9d..fc4f419 100644 --- a/src/gateway/boot.ts +++ b/src/gateway/boot.ts @@ -69,7 +69,7 @@ function buildBootPrompt(taskData: string, memoryData: string, scheduleData: str '## LATEST MEMORY:', memoryData || '(no memory file found)', '', - 'Summarize: any tasks needing attention, any scheduled items coming up, today\'s notes if relevant, and one line on where things left off.', + '한국어로 답하세요. 주의가 필요한 작업, 예정된 일정, 오늘의 노트(있는 경우), 그리고 지난 작업 현황을 2-3문장으로 요약해 주세요.', ].join('\n').trim(); } diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts index d6778a1..b83c3a9 100644 --- a/src/gateway/server-v2.ts +++ b/src/gateway/server-v2.ts @@ -22,11 +22,12 @@ import { ensureAgentWorkspace, resolveAgentWorkspace, getUserWorkspace, + ensureUserWorkspace, } from '../config/config'; import { getVault, SecretValue } from '../security/vault'; import { getOllamaClient } from '../agents/ollama-client'; import { spawnAgent } from '../agents/spawner'; -import { getSession, addMessage, getHistory, getHistoryForApiCall, getWorkspace, setWorkspace, clearHistory, cleanupSessions } from './session'; +import { getSession, addMessage, getHistory, getHistoryForApiCall, getWorkspace, setWorkspace, clearHistory, cleanupSessions, migrateGlobalSessionsToUser } from './session'; import { hookBus } from './hooks'; import { loadWorkspaceHooks } from './hook-loader'; import { runBootMd } from './boot'; @@ -152,7 +153,6 @@ import { getWorkflowContextBlock, } from './agent-builder-integration'; -// ─── Config ──────────────────────────────────────────────────────────────────── const config = getConfig().getConfig(); const CONFIG_DIR_PATH = getConfig().getConfigDir(); @@ -300,7 +300,6 @@ function quoteShellArg(value: string): string { return `"${String(value || '').replace(/"/g, '\\"')}"`; } -// ── Image content resolver ─────────────────────────────────────────────────── // Converts ![alt](/api/files/...) markdown in user messages to ContentPart[] // with image_url, so Ollama vision models receive the actual image data. function resolveImageContent(content: any, workspacePath: string): any { @@ -372,7 +371,6 @@ const IMAGE_TYPES: Record = { '.csv': 'text/csv', '.html': 'text/html', '.md': 'text/plain', }; -// ── Sub-Agent Tool Profiles ──────────────────────────────────────────────────────────── type SubagentProfile = 'file_editor' | 'researcher' | 'shell_runner' | 'reader_only'; const TOOL_PROFILES: Record> = { file_editor: new Set(['read_file', 'create_file', 'replace_lines', 'insert_after', 'delete_lines', 'find_replace', 'list_files']), @@ -513,14 +511,12 @@ function setOrchestrationEnabled(enabled: boolean): void { } })(); -// ─── Model-Busy Guard ────────────────────────────────────────────────────────── // Prevents cron scheduler from firing while user chat is in-flight. // Critical for 4B models — can't handle parallel inference. let isModelBusy = false; let lastMainSessionId = 'default'; -// ─── WebSocket Broadcast ─────────────────────────────────────────────────────── // wss is assigned after server creation below; broadcastWS is only ever called // after startup (by cron ticks), so the late assignment is safe. @@ -619,7 +615,6 @@ function resolveChannelsConfig(): ChannelsConfig { }; } -// ─── CronScheduler Init ──────────────────────────────────────────────────────── const cronStorePath = path.join(CONFIG_DIR_PATH, 'cron', 'jobs.json'); const cronScheduler = new CronScheduler({ @@ -647,7 +642,6 @@ const cronScheduler = new CronScheduler({ }, spawnBackgroundTask: async (job) => { try { - // ── Step 1: ask the preflight advisor to generate a real task plan ────── // Awaited properly so the cron scheduler gets a real taskId back. let taskTitle = job.name; let plan: Array<{ index: number; description: string; status: 'pending' }> = []; @@ -667,7 +661,6 @@ const cronScheduler = new CronScheduler({ console.warn(`[CronScheduler] Preflight unavailable for "${job.name}", using default plan:`, preflightErr.message); } - // ── Step 2: fall back to a smart default plan if preflight gave nothing ─ if (plan.length === 0) { const prompt = job.prompt.toLowerCase(); const isNews = /news|summar|stories|headlines|brief|report|digest/.test(prompt); @@ -700,7 +693,6 @@ const cronScheduler = new CronScheduler({ } } - // ── Step 3: create the task and launch the runner ──────────────────────── const cronSessionId = `cron_${job.id}`; const task = createTask({ title: taskTitle, @@ -721,7 +713,6 @@ const cronScheduler = new CronScheduler({ }, }); -// ─── Telegram Channel Init ───────────────────────────────────────────────────────── const telegramChannel = new TelegramChannel( resolveChannelsConfig().telegram, @@ -826,7 +817,7 @@ hookBus.register('gateway:startup', async ({ workspacePath }) => { await runBootMd(workspacePath, async (message, sessionId, sendSSE) => { const bootContext = [ 'CONTEXT: Internal startup BOOT.md turn. All data has been pre-fetched and is in the snapshot below.', - 'Do NOT call any tools. Read the snapshot and write a 2-3 sentence startup summary.', + 'Do NOT call any tools. Read the snapshot and write a 2-3 sentence startup summary in Korean.', '[BOOT STARTUP SNAPSHOT - pre-fetched runtime data, no tools needed]', startupSnapshot, '[/BOOT STARTUP SNAPSHOT]', @@ -857,7 +848,6 @@ hookBus.register('command:new', async ({ sessionId, workspacePath }) => { console.log(`[hooks:command:new] Saved session snapshot -> ${path.basename(outPath)}`); }); -// ─── Workspace Memory Loader ─────────────────────────────────────────────────── function loadWorkspaceFile(workspacePath: string, filename: string, maxChars: number = 500): string { try { @@ -899,7 +889,6 @@ function readDailyMemoryContext(workspacePath: string, maxTokens: number = 800): } } -// ─── Tiered Prompt System ───────────────────────────────────────────────────── // Intent detection: returns matched tool categories for the message function detectToolCategories(text: string): Set { @@ -1012,7 +1001,6 @@ async function buildPersonalityContext( historyLength: number, ): Promise { - // ── Path B: autonomous execution — full prompt, no changes ───────────────── const isAutonomous = executionMode === 'background_task' || executionMode === 'cron' || executionMode === 'heartbeat'; if (isAutonomous) { const identity = loadWorkspaceFile(workspacePath, 'IDENTITY.md', 400); @@ -1031,14 +1019,12 @@ async function buildPersonalityContext( return parts.length > 0 ? '\n\n' + parts.join('\n\n') : ''; } - // ── Path A: interactive chat — tiered ───────────────────────────────────── const identity = loadWorkspaceFile(workspacePath, 'IDENTITY.md', 400); const user = loadWorkspaceFile(workspacePath, 'USER.md', 500); const today = new Date().toISOString().split('T')[0]; const intradayPath = path.join(workspacePath, 'memory', `${today}-intraday-notes.md`); const intradayNotes = fs.existsSync(intradayPath) ? fs.readFileSync(intradayPath, 'utf-8').trim().slice(-600) : ''; - // ── Tier 1: first message in session ────────────────────────────────────── if (historyLength === 0) { const parts = [ identity ? `[IDENTITY]\n${identity}` : '', @@ -1049,7 +1035,6 @@ async function buildPersonalityContext( return parts.length > 0 ? '\n\n' + parts.join('\n\n') : ''; } - // ── Tier 2 / 3: subsequent messages — detect intent ─────────────────────── const cats = detectToolCategories(messageText); const soul = loadWorkspaceFile(workspacePath, 'SOUL.md', 600); @@ -1090,7 +1075,6 @@ async function buildPersonalityContext( return parts.length > 0 ? '\n\n' + parts.join('\n\n') : ''; } -// ─── Session Logger ──────────────────────────────────────────────────────────── function logToDaily(workspacePath: string, role: string, content: string) { try { @@ -1106,7 +1090,6 @@ function logToDaily(workspacePath: string, role: string, content: string) { } catch {} } -// ─── Tool Definitions ────────────────────────────────────────────────────────── function buildTools() { const toolDefs = [ @@ -1388,7 +1371,6 @@ function buildTools() { }, }]; })(), - // ── Sub-agent tools ── shown based on subagent_mode toggle ──────────────────────────────────── ...(() => { const subagentMode = (getConfig().getConfig() as any).orchestration?.subagent_mode === true; if (subagentMode) { @@ -1520,7 +1502,6 @@ function buildTools() { }, }, }, - // ── Memory Tools ────────────────────────────────────────────────────────── { type: 'function', function: { @@ -1565,7 +1546,6 @@ function buildTools() { }, }, }, - // ── Agent Builder Integration Tools ────────────────────────────────────── { type: 'function', function: { @@ -1661,7 +1641,6 @@ function buildTools() { return toolDefs; } -// ─── Search Providers ───────────────────────────────────────────────────────── async function tavilySearch(query: string, apiKey: string): Promise { try { @@ -1685,7 +1664,6 @@ async function tavilySearch(query: string, apiKey: string): Promise { console.log(`[v2] TAVILY AUTO-FETCH: ${topUrl.slice(0, 80)}`); const pageContent = await webFetch(topUrl); if (!pageContent.startsWith('Fetch failed') && !pageContent.startsWith('Fetch error') && !pageContent.startsWith('Fetch timed') && !pageContent.startsWith('Page fetched but very little')) { - output += '\n\n─── TOP RESULT FULL CONTENT ───\n' + pageContent; } } output += '\n\nOther URLs above can be read with web_fetch if needed.'; @@ -1735,7 +1713,6 @@ async function googleSearch(query: string): Promise { console.log(`[v2] AUTO-FETCH: Fetching top result: ${topUrl.slice(0, 80)}`); const pageContent = await webFetch(topUrl); if (!pageContent.startsWith('Fetch failed') && !pageContent.startsWith('Fetch error') && !pageContent.startsWith('Fetch timed') && !pageContent.startsWith('Page fetched but very little')) { - output += '\n\n─── TOP RESULT FULL CONTENT ───\n' + pageContent; } } @@ -1781,7 +1758,6 @@ async function webSearch(query: string): Promise { return duckDuckGoSearch(query); } -// ─── Web Fetch (full page content) ───────────────────────────────────────────── async function webFetch(url: string): Promise { try { @@ -1849,7 +1825,6 @@ async function webFetch(url: string): Promise { } } -// ─── Tool Execution ──────────────────────────────────────────────────────────── interface ToolResult { name: string; @@ -2843,7 +2818,6 @@ async function executeTool(name: string, args: any, workspacePath: string, sessi return { name, args, result: `Note saved [${noteTag}] (${noteContent.length} chars) → intraday-notes`, error: false }; } - // ── Agent Builder Integration ──────────────────────────────────────────── case 'architect_workflow': case 'verify_workflow_credentials': case 'test_workflow': @@ -2906,7 +2880,6 @@ async function executeTool(name: string, args: any, workspacePath: string, sessi } } -// ─── Audit Logger ────────────────────────────────────────────────────────────── function logToolCall(workspacePath: string, toolName: string, args: any, result: string, error: boolean) { try { @@ -2916,7 +2889,6 @@ function logToolCall(workspacePath: string, toolName: string, args: any, result: } catch {} } -// ─── Thinking Stripper ───────────────────────────────────────────────────────── function separateThinkingFromContent(text: string): { reply: string; thinking: string } { if (!text) return { reply: '', thinking: '' }; @@ -3071,7 +3043,6 @@ function stripExplicitThinkTags(text: string): { cleaned: string; thinking: stri return { cleaned, thinking: blocks.join('\n\n').trim() }; } -// ─── Main Chat Handler ───────────────────────────────────────────────────────── function isExecutionLikeRequest(message: string): boolean { const m = String(message || ''); @@ -3218,7 +3189,6 @@ function collectFileSnapshots( return out; } -// ─── Browser Tool Result Interceptor (Multi-Agent) ────────────────────────── // When multi-agent orchestrator is active, the LLM should NEVER see raw browser // snapshot data — only the secondary AI (via getBrowserAdvisorPacket) gets that. // The LLM receives a short acknowledgment so it knows the tool ran, then waits @@ -3431,7 +3401,6 @@ async function handleChat( const orchestrationSkillEnabled = isOrchestrationSkillEnabled(); const greetingLikeTurn = isGreetingLikeMessage(message); - // ── Preempt watchdog setup ───────────────────────────────────────────────── const rawCfgForPreempt = (getConfig().getConfig() as any); const primaryProvider = rawCfgForPreempt.llm?.provider || 'ollama'; const preemptCfg: { @@ -3984,7 +3953,6 @@ async function handleChat( assist_cap: preflightCfg.limits.max_assists_per_session, }); - // ── Background task route ──────────────────────────────────────────── if (preflight.route === 'background_task' && multiAgentActive) { const taskTitle = preflight.task_title || 'Background Task'; const taskPlan = (preflight.task_plan || []).map((desc, i) => ({ @@ -4325,7 +4293,6 @@ async function handleChat( ? browserAdvisorCollectedFeed.slice(-browserMaxCollectedItems) : (packet.extractedFeed as Array>); - // ── Change 5: chat_interface generation-wait — skip advisor, inject synthetic wait ── if (browserAutoSnapshotRetriesEnabled && packet.page.pageType === 'chat_interface' && packet.isGenerating) { sendSSE('info', { message: 'Browser: chat interface still generating — waiting for response before advising.' }); pendingSyntheticToolCalls = [ @@ -4447,7 +4414,6 @@ async function handleChat( orchestrationLog.push(`[browser:${advisor.route}] ${String(advisor.reason || 'n/a').slice(0, 200)}`); - // ── Synthetic tool call injection for deterministic collect_more scrolls ───────── // When the advisor says scroll (PageDown), skip LLM generation entirely. // Inject as a synthetic assistant message that the main loop executes directly. // This eliminates the 75s stall window between advisor directive and actual scroll. @@ -4476,7 +4442,6 @@ async function handleChat( } // For non-deterministic steps, use the normal message injection path - // ── Changes 2 & 3: context wipe + stripped executor system for browser ops ────────── // Secondary holds full state via buildSecondaryAssistContext(). // Primary only needs: minimal system + original goal + last 4 tool acks + this directive. // Wipe now, before pushing the hint pair, so the hint ends up at the bottom cleanly. @@ -4697,7 +4662,6 @@ RULES: logToDaily(workspacePath, 'User', message); - // ── FILE_CREATE upfront size routing ── // If the request is clearly secondary territory (full page / large template), // skip primary entirely — queue secondary patch plan now so round 0 executes // it as synthetic calls without ever running the LLM for generation. @@ -4738,7 +4702,6 @@ RULES: } sendSSE('info', { message: 'Thinking...' }); - console.log(`\n[v2] ── CHAT (native tools) ──`); for (let round = 0; ; round++) { if (round >= MAX_TOOL_ROUNDS) { @@ -4762,7 +4725,6 @@ RULES: return { type: 'execute', text: partial, toolResults: allToolResults.length > 0 ? allToolResults : undefined }; } - // ── Synthetic tool calls from browser advisor ───────────────────────────── // When the advisor queued deterministic tool calls (e.g. PageDown scroll), // skip LLM generation entirely for this round and execute them directly. if (pendingSyntheticToolCalls.length > 0) { @@ -4843,7 +4805,6 @@ RULES: continue; } - // ── Secondary-owned FILE_OP: skip Ollama, run verify, return directly ── // When secondary has already executed all patch calls there is nothing left // for primary to do. Build the reply from what we already know in-memory. if ( @@ -4953,7 +4914,6 @@ RULES: model: String(modelOverride || '').trim() || undefined, }); - // ── Preempt watchdog ──────────────────────────────────────────── if ( preemptCfg.enabled && ollamaProcMgr @@ -4969,7 +4929,6 @@ RULES: ); if (watchdogOutcome.timedOut) { - // ── FILE_OP stall: bypass preempt restart entirely, promote immediately ── if ( fileOpV2Active && (fileOpType === 'FILE_CREATE' || fileOpType === 'FILE_EDIT') @@ -5007,7 +4966,6 @@ RULES: continue; } - // ── Non-FILE_OP stall: normal preempt restart path ── preemptState.recordPreempt(round); const sessionPreemptCount = incrementPreemptSessionCount(sessionId); sendSSE('info', { @@ -5539,7 +5497,6 @@ RULES: const toolName = call.function?.name || 'unknown'; const toolArgs = normalizeToolArgs(call.function?.arguments); - // ── Scroll-before-act gate ──────────────────────────────────────────────── // Block PageDown / browser_scroll on interactive pages when the model hasn't // filled or clicked anything yet. This is the #1 cause of the scroll loop bug // where the AI scrolls past the X.com composer instead of filling it. @@ -5583,7 +5540,6 @@ RULES: browserFillOrClickDoneThisTurn = true; browserScrollBeforeActCount = 0; } - // ── End scroll-before-act gate ──────────────────────────────────────────── const loopSig = `${toolName}:${hashArgs(toolArgs)}`; const loopPivotNudge = 'Loop detector: you are looping on this tool, try a different approach or ask the user.'; @@ -5925,7 +5881,6 @@ RULES: }; } - // ── Sub-agent spawn / specialist delegate ────────────────────────────────────────────── if (toolName === 'delegate_to_specialist' || toolName === 'subagent_spawn') { const isTaskSession = sessionId.startsWith('task_'); @@ -6019,7 +5974,6 @@ RULES: break; } - // ── Orchestration: explicit request from primary if (toolName === 'request_secondary_assist') { const orchCfg = getOrchestrationConfig(); if (orchestrationSkillEnabled && orchCfg?.enabled) { @@ -6097,7 +6051,6 @@ RULES: if (fileOpV2Active && toolResult.error) fileOpHadToolFailure = true; if (!toolResult.error) roundHadProgress = true; - // ── Orchestration: track trigger state orchestrationState.recordToolResult(round, toolName, toolArgs, toolResult.error); orchestrationLog.push( toolResult.error @@ -6117,7 +6070,6 @@ RULES: && !toolResult.error) ? '\n\n[TASK COMPLETE: The presentation has been created. Summarize the result and STOP — do not call any more tools.]' : `\n\n[GOAL REMINDER: Your task is still: "${message.slice(0, 120)}". Stay focused on this goal only.]`; - // ── Multi-agent browser interception ──────────────────────────────────── // When orchestrator is active, LLM never sees raw snapshot/browser data. // Full data still flows to advisor via getBrowserAdvisorPacket(). const isBrowserTool = isBrowserToolName(toolName); @@ -6148,7 +6100,6 @@ RULES: await maybeRunDesktopAdvisorPass(toolName, toolResult); } - // ── Orchestration: auto-trigger check after each round const orchCfg = getOrchestrationConfig(); if (orchestrationSkillEnabled && orchCfg?.enabled && !isBootStartupTurn) { if (!roundHadProgress) orchestrationState.recordRoundNoProgress(round); @@ -6200,7 +6151,6 @@ RULES: return { type: 'execute', text: 'Hit max steps.', toolResults: allToolResults }; } -// ─── SSE + Routes ────────────────────────────────────────────────────────────── function createSSESender(res: express.Response): (event: string, data: any) => void { return (type: string, data: any) => { try { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); } catch {} }; @@ -6615,7 +6565,6 @@ async function tryHandleBlockedTaskFollowup(sessionId: string, rawMessage: strin const message = String(rawMessage || '').trim(); if (!message) return null; - // ── Path A: Explicit task UUID in the message (original behavior) ─────────── const explicitTaskId = parseTaskIdFromText(message); if (explicitTaskId) { const rerunRequested = isRerunIntent(message); @@ -6627,7 +6576,6 @@ async function tryHandleBlockedTaskFollowup(sessionId: string, rawMessage: strin return ctl.success ? (ctl.message || null) : null; } - // ── Path B: No UUID — look for a blocked/escalated task on this session ───── // Handles user messages like "proceed", "I logged in", "go ahead", "fixed it", etc. // where the task ID is implicit from context. const blockedTask = findBlockedTaskForSession(sessionId); @@ -6666,7 +6614,6 @@ async function tryHandleBlockedTaskFollowup(sessionId: string, rawMessage: strin return `${verb} task ${taskLabel}. ${ctl.message || ''}`.trim(); } -// ─── Auth: session store & helpers ────────────────────────────────────────────── interface SessionInfo { username: string; role: 'admin' | 'user'; @@ -6748,7 +6695,6 @@ function parseCookies(req: express.Request): Record { const AUTH_COOKIE = 'smallclaw_session'; -// ─── Express app ────────────────────────────────────────────────────────────────── const app = express(); app.set('trust proxy', 1); app.use(cors()); @@ -6756,7 +6702,6 @@ app.use(express.json()); const webUiPath = path.join(__dirname, '..', '..', 'web-ui'); -// ─── Auth API endpoints (before static & auth guard) ───────────────────────────── app.get('/api/auth/status', (_req, res) => { const auth = getAuthConfig(); if (!auth.enabled) { @@ -6925,7 +6870,6 @@ app.post('/api/auth/change-password', (req, res) => { res.json({ success: true }); }); -// ─── Auth middleware ─────────────────────────────────────────────────────────────── app.use((req, _res, next) => { const auth = getAuthConfig(); if (!auth.enabled) return next(); @@ -6951,7 +6895,6 @@ app.use((req, _res, next) => { } }); -// ─── User management endpoints (behind auth middleware) ──────────────────────── app.get('/api/users', (req, res) => { const session = getSessionUser(req); if (!session || session.role !== 'admin') return res.status(403).json({ error: 'Admin required' }); @@ -7011,7 +6954,6 @@ app.delete('/api/users/:username', (req, res) => { app.use(express.static(webUiPath, { setHeaders: (res) => { res.setHeader('Cache-Control', 'no-cache'); } })); -// ─── Workspace File Serving ───────────────────────────────────────────────── app.get('/api/files/{*filePath}', (req: express.Request, res: express.Response) => { try { @@ -7080,7 +7022,6 @@ app.get('/api/files/{*filePath}', (req: express.Request, res: express.Response) } }); -// ─── PPTX Preview ─────────────────────────────────────────────────────────────── app.get('/api/pptx/preview', async (req: express.Request, res: express.Response) => { try { let relPath = String(req.query.path || '').trim(); @@ -7180,7 +7121,6 @@ app.get('/api/pptx/preview', async (req: express.Request, res: express.Response) } }); -// ─── Image Upload ───────────────────────────────────────────────────────────── app.post('/api/upload/image', (req: express.Request, res: express.Response) => { const contentType = String(req.headers['content-type'] || ''); if (!contentType.includes('multipart/form-data')) { @@ -7262,7 +7202,17 @@ app.post('/api/chat', async (req, res) => { const { message, sessionId = 'default', pinnedMessages } = req.body; if (!message || typeof message !== 'string') { res.status(400).json({ error: 'Message required' }); return; } const user = (req as any).user; + + // Pre-initialise the session under the authenticated user's directory so that + // all subsequent addMessage / getHistory calls land in the right place. + // ensureUserWorkspace() bootstraps the user's workspace on first login. + if (user?.username) { + try { ensureUserWorkspace(user.username); } catch { /* non-fatal */ } + getSession(String(sessionId || 'default'), user.username); + } if (user?.workspace) setWorkspace(String(sessionId || 'default'), user.workspace); + // ---------------------------------------------------------------------------- + lastMainSessionId = String(sessionId || 'default'); res.setHeader('Content-Type', 'text/event-stream'); @@ -7273,7 +7223,6 @@ app.post('/api/chat', async (req, res) => { const sendSSE = createSSESender(res); const heartbeat = setInterval(() => sendSSE('heartbeat', { state: 'processing' }), 5000); - // ── Model busy guard — block cron scheduler while user chat is running ── isModelBusy = true; const abortSignal = { aborted: false }; @@ -7388,6 +7337,9 @@ app.get('/api/open-path', async (req, res) => { app.post('/api/clear-history', async (req, res) => { const sid = req.body.sessionId || 'default'; + const user = (req as any).user; + // Ensure session is scoped to the authenticated user before we access it. + if (user?.username) getSession(sid, user.username); const ws = getWorkspace(sid) || (getConfig().getConfig() as any).workspace?.path || ''; if (ws) { await hookBus.fire({ @@ -7407,7 +7359,6 @@ app.post('/api/clear-history', async (req, res) => { res.json({ success: true }); }); -// ─── Skills API ──────────────────────────────────────────────────────────────── app.get('/api/skills', async (_req, res) => { recoverSkillsIfEmpty(); @@ -7694,7 +7645,6 @@ app.get('/api/task-status', (req, res) => { res.json({ active: task.status === 'running', ...task, journal: task.journal.slice(-10) }); }); -// ─── Tasks / Cron API ────────────────────────────────────────────────────────── app.get('/api/tasks', (_req, res) => { res.json({ success: true, jobs: cronScheduler.getJobs(), config: cronScheduler.getConfig() }); @@ -7768,7 +7718,6 @@ app.put('/api/heartbeat/config', (req, res) => { res.json({ success: true, config: cfg }); }); -// ─── Background Task Kanban API ───────────────────────────────────────────────── app.get('/api/bg-tasks', (_req, res) => { const tasks = listTasks(); @@ -7829,7 +7778,6 @@ app.post('/api/bg-tasks/:id/resume', (req, res) => { } }); -// ─── Error Response Endpoint ──────────────────────────────────────────────── // Receives structured user response to a task error, injects it as a resume // instruction, and relaunches the task runner so the agent acts on it. app.post('/api/bg-tasks/:id/error-response', async (req: any, res: any) => { @@ -8042,7 +7990,6 @@ app.put('/api/bg-tasks/heartbeat/config', (req, res) => { res.json({ success: true, config: next }); }); -// ─── Task Heartbeat Scheduler ─────────────────────────────────────────────── let taskHeartbeatTimer: ReturnType | null = null; @@ -8183,7 +8130,6 @@ async function runTaskHeartbeat(): Promise { scheduleTaskHeartbeat(); } -// ─── Channels API ────────────────────────────────────────────────────────────── async function testTelegramConfig(token: string): Promise<{ success: boolean; bot?: any; error?: string }> { if (!token) return { success: false, error: 'No Telegram bot token provided' }; @@ -8707,7 +8653,6 @@ app.post('/api/agents/:id/spawn', async (req, res) => { res.json({ success: result.success, result, historyEntry }); }); -// ─── Scheduler API ──────────────────────────────────────────────────────────────── app.get('/api/schedules', (_req, res) => { const jobs = cronScheduler.getJobs(); @@ -8922,7 +8867,6 @@ app.post('/api/schedules/parse', (req: any, res: any) => { } }); -// ─── Settings API ──────────────────────────────────────────────────────────────── app.get('/api/settings/search', (_req, res) => { const cm = getConfig(); @@ -9054,7 +8998,6 @@ app.post('/api/settings/agent', (req, res) => { res.json({ success: true }); }); -// ─── Model / Ollama Settings API ────────────────────────────────────────────────── app.get('/api/settings/model', (_req, res) => { const cfg = getConfig().getConfig(); @@ -9109,7 +9052,6 @@ app.get('/api/ollama/models', async (_req, res) => { } }); -// ─── System Stats API ─────────────────────────────────────────────────────────── import * as osModule from 'os'; @@ -9205,10 +9147,11 @@ app.get('/api/system-stats', async (_req, res) => { }); }); -// ─── Agent Session Context API ──────────────────────────────────────────────── app.get('/api/agent/session/:id', (req, res) => { const sessionId = req.params.id; + const user = (req as any).user; + if (user?.username) getSession(sessionId, user.username); const history = getHistory(sessionId, 50); const userMessages = history.filter(h => h.role === 'user'); const aiMessages = history.filter(h => h.role === 'assistant'); @@ -9235,12 +9178,10 @@ app.get('/api/agent/session/:id', (req, res) => { // Track agent mode per-session (simplified) let useAgentMode = false; -// ─── Approvals API ─────────────────────────────────────────────────────────── // SECURITY: All approval endpoints require gateway auth. Approvals are the // confirmation gate before the agent executes irreversible actions — an // unauthenticated bypass here is a critical vulnerability. -// ─── Gateway Auth Middleware ────────────────────────────────────────────────── // CRIT-03 / CRIT-01 fix: protects approval, memory-confirm, and open-path // endpoints from unauthenticated access. // @@ -9331,7 +9272,6 @@ app.post('/api/approvals/:id', requireGatewayAuth, (req, res) => { res.json({ success: true, decision }); }); -// ─── Memory API (stub) ─────────────────────────────────────────────────────────── app.post('/api/memory/confirm', requireGatewayAuth, (req, res) => { // Memory persistence stub — can be wired to ChromaDB/vector store @@ -9376,7 +9316,6 @@ app.post('/api/open-path', requireGatewayAuth, async (req, res) => { } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); -// ─── Provider / Model Settings API ─────────────────────────────────────────── // Used by the Settings → Models tab to read/write provider config and // trigger the OpenAI OAuth flow. @@ -9498,7 +9437,6 @@ app.post('/api/auth/openai/disconnect', (_req, res) => { res.json({ success: true }); }); -// ─── Webhook Settings API ──────────────────────────────────────────────────── app.get('/api/settings/hooks', (_req, res) => { const cfg = (getConfig().getConfig() as any).hooks || {}; @@ -9541,7 +9479,6 @@ app.post('/api/settings/hooks/test', async (req, res) => { } }); -// ─── PPT Template & Skin API ─────────────────────────────────────────────────── const PPT_SKIN_DIR = path.join(process.cwd(), 'ppt', 'skin'); const PPT_TEMPLATE_DIR = path.join(process.cwd(), 'ppt', 'template'); @@ -9650,7 +9587,6 @@ app.put('/api/settings/ppt', (req, res) => { } }); -// ─── MCP API ────────────────────────────────────────────────────────────────── app.get('/api/mcp/servers', (_req, res) => { try { @@ -9719,7 +9655,6 @@ app.get('/api/mcp/tools', (_req, res) => { } }); -// ─── Webhook Routes ────────────────────────────────────────────────────────── // Mounted dynamically so the path is always read fresh from config. // Must be registered BEFORE the SPA catch-all below. (() => { @@ -9744,14 +9679,12 @@ app.get('/api/mcp/tools', (_req, res) => { console.log(`[Webhooks] Listening at ${hookCfg.path} (wake, agent, status)`); })(); -// ─── Internal Agent Task endpoint (called by Agent Builder for AI-authoring nodes) // Localhost-only unless SMALLCLAW_INTERNAL_TOKEN is set. app.use('/internal/agent-task', internalAgentTaskRouter); console.log('[InternalAgentTask] Endpoint mounted at POST /internal/agent-task'); app.get('/{*path}', (_req, res) => { res.sendFile(path.join(webUiPath, 'index.html')); }); -// ─── Server ──────────────────────────────────────────────────────────────────── const server = http.createServer(app); wss = new WebSocketServer({ server, path: '/ws' }); @@ -9794,7 +9727,6 @@ server.on('error', (err: any) => { // Setup error response endpoint setupErrorResponseEndpoint(app); -// ─── Initialize Advanced Error Response Systems ──────────────────────────────── const encryptionKey = process.env.CREDENTIAL_ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex'); const credentialHandler = initCredentialHandler(encryptionKey); const verificationFlowManager = getVerificationFlowManager(); @@ -9885,6 +9817,24 @@ server.listen(PORT, HOST, () => { .fire({ type: 'gateway:startup', workspacePath: bootWorkspace }) .catch((err: any) => console.warn('[hooks] gateway:startup error:', err?.message || err)); } + + // For every existing user account (created before multi-user workspace + // isolation was added), copy their legacy global sessions into their + // per-user directory and bootstrap their workspace from the global template. + // The migration is idempotent — a .migrated marker file prevents re-runs. + try { + const users: string[] = listUsers(); // already defined in server scope + for (const username of users) { + if (!username || username === 'legacy') continue; + try { ensureUserWorkspace(username); } catch { /* non-fatal */ } + migrateGlobalSessionsToUser(username); + } + if (users.length > 0) { + console.log(`[Migration] Multi-user workspace migration complete for: ${users.join(', ')}`); + } + } catch (err: any) { + console.warn('[Migration] Could not run multi-user migration:', err?.message || err); + } }); let shuttingDown = false; diff --git a/src/gateway/session.ts b/src/gateway/session.ts index 2d624e8..f57cd22 100644 --- a/src/gateway/session.ts +++ b/src/gateway/session.ts @@ -18,6 +18,8 @@ export interface ChatMessage { export interface Session { id: string; + /** Owning user. Undefined for CLI/cron sessions (uses global workspace). */ + username?: string; history: ChatMessage[]; workspace: string; createdAt: number; @@ -45,22 +47,48 @@ const AUTO_SESSION_ID_RE = /^(task_|cron_)/i; const SESSION_SAVE_DEBOUNCE_MS = 500; const sessionSaveTimers = new Map(); -const SESSION_DIR = (() => { +/** + * Resolve the sessions directory for a given user. + * + * - Authenticated users → .smallclaw/users//sessions/ + * - CLI / cron / legacy → .smallclaw/sessions/ (unchanged behaviour) + */ +function getSessionDir(username?: string): string { try { - return path.join(getConfig().getConfigDir(), 'sessions'); + const base = getConfig().getConfigDir(); + if (username && username !== 'legacy') { + return path.join(base, 'users', username, 'sessions'); + } + return path.join(base, 'sessions'); } catch { return path.join(process.cwd(), '.smallclaw', 'sessions'); } -})(); +} -function ensureSessionDir(): void { - if (!fs.existsSync(SESSION_DIR)) { - fs.mkdirSync(SESSION_DIR, { recursive: true }); +function ensureSessionDir(username?: string): void { + const dir = getSessionDir(username); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); } } -function getSessionPath(id: string): string { - return path.join(SESSION_DIR, `${id}.json`); +function getSessionPath(id: string, username?: string): string { + return path.join(getSessionDir(username), `${id}.json`); +} + +/** + * Derive the default workspace path for a user without importing config's + * getUserWorkspace (avoids a potential circular-dependency at module init). + */ +function resolveDefaultWorkspace(username?: string): string { + try { + if (username && username !== 'legacy') { + return path.join(getConfig().getConfigDir(), 'users', username, 'workspace'); + } + return getConfig().getWorkspacePath(); + } catch { + return process.cwd(); + } } function resolveNumCtx(): number { @@ -190,21 +218,44 @@ export interface AddMessageResult { thresholdTokens: number; } -export function getSession(id: string): Session { +/** + * Load or create a session. + * + * @param id Session identifier (e.g. 'default', 'task_xyz') + * @param username Optional owning user. When provided the session file is + * stored under .smallclaw/users//sessions/ and the + * default workspace resolves to that user's workspace dir. + * Pass undefined / omit for CLI and background tasks. + */ +export function getSession(id: string, username?: string): Session { + // Return in-memory session if already loaded. if (sessions.has(id)) { - return sessions.get(id)!; + const existing = sessions.get(id)!; + // If caller now knows the owner but the session was loaded without one, + // upgrade the session in place so subsequent saves go to the right dir. + if (username && !existing.username) { + existing.username = username; + existing.workspace = existing.workspace || resolveDefaultWorkspace(username); + } + return existing; } - ensureSessionDir(); - const filePath = getSessionPath(id); + // Try to load from disk — check user dir first, then global dir as fallback. + const searchDirs = (username && username !== 'legacy') + ? [getSessionDir(username), getSessionDir()] + : [getSessionDir()]; - if (fs.existsSync(filePath)) { + for (const dir of searchDirs) { + const filePath = path.join(dir, `${id}.json`); + if (!fs.existsSync(filePath)) continue; try { const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + const resolvedUsername = data.username || username; const session: Session = { id: data.id || id, + username: resolvedUsername, history: Array.isArray(data.history) ? data.history : [], - workspace: data.workspace || getConfig().getWorkspacePath(), + workspace: data.workspace || resolveDefaultWorkspace(resolvedUsername), createdAt: data.createdAt || Date.now(), lastActiveAt: data.lastActiveAt || Date.now(), pendingMemoryFlush: data.pendingMemoryFlush === true, @@ -216,14 +267,16 @@ export function getSession(id: string): Session { sessions.set(id, session); return session; } catch { - // Corrupted file, create new session + // Corrupted file — fall through to create a new session below. } } + // Create a brand-new session. const session: Session = { id, + username, history: [], - workspace: getConfig().getWorkspacePath(), + workspace: resolveDefaultWorkspace(username), createdAt: Date.now(), lastActiveAt: Date.now(), pendingMemoryFlush: false, @@ -365,35 +418,45 @@ export function clearHistory(id: string): void { } export function cleanupSessions(nowMs: number = Date.now()): { deleted: number; scanned: number } { - ensureSessionDir(); let deleted = 0; let scanned = 0; + + // Collect all session directories: global + one per user. + const dirsToScan: string[] = [getSessionDir()]; try { - const files = fs.readdirSync(SESSION_DIR).filter((f) => f.endsWith('.json')); - for (const file of files) { - scanned++; - const id = file.replace(/\.json$/i, ''); - if (!AUTO_SESSION_ID_RE.test(id)) continue; - const filePath = path.join(SESSION_DIR, file); - let st: fs.Stats; - try { - st = fs.statSync(filePath); - } catch { - continue; - } - const ageMs = nowMs - Number(st.mtimeMs || 0); - if (ageMs < SESSION_CLEANUP_MAX_AGE_MS) continue; - try { - fs.unlinkSync(filePath); - sessions.delete(id); - deleted++; - } catch { - // ignore unlink failures; next startup can retry + const usersRoot = path.join(getConfig().getConfigDir(), 'users'); + if (fs.existsSync(usersRoot)) { + for (const entry of fs.readdirSync(usersRoot, { withFileTypes: true })) { + if (entry.isDirectory()) { + const userSessionDir = path.join(usersRoot, entry.name, 'sessions'); + if (fs.existsSync(userSessionDir)) dirsToScan.push(userSessionDir); + } } } - } catch { - return { deleted: 0, scanned: 0 }; + } catch { /* unable to enumerate users dir — skip */ } + + for (const dir of dirsToScan) { + try { + ensureSessionDir(); // ensures global dir exists at minimum + const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json')); + for (const file of files) { + scanned++; + const id = file.replace(/\.json$/i, ''); + if (!AUTO_SESSION_ID_RE.test(id)) continue; + const filePath = path.join(dir, file); + let st: fs.Stats; + try { st = fs.statSync(filePath); } catch { continue; } + const ageMs = nowMs - Number(st.mtimeMs || 0); + if (ageMs < SESSION_CLEANUP_MAX_AGE_MS) continue; + try { + fs.unlinkSync(filePath); + sessions.delete(id); + deleted++; + } catch { /* ignore unlink failures; next startup can retry */ } + } + } catch { /* skip unreadable dirs */ } } + return { deleted, scanned }; } @@ -425,9 +488,9 @@ function saveSession(id: string): void { sessionSaveTimers.delete(id); const latest = sessions.get(id); if (!latest) return; - ensureSessionDir(); + ensureSessionDir(latest.username); try { - fs.writeFileSync(getSessionPath(id), JSON.stringify(scrubSession(latest), null, 2)); + fs.writeFileSync(getSessionPath(id, latest.username), JSON.stringify(scrubSession(latest), null, 2)); } catch (err) { console.warn(`[session] Failed to save session ${id}:`, err); } @@ -446,9 +509,9 @@ export function flushSession(id: string): void { } const session = sessions.get(id); if (!session) return; - ensureSessionDir(); + ensureSessionDir(session.username); try { - fs.writeFileSync(getSessionPath(id), JSON.stringify(session, null, 2)); + fs.writeFileSync(getSessionPath(id, session.username), JSON.stringify(session, null, 2)); } catch (err) { console.warn(`[session] Failed to flush session ${id}:`, err); } @@ -464,3 +527,67 @@ export function setWorkspace(id: string, workspacePath: string): void { session.lastActiveAt = Date.now(); saveSession(id); } + +// ─── One-time migration: global sessions → user directory ────────────────── + +/** + * Migrate existing session files from the legacy global sessions directory + * (.smallclaw/sessions/) into the user-scoped directory + * (.smallclaw/users//sessions/). + * + * Safe to call multiple times — skips files that already exist in the target + * directory, and writes a .migrated marker so the scan is O(1) on subsequent + * server starts. + * + * Returns a summary of what was copied/skipped. + */ +export function migrateGlobalSessionsToUser(username: string): { + copied: number; + skipped: number; + errors: number; +} { + if (!username || username === 'legacy') return { copied: 0, skipped: 0, errors: 0 }; + + const globalDir = getSessionDir(); // .smallclaw/sessions/ + const userDir = getSessionDir(username); // .smallclaw/users//sessions/ + + // ── Already migrated? ────────────────────────────────────────────────────── + const markerPath = path.join(userDir, '.migrated-from-global'); + if (fs.existsSync(markerPath)) return { copied: 0, skipped: 0, errors: 0 }; + + let copied = 0, skipped = 0, errors = 0; + + try { + if (!fs.existsSync(globalDir)) return { copied: 0, skipped: 0, errors: 0 }; + ensureSessionDir(username); + + const files = fs.readdirSync(globalDir).filter(f => f.endsWith('.json')); + for (const file of files) { + const src = path.join(globalDir, file); + const dst = path.join(userDir, file); + + // Skip if already present in user dir (previous partial migration). + if (fs.existsSync(dst)) { skipped++; continue; } + + try { + // Patch the session JSON: inject username & update workspace default. + const raw = fs.readFileSync(src, 'utf-8'); + const data = JSON.parse(raw); + + if (!data.username) data.username = username; + if (!data.workspace) data.workspace = resolveDefaultWorkspace(username); + fs.writeFileSync(dst, JSON.stringify(data, null, 2), 'utf-8'); + copied++; + } catch { + errors++; + } + } + + fs.writeFileSync(markerPath, new Date().toISOString(), 'utf-8'); + console.log(`[session] Migrated ${copied} session(s) for "${username}" (skipped ${skipped}, errors ${errors})`); + } catch (err) { + console.warn(`[session] Migration scan failed for "${username}":`, err); + } + + return { copied, skipped, errors }; +} diff --git a/web-ui/index.html b/web-ui/index.html index 778ec0a..53ed0a5 100644 --- a/web-ui/index.html +++ b/web-ui/index.html @@ -2154,9 +2154,9 @@
- - - + + +
@@ -2165,8 +2165,8 @@ class="icon-btn" data-theme-state="light" onclick="toggleTheme()" - title="Switch to dark mode" - aria-label="Toggle dark mode" + title="다크 모드로 전환" + aria-label="다크 모드 전환" > @@ -2180,9 +2180,18 @@ 설정 + +
- Checking... + 확인 중...
- @@ -2196,8 +2205,8 @@
@@ -2221,7 +2230,7 @@
-
Loading skills...
+
스킬 로딩 중...
@@ -2234,31 +2243,31 @@
🍒

CherryClaw

-

Chat directly with your local model. No API keys, no cloud — just you and the model.

-
Switch to Agent mode for multi-step agentic tasks with tools.
+

로컬 모델과 직접 대화하세요. API 키도, 클라우드도 없이 — 오직 나와 모델만.

+
멀티스텝 에이전트 작업은 에이전트 모드로 전환하세요.
- Mode: -
- Queued prompts - + 대기 중인 프롬프트 +
- + @@ -2308,7 +2317,7 @@
- Chatting with your model via CherryClaw. + 모델과 대화 중 — CherryClaw 경유.
@@ -2322,7 +2331,7 @@ @@ -2368,18 +2377,18 @@ @@ -2387,24 +2396,24 @@ @@ -2440,7 +2449,7 @@