npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

agent-source-locator

v0.3.2

Published

Alt+click any element in your browser and its source location is typed straight into the terminal running your AI agent.

Readme

agent-source-locator

English · 简体中文

在浏览器里 Alt+点击一个元素,它的源码位置会直接打进正在运行 AI Agent 的那个终端的输入行里。不用复制粘贴,不需要 MCP,没有工具栏。

❯ src/pages/Home.tsx:42:7 src/components/Header.tsx:10:3 ▊

点三个元素,切到终端,接着敲「把这几处都换成新的间距 token」,回车。光标本来就停在那些位置后面。

为什么不用现成的工具

  • LocatorJS 这类工具的目标是跳转到编辑器,不是喂给 Agent。而且它们的零接入模式依赖 React 的 _debugSourceReact 19 已经把它移除了,官方至今没有替代方案。
  • stagewiseDomscribeFrontman 确实是面向 AI Agent 的,但它们清一色走 MCP 或 IDE 扩展,并且都要附带一个工具栏、一个聊天框或一个常驻进程。

这个工具只做一件事,而这件事恰好是上面那些都不做的:把位置放到你终端的输入行上。

支持范围

| 构建工具 | React | Vue | 接入成本 | |---|---|---|---| | Vite | ✅ | ✅ | 一行 | | webpack | ✅ | — | 一行 | | rspack | ✅ | — | 一行 | | Next.js (Turbopack) | ✅ | — | 三处,见下文 |

React 端的注入与构建工具无关(基于 oxc-parser,不需要 Babel —— @vitejs/plugin-react-swcbuiltin:swc-loader 都能配合)。Vue 端仅支持 Vite:它的 transform 必须跑在 @vitejs/plugin-vue 之前,而其它构建工具里没有对应的东西。

安装

让 Agent 帮你装

接入方式随构建工具而异,最后还有一步必须在你自己的终端里执行——这恰好是那个 已经跑在这个终端里的 Agent 最适合干的活。把下面这段贴给 Claude Code、Codex 或任何 能执行命令的 Agent CLI:

在这个项目里接入 agent-source-locator —— 先执行 `npx agent-source-locator@latest prompt`,
然后按它打印出来的指南操作。

它打印的是 prompts/install.md:识别技术栈、安装依赖、接入 构建工具、应用不在仓库根目录时设置 pathPrefix、绑定终端、验证。指南随包一起发布, 所以它永远和正在安装的版本对得上。

如果你的 Agent 不能执行命令但能联网,就直接把原文喂给它:

按照 https://raw.githubusercontent.com/Hexi1997/agent-source-locator/main/prompts/install.md
在这个项目里接入 agent-source-locator

整个流程里唯一可能自己失败的是绑定终端这一步——Agent 继承的就是你的终端环境,所以 agent-source-locator attach 能成功的地方它就能成功,不能成功的地方它会把原因 告诉你(见支持的终端)。

或者手动装

npm i -D agent-source-locator
# pnpm add -D agent-source-locator
# yarn add -D agent-source-locator

然后按你用的构建工具选一节。所有接入都只在开发模式生效,不影响生产构建。

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' // 或 @vitejs/plugin-react-swc
import { agentSourceLocator } from 'agent-source-locator'

export default defineConfig({
  plugins: [agentSourceLocator(), react()],
})

顺序无所谓——插件是 enforce: 'pre',永远先于 React 的编译器拿到源码。Babel 版和 SWC 版都能用,因为注入完全不碰你的 JSX 编译链路。

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { agentSourceLocator } from 'agent-source-locator'

export default defineConfig({
  plugins: [agentSourceLocator(), vue()],
})

需要 Vue 3 SFC。@vue/compiler-sfc 是懒加载的,纯 React 项目不会为它付出任何代价。

构建工具报出的路径是相对它自己的 root 的,而 monorepo 里那就是子包目录。所以 apps/web/src/pages/Home.vue 这个文件产出的是:

src/pages/Home.vue:116:13

你的 Agent 从仓库根出发根本找不到它。声明一个前缀即可:

// vite.config.ts
agentSourceLocator({ pathPrefix: '@web' })
// webpack.config.js / rspack.config.js
new AgentSourceLocatorPlugin({ pathPrefix: '@web' })
// next.config.ts
withAgentSourceLocator(nextConfig, { pathPrefix: '@web' })
@web/src/pages/Home.vue:116:13

前缀是自由格式的——写成什么能让你的 Agent 找到文件,就写成什么。

只有当前缀以字母或数字结尾(也就是看起来像个目录名)时才会插入 / 分隔符。这样单独一个 @ 就能直接粘在路径前面——这一点很关键,因为 Agent CLI 会把 @some/path 当作文件引用来解析,而不是普通文本:

| pathPrefix | 结果 | 适用场景 | |---|---|---| | apps/web | apps/web/src/App.vue:12:3 | monorepo,纯路径 | | @web | @web/src/App.vue:12:3 | monorepo,作为文件引用 | | @ | @src/App.vue:12:3 | 单应用,作为文件引用 | | @/ | @/src/App.vue:12:3 | 你就是想要那个分隔符 |

端点会拒绝任何含 .. 的路径,所以 ../web 这样的前缀会让每次点击都静默地校验失败。配了这种前缀时插件会给出警告。

配置可以写在文件里而不是构建配置里,这样个人改自己的设置就不会动到团队共享的部分:

// asl.config.json —— 提交进仓库
{ "pathPrefix": "apps/web" }
// asl.config.local.json —— 加进 gitignore
{ "terminal": false }

terminal: false 会保留 overlay 和剪贴板,但永远不往终端写。此时 toast 只显示 copied to clipboard——因为那本来就是预期的全部结果,而不是什么东西失败了。

两个文件都是从项目根目录向上查找的,所以 monorepo 可以在仓库根放一份共享的 asl.config.json,而任意子包——或任意一个开发者——都能在更近的位置覆盖它。

优先级从高到低:

| 层级 | 例子 | 作用范围 | |---|---|---| | 环境变量 | ASL_TERMINAL=0 pnpm dev | 单次运行 | | asl.config.local.json | { "terminal": false } | 你自己 | | asl.config.json | { "pathPrefix": "apps/web" } | 整个项目 | | 构建配置 | agentSourceLocator({ pathPrefix: 'apps/web' }) | 整个项目 | | 内置默认值 | 终端写入开启 | — |

文件层高于构建配置是有意为之:如果写死在代码里的选项优先级更高,那么一个已提交的 vite.config.ts 就会让本地文件什么都覆盖不了——而那正是本地文件存在的全部意义。

可识别的键是 terminalpathPrefixport。写错的键会给出警告而不是被静默忽略, 文件格式损坏时会回退到默认值,而不是让 dev server 挂掉。

Next 那边,instrumentation.ts 通常只是一行 re-export,没地方传参数,所以请用配置文件 或环境变量——也可以直接调用:

import { register as start } from 'agent-source-locator/next/instrumentation'
export const register = () => start({ terminal: false })

默认两个 transform 都开着,但各自只对认识的文件生效(.jsx/.tsx.vue),所以单框架项目本来就没有额外开销。想显式指定也可以:

agentSourceLocator({ frameworks: ['react'] })  // 完全跳过 .vue
// webpack.config.js(ESM)
import { AgentSourceLocatorPlugin } from 'agent-source-locator/webpack'

export default {
  mode: 'development',
  plugins: [new AgentSourceLocatorPlugin()],
  devServer: { port: 3000 },
}
// webpack.config.cjs(CommonJS)
const { AgentSourceLocatorPlugin } = require('agent-source-locator/webpack')

module.exports = {
  mode: 'development',
  plugins: [new AgentSourceLocatorPlugin()],
}

插件会自己搞定全部三件事:把 loader 注册为 .jsx/.tsxpre 规则、把 overlay 追加到每个 entry、把 /__asl/emit 挂到 devServer 上。

两个需要注意的点:

  • mode 不能是 production —— 是的话插件会直接返回。webpack serve 默认是 development,但如果共用配置里写死了 mode: 'production',插件会静默失效。
  • 插件会读 devServer.host。如果你把服务暴露到 localhost 之外,终端注入会自动关闭,只保留剪贴板。

与 webpack 完全一致——rspack 实现了同样的 plugin 和 dev-server 契约,只是引入路径不同:

// rspack.config.js
import { AgentSourceLocatorPlugin } from 'agent-source-locator/rspack'

export default {
  mode: 'development',
  plugins: [new AgentSourceLocatorPlugin()],
  devServer: { port: 3000 },
}

需要安装 @rspack/dev-server,端点才会被挂载。

Turbopack(Next 16 起的默认构建器)不支持 webpack plugin,但支持 webpack loader。插件本来一次做完的另外两件事,只能分别接到 Next 自己的钩子上——所以是三个文件而不是一个。

1. next.config.ts —— 通过 turbopack.rules 注册 loader,并把 /__asl/* 重写到 sidecar,使端点保持同源:

import type { NextConfig } from 'next'
import { withAgentSourceLocator } from 'agent-source-locator/next'

const nextConfig: NextConfig = {
  // ...你的配置
}

export default withAgentSourceLocator(nextConfig)

2. instrumentation.ts —— 放在项目根目录,与 next.config.ts 同级;如果项目用了 src 目录,则放在 src/ 下。不要放进 app/register() 是唯一在 dev server 启动时执行 Node 代码的钩子,sidecar 就在这里启动:

export { register } from 'agent-source-locator/next/instrumentation'

如果你已经有自己的 register,从里面调用我们的:

export async function register() {
  await (await import('agent-source-locator/next/instrumentation')).register()
  // ...你自己的初始化
}

3. app/layout.tsx —— Turbopack 没有注入 script 标签的钩子,所以 overlay 要从组件树里引入。它不渲染任何内容:

import { AgentSourceLocator } from 'agent-source-locator/next/client'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>
        {children}
        <AgentSourceLocator />
      </body>
    </html>
  )
}

补充说明:

  • server component 和 client component 都能定位——loader 作用于所有 .tsx,所以服务端渲染出的标签同样带属性。
  • sidecar 监听 127.0.0.1:7091。同时跑多个 Next 项目时,用 ASL_PORT 指定一个空闲端口——next.config.tsinstrumentation.ts 都会读它,一个变量就够。
  • 这一节针对的是 Next 默认的 Turbopack。next dev --webpack 未经测试——那种情况下 loader 需要改从 next.configwebpack() 钩子注册,而不是 turbopack.rules

每个应用各自接入、各自声明前缀,不存在仓库级的统一配置:

// apps/web-vue/vite.config.ts
agentSourceLocator({ pathPrefix: 'apps/web-vue' })
// apps/web-next/next.config.ts
withAgentSourceLocator(nextConfig, { pathPrefix: 'apps/web-next' })

然后在仓库根目录 attach 一次就够:

cd <仓库根目录> && npx agent-source-locator attach

绑定关系的查找会逐级向上冒泡,所以底下每个应用都会命中这一条记录,全部写进同一个终端。在两个应用之间来回点,结果会交替累积在同一行:

❯ apps/web-vue/src/App.vue:13:5 apps/web-next/app/Counter.tsx:10:7 apps/web-vue/src/App.vue:16:5

各应用抵达终端的通道并不相同,这一点只在规划端口时才需要关心:

| 技术栈 | 通道 | 额外端口 | |---|---|---| | Vite | 挂在自己 dev server 上的 middleware | 无 | | webpack / rspack | 挂在自己 dev server 上的 middleware | 无 | | Next.js | sidecar + rewrite | 7091 |

所以任意多个 Vite/webpack 应用都能共存。但两个及以上的 Next 应用会争抢 7091。 后启动的那个仍会正常启动——它只是打印一条「端口被占用」的日志,然后在没有自己 sidecar 的情况下运行,也就是说它的点击会由第一个应用的 sidecar 处理。如果两个应用继承的是同一条仓库根 attach 记录,结果依然会落到正确的终端;但想让它们彼此独立,就给其中一个换个端口:

ASL_PORT=7092 next dev

next.config.tsinstrumentation.ts 读的是同一个变量,设一次就同时管住 rewrite 目标和 sidecar。

同一个仓库的两份 checkout(git worktree,或者直接复制的目录)各跑一套 dev 时也会撞上同样的问题。各 worktree 的 dev 端口通常是按偏移整体平移的,但 7091 不会跟着走,于是第二份会静默借用第一份的 sidecar,点击落进另一份 checkout 对应的终端。现在抢不到端口时,插件会指名道姓:

[agent-source-locator] clicks from /repo-b will be typed into the terminal
attached to /repo-a. Set ASL_PORT to a free port to keep them separate.

给每个 worktree 配一个跟它 dev 端口配套的 ASL_PORT 即可。

残留的 sidecar

sidecar 跑在 Next dev server 进程内部,所以 Ctrl+C 会连它一起带走。但关掉终端窗口、或者杀掉启动器进程时,next-server 会继续存活——Next 不会随父进程退出,而幸存下来的那个进程同时占着 dev 端口和 7091。

这种情况下 sidecar 会察觉自己已被 init 收养,并在约 30 秒内让出端口,好让下一个 dev server 起自己的:

[agent-source-locator] parent process is gone; releasing the sidecar port
so a new dev server can start its own.

dev server 本身是故意不动的——要不要杀掉它不该由这个工具决定。如果某个残留实例挡了路,Next 会直接告诉你它的 pid(Another next dev server is already running… PID: 12345),或者:

lsof -ti:7091   # 看谁占着 sidecar 端口

一个应用以 iframe 嵌入另一个

可以用,除了两个应用各自接入之外不需要任何额外配置:

  • iframe 是独立的 document,有自己的 overlay,所以在它内部的悬浮和点击都由它自己处理。父页面的 overlay 看不到 iframe 里的元素,反之亦然——不会出现双重高亮。
  • iframe 内发出的请求打到被嵌入应用自己的 originlocalhost:5599/__asl/emit,而不是父页面的),所以同源校验和 Next 的 rewrite 都照常工作。
  • 按住 Alt 时,无论焦点在哪个 document 上都能生效——overlay 是从鼠标事件里读这个修饰键的,而不是追踪键盘状态,正是因为没有焦点的 iframe 收不到任何键盘事件。

父页面这边需要加一件事,前提是你希望 iframe 内的剪贴板兜底也能用:

<iframe src="http://localhost:5599" allow="clipboard-write"></iframe>

不加的话,浏览器会拒绝跨域 iframe 的剪贴板写入(端口不同即为不同 origin)。终端注入不受影响——那条链路根本不经过剪贴板。

绑定终端

在运行 Agent 的那个终端里,进入项目目录执行:

npx agent-source-locator attach

然后在浏览器里按住 Alt/Option,点任意元素。

| 命令 | 作用 | |---|---| | agent-source-locator attach | 把当前项目指向你执行该命令的这个终端 session | | agent-source-locator status | 查看已绑定的 session 以及它是否还活着 | | agent-source-locator test | 写入一条示例位置,用来验证链路是否打通 | | agent-source-locator detach | 解除当前项目的绑定 | | agent-source-locator prompt | 打印一份给 Agent 照着做的接入指南 |

绑定关系按项目目录存放在 ~/.config/agent-source-locator/sessions.json,查找时会逐级向上冒泡——在仓库根目录 attach 一次,里面所有子应用都能用。重启 dev server 不会丢失绑定;关掉终端窗口会。

选中父级

左键点击命中的是最近的那个带位置的元素,多数时候这正是你要的——但不总是。 你能用鼠标指到的往往是叶子:卡片里的一行文字、按钮里的一个 span;而你想交给 Agent 的通常是包着它的那个组件。

Alt+右键会弹出一个候选列表,从上到下依次是该元素本身、以及所有包着它的 带位置元素:

┌────────────────────────────────────────────┐
│ <span>  src/components/Card.tsx:21:9  click│
│ <div>   src/components/Card.tsx:14:3       │
│ <li>    src/pages/Home.tsx:48:7            │
│ <ul>    src/pages/Home.tsx:44:5            │
└────────────────────────────────────────────┘

鼠标划过某一行,页面上就会高亮对应的元素——当几个候选的路径看起来差不多时, 这是唯一可靠的分辨方式。点某一行即发送该位置。标了 click 的那行就是直接 Alt+左键会发出去的那个,所以这个菜单是简单手势的超集,而不是另一套模式。

这条链一直通到 <body>,所以菜单只显示最近的 5 层——再往上基本都是没人 想指的布局容器。

按 Esc、滚动页面、或在别处点一下都会关掉它。不按 Alt 的右键完全 不受影响,浏览器自己的菜单和页面自己的菜单都照常工作。

下面「已知限制」里那条也由此得到补救:组件吞掉 props 时,点击会落到它最近的 带标记的祖先上,而菜单里会列出它上面的其余各层。

支持的终端

绑定的含义是:另一个进程要能往某个特定的 session 里打字。具备这个能力的 终端非常少,下面这张表就是由此而来——所以在怀疑哪里坏了之前,先看看你的终端在哪一栏:

| 终端 | 能否绑定 | 方式 | |---|---|---| | tmux | ✅ 内置支持 | send-keys -l,任意平台,含 WSL | | iTerm2 | ✅ 内置支持 | AppleScript write text … newline NO(macOS) | | kitty | ⚙️ 手动配置 | 有 kitty @ send-text,配成 command sink 即可 | | WezTerm | ⚙️ 手动配置 | 有 wezterm cli send-text,同理 | | VS Code / Cursor | ❌ | 不对外部进程开放接口——见下文 | | Terminal.app | ❌ | do script执行命令,无法只输入不回车 | | Warp、Ghostty、Alacritty | ❌ | 没有远程控制接口 | | Windows Terminal | ❌ | 无法定位到某个具体 session |

✅ 表示 agent-source-locator attach 会自动检测到;⚙️ 表示终端本身能做到,但需要你自己配; ❌ 表示从外部根本无法实现。

如果你的终端是 ❌

在里面跑一层 tmux,这是通用答案,只多一条命令:

tmux                          # 在 Cursor、Terminal.app、Windows Terminal…… 里都行
agent-source-locator attach   # 在 tmux 里绑定

之后所有文本都会打进那个 tmux pane——也就是你在编辑器里正看着的那个。

编辑器是这里最常见的情况。VS Code 和 Cursor 确实提供了往终端注入文本的能力,但只开放给自己的扩展,不对外部进程开放,也没有任何开关能改变这一点。

如果你的终端是 ⚙️

手动配一条 command sink,位置文本通过 stdin 传入,所以任何能触达你终端的命令都行:

// ~/.config/agent-source-locator/sessions.json
{
  "version": 1,
  "sessions": {
    "/path/to/your/project": {
      "type": "command",
      "command": ["kitty", "@", "send-text", "--match", "title:my-agent", "--stdin"],
      "attachedAt": "2026-01-01T00:00:00.000Z"
    }
  }
}

这两个是根据它们各自文档里的 CLI 写的,并未在此实测,所以具体参数请当作起点而不是定论。

绑定失败时怎么查

agent-source-locator attach 会说明它为什么绑不上——是终端本身不支持,还是单纯没继承到环境变量 (子进程里 $TERM_PROGRAM 为空是另一回事,修法也完全不同)。如果 dev 脚本只报一句 Command failed,手动跑一次 agent-source-locator attach:脚本经常把 stderr 吞掉,而解释就在那里。

以上都不影响剪贴板——无论有没有绑定,位置始终会写入剪贴板。

Windows

agent-source-locator attach 在 Windows 上找不到可绑定的目标,但其余部分都是跨平台的:注入、overlay、 端点、剪贴板都照常工作,产出的路径也会统一规范化成正斜杠。有两条路能把终端写入拿回来:

  • WSL + tmux —— tmux sink 与平台无关
  • 自定义 command sink —— 同上,位置文本通过 stdin 传入

一个注意点:残留 sidecar 的回收依赖「进程被 init 收养」这一 POSIX 语义,在 Windows 上 不生效,但也无害。

工作原理

开发模式下,你写的每个元素都会被注入 data-asl="path:line:column" 属性。React 端用 oxc-parser 拿 offset、用 magic-string 插入——只解析、不做 transform,所以与后续由谁来编译 JSX 无关。Vue 端用 @vue/compiler-sfc 做同样的事,并且跑在 @vitejs/plugin-vue 之前,这样后者缓存的 descriptor 已经是注入过的。

两边产出完全相同的属性,所以运行时不需要区分框架。

运行时 overlay 会把点击解析到最近一个带上述属性的祖先元素——所以即便你点的是第三方组件内部的 DOM,落点仍然是你自己写的那一行。Alt+右键则把这条祖先链整个列出来供你挑选。随后它把位置写入剪贴板,并 POST 给 dev server,由后者打进已绑定的终端 session。

所有逻辑都只在开发模式生效,生产构建产物里不含任何相关代码。

安全性

这个端点会往一个活着的终端里打字,所以它被当作真实攻击面对待:

  • 文本内容不由浏览器提供。 浏览器只发送 {file, line, column},服务端校验之后自行拼接字符串。
  • 所有 Unicode 控制字符一律拒绝。 payload 里的 \n 会直接提交那一行输入,而 ESC 可以驱动终端转义序列。
  • POST 拒绝,超过 4 KB 的请求体拒绝,含 .. 或超过 512 字符的路径拒绝。
  • Sink 全部走 execFile,不经过 shell。iTerm2 的脚本通过 argv 传值,绝不做字符串拼接。
  • 如果 dev server 绑定到了 localhost 之外,终端注入会自动禁用,只保留剪贴板。

Origin 校验在不同接入方式下略有差异。 Vite/webpack/rspack 下浏览器直连 dev server,所以要求 OriginHost 完全相等。Next 下请求经由 rewrite 代理,Host 会被替换成 sidecar 自己的地址,完全相等已不可能——因此 sidecar 改为要求 Origin 必须是 loopback 地址,并且只监听 127.0.0.1。真正的跨站页面依然会被拒绝;残留的缺口是另一个本地 dev server 上的页面有可能访问到它。

已知限制

  • Vue 仅支持 Vite,因为 SFC 的编译发生在 @vitejs/plugin-vue 里。
  • 点击一个「吞掉 props」(没有把 props 透传到 DOM 节点)的组件时,高亮的会是它最近的带标记祖先。这是预期中的降级行为,不是 bug——而且 Alt+右键会把它上面的各层都列出来。
  • Vue 模板里的 <template><slot> 会被跳过——它们本身不渲染成 DOM 元素,属性放上去也会消失。
  • oxc-parser 依赖原生二进制产物;没有预编译包的冷门平台无法使用。

许可

MIT