hoomanjs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/screenshot.png +0 -0
- package/.github/workflows/build-publish.yml +49 -0
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/docker-compose.yml +13 -0
- package/package.json +78 -0
- package/src/acp/acp-agent.ts +803 -0
- package/src/acp/approvals.ts +147 -0
- package/src/acp/index.ts +1 -0
- package/src/acp/meta/system-prompt.ts +44 -0
- package/src/acp/meta/user-id.ts +44 -0
- package/src/acp/prompt-invoke.ts +149 -0
- package/src/acp/sessions/config-options.ts +56 -0
- package/src/acp/sessions/replay.ts +131 -0
- package/src/acp/sessions/store.ts +158 -0
- package/src/acp/sessions/title.ts +22 -0
- package/src/acp/utils/paths.ts +5 -0
- package/src/acp/utils/tool-kind.ts +38 -0
- package/src/acp/utils/tool-locations.ts +46 -0
- package/src/acp/utils/tool-result-content.ts +27 -0
- package/src/chat/app.tsx +428 -0
- package/src/chat/approvals.ts +96 -0
- package/src/chat/components/ApprovalPrompt.tsx +25 -0
- package/src/chat/components/ChatMessage.tsx +47 -0
- package/src/chat/components/Composer.tsx +39 -0
- package/src/chat/components/EmptyChatBanner.tsx +26 -0
- package/src/chat/components/ReasoningStrip.tsx +30 -0
- package/src/chat/components/Spinner.tsx +34 -0
- package/src/chat/components/StatusBar.tsx +65 -0
- package/src/chat/components/ThinkingStatus.tsx +128 -0
- package/src/chat/components/ToolEvent.tsx +34 -0
- package/src/chat/components/Transcript.tsx +34 -0
- package/src/chat/components/ascii-logo.ts +11 -0
- package/src/chat/components/shared.ts +70 -0
- package/src/chat/index.tsx +42 -0
- package/src/chat/types.ts +21 -0
- package/src/cli.ts +146 -0
- package/src/configure/app.tsx +911 -0
- package/src/configure/components/BusyScreen.tsx +22 -0
- package/src/configure/components/HomeScreen.tsx +43 -0
- package/src/configure/components/MenuScreen.tsx +44 -0
- package/src/configure/components/PromptForm.tsx +40 -0
- package/src/configure/components/SelectMenuItem.tsx +30 -0
- package/src/configure/index.tsx +43 -0
- package/src/configure/open-in-editor.ts +133 -0
- package/src/configure/types.ts +45 -0
- package/src/configure/utils.ts +113 -0
- package/src/core/agent/index.ts +76 -0
- package/src/core/config.ts +157 -0
- package/src/core/index.ts +54 -0
- package/src/core/mcp/config.ts +80 -0
- package/src/core/mcp/index.ts +13 -0
- package/src/core/mcp/manager.ts +109 -0
- package/src/core/mcp/prefixed-mcp-tool.ts +45 -0
- package/src/core/mcp/tools.ts +92 -0
- package/src/core/mcp/types.ts +37 -0
- package/src/core/memory/index.ts +17 -0
- package/src/core/memory/ltm/embed.ts +67 -0
- package/src/core/memory/ltm/index.ts +18 -0
- package/src/core/memory/ltm/store.ts +376 -0
- package/src/core/memory/ltm/tools.ts +146 -0
- package/src/core/memory/ltm/types.ts +111 -0
- package/src/core/memory/ltm/utils.ts +218 -0
- package/src/core/memory/stm/index.ts +17 -0
- package/src/core/models/anthropic.ts +53 -0
- package/src/core/models/bedrock.ts +54 -0
- package/src/core/models/google.ts +51 -0
- package/src/core/models/index.ts +16 -0
- package/src/core/models/ollama/index.ts +13 -0
- package/src/core/models/ollama/strands-ollama.ts +439 -0
- package/src/core/models/openai.ts +12 -0
- package/src/core/prompts/index.ts +23 -0
- package/src/core/prompts/skills.ts +66 -0
- package/src/core/prompts/static/fetch.md +33 -0
- package/src/core/prompts/static/filesystem.md +38 -0
- package/src/core/prompts/static/identity.md +22 -0
- package/src/core/prompts/static/ltm.md +39 -0
- package/src/core/prompts/static/memory.md +39 -0
- package/src/core/prompts/static/shell.md +34 -0
- package/src/core/prompts/static/skills.md +19 -0
- package/src/core/prompts/static/thinking.md +27 -0
- package/src/core/prompts/system.ts +109 -0
- package/src/core/skills/index.ts +2 -0
- package/src/core/skills/registry.ts +239 -0
- package/src/core/skills/tools.ts +80 -0
- package/src/core/toolkit.ts +13 -0
- package/src/core/tools/fetch.ts +288 -0
- package/src/core/tools/filesystem.ts +747 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/shell.ts +426 -0
- package/src/core/tools/thinking.ts +184 -0
- package/src/core/tools/time.ts +121 -0
- package/src/core/utils/cwd-context.ts +11 -0
- package/src/core/utils/paths.ts +28 -0
- package/src/exec/approvals.ts +85 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## Memory
|
|
2
|
+
|
|
3
|
+
You have access to long-term memory tools.
|
|
4
|
+
|
|
5
|
+
### Retrieval (search_memory)
|
|
6
|
+
|
|
7
|
+
- Use memory when the current request may depend on past interactions, preferences, or ongoing tasks
|
|
8
|
+
- Especially use it for:
|
|
9
|
+
- follow-ups ("last time", "previously", "continue")
|
|
10
|
+
- user-specific preferences or history
|
|
11
|
+
- long-running tasks or projects
|
|
12
|
+
- Do NOT search memory for simple, self-contained questions
|
|
13
|
+
|
|
14
|
+
### Storage (store_memory)
|
|
15
|
+
|
|
16
|
+
- Only store information that is:
|
|
17
|
+
- reusable across conversations
|
|
18
|
+
- specific to the user (preferences, facts, goals, tasks)
|
|
19
|
+
- Good examples:
|
|
20
|
+
- "User prefers TypeScript"
|
|
21
|
+
- "User is building a CV SaaS"
|
|
22
|
+
- Do NOT store:
|
|
23
|
+
- one-off questions
|
|
24
|
+
- temporary context
|
|
25
|
+
- obvious or generic information
|
|
26
|
+
|
|
27
|
+
### Updates (update_memory)
|
|
28
|
+
|
|
29
|
+
- If new information corrects or refines an existing memory, update it instead of creating a new one
|
|
30
|
+
|
|
31
|
+
### Archival (archive_memory)
|
|
32
|
+
|
|
33
|
+
- If a memory becomes irrelevant, outdated, or incorrect, archive it instead of deleting
|
|
34
|
+
|
|
35
|
+
### General Rules
|
|
36
|
+
|
|
37
|
+
- Avoid redundant or duplicate memory
|
|
38
|
+
- Keep memory concise and compressed
|
|
39
|
+
- Prioritize current context over memory if they conflict
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## Shell
|
|
2
|
+
|
|
3
|
+
You have access to a `shell` tool for local command execution.
|
|
4
|
+
|
|
5
|
+
### When To Use It
|
|
6
|
+
|
|
7
|
+
- Use `shell` when executing a local command is the most direct or reliable way to inspect, verify, or operate on the environment
|
|
8
|
+
- Especially use it for:
|
|
9
|
+
- running project scripts, builds, tests, and CLIs
|
|
10
|
+
- checking system or repository state
|
|
11
|
+
- executing multiple related shell commands in sequence
|
|
12
|
+
- gathering output that is easier to obtain from the command line than from reasoning alone
|
|
13
|
+
- Do NOT use `shell` when the answer can be given directly without execution
|
|
14
|
+
- Do NOT use `shell` for destructive or risky commands unless they are clearly necessary and appropriate
|
|
15
|
+
|
|
16
|
+
### How To Use It
|
|
17
|
+
|
|
18
|
+
- Prefer the smallest command that answers the question
|
|
19
|
+
- Use `work_dir` when the command should run in a specific directory
|
|
20
|
+
- Use sequential commands for dependent steps
|
|
21
|
+
- Use `parallel` only for independent commands
|
|
22
|
+
- Set sensible timeouts for commands that may hang or run for a long time
|
|
23
|
+
- Use `ignore_errors` only when partial success is acceptable
|
|
24
|
+
|
|
25
|
+
### Safety
|
|
26
|
+
|
|
27
|
+
- Avoid commands that delete, overwrite, or broadly modify files unless required
|
|
28
|
+
- Prefer inspection and verification before making changes
|
|
29
|
+
- Be careful with package managers, process control, and commands that affect the wider system
|
|
30
|
+
|
|
31
|
+
### Goal
|
|
32
|
+
|
|
33
|
+
- Use the shell to improve accuracy and efficiency
|
|
34
|
+
- Keep command usage targeted, minimal, and relevant to the task
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## Skills
|
|
2
|
+
|
|
3
|
+
You may have **skills management tools** (install, list, search, delete) and a dynamic **Available skills** section elsewhere in the system prompt listing installed skills (title from the skills CLI) with absolute paths to each `SKILL.md`.
|
|
4
|
+
|
|
5
|
+
### When to use a skill during this turn
|
|
6
|
+
|
|
7
|
+
- When the user's goal, stack, or workflow clearly matches a listed skill (same product, API, or task family), treat that skill as the preferred playbook before improvising.
|
|
8
|
+
- When you are unsure but a skill's title plausibly fits the task, open its `SKILL.md` using the **absolute path** from the Available skills list and skim it; if it helps, follow it for the rest of the turn.
|
|
9
|
+
- Prefer **reading** `SKILL.md` over guessing conventions (naming, CLI flags, safety steps) that the skill is meant to encode.
|
|
10
|
+
- Do **not** load or follow skills that are unrelated to the current request, and do not treat the catalog listing as mandatory background reading for every reply.
|
|
11
|
+
|
|
12
|
+
### Coordination with tools
|
|
13
|
+
|
|
14
|
+
- Use **filesystem** tools to read `SKILL.md` at the given path when you need full instructions.
|
|
15
|
+
- Use **skills management** tools when the user wants to discover, install, or remove skills from the public catalog or local sources—not for ordinary coding that does not involve skills.
|
|
16
|
+
|
|
17
|
+
### Goal
|
|
18
|
+
|
|
19
|
+
Apply skills **selectively**: improve quality and consistency when a skill applies, and avoid extra I/O or scope when none do.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
## Thinking
|
|
2
|
+
|
|
3
|
+
You have access to a `think` tool for structured multi-step reasoning.
|
|
4
|
+
|
|
5
|
+
### When To Use It
|
|
6
|
+
|
|
7
|
+
- Use `think` when the task is complex, ambiguous, or likely benefits from deliberate multi-step planning
|
|
8
|
+
- Especially use it for:
|
|
9
|
+
- designing or comparing implementation approaches
|
|
10
|
+
- debugging non-obvious failures
|
|
11
|
+
- breaking down large tasks into clear steps
|
|
12
|
+
- revising an earlier conclusion after new evidence appears
|
|
13
|
+
- exploring alternative branches before committing to a solution
|
|
14
|
+
- Do NOT use `think` for simple, direct, or single-step requests
|
|
15
|
+
|
|
16
|
+
### How To Use It
|
|
17
|
+
|
|
18
|
+
- Start with a reasonable estimate for `totalThoughts`
|
|
19
|
+
- Set `nextThoughtNeeded` to `true` while analysis is still in progress
|
|
20
|
+
- Use revision fields when reconsidering earlier reasoning
|
|
21
|
+
- Use branch fields when exploring alternative approaches
|
|
22
|
+
- Only set `nextThoughtNeeded` to `false` when you have reached a satisfactory conclusion
|
|
23
|
+
|
|
24
|
+
### Goal
|
|
25
|
+
|
|
26
|
+
- Use the tool to improve reasoning quality, not to create unnecessary overhead
|
|
27
|
+
- Prefer concise, useful thought steps over verbose internal narration
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { compile } from "handlebars";
|
|
5
|
+
import type { Config } from "../config.ts";
|
|
6
|
+
import type { Toolkit } from "../toolkit.ts";
|
|
7
|
+
import { toolkitAtLeast } from "../toolkit.ts";
|
|
8
|
+
|
|
9
|
+
/** Bundled markdown next to this module (`prompts/static/`). */
|
|
10
|
+
const STATIC_PROMPT_FILES = [
|
|
11
|
+
"identity.md",
|
|
12
|
+
"ltm.md",
|
|
13
|
+
"thinking.md",
|
|
14
|
+
"filesystem.md",
|
|
15
|
+
"fetch.md",
|
|
16
|
+
"shell.md",
|
|
17
|
+
"skills.md",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const SECTION_BREAK = "\n\n---\n\n";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Loads `prompts/static/*.md` from the package, then `instructions.md` from disk,
|
|
24
|
+
* concatenates them, and renders with Handlebars using `context`.
|
|
25
|
+
*/
|
|
26
|
+
export class System {
|
|
27
|
+
private readonly path: string;
|
|
28
|
+
private readonly config: Config;
|
|
29
|
+
private readonly toolkit: Toolkit;
|
|
30
|
+
private data = "";
|
|
31
|
+
|
|
32
|
+
public constructor(path: string, config: Config, toolkit: Toolkit) {
|
|
33
|
+
this.path = path;
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.toolkit = toolkit;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private staticPromptFiles(): readonly (typeof STATIC_PROMPT_FILES)[number][] {
|
|
39
|
+
return STATIC_PROMPT_FILES.filter((file) => {
|
|
40
|
+
switch (file) {
|
|
41
|
+
case "ltm.md":
|
|
42
|
+
return this.config.ltm.enabled;
|
|
43
|
+
case "filesystem.md":
|
|
44
|
+
case "thinking.md":
|
|
45
|
+
case "shell.md":
|
|
46
|
+
return toolkitAtLeast(this.toolkit, "full");
|
|
47
|
+
case "skills.md":
|
|
48
|
+
return this.toolkit === "max";
|
|
49
|
+
default:
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private readBundledStaticPrompts(): string {
|
|
56
|
+
const dir = join(dirname(fileURLToPath(import.meta.url)), "static");
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
for (const file of this.staticPromptFiles()) {
|
|
59
|
+
const full = join(dir, file);
|
|
60
|
+
if (!existsSync(full)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const text = readFileSync(full, "utf8").trim();
|
|
64
|
+
if (text.length > 0) {
|
|
65
|
+
parts.push(text);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return parts.join("\n\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private readRawText(): string {
|
|
72
|
+
const instructions = existsSync(this.path)
|
|
73
|
+
? readFileSync(this.path, "utf8").trim()
|
|
74
|
+
: "";
|
|
75
|
+
const bundled = this.readBundledStaticPrompts();
|
|
76
|
+
|
|
77
|
+
const blocks: string[] = [];
|
|
78
|
+
if (bundled.length > 0) {
|
|
79
|
+
blocks.push(bundled);
|
|
80
|
+
}
|
|
81
|
+
if (instructions.length > 0) {
|
|
82
|
+
blocks.push(instructions);
|
|
83
|
+
}
|
|
84
|
+
if (blocks.length === 0) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
return blocks.join(SECTION_BREAK);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get content(): string {
|
|
91
|
+
return this.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Plain object for Handlebars (`{{name}}`, `{{llm.model}}`, …). */
|
|
95
|
+
private context(): Record<string, unknown> {
|
|
96
|
+
return {
|
|
97
|
+
name: this.config.name,
|
|
98
|
+
llm: this.config.llm,
|
|
99
|
+
ltm: this.config.ltm,
|
|
100
|
+
compaction: this.config.compaction,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public async reload(): Promise<void> {
|
|
105
|
+
const raw = this.readRawText();
|
|
106
|
+
const template = compile(raw);
|
|
107
|
+
this.data = template(this.context()).trim();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
/** Vercel `skills` CLI package; uses latest every time. */
|
|
10
|
+
const SKILLS_CLI = "skills@latest";
|
|
11
|
+
/** Agent target for `skills add/list` — OpenClaw layout → `./skills/`. */
|
|
12
|
+
const SKILLS_AGENT = "openclaw";
|
|
13
|
+
const SKILLS_API_URL = "https://skills.sh";
|
|
14
|
+
const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
15
|
+
|
|
16
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse name and description from a SKILL.md file's YAML frontmatter.
|
|
20
|
+
*/
|
|
21
|
+
function parseSkillFrontmatter(
|
|
22
|
+
content: string,
|
|
23
|
+
dirName: string,
|
|
24
|
+
): { name: string; description?: string } {
|
|
25
|
+
try {
|
|
26
|
+
const { data } = matter(content);
|
|
27
|
+
const name =
|
|
28
|
+
typeof data?.name === "string" && data.name.trim()
|
|
29
|
+
? data.name.trim()
|
|
30
|
+
: dirName;
|
|
31
|
+
const description =
|
|
32
|
+
typeof data?.description === "string" && data.description.trim()
|
|
33
|
+
? data.description.trim()
|
|
34
|
+
: undefined;
|
|
35
|
+
return { name, description };
|
|
36
|
+
} catch {
|
|
37
|
+
return { name: dirName };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripAnsi(s: string): string {
|
|
42
|
+
return s.replace(ANSI_RE, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type SkillsListJsonRow = {
|
|
46
|
+
name: string;
|
|
47
|
+
path: string;
|
|
48
|
+
scope?: string;
|
|
49
|
+
agents?: string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SkillListEntry = {
|
|
53
|
+
/** Display title (skill `name` from SKILL.md) */
|
|
54
|
+
name: string;
|
|
55
|
+
/** Full description read using gray-matter */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Absolute path to the skill package root from `skills list --json`. */
|
|
58
|
+
path: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface SkillSearchResult {
|
|
62
|
+
name: string;
|
|
63
|
+
slug: string;
|
|
64
|
+
source: string;
|
|
65
|
+
installs: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runNpxSkills(
|
|
69
|
+
args: string[],
|
|
70
|
+
options: { cwd?: string; timeout?: number } = {},
|
|
71
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
72
|
+
const { cwd, timeout = 300_000 } = options;
|
|
73
|
+
return execFileAsync(NPX_BIN, ["--yes", SKILLS_CLI, ...args], {
|
|
74
|
+
cwd,
|
|
75
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
76
|
+
timeout,
|
|
77
|
+
env: { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Install / list / delete skills via the Vercel [`skills` CLI](https://github.com/vercel-labs/skills)
|
|
83
|
+
* (`npx skills`), using OpenClaw scope (`./skills/`).
|
|
84
|
+
*/
|
|
85
|
+
export class Registry {
|
|
86
|
+
private readonly cwd: string;
|
|
87
|
+
|
|
88
|
+
constructor(cwd: string) {
|
|
89
|
+
this.cwd = cwd;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async list(): Promise<SkillListEntry[]> {
|
|
93
|
+
let stdout: string;
|
|
94
|
+
try {
|
|
95
|
+
const r = await runNpxSkills(["list", "--json", "-a", SKILLS_AGENT], {
|
|
96
|
+
cwd: this.cwd,
|
|
97
|
+
});
|
|
98
|
+
stdout = r.stdout;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const err = e as { stderr?: string; message?: string };
|
|
101
|
+
const detail = err.stderr ?? err.message ?? String(e);
|
|
102
|
+
throw new Error(
|
|
103
|
+
`skills list failed. Is Node/npm available?\n${stripAnsi(detail)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const text = stdout.trim();
|
|
107
|
+
if (!text) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
let rows: SkillsListJsonRow[];
|
|
111
|
+
try {
|
|
112
|
+
rows = JSON.parse(text) as SkillsListJsonRow[];
|
|
113
|
+
} catch {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Unexpected skills list output (expected JSON):\n${text.slice(0, 500)}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(rows)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const entries: SkillListEntry[] = [];
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
try {
|
|
124
|
+
const folder = basename(row.path);
|
|
125
|
+
const root = isAbsolute(row.path)
|
|
126
|
+
? resolve(row.path)
|
|
127
|
+
: resolve(this.cwd, row.path);
|
|
128
|
+
const md = resolve(join(root, "SKILL.md"));
|
|
129
|
+
const raw = await readFile(md, "utf-8");
|
|
130
|
+
const { name, description } = parseSkillFrontmatter(raw, folder);
|
|
131
|
+
entries.push({
|
|
132
|
+
name,
|
|
133
|
+
description,
|
|
134
|
+
path: md,
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `skills add <source> -y -a openclaw --copy` — supports owner/repo, Git URLs with tree paths, local paths, etc.
|
|
145
|
+
*/
|
|
146
|
+
async install(source: string): Promise<void> {
|
|
147
|
+
const raw = source.trim();
|
|
148
|
+
if (!raw) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Enter a skill source (e.g. owner/repo or a GitHub URL).",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await runNpxSkills(["add", raw, "-y", "-a", SKILLS_AGENT, "--copy"], {
|
|
155
|
+
cwd: this.cwd,
|
|
156
|
+
timeout: 600_000,
|
|
157
|
+
});
|
|
158
|
+
} catch (e) {
|
|
159
|
+
const err = e as { stderr?: string; stdout?: string; message?: string };
|
|
160
|
+
const detail = stripAnsi(
|
|
161
|
+
err.stderr || err.stdout || err.message || String(e),
|
|
162
|
+
);
|
|
163
|
+
throw new Error(`skills add failed:\n${detail}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Remove by on-disk folder name (basename of `path` from `skills list --json`).
|
|
169
|
+
*
|
|
170
|
+
* We do **not** pass `--agent` so removal matches the CLI’s universal layout behavior.
|
|
171
|
+
*/
|
|
172
|
+
async delete(folder: string): Promise<void> {
|
|
173
|
+
const safe = folder.trim();
|
|
174
|
+
if (!safe || /[\\/]/.test(safe) || safe.includes("..")) {
|
|
175
|
+
throw new Error("Invalid skill name.");
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await runNpxSkills(["remove", safe, "-y"], { cwd: this.cwd });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
const err = e as { stderr?: string; stdout?: string; message?: string };
|
|
181
|
+
const detail = stripAnsi(
|
|
182
|
+
err.stderr || err.stdout || err.message || String(e),
|
|
183
|
+
);
|
|
184
|
+
throw new Error(`skills remove failed:\n${detail}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await rm(join(this.cwd, "skills", safe), {
|
|
188
|
+
recursive: true,
|
|
189
|
+
force: true,
|
|
190
|
+
}).catch(() => {});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Search the public catalog at skills.sh (same API as the `skills` CLI `find` command).
|
|
195
|
+
*/
|
|
196
|
+
async search(query: string): Promise<SkillSearchResult[]> {
|
|
197
|
+
const q = query.trim();
|
|
198
|
+
if (!q) {
|
|
199
|
+
throw new Error("Enter a search term for the skills catalog.");
|
|
200
|
+
}
|
|
201
|
+
if (q.length < 2) {
|
|
202
|
+
throw new Error("Use at least 2 characters to search.");
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const url = `${SKILLS_API_URL}/api/search?q=${encodeURIComponent(q)}&limit=10`;
|
|
206
|
+
const res = await fetch(url);
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
const data = (await res.json()) as {
|
|
211
|
+
skills: Array<{
|
|
212
|
+
id: string;
|
|
213
|
+
name: string;
|
|
214
|
+
installs: number;
|
|
215
|
+
source: string;
|
|
216
|
+
}>;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (!Array.isArray(data.skills)) {
|
|
220
|
+
throw new Error("Skills search did not return .skills[] in response");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return data.skills
|
|
224
|
+
.map((skill) => ({
|
|
225
|
+
name: skill.name,
|
|
226
|
+
slug: skill.id,
|
|
227
|
+
source: skill.source || "",
|
|
228
|
+
installs: skill.installs,
|
|
229
|
+
}))
|
|
230
|
+
.sort((a, b) => (b.installs || 0) - (a.installs || 0));
|
|
231
|
+
} catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function create(cwd: string) {
|
|
238
|
+
return new Registry(cwd);
|
|
239
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { tool } from "@strands-agents/sdk";
|
|
2
|
+
import type { JSONValue } from "@strands-agents/sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { Registry } from "./registry.ts";
|
|
5
|
+
|
|
6
|
+
function toJsonValue(value: unknown): JSONValue {
|
|
7
|
+
return JSON.parse(JSON.stringify(value)) as JSONValue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function create(registry: Registry) {
|
|
11
|
+
return [
|
|
12
|
+
tool({
|
|
13
|
+
name: "list_skills",
|
|
14
|
+
description:
|
|
15
|
+
"List currently installed skills available to the local agent.",
|
|
16
|
+
inputSchema: z.object({}),
|
|
17
|
+
callback: async () => {
|
|
18
|
+
const skills = await registry.list();
|
|
19
|
+
return toJsonValue({
|
|
20
|
+
count: skills.length,
|
|
21
|
+
skills,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
tool({
|
|
26
|
+
name: "search_skills",
|
|
27
|
+
description:
|
|
28
|
+
"Search the public skills catalog for skills matching a query.",
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
query: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(2)
|
|
33
|
+
.describe("Search query for the public skills catalog."),
|
|
34
|
+
}),
|
|
35
|
+
callback: async (input) => {
|
|
36
|
+
const results = await registry.search(input.query);
|
|
37
|
+
return toJsonValue({
|
|
38
|
+
count: results.length,
|
|
39
|
+
results,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
tool({
|
|
44
|
+
name: "install_skill",
|
|
45
|
+
description:
|
|
46
|
+
"Install a skill from a source such as owner/repo, a GitHub URL, or a local path.",
|
|
47
|
+
inputSchema: z.object({
|
|
48
|
+
source: z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.describe("Skill source to install (repo, URL, or local path)."),
|
|
52
|
+
}),
|
|
53
|
+
callback: async (input) => {
|
|
54
|
+
await registry.install(input.source);
|
|
55
|
+
return toJsonValue({
|
|
56
|
+
installed: true,
|
|
57
|
+
source: input.source,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
tool({
|
|
62
|
+
name: "delete_skill",
|
|
63
|
+
description:
|
|
64
|
+
"Delete an installed skill by its folder name under the local skills directory.",
|
|
65
|
+
inputSchema: z.object({
|
|
66
|
+
folder: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.describe("Installed skill folder name to remove."),
|
|
70
|
+
}),
|
|
71
|
+
callback: async (input) => {
|
|
72
|
+
await registry.delete(input.folder);
|
|
73
|
+
return toJsonValue({
|
|
74
|
+
deleted: true,
|
|
75
|
+
folder: input.folder,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const TOOLKITS = ["lite", "full", "max"] as const;
|
|
2
|
+
|
|
3
|
+
export type Toolkit = (typeof TOOLKITS)[number];
|
|
4
|
+
|
|
5
|
+
const TOOLKIT_RANK: Record<Toolkit, number> = {
|
|
6
|
+
lite: 0,
|
|
7
|
+
full: 1,
|
|
8
|
+
max: 2,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function toolkitAtLeast(actual: Toolkit, minimum: Toolkit): boolean {
|
|
12
|
+
return TOOLKIT_RANK[actual] >= TOOLKIT_RANK[minimum];
|
|
13
|
+
}
|