mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c46b1e43d | |||
| 71453707ba | |||
| 0aa9e668f6 | |||
| 023128def5 | |||
| c4a1679666 | |||
| 2e40e17155 | |||
| 0835089a51 | |||
| c82f062d57 |
@@ -9,13 +9,13 @@ Installs [mnemon](https://github.com/mnemon-dev/mnemon) in the agent container i
|
||||
|
||||
## Provider Compatibility
|
||||
|
||||
mnemon hooks fire only under `--target claude-code`. Use this skill on agent groups that run the default Claude provider (`AGENT_PROVIDER=claude`). Confirm the provider before applying:
|
||||
mnemon hooks fire only under `--target claude-code`. Use this skill on agent groups that run the default Claude provider. The provider is the materialized `provider` key in each group's `container.json` (absent or `claude` = default Claude provider). Confirm it before applying:
|
||||
|
||||
```bash
|
||||
grep AGENT_PROVIDER .env groups/*/container.json 2>/dev/null
|
||||
grep -H '"provider"' groups/*/container.json 2>/dev/null # no match, or "provider": "claude" = Claude
|
||||
```
|
||||
|
||||
If a group uses a different provider (e.g. `AGENT_PROVIDER=opencode`), it spawns its own process and never invokes the `claude` CLI, so the hooks registered by `mnemon setup` do not run for that group.
|
||||
If a group sets a different provider (e.g. `"provider": "opencode"`), it spawns its own process and never invokes the `claude` CLI, so the hooks registered by `mnemon setup` do not run for that group.
|
||||
|
||||
## Phase 1: Pre-flight
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: add-opencode
|
||||
description: Use OpenCode as an agent provider (AGENT_PROVIDER=opencode). OpenRouter, OpenAI, Google, DeepSeek, etc. via OpenCode config — not the Anthropic Agent SDK. Per-session and per-group via agent_provider; host passes OPENCODE_* and XDG mount when spawning containers.
|
||||
description: Use OpenCode as an agent provider. OpenRouter, OpenAI, Google, DeepSeek, etc. via OpenCode config — not the Anthropic Agent SDK. Per group via `ncl groups config update --provider opencode`; host passes OPENCODE_* and XDG mount when spawning containers.
|
||||
---
|
||||
|
||||
# OpenCode agent provider
|
||||
|
||||
NanoClaw runs agents in a long-lived **poll loop** inside the container. The backend is selected with **`AGENT_PROVIDER`** (`claude` | `opencode` | `mock`).
|
||||
NanoClaw runs agents in a long-lived **poll loop** inside the container. The backend is selected per agent group by the **`provider`** key in that group's `container.json` (materialized from the `container_configs` table) — set it with `ncl groups config update --provider opencode`. Default is `claude`.
|
||||
|
||||
Trunk ships with only the `claude` provider baked in. This skill copies the OpenCode provider files in from the `providers` branch, wires them into the host and container barrels, installs dependencies, and rebuilds the image.
|
||||
|
||||
@@ -148,7 +148,7 @@ done
|
||||
|
||||
Set model/provider strings in the form OpenCode expects (often `provider/model-id`). **Put comments on their own lines** — a `#` inside a value is kept verbatim and breaks model IDs.
|
||||
|
||||
These variables are read **on the host** and passed into the container only when the effective provider is `opencode`. They do not switch the provider by themselves; the DB still needs `agent_provider` set (below).
|
||||
These variables are read **on the host** and passed into the container only when the effective provider is `opencode`. They do not switch the provider by themselves; the group still needs `provider` set to `opencode` (see [Select the provider](#select-the-provider) below).
|
||||
|
||||
- `OPENCODE_PROVIDER` — OpenCode provider id, e.g. `openrouter`, `anthropic`, `deepseek`.
|
||||
- `OPENCODE_MODEL` — full model id in `provider/model` form, e.g. `deepseek/deepseek-chat`.
|
||||
@@ -215,7 +215,7 @@ OPENCODE_SMALL_MODEL=anthropic/claude-haiku-4-5-20251001
|
||||
|
||||
Zen's HTTP API (e.g. `POST …/zen/v1/messages`) expects the key in the **`x-api-key`** header. If OneCLI injects **`Authorization: Bearer …`** only, Zen often returns **401 / "Missing API key"** even though the gateway is working.
|
||||
|
||||
**Naming:** NanoClaw **`AGENT_PROVIDER=opencode`** (DB `agent_provider`) means "run the **OpenCode agent provider**." Separately, **`OPENCODE_PROVIDER=opencode`** in `.env` is OpenCode's **Zen provider id** inside the OpenCode config (see [Zen docs](https://opencode.ai/docs/zen/)).
|
||||
**Naming:** NanoClaw's **`provider: opencode`** (the `container.json` key, set via `ncl groups config update --provider opencode`) means "run the **OpenCode agent provider**." Separately, **`OPENCODE_PROVIDER=opencode`** in `.env` is OpenCode's **Zen provider id** inside the OpenCode config (see [Zen docs](https://opencode.ai/docs/zen/)).
|
||||
|
||||
**Host `.env` (typical Zen shape):**
|
||||
|
||||
@@ -236,9 +236,16 @@ onecli secrets create --name "OpenCode Zen" --type generic \
|
||||
--header-name "x-api-key" --value-format "{value}"
|
||||
```
|
||||
|
||||
### Per group / per session
|
||||
### Select the provider
|
||||
|
||||
Set `"provider": "opencode"` in the group's **`container.json`** (`groups/<folder>/container.json`) — the in-container runner reads `provider` from there, not from the DB. The DB columns **`agent_groups.agent_provider`** and **`sessions.agent_provider`** (session overrides group) only drive host-side provider contribution — per-session XDG mount, `OPENCODE_*` env passthrough — and do not propagate into `container.json` at spawn time. Set both, or just edit `container.json`; if they disagree, the runner uses `container.json` and the host-side resolver falls back through session → group → `container.json` → `'claude'`.
|
||||
Per group, from the host:
|
||||
|
||||
```bash
|
||||
ncl groups config update --id <group-id> --provider opencode
|
||||
ncl groups restart --id <group-id>
|
||||
```
|
||||
|
||||
`ncl groups config update --provider` writes the `provider` value into the `container_configs` table; the host materializes it into `groups/<folder>/container.json` at spawn time and the in-container runner reads `provider` from there (defaulting to `claude`). The restart picks up the change. Switching is an operator action — run it from the host. Memory does NOT carry over automatically between providers — run `/migrate-memory` to carry it across.
|
||||
|
||||
Extra MCP servers still come from **`NANOCLAW_MCP_SERVERS`** / `container_config.mcpServers` on the host; the runner merges them into the same `mcpServers` object passed to **both** Claude and OpenCode providers.
|
||||
|
||||
@@ -250,6 +257,6 @@ Extra MCP servers still come from **`NANOCLAW_MCP_SERVERS`** / `container_config
|
||||
|
||||
## Next Steps
|
||||
|
||||
The registration and Dockerfile guards in step 7 verify the wiring. To confirm an end-to-end round-trip, set `agent_provider = 'opencode'` (or `"provider": "opencode"` in the group's `container.json`) on a test group, register the matching provider key in OneCLI, and send a message. A clean exchange returns the model's reply with no `Unknown provider: opencode` error and no UUID/session warnings in the logs.
|
||||
The registration and Dockerfile guards in step 7 verify the wiring. To confirm an end-to-end round-trip, switch a test group with `ncl groups config update --id <group-id> --provider opencode && ncl groups restart --id <group-id>`, register the matching provider key in OneCLI, and send a message. A clean exchange returns the model's reply with no `Unknown provider: opencode` error and no UUID/session warnings in the logs.
|
||||
|
||||
To remove this provider, see [REMOVE.md](REMOVE.md).
|
||||
|
||||
@@ -71,7 +71,7 @@ For ad-hoc queries from skills or scripts, use the in-tree wrapper rather than t
|
||||
| `src/command-gate.ts` | Router-side admin command gate — queries `user_roles` directly (no env var, no container-side check) |
|
||||
| `src/modules/approvals/onecli-approvals.ts` | OneCLI credentialed-action approval bridge |
|
||||
| `src/modules/permissions/user-dm.ts` | Cold-DM resolution + `user_dms` cache |
|
||||
| `src/group-init.ts` | Per-agent-group filesystem scaffold (CLAUDE.md, skills, agent-runner-src overlay) |
|
||||
| `src/group-init.ts` | Per-agent-group filesystem scaffold (CLAUDE.md, skills) — agent-runner source is a shared read-only mount, not copied per group |
|
||||
| `src/db/container-configs.ts` | CRUD for `container_configs` table (per-group container runtime config) |
|
||||
| `src/backfill-container-configs.ts` | Migrates legacy `container.json` files into the DB on startup |
|
||||
| `src/container-restart.ts` | Kill + on-wake respawn for agent group containers |
|
||||
@@ -79,8 +79,8 @@ For ad-hoc queries from skills or scripts, use the in-tree wrapper rather than t
|
||||
| `src/channels/` | Channel adapter infra (registry, Chat SDK bridge); specific channel adapters are skill-installed from the `channels` branch |
|
||||
| `src/providers/` | Host-side provider container-config (`claude` baked in; `opencode` etc. installed from the `providers` branch) |
|
||||
| `container/agent-runner/src/` | Agent-runner: poll loop, formatter, provider abstraction, MCP tools, destinations |
|
||||
| `container/skills/` | Container skills mounted into every agent session (`onecli-gateway`, `welcome`, `self-customize`, `agent-browser`, `slack-formatting`) |
|
||||
| `groups/<folder>/` | Per-agent-group filesystem (CLAUDE.md, skills, per-group `agent-runner-src/` overlay) |
|
||||
| `container/skills/` | Container skills mounted into every agent session (`agent-browser`, `frontend-engineer`, `onecli-gateway`, `self-customize`, `slack-formatting`, `vercel-cli`, `welcome`, `whatsapp-formatting`) |
|
||||
| `groups/<folder>/` | Per-agent-group filesystem (CLAUDE.md, skills) — agent-runner source is a shared read-only mount, not copied per group |
|
||||
| `scripts/init-first-agent.ts` | Bootstrap the first DM-wired agent (used by `/init-first-agent` skill) |
|
||||
| `migrate-v2.sh` + `setup/migrate-v2/` | v1→v2 migration. Standalone script: `bash migrate-v2.sh`. Seeds DB, copies groups/sessions, installs channels, builds container, offers service switchover, then hands off to `/migrate-from-v1` skill for owner setup and CLAUDE.md cleanup. See [docs/migration-dev.md](docs/migration-dev.md). |
|
||||
| `nanoclaw.sh --uninstall` + `setup/uninstall/` | Uninstall this copy only (slug-scoped): service, containers + image, `data/`, `logs/`, `groups/`, this copy's OneCLI agents. Confirms per group; `--dry-run` previews, `--yes` skips prompts. Other copies and the shared OneCLI app are untouched. Bypasses bootstrap entirely; `uninstall.sh` is a pointer that execs it. |
|
||||
@@ -182,7 +182,7 @@ Four types of skills. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxono
|
||||
- **Channel/provider install skills** — copy the relevant module(s) in from the `channels` or `providers` branch, wire imports, install pinned deps (e.g. `/add-discord`, `/add-slack`, `/add-whatsapp`, `/add-opencode`).
|
||||
- **Utility skills** — ship code files alongside `SKILL.md` (e.g. a `scripts/` CLI or helper).
|
||||
- **Operational skills** — instruction-only workflows (`/setup`, `/debug`, `/customize`, `/init-first-agent`, `/manage-channels`, `/init-onecli`, `/update-nanoclaw`).
|
||||
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`: `onecli-gateway`, `welcome`, `self-customize`, `agent-browser`, `slack-formatting`).
|
||||
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`: `agent-browser`, `frontend-engineer`, `onecli-gateway`, `self-customize`, `slack-formatting`, `vercel-cli`, `welcome`, `whatsapp-formatting`).
|
||||
|
||||
| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
|
||||
@@ -6,6 +6,5 @@ The files in this directory are original design documents and developer referenc
|
||||
|
||||
| This directory | Documentation site |
|
||||
|---|---|
|
||||
| [SPEC.md](SPEC.md) | [Architecture](https://docs.nanoclaw.dev/concepts/architecture) |
|
||||
| [SECURITY.md](SECURITY.md) | [Security model](https://docs.nanoclaw.dev/concepts/security) |
|
||||
| [REQUIREMENTS.md](REQUIREMENTS.md) | [Introduction](https://docs.nanoclaw.dev/introduction) |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# NanoClaw Specification
|
||||
|
||||
> **⚠️ Historical v1 spec.** This document describes the original NanoClaw v1 architecture — the single `store/messages.db`, the file-based IPC watcher, the `task-scheduler.ts` loop, the `MAX_CONCURRENT_CONTAINERS` cap, and the `groups/{channel}_{name}/` folder convention. **None of these exist in v2.** v2 replaced them with the two-DB session split (`inbound.db`/`outbound.db`), the entity model (users → messaging groups → agent groups → sessions), and the system-action delivery path. Kept for reference only. For the current architecture start at [architecture.md](architecture.md) and the root [CLAUDE.md](../CLAUDE.md); the v1→v2 diff is in [v1-to-v2-changes.md](v1-to-v2-changes.md).
|
||||
|
||||
A personal Claude assistant with multi-channel support, persistent memory per conversation, scheduled tasks, and container-isolated agent execution.
|
||||
|
||||
---
|
||||
|
||||
@@ -596,7 +596,7 @@ Schedule a one-shot or recurring task.
|
||||
}
|
||||
```
|
||||
|
||||
Implementation: write a `messages_in` row (to self) with `kind: 'task'`, `process_after`, and optionally `recurrence`. The host sweep picks it up when due.
|
||||
Implementation: the container can't write host-owned `inbound.db`, so this writes a `messages_out` row with `kind: 'system'` and `action: 'schedule_task'` (`container/agent-runner/src/mcp-tools/scheduling.ts`). During delivery the host's action handler (`src/modules/scheduling/actions.ts` → `insertTask()` in `src/modules/scheduling/db.ts`) inserts the `kind: 'task'` row into `inbound.db` with `process_after` and optionally `recurrence`. The host sweep picks it up when due.
|
||||
|
||||
#### list_tasks
|
||||
|
||||
@@ -609,7 +609,7 @@ List active scheduled/recurring tasks.
|
||||
}
|
||||
```
|
||||
|
||||
Implementation: query `messages_in WHERE recurrence IS NOT NULL AND status != 'failed'`.
|
||||
Implementation: a read, not a write — the container may read the read-only `inbound.db` mount directly. Returns one row per series (the live pending/paused occurrence): `SELECT series_id AS id, ... FROM messages_in WHERE kind = 'task' AND status IN ('pending','paused') GROUP BY series_id`. See `container/agent-runner/src/mcp-tools/scheduling.ts`.
|
||||
|
||||
#### cancel_task / pause_task / resume_task / update_task
|
||||
|
||||
@@ -625,7 +625,7 @@ Modify a scheduled task.
|
||||
// update_task: merge { prompt?, recurrence?, processAfter?, script? } into the live row
|
||||
```
|
||||
|
||||
Implementation: cancel/pause/resume update the live row(s) directly. update_task is sent as a system action — the host reads current content, merges supplied fields, and writes back. All four match by `(id = ? OR series_id = ?) AND kind='task' AND status IN ('pending','paused')`, so they reach the live next occurrence of a recurring task even when the agent passes the original (now-completed) id.
|
||||
Implementation: all four are sent as system actions (`messages_out`, `kind: 'system'`, `action: 'cancel_task' | 'pause_task' | 'resume_task' | 'update_task'`) — the container never writes `inbound.db`. The host's handlers in `src/modules/scheduling/actions.ts` apply the change against `inbound.db` via `src/modules/scheduling/db.ts`: cancel/pause/resume flip status on the live row(s); update_task reads current content, merges supplied fields, and writes back. All four match by `(id = ? OR series_id = ?) AND kind='task' AND status IN ('pending','paused')`, so they reach the live next occurrence of a recurring task even when the agent passes the original (now-completed) id.
|
||||
|
||||
#### register_agent_group
|
||||
|
||||
@@ -712,9 +712,9 @@ These are ephemeral to the container's lifetime. When the container is killed an
|
||||
|
||||
The agent-runner receives configuration via:
|
||||
|
||||
- **Environment variables:** `AGENT_PROVIDER` (claude/codex/opencode), `NANOCLAW_ADMIN_USER_ID`, provider-specific vars (API keys, model overrides), `TZ`
|
||||
- **`container.json`:** The provider name, model, assistant name, MCP servers, and other NanoClaw config are read from `/workspace/agent/container.json` (materialized by the host from the `container_configs` table), not from environment variables. See `container/agent-runner/src/config.ts`.
|
||||
- **Environment variables:** provider-specific vars only (API keys, model overrides), `TZ`.
|
||||
- **Fixed mount paths:** Session DB at `/workspace/session.db`. Agent group folder at `/workspace/agent/`. System prompt from `/workspace/agent/CLAUDE.md` and `/workspace/global/CLAUDE.md`.
|
||||
- **Optional startup config:** Some config may be passed as a JSON file at a fixed path (e.g., `/workspace/config.json`) for things like the session ID to resume, assistant name, and admin user ID. This avoids overloading environment variables.
|
||||
|
||||
The agent-runner reads config, creates the provider, and enters the poll loop. No stdin, no initial prompt — messages are already in the session DB.
|
||||
|
||||
@@ -731,7 +731,7 @@ function createProvider(name: ProviderName, config: ProviderConfig): AgentProvid
|
||||
}
|
||||
```
|
||||
|
||||
The provider name comes from the container's environment (`AGENT_PROVIDER` env var), set by the host based on `agent_groups.agent_provider` or `sessions.agent_provider`.
|
||||
The provider name comes from the `provider` key in `/workspace/agent/container.json` (defaulting to `'claude'`), which the host materializes from the `container_configs` table — set it with `ncl groups config update --provider`. It is not an environment variable.
|
||||
|
||||
`ProviderConfig` contains provider-specific settings (API keys, model overrides, etc.) passed via environment variables — not via the interface. Each provider reads what it needs from `env`.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# NanoClaw Architecture (Draft)
|
||||
|
||||
> **Draft — design intent, not a line-by-line spec.** Some passages predate the current implementation and can drift from it. The root [CLAUDE.md](../CLAUDE.md) and the cited source files (`src/`, `container/agent-runner/src/`) are the source of truth; when this doc and the code disagree, trust the code. Notably, scheduling MCP tools do **not** write `inbound.db` directly — they emit `messages_out` system actions that the host applies (see [agent-runner-details.md](agent-runner-details.md) and `src/modules/scheduling/`).
|
||||
|
||||
## Core Idea
|
||||
|
||||
Each agent session has a mounted SQLite DB. The DB is the one and only IO mechanism between host and container. No IPC files, no stdin piping. Two tables: messages_in (host → agent-runner) and messages_out (agent-runner → host). Everything is a message.
|
||||
@@ -128,7 +130,6 @@ Non-Chat-SDK channels (WhatsApp via Baileys, Gmail, custom integrations) impleme
|
||||
The host is an orchestrator:
|
||||
1. **Spawn** — when wakeUpAgent is called and no container exists for the session
|
||||
2. **Idle kill** — when a container has no unprocessed messages for some timeout period
|
||||
3. **Limits** — MAX_CONCURRENT_CONTAINERS caps active containers
|
||||
|
||||
When a container spins up, the agent-runner immediately starts polling its session DB. Messages are already there waiting.
|
||||
|
||||
@@ -244,7 +245,7 @@ One-shot and recurring tasks use the same tables — no separate scheduler.
|
||||
|
||||
**Active container poll** (~1s) checks the same conditions but only for sessions with running containers.
|
||||
|
||||
**Agent-runner creates schedules** by writing messages_in (to itself) or messages_out (reminders/notifications) with `process_after` and optionally `recurrence`.
|
||||
**Agent-runner creates schedules** by emitting a `messages_out` row with `kind: 'system'` and an `action` (`schedule_task`, `cancel_task`, …) — it cannot write host-owned `inbound.db` directly. The host applies the action during delivery (`src/modules/scheduling/actions.ts`), inserting/updating the `kind: 'task'` `messages_in` row with `process_after` and optionally `recurrence`.
|
||||
|
||||
### messages_in content by kind
|
||||
|
||||
@@ -829,7 +830,7 @@ Mixed batches (e.g., a chat message + a system result both pending) are combined
|
||||
|
||||
### MCP Tools
|
||||
|
||||
MCP tools write directly to the session DB.
|
||||
MCP tools write to the container's own `outbound.db`. Anything that needs a change in host-owned `inbound.db` (schedule/cancel/pause/resume/update a task, register a group) is emitted as a `kind: 'system'` `messages_out` action that the host applies during delivery — the container never writes `inbound.db`.
|
||||
|
||||
**Core tools:**
|
||||
|
||||
@@ -837,9 +838,9 @@ MCP tools write directly to the session DB.
|
||||
|------|-------------|
|
||||
| `send_message` | Write `messages_out` row, `kind: 'chat'` |
|
||||
| `send_file` | Move file to `outbox/{msg_id}/`, write `messages_out` with filenames |
|
||||
| `schedule_task` | Write `messages_in` row (to self) with `process_after` + `recurrence`. Or `messages_out` with `deliver_after` for outbound reminders. |
|
||||
| `list_tasks` | Query `messages_in WHERE recurrence IS NOT NULL` |
|
||||
| `pause_task` / `resume_task` / `cancel_task` | Modify `messages_in` rows (update status, clear/set recurrence) |
|
||||
| `schedule_task` | Write `messages_out`, `kind: 'system'`, `action: 'schedule_task'`; host inserts the `kind: 'task'` `messages_in` row with `process_after` + optional `recurrence` |
|
||||
| `list_tasks` | Read `messages_in` (read-only mount) — one row per series: `kind = 'task' AND status IN ('pending','paused') GROUP BY series_id` |
|
||||
| `pause_task` / `resume_task` / `cancel_task` | Write `messages_out`, `kind: 'system'`, matching `action`; host updates the live `messages_in` row(s) |
|
||||
| `register_agent_group` | Write `messages_out`, `kind: 'system'`, `action: 'register_agent_group'` |
|
||||
|
||||
**New tools:**
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ data/v2-sessions/<agent_group_id>/<session_id>/
|
||||
outbox/<message_id>/ ← attachments the agent produced
|
||||
```
|
||||
|
||||
One session = one folder = one pair of DBs. The `agent_group_id` parent directory also holds per-group state (`.claude-shared/`, `agent-runner-src/`) that is shared across every session of that agent group.
|
||||
One session = one folder = one pair of DBs. The `agent_group_id` parent directory also holds per-group state (`.claude-shared/`) that is shared across every session of that agent group. (The agent-runner source is not copied per group — it's a shared read-only mount from `container/agent-runner/src` into every container; see `src/container-runner.ts`.)
|
||||
|
||||
Path helpers in `src/session-manager.ts`: `sessionDir()`, `inboundDbPath()`, `outboundDbPath()`, `heartbeatPath()`.
|
||||
|
||||
@@ -55,7 +55,7 @@ CREATE INDEX idx_messages_in_series ON messages_in(series_id);
|
||||
|
||||
Content shapes: see [api-details.md §Session DB Schema Details](api-details.md#session-db-schema-details).
|
||||
|
||||
**Writers (host):** `insertMessage()`, `insertTask()`, `insertRecurrence()` — all in `src/db/session-db.ts`. Each calls `nextEvenSeq()`.
|
||||
**Writers (host):** `insertMessage()` (and `nextEvenSeq()`) in `src/db/session-db.ts`; `insertTask()` and `insertRecurrence()` in `src/modules/scheduling/db.ts`. Each calls `nextEvenSeq()`.
|
||||
**Reader (container):** `container/agent-runner/src/db/messages-in.ts` — polls `status='pending' AND (process_after IS NULL OR process_after <= now)`.
|
||||
|
||||
### 2.2 `delivered`
|
||||
|
||||
@@ -35,7 +35,6 @@ data/
|
||||
v2-sessions/
|
||||
<agent_group_id>/
|
||||
.claude-shared/ ← shared Claude state for the agent group
|
||||
agent-runner-src/ ← per-group agent-runner overlay
|
||||
<session_id>/
|
||||
inbound.db ← host writes, container reads
|
||||
outbound.db ← container writes, host reads
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install the Discord adapter, persist DISCORD_BOT_TOKEN / APPLICATION_ID /
|
||||
# PUBLIC_KEY to .env + data/env/env, and restart the service. Non-interactive —
|
||||
# PUBLIC_KEY to .env, and restart the service. Non-interactive —
|
||||
# the operator-facing "Create a bot" walkthrough, owner confirmation, and
|
||||
# server-invite step live in setup/channels/discord.ts. Credentials come in via
|
||||
# env vars: DISCORD_BOT_TOKEN, DISCORD_APPLICATION_ID, DISCORD_PUBLIC_KEY.
|
||||
@@ -105,10 +105,6 @@ upsert_env DISCORD_BOT_TOKEN "$DISCORD_BOT_TOKEN"
|
||||
upsert_env DISCORD_APPLICATION_ID "$DISCORD_APPLICATION_ID"
|
||||
upsert_env DISCORD_PUBLIC_KEY "$DISCORD_PUBLIC_KEY"
|
||||
|
||||
# Container reads from data/env/env (the host mounts it).
|
||||
mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
log "Restarting service so the new adapter picks up the credentials…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install the iMessage adapter, persist mode/creds to .env + data/env/env,
|
||||
# Install the iMessage adapter, persist mode/creds to .env,
|
||||
# and restart the service. Non-interactive — the Full Disk Access walkthrough
|
||||
# (local mode) and Photon URL/key prompts (remote mode) live in
|
||||
# setup/channels/imessage.ts. Creds come in via env vars:
|
||||
@@ -135,10 +135,6 @@ else
|
||||
remove_env IMESSAGE_ENABLED
|
||||
fi
|
||||
|
||||
# Container reads from data/env/env (the host mounts it).
|
||||
mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
log "Restarting service so the new adapter picks up the creds…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
|
||||
+1
-5
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Install the Slack adapter, persist SLACK_BOT_TOKEN plus the mode-specific
|
||||
# secret (SLACK_APP_TOKEN for Socket Mode, SLACK_SIGNING_SECRET for webhook) to
|
||||
# .env + data/env/env, and restart the service. Non-interactive — the
|
||||
# .env, and restart the service. Non-interactive — the
|
||||
# operator-facing app creation walkthrough + credential paste live in
|
||||
# setup/channels/slack.ts. Credentials come in via env vars:
|
||||
# SLACK_BOT_TOKEN, and SLACK_APP_TOKEN and/or SLACK_SIGNING_SECRET.
|
||||
@@ -108,10 +108,6 @@ if [ -n "${SLACK_SIGNING_SECRET:-}" ]; then
|
||||
upsert_env SLACK_SIGNING_SECRET "$SLACK_SIGNING_SECRET"
|
||||
fi
|
||||
|
||||
# Container reads from data/env/env (the host mounts it).
|
||||
mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
log "Restarting service so the new adapter picks up the credentials…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
|
||||
+1
-5
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install the Teams adapter, persist TEAMS_APP_ID / _PASSWORD / _TENANT_ID /
|
||||
# _TYPE to .env + data/env/env, and restart the service. Non-interactive —
|
||||
# _TYPE to .env, and restart the service. Non-interactive —
|
||||
# the operator-facing Azure portal walkthroughs live in
|
||||
# setup/channels/teams.ts. Credentials come in via env vars:
|
||||
# TEAMS_APP_ID (required)
|
||||
@@ -114,10 +114,6 @@ if [ -n "${TEAMS_APP_TENANT_ID:-}" ]; then
|
||||
upsert_env TEAMS_APP_TENANT_ID "$TEAMS_APP_TENANT_ID"
|
||||
fi
|
||||
|
||||
# Container reads from data/env/env (the host mounts it).
|
||||
mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
log "Restarting service so the new adapter picks up the credentials…"
|
||||
# shellcheck source=setup/lib/install-slug.sh
|
||||
source "$PROJECT_ROOT/setup/lib/install-slug.sh"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install the Telegram adapter, persist the bot token to .env + data/env/env,
|
||||
# Install the Telegram adapter, persist the bot token to .env,
|
||||
# restart the service, and open the bot's chat page in the local Telegram
|
||||
# client. Non-interactive — the operator-facing "Create a bot" instructions
|
||||
# and token paste live in setup/auto.ts. The token comes in via the
|
||||
@@ -134,10 +134,6 @@ if echo "$INFO" | grep -q '"ok":true'; then
|
||||
BOT_USERNAME=$(echo "$INFO" | sed -nE 's/.*"username":"([^"]+)".*/\1/p')
|
||||
fi
|
||||
|
||||
# Container reads from data/env/env (the host mounts it).
|
||||
mkdir -p data/env
|
||||
cp .env data/env/env
|
||||
|
||||
# Browser/app deep-link is done by the parent driver (setup/channels/telegram.ts)
|
||||
# BEFORE this script runs — gated on a clack confirm so focus-stealing doesn't
|
||||
# surprise the user. Keeping it out of here means this script stays pure
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 2. Install the adapter + qrcode via setup/add-signal.sh (idempotent).
|
||||
* 3. Run the signal-auth step, rendering each SIGNAL_AUTH_QR block as
|
||||
* a terminal QR the operator scans from Signal → Linked Devices.
|
||||
* 4. Persist SIGNAL_ACCOUNT to .env (+ data/env/env).
|
||||
* 4. Persist SIGNAL_ACCOUNT to .env.
|
||||
* 5. Kick the service so the adapter picks up the new credentials.
|
||||
* 6. Ask operator role + agent name.
|
||||
* 7. Wire the agent via scripts/init-first-agent.ts; the existing welcome
|
||||
@@ -333,7 +333,7 @@ async function renderQr(url: string): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist SIGNAL_ACCOUNT to .env and mirror to data/env/env for the container. */
|
||||
/** Persist SIGNAL_ACCOUNT to .env. */
|
||||
function writeSignalAccount(account: string): void {
|
||||
const envPath = path.join(process.cwd(), '.env');
|
||||
let contents = '';
|
||||
@@ -353,10 +353,6 @@ function writeSignalAccount(account: string): void {
|
||||
}
|
||||
fs.writeFileSync(envPath, contents);
|
||||
|
||||
const containerEnvDir = path.join(process.cwd(), 'data', 'env');
|
||||
fs.mkdirSync(containerEnvDir, { recursive: true });
|
||||
fs.copyFileSync(envPath, path.join(containerEnvDir, 'env'));
|
||||
|
||||
setupLog.userInput('signal_account', account);
|
||||
}
|
||||
|
||||
|
||||
@@ -433,7 +433,7 @@ async function askChatPhone(authedPhone: string): Promise<string> {
|
||||
return phone;
|
||||
}
|
||||
|
||||
/** Persist ASSISTANT_HAS_OWN_NUMBER=true to .env and data/env/env. */
|
||||
/** Persist ASSISTANT_HAS_OWN_NUMBER=true to .env. */
|
||||
function writeAssistantHasOwnNumber(): void {
|
||||
const envPath = path.join(process.cwd(), '.env');
|
||||
let contents = '';
|
||||
@@ -452,11 +452,6 @@ function writeAssistantHasOwnNumber(): void {
|
||||
contents += 'ASSISTANT_HAS_OWN_NUMBER=true\n';
|
||||
}
|
||||
fs.writeFileSync(envPath, contents);
|
||||
|
||||
// Container reads from data/env/env.
|
||||
const containerEnvDir = path.join(process.cwd(), 'data', 'env');
|
||||
fs.mkdirSync(containerEnvDir, { recursive: true });
|
||||
fs.copyFileSync(envPath, path.join(containerEnvDir, 'env'));
|
||||
}
|
||||
|
||||
async function resolveAgentName(): Promise<string> {
|
||||
|
||||
@@ -116,15 +116,6 @@ function main(): void {
|
||||
channelsProcessed++;
|
||||
}
|
||||
|
||||
// Sync to data/env/env
|
||||
if (fs.existsSync(v2EnvPath)) {
|
||||
const containerEnvDir = path.join(process.cwd(), 'data', 'env');
|
||||
try {
|
||||
fs.mkdirSync(containerEnvDir, { recursive: true });
|
||||
fs.copyFileSync(v2EnvPath, path.join(containerEnvDir, 'env'));
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
console.log(`OK:channels=${channelsProcessed},env_keys=${envKeysCopied},files=${filesCopied}`);
|
||||
if (missing.length > 0) {
|
||||
console.log(`MISSING:${missing.join(',')}`);
|
||||
|
||||
@@ -65,15 +65,6 @@ function main(): void {
|
||||
fs.writeFileSync(v2EnvPath, result);
|
||||
}
|
||||
|
||||
// Sync to data/env/env (container reads from here)
|
||||
const containerEnvDir = path.join(process.cwd(), 'data', 'env');
|
||||
try {
|
||||
fs.mkdirSync(containerEnvDir, { recursive: true });
|
||||
fs.copyFileSync(v2EnvPath, path.join(containerEnvDir, 'env'));
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
console.log(`OK:copied=${copied.length},skipped=${skipped.length}`);
|
||||
if (copied.length > 0) console.log(`COPIED:${copied.join(',')}`);
|
||||
}
|
||||
|
||||
+2
-14
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* Step: set-env — Write or update a KEY=VALUE in .env, with optional sync to
|
||||
* data/env/env (the container-mounted copy).
|
||||
* Step: set-env — Write or update a KEY=VALUE in .env.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm exec tsx setup/index.ts --step set-env -- \
|
||||
* --key TELEGRAM_BOT_TOKEN --value "<token>" [--sync-container]
|
||||
* --key TELEGRAM_BOT_TOKEN --value "<token>"
|
||||
*
|
||||
* Exists so channel-install flows don't have to invent grep/sed/rm pipelines
|
||||
* (which can't be allowlisted tightly — sed can read any file, and each
|
||||
@@ -21,7 +20,6 @@ import { emitStatus } from './status.js';
|
||||
export async function run(args: string[]): Promise<void> {
|
||||
const keyIdx = args.indexOf('--key');
|
||||
const valueIdx = args.indexOf('--value');
|
||||
const syncContainer = args.includes('--sync-container');
|
||||
|
||||
if (keyIdx === -1 || !args[keyIdx + 1]) {
|
||||
throw new Error('--key <KEY> is required');
|
||||
@@ -59,19 +57,9 @@ export async function run(args: string[]): Promise<void> {
|
||||
fs.writeFileSync(envFile, content);
|
||||
log.info('Updated .env', { key, existed });
|
||||
|
||||
let synced = false;
|
||||
if (syncContainer) {
|
||||
const dataEnvDir = path.join(projectRoot, 'data', 'env');
|
||||
fs.mkdirSync(dataEnvDir, { recursive: true });
|
||||
fs.copyFileSync(envFile, path.join(dataEnvDir, 'env'));
|
||||
synced = true;
|
||||
log.info('Synced .env to container mount', { path: 'data/env/env' });
|
||||
}
|
||||
|
||||
emitStatus('SET_ENV', {
|
||||
KEY: key,
|
||||
EXISTED: existed,
|
||||
SYNCED_TO_CONTAINER: synced,
|
||||
STATUS: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
*
|
||||
* Runs on every spawn from `container-runner.buildMounts()`. Deterministic —
|
||||
* same inputs produce the same CLAUDE.md, and stale fragments are pruned.
|
||||
*
|
||||
* See `docs/claude-md-composition.md` for the full design.
|
||||
* The composition order and fragment sources are documented inline above.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
Reference in New Issue
Block a user