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

@tutti-os/tutti-cli

v0.1.4

Published

Tutti account login, Room discovery and Task commands

Readme

Tutti public CLI

独立的 TypeScript CLI,使用 Node.js 24+。npm 包名为 @tutti-os/tutti-cli,安装后命令为 tutti-cli。 直接以当前用户身份调用 Tutti 服务;运行 CLI 的机器无需安装 Computer 或启动 desktopd。 触发 Agent 执行时,由服务端路由到所选 Agent 对应的 Computer,执行可用性和权限由服务端判断。

构建和安装

从 Tutti 仓库根目录执行:

pnpm install --frozen-lockfile
pnpm --filter @tutti-os/tutti-cli build
cd apps/tutti-cli
npm pack
npm install --global ./tutti-os-tutti-cli-0.1.0-beta.2.tgz
tutti-cli --help

包内包含编译后的 JavaScript 和从仓库唯一默认配置生成的配置快照,无运行时 npm 依赖。 也可以在源码目录直接运行 node apps/tutti-cli/src/main.ts --help。 本地构建和打包不会自动发布 npm 包。

GitHub Actions 发布

合入 main 后,在 Actions → Tutti CLI Release → Run workflow 选择 main:

| 输入 | beta 包 | 正式包 | | --------- | ----------------- | ------------ | | channel | beta | production | | version | 如 0.1.0-beta.3 | 如 0.1.0 | | npm 标签 | beta | latest |

dry_run=true 只构建、测试和保存 tarball,不发布。版本号必须未被发布,且 beta 只能是 X.Y.Z-beta.N、正式版只能是 X.Y.Z。版本只写入该次运行的 package.json,不自动提交回仓库; CLI 的 --version 和登录 clientVersion 都读取同一份包信息。发布源固定为触发时的 main commit。

流程依次完成版本校验、类型检查、源码接口测试、打包、安装 tarball 后再次测试;随后独立发布 job 验证下载产物的 SHA1/SHA512,使用组织级 npm 凭据发布同一 tarball,并核对注册表版本、标签和校验值。 产物保留 30 天,版本、来源 commit 和安装命令显示在运行摘要中。发布串行执行,不取消已开始的发布。 注册表可见性最多等待约 2 分钟;超时只报告待核实,不重复发布。重跑前应先检查 npm,已存在版本会被拒绝。

# 只验证下一个 beta 包
gh workflow run tutti-cli-release.yml --ref main -f channel=beta -f version=0.1.0-beta.3 -f dry_run=true
# 发布 beta 包
gh workflow run tutti-cli-release.yml --ref main -f channel=beta -f version=0.1.0-beta.3
# 发布正式包
gh workflow run tutti-cli-release.yml --ref main -f channel=production -f version=0.1.0

仓库已获组织级 Actions Secret NPM_TOKEN 的访问授权,流水线直接复用它;Token 只在发包步骤 映射到 NODE_AUTH_TOKEN,构建、依赖安装和测试步骤均不注入。发包前先校验凭据有效,随后发布已验证的产物。 该 Token 需有效、具有 @tutti-os/tutti-cli 写权限,并允许 CI 非交互发布;如果 npm 要求交互式二次验证, 流水线会失败,需要管理员更新组织 Secret 的权限配置。Secret 对仓库可用不代表这些权限已验证,首次真实发布仍需验收。

发布目标固定为 https://registry.npmjs.org,不使用组织的 NPM_REGISTRY_URL 改写已公开包的目标 registry。 当前实现不要求新增 npm Trusted Publisher,也不自动回退到其他凭据。后续如需改为 OIDC,可单独迁移, 参考 npm Trusted Publishing 文档。

登录

tutti-cli login
tutti-cli whoami --json
tutti-cli logout

login 打开现有 Web 登录页面,复用已有账号登录方式。--no-browser 仅打印登录 URL, 用户在本机浏览器打开后继续;它不是设备码登录,浏览器必须能访问运行 CLI 的本机回调。 登录最长等待 5 分钟,Ctrl+C 取消并释放监听器。

使用 Web 已有的 cli_bridge:绑定 127.0.0.1:38473–38492 中的空闲端口,生成随机 attempt/token/device ID;接收浏览器的一次性 transfer code,向 Account 服务兑换独立 WebSession, 查询账号身份后原子保存。浏览器可以给 state 增加 provider/nonce,原有 handoff 身份字段必须保持一致。 沿用 bridge 的 appCallbackUrl=tutti-amp://login/callback 兼容元数据,不调用该应用协议。 GET 回调成功保存登录态后,以 303 跳回同一站点的 /auth/cli/result?status=success; 已验证回调的兑换失败与用户取消分别返回 status=error、status=cancelled。 回跳只携带结果状态,设置 no-store 与 no-referrer,不转发凭据或 OAuth state;POST 完成接口继续返回原 JSON。 发布此版本前,须先部署 tsh-web/apps/auth 的结果页;旧 CLI 仍沿用本地结果页。 拒绝错误 Origin、Host、state、重复完成和超限 body。CLI 不读取浏览器 Cookie,也不读取 Computer 的凭据。

凭据默认保存在 ~/.tutti-cli/session.json;POSIX 目录权限为 0700,文件为 0600。 Windows 使用用户主目录的访问控制,POSIX mode 检查不等价于 Windows ACL 验证。 登录失败保留此前的会话;退出先撤销服务端会话再删除本地文件,服务端不可用时保留文件供再次退出。 HTTP 请求禁止跟随重定向,凭据只发往保存时对应的站点。whoami 和登录成功输出不包含会话密钥。

开发环境可显式设置:

TUTTI_CLI_ORIGIN=https://your-tutti-host.example tutti-cli login
TUTTI_CLI_STATE_DIR=/private/path/tutti-cli tutti-cli login

自定义 origin 必须承载 /auth/login、/auth/cli/result、/api/account 和 /api/desktop 入口。 默认站点为 https://tutti.sh;只允许 HTTPS,开发接口测试额外允许 loopback HTTP。 切换站点须重新登录或设置另一份独立状态目录,不能把某站点的会话发送到另一站点。

查询房间

tutti-cli room list
tutti-cli room list --type local
tutti-cli room list --type cloud
tutti-cli room list --page-size 20 --json
tutti-cli room list --page-size 20 --page-token "上一页的 nextPageToken" --json
tutti-cli room list --all --type local --json

使用 CLI 当前登录用户的房间目录,包含自己创建及有权访问的房间;默认只读一页,每页 100 条。 --page-size 可设置为 1–200,--page-token 原样传入上次返回的游标。 --all 从第一页开始读取全部分页,可与 --page-size 配合,但不能与 --page-token 同时使用。 默认按 Local rooms / Cloud rooms 分组展示本次结果的数量、房间 ID 和名称,本地房间有绑定 Computer ID 时一并显示。 --type all|local|cloud 默认 all,对本次读取的页筛选,不依赖本机 Computer 是否安装或在线。 服务端不提供类型筛选参数,因此筛选后可能不足 page-size 条甚至为空;只要有 nextPageToken 就仍有后续页。 使用 --all --type local|cloud 可获取该类型的完整列表。 本地指 Native directory Room,也包含绑定在其他电脑上的房间;云端指 Cloud Room,沿用 Web 的分类语义。

--json 输出单行 { "rooms": [...], "nextPageToken": "..." },无下一页时游标为空字符串; 文本模式也显示下一页游标。rooms 中每项包含 id、name、type(local 或 cloud), 以及接口提供的可选 nativeComputerId。空列表返回 rooms: [];文本模式显示对应分组数量为 0,不能据此判断已到最后一页。 分类使用服务端 executionLocationKind 的数字或 protobuf 名称;缺失为协议默认云端。 未知分类、非法响应、重复分页游标或任一分页失败均报错,不把部分结果作为完整列表输出。 此命令只查询目录,不启动 Room runtime,也不读取本机文件或 Computer 凭据。

创建和修改 Task

用 tutti-cli room list 获取 roomId;所有 Task 命令显式指定 --room-id,权限仍由服务端按 Room 成员身份判断。

tutti-cli task create --room-id ROOM_ID --title "排查构建失败" \
  --description "复现步骤" --priority high --json

tutti-cli task edit --room-id ROOM_ID --task-id TASK_UUID \
  --title "修复构建失败" --description-file ./description.md --json

tutti-cli task edit --room-id ROOM_ID --task-id TASK_UUID --description "" --json

tutti-cli task show --room-id ROOM_ID --task-id TASK_UUID --json
  • 创建默认使用 todo、未指派语义;可通过 --assignee-user-id 指派房间成员,不启动 Agent。优先级为 none(默认)、low、medium、high、urgent;--project-id 可关联同 Room Project。
  • 标题 trim 后为 1–512 个 Unicode 字符。描述是纯文本,保留换行,转换成既有 Task document。
  • 修改至少指定标题、描述或负责人。--description 与 --description-file 互斥;文件为 UTF-8,最大 1 MiB,- 从 stdin 读取。未指定字段保持原值,显式空描述清空内容。
  • 修改只发送包含改动字段的 updateMask,没有先读取再覆盖的过程。
  • --operation-id UUID 默认生成 UUID;创建时同时作为 Task ID。失败会输出该 ID,写入从不自动重试。不确定的写入用相同 ID 和相同输入重试;也可先 task show 核实结果。
  • --json 输出单行服务端 JSON;默认输出缩进 JSON。错误写 stderr,退出码 1,取消为 130。未知或不适用的参数明确报错。

修改 Task 状态

# 把已有任务改回“待办”
tutti-cli task status --room-id ROOM_ID --task-id TASK_UUID --status todo --json

tutti-cli task status --room-id ROOM_ID --task-id TASK_UUID \
  --status in_review --operation-id OPERATION_UUID --json

--status 必填,支持 todo、in_progress、in_review、blocked、done。 此全局 CLI 使用当前登录用户的成员权限;与仅能汇报当前 Agent Task 状态的执行内 CLI 分开。 复用看板的 POST /v2/rooms/{roomId}/tasks/{taskId}:move(RoomTaskService_MoveRoomTask), 不通过普通编辑接口修改状态。状态迁移不会启动或中断 Agent,不改变负责人、标题、描述或关闭状态; 已关闭 Task、权限不足或幂等输入冲突等错误由服务端拒绝,CLI 不自动重新打开任务或重试。 请求显式发送 confirmActiveExecution=false,与 Web 一致,避免旧服务端中断活动执行。

--operation-id UUID 省略时生成 UUID;失败保留 Task ID 和操作 ID,退出码为 1。 结果不确定时先用 task show 核实,再以相同操作 ID 和输入重试;新操作使用新的 ID。 成功输出完整服务端回执(--json 为单行 JSON)。幂等重放返回 Task 的当前状态,可能包含之后的状态变更, 因此回执状态不一定仍是原操作请求的状态。task create 和 task edit 的参数与默认行为保持不变。

指派 Task 负责人

先查询房间成员,选择准确的 userId;显示名称可能重名,不作为指派身份:

tutti-cli room members --room-id ROOM_ID --json

# 创建时实际指派负责人
tutti-cli task create --room-id ROOM_ID --title "修复文件链接无法打开" \
  --assignee-user-id USER_ID --json

# 修改已有 Task 的负责人,可单独使用,也可与标题、描述修改一起提交
tutti-cli task edit --room-id ROOM_ID --task-id TASK_UUID \
  --assignee-user-id USER_ID --json

# 取消指派(包括此前指派给 Agent 的 Task)
tutti-cli task edit --room-id ROOM_ID --task-id TASK_UUID --unassign --json

room members 返回 { roomId, members: [{ userId, displayName, role }] },角色为 owner 或 collaborator。 复用看板的房间成员列表接口,一次读取完整列表,接口没有分页参数;不会输出分享链接、密码等其他分享设置。 成员没有显示名称时,displayName 使用用户 ID。权限不足或响应不完整时明确报错。

--assignee-user-id 与 --unassign 互斥,创建和修改都支持;不传时创建保持未指派,修改保留原负责人。 用户 ID 是不透明身份,不要求 UUID;不得用用户名替代。人员指派写入真正的 assignee.memberUserId, 取消写入 assignee.unassigned=true;修改通过 updateMask=assignee 精确更新,不覆盖其他字段。 Task 只有一个负责人,换成人员会替换此前的人员或 Agent 指派;它不会启动 Agent,也不是取消已有执行的命令。 服务端按当前房间成员身份及操作权限校验目标,CLI 不用过期目录代替写入时的权限判断。 沿用 --operation-id 幂等语义,不自动重试写入;成功回执必须确认请求的负责人或未指派状态。

标签与 Bug 登记

标签属于房间。先查询或创建标签,再使用返回的 labelId 给 Bug Task 打标签:

tutti-cli label list --room-id ROOM_ID --json

tutti-cli label create --room-id ROOM_ID --name "Bug" --color '#EF4444' --json

tutti-cli task create --room-id ROOM_ID --title "文件链接无法打开" \
  --description "复现步骤及预期结果" --priority high \
  --assignee-user-id USER_ID --label-id BUG_LABEL_UUID --label-id WEB_LABEL_UUID --json

# 给已有 Task 添加或移除一个标签,保留其他标签
tutti-cli task label add --room-id ROOM_ID --task-id TASK_UUID --label-id LABEL_UUID --json
tutti-cli task label remove --room-id ROOM_ID --task-id TASK_UUID --label-id LABEL_UUID --json

label list 返回 { roomId, labels: [{ labelId, roomId, name, color }] };接口没有分页参数。 创建标签要求名称 trim 后为 1–32 个 Unicode 字符,颜色为 #RRGGBB(建议加引号),大小写均可,写入时统一大写。 label create 可用 --label-id UUID 指定身份,省略则生成 UUID;它没有 --operation-id 参数或幂等重放保证。 创建失败会输出标签 ID,先用 label list 核实是否已创建。同名或已有 ID 冲突按服务端错误处理,不静默复用或覆盖。

task create 的 --label-id 可重复,但各 ID 必须是不同的有效 UUID;标签随 Task 创建请求的 labelIds 一次提交, 不采用先创建 Task 再逐个打标签的流程。目标标签须已存在于同一房间,最终权限与存在性由服务端验证。 省略参数沿用无初始标签行为。task edit 不接受此参数;已有 Task 使用 task label add/remove,每次仅操作一个标签。

Task 标签添加/移除沿用 --operation-id UUID,省略则生成;不自动重试写入,错误保留 Task、标签和操作 ID。 不确定结果先用 task show 确认,只使用相同操作 ID 和输入重试;更改输入或发起新操作使用新的操作 ID。 服务端重放回执返回 Task 的当前标签状态,可能已经包含之后的修改,CLI 不把它误判成原操作失败。 移除标签只解除与该 Task 的关联,不删除房间标签,也不触发 Agent。 本次不提供房间标签重命名、改色或删除命令。

Task 评论与回复

统一使用 task comment,省略 --reply-to 表示发顶层评论。默认只发表评论;只有显式指定 --agent-target-id 才会在保存评论后触发该 Agent,避免意外触发 Task 已指派的 Agent。

# 1. 顶层评论,不执行 Agent
tutti-cli task comment --room-id ROOM_ID --task-id TASK_UUID \
  --content "补充一下验收标准" --json

# 2. 回复指定评论,不执行 Agent
tutti-cli task comment --room-id ROOM_ID --task-id TASK_UUID \
  --reply-to COMMENT_UUID --content "这个方案可以" --json

# 3. 顶层评论,并触发指定 Agent
tutti-cli task comment --room-id ROOM_ID --task-id TASK_UUID \
  --content "开始实现这个任务" --agent-target-id AGENT_ID --json

# 4. 在指定讨论串中回复,并触发 Agent 继续执行
tutti-cli task comment --room-id ROOM_ID --task-id TASK_UUID \
  --reply-to COMMENT_UUID --content "按上面的建议继续修改" \
  --agent-target-id AGENT_ID --json

--content TEXT 与 --content-file PATH|- 互斥且必须提供一个;文件为 UTF-8,最大 1 MiB, - 从 stdin 读取。支持 --operation-id UUID 进行显式幂等重试。 仅评论模式不接受 --model 或 --reasoning-effort;这两个参数必须与 Agent ID 一起使用。

服务端评论是两层结构。--reply-to 可以指定顶层评论或子回复:CLI 分页查询该 Task 的时间线, 找到指定评论及其根评论;新回复与 Agent 的回复都归入这个根评论的讨论串,不建立第三层嵌套。 回执的 replyTo 保留输入的评论 ID,parentCommentId 是实际所属根评论,commentId 是新发出的评论。 目标不存在、已删除、属于其他 Task、根评论不可用或时间线查询失败时不写入评论、不触发 Agent。 查询与写入之间的权限和删除竞态仍由服务端在写入时检查。

直接评论成功返回 status: "posted",退出码为 0;不会返回执行 ID。 触发 Agent 时返回下文的 accepted/pending/rejected 受理状态,Agent 回复留在同一讨论串。 已有 task run --instruction ... 保留,等价于必须指定 Agent 的评论执行入口,也支持 --reply-to。

选择 Agent 和模型

模型目录属于房间内的具体 Agent。先选 Agent,再查询它当前可用的模型:

tutti-cli agent list --room-id ROOM_ID --json

tutti-cli agent models --room-id ROOM_ID --agent-target-id AGENT_ID --json

# 将上一步 models[].value 原样传给 --model
tutti-cli task comment --room-id ROOM_ID --task-id TASK_UUID \
  --content "按任务描述开始实现" --agent-target-id AGENT_ID \
  --model MODEL_VALUE --reasoning-effort high --json

agent models 返回 models、modelConfigurable、effectiveModel(如有)、effectiveSettings, 以及 reasoningConfigurable、reasoningEfforts 和按模型区分的 reasoningOptionsByModel。 使用模型的 value,不要使用展示用的 label;value 可能包含连接 ID 前缀,必须完整保留。 当 modelConfigurable=false 时不传 --model;推理强度使用该模型提供的选项,不能假定所有模型都支持 high。 目录提供空字符串推理选项时,可用 --reasoning-effort "" 显式选择该默认档位。 查询的有效设置代表本次目录查询的默认上下文,不保证等于已有 Task 会话的设置;执行时仍由服务端校验并应用设置。

查询使用与 Web 相同的实时能力协议,确认 Agent 属于该房间后向它查询模型;不会创建评论或触发 Task 执行。 Agent 不可用、目录响应非法或查询失败时返回错误;总等待时间最多 90 秒,Ctrl+C 可取消。 部分模型来源失败时仍返回可用模型,并标记 partial=true、列出 modelDiscoveryFailures,同时向 stderr 提示目录不完整。 空 models 是有效结果,不代表存在可选模型。命令的 JSON 输出不会混入提示文字。

在 Task 中触发 Agent

先查看这个 Room 中的 Agent,使用返回的 agentTargetId 精确选择执行目标:

tutti-cli agent list --room-id ROOM_ID --json

tutti-cli task run --room-id ROOM_ID --task-id TASK_UUID \
  --agent-target-id AGENT_ID --instruction "修复任务中描述的问题,完成后汇报验证结果" --json

# 从 UTF-8 文件读取指令;用 - 可从 stdin 读取
tutti-cli task run --room-id ROOM_ID --task-id TASK_UUID \
  --agent-target-id AGENT_ID --instruction-file ./instruction.md \
  --model MODEL_ID --reasoning-effort high --operation-id OPERATION_UUID --json

agent list 返回 { roomId, agents },列出名称、Provider、Computer 类型及执行可用性。 availability.status 为服务端的 available、checking 或 unavailable,有阻塞原因时显示 reason。 目录包含可见但不能立即执行的目标:ownership=requestable 需要授权,referenceOnly=true 是历史引用。 目录查询不启动执行,也不以 Computer 在线状态推断 Agent 可用。该目录接口没有分页参数。

task run 在已有 Task 中先发表一条当前用户的评论,再使用与 Web 相同的 :start-agent 接口触发 Agent。 必填 Room、Task、精确 Agent ID 和非空指令;--instruction 与 --instruction-file 互斥。 指令保留换行,文件须为 UTF-8 且不超过 1 MiB;只发送指令,不修改 Task 标题、描述或指派。 本次指令显示在 Task 评论中。顶层发言以新评论作为执行请求的 parentCommentId;指定 --reply-to 时使用已有根评论。 Agent 的最终回复由服务端发布到同一个评论串。 评论使用 COMMENT_ONLY 模式,禁止保存评论时自动触发已指派的 Agent;之后只对显式选中的 Agent 发起一次执行。 服务端继续管理会话创建或复用及执行,CLI 不创建脱离 Task 的独立会话。 --model 和 --reasoning-effort 可选,合法值由所选 Agent 的服务端能力校验;未指定时不覆盖服务端配置。

返回示例:

{
  "roomId": "ROOM_ID",
  "taskId": "TASK_UUID",
  "agentTargetId": "AGENT_ID",
  "operationId": "CLIENT_OPERATION_UUID",
  "commentId": "CLIENT_OPERATION_UUID",
  "admissionOperationId": "SERVER_ADMISSION_ID",
  "status": "accepted",
  "invocationId": "INVOCATION_ID"
}
  • accepted:服务端已受理,并返回 invocationId;退出码为 0,不表示执行已经完成。
  • pending:请求已保存,执行受理仍待处理;退出码为 0,不会轮询或重新提交。后续在 Web Task 中查看执行进展。
  • rejected:明确拒绝,退出码为 1;stdout 仍返回回执及可用的 rejectionCode,stderr 显示错误。
  • 非法参数、HTTP 错误或回执身份不匹配:退出码为 1,不输出成功回执。Ctrl+C 取消返回 130;已提交的请求可能继续执行。

operationId 是本次 Task 写操作的幂等 ID;commentId 使用同一个 UUID,利用评论与 Task 写操作各自独立的幂等空间,使显式重试复用同一条评论。 admissionOperationId 是服务端根据活动生成的执行受理 ID,不能拿它代替 --operation-id。 未提供 --operation-id 时自动生成;请求失败也会在 stderr 保留该 ID。所有写入都不自动重试。 网络错误或取消后先用 task show 查询 Task,或在 Web 查看执行活动;不确定的写入只使用相同操作 ID 和相同输入重试,避免重复触发。 评论保存失败或回执身份不符时不触发 Agent;评论已保存但执行失败或被拒绝时保留评论,并输出可定位的 comment ID。 这两个请求不是一个原子事务;中途失败不会删除已保存的评论。同 ID、同输入重试会复用评论,再重放幂等执行请求。 明确拒绝的操作结果不可改写,解决原因后应使用新的操作 ID 提交,新操作也会创建新评论。 这两个命令复用 CLI 登录态和服务端权限检查,不读取本机 Computer 凭据,不为本地与云端 Agent 建立不同执行协议。

文件附件

tutti-cli task create --room-id ROOM_ID --title "构建日志" --file ./build.log --json

tutti-cli task edit --room-id ROOM_ID --task-id TASK_UUID \
  --description "现场证据" --file ./screenshot.png --room-file /room/report.pdf --json

tutti-cli task comment --room-id ROOM_ID --task-id TASK_UUID \
  --content-file ./reply.md --file ./fix-1.png --file ./fix-2.png --json

--file 与 --room-file 都可重复、可混用,适用于 task create、task edit、task comment 和 task run。 创建可以只提供标题和附件;编辑附件时必须显式替换描述。评论与执行指令的附件挂在这条评论上,放在正文之后。 本地文件必须非空且为普通文件,拒绝符号链接;文件名不重复,合计不超过 500 MiB,附件总数不超过 256。 快照使用私有临时目录、分块读取并校验源文件未发生变化,命令结束时清理;SHA-256 与上传字节来自同一快照。 HTML、XHTML、SVG 沿用原 CLI 的主动内容限制;其余未知类型按 application/octet-stream 上传。 文本附件(.txt、.md、.csv)声明不带 charset 参数的 MIME,与服务端上传回执保持一致;文件按原始字节上传,不转码。

本地文件使用 room_task_attachment 私有上传,完成确认后才写 Task;上传目标不携带账号 Cookie, 不会打印签名 URL。--room-file 沿用 Room 成员可读共享,逻辑路径必须是规范的 /room/... 文件路径, 只适用于存在 Workspace 的 Room;不能建立公共分享。

附件处理失败不提交 Task 或评论,但此前已完成的私有上传或共享可能保留。重新上传会产生新 upload ID; 若最终 Task 写入结果不明,先查询结果,不能用同一 operation ID 重放不同的附件引用。 评论的 operation ID 就是评论 ID:结果不明时用同一 operation ID 和相同输入重试,评论未写入则正常发出; 已写入时服务端因附件引用不同返回 COMMENT_IDEMPOTENCY_CONFLICT,说明评论已存在,不再重发。 服务端须已具备 Task 私有附件能力(tutti-server #1020),没有客户端降级分支。

实现来源与协议

| 既有概念 / 实现 | 对外 CLI 实现 | | -------------------------------------------------------------------- | ----------------------------------------------------- | | gopackages/agent/cli/task_create.go 的创建参数 | src/main.ts 参数解析与 src/task.ts 创建行为 | | gopackages/agent/cli/task_commands.go 的局部编辑 | src/task.ts,保留标题、描述、幂等和 updateMask 语义 | | gopackages/agent/cli/task_files.go 的文件处理 | src/files.ts,Node 文件快照、上传与共享 | | desktopd authbridge/protocol 与 tsh-web apps/auth 的登录 handoff | src/login.ts,复用 Web cli_bridge 协议 | | Agent 执行上下文中的 Room | 用户显式输入 --room-id | | desktopd Account session | CLI 自己持有、按 origin 隔离的用户会话 |

Task、Room、Project、operation ID 等在新入口仍是同一领域概念,因此保留名称。 参考参数、状态流和错误边界,以 TypeScript/Node 实现;不调用或复制 Go executable,不迁入 Agent execution authority。 没有渲染层改动,无需 UI System 依赖或样式。

严格保留的服务端协议:

| 用途 | 现有接口 / operation | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | 登录 | /auth/login?state=...,bridge state v1、cli_bridge、规定的回调端口 | | 兑换与校验账号 | Account POST /auth/v1/redeem_desktop_transfer_code、POST /user/v1/user_info | | 退出 | Account POST /auth/v1/logout-web-session | | 创建 | RoomTaskService_CreateRoomTask,POST /v2/rooms/{roomId}/tasks | | 修改 | RoomTaskService_PatchRoomTask,PATCH /v2/rooms/{roomId}/tasks/{taskId} | | 房间目录 | RoomService_ListRoomDirectory,GET /v2/rooms/directory?pageSize=...[&pageToken=...] | | 查询 | RoomTaskService_GetRoomTask,GET /v2/rooms/{roomId}/tasks/{taskId} | | 标签目录与创建 | GET/POST /v2/rooms/{roomId}/task-labels;创建字段 labelId/name/color | | Task 标签 | POST /v2/rooms/{roomId}/tasks/{taskId}:attach-label 或 :detach-label,字段 labelId/clientOperationId | | 房间成员 | RoomService_GetRoomShareState,GET /v2/rooms/{roomId}/share,仅输出成员信息 | | Agent 目录 | ComputerService_GetAgentDirectoryProjection,GET /v2/agents/directory-projection?roomId=... | | 模型目录 | POST /v2/rooms/{roomId}/agent-capability-queries(composer-options、section=core),GET /v2/rooms/{roomId}/agent-capability-queries/{queryId} | | 回复定位 | RoomTaskService_ListRoomTaskTimeline,GET /v2/rooms/{roomId}/tasks/{taskId}/timeline?pageSize=100[&pageToken=...] | | 执行指令评论 | RoomTaskService_CreateRoomTaskComment,POST /v2/rooms/{roomId}/tasks/{taskId}/comments,triggerMode=ROOM_TASK_COMMENT_TRIGGER_MODE_COMMENT_ONLY | | Task 执行 | RoomTaskService_StartRoomTaskAgent,POST /v2/rooms/{roomId}/tasks/{taskId}:start-agent | | 附件 | UploadService_CreateUpload、UploadService_CompleteUpload | | Room 文件 | WorkspaceFilesystemService_CreateWorkspaceSharedPath,成员可见性与 X-Operation-Id |

Task 创建的 labelIds、标签 UUID/名称/颜色和添加/移除 operation 协议保持不变。 Task document、priority/status 枚举、clientOperationId、updateMask、上传字段及会话 Cookie 均沿用已有协议。 这些 server operations 的 repository consumer 已登记为 tutti,无需重复登记或修改服务端。

长期实现选择:独立 TS host 直接调用已有账号和 Task 契约。兼容方案是 TS 包装现有 Go CLI,但会依赖 Computer/Agent execution,无法满足普通用户独立安装登录的用途,因此没有采用。 能力边界为登录、房间与 Agent 目录查询、所选 Agent 的模型目录查询、房间成员查询、Task 创建、编辑、人员指派、取消指派、房间标签查询/创建、Task 标签添加/移除、附件、结果查询、评论发表与回复,以及 Task Agent 执行触发; 不包含 Agent 指派、评论编辑/删除、Task 删除、生命周期管理、执行等待或取消命令。

验证

pnpm --filter @tutti-os/tutti-cli typecheck
pnpm --filter @tutti-os/tutti-cli test
pnpm --filter @tutti-os/tutti-cli build
pnpm exec oxlint apps/tutti-cli --deny-warnings

进程级接口测试运行真实 CLI、临时 HTTP 服务和隔离凭据目录,覆盖登录 handoff 校验、取消、失败保留会话、 凭据隔离、房间目录默认单页、原样游标续页、显式全部分页和本地/云端分类、筛选、空页仍保留游标、非法响应和分页失败不输出部分结果、精确创建/修改 JSON、描述清除、UTF-8 stdin、写入不重试、回执校验、附件上传顺序及失败时不写 Task 或评论。 Agent/模型与评论执行测试覆盖房间目标选择、实时模型查询与取消、模型连接值原样用于执行、部分发现失败、顶层/指定评论回复、仅评论不执行、评论失败不执行、执行受理状态与幂等重试。 此证据不能替代真实 Web 身份提供方与已部署服务的端到端验收;真实账号登录及目标 Room 创建/编辑应在发布前人工验收。 macOS 已运行;Linux/Windows 原生浏览器启动和 Windows ACL 仍需对应平台验证。 changed-aware 检查会按 apps/tutti-cli/package.json 自动选择 typecheck/test,push-ready 同时选择 build。