@jupyterlite/ai 0.17.0 → 0.19.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/lib/chat-commands/clear.d.ts +1 -0
- package/lib/chat-commands/index.d.ts +1 -0
- package/lib/chat-commands/skills.d.ts +2 -1
- package/lib/chat-model-handler.d.ts +4 -3
- package/lib/chat-model-handler.js +2 -1
- package/lib/chat-model.d.ts +148 -8
- package/lib/chat-model.js +368 -79
- package/lib/completion/completion-provider.d.ts +3 -1
- package/lib/completion/completion-provider.js +1 -2
- package/lib/completion/index.d.ts +1 -0
- package/lib/components/clear-button.d.ts +1 -0
- package/lib/components/clear-button.js +3 -4
- package/lib/components/completion-status.d.ts +1 -0
- package/lib/components/completion-status.js +5 -4
- package/lib/components/index.d.ts +1 -0
- package/lib/components/model-select.d.ts +1 -0
- package/lib/components/model-select.js +62 -67
- package/lib/components/save-button.d.ts +3 -2
- package/lib/components/save-button.js +4 -5
- package/lib/components/stop-button.d.ts +1 -0
- package/lib/components/stop-button.js +3 -4
- package/lib/components/tool-select.d.ts +3 -1
- package/lib/components/tool-select.js +47 -60
- package/lib/components/usage-display.d.ts +4 -2
- package/lib/components/usage-display.js +50 -61
- package/lib/diff-manager.d.ts +3 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +50 -59
- package/lib/models/settings-model.d.ts +3 -1
- package/lib/models/settings-model.js +1 -0
- package/lib/rendered-message-outputarea.d.ts +1 -0
- package/lib/tokens.d.ts +48 -597
- package/lib/tokens.js +2 -31
- package/lib/widgets/ai-settings.d.ts +3 -1
- package/lib/widgets/ai-settings.js +185 -344
- package/lib/widgets/main-area-chat.d.ts +3 -3
- package/lib/widgets/main-area-chat.js +2 -4
- package/lib/widgets/provider-config-dialog.d.ts +2 -1
- package/lib/widgets/provider-config-dialog.js +102 -167
- package/package.json +111 -258
- package/schema/settings-model.json +6 -0
- package/src/chat-commands/skills.ts +2 -2
- package/src/chat-model-handler.ts +10 -6
- package/src/chat-model.ts +488 -96
- package/src/completion/completion-provider.ts +6 -6
- package/src/components/clear-button.tsx +0 -2
- package/src/components/completion-status.tsx +2 -2
- package/src/components/model-select.tsx +1 -1
- package/src/components/save-button.tsx +3 -3
- package/src/components/stop-button.tsx +0 -2
- package/src/components/tool-select.tsx +10 -9
- package/src/components/usage-display.tsx +4 -2
- package/src/diff-manager.ts +4 -3
- package/src/index.ts +103 -107
- package/src/models/settings-model.ts +7 -6
- package/src/tokens.ts +54 -744
- package/src/widgets/ai-settings.tsx +40 -11
- package/src/widgets/main-area-chat.ts +5 -8
- package/src/widgets/provider-config-dialog.tsx +8 -8
- package/LICENSE +0 -30
- package/README.md +0 -49
- package/lib/agent.d.ts +0 -277
- package/lib/agent.js +0 -1116
- package/lib/icons.d.ts +0 -3
- package/lib/icons.js +0 -8
- package/lib/providers/built-in-providers.d.ts +0 -21
- package/lib/providers/built-in-providers.js +0 -233
- package/lib/providers/generated-context-windows.d.ts +0 -8
- package/lib/providers/generated-context-windows.js +0 -96
- package/lib/providers/model-info.d.ts +0 -3
- package/lib/providers/model-info.js +0 -58
- package/lib/providers/models.d.ts +0 -37
- package/lib/providers/models.js +0 -28
- package/lib/providers/provider-registry.d.ts +0 -49
- package/lib/providers/provider-registry.js +0 -72
- package/lib/providers/provider-tools.d.ts +0 -36
- package/lib/providers/provider-tools.js +0 -93
- package/lib/skills/index.d.ts +0 -4
- package/lib/skills/index.js +0 -7
- package/lib/skills/parse-skill.d.ts +0 -25
- package/lib/skills/parse-skill.js +0 -69
- package/lib/skills/skill-loader.d.ts +0 -25
- package/lib/skills/skill-loader.js +0 -133
- package/lib/skills/skill-registry.d.ts +0 -31
- package/lib/skills/skill-registry.js +0 -100
- package/lib/skills/types.d.ts +0 -29
- package/lib/skills/types.js +0 -5
- package/lib/tools/commands.d.ts +0 -11
- package/lib/tools/commands.js +0 -154
- package/lib/tools/skills.d.ts +0 -9
- package/lib/tools/skills.js +0 -73
- package/lib/tools/tool-registry.d.ts +0 -35
- package/lib/tools/tool-registry.js +0 -55
- package/lib/tools/web.d.ts +0 -8
- package/lib/tools/web.js +0 -196
- package/src/agent.ts +0 -1441
- package/src/icons.ts +0 -11
- package/src/providers/built-in-providers.ts +0 -241
- package/src/providers/generated-context-windows.ts +0 -102
- package/src/providers/model-info.ts +0 -88
- package/src/providers/models.ts +0 -76
- package/src/providers/provider-registry.ts +0 -88
- package/src/providers/provider-tools.ts +0 -179
- package/src/skills/index.ts +0 -14
- package/src/skills/parse-skill.ts +0 -91
- package/src/skills/skill-loader.ts +0 -175
- package/src/skills/skill-registry.ts +0 -137
- package/src/skills/types.ts +0 -37
- package/src/tools/commands.ts +0 -210
- package/src/tools/skills.ts +0 -84
- package/src/tools/tool-registry.ts +0 -63
- package/src/tools/web.ts +0 -238
- package/src/types.d.ts +0 -4
- package/style/icons/jupyternaut-lite.svg +0 -7
package/lib/tools/commands.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { Widget } from '@lumino/widgets';
|
|
2
|
-
import { tool } from 'ai';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
/**
|
|
5
|
-
* Search commands using case-insensitive term matching across command metadata.
|
|
6
|
-
*
|
|
7
|
-
* Multi-word queries are split into individual terms, and every term must be
|
|
8
|
-
* contained in at least one searchable field. Results are ranked by stronger
|
|
9
|
-
* field matches while keeping a stable fallback order.
|
|
10
|
-
*/
|
|
11
|
-
function searchCommands(commands, query) {
|
|
12
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
13
|
-
if (!normalizedQuery) {
|
|
14
|
-
return commands;
|
|
15
|
-
}
|
|
16
|
-
const terms = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
17
|
-
return commands
|
|
18
|
-
.map((command, index) => {
|
|
19
|
-
const fields = [
|
|
20
|
-
{ value: command.label, weight: 4 },
|
|
21
|
-
{ value: command.caption, weight: 3 },
|
|
22
|
-
{ value: command.id, weight: 2 },
|
|
23
|
-
{ value: command.description, weight: 1 }
|
|
24
|
-
];
|
|
25
|
-
const normalizedFields = fields.map(field => ({
|
|
26
|
-
normalizedValue: field.value?.toLowerCase() ?? '',
|
|
27
|
-
weight: field.weight
|
|
28
|
-
}));
|
|
29
|
-
const matchesAllTerms = terms.every(term => normalizedFields.some(field => field.normalizedValue.includes(term)));
|
|
30
|
-
if (!matchesAllTerms) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
const score = normalizedFields.reduce((total, field) => {
|
|
34
|
-
if (!field.normalizedValue) {
|
|
35
|
-
return total;
|
|
36
|
-
}
|
|
37
|
-
let fieldScore = 0;
|
|
38
|
-
if (field.normalizedValue.includes(normalizedQuery)) {
|
|
39
|
-
fieldScore += field.weight * 4;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
fieldScore +=
|
|
43
|
-
terms.filter(term => field.normalizedValue.includes(term)).length *
|
|
44
|
-
field.weight;
|
|
45
|
-
}
|
|
46
|
-
return total + fieldScore;
|
|
47
|
-
}, 0);
|
|
48
|
-
return { command, index, score };
|
|
49
|
-
})
|
|
50
|
-
.filter((result) => result !== null)
|
|
51
|
-
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
52
|
-
.map(result => result.command);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Create a tool to discover all available commands and their metadata
|
|
56
|
-
*/
|
|
57
|
-
export function createDiscoverCommandsTool(commands) {
|
|
58
|
-
return tool({
|
|
59
|
-
title: 'Discover Commands',
|
|
60
|
-
description: 'Discover all available JupyterLab commands with their metadata, arguments, and descriptions',
|
|
61
|
-
inputSchema: z.object({
|
|
62
|
-
query: z
|
|
63
|
-
.string()
|
|
64
|
-
.optional()
|
|
65
|
-
.nullable()
|
|
66
|
-
.describe('Optional search query to filter commands. Supports multi-word queries (whitespace-separated) by requiring each word to be contained in the command id, label, caption, or description. Leave empty to list all commands.')
|
|
67
|
-
}),
|
|
68
|
-
execute: async (input) => {
|
|
69
|
-
const { query } = input;
|
|
70
|
-
// Build the full command list first.
|
|
71
|
-
const commandIds = commands.listCommands();
|
|
72
|
-
const allCommands = [];
|
|
73
|
-
for (const id of commandIds) {
|
|
74
|
-
const description = await commands.describedBy(id);
|
|
75
|
-
const label = commands.label(id);
|
|
76
|
-
const caption = commands.caption(id);
|
|
77
|
-
const usage = commands.usage(id);
|
|
78
|
-
allCommands.push({
|
|
79
|
-
id,
|
|
80
|
-
label: label || undefined,
|
|
81
|
-
caption: caption || undefined,
|
|
82
|
-
description: usage || undefined,
|
|
83
|
-
args: description?.args || undefined
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
const commandList = query
|
|
87
|
-
? searchCommands(allCommands, query)
|
|
88
|
-
: allCommands;
|
|
89
|
-
return {
|
|
90
|
-
success: true,
|
|
91
|
-
commandCount: commandList.length,
|
|
92
|
-
commands: commandList
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Create a tool to execute a specific JupyterLab command.
|
|
99
|
-
* Commands in the settings' commandsRequiringApproval list will need approval.
|
|
100
|
-
*/
|
|
101
|
-
export function createExecuteCommandTool(commands, settingsModel) {
|
|
102
|
-
return tool({
|
|
103
|
-
title: 'Execute Command',
|
|
104
|
-
description: 'Execute a specific JupyterLab command with optional arguments',
|
|
105
|
-
inputSchema: z.object({
|
|
106
|
-
commandId: z.string().describe('The ID of the command to execute'),
|
|
107
|
-
args: z
|
|
108
|
-
.record(z.string(), z.unknown())
|
|
109
|
-
.optional()
|
|
110
|
-
.describe('Optional arguments object to pass to the command (must be an object, not a string)')
|
|
111
|
-
}),
|
|
112
|
-
needsApproval: (input) => {
|
|
113
|
-
const commandsRequiringApproval = settingsModel.config.commandsRequiringApproval || [];
|
|
114
|
-
return commandsRequiringApproval.includes(input.commandId);
|
|
115
|
-
},
|
|
116
|
-
execute: async (input) => {
|
|
117
|
-
const { commandId, args } = input;
|
|
118
|
-
// Check if command exists
|
|
119
|
-
if (!commands.hasCommand(commandId)) {
|
|
120
|
-
return {
|
|
121
|
-
success: false,
|
|
122
|
-
error: `Command '${commandId}' does not exist. Use 'discover_commands' to see available commands.`
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
// Execute the command
|
|
126
|
-
const result = await commands.execute(commandId, args);
|
|
127
|
-
// Handle actual Lumino widgets specially by extracting id and title.
|
|
128
|
-
// Avoid collapsing plain command results that happen to contain an `id` field.
|
|
129
|
-
let serializedResult;
|
|
130
|
-
if (result instanceof Widget) {
|
|
131
|
-
serializedResult = {
|
|
132
|
-
id: result.id,
|
|
133
|
-
title: result.title?.label || result.title
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// For other objects, try JSON serialization with fallback
|
|
138
|
-
try {
|
|
139
|
-
serializedResult = JSON.parse(JSON.stringify(result));
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
serializedResult = result
|
|
143
|
-
? '[Complex object - cannot serialize]'
|
|
144
|
-
: 'Command executed successfully';
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
success: true,
|
|
149
|
-
commandId,
|
|
150
|
-
result: serializedResult
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
package/lib/tools/skills.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { ISkillRegistry, ITool } from '../tokens';
|
|
2
|
-
/**
|
|
3
|
-
* Create a tool to discover available skills and their summaries.
|
|
4
|
-
*/
|
|
5
|
-
export declare function createDiscoverSkillsTool(skillRegistry: ISkillRegistry): ITool;
|
|
6
|
-
/**
|
|
7
|
-
* Create a tool to load skill instructions or a bundled resource.
|
|
8
|
-
*/
|
|
9
|
-
export declare function createLoadSkillTool(skillRegistry: ISkillRegistry): ITool;
|
package/lib/tools/skills.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
/**
|
|
4
|
-
* Create a tool to discover available skills and their summaries.
|
|
5
|
-
*/
|
|
6
|
-
export function createDiscoverSkillsTool(skillRegistry) {
|
|
7
|
-
return tool({
|
|
8
|
-
title: 'Discover Skills',
|
|
9
|
-
description: 'Discover available agent skills with their names and descriptions',
|
|
10
|
-
inputSchema: z.object({
|
|
11
|
-
query: z
|
|
12
|
-
.string()
|
|
13
|
-
.optional()
|
|
14
|
-
.nullable()
|
|
15
|
-
.describe('Optional search query to filter skills')
|
|
16
|
-
}),
|
|
17
|
-
execute: async (input) => {
|
|
18
|
-
const filtered = skillRegistry.listSkills(input.query ?? undefined);
|
|
19
|
-
return {
|
|
20
|
-
success: true,
|
|
21
|
-
skillCount: filtered.length,
|
|
22
|
-
skills: filtered
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Create a tool to load skill instructions or a bundled resource.
|
|
29
|
-
*/
|
|
30
|
-
export function createLoadSkillTool(skillRegistry) {
|
|
31
|
-
return tool({
|
|
32
|
-
title: 'Load Skill',
|
|
33
|
-
description: 'Load a skill definition or a specific resource file bundled with a skill',
|
|
34
|
-
inputSchema: z.object({
|
|
35
|
-
name: z.string().describe('The name of the skill to load'),
|
|
36
|
-
resource: z
|
|
37
|
-
.string()
|
|
38
|
-
.optional()
|
|
39
|
-
.nullable()
|
|
40
|
-
.describe('Optional resource path to load from the skill (e.g. references/REFERENCE.md)')
|
|
41
|
-
}),
|
|
42
|
-
execute: async (input) => {
|
|
43
|
-
const { name, resource } = input;
|
|
44
|
-
if (resource) {
|
|
45
|
-
const result = await skillRegistry.getSkillResource(name, resource);
|
|
46
|
-
if (result.error) {
|
|
47
|
-
return {
|
|
48
|
-
success: false,
|
|
49
|
-
...result
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
success: true,
|
|
54
|
-
...result
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const skill = skillRegistry.getSkill(name);
|
|
58
|
-
if (!skill) {
|
|
59
|
-
return {
|
|
60
|
-
success: false,
|
|
61
|
-
error: `Skill not found: ${name}`
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
success: true,
|
|
66
|
-
name: skill.name,
|
|
67
|
-
description: skill.description,
|
|
68
|
-
instructions: skill.instructions,
|
|
69
|
-
...(skill.resources.length > 0 && { resources: skill.resources })
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { ISignal } from '@lumino/signaling';
|
|
2
|
-
import { ITool, IToolRegistry, INamedTool } from '../tokens';
|
|
3
|
-
/**
|
|
4
|
-
* Implementation of the tool registry for managing AI tools
|
|
5
|
-
*/
|
|
6
|
-
export declare class ToolRegistry implements IToolRegistry {
|
|
7
|
-
/**
|
|
8
|
-
* The registered tools as a record (name -> tool mapping).
|
|
9
|
-
*/
|
|
10
|
-
get tools(): Record<string, ITool>;
|
|
11
|
-
/**
|
|
12
|
-
* The registered named tools array.
|
|
13
|
-
*/
|
|
14
|
-
get namedTools(): INamedTool[];
|
|
15
|
-
/**
|
|
16
|
-
* A signal triggered when the tools have changed.
|
|
17
|
-
*/
|
|
18
|
-
get toolsChanged(): ISignal<IToolRegistry, void>;
|
|
19
|
-
/**
|
|
20
|
-
* Add a new tool to the registry.
|
|
21
|
-
*/
|
|
22
|
-
add(name: string, tool: ITool): void;
|
|
23
|
-
/**
|
|
24
|
-
* Get a tool for a given name.
|
|
25
|
-
* Return null if the name is not provided or if there is no registered tool with the
|
|
26
|
-
* given name.
|
|
27
|
-
*/
|
|
28
|
-
get(name: string | null): ITool | null;
|
|
29
|
-
/**
|
|
30
|
-
* Remove a tool from the registry by name.
|
|
31
|
-
*/
|
|
32
|
-
remove(name: string): boolean;
|
|
33
|
-
private _tools;
|
|
34
|
-
private _toolsChanged;
|
|
35
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Signal } from '@lumino/signaling';
|
|
2
|
-
/**
|
|
3
|
-
* Implementation of the tool registry for managing AI tools
|
|
4
|
-
*/
|
|
5
|
-
export class ToolRegistry {
|
|
6
|
-
/**
|
|
7
|
-
* The registered tools as a record (name -> tool mapping).
|
|
8
|
-
*/
|
|
9
|
-
get tools() {
|
|
10
|
-
return { ...this._tools }; // Return a copy to prevent external modification
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* The registered named tools array.
|
|
14
|
-
*/
|
|
15
|
-
get namedTools() {
|
|
16
|
-
return Object.entries(this._tools).map(([name, tool]) => ({ name, tool }));
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* A signal triggered when the tools have changed.
|
|
20
|
-
*/
|
|
21
|
-
get toolsChanged() {
|
|
22
|
-
return this._toolsChanged;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Add a new tool to the registry.
|
|
26
|
-
*/
|
|
27
|
-
add(name, tool) {
|
|
28
|
-
this._tools[name] = tool;
|
|
29
|
-
this._toolsChanged.emit();
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Get a tool for a given name.
|
|
33
|
-
* Return null if the name is not provided or if there is no registered tool with the
|
|
34
|
-
* given name.
|
|
35
|
-
*/
|
|
36
|
-
get(name) {
|
|
37
|
-
if (name === null) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
return this._tools[name] || null;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Remove a tool from the registry by name.
|
|
44
|
-
*/
|
|
45
|
-
remove(name) {
|
|
46
|
-
if (name in this._tools) {
|
|
47
|
-
delete this._tools[name];
|
|
48
|
-
this._toolsChanged.emit();
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
_tools = {};
|
|
54
|
-
_toolsChanged = new Signal(this);
|
|
55
|
-
}
|
package/lib/tools/web.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { ITool } from '../tokens';
|
|
2
|
-
/**
|
|
3
|
-
* Create a browser-native URL fetch tool.
|
|
4
|
-
*
|
|
5
|
-
* This is best-effort and subject to normal browser constraints (CORS, CSP,
|
|
6
|
-
* mixed content, bot protections).
|
|
7
|
-
*/
|
|
8
|
-
export declare function createBrowserFetchTool(): ITool;
|
package/lib/tools/web.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { tool } from 'ai';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
const DEFAULT_MAX_CONTENT_CHARS = 20000;
|
|
4
|
-
const MAX_ALLOWED_CONTENT_CHARS = 100000;
|
|
5
|
-
const DEFAULT_TIMEOUT_MS = 20000;
|
|
6
|
-
const MAX_TIMEOUT_MS = 120000;
|
|
7
|
-
/**
|
|
8
|
-
* Read response body text with a character cap.
|
|
9
|
-
*
|
|
10
|
-
* Stops early once the cap is reached to avoid buffering arbitrarily large
|
|
11
|
-
* payloads in memory.
|
|
12
|
-
*/
|
|
13
|
-
async function readResponseText(response, maxContentChars) {
|
|
14
|
-
if (!response.body) {
|
|
15
|
-
const body = await response.text();
|
|
16
|
-
return {
|
|
17
|
-
content: body.slice(0, maxContentChars),
|
|
18
|
-
isTruncated: body.length > maxContentChars,
|
|
19
|
-
totalChars: body.length,
|
|
20
|
-
totalCharsExact: true
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
const reader = response.body.getReader();
|
|
24
|
-
const decoder = new TextDecoder();
|
|
25
|
-
let content = '';
|
|
26
|
-
let totalChars = 0;
|
|
27
|
-
let isTruncated = false;
|
|
28
|
-
let done = false;
|
|
29
|
-
while (!done) {
|
|
30
|
-
const readResult = await reader.read();
|
|
31
|
-
done = readResult.done;
|
|
32
|
-
if (done) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const chunk = decoder.decode(readResult.value, { stream: true });
|
|
36
|
-
if (!chunk) {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
totalChars += chunk.length;
|
|
40
|
-
if (!isTruncated) {
|
|
41
|
-
const remaining = maxContentChars - content.length;
|
|
42
|
-
if (chunk.length <= remaining) {
|
|
43
|
-
content += chunk;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
content += chunk.slice(0, remaining);
|
|
47
|
-
isTruncated = true;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (isTruncated) {
|
|
51
|
-
await reader.cancel();
|
|
52
|
-
return {
|
|
53
|
-
content,
|
|
54
|
-
isTruncated: true,
|
|
55
|
-
totalChars,
|
|
56
|
-
totalCharsExact: false
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const tail = decoder.decode();
|
|
61
|
-
if (tail) {
|
|
62
|
-
totalChars += tail.length;
|
|
63
|
-
const remaining = maxContentChars - content.length;
|
|
64
|
-
if (tail.length <= remaining) {
|
|
65
|
-
content += tail;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
content += tail.slice(0, remaining);
|
|
69
|
-
isTruncated = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
content,
|
|
74
|
-
isTruncated,
|
|
75
|
-
totalChars,
|
|
76
|
-
totalCharsExact: true
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Create a browser-native URL fetch tool.
|
|
81
|
-
*
|
|
82
|
-
* This is best-effort and subject to normal browser constraints (CORS, CSP,
|
|
83
|
-
* mixed content, bot protections).
|
|
84
|
-
*/
|
|
85
|
-
export function createBrowserFetchTool() {
|
|
86
|
-
return tool({
|
|
87
|
-
title: 'Browser Fetch',
|
|
88
|
-
description: 'Fetch a URL directly from the browser using HTTP GET for exact URL inspection when CORS/access permits.',
|
|
89
|
-
inputSchema: z.object({
|
|
90
|
-
url: z.string().describe('HTTP(S) URL to fetch'),
|
|
91
|
-
maxContentChars: z
|
|
92
|
-
.number()
|
|
93
|
-
.int()
|
|
94
|
-
.min(1)
|
|
95
|
-
.max(MAX_ALLOWED_CONTENT_CHARS)
|
|
96
|
-
.optional()
|
|
97
|
-
.describe(`Maximum number of response characters to return (default: ${DEFAULT_MAX_CONTENT_CHARS})`),
|
|
98
|
-
timeoutMs: z
|
|
99
|
-
.number()
|
|
100
|
-
.int()
|
|
101
|
-
.min(1000)
|
|
102
|
-
.max(MAX_TIMEOUT_MS)
|
|
103
|
-
.optional()
|
|
104
|
-
.describe(`Timeout in milliseconds (default: ${DEFAULT_TIMEOUT_MS}, max: ${MAX_TIMEOUT_MS})`)
|
|
105
|
-
}),
|
|
106
|
-
execute: async (input) => {
|
|
107
|
-
const maxContentChars = Math.min(input.maxContentChars ?? DEFAULT_MAX_CONTENT_CHARS, MAX_ALLOWED_CONTENT_CHARS);
|
|
108
|
-
const timeoutMs = Math.min(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
109
|
-
let parsedUrl;
|
|
110
|
-
try {
|
|
111
|
-
parsedUrl = new URL(input.url);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
return {
|
|
115
|
-
success: false,
|
|
116
|
-
errorType: 'invalid_url',
|
|
117
|
-
error: 'Invalid URL format',
|
|
118
|
-
url: input.url
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
122
|
-
return {
|
|
123
|
-
success: false,
|
|
124
|
-
errorType: 'unsupported_protocol',
|
|
125
|
-
error: 'Only http:// and https:// URLs are supported',
|
|
126
|
-
url: input.url
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
const controller = new AbortController();
|
|
130
|
-
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
131
|
-
try {
|
|
132
|
-
const response = await fetch(parsedUrl.toString(), {
|
|
133
|
-
method: 'GET',
|
|
134
|
-
credentials: 'omit',
|
|
135
|
-
redirect: 'follow',
|
|
136
|
-
signal: controller.signal,
|
|
137
|
-
headers: {
|
|
138
|
-
Accept: 'text/html,text/plain,application/json,text/markdown,*/*;q=0.8'
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
const contentType = response.headers.get('content-type') || '';
|
|
142
|
-
const contentLength = response.headers.get('content-length');
|
|
143
|
-
const body = await readResponseText(response, maxContentChars);
|
|
144
|
-
const success = response.ok;
|
|
145
|
-
return {
|
|
146
|
-
success,
|
|
147
|
-
url: response.url,
|
|
148
|
-
requestedUrl: parsedUrl.toString(),
|
|
149
|
-
status: response.status,
|
|
150
|
-
statusText: response.statusText,
|
|
151
|
-
contentType,
|
|
152
|
-
contentLength,
|
|
153
|
-
...(success
|
|
154
|
-
? {}
|
|
155
|
-
: {
|
|
156
|
-
errorType: 'http_error',
|
|
157
|
-
error: `HTTP ${response.status} ${response.statusText}`
|
|
158
|
-
}),
|
|
159
|
-
isTruncated: body.isTruncated,
|
|
160
|
-
returnedChars: body.content.length,
|
|
161
|
-
totalChars: body.totalChars,
|
|
162
|
-
totalCharsExact: body.totalCharsExact,
|
|
163
|
-
content: body.content,
|
|
164
|
-
limitations: 'Browser fetch is subject to CORS, site bot protections, and browser network policy.'
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
if (error.name === 'AbortError') {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
errorType: 'timeout',
|
|
172
|
-
error: `Request timed out after ${timeoutMs} ms`,
|
|
173
|
-
url: parsedUrl.toString()
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
success: false,
|
|
178
|
-
errorType: 'network_or_cors',
|
|
179
|
-
error: error instanceof Error && error.message
|
|
180
|
-
? error.message
|
|
181
|
-
: 'Fetch failed',
|
|
182
|
-
url: parsedUrl.toString(),
|
|
183
|
-
likelyCauses: [
|
|
184
|
-
'CORS blocked by the target website',
|
|
185
|
-
'DNS/network resolution failure',
|
|
186
|
-
'TLS/certificate issue',
|
|
187
|
-
'Target server rejected browser access'
|
|
188
|
-
]
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
finally {
|
|
192
|
-
clearTimeout(timeoutHandle);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
}
|