Merge branch 'main' into fix/text-styles

This commit is contained in:
gavrielc
2026-03-27 13:42:05 +03:00
committed by GitHub
23 changed files with 1130 additions and 28 deletions

View File

@@ -0,0 +1,289 @@
---
name: add-emacs
description: Add Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Uses a local HTTP bridge — no bot token or external service needed.
---
# Add Emacs Channel
This skill adds Emacs support to NanoClaw, then walks through interactive setup.
Works with Doom Emacs, Spacemacs, and vanilla Emacs 27.1+.
## What you can do with this
- **Ask while coding** — open the chat buffer (`C-c n c` / `SPC N c`), ask about a function or error without leaving Emacs
- **Code review** — select a region and send it with `nanoclaw-org-send`; the response appears as a child heading inline in your org file
- **Meeting notes** — send an org agenda entry; get a summary or action item list back as a child node
- **Draft writing** — send org prose; receive revisions or continuations in place
- **Research capture** — ask a question directly in your org notes; the answer lands exactly where you need it
- **Schedule tasks** — ask Andy to set a reminder or create a scheduled NanoClaw task (e.g. "remind me tomorrow to review the PR")
## Phase 1: Pre-flight
### Check if already applied
Check if `src/channels/emacs.ts` exists:
```bash
test -f src/channels/emacs.ts && echo "already applied" || echo "not applied"
```
If it exists, skip to Phase 3 (Setup). The code changes are already in place.
## Phase 2: Apply Code Changes
### Ensure the upstream remote
```bash
git remote -v
```
If an `upstream` remote pointing to `https://github.com/qwibitai/nanoclaw.git` is missing,
add it:
```bash
git remote add upstream https://github.com/qwibitai/nanoclaw.git
```
### Merge the skill branch
```bash
git fetch upstream skill/emacs
git merge upstream/skill/emacs
```
If there are merge conflicts on `package-lock.json`, resolve them by accepting the incoming
version and continuing:
```bash
git checkout --theirs package-lock.json
git add package-lock.json
git merge --continue
```
For any other conflict, read the conflicted file and reconcile both sides manually.
This adds:
- `src/channels/emacs.ts``EmacsBridgeChannel` HTTP server (port 8766)
- `src/channels/emacs.test.ts` — unit tests
- `emacs/nanoclaw.el` — Emacs Lisp package (`nanoclaw-chat`, `nanoclaw-org-send`)
- `import './emacs.js'` appended to `src/channels/index.ts`
If the merge reports conflicts, resolve them by reading the conflicted files and understanding the intent of both sides.
### Validate code changes
```bash
npm run build
npx vitest run src/channels/emacs.test.ts
```
Build must be clean and tests must pass before proceeding.
## Phase 3: Setup
### Configure environment (optional)
The channel works out of the box with defaults. Add to `.env` only if you need non-defaults:
```bash
EMACS_CHANNEL_PORT=8766 # default — change if 8766 is already in use
EMACS_AUTH_TOKEN=<random> # optional — locks the endpoint to Emacs only
```
If you change or add values, sync to the container environment:
```bash
mkdir -p data/env && cp .env data/env/env
```
### Configure Emacs
The `nanoclaw.el` package requires only Emacs 27.1+ built-in libraries (`url`, `json`, `org`) — no package manager setup needed.
AskUserQuestion: Which Emacs distribution are you using?
- **Doom Emacs** - config.el with map! keybindings
- **Spacemacs** - dotspacemacs/user-config in ~/.spacemacs
- **Vanilla Emacs / other** - init.el with global-set-key
**Doom Emacs** — add to `~/.config/doom/config.el` (or `~/.doom.d/config.el`):
```elisp
;; NanoClaw — personal AI assistant channel
(load (expand-file-name "~/src/nanoclaw/emacs/nanoclaw.el"))
(map! :leader
:prefix ("N" . "NanoClaw")
:desc "Chat buffer" "c" #'nanoclaw-chat
:desc "Send org" "o" #'nanoclaw-org-send)
```
Then reload: `M-x doom/reload`
**Spacemacs** — add to `dotspacemacs/user-config` in `~/.spacemacs`:
```elisp
;; NanoClaw — personal AI assistant channel
(load-file "~/src/nanoclaw/emacs/nanoclaw.el")
(spacemacs/set-leader-keys "aNc" #'nanoclaw-chat)
(spacemacs/set-leader-keys "aNo" #'nanoclaw-org-send)
```
Then reload: `M-x dotspacemacs/sync-configuration-layers` or restart Emacs.
**Vanilla Emacs** — add to `~/.emacs.d/init.el` (or `~/.emacs`):
```elisp
;; NanoClaw — personal AI assistant channel
(load-file "~/src/nanoclaw/emacs/nanoclaw.el")
(global-set-key (kbd "C-c n c") #'nanoclaw-chat)
(global-set-key (kbd "C-c n o") #'nanoclaw-org-send)
```
Then reload: `M-x eval-buffer` or restart Emacs.
If `EMACS_AUTH_TOKEN` was set, also add (any distribution):
```elisp
(setq nanoclaw-auth-token "<your-token>")
```
If `EMACS_CHANNEL_PORT` was changed from the default, also add:
```elisp
(setq nanoclaw-port <your-port>)
```
### Restart NanoClaw
```bash
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclaw
```
## Phase 4: Verify
### Test the HTTP endpoint
```bash
curl -s "http://localhost:8766/api/messages?since=0"
```
Expected: `{"messages":[]}`
If you set `EMACS_AUTH_TOKEN`:
```bash
curl -s -H "Authorization: Bearer <token>" "http://localhost:8766/api/messages?since=0"
```
### Test from Emacs
Tell the user:
> 1. Open the chat buffer with your keybinding (`SPC N c`, `SPC a N c`, or `C-c n c`)
> 2. Type a message and press `RET`
> 3. A response from Andy should appear within a few seconds
>
> For org-mode: open any `.org` file, position the cursor on a heading, and use `SPC N o` / `SPC a N o` / `C-c n o`
### Check logs if needed
```bash
tail -f logs/nanoclaw.log
```
Look for `Emacs channel listening` at startup and `Emacs message received` when a message is sent.
## Troubleshooting
### Port already in use
```
Error: listen EADDRINUSE: address already in use :::8766
```
Either a stale NanoClaw process is running, or 8766 is taken by another app.
Find and kill the stale process:
```bash
lsof -ti :8766 | xargs kill -9
```
Or change the port in `.env` (`EMACS_CHANNEL_PORT=8767`) and update `nanoclaw-port` in Emacs config.
### No response from agent
Check:
1. NanoClaw is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
2. Emacs group is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid = 'emacs:default'"`
3. Logs show activity: `tail -50 logs/nanoclaw.log`
If the group is not registered, it will be created automatically on the next NanoClaw restart.
### Auth token mismatch (401 Unauthorized)
Verify the token in Emacs matches `.env`:
```elisp
;; M-x describe-variable RET nanoclaw-auth-token RET
```
Must exactly match `EMACS_AUTH_TOKEN` in `.env`.
### nanoclaw.el not loading
Check the path is correct:
```bash
ls ~/src/nanoclaw/emacs/nanoclaw.el
```
If NanoClaw is cloned elsewhere, update the `load`/`load-file` path in your Emacs config.
## After Setup
If running `npm run dev` while the service is active:
```bash
# macOS:
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
# Linux:
# systemctl --user stop nanoclaw
# npm run dev
# systemctl --user start nanoclaw
```
## Agent Formatting
The Emacs bridge converts markdown → org-mode automatically. Agents should
output standard markdown — **not** org-mode syntax. The conversion handles:
| Markdown | Org-mode |
|----------|----------|
| `**bold**` | `*bold*` |
| `*italic*` | `/italic/` |
| `~~text~~` | `+text+` |
| `` `code` `` | `~code~` |
| ` ```lang ` | `#+begin_src lang` |
If an agent outputs org-mode directly, bold/italic/etc. will be double-converted
and render incorrectly.
## Removal
To remove the Emacs channel:
1. Delete `src/channels/emacs.ts`, `src/channels/emacs.test.ts`, and `emacs/nanoclaw.el`
2. Remove `import './emacs.js'` from `src/channels/index.ts`
3. Remove the NanoClaw block from your Emacs config file
4. Remove Emacs registration from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid = 'emacs:default'"`
5. Remove `EMACS_CHANNEL_PORT` and `EMACS_AUTH_TOKEN` from `.env` if set
6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)

View File

@@ -0,0 +1,133 @@
---
name: add-macos-statusbar
description: Add a macOS menu bar status indicator for NanoClaw. Shows a bolt icon with a green/red dot indicating whether NanoClaw is running, with Start, Stop, and Restart controls. macOS only.
---
# Add macOS Menu Bar Status Indicator
Adds a persistent menu bar icon that shows NanoClaw's running status and lets the user
start, stop, or restart the service — similar to how Docker Desktop appears in the menu bar.
**macOS only.** Requires Xcode Command Line Tools (`swiftc`).
## Phase 1: Pre-flight
### Check platform
If not on macOS, stop and tell the user:
> This skill is macOS only. The menu bar status indicator uses AppKit and requires `swiftc` (Xcode Command Line Tools).
### Check for swiftc
```bash
which swiftc
```
If not found, tell the user:
> Xcode Command Line Tools are required. Install them by running:
>
> ```bash
> xcode-select --install
> ```
>
> Then re-run `/add-macos-statusbar`.
### Check if already installed
```bash
launchctl list | grep com.nanoclaw.statusbar
```
If it returns a PID (not `-`), tell the user it's already installed and skip to Phase 3 (Verify).
## Phase 2: Compile and Install
### Compile the Swift binary
The source lives in the skill directory. Compile it into `dist/`:
```bash
mkdir -p dist
swiftc -O -o dist/statusbar "${CLAUDE_SKILL_DIR}/add/src/statusbar.swift"
```
This produces a small native binary at `dist/statusbar`.
On macOS Sequoia or later, clear the quarantine attribute so the binary can run:
```bash
xattr -cr dist/statusbar
```
### Create the launchd plist
Determine the absolute project root and home directory:
```bash
pwd
echo $HOME
```
Create `~/Library/LaunchAgents/com.nanoclaw.statusbar.plist`, substituting the actual values
for `{PROJECT_ROOT}` and `{HOME}`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.nanoclaw.statusbar</string>
<key>ProgramArguments</key>
<array>
<string>{PROJECT_ROOT}/dist/statusbar</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>{HOME}</string>
</dict>
<key>StandardOutPath</key>
<string>{PROJECT_ROOT}/logs/statusbar.log</string>
<key>StandardErrorPath</key>
<string>{PROJECT_ROOT}/logs/statusbar.error.log</string>
</dict>
</plist>
```
### Load the service
```bash
launchctl load ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist
```
## Phase 3: Verify
```bash
launchctl list | grep com.nanoclaw.statusbar
```
The first column should show a PID (not `-`).
Tell the user:
> The bolt icon should now appear in your macOS menu bar. Click it to see NanoClaw's status and control the service.
>
> - **Green dot** — NanoClaw is running
> - **Red dot** — NanoClaw is stopped
>
> Use **Restart** after making code changes, and **View Logs** to open the log file directly.
## Removal
```bash
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist
rm ~/Library/LaunchAgents/com.nanoclaw.statusbar.plist
rm dist/statusbar
```

View File

@@ -0,0 +1,147 @@
import AppKit
class StatusBarController: NSObject {
private var statusItem: NSStatusItem!
private var isRunning = false
private var timer: Timer?
private let plistPath = "\(NSHomeDirectory())/Library/LaunchAgents/com.nanoclaw.plist"
/// Derive the NanoClaw project root from the binary location.
/// The binary is compiled to {project}/dist/statusbar, so the parent of
/// the parent directory is the project root.
private static let projectRoot: String = {
let binary = URL(fileURLWithPath: CommandLine.arguments[0]).resolvingSymlinksInPath()
return binary.deletingLastPathComponent().deletingLastPathComponent().path
}()
override init() {
super.init()
setupStatusItem()
isRunning = checkRunning()
updateMenu()
// Poll every 5 seconds to reflect external state changes
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
guard let self else { return }
let current = self.checkRunning()
if current != self.isRunning {
self.isRunning = current
self.updateMenu()
}
}
}
private func setupStatusItem() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem.button {
if let image = NSImage(systemSymbolName: "bolt.fill", accessibilityDescription: "NanoClaw") {
image.isTemplate = true
button.image = image
} else {
button.title = ""
}
button.toolTip = "NanoClaw"
}
}
private func checkRunning() -> Bool {
let task = Process()
task.launchPath = "/bin/launchctl"
task.arguments = ["list", "com.nanoclaw"]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = Pipe()
guard (try? task.run()) != nil else { return false }
task.waitUntilExit()
if task.terminationStatus != 0 { return false }
let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
// launchctl list output: "PID\tExitCode\tLabel" "-" means not running
let pid = output.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: "\t").first ?? "-"
return pid != "-"
}
private func updateMenu() {
let menu = NSMenu()
// Status row with colored dot
let statusItem = NSMenuItem()
let dot = ""
let dotColor: NSColor = isRunning ? .systemGreen : .systemRed
let attr = NSMutableAttributedString(string: dot, attributes: [.foregroundColor: dotColor])
let label = isRunning ? "NanoClaw is running" : "NanoClaw is stopped"
attr.append(NSAttributedString(string: label, attributes: [.foregroundColor: NSColor.labelColor]))
statusItem.attributedTitle = attr
statusItem.isEnabled = false
menu.addItem(statusItem)
menu.addItem(NSMenuItem.separator())
if isRunning {
let stop = NSMenuItem(title: "Stop", action: #selector(stopService), keyEquivalent: "")
stop.target = self
menu.addItem(stop)
let restart = NSMenuItem(title: "Restart", action: #selector(restartService), keyEquivalent: "r")
restart.target = self
menu.addItem(restart)
} else {
let start = NSMenuItem(title: "Start", action: #selector(startService), keyEquivalent: "")
start.target = self
menu.addItem(start)
}
menu.addItem(NSMenuItem.separator())
let logs = NSMenuItem(title: "View Logs", action: #selector(viewLogs), keyEquivalent: "")
logs.target = self
menu.addItem(logs)
self.statusItem.menu = menu
}
@objc private func startService() {
run("/bin/launchctl", ["load", plistPath])
refresh(after: 2)
}
@objc private func stopService() {
run("/bin/launchctl", ["unload", plistPath])
refresh(after: 2)
}
@objc private func restartService() {
let uid = getuid()
run("/bin/launchctl", ["kickstart", "-k", "gui/\(uid)/com.nanoclaw"])
refresh(after: 3)
}
@objc private func viewLogs() {
let logPath = "\(StatusBarController.projectRoot)/logs/nanoclaw.log"
NSWorkspace.shared.open(URL(fileURLWithPath: logPath))
}
private func refresh(after seconds: Double) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
guard let self else { return }
self.isRunning = self.checkRunning()
self.updateMenu()
}
}
@discardableResult
private func run(_ path: String, _ args: [String]) -> Int32 {
let task = Process()
task.launchPath = path
task.arguments = args
task.standardOutput = Pipe()
task.standardError = Pipe()
try? task.run()
task.waitUntilExit()
return task.terminationStatus
}
}
let app = NSApplication.shared
app.setActivationPolicy(.accessory)
let controller = StatusBarController()
app.run()

View File

@@ -121,8 +121,48 @@ def find_group(groups: list[dict], query: str) -> dict | None:
return None
def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -> None:
cmd = [runtime, "run", "-i", "--rm", image]
def build_mounts(folder: str, is_main: bool) -> list[tuple[str, str, bool]]:
"""Return list of (host_path, container_path, readonly) tuples."""
groups_dir = NANOCLAW_DIR / "groups"
data_dir = NANOCLAW_DIR / "data"
sessions_dir = data_dir / "sessions" / folder
ipc_dir = data_dir / "ipc" / folder
# Ensure required dirs exist
group_dir = groups_dir / folder
group_dir.mkdir(parents=True, exist_ok=True)
(sessions_dir / ".claude").mkdir(parents=True, exist_ok=True)
for sub in ("messages", "tasks", "input"):
(ipc_dir / sub).mkdir(parents=True, exist_ok=True)
agent_runner_src = sessions_dir / "agent-runner-src"
project_agent_runner = NANOCLAW_DIR / "container" / "agent-runner" / "src"
if not agent_runner_src.exists() and project_agent_runner.exists():
import shutil
shutil.copytree(project_agent_runner, agent_runner_src)
mounts: list[tuple[str, str, bool]] = []
if is_main:
mounts.append((str(NANOCLAW_DIR), "/workspace/project", True))
mounts.append((str(group_dir), "/workspace/group", False))
mounts.append((str(sessions_dir / ".claude"), "/home/node/.claude", False))
mounts.append((str(ipc_dir), "/workspace/ipc", False))
if agent_runner_src.exists():
mounts.append((str(agent_runner_src), "/app/src", False))
return mounts
def run_container(runtime: str, image: str, payload: dict,
folder: str | None = None, is_main: bool = False,
timeout: int = 300) -> None:
cmd = [runtime, "run", "-i", "--rm"]
if folder:
for host, container, readonly in build_mounts(folder, is_main):
if readonly:
cmd += ["--mount", f"type=bind,source={host},target={container},readonly"]
else:
cmd += ["-v", f"{host}:{container}"]
cmd.append(image)
dbg(f"cmd: {' '.join(cmd)}")
# Show payload sans secrets
@@ -167,7 +207,12 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -
dbg("output sentinel found, terminating container")
done.set()
try:
proc.kill()
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
dbg("graceful stop timed out, force killing container")
proc.kill()
except ProcessLookupError:
pass
return
@@ -197,6 +242,8 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -
stdout,
re.DOTALL,
)
success = False
if match:
try:
data = json.loads(match.group(1))
@@ -206,6 +253,7 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -
session_id = data.get("newSessionId") or data.get("sessionId")
if session_id:
print(f"\n[session: {session_id}]", file=sys.stderr)
success = True
else:
print(f"[{status}] {data.get('result', '')}", file=sys.stderr)
sys.exit(1)
@@ -215,6 +263,9 @@ def run_container(runtime: str, image: str, payload: dict, timeout: int = 300) -
# No structured output — print raw stdout
print(stdout)
if success:
return
if proc.returncode not in (0, None):
sys.exit(proc.returncode)
@@ -273,6 +324,7 @@ def main():
# Resolve group → jid
jid = args.jid
group_name = None
group_folder = None
is_main = False
if args.group:
@@ -281,6 +333,7 @@ def main():
sys.exit(f"error: group '{args.group}' not found. Run --list-groups to see options.")
jid = g["jid"]
group_name = g["name"]
group_folder = g["folder"]
is_main = g["is_main"]
elif not jid:
# Default: main group
@@ -288,6 +341,7 @@ def main():
if mains:
jid = mains[0]["jid"]
group_name = mains[0]["name"]
group_folder = mains[0]["folder"]
is_main = True
else:
sys.exit("error: no group specified and no main group found. Use -g or -j.")
@@ -311,7 +365,9 @@ def main():
payload["resumeAt"] = "latest"
print(f"[{group_name or jid}] running via {runtime}...", file=sys.stderr)
run_container(runtime, args.image, payload, timeout=args.timeout)
run_container(runtime, args.image, payload,
folder=group_folder, is_main=is_main,
timeout=args.timeout)
if __name__ == "__main__":

View File

@@ -0,0 +1,276 @@
---
name: init-onecli
description: Install and initialize OneCLI Agent Vault. Migrates existing .env credentials to the vault. Use after /update-nanoclaw brings in OneCLI as a breaking change, or for first-time OneCLI setup.
---
# Initialize OneCLI Agent Vault
This skill installs OneCLI, configures the Agent Vault gateway, and migrates any existing `.env` credentials into it. Run this after `/update-nanoclaw` introduces OneCLI as a breaking change, or any time OneCLI needs to be set up from scratch.
**Principle:** When something is broken or missing, fix it. Don't tell the user to go fix it themselves unless it genuinely requires their manual action (e.g. pasting a token).
## Phase 1: Pre-flight
### Check if OneCLI is already working
```bash
onecli version 2>/dev/null
```
If the command succeeds, OneCLI is installed. Check if the gateway is reachable:
```bash
curl -sf http://127.0.0.1:10254/health
```
If both succeed, check for an Anthropic secret:
```bash
onecli secrets list
```
If an Anthropic secret exists, tell the user OneCLI is already configured and working. Use AskUserQuestion:
1. **Keep current setup** — description: "OneCLI is installed and has credentials configured. Nothing to do."
2. **Reconfigure** — description: "Start fresh — reinstall OneCLI and re-register credentials."
If they choose to keep, skip to Phase 5 (Verify). If they choose to reconfigure, continue.
### Check for native credential proxy
```bash
grep "credential-proxy" src/index.ts 2>/dev/null
```
If `startCredentialProxy` is imported, the native credential proxy skill is active. Tell the user: "You're currently using the native credential proxy (`.env`-based). This skill will switch you to OneCLI's Agent Vault, which adds per-agent policies and rate limits. Your `.env` credentials will be migrated to the vault."
Use AskUserQuestion:
1. **Continue** — description: "Switch to OneCLI Agent Vault."
2. **Cancel** — description: "Keep the native credential proxy."
If they cancel, stop.
### Check the codebase expects OneCLI
```bash
grep "@onecli-sh/sdk" package.json
```
If `@onecli-sh/sdk` is NOT in package.json, the codebase hasn't been updated to use OneCLI yet. Tell the user to run `/update-nanoclaw` first to get the OneCLI integration, then retry `/init-onecli`. Stop here.
## Phase 2: Install OneCLI
### Install the gateway and CLI
```bash
curl -fsSL onecli.sh/install | sh
curl -fsSL onecli.sh/cli/install | sh
```
Verify: `onecli version`
If the command is not found, the CLI was likely installed to `~/.local/bin/`. Add it to PATH:
```bash
export PATH="$HOME/.local/bin:$PATH"
grep -q '.local/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
grep -q '.local/bin' ~/.zshrc 2>/dev/null || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
```
Re-verify with `onecli version`.
### Configure the CLI
Point the CLI at the local OneCLI instance:
```bash
onecli config set api-host http://127.0.0.1:10254
```
### Set ONECLI_URL in .env
```bash
grep -q 'ONECLI_URL' .env 2>/dev/null || echo 'ONECLI_URL=http://127.0.0.1:10254' >> .env
```
### Wait for gateway readiness
The gateway may take a moment to start after installation. Poll for up to 15 seconds:
```bash
for i in $(seq 1 15); do
curl -sf http://127.0.0.1:10254/health && break
sleep 1
done
```
If it never becomes healthy, check if the gateway process is running:
```bash
ps aux | grep -i onecli | grep -v grep
```
If it's not running, try starting it manually: `onecli start`. If that fails, show the error and stop — the user needs to debug their OneCLI installation.
## Phase 3: Migrate existing credentials
### Scan .env for credentials to migrate
Read the `.env` file and look for these credential variables:
| .env variable | OneCLI secret type | Host pattern |
|---|---|---|
| `ANTHROPIC_API_KEY` | `anthropic` | `api.anthropic.com` |
| `CLAUDE_CODE_OAUTH_TOKEN` | `anthropic` | `api.anthropic.com` |
| `ANTHROPIC_AUTH_TOKEN` | `anthropic` | `api.anthropic.com` |
Read `.env`:
```bash
cat .env
```
Parse the file for any of the credential variables listed above.
### If credentials found in .env
For each credential found, migrate it to OneCLI:
**Anthropic API key** (`ANTHROPIC_API_KEY=sk-ant-...`):
```bash
onecli secrets create --name Anthropic --type anthropic --value <key> --host-pattern api.anthropic.com
```
**Claude OAuth token** (`CLAUDE_CODE_OAUTH_TOKEN=...` or `ANTHROPIC_AUTH_TOKEN=...`):
```bash
onecli secrets create --name Anthropic --type anthropic --value <token> --host-pattern api.anthropic.com
```
After successful migration, remove the credential lines from `.env`. Use the Edit tool to remove only the credential variable lines (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN`). Keep all other `.env` entries intact (e.g. `ONECLI_URL`, `TELEGRAM_BOT_TOKEN`, channel tokens).
Verify the secret was registered:
```bash
onecli secrets list
```
Tell the user: "Migrated your Anthropic credentials from `.env` to the OneCLI Agent Vault. The raw keys have been removed from `.env` — they're now managed by OneCLI and will be injected at request time without entering containers."
### Offer to migrate other container-facing credentials
After handling Anthropic credentials (whether migrated or freshly registered), scan `.env` again for remaining credential variables that containers use for outbound API calls.
**Important:** Only migrate credentials that containers use via outbound HTTPS. Channel tokens (`TELEGRAM_BOT_TOKEN`, `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `DISCORD_BOT_TOKEN`) are used by the NanoClaw host process to connect to messaging platforms — they must stay in `.env`.
Known container-facing credentials:
| .env variable | Secret name | Host pattern |
|---|---|---|
| `OPENAI_API_KEY` | `OpenAI` | `api.openai.com` |
| `PARALLEL_API_KEY` | `Parallel` | `api.parallel.ai` |
If any of these are found with non-empty values, present them to the user:
AskUserQuestion (multiSelect): "These credentials are used by container agents for outbound API calls. Moving them to the vault means agents never see the raw keys, and you can apply rate limits and policies."
- One option per credential found (e.g., "OPENAI_API_KEY" — description: "Used by voice transcription and other OpenAI integrations inside containers")
- **Skip — keep them in .env** — description: "Leave these in .env for now. You can move them later."
For each credential the user selects:
```bash
onecli secrets create --name <SecretName> --type api_key --value <value> --host-pattern <host>
```
If there are credential variables not in the table above that look container-facing (i.e. not a channel token), ask the user: "Is `<VARIABLE_NAME>` used by agents inside containers? If so, what API host does it authenticate against? (e.g., `api.example.com`)" — then migrate accordingly.
After migration, remove the migrated lines from `.env` using the Edit tool. Keep channel tokens and any credentials the user chose not to migrate.
Verify all secrets were registered:
```bash
onecli secrets list
```
### If no credentials found in .env
No migration needed. Proceed to register credentials fresh.
Check if OneCLI already has an Anthropic secret:
```bash
onecli secrets list
```
If an Anthropic secret already exists, skip to Phase 4.
Otherwise, register credentials using the same flow as `/setup`:
AskUserQuestion: Do you want to use your **Claude subscription** (Pro/Max) or an **Anthropic API key**?
1. **Claude subscription (Pro/Max)** — description: "Uses your existing Claude Pro or Max subscription. You'll run `claude setup-token` in another terminal to get your token."
2. **Anthropic API key** — description: "Pay-per-use API key from console.anthropic.com."
#### Subscription path
Tell the user to run `claude setup-token` in another terminal and copy the token it outputs. Do NOT collect the token in chat.
Once they have the token, AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI. Use type 'anthropic' and paste your token as the value."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_TOKEN --host-pattern api.anthropic.com`"
#### API key path
Tell the user to get an API key from https://console.anthropic.com/settings/keys if they don't have one.
AskUserQuestion with two options:
1. **Dashboard** — description: "Best if you have a browser on this machine. Open http://127.0.0.1:10254 and add the secret in the UI."
2. **CLI** — description: "Best for remote/headless servers. Run: `onecli secrets create --name Anthropic --type anthropic --value YOUR_KEY --host-pattern api.anthropic.com`"
#### After either path
Ask them to let you know when done.
**If the user's response happens to contain a token or key** (starts with `sk-ant-` or looks like a token): handle it gracefully — run the `onecli secrets create` command with that value on their behalf.
**After user confirms:** verify with `onecli secrets list` that an Anthropic secret exists. If not, ask again.
## Phase 4: Build and restart
```bash
npm run build
```
If build fails, diagnose and fix. Common issue: `@onecli-sh/sdk` not installed — run `npm install` first.
Restart the service:
- macOS (launchd): `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
- Linux (systemd): `systemctl --user restart nanoclaw`
- WSL/manual: stop and re-run `bash start-nanoclaw.sh`
## Phase 5: Verify
Check logs for successful OneCLI integration:
```bash
tail -30 logs/nanoclaw.log | grep -i "onecli\|gateway"
```
Expected: `OneCLI gateway config applied` messages when containers start.
If the service is running and a channel is configured, tell the user to send a test message to verify the agent responds.
Tell the user:
- OneCLI Agent Vault is now managing credentials
- Agents never see raw API keys — credentials are injected at the gateway level
- To manage secrets: `onecli secrets list`, or open http://127.0.0.1:10254
- To add rate limits or policies: `onecli rules create --help`
## Troubleshooting
**"OneCLI gateway not reachable" in logs:** The gateway isn't running. Check with `curl -sf http://127.0.0.1:10254/health`. Start it with `onecli start` if needed.
**Container gets no credentials:** Verify `ONECLI_URL` is set in `.env` and the gateway has an Anthropic secret (`onecli secrets list`).
**Old .env credentials still present:** This skill should have removed them. Double-check `.env` for `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, or `ANTHROPIC_AUTH_TOKEN` and remove them manually if still present.
**Port 10254 already in use:** Another OneCLI instance may be running. Check with `lsof -i :10254` and kill the old process, or configure a different port.

View File

@@ -64,6 +64,16 @@ This merges in:
If the merge reports conflicts beyond `package-lock.json`, resolve them by reading the conflicted files and understanding the intent of both sides.
### Update main group CLAUDE.md
Replace the OneCLI auth reference with the native proxy:
In `groups/main/CLAUDE.md`, replace:
> OneCLI manages credentials (including Anthropic auth) — run `onecli --help`.
with:
> The native credential proxy manages credentials (including Anthropic auth) via `.env` — see `src/credential-proxy.ts`.
### Validate code changes
```bash