npx denchclaw

This commit is contained in:
kumarabhirup 2026-03-04 13:23:34 -08:00
parent 1c93a3b525
commit 4d6eec741d
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
107 changed files with 587 additions and 579 deletions

View File

@ -1,18 +1,18 @@
---
name: Bootstrap dev testing
overview: Remove local OpenClaw paths from the web app, always use global `openclaw` binary, rename dev scripts to `ironclaw`, and verify bootstrap works standalone.
overview: Remove local OpenClaw paths from the web app, always use global `openclaw` binary, rename dev scripts to `denchclaw`, and verify bootstrap works standalone.
todos:
- id: remove-local-openclaw-agent-runner
content: Remove resolvePackageRoot, resolveOpenClawLaunch, IRONCLAW_USE_LOCAL_OPENCLAW from agent-runner.ts; spawn global `openclaw` directly
content: Remove resolvePackageRoot, resolveOpenClawLaunch, DENCHCLAW_USE_LOCAL_OPENCLAW from agent-runner.ts; spawn global `openclaw` directly
status: completed
- id: remove-local-openclaw-subagent-runs
content: Remove local script paths from subagent-runs.ts (sendGatewayAbortForSubagent, spawnSubagentMessage); use global `openclaw` instead
status: completed
- id: rename-pnpm-scripts
content: Rename `pnpm openclaw` to `pnpm ironclaw` and `openclaw:rpc` to `ironclaw:rpc` in package.json
content: Rename `pnpm openclaw` to `pnpm denchclaw` and `openclaw:rpc` to `denchclaw:rpc` in package.json
status: completed
- id: update-agent-runner-tests
content: "Update agent-runner.test.ts: remove resolvePackageRoot tests, IRONCLAW_USE_LOCAL_OPENCLAW, update spawn assertions"
content: "Update agent-runner.test.ts: remove resolvePackageRoot tests, DENCHCLAW_USE_LOCAL_OPENCLAW, update spawn assertions"
status: completed
- id: verify-builds-pass
content: Verify pnpm build, pnpm web:build, and workspace tests pass after changes
@ -20,16 +20,16 @@ todos:
isProject: false
---
# IronClaw Bootstrap: Clean Separation and Dev Testing
# DenchClaw Bootstrap: Clean Separation and Dev Testing
## Architecture
IronClaw is a frontend/UI/skills layer. OpenClaw is a separate, globally-installed runtime. IronClaw should NEVER bundle or run a local copy of OpenClaw.
DenchClaw is a frontend/UI/skills layer. OpenClaw is a separate, globally-installed runtime. DenchClaw should NEVER bundle or run a local copy of OpenClaw.
```mermaid
flowchart TD
npx["npx ironclaw (or ironclaw)"] --> entry["openclaw.mjs → dist/entry.js"]
entry --> runMain["run-main.ts: bare ironclaw → bootstrap"]
npx["npx denchclaw (or denchclaw)"] --> entry["openclaw.mjs → dist/entry.js"]
entry --> runMain["run-main.ts: bare denchclaw → bootstrap"]
runMain --> delegate{"primary == bootstrap?"}
delegate -->|yes, keep local| bootstrap["bootstrapCommand()"]
delegate -->|no, delegate| globalOC["spawn openclaw ...args"]
@ -45,7 +45,7 @@ flowchart TD
The bootstrap flow is correctly wired:
- Bare `ironclaw` rewrites to `ironclaw bootstrap`
- Bare `denchclaw` rewrites to `denchclaw bootstrap`
- `bootstrap` is never delegated to global `openclaw`
- `bootstrapCommand` calls `ensureOpenClawCliAvailable` which prompts to install
- Onboarding sets `gateway.webApp.enabled: true`
@ -54,22 +54,22 @@ The bootstrap flow is correctly wired:
## Problem 1: Local OpenClaw paths in web app (must remove)
`[apps/web/lib/agent-runner.ts](apps/web/lib/agent-runner.ts)` has `resolveOpenClawLaunch` which, when `IRONCLAW_USE_LOCAL_OPENCLAW=1`, resolves a local `scripts/run-node.mjs` or `openclaw.mjs` and spawns it with `node`. This contradicts the architecture: IronClaw should always spawn the global `openclaw` binary.
`[apps/web/lib/agent-runner.ts](apps/web/lib/agent-runner.ts)` has `resolveOpenClawLaunch` which, when `DENCHCLAW_USE_LOCAL_OPENCLAW=1`, resolves a local `scripts/run-node.mjs` or `openclaw.mjs` and spawns it with `node`. This contradicts the architecture: DenchClaw should always spawn the global `openclaw` binary.
The same pattern exists in `[apps/web/lib/subagent-runs.ts](apps/web/lib/subagent-runs.ts)` where `sendGatewayAbortForSubagent` and `spawnSubagentMessage` hardcode `node <local-script>` paths.
**Fix:**
- Remove `IRONCLAW_USE_LOCAL_OPENCLAW`, `resolveOpenClawLaunch`, `resolvePackageRoot`, and `OpenClawLaunch` type from `agent-runner.ts`
- Remove `DENCHCLAW_USE_LOCAL_OPENCLAW`, `resolveOpenClawLaunch`, `resolvePackageRoot`, and `OpenClawLaunch` type from `agent-runner.ts`
- All spawn calls become `spawn("openclaw", [...args], { env, stdio })`
- In `subagent-runs.ts`: replace `node <scriptPath> gateway call ...` with `openclaw gateway call ...`
- Remove `resolvePackageRoot` import from `subagent-runs.ts`
## Problem 2: `pnpm openclaw` script name is wrong
`package.json` has `"openclaw": "node scripts/run-node.mjs"`. This repo IS IronClaw, not OpenClaw.
`package.json` has `"openclaw": "node scripts/run-node.mjs"`. This repo IS DenchClaw, not OpenClaw.
**Fix:** Rename to `"ironclaw": "node scripts/run-node.mjs"`. Also `"openclaw:rpc"` to `"ironclaw:rpc"`.
**Fix:** Rename to `"denchclaw": "node scripts/run-node.mjs"`. Also `"openclaw:rpc"` to `"denchclaw:rpc"`.
## Dev workflow (after fixes)
@ -77,11 +77,11 @@ The same pattern exists in `[apps/web/lib/subagent-runs.ts](apps/web/lib/subagen
# Prerequisite: install OpenClaw globally (one-time)
npm install -g openclaw
# Run IronClaw bootstrap (installs/configures everything, opens UI)
pnpm ironclaw
# Run DenchClaw bootstrap (installs/configures everything, opens UI)
pnpm denchclaw
# Or for web UI dev only:
openclaw --profile ironclaw gateway --port 18789 # Terminal 1
openclaw --profile denchclaw gateway --port 18789 # Terminal 1
pnpm web:dev # Terminal 2
```
@ -131,7 +131,7 @@ spawn("openclaw", ["gateway", "call", ...], { env: process.env, ... });
### 3. Update agent-runner.test.ts
- Remove `process.env.IRONCLAW_USE_LOCAL_OPENCLAW = "1"` from `beforeEach`
- Remove `process.env.DENCHCLAW_USE_LOCAL_OPENCLAW = "1"` from `beforeEach`
- Remove entire `resolvePackageRoot` describe block (~5 tests)
- The "uses global openclaw by default" test becomes the only spawn behavior test
- Update mock assertions: command is always `"openclaw"`, no `prefixArgs`
@ -141,6 +141,6 @@ spawn("openclaw", ["gateway", "call", ...], { env: process.env, ... });
```diff
- "openclaw": "node scripts/run-node.mjs",
- "openclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
+ "ironclaw": "node scripts/run-node.mjs",
+ "ironclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
+ "denchclaw": "node scripts/run-node.mjs",
+ "denchclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
```

View File

@ -1,12 +1,12 @@
---
name: ironclaw_frontend_split
overview: Re-architect IronClaw into a separate frontend/bootstrap CLI that runs on top of OpenClaw, while preserving current IronClaw UX/features through compatibility adapters and phased cutover. Keep OpenClaw Gateway on its standard port and expose IronClaw UI on localhost:3100 with user-approved OpenClaw updates.
name: denchclaw_frontend_split
overview: Re-architect DenchClaw into a separate frontend/bootstrap CLI that runs on top of OpenClaw, while preserving current DenchClaw UX/features through compatibility adapters and phased cutover. Keep OpenClaw Gateway on its standard port and expose DenchClaw UI on localhost:3100 with user-approved OpenClaw updates.
todos:
- id: freeze-migration-contract-tests
content: Add migration contract tests covering stream-json, session subscribe, profile/workspace resolution, and Dench always-on skill behavior
status: completed
- id: build-ironclaw-bootstrap-layer
content: Implement IronClaw bootstrap path that verifies/installs OpenClaw, runs onboard --install-daemon for profile ironclaw, and launches UI on 3100 with explicit update approval
- id: build-denchclaw-bootstrap-layer
content: Implement DenchClaw bootstrap path that verifies/installs OpenClaw, runs onboard --install-daemon for profile denchclaw, and launches UI on 3100 with explicit update approval
status: completed
- id: extract-gateway-stream-client
content: Extract reusable gateway streaming client from agent-via-gateway and wire web chat APIs to it instead of spawning CLI processes
@ -14,8 +14,8 @@ todos:
- id: unify-profile-storage-paths
content: Align apps/web workspace and web-chat storage resolution with src/config/paths + src/cli/profile semantics and add migration for existing UI state
status: completed
- id: externalize-ironclaw-product-layer
content: Move IronClaw prompt/skill packaging out of core defaults into a product adapter/skill pack while preserving inject behavior
- id: externalize-denchclaw-product-layer
content: Move DenchClaw prompt/skill packaging out of core defaults into a product adapter/skill pack while preserving inject behavior
status: completed
- id: harden-onboarding-and-rollout
content: Add first-run diagnostics, side-by-side safety checks, staged feature flags, and fallback path before full cutover
@ -23,30 +23,30 @@ todos:
isProject: false
---
# IronClaw Frontend-Only Rewrite (No-Break Migration)
# DenchClaw Frontend-Only Rewrite (No-Break Migration)
## Locked Decisions
- Runtime topology: OpenClaw Gateway stays on its normal port (default `18789`), IronClaw UI runs on `3100`.
- Runtime topology: OpenClaw Gateway stays on its normal port (default `18789`), DenchClaw UI runs on `3100`.
- Update policy: install OpenClaw once, then update only when user explicitly approves.
## Target Architecture
```mermaid
flowchart LR
ironclawCli[IronclawCLI] --> bootstrapManager[BootstrapManager]
denchclawCli[DenchClawCLI] --> bootstrapManager[BootstrapManager]
bootstrapManager --> openclawCli[OpenClawCLI]
bootstrapManager --> ironclawProfile[IronclawProfileState]
ironclawUi[IronclawUI3100] --> gatewayWs[GatewayWS18789]
bootstrapManager --> denchclawProfile[DenchClawProfileState]
denchclawUi[DenchClawUI3100] --> gatewayWs[GatewayWS18789]
gatewayWs --> openclawCore[OpenClawCore]
openclawCore --> workspaceData[WorkspaceAndChatStorage]
ironclawSkills[IronclawSkillsPack] --> openclawCore
denchclawSkills[DenchClawSkillsPack] --> openclawCore
```
## Why This Rewrite Is Needed (from current code)
- Web chat currently spawns the CLI directly in `[apps/web/lib/agent-runner.ts](apps/web/lib/agent-runner.ts)` (`openclaw.mjs` + `--stream-json`), which tightly couples UI and CLI process model.
- IronClaw product content is hardcoded in core prompt generation in `[src/agents/system-prompt.ts](src/agents/system-prompt.ts)` (`buildIronclawSection`).
- DenchClaw product content is hardcoded in core prompt generation in `[src/agents/system-prompt.ts](src/agents/system-prompt.ts)` (`buildDenchClawSection`).
- Web workspace/profile logic in `[apps/web/lib/workspace.ts](apps/web/lib/workspace.ts)` is not aligned with core state-dir resolution in `[src/config/paths.ts](src/config/paths.ts)` and profile env wiring in `[src/cli/profile.ts](src/cli/profile.ts)`.
- Bootstrapping and daemon install logic already exists and should be reused, not forked: `[src/commands/onboard.ts](src/commands/onboard.ts)`, `[src/wizard/onboarding.finalize.ts](src/wizard/onboarding.finalize.ts)`, `[src/commands/daemon-install-helpers.ts](src/commands/daemon-install-helpers.ts)`.
@ -54,18 +54,18 @@ flowchart LR
## Phase 1: Freeze Behavior With Contract Tests
- Add regression tests that codify current IronClaw-critical behavior before changing architecture:
- Add regression tests that codify current DenchClaw-critical behavior before changing architecture:
- stream transport + session subscribe behavior (`--stream-json`, `--subscribe-session-key`) from `[src/cli/program/register.agent.ts](src/cli/program/register.agent.ts)` and `[src/commands/agent-via-gateway.ts](src/commands/agent-via-gateway.ts)`.
- workspace/profile + web-chat path behavior from `[apps/web/lib/workspace.ts](apps/web/lib/workspace.ts)` and `[apps/web/lib/workspace-profiles.test.ts](apps/web/lib/workspace-profiles.test.ts)`.
- always-on injected skill behavior for Dench skill loading.
- Produce a “must-pass” migration suite so we can safely refactor internals without user-visible regressions.
## Phase 2: Create IronClaw Bootstrap Layer (Separate CLI Behavior)
## Phase 2: Create DenchClaw Bootstrap Layer (Separate CLI Behavior)
- Introduce a bootstrap command path for `ironclaw` that:
- Introduce a bootstrap command path for `denchclaw` that:
- verifies OpenClaw availability;
- installs OpenClaw if missing (first-run flow);
- runs onboarding (`openclaw --profile ironclaw onboard --install-daemon`);
- runs onboarding (`openclaw --profile denchclaw onboard --install-daemon`);
- starts/opens UI at `http://localhost:3100`.
- Reuse existing onboarding/daemon machinery instead of duplicating logic in a second stack:
- `[src/commands/onboard.ts](src/commands/onboard.ts)`
@ -87,19 +87,19 @@ flowchart LR
- Replace web-only state resolution logic with shared core semantics from `[src/config/paths.ts](src/config/paths.ts)` and profile env behavior from `[src/cli/profile.ts](src/cli/profile.ts)`.
- Normalize chat/workspace storage to profile-scoped OpenClaw state consistently (no split-brain between `~/.openclaw-*` and `~/.openclaw/web-chat-*` behaviors).
- Add one-time migration for existing `.ironclaw-ui-state.json` / web-chat index data to the new canonical profile paths.
- Add one-time migration for existing `.denchclaw-ui-state.json` / web-chat index data to the new canonical profile paths.
## Phase 5: Move IronClaw Product Layer Outside Core
## Phase 5: Move DenchClaw Product Layer Outside Core
- Externalize IronClaw-specific identity/prompt sections currently in `[src/agents/system-prompt.ts](src/agents/system-prompt.ts)` behind a product adapter/config hook.
- Move Dench/IronClaw always-on skill packaging out of core bundled defaults and load it as IronClaw-provided skill pack.
- Keep `inject` capability in core, but remove hardcoded IronClaw assumptions from default OpenClaw prompt path.
- Externalize DenchClaw-specific identity/prompt sections currently in `[src/agents/system-prompt.ts](src/agents/system-prompt.ts)` behind a product adapter/config hook.
- Move Dench/DenchClaw always-on skill packaging out of core bundled defaults and load it as DenchClaw-provided skill pack.
- Keep `inject` capability in core, but remove hardcoded DenchClaw assumptions from default OpenClaw prompt path.
## Phase 6: Onboarding UX Hardening (Zero-Conf Side-by-Side)
- First-run checklist in IronClaw bootstrap:
- First-run checklist in DenchClaw bootstrap:
- OpenClaw installed and version shown
- profile verified (`ironclaw`)
- profile verified (`denchclaw`)
- gateway reachable
- UI reachable at `3100`
- clear remediation output for port/token/device mismatch
@ -116,7 +116,7 @@ flowchart LR
## Definition of Done
- `npx ironclaw` bootstraps OpenClaw (if missing), runs guided onboarding, and reliably opens/serves UI on `localhost:3100`.
- IronClaw runs alongside default OpenClaw without daemon/profile/token collisions.
- `npx denchclaw` bootstraps OpenClaw (if missing), runs guided onboarding, and reliably opens/serves UI on `localhost:3100`.
- DenchClaw runs alongside default OpenClaw without daemon/profile/token collisions.
- Stream, workspaces, always-on skills, and storage features remain intact during and after migration.
- OpenClaw upgrades do not break IronClaw because integration is through stable gateway/CLI interfaces, not forked internals.
- OpenClaw upgrades do not break DenchClaw because integration is through stable gateway/CLI interfaces, not forked internals.

View File

@ -1,6 +1,6 @@
---
name: gateway-ws ironclaw lock
overview: Migrate `apps/web` chat transport from CLI `--stream-json` processes to Gateway WebSocket while preserving the existing SSE API contract, then lock web to a single `ironclaw` profile and disable workspace/profile switching (403 for disabled APIs). Add targeted web and bootstrap tests for the new behavior.
name: gateway-ws denchclaw lock
overview: Migrate `apps/web` chat transport from CLI `--stream-json` processes to Gateway WebSocket while preserving the existing SSE API contract, then lock web to a single `denchclaw` profile and disable workspace/profile switching (403 for disabled APIs). Add targeted web and bootstrap tests for the new behavior.
todos:
- id: ws-transport-adapter
content: Implement Gateway WebSocket-backed AgentProcessHandle adapter in apps/web/lib/agent-runner.ts while keeping existing NDJSON event contract.
@ -9,19 +9,19 @@ todos:
content: Swap abort and subagent follow-up CLI gateway calls to WebSocket RPC calls in active-runs/subagent-runs.
status: completed
- id: profile-default-lock
content: Default web runtime profile resolution to ironclaw in workspace.ts and ensure state/web-chat/workspace paths resolve under ~/.openclaw-ironclaw.
content: Default web runtime profile resolution to denchclaw in workspace.ts and ensure state/web-chat/workspace paths resolve under ~/.openclaw-denchclaw.
status: completed
- id: api-lockdown
content: Return 403 for profile/workspace mutation APIs and keep /api/profiles compatible with a single ironclaw profile payload.
content: Return 403 for profile/workspace mutation APIs and keep /api/profiles compatible with a single denchclaw profile payload.
status: completed
- id: ui-single-profile
content: Remove profile switch/create workspace controls from sidebars and empty state; clean workspace page wiring accordingly.
status: completed
- id: dench-path-update
content: Update skills/dench/SKILL.md workspace path references to ~/.openclaw-ironclaw/workspace.
content: Update skills/dench/SKILL.md workspace path references to ~/.openclaw-denchclaw/workspace.
status: completed
- id: web-tests
content: Update/add apps/web tests covering WS transport behavior, API lock responses, and ironclaw path resolution.
content: Update/add apps/web tests covering WS transport behavior, API lock responses, and denchclaw path resolution.
status: completed
- id: bootstrap-tests
content: Add src/cli tests for run-main bootstrap cutover logic and bootstrap-external diagnostics behavior.
@ -29,13 +29,13 @@ todos:
isProject: false
---
# Migrate Web Chat to Gateway WS + Lock Ironclaw Profile
# Migrate Web Chat to Gateway WS + Lock DenchClaw Profile
## Final behavior
- Keep frontend transport unchanged (`/api/chat` + `/api/chat/stream` SSE contract remains intact).
- Replace backend CLI stream/process transport with Gateway WebSocket transport.
- Force single-profile behavior in web runtime (`ironclaw`), so workspace/chat/session paths resolve to `~/.openclaw-ironclaw/*`.
- Force single-profile behavior in web runtime (`denchclaw`), so workspace/chat/session paths resolve to `~/.openclaw-denchclaw/*`.
- Disable profile/workspace mutation endpoints with `403` (`/api/profiles/switch`, `/api/workspace/init`).
- Remove/disable UI controls for profile switching and workspace creation.
@ -53,16 +53,16 @@ isProject: false
## Profile/path locking
- Update profile resolution in `[apps/web/lib/workspace.ts](apps/web/lib/workspace.ts)` so web runtime defaults to `ironclaw` (without changing test-mode assumptions), ensuring state dir resolves to `~/.openclaw-ironclaw` unless explicitly overridden.
- Update profile resolution in `[apps/web/lib/workspace.ts](apps/web/lib/workspace.ts)` so web runtime defaults to `denchclaw` (without changing test-mode assumptions), ensuring state dir resolves to `~/.openclaw-denchclaw` unless explicitly overridden.
- Keep filesystem resolvers (`resolveOpenClawStateDir`, `resolveWebChatDir`, `resolveWorkspaceRoot`) as the single source of truth used by chat/session/tree APIs.
- Update watcher ignore path in `[apps/web/next.config.ts](apps/web/next.config.ts)` to include ironclaw state dir.
- Update watcher ignore path in `[apps/web/next.config.ts](apps/web/next.config.ts)` to include denchclaw state dir.
## Disable profile/workspace mutation surfaces
- Return `403` in:
- `[apps/web/app/api/profiles/switch/route.ts](apps/web/app/api/profiles/switch/route.ts)`
- `[apps/web/app/api/workspace/init/route.ts](apps/web/app/api/workspace/init/route.ts)`
- Make `[apps/web/app/api/profiles/route.ts](apps/web/app/api/profiles/route.ts)` return a single effective `ironclaw` profile payload for UI compatibility.
- Make `[apps/web/app/api/profiles/route.ts](apps/web/app/api/profiles/route.ts)` return a single effective `denchclaw` profile payload for UI compatibility.
## UI updates (single-profile UX)
@ -75,7 +75,7 @@ isProject: false
## Dench skill path update
- Replace `~/.openclaw/workspace` references with `~/.openclaw-ironclaw/workspace` in `[skills/dench/SKILL.md](skills/dench/SKILL.md)`.
- Replace `~/.openclaw/workspace` references with `~/.openclaw-denchclaw/workspace` in `[skills/dench/SKILL.md](skills/dench/SKILL.md)`.
## Tests to add/update
@ -86,7 +86,7 @@ isProject: false
- update `[apps/web/app/api/profiles/route.test.ts](apps/web/app/api/profiles/route.test.ts)` for single-profile payload and `403` switch behavior.
- update `[apps/web/app/api/workspace/init/route.test.ts](apps/web/app/api/workspace/init/route.test.ts)` for `403` lock behavior.
- Path behavior tests:
- add/adjust targeted assertions in workspace resolver tests for ironclaw state/web-chat/workspace directories.
- add/adjust targeted assertions in workspace resolver tests for denchclaw state/web-chat/workspace directories.
- Bootstrap tests (new):
- add `src/cli` tests for rollout/cutover behavior in `[src/cli/run-main.ts](src/cli/run-main.ts)`.
- add diagnostics/rollout gate tests for `[src/cli/bootstrap-external.ts](src/cli/bootstrap-external.ts)` exported helpers.
@ -109,4 +109,4 @@ sse --> chatPanel
- Run web tests for changed areas (`agent-runner`, `active-runs`, chat API, profiles/workspace-init API).
- Run bootstrap-focused tests for `src/cli/run-main.ts` and `src/cli/bootstrap-external.ts`.
- Smoke-check workspace tree and web sessions resolve under `~/.openclaw-ironclaw` with switching/creation controls disabled.
- Smoke-check workspace tree and web sessions resolve under `~/.openclaw-denchclaw` with switching/creation controls disabled.

View File

@ -1,15 +1,15 @@
---
name: strict-external-openclaw
overview: Convert this repo into an IronClaw-only package that uses globally installed `openclaw` as an external runtime, with strict removal of bundled OpenClaw core source and full cutover of CLI/web flows to external contracts (CLI + gateway protocol).
overview: Convert this repo into an DenchClaw-only package that uses globally installed `openclaw` as an external runtime, with strict removal of bundled OpenClaw core source and full cutover of CLI/web flows to external contracts (CLI + gateway protocol).
todos:
- id: ironclaw-boundary-definition
content: Lock IronClaw-only module boundary and mark all OpenClaw-owned code paths for removal
- id: denchclaw-boundary-definition
content: Lock DenchClaw-only module boundary and mark all OpenClaw-owned code paths for removal
status: completed
- id: remove-cross-imports
content: Eliminate `apps/web` and `ui` internal imports of local OpenClaw source by replacing with IronClaw-local adapters over CLI/gateway contracts
content: Eliminate `apps/web` and `ui` internal imports of local OpenClaw source by replacing with DenchClaw-local adapters over CLI/gateway contracts
status: completed
- id: cli-delegation-cutover
content: Implement IronClaw command delegation to global `openclaw` for non-bootstrap commands
content: Implement DenchClaw command delegation to global `openclaw` for non-bootstrap commands
status: completed
- id: peer-global-packaging
content: Update package metadata/docs to enforce peer + global OpenClaw installation model
@ -18,7 +18,7 @@ todos:
content: Remove OpenClaw core runtime source and obsolete shims/scripts from this repository
status: completed
- id: release-pipeline-realignment
content: Rework build/release checks to publish IronClaw-only artifacts with strict external OpenClaw dependency
content: Rework build/release checks to publish DenchClaw-only artifacts with strict external OpenClaw dependency
status: completed
- id: full-cutover-validation
content: Run full test/smoke matrix and keep one-release emergency fallback
@ -30,10 +30,10 @@ isProject: false
## Goal
- Make this repository IronClaw-only.
- Make this repository DenchClaw-only.
- Remove OpenClaw core runtime code from this repo.
- Depend on globally installed `openclaw` (peer/global model), not bundled source.
- Keep IronClaw UX: `npx ironclaw` bootstrap + UI on `3100` over gateway `18789`.
- Keep DenchClaw UX: `npx denchclaw` bootstrap + UI on `3100` over gateway `18789`.
Reference upstream runtime source of truth: [openclaw/openclaw](https://github.com/openclaw/openclaw).
@ -41,7 +41,7 @@ Reference upstream runtime source of truth: [openclaw/openclaw](https://github.c
- No vendored OpenClaw core runtime in this repo after cutover.
- `openclaw` consumed as global binary requirement (peer + global install), not shipped here.
- IronClaw must communicate with OpenClaw only via stable external contracts:
- DenchClaw must communicate with OpenClaw only via stable external contracts:
- `openclaw` CLI commands
- Gateway WebSocket protocol
@ -49,15 +49,15 @@ Reference upstream runtime source of truth: [openclaw/openclaw](https://github.c
```mermaid
flowchart LR
ironclawCli[ironclawCli] --> bootstrap[bootstrapFlow]
denchclawCli[denchclawCli] --> bootstrap[bootstrapFlow]
bootstrap --> openclawBin[globalOpenclawBin]
ironclawUi[ironclawUi3100] --> gatewayWs[gatewayWs18789]
denchclawUi[denchclawUi3100] --> gatewayWs[gatewayWs18789]
gatewayWs --> openclawRuntime[openclawRuntimeExternal]
```
## Phase 1: Define IronClaw-Only Boundary
## Phase 1: Define DenchClaw-Only Boundary
- Keep only IronClaw-owned surfaces:
- Keep only DenchClaw-owned surfaces:
- product layer and branding
- bootstrap/orchestration CLI
- web UI and workspace UX
@ -72,14 +72,14 @@ flowchart LR
## Phase 2: Replace Internal Core Imports With External Contracts
- Remove all `apps/web` / `ui` imports that currently reach into local OpenClaw source internals.
- Re-implement required behavior in IronClaw-local adapters using gateway protocol + local helpers.
- Re-implement required behavior in DenchClaw-local adapters using gateway protocol + local helpers.
- First critical edge:
- [apps/web/lib/agent-runner.ts](apps/web/lib/agent-runner.ts)
- Also migrate `ui/src/ui/**` consumers that import `../../../../src/*` internals.
## Phase 3: CLI Delegation Model
- Make IronClaw CLI own only bootstrap/product UX.
- Make DenchClaw CLI own only bootstrap/product UX.
- Delegate non-bootstrap command execution to global `openclaw` binary.
- Keep rollout/fallback env gates while switching default to external execution.
- Primary files:
@ -89,7 +89,7 @@ flowchart LR
## Phase 4: Package + Dependency Model (Peer + Global)
- Update package metadata so IronClaw does not bundle OpenClaw runtime code.
- Update package metadata so DenchClaw does not bundle OpenClaw runtime code.
- Add peer requirement/documentation for global `openclaw` presence.
- Ensure bootstrap validates and remediates missing global CLI (`npm i -g openclaw`).
- Primary files:
@ -100,7 +100,7 @@ flowchart LR
## Phase 5: Remove OpenClaw Core Source From Repo
- Delete OpenClaw-owned runtime modules from this repository once delegation and adapters are complete.
- Retain only IronClaw package code and tests.
- Retain only DenchClaw package code and tests.
- Remove obsolete build/release scripts that assume monolithic runtime shipping.
- Primary files/areas:
- `src/` (OpenClaw runtime portions)
@ -109,7 +109,7 @@ flowchart LR
## Phase 6: Build/Release Pipeline Realignment
- Adjust build outputs to ship IronClaw only.
- Adjust build outputs to ship DenchClaw only.
- Remove checks that require bundled OpenClaw dist artifacts.
- Keep web standalone packaging + bootstrap checks.
- Primary files:
@ -123,10 +123,10 @@ flowchart LR
- Unit/e2e coverage for:
- bootstrap diagnostics and remediation
- command delegation to global `openclaw`
- gateway streaming from IronClaw UI
- gateway streaming from DenchClaw UI
- End-to-end smoke:
- clean machine with only global `openclaw`
- `npx ironclaw` bootstrap succeeds
- `npx denchclaw` bootstrap succeeds
- UI works on `3100`, gateway on `18789`, no profile/daemon collisions.
## Rollout Safety

View File

@ -1,6 +1,6 @@
---
name: Workspace profile support
overview: Add full workspace profile and custom path support to the Ironclaw web app and the dench SKILL.md, so they respect OPENCLAW_PROFILE, OPENCLAW_HOME, OPENCLAW_STATE_DIR, and per-agent workspace config — matching the CLI's existing resolution logic.
overview: Add full workspace profile and custom path support to the DenchClaw web app and the dench SKILL.md, so they respect OPENCLAW_PROFILE, OPENCLAW_HOME, OPENCLAW_STATE_DIR, and per-agent workspace config — matching the CLI's existing resolution logic.
todos:
- id: centralize-helpers
content: Add resolveOpenClawStateDir() to apps/web/lib/workspace.ts and update resolveWorkspaceRoot() with OPENCLAW_PROFILE + OPENCLAW_HOME + OPENCLAW_STATE_DIR support

4
.github/labeler.yml vendored
View File

@ -212,10 +212,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/google-gemini-cli-auth/**"
"extensions: ironclaw-auth":
"extensions: denchclaw-auth":
- changed-files:
- any-glob-to-any-file:
- "extensions/ironclaw-auth/**"
- "extensions/denchclaw-auth/**"
"extensions: llm-task":
- changed-files:
- any-glob-to-any-file:

View File

@ -18,13 +18,13 @@
</p>
<p align="center">
<a href="https://www.npmjs.com/package/ironclaw"><img src="https://img.shields.io/npm/v/ironclaw?style=for-the-badge&color=000" alt="npm version"></a>
<a href="https://www.npmjs.com/package/denchclaw"><img src="https://img.shields.io/npm/v/denchclaw?style=for-the-badge&color=000" alt="npm version"></a>
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</p>
<p align="center">
<a href="https://ironclaw.sh">Website</a> · <a href="https://docs.openclaw.ai">Docs</a> · <a href="https://github.com/openclaw/openclaw">OpenClaw Framework</a> · <a href="https://discord.gg/clawd">Discord</a> · <a href="https://clawhub.com">Skills Store</a>
<a href="https://denchclaw.sh">Website</a> · <a href="https://docs.openclaw.ai">Docs</a> · <a href="https://github.com/openclaw/openclaw">OpenClaw Framework</a> · <a href="https://discord.gg/clawd">Discord</a> · <a href="https://clawhub.com">Skills Store</a>
</p>
---
@ -35,7 +35,7 @@
```bash
npm i -g openclaw
npx ironclaw
npx denchclaw
```
Opens at `localhost:3100`. That's it.
@ -44,15 +44,15 @@ Three steps total:
```
1. npm i -g openclaw
2. npx ironclaw
2. npx denchclaw
3. bootstrap opens UI on localhost:3100
```
---
## What is Ironclaw?
## What is DenchClaw?
Ironclaw is a personal AI agent and CRM that runs locally on your machine. It connects to every messaging channel you use, manages structured data through DuckDB, browses the web with your Chrome profile, and gives you a full web UI for pipeline management, analytics, and document management.
DenchClaw is a personal AI agent and CRM that runs locally on your machine. It connects to every messaging channel you use, manages structured data through DuckDB, browses the web with your Chrome profile, and gives you a full web UI for pipeline management, analytics, and document management.
Built on [OpenClaw](https://github.com/openclaw/openclaw) with **Vercel AI SDK v6** as the LLM orchestration layer.
@ -70,7 +70,7 @@ Built on [OpenClaw](https://github.com/openclaw/openclaw) with **Vercel AI SDK v
### Find Leads
Type a prompt, Ironclaw scrapes the web using your actual Chrome profile (all your auth sessions, cookies, history). It logs into LinkedIn, browses YC batches, pulls company data. No separate login, no API keys for browsing.
Type a prompt, DenchClaw scrapes the web using your actual Chrome profile (all your auth sessions, cookies, history). It logs into LinkedIn, browses YC batches, pulls company data. No separate login, no API keys for browsing.
### Enrich Data
@ -82,7 +82,7 @@ Personalized LinkedIn messages, cold emails, follow-up sequences. Each message i
### Analyze Pipeline
Ask for analytics in plain English. Ironclaw queries your DuckDB workspace and generates interactive Recharts dashboards inline. Pipeline funnels, outreach activity charts, conversion rates, donut breakdowns.
Ask for analytics in plain English. DenchClaw queries your DuckDB workspace and generates interactive Recharts dashboards inline. Pipeline funnels, outreach activity charts, conversion rates, donut breakdowns.
### Automate Everything
@ -94,11 +94,11 @@ Cron jobs that run on schedule. Follow-up if no reply after 3 days. Move leads t
### Uses Your Chrome Profile
Unlike other AI tools, Ironclaw copies your existing Chrome profile with all your auth sessions, cookies, and history. It logs into LinkedIn, scrapes YC batches, and sends messages as you. No separate browser login needed.
Unlike other AI tools, DenchClaw copies your existing Chrome profile with all your auth sessions, cookies, and history. It logs into LinkedIn, scrapes YC batches, and sends messages as you. No separate browser login needed.
### Chat with Your Database
Ask questions in plain English. Ironclaw translates to SQL, queries your local DuckDB, and returns structured results. Like having a data analyst on speed dial.
Ask questions in plain English. DenchClaw translates to SQL, queries your local DuckDB, and returns structured results. Like having a data analyst on speed dial.
```
You: "How many founders have we contacted from YC W26?"
@ -111,7 +111,7 @@ Reply rate is 34%.
### Coding Agent with Diffs
Ironclaw writes code. Review changes in a rich diff viewer before applying. Config changes, automation scripts, data transformations. All with diffs you approve.
DenchClaw writes code. Review changes in a rich diff viewer before applying. Config changes, automation scripts, data transformations. All with diffs you approve.
### Your Second Brain
@ -138,18 +138,18 @@ The web app runs at `localhost:3100` and includes:
One agent, every channel. Connect any messaging platform. Your AI agent responds everywhere, managed from a single terminal.
| Channel | Setup |
| ------------------- | ------------------------------------------------------------- |
| **WhatsApp** | `ironclaw channels login` + set `channels.whatsapp.allowFrom` |
| **Telegram** | Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` |
| **Slack** | Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` |
| **Discord** | Set `DISCORD_BOT_TOKEN` or `channels.discord.token` |
| **Signal** | Requires `signal-cli` + `channels.signal` config |
| **iMessage** | Via BlueBubbles (recommended) or legacy macOS integration |
| **Microsoft Teams** | Configure Teams app + Bot Framework |
| **Google Chat** | Chat API integration |
| **Matrix** | Extension channel |
| **WebChat** | Built-in, uses Gateway WebSocket directly |
| Channel | Setup |
| ------------------- | -------------------------------------------------------------- |
| **WhatsApp** | `denchclaw channels login` + set `channels.whatsapp.allowFrom` |
| **Telegram** | Set `TELEGRAM_BOT_TOKEN` or `channels.telegram.botToken` |
| **Slack** | Set `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` |
| **Discord** | Set `DISCORD_BOT_TOKEN` or `channels.discord.token` |
| **Signal** | Requires `signal-cli` + `channels.signal` config |
| **iMessage** | Via BlueBubbles (recommended) or legacy macOS integration |
| **Microsoft Teams** | Configure Teams app + Bot Framework |
| **Google Chat** | Chat API integration |
| **Matrix** | Extension channel |
| **WebChat** | Built-in, uses Gateway WebSocket directly |
```
WhatsApp · Telegram · Slack · Discord
@ -157,7 +157,7 @@ One agent, every channel. Connect any messaging platform. Your AI agent responds
┌────────────────────────────┐
Ironclaw Gateway │
DenchClaw Gateway │
│ ws://127.0.0.1:18789 │
└─────────────┬──────────────┘
@ -165,7 +165,7 @@ One agent, every channel. Connect any messaging platform. Your AI agent responds
│ │ │
▼ ▼ ▼
AI SDK Web UI CLI
Engine (Dench) (ironclaw)
Engine (Dench) (denchclaw)
```
---
@ -217,7 +217,7 @@ Reports use the `report-json` format and render inline in chat as interactive Re
## Kanban Pipeline
Drag-and-drop kanban boards that auto-update as leads reply. Ironclaw moves cards through your pipeline automatically.
Drag-and-drop kanban boards that auto-update as leads reply. DenchClaw moves cards through your pipeline automatically.
Columns like New Lead → Contacted → Qualified → Demo Scheduled → Closed map to your sales process. Each card shows the lead name, company, and last action taken.
@ -243,7 +243,7 @@ Scheduled automations that run in the background:
| CRM backup to S3 | `0 2 * * *` | Nightly workspace backup |
```bash
ironclaw cron list
denchclaw cron list
```
---
@ -267,9 +267,9 @@ The Gateway is the local-first WebSocket control plane that routes everything:
### Security
- **DM pairing** enabled by default. Unknown senders get a pairing code.
- Approve with `ironclaw pairing approve <channel> <code>`
- Approve with `denchclaw pairing approve <channel> <code>`
- Non-main sessions can be sandboxed in Docker
- Run `ironclaw doctor` to audit DM policies
- Run `denchclaw doctor` to audit DM policies
---
@ -329,22 +329,22 @@ Features:
```bash
# Install
npm i -g ironclaw
npm i -g denchclaw
# Run onboarding wizard
ironclaw onboard --install-daemon
denchclaw onboard --install-daemon
# Start the gateway
ironclaw gateway start
denchclaw gateway start
# Open the web UI
open http://localhost:3100
# Talk to your agent from CLI
ironclaw agent --message "Summarize my inbox" --thinking high
denchclaw agent --message "Summarize my inbox" --thinking high
# Send a message
ironclaw message send --to +1234567890 --message "Hello from Ironclaw"
denchclaw message send --to +1234567890 --message "Hello from DenchClaw"
```
---
@ -352,8 +352,8 @@ ironclaw message send --to +1234567890 --message "Hello from Ironclaw"
## From Source
```bash
git clone https://github.com/kumarabhirup/ironclaw.git
cd ironclaw
git clone https://github.com/kumarabhirup/denchclaw.git
cd denchclaw
pnpm install
pnpm build
@ -402,7 +402,7 @@ pnpm dev # Dev mode (auto-reload)
## Upstream
Ironclaw is built on [OpenClaw](https://github.com/openclaw/openclaw). To sync with upstream:
DenchClaw is built on [OpenClaw](https://github.com/openclaw/openclaw). To sync with upstream:
```bash
git remote add upstream https://github.com/openclaw/openclaw.git
@ -416,8 +416,8 @@ git merge upstream/main
MIT Licensed. Fork it, extend it, make it yours.
[![Star History Chart](https://api.star-history.com/image?repos=denchHQ/ironclaw&type=date&legend=top-left)](https://www.star-history.com/?repos=denchHQ%2Fironclaw&type=date&legend=top-left)
[![Star History Chart](https://api.star-history.com/image?repos=denchHQ/denchclaw&type=date&legend=top-left)](https://www.star-history.com/?repos=denchHQ%2Fdenchclaw&type=date&legend=top-left)
<p align="center">
<a href="https://github.com/DenchHQ/ironclaw"><img src="https://img.shields.io/github/stars/DenchHQ/ironclaw?style=for-the-badge" alt="GitHub stars"></a>
<a href="https://github.com/DenchHQ/denchclaw"><img src="https://img.shields.io/github/stars/DenchHQ/denchclaw?style=for-the-badge" alt="GitHub stars"></a>
</p>

View File

@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("@/lib/workspace", () => ({
discoverWorkspaces: vi.fn(() => []),
getActiveWorkspaceName: vi.fn(() => null),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-dench"),
resolveWorkspaceRoot: vi.fn(() => null),
setUIActiveWorkspace: vi.fn(),
}));
@ -18,7 +18,7 @@ vi.mock("node:fs", () => ({
describe("profiles API", () => {
const originalEnv = { ...process.env };
const STATE_DIR = "/home/testuser/.openclaw-ironclaw";
const STATE_DIR = "/home/testuser/.openclaw-dench";
beforeEach(() => {
vi.resetModules();

View File

@ -51,7 +51,7 @@ describe("POST /api/workspace/delete", () => {
vi.mocked(workspace.discoverWorkspaces).mockReturnValue([
{
name: "work",
stateDir: "/home/testuser/.openclaw-ironclaw",
stateDir: "/home/testuser/.openclaw-dench",
workspaceDir: null,
isActive: false,
hasConfig: true,
@ -65,13 +65,13 @@ describe("POST /api/workspace/delete", () => {
it("deletes workspace directory directly via rmSync", async () => {
const workspace = await import("@/lib/workspace");
const { rmSync } = await import("node:fs");
const workspaceDir = "/home/testuser/.openclaw-ironclaw/workspace-work";
const workspaceDir = "/home/testuser/.openclaw-dench/workspace-work";
vi.mocked(workspace.discoverWorkspaces)
.mockReturnValueOnce([
{
name: "work",
stateDir: "/home/testuser/.openclaw-ironclaw",
stateDir: "/home/testuser/.openclaw-dench",
workspaceDir,
isActive: true,
hasConfig: true,
@ -98,12 +98,12 @@ describe("POST /api/workspace/delete", () => {
it("returns 500 when rmSync fails", async () => {
const workspace = await import("@/lib/workspace");
const { rmSync } = await import("node:fs");
const workspaceDir = "/home/testuser/.openclaw-ironclaw/workspace-work";
const workspaceDir = "/home/testuser/.openclaw-dench/workspace-work";
vi.mocked(workspace.discoverWorkspaces).mockReturnValue([
{
name: "work",
stateDir: "/home/testuser/.openclaw-ironclaw",
stateDir: "/home/testuser/.openclaw-dench",
workspaceDir,
isActive: false,
hasConfig: true,

View File

@ -1,7 +1,7 @@
import { join } from "node:path";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
const STATE_DIR = "/home/testuser/.openclaw-ironclaw";
const STATE_DIR = "/home/testuser/.openclaw-dench";
vi.mock("node:fs", () => ({
existsSync: vi.fn(() => false),
@ -17,12 +17,13 @@ vi.mock("@/lib/workspace", () => ({
discoverWorkspaces: vi.fn(() => []),
setUIActiveWorkspace: vi.fn(),
getActiveWorkspaceName: vi.fn(() => "work"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-dench"),
resolveWorkspaceDirForName: vi.fn((name: string) =>
join("/home/testuser/.openclaw-ironclaw", `workspace-${name}`),
join("/home/testuser/.openclaw-dench", `workspace-${name}`),
),
isValidWorkspaceName: vi.fn(() => true),
resolveWorkspaceRoot: vi.fn(() => null),
ensureAgentInConfig: vi.fn(),
}));
describe("POST /api/workspace/init", () => {
@ -83,7 +84,7 @@ describe("POST /api/workspace/init", () => {
expect(response.status).toBe(409);
});
it("creates workspace directory at ~/.openclaw-ironclaw/workspace-<name> (enforces fixed layout)", async () => {
it("creates workspace directory at ~/.openclaw-dench/workspace-<name> (enforces fixed layout)", async () => {
const { mkdirSync, writeFileSync } = await import("node:fs");
const workspace = await import("@/lib/workspace");
vi.mocked(workspace.discoverWorkspaces).mockReturnValue([]);
@ -158,7 +159,7 @@ describe("POST /api/workspace/init", () => {
const raw = identityWrites[identityWrites.length - 1][1];
const identityContent = typeof raw === "string" ? raw : JSON.stringify(raw);
expect(identityContent).toContain(expectedSkillPath);
expect(identityContent).toContain("Ironclaw");
expect(identityContent).toContain("DenchClaw");
expect(identityContent).not.toContain("~skills");
});
});

View File

@ -17,7 +17,7 @@ import {
} from "@/lib/workspace";
import {
seedWorkspaceFromAssets,
buildIronclawIdentity,
buildDenchClawIdentity,
} from "@repo/cli/workspace-seed";
export const dynamic = "force-dynamic";
@ -108,7 +108,7 @@ export async function POST(req: Request) {
}
if (body.path?.trim()) {
return Response.json(
{ error: "Custom workspace paths are currently disabled. Workspaces are created in ~/.openclaw-ironclaw." },
{ error: "Custom workspace paths are currently disabled. Workspaces are created in ~/.openclaw-dench." },
{ status: 400 },
);
}
@ -163,7 +163,7 @@ export async function POST(req: Request) {
}
}
// Seed managed skills, Ironclaw identity, DuckDB, and CRM object projections.
// Seed managed skills, DenchClaw identity, DuckDB, and CRM object projections.
// This is the single source of truth shared with the CLI bootstrap path.
if (projectRoot) {
const seedResult = seedWorkspaceFromAssets({ workspaceDir, packageRoot: projectRoot });
@ -173,10 +173,10 @@ export async function POST(req: Request) {
}
} else {
// No project root available (e.g. standalone/production build without
// the repo tree). Still write the Ironclaw identity so the agent has
// the repo tree). Still write the DenchClaw identity so the agent has
// a usable IDENTITY.md.
const identityPath = join(workspaceDir, "IDENTITY.md");
writeFileSync(identityPath, buildIronclawIdentity(workspaceDir) + "\n", "utf-8");
writeFileSync(identityPath, buildDenchClawIdentity(workspaceDir) + "\n", "utf-8");
seeded.push("IDENTITY.md");
}

View File

@ -8,7 +8,7 @@ import { safeResolvePath } from "@/lib/workspace";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
const THUMB_DIR = join(tmpdir(), "ironclaw-thumbs");
const THUMB_DIR = join(tmpdir(), "denchclaw-thumbs");
mkdirSync(THUMB_DIR, { recursive: true });
/**

View File

@ -17,7 +17,7 @@ vi.mock("node:os", () => ({
// Mock workspace
vi.mock("@/lib/workspace", () => ({
resolveWorkspaceRoot: vi.fn(() => null),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-dench"),
getActiveWorkspaceName: vi.fn(() => null),
parseSimpleYaml: vi.fn(() => ({})),
duckdbQueryAll: vi.fn(() => []),
@ -57,7 +57,7 @@ describe("Workspace Tree & Browse API", () => {
}));
vi.mock("@/lib/workspace", () => ({
resolveWorkspaceRoot: vi.fn(() => null),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-ironclaw"),
resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-dench"),
getActiveWorkspaceName: vi.fn(() => null),
parseSimpleYaml: vi.fn(() => ({})),
duckdbQueryAll: vi.fn(() => []),

View File

@ -197,7 +197,7 @@ export function CronDashboard({
}}
>
<p className="text-sm" style={{ color: "var(--color-text-muted)" }}>
No cron jobs configured. Use <code className="px-1.5 py-0.5 rounded text-xs" style={{ background: "var(--color-surface-hover)" }}>ironclaw cron add</code> to create one.
No cron jobs configured. Use <code className="px-1.5 py-0.5 rounded text-xs" style={{ background: "var(--color-surface-hover)" }}>denchclaw cron add</code> to create one.
</p>
</div>
) : (

View File

@ -406,7 +406,7 @@ export function Sidebar({
<div className="px-4 py-3 border-b border-[var(--color-border)]">
<div className="flex items-center justify-between mb-1.5">
<h1 className="text-base font-bold flex items-center gap-2">
<span>Ironclaw</span>
<span>DenchClaw</span>
</h1>
<button
onClick={onNewSession}

View File

@ -703,27 +703,27 @@ describe("MonacoCodeEditor theme", () => {
it("uses light theme when html element does not have dark class", () => {
render(<MonacoCodeEditor content="hello" filename="app.ts" filePath="app.ts" />);
const editor = screen.getByTestId("monaco-editor");
expect(editor.dataset.theme).toBe("ironclaw-light");
expect(editor.dataset.theme).toBe("denchclaw-light");
});
it("uses dark theme when html element has dark class", () => {
document.documentElement.classList.add("dark");
render(<MonacoCodeEditor content="hello" filename="app.ts" filePath="app.ts" />);
const editor = screen.getByTestId("monaco-editor");
expect(editor.dataset.theme).toBe("ironclaw-dark");
expect(editor.dataset.theme).toBe("denchclaw-dark");
});
it("switches theme dynamically when html class changes (MutationObserver)", async () => {
render(<MonacoCodeEditor content="hello" filename="app.ts" filePath="app.ts" />);
const editor = screen.getByTestId("monaco-editor");
expect(editor.dataset.theme).toBe("ironclaw-light");
expect(editor.dataset.theme).toBe("denchclaw-light");
act(() => {
document.documentElement.classList.add("dark");
});
await waitFor(() => {
expect(editor.dataset.theme).toBe("ironclaw-dark");
expect(editor.dataset.theme).toBe("denchclaw-dark");
});
});
@ -731,14 +731,14 @@ describe("MonacoCodeEditor theme", () => {
document.documentElement.classList.add("dark");
render(<MonacoCodeEditor content="hello" filename="app.ts" filePath="app.ts" />);
const editor = screen.getByTestId("monaco-editor");
expect(editor.dataset.theme).toBe("ironclaw-dark");
expect(editor.dataset.theme).toBe("denchclaw-dark");
act(() => {
document.documentElement.classList.remove("dark");
});
await waitFor(() => {
expect(editor.dataset.theme).toBe("ironclaw-light");
expect(editor.dataset.theme).toBe("denchclaw-light");
});
});
});

View File

@ -99,7 +99,7 @@ function registerThemes(monaco: typeof import("monaco-editor")) {
if (themesRegistered) {return;}
themesRegistered = true;
monaco.editor.defineTheme("ironclaw-light", {
monaco.editor.defineTheme("denchclaw-light", {
base: "vs",
inherit: true,
rules: [],
@ -136,7 +136,7 @@ function registerThemes(monaco: typeof import("monaco-editor")) {
},
});
monaco.editor.defineTheme("ironclaw-dark", {
monaco.editor.defineTheme("denchclaw-dark", {
base: "vs-dark",
inherit: true,
rules: [],
@ -205,7 +205,7 @@ type SaveState = "clean" | "dirty" | "saving" | "saved" | "error";
function EditorInner({ content, filename, filePath, className }: CodeEditorProps) {
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<typeof import("monaco-editor") | null>(null);
const [theme, setTheme] = useState<string>(isDarkMode() ? "ironclaw-dark" : "ironclaw-light");
const [theme, setTheme] = useState<string>(isDarkMode() ? "denchclaw-dark" : "denchclaw-light");
const [saveState, setSaveState] = useState<SaveState>("clean");
const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -221,7 +221,7 @@ function EditorInner({ content, filename, filePath, className }: CodeEditorProps
// Watch for theme changes via MutationObserver on <html> class
useEffect(() => {
const html = document.documentElement;
const update = () => setTheme(isDarkMode() ? "ironclaw-dark" : "ironclaw-light");
const update = () => setTheme(isDarkMode() ? "denchclaw-dark" : "denchclaw-light");
const observer = new MutationObserver(update);
observer.observe(html, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
@ -253,7 +253,7 @@ function EditorInner({ content, filename, filePath, className }: CodeEditorProps
editorRef.current = ed;
monacoRef.current = monaco;
registerThemes(monaco);
monaco.editor.setTheme(isDarkMode() ? "ironclaw-dark" : "ironclaw-light");
monaco.editor.setTheme(isDarkMode() ? "denchclaw-dark" : "denchclaw-light");
// Cmd+S / Ctrl+S save
ed.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {

View File

@ -199,7 +199,7 @@ export function CreateWorkspaceDialog({ isOpen, onClose, onCreated }: CreateWork
className="text-xs mt-1"
style={{ color: "var(--color-text-muted)" }}
>
This creates a workspace under ~/.openclaw-ironclaw/workspace-{"{name}"}.
This creates a workspace under ~/.openclaw-dench/workspace-{"{name}"}.
</p>
</div>

View File

@ -89,7 +89,7 @@ export function EmptyState({
) : (
<>
The workspace directory was not
found. Run the ironclaw bootstrap flow or start a
found. Run the denchclaw bootstrap flow or start a
conversation and the agent will set it up
automatically in the managed profile.
</>
@ -136,7 +136,7 @@ export function EmptyState({
>
{expectedPath
? shortenPath(expectedPath)
: "~/.openclaw-ironclaw/workspace-<name>"}
: "~/.openclaw-dench/workspace-<name>"}
</code>
</span>
</div>

View File

@ -129,15 +129,15 @@ describe("ProfileSwitcher workspace delete action", () => {
workspaces: [
{
name: "ghost",
stateDir: "/home/testuser/.openclaw-ironclaw",
stateDir: "/home/testuser/.openclaw-dench",
workspaceDir: null,
isActive: true,
hasConfig: true,
},
{
name: "ironclaw",
stateDir: "/home/testuser/.openclaw-ironclaw",
workspaceDir: "/home/testuser/.openclaw-ironclaw/workspace",
name: "dench",
stateDir: "/home/testuser/.openclaw-dench",
workspaceDir: "/home/testuser/.openclaw-dench/workspace",
isActive: false,
hasConfig: true,
},
@ -154,12 +154,12 @@ describe("ProfileSwitcher workspace delete action", () => {
});
await waitFor(() => {
expect(screen.getByText("ironclaw")).toBeInTheDocument();
expect(screen.getByText("dench")).toBeInTheDocument();
expect(screen.queryByText("ghost")).not.toBeInTheDocument();
});
await user.click(screen.getByTitle("Switch workspace"));
expect(screen.queryByTitle("Delete workspace ghost")).not.toBeInTheDocument();
expect(screen.getByTitle("Delete workspace ironclaw")).toBeInTheDocument();
expect(screen.getByTitle("Delete workspace dench")).toBeInTheDocument();
});
});

View File

@ -591,13 +591,13 @@ export function WorkspaceSidebar({
style={{ borderColor: "var(--color-border)" }}
>
<a
href="https://ironclaw.sh"
href="https://denchclaw.sh"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-2 py-1.5 rounded-lg text-sm"
style={{ color: "var(--color-text-muted)" }}
>
ironclaw.sh
denchclaw.sh
</a>
<div className="flex items-center gap-0.5">
{onToggleHidden && (

View File

@ -2,7 +2,7 @@ import type { Metadata, Viewport } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Ironclaw",
title: "DenchClaw",
description:
"AI Workspace with an agent that connects to your apps and does the work for you",
};

View File

@ -37,7 +37,7 @@ const CLAW_ASCII = [
" ░ ░░ ",
];
const IRONCLAW_ASCII = [
const DENCHCLAW_ASCII = [
" ██╗██████╗ ██████╗ ███╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
" ██║██╔══██╗██╔═══██╗████╗ ██║██╔════╝██║ ██╔══██╗██║ ██║",
" ██║██████╔╝██║ ██║██╔██╗ ██║██║ ██║ ███████║██║ █╗ ██║",
@ -103,11 +103,11 @@ export default function Home() {
{/* Foreground content */}
<div className="relative z-10 flex flex-col items-center">
<div className="ascii-banner select-none hidden sm:block" aria-label="IRONCLAW">
{IRONCLAW_ASCII.join("\n")}
<div className="ascii-banner select-none hidden sm:block" aria-label="DENCHCLAW">
{DENCHCLAW_ASCII.join("\n")}
</div>
<h1 className="sm:hidden text-3xl font-bold text-stone-600" style={{ fontFamily: "monospace" }}>
IRONCLAW
DENCHCLAW
</h1>
<Link
href="/workspace"

View File

@ -186,8 +186,8 @@ const LEFT_SIDEBAR_MIN = 200;
const LEFT_SIDEBAR_MAX = 480;
const RIGHT_SIDEBAR_MIN = 260;
const RIGHT_SIDEBAR_MAX = 900;
const STORAGE_LEFT = "ironclaw-workspace-left-sidebar-width";
const STORAGE_RIGHT = "ironclaw-workspace-right-sidebar-width";
const STORAGE_LEFT = "dench-workspace-left-sidebar-width";
const STORAGE_RIGHT = "dench-workspace-right-sidebar-width";
function clamp(n: number, min: number, max: number): number {
return Math.min(max, Math.max(min, n));

View File

@ -272,7 +272,7 @@ describe("agent-runner", () => {
describe("spawnAgentProcess", () => {
it("connects via ws module with Origin header matching the gateway URL (prevents origin rejection)", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentProcess } = await import("./agent-runner.js");
const proc = spawnAgentProcess("hello", "sess-1");
@ -298,7 +298,7 @@ describe("agent-runner", () => {
it("sets wss: origin to https: (prevents origin mismatch on TLS gateways)", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
process.env.OPENCLAW_GATEWAY_URL = "wss://gateway.example.com:443";
const { spawnAgentProcess } = await import("./agent-runner.js");
@ -313,7 +313,7 @@ describe("agent-runner", () => {
it("falls back to config gateway port when env port is stale", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
process.env.OPENCLAW_GATEWAY_PORT = "19001";
MockWs.failOpenForUrls.add("ws://127.0.0.1:19001/");
@ -342,7 +342,7 @@ describe("agent-runner", () => {
it("does not use child_process.spawn for WebSocket transport", async () => {
installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawn: mockSpawn } = await import("node:child_process");
vi.mocked(mockSpawn).mockClear();
const { spawnAgentProcess } = await import("./agent-runner.js");
@ -354,8 +354,8 @@ describe("agent-runner", () => {
proc.kill("SIGTERM");
});
it("falls back to CLI spawn when IRONCLAW_WEB_FORCE_LEGACY_STREAM is set", async () => {
process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM = "1";
it("falls back to CLI spawn when DENCHCLAW_WEB_FORCE_LEGACY_STREAM is set", async () => {
process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM = "1";
const { spawn: mockSpawn } = await import("node:child_process");
const child = mockChildProcess();
vi.mocked(mockSpawn).mockReturnValue(child as unknown as ChildProcess);
@ -373,7 +373,7 @@ describe("agent-runner", () => {
});
it("includes session-key and lane args in legacy CLI mode", async () => {
process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM = "1";
process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM = "1";
const { spawn: mockSpawn } = await import("node:child_process");
const child = mockChildProcess();
vi.mocked(mockSpawn).mockReturnValue(child as unknown as ChildProcess);
@ -399,7 +399,7 @@ describe("agent-runner", () => {
describe("spawnAgentSubscribeProcess", () => {
it("subscribes via connect -> sessions.patch -> agent.subscribe", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentSubscribeProcess } = await import("./agent-runner.js");
const proc = spawnAgentSubscribeProcess("agent:main:web:sess-sub", 12);
@ -426,7 +426,7 @@ describe("agent-runner", () => {
it("uses payload.globalSeq (not frame seq) for cursor filtering", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentSubscribeProcess } = await import("./agent-runner.js");
const proc = spawnAgentSubscribeProcess("agent:main:web:sess-gseq", 5);
@ -489,7 +489,7 @@ describe("agent-runner", () => {
it("keeps subscribe workers alive across lifecycle end events", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentSubscribeProcess } = await import("./agent-runner.js");
const proc = spawnAgentSubscribeProcess("agent:main:web:sess-sticky", 0);
@ -545,7 +545,7 @@ describe("agent-runner", () => {
it("drops subscribe events missing a matching session key", async () => {
const MockWs = installMockWsModule();
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentSubscribeProcess } = await import("./agent-runner.js");
const proc = spawnAgentSubscribeProcess("agent:main:web:sess-filter", 0);
@ -602,7 +602,7 @@ describe("agent-runner", () => {
ok: false,
error: { message: "unknown method: agent.subscribe" },
});
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentSubscribeProcess } = await import("./agent-runner.js");
const proc = spawnAgentSubscribeProcess("agent:main:web:sess-passive", 0);
@ -643,7 +643,7 @@ describe("agent-runner", () => {
ok: false,
error: { message: "unknown method: agent.subscribe" },
});
delete process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM;
delete process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM;
const { spawnAgentSubscribeProcess } = await import("./agent-runner.js");
const first = spawnAgentSubscribeProcess("agent:main:web:sess-cache", 0);

View File

@ -359,10 +359,10 @@ export function buildConnectParams(
version: "dev",
platform: process.platform,
mode: clientMode,
instanceId: "ironclaw-web-server",
instanceId: "denchclaw-web-server",
},
locale: "en-US",
userAgent: "ironclaw-web",
userAgent: "denchclaw-web",
role: "operator",
scopes: ["operator.read", "operator.write", "operator.admin"],
caps,
@ -880,7 +880,7 @@ class GatewayProcessHandle
}
function shouldForceLegacyStream(): boolean {
const raw = process.env.IRONCLAW_WEB_FORCE_LEGACY_STREAM?.trim().toLowerCase();
const raw = process.env.DENCHCLAW_WEB_FORCE_LEGACY_STREAM?.trim().toLowerCase();
return raw === "1" || raw === "true" || raw === "yes";
}
@ -1285,7 +1285,7 @@ export async function runAgent(
child.stderr?.on("data", (chunk: Buffer) => {
const text = chunk.toString();
stderrChunks.push(text);
console.error("[ironclaw stderr]", text);
console.error("[denchclaw stderr]", text);
});
});
}

View File

@ -50,7 +50,7 @@ import { join } from "node:path";
describe("workspace-scoped chat session isolation", () => {
const originalEnv = { ...process.env };
const STATE_DIR = "/home/testuser/.openclaw-ironclaw";
const STATE_DIR = "/home/testuser/.openclaw-dench";
const workspaceDir = (name: string) =>
name === "default"

View File

@ -65,8 +65,8 @@ function makeDirent(name: string, isDir: boolean): Dirent {
describe("workspace (flat workspace model)", () => {
const originalEnv = { ...process.env };
const STATE_DIR = "/home/testuser/.openclaw-ironclaw";
const UI_STATE_PATH = join(STATE_DIR, ".ironclaw-ui-state.json");
const STATE_DIR = "/home/testuser/.openclaw-dench";
const UI_STATE_PATH = join(STATE_DIR, ".dench-ui-state.json");
beforeEach(() => {
vi.resetModules();
@ -147,7 +147,7 @@ describe("workspace (flat workspace model)", () => {
// ─── getEffectiveProfile ──────────────────────────────────────────
describe("getEffectiveProfile", () => {
it("always returns 'ironclaw' regardless of env/state (single profile enforcement)", async () => {
it("always returns 'dench' regardless of env/state (single profile enforcement)", async () => {
process.env.OPENCLAW_PROFILE = "work";
const { getEffectiveProfile, setUIActiveProfile, mockReadFile } =
await importWorkspace();
@ -155,7 +155,7 @@ describe("workspace (flat workspace model)", () => {
JSON.stringify({ activeWorkspace: "something" }) as never,
);
setUIActiveProfile("custom");
expect(getEffectiveProfile()).toBe("ironclaw");
expect(getEffectiveProfile()).toBe("dench");
});
});
@ -369,7 +369,7 @@ describe("workspace (flat workspace model)", () => {
expect(workspaces[0]?.isActive).toBe(true);
});
it("keeps root default and workspace-ironclaw as distinct workspaces", async () => {
it("keeps root default and workspace-dench as distinct workspaces", async () => {
const { discoverWorkspaces, mockReaddir, mockExists, mockReadFile } =
await importWorkspace();
mockReadFile.mockImplementation(() => {
@ -377,25 +377,25 @@ describe("workspace (flat workspace model)", () => {
});
mockReaddir.mockReturnValue([
makeDirent("workspace", true),
makeDirent("workspace-ironclaw", true),
makeDirent("workspace-dench", true),
] as unknown as Dirent[]);
mockExists.mockImplementation((p) => {
const s = String(p);
return s === join(STATE_DIR, "workspace") || s === join(STATE_DIR, "workspace-ironclaw");
return s === join(STATE_DIR, "workspace") || s === join(STATE_DIR, "workspace-dench");
});
const workspaces = discoverWorkspaces();
expect(workspaces).toHaveLength(2);
const names = workspaces.map((workspace) => workspace.name);
expect(names).toContain("default");
expect(names).toContain("ironclaw");
expect(names).toContain("dench");
const rootDefault = workspaces.find((workspace) => workspace.name === "default");
const profileIronclaw = workspaces.find((workspace) => workspace.name === "ironclaw");
const profileDench = workspaces.find((workspace) => workspace.name === "dench");
expect(rootDefault?.workspaceDir).toBe(join(STATE_DIR, "workspace"));
expect(profileIronclaw?.workspaceDir).toBe(join(STATE_DIR, "workspace-ironclaw"));
expect(profileDench?.workspaceDir).toBe(join(STATE_DIR, "workspace-dench"));
});
it("lists default, ironclaw, and custom workspace side by side", async () => {
it("lists default, dench, and custom workspace side by side", async () => {
const { discoverWorkspaces, mockReaddir, mockExists, mockReadFile } =
await importWorkspace();
mockReadFile.mockImplementation(() => {
@ -403,14 +403,14 @@ describe("workspace (flat workspace model)", () => {
});
mockReaddir.mockReturnValue([
makeDirent("workspace", true),
makeDirent("workspace-ironclaw", true),
makeDirent("workspace-dench", true),
makeDirent("workspace-kumareth", true),
] as unknown as Dirent[]);
mockExists.mockImplementation((p) => {
const s = String(p);
return (
s === join(STATE_DIR, "workspace") ||
s === join(STATE_DIR, "workspace-ironclaw") ||
s === join(STATE_DIR, "workspace-dench") ||
s === join(STATE_DIR, "workspace-kumareth")
);
});
@ -418,7 +418,7 @@ describe("workspace (flat workspace model)", () => {
const workspaces = discoverWorkspaces();
expect(workspaces.map((workspace) => workspace.name)).toEqual([
"default",
"ironclaw",
"dench",
"kumareth",
]);
});
@ -611,7 +611,7 @@ describe("workspace (flat workspace model)", () => {
mockWriteFile.mockClear();
registerWorkspacePath("myprofile", "/my/workspace");
const stateWrites = mockWriteFile.mock.calls.filter((c) =>
(c[0] as string).includes(".ironclaw-ui-state.json"),
(c[0] as string).includes(".dench-ui-state.json"),
);
expect(stateWrites).toHaveLength(0);
});

View File

@ -50,7 +50,7 @@ function makeDirent(name: string, isDir: boolean): Dirent {
describe("workspace utilities", () => {
const originalEnv = { ...process.env };
const STATE_DIR = join("/home/testuser", ".openclaw-ironclaw");
const STATE_DIR = join("/home/testuser", ".openclaw-dench");
const WS_DIR = join(STATE_DIR, "workspace-test");
beforeEach(() => {
@ -155,7 +155,7 @@ describe("workspace utilities", () => {
expect(resolveWorkspaceRoot()).toBe(fallbackWs);
});
it("resolves bootstrap root workspace as ironclaw default", async () => {
it("resolves bootstrap root workspace as dench default", async () => {
delete process.env.OPENCLAW_WORKSPACE;
const { resolveWorkspaceRoot, mockExists, mockReaddir } = await importWorkspace();
const rootWorkspace = join(STATE_DIR, "workspace");
@ -173,7 +173,7 @@ describe("workspace utilities", () => {
// ─── resolveWebChatDir ────────────────────────────────────────────
describe("resolveWebChatDir", () => {
it("falls back to root workspace chat dir for ironclaw default", async () => {
it("falls back to root workspace chat dir for dench default", async () => {
delete process.env.OPENCLAW_WORKSPACE;
const { resolveWebChatDir, mockReadFile, mockReaddir } = await importWorkspace();
mockReadFile.mockImplementation(() => {

View File

@ -8,13 +8,13 @@ import { normalizeFilterGroup, type SavedView, type ViewTypeSettings } from "./o
const execAsync = promisify(exec);
const UI_STATE_FILENAME = ".ironclaw-ui-state.json";
const FIXED_STATE_DIRNAME = ".openclaw-ironclaw";
const UI_STATE_FILENAME = ".dench-ui-state.json";
const FIXED_STATE_DIRNAME = ".openclaw-dench";
const WORKSPACE_PREFIX = "workspace-";
const ROOT_WORKSPACE_DIRNAME = "workspace";
const WORKSPACE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
const DEFAULT_WORKSPACE_NAME = "default";
const IRONCLAW_PROFILE = "ironclaw";
const DENCHCLAW_PROFILE = "dench";
/** In-memory override; takes precedence over persisted state. */
let _uiActiveWorkspace: string | null | undefined;
@ -223,7 +223,7 @@ export function discoverProfiles(): DiscoveredProfile[] {
return discoverWorkspaces();
}
export function getEffectiveProfile(): string {
return IRONCLAW_PROFILE;
return DENCHCLAW_PROFILE;
}
export function setUIActiveProfile(profile: string | null): void {
setUIActiveWorkspace(normalizeWorkspaceName(profile));
@ -239,7 +239,7 @@ export function getRegisteredWorkspacePath(_profile: string | null): string | nu
}
export function registerWorkspacePath(_profile: string, _absolutePath: string): void {
// No-op: workspace paths are discovered from managed dirs:
// ~/.openclaw-ironclaw/workspace (default) and ~/.openclaw-ironclaw/workspace-<name>.
// ~/.openclaw-dench/workspace (default) and ~/.openclaw-dench/workspace-<name>.
}
export function isValidWorkspaceName(name: string): boolean {

View File

@ -1,5 +1,5 @@
{
"name": "ironclaw-web",
"name": "denchclaw-web",
"version": "0.1.0",
"private": true,
"scripts": {

View File

@ -9,6 +9,7 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname),
"@repo": path.resolve(__dirname, "../../src"),
},
},
test: {

View File

@ -10,7 +10,7 @@ title: "Updating"
OpenClaw is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use `openclaw update`, which restarts) → verify.
If you run **IronClaw** as a frontend package, keep OpenClaw installed globally (`npm i -g openclaw`) and update OpenClaw separately; IronClaw delegates runtime commands to that global OpenClaw install.
If you run **DenchClaw** as a frontend package, keep OpenClaw installed globally (`npm i -g openclaw`) and update OpenClaw separately; DenchClaw delegates runtime commands to that global OpenClaw install.
## Recommended: re-run the website installer (upgrade in place)

View File

@ -23,7 +23,7 @@ When the operator says “release”, immediately do this preflight (no extra qu
- [ ] Bump `package.json` version (e.g., `2026.1.29`).
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/openclaw/openclaw/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/openclaw/openclaw/blob/main/src/provider-web.ts).
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs) for `ironclaw`.
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs) for `denchclaw`.
- [ ] Confirm release notes/documentation call out the global runtime prerequisite: `npm i -g openclaw`.
- [ ] If dependencies changed, run `pnpm install` so `pnpm-lock.yaml` is current.
@ -31,7 +31,7 @@ When the operator says “release”, immediately do this preflight (no extra qu
- [ ] If A2UI inputs changed, run `pnpm canvas:a2ui:bundle` and commit any updated [`src/canvas-host/a2ui/a2ui.bundle.js`](https://github.com/openclaw/openclaw/blob/main/src/canvas-host/a2ui/a2ui.bundle.js).
- [ ] `pnpm run build` (regenerates `dist/`).
- [ ] Verify npm package `files` includes only IronClaw artifacts (`dist/entry*`, web standalone, skills/assets) and does not rely on bundled OpenClaw core runtime code.
- [ ] Verify npm package `files` includes only DenchClaw artifacts (`dist/entry*`, web standalone, skills/assets) and does not rely on bundled OpenClaw core runtime code.
- [ ] Confirm `dist/build-info.json` exists and includes the expected `commit` hash (CLI banner uses this for npm installs).
- [ ] Optional: `npm pack --pack-destination /tmp` after the build; inspect the tarball contents and keep it handy for the GitHub release (do **not** commit it).

View File

@ -4,7 +4,7 @@
"description": "OpenClaw BlueBubbles channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -0,0 +1,39 @@
# DenchClaw OAuth (OpenClaw plugin)
OAuth provider plugin for DenchClaw-hosted models.
## Enable
Bundled plugins are disabled by default. Enable this one:
```bash
openclaw plugins enable denchclaw-auth
```
Restart the Gateway after enabling.
## Authenticate
Set at least a client id, then run provider login:
```bash
export DENCHCLAW_OAUTH_CLIENT_ID="<your-client-id>"
openclaw models auth login --provider denchclaw --set-default
```
## Optional env vars
- `DENCHCLAW_OAUTH_CLIENT_SECRET`
- `DENCHCLAW_OAUTH_AUTH_URL` (default: `https://auth.denchclaw.ai/oauth/authorize`)
- `DENCHCLAW_OAUTH_TOKEN_URL` (default: `https://auth.denchclaw.ai/oauth/token`)
- `DENCHCLAW_OAUTH_REDIRECT_URI` (default: `http://127.0.0.1:47089/oauth/callback`)
- `DENCHCLAW_OAUTH_SCOPES` (space/comma separated)
- `DENCHCLAW_OAUTH_USERINFO_URL` (optional for email display)
- `DENCHCLAW_PROVIDER_BASE_URL` (default: `https://api.denchclaw.ai/v1`)
- `DENCHCLAW_PROVIDER_MODEL_IDS` (space/comma separated, default: `chat`)
- `DENCHCLAW_PROVIDER_DEFAULT_MODEL` (default: first model id)
## Notes
- This plugin configures `models.providers.denchclaw` as `openai-completions`.
- OAuth tokens are stored in auth profiles and the provider is patched into config automatically.

View File

@ -4,45 +4,45 @@ import {
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk";
import { loginIronclawOAuth, type IronclawOAuthConfig } from "./oauth.js";
import { loginDenchClawOAuth, type DenchClawOAuthConfig } from "./oauth.js";
const PLUGIN_ID = "ironclaw-auth";
const PROVIDER_ID = "ironclaw";
const PROVIDER_LABEL = "Ironclaw";
const OAUTH_PLACEHOLDER = "ironclaw-oauth";
const DEFAULT_AUTH_URL = "https://auth.ironclaw.ai/oauth/authorize";
const DEFAULT_TOKEN_URL = "https://auth.ironclaw.ai/oauth/token";
const PLUGIN_ID = "denchclaw-auth";
const PROVIDER_ID = "denchclaw";
const PROVIDER_LABEL = "DenchClaw";
const OAUTH_PLACEHOLDER = "denchclaw-oauth";
const DEFAULT_AUTH_URL = "https://auth.denchclaw.ai/oauth/authorize";
const DEFAULT_TOKEN_URL = "https://auth.denchclaw.ai/oauth/token";
const DEFAULT_REDIRECT_URI = "http://127.0.0.1:47089/oauth/callback";
const DEFAULT_SCOPES = ["openid", "profile", "email", "offline_access"];
const DEFAULT_BASE_URL = "https://api.ironclaw.ai/v1";
const DEFAULT_BASE_URL = "https://api.denchclaw.ai/v1";
const DEFAULT_MODEL_ID = "chat";
const DEFAULT_CONTEXT_WINDOW = 128000;
const DEFAULT_MAX_TOKENS = 8192;
const CLIENT_ID_KEYS = ["IRONCLAW_OAUTH_CLIENT_ID", "OPENCLAW_IRONCLAW_OAUTH_CLIENT_ID"];
const CLIENT_ID_KEYS = ["DENCHCLAW_OAUTH_CLIENT_ID", "OPENCLAW_DENCHCLAW_OAUTH_CLIENT_ID"];
const CLIENT_SECRET_KEYS = [
"IRONCLAW_OAUTH_CLIENT_SECRET",
"OPENCLAW_IRONCLAW_OAUTH_CLIENT_SECRET",
"DENCHCLAW_OAUTH_CLIENT_SECRET",
"OPENCLAW_DENCHCLAW_OAUTH_CLIENT_SECRET",
];
const AUTH_URL_KEYS = ["IRONCLAW_OAUTH_AUTH_URL", "OPENCLAW_IRONCLAW_OAUTH_AUTH_URL"];
const TOKEN_URL_KEYS = ["IRONCLAW_OAUTH_TOKEN_URL", "OPENCLAW_IRONCLAW_OAUTH_TOKEN_URL"];
const REDIRECT_URI_KEYS = ["IRONCLAW_OAUTH_REDIRECT_URI", "OPENCLAW_IRONCLAW_OAUTH_REDIRECT_URI"];
const SCOPES_KEYS = ["IRONCLAW_OAUTH_SCOPES", "OPENCLAW_IRONCLAW_OAUTH_SCOPES"];
const USERINFO_URL_KEYS = ["IRONCLAW_OAUTH_USERINFO_URL", "OPENCLAW_IRONCLAW_OAUTH_USERINFO_URL"];
const AUTH_URL_KEYS = ["DENCHCLAW_OAUTH_AUTH_URL", "OPENCLAW_DENCHCLAW_OAUTH_AUTH_URL"];
const TOKEN_URL_KEYS = ["DENCHCLAW_OAUTH_TOKEN_URL", "OPENCLAW_DENCHCLAW_OAUTH_TOKEN_URL"];
const REDIRECT_URI_KEYS = ["DENCHCLAW_OAUTH_REDIRECT_URI", "OPENCLAW_DENCHCLAW_OAUTH_REDIRECT_URI"];
const SCOPES_KEYS = ["DENCHCLAW_OAUTH_SCOPES", "OPENCLAW_DENCHCLAW_OAUTH_SCOPES"];
const USERINFO_URL_KEYS = ["DENCHCLAW_OAUTH_USERINFO_URL", "OPENCLAW_DENCHCLAW_OAUTH_USERINFO_URL"];
const BASE_URL_KEYS = [
"IRONCLAW_PROVIDER_BASE_URL",
"IRONCLAW_API_BASE_URL",
"OPENCLAW_IRONCLAW_PROVIDER_BASE_URL",
"DENCHCLAW_PROVIDER_BASE_URL",
"DENCHCLAW_API_BASE_URL",
"OPENCLAW_DENCHCLAW_PROVIDER_BASE_URL",
];
const MODEL_IDS_KEYS = [
"IRONCLAW_PROVIDER_MODEL_IDS",
"IRONCLAW_MODEL_IDS",
"OPENCLAW_IRONCLAW_MODEL_IDS",
"DENCHCLAW_PROVIDER_MODEL_IDS",
"DENCHCLAW_MODEL_IDS",
"OPENCLAW_DENCHCLAW_MODEL_IDS",
];
const DEFAULT_MODEL_KEYS = [
"IRONCLAW_PROVIDER_DEFAULT_MODEL",
"IRONCLAW_DEFAULT_MODEL",
"OPENCLAW_IRONCLAW_DEFAULT_MODEL",
"DENCHCLAW_PROVIDER_DEFAULT_MODEL",
"DENCHCLAW_DEFAULT_MODEL",
"OPENCLAW_DENCHCLAW_DEFAULT_MODEL",
];
const ENV_VARS = [
@ -118,11 +118,11 @@ function buildModelDefinition(modelId: string) {
};
}
function resolveOAuthConfig(): IronclawOAuthConfig {
function resolveOAuthConfig(): DenchClawOAuthConfig {
const clientId = resolveEnv(CLIENT_ID_KEYS);
if (!clientId) {
throw new Error(
["Ironclaw OAuth client id is required.", `Set one of: ${CLIENT_ID_KEYS.join(", ")}`].join(
["DenchClaw OAuth client id is required.", `Set one of: ${CLIENT_ID_KEYS.join(", ")}`].join(
"\n",
),
);
@ -158,7 +158,7 @@ function buildAuthResult(params: {
const agentModels = Object.fromEntries(
finalModelIds.map((modelId, index) => [
`${PROVIDER_ID}/${modelId}`,
index === 0 ? { alias: "ironclaw" } : {},
index === 0 ? { alias: "denchclaw" } : {},
]),
);
@ -201,29 +201,29 @@ function buildAuthResult(params: {
};
}
const ironclawAuthPlugin = {
const denchclawAuthPlugin = {
id: PLUGIN_ID,
name: "Ironclaw OAuth",
description: "OAuth flow for Ironclaw-hosted models",
name: "DenchClaw OAuth",
description: "OAuth flow for DenchClaw-hosted models",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
label: PROVIDER_LABEL,
docsPath: "/providers/models",
aliases: ["ironclaw-ai"],
aliases: ["denchclaw-ai"],
envVars: ENV_VARS,
auth: [
{
id: "oauth",
label: "Ironclaw OAuth",
label: "DenchClaw OAuth",
hint: "PKCE + localhost callback",
kind: "oauth",
run: async (ctx: ProviderAuthContext) => {
const progress = ctx.prompter.progress("Starting Ironclaw OAuth...");
const progress = ctx.prompter.progress("Starting DenchClaw OAuth...");
try {
const oauthConfig = resolveOAuthConfig();
const result = await loginIronclawOAuth(
const result = await loginDenchClawOAuth(
{
isRemote: ctx.isRemote,
openUrl: ctx.openUrl,
@ -235,16 +235,16 @@ const ironclawAuthPlugin = {
oauthConfig,
);
progress.stop("Ironclaw OAuth complete");
progress.stop("DenchClaw OAuth complete");
return buildAuthResult(result);
} catch (error) {
progress.stop("Ironclaw OAuth failed");
progress.stop("DenchClaw OAuth failed");
await ctx.prompter.note(
[
"Set IRONCLAW_OAUTH_CLIENT_ID (and optionally auth/token URLs) before retrying.",
"You can also configure model ids with IRONCLAW_PROVIDER_MODEL_IDS.",
"Set DENCHCLAW_OAUTH_CLIENT_ID (and optionally auth/token URLs) before retrying.",
"You can also configure model ids with DENCHCLAW_PROVIDER_MODEL_IDS.",
].join("\n"),
"Ironclaw OAuth",
"DenchClaw OAuth",
);
throw error;
}
@ -255,4 +255,4 @@ const ironclawAuthPlugin = {
},
};
export default ironclawAuthPlugin;
export default denchclawAuthPlugin;

View File

@ -6,7 +6,7 @@ const RESPONSE_PAGE = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ironclaw OAuth</title>
<title>DenchClaw OAuth</title>
</head>
<body>
<main>
@ -16,7 +16,7 @@ const RESPONSE_PAGE = `<!DOCTYPE html>
</body>
</html>`;
export type IronclawOAuthConfig = {
export type DenchClawOAuthConfig = {
clientId: string;
clientSecret?: string;
authUrl: string;
@ -26,14 +26,14 @@ export type IronclawOAuthConfig = {
userInfoUrl?: string;
};
export type IronclawOAuthCredentials = {
export type DenchClawOAuthCredentials = {
access: string;
refresh: string;
expires: number;
email?: string;
};
export type IronclawOAuthContext = {
export type DenchClawOAuthContext = {
isRemote: boolean;
openUrl: (url: string) => Promise<void>;
log: (message: string) => void;
@ -65,11 +65,11 @@ function normalizeUrl(value: string, fieldName: string): string {
}
function buildAuthUrl(params: {
config: IronclawOAuthConfig;
config: DenchClawOAuthConfig;
challenge: string;
state: string;
}): string {
const authUrl = normalizeUrl(params.config.authUrl, "IRONCLAW_OAUTH_AUTH_URL");
const authUrl = normalizeUrl(params.config.authUrl, "DENCHCLAW_OAUTH_AUTH_URL");
const url = new URL(authUrl);
url.searchParams.set("client_id", params.config.clientId);
url.searchParams.set("response_type", "code");
@ -112,7 +112,7 @@ function parseCallbackInput(
}
async function startCallbackServer(params: { redirectUri: string; timeoutMs: number }) {
const redirect = new URL(normalizeUrl(params.redirectUri, "IRONCLAW_OAUTH_REDIRECT_URI"));
const redirect = new URL(normalizeUrl(params.redirectUri, "DENCHCLAW_OAUTH_REDIRECT_URI"));
const port = redirect.port ? Number(redirect.port) : 80;
const host =
redirect.hostname === "localhost" || redirect.hostname === "127.0.0.1"
@ -189,11 +189,11 @@ async function startCallbackServer(params: { redirectUri: string; timeoutMs: num
}
async function exchangeCode(params: {
config: IronclawOAuthConfig;
config: DenchClawOAuthConfig;
code: string;
verifier: string;
}): Promise<IronclawOAuthCredentials> {
const tokenUrl = normalizeUrl(params.config.tokenUrl, "IRONCLAW_OAUTH_TOKEN_URL");
}): Promise<DenchClawOAuthCredentials> {
const tokenUrl = normalizeUrl(params.config.tokenUrl, "DENCHCLAW_OAUTH_TOKEN_URL");
const body = new URLSearchParams({
grant_type: "authorization_code",
code: params.code,
@ -240,7 +240,7 @@ async function exchangeCode(params: {
}
async function fetchUserEmail(
config: IronclawOAuthConfig,
config: DenchClawOAuthConfig,
accessToken: string,
): Promise<string | undefined> {
if (!config.userInfoUrl?.trim()) {
@ -248,7 +248,7 @@ async function fetchUserEmail(
}
let url: string;
try {
url = normalizeUrl(config.userInfoUrl, "IRONCLAW_OAUTH_USERINFO_URL");
url = normalizeUrl(config.userInfoUrl, "DENCHCLAW_OAUTH_USERINFO_URL");
} catch {
return undefined;
}
@ -270,10 +270,10 @@ async function fetchUserEmail(
}
}
export async function loginIronclawOAuth(
ctx: IronclawOAuthContext,
config: IronclawOAuthConfig,
): Promise<IronclawOAuthCredentials> {
export async function loginDenchClawOAuth(
ctx: DenchClawOAuthContext,
config: DenchClawOAuthConfig,
): Promise<DenchClawOAuthCredentials> {
const { verifier, challenge } = generatePkce();
const state = randomBytes(16).toString("hex");
const authUrl = buildAuthUrl({ config, challenge, state });
@ -300,14 +300,14 @@ export async function loginIronclawOAuth(
`Auth URL: ${authUrl}`,
`Redirect URI: ${config.redirectUri}`,
].join("\n"),
"Ironclaw OAuth",
"DenchClaw OAuth",
);
ctx.log("");
ctx.log("Copy this URL:");
ctx.log(authUrl);
ctx.log("");
} else {
ctx.progress.update("Opening Ironclaw sign-in...");
ctx.progress.update("Opening DenchClaw sign-in...");
try {
await ctx.openUrl(authUrl);
} catch {

View File

@ -1,6 +1,6 @@
{
"id": "ironclaw-auth",
"providers": ["ironclaw"],
"id": "denchclaw-auth",
"providers": ["denchclaw"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -17,7 +17,7 @@
"@opentelemetry/semantic-conventions": "^1.39.0"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -4,7 +4,7 @@
"description": "OpenClaw Discord channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -9,7 +9,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -77,8 +77,9 @@ export async function maybeCreateDynamicAgent(params: {
}
// Resolve path templates with substitutions
const workspaceTemplate = dynamicCfg.workspaceTemplate ?? "~/.openclaw/workspace-{agentId}";
const agentDirTemplate = dynamicCfg.agentDirTemplate ?? "~/.openclaw/agents/{agentId}/agent";
const workspaceTemplate = dynamicCfg.workspaceTemplate ?? "~/.openclaw-dench/workspace-{agentId}";
const agentDirTemplate =
dynamicCfg.agentDirTemplate ?? "~/.openclaw-dench/agents/{agentId}/agent";
const workspace = resolveUserPath(
workspaceTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),

View File

@ -5,7 +5,7 @@
"description": "OpenClaw Google Antigravity OAuth provider plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw Gemini CLI OAuth provider plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -8,10 +8,10 @@
"google-auth-library": "^10.5.0"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"peerDependencies": {
"ironclaw": ">=2026.1.26"
"denchclaw": ">=2026.1.26"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw iMessage channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -4,7 +4,7 @@
"description": "OpenClaw IRC channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -1,39 +0,0 @@
# Ironclaw OAuth (OpenClaw plugin)
OAuth provider plugin for Ironclaw-hosted models.
## Enable
Bundled plugins are disabled by default. Enable this one:
```bash
openclaw plugins enable ironclaw-auth
```
Restart the Gateway after enabling.
## Authenticate
Set at least a client id, then run provider login:
```bash
export IRONCLAW_OAUTH_CLIENT_ID="<your-client-id>"
openclaw models auth login --provider ironclaw --set-default
```
## Optional env vars
- `IRONCLAW_OAUTH_CLIENT_SECRET`
- `IRONCLAW_OAUTH_AUTH_URL` (default: `https://auth.ironclaw.ai/oauth/authorize`)
- `IRONCLAW_OAUTH_TOKEN_URL` (default: `https://auth.ironclaw.ai/oauth/token`)
- `IRONCLAW_OAUTH_REDIRECT_URI` (default: `http://127.0.0.1:47089/oauth/callback`)
- `IRONCLAW_OAUTH_SCOPES` (space/comma separated)
- `IRONCLAW_OAUTH_USERINFO_URL` (optional for email display)
- `IRONCLAW_PROVIDER_BASE_URL` (default: `https://api.ironclaw.ai/v1`)
- `IRONCLAW_PROVIDER_MODEL_IDS` (space/comma separated, default: `chat`)
- `IRONCLAW_PROVIDER_DEFAULT_MODEL` (default: first model id)
## Notes
- This plugin configures `models.providers.ironclaw` as `openai-completions`.
- OAuth tokens are stored in auth profiles and the provider is patched into config automatically.

View File

@ -5,7 +5,7 @@
"description": "OpenClaw LINE channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw JSON-only LLM task plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -4,7 +4,7 @@
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -11,7 +11,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -4,7 +4,7 @@
"description": "OpenClaw Mattermost channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,10 +5,10 @@
"description": "OpenClaw core memory search plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"peerDependencies": {
"ironclaw": ">=2026.1.26"
"denchclaw": ">=2026.1.26"
},
"openclaw": {
"extensions": [

View File

@ -23,7 +23,7 @@ const LEGACY_STATE_DIRS: string[] = [];
function resolveDefaultDbPath(): string {
const home = homedir();
const preferred = join(home, ".openclaw", "memory", "lancedb");
const preferred = join(home, ".openclaw-dench", "memory", "lancedb");
try {
if (fs.existsSync(preferred)) {
return preferred;
@ -140,7 +140,7 @@ export const memoryConfigSchema = {
},
dbPath: {
label: "Database Path",
placeholder: "~/.openclaw/memory/lancedb",
placeholder: "~/.openclaw-dench/memory/lancedb",
advanced: true,
},
autoCapture: {

View File

@ -15,7 +15,7 @@
},
"dbPath": {
"label": "Database Path",
"placeholder": "~/.openclaw/memory/lancedb",
"placeholder": "~/.openclaw-dench/memory/lancedb",
"advanced": true
},
"autoCapture": {

View File

@ -10,7 +10,7 @@
"openai": "^6.22.0"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -8,7 +8,7 @@
"express": "^5.2.1"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -4,7 +4,7 @@
"description": "OpenClaw Nextcloud Talk channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -8,7 +8,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw Signal channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw Slack channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "Synology Chat channel plugin for OpenClaw",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -5,7 +5,7 @@
"description": "OpenClaw Telegram channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -7,7 +7,7 @@
"@urbit/aura": "^3.0.0"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -10,7 +10,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -9,7 +9,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -27,7 +27,7 @@ function resolveMode(input: string): "off" | "serve" | "funnel" {
}
function resolveDefaultStorePath(config: VoiceCallConfig): string {
const preferred = path.join(os.homedir(), ".openclaw", "voice-calls");
const preferred = path.join(os.homedir(), ".openclaw-dench", "voice-calls");
const resolvedPreferred = resolveUserPath(preferred);
const existing =
[resolvedPreferred].find((dir) => {

View File

@ -109,7 +109,7 @@ function resolveOpenClawRoot(): string {
}
for (const start of candidates) {
for (const name of ["ironclaw", "openclaw"]) {
for (const name of ["denchclaw", "openclaw"]) {
const found = findPackageRoot(start, name);
if (found) {
coreRootCache = found;

View File

@ -22,7 +22,7 @@ function resolveDefaultStoreBase(config: VoiceCallConfig, storePath?: string): s
if (rawOverride) {
return resolveUserPath(rawOverride);
}
const preferred = path.join(os.homedir(), ".openclaw", "voice-calls");
const preferred = path.join(os.homedir(), ".openclaw-dench", "voice-calls");
const candidates = [preferred].map((dir) => resolveUserPath(dir));
const existing =
candidates.find((dir) => {

View File

@ -5,7 +5,7 @@
"description": "OpenClaw WhatsApp channel plugin",
"type": "module",
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -7,7 +7,7 @@
"undici": "7.22.0"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -7,7 +7,7 @@
"@sinclair/typebox": "0.34.48"
},
"devDependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
},
"openclaw": {
"extensions": [

View File

@ -52,5 +52,5 @@ if (await tryImport("./dist/entry.js")) {
} else if (await tryImport("./dist/entry.mjs")) {
// OK
} else {
throw new Error("ironclaw: missing dist/entry.(m)js (build output).");
throw new Error("denchclaw: missing dist/entry.(m)js (build output).");
}

View File

@ -1,5 +1,5 @@
{
"name": "ironclaw",
"name": "denchclaw",
"version": "2026.2.22-1.1",
"description": "AI-powered CRM platform with multi-channel agent gateway, DuckDB workspace, and knowledge management",
"keywords": [],
@ -14,7 +14,8 @@
"url": "git+https://github.com/openclaw/openclaw.git"
},
"bin": {
"ironclaw": "openclaw.mjs"
"dench": "openclaw.mjs",
"denchclaw": "openclaw.mjs"
},
"directories": {
"doc": "docs",
@ -57,6 +58,8 @@
"deadcode:report:ci:ts-unused": "mkdir -p .artifacts/deadcode && pnpm deadcode:ts-unused > .artifacts/deadcode/ts-unused-exports.txt 2>&1 || true",
"deadcode:ts-prune": "pnpm dlx ts-prune src extensions scripts",
"deadcode:ts-unused": "pnpm dlx ts-unused-exports tsconfig.json --ignoreTestFiles --exitWithCount",
"denchclaw": "node scripts/run-node.mjs",
"denchclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
"dev": "node scripts/run-node.mjs",
"docs:bin": "node scripts/build-docs-list.mjs",
"docs:check-links": "node scripts/docs-link-audit.mjs",
@ -79,8 +82,6 @@
"ios:gen": "bash -lc './scripts/ios-configure-signing.sh && cd apps/ios && xcodegen generate'",
"ios:open": "bash -lc './scripts/ios-configure-signing.sh && cd apps/ios && xcodegen generate && open OpenClaw.xcodeproj'",
"ios:run": "bash -lc './scripts/ios-configure-signing.sh && cd apps/ios && xcodegen generate && xcodebuild -project OpenClaw.xcodeproj -scheme OpenClaw -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build && xcrun simctl boot \"${IOS_SIM:-iPhone 17}\" || true && xcrun simctl launch booted ai.openclaw.ios'",
"ironclaw": "node scripts/run-node.mjs",
"ironclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
"lint": "oxlint --type-aware",
"lint:all": "pnpm lint && pnpm lint:swift",
"lint:docs": "pnpm dlx markdownlint-cli2",

View File

@ -11,6 +11,6 @@
"./cli-entry": "./bin/clawdbot.js"
},
"dependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
}
}

View File

@ -11,6 +11,6 @@
"./cli-entry": "./bin/moltbot.js"
},
"dependencies": {
"ironclaw": "workspace:*"
"denchclaw": "workspace:*"
}
}

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# deploy.sh — build and publish ironclaw to npm
# deploy.sh — build and publish denchclaw to npm
#
# Versioning convention (mirrors upstream openclaw tags):
# --upstream <ver> Sync to an upstream release version.
@ -19,7 +19,7 @@
set -euo pipefail
PACKAGE_NAME="ironclaw"
PACKAGE_NAME="denchclaw"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
@ -188,7 +188,7 @@ fi
# ── build ────────────────────────────────────────────────────────────────────
# The `prepack` script (triggered by `npm publish`) runs the IronClaw build chain:
# The `prepack` script (triggered by `npm publish`) runs the DenchClaw build chain:
# pnpm build && pnpm web:build && pnpm web:prepack
# Running `pnpm build` here is a redundant fail-fast: catch CLI build errors
# before committing to a publish attempt.
@ -203,7 +203,7 @@ fi
# ── publish ──────────────────────────────────────────────────────────────────
# Always tag as "latest" — npm skips the latest tag for prerelease versions
# by default, but we want `npm i -g ironclaw` to always resolve to
# by default, but we want `npm i -g denchclaw` to always resolve to
# the most recently published version.
echo "publishing ${PACKAGE_NAME}@${VERSION}..."
npm publish --access public --tag latest "${NPM_FLAGS[@]}"

View File

@ -14,32 +14,32 @@ argument/env handling are treated as contract.
## Edge Matrix
| Module | Edge Case | Invariant Protected | Test File |
| ----------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `argv.ts` | `--` terminator handling | Flags after terminator are ignored for root parsing decisions | `src/cli/argv.test.ts` (new) |
| `argv.ts` | `--profile` missing/empty/equals forms | Missing value returns `null`/`undefined` exactly as designed; invalid values are not coerced | `src/cli/argv.test.ts` (new) |
| `argv.ts` | Positive int flag parsing (`0`, negative, NaN) | Invalid positive-int values are rejected without accidental fallback | `src/cli/argv.test.ts` (new) |
| `argv.ts` | Root `-v` alias vs command path | Root version alias is honored only in root-flag contexts, never when a command path is present | `src/cli/argv.test.ts` (new) |
| `argv.ts` | `buildParseArgv` runtime forms (`node`, `node-XX`, `bun`, direct binary) | Parse argv bootstrap is stable across runtimes and executable names | `src/cli/argv.test.ts` (new) |
| `argv.ts` | `shouldMigrateState` exemptions | Read-only/status commands never trigger migration path | `src/cli/argv.test.ts` (new) |
| `run-main.ts` | bootstrap cutover rollout stages | `legacy` always disables cutover, `beta` requires explicit opt-in, `default/internal` enable by default | `src/cli/run-main.test.ts` (existing, expand) |
| `run-main.ts` | delegation disabled flags | Delegation is off when disable env is truthy in either namespace | `src/cli/run-main.test.ts` (existing, expand) |
| `run-main.ts` | delegation loop prevention | Delegation loop env markers hard-stop with explicit error | `src/cli/run-main.test.ts` (existing, expand) |
| `run-main.ts` | `shouldEnsureCliPath` command carve-outs | Health/status/read-only commands skip path mutations | `src/cli/run-main.test.ts` (existing, expand) |
| `profile-utils.ts` | profile name normalization | Only valid profile names are accepted; normalization is idempotent | `src/cli/profile-utils.test.ts` (new) |
| `profile.ts` | `--dev` + `--profile` conflict | Conflict is rejected with non-zero outcome and actionable error text | `src/cli/profile.test.ts` (new) |
| `profile.ts` | explicit profile propagation | Parsed profile and env output are stable regardless of option ordering | `src/cli/profile.test.ts` (new) |
| `profile.ts` | root vs command-local bootstrap profile flag | `ironclaw --profile X bootstrap` and `ironclaw bootstrap --profile X` resolve to identical profile env | `src/cli/profile.test.ts` (existing, expand) |
| `windows-argv.ts` | control chars and duplicate exec path | Normalization removes terminal control noise while preserving args | `src/cli/windows-argv.test.ts` (new) |
| `windows-argv.ts` | quoted executable path stripping | Windows executable wrappers are normalized without dropping real args | `src/cli/windows-argv.test.ts` (new) |
| `respawn-policy.ts` | help/version short-circuit | Help/version always bypass respawn behavior | `src/cli/respawn-policy.test.ts` (new) |
| `cli-name.ts` | cli name resolution/replacement | Name replacement only targets intended CLI token boundaries | `src/cli/cli-name.test.ts` (new) |
| `ports.ts` | malformed `lsof` lines | Port parser tolerates malformed rows and only returns valid process records | `src/cli/ports.test.ts` (new) |
| `cli-utils.ts` | runtime command failure path | Command failures return deterministic non-zero exit behavior | `src/cli/cli-utils.test.ts` (new) |
| `bootstrap-external.ts` | auth profile mismatch/missing | Missing or mismatched provider auth fails with remediation | `src/cli/bootstrap-external.test.ts` (existing) |
| `bootstrap-external.ts` | onboarding/gateway auto-fix workflow | Bootstrap command executes expected fallback sequence and reports recovery outcome | `src/cli/bootstrap-external.bootstrap-command.test.ts` (existing) |
| `bootstrap-external.ts` | device signature/token mismatch remediation | Device-auth failures provide reset-first guidance + break-glass toggle with explicit revert | `src/cli/bootstrap-external.test.ts` (existing, expand) |
| `bootstrap-external.ts` | web UI port ownership and deterministic bootstrap port selection | Bootstrap never silently drifts to sibling web ports and keeps expected UI URL stable | `src/cli/bootstrap-external.bootstrap-command.test.ts` (expand) |
| Module | Edge Case | Invariant Protected | Test File |
| ----------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `argv.ts` | `--` terminator handling | Flags after terminator are ignored for root parsing decisions | `src/cli/argv.test.ts` (new) |
| `argv.ts` | `--profile` missing/empty/equals forms | Missing value returns `null`/`undefined` exactly as designed; invalid values are not coerced | `src/cli/argv.test.ts` (new) |
| `argv.ts` | Positive int flag parsing (`0`, negative, NaN) | Invalid positive-int values are rejected without accidental fallback | `src/cli/argv.test.ts` (new) |
| `argv.ts` | Root `-v` alias vs command path | Root version alias is honored only in root-flag contexts, never when a command path is present | `src/cli/argv.test.ts` (new) |
| `argv.ts` | `buildParseArgv` runtime forms (`node`, `node-XX`, `bun`, direct binary) | Parse argv bootstrap is stable across runtimes and executable names | `src/cli/argv.test.ts` (new) |
| `argv.ts` | `shouldMigrateState` exemptions | Read-only/status commands never trigger migration path | `src/cli/argv.test.ts` (new) |
| `run-main.ts` | bootstrap cutover rollout stages | `legacy` always disables cutover, `beta` requires explicit opt-in, `default/internal` enable by default | `src/cli/run-main.test.ts` (existing, expand) |
| `run-main.ts` | delegation disabled flags | Delegation is off when disable env is truthy in either namespace | `src/cli/run-main.test.ts` (existing, expand) |
| `run-main.ts` | delegation loop prevention | Delegation loop env markers hard-stop with explicit error | `src/cli/run-main.test.ts` (existing, expand) |
| `run-main.ts` | `shouldEnsureCliPath` command carve-outs | Health/status/read-only commands skip path mutations | `src/cli/run-main.test.ts` (existing, expand) |
| `profile-utils.ts` | profile name normalization | Only valid profile names are accepted; normalization is idempotent | `src/cli/profile-utils.test.ts` (new) |
| `profile.ts` | `--dev` + `--profile` conflict | Conflict is rejected with non-zero outcome and actionable error text | `src/cli/profile.test.ts` (new) |
| `profile.ts` | explicit profile propagation | Parsed profile and env output are stable regardless of option ordering | `src/cli/profile.test.ts` (new) |
| `profile.ts` | root vs command-local bootstrap profile flag | `denchclaw --profile X bootstrap` and `denchclaw bootstrap --profile X` resolve to identical profile env | `src/cli/profile.test.ts` (existing, expand) |
| `windows-argv.ts` | control chars and duplicate exec path | Normalization removes terminal control noise while preserving args | `src/cli/windows-argv.test.ts` (new) |
| `windows-argv.ts` | quoted executable path stripping | Windows executable wrappers are normalized without dropping real args | `src/cli/windows-argv.test.ts` (new) |
| `respawn-policy.ts` | help/version short-circuit | Help/version always bypass respawn behavior | `src/cli/respawn-policy.test.ts` (new) |
| `cli-name.ts` | cli name resolution/replacement | Name replacement only targets intended CLI token boundaries | `src/cli/cli-name.test.ts` (new) |
| `ports.ts` | malformed `lsof` lines | Port parser tolerates malformed rows and only returns valid process records | `src/cli/ports.test.ts` (new) |
| `cli-utils.ts` | runtime command failure path | Command failures return deterministic non-zero exit behavior | `src/cli/cli-utils.test.ts` (new) |
| `bootstrap-external.ts` | auth profile mismatch/missing | Missing or mismatched provider auth fails with remediation | `src/cli/bootstrap-external.test.ts` (existing) |
| `bootstrap-external.ts` | onboarding/gateway auto-fix workflow | Bootstrap command executes expected fallback sequence and reports recovery outcome | `src/cli/bootstrap-external.bootstrap-command.test.ts` (existing) |
| `bootstrap-external.ts` | device signature/token mismatch remediation | Device-auth failures provide reset-first guidance + break-glass toggle with explicit revert | `src/cli/bootstrap-external.test.ts` (existing, expand) |
| `bootstrap-external.ts` | web UI port ownership and deterministic bootstrap port selection | Bootstrap never silently drifts to sibling web ports and keeps expected UI URL stable | `src/cli/bootstrap-external.bootstrap-command.test.ts` (expand) |
## Exit/Output Contract Checks

View File

@ -13,64 +13,66 @@ import {
describe("argv helpers", () => {
it("detects help/version flags and root -v alias only in root-flag contexts", () => {
expect(hasHelpOrVersion(["node", "ironclaw", "--help"])).toBe(true);
expect(hasHelpOrVersion(["node", "ironclaw", "-V"])).toBe(true);
expect(hasHelpOrVersion(["node", "ironclaw", "-v"])).toBe(true);
expect(hasRootVersionAlias(["node", "ironclaw", "-v", "chat"])).toBe(false);
expect(hasHelpOrVersion(["node", "denchclaw", "--help"])).toBe(true);
expect(hasHelpOrVersion(["node", "denchclaw", "-V"])).toBe(true);
expect(hasHelpOrVersion(["node", "denchclaw", "-v"])).toBe(true);
expect(hasRootVersionAlias(["node", "denchclaw", "-v", "chat"])).toBe(false);
});
it("extracts flag values across --name value and --name=value forms", () => {
expect(getFlagValue(["node", "ironclaw", "--profile", "dev"], "--profile")).toBe("dev");
expect(getFlagValue(["node", "ironclaw", "--profile=team-a"], "--profile")).toBe("team-a");
expect(getFlagValue(["node", "ironclaw", "--profile", "--verbose"], "--profile")).toBeNull();
expect(getFlagValue(["node", "ironclaw", "--profile="], "--profile")).toBeNull();
expect(getFlagValue(["node", "denchclaw", "--profile", "dev"], "--profile")).toBe("dev");
expect(getFlagValue(["node", "denchclaw", "--profile=team-a"], "--profile")).toBe("team-a");
expect(getFlagValue(["node", "denchclaw", "--profile", "--verbose"], "--profile")).toBeNull();
expect(getFlagValue(["node", "denchclaw", "--profile="], "--profile")).toBeNull();
});
it("parses positive integer flags and rejects invalid numeric values", () => {
expect(getPositiveIntFlagValue(["node", "ironclaw", "--port", "19001"], "--port")).toBe(19001);
expect(getPositiveIntFlagValue(["node", "ironclaw", "--port", "0"], "--port")).toBeUndefined();
expect(getPositiveIntFlagValue(["node", "ironclaw", "--port", "-1"], "--port")).toBeUndefined();
expect(getPositiveIntFlagValue(["node", "denchclaw", "--port", "19001"], "--port")).toBe(19001);
expect(getPositiveIntFlagValue(["node", "denchclaw", "--port", "0"], "--port")).toBeUndefined();
expect(
getPositiveIntFlagValue(["node", "ironclaw", "--port", "abc"], "--port"),
getPositiveIntFlagValue(["node", "denchclaw", "--port", "-1"], "--port"),
).toBeUndefined();
expect(
getPositiveIntFlagValue(["node", "denchclaw", "--port", "abc"], "--port"),
).toBeUndefined();
});
it("derives command path while skipping leading flags and stopping at terminator", () => {
// Low-level parser skips flag tokens but not their values.
expect(getCommandPath(["node", "ironclaw", "--profile", "dev", "chat"], 2)).toEqual([
expect(getCommandPath(["node", "denchclaw", "--profile", "dev", "chat"], 2)).toEqual([
"dev",
"chat",
]);
expect(getCommandPath(["node", "ironclaw", "config", "get"], 2)).toEqual(["config", "get"]);
expect(getCommandPath(["node", "ironclaw", "--", "chat", "send"], 2)).toEqual([]);
expect(getPrimaryCommand(["node", "ironclaw", "--verbose", "status"])).toBe("status");
expect(getCommandPath(["node", "denchclaw", "config", "get"], 2)).toEqual(["config", "get"]);
expect(getCommandPath(["node", "denchclaw", "--", "chat", "send"], 2)).toEqual([]);
expect(getPrimaryCommand(["node", "denchclaw", "--verbose", "status"])).toBe("status");
});
it("builds parse argv consistently across runtime invocation styles", () => {
expect(
buildParseArgv({
programName: "ironclaw",
programName: "denchclaw",
rawArgs: ["node", "cli.js", "status"],
}),
).toEqual(["node", "cli.js", "status"]);
expect(
buildParseArgv({
programName: "ironclaw",
rawArgs: ["ironclaw", "status"],
programName: "denchclaw",
rawArgs: ["denchclaw", "status"],
}),
).toEqual(["node", "ironclaw", "status"]);
).toEqual(["node", "denchclaw", "status"]);
expect(
buildParseArgv({
programName: "ironclaw",
programName: "denchclaw",
rawArgs: ["node-22.12.0.exe", "cli.js", "agent", "run"],
}),
).toEqual(["node-22.12.0.exe", "cli.js", "agent", "run"]);
expect(
buildParseArgv({
programName: "ironclaw",
programName: "denchclaw",
rawArgs: ["bun", "cli.ts", "status"],
}),
).toEqual(["bun", "cli.ts", "status"]);
@ -87,7 +89,7 @@ describe("argv helpers", () => {
expect(shouldMigrateStateFromPath(["agent"])).toBe(false);
expect(shouldMigrateStateFromPath(["chat", "send"])).toBe(true);
expect(shouldMigrateState(["node", "ironclaw", "health"])).toBe(false);
expect(shouldMigrateState(["node", "ironclaw", "chat", "send"])).toBe(true);
expect(shouldMigrateState(["node", "denchclaw", "health"])).toBe(false);
expect(shouldMigrateState(["node", "denchclaw", "chat", "send"])).toBe(true);
});
});

View File

@ -160,7 +160,7 @@ export function buildParseArgv(params: {
const normalizedArgv =
programName && baseArgv[0] === programName
? baseArgv.slice(1)
: baseArgv[0]?.endsWith("openclaw") || baseArgv[0]?.endsWith("ironclaw")
: baseArgv[0]?.endsWith("openclaw") || baseArgv[0]?.endsWith("denchclaw")
? baseArgv.slice(1)
: baseArgv;
const executable = (normalizedArgv[0]?.split(/[/\\]/).pop() ?? "").toLowerCase();
@ -169,7 +169,7 @@ export function buildParseArgv(params: {
if (looksLikeNode) {
return normalizedArgv;
}
return ["node", programName || "ironclaw", ...normalizedArgv];
return ["node", programName || "denchclaw", ...normalizedArgv];
}
const nodeExecutablePattern = /^node-\d+(?:\.\d+)*(?:\.exe)?$/;

View File

@ -21,15 +21,15 @@ const hasVersionFlag = (argv: string[]) =>
argv.some((arg) => arg === "--version" || arg === "-V") || hasRootVersionAlias(argv);
// ---------------------------------------------------------------------------
// IRONCLAW ASCII art (figlet "ANSI Shadow" font, baked at build time)
// DENCHCLAW ASCII art (figlet "ANSI Shadow" font, baked at build time)
// ---------------------------------------------------------------------------
const IRONCLAW_ASCII = [
" ██╗██████╗ ██████╗ ███╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
" ██║██╔══██╗██╔═══██╗████╗ ██║██╔════╝██║ ██╔══██╗██║ ██║",
" ██║██████╔╝██║ ██║██╔██╗ ██║██║ ██║ ███████║██║ █╗ ██║",
" ██║██╔══██╗██║ ██║██║╚██╗██║██║ ██║ ██╔══██║██║███╗██║",
" ██║██║ ██║╚██████╔╝██║ ╚████║╚██████╗███████╗██║ ██║╚███╔███╔╝",
" ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ",
const DENCHCLAW_ASCII = [
"██████╗ ███████╗███╗ ██╗ ██████╗██╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
"██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██║██╔════╝██║ ██╔══██╗██║ ██║",
"██║ ██║█████╗ ██╔██╗ ██║██║ ███████║██║ ██║ ███████║██║ █╗ ██║",
"██║ ██║██╔══╝ ██║╚██╗██║██║ ██╔══██║██║ ██║ ██╔══██║██║███╗██║",
"██████╔╝███████╗██║ ╚████║╚██████╗██║ ██║╚██████╗███████╗██║ ██║╚███╔███╔╝",
"╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ",
];
// ---------------------------------------------------------------------------
@ -71,19 +71,19 @@ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve,
* at 12 fps, completing 3 full gradient cycles.
*/
async function animateIronBanner(): Promise<void> {
const lineCount = IRONCLAW_ASCII.length;
const lineCount = DENCHCLAW_ASCII.length;
const fps = 12;
const totalFrames = IRON_GRADIENT_COLORS.length * 3; // 3 full shimmer sweeps
const frameMs = Math.round(1000 / fps);
// Print the first frame to claim vertical space
process.stdout.write(renderGradientFrame(IRONCLAW_ASCII, 0) + "\n");
process.stdout.write(renderGradientFrame(DENCHCLAW_ASCII, 0) + "\n");
for (let frame = 1; frame < totalFrames; frame++) {
await sleep(frameMs);
// Move cursor up to overwrite the previous frame
process.stdout.write(`\x1b[${lineCount}A\r`);
process.stdout.write(renderGradientFrame(IRONCLAW_ASCII, frame) + "\n");
process.stdout.write(renderGradientFrame(DENCHCLAW_ASCII, frame) + "\n");
}
}
@ -94,9 +94,9 @@ async function animateIronBanner(): Promise<void> {
export function formatCliBannerArt(options: BannerOptions = {}): string {
const rich = options.richTty ?? isRich();
if (!rich) {
return IRONCLAW_ASCII.join("\n");
return DENCHCLAW_ASCII.join("\n");
}
return renderGradientFrame(IRONCLAW_ASCII, 0);
return renderGradientFrame(DENCHCLAW_ASCII, 0);
}
// ---------------------------------------------------------------------------
@ -108,7 +108,7 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {}
const commitLabel = commit ?? "unknown";
const tagline = pickTagline(options);
const rich = options.richTty ?? isRich();
const title = "IRONCLAW";
const title = "DENCHCLAW";
const prefix = " ";
const columns = options.columns ?? process.stdout.columns ?? 120;
const plainFullLine = `${prefix}${title} ${version} (${commitLabel}) — ${tagline}`;
@ -162,7 +162,7 @@ export async function emitCliBanner(version: string, options: BannerOptions = {}
await animateIronBanner();
} else {
// Plain ASCII fallback
process.stdout.write(IRONCLAW_ASCII.join("\n") + "\n");
process.stdout.write(DENCHCLAW_ASCII.join("\n") + "\n");
}
const line = formatCliBannerLine(version, options);

View File

@ -20,12 +20,13 @@ function getCheck(
}
function createTempStateDir(): string {
const dir = path.join(
const homeDir = path.join(
tmpdir(),
`ironclaw-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
`denchclaw-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
mkdirSync(dir, { recursive: true });
return dir;
const stateDir = path.join(homeDir, ".openclaw-dench");
mkdirSync(stateDir, { recursive: true });
return stateDir;
}
function writeConfig(stateDir: string, config: Record<string, unknown>): void {
@ -63,7 +64,7 @@ describe("bootstrap-external diagnostics", () => {
});
const baseParams = (dir: string) => ({
profile: "ironclaw",
profile: "dench",
openClawCliAvailable: true,
openClawVersion: "2026.3.1",
gatewayPort: 18789,
@ -74,7 +75,7 @@ describe("bootstrap-external diagnostics", () => {
rolloutStage: "default" as const,
legacyFallbackEnabled: false,
stateDir: dir,
env: { HOME: "/home/testuser" },
env: { HOME: path.dirname(dir), OPENCLAW_HOME: path.dirname(dir) },
});
it("reports passing checks including agent-auth when config and keys exist", () => {
@ -167,7 +168,7 @@ describe("bootstrap-external diagnostics", () => {
expect(gateway.status).toBe("fail");
expect(String(gateway.remediation)).toContain("dangerouslyDisableDeviceAuth true");
expect(String(gateway.remediation)).toContain("dangerouslyDisableDeviceAuth false");
expect(String(gateway.remediation)).toContain("--profile ironclaw");
expect(String(gateway.remediation)).toContain("--profile dench");
});
it("marks rollout-stage as warning for beta and includes opt-in guidance", () => {
@ -178,13 +179,17 @@ describe("bootstrap-external diagnostics", () => {
const rollout = getCheck(diagnostics, "rollout-stage");
expect(rollout.status).toBe("warn");
expect(String(rollout.remediation)).toContain("IRONCLAW_BOOTSTRAP_BETA_OPT_IN");
expect(String(rollout.remediation)).toContain("DENCHCLAW_BOOTSTRAP_BETA_OPT_IN");
});
it("fails cutover-gates when enforcement is enabled without gate envs", () => {
const diagnostics = buildBootstrapDiagnostics({
...baseParams(stateDir),
env: { HOME: "/home/testuser", IRONCLAW_BOOTSTRAP_ENFORCE_SAFETY_GATES: "1" },
env: {
HOME: path.dirname(stateDir),
OPENCLAW_HOME: path.dirname(stateDir),
DENCHCLAW_BOOTSTRAP_ENFORCE_SAFETY_GATES: "1",
},
});
expect(getCheck(diagnostics, "cutover-gates").status).toBe("fail");
@ -195,9 +200,10 @@ describe("bootstrap-external diagnostics", () => {
const diagnostics = buildBootstrapDiagnostics({
...baseParams(stateDir),
env: {
HOME: "/home/testuser",
IRONCLAW_BOOTSTRAP_MIGRATION_SUITE_OK: "1",
IRONCLAW_BOOTSTRAP_ONBOARDING_E2E_OK: "1",
HOME: path.dirname(stateDir),
OPENCLAW_HOME: path.dirname(stateDir),
DENCHCLAW_BOOTSTRAP_MIGRATION_SUITE_OK: "1",
DENCHCLAW_BOOTSTRAP_ONBOARDING_E2E_OK: "1",
},
});
@ -271,16 +277,18 @@ describe("checkAgentAuth", () => {
});
describe("bootstrap-external rollout env helpers", () => {
it("resolves rollout stage from ironclaw/openclaw env vars", () => {
expect(resolveBootstrapRolloutStage({ IRONCLAW_BOOTSTRAP_ROLLOUT: "beta" })).toBe("beta");
it("resolves rollout stage from denchclaw/openclaw env vars", () => {
expect(resolveBootstrapRolloutStage({ DENCHCLAW_BOOTSTRAP_ROLLOUT: "beta" })).toBe("beta");
expect(resolveBootstrapRolloutStage({ OPENCLAW_BOOTSTRAP_ROLLOUT: "internal" })).toBe(
"internal",
);
expect(resolveBootstrapRolloutStage({ IRONCLAW_BOOTSTRAP_ROLLOUT: "invalid" })).toBe("default");
expect(resolveBootstrapRolloutStage({ DENCHCLAW_BOOTSTRAP_ROLLOUT: "invalid" })).toBe(
"default",
);
});
it("detects legacy fallback via either env namespace", () => {
expect(isLegacyFallbackEnabled({ IRONCLAW_BOOTSTRAP_LEGACY_FALLBACK: "1" })).toBe(true);
expect(isLegacyFallbackEnabled({ DENCHCLAW_BOOTSTRAP_LEGACY_FALLBACK: "1" })).toBe(true);
expect(isLegacyFallbackEnabled({ OPENCLAW_BOOTSTRAP_LEGACY_FALLBACK: "true" })).toBe(true);
expect(isLegacyFallbackEnabled({})).toBe(false);
});

View File

@ -4,7 +4,7 @@ import { DEFAULT_CLI_NAME, replaceCliName, resolveCliName } from "./cli-name.js"
describe("cli-name", () => {
it("resolves known CLI names from argv[1]", () => {
expect(resolveCliName(["node", "openclaw"])).toBe("openclaw");
expect(resolveCliName(["node", "ironclaw"])).toBe("ironclaw");
expect(resolveCliName(["node", "denchclaw"])).toBe("denchclaw");
expect(resolveCliName(["node", "/usr/local/bin/openclaw"])).toBe("openclaw");
});
@ -13,13 +13,13 @@ describe("cli-name", () => {
});
it("replaces CLI name in command prefixes while preserving package runner prefix", () => {
expect(replaceCliName("openclaw status", "ironclaw")).toBe("ironclaw status");
expect(replaceCliName("pnpm openclaw status", "ironclaw")).toBe("pnpm ironclaw status");
expect(replaceCliName("npx ironclaw status", "openclaw")).toBe("npx openclaw status");
expect(replaceCliName("openclaw status", "denchclaw")).toBe("denchclaw status");
expect(replaceCliName("pnpm openclaw status", "denchclaw")).toBe("pnpm denchclaw status");
expect(replaceCliName("npx denchclaw status", "openclaw")).toBe("npx openclaw status");
});
it("keeps command unchanged when it does not start with a known CLI prefix", () => {
expect(replaceCliName("echo openclaw status", "ironclaw")).toBe("echo openclaw status");
expect(replaceCliName("echo openclaw status", "denchclaw")).toBe("echo openclaw status");
expect(replaceCliName(" ", "openclaw")).toBe(" ");
});
});

View File

@ -1,9 +1,9 @@
import path from "node:path";
export const DEFAULT_CLI_NAME = "ironclaw";
export const DEFAULT_CLI_NAME = "denchclaw";
const KNOWN_CLI_NAMES = new Set([DEFAULT_CLI_NAME, "openclaw"]);
const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(ironclaw|openclaw)\b/;
const CLI_PREFIX_RE = /^(?:((?:pnpm|npm|bunx|npx)\s+))?(denchclaw|openclaw)\b/;
export function resolveCliName(argv: string[] = process.argv): string {
const argv1 = argv[1];

View File

@ -3,7 +3,7 @@ import { isValidProfileName, normalizeProfileName } from "./profile-utils.js";
describe("profile-utils", () => {
it("accepts path-safe profile names and rejects unsafe values", () => {
expect(isValidProfileName("ironclaw")).toBe(true);
expect(isValidProfileName("denchclaw")).toBe(true);
expect(isValidProfileName("Team_A-1")).toBe(true);
expect(isValidProfileName("")).toBe(false);
expect(isValidProfileName(" has-space ")).toBe(false);

View File

@ -1,43 +1,43 @@
import { describe, expect, it } from "vitest";
import { applyCliProfileEnv, parseCliProfileArgs, IRONCLAW_PROFILE } from "./profile.js";
import { applyCliProfileEnv, parseCliProfileArgs, DENCHCLAW_PROFILE } from "./profile.js";
describe("parseCliProfileArgs", () => {
it("returns default profile parsing when no args are provided", () => {
expect(parseCliProfileArgs(["node", "ironclaw"])).toEqual({
expect(parseCliProfileArgs(["node", "denchclaw"])).toEqual({
ok: true,
profile: null,
argv: ["node", "ironclaw"],
argv: ["node", "denchclaw"],
});
});
it("parses --profile and strips profile flags before command execution", () => {
expect(parseCliProfileArgs(["node", "ironclaw", "--profile", "dev", "chat"])).toEqual({
expect(parseCliProfileArgs(["node", "denchclaw", "--profile", "dev", "chat"])).toEqual({
ok: true,
profile: "dev",
argv: ["node", "ironclaw", "chat"],
argv: ["node", "denchclaw", "chat"],
});
expect(parseCliProfileArgs(["node", "ironclaw", "--profile=team-a", "status"])).toEqual({
expect(parseCliProfileArgs(["node", "denchclaw", "--profile=team-a", "status"])).toEqual({
ok: true,
profile: "team-a",
argv: ["node", "ironclaw", "status"],
argv: ["node", "denchclaw", "status"],
});
});
it("rejects missing and invalid profile inputs", () => {
expect(parseCliProfileArgs(["node", "ironclaw", "--profile"])).toEqual({
expect(parseCliProfileArgs(["node", "denchclaw", "--profile"])).toEqual({
ok: false,
error: "--profile requires a value",
});
expect(parseCliProfileArgs(["node", "ironclaw", "--profile", "bad profile"])).toEqual({
expect(parseCliProfileArgs(["node", "denchclaw", "--profile", "bad profile"])).toEqual({
ok: false,
error: 'Invalid --profile (use letters, numbers, "_", "-" only)',
});
});
it("allows --dev and --profile together (Ironclaw forces ironclaw anyway)", () => {
const result = parseCliProfileArgs(["node", "ironclaw", "--dev", "--profile", "team-a"]);
it("allows --dev and --profile together (DenchClaw forces dench anyway)", () => {
const result = parseCliProfileArgs(["node", "denchclaw", "--dev", "--profile", "team-a"]);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.profile).toBe("team-a");
@ -45,16 +45,16 @@ describe("parseCliProfileArgs", () => {
});
it("stops profile parsing once command path begins", () => {
expect(parseCliProfileArgs(["node", "ironclaw", "chat", "--profile", "dev"])).toEqual({
expect(parseCliProfileArgs(["node", "denchclaw", "chat", "--profile", "dev"])).toEqual({
ok: true,
profile: null,
argv: ["node", "ironclaw", "chat", "--profile", "dev"],
argv: ["node", "denchclaw", "chat", "--profile", "dev"],
});
});
});
describe("applyCliProfileEnv", () => {
it("always forces ironclaw profile regardless of requested profile (single profile enforcement)", () => {
it("always forces dench profile regardless of requested profile (single profile enforcement)", () => {
const env: Record<string, string | undefined> = {};
const result = applyCliProfileEnv({
profile: "team-a",
@ -62,13 +62,13 @@ describe("applyCliProfileEnv", () => {
homedir: () => "/tmp/home",
});
expect(result.effectiveProfile).toBe(IRONCLAW_PROFILE);
expect(env.OPENCLAW_PROFILE).toBe(IRONCLAW_PROFILE);
expect(env.OPENCLAW_STATE_DIR).toBe("/tmp/home/.openclaw-ironclaw");
expect(env.OPENCLAW_CONFIG_PATH).toBe("/tmp/home/.openclaw-ironclaw/openclaw.json");
expect(result.effectiveProfile).toBe(DENCHCLAW_PROFILE);
expect(env.OPENCLAW_PROFILE).toBe(DENCHCLAW_PROFILE);
expect(env.OPENCLAW_STATE_DIR).toBe("/tmp/home/.openclaw-dench");
expect(env.OPENCLAW_CONFIG_PATH).toBe("/tmp/home/.openclaw-dench/openclaw.json");
});
it("emits warning when non-ironclaw profile is requested (prevents silent override)", () => {
it("emits warning when non-dench profile is requested (prevents silent override)", () => {
const env: Record<string, string | undefined> = {};
const result = applyCliProfileEnv({
profile: "team-a",
@ -78,20 +78,20 @@ describe("applyCliProfileEnv", () => {
expect(result.warning).toBeDefined();
expect(result.warning).toContain("team-a");
expect(result.warning).toContain(IRONCLAW_PROFILE);
expect(result.warning).toContain(DENCHCLAW_PROFILE);
expect(result.requestedProfile).toBe("team-a");
});
it("no warning when ironclaw profile is requested (normal path)", () => {
it("no warning when dench profile is requested (normal path)", () => {
const env: Record<string, string | undefined> = {};
const result = applyCliProfileEnv({
profile: IRONCLAW_PROFILE,
profile: DENCHCLAW_PROFILE,
env,
homedir: () => "/tmp/home",
});
expect(result.warning).toBeUndefined();
expect(result.effectiveProfile).toBe(IRONCLAW_PROFILE);
expect(result.effectiveProfile).toBe(DENCHCLAW_PROFILE);
});
it("no warning when no profile is specified (default path)", () => {
@ -102,7 +102,7 @@ describe("applyCliProfileEnv", () => {
});
expect(result.warning).toBeUndefined();
expect(result.effectiveProfile).toBe(IRONCLAW_PROFILE);
expect(result.effectiveProfile).toBe(DENCHCLAW_PROFILE);
});
it("always overwrites OPENCLAW_STATE_DIR to pinned path (prevents state drift)", () => {
@ -116,9 +116,9 @@ describe("applyCliProfileEnv", () => {
homedir: () => "/tmp/home",
});
expect(env.OPENCLAW_STATE_DIR).toBe("/tmp/home/.openclaw-ironclaw");
expect(env.OPENCLAW_CONFIG_PATH).toBe("/tmp/home/.openclaw-ironclaw/openclaw.json");
expect(result.stateDir).toBe("/tmp/home/.openclaw-ironclaw");
expect(env.OPENCLAW_STATE_DIR).toBe("/tmp/home/.openclaw-dench");
expect(env.OPENCLAW_CONFIG_PATH).toBe("/tmp/home/.openclaw-dench/openclaw.json");
expect(result.stateDir).toBe("/tmp/home/.openclaw-dench");
});
it("picks up OPENCLAW_PROFILE from env when no explicit profile is passed", () => {
@ -131,7 +131,7 @@ describe("applyCliProfileEnv", () => {
});
expect(result.requestedProfile).toBe("from-env");
expect(result.effectiveProfile).toBe(IRONCLAW_PROFILE);
expect(result.effectiveProfile).toBe(DENCHCLAW_PROFILE);
expect(result.warning).toContain("from-env");
});

View File

@ -3,8 +3,8 @@ import path from "node:path";
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
import { isValidProfileName } from "./profile-utils.js";
export const IRONCLAW_PROFILE = "ironclaw";
const IRONCLAW_STATE_DIRNAME = ".openclaw-ironclaw";
export const DENCHCLAW_PROFILE = "dench";
const DENCHCLAW_STATE_DIRNAME = ".openclaw-dench";
export type CliProfileParseResult =
| { ok: true; profile: string | null; argv: string[] }
@ -89,7 +89,7 @@ function resolveProfileStateDir(
): string {
return path.join(
resolveRequiredHomeDir(env as NodeJS.ProcessEnv, homedir),
IRONCLAW_STATE_DIRNAME,
DENCHCLAW_STATE_DIRNAME,
);
}
@ -106,9 +106,9 @@ export function applyCliProfileEnv(params: {
const env = params.env ?? (process.env as Record<string, string | undefined>);
const homedir = params.homedir ?? os.homedir;
const requestedProfile = (params.profile?.trim() || env.OPENCLAW_PROFILE?.trim() || null) ?? null;
const profile = IRONCLAW_PROFILE;
const profile = DENCHCLAW_PROFILE;
// Ironclaw always runs in the pinned profile/state path.
// DenchClaw always runs in the pinned profile/state path.
env.OPENCLAW_PROFILE = profile;
const stateDir = resolveProfileStateDir(env, homedir);
@ -117,7 +117,7 @@ export function applyCliProfileEnv(params: {
const warning =
requestedProfile && requestedProfile !== profile
? `Ignoring requested profile '${requestedProfile}'; Ironclaw always uses --profile ${IRONCLAW_PROFILE}.`
? `Ignoring requested profile '${requestedProfile}'; DenchClaw always uses --profile ${DENCHCLAW_PROFILE}.`
: undefined;
return {

View File

@ -18,7 +18,7 @@ type CoreCliEntry = {
const BOOTSTRAP_ENTRY: CoreCliEntry = {
name: "bootstrap",
description: "Bootstrap IronClaw + OpenClaw and launch the web UI",
description: "Bootstrap DenchClaw + OpenClaw and launch the web UI",
register: async ({ program }) => {
const mod = await import("./register.bootstrap.js");
mod.registerBootstrapCommand(program);

View File

@ -27,7 +27,7 @@ const EXAMPLES = [
["openclaw gateway --port 18789", "Run the WebSocket Gateway locally."],
[
"openclaw --profile team-a gateway",
"Compatibility flag example: warns and still runs with --profile ironclaw.",
"Compatibility flag example: warns and still runs with --profile dench.",
],
["openclaw gateway --force", "Kill anything bound to the default gateway port, then start it."],
["openclaw gateway ...", "Gateway control via WebSocket."],
@ -48,12 +48,9 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
.version(ctx.programVersion)
.option(
"--dev",
"Compatibility flag; Ironclaw always uses --profile ironclaw and ~/.openclaw-ironclaw",
"Compatibility flag; DenchClaw always uses --profile dench and ~/.openclaw-dench",
)
.option(
"--profile <name>",
"Compatibility flag; non-ironclaw values are ignored with a warning",
);
.option("--profile <name>", "Compatibility flag; non-dench values are ignored with a warning");
program.option("--no-color", "Disable ANSI colors", false);
program.helpOption("-h, --help", "Display help for command");

View File

@ -8,11 +8,8 @@ import { runCommandWithRuntime } from "../cli-utils.js";
export function registerBootstrapCommand(program: Command) {
program
.command("bootstrap")
.description("Bootstrap IronClaw on top of OpenClaw and open the web UI")
.option(
"--profile <name>",
"Compatibility flag; non-ironclaw values are ignored with a warning",
)
.description("Bootstrap DenchClaw on top of OpenClaw and open the web UI")
.option("--profile <name>", "Compatibility flag; non-dench values are ignored with a warning")
.option("--force-onboard", "Run onboarding even if config already exists", false)
.option("--non-interactive", "Skip prompts where possible", false)
.option("--yes", "Auto-approve install prompts", false)

View File

@ -3,11 +3,11 @@ import { shouldSkipRespawnForArgv } from "./respawn-policy.js";
describe("shouldSkipRespawnForArgv", () => {
it("skips respawn for help/version invocations", () => {
expect(shouldSkipRespawnForArgv(["node", "ironclaw", "--help"])).toBe(true);
expect(shouldSkipRespawnForArgv(["node", "ironclaw", "-V"])).toBe(true);
expect(shouldSkipRespawnForArgv(["node", "denchclaw", "--help"])).toBe(true);
expect(shouldSkipRespawnForArgv(["node", "denchclaw", "-V"])).toBe(true);
});
it("does not skip respawn for normal command execution", () => {
expect(shouldSkipRespawnForArgv(["node", "ironclaw", "chat", "send"])).toBe(false);
expect(shouldSkipRespawnForArgv(["node", "denchclaw", "chat", "send"])).toBe(false);
});
});

View File

@ -7,32 +7,32 @@ import {
} from "./run-main.js";
describe("run-main bootstrap cutover", () => {
it("rewrites bare ironclaw invocations to bootstrap by default", () => {
const argv = ["node", "ironclaw"];
expect(rewriteBareArgvToBootstrap(argv, {})).toEqual(["node", "ironclaw", "bootstrap"]);
it("rewrites bare denchclaw invocations to bootstrap by default", () => {
const argv = ["node", "denchclaw"];
expect(rewriteBareArgvToBootstrap(argv, {})).toEqual(["node", "denchclaw", "bootstrap"]);
});
it("does not rewrite when a command already exists", () => {
const argv = ["node", "ironclaw", "chat"];
const argv = ["node", "denchclaw", "chat"];
expect(rewriteBareArgvToBootstrap(argv, {})).toEqual(argv);
});
it("does not rewrite non-ironclaw CLIs", () => {
it("does not rewrite non-denchclaw CLIs", () => {
const argv = ["node", "openclaw"];
expect(rewriteBareArgvToBootstrap(argv, {})).toEqual(argv);
});
it("disables cutover in legacy rollout stage", () => {
const env = { IRONCLAW_BOOTSTRAP_ROLLOUT: "legacy" };
const env = { DENCHCLAW_BOOTSTRAP_ROLLOUT: "legacy" };
expect(shouldEnableBootstrapCutover(env)).toBe(false);
expect(rewriteBareArgvToBootstrap(["node", "ironclaw"], env)).toEqual(["node", "ironclaw"]);
expect(rewriteBareArgvToBootstrap(["node", "denchclaw"], env)).toEqual(["node", "denchclaw"]);
});
it("requires opt-in for beta rollout stage", () => {
const envNoOptIn = { IRONCLAW_BOOTSTRAP_ROLLOUT: "beta" };
const envNoOptIn = { DENCHCLAW_BOOTSTRAP_ROLLOUT: "beta" };
const envOptIn = {
IRONCLAW_BOOTSTRAP_ROLLOUT: "beta",
IRONCLAW_BOOTSTRAP_BETA_OPT_IN: "1",
DENCHCLAW_BOOTSTRAP_ROLLOUT: "beta",
DENCHCLAW_BOOTSTRAP_BETA_OPT_IN: "1",
};
expect(shouldEnableBootstrapCutover(envNoOptIn)).toBe(false);
@ -40,37 +40,37 @@ describe("run-main bootstrap cutover", () => {
});
it("honors explicit legacy fallback override", () => {
const env = { IRONCLAW_BOOTSTRAP_LEGACY_FALLBACK: "1" };
const env = { DENCHCLAW_BOOTSTRAP_LEGACY_FALLBACK: "1" };
expect(shouldEnableBootstrapCutover(env)).toBe(false);
expect(rewriteBareArgvToBootstrap(["node", "ironclaw"], env)).toEqual(["node", "ironclaw"]);
expect(rewriteBareArgvToBootstrap(["node", "denchclaw"], env)).toEqual(["node", "denchclaw"]);
});
});
describe("run-main delegation and path guards", () => {
it("skips CLI path bootstrap for read-only status/help commands", () => {
expect(shouldEnsureCliPath(["node", "ironclaw", "--help"])).toBe(false);
expect(shouldEnsureCliPath(["node", "ironclaw", "status"])).toBe(false);
expect(shouldEnsureCliPath(["node", "ironclaw", "health"])).toBe(false);
expect(shouldEnsureCliPath(["node", "ironclaw", "sessions"])).toBe(false);
expect(shouldEnsureCliPath(["node", "ironclaw", "config", "get"])).toBe(false);
expect(shouldEnsureCliPath(["node", "ironclaw", "models", "list"])).toBe(false);
expect(shouldEnsureCliPath(["node", "ironclaw", "chat", "send"])).toBe(true);
expect(shouldEnsureCliPath(["node", "denchclaw", "--help"])).toBe(false);
expect(shouldEnsureCliPath(["node", "denchclaw", "status"])).toBe(false);
expect(shouldEnsureCliPath(["node", "denchclaw", "health"])).toBe(false);
expect(shouldEnsureCliPath(["node", "denchclaw", "sessions"])).toBe(false);
expect(shouldEnsureCliPath(["node", "denchclaw", "config", "get"])).toBe(false);
expect(shouldEnsureCliPath(["node", "denchclaw", "models", "list"])).toBe(false);
expect(shouldEnsureCliPath(["node", "denchclaw", "chat", "send"])).toBe(true);
});
it("delegates non-bootstrap commands by default and never delegates bootstrap", () => {
expect(shouldDelegateToGlobalOpenClaw(["node", "ironclaw", "chat"])).toBe(true);
expect(shouldDelegateToGlobalOpenClaw(["node", "ironclaw", "bootstrap"])).toBe(false);
expect(shouldDelegateToGlobalOpenClaw(["node", "ironclaw"])).toBe(false);
expect(shouldDelegateToGlobalOpenClaw(["node", "denchclaw", "chat"])).toBe(true);
expect(shouldDelegateToGlobalOpenClaw(["node", "denchclaw", "bootstrap"])).toBe(false);
expect(shouldDelegateToGlobalOpenClaw(["node", "denchclaw"])).toBe(false);
});
it("disables delegation when explicit env disable flag is set", () => {
expect(
shouldDelegateToGlobalOpenClaw(["node", "ironclaw", "chat"], {
IRONCLAW_DISABLE_OPENCLAW_DELEGATION: "1",
shouldDelegateToGlobalOpenClaw(["node", "denchclaw", "chat"], {
DENCHCLAW_DISABLE_OPENCLAW_DELEGATION: "1",
}),
).toBe(false);
expect(
shouldDelegateToGlobalOpenClaw(["node", "ironclaw", "chat"], {
shouldDelegateToGlobalOpenClaw(["node", "denchclaw", "chat"], {
OPENCLAW_DISABLE_OPENCLAW_DELEGATION: "true",
}),
).toBe(false);

View File

@ -66,13 +66,13 @@ function normalizeBootstrapRolloutStage(
export function resolveBootstrapRolloutStage(
env: NodeJS.ProcessEnv = process.env,
): BootstrapRolloutStage {
const raw = env.IRONCLAW_BOOTSTRAP_ROLLOUT ?? env.OPENCLAW_BOOTSTRAP_ROLLOUT;
const raw = env.DENCHCLAW_BOOTSTRAP_ROLLOUT ?? env.OPENCLAW_BOOTSTRAP_ROLLOUT;
return normalizeBootstrapRolloutStage(raw) ?? "default";
}
export function shouldEnableBootstrapCutover(env: NodeJS.ProcessEnv = process.env): boolean {
if (
isTruthyEnvValue(env.IRONCLAW_BOOTSTRAP_LEGACY_FALLBACK) ||
isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_LEGACY_FALLBACK) ||
isTruthyEnvValue(env.OPENCLAW_BOOTSTRAP_LEGACY_FALLBACK)
) {
return false;
@ -83,7 +83,7 @@ export function shouldEnableBootstrapCutover(env: NodeJS.ProcessEnv = process.en
}
if (stage === "beta") {
return (
isTruthyEnvValue(env.IRONCLAW_BOOTSTRAP_BETA_OPT_IN) ||
isTruthyEnvValue(env.DENCHCLAW_BOOTSTRAP_BETA_OPT_IN) ||
isTruthyEnvValue(env.OPENCLAW_BOOTSTRAP_BETA_OPT_IN)
);
}
@ -100,7 +100,7 @@ export function rewriteBareArgvToBootstrap(
if (getPrimaryCommand(argv)) {
return argv;
}
if (resolveCliName(argv) !== "ironclaw") {
if (resolveCliName(argv) !== "denchclaw") {
return argv;
}
if (!shouldEnableBootstrapCutover(env)) {
@ -111,7 +111,7 @@ export function rewriteBareArgvToBootstrap(
function isDelegationDisabled(env: NodeJS.ProcessEnv = process.env): boolean {
return (
isTruthyEnvValue(env.IRONCLAW_DISABLE_OPENCLAW_DELEGATION) ||
isTruthyEnvValue(env.DENCHCLAW_DISABLE_OPENCLAW_DELEGATION) ||
isTruthyEnvValue(env.OPENCLAW_DISABLE_OPENCLAW_DELEGATION)
);
}
@ -132,7 +132,7 @@ export function shouldDelegateToGlobalOpenClaw(
async function delegateToGlobalOpenClaw(argv: string[]): Promise<number> {
if (
isTruthyEnvValue(process.env.IRONCLAW_DELEGATED) ||
isTruthyEnvValue(process.env.DENCHCLAW_DELEGATED) ||
isTruthyEnvValue(process.env.OPENCLAW_DELEGATED)
) {
throw new Error(
@ -145,7 +145,7 @@ async function delegateToGlobalOpenClaw(argv: string[]): Promise<number> {
stdio: "inherit",
env: {
...process.env,
IRONCLAW_DELEGATED: "1",
DENCHCLAW_DELEGATED: "1",
OPENCLAW_DELEGATED: "1",
},
});
@ -186,12 +186,12 @@ export async function runCli(argv: string[] = process.argv) {
// Enforce the minimum supported runtime before doing any work.
assertSupportedRuntime();
// Show the animated Ironclaw banner early so it appears for ALL invocations
// (bare `ironclaw`, subcommands, help, etc.). The bannerEmitted flag inside
// Show the animated DenchClaw banner early so it appears for ALL invocations
// (bare `denchclaw`, subcommands, help, etc.). The bannerEmitted flag inside
// emitCliBanner prevents double-emission from the route / preAction hooks.
const commandPath = getCommandPath(normalizedArgv, 2);
const hideBanner =
isTruthyEnvValue(process.env.IRONCLAW_HIDE_BANNER) ||
isTruthyEnvValue(process.env.DENCHCLAW_HIDE_BANNER) ||
isTruthyEnvValue(process.env.OPENCLAW_HIDE_BANNER) ||
commandPath[0] === "update" ||
commandPath[0] === "completion" ||

View File

@ -245,8 +245,8 @@ export function activeTaglines(options: TaglineOptions = {}): string[] {
export function pickTagline(options: TaglineOptions = {}): string {
const env = options.env ?? process.env;
// Check Ironclaw env first, fall back to legacy OpenClaw env
const override = env?.IRONCLAW_TAGLINE_INDEX ?? env?.OPENCLAW_TAGLINE_INDEX;
// Check DenchClaw env first, fall back to legacy OpenClaw env
const override = env?.DENCHCLAW_TAGLINE_INDEX ?? env?.OPENCLAW_TAGLINE_INDEX;
if (override !== undefined) {
const parsed = Number.parseInt(override, 10);
if (!Number.isNaN(parsed) && parsed >= 0) {

View File

@ -3,7 +3,7 @@ import { normalizeWindowsArgv } from "./windows-argv.js";
describe("normalizeWindowsArgv", () => {
it("returns argv unchanged on non-windows platforms", () => {
const argv = ["node", "ironclaw", "status"];
const argv = ["node", "denchclaw", "status"];
expect(
normalizeWindowsArgv(argv, {
platform: "darwin",

Some files were not shown because too many files have changed in this diff Show More