跳到正文
Hacker News· thekevinscott·· 10 天前AI 评分60

dirsql:用 SQL 把文件系统当数据库查询

Use Your Filesystem as a Database

AI 导读

作者发布 dirsql,一个用 SQL 查询本地文件的工具,把文件作为数据源,磁盘上增删文件时应用行数据自动更新,无需导出。它支持 SELECT * FROM './' 列出文件、按扩展名分组统计、用 --on-file 自定义解析器把 markdown 日记拆成行,并可通过 dirsql-plugin-embeddings 做向量语义检索。

正文 · AI 翻译

如果你和我一样,你会用你的代理生成大量 markdown 文件。也许它们是研究文档或每日摘要之类的东西。它们可能是沟通工具,比如写入磁盘的计划,或者你分享的日志。也许它们是代理在每个会话开始时读取的记忆文件,或者是从一个会话到下一个会话的交接笔记。

Markdown 文件是我日常工作流程的核心,是我所做一切工作的原材料。它们便携、易读且易于备份。它们有很多值得推荐的地方。

与此同时,我为自己构建了大量应用,其中大多数都需要数据库。你无法像打开文本文件那样打开数据库并阅读它。我的代理必须先对数据库进行探查,然后才能读写任何内容,我也一样(或者让我的代理替我操作)。

而且由于数据通常以 markdown 开头,每个应用都需要一个脚本将数据导入应用并保持同步。如果我在应用中删除了一行,哪份副本是正确的?然后我又需要另一个脚本来导出数据,以备我停止使用这个凭感觉编写的应用的那一天(但仍然想保留我的数据)。

我构建了一个工具,dirsql,它让你用 SQL 查询你的文件。它就像数据的 React,一种以文件为真相来源声明式地派生数据库的方式。这意味着如果你在磁盘上添加或删除文件,应用的行会自动更新。如果你停止使用该应用,也无需导出任何内容,因为你的数据就是你的文件。你和你的代理在纯文本文件上协作,但你仍然可以构建我们所熟知和喜爱的动态软件,而无需后端机制。

我一直在自己的很多个人软件中使用它,我认为它可能对你的软件也有帮助。

当你的文件系统成为数据库时,你能做什么?

我认为展示它为何有用的最佳方式是展示我如何在自己的软件中使用 dirsql。如果你更喜欢技术性介绍,也有大量文档可供参考。1

让我们开始吧。

你可以 SELECT * FROM './'

通过 dirsql 使用 SQL 可以让你在文件系统上查找文件。

下面我将同时提供 dirsql 命令和原生等效命令。对于基本命令,原生工具更易读。越复杂,SQL 就越能体现其价值。

从基本列表开始:

dirsql

uvx dirsql "SELECT * FROM './'"

原生

ls

输出

path             basename         dir  ext   size   mtime       ctime
---------------  ---------------  ---  ----  -----  ----------  ----------
AGENTS.md        AGENTS.md             md    3941   1791240287  1791281924
ARCHITECTURE.md  ARCHITECTURE.md       md    23080  1791199872  1791281924
CHANGELOG.md     CHANGELOG.md          md    67821  1784394668  1791281924
Cargo.lock       Cargo.lock            lock  91276  1791224560  1791281924

你也可以轻松地用 ./** 递归:

dirsql

uvx dirsql "
SELECT * FROM './**'"

原生

find . -type f -not -path '*/.*'

输出

path                           basename         dir                  ext  size   mtime       ctime
-----------------------------  ---------------  -------------------  ---  -----  ----------  ----------
AGENTS.md                      AGENTS.md                             md   3941   1791240287  1791281924
ARCHITECTURE.md                ARCHITECTURE.md                       md   23080  1791199872  1791281924
…
agents/environments/local.md   local.md         agents/environments  md   7450   1789758352  1791281924
agents/environments/remote.md  remote.md        agents/environments  md   5037   1791224560  1791281924
…

在单个 SQL 命令中重新排序、限制和过滤:

dirsql

uvx dirsql "
SELECT path FROM './**'
WHERE ext = 'md'
ORDER BY mtime DESC
LIMIT 5"

原生

ls -t $(find . -name '*.md' -not -path '*/.*') | head -5

输出

path
-----------------------------------
PARITY.md
README.md
agents/reference/pr-monitor.md
agents/reference/session-handoff.md
docs/reference/cli.md

选择列并计算新列:

dirsql

uvx dirsql "
SELECT path, size / 1024 AS kb,
       date(mtime, 'unixepoch') AS day
FROM './**'
WHERE ext = 'md'
ORDER BY size DESC
LIMIT 5"

原生

find . -name '*.md' -not -path '*/.*' \
  -printf '%s %TF %P\n' \
  | sort -rn \
  | head -5 \
  | awk '{ print $3, int($1 / 1024), $2 }'

输出

path                                     kb  day
---------------------------------------  --  ----------
MIGRATIONS.md                            74  2026-09-02
CHANGELOG.md                             66  2026-07-18
PARITY.md                                49  2026-10-06
notes/recall-skill-review-2026-09-25.md  35  2026-09-28
agents/reference/testing-gates.md        32  2026-10-05

分组和计数:

dirsql

uvx dirsql "
SELECT ext,
       count(*) AS files,
       sum(size) / 1024 AS kb
FROM './**'
GROUP BY ext
ORDER BY files DESC
LIMIT 5"

原生

find . -type f -not -path '*/.*' \
  -printf '%s %f\n' \
  | awk '{
      n = split($2, part, ".")
      ext = n > 1 ? part[n] : "NULL"
      files[ext]++
      kb[ext] += $1
    }
    END {
      for (e in files)
        print e, files[e], int(kb[e] / 1024)
    }' \
  | sort -k2rn \
  | head -5

输出

ext   files  kb
----  -----  ----
md    367    907
py    340    669
json  188    40
rs    104    1539
ts    77     259

你可以将多个查询拼接成一个列表:

dirsql

uvx dirsql "
SELECT 'files' AS stat, count(*) AS value
  FROM './**'
UNION ALL
SELECT 'markdown', count(*)
  FROM './**' WHERE ext = 'md'
UNION ALL
SELECT 'kb', sum(size) / 1024
  FROM './**'
UNION ALL
SELECT 'newest', date(max(mtime), 'unixepoch')
  FROM './**'"

原生

find . -type f -not -path '*/.*' \
  -printf '%s\t%T@\t%P\n' \
  | awk -F'\t' '{
      files++; kb += $1
      if ($3 ~ /\.md$/) md++
      if ($2 > newest) newest = $2
    }
    END {
      print "files", files
      print "markdown", md
      print "kb", int(kb / 1024)
      print "newest", strftime("%Y-%m-%d", newest, 1)
    }'

输出

stat      value
--------  ----------
files     1106
markdown  367
kb        3728
newest    2026-10-06

SQL 真正出彩的地方在于组合规则或对列进行过滤,这使代理的行为更易读、更精确(同时消耗的 token 也更少!)

一个具体例子:我运行一个每日“档案生成”脚本。SQL 让我能精确控制传递给代理的文件列表。否则代理会自己用十几个非确定性的工具调用四处探查,这既消耗 token 又浪费时间2:

uvx dirsql "
SELECT 1 AS rank, 'standing' AS kind, path, date(mtime, 'unixepoch') AS modified
FROM './planner/background/*.md'
UNION ALL
SELECT 2, 'daily', path, date(mtime, 'unixepoch')
FROM './journal/*.md'
WHERE mtime > unixepoch('now', '-3 days')
UNION ALL
SELECT 3, 'recent', path, date(mtime, 'unixepoch')
FROM './**'
WHERE ext = 'md'
  AND mtime > unixepoch('now', '-7 days')
  AND path NOT LIKE 'journal/%'
  AND path NOT LIKE 'planner/background/%'
ORDER BY rank, modified DESC"

输出

rank  kind      path                                       modified
----  --------  -----------------------------------------  ----------
1     standing  planner/background/projects.md             2026-09-10
1     standing  planner/background/profile.md              2026-08-13
2     daily     journal/2026-10-01.md                      2026-10-01
2     daily     journal/2026-09-30.md                      2026-09-30
2     daily     journal/2026-09-29.md                      2026-09-30
3     recent    planner/dockets/2026-09-30.md              2026-09-30
3     recent    projects/dirsql/notes/launch-checklist.md  2026-09-29
3     recent    projects/blog/drafts/dirsql-outline.md     2026-09-28
3     recent    reading/sqlite-virtual-tables.md           2026-09-26

你的文本文件可以成为你的数据库

我特别喜欢这个用例。

dirsql 可以将你的文件内容转化为数据库。

假设你写日记、实验记录,或者某种按日期划分条目的文件:

# Journal of my time in the Antarctic

## 2026-09-22

Dang, saw some really cool Penguins today

## 2026-09-25

More penguins! How many penguins can one man see?

## 2026-09-26

Saw a walrus today.

## 2026-09-27

An albatross circled the ship for an hour.

## 2026-09-28

Ran out of coffee at the station. Morale is low.

能先看到最新的条目当然很好!让我们这样做:

uvx dirsql \
"SELECT date, content FROM './journal.md' ORDER BY date DESC" \
  --on-file 'python3 parse.py'

这里有一个新标志,--on-file。它指定一个将文件内容转化为行的命令。在 Python 中,parse.py 可能看起来像:

import json, sys

for path in sys.argv[1:]:
    for entry in open(path).read().split("\n## ")[1:]:
        date, content = entry.split("\n", 1)
        print(json.dumps({"date": date, "content": content.strip()}))

但这不一定非得是 Python,你可以使用系统支持的任何语言来进行解析:

uvx dirsql "SELECT date, content FROM './journal.md'
ORDER BY date DESC" \
  --on-file "jq -Rsc '
    [splits(\"\n## \")][1:]
    | map(split(\"\n\n\"))
    | map({date: .[0], content: (.[1] | rtrimstr(\"\n\"))})
    | .[]
  '"

其输出为:

输出

date        content
----------  -------------------------------------------------
2026-09-28  Ran out of coffee at the station. Morale is low.
2026-09-27  An albatross circled the ship for an hour.
2026-09-26  Saw a walrus today.
2026-09-25  More penguins! How many penguins can one man see?
2026-09-22  Dang, saw some really cool Penguins today

当然,我们也可以使用原生 SQL。这个 notebook 确实经常提到企鹅;哪些条目没有提到企鹅呢?

uvx dirsql "SELECT date, content FROM './journal.md'
WHERE content NOT LIKE '%penguin%'" \
  --on-file 'python3 parse.py'

输出

date        content
----------  -------------------------------------------------
2026-09-26  Saw a walrus today.
2026-09-27  An albatross circled the ship for an hour.
2026-09-28  Ran out of coffee at the station. Morale is low.

你可以按语义搜索

因为我们使用的是 SQL(具体来说是 SQLite),所以我们可以访问整个 SQLite 生态系统。一件很酷的事情是我们可以使用向量嵌入。

向量嵌入是一种通过语义来表示文本的方式。你不再需要按关键词搜索(给我所有提到 penguins 的条目),而是可以按意图搜索(给我所有关于 polar birds 的条目)。

Penguin, puffin and duck cluster together, walrus and seal in another cluster, SQL and database in a third. The query “polar birds” lands among the birds, nearest to penguin, emperor penguin and puffin, despite sharing no words with them.Penguin, puffin and duck cluster together, walrus and seal in another cluster, SQL and database in a third. The query “polar birds” lands among the birds, nearest to penguin, emperor penguin and puffin, despite sharing no words with them.

为了支持向量嵌入,我们需要包含一个插件(顺便说一句,dirsql 支持插件 3)。

让我们复用上面的日志。哪些条目讨论了 polar birds?

uvx --with dirsql-plugin-embeddings dirsql "
SELECT
  round(vec_distance_cosine(
    embed(content),
    embed('polar birds')
  ), 3) AS distance,
  date,
  content
FROM './journal.md'
ORDER BY distance" --on-file 'python3 parse.py'

输出

distance  date        content
--------  ----------  -------------------------------------------------
0.637     2026-09-25  More penguins! How many penguins can one man see?
0.766     2026-09-22  Dang, saw some really cool Penguins today
0.789     2026-09-27  An albatross circled the ship for an hour.
0.821     2026-09-26  Saw a walrus today.
1.031     2026-09-28  Ran out of coffee at the station. Morale is low.

这是一个刻意构造的例子,所以让我们用更具体的例子。

arXiv 托管了大量学术论文,我有一个每晚运行的脚本,会把所有“机器学习”论文摘要拉取到磁盘上。使用 dirsql,我可以按语义搜索所有论文,这让找到相关且有趣的论文变得容易得多。

例如,我们可以搜索如何将自拍变成 3D 人脸:

uvx --with dirsql-plugin-embeddings dirsql "
SELECT
  round(vec_distance_cosine(
    embed(content ->> 'abstract'),
    embed('turning a selfie into a 3D face')
  ), 3) AS distance,
  substr(content ->> 'title', 1, 58) AS title
FROM './papers/*/metadata.json'
ORDER BY distance
LIMIT 5"

输出

distance  title
--------  ----------------------------------------------------------
0.358     FaceGPT: Self-supervised Learning to Chat about 3D Human F
0.413     SARS: A Novel Face and Body Shape and Appearance Aware 3D 
0.416     3DRealHead: Few-Shot Detailed Head Avatar
0.436     Generative Landmarks Guided Eyeglasses Removal 3D Face Rec
0.439     Generative Face Parsing Map Guided 3D Face Reconstruction 

结果中没有一个包含 selfie。按关键词搜索会全部跳过它们。

你可以查询你的 Claude Code 对话记录

我每天都会使用的一个东西是我自己的个人对话记录数据库,它基本上不需要任何摄取管道就能免费获得。

Claude Code 将所有对话记录存储在一个本地目录 ~/.claude 中。让我们从中查询,看看它长什么样:

uvx dirsql "
SELECT * FROM '~/.claude/projects/*/*.jsonl'
ORDER BY mtime DESC
LIMIT 5"

输出

path                                                           basename                                    dir                                                   ext    size     mtime       ctime
-------------------------------------------------------------  ------------------------------------------  ----------------------------------------------------  -----  -------  ----------  ----------
/home/you/.claude/projects/-home-you-code-recipe-app/3f9c2a7…  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13.jsonl  /home/you/.claude/projects/-home-you-code-recipe-app  jsonl  569836   1790958581  1790958384
/home/you/.claude/projects/-home-you-code-recipe-app/a81d4e0…  a81d4e09-2c7f-4b3e-8f15-6e9a3c7b2d40.jsonl  /home/you/.claude/projects/-home-you-code-recipe-app  jsonl  5840861  1790958557  1790937682
/home/you/.claude/projects/-home-you-notes/c2e7b5f3-91a4-4f8…  c2e7b5f3-91a4-4f8d-b026-3d8e1f6a9c57.jsonl  /home/you/.claude/projects/-home-you-notes            jsonl  745711   1790958488  1790898538
/home/you/.claude/projects/-home-you-code-budget-cli/7b40f8d…  7b40f8d2-5e3c-4a91-8d7e-0f2c6b9e4a18.jsonl  /home/you/.claude/projects/-home-you-code-budget-cli  jsonl  3368883  1790958469  1790865520
/home/you/.claude/projects/-home-you-notes/e95a1c3d-4f07-4b6…  e95a1c3d-4f07-4b6e-a2d9-8c1f5e7b3a62.jsonl  /home/you/.claude/projects/-home-you-notes            jsonl  716330   1790958384  1790953793

Claude 按项目存储这些记录,并带有会话 ID。它还会将记忆和子代理对话记录存储在嵌套文件夹中。让我们调整查询来展示这一点:

uvx dirsql "
SELECT
  CASE
    WHEN path LIKE '%/memory/%' THEN 'memory'
    WHEN path LIKE '%/subagents/%' THEN 'subagent transcript'
    ELSE 'session transcript'
  END AS kind,
  count(*) AS files
FROM '~/.claude/projects/**'
WHERE ext = 'jsonl' OR path LIKE '%/memory/%'
GROUP BY kind
ORDER BY files DESC"

输出

kind                 files
-------------------  -----
subagent transcript  1342
memory               490
session transcript   405

jsonl 文件将对话的每一轮存储为一行 JSON。让我们添加一个解析器来将它们拆开:

uvx dirsql "
SELECT
  cwd AS project,
  sessionId AS session,
  type AS role,
  CASE json_type(message, '$.content')
    WHEN 'text' THEN message ->> '$.content'
    ELSE message ->> '$.content[0].text'
  END AS text,
  timestamp
FROM '~/.claude/projects/*/*.jsonl'
WHERE text IS NOT NULL
ORDER BY timestamp DESC
LIMIT 5" --on-file 'cat'

输出

project                    session                               role       text                                                       timestamp
-------------------------  ------------------------------------  ---------  ---------------------------------------------------------  ------------------------
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  assistant  Let me look at the save handler.                           2026-10-02T16:29:36.214Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  user       Why does the ingredient list reorder itself on save?       2026-10-02T16:29:30.871Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  assistant  Done: the recipe page has an Export shopping list button.  2026-10-02T16:28:40.502Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  assistant  I'll add an export button below the ingredient list.       2026-10-02T16:27:20.093Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  user       Add a shopping-list export to the recipe page              2026-10-02T16:27:12.647Z

理想情况下,我们会有一个规范化的布局,包含 messages 和 sessions 表。我们可以做到!我们需要一个配置文件:

[[table]]
name    = "sessions"
glob    = "*/*.jsonl"
ddl     = "CREATE TABLE sessions (session TEXT, project TEXT)"
on-file = '''
jq -c -s '
  map(select(.sessionId))
  | group_by(.sessionId)
  | map({
      session: .[0].sessionId,
      project: (map(.cwd | values) | first)
    })
  | .[]
'
'''

[[table]]
name    = "messages"
glob    = "*/*.jsonl"
ddl     = "CREATE TABLE messages (session TEXT, role TEXT, text TEXT, timestamp TEXT)"
on-file = '''
jq -c '
  {
    session: .sessionId,
    role: .type,
    text: (.message.content | if type == "string" then . else .[0]?.text end),
    timestamp
  }
  | select(.text)
'
'''

所以现在我们的查询变成了:

cd ~/.claude/projects
uvx dirsql "
SELECT s.project, m.session, m.role, m.text, m.timestamp
FROM messages m
JOIN sessions s ON s.session = m.session
ORDER BY m.timestamp DESC
LIMIT 5" -c ./.dirsql.toml

输出

project                    session                               role       text                                                       timestamp
-------------------------  ------------------------------------  ---------  ---------------------------------------------------------  ------------------------
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  assistant  Let me look at the save handler.                           2026-10-02T16:29:36.214Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  user       Why does the ingredient list reorder itself on save?       2026-10-02T16:29:30.871Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  assistant  Done: the recipe page has an Export shopping list button.  2026-10-02T16:28:40.502Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  assistant  I'll add an export button below the ingredient list.       2026-10-02T16:27:20.093Z
/home/you/code/recipe-app  3f9c2a71-8e4b-4d2a-9c61-7b5e0d2f8a13  user       Add a shopping-list export to the recipe page              2026-10-02T16:27:12.647Z

你也可以将 dirsql 作为服务器运行:

cd ~/.claude/projects
uvx dirsql server -c ./.dirsql.toml
> Running at localhost:7117

然后你可以对它运行 POST 请求。你可以让你的代理在此基础上构建一个前端,让你可以选择、重新排序、按语义搜索;随你所愿。dirsql 不提供前端 4,但它免费为你提供后端!

你的文件保持不变

归根结底,我对锁定深恶痛绝。

而这确实是我过去几年的生活趋势,尽可能避免我的数据被锁定。我从 Gmail 迁移到了自己的电子邮件域名,从 Evernote 迁移到了 Obsidian。我不喜欢我的数据被困在围墙花园里,这既适用于我自己的软件,也适用于别人的软件。

使用 dirsql,我的文件仍然属于我。我的日志仍然是 markdown 文件,arXiv 论文仍然是磁盘上的 JSON,Claude 对话记录仍然是我 ~/.claude 文件夹中的 JSONL,而代理们继续工作,全然不知上面有一个只读层通过 SQL 将它们暴露出来。而当我不可避免地决定扔掉我周末随手 vibe-code 出来的应用时,这不会影响我的数据,因为没有什么需要导出的。

在文档中查看 dirsql,或者直接在一个目录中尝试运行:

uvx dirsql "SELECT * FROM './'"

或者

npx -y dirsql "SELECT * FROM './'"

如果你使用它并且它对你有帮助(或者没有帮助!),请告诉我。

祝你 SQL 愉快!


  1. dirsql 可在 PyPI、npm 和 Cargo 上获取,并且可以通过 uvx 或 npx 免安装使用;入门说明在这里。 ↩︎

  2. 当我在最近的一次 AI Tinkerers 活动上展示dirsql时,有人问把dirsql交给一个 agent 是否会改变它的行为。我还没有一个好的答案。Agent 很少会自己主动去用它。我得提示它们。但一旦通过 skill 调用它,它们就用得很好。 ↩︎

  3. 这个插件会自动引入一个向量嵌入模型。你也可以使用不同的模型。 ↩︎

  4. 也许应该这样?其实我喜欢这个想法,所以我把它作为一个 issue 提交了。 ↩︎

来源:Hacker News · thekevinscott.com