@wrongstack/plugins 0.277.2 → 0.280.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +838 -0
  2. package/dist/auto-doc.d.ts +8 -0
  3. package/dist/auto-doc.js +175 -13
  4. package/dist/auto-escalate.d.ts +45 -0
  5. package/dist/auto-escalate.js +190 -0
  6. package/dist/branch-guard.d.ts +33 -0
  7. package/dist/branch-guard.js +228 -0
  8. package/dist/changelog-writer.d.ts +73 -0
  9. package/dist/changelog-writer.js +369 -0
  10. package/dist/checkpoint.d.ts +55 -0
  11. package/dist/checkpoint.js +305 -0
  12. package/dist/commit-validator.d.ts +33 -0
  13. package/dist/commit-validator.js +315 -0
  14. package/dist/config-validator.d.ts +48 -0
  15. package/dist/config-validator.js +347 -0
  16. package/dist/context-pins.d.ts +45 -0
  17. package/dist/context-pins.js +240 -0
  18. package/dist/cost-tracker.d.ts +40 -1
  19. package/dist/cost-tracker.js +105 -4
  20. package/dist/dep-guard.d.ts +65 -0
  21. package/dist/dep-guard.js +316 -0
  22. package/dist/diff-summary.d.ts +36 -0
  23. package/dist/diff-summary.js +235 -0
  24. package/dist/error-lens.d.ts +67 -0
  25. package/dist/error-lens.js +280 -0
  26. package/dist/format-on-save.d.ts +35 -0
  27. package/dist/format-on-save.js +219 -0
  28. package/dist/git-autocommit.js +186 -26
  29. package/dist/import-organizer.d.ts +52 -0
  30. package/dist/import-organizer.js +274 -0
  31. package/dist/index.d.ts +32 -6
  32. package/dist/index.js +10151 -1628
  33. package/dist/injection-shield.d.ts +49 -0
  34. package/dist/injection-shield.js +205 -0
  35. package/dist/lint-gate.d.ts +33 -0
  36. package/dist/lint-gate.js +394 -0
  37. package/dist/llm-cache.d.ts +56 -0
  38. package/dist/llm-cache.js +251 -0
  39. package/dist/loop-breaker.d.ts +43 -0
  40. package/dist/loop-breaker.js +241 -0
  41. package/dist/model-router.d.ts +69 -0
  42. package/dist/model-router.js +198 -0
  43. package/dist/notify-hub.d.ts +45 -0
  44. package/dist/notify-hub.js +304 -0
  45. package/dist/path-guard.d.ts +54 -0
  46. package/dist/path-guard.js +235 -0
  47. package/dist/prompt-firewall.d.ts +57 -0
  48. package/dist/prompt-firewall.js +290 -0
  49. package/dist/secret-scanner.d.ts +34 -0
  50. package/dist/secret-scanner.js +409 -0
  51. package/dist/semver-bump.js +45 -0
  52. package/dist/session-recap.d.ts +50 -0
  53. package/dist/session-recap.js +421 -0
  54. package/dist/shell-check.js +52 -4
  55. package/dist/spec-linker.d.ts +51 -0
  56. package/dist/spec-linker.js +541 -0
  57. package/dist/template-engine.js +19 -1
  58. package/dist/test-runner-gate.d.ts +37 -0
  59. package/dist/test-runner-gate.js +356 -0
  60. package/dist/todo-listener.d.ts +37 -0
  61. package/dist/todo-listener.js +216 -0
  62. package/dist/todo-tracker.d.ts +5 -0
  63. package/dist/todo-tracker.js +441 -0
  64. package/dist/token-budget.d.ts +40 -0
  65. package/dist/token-budget.js +254 -0
  66. package/dist/token-throttle.d.ts +54 -0
  67. package/dist/token-throttle.js +203 -0
  68. package/package.json +116 -12
  69. package/dist/json-path.d.ts +0 -18
  70. package/dist/json-path.js +0 -15
  71. package/dist/web-search.d.ts +0 -19
  72. package/dist/web-search.js +0 -15
@@ -0,0 +1,441 @@
1
+ import * as fsp from 'fs/promises';
2
+ import { randomUUID } from 'crypto';
3
+
4
+ // src/todo-tracker/index.ts
5
+ function deriveFilePath(api) {
6
+ const raw = api.config.extensions?.["todo-tracker"];
7
+ const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
8
+ if (explicit) {
9
+ const base = explicit.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "tracker";
10
+ return { filePath: explicit, projectSlug: base };
11
+ }
12
+ return { filePath: null, projectSlug: null };
13
+ }
14
+ var FILE_VERSION = 1;
15
+ async function loadFile(filePath) {
16
+ let raw;
17
+ try {
18
+ raw = await fsp.readFile(filePath, "utf8");
19
+ } catch (err) {
20
+ if (err.code === "ENOENT") return null;
21
+ throw err;
22
+ }
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (parsed.version !== FILE_VERSION || !Array.isArray(parsed.items)) {
26
+ return null;
27
+ }
28
+ return parsed;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function saveFile(filePath, file) {
34
+ const tmp = `${filePath}.${randomUUID().slice(0, 8)}.tmp`;
35
+ await fsp.mkdir(filePath.replace(/[/\\][^/\\]+$/, ""), { recursive: true });
36
+ await fsp.writeFile(tmp, JSON.stringify(file, null, 2), { encoding: "utf8", mode: 384 });
37
+ await fsp.rename(tmp, filePath);
38
+ }
39
+ var state = {
40
+ filePath: null,
41
+ projectSlug: null,
42
+ file: null,
43
+ addCount: 0,
44
+ completeCount: 0,
45
+ dropCount: 0,
46
+ removeCount: 0,
47
+ pullCount: 0,
48
+ /** Most recent mutation for /diag plugins visibility. */
49
+ lastMutation: null
50
+ };
51
+ function nowIso() {
52
+ return (/* @__PURE__ */ new Date()).toISOString();
53
+ }
54
+ function ensureFile() {
55
+ if (!state.file) {
56
+ state.file = {
57
+ version: FILE_VERSION,
58
+ projectSlug: state.projectSlug ?? "unconfigured",
59
+ updatedAt: nowIso(),
60
+ items: []
61
+ };
62
+ }
63
+ return state.file;
64
+ }
65
+ function recordMutation(op, itemId) {
66
+ state.lastMutation = { op, itemId, when: nowIso() };
67
+ if (op === "add") state.addCount += 1;
68
+ else if (op === "complete") state.completeCount += 1;
69
+ else if (op === "drop") state.dropCount += 1;
70
+ else if (op === "remove") state.removeCount += 1;
71
+ else if (op === "pull") state.pullCount += 1;
72
+ }
73
+ function findItemIndex(id) {
74
+ return ensureFile().items.findIndex((it) => it.id === id);
75
+ }
76
+ function notConfiguredError() {
77
+ return {
78
+ ok: false,
79
+ error: 'todo-tracker: no file path configured. Set `filePath` under `config.extensions["todo-tracker"]` or run inside a session where `paths.projectDir` is provided by the host (e.g. `wstack` CLI).'
80
+ };
81
+ }
82
+ var plugin = {
83
+ name: "todo-tracker",
84
+ version: "0.1.0",
85
+ description: "Persistent, project-scoped todo backlog that survives across sessions",
86
+ apiVersion: "^0.1.10",
87
+ capabilities: { tools: true },
88
+ defaultConfig: {
89
+ filePath: ""
90
+ },
91
+ configSchema: {
92
+ type: "object",
93
+ properties: {
94
+ filePath: {
95
+ type: "string",
96
+ description: "Override the auto-derived per-project path. Defaults to <projectDir>/todo-tracker.json when `paths.projectDir` is provided by the host."
97
+ }
98
+ }
99
+ },
100
+ async setup(api) {
101
+ state.addCount = 0;
102
+ state.completeCount = 0;
103
+ state.dropCount = 0;
104
+ state.removeCount = 0;
105
+ state.pullCount = 0;
106
+ state.lastMutation = null;
107
+ state.file = null;
108
+ const derived = deriveFilePath(api);
109
+ if (derived.filePath === null) {
110
+ api.log.warn(
111
+ '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'
112
+ );
113
+ return;
114
+ }
115
+ state.filePath = derived.filePath;
116
+ state.projectSlug = derived.projectSlug;
117
+ state.file = await loadFile(state.filePath);
118
+ if (state.file === null) {
119
+ state.file = {
120
+ version: FILE_VERSION,
121
+ projectSlug: state.projectSlug ?? "tracker",
122
+ updatedAt: nowIso(),
123
+ items: []
124
+ };
125
+ }
126
+ api.tools.register({
127
+ name: "todo_tracker_list",
128
+ description: "List persistent todo-tracker items. Filterable by status, priority, and tag. By default only pending + in_progress items are shown.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ status: {
133
+ type: "string",
134
+ enum: ["pending", "in_progress", "completed", "dropped", "all"],
135
+ description: "Filter by status. 'all' returns every item; default is pending+in_progress."
136
+ },
137
+ priority: { type: "string", enum: ["low", "normal", "high"] },
138
+ tag: { type: "string", description: "Filter by exact tag match" },
139
+ limit: { type: "number", description: "Max items to return (default 50, max 200)" }
140
+ }
141
+ },
142
+ permission: "auto",
143
+ mutating: false,
144
+ async execute(input) {
145
+ if (state.filePath === null) return notConfiguredError();
146
+ const status = input["status"] ?? "active";
147
+ const priority = input["priority"];
148
+ const tag = input["tag"];
149
+ const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
150
+ const file = ensureFile();
151
+ let items = file.items;
152
+ if (status !== "all") {
153
+ if (status === "active") {
154
+ items = items.filter((it) => it.status === "pending" || it.status === "in_progress");
155
+ } else {
156
+ items = items.filter((it) => it.status === status);
157
+ }
158
+ }
159
+ if (priority) items = items.filter((it) => it.priority === 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
+ }
171
+ });
172
+ api.tools.register({
173
+ name: "todo_tracker_add",
174
+ description: "Append a new item to the persistent todo-tracker backlog.",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ content: { type: "string", description: "What needs doing (required)" },
179
+ priority: { type: "string", enum: ["low", "normal", "high"], default: "normal" },
180
+ tags: {
181
+ type: "array",
182
+ items: { type: "string" },
183
+ description: "Optional tags for filtering"
184
+ },
185
+ sourceSessionId: { type: "string", description: "Session that created this item" },
186
+ notes: { type: "string", description: "Optional free-form notes" }
187
+ },
188
+ required: ["content"]
189
+ },
190
+ permission: "auto",
191
+ mutating: true,
192
+ async execute(input) {
193
+ if (state.filePath === null) return notConfiguredError();
194
+ const content = typeof input["content"] === "string" ? input["content"].trim() : "";
195
+ if (!content) {
196
+ return { ok: false, error: "content is required and must be a non-empty string" };
197
+ }
198
+ const priority = input["priority"] === "low" || input["priority"] === "high" ? input["priority"] : "normal";
199
+ const tags = Array.isArray(input["tags"]) ? input["tags"].filter((t) => typeof t === "string") : [];
200
+ const sourceSessionId = typeof input["sourceSessionId"] === "string" ? input["sourceSessionId"] : null;
201
+ const notes = typeof input["notes"] === "string" ? input["notes"] : null;
202
+ const now = nowIso();
203
+ const item = {
204
+ id: randomUUID(),
205
+ content,
206
+ status: "pending",
207
+ priority,
208
+ tags,
209
+ createdAt: now,
210
+ updatedAt: now,
211
+ completedAt: null,
212
+ sourceSessionId,
213
+ notes
214
+ };
215
+ const file = ensureFile();
216
+ file.items.push(item);
217
+ file.updatedAt = now;
218
+ await saveFile(state.filePath, file);
219
+ recordMutation("add", item.id);
220
+ api.log.info("todo-tracker: added item", { id: item.id, content });
221
+ try {
222
+ await api.session.append({
223
+ type: "todo-tracker:add",
224
+ ts: now,
225
+ id: item.id,
226
+ content,
227
+ priority,
228
+ tags
229
+ });
230
+ } catch {
231
+ }
232
+ return { ok: true, item };
233
+ }
234
+ });
235
+ api.tools.register({
236
+ name: "todo_tracker_complete",
237
+ description: "Mark a tracked item as completed. Idempotent.",
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ id: { type: "string", description: "Item id" }
242
+ },
243
+ required: ["id"]
244
+ },
245
+ permission: "auto",
246
+ mutating: true,
247
+ async execute(input) {
248
+ if (state.filePath === null) return notConfiguredError();
249
+ const id = typeof input["id"] === "string" ? input["id"] : "";
250
+ if (!id) return { ok: false, error: "id is required" };
251
+ const idx = findItemIndex(id);
252
+ if (idx === -1) return { ok: false, error: `no item with id ${id}` };
253
+ const file = ensureFile();
254
+ const item = file.items[idx];
255
+ if (item.status === "completed") {
256
+ return { ok: true, item, message: "already completed (idempotent)" };
257
+ }
258
+ const now = nowIso();
259
+ item.status = "completed";
260
+ item.updatedAt = now;
261
+ item.completedAt = now;
262
+ file.updatedAt = now;
263
+ await saveFile(state.filePath, file);
264
+ recordMutation("complete", id);
265
+ api.log.info("todo-tracker: completed item", { id });
266
+ return { ok: true, item };
267
+ }
268
+ });
269
+ api.tools.register({
270
+ name: "todo_tracker_drop",
271
+ description: "Mark a tracked item as dropped (skipped/obsolete). The row is kept for audit. Idempotent.",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ id: { type: "string", description: "Item id" }
276
+ },
277
+ required: ["id"]
278
+ },
279
+ permission: "auto",
280
+ mutating: true,
281
+ async execute(input) {
282
+ if (state.filePath === null) return notConfiguredError();
283
+ const id = typeof input["id"] === "string" ? input["id"] : "";
284
+ if (!id) return { ok: false, error: "id is required" };
285
+ const idx = findItemIndex(id);
286
+ if (idx === -1) return { ok: false, error: `no item with id ${id}` };
287
+ const file = ensureFile();
288
+ const item = file.items[idx];
289
+ if (item.status === "dropped") {
290
+ return { ok: true, item, message: "already dropped (idempotent)" };
291
+ }
292
+ const now = nowIso();
293
+ item.status = "dropped";
294
+ item.updatedAt = now;
295
+ item.completedAt = now;
296
+ file.updatedAt = now;
297
+ await saveFile(state.filePath, file);
298
+ recordMutation("drop", id);
299
+ return { ok: true, item };
300
+ }
301
+ });
302
+ api.tools.register({
303
+ name: "todo_tracker_remove",
304
+ description: "Permanently delete a tracked item by id. Use todo_tracker_drop instead if you want to keep the audit row.",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ id: { type: "string", description: "Item id" }
309
+ },
310
+ required: ["id"]
311
+ },
312
+ permission: "confirm",
313
+ mutating: true,
314
+ async execute(input) {
315
+ if (state.filePath === null) return notConfiguredError();
316
+ const id = typeof input["id"] === "string" ? input["id"] : "";
317
+ if (!id) return { ok: false, error: "id is required" };
318
+ const idx = findItemIndex(id);
319
+ if (idx === -1) return { ok: false, error: `no item with id ${id}` };
320
+ const file = ensureFile();
321
+ const [removed] = file.items.splice(idx, 1);
322
+ file.updatedAt = nowIso();
323
+ await saveFile(state.filePath, file);
324
+ recordMutation("remove", id);
325
+ return { ok: true, removed };
326
+ }
327
+ });
328
+ api.tools.register({
329
+ name: "todo_tracker_pull",
330
+ 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.",
331
+ inputSchema: {
332
+ type: "object",
333
+ properties: {
334
+ limit: { type: "number", description: "Max items to return (default 50, max 200)" }
335
+ }
336
+ },
337
+ permission: "auto",
338
+ mutating: false,
339
+ async execute(input) {
340
+ if (state.filePath === null) return notConfiguredError();
341
+ const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
342
+ const file = ensureFile();
343
+ const items = file.items.filter((it) => it.status === "pending" || it.status === "in_progress").slice(0, limit);
344
+ if (items.length > 0) {
345
+ recordMutation("pull", items[0].id);
346
+ }
347
+ return {
348
+ ok: true,
349
+ total: items.length,
350
+ items,
351
+ 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."
352
+ };
353
+ }
354
+ });
355
+ api.tools.register({
356
+ name: "todo_tracker_status",
357
+ description: "Report todo-tracker counters (per-status totals) + the file path + last update timestamp.",
358
+ inputSchema: { type: "object", properties: {} },
359
+ permission: "auto",
360
+ mutating: false,
361
+ async execute() {
362
+ if (state.filePath === null) return notConfiguredError();
363
+ const file = ensureFile();
364
+ const byStatus = {
365
+ pending: 0,
366
+ in_progress: 0,
367
+ completed: 0,
368
+ dropped: 0
369
+ };
370
+ for (const it of file.items) byStatus[it.status] += 1;
371
+ return {
372
+ ok: true,
373
+ filePath: state.filePath,
374
+ projectSlug: state.projectSlug,
375
+ updatedAt: file.updatedAt,
376
+ counters: byStatus,
377
+ total: file.items.length,
378
+ session: {
379
+ add: state.addCount,
380
+ complete: state.completeCount,
381
+ drop: state.dropCount,
382
+ remove: state.removeCount,
383
+ pull: state.pullCount
384
+ },
385
+ lastMutation: state.lastMutation
386
+ };
387
+ }
388
+ });
389
+ api.log.info("todo-tracker plugin loaded", {
390
+ filePath: state.filePath,
391
+ projectSlug: state.projectSlug,
392
+ initialItemCount: state.file.items.length
393
+ });
394
+ },
395
+ teardown(api) {
396
+ const finalCounts = {
397
+ add: state.addCount,
398
+ complete: state.completeCount,
399
+ drop: state.dropCount,
400
+ remove: state.removeCount,
401
+ pull: state.pullCount
402
+ };
403
+ state.addCount = 0;
404
+ state.completeCount = 0;
405
+ state.dropCount = 0;
406
+ state.removeCount = 0;
407
+ state.pullCount = 0;
408
+ state.lastMutation = null;
409
+ state.file = null;
410
+ state.filePath = null;
411
+ state.projectSlug = null;
412
+ api.log.info("todo-tracker: teardown complete", { sessionCounts: finalCounts });
413
+ },
414
+ async health() {
415
+ if (state.filePath === null) {
416
+ return {
417
+ ok: false,
418
+ message: "todo-tracker: no file path configured \u2014 tools will error"
419
+ };
420
+ }
421
+ const file = ensureFile();
422
+ return {
423
+ ok: true,
424
+ message: `todo-tracker: ${file.items.length} item(s) at ${state.filePath}`,
425
+ filePath: state.filePath,
426
+ projectSlug: state.projectSlug,
427
+ total: file.items.length,
428
+ sessionCounts: {
429
+ add: state.addCount,
430
+ complete: state.completeCount,
431
+ drop: state.dropCount,
432
+ remove: state.removeCount,
433
+ pull: state.pullCount
434
+ },
435
+ lastMutation: state.lastMutation
436
+ };
437
+ }
438
+ };
439
+ var todo_tracker_default = plugin;
440
+
441
+ export { todo_tracker_default as default };
@@ -0,0 +1,40 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * token-budget plugin — Enforces a per-session token budget.
5
+ *
6
+ * Complements cost-tracker (which tracks but does not enforce).
7
+ * When token usage crosses the warning threshold, injects a "wrap up"
8
+ * signal into the LLM's context. When it crosses the hard limit,
9
+ * triggers a Stop hook to end the agent loop.
10
+ *
11
+ * Tools registered:
12
+ * - token_budget_status : Show budget limit, current usage, percent
13
+ * consumed, and whether the warning/stop
14
+ * threshold has been crossed.
15
+ *
16
+ * Hooks registered:
17
+ * - Stop : checks whether the budget is exhausted; if so, returns
18
+ * `decision: 'block'` with a reason so the agent loop does
19
+ * not continue.
20
+ *
21
+ * Events subscribed:
22
+ * - provider.response : accumulates prompt + completion tokens.
23
+ *
24
+ * Config surface (`config.extensions['token-budget']`):
25
+ *
26
+ * ```jsonc
27
+ * {
28
+ * "limit": 500000, // hard token limit (prompt + completion)
29
+ * "warnPercent": 80, // inject "wrap up" at this % of limit
30
+ * "stopPercent": 100, // trigger Stop at this % of limit
31
+ * "model": null // null = all models; or restrict to one
32
+ * }
33
+ * ```
34
+ *
35
+ * @public
36
+ */
37
+
38
+ declare const plugin: Plugin;
39
+
40
+ export { plugin as default };