anyclaude-sdk 0.1.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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/agent.d.ts +110 -0
- package/dist/agent.js +897 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.js +9 -0
- package/dist/background/manager.d.ts +32 -0
- package/dist/background/manager.js +108 -0
- package/dist/background/tools.d.ts +5 -0
- package/dist/background/tools.js +98 -0
- package/dist/background/worker.d.ts +19 -0
- package/dist/background/worker.js +30 -0
- package/dist/commands/builtins.d.ts +2 -0
- package/dist/commands/builtins.js +306 -0
- package/dist/commands/index.d.ts +21 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/types.d.ts +110 -0
- package/dist/commands/types.js +5 -0
- package/dist/compact.d.ts +22 -0
- package/dist/compact.js +67 -0
- package/dist/fs/dexie.d.ts +57 -0
- package/dist/fs/dexie.js +243 -0
- package/dist/fs/index.d.ts +4 -0
- package/dist/fs/index.js +13 -0
- package/dist/fs/linuxTree.d.ts +11 -0
- package/dist/fs/linuxTree.js +43 -0
- package/dist/fs/opfs.d.ts +23 -0
- package/dist/fs/opfs.js +112 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +29 -0
- package/dist/llm/anthropic.d.ts +24 -0
- package/dist/llm/anthropic.js +280 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/inlineTools.d.ts +11 -0
- package/dist/llm/inlineTools.js +72 -0
- package/dist/llm/openai.d.ts +29 -0
- package/dist/llm/openai.js +224 -0
- package/dist/llm/responses.d.ts +18 -0
- package/dist/llm/responses.js +256 -0
- package/dist/mcp/client.d.ts +20 -0
- package/dist/mcp/client.js +156 -0
- package/dist/mcp/index.d.ts +24 -0
- package/dist/mcp/index.js +157 -0
- package/dist/mcp/proxy.d.ts +3 -0
- package/dist/mcp/proxy.js +25 -0
- package/dist/mcp/sdkServer.d.ts +21 -0
- package/dist/mcp/sdkServer.js +28 -0
- package/dist/mcp/types.d.ts +92 -0
- package/dist/mcp/types.js +5 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/render.d.ts +7 -0
- package/dist/memory/render.js +46 -0
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +79 -0
- package/dist/memory/tools.d.ts +5 -0
- package/dist/memory/tools.js +95 -0
- package/dist/memory/types.d.ts +15 -0
- package/dist/memory/types.js +4 -0
- package/dist/permissions/dangerous.d.ts +4 -0
- package/dist/permissions/dangerous.js +24 -0
- package/dist/permissions/gate.d.ts +21 -0
- package/dist/permissions/gate.js +66 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +6 -0
- package/dist/permissions/match.d.ts +19 -0
- package/dist/permissions/match.js +104 -0
- package/dist/permissions/planMode.d.ts +3 -0
- package/dist/permissions/planMode.js +33 -0
- package/dist/permissions/types.d.ts +19 -0
- package/dist/permissions/types.js +2 -0
- package/dist/persist.d.ts +15 -0
- package/dist/persist.js +58 -0
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +34 -0
- package/dist/query.d.ts +105 -0
- package/dist/query.js +115 -0
- package/dist/queue.d.ts +23 -0
- package/dist/queue.js +43 -0
- package/dist/sandbox/cloudflare.d.ts +48 -0
- package/dist/sandbox/cloudflare.js +124 -0
- package/dist/sandbox/daytona.d.ts +48 -0
- package/dist/sandbox/daytona.js +79 -0
- package/dist/sandbox/e2b.d.ts +54 -0
- package/dist/sandbox/e2b.js +87 -0
- package/dist/sandbox/index.d.ts +8 -0
- package/dist/sandbox/index.js +19 -0
- package/dist/sandbox/local.d.ts +51 -0
- package/dist/sandbox/local.js +155 -0
- package/dist/sandbox/types.d.ts +18 -0
- package/dist/sandbox/types.js +27 -0
- package/dist/sandbox/util.d.ts +15 -0
- package/dist/sandbox/util.js +100 -0
- package/dist/sandbox/vercel.d.ts +48 -0
- package/dist/sandbox/vercel.js +130 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +6 -0
- package/dist/session/store.d.ts +28 -0
- package/dist/session/store.js +122 -0
- package/dist/session/types.d.ts +22 -0
- package/dist/session/types.js +2 -0
- package/dist/settings/index.d.ts +3 -0
- package/dist/settings/index.js +3 -0
- package/dist/settings/load.d.ts +20 -0
- package/dist/settings/load.js +36 -0
- package/dist/settings/merge.d.ts +13 -0
- package/dist/settings/merge.js +65 -0
- package/dist/settings/types.d.ts +17 -0
- package/dist/settings/types.js +3 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +5 -0
- package/dist/skills/load.d.ts +23 -0
- package/dist/skills/load.js +54 -0
- package/dist/skills/parse.d.ts +7 -0
- package/dist/skills/parse.js +40 -0
- package/dist/skills/tool.d.ts +2 -0
- package/dist/skills/tool.js +39 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/skills/types.js +4 -0
- package/dist/team/dispatch.d.ts +2 -0
- package/dist/team/dispatch.js +41 -0
- package/dist/team/index.d.ts +9 -0
- package/dist/team/index.js +11 -0
- package/dist/team/mailbox.d.ts +24 -0
- package/dist/team/mailbox.js +33 -0
- package/dist/team/prompt.d.ts +1 -0
- package/dist/team/prompt.js +12 -0
- package/dist/team/runner.d.ts +20 -0
- package/dist/team/runner.js +45 -0
- package/dist/team/taskBoard.d.ts +41 -0
- package/dist/team/taskBoard.js +73 -0
- package/dist/team/tools.d.ts +7 -0
- package/dist/team/tools.js +190 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +45 -0
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.js +44 -0
- package/dist/tools/define.d.ts +18 -0
- package/dist/tools/define.js +21 -0
- package/dist/tools/delete_file.d.ts +2 -0
- package/dist/tools/delete_file.js +33 -0
- package/dist/tools/edit_file.d.ts +2 -0
- package/dist/tools/edit_file.js +93 -0
- package/dist/tools/fileTypes.d.ts +32 -0
- package/dist/tools/fileTypes.js +166 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +53 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +110 -0
- package/dist/tools/imageProcessor.d.ts +15 -0
- package/dist/tools/imageProcessor.js +83 -0
- package/dist/tools/index.d.ts +28 -0
- package/dist/tools/index.js +45 -0
- package/dist/tools/list_files.d.ts +2 -0
- package/dist/tools/list_files.js +42 -0
- package/dist/tools/multi_edit.d.ts +2 -0
- package/dist/tools/multi_edit.js +112 -0
- package/dist/tools/notebook_edit.d.ts +2 -0
- package/dist/tools/notebook_edit.js +118 -0
- package/dist/tools/plan_mode.d.ts +4 -0
- package/dist/tools/plan_mode.js +44 -0
- package/dist/tools/read_file.d.ts +2 -0
- package/dist/tools/read_file.js +193 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.js +77 -0
- package/dist/tools/todo_write.d.ts +2 -0
- package/dist/tools/todo_write.js +104 -0
- package/dist/tools/tool_search.d.ts +2 -0
- package/dist/tools/tool_search.js +49 -0
- package/dist/tools/types.d.ts +82 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/walk.d.ts +29 -0
- package/dist/tools/walk.js +82 -0
- package/dist/tools/web_fetch.d.ts +2 -0
- package/dist/tools/web_fetch.js +76 -0
- package/dist/tools/web_search.d.ts +22 -0
- package/dist/tools/web_search.js +195 -0
- package/dist/tools/write_file.d.ts +2 -0
- package/dist/tools/write_file.js +39 -0
- package/dist/types/index.d.ts +363 -0
- package/dist/types/index.js +9 -0
- package/dist/util/ids.d.ts +3 -0
- package/dist/util/ids.js +22 -0
- package/dist/util/paths.d.ts +16 -0
- package/dist/util/paths.js +72 -0
- package/dist/util/pricing.d.ts +15 -0
- package/dist/util/pricing.js +81 -0
- package/dist/workspace/index.d.ts +2 -0
- package/dist/workspace/index.js +2 -0
- package/dist/workspace/memory.d.ts +28 -0
- package/dist/workspace/memory.js +97 -0
- package/dist/workspace/webcontainer.d.ts +65 -0
- package/dist/workspace/webcontainer.js +156 -0
- package/package.json +78 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AgentStore, CommandExecutor, ContentBlockParam, FileSystem, ToolDef } from '../types/index.js';
|
|
2
|
+
/** Tunable caps for the read_file tool. */
|
|
3
|
+
export interface FileReadLimits {
|
|
4
|
+
/** Max total file size in bytes before a text read is rejected. Default 256 KB. */
|
|
5
|
+
maxSizeBytes: number;
|
|
6
|
+
/** Max output tokens (rough estimate) before a text read is rejected. Default 25000. */
|
|
7
|
+
maxTokens: number;
|
|
8
|
+
/** Max image bytes after downsampling. Default ~3.75 MB. */
|
|
9
|
+
maxImageBytes: number;
|
|
10
|
+
/** Max PDF pages returned per read. Default 20. */
|
|
11
|
+
maxPdfPages: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execution context handed to every tool. The agent loop owns the lifetime of
|
|
15
|
+
* `readFiles` (the set of paths the model has read this session) so that
|
|
16
|
+
* edit_file can enforce the "read before edit" rule.
|
|
17
|
+
*/
|
|
18
|
+
export interface ToolContext {
|
|
19
|
+
fs: FileSystem;
|
|
20
|
+
exec: CommandExecutor;
|
|
21
|
+
cwd: string;
|
|
22
|
+
readFiles: Set<string>;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** Mutable session state (e.g. todos). The agent loop owns its lifetime. */
|
|
25
|
+
store?: AgentStore;
|
|
26
|
+
/** Optional read limits; tools fall back to sensible defaults when omitted. */
|
|
27
|
+
limits?: Partial<FileReadLimits>;
|
|
28
|
+
/**
|
|
29
|
+
* Spawn a sub-agent to completion and return its final text. Provided by the
|
|
30
|
+
* agent loop when sub-agents are enabled; the `task` tool relies on it.
|
|
31
|
+
*/
|
|
32
|
+
runSubagent?: (opts: {
|
|
33
|
+
description: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
agentType?: string;
|
|
36
|
+
/** Abort signal (e.g. a background task's stop) — cancels the sub-agent. */
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
/** Streamed progress (assistant text + tool names) as the sub-agent works. */
|
|
39
|
+
onProgress?: (text: string) => void;
|
|
40
|
+
}) => Promise<{
|
|
41
|
+
text: string;
|
|
42
|
+
isError?: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/** Background task manager, present when background tasks are enabled. */
|
|
45
|
+
background?: import('../background/manager.js').BackgroundTaskManager;
|
|
46
|
+
/** Inter-agent mailbox, present when teammates are enabled. Shared with sub-agents. */
|
|
47
|
+
mailbox?: import('../team/mailbox.js').Mailbox;
|
|
48
|
+
/** Shared task board, present when teammates are enabled. Shared with sub-agents. */
|
|
49
|
+
board?: import('../team/taskBoard.js').TaskBoard;
|
|
50
|
+
/** This agent's name/label (e.g. 'coordinator' or a sub-agent type) for messaging. */
|
|
51
|
+
agentName?: string;
|
|
52
|
+
/** Index of all tools available this session (name + description), for tool_search. */
|
|
53
|
+
toolIndex?: Array<{
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** Persistent memory store, present when memory is enabled. Shared with sub-agents. */
|
|
58
|
+
memory?: import('../memory/store.js').MemoryStore;
|
|
59
|
+
/** Available skills (markdown prompt templates), for the `skill` tool. */
|
|
60
|
+
skills?: import('../skills/index.js').Skill[];
|
|
61
|
+
/** Plan-mode state; when active, mutating tools are denied. */
|
|
62
|
+
planMode?: {
|
|
63
|
+
active: boolean;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Result returned by a tool run. */
|
|
67
|
+
export interface ToolResult {
|
|
68
|
+
content: string | ContentBlockParam[];
|
|
69
|
+
isError?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** A tool pairs an OpenAI-shape function definition with its implementation. */
|
|
72
|
+
export interface Tool {
|
|
73
|
+
def: ToolDef;
|
|
74
|
+
run(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
|
|
75
|
+
/**
|
|
76
|
+
* Max result size in characters before the agent loop spills the full output
|
|
77
|
+
* to a file and replaces it with a preview + path (see large-output handling).
|
|
78
|
+
* Set to `Infinity` to opt out (e.g. read_file, which is already self-bounded).
|
|
79
|
+
* When omitted, the loop uses its global default threshold.
|
|
80
|
+
*/
|
|
81
|
+
maxResultChars?: number;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FileSystem } from '../types/index.js';
|
|
2
|
+
/** Directories that are skipped during recursive walks by default. */
|
|
3
|
+
export declare const DEFAULT_IGNORE: Set<string>;
|
|
4
|
+
/** Join two path segments with a single separator, normalizing `.`. */
|
|
5
|
+
export declare function joinPath(base: string, name: string): string;
|
|
6
|
+
export interface WalkEntry {
|
|
7
|
+
/** Path relative to the walk root (POSIX separators). */
|
|
8
|
+
path: string;
|
|
9
|
+
isDir: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface WalkOptions {
|
|
12
|
+
/** Directory names to skip. Defaults to {@link DEFAULT_IGNORE}. */
|
|
13
|
+
ignore?: Set<string>;
|
|
14
|
+
/** Hard cap on entries yielded, to bound runaway trees. */
|
|
15
|
+
maxEntries?: number;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Recursively walk a directory tree, yielding files and directories with paths
|
|
20
|
+
* relative to `root`. Directories in the ignore set are pruned. Unreadable
|
|
21
|
+
* directories are skipped silently.
|
|
22
|
+
*/
|
|
23
|
+
export declare function walk(fs: FileSystem, root: string, opts?: WalkOptions): AsyncGenerator<WalkEntry>;
|
|
24
|
+
/**
|
|
25
|
+
* Translate a glob pattern into a RegExp.
|
|
26
|
+
* Supports `**` (any depth, including zero segments), `*` (within a segment),
|
|
27
|
+
* and `?` (single non-separator char). Other regex metachars are escaped.
|
|
28
|
+
*/
|
|
29
|
+
export declare function globToRegExp(pattern: string): RegExp;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Directories that are skipped during recursive walks by default. */
|
|
2
|
+
export const DEFAULT_IGNORE = new Set(['node_modules', '.git', 'dist']);
|
|
3
|
+
/** Join two path segments with a single separator, normalizing `.`. */
|
|
4
|
+
export function joinPath(base, name) {
|
|
5
|
+
if (base === '' || base === '.')
|
|
6
|
+
return name;
|
|
7
|
+
return `${base.replace(/\/+$/, '')}/${name}`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Recursively walk a directory tree, yielding files and directories with paths
|
|
11
|
+
* relative to `root`. Directories in the ignore set are pruned. Unreadable
|
|
12
|
+
* directories are skipped silently.
|
|
13
|
+
*/
|
|
14
|
+
export async function* walk(fs, root, opts = {}) {
|
|
15
|
+
const ignore = opts.ignore ?? DEFAULT_IGNORE;
|
|
16
|
+
const max = opts.maxEntries ?? 50_000;
|
|
17
|
+
let count = 0;
|
|
18
|
+
// Iterative BFS to avoid deep recursion and keep ordering stable.
|
|
19
|
+
const queue = [''];
|
|
20
|
+
while (queue.length > 0) {
|
|
21
|
+
if (opts.signal?.aborted)
|
|
22
|
+
return;
|
|
23
|
+
const rel = queue.shift();
|
|
24
|
+
const dirPath = rel === '' ? root : joinPath(root, rel);
|
|
25
|
+
const entries = await fs.readdir(dirPath);
|
|
26
|
+
if (!entries)
|
|
27
|
+
continue;
|
|
28
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const childRel = rel === '' ? entry.name : `${rel}/${entry.name}`;
|
|
31
|
+
if (entry.isDir) {
|
|
32
|
+
if (ignore.has(entry.name))
|
|
33
|
+
continue;
|
|
34
|
+
yield { path: childRel, isDir: true };
|
|
35
|
+
queue.push(childRel);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
yield { path: childRel, isDir: false };
|
|
39
|
+
}
|
|
40
|
+
if (++count >= max)
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Translate a glob pattern into a RegExp.
|
|
47
|
+
* Supports `**` (any depth, including zero segments), `*` (within a segment),
|
|
48
|
+
* and `?` (single non-separator char). Other regex metachars are escaped.
|
|
49
|
+
*/
|
|
50
|
+
export function globToRegExp(pattern) {
|
|
51
|
+
let re = '';
|
|
52
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
53
|
+
const c = pattern[i];
|
|
54
|
+
if (c === '*') {
|
|
55
|
+
if (pattern[i + 1] === '*') {
|
|
56
|
+
// `**` — match across directory separators.
|
|
57
|
+
i++;
|
|
58
|
+
// Consume a trailing slash so `**/foo` also matches `foo`.
|
|
59
|
+
if (pattern[i + 1] === '/') {
|
|
60
|
+
i++;
|
|
61
|
+
re += '(?:.*/)?';
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
re += '.*';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
re += '[^/]*';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (c === '?') {
|
|
72
|
+
re += '[^/]';
|
|
73
|
+
}
|
|
74
|
+
else if ('\\^$.|+()[]{}'.includes(c)) {
|
|
75
|
+
re += '\\' + c;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
re += c;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return new RegExp(`^${re}$`);
|
|
82
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const DESCRIPTION = `Fetches content from a URL and returns it as clean, readable text.
|
|
2
|
+
|
|
3
|
+
- Takes a \`url\` and an optional \`prompt\` describing what to extract.
|
|
4
|
+
- Powered by the Jina Reader (https://r.jina.ai/), which renders the page and returns LLM-ready Markdown — so JavaScript-heavy pages work and there are no CORS restrictions in the browser.
|
|
5
|
+
- This tool is read-only and does not modify files.
|
|
6
|
+
- The output may be truncated if the page is very large.`;
|
|
7
|
+
/** Jina Reader endpoint. GET https://r.jina.ai/<target-url> → clean Markdown. */
|
|
8
|
+
const JINA_READER = 'https://r.jina.ai/';
|
|
9
|
+
const MAX_OUTPUT = 50_000;
|
|
10
|
+
export const webFetch = {
|
|
11
|
+
def: {
|
|
12
|
+
type: 'function',
|
|
13
|
+
function: {
|
|
14
|
+
name: 'web_fetch',
|
|
15
|
+
description: DESCRIPTION,
|
|
16
|
+
parameters: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
url: { type: 'string', description: 'Fully-formed URL to fetch.' },
|
|
20
|
+
prompt: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'What information to extract from the page (for your own focus).',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ['url'],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
async run(input, ctx) {
|
|
30
|
+
let url = String(input.url ?? '').trim();
|
|
31
|
+
if (!url)
|
|
32
|
+
return { content: 'Error: `url` is required.', isError: true };
|
|
33
|
+
if (!/^https?:\/\//i.test(url))
|
|
34
|
+
url = 'https://' + url;
|
|
35
|
+
// Route through Jina Reader: https://r.jina.ai/<target-url>.
|
|
36
|
+
const readerUrl = JINA_READER + url;
|
|
37
|
+
let res;
|
|
38
|
+
try {
|
|
39
|
+
res = await fetch(readerUrl, {
|
|
40
|
+
signal: ctx.signal,
|
|
41
|
+
redirect: 'follow',
|
|
42
|
+
headers: {
|
|
43
|
+
accept: 'text/plain, text/markdown, */*',
|
|
44
|
+
// Ask the reader to skip inline images so the text stays compact.
|
|
45
|
+
'x-retain-images': 'none',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
content: `Error fetching ${url} via Jina Reader: ${err instanceof Error ? err.message : String(err)}`,
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
return {
|
|
57
|
+
content: `Error: Jina Reader returned HTTP ${res.status} ${res.statusText} for ${url}.`,
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
let body;
|
|
62
|
+
try {
|
|
63
|
+
body = await res.text();
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return { content: `Error reading content of ${url}: ${err.message}`, isError: true };
|
|
67
|
+
}
|
|
68
|
+
let text = body.trim();
|
|
69
|
+
let note = '';
|
|
70
|
+
if (text.length > MAX_OUTPUT) {
|
|
71
|
+
text = text.slice(0, MAX_OUTPUT);
|
|
72
|
+
note = `\n\n[truncated to ${MAX_OUTPUT} characters]`;
|
|
73
|
+
}
|
|
74
|
+
return { content: `Fetched ${url} (via r.jina.ai):\n\n${text}${note}` };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Tool } from './types.js';
|
|
2
|
+
export type SearchResult = {
|
|
3
|
+
title: string;
|
|
4
|
+
url: string;
|
|
5
|
+
snippet: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const webSearch: Tool;
|
|
8
|
+
/**
|
|
9
|
+
* Parse the Jina-rendered DuckDuckGo HTML results page (Markdown) into
|
|
10
|
+
* structured results.
|
|
11
|
+
*
|
|
12
|
+
* Each result renders as a block:
|
|
13
|
+
* ## [Title](//duckduckgo.com/l/?uddg=<real-url>&rut=…)
|
|
14
|
+
* [](…redirect…)[www.example.com](…redirect…)
|
|
15
|
+
* [Snippet text with **bold** terms…](…redirect…)
|
|
16
|
+
*
|
|
17
|
+
* We split on the `##` headings, take the title + decoded real URL from the
|
|
18
|
+
* heading link, and pick the longest prose link text in the block as the
|
|
19
|
+
* snippet (the bare-domain and favicon links are skipped). Falls back to a
|
|
20
|
+
* generic link scan if the page isn't heading-structured.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseDuckDuckGoResults(markdown: string): SearchResult[];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const DESCRIPTION = `Searches the web and returns the top results (title, URL, snippet).
|
|
2
|
+
|
|
3
|
+
- Takes a \`query\` and an optional \`num_results\` (default 5).
|
|
4
|
+
- Powered by the Jina Reader over DuckDuckGo's HTML endpoint, so it works in the
|
|
5
|
+
browser without CORS issues and returns clean, parseable results.
|
|
6
|
+
- Use this to find pages; follow up with \`web_fetch\` to read a specific result.`;
|
|
7
|
+
/** Jina Reader prefix — renders a target URL to clean Markdown. */
|
|
8
|
+
const JINA_READER = 'https://r.jina.ai/';
|
|
9
|
+
/** DuckDuckGo's no-JS HTML results endpoint. */
|
|
10
|
+
const DDG_HTML = 'https://html.duckduckgo.com/html/?q=';
|
|
11
|
+
const DEFAULT_RESULTS = 5;
|
|
12
|
+
const MAX_RESULTS = 20;
|
|
13
|
+
export const webSearch = {
|
|
14
|
+
def: {
|
|
15
|
+
type: 'function',
|
|
16
|
+
function: {
|
|
17
|
+
name: 'web_search',
|
|
18
|
+
description: DESCRIPTION,
|
|
19
|
+
parameters: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
query: { type: 'string', description: 'The search query.' },
|
|
23
|
+
num_results: {
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: `How many results to return (default ${DEFAULT_RESULTS}, max ${MAX_RESULTS}).`,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ['query'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
async run(input, ctx) {
|
|
33
|
+
const query = String(input.query ?? '').trim();
|
|
34
|
+
if (!query)
|
|
35
|
+
return { content: 'Error: `query` is required.', isError: true };
|
|
36
|
+
const n = clampResults(input.num_results);
|
|
37
|
+
// Route the DuckDuckGo HTML results page through the Jina Reader.
|
|
38
|
+
const target = DDG_HTML + encodeURIComponent(query);
|
|
39
|
+
const readerUrl = JINA_READER + target;
|
|
40
|
+
let res;
|
|
41
|
+
try {
|
|
42
|
+
res = await fetch(readerUrl, {
|
|
43
|
+
signal: ctx.signal,
|
|
44
|
+
redirect: 'follow',
|
|
45
|
+
headers: { accept: 'text/plain, text/markdown, */*', 'x-retain-images': 'none' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return {
|
|
50
|
+
content: `Error searching for "${query}" via Jina/DuckDuckGo: ${err instanceof Error ? err.message : String(err)}`,
|
|
51
|
+
isError: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
return {
|
|
56
|
+
content: `Error: search backend returned HTTP ${res.status} ${res.statusText}.`,
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const markdown = await res.text().catch(() => '');
|
|
61
|
+
const results = parseDuckDuckGoResults(markdown).slice(0, n);
|
|
62
|
+
if (!results.length) {
|
|
63
|
+
return {
|
|
64
|
+
content: `No results found for "${query}".`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const rendered = results
|
|
68
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}${r.snippet ? `\n ${r.snippet}` : ''}`)
|
|
69
|
+
.join('\n\n');
|
|
70
|
+
return { content: `Search results for "${query}" (top ${results.length}):\n\n${rendered}` };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
function clampResults(raw) {
|
|
74
|
+
const v = typeof raw === 'number' ? raw : parseInt(String(raw ?? ''), 10);
|
|
75
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
76
|
+
return DEFAULT_RESULTS;
|
|
77
|
+
return Math.min(Math.floor(v), MAX_RESULTS);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse the Jina-rendered DuckDuckGo HTML results page (Markdown) into
|
|
81
|
+
* structured results.
|
|
82
|
+
*
|
|
83
|
+
* Each result renders as a block:
|
|
84
|
+
* ## [Title](//duckduckgo.com/l/?uddg=<real-url>&rut=…)
|
|
85
|
+
* [](…redirect…)[www.example.com](…redirect…)
|
|
86
|
+
* [Snippet text with **bold** terms…](…redirect…)
|
|
87
|
+
*
|
|
88
|
+
* We split on the `##` headings, take the title + decoded real URL from the
|
|
89
|
+
* heading link, and pick the longest prose link text in the block as the
|
|
90
|
+
* snippet (the bare-domain and favicon links are skipped). Falls back to a
|
|
91
|
+
* generic link scan if the page isn't heading-structured.
|
|
92
|
+
*/
|
|
93
|
+
export function parseDuckDuckGoResults(markdown) {
|
|
94
|
+
const out = [];
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
// Split into per-result blocks on the `##` result headings.
|
|
97
|
+
const blocks = markdown.split(/^\s*##\s+/m).slice(1);
|
|
98
|
+
for (const block of blocks) {
|
|
99
|
+
const head = block.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
100
|
+
if (!head)
|
|
101
|
+
continue;
|
|
102
|
+
const title = cleanText(head[1]);
|
|
103
|
+
const url = decodeDdgUrl(head[2].trim());
|
|
104
|
+
if (!title || !url || !isResultUrl(url) || seen.has(url))
|
|
105
|
+
continue;
|
|
106
|
+
// Snippet = the longest prose link text in the block (has spaces, not the
|
|
107
|
+
// title, not an image, not a bare domain/url).
|
|
108
|
+
let snippet = '';
|
|
109
|
+
const linkRe = /\[([^\]]+)\]\([^)]+\)/g;
|
|
110
|
+
let m;
|
|
111
|
+
while ((m = linkRe.exec(block)) !== null) {
|
|
112
|
+
const t = cleanText(m[1]);
|
|
113
|
+
if (!t || t === title || t.startsWith('!'))
|
|
114
|
+
continue;
|
|
115
|
+
if (!/\s/.test(t))
|
|
116
|
+
continue; // bare domain / url fragment
|
|
117
|
+
if (t.length > snippet.length)
|
|
118
|
+
snippet = t;
|
|
119
|
+
}
|
|
120
|
+
seen.add(url);
|
|
121
|
+
out.push({ title, url, snippet: snippet.slice(0, 300) });
|
|
122
|
+
}
|
|
123
|
+
if (out.length)
|
|
124
|
+
return out;
|
|
125
|
+
return parseGenericLinks(markdown, seen);
|
|
126
|
+
}
|
|
127
|
+
/** Fallback: scan every markdown link when the page isn't heading-structured. */
|
|
128
|
+
function parseGenericLinks(markdown, seen) {
|
|
129
|
+
const out = [];
|
|
130
|
+
const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
131
|
+
let m;
|
|
132
|
+
while ((m = linkRe.exec(markdown)) !== null) {
|
|
133
|
+
const title = cleanText(m[1]);
|
|
134
|
+
if (!title || title.startsWith('!'))
|
|
135
|
+
continue;
|
|
136
|
+
const url = decodeDdgUrl(m[2].trim());
|
|
137
|
+
if (!url || !isResultUrl(url) || seen.has(url))
|
|
138
|
+
continue;
|
|
139
|
+
seen.add(url);
|
|
140
|
+
out.push({ title, url, snippet: '' });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/** Decode DuckDuckGo's `/l/?uddg=` redirect wrapper to the real URL. */
|
|
145
|
+
function decodeDdgUrl(raw) {
|
|
146
|
+
let url = raw;
|
|
147
|
+
if (url.startsWith('//'))
|
|
148
|
+
url = 'https:' + url;
|
|
149
|
+
try {
|
|
150
|
+
const u = new URL(url, 'https://duckduckgo.com');
|
|
151
|
+
if (u.pathname.startsWith('/l/') || u.hostname.endsWith('duckduckgo.com')) {
|
|
152
|
+
const uddg = u.searchParams.get('uddg');
|
|
153
|
+
if (uddg)
|
|
154
|
+
return decodeURIComponent(uddg);
|
|
155
|
+
}
|
|
156
|
+
return u.toString();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return url;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Keep only http(s) result links; drop DDG nav/ad/internal anchors. */
|
|
163
|
+
function isResultUrl(url) {
|
|
164
|
+
if (!/^https?:\/\//i.test(url))
|
|
165
|
+
return false;
|
|
166
|
+
if (/duckduckgo\.com\/(?!l\/)/i.test(url))
|
|
167
|
+
return false; // nav/settings links
|
|
168
|
+
if (/(^|\.)duckduckgo\.com$/i.test(safeHost(url)) && !/\/l\//.test(url))
|
|
169
|
+
return false;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
function safeHost(url) {
|
|
173
|
+
try {
|
|
174
|
+
return new URL(url).hostname;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function extractSnippet(text) {
|
|
181
|
+
// Take the first non-empty, non-link paragraph as the snippet.
|
|
182
|
+
const lines = text.split('\n');
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
const t = cleanText(line);
|
|
185
|
+
if (t && !t.startsWith('[') && t.length > 20)
|
|
186
|
+
return t.slice(0, 300);
|
|
187
|
+
}
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
function cleanText(s) {
|
|
191
|
+
return s
|
|
192
|
+
.replace(/[*_`>#]/g, '')
|
|
193
|
+
.replace(/\s+/g, ' ')
|
|
194
|
+
.trim();
|
|
195
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const DESCRIPTION = `Writes a file to the workspace filesystem, creating parent directories as needed and overwriting any existing file.
|
|
2
|
+
|
|
3
|
+
Prefer edit_file for modifying existing files; use write_file for new files or full rewrites.`;
|
|
4
|
+
export const writeFile = {
|
|
5
|
+
def: {
|
|
6
|
+
type: 'function',
|
|
7
|
+
function: {
|
|
8
|
+
name: 'write_file',
|
|
9
|
+
description: DESCRIPTION,
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
path: { type: 'string', description: 'Path to the file to write.' },
|
|
14
|
+
content: { type: 'string', description: 'Full contents to write.' },
|
|
15
|
+
},
|
|
16
|
+
required: ['path', 'content'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
async run(input, ctx) {
|
|
21
|
+
const path = String(input.path ?? '');
|
|
22
|
+
if (!path)
|
|
23
|
+
return { content: 'Error: `path` is required.', isError: true };
|
|
24
|
+
const content = typeof input.content === 'string' ? input.content : '';
|
|
25
|
+
try {
|
|
26
|
+
await ctx.fs.writeFile(path, content);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return {
|
|
30
|
+
content: `Error writing ${path}: ${err.message}`,
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// A freshly written file counts as "read" so it can be edited next.
|
|
35
|
+
ctx.readFiles.add(path);
|
|
36
|
+
const bytes = new TextEncoder().encode(content).length;
|
|
37
|
+
return { content: `Wrote ${bytes} bytes to ${path}` };
|
|
38
|
+
},
|
|
39
|
+
};
|