@kairyou/agent-tools 0.16.0 → 0.18.0

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.
Files changed (48) hide show
  1. package/README.md +6 -5
  2. package/README.zh-CN.md +6 -5
  3. package/{integrations → capabilities}/vision/mcp-server.mjs +1 -1
  4. package/dist/log/hook.mjs +2 -2
  5. package/dist/log/opencode-plugin.mjs +1 -1
  6. package/dist/statusline/claude-statusline.mjs +2 -2
  7. package/dist/usage/cli.mjs +4 -4
  8. package/dist/usage/codex-hook.mjs +1 -1
  9. package/dist/usage/core.mjs +11 -11
  10. package/dist/usage/opencode-plugin.mjs +1 -1
  11. package/dist/usage/opencode-tui.mjs +1 -1
  12. package/dist/vision/cli.mjs +13 -13
  13. package/dist/vision/mcp-server.mjs +12 -12
  14. package/docs/en/repository-structure.md +3 -3
  15. package/docs/zh-CN/repository-structure.md +3 -3
  16. package/package.json +3 -3
  17. package/scripts/build.mjs +13 -13
  18. package/scripts/install.mjs +13 -13
  19. package/skills/{integrations → systems}/at-zentao/SKILL.md +29 -6
  20. package/skills/{integrations → systems}/at-zentao/scripts/zentao-cli.mjs +103 -8
  21. /package/{integrations → capabilities}/log/hook.mjs +0 -0
  22. /package/{integrations → capabilities}/log/opencode-plugin.mjs +0 -0
  23. /package/{integrations → capabilities}/statusline/claude-statusline.mjs +0 -0
  24. /package/{integrations → capabilities}/usage/cli.mjs +0 -0
  25. /package/{integrations → capabilities}/usage/codex-hook.mjs +0 -0
  26. /package/{integrations → capabilities}/usage/core.mjs +0 -0
  27. /package/{integrations → capabilities}/usage/lib/cache.mjs +0 -0
  28. /package/{integrations → capabilities}/usage/lib/config.mjs +0 -0
  29. /package/{integrations → capabilities}/usage/lib/context.mjs +0 -0
  30. /package/{integrations → capabilities}/usage/lib/format.mjs +0 -0
  31. /package/{integrations → capabilities}/usage/lib/http.mjs +0 -0
  32. /package/{integrations → capabilities}/usage/lib/routes.mjs +0 -0
  33. /package/{integrations → capabilities}/usage/lib/urls.mjs +0 -0
  34. /package/{integrations → capabilities}/usage/opencode-plugin.mjs +0 -0
  35. /package/{integrations → capabilities}/usage/opencode-tui.mjs +0 -0
  36. /package/{integrations → capabilities}/usage/routes/.gitkeep +0 -0
  37. /package/{integrations → capabilities}/usage/skills/at-usage/SKILL.md +0 -0
  38. /package/{integrations → capabilities}/vision/lib/cli.mjs +0 -0
  39. /package/{integrations → capabilities}/vision/lib/config.mjs +0 -0
  40. /package/{integrations → capabilities}/vision/lib/errors.mjs +0 -0
  41. /package/{integrations → capabilities}/vision/lib/image-source.mjs +0 -0
  42. /package/{integrations → capabilities}/vision/lib/inspect.mjs +0 -0
  43. /package/{integrations → capabilities}/vision/lib/providers/anthropic-compatible.mjs +0 -0
  44. /package/{integrations → capabilities}/vision/lib/providers/openai-compatible.mjs +0 -0
  45. /package/{integrations → capabilities}/vision/lib/providers/shared.mjs +0 -0
  46. /package/{integrations → capabilities}/vision/lib/rate-limit.mjs +0 -0
  47. /package/{integrations → capabilities}/vision/lib/redact.mjs +0 -0
  48. /package/{integrations → capabilities}/vision/skills/at-vision/SKILL.md +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent Tools
2
2
 
3
- Reusable Agent Skills, plus integrations (statusline, provider usage, vision) that install into Codex, Claude Code, and opencode.
3
+ Reusable Agent Skills, plus runtime capabilities (statusline, provider usage, vision) for Codex, Claude Code, and opencode.
4
4
 
5
5
  Requires Node.js >= 22.
6
6
 
@@ -56,7 +56,7 @@ Usage:
56
56
 
57
57
  ### at-zentao
58
58
 
59
- Work ZenTao (禅道) bugs/tasks end to end: fix, verify, stage; asks before committing and before writing status back.
59
+ Work ZenTao (禅道) bugs/tasks end to end, read linked story context, or log task hours; asks before committing and before writing back.
60
60
 
61
61
  ```bash
62
62
  npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
@@ -67,11 +67,12 @@ Usage:
67
67
  - `/at-zentao bugs` — list bugs assigned to you (the configured account); pick one or several (several = batch mode)
68
68
  - `/at-zentao tasks` — same, for tasks
69
69
  - `/at-zentao bug <id>` — work a specific bug
70
- - `/at-zentao task <id>` — work a specific task
70
+ - `/at-zentao task <id> [request]` — work a specific task or log hours with a natural-language request
71
+ - `/at-zentao story <id>` — read requirement scope and acceptance context without writing back
71
72
 
72
73
  Config: `~/.agent-tools/config.jsonc` → `"zentao": { "url", "account", "password" }`. The password can be entered directly or referenced with `"password": { "env": "ZENTAO_PASSWORD" }`; never paste credentials or tokens into chat.
73
74
 
74
- ## Integrations
75
+ ## Capabilities
75
76
 
76
77
  Runtime capabilities, installed per agent:
77
78
 
@@ -80,7 +81,7 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
80
81
  # npx -y github:kairyou/agent-tools <capability> -a <agent...> # Or the latest code from GitHub (needs Git)
81
82
  ```
82
83
 
83
- `--dry-run` previews, `--uninstall` unwires the integration from the agent, and
84
+ `--dry-run` previews, `--uninstall` removes the capability from the agent, and
84
85
  re-running the install command updates. The installer only touches config
85
86
  entries it wrote itself, and `config.jsonc` updates only add missing default
86
87
  keys without touching your edits or comments.
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md)
4
4
 
5
- 可复用的 Agent Skills, 以及可装入 Codex, Claude Code 与 opencode 的 integrations: statusline, provider usage 与跨模型识图.
5
+ 可复用的 Agent Skills, 以及适用于 Codex, Claude Code 与 opencode 的 runtime capabilities: statusline, provider usage 与跨模型识图.
6
6
 
7
7
  需要 Node.js >= 22.
8
8
 
@@ -56,7 +56,7 @@ npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
56
56
 
57
57
  ### at-zentao
58
58
 
59
- 读取禅道 bug/task 并端到端处理: 修复, 验证, 暂存; 提交和回写状态前均需确认.
59
+ 读取禅道 bug/task 并端到端处理, 补充关联 story 上下文, 或记录任务工时; 提交和回写前均需确认.
60
60
 
61
61
  ```bash
62
62
  npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
@@ -67,11 +67,12 @@ npx -y skills@latest add kairyou/agent-tools --skill at-zentao -g -y
67
67
  - `/at-zentao bugs` — 列出指派给你(配置的账号)的 bug, 挑一个或多个(多个 = 批量模式)
68
68
  - `/at-zentao tasks` — 同上, 任务清单
69
69
  - `/at-zentao bug <id>` — 直接处理指定 bug
70
- - `/at-zentao task <id>`直接处理指定 task
70
+ - `/at-zentao task <id> [请求]` 处理指定 task, 或用自然语言记录工时
71
+ - `/at-zentao story <id>` — 只读查看研发需求范围和验收信息
71
72
 
72
73
  配置: `~/.agent-tools/config.jsonc` → `"zentao": { "url", "account", "password" }`. 密码可以直接填写, 也可以使用 `"password": { "env": "ZENTAO_PASSWORD" }` 引用环境变量; 不要在对话中粘贴凭据或 token.
73
74
 
74
- ## Integrations
75
+ ## Capabilities
75
76
 
76
77
  Runtime capability, 按 agent 选装:
77
78
 
@@ -80,7 +81,7 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
80
81
  # npx -y github:kairyou/agent-tools <capability> -a <agent...> # 或用 GitHub 上的最新代码(需 Git)
81
82
  ```
82
83
 
83
- `--dry-run` 预览, `--uninstall` 解除对应集成, 重新执行安装命令即为更新.
84
+ `--dry-run` 预览, `--uninstall` 移除对应 capability, 重新执行安装命令即为更新.
84
85
  安装器只改动自己写入的配置项, 更新 `config.jsonc` 时只补充缺失的默认键,
85
86
  不会动你的修改和注释.
86
87
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Vision MCP stdio server. Thin shell over ./lib: registers the
3
3
  // inspect_image tool, translates results/errors, and nothing else. Launched by
4
- // hosts as `agent-tools mcp-vision` (or `node integrations/vision/mcp-server.mjs`).
4
+ // hosts as `agent-tools mcp-vision` (or `node capabilities/vision/mcp-server.mjs`).
5
5
 
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/log/hook.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // integrations/log/hook.mjs
3
+ // capabilities/log/hook.mjs
4
4
  import { spawnSync } from "node:child_process";
5
5
  import fs from "node:fs/promises";
6
6
  import os from "node:os";
@@ -865,7 +865,7 @@ var ParseErrorCode;
865
865
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
866
866
  })(ParseErrorCode || (ParseErrorCode = {}));
867
867
 
868
- // integrations/log/hook.mjs
868
+ // capabilities/log/hook.mjs
869
869
  var MAX_SNAPSHOT_BYTES = 512 * 1024;
870
870
  var MIN_RESULT_SUMMARY_LENGTH = 24;
871
871
  var DAILY_ITEM_TARGET_CHARS = 160;
@@ -1,4 +1,4 @@
1
- // integrations/log/opencode-plugin.mjs
1
+ // capabilities/log/opencode-plugin.mjs
2
2
  import { spawn } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // integrations/statusline/claude-statusline.mjs
3
+ // capabilities/statusline/claude-statusline.mjs
4
4
  import { execFileSync, spawn } from "node:child_process";
5
5
  import fs from "node:fs";
6
6
  import { basename, dirname, join } from "node:path";
@@ -865,7 +865,7 @@ var ParseErrorCode;
865
865
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
866
866
  })(ParseErrorCode || (ParseErrorCode = {}));
867
867
 
868
- // integrations/statusline/claude-statusline.mjs
868
+ // capabilities/statusline/claude-statusline.mjs
869
869
  var SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
870
870
  var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(SCRIPT_DIR, "..", "..");
871
871
  var DEFAULT_CONFIG_FILE = join(AGENT_TOOLS_HOME, "config.jsonc");
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // integrations/usage/cli.mjs
3
+ // capabilities/usage/cli.mjs
4
4
  import { queryAgentProviderUsage } from "./core.mjs";
5
5
 
6
- // integrations/usage/lib/config.mjs
6
+ // capabilities/usage/lib/config.mjs
7
7
  import { readFile, writeFile, mkdir, open, stat } from "node:fs/promises";
8
8
  import { existsSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
@@ -868,7 +868,7 @@ var ParseErrorCode;
868
868
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
869
869
  })(ParseErrorCode || (ParseErrorCode = {}));
870
870
 
871
- // integrations/usage/lib/config.mjs
871
+ // capabilities/usage/lib/config.mjs
872
872
  var CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
873
873
  var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
874
874
  var AUTH_PATH = join(CODEX_HOME, "auth.json");
@@ -931,7 +931,7 @@ async function debugLog(event) {
931
931
  }
932
932
  var HOOK_SNAPSHOT_MAX_AGE_MS = 10 * 6e4;
933
933
 
934
- // integrations/usage/cli.mjs
934
+ // capabilities/usage/cli.mjs
935
935
  function parseAgent(argv) {
936
936
  for (let index = 0; index < argv.length; index += 1) {
937
937
  const arg = argv[index];
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // integrations/usage/codex-hook.mjs
3
+ // capabilities/usage/codex-hook.mjs
4
4
  import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // integrations/usage/core.mjs
3
+ // capabilities/usage/core.mjs
4
4
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
6
 
7
- // integrations/usage/lib/config.mjs
7
+ // capabilities/usage/lib/config.mjs
8
8
  import { readFile, writeFile, mkdir, open, stat } from "node:fs/promises";
9
9
  import { existsSync } from "node:fs";
10
10
  import { dirname, join } from "node:path";
@@ -869,7 +869,7 @@ var ParseErrorCode;
869
869
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
870
870
  })(ParseErrorCode || (ParseErrorCode = {}));
871
871
 
872
- // integrations/usage/lib/config.mjs
872
+ // capabilities/usage/lib/config.mjs
873
873
  var CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
874
874
  var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
875
875
  var AUTH_PATH = join(CODEX_HOME, "auth.json");
@@ -954,7 +954,7 @@ async function newApiQuotaScale() {
954
954
  return Number.isFinite(scale) && scale > 0 ? scale : 0;
955
955
  }
956
956
 
957
- // integrations/usage/lib/urls.mjs
957
+ // capabilities/usage/lib/urls.mjs
958
958
  function isOfficialBaseUrl(baseUrl) {
959
959
  if (!baseUrl) return true;
960
960
  const clean = baseUrl.replace(/\/+$/, "");
@@ -994,7 +994,7 @@ function hostIncludes(baseUrl, value) {
994
994
  }
995
995
  }
996
996
 
997
- // integrations/usage/lib/cache.mjs
997
+ // capabilities/usage/lib/cache.mjs
998
998
  import { writeFile as writeFile2, mkdir as mkdir2, open as open2, unlink, rename, stat as stat2, utimes } from "node:fs/promises";
999
999
  import { createHash, randomUUID } from "node:crypto";
1000
1000
  import { dirname as dirname2, join as join2 } from "node:path";
@@ -1142,12 +1142,12 @@ async function acquireWriteLock({ timeoutMs = 500, staleMs = 5e3 } = {}) {
1142
1142
  return null;
1143
1143
  }
1144
1144
 
1145
- // integrations/usage/lib/routes.mjs
1145
+ // capabilities/usage/lib/routes.mjs
1146
1146
  import { readdir } from "node:fs/promises";
1147
1147
  import { basename, extname, isAbsolute, join as join3 } from "node:path";
1148
1148
  import { pathToFileURL } from "node:url";
1149
1149
 
1150
- // integrations/usage/lib/http.mjs
1150
+ // capabilities/usage/lib/http.mjs
1151
1151
  import { createContext, runInContext } from "node:vm";
1152
1152
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1153
1153
  var SHIELD_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
@@ -1307,7 +1307,7 @@ async function requestJson(url, options = {}) {
1307
1307
  throw new Error(`${name} unavailable`);
1308
1308
  }
1309
1309
 
1310
- // integrations/usage/lib/format.mjs
1310
+ // capabilities/usage/lib/format.mjs
1311
1311
  var ONE_API_HARD_LIMIT_SENTINEL_USD = 1e6;
1312
1312
  var RESET_TIME_FIELDS = Object.freeze({
1313
1313
  // Sub2API rate-limit entries returned alongside quota usage.
@@ -1591,7 +1591,7 @@ function formatUsageLine(root) {
1591
1591
  return parts.join(" | ");
1592
1592
  }
1593
1593
 
1594
- // integrations/usage/lib/routes.mjs
1594
+ // capabilities/usage/lib/routes.mjs
1595
1595
  async function subscriptionUrl(baseUrl) {
1596
1596
  const clean = baseUrl.replace(/\/+$/, "");
1597
1597
  const url = clean.endsWith("/v1") ? `${clean}/usage` : `${clean}/v1/usage`;
@@ -1834,7 +1834,7 @@ async function orderedUsageRoutes(context) {
1834
1834
  return [cached.route, ...routeIds.filter((id) => id !== cached.route)].map((id) => registry[id]).filter(Boolean);
1835
1835
  }
1836
1836
 
1837
- // integrations/usage/lib/context.mjs
1837
+ // capabilities/usage/lib/context.mjs
1838
1838
  import { existsSync as existsSync2 } from "node:fs";
1839
1839
  function stripInlineComment(value) {
1840
1840
  let inSingle = false;
@@ -1938,7 +1938,7 @@ function normalizeUsageContext(input) {
1938
1938
  };
1939
1939
  }
1940
1940
 
1941
- // integrations/usage/core.mjs
1941
+ // capabilities/usage/core.mjs
1942
1942
  function parseArgs(argv) {
1943
1943
  const opts = { mode: "hook", agent: "codex", silent: false };
1944
1944
  let modeSet = false;
@@ -1,4 +1,4 @@
1
- // integrations/usage/opencode-plugin.mjs
1
+ // capabilities/usage/opencode-plugin.mjs
2
2
  import { queryProviderUsage } from "./core.mjs";
3
3
  var DEFAULT_REFRESH_MS = 6e4;
4
4
  function firstString(value, keys) {
@@ -1,4 +1,4 @@
1
- // integrations/usage/opencode-tui.mjs
1
+ // capabilities/usage/opencode-tui.mjs
2
2
  import { readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
@@ -1,13 +1,13 @@
1
- // integrations/vision/lib/cli.mjs
1
+ // capabilities/vision/lib/cli.mjs
2
2
  import fs4 from "node:fs";
3
3
  import path5 from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- // integrations/vision/lib/inspect.mjs
6
+ // capabilities/vision/lib/inspect.mjs
7
7
  import crypto3 from "node:crypto";
8
8
  import path4 from "node:path";
9
9
 
10
- // integrations/vision/lib/config.mjs
10
+ // capabilities/vision/lib/config.mjs
11
11
  import fs from "node:fs";
12
12
  import os from "node:os";
13
13
  import path from "node:path";
@@ -871,7 +871,7 @@ var ParseErrorCode;
871
871
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
872
872
  })(ParseErrorCode || (ParseErrorCode = {}));
873
873
 
874
- // integrations/vision/lib/errors.mjs
874
+ // capabilities/vision/lib/errors.mjs
875
875
  var ERROR_CODES = Object.freeze({
876
876
  CONFIG: "config_error",
877
877
  INPUT: "input_error",
@@ -899,7 +899,7 @@ function toVisionError(err, fallbackCode = ERROR_CODES.PROVIDER_HTTP) {
899
899
  return new VisionError(fallbackCode, message, { cause: err });
900
900
  }
901
901
 
902
- // integrations/vision/lib/config.mjs
902
+ // capabilities/vision/lib/config.mjs
903
903
  var PROVIDERS = Object.freeze(["openai-compatible", "anthropic-compatible"]);
904
904
  var CONFIG_DEFAULTS = Object.freeze({
905
905
  timeoutMs: 3e4,
@@ -1037,7 +1037,7 @@ function loadVisionConfig({ file, env = process.env } = {}) {
1037
1037
  };
1038
1038
  }
1039
1039
 
1040
- // integrations/vision/lib/image-source.mjs
1040
+ // capabilities/vision/lib/image-source.mjs
1041
1041
  import fs2 from "node:fs";
1042
1042
  import os2 from "node:os";
1043
1043
  import path2 from "node:path";
@@ -1285,7 +1285,7 @@ async function loadImageSource(source, { maxImageBytes, timeoutMs, fetchImpl } =
1285
1285
  throw new VisionError(ERROR_CODES.INPUT, `Unsupported image_source.type: ${JSON.stringify(source.type)}`);
1286
1286
  }
1287
1287
 
1288
- // integrations/vision/lib/rate-limit.mjs
1288
+ // capabilities/vision/lib/rate-limit.mjs
1289
1289
  import crypto from "node:crypto";
1290
1290
  import fs3 from "node:fs";
1291
1291
  import path3 from "node:path";
@@ -1442,7 +1442,7 @@ function createLimiter(config, now = Date.now, { stateFile = null } = {}) {
1442
1442
  return stateFile ? createSharedLimiter(config, now, stateFile) : createProcessLimiter(config, now);
1443
1443
  }
1444
1444
 
1445
- // integrations/vision/lib/redact.mjs
1445
+ // capabilities/vision/lib/redact.mjs
1446
1446
  var MASK = "***";
1447
1447
  function redactSecrets(text, secrets) {
1448
1448
  if (typeof text !== "string" || text.length === 0) return text;
@@ -1454,7 +1454,7 @@ function redactSecrets(text, secrets) {
1454
1454
  return out;
1455
1455
  }
1456
1456
 
1457
- // integrations/vision/lib/providers/shared.mjs
1457
+ // capabilities/vision/lib/providers/shared.mjs
1458
1458
  import crypto2 from "node:crypto";
1459
1459
  async function* encodeBase64(readable) {
1460
1460
  let carry = Buffer.alloc(0);
@@ -1667,7 +1667,7 @@ async function postJson({ url, headers, body, timeoutMs, fetchImpl, providerLabe
1667
1667
  }
1668
1668
  }
1669
1669
 
1670
- // integrations/vision/lib/providers/anthropic-compatible.mjs
1670
+ // capabilities/vision/lib/providers/anthropic-compatible.mjs
1671
1671
  function messagesUrl(baseUrl) {
1672
1672
  return baseUrl.endsWith("/v1") ? `${baseUrl}/messages` : `${baseUrl}/v1/messages`;
1673
1673
  }
@@ -1718,7 +1718,7 @@ async function inspectWithAnthropicCompatible({ config, image, questions, fetchI
1718
1718
  return normalizeAnswers(text, questions);
1719
1719
  }
1720
1720
 
1721
- // integrations/vision/lib/providers/openai-compatible.mjs
1721
+ // capabilities/vision/lib/providers/openai-compatible.mjs
1722
1722
  function replyText2(json) {
1723
1723
  const message = json?.choices?.[0]?.message;
1724
1724
  if (typeof message?.content === "string") return message.content;
@@ -1765,7 +1765,7 @@ async function inspectWithOpenAICompatible({ config, image, questions, fetchImpl
1765
1765
  return normalizeAnswers(text, questions);
1766
1766
  }
1767
1767
 
1768
- // integrations/vision/lib/inspect.mjs
1768
+ // capabilities/vision/lib/inspect.mjs
1769
1769
  var PROVIDER_IMPL = {
1770
1770
  "openai-compatible": inspectWithOpenAICompatible,
1771
1771
  "anthropic-compatible": inspectWithAnthropicCompatible
@@ -1849,7 +1849,7 @@ function createVisionService({ config, fetchImpl, now, limiterStateFile } = {})
1849
1849
  return { config: resolved, inspect };
1850
1850
  }
1851
1851
 
1852
- // integrations/vision/lib/cli.mjs
1852
+ // capabilities/vision/lib/cli.mjs
1853
1853
  function printHelp() {
1854
1854
  const lines = [];
1855
1855
  for (const line of fs4.readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n")) {
@@ -30952,11 +30952,11 @@ var StdioServerTransport = class {
30952
30952
  }
30953
30953
  };
30954
30954
 
30955
- // integrations/vision/lib/inspect.mjs
30955
+ // capabilities/vision/lib/inspect.mjs
30956
30956
  import crypto3 from "node:crypto";
30957
30957
  import path4 from "node:path";
30958
30958
 
30959
- // integrations/vision/lib/config.mjs
30959
+ // capabilities/vision/lib/config.mjs
30960
30960
  import fs from "node:fs";
30961
30961
  import os from "node:os";
30962
30962
  import path from "node:path";
@@ -31820,7 +31820,7 @@ var ParseErrorCode;
31820
31820
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
31821
31821
  })(ParseErrorCode || (ParseErrorCode = {}));
31822
31822
 
31823
- // integrations/vision/lib/errors.mjs
31823
+ // capabilities/vision/lib/errors.mjs
31824
31824
  var ERROR_CODES = Object.freeze({
31825
31825
  CONFIG: "config_error",
31826
31826
  INPUT: "input_error",
@@ -31848,7 +31848,7 @@ function toVisionError(err, fallbackCode = ERROR_CODES.PROVIDER_HTTP) {
31848
31848
  return new VisionError(fallbackCode, message, { cause: err });
31849
31849
  }
31850
31850
 
31851
- // integrations/vision/lib/config.mjs
31851
+ // capabilities/vision/lib/config.mjs
31852
31852
  var PROVIDERS = Object.freeze(["openai-compatible", "anthropic-compatible"]);
31853
31853
  var CONFIG_DEFAULTS = Object.freeze({
31854
31854
  timeoutMs: 3e4,
@@ -31986,7 +31986,7 @@ function loadVisionConfig({ file: file2, env = process.env } = {}) {
31986
31986
  };
31987
31987
  }
31988
31988
 
31989
- // integrations/vision/lib/image-source.mjs
31989
+ // capabilities/vision/lib/image-source.mjs
31990
31990
  import fs2 from "node:fs";
31991
31991
  import os2 from "node:os";
31992
31992
  import path2 from "node:path";
@@ -32234,7 +32234,7 @@ async function loadImageSource(source, { maxImageBytes, timeoutMs, fetchImpl } =
32234
32234
  throw new VisionError(ERROR_CODES.INPUT, `Unsupported image_source.type: ${JSON.stringify(source.type)}`);
32235
32235
  }
32236
32236
 
32237
- // integrations/vision/lib/rate-limit.mjs
32237
+ // capabilities/vision/lib/rate-limit.mjs
32238
32238
  import crypto from "node:crypto";
32239
32239
  import fs3 from "node:fs";
32240
32240
  import path3 from "node:path";
@@ -32391,7 +32391,7 @@ function createLimiter(config2, now = Date.now, { stateFile = null } = {}) {
32391
32391
  return stateFile ? createSharedLimiter(config2, now, stateFile) : createProcessLimiter(config2, now);
32392
32392
  }
32393
32393
 
32394
- // integrations/vision/lib/redact.mjs
32394
+ // capabilities/vision/lib/redact.mjs
32395
32395
  var MASK = "***";
32396
32396
  function redactSecrets(text, secrets) {
32397
32397
  if (typeof text !== "string" || text.length === 0) return text;
@@ -32403,7 +32403,7 @@ function redactSecrets(text, secrets) {
32403
32403
  return out;
32404
32404
  }
32405
32405
 
32406
- // integrations/vision/lib/providers/shared.mjs
32406
+ // capabilities/vision/lib/providers/shared.mjs
32407
32407
  import crypto2 from "node:crypto";
32408
32408
  async function* encodeBase64(readable) {
32409
32409
  let carry = Buffer.alloc(0);
@@ -32616,7 +32616,7 @@ async function postJson({ url: url2, headers, body, timeoutMs, fetchImpl, provid
32616
32616
  }
32617
32617
  }
32618
32618
 
32619
- // integrations/vision/lib/providers/anthropic-compatible.mjs
32619
+ // capabilities/vision/lib/providers/anthropic-compatible.mjs
32620
32620
  function messagesUrl(baseUrl) {
32621
32621
  return baseUrl.endsWith("/v1") ? `${baseUrl}/messages` : `${baseUrl}/v1/messages`;
32622
32622
  }
@@ -32667,7 +32667,7 @@ async function inspectWithAnthropicCompatible({ config: config2, image, question
32667
32667
  return normalizeAnswers(text, questions);
32668
32668
  }
32669
32669
 
32670
- // integrations/vision/lib/providers/openai-compatible.mjs
32670
+ // capabilities/vision/lib/providers/openai-compatible.mjs
32671
32671
  function replyText2(json2) {
32672
32672
  const message = json2?.choices?.[0]?.message;
32673
32673
  if (typeof message?.content === "string") return message.content;
@@ -32714,7 +32714,7 @@ async function inspectWithOpenAICompatible({ config: config2, image, questions,
32714
32714
  return normalizeAnswers(text, questions);
32715
32715
  }
32716
32716
 
32717
- // integrations/vision/lib/inspect.mjs
32717
+ // capabilities/vision/lib/inspect.mjs
32718
32718
  var PROVIDER_IMPL = {
32719
32719
  "openai-compatible": inspectWithOpenAICompatible,
32720
32720
  "anthropic-compatible": inspectWithAnthropicCompatible
@@ -32798,7 +32798,7 @@ function createVisionService({ config: config2, fetchImpl, now, limiterStateFile
32798
32798
  return { config: resolved, inspect };
32799
32799
  }
32800
32800
 
32801
- // integrations/vision/mcp-server.mjs
32801
+ // capabilities/vision/mcp-server.mjs
32802
32802
  var TOOL_DESCRIPTION = [
32803
32803
  "Use the configured vision model when the user's task depends on visible content and only a local image path or http(s) URL is available, direct inspection failed, or the user explicitly requested the provider.",
32804
32804
  "If the prompt already contains actual image content or a host image viewer returned it, inspect that content directly; a bare path or URL without a visual task is not a reason to call this.",
@@ -4,7 +4,7 @@
4
4
  agent-tools/
5
5
  ├── .claude-plugin/ # Claude Code/plugin ecosystem manifest.
6
6
  ├── .codex-plugin/ # Codex plugin manifest.
7
- ├── integrations/ # Installable capabilities, one directory each.
7
+ ├── capabilities/ # Runtime capabilities, one directory each.
8
8
  │ ├── statusline/ # Agent status line: branch, model, usage.
9
9
  │ ├── usage/ # Provider balance / quota display.
10
10
  │ └── vision/ # Cross-model image understanding.
@@ -13,8 +13,8 @@ agent-tools/
13
13
  │ │ ├── at-commit/ # Conventional Commit message skill.
14
14
  │ │ ├── at-review/ # Review local or hosted changes for bugs and regressions.
15
15
  │ │ └── at-simplify/ # Reduce complexity and duplication in changes.
16
- │ └── integrations/ # Skills that integrate external systems.
17
- │ └── at-zentao/ # Self-contained ZenTao workflow and secure API script.
16
+ │ └── systems/ # Self-contained Skills for external systems.
17
+ │ └── at-zentao/ # Self-contained ZenTao development and time-entry workflow.
18
18
  ├── docs/ # Advanced guides and contributor reference.
19
19
  ├── tools/ # Maintainer-only upstream sync and repository tooling.
20
20
  └── scripts/ # Install, sync, validation, and maintenance scripts.
@@ -4,7 +4,7 @@
4
4
  agent-tools/
5
5
  ├── .claude-plugin/ # Claude Code/plugin 生态的 manifest.
6
6
  ├── .codex-plugin/ # Codex plugin manifest.
7
- ├── integrations/ # 可安装的 capability, 一个一目录.
7
+ ├── capabilities/ # Runtime capabilities, 一个一目录.
8
8
  │ ├── statusline/ # Agent 状态栏: 分支, 模型, 用量.
9
9
  │ ├── usage/ # Provider 余额/额度显示.
10
10
  │ └── vision/ # 跨模型识图.
@@ -13,8 +13,8 @@ agent-tools/
13
13
  │ │ ├── at-commit/ # 生成 Conventional Commits message.
14
14
  │ │ ├── at-review/ # 审查本地或托管改动中的 bug 与回归风险.
15
15
  │ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
16
- │ └── integrations/ # 对接外部系统的 skills.
17
- │ └── at-zentao/ # 自包含的禅道工作流和安全 API 脚本.
16
+ │ └── systems/ # 面向外部系统的自包含 Skills.
17
+ │ └── at-zentao/ # 自包含的禅道研发事项与工时工作流.
18
18
  ├── docs/ # 高级指南和贡献者参考.
19
19
  ├── tools/ # 仅供维护者使用的上游同步和仓库工具.
20
20
  └── scripts/ # 安装, 同步, 校验和仓库维护脚本.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.16.0",
4
- "description": "Reusable Agent Skills, plus integrations (statusline, provider usage, vision) that install into Codex, Claude Code, and opencode.",
3
+ "version": "0.18.0",
4
+ "description": "Reusable Agent Skills, plus runtime capabilities (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  "config.default.jsonc",
18
18
  "docs/",
19
19
  "dist/",
20
- "integrations/",
20
+ "capabilities/",
21
21
  "scripts/",
22
22
  "skills/"
23
23
  ],
package/scripts/build.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Bundles everything that ships to ~/.agent-tools into dist/<capability>.
3
- // Installed artifacts are always built output; integrations/ holds the sources.
3
+ // Installed artifacts are always built output; capabilities/ holds the sources.
4
4
 
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
@@ -13,16 +13,16 @@ const DIST = path.join(ROOT, "dist");
13
13
  const TARGETS = {
14
14
  statusline: {
15
15
  entryPoints: {
16
- "claude-statusline": path.join(ROOT, "integrations", "statusline", "claude-statusline.mjs"),
16
+ "claude-statusline": path.join(ROOT, "capabilities", "statusline", "claude-statusline.mjs"),
17
17
  },
18
18
  },
19
19
  usage: {
20
20
  entryPoints: {
21
- core: path.join(ROOT, "integrations", "usage", "core.mjs"),
22
- cli: path.join(ROOT, "integrations", "usage", "cli.mjs"),
23
- "codex-hook": path.join(ROOT, "integrations", "usage", "codex-hook.mjs"),
24
- "opencode-plugin": path.join(ROOT, "integrations", "usage", "opencode-plugin.mjs"),
25
- "opencode-tui": path.join(ROOT, "integrations", "usage", "opencode-tui.mjs"),
21
+ core: path.join(ROOT, "capabilities", "usage", "core.mjs"),
22
+ cli: path.join(ROOT, "capabilities", "usage", "cli.mjs"),
23
+ "codex-hook": path.join(ROOT, "capabilities", "usage", "codex-hook.mjs"),
24
+ "opencode-plugin": path.join(ROOT, "capabilities", "usage", "opencode-plugin.mjs"),
25
+ "opencode-tui": path.join(ROOT, "capabilities", "usage", "opencode-tui.mjs"),
26
26
  },
27
27
  // core.mjs detects "run as a script" via process.argv[1]; inlining it into
28
28
  // the other entries would re-trigger that check inside their bundles, so it
@@ -31,21 +31,21 @@ const TARGETS = {
31
31
  },
32
32
  vision: {
33
33
  entryPoints: {
34
- "mcp-server": path.join(ROOT, "integrations", "vision", "mcp-server.mjs"),
35
- cli: path.join(ROOT, "integrations", "vision", "lib", "cli.mjs"),
34
+ "mcp-server": path.join(ROOT, "capabilities", "vision", "mcp-server.mjs"),
35
+ cli: path.join(ROOT, "capabilities", "vision", "lib", "cli.mjs"),
36
36
  },
37
37
  },
38
38
  log: {
39
39
  entryPoints: {
40
- hook: path.join(ROOT, "integrations", "log", "hook.mjs"),
41
- "opencode-plugin": path.join(ROOT, "integrations", "log", "opencode-plugin.mjs"),
40
+ hook: path.join(ROOT, "capabilities", "log", "hook.mjs"),
41
+ "opencode-plugin": path.join(ROOT, "capabilities", "log", "opencode-plugin.mjs"),
42
42
  },
43
43
  },
44
44
  };
45
45
 
46
- // Repo-shipped usage routes (a fork can commit integrations/usage/routes/*.mjs
46
+ // Repo-shipped usage routes (a fork can commit capabilities/usage/routes/*.mjs
47
47
  // to distribute custom gateways to everyone who installs). Absent upstream.
48
- const USAGE_ROUTES_DIR = path.join(ROOT, "integrations", "usage", "routes");
48
+ const USAGE_ROUTES_DIR = path.join(ROOT, "capabilities", "usage", "routes");
49
49
  if (fs.existsSync(USAGE_ROUTES_DIR)) {
50
50
  for (const file of fs.readdirSync(USAGE_ROUTES_DIR).filter((n) => n.endsWith(".mjs"))) {
51
51
  TARGETS.usage.entryPoints[`routes/${file.slice(0, -4)}`] = path.join(USAGE_ROUTES_DIR, file);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // agent-tools installer: wires statusline / usage into each agent's config.
3
3
  // Runtime-dependent skills are installed with their capability. Standalone
4
- // workflow/integration skills are still handled by `npx skills add`.
4
+ // Standalone skills are still handled by `npx skills add`.
5
5
  //
6
6
  // Capabilities (all global for now — they target the user-level config):
7
7
  // statusline Claude Code statusLine script (claude only).
@@ -68,7 +68,7 @@ const PACKAGE_VERSION = (() => {
68
68
  }
69
69
  })();
70
70
  // Everything copied into ~/.agent-tools is built output from dist/ (see
71
- // scripts/build.mjs); integrations/ holds the sources.
71
+ // scripts/build.mjs); capabilities/ holds the sources.
72
72
  const SOURCE = {
73
73
  logHook: path.join(REPO_ROOT, "dist", "log", "hook.mjs"),
74
74
  logOpencodePlugin: path.join(REPO_ROOT, "dist", "log", "opencode-plugin.mjs"),
@@ -100,12 +100,12 @@ const AGENT_CAPS = {
100
100
  };
101
101
  const VISION_MCP_NAME = "agent-tools-vision";
102
102
  const VISION_SKILL_NAME = "at-vision";
103
- // The at-vision skill ships inside the vision capability dir (integrations/vision),
103
+ // The at-vision skill ships inside the vision capability dir (capabilities/vision),
104
104
  // not skills/: it is unusable without the MCP server, so it must not surface
105
105
  // as an independently installable skill.
106
- const VISION_SKILL_SRC = path.join(REPO_ROOT, "integrations", "vision", "skills", VISION_SKILL_NAME);
106
+ const VISION_SKILL_SRC = path.join(REPO_ROOT, "capabilities", "vision", "skills", VISION_SKILL_NAME);
107
107
  const USAGE_SKILL_NAME = "at-usage";
108
- const USAGE_SKILL_SRC = path.join(REPO_ROOT, "integrations", "usage", "skills", USAGE_SKILL_NAME);
108
+ const USAGE_SKILL_SRC = path.join(REPO_ROOT, "capabilities", "usage", "skills", USAGE_SKILL_NAME);
109
109
  const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
110
110
  const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
111
111
  const INSTALL_STATE_PATH = path.join(INSTALL_ROOT, "install-state.json");
@@ -292,7 +292,7 @@ function syncUsageRoutesDir(dryRun) {
292
292
  function parseArgs(argv) {
293
293
  const opts = {
294
294
  agents: [],
295
- integrations: [],
295
+ capabilities: [],
296
296
  settings: null,
297
297
  codexHooks: null,
298
298
  opencodeConfigDir: null,
@@ -343,15 +343,15 @@ function parseArgs(argv) {
343
343
  console.error(`Unknown option: ${a}`);
344
344
  process.exit(2);
345
345
  }
346
- opts.integrations.push(a);
346
+ opts.capabilities.push(a);
347
347
  }
348
348
  }
349
349
  if (opts.agents.length === 0) opts.agents = ["claude"];
350
- if (!opts.help && opts.integrations.length === 0) {
350
+ if (!opts.help && opts.capabilities.length === 0) {
351
351
  console.error(`Missing capability (available: ${ALL_CAPS.join(", ")})`);
352
352
  process.exit(2);
353
353
  }
354
- for (const name of opts.integrations) {
354
+ for (const name of opts.capabilities) {
355
355
  if (!ALL_CAPS.includes(name)) {
356
356
  console.error(`Unknown capability: ${name} (available: ${ALL_CAPS.join(", ")})`);
357
357
  process.exit(2);
@@ -361,14 +361,14 @@ function parseArgs(argv) {
361
361
  }
362
362
 
363
363
  function wants(opts, cap) {
364
- return opts.integrations.length === 0 || opts.integrations.includes(cap);
364
+ return opts.capabilities.length === 0 || opts.capabilities.includes(cap);
365
365
  }
366
366
 
367
367
  function validateAgentCapabilities(opts) {
368
368
  const invalid = [];
369
369
  for (const agent of opts.agents) {
370
370
  const supported = AGENT_CAPS[agent] || [];
371
- for (const cap of opts.integrations) {
371
+ for (const cap of opts.capabilities) {
372
372
  if (!supported.includes(cap)) invalid.push(`${cap} -a ${agent}`);
373
373
  }
374
374
  }
@@ -1181,7 +1181,7 @@ function cleanupVisionRuntimeIfUnused(opts) {
1181
1181
  try {
1182
1182
  fs.rmdirSync(path.dirname(VISION_RATE_LIMIT_STATE));
1183
1183
  } catch {
1184
- // The shared cache directory may contain state for other integrations.
1184
+ // The shared cache directory may contain state for other capabilities.
1185
1185
  }
1186
1186
  }
1187
1187
  console.log(` removed unused vision runtime ${VISION_RUNTIME_DIR}`);
@@ -1218,7 +1218,7 @@ function main() {
1218
1218
  }
1219
1219
 
1220
1220
  // Standalone vision commands dispatch before capability parsing so image
1221
- // paths and questions are never mistaken for integrations.
1221
+ // paths and questions are never mistaken for capabilities.
1222
1222
  const subcommand = process.argv[2];
1223
1223
  if (subcommand === "inspect-image") {
1224
1224
  const { runInspectImageCli } = await import(
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: at-zentao
3
- description: "Work ZenTao bugs/tasks end to end: fetch details, confirm understanding, fix, verify, stage with git add, then ask before committing and before writing status back to ZenTao. Supports single items and sequential batches. Use when the user references ZenTao (禅道) bugs or tasks."
4
- argument-hint: "bug <id> | task <id> | bugs | tasks | export bug|task <id>"
3
+ description: "Work ZenTao bugs/tasks end to end, log task hours, or read a linked story as development context: fetch details, handle the user's request, verify changes, and ask before committing or writing back to ZenTao. Supports single items and sequential batches. Use when the user references ZenTao (禅道) bugs, tasks, stories, requirements, or task hours."
4
+ argument-hint: "bug <id> [request] | task <id> [request] | story <id> | bugs | tasks | export bug|task <id>"
5
5
  ---
6
6
 
7
7
  # ZenTao Bug/Task Workflow
@@ -70,6 +70,7 @@ node <skill-root>/scripts/zentao-cli.mjs list bugs
70
70
  node <skill-root>/scripts/zentao-cli.mjs list tasks
71
71
  node <skill-root>/scripts/zentao-cli.mjs get bug <id>
72
72
  node <skill-root>/scripts/zentao-cli.mjs get task <id>
73
+ node <skill-root>/scripts/zentao-cli.mjs get story <id>
73
74
  node <skill-root>/scripts/zentao-cli.mjs get bug <id> --download-dir <path>
74
75
  ```
75
76
 
@@ -84,6 +85,7 @@ confirmation steps below:
84
85
  node <skill-root>/scripts/zentao-cli.mjs comment bug <id>
85
86
  node <skill-root>/scripts/zentao-cli.mjs comment task <id>
86
87
  node <skill-root>/scripts/zentao-cli.mjs resolve bug <id>
88
+ node <skill-root>/scripts/zentao-cli.mjs log-hours task <id>
87
89
  node <skill-root>/scripts/zentao-cli.mjs finish task <id>
88
90
  ```
89
91
 
@@ -92,28 +94,43 @@ Input shapes:
92
94
  ```json
93
95
  {"comment":"Root cause and result."}
94
96
  {"resolution":"fixed","resolvedBuild":"trunk","comment":"Root cause and result, commit abc1234."}
97
+ {"date":"2026-08-11","consumed":2,"left":14,"work":"Implemented the first part of the task."}
95
98
  {"currentConsumed":1.5,"realStarted":"2026-08-11 09:00:00","finishedDate":"2026-08-11 10:30:00"}
96
99
  ```
97
100
 
98
101
  For `duplicate`, also pass `"duplicateBug": <id>`. Send JSON through stdin,
99
102
  not as a command-line argument. The CLI handles UTF-8 form encoding and
100
103
  computes a task's total consumed hours from its current ZenTao value.
104
+ `log-hours` defaults `date` to today, requires positive remaining hours,
105
+ and keeps the task open. Use `finish` when the task is complete.
101
106
 
102
107
  ## Usage
103
108
 
104
109
  - `/at-zentao bug <id>` — handle a single bug.
105
110
  - `/at-zentao task <id>` — handle a single task.
111
+ - `/at-zentao story <id>` — read requirement scope and acceptance context.
112
+ This mode is read-only; do not implement a Story status or comment workflow.
106
113
  - `/at-zentao bugs` — list bugs assigned to the configured account; let the
107
114
  user select one or more.
108
115
  - `/at-zentao tasks` — list assigned tasks and let the user select.
109
116
  - `/at-zentao export bug <id>` or `export task <id>` — create a read-only,
110
117
  self-contained handoff bundle.
111
118
 
119
+ Treat text after an item id as a natural-language request. Phrases such as
120
+ `填工时`, `记录工时`, `log hours`, and `worklog` enter task time-entry mode;
121
+ users do not need to know the internal `log-hours` CLI command. Reuse any date,
122
+ hours, or work description already supplied instead of asking twice.
123
+
112
124
  If a list response includes pager data showing more items than returned, tell
113
125
  the user the shown and total counts. Do not silently imply the list is complete.
114
126
  Do not browse through products/projects; start from assigned lists or an
115
127
  explicit item id.
116
128
 
129
+ When a fetched Bug or Task has a positive `story` id, fetch that Story before
130
+ planning the implementation. Use its `spec` and `verify` fields to identify
131
+ scope, acceptance criteria, constraints, and non-goals. Keep the Bug or Task as
132
+ the unit of work: never change, close, activate, or comment on the Story.
133
+
117
134
  ## Per-item workflow
118
135
 
119
136
  Follow these steps in order:
@@ -147,10 +164,16 @@ Bug resolutions are `fixed`, `notrepro`, `duplicate`, `bydesign`, `external`,
147
164
  write-back comment is one sentence containing root cause, change summary, and
148
165
  the commit hash when committed.
149
166
 
150
- For tasks, default to comment only. Offer `finish` only for a simple task
151
- completed in one sitting, and ask the user for `currentConsumed`; never invent
152
- hours. For multi-day work or per-day time records, comment and direct the user
153
- to ZenTao's web UI.
167
+ For tasks, ask whether to record the current work after the verified result.
168
+ For an incomplete task, collect the actual `consumed` hours and work date. When
169
+ the task has a numeric current `left`, suggest the new `left` by subtracting the
170
+ current entry and make that estimate editable in the confirmation; ask only
171
+ when no reliable suggestion is possible. Draft `work` from the verified result
172
+ when context is available. It is optional, so mention the omission without
173
+ blocking the write when there is nothing useful to add. For a completed task,
174
+ collect `currentConsumed` and draft a `finish` write. Never infer consumed
175
+ hours. Show all submitted values and require the same explicit ZenTao
176
+ confirmation before either write.
154
177
 
155
178
  ## Export mode
156
179
 
@@ -355,6 +355,13 @@ function itemKind(value) {
355
355
  return value;
356
356
  }
357
357
 
358
+ function detailKind(value) {
359
+ if (value !== "bug" && value !== "task" && value !== "story") {
360
+ throw new CliError("usage_error", "detail type must be bug, task, or story");
361
+ }
362
+ return value;
363
+ }
364
+
358
365
  function pick(source, keys) {
359
366
  const output = {};
360
367
  for (const key of keys) if (source?.[key] !== undefined) output[key] = source[key];
@@ -367,7 +374,7 @@ function normalizeDetail(kind, response) {
367
374
  if (!detail || typeof detail !== "object") {
368
375
  throw new CliError("response_error", `ZenTao response has no ${kind} detail`);
369
376
  }
370
- return { raw: detail, safe: pick(detail, [
377
+ const fields = [
371
378
  "id",
372
379
  "title",
373
380
  "name",
@@ -380,15 +387,24 @@ function normalizeDetail(kind, response) {
380
387
  "product",
381
388
  "project",
382
389
  "execution",
390
+ "story",
383
391
  "type",
384
392
  "openedDate",
385
393
  "deadline",
386
- ]) };
394
+ ];
395
+ if (kind === "task") {
396
+ fields.push("estimate", "consumed", "left", "realStarted", "finishedDate");
397
+ } else if (kind === "story") {
398
+ fields.push("stage", "category", "plan", "estimate", "spec", "verify", "source", "sourceNote");
399
+ }
400
+ return { raw: detail, safe: pick(detail, fields) };
387
401
  }
388
402
 
389
403
  function attachmentUrls(detail) {
390
404
  const found = new Set();
391
- const html = [detail.steps, detail.desc].filter((entry) => typeof entry === "string").join("\n");
405
+ const html = [detail.steps, detail.desc, detail.spec, detail.verify]
406
+ .filter((entry) => typeof entry === "string")
407
+ .join("\n");
392
408
  for (const match of html.matchAll(/(?:src|href)=["']([^"']*\/file-(?:read|download)-\d+[^"']*)["']/gi)) {
393
409
  found.add(match[1].replaceAll("&amp;", "&"));
394
410
  }
@@ -461,18 +477,64 @@ function legacyResult(response) {
461
477
  return { ok: true, result, ...(typeof message === "string" ? { message: message.slice(0, 500) } : {}) };
462
478
  }
463
479
 
480
+ async function workhourVariant(client, id) {
481
+ const variants = [
482
+ { route: `task-recordworkhour-${id}.json`, dateField: "date[1]", legacy: false },
483
+ { route: `task-recordestimate-${id}.json`, dateField: "dates[1]", legacy: true },
484
+ ];
485
+ for (const variant of variants) {
486
+ try {
487
+ decodeLegacy(await client.json(variant.route));
488
+ return variant;
489
+ } catch (error) {
490
+ const unavailable = error instanceof CliError && (
491
+ (error.code === "http_error" && error.status === 404) ||
492
+ error.code === "response_error"
493
+ );
494
+ if (!unavailable) throw error;
495
+ }
496
+ }
497
+ throw new CliError(
498
+ "unsupported_version",
499
+ "ZenTao exposes neither recordWorkhour nor recordEstimate for this task"
500
+ );
501
+ }
502
+
464
503
  function localDateTime(date = new Date()) {
465
504
  const part = (value) => String(value).padStart(2, "0");
466
505
  return `${date.getFullYear()}-${part(date.getMonth() + 1)}-${part(date.getDate())} ${part(date.getHours())}:${part(date.getMinutes())}:${part(date.getSeconds())}`;
467
506
  }
468
507
 
508
+ function localDate(date = new Date()) {
509
+ return localDateTime(date).slice(0, 10);
510
+ }
511
+
512
+ function effortDate(value) {
513
+ const date = value === undefined ? localDate() : value;
514
+ if (typeof date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
515
+ throw new CliError("usage_error", "date must use YYYY-MM-DD");
516
+ }
517
+ const [year, month, day] = date.split("-").map(Number);
518
+ const parsed = new Date(year, month - 1, day);
519
+ if (
520
+ parsed.getFullYear() !== year ||
521
+ parsed.getMonth() !== month - 1 ||
522
+ parsed.getDate() !== day
523
+ ) {
524
+ throw new CliError("usage_error", "date must be a valid calendar date");
525
+ }
526
+ if (date > localDate()) throw new CliError("usage_error", "date cannot be in the future");
527
+ return date;
528
+ }
529
+
469
530
  function help() {
470
531
  return `Usage:
471
532
  zentao-cli.mjs doctor
472
533
  zentao-cli.mjs list <bugs|tasks>
473
- zentao-cli.mjs get <bug|task> <id> [--download-dir <path>]
534
+ zentao-cli.mjs get <bug|task|story> <id> [--download-dir <path>]
474
535
  zentao-cli.mjs resolve bug <id> # JSON on stdin
475
536
  zentao-cli.mjs comment <bug|task> <id> # {"comment":"..."} on stdin
537
+ zentao-cli.mjs log-hours task <id> # JSON on stdin
476
538
  zentao-cli.mjs finish task <id> # JSON on stdin`;
477
539
  }
478
540
 
@@ -497,20 +559,21 @@ export async function run(argv, { env = process.env } = {}) {
497
559
  const singular = plural.slice(0, -1);
498
560
  const data = decodeLegacy(await client.json(`my-work-${singular}.json`));
499
561
  const fields = singular === "bug"
500
- ? ["id", "title", "severity", "pri", "status", "project", "product"]
501
- : ["id", "name", "title", "pri", "status", "project", "execution", "module"];
562
+ ? ["id", "title", "severity", "pri", "status", "project", "product", "story"]
563
+ : ["id", "name", "title", "pri", "status", "project", "execution", "module", "story", "estimate", "consumed", "left", "realStarted", "finishedDate"];
502
564
  const items = Array.isArray(data?.[plural]) ? data[plural].map((item) => pick(item, fields)) : [];
503
565
  return { items, ...(data?.pager ? { pager: pick(data.pager, ["recTotal", "recPerPage", "pageID", "pageTotal"]) } : {}) };
504
566
  }
505
567
 
506
568
  if (command === "get") {
507
- const kind = itemKind(args[0]);
569
+ const kind = detailKind(args[0]);
508
570
  const id = positiveId(args[1]);
509
571
  let directory;
510
572
  if (args[2] === "--download-dir" && args[3]) directory = path.resolve(args[3]);
511
573
  else if (args.length > 2) throw new CliError("usage_error", "get accepts only --download-dir <path>");
512
574
  else directory = fs.mkdtempSync(path.join(os.tmpdir(), `agent-tools-zentao-${kind}-${id}-`));
513
- const detail = normalizeDetail(kind, await client.json(`api.php/v1/${kind}s/${id}`));
575
+ const resource = kind === "story" ? "stories" : `${kind}s`;
576
+ const detail = normalizeDetail(kind, await client.json(`api.php/v1/${resource}/${id}`));
514
577
  const attachments = await downloadAttachments(client, detail.raw, directory);
515
578
  return { item: detail.safe, attachments };
516
579
  }
@@ -551,6 +614,38 @@ export async function run(argv, { env = process.env } = {}) {
551
614
  return legacyResult(response);
552
615
  }
553
616
 
617
+ if (command === "log-hours") {
618
+ if (args[0] !== "task") throw new CliError("usage_error", "log-hours supports tasks only");
619
+ const id = positiveId(args[1]);
620
+ const input = await readInput();
621
+ const consumed = Number(input.consumed);
622
+ const left = Number(input.left);
623
+ if (!Number.isFinite(consumed) || consumed <= 0) {
624
+ throw new CliError("usage_error", "consumed must be positive");
625
+ }
626
+ if (!Number.isFinite(left) || left <= 0) {
627
+ throw new CliError("usage_error", "left must be positive; use finish to complete a task");
628
+ }
629
+ if (input.work !== undefined && typeof input.work !== "string") {
630
+ throw new CliError("usage_error", "work must be a string when provided");
631
+ }
632
+ const date = effortDate(input.date);
633
+ const variant = await workhourVariant(client, id);
634
+ const fields = {
635
+ [variant.dateField]: date,
636
+ "work[1]": input.work?.trim(),
637
+ "consumed[1]": consumed,
638
+ "left[1]": left,
639
+ };
640
+ if (variant.legacy) fields["id[1]"] = 1;
641
+ const response = await client.json(variant.route, {
642
+ method: "POST",
643
+ headers: { "content-type": "application/x-www-form-urlencoded" },
644
+ body: formBody(fields),
645
+ });
646
+ return legacyResult(response);
647
+ }
648
+
554
649
  if (command === "finish") {
555
650
  if (args[0] !== "task") throw new CliError("usage_error", "finish supports tasks only");
556
651
  const id = positiveId(args[1]);
File without changes
File without changes
File without changes