diff --git a/.env.example b/.env.example index 1f0005b..c5aef1a 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,11 @@ DEBUG=true # Optional: required only for public/remote deployments # STUDIO_ACCESS_TOKEN= +# Advanced only: some gateway-host operations can still use SSH, but marketplace skill install does not require it. +# OPENCLAW_GATEWAY_SSH_TARGET= +# OPENCLAW_GATEWAY_SSH_USER= +# OPENCLAW_GATEWAY_SSH_PORT= +# OPENCLAW_GATEWAY_SSH_STRICT_HOST_KEY_CHECKING=accept-new # Optional: voice features # ELEVENLABS_API_KEY= diff --git a/CREATING_SKILLS.md b/CREATING_SKILLS.md new file mode 100644 index 0000000..7af6fc3 --- /dev/null +++ b/CREATING_SKILLS.md @@ -0,0 +1,244 @@ +# Creating Skills + +This repository ships developer-created marketplace skills as packaged assets that Claw3D can install into an OpenClaw workspace through the gateway. + +Use the existing `todo-board` skill as the reference implementation. + +## Mental model + +- `assets/skills//` is the human-friendly source layout for each packaged skill. +- `src/lib/skills/packaged.ts` contains the client-safe embedded copy of those files used by the marketplace install flow. +- `src/lib/skills/catalog.ts` registers the skill so it appears in the marketplace. +- `src/lib/skills/install-gateway.ts` installs the packaged files into the selected workspace by creating a temporary gateway agent and asking it to write the files. + +## Folder structure + +Follow this structure for every packaged skill: + +```text +assets/ + skills/ + / + SKILL.md + +``` + +Current example: + +```text +assets/ + skills/ + todo-board/ + SKILL.md + todo-list.example.json +``` + +## 1. Create the skill files + +Create a new folder under `assets/skills//`. + +Required file: + +- `SKILL.md` + +Optional files: + +- Example JSON files. +- Templates. +- Any additional files the installed skill should include. + +### `SKILL.md` requirements + +Your `SKILL.md` should include frontmatter, a trigger section, and clear operating instructions for the agent. + +Use this pattern: + +```md +--- +name: my-skill +description: Short description of what the skill does. +metadata: {"openclaw":{"skillKey":"my-skill-key"}} +--- + +# My Skill + +Explain when the skill should be used, where it stores state, how it reads and writes files, and the exact workflow rules the agent must follow. +``` + +Notes: + +- `name` is the user-facing skill name. +- `metadata.openclaw.skillKey` must stay stable and should match the installed folder name. +- Every skill must define a `## Trigger` section that explains what activates the skill and what the agent should physically do in Claw3D when it activates. +- Write instructions as if the model will follow them directly. +- If the skill stores state, define the exact file path and schema. +- Be explicit about read-before-write, validation, ambiguity handling, and response behavior. + +### Trigger requirements + +Every skill must contain a trigger. + +At minimum, the trigger section should define: + +- What kind of user request or external event activates the skill. +- What physical behavior the agent should perform in the office when the skill starts. +- Whether that movement should be skipped when the agent is already at the right location. + +Example: + +````md +## Trigger + +```json +{ + "activation": { + "anyPhrases": [ + "todo", + "todo list", + "blocked task" + ] + }, + "movement": { + "target": "desk", + "skipIfAlreadyThere": true + } +} +``` + +When this skill is activated, the agent should go to its assigned desk before handling the request. + +- Treat requests from Telegram or other external channels as valid triggers when they match this skill. +- If the agent is already at the desk, continue immediately. +```` + +Current runtime support for `movement.target` values: + +- Defined in one source of truth: `src/lib/office/places.ts`. +- Current values: +- `desk` +- `github` +- `gym` +- `qa_lab` + +Important: + +- The JSON block is what Claw3D parses at runtime. +- Keep the prose explanation too, but do not rely on prose alone for the trigger behavior. +- `activation.anyPhrases` should contain short, stable phrases that are likely to appear in the user request. +- If a skill has no trigger block, Claw3D can fall back to the central default trigger registry in `src/lib/office/places.ts`. + +## 2. Mirror the files into `src/lib/skills/packaged.ts` + +Claw3D installs packaged skills from client-safe embedded strings, not by reading `assets/skills/...` directly at runtime. + +That means every new packaged skill must also be added to `src/lib/skills/packaged.ts`. + +For each file in `assets/skills//`, add a matching entry in the packaged file map: + +```ts +const PACKAGED_SKILL_FILES: Record = { + "my-package-id": [ + { + relativePath: "SKILL.md", + content: MY_SKILL_MD, + }, + { + relativePath: "example.json", + content: MY_EXAMPLE_JSON, + }, + ], +}; +``` + +Important: + +- Keep the embedded strings exactly synchronized with the asset files. +- Do not change spacing, frontmatter, or filenames between the asset copy and packaged copy. +- `tests/unit/packagedSkills.test.ts` exists to catch drift for the current example. Extend it when you add more packaged skills. + +## 3. Register the skill in `src/lib/skills/catalog.ts` + +Add a `PackagedSkillDefinition` entry: + +```ts +{ + packageId: "my-package-id", + skillKey: "my-skill-key", + name: "my-skill", + description: "Short description.", + installSource: "openclaw-workspace", + creatorName: "your-handle", + creatorUrl: "https://x.com/your-handle/", +} +``` + +Field meanings: + +- `packageId`: internal packaged asset ID, usually the folder name under `assets/skills/`. +- `skillKey`: the OpenClaw skill key and installed folder name. +- `name`: the human-facing skill name shown in the UI. +- `installSource`: where the skill is installed. For current packaged skills this should be `"openclaw-workspace"`. +- `creatorName` and `creatorUrl`: shown as `Powered by ...` in the marketplace. + +## 4. Add marketplace presentation metadata + +If you want custom category, tagline, badges, or capability labels, add an override in `src/lib/skills/marketplace.ts`. + +Example fields: + +- `category` +- `tagline` +- `capabilities` +- `editorBadge` +- `hideStats` + +For packaged skills, creator attribution normally comes from `src/lib/skills/catalog.ts`. + +For developer-created packaged skills, prefer real attribution over fake popularity numbers. + +## 5. Understand where the files get installed + +The current packaged install flow writes files into the selected workspace here: + +```text +/skills// +``` + +For the TODO example, that becomes: + +```text +/skills/todo-board/ + SKILL.md + todo-list.example.json +``` + +The skill itself can then manage additional workspace files such as: + +```text +/todo-skill/todo-list.json +``` + +That state file is runtime data created by the skill instructions. It is separate from the installed skill package. + +## 6. Keep the example production-ready + +Use the `todo-board` example as the quality bar: + +- The skill must define a clear trigger and physical office behavior. +- The instructions should be explicit and deterministic. +- State storage should be file-backed and documented. +- Ambiguous requests should force clarification instead of guessing. +- The installed package should contain only the files needed by the skill. +- Marketplace metadata should be honest and attributed. + +## 7. Verify your changes + +After creating or editing a packaged skill, run: + +```bash +npm test -- tests/unit/packagedSkills.test.ts tests/unit/skillsInstallGateway.test.ts +npm run lint +npm run typecheck +``` + +If you add new packaged skills, update or extend the packaged skill tests so asset files, embedded copies, and marketplace metadata stay aligned. diff --git a/README.md b/README.md index bd2b441..335f305 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ Common environment variables: - `STUDIO_ACCESS_TOKEN` protects Studio when binding to a public host. - `NEXT_PUBLIC_GATEWAY_URL` provides the default upstream gateway URL when Studio settings are empty. - `OPENCLAW_STATE_DIR` and `OPENCLAW_CONFIG_PATH` override the default OpenClaw paths. -- `OPENCLAW_GATEWAY_SSH_TARGET` and `OPENCLAW_GATEWAY_SSH_USER` support gateway-host operations over SSH. +- `OPENCLAW_GATEWAY_SSH_TARGET`, `OPENCLAW_GATEWAY_SSH_USER`, `OPENCLAW_GATEWAY_SSH_PORT`, and `OPENCLAW_GATEWAY_SSH_STRICT_HOST_KEY_CHECKING` support advanced gateway-host operations over SSH when needed. - `ELEVENLABS_API_KEY`, `ELEVENLABS_VOICE_ID`, and `ELEVENLABS_MODEL_ID` enable voice reply integration. See [`.env.example`](.env.example) for the full local development template. @@ -202,6 +202,16 @@ If the UI loads but Connect fails, the problem is usually on the Studio -> Gatew - `401 Studio access token required` usually means `STUDIO_ACCESS_TOKEN` is enabled and the request is missing the expected `studio_access` cookie. - Helpful proxy error codes include `studio.gateway_url_missing`, `studio.gateway_token_missing`, `studio.upstream_error`, and `studio.upstream_closed`. +Marketplace skill installs now use a gateway-native workspace flow and do not require enabling SSH on the user machine. + +If you use other advanced gateway-host operations over SSH: + +- macOS: enable `System Settings` -> `General` -> `Sharing` -> `Remote Login`, and make sure the target user is allowed. +- Windows: enable the `OpenSSH Server` optional feature, start the `sshd` service, and allow it through the firewall. +- Linux: make sure `sshd` is installed, running, and reachable from the Studio machine. + +For first-time SSH connections, Claw3D uses `StrictHostKeyChecking=accept-new` by default so a new host key can be trusted automatically. If you need stricter behavior, set `OPENCLAW_GATEWAY_SSH_STRICT_HOST_KEY_CHECKING=yes`, or set it to `no` only if you explicitly want to skip host key checks. + ## Contributing Keep pull requests focused, run `npm run lint`, `npm run typecheck`, and `npm run test` before opening a PR, and update docs when behavior or architecture changes. diff --git a/assets/skills/todo-board/SKILL.md b/assets/skills/todo-board/SKILL.md new file mode 100644 index 0000000..c80b364 --- /dev/null +++ b/assets/skills/todo-board/SKILL.md @@ -0,0 +1,109 @@ +--- +name: todo +description: Maintain a shared workspace TODO list with blocked tasks. +metadata: {"openclaw":{"skillKey":"todo-board"}} +--- + +# TODO Board + +Use this skill when the user wants to manage a shared task list for the current workspace. + +## Trigger + +```json +{ + "activation": { + "anyPhrases": [ + "todo", + "todo list", + "blocked task", + "blocked tasks", + "add to my todo", + "show my todo" + ] + }, + "movement": { + "target": "desk", + "skipIfAlreadyThere": true + } +} +``` + +When this skill is activated, the agent should return to its assigned desk before handling the request. + +- If the user asks from Telegram or any other external surface to add, block, unblock, remove, or read TODO items, treat that as a trigger for this skill. +- The physical behavior for this skill is: go sit at the assigned desk, then perform the TODO board workflow. +- If the agent is already at the desk, continue without adding extra movement narration. + +## Storage location + +The authoritative task file is `todo-skill/todo-list.json` in the workspace root. + +- Always treat that file as the source of truth. +- Never rely on chat memory alone for the latest task state. +- Create the `todo-skill` directory and `todo-list.json` file if they do not exist. + +## Required workflow + +1. Read `todo-skill/todo-list.json` before answering any task-management request. +2. If the file does not exist, create it with the schema in this document before continuing. +3. After every add, remove, block, or unblock action, write the full updated JSON back to disk. +4. If the file exists but is invalid JSON or does not match the schema, repair it into a valid structure, preserve any recoverable items, and mention that repair in your response. +5. If the user request is ambiguous, ask a clarifying question instead of guessing. + +## Supported actions + +- Add a task. + Create a new item unless an equivalent active item already exists. +- Block a task. + Change the matching item to `status: "blocked"`. If the task does not exist and the request is clear, create it directly as blocked. +- Unblock a task. + Change the matching item back to `status: "todo"` and clear `blockReason`. +- Remove a task. + Delete only the matching item. If multiple items could match, ask for clarification. +- Read the list. + Summarize tasks grouped into `TODO` and `BLOCKED`. + +## File format + +Use this JSON shape: + +```json +{ + "version": 1, + "updatedAt": "2026-03-22T00:00:00.000Z", + "items": [ + { + "id": "task-1", + "title": "Example task", + "status": "todo", + "createdAt": "2026-03-22T00:00:00.000Z", + "updatedAt": "2026-03-22T00:00:00.000Z", + "blockReason": null + } + ] +} +``` + +## Field rules + +- Keep `version` at `1`. +- Generate stable, human-readable IDs such as `prepare-demo` or `task-2`. +- Keep titles concise and preserve the user's intent. +- Use only `todo` or `blocked` for `status`. +- Use ISO timestamps for `createdAt`, item `updatedAt`, and top-level `updatedAt`. +- Keep `blockReason` as `null` unless the user gave a reason or a short precise reason is clearly implied. + +## Mutation rules + +- Avoid duplicate active items that describe the same work. +- Preserve existing IDs and `createdAt` values for unchanged items. +- Update the touched item's `updatedAt` whenever you modify it. +- Update the top-level `updatedAt` on every write. +- Keep untouched items in their original order unless there is a strong reason to reorder them. + +## Response style + +- After each mutation, say what changed. +- When showing the list, group tasks into `TODO` and `BLOCKED`. +- Include each blocked task's reason when one exists. diff --git a/assets/skills/todo-board/todo-list.example.json b/assets/skills/todo-board/todo-list.example.json new file mode 100644 index 0000000..54d646a --- /dev/null +++ b/assets/skills/todo-board/todo-list.example.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "updatedAt": "2026-03-22T00:00:00.000Z", + "items": [ + { + "id": "draft-roadmap", + "title": "Draft the TODO skill roadmap", + "status": "todo", + "createdAt": "2026-03-22T00:00:00.000Z", + "updatedAt": "2026-03-22T00:00:00.000Z", + "blockReason": null + }, + { + "id": "gateway-access", + "title": "Confirm gateway install access", + "status": "blocked", + "createdAt": "2026-03-22T00:00:00.000Z", + "updatedAt": "2026-03-22T00:00:00.000Z", + "blockReason": "Waiting for gateway credentials" + } + ] +} diff --git a/src/features/agents/components/AgentSkillsSetupModal.tsx b/src/features/agents/components/AgentSkillsSetupModal.tsx index a250aa5..f35fa1b 100644 --- a/src/features/agents/components/AgentSkillsSetupModal.tsx +++ b/src/features/agents/components/AgentSkillsSetupModal.tsx @@ -211,7 +211,7 @@ export const AgentSkillsSetupModal = ({ disabled={anySkillBusy} onClick={() => { const approved = window.confirm( - `Remove ${skill.name} from the gateway? This affects all agents.` + `Remove ${skill.name} from the gateway for all agents?` ); if (!approved) { return; @@ -224,7 +224,7 @@ export const AgentSkillsSetupModal = ({ onClose(); }} > - Remove skill from gateway + Remove for all agents ) : null} diff --git a/src/features/agents/operations/useAgentSettingsMutationController.ts b/src/features/agents/operations/useAgentSettingsMutationController.ts index a7e5e80..5942f3f 100644 --- a/src/features/agents/operations/useAgentSettingsMutationController.ts +++ b/src/features/agents/operations/useAgentSettingsMutationController.ts @@ -932,6 +932,7 @@ export function useAgentSettingsMutationController(params: UseAgentSettingsMutat label: `Remove ${normalizedSkillKey}`, run: async () => { const result = await removeSkillFromGateway({ + client: params.client, skillKey: normalizedSkillKey, source: normalizedSource, baseDir: skill.baseDir, diff --git a/src/features/office/components/HQSidebar.tsx b/src/features/office/components/HQSidebar.tsx index eaf67e6..9479bc4 100644 --- a/src/features/office/components/HQSidebar.tsx +++ b/src/features/office/components/HQSidebar.tsx @@ -6,7 +6,6 @@ export type HQSidebarTab = | "inbox" | "history" | "playbooks" - | "marketplace" | "analytics"; type HQSidebarProps = { @@ -15,10 +14,10 @@ type HQSidebarProps = { inboxCount: number; onToggle: () => void; onTabChange: (tab: HQSidebarTab) => void; + onOpenMarketplace: () => void; inboxPanel: ReactNode; historyPanel: ReactNode; playbooksPanel: ReactNode; - marketplacePanel: ReactNode; analyticsPanel: ReactNode; }; @@ -26,7 +25,6 @@ const TAB_LABELS: Record = { inbox: "Inbox", history: "History", playbooks: "Playbooks", - marketplace: "Marketplace", analytics: "Analytics", }; @@ -38,15 +36,14 @@ export function HQSidebar({ inboxCount, onToggle, onTabChange, + onOpenMarketplace, inboxPanel, historyPanel, playbooksPanel, - marketplacePanel, analyticsPanel, }: HQSidebarProps) { const analyticsOnly = activeTab === "analytics"; - const marketplaceOnly = activeTab === "marketplace"; - const railOnly = analyticsOnly || marketplaceOnly; + const railOnly = analyticsOnly; const activePanel = activeTab === "inbox" ? inboxPanel @@ -54,9 +51,7 @@ export function HQSidebar({ ? historyPanel : activeTab === "playbooks" ? playbooksPanel - : activeTab === "marketplace" - ? marketplacePanel - : analyticsPanel; + : analyticsPanel; return (