freeturtle 0.1.28 → 0.1.30

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.
Files changed (45) hide show
  1. package/dist/bin/freeturtle.js +26 -2
  2. package/dist/bin/freeturtle.js.map +1 -1
  3. package/dist/src/cli/daemon-utils.d.ts +3 -0
  4. package/dist/src/cli/daemon-utils.js +49 -0
  5. package/dist/src/cli/daemon-utils.js.map +1 -0
  6. package/dist/src/cli/restart.d.ts +1 -0
  7. package/dist/src/cli/restart.js +23 -0
  8. package/dist/src/cli/restart.js.map +1 -0
  9. package/dist/src/cli/skills.d.ts +1 -0
  10. package/dist/src/cli/skills.js +28 -0
  11. package/dist/src/cli/skills.js.map +1 -0
  12. package/dist/src/cli/stop.d.ts +1 -0
  13. package/dist/src/cli/stop.js +11 -0
  14. package/dist/src/cli/stop.js.map +1 -0
  15. package/dist/src/cli/update.js +12 -56
  16. package/dist/src/cli/update.js.map +1 -1
  17. package/dist/src/config.d.ts +2 -0
  18. package/dist/src/config.js +9 -0
  19. package/dist/src/config.js.map +1 -1
  20. package/dist/src/daemon.d.ts +1 -0
  21. package/dist/src/daemon.js +37 -6
  22. package/dist/src/daemon.js.map +1 -1
  23. package/dist/src/modules/loader.js +2 -0
  24. package/dist/src/modules/loader.js.map +1 -1
  25. package/dist/src/modules/shell/index.d.ts +10 -0
  26. package/dist/src/modules/shell/index.js +298 -0
  27. package/dist/src/modules/shell/index.js.map +1 -0
  28. package/dist/src/policy.js +2 -0
  29. package/dist/src/policy.js.map +1 -1
  30. package/dist/src/runner.d.ts +3 -0
  31. package/dist/src/runner.js +12 -0
  32. package/dist/src/runner.js.map +1 -1
  33. package/dist/src/skills/index.d.ts +3 -0
  34. package/dist/src/skills/index.js +3 -0
  35. package/dist/src/skills/index.js.map +1 -0
  36. package/dist/src/skills/loader.d.ts +33 -0
  37. package/dist/src/skills/loader.js +211 -0
  38. package/dist/src/skills/loader.js.map +1 -0
  39. package/dist/src/skills/parser.d.ts +13 -0
  40. package/dist/src/skills/parser.js +84 -0
  41. package/dist/src/skills/parser.js.map +1 -0
  42. package/dist/src/skills/types.d.ts +52 -0
  43. package/dist/src/skills/types.js +7 -0
  44. package/dist/src/skills/types.js.map +1 -0
  45. package/package.json +8 -3
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Skill loader — discovers and loads Agent Skills from the filesystem.
3
+ *
4
+ * Follows the same progressive disclosure model as OpenClaw:
5
+ * - Only name/description/location are injected into the system prompt
6
+ * - Full SKILL.md body is loaded on-demand via the read_file tool
7
+ * - XML format for the available_skills block
8
+ *
9
+ * Scan order (highest to lowest precedence):
10
+ * 1. Workspace skills: <workspace>/skills/
11
+ * 2. Managed skills: ~/.freeturtle/skills/ (shared across agents)
12
+ * 3. Extra dirs: Configured via skills.extra_dirs in config.md
13
+ *
14
+ * Compatible with OpenClaw, Claude Code, and ClawHub-installed skills.
15
+ * Skills installed via `clawhub install` into <workspace>/skills/ are picked up automatically.
16
+ */
17
+ import { readdir, readFile, stat } from "node:fs/promises";
18
+ import { join, basename } from "node:path";
19
+ import { homedir } from "node:os";
20
+ import { parseSkillMd } from "./parser.js";
21
+ const SKILL_FILENAME = "SKILL.md";
22
+ // Limits aligned with OpenClaw defaults
23
+ const MAX_SKILLS_IN_PROMPT = 150;
24
+ const MAX_PROMPT_CHARS = 30_000;
25
+ const MAX_SKILL_FILE_BYTES = 256 * 1024; // 256 KB
26
+ const MAX_SKILLS_PER_SOURCE = 200;
27
+ export async function loadSkills(workspaceDir, config, logger) {
28
+ if (config && !config.enabled) {
29
+ logger?.info("Skills disabled in config");
30
+ return [];
31
+ }
32
+ const skills = [];
33
+ const seenNames = new Set();
34
+ // 1. Workspace skills (highest precedence)
35
+ const workspaceSkillsDir = join(workspaceDir, "skills");
36
+ await scanSkillDir(workspaceSkillsDir, "workspace", skills, seenNames, logger);
37
+ // 2. Managed skills (shared across agents)
38
+ const managedDir = join(homedir(), ".freeturtle", "skills");
39
+ if (managedDir !== workspaceSkillsDir) {
40
+ await scanSkillDir(managedDir, "managed", skills, seenNames, logger);
41
+ }
42
+ // 3. Extra directories from config
43
+ if (config?.extra_dirs) {
44
+ for (const dir of config.extra_dirs) {
45
+ await scanSkillDir(dir, "extra", skills, seenNames, logger);
46
+ }
47
+ }
48
+ if (skills.length > 0) {
49
+ logger?.info(`Loaded ${skills.length} skill(s): ${skills.map((s) => s.name).join(", ")}`);
50
+ }
51
+ return skills;
52
+ }
53
+ async function scanSkillDir(dir, source, skills, seenNames, logger) {
54
+ let entries;
55
+ try {
56
+ entries = await readdir(dir);
57
+ }
58
+ catch {
59
+ // Directory doesn't exist — that's fine
60
+ return;
61
+ }
62
+ let loadedFromSource = 0;
63
+ for (const entry of entries) {
64
+ if (loadedFromSource >= MAX_SKILLS_PER_SOURCE) {
65
+ logger?.warn(`Hit max skills per source (${MAX_SKILLS_PER_SOURCE}) for ${dir}`);
66
+ break;
67
+ }
68
+ const skillDir = join(dir, entry);
69
+ // Must be a directory
70
+ try {
71
+ const s = await stat(skillDir);
72
+ if (!s.isDirectory())
73
+ continue;
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ // Must contain SKILL.md
79
+ const skillMdPath = join(skillDir, SKILL_FILENAME);
80
+ let raw;
81
+ try {
82
+ const s = await stat(skillMdPath);
83
+ if (s.size > MAX_SKILL_FILE_BYTES) {
84
+ logger?.warn(`SKILL.md in ${skillDir} exceeds ${MAX_SKILL_FILE_BYTES / 1024}KB — skipping`);
85
+ continue;
86
+ }
87
+ raw = await readFile(skillMdPath, "utf-8");
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ // Parse the SKILL.md
93
+ const parsed = parseSkillMd(raw);
94
+ if (!parsed) {
95
+ logger?.warn(`Invalid SKILL.md in ${skillDir} — skipping`);
96
+ continue;
97
+ }
98
+ // Skill name: frontmatter.name or directory name
99
+ const name = parsed.frontmatter.name || basename(skillDir);
100
+ // Validate name format (Agent Skills spec: lowercase alphanumeric + hyphens)
101
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
102
+ logger?.warn(`Skill "${name}" has invalid name format — skipping`);
103
+ continue;
104
+ }
105
+ // Higher-precedence skills shadow lower ones
106
+ if (seenNames.has(name)) {
107
+ logger?.info(`Skill "${name}" from ${source} shadowed by higher-precedence source`);
108
+ continue;
109
+ }
110
+ seenNames.add(name);
111
+ const allowedToolsRaw = parsed.frontmatter["allowed-tools"];
112
+ const allowedTools = allowedToolsRaw
113
+ ? allowedToolsRaw.split(/\s+/).filter((t) => t.length > 0)
114
+ : [];
115
+ skills.push({
116
+ name,
117
+ description: parsed.frontmatter.description || "",
118
+ body: parsed.body,
119
+ frontmatter: parsed.frontmatter,
120
+ dir: skillDir,
121
+ source,
122
+ modelInvocable: parsed.frontmatter["disable-model-invocation"] !== true,
123
+ userInvocable: parsed.frontmatter["user-invocable"] !== false,
124
+ allowedTools,
125
+ });
126
+ loadedFromSource++;
127
+ }
128
+ }
129
+ /**
130
+ * Compact a file path by replacing the home directory with ~.
131
+ * Saves tokens in the system prompt (same as OpenClaw).
132
+ */
133
+ function compactPath(p) {
134
+ const home = homedir();
135
+ if (p.startsWith(home)) {
136
+ return "~" + p.slice(home.length);
137
+ }
138
+ return p;
139
+ }
140
+ /**
141
+ * Build the skills section for the system prompt.
142
+ *
143
+ * Uses the same progressive disclosure model as OpenClaw:
144
+ * - Only name/description/location are injected (not the full body)
145
+ * - The LLM reads the full SKILL.md via read_file when a skill matches
146
+ * - XML format for the available_skills block
147
+ * - Token budget enforcement via character limits
148
+ */
149
+ export function buildSkillsPrompt(skills) {
150
+ const eligible = skills.filter((s) => s.modelInvocable);
151
+ if (eligible.length === 0)
152
+ return "";
153
+ // Apply limits: max skills in prompt
154
+ const capped = eligible.slice(0, MAX_SKILLS_IN_PROMPT);
155
+ const header = [
156
+ "## Skills (mandatory)",
157
+ "",
158
+ "Before replying, scan the <available_skills> descriptions below.",
159
+ "- If exactly one skill clearly applies: read its SKILL.md at the given location with `read_file`, then follow its instructions.",
160
+ "- If multiple could apply: choose the most specific one, then read and follow it.",
161
+ "- If none clearly apply: do not read any SKILL.md.",
162
+ "- Never read more than one skill up front; only read after selecting.",
163
+ "- When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md).",
164
+ "",
165
+ ].join("\n");
166
+ // Build XML block, enforcing character budget
167
+ const xmlOpen = "<available_skills>\n";
168
+ const xmlClose = "</available_skills>";
169
+ let promptChars = header.length + xmlOpen.length + xmlClose.length;
170
+ const skillEntries = [];
171
+ for (const skill of capped) {
172
+ const location = compactPath(join(skill.dir, SKILL_FILENAME));
173
+ const entry = [
174
+ " <skill>",
175
+ ` <name>${escapeXml(skill.name)}</name>`,
176
+ ` <description>${escapeXml(skill.description)}</description>`,
177
+ ` <location>${escapeXml(location)}</location>`,
178
+ " </skill>",
179
+ ].join("\n");
180
+ if (promptChars + entry.length + 1 > MAX_PROMPT_CHARS) {
181
+ break;
182
+ }
183
+ skillEntries.push(entry);
184
+ promptChars += entry.length + 1; // +1 for newline
185
+ }
186
+ if (skillEntries.length === 0)
187
+ return "";
188
+ return header + xmlOpen + skillEntries.join("\n") + "\n" + xmlClose;
189
+ }
190
+ function escapeXml(s) {
191
+ return s
192
+ .replace(/&/g, "&amp;")
193
+ .replace(/</g, "&lt;")
194
+ .replace(/>/g, "&gt;")
195
+ .replace(/"/g, "&quot;");
196
+ }
197
+ // --- Deprecated, kept for backward compat ---
198
+ /** @deprecated Use buildSkillsPrompt instead */
199
+ export function buildSkillIndex(skills) {
200
+ return buildSkillsPrompt(skills);
201
+ }
202
+ /** @deprecated Skills are now loaded on-demand via read_file */
203
+ export function getSkillPrompt(skill) {
204
+ const parts = [`## Skill: ${skill.name}`, ""];
205
+ if (skill.frontmatter.compatibility) {
206
+ parts.push(`Compatibility: ${skill.frontmatter.compatibility}`, "");
207
+ }
208
+ parts.push(skill.body);
209
+ return parts.join("\n");
210
+ }
211
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/skills/loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SKILL.md frontmatter parser.
3
+ * Parses YAML frontmatter between --- markers without external dependencies.
4
+ */
5
+ import type { SkillFrontmatter } from "./types.js";
6
+ /**
7
+ * Parse a SKILL.md file into frontmatter + body.
8
+ * Returns null if the file doesn't have valid frontmatter.
9
+ */
10
+ export declare function parseSkillMd(raw: string): {
11
+ frontmatter: SkillFrontmatter;
12
+ body: string;
13
+ } | null;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * SKILL.md frontmatter parser.
3
+ * Parses YAML frontmatter between --- markers without external dependencies.
4
+ */
5
+ /**
6
+ * Parse a SKILL.md file into frontmatter + body.
7
+ * Returns null if the file doesn't have valid frontmatter.
8
+ */
9
+ export function parseSkillMd(raw) {
10
+ const trimmed = raw.trimStart();
11
+ if (!trimmed.startsWith("---"))
12
+ return null;
13
+ const secondFence = trimmed.indexOf("---", 3);
14
+ if (secondFence === -1)
15
+ return null;
16
+ const yamlBlock = trimmed.slice(3, secondFence).trim();
17
+ const body = trimmed.slice(secondFence + 3).trim();
18
+ const frontmatter = parseSimpleYaml(yamlBlock);
19
+ if (!frontmatter.name && !frontmatter.description)
20
+ return null;
21
+ return { frontmatter: frontmatter, body };
22
+ }
23
+ /**
24
+ * Minimal YAML parser for flat key-value frontmatter.
25
+ * Handles strings, booleans, and simple nested metadata maps.
26
+ * Does NOT handle arrays, multi-line values, or complex YAML.
27
+ */
28
+ function parseSimpleYaml(yaml) {
29
+ const result = {};
30
+ let currentKey = null;
31
+ let currentMap = null;
32
+ for (const line of yaml.split("\n")) {
33
+ // Blank line
34
+ if (line.trim() === "")
35
+ continue;
36
+ // Check indentation — if indented and we're in a map, it's a nested value
37
+ const indent = line.length - line.trimStart().length;
38
+ if (indent > 0 && currentMap !== null && currentKey !== null) {
39
+ const kvMatch = line.trim().match(/^([a-zA-Z0-9_.-]+):\s*(.*)/);
40
+ if (kvMatch) {
41
+ currentMap[kvMatch[1]] = unquote(kvMatch[2].trim());
42
+ }
43
+ continue;
44
+ }
45
+ // Close any open map
46
+ if (currentMap !== null && currentKey !== null) {
47
+ result[currentKey] = currentMap;
48
+ currentMap = null;
49
+ currentKey = null;
50
+ }
51
+ // Top-level key: value
52
+ const kvMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)/);
53
+ if (!kvMatch)
54
+ continue;
55
+ const key = kvMatch[1];
56
+ const rawValue = kvMatch[2].trim();
57
+ // Empty value — could be a map start
58
+ if (rawValue === "") {
59
+ currentKey = key;
60
+ currentMap = {};
61
+ continue;
62
+ }
63
+ result[key] = parseValue(rawValue);
64
+ }
65
+ // Close trailing map
66
+ if (currentMap !== null && currentKey !== null) {
67
+ result[currentKey] = currentMap;
68
+ }
69
+ return result;
70
+ }
71
+ function parseValue(raw) {
72
+ if (raw === "true")
73
+ return true;
74
+ if (raw === "false")
75
+ return false;
76
+ return unquote(raw);
77
+ }
78
+ function unquote(s) {
79
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
80
+ return s.slice(1, -1);
81
+ }
82
+ return s;
83
+ }
84
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/skills/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Agent Skills specification types.
3
+ * Compatible with OpenClaw, Claude Code, Codex, and ClawHub.
4
+ * Spec: https://agentskills.io/specification
5
+ */
6
+ export interface SkillFrontmatter {
7
+ /** 1-64 chars, lowercase alphanumeric + hyphens, must match directory name */
8
+ name: string;
9
+ /** 1-1024 chars, describes what skill does and when to use it */
10
+ description: string;
11
+ /** License name or reference to bundled file */
12
+ license?: string;
13
+ /** Max 500 chars, environment requirements */
14
+ compatibility?: string;
15
+ /** Arbitrary key-value metadata (string to string) */
16
+ metadata?: Record<string, string>;
17
+ /** Space-delimited list of pre-approved tools */
18
+ "allowed-tools"?: string;
19
+ /** Expose as slash command (default: true) */
20
+ "user-invocable"?: boolean;
21
+ /** Prevent auto-invocation by LLM (default: false) */
22
+ "disable-model-invocation"?: boolean;
23
+ "argument-hint"?: string;
24
+ model?: string;
25
+ context?: string;
26
+ agent?: string;
27
+ }
28
+ export interface LoadedSkill {
29
+ /** Skill name (from frontmatter or directory name) */
30
+ name: string;
31
+ /** Short description for metadata-level loading */
32
+ description: string;
33
+ /** Full SKILL.md body (markdown after frontmatter) */
34
+ body: string;
35
+ /** Parsed frontmatter */
36
+ frontmatter: SkillFrontmatter;
37
+ /** Absolute path to skill directory */
38
+ dir: string;
39
+ /** Where the skill was loaded from */
40
+ source: "workspace" | "managed" | "bundled" | "extra";
41
+ /** Whether the LLM can auto-invoke this skill */
42
+ modelInvocable: boolean;
43
+ /** Whether the user can invoke as a slash command */
44
+ userInvocable: boolean;
45
+ /** Pre-approved tool names */
46
+ allowedTools: string[];
47
+ }
48
+ export interface SkillsConfig {
49
+ enabled: boolean;
50
+ /** Additional directories to scan for skills */
51
+ extra_dirs?: string[];
52
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Agent Skills specification types.
3
+ * Compatible with OpenClaw, Claude Code, Codex, and ClawHub.
4
+ * Spec: https://agentskills.io/specification
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/skills/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freeturtle",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "An open-source framework for deploying autonomous AI CEOs that run onchain businesses.",
5
5
  "type": "module",
6
6
  "main": "dist/bin/freeturtle.js",
@@ -17,7 +17,10 @@
17
17
  "build": "tsc",
18
18
  "dev": "tsc --watch",
19
19
  "lint": "eslint .",
20
- "lint:fix": "eslint . --fix"
20
+ "lint:fix": "eslint . --fix",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:coverage": "vitest run --coverage"
21
24
  },
22
25
  "keywords": [
23
26
  "ai",
@@ -36,9 +39,11 @@
36
39
  "@types/pg": "^8.18.0",
37
40
  "@types/qrcode-terminal": "^0.12.2",
38
41
  "@types/ws": "^8.18.1",
42
+ "@vitest/coverage-v8": "^4.0.18",
39
43
  "eslint": "^10.0.2",
40
44
  "typescript": "^5.9.3",
41
- "typescript-eslint": "^8.56.1"
45
+ "typescript-eslint": "^8.56.1",
46
+ "vitest": "^4.0.18"
42
47
  },
43
48
  "dependencies": {
44
49
  "@anthropic-ai/sdk": "^0.78.0",