文件、剪贴板与用量
工作区文件夹中的文件
读取需要 fs.read,修改需要 fs.write。
每个路径都相对于工作区文件夹——也就是用户为工作区选择的项目文件夹——分隔符用 / 或 \ 均可;'' 或 '.' 表示文件夹本身。
// Read
const pkg = JSON.parse(await card.fs.readText('package.json')) as { name: string }
const logo = await card.fs.readBytes('public/logo.png')
const info = await card.fs.stat('src/index.ts') // { path, type, size, mtimeMs }
const { entries, truncated } = await card.fs.list('src', { recursive: true, maxEntries: 2000 })
render(pkg.name, logo.byteLength, info.size, entries.length, truncated)
// Write (atomically: a temporary file, then a rename)
await card.fs.writeText('reports/latest.md', '# Report\n', { createDirs: true })
await card.fs.mkdir('reports/archive')
await card.fs.trash('reports/old.md') // to the OS trash; there is no hard delete
// Write only if nobody changed it since you read it
const before = await card.fs.stat('TODO.md')
await card.fs.writeText('TODO.md', '- [ ] ship it\n', { ifMtimeMs: before.mtimeMs })| 方法 | 结果 |
|---|---|
stat(path) | { path, type: 'file' or 'directory', size, mtimeMs } |
list(path?, { recursive?, maxEntries? }) | { entries: { path, type, size? }[], truncated }——≤ 5 000 项;递归列出时会跳过 .git 和 node_modules,除非你列的就是它们内部 |
readText(path, { maxBytes? }) | UTF-8 文本 |
readBytes(path, { maxBytes? }) | Uint8Array |
read(path, { encoding?, maxBytes? }) | { data, encoding, size, truncated }——原始结果 |
writeText(path, text, { createDirs?, ifMtimeMs? }) | 新的 FsStat |
writeBytes(path, bytes, { createDirs?, ifMtimeMs? }) | 新的 FsStat |
mkdir(path) | FsStat |
trash(path) | 把文件或文件夹移入系统回收站(每分钟 60 次) |
watch(path, handler, { recursive? }) | 返回一个用于停止监视的函数 |
每次读写最多 10 MB。
监视:
const stop = await card.fs.watch('reports', ({ path, type }) => {
render(`${path} ${type === 'rename' ? 'was added or removed' : 'changed'}`)
}, { recursive: true })
// later
await stop()变化会以 100 毫秒去抖;每张卡片最多 20 个监视器;卡片页面被卸载时监视器随之停止(在 launch === 'resumed' 之后重新监视)。
边界。 绝对路径和任何 .. 都会被直接拒绝。之后,应用会解析目标的真实路径(对于新文件,则解析其最近的已存在文件夹),
不在工作区文件夹之内就拒绝——所以指向外部的符号链接或目录联接(junction)也没用。
这些情况都以 FS_DENIED 失败;其他文件系统问题(找不到、不是文件夹……)以 FS_ERROR 失败,ifMtimeMs 检查不通过时以 FS_ERROR 失败并带有 data.conflict。
当工作区文件夹是用户的主文件夹、某个磁盘的根目录,或包含 NeuroSquad 自己的数据文件夹时,完全没有文件访问权限——
每个 fs.* 调用都会以 FS_DENIED 失败。
受保护的路径
对以下路径的写入、mkdir 和 trash 会被拒绝(FS_DENIED),无论位于哪一层,检查的既是你给出的路径,也是任何链接背后的真实路径——
因为它们会让卡片借助 git、用户的智能体或他们的工具来运行代码:
- 名为
.git的文件夹中的任何内容(包括子模块或 worktree 的.git文件)、.gitmodules、.gitattributes; - 文件夹
.claude、.codex、.cursor、.gemini、.qwen、.opencode、.kilocode、.windsurf、.continue、.vscode、.idea、.husky、.devcontainer以及.github/workflows; - 文件
.mcp.json、CLAUDE.md、CLAUDE.local.md、AGENTS.md、GEMINI.md、QWEN.md、.cursorrules、.windsurfrules、opencode.json、opencode.jsonc、.envrc、.npmrc、.yarnrc、.yarnrc.yml、.pnpmfile.cjs。
有 fs.read 时可以读取它们。
第 1 版没有文件选择器:卡片只能处理工作区文件夹。要让卡片处理某个文件,可以让用户在设置中输入路径,或通过端口接收一个 ns:file-ref。
剪贴板
需要 clipboard.write——很适合设为可选权限。
await card.copyText('npm test -- --grep checkout')每秒最多一次,≤ 1 MB。浏览器自带的 navigator.clipboard 在卡片中不可用,而且完全无法读取剪贴板。
令牌用量和费用
需要 usage.read。
const summary = await card.usage('7d') // 'today' (default), '7d' or '30d'
const dollars = (summary.totalCostMicroUsd / 1_000_000).toFixed(2)
render(`$${dollars}${summary.partial ? ' + unpriced models' : ''}`)
for (const row of summary.rows) {
render(row.name, row.harness, row.inputTokens, row.outputTokens, row.costMicroUsd ?? 'no price')
}数字与应用的用量页面相同,针对卡片所在的工作区:period、from 和 to(按本地日期,结束日不含),以及每个智能体的
inputTokens、outputTokens、cacheReadTokens、cacheWriteTokens 和 costMicroUsd。金额以整数微美元计(1 000 000 = 1 美元)。
价格未知的模型 costMicroUsd 为 null——从不为 0——不计入 totalCostMicroUsd,并会把 partial 设为 true。