@wrongstack/plugins 1.0.7 → 1.0.10
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/dist/accessibility-auditor/index.d.ts +1 -1
- package/dist/accessibility-auditor.js +15 -3
- package/dist/agent-handoff.js +6 -6
- package/dist/auto-doc/index.d.ts +1 -1
- package/dist/auto-doc.js +31 -20
- package/dist/auto-i18n-extractor/index.d.ts +1 -1
- package/dist/auto-i18n-extractor.js +12 -8
- package/dist/branch-guard.js +3 -3
- package/dist/changelog-writer/index.d.ts +1 -1
- package/dist/changelog-writer.js +19 -10
- package/dist/checkpoint/index.d.ts +1 -1
- package/dist/checkpoint.js +38 -24
- package/dist/code-metrics/index.d.ts +1 -1
- package/dist/code-metrics.js +12 -4
- package/dist/commit-validator.js +5 -5
- package/dist/context-pins/index.d.ts +1 -1
- package/dist/context-pins.js +12 -9
- package/dist/cost-tracker.js +21 -9
- package/dist/cron/index.d.ts +1 -1
- package/dist/cron.js +18 -5
- package/dist/dead-code-detector/index.d.ts +1 -1
- package/dist/dead-code-detector.js +14 -12
- package/dist/duplicate-code-detector/index.d.ts +1 -1
- package/dist/duplicate-code-detector.js +11 -3
- package/dist/feature-flag-tracker/index.d.ts +1 -1
- package/dist/feature-flag-tracker.js +12 -4
- package/dist/file-watcher/index.d.ts +1 -1
- package/dist/file-watcher.js +37 -38
- package/dist/git-autocommit/index.d.ts +1 -1
- package/dist/git-autocommit.js +44 -39
- package/dist/gitignore-guard/index.d.ts +1 -1
- package/dist/gitignore-guard.js +12 -6
- package/dist/index.js +1534 -1080
- package/dist/interface-contract-guard/index.d.ts +1 -1
- package/dist/interface-contract-guard.js +12 -4
- package/dist/knowledge-graph/index.d.ts +1 -1
- package/dist/knowledge-graph.js +11 -9
- package/dist/migration-planner/index.d.ts +1 -1
- package/dist/migration-planner.js +6 -2
- package/dist/notify-hub/index.d.ts +1 -1
- package/dist/notify-hub.js +12 -11
- package/dist/performance-regression-gate/index.d.ts +1 -1
- package/dist/performance-regression-gate.js +33 -13
- package/dist/pr-drafter/index.d.ts +10 -1
- package/dist/pr-drafter.js +57 -26
- package/dist/refactor-suggester/index.d.ts +1 -1
- package/dist/refactor-suggester.js +17 -4
- package/dist/release-notes-generator.js +2 -2
- package/dist/secret-scanner/index.d.ts +1 -1
- package/dist/secret-scanner.js +11 -3
- package/dist/security-hotspot-scanner/index.d.ts +1 -1
- package/dist/security-hotspot-scanner.js +6 -2
- package/dist/semantic-search-indexer/index.d.ts +1 -1
- package/dist/semantic-search-indexer.js +19 -3
- package/dist/semver-bump/index.d.ts +1 -1
- package/dist/semver-bump.js +57 -37
- package/dist/session-recap.js +4 -2
- package/dist/shell-check/index.d.ts +1 -1
- package/dist/shell-check.js +30 -39
- package/dist/smart-rename/index.d.ts +1 -1
- package/dist/smart-rename.js +23 -10
- package/dist/template-engine/index.d.ts +1 -1
- package/dist/template-engine.js +51 -38
- package/dist/test-flake-detector/index.d.ts +1 -1
- package/dist/test-flake-detector.js +11 -5
- package/dist/test-generator/index.d.ts +1 -1
- package/dist/test-generator.js +12 -8
- package/dist/todo-tracker/index.d.ts +68 -1
- package/dist/todo-tracker.js +579 -395
- package/dist/token-budget.js +7 -4
- package/dist/token-throttle.js +6 -3
- package/package.json +7 -7
package/dist/todo-tracker.js
CHANGED
|
@@ -1,447 +1,631 @@
|
|
|
1
1
|
// src/todo-tracker/index.ts
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fsp from "node:fs/promises";
|
|
4
|
-
import { dirname } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { basename, dirname, extname } from "node:path";
|
|
5
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
6
|
+
import { atomicWrite, ensureDir, withFileLock } from "@wrongstack/core/utils";
|
|
6
7
|
import { nowIso } from "@wrongstack/primitives";
|
|
8
|
+
var STATUSES = ["pending", "in_progress", "completed", "dropped"];
|
|
9
|
+
var PRIORITIES = ["low", "normal", "high"];
|
|
10
|
+
var defaultDeps = {
|
|
11
|
+
readFile: (p) => fsp.readFile(p, "utf8"),
|
|
12
|
+
rename: (from, to) => fsp.rename(from, to),
|
|
13
|
+
atomicWrite: (p, content, opts) => atomicWrite(p, content, opts),
|
|
14
|
+
withFileLock: (p, fn) => withFileLock(p, fn),
|
|
15
|
+
ensureDir: (dir) => ensureDir(dir)
|
|
16
|
+
};
|
|
17
|
+
function deriveProjectSlug(filePath) {
|
|
18
|
+
const base = basename(filePath.replace(/[\\/]+$/, "").replace(/\\/g, "/"));
|
|
19
|
+
const ext = extname(base);
|
|
20
|
+
const stem = ext && ext !== base ? base.slice(0, -ext.length) : base;
|
|
21
|
+
return stem || "tracker";
|
|
22
|
+
}
|
|
7
23
|
function deriveFilePath(api) {
|
|
8
24
|
const raw = api.config.extensions?.["todo-tracker"];
|
|
9
25
|
const rawPath = raw?.["filePath"] ?? raw?.["file_path"] ?? raw?.["path"] ?? raw?.["file"] ?? raw?.["targetFile"];
|
|
10
26
|
const explicit = typeof rawPath === "string" && rawPath.trim().length > 0 ? rawPath.trim() : null;
|
|
11
27
|
if (explicit) {
|
|
12
|
-
|
|
13
|
-
return { filePath: explicit, projectSlug: base };
|
|
28
|
+
return { filePath: explicit, projectSlug: deriveProjectSlug(explicit) };
|
|
14
29
|
}
|
|
15
30
|
return { filePath: null, projectSlug: null };
|
|
16
31
|
}
|
|
17
32
|
var FILE_VERSION = 1;
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
var isStr = (v) => typeof v === "string";
|
|
34
|
+
var isOptStr = (v) => v === void 0 || v === null || typeof v === "string";
|
|
35
|
+
function validateItem(raw, index) {
|
|
36
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
37
|
+
return `items[${index}] is not an object`;
|
|
38
|
+
}
|
|
39
|
+
const it = raw;
|
|
40
|
+
if (!isStr(it["id"]) || it["id"].length === 0) return `items[${index}].id is not a string`;
|
|
41
|
+
if (!isStr(it["content"])) return `items[${index}].content is not a string`;
|
|
42
|
+
if (!STATUSES.includes(it["status"])) {
|
|
43
|
+
return `items[${index}].status is not one of ${STATUSES.join("|")}`;
|
|
44
|
+
}
|
|
45
|
+
if (!PRIORITIES.includes(it["priority"])) {
|
|
46
|
+
return `items[${index}].priority is not one of ${PRIORITIES.join("|")}`;
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(it["tags"]) || !it["tags"].every(isStr)) {
|
|
49
|
+
return `items[${index}].tags is not a string[]`;
|
|
50
|
+
}
|
|
51
|
+
if (!isStr(it["createdAt"]) || !isStr(it["updatedAt"])) {
|
|
52
|
+
return `items[${index}] timestamps are not strings`;
|
|
53
|
+
}
|
|
54
|
+
if (!isOptStr(it["completedAt"]) || !isOptStr(it["sourceSessionId"]) || !isOptStr(it["notes"])) {
|
|
55
|
+
return `items[${index}] optional fields are not string|null`;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
id: it["id"],
|
|
59
|
+
content: it["content"],
|
|
60
|
+
status: it["status"],
|
|
61
|
+
priority: it["priority"],
|
|
62
|
+
tags: [...it["tags"]],
|
|
63
|
+
createdAt: it["createdAt"],
|
|
64
|
+
updatedAt: it["updatedAt"],
|
|
65
|
+
completedAt: it["completedAt"] ?? null,
|
|
66
|
+
sourceSessionId: it["sourceSessionId"] ?? null,
|
|
67
|
+
notes: it["notes"] ?? null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function parseTrackerFile(rawText) {
|
|
71
|
+
const text = rawText.charCodeAt(0) === 65279 ? rawText.slice(1) : rawText;
|
|
72
|
+
let parsed;
|
|
20
73
|
try {
|
|
21
|
-
|
|
74
|
+
parsed = JSON.parse(text);
|
|
22
75
|
} catch (err) {
|
|
23
|
-
|
|
24
|
-
throw err;
|
|
76
|
+
return { kind: "corrupt", reason: `invalid JSON: ${err.message}` };
|
|
25
77
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
78
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
79
|
+
return { kind: "corrupt", reason: "top-level value is not an object" };
|
|
80
|
+
}
|
|
81
|
+
const obj = parsed;
|
|
82
|
+
if (typeof obj["version"] !== "number") {
|
|
83
|
+
return { kind: "corrupt", reason: "missing or non-numeric version" };
|
|
84
|
+
}
|
|
85
|
+
if (obj["version"] !== FILE_VERSION) {
|
|
86
|
+
return { kind: "unsupportedVersion", version: obj["version"] };
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(obj["items"])) {
|
|
89
|
+
return { kind: "invalidItems", reason: "items is not an array" };
|
|
90
|
+
}
|
|
91
|
+
const items = [];
|
|
92
|
+
for (const [i, rawItem] of obj["items"].entries()) {
|
|
93
|
+
const v = validateItem(rawItem, i);
|
|
94
|
+
if (typeof v === "string") return { kind: "invalidItems", reason: v };
|
|
95
|
+
items.push(v);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
kind: "ok",
|
|
99
|
+
file: {
|
|
100
|
+
version: FILE_VERSION,
|
|
101
|
+
// Any stored slug is accepted (older versions stored the basename
|
|
102
|
+
// including the extension); the current slug is written on next save.
|
|
103
|
+
projectSlug: isStr(obj["projectSlug"]) ? obj["projectSlug"] : "",
|
|
104
|
+
updatedAt: isStr(obj["updatedAt"]) ? obj["updatedAt"] : "",
|
|
105
|
+
items
|
|
30
106
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function loadFile(deps, filePath) {
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = await deps.readFile(filePath);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err.code === "ENOENT") return { kind: "missing" };
|
|
115
|
+
throw err;
|
|
34
116
|
}
|
|
117
|
+
return parseTrackerFile(raw);
|
|
35
118
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await atomicWrite(filePath, JSON.stringify(file, null, 2), { mode: 384 });
|
|
119
|
+
function quarantineSuffix() {
|
|
120
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
|
|
39
121
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
projectSlug: null,
|
|
43
|
-
file: null,
|
|
44
|
-
addCount: 0,
|
|
45
|
-
completeCount: 0,
|
|
46
|
-
dropCount: 0,
|
|
47
|
-
removeCount: 0,
|
|
48
|
-
pullCount: 0,
|
|
49
|
-
/** Most recent mutation for /diag plugins visibility. */
|
|
50
|
-
lastMutation: null
|
|
51
|
-
};
|
|
52
|
-
function ensureFile() {
|
|
53
|
-
if (!state.file) {
|
|
54
|
-
state.file = {
|
|
55
|
-
version: FILE_VERSION,
|
|
56
|
-
projectSlug: state.projectSlug ?? "unconfigured",
|
|
57
|
-
updatedAt: nowIso(),
|
|
58
|
-
items: []
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
return state.file;
|
|
122
|
+
function emptyFile(slug) {
|
|
123
|
+
return { version: FILE_VERSION, projectSlug: slug, updatedAt: nowIso(), items: [] };
|
|
62
124
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
else if (op === "remove") state.removeCount += 1;
|
|
69
|
-
else if (op === "pull") state.pullCount += 1;
|
|
125
|
+
function requireItemId(input) {
|
|
126
|
+
const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
|
|
127
|
+
const id = typeof rawId === "string" ? rawId.trim() : "";
|
|
128
|
+
if (!id) throw new ToolValidationError({ message: "id is required", field: "id" });
|
|
129
|
+
return id;
|
|
70
130
|
}
|
|
71
|
-
function
|
|
72
|
-
|
|
131
|
+
function commonPrefixLength(a, b) {
|
|
132
|
+
const n = Math.min(a.length, b.length);
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (i < n && a[i] === b[i]) i++;
|
|
135
|
+
return i;
|
|
73
136
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
function requireItemIndex(file, id) {
|
|
138
|
+
const idx = file.items.findIndex((it) => it.id === id);
|
|
139
|
+
if (idx !== -1) return idx;
|
|
140
|
+
const open = file.items.filter((it) => it.status === "pending" || it.status === "in_progress");
|
|
141
|
+
const pool = open.length > 0 ? open : file.items;
|
|
142
|
+
const candidates = [...pool].sort((a, b) => commonPrefixLength(b.id, id) - commonPrefixLength(a.id, id)).slice(0, 5).map((it) => `${it.id} ("${it.content.slice(0, 40)}")`);
|
|
143
|
+
const hint = candidates.length > 0 ? ` Known ${open.length > 0 ? "open " : ""}ids: ${candidates.join(", ")}` : " The tracker is empty.";
|
|
144
|
+
throw new ToolValidationError({ message: `no item with id ${id}.${hint}`, field: "id" });
|
|
79
145
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
filePath: {
|
|
93
|
-
type: "string",
|
|
94
|
-
description: "Override the auto-derived per-project path. Defaults to <projectDir>/todo-tracker.json when `paths.projectDir` is provided by the host."
|
|
146
|
+
function createTodoTrackerPlugin(overrides = {}) {
|
|
147
|
+
const deps = { ...defaultDeps, ...overrides };
|
|
148
|
+
const instances = /* @__PURE__ */ new WeakMap();
|
|
149
|
+
let latest = null;
|
|
150
|
+
function unregisterTools(inst) {
|
|
151
|
+
const unregister = inst.api.tools.unregister;
|
|
152
|
+
for (const name of inst.registeredTools.splice(0)) {
|
|
153
|
+
if (typeof unregister !== "function") continue;
|
|
154
|
+
try {
|
|
155
|
+
unregister.call(inst.api.tools, name);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
inst.api.log.warn("todo-tracker: failed to unregister tool", { name, err });
|
|
95
158
|
}
|
|
96
159
|
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
160
|
+
}
|
|
161
|
+
function assertWritable(inst) {
|
|
162
|
+
if (inst.readOnlyReason !== null) {
|
|
163
|
+
throw new Error(`todo-tracker: store is read-only \u2014 ${inst.readOnlyReason}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function serialize(inst, fn) {
|
|
167
|
+
const run = inst.queue.then(fn, fn);
|
|
168
|
+
inst.queue = run.catch(() => void 0);
|
|
169
|
+
return run;
|
|
170
|
+
}
|
|
171
|
+
async function quarantineLocked(inst, reason) {
|
|
172
|
+
const target = `${inst.filePath}.corrupt-${quarantineSuffix()}`;
|
|
173
|
+
try {
|
|
174
|
+
await deps.rename(inst.filePath, target);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
inst.readOnlyReason = `${inst.filePath} is unreadable (${reason}) and could not be moved aside (${err.message}); fix or remove the file, then reload the plugin`;
|
|
177
|
+
inst.api.log.error(
|
|
178
|
+
"todo-tracker: corrupt file could not be quarantined; store is read-only",
|
|
179
|
+
{
|
|
180
|
+
filePath: inst.filePath,
|
|
181
|
+
reason,
|
|
182
|
+
err
|
|
183
|
+
}
|
|
110
184
|
);
|
|
111
|
-
return;
|
|
185
|
+
return false;
|
|
112
186
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
187
|
+
inst.degradedReason = `${inst.filePath} was unreadable (${reason}); original moved to ${target}`;
|
|
188
|
+
inst.api.log.error(`todo-tracker: corrupt file quarantined to ${target}`, {
|
|
189
|
+
filePath: inst.filePath,
|
|
190
|
+
quarantinedTo: target,
|
|
191
|
+
reason
|
|
192
|
+
});
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
async function resolveLoad(inst, res, opts) {
|
|
196
|
+
switch (res.kind) {
|
|
197
|
+
case "ok":
|
|
198
|
+
return res.file;
|
|
199
|
+
case "missing":
|
|
200
|
+
return emptyFile(inst.projectSlug);
|
|
201
|
+
case "unsupportedVersion": {
|
|
202
|
+
inst.readOnlyReason = `${inst.filePath} has format version ${JSON.stringify(res.version)}, this plugin only writes version ${FILE_VERSION}; refusing to modify it`;
|
|
203
|
+
inst.api.log.error("todo-tracker: unsupported file version; store is read-only", {
|
|
204
|
+
filePath: inst.filePath,
|
|
205
|
+
version: res.version
|
|
206
|
+
});
|
|
207
|
+
if (opts.strict) assertWritable(inst);
|
|
208
|
+
return emptyFile(inst.projectSlug);
|
|
209
|
+
}
|
|
210
|
+
case "corrupt":
|
|
211
|
+
case "invalidItems": {
|
|
212
|
+
const quarantine = async () => {
|
|
213
|
+
const again = opts.locked ? res : await loadFile(deps, inst.filePath);
|
|
214
|
+
if (again.kind === "ok") return again.file;
|
|
215
|
+
if (again.kind === "missing") return emptyFile(inst.projectSlug);
|
|
216
|
+
if (again.kind === "unsupportedVersion") {
|
|
217
|
+
return resolveLoad(inst, again, { locked: true, strict: opts.strict });
|
|
218
|
+
}
|
|
219
|
+
const moved = await quarantineLocked(inst, again.reason);
|
|
220
|
+
if (!moved && opts.strict) assertWritable(inst);
|
|
221
|
+
return emptyFile(inst.projectSlug);
|
|
222
|
+
};
|
|
223
|
+
return opts.locked ? quarantine() : deps.withFileLock(inst.filePath, quarantine);
|
|
224
|
+
}
|
|
123
225
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
226
|
+
}
|
|
227
|
+
async function refresh(inst) {
|
|
228
|
+
if (inst.readOnlyReason !== null) return inst.file;
|
|
229
|
+
const res = await loadFile(deps, inst.filePath);
|
|
230
|
+
const file = await resolveLoad(inst, res, { locked: false, strict: false });
|
|
231
|
+
inst.file = file;
|
|
232
|
+
return file;
|
|
233
|
+
}
|
|
234
|
+
function mutate(inst, apply) {
|
|
235
|
+
return serialize(
|
|
236
|
+
inst,
|
|
237
|
+
() => deps.withFileLock(inst.filePath, async () => {
|
|
238
|
+
assertWritable(inst);
|
|
239
|
+
const res = await loadFile(deps, inst.filePath);
|
|
240
|
+
const current = await resolveLoad(inst, res, { locked: true, strict: true });
|
|
241
|
+
const draft = structuredClone(current);
|
|
242
|
+
const now = nowIso();
|
|
243
|
+
const { changed, result } = apply(draft, now);
|
|
244
|
+
if (changed) {
|
|
245
|
+
draft.version = FILE_VERSION;
|
|
246
|
+
draft.projectSlug = inst.projectSlug;
|
|
247
|
+
draft.updatedAt = now;
|
|
248
|
+
await deps.ensureDir(dirname(inst.filePath));
|
|
249
|
+
await deps.atomicWrite(inst.filePath, JSON.stringify(draft, null, 2), { mode: 384 });
|
|
250
|
+
inst.file = draft;
|
|
251
|
+
} else {
|
|
252
|
+
inst.file = current;
|
|
138
253
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
254
|
+
return result;
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function recordMutation(inst, op, itemId) {
|
|
259
|
+
inst.lastMutation = { op, itemId, when: nowIso() };
|
|
260
|
+
if (op === "add") inst.addCount += 1;
|
|
261
|
+
else if (op === "complete") inst.completeCount += 1;
|
|
262
|
+
else if (op === "drop") inst.dropCount += 1;
|
|
263
|
+
else if (op === "remove") inst.removeCount += 1;
|
|
264
|
+
}
|
|
265
|
+
function sessionCounts(inst) {
|
|
266
|
+
return {
|
|
267
|
+
add: inst.addCount,
|
|
268
|
+
complete: inst.completeCount,
|
|
269
|
+
drop: inst.dropCount,
|
|
270
|
+
remove: inst.removeCount,
|
|
271
|
+
pull: inst.pullCount
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function disposeInstance(api) {
|
|
275
|
+
const inst = instances.get(api);
|
|
276
|
+
if (!inst) return void 0;
|
|
277
|
+
unregisterTools(inst);
|
|
278
|
+
instances.delete(api);
|
|
279
|
+
if (latest === inst) latest = null;
|
|
280
|
+
return inst;
|
|
281
|
+
}
|
|
282
|
+
const plugin2 = {
|
|
283
|
+
name: "todo-tracker",
|
|
284
|
+
version: "0.1.0",
|
|
285
|
+
description: "Persistent, project-scoped todo backlog that survives across sessions",
|
|
286
|
+
apiVersion: "^0.1.10",
|
|
287
|
+
capabilities: { tools: true },
|
|
288
|
+
defaultConfig: {
|
|
289
|
+
filePath: ""
|
|
290
|
+
},
|
|
291
|
+
configSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
filePath: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: "Override the auto-derived per-project path. Defaults to <projectDir>/todo-tracker.json when `paths.projectDir` is provided by the host."
|
|
158
297
|
}
|
|
159
|
-
if (priority) items = items.filter((it) => it.priority.toLowerCase() === priority);
|
|
160
|
-
if (tag) items = items.filter((it) => it.tags.includes(tag));
|
|
161
|
-
const total = items.length;
|
|
162
|
-
const truncated = items.slice(0, limit);
|
|
163
|
-
return {
|
|
164
|
-
ok: true,
|
|
165
|
-
total,
|
|
166
|
-
returned: truncated.length,
|
|
167
|
-
truncated: total > truncated.length,
|
|
168
|
-
items: truncated
|
|
169
|
-
};
|
|
170
298
|
}
|
|
171
|
-
}
|
|
172
|
-
api
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
299
|
+
},
|
|
300
|
+
async setup(api) {
|
|
301
|
+
disposeInstance(api);
|
|
302
|
+
const derived = deriveFilePath(api);
|
|
303
|
+
if (derived.filePath === null) {
|
|
304
|
+
latest = null;
|
|
305
|
+
api.log.warn(
|
|
306
|
+
'todo-tracker: no file path configured (set `config.extensions["todo-tracker"].filePath` or wire `paths.projectDir` through PluginAPI) \u2014 tools will report a clear error'
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const inst = {
|
|
311
|
+
api,
|
|
312
|
+
filePath: derived.filePath,
|
|
313
|
+
projectSlug: derived.projectSlug ?? "tracker",
|
|
314
|
+
file: emptyFile(derived.projectSlug ?? "tracker"),
|
|
315
|
+
readOnlyReason: null,
|
|
316
|
+
degradedReason: null,
|
|
317
|
+
queue: Promise.resolve(),
|
|
318
|
+
registeredTools: [],
|
|
319
|
+
addCount: 0,
|
|
320
|
+
completeCount: 0,
|
|
321
|
+
dropCount: 0,
|
|
322
|
+
removeCount: 0,
|
|
323
|
+
pullCount: 0,
|
|
324
|
+
lastMutation: null
|
|
325
|
+
};
|
|
326
|
+
instances.set(api, inst);
|
|
327
|
+
latest = inst;
|
|
328
|
+
await refresh(inst);
|
|
329
|
+
const register = (tool) => {
|
|
330
|
+
api.tools.register(tool);
|
|
331
|
+
inst.registeredTools.push(tool.name);
|
|
332
|
+
};
|
|
333
|
+
register({
|
|
334
|
+
name: "todo_tracker_list",
|
|
335
|
+
description: "List persistent todo-tracker items. Filterable by status, priority, and tag. By default only pending + in_progress items are shown.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
status: {
|
|
340
|
+
type: "string",
|
|
341
|
+
enum: ["pending", "in_progress", "completed", "dropped", "all"],
|
|
342
|
+
description: "Filter by status. 'all' returns every item; default is pending+in_progress."
|
|
343
|
+
},
|
|
344
|
+
priority: { type: "string", enum: ["low", "normal", "high"] },
|
|
345
|
+
tag: { type: "string", description: "Filter by exact tag match" },
|
|
346
|
+
limit: { type: "number", description: "Max items to return (default 50, max 200)" }
|
|
347
|
+
}
|
|
187
348
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
349
|
+
permission: "auto",
|
|
350
|
+
mutating: false,
|
|
351
|
+
async execute(input) {
|
|
352
|
+
const rawStatus = typeof input["status"] === "string" ? input["status"].trim().toLowerCase() : void 0;
|
|
353
|
+
const status = rawStatus ?? "active";
|
|
354
|
+
const priority = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : void 0;
|
|
355
|
+
const tag = typeof input["tag"] === "string" ? input["tag"] : void 0;
|
|
356
|
+
const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
|
|
357
|
+
const file = await refresh(inst);
|
|
358
|
+
let items = file.items;
|
|
359
|
+
if (status !== "all") {
|
|
360
|
+
if (status === "active") {
|
|
361
|
+
items = items.filter((it) => it.status === "pending" || it.status === "in_progress");
|
|
362
|
+
} else {
|
|
363
|
+
items = items.filter((it) => it.status === status);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (priority) items = items.filter((it) => it.priority === priority);
|
|
367
|
+
if (tag) items = items.filter((it) => it.tags.includes(tag));
|
|
368
|
+
const total = items.length;
|
|
369
|
+
const truncated = items.slice(0, limit);
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
total,
|
|
373
|
+
returned: truncated.length,
|
|
374
|
+
truncated: total > truncated.length,
|
|
375
|
+
items: truncated,
|
|
376
|
+
...inst.readOnlyReason ? { readOnly: inst.readOnlyReason } : {}
|
|
377
|
+
};
|
|
198
378
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
379
|
+
});
|
|
380
|
+
register({
|
|
381
|
+
name: "todo_tracker_add",
|
|
382
|
+
description: "Append a new item to the persistent todo-tracker backlog.",
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {
|
|
386
|
+
content: { type: "string", description: "What needs doing (required)" },
|
|
387
|
+
priority: { type: "string", enum: ["low", "normal", "high"], default: "normal" },
|
|
388
|
+
tags: {
|
|
389
|
+
type: "array",
|
|
390
|
+
items: { type: "string" },
|
|
391
|
+
description: "Optional tags for filtering"
|
|
392
|
+
},
|
|
393
|
+
sourceSessionId: { type: "string", description: "Session that created this item" },
|
|
394
|
+
notes: { type: "string", description: "Optional free-form notes" }
|
|
395
|
+
},
|
|
396
|
+
required: ["content"]
|
|
397
|
+
},
|
|
398
|
+
permission: "auto",
|
|
399
|
+
mutating: true,
|
|
400
|
+
async execute(input) {
|
|
401
|
+
const rawContent = input["content"] ?? input["text"] ?? input["task"] ?? input["title"] ?? input["todo"] ?? input["message"] ?? input["item"];
|
|
402
|
+
const content = typeof rawContent === "string" ? rawContent.trim() : "";
|
|
403
|
+
if (!content) {
|
|
404
|
+
throw new ToolValidationError({
|
|
405
|
+
message: "content is required and must be a non-empty string",
|
|
406
|
+
field: "content"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
const rawPri = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : "";
|
|
410
|
+
const priority = rawPri === "low" || rawPri === "high" ? rawPri : "normal";
|
|
411
|
+
const tags = Array.isArray(input["tags"]) ? input["tags"].filter((t) => typeof t === "string") : [];
|
|
412
|
+
const sourceSessionId = typeof input["sourceSessionId"] === "string" ? input["sourceSessionId"] : null;
|
|
413
|
+
const notes = typeof input["notes"] === "string" ? input["notes"] : null;
|
|
414
|
+
const item = await mutate(inst, (draft, now) => {
|
|
415
|
+
const created = {
|
|
416
|
+
id: randomUUID(),
|
|
417
|
+
content,
|
|
418
|
+
status: "pending",
|
|
419
|
+
priority,
|
|
420
|
+
tags,
|
|
421
|
+
createdAt: now,
|
|
422
|
+
updatedAt: now,
|
|
423
|
+
completedAt: null,
|
|
424
|
+
sourceSessionId,
|
|
425
|
+
notes
|
|
426
|
+
};
|
|
427
|
+
draft.items.push(created);
|
|
428
|
+
return { changed: true, result: created };
|
|
231
429
|
});
|
|
232
|
-
|
|
430
|
+
recordMutation(inst, "add", item.id);
|
|
431
|
+
api.log.info("todo-tracker: added item", { id: item.id, content });
|
|
432
|
+
try {
|
|
433
|
+
await api.session?.append?.({
|
|
434
|
+
type: "todo-tracker:add",
|
|
435
|
+
ts: item.createdAt,
|
|
436
|
+
id: item.id,
|
|
437
|
+
content,
|
|
438
|
+
priority,
|
|
439
|
+
tags
|
|
440
|
+
});
|
|
441
|
+
} catch (err) {
|
|
442
|
+
api.log.warn("todo-tracker: session.append failed (item was saved)", {
|
|
443
|
+
id: item.id,
|
|
444
|
+
err
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return { ok: true, item };
|
|
233
448
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
449
|
+
});
|
|
450
|
+
const setTerminalStatus = async (input, target) => {
|
|
451
|
+
const id = requireItemId(input);
|
|
452
|
+
return mutate(inst, (draft, now) => {
|
|
453
|
+
const item = draft.items[requireItemIndex(draft, id)];
|
|
454
|
+
if (item.status === target) {
|
|
455
|
+
return {
|
|
456
|
+
changed: false,
|
|
457
|
+
result: { ok: true, item, message: `already ${target} (idempotent)` }
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
item.status = target;
|
|
461
|
+
item.updatedAt = now;
|
|
462
|
+
item.completedAt = now;
|
|
463
|
+
return { changed: true, result: { ok: true, item } };
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
register({
|
|
467
|
+
name: "todo_tracker_complete",
|
|
468
|
+
description: "Mark a tracked item as completed. Idempotent.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
id: { type: "string", description: "Item id" }
|
|
473
|
+
},
|
|
474
|
+
required: ["id"]
|
|
244
475
|
},
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const idx = findItemIndex(id);
|
|
255
|
-
if (idx === -1) return { ok: false, error: `no item with id ${id}` };
|
|
256
|
-
const file = ensureFile();
|
|
257
|
-
const item = file.items[idx];
|
|
258
|
-
if (item.status === "completed") {
|
|
259
|
-
return { ok: true, item, message: "already completed (idempotent)" };
|
|
476
|
+
permission: "auto",
|
|
477
|
+
mutating: true,
|
|
478
|
+
async execute(input) {
|
|
479
|
+
const result = await setTerminalStatus(input, "completed");
|
|
480
|
+
if (!result.message) {
|
|
481
|
+
recordMutation(inst, "complete", result.item.id);
|
|
482
|
+
api.log.info("todo-tracker: completed item", { id: result.item.id });
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
260
485
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
item.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
api.tools.register({
|
|
273
|
-
name: "todo_tracker_drop",
|
|
274
|
-
description: "Mark a tracked item as dropped (skipped/obsolete). The row is kept for audit. Idempotent.",
|
|
275
|
-
inputSchema: {
|
|
276
|
-
type: "object",
|
|
277
|
-
properties: {
|
|
278
|
-
id: { type: "string", description: "Item id" }
|
|
486
|
+
});
|
|
487
|
+
register({
|
|
488
|
+
name: "todo_tracker_drop",
|
|
489
|
+
description: "Mark a tracked item as dropped (skipped/obsolete). The row is kept for audit. Idempotent.",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
type: "object",
|
|
492
|
+
properties: {
|
|
493
|
+
id: { type: "string", description: "Item id" }
|
|
494
|
+
},
|
|
495
|
+
required: ["id"]
|
|
279
496
|
},
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
|
|
287
|
-
const id = typeof rawId === "string" ? rawId.trim() : "";
|
|
288
|
-
if (!id) return { ok: false, error: "id is required" };
|
|
289
|
-
const idx = findItemIndex(id);
|
|
290
|
-
if (idx === -1) return { ok: false, error: `no item with id ${id}` };
|
|
291
|
-
const file = ensureFile();
|
|
292
|
-
const item = file.items[idx];
|
|
293
|
-
if (item.status === "dropped") {
|
|
294
|
-
return { ok: true, item, message: "already dropped (idempotent)" };
|
|
497
|
+
permission: "auto",
|
|
498
|
+
mutating: true,
|
|
499
|
+
async execute(input) {
|
|
500
|
+
const result = await setTerminalStatus(input, "dropped");
|
|
501
|
+
if (!result.message) recordMutation(inst, "drop", result.item.id);
|
|
502
|
+
return result;
|
|
295
503
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
item.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
api.tools.register({
|
|
307
|
-
name: "todo_tracker_remove",
|
|
308
|
-
description: "Permanently delete a tracked item by id. Use todo_tracker_drop instead if you want to keep the audit row.",
|
|
309
|
-
inputSchema: {
|
|
310
|
-
type: "object",
|
|
311
|
-
properties: {
|
|
312
|
-
id: { type: "string", description: "Item id" }
|
|
504
|
+
});
|
|
505
|
+
register({
|
|
506
|
+
name: "todo_tracker_remove",
|
|
507
|
+
description: "Permanently delete a tracked item by id. Use todo_tracker_drop instead if you want to keep the audit row.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
properties: {
|
|
511
|
+
id: { type: "string", description: "Item id" }
|
|
512
|
+
},
|
|
513
|
+
required: ["id"]
|
|
313
514
|
},
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (idx === -1) return { ok: false, error: `no item with id ${id}` };
|
|
325
|
-
const file = ensureFile();
|
|
326
|
-
const [removed] = file.items.splice(idx, 1);
|
|
327
|
-
file.updatedAt = nowIso();
|
|
328
|
-
await saveFile(state.filePath, file);
|
|
329
|
-
recordMutation("remove", id);
|
|
330
|
-
return { ok: true, removed };
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
api.tools.register({
|
|
334
|
-
name: "todo_tracker_pull",
|
|
335
|
-
description: "Return all pending + in_progress items. The LLM is expected to take this list and re-register each entry with the session-local `todo` tool (which mutates ctx.todos). After pull, the LLM may also choose to call todo_tracker_complete on items it finishes mid-session.",
|
|
336
|
-
inputSchema: {
|
|
337
|
-
type: "object",
|
|
338
|
-
properties: {
|
|
339
|
-
limit: { type: "number", description: "Max items to return (default 50, max 200)" }
|
|
515
|
+
permission: "confirm",
|
|
516
|
+
mutating: true,
|
|
517
|
+
async execute(input) {
|
|
518
|
+
const id = requireItemId(input);
|
|
519
|
+
const removed = await mutate(inst, (draft) => {
|
|
520
|
+
const [gone] = draft.items.splice(requireItemIndex(draft, id), 1);
|
|
521
|
+
return { changed: true, result: gone };
|
|
522
|
+
});
|
|
523
|
+
recordMutation(inst, "remove", id);
|
|
524
|
+
return { ok: true, removed };
|
|
340
525
|
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
526
|
+
});
|
|
527
|
+
register({
|
|
528
|
+
name: "todo_tracker_pull",
|
|
529
|
+
description: "Return all pending + in_progress items. The LLM is expected to take this list and re-register each entry with the session-local `todo` tool (which mutates ctx.todos). After pull, the LLM may also choose to call todo_tracker_complete on items it finishes mid-session.",
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {
|
|
533
|
+
limit: { type: "number", description: "Max items to return (default 50, max 200)" }
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
permission: "auto",
|
|
537
|
+
mutating: false,
|
|
538
|
+
async execute(input) {
|
|
539
|
+
const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
|
|
540
|
+
const file = await refresh(inst);
|
|
541
|
+
const open = file.items.filter(
|
|
542
|
+
(it) => it.status === "pending" || it.status === "in_progress"
|
|
543
|
+
);
|
|
544
|
+
const items = open.slice(0, limit);
|
|
545
|
+
if (items.length > 0) inst.pullCount += 1;
|
|
546
|
+
return {
|
|
547
|
+
ok: true,
|
|
548
|
+
total: open.length,
|
|
549
|
+
returned: items.length,
|
|
550
|
+
truncated: open.length > items.length,
|
|
551
|
+
items,
|
|
552
|
+
hint: "These are persistent items. To work on them this session, register each one with the built-in `todo` tool. Mark them `completed` via todo_tracker_complete when done."
|
|
553
|
+
};
|
|
351
554
|
}
|
|
555
|
+
});
|
|
556
|
+
register({
|
|
557
|
+
name: "todo_tracker_status",
|
|
558
|
+
description: "Report todo-tracker counters (per-status totals) + the file path + last update timestamp.",
|
|
559
|
+
inputSchema: { type: "object", properties: {} },
|
|
560
|
+
permission: "auto",
|
|
561
|
+
mutating: false,
|
|
562
|
+
async execute() {
|
|
563
|
+
const file = await refresh(inst);
|
|
564
|
+
const byStatus = {
|
|
565
|
+
pending: 0,
|
|
566
|
+
in_progress: 0,
|
|
567
|
+
completed: 0,
|
|
568
|
+
dropped: 0
|
|
569
|
+
};
|
|
570
|
+
for (const it of file.items) {
|
|
571
|
+
if (STATUSES.includes(it.status)) byStatus[it.status] += 1;
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
ok: true,
|
|
575
|
+
filePath: inst.filePath,
|
|
576
|
+
projectSlug: inst.projectSlug,
|
|
577
|
+
updatedAt: file.updatedAt,
|
|
578
|
+
counters: byStatus,
|
|
579
|
+
total: file.items.length,
|
|
580
|
+
session: sessionCounts(inst),
|
|
581
|
+
lastMutation: inst.lastMutation,
|
|
582
|
+
readOnly: inst.readOnlyReason,
|
|
583
|
+
degraded: inst.degradedReason
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
api.log.info("todo-tracker plugin loaded", {
|
|
588
|
+
filePath: inst.filePath,
|
|
589
|
+
projectSlug: inst.projectSlug,
|
|
590
|
+
initialItemCount: inst.file.items.length,
|
|
591
|
+
readOnly: inst.readOnlyReason,
|
|
592
|
+
degraded: inst.degradedReason
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
teardown(api) {
|
|
596
|
+
const inst = disposeInstance(api);
|
|
597
|
+
if (!inst) return;
|
|
598
|
+
api.log.info("todo-tracker: teardown complete", { sessionCounts: sessionCounts(inst) });
|
|
599
|
+
},
|
|
600
|
+
async health() {
|
|
601
|
+
const inst = latest;
|
|
602
|
+
if (inst === null) {
|
|
352
603
|
return {
|
|
353
|
-
ok:
|
|
354
|
-
|
|
355
|
-
items,
|
|
356
|
-
hint: "These are persistent items. To work on them this session, register each one with the built-in `todo` tool. Mark them `completed` via todo_tracker_complete when done."
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
api.tools.register({
|
|
361
|
-
name: "todo_tracker_status",
|
|
362
|
-
description: "Report todo-tracker counters (per-status totals) + the file path + last update timestamp.",
|
|
363
|
-
inputSchema: { type: "object", properties: {} },
|
|
364
|
-
permission: "auto",
|
|
365
|
-
mutating: false,
|
|
366
|
-
async execute() {
|
|
367
|
-
if (state.filePath === null) return notConfiguredError();
|
|
368
|
-
const file = ensureFile();
|
|
369
|
-
const byStatus = {
|
|
370
|
-
pending: 0,
|
|
371
|
-
in_progress: 0,
|
|
372
|
-
completed: 0,
|
|
373
|
-
dropped: 0
|
|
374
|
-
};
|
|
375
|
-
for (const it of file.items) byStatus[it.status] += 1;
|
|
376
|
-
return {
|
|
377
|
-
ok: true,
|
|
378
|
-
filePath: state.filePath,
|
|
379
|
-
projectSlug: state.projectSlug,
|
|
380
|
-
updatedAt: file.updatedAt,
|
|
381
|
-
counters: byStatus,
|
|
382
|
-
total: file.items.length,
|
|
383
|
-
session: {
|
|
384
|
-
add: state.addCount,
|
|
385
|
-
complete: state.completeCount,
|
|
386
|
-
drop: state.dropCount,
|
|
387
|
-
remove: state.removeCount,
|
|
388
|
-
pull: state.pullCount
|
|
389
|
-
},
|
|
390
|
-
lastMutation: state.lastMutation
|
|
604
|
+
ok: false,
|
|
605
|
+
message: "todo-tracker: no file path configured \u2014 tools will error"
|
|
391
606
|
};
|
|
392
607
|
}
|
|
393
|
-
|
|
394
|
-
api.log.info("todo-tracker plugin loaded", {
|
|
395
|
-
filePath: state.filePath,
|
|
396
|
-
projectSlug: state.projectSlug,
|
|
397
|
-
initialItemCount: state.file.items.length
|
|
398
|
-
});
|
|
399
|
-
},
|
|
400
|
-
teardown(api) {
|
|
401
|
-
const finalCounts = {
|
|
402
|
-
add: state.addCount,
|
|
403
|
-
complete: state.completeCount,
|
|
404
|
-
drop: state.dropCount,
|
|
405
|
-
remove: state.removeCount,
|
|
406
|
-
pull: state.pullCount
|
|
407
|
-
};
|
|
408
|
-
state.addCount = 0;
|
|
409
|
-
state.completeCount = 0;
|
|
410
|
-
state.dropCount = 0;
|
|
411
|
-
state.removeCount = 0;
|
|
412
|
-
state.pullCount = 0;
|
|
413
|
-
state.lastMutation = null;
|
|
414
|
-
state.file = null;
|
|
415
|
-
state.filePath = null;
|
|
416
|
-
state.projectSlug = null;
|
|
417
|
-
api.log.info("todo-tracker: teardown complete", { sessionCounts: finalCounts });
|
|
418
|
-
},
|
|
419
|
-
async health() {
|
|
420
|
-
if (state.filePath === null) {
|
|
608
|
+
const reason = inst.readOnlyReason ?? inst.degradedReason;
|
|
421
609
|
return {
|
|
422
|
-
ok:
|
|
423
|
-
message:
|
|
610
|
+
ok: reason === null,
|
|
611
|
+
message: reason === null ? `todo-tracker: ${inst.file.items.length} item(s) at ${inst.filePath}` : `todo-tracker: ${inst.readOnlyReason ? "read-only" : "degraded"} \u2014 ${reason}`,
|
|
612
|
+
filePath: inst.filePath,
|
|
613
|
+
projectSlug: inst.projectSlug,
|
|
614
|
+
total: inst.file.items.length,
|
|
615
|
+
readOnly: inst.readOnlyReason,
|
|
616
|
+
degraded: inst.degradedReason,
|
|
617
|
+
sessionCounts: sessionCounts(inst),
|
|
618
|
+
lastMutation: inst.lastMutation
|
|
424
619
|
};
|
|
425
620
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
filePath: state.filePath,
|
|
431
|
-
projectSlug: state.projectSlug,
|
|
432
|
-
total: file.items.length,
|
|
433
|
-
sessionCounts: {
|
|
434
|
-
add: state.addCount,
|
|
435
|
-
complete: state.completeCount,
|
|
436
|
-
drop: state.dropCount,
|
|
437
|
-
remove: state.removeCount,
|
|
438
|
-
pull: state.pullCount
|
|
439
|
-
},
|
|
440
|
-
lastMutation: state.lastMutation
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
};
|
|
621
|
+
};
|
|
622
|
+
return plugin2;
|
|
623
|
+
}
|
|
624
|
+
var plugin = createTodoTrackerPlugin();
|
|
444
625
|
var todo_tracker_default = plugin;
|
|
445
626
|
export {
|
|
446
|
-
|
|
627
|
+
createTodoTrackerPlugin,
|
|
628
|
+
todo_tracker_default as default,
|
|
629
|
+
deriveProjectSlug,
|
|
630
|
+
parseTrackerFile
|
|
447
631
|
};
|