AI CLI 与官方 skill
无窗口运行
ducklocal --help 列出九个子命令:
| 命令 | 作用 |
|---|---|
ducklocal query | 执行一条 SQL,把结果输出为 JSON |
ducklocal schema | 列出有哪些东西可查、各有哪些列;--stats 给出单个关系的逐列统计 |
ducklocal open | 把 SQL、仪表盘或数据显示到运行中的窗口里;--state 读回窗口当前的内容 |
ducklocal check | 校验 .dash 仪表盘 |
ducklocal comments | 读取并回复别人在仪表盘上留下的审阅评论 |
ducklocal propose | 提议一处仪表盘修改,由人审阅后应用 |
ducklocal project | 检查可移动的分析文件夹及其依赖;relink 修正移动后失效的路径 |
ducklocal session | 读回一次分析是怎么一步步得出结论的 |
ducklocal presence | 显示谁正在处理已打开的仪表盘,以及接下来要改哪些图表 |
ducklocal lsp(编辑器为 .dash 文件启动的 language server)和 ducklocal profile(schema --stats 的旧写法)仍然可用,只是不列在 --help 里。
propose、project、session 三个子命令,以及 --restricted、--wait、--if-revision 选项,都是 0.4.0 新增的,详见 Agent 工作流。
ducklocal query 执行 SQL 时,不会初始化 GUI、历史记录、已注册文件、语言设置,也不会创建 GUI 的全局连接。每次调用都会新建连接。若不带子命令(或仅传入文件、目录、glob 参数),则正常启动 GUI;若要在 GUI 中打开与子命令同名的路径(query、schema、open、check、comments、propose、project、session、presence、profile、lsp),请在前面加上 ./,例如 ./query。
获取命令
DuckLocal 支持 macOS 12 及以上(Apple 芯片)、Windows(x86_64)和 Linux(x86_64)。CLI 就是应用本身的可执行文件,不需要另装 DuckDB CLI。
把命令加入 PATH,最快的办法是在应用里操作(0.1.1 及以上):打开 命令行与 AI…(标题栏的机器人按钮,或 DuckLocal 菜单),点击 安装命令。同一个对话框还能为 agent(AI 助手)安装 skill,见安装官方 skill。不安装的话,也可以按所在路径直接调用可执行文件:
# macOS
/Applications/DuckLocal.app/Contents/MacOS/ducklocal --help
# Linux:tar 包解压到的位置
./ducklocal-linux-x86_64/ducklocal --help
# Windows(PowerShell):zip 解压到的位置
.\ducklocal-windows-x86_64\ducklocal.exe --help从源码构建:在 DuckLocal 源码仓库运行 cargo build --locked,得到 ./target/debug/ducklocal。以下示例假定 ducklocal 已在 PATH 中。旧版可能没有 query,先查看 --help。JSON 和 Parquet 支持已编入二进制,查询时不会再下载。
查询选项
ducklocal --help
ducklocal --version
ducklocal query --help
ducklocal query --sql "SELECT 1 AS n"
ducklocal query --sql "SELECT 1 AS n" --format md
ducklocal query --sql-file analysis.sql --limit 100
ducklocal query --sql-file - < analysis.sql
ducklocal query --database warehouse.duckdb --sql "SHOW TABLES"- 必须且只能提供
--sql SQL或--sql-file FILE。文件必须为 UTF-8;-从 stdin 读取。SQL 只能包含一条语句,DuckDB 解析器会在打开目标数据库、执行任何操作之前检查这一点;注释、引号里的分号、末尾的分号都没问题。多语句脚本直接拒绝,不会只执行其中一部分。 --database PATH默认以只读打开已有文件;省略则用新的内存数据库。--read-write必须和--database一起用,明确允许写入和新建数据库(但不会创建父目录)。数据库出错时不会退回内存数据库。DuckDB 打不开某个文件时,错误信息开头是 DuckLocal 从文件头读出的原因(由更新的 DuckDB 写入、v0.9 时代的文件、SQLite 文件,或者根本不是数据库)以及该怎么办(更新版本写的文件,给出在那个版本里执行的ATTACH … (STORAGE_VERSION)/COPY FROM DATABASE复制语句;SQLite 文件,给出读取它的ATTACH),后面才是 DuckDB 自己的报错。--limit N为正整数,默认 1000,限制返回行数;另有 2,000,000 个单元格的预算。达到输出上限后,CLI 仍会继续统计最多额外 100,000 行,不再编码这些行的值,并报告total_rows与total_rows_exact。这限制的是输出量,不是计算量或内存;排序、聚合仍可能处理全部输入。输出上限之后遇到的执行错误仍会使命令失败。--format json(默认)输出下文的 JSON 契约;--format md把同一份结果输出为 Markdown 表格,方便放进文档或交给 agent 阅读。其他一概不变:同样的 SQL、同样的限制、同样的错误。- 重复/未知参数、缺失值、空 SQL、非法 limit 均报错。参数和值要分开写,不支持
--flag=value。相对路径以进程的工作目录为准,而不是 SQL 文件所在的目录。shell 里的路径要加引号;SQL 里的路径含单引号时写两个('O''Brien.csv');SQL 标识符用双引号。
数据库只读,不等于文件系统或网络沙箱。 即使数据库只读,COPY 这类 SQL 照样能写文件。输出路径、覆盖文件、修改数据库、安装扩展、访问外部资源,都要在执行前确认已获授权。DuckLocal 不会自动安装扩展,也不会自动重试;获得授权后仍可显式执行 INSTALL/LOAD,已安装的扩展可能自动加载。命令失败时也可能已经产生副作用(例如输出出错之前已经写入),重试前先检查目标位置。
Markdown 输出
--format md 只是换一种呈现方式,不是另一份契约:值与 JSON 里的完全相同,写法与结果表格里的显示一致。
| 值 | 输出为 |
|---|---|
NULL | NULL |
| 整数、DECIMAL、浮点 | 值本身的全部数字,一位不差:DECIMAL(38,10) 保留刻度,NaN/inf/-inf 原样写出 |
DATE | YYYY-MM-DD |
TIMESTAMP | YYYY-MM-DD HH:MM:SS[.fffffffff],只在确有小数时带小数,有几位显示几位。不附加时区;原始计数在 --format json 里 |
TIME | HH:MM:SS[.ffffff] |
INTERVAL | 和 DuckDB 的写法一致:1 month 2 days 00:00:00.000003 |
BLOB、GEOMETRY | 0x00ff,过长时以 … 省略 |
LIST、STRUCT、MAP | [1, 2]、{x: 1},超过 120 字符时以 … 省略 |
单元格里的 | 会转义,换行写成 <br>,值里有什么都不会打乱表格。结果为零行时,表格后面跟一行 _0 rows._;结果因 --limit 或单元格预算被截断时,后面跟 _Truncated at 2 of 5000 rows, so this is not the whole answer._(超过 100,000 行的计数上限时是 of at least …),免得把预览读成完整答案。出错时的行为不变:JSON 写到 stderr,stdout 为空。
看看有什么可查
ducklocal schema 一次回答"有哪些东西、各有哪些列",DESCRIBE 则要一个关系一个关系地问。agent 应该先跑它。
ducklocal schema ./data/ # 文件、文件夹、通配符
ducklocal schema --database warehouse.duckdb # 表和视图
ducklocal schema warehouse.duckdb --table orders # 单个关系的完整信息
ducklocal schema ./logs/ --full # 全部列出,不管多大PATH 的展开方式和拖到窗口上一样:文件夹递归找出其中的数据文件(跳过隐藏文件,最多 256 个),通配符取所有匹配,数据库文件等同于用 --database 指定。返回一个 JSON 对象:
{"detail":"full","relations":[{"name":"data/orders.csv","kind":"file","from":"'data/orders.csv'","rows":null,"column_count":2,"path":"data/orders.csv","columns":[{"name":"channel","type":"VARCHAR"},{"name":"amount","type":"BIGINT"}]}],"without_columns":0}| 字段 | 含义 |
|---|---|
name | 文件按给出的路径显示(在工作目录下时写成相对路径);表按当前连接上 SQL 能访问到的名字显示:默认 schema 里只写表名,其他 schema 写 schema.table |
kind | file、glob、table、view 或 workbook |
from | 读取它的 SQL,可以直接接在 FROM 后面:文件是带引号的路径,需要时表名会加引号 |
rows | 表取目录里的估计值,Parquet 取文件尾部记录的精确值;CSV、JSON、视图要扫描才知道,给 null |
column_count、columns | DuckDB 描述的列 |
files | glob 匹配到的文件数 |
匹配到的文件都是同一种格式时,一个 glob 算作一个关系,读法和 .dash 的 source 一样(ducklocal schema 'logs/*.csv'),--stats 也把它当作一个整体统计。格式混杂的 glob 和文件夹,则逐个列出其中的文件。
目录小时完整返回("detail": "full")。超过 12 个关系或 400 列时改为返回摘要("detail": "summary")。行数最多的 5 个关系带完整的列,其余只列名字、类型、行数和列数,without_columns 是这类关系的个数。要看更多,hint 给出两种办法:--table NAME 看其中一个(可以按列出的名字、不带 schema 的表名或文件名匹配),--full 看全部。工作簿没有对应的表函数,只列出 sheets,不给列;--stats 会读它的第一个工作表。PATH 不存在,或 --table 找不到对应的关系,退出码为 2。
探查与转换
确认文件实际存在后:
ducklocal query --sql "DESCRIBE SELECT * FROM 'sales.csv'"
ducklocal query --sql "SELECT * FROM 'sales.csv'" --limit 20
ducklocal query --sql "SELECT sum(amount) AS total FROM 'sales.csv'"
# 仅在确认此输出路径已获授权后运行:
ducklocal query --sql "COPY (SELECT * FROM 'sales.csv') TO 'sales.parquet' (FORMAT PARQUET)"
ducklocal query --sql "SELECT count(*), sum(amount) FROM 'sales.parquet'"
ducklocal query --sql "DESCRIBE SELECT * FROM 'events.json'"别猜 amount 这样的列存不存在,先跑 ducklocal schema 或 DESCRIBE。完整的汇总要在 SQL 里算,不要把截断的预览加起来。CLI 调用看不到 GUI 注册的视图,彼此之间也不共享内存表;需要持久化时,显式写入数据库:
# 分两次调用执行,每次只有一条语句;写入必须获得授权。
ducklocal query --database warehouse.duckdb --read-write --sql "CREATE TABLE sales AS SELECT * FROM 'sales.csv'"
ducklocal query --database warehouse.duckdb --sql "SELECT count(*) FROM sales"数据库锁冲突直接报错。关闭另一个写入方(包括 GUI),或改用经过授权的副本;不要删除锁文件,也不要悄悄另建一个数据库。
JSONL 与嵌套 JSON
read_ndjson_objects 是换行分隔 JSON(每行一个对象,.jsonl/.ndjson)的读取函数;read_json/read_json_auto 读取的是 JSON 文档(一个数组或单个对象)。读 JSONL 文件时显式写明读取函数,别依赖自动探测。json_extract_string(col, '$.a.b[0].c') 按嵌套路径(对象键和数组下标)取值,返回 VARCHAR:
ducklocal query --sql "SELECT json_extract_string(event, '$.payload.items[0].sku') AS sku, count(*) AS n FROM read_ndjson_objects('events.jsonl') GROUP BY 1 ORDER BY n DESC"叶子节点不是字符串、想按 JSON 取出时,改用 json_extract。
逐列统计
动手之前,得先定下画什么图、用什么刻度、数字怎么格式化,ducklocal schema --stats 回答的就是这些问题。DESCRIBE 只说某列是 DOUBLE;统计则告诉你:它实际只用两位小数,最大值是中位数的一千倍,日期在中间断了十一天。
ducklocal schema sales.csv --stats
ducklocal schema ./exports/ --table orders.parquet --stats
ducklocal schema --database warehouse.duckdb --table "my orders" --stats--stats 只统计一个关系:参数只指向一个时就是它,否则用 --table 指定;指向两个以上又没写 --table 时退出码为 2,并提示加 --table。它不能和 --full 一起用。早先版本的 ducklocal profile TARGET [--database PATH] 输出同样的报告,现在仍然可用。
关系可以是数据文件(csv、tsv、txt、parquet、json、ndjson、jsonl),或工作簿(xlsx、xls、xlsb、ods,取第一个工作表导入为 TEMP 表后做逐列统计);配合 --database 时,也可以是表名或视图名。带点的名字按 schema.table 拆开并逐段加引号,所以带空格或大写的名字可以照原样写。指向的对象不存在时,报参数错误(退出码 2),而不是 SQL 错误。
返回一个 JSON 对象:target、relation(统计实际针对的 SQL)、row_count、elapsed_ms,以及 columns(按关系本身的列顺序排列)。每列包含:
| 字段 | 含义 |
|---|---|
name、type | 与 DESCRIBE 一致 |
nulls | 该列为 NULL 的行数 |
distinct | 精确值,不是估算 |
unique | distinct 等于 row_count 时出现,值为 true |
min、max | 以文本给出,DECIMAL 因此不丢位数 |
decimals | 小数点后实际用到的位数,不是声明的刻度 |
median | 该列真实存在的一个值,不是两个值之间的插值 |
max_over_median | 最大值是中位数的多少倍;用线性刻度还是对数刻度,就看这个数 |
covered_days、span_days、missing_days | 该列出现过的天数、首尾之间的天数,以及两者之差:0 表示连续,非零说明中间有空缺,折线会直接连过去 |
decimals、median、max_over_median 只出现在数值列,日期三项只出现在 DATE/TIMESTAMP 列。LIST、STRUCT、MAP、UNION 列只报告 nulls,因为这些类型没有定义 min/max。
如果文本列里是数字加一个短词(比如 R 的 NA),还会报告 null_marker 和 null_marker_rows,报告里也会多一条 hint,写明能把这一列读成数字的 read_csv 调用。
统计值是精确的,要读完整个关系,所以一次统计就是一次全表扫描。--limit 对它不起作用:只看样本,算不上统计。
把结果显示到运行中的窗口
ducklocal open 把东西交给已经在运行的 DuckLocal 窗口(没有就先启动一个),并把窗口提到前面。agent 用它把结果摆到人面前,人可以接着改:
ducklocal open --title "Revenue by channel" --run --sql "SELECT channel, sum(amount) AS total FROM 'orders.csv' GROUP BY 1"
ducklocal open sales.dash # 打开为仪表盘标签页
ducklocal open ./data/ --sql-file analysis.sql
echo "SELECT 42" | ducklocal open --sql-file - --no-launch- PATH 的打开方式和拖到窗口上一样:
.dash文件打开为标签页,DuckDB 数据库文件作为窗口的数据库打开,数据文件、文件夹和通配符挂载为视图。相对路径按命令所在的目录解析。窗口还在启动时收到的请求,会等启动时的打开完成后再处理。 --sql或--sql-file(-表示 stdin)打开一个新的查询标签页,标题为--title;--run会在 PATH 挂载完之后执行它。--no-launch在没有窗口时直接失败(kind 为not_running),不启动窗口。适合不希望突然弹出窗口的机器。
成功时输出 {"delivered": true, "launched": BOOL}:只表示送达,不是查询结果。打开的内容在窗口自己的会话里运行(它的连接、挂载的视图、历史记录),出错也显示在窗口里,不在 stdout。参数错误退出码为 2;无法送达时退出码为 1,kind 为 not_running(找不到窗口)、refused(窗口拒绝了请求)或 launch。
窗口在本机回环端口上监听,端口和一个随机会话令牌写在只有你自己能读的文件里(见设置与应用数据);本机其他用户或指向 localhost 的网页都连不上。
读取窗口当前的内容
ducklocal open --state 方向相反:什么都不打开,也从不启动窗口,只输出运行中的窗口正在显示什么。这样 agent 可以从人停下的地方接着做,而不用请人把查询贴过来。
ducklocal open --state
ducklocal open --state sales.dash
ducklocal open --state --active{"database":":memory:","attached":[{"path":"/Users/me/data/orders.csv","name":"orders","rows":224}],"active_tab":1,"tabs":[{"kind":"query","title":"Query 1","active":false,"sql":"SELECT …","result":{"status":"rows","columns":[…],"row_count":5,"truncated":false,"preview":[["Web","132771.24"],…],"sql":"SELECT …","elapsed_ms":4}},{"kind":"dashboard","title":"sales","active":true,"path":"/Users/me/data/sales.dash","unsaved":false,"dashboard":{"running":false,"error":null,"stale":null,"plots":[…],"filters":[{"name":"channel","plot":"revenue_by_channel","column":"channel","picked":true,"value":"Web"}]}}]}database是当前打开的数据库文件,或":memory:";attached列出窗口注册为视图的文件及视图的name。CLI 查询读不到这些视图,要用path。- 查询标签页有
sql(编辑器里的文本,不管执行过没有)和result:status为empty、running、rows、affected、failed、explain或profile。有行时给出columns、row_count、truncated、preview(前 20 行,和表格里显示的一样,都是字符串)、elapsed_ms以及产生它们的sql;affected给出count和elapsed_ms;失败时给出error。 - 用户在图表标签页选过列之后,结果里还会有
chart:type、x、y、split,以及dash,即能在仪表盘里画出同一张图的query和plotblock。 explain对应执行计划按钮显示的内容:plan是 DuckDBEXPLAIN的输出,按行组成的字符串数组;另有elapsed_ms。profile对应性能分析按钮显示的内容(见 SQL 编辑器):plan是EXPLAIN ANALYZE实测的算子树,一个根节点数组,每个节点为{operator, seconds, rows, estimated, children}。seconds是该算子自身的耗时,不含子节点;rows是它产出的行数;estimated是优化器对rows的估计(计划里没给时为null);children结构相同。operator_seconds是所有算子自身耗时之和,各算子的占比以它为分母,它不是墙钟时间;elapsed_ms是整次运行的耗时。- 仪表盘标签页有
path、unsaved和dashboard:其中包含comments_file路径、plots(名字、类型、标题、查询、行数、open_comments,画不出来时有failure)和filters(每个 filter 当前选中了什么)。picked: true而value: null表示选中的是 NULL。
--state sales.dash 只保留显示该仪表盘的标签页,--state --active 只保留当前标签页。指定路径没有打开时,退出码为 1,kind 为 not_open,提示中列出已打开的仪表盘。active_tab 是当前标签页在返回的 tabs 中的位置;若它不在其中,则为 null。database 与 attached 始终保留。
--state 可带一个 .dash PATH 或 --active,两者不能同时使用;可以带 --no-launch,但不起作用;不能再带 --sql、--run 等其他选项(退出码 2)。没有窗口时退出码为 1,kind 为 not_running;窗口 5 秒内没有回应时 kind 为 timeout。
校验仪表盘规格文件
格式本身(block、属性、图表类型、SQL 规则与常见提示信息)见 仪表盘(Dashboard,.dash);本节讲命令本身。
.dash 文件用声明的方式描述仪表盘,就像 Terraform 声明基础设施:查询和图表写成 block,彼此通过引用连接,不用写脚本。人和 agent 都容易写、容易 diff。
query "latency" {
sql = <<SQL
SELECT timestamp, service, avg(latency) AS latency
FROM logs
GROUP BY timestamp, service
SQL
}
plot "latency" {
type = "line"
query = query.latency
x = timestamp
y = latency
series = service
}ducklocal check dashboard.dash
ducklocal check dashboard.dash --database warehouse.duckdb
ducklocal check dashboard.dash --summarysource block 给数据文件起名字,路径相对于 .dash 文件,供查询读取。query block 只有一个 sql 属性,内容是一条只读语句,写成 heredoc 或字符串均可。接受 SELECT、WITH、FROM 开头、VALUES、SHOW、DESCRIBE、SUMMARIZE 与 PIVOT;任何可能写入的语句(DDL、DML、COPY、ATTACH、INSTALL)都会报诊断,GUI 也拒绝执行,因为文件一打开,仪表盘的查询就会运行。plot block 含 type(line、bar、stacked_bar、area、scatter、pie、heatmap、histogram、map、table、card、text 之一)、query(指向同文件某个 query block 的引用,如 query.latency)、它用到的列,以及可选的 title 和 width。filter block 让柱状图或表格可以点选,查询用 $name 读取选中的值。# 和 // 之后到行尾都是注释。全部属性见 仪表盘(Dashboard,.dash)。
text plot 是例外:它只显示 title、text 或两者,不接受 query。卡片还支持 note 与 tone,见卡片与文本面板。
先做静态检查:每条查询的 SQL 都在一次性连接上交给真正的 DuckDB parser 校验,与 query 执行前的校验相同;每个 $name 都必须对应一个 filter block,解析和执行 SQL 时,每个 $name 都按 TRUE 处理,也就是仪表盘什么都没选时执行的 SQL。
然后每条查询都真正执行一遍,和打开仪表盘时完全一样(只读,带着 source,同一条代码路径、同样的行数上限),并把每个 plot 用到的列与查询实际返回的列逐一核对:图表的 y 不是数值列算错误,地图坐标或 size 不是数值、filter 的 column 不在它那张图的查询结果里,也都算错误。
- 不带
--database时在内存里执行,只读自己source文件的仪表盘这样就够了。读取了某张 source 没定义的表的查询,可能是写给某个没提供的数据库的:它列在unresolved下(查询、行号、原因)而不算失败,它的 plot 也不核对列。 - 带
--database PATH(已存在的文件,只读打开)时,在该数据库上执行。查询读的是数据库里的表时,一定要带上它:unresolved下的都没核对过。
每个读取 filter 的查询,还会带一个选中值做绑定检查,但不执行这个变体。即使初始的 $name = TRUE 版本能运行,也能发现消费筛选条件的查询中缺少对应列的问题。筛选器的取值列也必须由点选图表的查询返回。
要真正执行而不只是生成查询计划,是因为有些错误(比如当前构建做不了的类型转换、数据里某个转换不了的值)要读到行时才会暴露;只做规划的 check 会放过它们,交出一个画不出来的仪表盘。查询会完整执行,所以对大库做 check,耗时和打开仪表盘差不多。
成功时输出一个 JSON 对象,列出 source(附带 resolved 路径)、查询(附带结果列和 rows:什么都不选时返回的行数;被仪表盘的行数预算截断时另有 truncated: true)、filter(附带读取它的 queries)、plot、unresolved,以及 empty:执行了但一行都没返回的查询。空查询能通过其他所有检查,但通常是正则、筛选条件或 join 什么都没匹配上,逐个看一眼。使用 --summary 时,成功输出改为一行精简 JSON:
{"file":"sales.dash","ok":true,"sources":1,"queries":3,"filters":1,"plots":6,"unresolved":[],"empty":[]}四种 block 字段为数量;unresolved 是未能核对的查询名列表,empty 是没有返回行的查询名列表。
有错的 block 会被跳过,其余有效 block 继续校验。同一次运行汇总规格诊断与查询失败,每条占一行 文件:行号: 消息。只要包含规格诊断,退出码就是 2,kind 为 spec;仅有 SQL 执行或绑定失败时,退出码为 1,kind 为 sql。数据库或 I/O 错误也以 1 退出。--summary 不会隐藏这些诊断。
同一个文件也能在 GUI 里作为仪表盘标签页打开(ducklocal dashboard.dash、ducklocal open dashboard.dash,或把文件拖到窗口上):plot 排在 12 列的网格上;画不出来的 plot 在自己的位置上显示原因。见仪表盘(.dash)。
回复仪表盘上的评论
人在窗口里给 plot 写评论来审阅仪表盘:鼠标移到 plot 上,点对话气泡(见用评论审阅)。ducklocal comments 是 agent 这一侧:不开窗口,读取并回复这些评论。
ducklocal comments sales.dash [--all]
ducklocal comments sales.dash --reply c1 --text "已按收入降序排列" [--resolve]
ducklocal comments sales.dash --resolve c1
ducklocal comments sales.dash --reopen c1
ducklocal comments sales.dash --add revenue_by_channel --text "这里扣掉退款了吗?"- 不带操作时,输出未解决的讨论:
{file, comments_file, open, threads}。--all连已解决的也列出。 - 每条讨论有
id、plot,以及在规格文件里找到它所需的信息:plot_title、plot_query,和plot_lines(plotblock 的首行与末行;规格文件解析不了时为null)。同名的 plot 已经不存在时,plot_missing为true。然后是resolved,以及comments,每条有author(user或agent)、agent 写的评论还有name(就是presence显示的名字)、text和at(RFC 3339,本地时间)。 --reply ID --text TEXT添加 agent 的回复。回复一条已解决的讨论会重新打开它,除非同时给了--resolve。--resolve ID和--reopen ID改变讨论的状态;--add PLOT --text TEXT以 agent 的身份新开一条讨论。- 讨论的编号也可以当 ID 用:
--reply 1就是c1。--add不接受规格文件里没有的 plot(退出码 2),提示里会列出已有的 plot。 - 有操作时输出修改后的
{thread}。
讨论保存在规格文件旁边的 FILE.comments.json 里。运行中的窗口一秒内就能察觉变化,并显示在对应的 plot 上。讨论 id 不存在时退出码为 2,并提示先列出讨论;规格文件不存在时退出码为 1。
一次完整的审阅:读出未解决的讨论,修改规格文件,跑 ducklocal check,再逐条回复改了什么并 --resolve。想反过来问对方时,只用 --reply,不加 --resolve。
显示 agent 的工作状态
check 和 comments 会自动告知运行中的窗口,agent 正在处理哪份仪表盘。presence 还可以提前说明下一次修改的位置:
ducklocal presence sales.dash --doing editing --plot revenue_by_channel,daily_revenue
ducklocal presence sales.dash --doing reading --agent "Analyst"
ducklocal presence sales.dash --doing idle--doing接受reading、editing或idle,done是idle的别名。--plot接受逗号分隔的 plot 名。editing允许尚不存在的名字,窗口会显示占位提示;reading或idle指定未知名字时,退出码为 2。- 显示名称依次取自
--agent、DUCKLOCAL_AGENT、识别到的 agent 环境(Claude、Codex、Cursor、Gemini),最后回退为Agent。 - 输出为
{delivered}。没有运行中的窗口时返回{"delivered":false},不报错,也不启动窗口。
命令持续活动时,仪表盘角落会保留 agent 名字,点击可看活动记录。30 秒没有活动,或传入 --doing idle 后,会话结束;改动和新增的图表仍保留标记,直到被看过,删除提示则需要手动关闭。见看见 agent 正在做什么。
用 LSP 编辑仪表盘规格文件
ducklocal lsp 是面向 .dash 文件的 Language Server Protocol 服务器,通过 stdio 通信,由编辑器启动。任何支持 LSP 的编辑器都能借此获得 GUI 自带规格编辑器的功能:
- 诊断:与
check报告的一致,每次打开和修改时推送(全量文档同步,严重级别一律为 Error,因为它们是错误,不是风格建议)。 - 补全:block 类型、属性名、图表类型以及
query.、plot.后面的名字,每一项都带一个 text edit,替换正在输入的单词。 - 悬停:block、属性和引用的文档。
- 跳转到定义:从
query.name或plot.name引用跳到对应 block 的名字。
带 --database PATH 时,每条查询的 SQL 还会交给真正的 DuckDB parser 校验,与 check --database 相同。
在编辑器的通用 LSP 支持里,为 *.dash 文件配置 ducklocal lsp 即可。命令不接受文档参数,编辑器通过 stdio 与它通信。Neovim(0.10+)示例:
vim.api.nvim_create_autocmd("FileType", {
pattern = "dash",
callback = function(event)
vim.lsp.start({
name = "ducklocal",
cmd = { "ducklocal", "lsp" },
root_dir = vim.fs.dirname(vim.api.nvim_buf_get_name(event.buf)),
})
end,
})VS Code 里任何通用 LSP 扩展都可以用命令 ducklocal、参数 ["lsp"]、文档选择器 dash 完成同样配置。退出码:正常 shutdown/exit 为 0;客户端没走完退出流程就断开为 1;命令行错误或协议握手失败为 2。
JSON 契约
成功时 stdout 为单个 JSON 对象,末尾换行:
{"columns":[{"name":"n","type":"Int32"}],"rows":[[1]],"row_count":1,"truncated":false,"total_rows":1,"total_rows_exact":true,"elapsed_ms":1}columns 和每行都保留列顺序与重复列名。type 是 Arrow debug 表示,不是 DuckDB 原始 SQL 类型名(HUGEINT 与 DECIMAL 等可能使用相同 Arrow 表示)。row_count 为返回行数。total_rows 包含输出上限之后继续统计到的行;total_rows_exact: false 表示额外统计最多 100,000 行后停止,该总数只是下限。这两个字段都不代表修改行数。elapsed_ms 包含准备、执行、行编码,不含读取 SQL 文件及建立连接。零行结果也带列信息。DDL/DML 按 DuckDB 的实际结果输出,包括 Count 列:INSERT/COPY 通常返回 [[N]],CREATE TABLE 通常返回零行;不根据 SQL 的第一个关键字来猜。
单元格编码:
| SQL 值 | JSON 编码 |
|---|---|
| NULL | null,不是字符串 "NULL" |
| 布尔、有限浮点数 | JSON 布尔/数字 |
| ±9,007,199,254,740,991 范围内整数 | JSON 数字 |
| 更大整数(含 HUGEINT/UHUGEINT) | {"encoding":"integer","value":"9007199254740992"} |
| Decimal | {"encoding":"decimal","value":"123.450"},精确十进制文本 |
| 文本、enum | JSON 字符串 |
| Blob、geometry | {"encoding":"hex","value":"00ff"},小写十六进制;geometry 为 WKB |
| Date | {"encoding":"date","unit":"Day","value":"1","iso":"1970-01-02"},自 1970-01-01 起天数 |
| Timestamp | {"encoding":"timestamp","unit":"Nanosecond","value":"123456789","iso":"1970-01-01T00:00:00.123456789"},自 Unix epoch 起有符号计数;也可能是 Second/Millisecond/Microsecond |
| Time | {"encoding":"time","unit":"Microsecond","value":"123456","iso":"00:00:00.123456"},自午夜起计数 |
| Interval | {"encoding":"interval","months":1,"days":2,"nanos":"3000"} |
| NaN、正/负无穷 | {"encoding":"float","value":"NaN"} / "inf" / "-inf" |
| List、array | 递归编码的 JSON 数组,保留嵌套 null |
| Struct | {"encoding":"struct","fields":[["name",VALUE],...]} |
| Map | {"encoding":"map","entries":[[KEY,VALUE],...]},键保留类型,不转换为 JSON 对象 |
| Union | {"encoding":"union-value","value":VALUE},仅活动值,不保留成员标签 |
时间值的 value 是精确的原始计数,不会丢失小数精度;日期和时间戳的无穷哨兵值同样保留原始计数。旁边的 iso 是同一个值的 ISO 8601 文本,方便阅读,小数位数和单位的精度一致。带时区的列(Arrow 元数据)里的时间戳是 UTC 时刻,iso 以 Z 结尾。--format md 也以可读形式输出日期和时间戳。当前 DuckDB Arrow 封装分不清嵌套的 HUGEINT、UHUGEINT 和 DECIMAL(38,0),遇到这类非空值会明确报错,不会悄悄改变符号或精度;这类字段要在 SQL 里显式 CAST 为 VARCHAR。其他不支持的未知类型也会报错,别把报错当成空结果。Union 只输出活动值,无法无损往返还原 union 标签。
错误时 stdout 留空,stderr 输出单个 JSON 对象:
{"error":{"kind":"argument","message":"Unknown query option: --limt","hint":"Did you mean --limit?"}}有明确的下一步时会带 hint,用的是这个 CLI 自己的说法:拼错的参数本来是哪个;只读数据库上写入被拒时加 --read-write;表不存在时先跑 ducklocal schema;值转换不了时用 TRY_CAST;一次传了多条语句时拆开;把关键字当裸别名时加 AS(SELECT count(*) AS minutes,或者给别名加引号)。agent 应先照它做,再考虑改写 SQL。DuckDB 自己的信息已经说清楚时(比如语法错误)不带 hint。
退出码:0 成功,2 参数/语句数量错误,1 SQL、数据库、I/O 或输出错误。错误 kind 为 argument、sql、database、io、output,check 的 spec,以及 open 的 not_running、not_open、refused、timeout、launch。先看退出码再解析 stdout;说结果完整之前,先检查 truncated。帮助和版本信息是例外,输出纯文本。stdout 管道被关闭之类的传输错误,无法保证输出为空或完整。
安装官方 skill
skill 教会 agent 几个让答案可信的习惯:先看 schema,再做有限预览,用 SQL 算聚合,检查 truncated,并基于返回的值回答。它覆盖 schema 探查、逐列统计、格式转换、数据库查询、.dash 仪表盘及其审阅评论,以及把结果交给窗口并读回;不提供 S3 浏览、空间专用命令、会话记忆或 MCP 服务。
在应用里安装。 打开 命令行与 AI…(标题栏的机器人按钮,或 DuckLocal 菜单):
- 安装命令:把
ducklocal加入PATH,agent 的 shell 才能运行它。 - 添加到 Claude Code:把 skill 写入
~/.claude/skills/ducklocal,Claude Code 在所有项目里都能找到。只写 skill 自己的两个文件,该目录下的其他内容保持不变。DuckLocal 更新带来新版 skill 时,对话框会提供 更新技能。 - 复制说明:复制一段简短说明,粘贴给 Codex、Cursor 或任何聊天窗口,告诉它命令在哪、怎么用。
手动安装。 skill 位于 skills/ducklocal。如果只想加到某一个项目(以 Claude Code 为例):
# 在 DuckLocal 仓库根目录执行;把目标路径换成你的项目。
mkdir -p /path/to/your-project/.claude/skills
cp -R skills/ducklocal /path/to/your-project/.claude/skills/目标已存在时先检查,避免覆盖本地修改。其他 agent 可以把同样的 SKILL.md 与 references/cli.md 放进各自支持的 skill 目录。
