zoe-agent 0.3.1
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/CHANGELOG.md +154 -0
- package/LICENSE +96 -0
- package/README.md +568 -0
- package/dist/adapters/cli/agent.d.ts +59 -0
- package/dist/adapters/cli/agent.js +232 -0
- package/dist/adapters/cli/bootstrap.d.ts +25 -0
- package/dist/adapters/cli/bootstrap.js +204 -0
- package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
- package/dist/adapters/cli/commands/build-registry.js +88 -0
- package/dist/adapters/cli/commands/clear.d.ts +7 -0
- package/dist/adapters/cli/commands/clear.js +10 -0
- package/dist/adapters/cli/commands/compact.d.ts +13 -0
- package/dist/adapters/cli/commands/compact.js +96 -0
- package/dist/adapters/cli/commands/exit.d.ts +7 -0
- package/dist/adapters/cli/commands/exit.js +9 -0
- package/dist/adapters/cli/commands/gateway.d.ts +7 -0
- package/dist/adapters/cli/commands/gateway.js +152 -0
- package/dist/adapters/cli/commands/help.d.ts +9 -0
- package/dist/adapters/cli/commands/help.js +12 -0
- package/dist/adapters/cli/commands/models.d.ts +10 -0
- package/dist/adapters/cli/commands/models.js +32 -0
- package/dist/adapters/cli/commands/registry.d.ts +70 -0
- package/dist/adapters/cli/commands/registry.js +111 -0
- package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
- package/dist/adapters/cli/commands/settings-utils.js +182 -0
- package/dist/adapters/cli/commands/settings.d.ts +9 -0
- package/dist/adapters/cli/commands/settings.js +395 -0
- package/dist/adapters/cli/commands/skills.d.ts +7 -0
- package/dist/adapters/cli/commands/skills.js +21 -0
- package/dist/adapters/cli/config-loader.d.ts +27 -0
- package/dist/adapters/cli/config-loader.js +48 -0
- package/dist/adapters/cli/docker-utils.d.ts +37 -0
- package/dist/adapters/cli/docker-utils.js +90 -0
- package/dist/adapters/cli/index.d.ts +2 -0
- package/dist/adapters/cli/index.js +88 -0
- package/dist/adapters/cli/repl.d.ts +22 -0
- package/dist/adapters/cli/repl.js +256 -0
- package/dist/adapters/cli/setup.d.ts +19 -0
- package/dist/adapters/cli/setup.js +613 -0
- package/dist/adapters/cli/system-prompts.d.ts +56 -0
- package/dist/adapters/cli/system-prompts.js +131 -0
- package/dist/adapters/cli/tui/app.d.ts +58 -0
- package/dist/adapters/cli/tui/app.js +314 -0
- package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
- package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
- package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
- package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
- package/dist/adapters/cli/tui/components/command-palette.js +50 -0
- package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
- package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
- package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/error-message.js +8 -0
- package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
- package/dist/adapters/cli/tui/components/footer.js +19 -0
- package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
- package/dist/adapters/cli/tui/components/goal-status.js +22 -0
- package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/info-message.js +8 -0
- package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
- package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
- package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
- package/dist/adapters/cli/tui/components/markdown.js +92 -0
- package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
- package/dist/adapters/cli/tui/components/message-area.js +55 -0
- package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
- package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
- package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
- package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
- package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
- package/dist/adapters/cli/tui/components/text-input.js +142 -0
- package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
- package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
- package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/user-message.js +8 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
- package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
- package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
- package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
- package/dist/adapters/cli/tui/feed-serializer.js +70 -0
- package/dist/adapters/cli/tui/file-index.d.ts +8 -0
- package/dist/adapters/cli/tui/file-index.js +41 -0
- package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
- package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
- package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
- package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
- package/dist/adapters/cli/tui/index.d.ts +19 -0
- package/dist/adapters/cli/tui/index.js +206 -0
- package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
- package/dist/adapters/cli/tui/ink-reset.js +57 -0
- package/dist/adapters/cli/tui/layout.d.ts +15 -0
- package/dist/adapters/cli/tui/layout.js +15 -0
- package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
- package/dist/adapters/cli/tui/logo/gradient.js +31 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
- package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
- package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
- package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
- package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
- package/dist/adapters/cli/tui/session-export.d.ts +21 -0
- package/dist/adapters/cli/tui/session-export.js +63 -0
- package/dist/adapters/cli/tui/theme.d.ts +23 -0
- package/dist/adapters/cli/tui/theme.js +22 -0
- package/dist/adapters/cli/tui/types.d.ts +52 -0
- package/dist/adapters/cli/tui/types.js +12 -0
- package/dist/adapters/sdk/agent.d.ts +20 -0
- package/dist/adapters/sdk/agent.js +356 -0
- package/dist/adapters/sdk/http.d.ts +43 -0
- package/dist/adapters/sdk/http.js +61 -0
- package/dist/adapters/sdk/index.d.ts +58 -0
- package/dist/adapters/sdk/index.js +209 -0
- package/dist/adapters/sdk/settings.d.ts +18 -0
- package/dist/adapters/sdk/settings.js +57 -0
- package/dist/adapters/sdk/tools.d.ts +7 -0
- package/dist/adapters/sdk/tools.js +13 -0
- package/dist/adapters/server/auth.d.ts +53 -0
- package/dist/adapters/server/auth.js +168 -0
- package/dist/adapters/server/index.d.ts +40 -0
- package/dist/adapters/server/index.js +255 -0
- package/dist/adapters/server/rest-gateway.d.ts +13 -0
- package/dist/adapters/server/rest-gateway.js +218 -0
- package/dist/adapters/server/rest.d.ts +37 -0
- package/dist/adapters/server/rest.js +341 -0
- package/dist/adapters/server/server-core.d.ts +55 -0
- package/dist/adapters/server/server-core.js +121 -0
- package/dist/adapters/server/session-store.d.ts +81 -0
- package/dist/adapters/server/session-store.js +272 -0
- package/dist/adapters/server/settings-handlers.d.ts +24 -0
- package/dist/adapters/server/settings-handlers.js +360 -0
- package/dist/adapters/server/standalone.d.ts +19 -0
- package/dist/adapters/server/standalone.js +113 -0
- package/dist/adapters/server/websocket.d.ts +26 -0
- package/dist/adapters/server/websocket.js +68 -0
- package/dist/adapters/server/ws-handlers.d.ts +32 -0
- package/dist/adapters/server/ws-handlers.js +523 -0
- package/dist/adapters/server/ws-types.d.ts +304 -0
- package/dist/adapters/server/ws-types.js +7 -0
- package/dist/core/agent-loop.d.ts +68 -0
- package/dist/core/agent-loop.js +423 -0
- package/dist/core/config.d.ts +115 -0
- package/dist/core/config.js +189 -0
- package/dist/core/errors.d.ts +58 -0
- package/dist/core/errors.js +88 -0
- package/dist/core/hooks.d.ts +35 -0
- package/dist/core/hooks.js +49 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +29 -0
- package/dist/core/message-convert.d.ts +41 -0
- package/dist/core/message-convert.js +94 -0
- package/dist/core/middleware/auth.d.ts +24 -0
- package/dist/core/middleware/auth.js +28 -0
- package/dist/core/middleware/logging.d.ts +23 -0
- package/dist/core/middleware/logging.js +28 -0
- package/dist/core/middleware/rate-limit.d.ts +27 -0
- package/dist/core/middleware/rate-limit.js +38 -0
- package/dist/core/middleware/semantic-tools.d.ts +10 -0
- package/dist/core/middleware/semantic-tools.js +43 -0
- package/dist/core/middleware.d.ts +48 -0
- package/dist/core/middleware.js +38 -0
- package/dist/core/permission.d.ts +25 -0
- package/dist/core/permission.js +50 -0
- package/dist/core/provider-config.d.ts +129 -0
- package/dist/core/provider-config.js +273 -0
- package/dist/core/provider-env.d.ts +39 -0
- package/dist/core/provider-env.js +142 -0
- package/dist/core/provider-resolver.d.ts +12 -0
- package/dist/core/provider-resolver.js +12 -0
- package/dist/core/session-store.d.ts +75 -0
- package/dist/core/session-store.js +245 -0
- package/dist/core/settings-manager.d.ts +57 -0
- package/dist/core/settings-manager.js +359 -0
- package/dist/core/settings-schema.d.ts +38 -0
- package/dist/core/settings-schema.js +171 -0
- package/dist/core/skill-catalog.d.ts +6 -0
- package/dist/core/skill-catalog.js +17 -0
- package/dist/core/skill-invoker.d.ts +127 -0
- package/dist/core/skill-invoker.js +182 -0
- package/dist/core/stream-accumulator.d.ts +21 -0
- package/dist/core/stream-accumulator.js +51 -0
- package/dist/core/stream-manager.d.ts +58 -0
- package/dist/core/stream-manager.js +212 -0
- package/dist/core/tool-executor.d.ts +84 -0
- package/dist/core/tool-executor.js +256 -0
- package/dist/core/types.d.ts +259 -0
- package/dist/core/types.js +11 -0
- package/dist/gateway/gateway.d.ts +52 -0
- package/dist/gateway/gateway.js +537 -0
- package/dist/gateway/index.d.ts +21 -0
- package/dist/gateway/index.js +31 -0
- package/dist/gateway/openapi-importer.d.ts +15 -0
- package/dist/gateway/openapi-importer.js +66 -0
- package/dist/gateway/semantic-scorer.d.ts +7 -0
- package/dist/gateway/semantic-scorer.js +24 -0
- package/dist/gateway/settings-adapter.d.ts +49 -0
- package/dist/gateway/settings-adapter.js +137 -0
- package/dist/gateway/tool-factory.d.ts +9 -0
- package/dist/gateway/tool-factory.js +414 -0
- package/dist/gateway/types.d.ts +68 -0
- package/dist/gateway/types.js +7 -0
- package/dist/models-catalog.js +46 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +148 -0
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +71 -0
- package/dist/providers/types.d.ts +48 -0
- package/dist/providers/types.js +1 -0
- package/dist/skills/args.d.ts +37 -0
- package/dist/skills/args.js +99 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +23 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/parser.d.ts +7 -0
- package/dist/skills/parser.js +152 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +74 -0
- package/dist/skills/resolver.d.ts +19 -0
- package/dist/skills/resolver.js +116 -0
- package/dist/skills/types.d.ts +74 -0
- package/dist/skills/types.js +50 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +68 -0
- package/dist/tools/core.d.ts +20 -0
- package/dist/tools/core.js +244 -0
- package/dist/tools/email.d.ts +2 -0
- package/dist/tools/email.js +61 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.js +257 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +88 -0
- package/dist/tools/interface.d.ts +22 -0
- package/dist/tools/interface.js +1 -0
- package/dist/tools/notify.d.ts +2 -0
- package/dist/tools/notify.js +100 -0
- package/dist/tools/prompt-optimizer.d.ts +2 -0
- package/dist/tools/prompt-optimizer.js +65 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +184 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/todos.d.ts +10 -0
- package/dist/tools/todos.js +50 -0
- package/package.json +119 -0
- package/skills/docker-ops/SKILL.md +329 -0
- package/skills/k8s-deploy/SKILL.md +397 -0
- package/skills/log-analyzer/SKILL.md +331 -0
- package/skills/speckit-analyze/SKILL.md +260 -0
- package/skills/speckit-checklist/SKILL.md +374 -0
- package/skills/speckit-clarify/SKILL.md +286 -0
- package/skills/speckit-constitution/SKILL.md +157 -0
- package/skills/speckit-implement/SKILL.md +224 -0
- package/skills/speckit-plan/SKILL.md +171 -0
- package/skills/speckit-specify/SKILL.md +346 -0
- package/skills/speckit-tasks/SKILL.md +215 -0
- package/skills/speckit-taskstoissues/SKILL.md +107 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { parseFrontmatter } from './parser.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
export function getSkillPaths(cwd) {
|
|
9
|
+
const paths = [];
|
|
10
|
+
// 1. Environment variable (highest priority, colon-separated)
|
|
11
|
+
const envPath = process.env.ZOE_SKILLS_PATH;
|
|
12
|
+
if (envPath) {
|
|
13
|
+
paths.push(...envPath.split(':').filter(p => p));
|
|
14
|
+
}
|
|
15
|
+
// 2. Project skills
|
|
16
|
+
paths.push(join(cwd, '.zoe', 'skills'));
|
|
17
|
+
// 3. Volume-mounted skills (Docker)
|
|
18
|
+
paths.push('/mnt/skills');
|
|
19
|
+
// 4. Bundled skills (shipped with zoe)
|
|
20
|
+
if (!process.env.ZOE_NO_BUNDLED_SKILLS) {
|
|
21
|
+
paths.push(join(__dirname, '..', '..', 'skills'));
|
|
22
|
+
}
|
|
23
|
+
return paths;
|
|
24
|
+
}
|
|
25
|
+
export async function discoverSkills(cwd) {
|
|
26
|
+
const paths = getSkillPaths(cwd);
|
|
27
|
+
const skills = new Map();
|
|
28
|
+
// Load in reverse priority order so higher priority overwrites
|
|
29
|
+
for (const searchPath of [...paths].reverse()) {
|
|
30
|
+
if (!existsSync(searchPath))
|
|
31
|
+
continue;
|
|
32
|
+
try {
|
|
33
|
+
const entries = await readdir(searchPath, { withFileTypes: true });
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (!entry.isDirectory())
|
|
36
|
+
continue;
|
|
37
|
+
const skillFile = join(searchPath, entry.name, 'SKILL.md');
|
|
38
|
+
if (!existsSync(skillFile))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const skill = await parseFrontmatter(skillFile);
|
|
42
|
+
skill.basePath = join(searchPath, entry.name);
|
|
43
|
+
skill.source = searchPath;
|
|
44
|
+
const existing = skills.get(skill.name);
|
|
45
|
+
if (!existing || skill.priority >= existing.priority) {
|
|
46
|
+
skills.set(skill.name, skill);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.warn(`Warning: Failed to load skill from ${skillFile}: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Directory not readable, skip silently
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Array.from(skills.values());
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Skill } from './types.js';
|
|
2
|
+
export declare function parseSkillFile(filePath: string): Promise<Skill>;
|
|
3
|
+
/**
|
|
4
|
+
* Parse only the YAML frontmatter from a skill file.
|
|
5
|
+
* Discards the body text immediately — body is loaded lazily via getBody().
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseFrontmatter(filePath: string): Promise<Skill>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { getSkillBodyLimits } from './types.js';
|
|
3
|
+
export async function parseSkillFile(filePath) {
|
|
4
|
+
const content = await readFile(filePath, 'utf-8');
|
|
5
|
+
const { frontmatter, body } = extractFrontmatter(content);
|
|
6
|
+
if (!frontmatter.name)
|
|
7
|
+
throw new Error(`Skill missing 'name' field: ${filePath}`);
|
|
8
|
+
if (!frontmatter.description)
|
|
9
|
+
throw new Error(`Skill missing 'description' field: ${filePath}`);
|
|
10
|
+
// Warn at load time if body exceeds the soft warning threshold
|
|
11
|
+
const { warnChars } = getSkillBodyLimits();
|
|
12
|
+
if (body.length > warnChars) {
|
|
13
|
+
console.warn(`[SKILLS] Warning: Skill "${frontmatter.name}" body is ${body.length} chars ` +
|
|
14
|
+
`(~${Math.ceil(body.length / 4)} tokens). Consider trimming below ${warnChars} chars ` +
|
|
15
|
+
`for optimal context usage.`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
name: frontmatter.name,
|
|
19
|
+
description: frontmatter.description,
|
|
20
|
+
version: frontmatter.version || '1.0.0',
|
|
21
|
+
author: frontmatter.author,
|
|
22
|
+
tags: frontmatter.tags || [],
|
|
23
|
+
allowedTools: frontmatter.allowedTools,
|
|
24
|
+
priority: frontmatter.priority || 0,
|
|
25
|
+
basePath: '',
|
|
26
|
+
source: '',
|
|
27
|
+
frontmatter,
|
|
28
|
+
filePath,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse only the YAML frontmatter from a skill file.
|
|
33
|
+
* Discards the body text immediately — body is loaded lazily via getBody().
|
|
34
|
+
*/
|
|
35
|
+
export async function parseFrontmatter(filePath) {
|
|
36
|
+
const content = await readFile(filePath, 'utf-8');
|
|
37
|
+
const frontmatter = extractFrontmatterOnly(content);
|
|
38
|
+
if (!frontmatter.name)
|
|
39
|
+
throw new Error(`Skill missing 'name' field: ${filePath}`);
|
|
40
|
+
if (!frontmatter.description)
|
|
41
|
+
throw new Error(`Skill missing 'description' field: ${filePath}`);
|
|
42
|
+
return {
|
|
43
|
+
name: frontmatter.name,
|
|
44
|
+
description: frontmatter.description,
|
|
45
|
+
version: frontmatter.version || '1.0.0',
|
|
46
|
+
author: frontmatter.author,
|
|
47
|
+
tags: frontmatter.tags || [],
|
|
48
|
+
allowedTools: frontmatter.allowedTools,
|
|
49
|
+
priority: frontmatter.priority || 0,
|
|
50
|
+
basePath: '',
|
|
51
|
+
source: '',
|
|
52
|
+
frontmatter,
|
|
53
|
+
filePath,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function extractFrontmatter(content) {
|
|
57
|
+
const trimmed = content.trimStart();
|
|
58
|
+
if (!trimmed.startsWith('---')) {
|
|
59
|
+
return { frontmatter: {}, body: content };
|
|
60
|
+
}
|
|
61
|
+
const endIdx = trimmed.indexOf('---', 3);
|
|
62
|
+
if (endIdx === -1) {
|
|
63
|
+
return { frontmatter: {}, body: content };
|
|
64
|
+
}
|
|
65
|
+
const yaml = trimmed.slice(3, endIdx);
|
|
66
|
+
const body = trimmed.slice(endIdx + 3).trimStart();
|
|
67
|
+
return { frontmatter: parseYaml(yaml), body };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extract only the frontmatter from a skill file.
|
|
71
|
+
* The body text is not returned — used during discovery to avoid holding bodies in memory.
|
|
72
|
+
*/
|
|
73
|
+
function extractFrontmatterOnly(content) {
|
|
74
|
+
const trimmed = content.trimStart();
|
|
75
|
+
if (!trimmed.startsWith('---')) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
const endIdx = trimmed.indexOf('---', 3);
|
|
79
|
+
if (endIdx === -1) {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
const yaml = trimmed.slice(3, endIdx);
|
|
83
|
+
return parseYaml(yaml);
|
|
84
|
+
}
|
|
85
|
+
function parseYaml(yaml) {
|
|
86
|
+
const result = {};
|
|
87
|
+
const lines = yaml.split('\n');
|
|
88
|
+
let i = 0;
|
|
89
|
+
while (i < lines.length) {
|
|
90
|
+
const line = lines[i];
|
|
91
|
+
// Dash-based array item (skip — consumed by parent key)
|
|
92
|
+
if (/^\s+-\s/.test(line)) {
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const match = line.match(/^(\w+):\s*(.*)/);
|
|
97
|
+
if (!match) {
|
|
98
|
+
i++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const [, key, rawValue] = match;
|
|
102
|
+
const val = rawValue.trim();
|
|
103
|
+
// Multiline literal (|) or folded (>)
|
|
104
|
+
if (val === '|' || val === '>') {
|
|
105
|
+
const multiline = [];
|
|
106
|
+
i++;
|
|
107
|
+
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) {
|
|
108
|
+
multiline.push(lines[i].replace(/^ /, ''));
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
result[key] = multiline.join('\n').trim();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// Empty value — check if next lines are dash-based array items
|
|
115
|
+
if (val === '') {
|
|
116
|
+
const arr = [];
|
|
117
|
+
let j = i + 1;
|
|
118
|
+
while (j < lines.length) {
|
|
119
|
+
const arrMatch = lines[j].match(/^\s+-\s+(.*)/);
|
|
120
|
+
if (!arrMatch)
|
|
121
|
+
break;
|
|
122
|
+
arr.push(arrMatch[1].trim().replace(/^['"]|['"]$/g, ''));
|
|
123
|
+
j++;
|
|
124
|
+
}
|
|
125
|
+
if (arr.length > 0) {
|
|
126
|
+
result[key] = arr;
|
|
127
|
+
i = j;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Inline array [a, b, c]
|
|
132
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
133
|
+
result[key] = val
|
|
134
|
+
.slice(1, -1)
|
|
135
|
+
.split(',')
|
|
136
|
+
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
i++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Number
|
|
142
|
+
if (/^\d+(\.\d+)?$/.test(val)) {
|
|
143
|
+
result[key] = Number(val);
|
|
144
|
+
i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// String (strip surrounding quotes)
|
|
148
|
+
result[key] = val.replace(/^['"]|['"]$/g, '');
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Skill, SkillMetadata, SkillRegistry } from './types.js';
|
|
2
|
+
export declare class DefaultSkillRegistry implements SkillRegistry {
|
|
3
|
+
private skills;
|
|
4
|
+
private bodyCache;
|
|
5
|
+
private readonly maxCacheSize;
|
|
6
|
+
constructor(skills: Skill[]);
|
|
7
|
+
get(name: string): Skill | undefined;
|
|
8
|
+
getAll(): Skill[];
|
|
9
|
+
getMetadata(): SkillMetadata[];
|
|
10
|
+
getBody(name: string): Promise<string | undefined>;
|
|
11
|
+
private setCache;
|
|
12
|
+
getNames(): string[];
|
|
13
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
export class DefaultSkillRegistry {
|
|
3
|
+
skills;
|
|
4
|
+
bodyCache;
|
|
5
|
+
maxCacheSize = 5;
|
|
6
|
+
constructor(skills) {
|
|
7
|
+
this.skills = new Map(skills.map(s => [s.name, s]));
|
|
8
|
+
this.bodyCache = new Map();
|
|
9
|
+
}
|
|
10
|
+
get(name) {
|
|
11
|
+
return this.skills.get(name);
|
|
12
|
+
}
|
|
13
|
+
getAll() {
|
|
14
|
+
return Array.from(this.skills.values());
|
|
15
|
+
}
|
|
16
|
+
getMetadata() {
|
|
17
|
+
return this.getAll().map(s => ({
|
|
18
|
+
name: s.name,
|
|
19
|
+
description: s.description,
|
|
20
|
+
version: s.version,
|
|
21
|
+
tags: s.tags,
|
|
22
|
+
allowedTools: s.allowedTools,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
async getBody(name) {
|
|
26
|
+
const skill = this.get(name);
|
|
27
|
+
if (!skill)
|
|
28
|
+
return undefined;
|
|
29
|
+
// Check cache first
|
|
30
|
+
const cached = this.bodyCache.get(name);
|
|
31
|
+
if (cached !== undefined)
|
|
32
|
+
return cached;
|
|
33
|
+
// Load body lazily from disk
|
|
34
|
+
try {
|
|
35
|
+
const content = await readFile(skill.filePath, 'utf-8');
|
|
36
|
+
const body = extractBody(content);
|
|
37
|
+
if (body === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
this.setCache(name, body);
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// File deleted, moved, or unreadable
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
setCache(name, body) {
|
|
48
|
+
this.bodyCache.delete(name); // Remove if exists (moves to end)
|
|
49
|
+
this.bodyCache.set(name, body);
|
|
50
|
+
// Evict oldest
|
|
51
|
+
if (this.bodyCache.size > this.maxCacheSize) {
|
|
52
|
+
const firstKey = this.bodyCache.keys().next().value;
|
|
53
|
+
if (firstKey)
|
|
54
|
+
this.bodyCache.delete(firstKey);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
getNames() {
|
|
58
|
+
return Array.from(this.skills.keys());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract the body text after the closing --- delimiter of YAML frontmatter.
|
|
63
|
+
*/
|
|
64
|
+
function extractBody(content) {
|
|
65
|
+
const trimmed = content.trimStart();
|
|
66
|
+
if (!trimmed.startsWith('---')) {
|
|
67
|
+
return content;
|
|
68
|
+
}
|
|
69
|
+
const endIdx = trimmed.indexOf('---', 3);
|
|
70
|
+
if (endIdx === -1) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return trimmed.slice(endIdx + 3).trimStart() || undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @path file reference resolver for skills and chat.
|
|
3
|
+
*
|
|
4
|
+
* Supported patterns:
|
|
5
|
+
* - @path/to/file — relative to project root (process.cwd())
|
|
6
|
+
* - @zoe_documents/file — resolves to ~/zoe_documents/file
|
|
7
|
+
* - @~/path/to/file — explicit home directory path
|
|
8
|
+
*
|
|
9
|
+
* Resolution flow:
|
|
10
|
+
* 1. Scan text for @reference patterns
|
|
11
|
+
* 2. Resolve each path
|
|
12
|
+
* 3. Read file content
|
|
13
|
+
* 4. Replace @reference with inlined content
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Resolve all @path references in a text string, inlining file contents.
|
|
17
|
+
* Returns the text with references replaced by file contents.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveReferences(text: string, projectRoot?: string): Promise<string>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @path file reference resolver for skills and chat.
|
|
3
|
+
*
|
|
4
|
+
* Supported patterns:
|
|
5
|
+
* - @path/to/file — relative to project root (process.cwd())
|
|
6
|
+
* - @zoe_documents/file — resolves to ~/zoe_documents/file
|
|
7
|
+
* - @~/path/to/file — explicit home directory path
|
|
8
|
+
*
|
|
9
|
+
* Resolution flow:
|
|
10
|
+
* 1. Scan text for @reference patterns
|
|
11
|
+
* 2. Resolve each path
|
|
12
|
+
* 3. Read file content
|
|
13
|
+
* 4. Replace @reference with inlined content
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from 'fs/promises';
|
|
16
|
+
import { resolve, join } from 'path';
|
|
17
|
+
import { existsSync, statSync } from 'fs';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB per file
|
|
20
|
+
const MAX_REFERENCES = 10; // Max @references per input
|
|
21
|
+
const MAX_TOTAL_RESOLVED_SIZE = 2 * 1024 * 1024; // 2MB total across all inlined files
|
|
22
|
+
/**
|
|
23
|
+
* Extract all @reference patterns from text.
|
|
24
|
+
* Matches @path/to/file, @zoe_documents/foo, @~/foo/bar
|
|
25
|
+
* Does NOT match email addresses (requires word boundary before @)
|
|
26
|
+
*/
|
|
27
|
+
function extractReferences(text) {
|
|
28
|
+
// Match @path patterns but exclude email addresses
|
|
29
|
+
// Pattern: @ followed by a path-like string (no spaces, starts with alphanumeric or ~ or zoe_documents)
|
|
30
|
+
const pattern = /(?:^|[^a-zA-Z0-9])@(~?\/?[a-zA-Z0-9_][a-zA-Z0-9_./-]*)/g;
|
|
31
|
+
const matches = [];
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
34
|
+
matches.push(match[1]); // The path part without @
|
|
35
|
+
}
|
|
36
|
+
return matches.slice(0, MAX_REFERENCES);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a reference path to an absolute file path.
|
|
40
|
+
*/
|
|
41
|
+
function resolveReference(refPath, projectRoot) {
|
|
42
|
+
// @~/... → explicit home directory
|
|
43
|
+
if (refPath.startsWith('~/')) {
|
|
44
|
+
return resolve(join(homedir(), refPath.slice(2)));
|
|
45
|
+
}
|
|
46
|
+
// @zoe_documents/... → ~/zoe_documents/...
|
|
47
|
+
if (refPath.startsWith('zoe_documents/') || refPath === 'zoe_documents') {
|
|
48
|
+
return resolve(join(homedir(), refPath));
|
|
49
|
+
}
|
|
50
|
+
// @path/to/file → relative to project root
|
|
51
|
+
return resolve(join(projectRoot, refPath));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate a resolved path is within allowed boundaries.
|
|
55
|
+
* Prevents path traversal attacks.
|
|
56
|
+
*/
|
|
57
|
+
function isPathAllowed(resolvedPath, projectRoot) {
|
|
58
|
+
const home = homedir();
|
|
59
|
+
const allowedPrefixes = [
|
|
60
|
+
projectRoot, // Project files (read-only via @)
|
|
61
|
+
join(home, 'zoe_documents'), // Agent workspace
|
|
62
|
+
join(home, '.zoe'), // Config/skills
|
|
63
|
+
];
|
|
64
|
+
return allowedPrefixes.some(prefix => resolvedPath.startsWith(prefix));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve all @path references in a text string, inlining file contents.
|
|
68
|
+
* Returns the text with references replaced by file contents.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveReferences(text, projectRoot) {
|
|
71
|
+
const root = projectRoot || process.cwd();
|
|
72
|
+
const refs = extractReferences(text);
|
|
73
|
+
if (refs.length === 0)
|
|
74
|
+
return text;
|
|
75
|
+
let result = text;
|
|
76
|
+
let cumulativeSize = 0;
|
|
77
|
+
for (const ref of refs) {
|
|
78
|
+
const resolvedPath = resolveReference(ref, root);
|
|
79
|
+
// Security check
|
|
80
|
+
if (!isPathAllowed(resolvedPath, root)) {
|
|
81
|
+
result = result.replace(`@${ref}`, `[Error: Access denied — path outside allowed boundaries: @${ref}]`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Existence check
|
|
85
|
+
if (!existsSync(resolvedPath)) {
|
|
86
|
+
result = result.replace(`@${ref}`, `[Error: File not found: @${ref}]`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const stat = statSync(resolvedPath);
|
|
91
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
92
|
+
result = result.replace(`@${ref}`, `[Error: File too large (${Math.round(stat.size / 1024)}KB exceeds 1MB limit): @${ref}]`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const content = await readFile(resolvedPath, 'utf-8');
|
|
96
|
+
// Cumulative size cap — stop inlining if total exceeds budget
|
|
97
|
+
if (cumulativeSize + content.length > MAX_TOTAL_RESOLVED_SIZE) {
|
|
98
|
+
result = result.replace(`@${ref}`, `[Skipped: cumulative resolved size would exceed ${Math.round(MAX_TOTAL_RESOLVED_SIZE / 1024)}KB limit]: @${ref}]`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
cumulativeSize += content.length;
|
|
102
|
+
const ext = resolvedPath.split('.').pop() || '';
|
|
103
|
+
const langMap = {
|
|
104
|
+
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
|
|
105
|
+
py: 'python', rs: 'rust', go: 'go', json: 'json', yaml: 'yaml', yml: 'yaml',
|
|
106
|
+
md: 'markdown', html: 'html', css: 'css', sh: 'bash', sql: 'sql',
|
|
107
|
+
};
|
|
108
|
+
const lang = langMap[ext] || ext;
|
|
109
|
+
result = result.replace(`@${ref}`, `\n---\n**File: ${ref}**\n\`\`\`${lang}\n${content}\n\`\`\`\n---\n`);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
result = result.replace(`@${ref}`, `[Error: Failed to read @${ref}: ${error.message}]`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface SkillModelConfig {
|
|
2
|
+
provider?: string;
|
|
3
|
+
model: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SkillFrontmatter {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
author?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
allowedTools?: string[];
|
|
12
|
+
priority?: number;
|
|
13
|
+
args?: string[];
|
|
14
|
+
model?: SkillModelConfig;
|
|
15
|
+
}
|
|
16
|
+
export interface Skill {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
version: string;
|
|
20
|
+
author?: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
allowedTools?: string[];
|
|
23
|
+
priority: number;
|
|
24
|
+
basePath: string;
|
|
25
|
+
source: string;
|
|
26
|
+
frontmatter: SkillFrontmatter;
|
|
27
|
+
filePath: string;
|
|
28
|
+
}
|
|
29
|
+
export interface SkillMetadata {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
version: string;
|
|
33
|
+
tags: string[];
|
|
34
|
+
allowedTools?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface SkillRegistry {
|
|
37
|
+
get(name: string): Skill | undefined;
|
|
38
|
+
getAll(): Skill[];
|
|
39
|
+
getMetadata(): SkillMetadata[];
|
|
40
|
+
getBody(name: string): Promise<string | undefined>;
|
|
41
|
+
}
|
|
42
|
+
/** Default maximum skill body size in characters (~8k tokens at 4 chars/token). */
|
|
43
|
+
export declare const DEFAULT_SKILL_BODY_MAX_CHARS = 32000;
|
|
44
|
+
/** Default warning threshold in characters (~2k tokens at 4 chars/token). */
|
|
45
|
+
export declare const DEFAULT_SKILL_BODY_WARN_CHARS = 8000;
|
|
46
|
+
/**
|
|
47
|
+
* Resolved skill body limits from environment variables.
|
|
48
|
+
* Falls back to defaults if not set or unparsable.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getSkillBodyLimits(): {
|
|
51
|
+
maxChars: number;
|
|
52
|
+
warnChars: number;
|
|
53
|
+
};
|
|
54
|
+
/** Result of applying skill body size limits. */
|
|
55
|
+
export interface TruncationResult {
|
|
56
|
+
/** The (possibly truncated) body */
|
|
57
|
+
body: string;
|
|
58
|
+
/** Whether truncation was applied */
|
|
59
|
+
truncated: boolean;
|
|
60
|
+
/** Original body size in characters */
|
|
61
|
+
originalChars: number;
|
|
62
|
+
/** Estimated original token count (chars / 4) */
|
|
63
|
+
originalTokenEstimate: number;
|
|
64
|
+
/** Final body size in characters */
|
|
65
|
+
finalChars: number;
|
|
66
|
+
/** Estimated final token count (chars / 4) */
|
|
67
|
+
finalTokenEstimate: number;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Enforce size limits on a skill body.
|
|
71
|
+
* Truncates with a clear marker if the body exceeds maxChars.
|
|
72
|
+
* Fail-soft: never throws, always returns a usable body.
|
|
73
|
+
*/
|
|
74
|
+
export declare function limitSkillBody(body: string, maxChars?: number, warnChars?: number): TruncationResult;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Default maximum skill body size in characters (~8k tokens at 4 chars/token). */
|
|
2
|
+
export const DEFAULT_SKILL_BODY_MAX_CHARS = 32_000;
|
|
3
|
+
/** Default warning threshold in characters (~2k tokens at 4 chars/token). */
|
|
4
|
+
export const DEFAULT_SKILL_BODY_WARN_CHARS = 8_000;
|
|
5
|
+
/**
|
|
6
|
+
* Resolved skill body limits from environment variables.
|
|
7
|
+
* Falls back to defaults if not set or unparsable.
|
|
8
|
+
*/
|
|
9
|
+
export function getSkillBodyLimits() {
|
|
10
|
+
const maxChars = parseInt(process.env.ZOE_SKILL_BODY_MAX_CHARS || '', 10);
|
|
11
|
+
const warnChars = parseInt(process.env.ZOE_SKILL_BODY_WARN_CHARS || '', 10);
|
|
12
|
+
return {
|
|
13
|
+
maxChars: Number.isFinite(maxChars) && maxChars > 0 ? maxChars : DEFAULT_SKILL_BODY_MAX_CHARS,
|
|
14
|
+
warnChars: Number.isFinite(warnChars) && warnChars > 0 ? warnChars : DEFAULT_SKILL_BODY_WARN_CHARS,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Enforce size limits on a skill body.
|
|
19
|
+
* Truncates with a clear marker if the body exceeds maxChars.
|
|
20
|
+
* Fail-soft: never throws, always returns a usable body.
|
|
21
|
+
*/
|
|
22
|
+
export function limitSkillBody(body, maxChars, warnChars) {
|
|
23
|
+
const limits = getSkillBodyLimits();
|
|
24
|
+
const max = maxChars ?? limits.maxChars;
|
|
25
|
+
const originalChars = body.length;
|
|
26
|
+
const originalTokenEstimate = Math.ceil(originalChars / 4);
|
|
27
|
+
if (originalChars <= max) {
|
|
28
|
+
return {
|
|
29
|
+
body,
|
|
30
|
+
truncated: false,
|
|
31
|
+
originalChars,
|
|
32
|
+
originalTokenEstimate,
|
|
33
|
+
finalChars: originalChars,
|
|
34
|
+
finalTokenEstimate: originalTokenEstimate,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const marker = `\n\n[... Skill body truncated: ${originalChars} chars total, ${max} shown. ` +
|
|
38
|
+
`Reduce skill body size or set ZOE_SKILL_BODY_MAX_CHARS to increase the limit. ...]`;
|
|
39
|
+
const truncatedBody = body.slice(0, max - marker.length) + marker;
|
|
40
|
+
const finalChars = truncatedBody.length;
|
|
41
|
+
const finalTokenEstimate = Math.ceil(finalChars / 4);
|
|
42
|
+
return {
|
|
43
|
+
body: truncatedBody,
|
|
44
|
+
truncated: true,
|
|
45
|
+
originalChars,
|
|
46
|
+
originalTokenEstimate,
|
|
47
|
+
finalChars,
|
|
48
|
+
finalTokenEstimate,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { Readability } from '@mozilla/readability';
|
|
3
|
+
import { JSDOM } from 'jsdom';
|
|
4
|
+
export const BrowserTool = {
|
|
5
|
+
name: "Web Browser",
|
|
6
|
+
risk: "safe",
|
|
7
|
+
configKeys: [],
|
|
8
|
+
definition: {
|
|
9
|
+
type: "function",
|
|
10
|
+
function: {
|
|
11
|
+
name: "read_website",
|
|
12
|
+
description: "Reads and extracts the main content from a website URL. Use this to summarize articles or get information from specific pages.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
url: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The full URL of the website to read (e.g., https://example.com/article)."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
required: ["url"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
handler: async (args, config) => {
|
|
26
|
+
let browser;
|
|
27
|
+
try {
|
|
28
|
+
browser = await chromium.launch({ headless: true });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.message.includes("Executable doesn't exist")) {
|
|
32
|
+
return "Error: Playwright browsers are not installed. Please run `npx playwright install chromium` to enable this feature.";
|
|
33
|
+
}
|
|
34
|
+
return `Error launching browser: ${error.message}`;
|
|
35
|
+
}
|
|
36
|
+
const context = await browser.newContext({
|
|
37
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
38
|
+
});
|
|
39
|
+
const page = await context.newPage();
|
|
40
|
+
try {
|
|
41
|
+
console.log(`Navigating to ${args.url}...`);
|
|
42
|
+
await page.goto(args.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
43
|
+
// Get the full HTML content
|
|
44
|
+
const html = await page.content();
|
|
45
|
+
// Use JSDOM to create a virtual DOM for Readability
|
|
46
|
+
const dom = new JSDOM(html, { url: args.url });
|
|
47
|
+
const reader = new Readability(dom.window.document);
|
|
48
|
+
const article = reader.parse();
|
|
49
|
+
if (!article) {
|
|
50
|
+
// Fallback: just return body text if Readability fails
|
|
51
|
+
const bodyText = await page.innerText('body');
|
|
52
|
+
return `Could not parse article content with Readability. Raw text content:
|
|
53
|
+
|
|
54
|
+
${bodyText.slice(0, 5000)}... (truncated)`;
|
|
55
|
+
}
|
|
56
|
+
return `Title: ${article.title}
|
|
57
|
+
|
|
58
|
+
Content:
|
|
59
|
+
${(article.textContent || "").trim()}`;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return `Error reading website: ${error.message}`;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await browser.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ToolModule } from './interface.js';
|
|
2
|
+
export declare const ShellTool: ToolModule;
|
|
3
|
+
export declare const ReadFileTool: ToolModule;
|
|
4
|
+
/** Diff-viewer payload for `write_file`. Owned by the producer (this module);
|
|
5
|
+
* the TUI parses it via `isFileWriteMetadata`. `oldContent`/`newContent` are
|
|
6
|
+
* omitted when `diffSkipped` (oversized) so we don't hold large strings. */
|
|
7
|
+
export interface FileWriteMetadata {
|
|
8
|
+
path: string;
|
|
9
|
+
isNewFile: boolean;
|
|
10
|
+
byteDelta: number;
|
|
11
|
+
oldContent?: string | null;
|
|
12
|
+
newContent?: string;
|
|
13
|
+
diffSkipped?: boolean;
|
|
14
|
+
skipReason?: string;
|
|
15
|
+
/** Index signature so this typed payload satisfies ToolResult.metadata's
|
|
16
|
+
* Record<string, unknown> without a cast at the producer site. */
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare const WriteFileTool: ToolModule;
|
|
20
|
+
export declare const DateTimeTool: ToolModule;
|