@kairyou/agent-tools 0.19.0 → 0.20.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.
package/package.json
CHANGED
|
@@ -72,6 +72,7 @@ node <skill-root>/scripts/zentao-cli.mjs get bug <id>
|
|
|
72
72
|
node <skill-root>/scripts/zentao-cli.mjs get task <id>
|
|
73
73
|
node <skill-root>/scripts/zentao-cli.mjs get story <id>
|
|
74
74
|
node <skill-root>/scripts/zentao-cli.mjs get bug <id> --download-dir <path>
|
|
75
|
+
node <skill-root>/scripts/zentao-cli.mjs hours task <id>
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
`get` downloads token-gated inline images and attachments into a temporary
|
|
@@ -89,6 +90,7 @@ node <skill-root>/scripts/zentao-cli.mjs start task <id>
|
|
|
89
90
|
node <skill-root>/scripts/zentao-cli.mjs pause task <id>
|
|
90
91
|
node <skill-root>/scripts/zentao-cli.mjs resume task <id>
|
|
91
92
|
node <skill-root>/scripts/zentao-cli.mjs log-hours task <id>
|
|
93
|
+
node <skill-root>/scripts/zentao-cli.mjs edit-hours task <id> <effort-id>
|
|
92
94
|
node <skill-root>/scripts/zentao-cli.mjs finish task <id>
|
|
93
95
|
```
|
|
94
96
|
|
|
@@ -99,6 +101,7 @@ Input shapes:
|
|
|
99
101
|
{"resolution":"fixed","resolvedBuild":"trunk","comment":"Root cause and result, commit abc1234."}
|
|
100
102
|
{"realStarted":"2026-08-11 09:00:00","comment":"Started implementation."}
|
|
101
103
|
{"date":"2026-08-11","consumed":2,"left":14,"work":"Implemented the first part of the task."}
|
|
104
|
+
{"work":"Corrected work description, commit abc1234."}
|
|
102
105
|
{"currentConsumed":1.5,"realStarted":"2026-08-11 09:00:00","finishedDate":"2026-08-11 10:30:00"}
|
|
103
106
|
```
|
|
104
107
|
|
|
@@ -109,7 +112,9 @@ computes a task's total consumed hours from its current ZenTao value.
|
|
|
109
112
|
accepts `realStarted` and otherwise uses the current time. The CLI preserves
|
|
110
113
|
the task's current hours when starting or resuming it.
|
|
111
114
|
`log-hours` defaults `date` to today, requires positive remaining hours,
|
|
112
|
-
and keeps the task open.
|
|
115
|
+
and keeps the task open. `hours` is read-only. `edit-hours` preserves omitted
|
|
116
|
+
fields from the existing record and updates it through ZenTao's native effort
|
|
117
|
+
workflow. Use `finish` when the task is complete.
|
|
113
118
|
|
|
114
119
|
## Usage
|
|
115
120
|
|
|
@@ -128,6 +133,9 @@ lifecycle requests such as `开始`, `暂停`, `继续`, `start`, `pause`, and
|
|
|
128
133
|
`resume`, and time-entry requests such as `填工时`, `记录工时`, `log hours`,
|
|
129
134
|
and `worklog`. Users do not need to know the internal CLI commands. Reuse any
|
|
130
135
|
date, hours, or work description already supplied instead of asking twice.
|
|
136
|
+
Recognize corrections to an existing time entry. Run `hours`, identify the
|
|
137
|
+
record from returned ids and values, and ask when more than one record could
|
|
138
|
+
match; never guess or call `log-hours` again to compensate for a mistake.
|
|
131
139
|
|
|
132
140
|
If a list response includes pager data showing more items than returned, tell
|
|
133
141
|
the user the shown and total counts. Do not silently imply the list is complete.
|
|
@@ -180,6 +188,11 @@ Bug resolutions are `fixed`, `notrepro`, `duplicate`, `bydesign`, `external`,
|
|
|
180
188
|
write-back comment is one sentence containing root cause, change summary, and
|
|
181
189
|
the commit hash when committed.
|
|
182
190
|
|
|
191
|
+
Immediately before confirming any ZenTao write that cites the latest commit,
|
|
192
|
+
run `git rev-parse HEAD` and `git log -1 --format=%h`. Do not reuse a hash from
|
|
193
|
+
earlier conversation. If HEAD is not the item-specific commit, identify the
|
|
194
|
+
relevant commit and tell the user instead of blindly citing HEAD.
|
|
195
|
+
|
|
183
196
|
For tasks, ask whether to record the current work after the verified result.
|
|
184
197
|
For an incomplete task, collect the actual `consumed` hours and work date. When
|
|
185
198
|
the task has a numeric current `left`, suggest the new `left` by subtracting the
|
|
@@ -191,6 +204,15 @@ collect `currentConsumed` and draft a `finish` write. Never infer consumed
|
|
|
191
204
|
hours. Show all submitted values and require the same explicit ZenTao
|
|
192
205
|
confirmation before either write.
|
|
193
206
|
|
|
207
|
+
To correct an existing time entry, use `hours` to select its effort id. Show
|
|
208
|
+
the current and proposed `date`, `consumed`, `left`, and `work`, then obtain
|
|
209
|
+
explicit confirmation and call `edit-hours` once. Preserve every field the
|
|
210
|
+
user did not change. Stop when ownership cannot be verified or the edit route
|
|
211
|
+
is unsupported. If the proposed `left` is zero, explicitly warn that ZenTao
|
|
212
|
+
may change the task status as part of its native recalculation. Never edit an
|
|
213
|
+
action comment as a substitute for correcting the underlying work-hour record,
|
|
214
|
+
and never delete a record.
|
|
215
|
+
|
|
194
216
|
When the user wants to record hours and pause, show both exact writes in one
|
|
195
217
|
confirmation, then run `log-hours` before `pause`. Stop if the hour write fails.
|
|
196
218
|
Do not add lifecycle support for `activate`, `cancel`, or `close`.
|
|
@@ -459,10 +459,13 @@ async function readInput() {
|
|
|
459
459
|
return value;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
function formBody(fields) {
|
|
462
|
+
function formBody(fields, includeEmpty = []) {
|
|
463
463
|
const body = new URLSearchParams();
|
|
464
|
+
const keepEmpty = new Set(includeEmpty);
|
|
464
465
|
for (const [key, value] of Object.entries(fields)) {
|
|
465
|
-
if (value !== undefined && value !== null && value !== ""
|
|
466
|
+
if (value !== undefined && value !== null && (value !== "" || keepEmpty.has(key))) {
|
|
467
|
+
body.set(key, String(value));
|
|
468
|
+
}
|
|
466
469
|
}
|
|
467
470
|
return body;
|
|
468
471
|
}
|
|
@@ -484,8 +487,8 @@ async function workhourVariant(client, id) {
|
|
|
484
487
|
];
|
|
485
488
|
for (const variant of variants) {
|
|
486
489
|
try {
|
|
487
|
-
decodeLegacy(await client.json(variant.route));
|
|
488
|
-
return variant;
|
|
490
|
+
const form = decodeLegacy(await client.json(variant.route));
|
|
491
|
+
return { ...variant, form };
|
|
489
492
|
} catch (error) {
|
|
490
493
|
const unavailable = error instanceof CliError && (
|
|
491
494
|
(error.code === "http_error" && error.status === 404) ||
|
|
@@ -500,6 +503,88 @@ async function workhourVariant(client, id) {
|
|
|
500
503
|
);
|
|
501
504
|
}
|
|
502
505
|
|
|
506
|
+
function effortRecords(form) {
|
|
507
|
+
const candidates = [
|
|
508
|
+
form?.efforts,
|
|
509
|
+
form?.estimates,
|
|
510
|
+
form?.workhours,
|
|
511
|
+
form?.taskEfforts,
|
|
512
|
+
form?.task?.efforts,
|
|
513
|
+
form?.task?.estimates,
|
|
514
|
+
];
|
|
515
|
+
const collection = candidates.find((value) => value && typeof value === "object");
|
|
516
|
+
if (!collection) return [];
|
|
517
|
+
const entries = Array.isArray(collection)
|
|
518
|
+
? collection.map((value) => [null, value])
|
|
519
|
+
: Object.entries(collection);
|
|
520
|
+
const records = entries
|
|
521
|
+
.filter(([, value]) => value && typeof value === "object")
|
|
522
|
+
.map(([key, value]) => ({
|
|
523
|
+
...(value.id === undefined && /^\d+$/.test(key || "") ? { id: Number(key) } : {}),
|
|
524
|
+
...pick(value, ["id", "date", "consumed", "left", "work"]),
|
|
525
|
+
}));
|
|
526
|
+
return records;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function effortFromEditForm(form) {
|
|
530
|
+
const effort = form?.effort || form?.estimate || form?.workhour ||
|
|
531
|
+
(form?.id !== undefined ? form : null);
|
|
532
|
+
if (!effort || typeof effort !== "object") {
|
|
533
|
+
throw new CliError("response_error", "ZenTao returned no editable work-hour record");
|
|
534
|
+
}
|
|
535
|
+
return effort;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function verifyEffortTask(effort, taskId) {
|
|
539
|
+
if (effort.objectType !== undefined && effort.objectType !== "task") {
|
|
540
|
+
throw new CliError("ownership_error", "The work-hour record does not belong to a task");
|
|
541
|
+
}
|
|
542
|
+
const owner = effort.objectID ?? effort.task ?? effort.taskID;
|
|
543
|
+
if (owner === undefined || String(owner) !== String(taskId)) {
|
|
544
|
+
throw new CliError("ownership_error", "The work-hour record does not belong to the requested task");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function editableEffortVariant(client, effortId) {
|
|
549
|
+
const variants = [
|
|
550
|
+
{ route: `task-editeffort-${effortId}.json`, legacy: false },
|
|
551
|
+
{ route: `task-editestimate-${effortId}.json`, legacy: true },
|
|
552
|
+
];
|
|
553
|
+
for (const variant of variants) {
|
|
554
|
+
try {
|
|
555
|
+
const form = decodeLegacy(await client.json(variant.route));
|
|
556
|
+
return { ...variant, effort: effortFromEditForm(form) };
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const unavailable = error instanceof CliError && (
|
|
559
|
+
(error.code === "http_error" && error.status === 404) ||
|
|
560
|
+
error.code === "response_error"
|
|
561
|
+
);
|
|
562
|
+
if (!unavailable) throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
throw new CliError(
|
|
566
|
+
"unsupported_version",
|
|
567
|
+
"ZenTao exposes neither editEffort nor editEstimate for this work-hour record"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function effortFields(effort, input) {
|
|
572
|
+
const date = effortDate(input.date ?? effort.date);
|
|
573
|
+
const consumed = Number(input.consumed ?? effort.consumed);
|
|
574
|
+
const left = Number(input.left ?? effort.left);
|
|
575
|
+
const work = input.work ?? effort.work ?? "";
|
|
576
|
+
if (!Number.isFinite(consumed) || consumed <= 0) {
|
|
577
|
+
throw new CliError("usage_error", "consumed must be positive");
|
|
578
|
+
}
|
|
579
|
+
if (!Number.isFinite(left) || left < 0) {
|
|
580
|
+
throw new CliError("usage_error", "left must be zero or positive");
|
|
581
|
+
}
|
|
582
|
+
if (typeof work !== "string") {
|
|
583
|
+
throw new CliError("usage_error", "work must be a string");
|
|
584
|
+
}
|
|
585
|
+
return { date, consumed, left, work: work.trim() };
|
|
586
|
+
}
|
|
587
|
+
|
|
503
588
|
function localDateTime(date = new Date()) {
|
|
504
589
|
const part = (value) => String(value).padStart(2, "0");
|
|
505
590
|
return `${date.getFullYear()}-${part(date.getMonth() + 1)}-${part(date.getDate())} ${part(date.getHours())}:${part(date.getMinutes())}:${part(date.getSeconds())}`;
|
|
@@ -537,7 +622,9 @@ function help() {
|
|
|
537
622
|
zentao-cli.mjs start task <id> # JSON on stdin
|
|
538
623
|
zentao-cli.mjs pause task <id> # JSON on stdin
|
|
539
624
|
zentao-cli.mjs resume task <id> # JSON on stdin
|
|
625
|
+
zentao-cli.mjs hours task <id>
|
|
540
626
|
zentao-cli.mjs log-hours task <id> # JSON on stdin
|
|
627
|
+
zentao-cli.mjs edit-hours task <id> <effort-id> # JSON on stdin
|
|
541
628
|
zentao-cli.mjs finish task <id> # JSON on stdin`;
|
|
542
629
|
}
|
|
543
630
|
|
|
@@ -702,6 +789,36 @@ export async function run(argv, { env = process.env } = {}) {
|
|
|
702
789
|
return legacyResult(response);
|
|
703
790
|
}
|
|
704
791
|
|
|
792
|
+
if (command === "hours") {
|
|
793
|
+
if (args[0] !== "task") throw new CliError("usage_error", "hours supports tasks only");
|
|
794
|
+
const id = positiveId(args[1]);
|
|
795
|
+
const variant = await workhourVariant(client, id);
|
|
796
|
+
return { records: effortRecords(variant.form) };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (command === "edit-hours") {
|
|
800
|
+
if (args[0] !== "task") throw new CliError("usage_error", "edit-hours supports tasks only");
|
|
801
|
+
const taskId = positiveId(args[1]);
|
|
802
|
+
const effortId = positiveId(args[2]);
|
|
803
|
+
const input = await readInput();
|
|
804
|
+
const allowed = ["date", "consumed", "left", "work"];
|
|
805
|
+
if (!allowed.some((field) => Object.hasOwn(input, field))) {
|
|
806
|
+
throw new CliError("usage_error", "at least one work-hour field is required");
|
|
807
|
+
}
|
|
808
|
+
if (input.work !== undefined && typeof input.work !== "string") {
|
|
809
|
+
throw new CliError("usage_error", "work must be a string when provided");
|
|
810
|
+
}
|
|
811
|
+
const variant = await editableEffortVariant(client, effortId);
|
|
812
|
+
verifyEffortTask(variant.effort, taskId);
|
|
813
|
+
const fields = effortFields(variant.effort, input);
|
|
814
|
+
const response = await client.json(variant.route, {
|
|
815
|
+
method: "POST",
|
|
816
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
817
|
+
body: formBody(fields, ["work"]),
|
|
818
|
+
});
|
|
819
|
+
return legacyResult(response);
|
|
820
|
+
}
|
|
821
|
+
|
|
705
822
|
if (command === "finish") {
|
|
706
823
|
if (args[0] !== "task") throw new CliError("usage_error", "finish supports tasks only");
|
|
707
824
|
const id = positiveId(args[1]);
|