@metagptx/deepflow-suite-cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,106 +15,19 @@ CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI
15
15
 
16
16
  ## 任务描述
17
17
 
18
- 每轮工作先用 `df-task get "$DF_TASK_ID" --json` 读取 `description` 与 `description_version`。标题是简短名称,描述是当前需求,首条消息仅是历史事实;不能用首条消息替代描述。汇报前再次核对版本,若描述变化需重新核对范围与验收条件。
18
+ 每轮工作先读取当前 `description` 与 `description_version`。支持字段选择的 CLI 可用 `df-task get "$DF_TASK_ID" --fields id,description,description_version --json`,只在需要时追加明确的 metadata 路径;旧版用 `get --json` 保存到本轮文件再提取,不把完整 metadata 打进上下文。标题是简短名称,描述是当前需求,首条消息仅是历史事实;不能用首条消息替代描述。汇报前再次核对版本,若描述变化需重新核对范围与验收条件。
19
19
 
20
- ## 目标环境门禁与地址
21
-
22
- 每次调用服务前,必须先确认用户要操作的目标环境。在推进器拉起的 Run 里按下文「Run 内的目标环境」确定,不询问。其他情况下,用户没有明确说 `local`、本地、`dev`、开发环境或 `prod`、生产环境时,先询问:
23
-
24
- ```text
25
- 请指定目标环境:local(本地)、dev(开发环境)还是 prod(生产环境)?
26
- ```
27
-
28
- 环境一旦选定,本轮所有服务调用都必须保持在同一环境;不能因为连接失败就切换到另一环境,也不能把当前机器所在位置当成目标环境。
29
-
30
- | 目标环境 | 本地执行时的地址 | 在 dev-k8s 内执行时的地址 |
31
- | --- | --- | --- |
32
- | `local` | `http://127.0.0.1:8090` | 不适用 |
33
- | `dev` | `https://deepflowtask.dev.deepwisdomai.com` | `http://deepflowtask.deepflow.svc.cluster.local:8090` |
34
- | `prod` | 尚未配置 | 尚未配置 |
35
-
36
- 地址列描述的是同一目标环境的两种访问方式:本地 CLI 使用 dev Ingress,集群内 Agent 使用 dev Service DNS。确认环境后,可以用用户明确提供的 `--base-url` 覆盖该环境的默认地址,但该地址必须属于已选环境;如果地址与环境冲突,先停止并澄清,不能用它绕过环境门禁。`prod` 目前没有地址,任何 prod 操作都必须停止并说明尚未接入。
37
-
38
- 本地操作 dev 时示例:
39
-
40
- ```bash
41
- df-task --base-url https://deepflowtask.dev.deepwisdomai.com facts "$DF_TASK_ID" --json
42
- ```
20
+ ## 环境与身份
43
21
 
44
22
  ### Run 内的目标环境
45
23
 
46
- `DF_TASK_ID`、`DF_RUN_ID`、`DF_TASK_ACTOR_KIND` 都非空时,你运行在推进器拉起的 Run 里,没有用户可以询问。此时目标环境由部署决定:用注入的 `DF_TASK_BASE_URL` 对照上表反查所属环境,例如 `http://deepflowtask.deepflow.svc.cluster.local:8090` 即 `dev`。每一轮、每个 Run 都按同一规则确定,不需要也不应该等待任何人确认。
47
-
48
- 反查不到任何环境、地址属于 `prod`,或指令要求操作的环境与反查结果不一致时,停止,不调用服务、不改用其他地址,在最终回复里说明注入地址和冲突原因。推进器会照常记录本次执行结束,交给下一轮调度或人处理。
49
-
50
- 不在 Run 里时,只有在用户明确选择 `dev` 且 `DF_TASK_BASE_URL` 确认属于 dev 时,才可以复用它;用户选择 `local` 时必须忽略该值并使用 `http://127.0.0.1:8090`。用户没有先选环境都不能执行请求。
51
-
52
- 先用 `GET /healthz` 验证选定地址,再执行读写操作。健康检查失败时报告该环境不可达,不得静默改用 localhost 或其他环境。
24
+ 托管 Run 中 `DF_TASK_ID/DF_RUN_ID/DF_TASK_ACTOR_KIND` 非空,`DF_TASK_BASE_URL` 由部署注入,绑定本 Run 的事实服务;不询问、不根据同名 Service DNS 猜成 dev,不覆盖写入身份,不因业务目标出现“生产”而切换流程服务。身份或地址缺失/冲突先报告,不能伪造或回退 localhost。只核对必要值,不输出完整环境。在 Run 内一律使用 `--json`。
53
25
 
54
- 下面的示例还用到两个变量,同样由 runtime 注入,同样不要自己编:`DF_TASK_ID` 是你这次执行所属的 Task,`DF_RUN_ID` 是你自己这个 Run 的标识(写事实时 payload 里的 `run_id` 填它)。如果它们是空的,说明这次执行不是由推进器兑现唤起起来的 —— 别猜一个填进去,写一条 `summary` 说明情况。
55
-
56
- **在 Run 里一律加 `--json`。** 不加 `--json` 的输出是给人看的排版,字段随时会调整。
57
-
58
- ```bash
59
- # 按本轮需要选择事实类型(选择标准见「读日志」)
60
- df-task facts "$DF_TASK_ID" --types user_message,artifact_published --json
61
- df-task facts "$DF_TASK_ID" --types user_message,artifact_published --after 128 --limit 100 --json
62
-
63
- # 追加一条事实:payload 写成 JSON 文件再传
64
- # 注意 heredoc 的标记不加引号,加了引号 $DF_RUN_ID 就不会展开
65
- cat > /tmp/artifact.json <<JSON
66
- {"run_id":"$DF_RUN_ID","kind":"diff","ref":"abc1234def5678","title":"登录按钮溢出修复"}
67
- JSON
68
- df-task append "$DF_TASK_ID" --type artifact_published --file /tmp/artifact.json --json
69
- ```
26
+ 外部调用须先确认 local/dev/prod,再按[环境与地址](references/environment.md)显式选址;用户未明确时先询问,连接失败不切换,跨服务保持同一环境。先验证所选地址 `/healthz`,不能用内部同名 DNS 猜环境。生产写操作仅限已有授权。
70
27
 
71
- ## 业务 metadata 与软范围查询
28
+ ## metadata 与工具可用性
72
29
 
73
- Task 可以携带业务方自定义的 JSON metadata。基础服务只约定顶层的
74
- `source`、`query_keys` 和 `data`:前两个用于可选的软范围查询,`data` 完全由业务方组织。
75
- 例如:
76
-
77
- ```bash
78
- df-task create \
79
- --title '订单分析' \
80
- --metadata '{"source":"workbench","query_keys":["account:u123"],"data":{"project":"orders"}}' \
81
- --json
82
- ```
83
-
84
- `--metadata` 必须是 JSON 对象;没有 metadata 时服务保存 `{}`,不会改变原有流程。创建、
85
- 列表和单Task读取会返回 metadata。事实读取也支持同一范围过滤:
86
-
87
- ```bash
88
- SCOPE_SOURCE=workbench
89
- SCOPE_KEY=account:u123
90
-
91
- df-task ls \
92
- --metadata-source "$SCOPE_SOURCE" \
93
- --metadata-query-key "$SCOPE_KEY" \
94
- --json
95
- df-task get "$DF_TASK_ID" \
96
- --metadata-source "$SCOPE_SOURCE" \
97
- --metadata-query-key "$SCOPE_KEY" \
98
- --json
99
- df-task facts "$DF_TASK_ID" \
100
- --metadata-source "$SCOPE_SOURCE" \
101
- --metadata-query-key "$SCOPE_KEY" \
102
- --json
103
- df-task stream \
104
- --metadata-source "$SCOPE_SOURCE" \
105
- --metadata-query-key "$SCOPE_KEY" \
106
- --json
107
- ```
108
-
109
- 查询规则:
110
-
111
- - 同时提供 `source` 和 `query_key` 时,Task 必须匹配来源和查询标识;只提供其中一个时只按该条件过滤。`query_keys` 包含 `"*"` 的显式共享Task例外于这两个条件。
112
- - `query_keys` 可以包含多个标识;`"*"` 表示显式共享,匹配任意查询范围。
113
- - 不提供过滤参数仍然是历史上的全量查询。业务上下文存在时才携带过滤参数,不要把它当成权限边界。
114
- - 先从Task响应的 metadata 读取 `source` 和 `query_keys`,再把当前业务上下文中的具体 key 传给后续查询;不要从 `data` 中猜测过滤字段。
115
-
116
- metadata 只影响读请求的范围。`append`、`dispatch`、`wait`、`handoff` 和 `title` 仍按明确的
117
- Task ID 操作,不支持 metadata 过滤参数;事实服务不会根据 metadata 做鉴权。
30
+ metadata 的 `source`、`query_keys` 用于可选软范围过滤,`data` 由业务方定义;它们不是权限边界。有业务上下文时沿用已确认的来源和具体 key,不能从 data 猜测。需要创建、查询或核对共享范围时读[范围查询说明](references/query-scope.md)。
118
31
 
119
32
  托管 Run 中 `df-task` 由 Runtime 镜像提供。命令缺失或 `--help` 无法执行时,报告执行环境异常并结束;不要在 Run 中安装、编译 CLI 或猜测绝对路径来继续。非托管的本地开发可按项目 README 安装 CLI。**注意不要把 runtime 的地址填成 Task 服务的地址** —— `df-agentrt` 和 `df-task` 是两个服务、两个端口、两套授权。
120
33
 
@@ -147,6 +60,25 @@ df-task facts "$DF_TASK_ID" \
147
60
  - API 不支持按 `run_id` 过滤、精确查单个 `seq` 或倒序取尾页。返回中按关联字段核对 Run/引用;不要尝试不存在的参数。
148
61
  - `artifact_published` 只有引用坐标。需要 diff、spec、MR 正文时,用 `ref` 到对应系统读取。
149
62
 
63
+ ### 字段选择与有界分页
64
+
65
+ 以下是新 CLI 的可选读取能力;以实际 `df-task help get/facts` 为准,旧版继续使用上面的单页 JSON 及游标,不在 Run 中安装或反复试错。
66
+
67
+ ```bash
68
+ # get 的已知顶层字段;metadata 可选对象路径
69
+ df-task get "$DF_TASK_ID" --fields id,description,description_version --json
70
+ # --fields 作用于每条事实,分页信封保留;payload 可整体保留
71
+ df-task facts "$DF_TASK_ID" --types summary,run_outcome --fields seq,type,actor_id,payload --json
72
+ # 原文写本轮新文件,stdout 仅返回查询/数量/类型/完整性/文件摘要
73
+ df-task facts "$DF_TASK_ID" --types dispatch_intent,invocation_started,invocation_accepted,invocation_ended \
74
+ --all --output-file /tmp/dispatch-facts.json --json
75
+ ```
76
+
77
+ - `--fields` 必须显式指定且配合 `--json`。支持的顶层字段和对象路径、缺字段与 null 的区别见[读取契约](references/reading.md)。选择过的输出不证明其他字段没有信息;关键结论需要读原事实。
78
+ - `--all` 必须配合 `--output-file`、`--json`,不能同时使用 `--fields`。文件保留完整事实;清单不是事实正文,按当前判断从文件提取必要的关联和 payload。
79
+ - 默认最多 10 页、8 MiB,只有观察到空页才 `complete=true`。预算耗尽或请求失败会保留已接纳的完整页,返回 `complete=false`、`stop_reason` 和准确 `next_after_seq`,**退出 1**。不要把文件存在或读到部分事实当成读完;按缺口显式续读或报告未完成。
80
+ - 文件是本轮私有缓存,拒绝覆盖;不是跨 Session 产物引用,不替代 Task 事实。`complete` 仅限清单所列类型/过滤/起始游标与本次查询,仍遵守上面的晚提交、换类型和水位规则。
81
+
150
82
  ## 任务 Run 可以写什么
151
83
 
152
84
  | 命令 | 落成哪条事实 | 什么时候写 |
@@ -195,7 +127,7 @@ df-task facts "$DF_TASK_ID" \
195
127
 
196
128
  ## 输出与错误
197
129
 
198
- `--json` 下 stdout 是服务端原样返回的 JSON。退出码只有三个:`0` 成功、`1` 网络或服务端失败、`2` 本地参数无效。**参数错误不会发出任何请求**,所以拿到 `2` 时日志里一定没有任何痕迹。
130
+ 默认 `--json` 下 stdout 是服务端原样 JSON;显式 `--fields` 输出所选字段,`--all` 输出读取清单。退出码 `0` 表示读取/写入成功,`1` 表示请求、解码、文件写入失败或有界分页未完成,`2` 表示本地参数/路径预检失败(未发请求)。`complete=false` 必须作为未完成读取处理。
199
131
 
200
132
  追加成功返回那条事实,含它的 `seq`。`--idempotency-key` 重复提交同一个键时返回**已存在的那条事实**,HTTP 状态是 `200` 而不是 `201` —— 这不是错误,重试是安全的。
201
133
 
@@ -0,0 +1,50 @@
1
+ # 环境与地址
2
+
3
+
4
+ 每次调用服务前,必须先确认用户要操作的目标环境。在推进器拉起的 Run 里按下文「Run 内的目标环境」确定,不询问。其他情况下,用户没有明确说 `local`、本地、`dev`、开发环境或 `prod`、生产环境时,先询问:
5
+
6
+ ```text
7
+ 请指定目标环境:local(本地)、dev(开发环境)还是 prod(生产环境)?
8
+ ```
9
+
10
+ 环境一旦选定,本轮所有服务调用都必须保持在同一环境;不能因为连接失败就切换到另一环境,也不能把当前机器所在位置当成目标环境。
11
+
12
+ | 目标环境 | 集群外访问地址 | 对应环境集群内地址 |
13
+ | --- | --- | --- |
14
+ | `local` | `http://127.0.0.1:8090` | 不适用 |
15
+ | `dev` | `https://deepflowtask.dev.deepwisdomai.com` | `http://deepflowtask.deepflow.svc.cluster.local:8090` |
16
+ | `prod` | `https://deepflowtask.deepwisdomai.com` | `http://deepflowtask.deepflow.svc.cluster.local:8090` |
17
+
18
+ `prod` 指国内生产 `prod-cn`。集群内地址只在对应环境集群中访问;dev 与 prod 的 Service DNS 字符串相同,不能仅凭该字符串反查环境,也不能从本机直接访问它来代替生产入口。CLI 不会按环境名自动选址:本地调用须显式设置已选环境的地址;可以使用用户提供且已确认归属的 `--base-url`,地址与已选环境冲突时先澄清,连接失败不得改用其他环境。生产写操作仍只执行当前任务已授权的范围。
19
+
20
+ 本地操作 dev 时示例:
21
+
22
+ ```bash
23
+ df-task --base-url https://deepflowtask.dev.deepwisdomai.com facts "$DF_TASK_ID" --json
24
+ ```
25
+
26
+ ### Run 内的目标环境
27
+
28
+ `DF_TASK_ID`、`DF_RUN_ID`、`DF_TASK_ACTOR_KIND` 都非空时,你运行在推进器拉起的 Run 里。`DF_TASK_BASE_URL` 由部署注入,绑定当前 Run 所属的事实服务;沿用它访问本 Task,不重新询问环境、不根据同名 Service DNS 猜成 dev,也不因业务需求中出现“生产/开发”就重定向流程服务。业务项目的目标环境与 DeepFlow 流程服务的部署环境分别核对。
29
+
30
+ 注入地址缺失或与当前流程已确认的部署环境冲突时,停止相关调用并说明缺口;不回退 localhost、不自行改地址或伪造身份。不在 Run 里时,先由用户选择环境,再按上表显式选址;内部 DNS 需要已确认的集群访问上下文。
31
+
32
+ 先用 `GET /healthz` 验证选定地址,再执行读写操作。健康检查失败时报告该环境不可达,不得静默改用 localhost 或其他环境。
33
+
34
+ 下面的示例还用到两个变量,同样由 runtime 注入,同样不要自己编:`DF_TASK_ID` 是你这次执行所属的 Task,`DF_RUN_ID` 是你自己这个 Run 的标识(写事实时 payload 里的 `run_id` 填它)。如果它们是空的,说明这次执行不是由推进器兑现唤起起来的 —— 别猜一个填进去,写一条 `summary` 说明情况。
35
+
36
+ **在 Run 里一律加 `--json`。** 不加 `--json` 的输出是给人看的排版,字段随时会调整。
37
+
38
+ ```bash
39
+ # 按本轮需要选择事实类型(选择标准见「读日志」)
40
+ df-task facts "$DF_TASK_ID" --types user_message,artifact_published --json
41
+ df-task facts "$DF_TASK_ID" --types user_message,artifact_published --after 128 --limit 100 --json
42
+
43
+ # 追加一条事实:payload 写成 JSON 文件再传
44
+ # 注意 heredoc 的标记不加引号,加了引号 $DF_RUN_ID 就不会展开
45
+ cat > /tmp/artifact.json <<JSON
46
+ {"run_id":"$DF_RUN_ID","kind":"diff","ref":"abc1234def5678","title":"登录按钮溢出修复"}
47
+ JSON
48
+ df-task append "$DF_TASK_ID" --type artifact_published --file /tmp/artifact.json --json
49
+ ```
50
+
@@ -0,0 +1,49 @@
1
+ # metadata 与软范围查询
2
+
3
+
4
+ Task 可以携带业务方自定义的 JSON metadata。基础服务只约定顶层的
5
+ `source`、`query_keys` 和 `data`:前两个用于可选的软范围查询,`data` 完全由业务方组织。
6
+ 例如:
7
+
8
+ ```bash
9
+ df-task create \
10
+ --title '订单分析' \
11
+ --metadata '{"source":"workbench","query_keys":["account:u123"],"data":{"project":"orders"}}' \
12
+ --json
13
+ ```
14
+
15
+ `--metadata` 必须是 JSON 对象;没有 metadata 时服务保存 `{}`,不会改变原有流程。创建、
16
+ 列表和单Task读取会返回 metadata。事实读取也支持同一范围过滤:
17
+
18
+ ```bash
19
+ SCOPE_SOURCE=workbench
20
+ SCOPE_KEY=account:u123
21
+
22
+ df-task ls \
23
+ --metadata-source "$SCOPE_SOURCE" \
24
+ --metadata-query-key "$SCOPE_KEY" \
25
+ --json
26
+ df-task get "$DF_TASK_ID" \
27
+ --metadata-source "$SCOPE_SOURCE" \
28
+ --metadata-query-key "$SCOPE_KEY" \
29
+ --json
30
+ df-task facts "$DF_TASK_ID" \
31
+ --metadata-source "$SCOPE_SOURCE" \
32
+ --metadata-query-key "$SCOPE_KEY" \
33
+ --json
34
+ df-task stream \
35
+ --metadata-source "$SCOPE_SOURCE" \
36
+ --metadata-query-key "$SCOPE_KEY" \
37
+ --json
38
+ ```
39
+
40
+ 查询规则:
41
+
42
+ - 同时提供 `source` 和 `query_key` 时,Task 必须匹配来源和查询标识;只提供其中一个时只按该条件过滤。`query_keys` 包含 `"*"` 的显式共享Task例外于这两个条件。
43
+ - `query_keys` 可以包含多个标识;`"*"` 表示显式共享,匹配任意查询范围。
44
+ - 不提供过滤参数仍然是历史上的全量查询。业务上下文存在时才携带过滤参数,不要把它当成权限边界。
45
+ - 先从Task响应的 metadata 读取 `source` 和 `query_keys`,再把当前业务上下文中的具体 key 传给后续查询;不要从 `data` 中猜测过滤字段。
46
+
47
+ metadata 只影响读请求的范围。`append`、`dispatch`、`wait`、`handoff` 和 `title` 仍按明确的
48
+ Task ID 操作,不支持 metadata 过滤参数;事实服务不会根据 metadata 做鉴权。
49
+
@@ -0,0 +1,9 @@
1
+ # CLI 读取契约
2
+
3
+ `--fields` 只在 `get/facts --json` 可用,默认 `--json` 不变。get 顶层可选 `id,title,description,description_version,metadata,created_at,updated_at`;facts 每条可选 `seq,type,actor_kind,actor_id,payload,idempotency_key,created_at`。metadata/payload 允许点分对象路径;无数组下标、表达式、通配符或父子路径重叠。不存在的路径省略,直接选中的 null 保留;数值不经过浮点转换。facts 的 task_id、next_after_seq 及其他信封字段保留。没有业务摘要、排序或正文裁剪。
4
+
5
+ `facts --all --output-file PATH --json` 可沿用 `--after/--types/--limit/--metadata-source/--metadata-query-key`。默认 page limit=200,max-pages=10,max-bytes=8388608;显式预算范围分别 1..100、1..67108864。字节预算按接纳前的 HTTP 响应计,超过预算的页整体不接纳,游标不跳过该页。页必须匹配 Task、严格递增,next_after_seq 必须等于该页末 seq;空页保留输入游标。
6
+
7
+ 清单字段:query(Task、地址、类型、过滤、起始游标、page limit)、complete、stop_reason、pages(接纳页数,含终止空页)、count、first_seq/last_seq(有事实时)、next_after_seq、type_counts、response_bytes、output_file、file_bytes、file_sha256。正文不会进入清单。文件包含 query、facts、complete、stop_reason、next_after_seq,权限 0600,原子发布且不覆盖。SHA 对应实际文件字节。
8
+
9
+ stop_reason:empty_page 表示本次见到空页;page_limit/byte_limit 表示预算耗尽;request_error/invalid_page 表示失败。除 empty_page 外 complete=false 且退出 1;本地参数/路径预检错误退出 2(未发请求)。本地写入失败不返回伪造的成功清单。显式续读须保持过滤条件,使用已返回的 next_after_seq 和新的文件名;先前文件不会被补写。complete 不承诺事务快照或不存在晚提交。
@@ -21,13 +21,13 @@ CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI
21
21
 
22
22
  环境一旦选定,本轮所有服务调用都必须保持在同一环境;不能因为连接失败就切换到另一环境,也不能把当前机器所在位置当成目标环境。
23
23
 
24
- | 目标环境 | 本地执行时的地址 | 在 dev-k8s 内执行时的地址 |
24
+ | 目标环境 | 集群外访问地址 | 对应环境集群内地址 |
25
25
  | --- | --- | --- |
26
26
  | `local` | `http://127.0.0.1:8081` | 不适用 |
27
27
  | `dev` | `https://deepflowtaskdriver.dev.deepwisdomai.com` | `http://deepflowtaskdriver.deepflow.svc.cluster.local:8081` |
28
- | `prod` | 尚未配置 | 尚未配置 |
28
+ | `prod` | `https://deepflowtaskdriver.deepwisdomai.com` | `http://deepflowtaskdriver.deepflow.svc.cluster.local:8081` |
29
29
 
30
- 地址列描述的是同一目标环境的两种访问方式:本地 CLI 使用 dev Ingress,集群内 Agent 使用 dev Service DNS。确认环境后,可以用用户明确提供的 `--base-url` 覆盖该环境的默认地址,但该地址必须属于已选环境;如果地址与环境冲突,先停止并澄清,不能用它绕过环境门禁。`prod` 目前没有地址,任何 prod 操作都必须停止并说明尚未接入。
30
+ `prod` 指国内生产 `prod-cn`。集群内地址只在对应环境集群中访问;dev 与 prod 的 Service DNS 字符串相同,不能仅凭该字符串反查环境,也不能从本机直接访问它来代替生产入口。CLI 不会按环境名自动选址:本地调用须显式设置已选环境的地址;可以使用用户提供且已确认归属的 `--base-url`,地址与已选环境冲突时先澄清,连接失败不得改用其他环境。生产写操作仍只执行当前任务已授权的范围。
31
31
 
32
32
  本地操作 dev 时示例:
33
33
 
@@ -15,6 +15,14 @@ CLI 由 `@metagptx/deepflow-suite-cli` 统一安装,本 Skill 不携带 CLI
15
15
 
16
16
  用户必须先明确目标环境:`local`、`dev` 或 `prod`。本轮不要因为连接失败自动切换环境。
17
17
 
18
+ | 环境 | Workbench 根地址 |
19
+ | --- | --- |
20
+ | `local` | `http://127.0.0.1:8793` |
21
+ | `dev` | `https://deepflowworkbench.dev.deepwisdomai.com` |
22
+ | `prod`(国内生产 `prod-cn`) | `https://deepflowworkbench.deepwisdomai.com` |
23
+
24
+ CLI 不会根据环境名自动切换地址。确认环境后显式设置 `DF_WORKBENCH_BASE_URL`;Token 与工作区也须属于该入口,不能复用 dev 身份去操作 prod。生产写操作只执行当前任务已授权的范围。
25
+
18
26
  CLI 配置:
19
27
 
20
28
  ```text
@@ -32,7 +40,11 @@ df-workbench config --json
32
40
 
33
41
  `me` 返回当前用户和可访问工作区;`config` 返回认证模式、当前工作区、Task 写入能力和 Workbench UI 能力。不要打印 Token、Cookie 或其他凭据。
34
42
 
35
- local 环境只允许从本机回环地址访问;OIDC 环境使用 Workbench personal token。Token 应通过公司内部配置或 Skill 平台提供,不在命令参数中传递。
43
+ local 环境只允许从本机回环地址访问,无需 Token;OIDC 环境使用 Workbench personal token。
44
+
45
+ Token 获取入口:使用公司账号登录目标环境 Workbench,打开「头像 → 个人设置 → 个人 Token → 创建 Token」,填写用途名称和到期时间。完整 Token 只在创建时显示一次,将其配置为 `DF_WORKBENCH_TOKEN`;`DF_WORKBENCH_BASE_URL` 使用同一环境的 Workbench 根地址。也可通过公司内部配置或 Skill 平台的个人凭据配置提供,不在命令参数中传递,不打印凭据。
46
+
47
+ 列表仅显示前缀和使用状态,无法找回完整 Token;遗失、到期或不再使用时,在同一入口撤销并按需新建。独立管理员账号不支持创建个人 Token,需使用公司账号登录。网页退出不会撤销 Token。
36
48
 
37
49
  ## 用户任务流程
38
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metagptx/deepflow-suite-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Agent-friendly installer and distribution CLI for the DeepFlow suite.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-agent-ctrl","sourceCommit":"e3aa5dee2eb31a7267e69f48127175deab664fe0","sourceDirty":false}
1
+ {"version":"0.3.1","project":"deepflow-agent-ctrl","sourceCommit":"10e745773559f4712b04ab61915a018995e30630","sourceDirty":false}
@@ -1 +1 @@
1
- {"version":"0.3.0","project":"deepflow-agent-runtime","sourceCommit":"12624d320d032b65d8f4ed3fdd0193d33d95be7d","sourceDirty":false}
1
+ {"version":"0.3.1","project":"deepflow-agent-runtime","sourceCommit":"420ebb91ed4cf92ca81eb8c295cc111f94b9da2c","sourceDirty":false}
@@ -0,0 +1,44 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdtemp, open, link, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const terminal = (status) => ['succeeded', 'failed', 'cancelled'].includes(status);
6
+ const statuses = ['pending', 'waiting_for_runtime', 'claimed', 'running', 'succeeded', 'failed', 'cancelled'];
7
+ export function resultView(inv, maxBytes) {
8
+ const view = { invocation_id: inv.id, session_id: inv.session_id, status: inv.status, terminal: terminal(inv.status), result_state: terminal(inv.status) ? 'missing' : 'pending', output_present: false, output: '', output_bytes: 0, output_returned_bytes: 0, output_truncated: false };
9
+ if (inv.finished_at) view.finished_at = inv.finished_at;
10
+ if (inv.error) view.error = inv.error;
11
+ const invalid = (message) => { view.result_state = 'invalid'; view.format_error = message; return { view, output: '' }; };
12
+ if (!statuses.includes(inv.status)) { view.result_state = 'invalid'; view.format_error = 'unknown invocation status'; }
13
+ if (!inv.result) return { view, output: '' };
14
+ let result;
15
+ try { result = JSON.parse(inv.result); } catch { return invalid('result is not a JSON object'); }
16
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return invalid('result is not a JSON object');
17
+ for (const [field, target] of [['error', 'result_error'], ['failure_reason', 'failure_reason']]) {
18
+ if (!Object.hasOwn(result, field) || result[field] === null) continue;
19
+ if (typeof result[field] !== 'string') return invalid(`${field} is not a string`);
20
+ if (result[field]) view[target] = result[field];
21
+ }
22
+ if (!Object.hasOwn(result, 'output') || result.output === null) return { view, output: '' };
23
+ if (typeof result.output !== 'string') return invalid('output is not a string');
24
+ const bytes = Buffer.from(result.output);
25
+ view.output_present = true; view.output_bytes = bytes.length;
26
+ if (view.terminal) view.result_state = 'available';
27
+ view.output_sha256 = createHash('sha256').update(bytes).digest('hex');
28
+ let end = Math.min(maxBytes, bytes.length);
29
+ if (end < bytes.length) while (end > 0 && (bytes[end] & 0xc0) === 0x80) end--;
30
+ view.output = bytes.subarray(0, end).toString('utf8');
31
+ view.output_truncated = bytes.length > maxBytes;
32
+ view.output_returned_bytes = end;
33
+ return { view, output: result.output };
34
+ }
35
+
36
+ // Publish a private file atomically without replacing a concurrent destination.
37
+ export async function writeResultFile(destination, data) {
38
+ const directory = await mkdtemp(path.join(path.dirname(destination), '.invocation-result-'));
39
+ try {
40
+ const staging = path.join(directory, 'output'), file = await open(staging, 'wx', 0o600);
41
+ try { await file.writeFile(data); } finally { await file.close(); }
42
+ await link(staging, destination);
43
+ } finally { await rm(directory, { recursive: true, force: true }); }
44
+ }
@@ -1,9 +1,10 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, lstat } from 'node:fs/promises';
2
2
  import { spawn } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { setTimeout as delay } from 'node:timers/promises';
6
6
  import { usage } from './usage.mjs';
7
+ import { resultView, writeResultFile } from './result.mjs';
7
8
 
8
9
  const terminal = (status) => ['succeeded', 'failed', 'cancelled'].includes(status);
9
10
  const decode = (raw) => JSON.parse(raw, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value);
@@ -49,11 +50,11 @@ export class RuntimeCLI {
49
50
  if (!addr) this.environment('MULTICA_SERVER_URL is not set, so there is no runtime API to talk to; this command only works inside a running task');
50
51
  return addr.replace(/\/+$/, '');
51
52
  }
52
- async request(method, route, body, { headers = {}, raw = false, stream = false, signal } = {}) {
53
+ async request(method, route, body, { headers = {}, raw = false, stream = false, signal, responseLimit = 0, timeout = false } = {}) {
53
54
  const addr = this.address();
54
55
  let response;
55
56
  try {
56
- response = await fetch(addr + route, { method, headers: { ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}), ...(stream ? { Accept: 'text/event-stream' } : {}), ...headers }, body: body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body), signal: signal || (!stream && this.self ? AbortSignal.timeout(30000) : undefined) });
57
+ response = await fetch(addr + route, { method, headers: { ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}), ...(stream ? { Accept: 'text/event-stream' } : {}), ...headers }, body: body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body), signal: signal || (!stream && (this.self || timeout) ? AbortSignal.timeout(30000) : undefined) });
57
58
  } catch (error) { throw new Failure(`cannot reach the runtime API at ${addr}: ${error.message}`); }
58
59
  if (!response.ok) {
59
60
  const text = (await response.text()).slice(0, 65536); let error;
@@ -62,7 +63,16 @@ export class RuntimeCLI {
62
63
  }
63
64
  if (stream) return response.body;
64
65
  if (method === 'DELETE') { await response.body?.cancel(); return; }
65
- const text = await response.text();
66
+ let text;
67
+ if (responseLimit) {
68
+ const chunks = []; let bytes = 0;
69
+ for await (const chunk of response.body) {
70
+ bytes += chunk.length;
71
+ if (bytes > responseLimit) throw new Failure(`response exceeds ${responseLimit} bytes`);
72
+ chunks.push(chunk);
73
+ }
74
+ text = Buffer.concat(chunks).toString('utf8');
75
+ } else text = await response.text();
66
76
  try { const value = decode(text); return raw ? text.trim() : value; } catch (error) { throw new Failure(`decode response: ${error.message}`); }
67
77
  }
68
78
  async ownSession() {
@@ -86,6 +96,27 @@ export class RuntimeCLI {
86
96
  return encodeURIComponent(rest[0]);
87
97
  }
88
98
  async getJSON(route) { this.stdout.write(await this.request('GET', route, undefined, { raw: true }) + '\n'); }
99
+ async invocationCmd(command, args) {
100
+ const form = `usage: ${this.name} invocation result <invocation-id> [--json] [--max-bytes N] [--output-file PATH]`;
101
+ if (command !== 'result') this.fail(form);
102
+ if (args.includes('--help') || args.includes('-h')) { this.stdout.write(form + '\n'); return; }
103
+ const { v, rest } = flags(args, ['max-bytes', 'output-file'], ['json']);
104
+ numeric(v['max-bytes'], 'max-bytes', 1n << 20n);
105
+ const maxBytes = Number(v['max-bytes'] ?? 4096);
106
+ if (rest.length !== 1 || !rest[0].trim() || maxBytes < 1) this.fail(form);
107
+ let destination;
108
+ if ('output-file' in v) {
109
+ if (!v['output-file'].trim()) this.fail('--output-file must not be empty');
110
+ destination = path.resolve(v['output-file']);
111
+ try { await lstat(destination); this.fail(`output file already exists: ${destination}`); }
112
+ catch (error) { if (error.code !== 'ENOENT') this.fail(error.message); }
113
+ }
114
+ const inv = await this.request('GET', `/v1/invocations/${encodeURIComponent(rest[0])}`, undefined, { responseLimit: 64 << 20, timeout: true });
115
+ if (!inv || inv.id !== rest[0] || typeof inv.session_id !== 'string' || !inv.session_id || typeof inv.status !== 'string' || !inv.status) throw new Failure('invalid invocation identity or status in response');
116
+ const { view, output } = resultView(inv, maxBytes);
117
+ if (destination && view.output_present) { await writeResultFile(destination, output); view.output_file = destination; }
118
+ this.stdout.write(JSON.stringify(view, null, v.json ? undefined : 2) + '\n');
119
+ }
89
120
  printSession(s) {
90
121
  const rows = [['id', s.id], ['provider', s.provider], ['status', s.status]];
91
122
  if (s.client_ref) rows.push(['client_ref', s.client_ref]);
@@ -301,6 +332,7 @@ export class RuntimeCLI {
301
332
  }
302
333
  const [group, command, ...rest] = args;
303
334
  if (group === 'events') await this.events(args.slice(1));
335
+ else if (group === 'invocation') await this.invocationCmd(command, rest);
304
336
  else if (['session', 'repo', 'msg', 'ops'].includes(group)) {
305
337
  if (!command) this.groupUsage(group);
306
338
  else if (group === 'session') await this.sessionCmd(command, rest);
@@ -1,13 +1,13 @@
1
1
  export const usage = {
2
2
  "df-agentrt": {
3
- "": "df-agentrt -- DeepFlow Agent Runtime, from inside a task\n\nTHIS SESSION\n df-agentrt session info Which session this is:\n provider, model, working\n directory, repositories,\n and whether the\n conversation history\n survived into this turn.\n df-agentrt session turns [--session ID] [--json] Every turn; defaults to\n this session.\n df-agentrt events [--session ID] [--after N] The message stream. Also\n --limit N, --raw and\n --follow.\n df-agentrt repo list The repositories this\n session may check out.\n df-agentrt repo search <name> Find an application and\n the repository its code\n lives in.\n df-agentrt repo add <url|code> ... Make another repository\n available and check it out\n now. Prints the absolute\n path of each checkout.\n df-agentrt msg cancel [invocation-id] Ask a turn to stop;\n defaults to the current\n turn.\n\nOTHER SESSIONS\n df-agentrt session ls Sessions on this runtime.\n df-agentrt session get <id> [--json] One session in full.\n df-agentrt session create ... Start a new session. It\n runs an agent and costs\n money, and it does not end\n by itself.\n df-agentrt msg send <id> <text> Queue a turn on another\n session. Same caveat.\n\nRUNTIME\n df-agentrt ops runtimes [--json] Which execution engines\n are registered and alive.\n df-agentrt ops orphans Where the records and the\n world have drifted apart.\n\nRun `df-agentrt <group>` for the flags of one group.\n",
3
+ "": "df-agentrt -- DeepFlow Agent Runtime, from inside a task\n\nTHIS SESSION\n df-agentrt session info Which session this is:\n provider, model, working\n directory, repositories,\n and whether the\n conversation history\n survived into this turn.\n df-agentrt session turns [--session ID] [--json] Every turn; defaults to\n this session.\n df-agentrt events [--session ID] [--after N] The message stream. Also\n --limit N, --raw and\n --follow.\n df-agentrt repo list The repositories this\n session may check out.\n df-agentrt repo search <name> Find an application and\n the repository its code\n lives in.\n df-agentrt repo add <url|code> ... Make another repository\n available and check it out\n now. Prints the absolute\n path of each checkout.\n df-agentrt msg cancel [invocation-id] Ask a turn to stop;\n defaults to the current\n turn.\n\nOTHER SESSIONS\n df-agentrt invocation result <id> [--json] Read a bounded final result.\n --output-file saves full text.\n df-agentrt session ls Sessions on this runtime.\n df-agentrt session get <id> [--json] One session in full.\n df-agentrt session create ... Start a new session. It\n runs an agent and costs\n money, and it does not end\n by itself.\n df-agentrt msg send <id> <text> Queue a turn on another\n session. Same caveat.\n\nRUNTIME\n df-agentrt ops runtimes [--json] Which execution engines\n are registered and alive.\n df-agentrt ops orphans Where the records and the\n world have drifted apart.\n\nRun `df-agentrt <group>` for the flags of one group.\n",
4
4
  "session": "df-agentrt session <command>\n\n info This session. Includes whether the conversation history\n carried over into this turn -- if it did not, you have the\n same files but none of what was said before.\n turns [--session ID] [--json]\n Every turn; defaults to this session, oldest first.\n ls Sessions on this runtime.\n get <id> [--json] [--metadata-source S] [--metadata-query-key K]\n Another session in full.\n create --provider P [--model M] [--thinking-level L] [--instructions S]\n [--skill S ...] [--repo R ...] [--client-ref REF] [--mcp-config FILE]\n [--metadata JSON]\n [--idempotency-key KEY]\n Start a new session. It runs an agent and costs money, and\n nothing ends it for you.\n release <id> Stop a session accepting turns and ask for its working\n directory back.\n rm <id> Delete a session's record, message stream included.\n\nEverything about a session except its repository set is fixed at creation.\nFor repositories see `df-agentrt repo` -- those can be appended afterwards.\n",
5
5
  "repo": "df-agentrt repo <command>\n\n list The repositories this session may check out.\n search <name> Find an application and the repository its code\n lives in. Use this when you know what a thing is\n called but not where its code is.\n add <url|app-code> ... Make another repository available to this session\n [--ref REF] and check it out now. Prints the absolute path of\n each checkout on stdout; nothing else to run\n afterwards. --ref takes one repository only.\n\nAn argument containing \"://\" or \"@\" is treated as a clone url and stored\nverbatim; anything else is treated as an application code and looked up for\nyou. A code can never contain either, so the two cannot be confused.\n\nAppend-only: there is no remove. Removing a repository you are working inside\nwould break this turn.\n",
6
6
  "msg": "df-agentrt msg <command>\n\n send <session-id> <text> [--follow] [--idempotency-key KEY] [--raw]\n Queue a turn on another session. That session runs an agent and\n costs money; it is not this one. --follow waits for the turn to\n finish, which blocks you for as long as it runs.\n cancel [<turn-id>]\n Ask a turn to stop. With no argument, the turn you are executing\n right now -- you will be asked to wind down, not killed outright.\n",
7
7
  "ops": "df-agentrt ops <command>\n\n runtimes [--json] Which daemons have registered, and which are still alive.\n orphans [--fix] Where the records and the world have drifted apart.\n Reports only unless --fix is given; --fix fails stranded\n turns on other sessions and cannot be undone.\n"
8
8
  },
9
9
  "agentrt": {
10
- "": "agentrt -- DeepFlow Agent Runtime CLI\n\nUSAGE\n agentrt <group> <command> [flags]\n\nGROUPS\n session info | create | get | ls | turns | release | rm\n repo ls | add | search\n msg send | cancel\n events events <session> [--after N]\n ops runtimes | orphans\n\nENVIRONMENT\n AGENTRT_ADDR Base URL of the runtime API (default http://127.0.0.1:8141)\n\nRun `agentrt <group>` for that group's commands.\n",
10
+ "": "agentrt -- DeepFlow Agent Runtime CLI\n\nUSAGE\n agentrt <group> <command> [flags]\n\nGROUPS\n session info | create | get | ls | turns | release | rm\n invocation result <id> [--json] [--max-bytes N] [--output-file PATH]\n repo ls | add | search\n msg send | cancel\n events events <session> [--after N]\n ops runtimes | orphans\n\nENVIRONMENT\n AGENTRT_ADDR Base URL of the runtime API (default http://127.0.0.1:8141)\n\nRun `agentrt <group>` for that group's commands.\n",
11
11
  "session": "agentrt session <command>\n\n info <session-id> The same view as get. Named for the agent-side CLI,\n where it is the command for \"which session am I in\".\n create --provider P [--model M] [--thinking-level L] [--instructions S]\n [--skill S ...] [--repo R ...] [--client-ref REF] [--mcp-config FILE]\n [--idempotency-key KEY]\n get <session-id> [--json] [--metadata-source S] [--metadata-query-key K]\n\t ls [--status active|released] [--limit N] [--metadata-source S] [--metadata-query-key K]\n turns <session-id> [--json] (or --session <session-id>)\n Every turn of one session, oldest first -- the same\n timeline the web UI draws, from the same endpoint.\n release <session-id> Stop accepting turns; Phase 1 keeps the Worker workspace.\n Events and active workspace reads stay available afterwards.\n\t rm <session-id> Delete the record. Workspace archive is not a Phase 1 operation.\n\nEverything here except the repository set is fixed when the session is created.\nFor repositories, see `agentrt repo` -- they can be appended to afterwards.\n",
12
12
  "repo": "agentrt repo <command>\n\n ls <session-id> Repositories this session may check out.\n add <session-id> <url|app-code> ... Append repositories. Takes effect on the\n next checkout, including mid-turn.\n [--ref REF] Branch, tag or commit. One repository only.\n search <name> Find an application and its repository in\n the registry.\n\nAn argument containing \"://\" or \"@\" is taken as a clone URL and stored\nverbatim; anything else is taken as an application code and resolved against\nthe registry. A code can never contain either, so the two cannot be confused.\n\nAppend-only: there is no remove. Removing a repository the agent is working\ninside would break the turn in flight.\n",
13
13
  "msg": "agentrt msg <command>\n\n send <session-id> <text> [--follow] [--idempotency-key KEY] [--raw]\n Queue one turn. --follow attaches to the event stream and waits for\n the turn to finish, exiting non-zero if it failed.\n cancel <invocation-id>\n Ask for a running turn to stop. Returns as soon as the request is\n recorded, which is before the agent has actually wound down.\n",
@@ -2,9 +2,10 @@ export const help = `df-task — DeepFlow Task fact log CLI
2
2
 
3
3
  Usage:
4
4
  df-task ls [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
5
- df-task get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]
5
+ df-task get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]
6
6
  df-task create --title <text> [--description <text>] [--message <text>] [--metadata <json>]
7
7
  df-task facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
8
+ df-task facts <task-id> --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]
8
9
  df-task append <task-id> --type <type> --file payload.json [--idempotency-key <k>]
9
10
  df-task stream [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]
10
11
  df-task title <task-id> --title <text>
@@ -41,9 +42,10 @@ export const commands = {
41
42
  "writes": false
42
43
  },
43
44
  "get": {
44
- "usage": "get <task-id> [--metadata-source <source>] [--metadata-query-key <key>]",
45
- "description": "Read one task's identity.",
45
+ "usage": "get <task-id> [--fields <paths> --json] [--metadata-source <source>] [--metadata-query-key <key>]",
46
+ "description": "Read one task's identity. --fields selects known fields and object paths; absent paths are omitted.",
46
47
  "flags": [
48
+ "--fields",
47
49
  "--metadata-source",
48
50
  "--metadata-query-key"
49
51
  ],
@@ -67,9 +69,14 @@ export const commands = {
67
69
  "writes": true
68
70
  },
69
71
  "facts": {
70
- "usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--metadata-source <source>] [--metadata-query-key <key>]",
71
- "description": "Read one task's facts in seq order.",
72
+ "usage": "facts <task-id> [--after <seq>] [--types <a,b>] [--limit <n>] [--fields <paths> --json | --all --output-file <path> --json [--max-pages <n>] [--max-bytes <n>]] [--metadata-source <source>] [--metadata-query-key <key>]",
73
+ "description": "Read facts in seq order. --fields selects fields per fact, retaining pagination. --all writes complete raw pages to a private file and prints a manifest; limits default to 10 pages / 8 MiB. Only an observed empty page marks complete. Partial reads exit 1.",
72
74
  "flags": [
75
+ "--fields",
76
+ "--all",
77
+ "--output-file",
78
+ "--max-pages",
79
+ "--max-bytes",
73
80
  "--after",
74
81
  "--types",
75
82
  "--limit",
@@ -4,9 +4,10 @@ import { fileURLToPath } from 'node:url';
4
4
  import { realpathSync } from 'node:fs';
5
5
  import { help, commands } from './commands.mjs';
6
6
  import { factTypes, actorKinds } from './protocol.mjs';
7
+ import { validateReadOptions, projectResponse, readAllFacts, readResponse } from './read.mjs';
7
8
 
8
9
  const uuid = /^(?:urn:uuid:)?(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})$/i;
9
- const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags)]);
10
+ const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags).filter((flag) => flag !== '--all')]);
10
11
  export function parse(args) {
11
12
  const o = { command: '', args: [], values: {}, json: false, help: false };
12
13
  const seen = new Set();
@@ -20,6 +21,9 @@ export function parse(args) {
20
21
  if (['--help', '-h', '--json'].includes(key)) {
21
22
  if (equals >= 0) throw Error(`${key} does not take a value`);
22
23
  o[key === '--json' ? 'json' : 'help'] = true;
24
+ } else if (key === '--all') {
25
+ if (equals >= 0) throw Error(`${key} does not take a value`);
26
+ o.values[key] = 'true';
23
27
  } else if (valueFlags.has(key)) {
24
28
  if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
25
29
  o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
@@ -57,6 +61,7 @@ export function validate(o, env) {
57
61
  if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
58
62
  if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
59
63
  }
64
+ validateReadOptions(o);
60
65
  }
61
66
  function object(raw, message) {
62
67
  let value;
@@ -127,7 +132,7 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
127
132
  stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-task ${version}`) + '\n');
128
133
  return 0;
129
134
  }
130
- let o, base, req;
135
+ let o, base, initialRequest;
131
136
  try {
132
137
  o = parse(args);
133
138
  if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
@@ -139,15 +144,22 @@ export async function run(args, { env = process.env, stdout = process.stdout, st
139
144
  base = o.values['--base-url'] || env.DF_TASK_BASE_URL || 'http://127.0.0.1:8090';
140
145
  const url = new URL(base);
141
146
  if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
142
- req = await requestFor(o);
147
+ initialRequest = await requestFor(o);
143
148
  } catch (error) { stderr.write(error.message + '\n'); return 2; }
144
- try {
149
+ const request = async (options, limit = 0) => {
150
+ const req = options === o ? initialRequest : await requestFor(options);
145
151
  const headers = req.body ? { 'Content-Type': 'application/json' } : {};
146
152
  if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
147
153
  const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
148
- const data = await response.text();
154
+ const data = await readResponse(response, limit);
149
155
  if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
150
156
  try { JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
157
+ return data;
158
+ };
159
+ if (o.values['--all']) return readAllFacts(o, base, request, stdout, stderr);
160
+ try {
161
+ let data = await request(o, '--fields' in o.values ? 64 << 20 : 0);
162
+ if ('--fields' in o.values) data = projectResponse(o.command, data, o.values['--fields'].split(','));
151
163
  if (o.json) stdout.write(data + '\n'); else human(o.command, data, stdout);
152
164
  return 0;
153
165
  } catch (error) { stderr.write(error.message + '\n'); return 1; }