diff --git a/.changeset/stable-amp-plugin-api.md b/.changeset/stable-amp-plugin-api.md new file mode 100644 index 0000000..fa421bd --- /dev/null +++ b/.changeset/stable-amp-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@curl.md/amp': patch +--- + +Updated the Amp plugin for the stable plugin API. diff --git a/docs/plugins/amp.mdx b/docs/plugins/amp.mdx index 1ab961a..36bbc3d 100644 --- a/docs/plugins/amp.mdx +++ b/docs/plugins/amp.mdx @@ -7,7 +7,7 @@ import packageJson from '../../plugins/amp/package.json' # Amp -First-party [Amp](https://ampcode.com) plugin that routes Amp’s built-in `read_web_page` tool through curl.md and also adds a specific `curl_md` tool. +First-party [Amp](https://ampcode.com) plugin that adds a `curl_md` tool and steers URL reads through curl.md. -:::important -The Amp [Plugin API](https://ampcode.com/manual/plugin-api) is still experimental. As a result, the curl.md Amp plugin may also change without warning. -::: - ## Quick Start Install the plugin, launch Amp, and start curling. @@ -37,16 +33,12 @@ Successful installs create the Amp plugin shim in `~/.config/amp/plugins/curlmd. ### Start Amp -Start Amp with plugins enabled via the `PLUGINS=all` env var: +Start Amp: ```sh -$ PLUGINS=all amp +$ amp ``` -:::tip -Amp requires `PLUGINS=all` to be set in order to load plugins. Add this to your environment so you do not need to prefix every Amp command manually. -::: - ### Use Amp Now that Amp is running with the curl.md plugin, all you need to do is use Amp. To confirm everything is set up, try this out: @@ -56,7 +48,7 @@ Read the curl.md Amp plugin docs and summarize how it works. https://curl.md/docs/plugins/amp ``` -Amp will automatically use curl.md to turn URLs you paste, or URLs it decides to fetch on its own, into markdown. +Amp will use curl.md to turn URLs you paste, or URLs it decides to fetch on its own, into markdown. :::: @@ -79,7 +71,6 @@ $ npx @curl.md/amp@latest install --yes If you prefer to add the shim manually, create your own plugin entry in `~/.config/amp/plugins`: ```ts title="~/.config/amp/plugins/curlmd.ts" -// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now import plugin from '@curl.md/amp' export default plugin ``` @@ -113,14 +104,15 @@ For non-interactive environments, set [`CURLMD_API_KEY`](/docs/guide/cli#environ ### Tools -The plugin registers the following tools: +The plugin registers the following tool: + +| Tool | Description | +| --------- | ------------------------ | +| `curl_md` | Fetch a URL as markdown. | -| Tool | Description | -| --------------- | -------------------------------------------------------- | -| `curl_md` | Fetch a URL as markdown. | -| `read_web_page` | Overrides built-in `read_web_page` with markdown output. | +The plugin also keeps a best-effort legacy hook for `read_web_page` if Amp exposes built-in tool calls to plugins. -Both `curl_md` and the intercepted `read_web_page` tool accept the following inputs: +`curl_md` accepts the following inputs: | Input | Type | Description | | ------------ | ---------------- | ------------------------------------------------------------------------------------ | diff --git a/plugins/amp/package.json b/plugins/amp/package.json index a0d47ad..f4dd428 100644 --- a/plugins/amp/package.json +++ b/plugins/amp/package.json @@ -41,6 +41,6 @@ "curl.md": "workspace:*" }, "devDependencies": { - "@ampcode/plugin": "0.0.0-dev" + "@ampcode/plugin": "0.0.0-20260527003623-g8232386" } } diff --git a/plugins/amp/src/install.test.ts b/plugins/amp/src/install.test.ts index 7fc5c62..82d5452 100644 --- a/plugins/amp/src/install.test.ts +++ b/plugins/amp/src/install.test.ts @@ -47,13 +47,7 @@ test('installs the package and writes the global shim', async () => { }\n`) await expect(fs.readFile(path.join(tempDir, 'plugins', 'curlmd.ts'), 'utf8')).resolves.toBe( - [ - '// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now', - "import plugin from '@curl.md/amp'", - '', - 'export default plugin', - '', - ].join('\n'), + ["import plugin from '@curl.md/amp'", '', 'export default plugin', ''].join('\n'), ) }) @@ -84,15 +78,9 @@ test('runs when invoked through a symlinked bin path', async () => { expect(result.stderr).toBe('') expect(result.stdout).toContain(`Installed @curl.md/amp@`) expect(result.stdout).toContain(` to ${ampConfigDir}`) - expect(result.stdout).toContain("Run 'PLUGINS=all amp' to load plugins") + expect(result.stdout).toContain("Run 'amp' to load plugins") await expect(fs.readFile(path.join(ampConfigDir, 'plugins', 'curlmd.ts'), 'utf8')).resolves.toBe( - [ - '// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now', - "import plugin from '@curl.md/amp'", - '', - 'export default plugin', - '', - ].join('\n'), + ["import plugin from '@curl.md/amp'", '', 'export default plugin', ''].join('\n'), ) }) diff --git a/plugins/amp/src/install.ts b/plugins/amp/src/install.ts index 7696a44..44b33c7 100644 --- a/plugins/amp/src/install.ts +++ b/plugins/amp/src/install.ts @@ -35,7 +35,7 @@ if (isMain) { const result = await installAmpPlugin() console.log(`Installed ${result.packageSpec} to ${result.ampConfigDir}`) console.log(`Plugin shim: ${result.shimPath}`) - console.log("Run 'PLUGINS=all amp' to load plugins") + console.log("Run 'amp' to load plugins") console.log('If auth is needed, set `CURLMD_API_KEY` or run `curl.md auth login`.') } catch (error) { console.error(`error: ${error instanceof Error ? error.message : String(error)}`) @@ -127,13 +127,7 @@ export async function installAmpPlugin( await fs.mkdir(path.dirname(shimPath), { recursive: true }) await fs.writeFile( shimPath, - [ - '// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now', - "import plugin from '@curl.md/amp'", - '', - 'export default plugin', - '', - ].join('\n'), + ["import plugin from '@curl.md/amp'", '', 'export default plugin', ''].join('\n'), 'utf8', ) diff --git a/plugins/amp/src/plugin.test.ts b/plugins/amp/src/plugin.test.ts index 53ff7a2..2a80d27 100644 --- a/plugins/amp/src/plugin.test.ts +++ b/plugins/amp/src/plugin.test.ts @@ -34,10 +34,31 @@ afterEach(() => { test('registers tool hooks and fallback tool', () => { const { handlers, tools } = loadPlugin() - expect(handlers.map((handler) => handler.event)).toEqual(['tool.call', 'tool.result']) + expect(handlers.map((handler) => handler.event)).toEqual([ + 'agent.start', + 'tool.call', + 'tool.result', + ]) expect(tools.map((t) => t.name)).toEqual(['curl_md']) }) +test('agent.start hook steers URL reads to curl_md', () => { + const { handlers } = loadPlugin() + const handler = handlers.find((h) => h.event === 'agent.start')! + + const result = handler.fn( + { id: 'msg_1', message: 'Read https://example.com', thread: { id: 'T-test' } }, + {} as any, + ) + + expect(result).toEqual({ + message: { + content: + 'For web page or URL reads, use the curl_md tool instead of read_web_page. curl_md returns optimized markdown and supports url, objective, keywords, mode, and fresh inputs.', + }, + }) +}) + test('tool.call hook allows non-read_web_page tools', async () => { const { handlers } = loadPlugin() const handler = handlers.find((h) => h.event === 'tool.call')! @@ -202,7 +223,7 @@ test('preserves URL fragments', async () => { } as any) expect(requests[0]?.url).toContain('anchor=section') - expect((result as any).url).toBe('https://example.com/docs?q=1#section') + expect(result).toBe('# Fragment') }) // --- Anonymous fetch --- @@ -244,20 +265,7 @@ test('fetches anonymously and returns expected shape', async () => { expect(requests[0]?.url).toContain(`${defaultBaseUrl}/api/https://example.com/docs`) expect(requests[0]?.url).toContain('anchor=intro') - expect(result).toEqual({ - auth: 'anon', - cache: 'HIT', - credits_remaining: 42, - fresh: true, - keywords: ['a'], - markdown: '# Example\n\n---\n\nPowered by [curl.md](https://curl.md)', - mode: 'rush', - objective: 'test', - request_id: 'req_abc', - tokens_count: 100, - tokens_saved: 50, - url: 'https://example.com/docs#intro', - }) + expect(result).toBe('# Example\n\n---\n\nPowered by [curl.md](https://curl.md)') }) // --- API key auth --- @@ -411,8 +419,7 @@ test('retries once on session 401 with forced auth refresh', async () => { 'Bearer access-token-stale', 'Bearer access-token-fresh', ]) - expect((result as any).auth).toBe('session') - expect((result as any).markdown).toBe('# Retried') + expect(result).toBe('# Retried') Session.delete() }) diff --git a/plugins/amp/src/plugin.ts b/plugins/amp/src/plugin.ts index af53a76..baade5b 100644 --- a/plugins/amp/src/plugin.ts +++ b/plugins/amp/src/plugin.ts @@ -1,4 +1,3 @@ -// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now import type { PluginAPI } from '@ampcode/plugin' import { createClient, defaultBaseUrl } from 'curl.md' import { Auth, Session } from 'curl.md/internal' @@ -10,6 +9,15 @@ export default function (amp: PluginAPI) { const apiKey = process.env.CURLMD_API_KEY const resolver = Auth.createResolver(baseUrl, apiKey) + amp.on('agent.start', () => { + return { + message: { + content: + 'For web page or URL reads, use the curl_md tool instead of read_web_page. curl_md returns optimized markdown and supports url, objective, keywords, mode, and fresh inputs.', + }, + } + }) + amp.on('tool.call', async (event, ctx) => { if (event.tool !== 'read_web_page') return { action: 'allow' } ctx.logger.log(`curl.md intercepting read_web_page: ${String(event.input.url)}`) @@ -74,13 +82,14 @@ export default function (amp: PluginAPI) { required: ['url'], }, async execute(input) { - return fetchPage({ + const result = await fetchPage({ fresh: input.fresh as boolean | undefined, keywords: input.keywords as string[] | undefined, mode: input.mode as 'rush' | 'smart' | undefined, objective: input.objective as string | undefined, url: input.url as string, }) + return result.markdown }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55a55e0..360bc7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,8 +292,8 @@ importers: version: link:../../cli devDependencies: '@ampcode/plugin': - specifier: 0.0.0-dev - version: 0.0.0-dev + specifier: 0.0.0-20260527003623-g8232386 + version: 0.0.0-20260527003623-g8232386 plugins/claude: dependencies: @@ -338,8 +338,8 @@ importers: packages: - '@ampcode/plugin@0.0.0-dev': - resolution: {integrity: sha512-6Bd+X7RTRDRnDouF9c++Ds+/an5XCOak/iWyqcW353lAX5VNy3uOxLquRO3UHjIJsQ5SO1Ld/X89K2fz3eEnIw==} + '@ampcode/plugin@0.0.0-20260527003623-g8232386': + resolution: {integrity: sha512-APS3R/NXJzAFT1VG5S3iP90DrkhlIMf3ijrYJ2IXWwgX5fS+OL4+vuxOTvctGfCvUDUwxSZXtzU/PKY1HssRMg==} '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -6687,7 +6687,7 @@ packages: snapshots: - '@ampcode/plugin@0.0.0-dev': {} + '@ampcode/plugin@0.0.0-20260527003623-g8232386': {} '@antfu/install-pkg@1.1.0': dependencies: diff --git a/scripts/plugin.ts b/scripts/plugin.ts index 8044f19..13ee5a6 100644 --- a/scripts/plugin.ts +++ b/scripts/plugin.ts @@ -61,7 +61,6 @@ function getProviders() { AI_AGENT: process.env.AI_AGENT || 'amp', CURLMD_BASE_URL: process.env.CURLMD_BASE_URL || 'https://curl.local', NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED || '0', - PLUGINS: process.env.PLUGINS || 'all', }, } },