@wrongstack/plugins 0.309.1 → 0.310.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.
- package/dist/cost-tracker/index.d.ts +2 -0
- package/dist/cost-tracker.js +24 -9
- package/dist/git-autocommit/index.d.ts +17 -2
- package/dist/git-autocommit.js +162 -53
- package/dist/index.js +185 -65
- package/dist/todo-tracker/index.d.ts +29 -0
- package/dist/todo-tracker.js +2 -4
- package/package.json +5 -4
|
@@ -45,6 +45,8 @@ export interface ModelPricing {
|
|
|
45
45
|
input: number;
|
|
46
46
|
/** Cost per 1M output (completion) tokens in USD. */
|
|
47
47
|
output: number;
|
|
48
|
+
/** Cost per 1M prompt-cache read tokens; input rate is the safe fallback. */
|
|
49
|
+
cacheRead?: number | undefined;
|
|
48
50
|
}
|
|
49
51
|
declare const plugin: Plugin;
|
|
50
52
|
export default plugin;
|
package/dist/cost-tracker.js
CHANGED
|
@@ -12,13 +12,15 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
12
12
|
};
|
|
13
13
|
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
14
14
|
|
|
15
|
+
// src/cost-tracker/index.ts
|
|
16
|
+
import { expectDefined } from "@wrongstack/core/utils";
|
|
17
|
+
|
|
15
18
|
// src/runtime/index.ts
|
|
16
19
|
var runtime_exports = {};
|
|
17
20
|
__reExport(runtime_exports, runtime_star);
|
|
18
21
|
import * as runtime_star from "@wrongstack/plugin-sdk/runtime";
|
|
19
22
|
|
|
20
23
|
// src/cost-tracker/index.ts
|
|
21
|
-
import { expectDefined } from "@wrongstack/core/utils";
|
|
22
24
|
var API_VERSION = "^0.1.10";
|
|
23
25
|
var PRICING = {
|
|
24
26
|
"gpt-4o": { input: 5, output: 15 },
|
|
@@ -48,14 +50,14 @@ function readCostTrackerConfig(raw) {
|
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
var modelKeyCache = new runtime_exports.BoundedMap({ max: 256 });
|
|
51
|
-
function estimateCost(model,
|
|
53
|
+
function estimateCost(model, freshTokens, completionTokens, cachedTokens = 0) {
|
|
52
54
|
let key = modelKeyCache.get(model);
|
|
53
55
|
if (!key) {
|
|
54
56
|
key = model.toLowerCase();
|
|
55
57
|
modelKeyCache.set(model, key);
|
|
56
58
|
}
|
|
57
59
|
const pricing = pricingOverrides[key] ?? bundledFromRegistry[key] ?? PRICING[key] ?? DEFAULT_PRICING;
|
|
58
|
-
const inputCost =
|
|
60
|
+
const inputCost = freshTokens / 1e6 * pricing.input + cachedTokens / 1e6 * (pricing.cacheRead ?? pricing.input);
|
|
59
61
|
const outputCost = completionTokens / 1e6 * pricing.output;
|
|
60
62
|
return inputCost + outputCost;
|
|
61
63
|
}
|
|
@@ -89,12 +91,17 @@ var plugin = {
|
|
|
89
91
|
},
|
|
90
92
|
pricingOverrides: {
|
|
91
93
|
type: "object",
|
|
92
|
-
description: "Per-model pricing overrides in USD per 1M tokens.
|
|
94
|
+
description: "Per-model pricing overrides in USD per 1M tokens. Values are { input, output, cacheRead? }.",
|
|
93
95
|
additionalProperties: {
|
|
94
96
|
type: "object",
|
|
95
97
|
properties: {
|
|
96
98
|
input: { type: "number", minimum: 0, description: "Cost per 1M input tokens in USD" },
|
|
97
|
-
output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" }
|
|
99
|
+
output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" },
|
|
100
|
+
cacheRead: {
|
|
101
|
+
type: "number",
|
|
102
|
+
minimum: 0,
|
|
103
|
+
description: "Cost per 1M prompt-cache read tokens in USD"
|
|
104
|
+
}
|
|
98
105
|
},
|
|
99
106
|
required: ["input", "output"],
|
|
100
107
|
additionalProperties: false
|
|
@@ -127,7 +134,12 @@ var plugin = {
|
|
|
127
134
|
const input = v["input"];
|
|
128
135
|
const output = v["output"];
|
|
129
136
|
if (typeof input !== "number" || typeof output !== "number") continue;
|
|
130
|
-
|
|
137
|
+
const cacheRead = v["cacheRead"];
|
|
138
|
+
pricingOverrides[model.toLowerCase()] = {
|
|
139
|
+
input,
|
|
140
|
+
output,
|
|
141
|
+
...typeof cacheRead === "number" ? { cacheRead } : {}
|
|
142
|
+
};
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
145
|
if (api.modelsRegistry) {
|
|
@@ -142,7 +154,8 @@ var plugin = {
|
|
|
142
154
|
if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
|
|
143
155
|
bundledFromRegistry[modelId.toLowerCase()] = {
|
|
144
156
|
input: cost.input,
|
|
145
|
-
output: cost.output
|
|
157
|
+
output: cost.output,
|
|
158
|
+
...typeof cost.cache_read === "number" ? { cacheRead: cost.cache_read } : {}
|
|
146
159
|
};
|
|
147
160
|
hydrated += 1;
|
|
148
161
|
}
|
|
@@ -169,10 +182,12 @@ var plugin = {
|
|
|
169
182
|
api.onEvent("provider.response", async (payload) => {
|
|
170
183
|
const usage = payload.usage;
|
|
171
184
|
const model = payload.ctx?.model ?? "unknown";
|
|
172
|
-
const
|
|
185
|
+
const cachedTokens = usage.cacheRead ?? 0;
|
|
186
|
+
const freshTokens = (usage.input ?? 0) + (usage.cacheWrite ?? 0);
|
|
187
|
+
const promptTokens = freshTokens + cachedTokens;
|
|
173
188
|
const completionTokens = usage.output ?? 0;
|
|
174
189
|
const totalTokens = promptTokens + completionTokens;
|
|
175
|
-
const costUsd = estimateCost(model,
|
|
190
|
+
const costUsd = estimateCost(model, freshTokens, completionTokens, cachedTokens);
|
|
176
191
|
const record = {
|
|
177
192
|
promptTokens,
|
|
178
193
|
completionTokens,
|
|
@@ -3,10 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tools registered:
|
|
5
5
|
* - git_autocommit: Stage files and create a commit with AI-written conventional commit messages.
|
|
6
|
-
* Supports `files` for specific staging
|
|
6
|
+
* Supports `files` for specific staging, `paths` for scoped pathspec staging,
|
|
7
|
+
* and `dry_run` for preview.
|
|
8
|
+
*
|
|
9
|
+
* Scope guard (2026-08): this tool previously committed the ENTIRE git index,
|
|
10
|
+
* and auto-staged every changed file in the tree when the index was empty —
|
|
11
|
+
* while its own `autoStage: false` default was never consulted. On a shared
|
|
12
|
+
* working tree that let one agent's commit absorb files another process had
|
|
13
|
+
* staged concurrently (observed: a release commit absorbed a concurrently
|
|
14
|
+
* staged workstream it never asked for). The guard:
|
|
15
|
+
* - `files` callers commit via `git commit --only -- <files>` — exactly
|
|
16
|
+
* those paths; anything else staged stays in the index for its owner.
|
|
17
|
+
* - `paths` callers stage ONLY changed files matching the pathspecs (git
|
|
18
|
+
* resolves the globs) and commit those, fenced the same way.
|
|
19
|
+
* - With no files/paths and an empty index, the tool now honors `autoStage`
|
|
20
|
+
* (default false) and returns an instructive error instead of silently
|
|
21
|
+
* staging the whole tree. Set `autoStage: true` for the legacy behavior.
|
|
7
22
|
*
|
|
8
23
|
* Note: The former `git_autocommit` and `git_autocommit` tools have been removed.
|
|
9
|
-
* - For staging: use `git_autocommit` with `files` (it stages automatically), or `bash` with `git add`.
|
|
24
|
+
* - For staging: use `git_autocommit` with `files` or `paths` (it stages automatically), or `bash` with `git add`.
|
|
10
25
|
* - For status: use the built-in `git` tool with `command: "status"` or `command: "diff"`.
|
|
11
26
|
*/
|
|
12
27
|
import type { Plugin } from '@wrongstack/core/types';
|
package/dist/git-autocommit.js
CHANGED
|
@@ -74,9 +74,15 @@ function unquotePorcelainPath(raw) {
|
|
|
74
74
|
return Buffer.from(bytes).toString("utf8");
|
|
75
75
|
}
|
|
76
76
|
function parsePorcelainLine(line) {
|
|
77
|
-
const
|
|
77
|
+
const twoColumn = /^[MADRCUTX?! ]{2} /.test(line);
|
|
78
|
+
const oneColumnTrimmed = !twoColumn && /^[MADRCUTX?!] /.test(line);
|
|
79
|
+
if (!twoColumn && !oneColumnTrimmed) {
|
|
80
|
+
const bodyAny = line.slice(3);
|
|
81
|
+
return bodyAny ? unquotePorcelainPath(bodyAny.trim()) : null;
|
|
82
|
+
}
|
|
83
|
+
const body = oneColumnTrimmed ? line.slice(2) : line.slice(3);
|
|
78
84
|
if (!body) return null;
|
|
79
|
-
const status = line.slice(0, 2);
|
|
85
|
+
const status = oneColumnTrimmed ? ` ${line.slice(0, 1)}` : line.slice(0, 2);
|
|
80
86
|
if (status.includes("R") || status.includes("C")) {
|
|
81
87
|
const arrow = body.lastIndexOf(" -> ");
|
|
82
88
|
if (arrow !== -1) return unquotePorcelainPath(body.slice(arrow + 4).trim());
|
|
@@ -92,20 +98,38 @@ async function getStagedFiles(cwd) {
|
|
|
92
98
|
const output = await runGit(["diff", "--cached", "--name-only"], cwd);
|
|
93
99
|
return output ? output.split("\n").filter(Boolean) : [];
|
|
94
100
|
}
|
|
101
|
+
async function getScopedStagedFiles(paths, cwd) {
|
|
102
|
+
const output = await runGit(["diff", "--cached", "--name-only", "--", ...paths], cwd);
|
|
103
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
104
|
+
}
|
|
95
105
|
async function stageFiles(files, cwd) {
|
|
96
|
-
if (!files || !Array.isArray(files)) return;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
if (!files || !Array.isArray(files) || files.length === 0) return;
|
|
107
|
+
const hasPattern = files.some((f) => /[*?[\]]/.test(f));
|
|
108
|
+
if (!hasPattern) {
|
|
109
|
+
const existing = files.filter((f) => {
|
|
110
|
+
try {
|
|
111
|
+
return existsSync(f);
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
if (existing.length === 0) throw new Error("No files exist to stage");
|
|
117
|
+
await runGit(["add", "--", ...existing], cwd);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await runGit(["add", "--", ...files], cwd);
|
|
121
|
+
}
|
|
122
|
+
async function commitWithMessage(message, cwd, paths) {
|
|
123
|
+
const scoped = paths && paths.length > 0 ? ["--only", "--", ...paths] : [];
|
|
124
|
+
return await runGit(["commit", "-m", message, ...scoped], cwd, GIT_COMMIT_TIMEOUT_MS);
|
|
106
125
|
}
|
|
107
|
-
async function
|
|
108
|
-
|
|
126
|
+
async function scopedPathsDrifted(paths, cwd) {
|
|
127
|
+
try {
|
|
128
|
+
const out = await runGit(["diff", "--name-only", "--", ...paths], cwd);
|
|
129
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
109
133
|
}
|
|
110
134
|
async function getWorktrees(cwd) {
|
|
111
135
|
try {
|
|
@@ -148,6 +172,17 @@ async function getStagedDiff(cwd) {
|
|
|
148
172
|
return { stat: "(unavailable)", diff: "(unavailable)" };
|
|
149
173
|
}
|
|
150
174
|
}
|
|
175
|
+
async function getScopedStagedDiff(paths, cwd) {
|
|
176
|
+
try {
|
|
177
|
+
const stat = await runGit(["diff", "--cached", "--stat", "--", ...paths], cwd);
|
|
178
|
+
const diff = await runGit(["diff", "--cached", "--", ...paths], cwd);
|
|
179
|
+
const MAX_DIFF = 2e4;
|
|
180
|
+
const truncated = diff.length > MAX_DIFF ? diff.slice(0, MAX_DIFF) + "\n\n... (diff truncated)" : diff;
|
|
181
|
+
return { stat: stat || "(no stat)", diff: truncated || "(clean)" };
|
|
182
|
+
} catch {
|
|
183
|
+
return { stat: "(unavailable)", diff: "(unavailable)" };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
151
186
|
async function externalChangesSinceStage(cwd) {
|
|
152
187
|
try {
|
|
153
188
|
const out = await runGit(["status", "--porcelain"], cwd);
|
|
@@ -219,7 +254,7 @@ function extractJsonObject(text) {
|
|
|
219
254
|
}
|
|
220
255
|
var plugin = {
|
|
221
256
|
name: "git-autocommit",
|
|
222
|
-
version: "0.
|
|
257
|
+
version: "0.3.0",
|
|
223
258
|
description: "AI-powered git staging and conventional commit message generation",
|
|
224
259
|
apiVersion: API_VERSION,
|
|
225
260
|
capabilities: { tools: true, llm: true },
|
|
@@ -233,7 +268,11 @@ var plugin = {
|
|
|
233
268
|
type: "object",
|
|
234
269
|
properties: {
|
|
235
270
|
conventionalCommits: { type: "boolean", default: true },
|
|
236
|
-
autoStage: {
|
|
271
|
+
autoStage: {
|
|
272
|
+
type: "boolean",
|
|
273
|
+
default: false,
|
|
274
|
+
description: "When the index is empty and no files/paths were given, stage every changed file before committing (legacy whole-tree behavior). Default false: the tool returns an instructive error instead, so a commit never absorbs unrelated concurrently staged work."
|
|
275
|
+
},
|
|
237
276
|
defaultType: { type: "string", default: "feat" },
|
|
238
277
|
useLlm: {
|
|
239
278
|
type: "boolean",
|
|
@@ -263,14 +302,19 @@ var plugin = {
|
|
|
263
302
|
};
|
|
264
303
|
api.tools.register({
|
|
265
304
|
name: "git_autocommit",
|
|
266
|
-
description:
|
|
305
|
+
description: 'Stage files and create a git commit with an AI-generated conventional commit message. Pass files for exact paths, or paths (git pathspec globs like "**/package.json", "website/**") to stage only matching changed files. Commits are fenced to the staged scope \u2014 unrelated concurrently staged files are left in the index, not absorbed.',
|
|
267
306
|
inputSchema: {
|
|
268
307
|
type: "object",
|
|
269
308
|
properties: {
|
|
270
309
|
files: {
|
|
271
310
|
type: "array",
|
|
272
311
|
items: { type: "string" },
|
|
273
|
-
description: "Specific files to stage.
|
|
312
|
+
description: "Specific files to stage and commit. The commit is fenced to exactly these paths."
|
|
313
|
+
},
|
|
314
|
+
paths: {
|
|
315
|
+
type: "array",
|
|
316
|
+
items: { type: "string" },
|
|
317
|
+
description: 'Git pathspec globs limiting what this commit may include (e.g. ["**/package.json", "CHANGELOG.md", "website/**"] for a release). Only changed files matching these patterns are staged and committed.'
|
|
274
318
|
},
|
|
275
319
|
type: {
|
|
276
320
|
type: "string",
|
|
@@ -324,7 +368,52 @@ var plugin = {
|
|
|
324
368
|
}
|
|
325
369
|
files = rawFiles;
|
|
326
370
|
}
|
|
327
|
-
|
|
371
|
+
let pathspecs;
|
|
372
|
+
const rawPaths = input["paths"];
|
|
373
|
+
if (rawPaths !== void 0) {
|
|
374
|
+
if (!Array.isArray(rawPaths)) {
|
|
375
|
+
return { ok: false, error: "paths must be an array of pathspec patterns" };
|
|
376
|
+
}
|
|
377
|
+
pathspecs = rawPaths.filter((p) => typeof p === "string" && p.length > 0);
|
|
378
|
+
if (pathspecs.length === 0) {
|
|
379
|
+
return { ok: false, error: "paths must contain at least one non-empty pattern" };
|
|
380
|
+
}
|
|
381
|
+
if (files && files.length > 0) {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
error: "Pass either files (exact paths) or paths (pathspec globs), not both \u2014 the other would be silently ignored."
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let commitScope;
|
|
389
|
+
let staged = [];
|
|
390
|
+
if (pathspecs) {
|
|
391
|
+
try {
|
|
392
|
+
await stageFiles(pathspecs);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
error: `Failed to stage files matching paths: ${err instanceof Error ? err.message : String(err)}`
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
staged = await getScopedStagedFiles(pathspecs);
|
|
401
|
+
} catch {
|
|
402
|
+
staged = [];
|
|
403
|
+
}
|
|
404
|
+
if (staged.length === 0) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
error: "No changed files match the given paths \u2014 refusing to commit anything else."
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
commitScope = staged;
|
|
411
|
+
try {
|
|
412
|
+
staged = await getStagedFiles();
|
|
413
|
+
} catch {
|
|
414
|
+
staged = commitScope;
|
|
415
|
+
}
|
|
416
|
+
} else if (files && files.length > 0) {
|
|
328
417
|
try {
|
|
329
418
|
await stageFiles(files);
|
|
330
419
|
} catch (err) {
|
|
@@ -333,31 +422,37 @@ var plugin = {
|
|
|
333
422
|
error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}`
|
|
334
423
|
};
|
|
335
424
|
}
|
|
336
|
-
|
|
337
|
-
let staged = [];
|
|
338
|
-
try {
|
|
339
|
-
staged = await getStagedFiles();
|
|
340
|
-
} catch {
|
|
341
|
-
staged = [];
|
|
342
|
-
}
|
|
343
|
-
if (staged.length === 0) {
|
|
425
|
+
commitScope = files;
|
|
344
426
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
427
|
+
staged = await getStagedFiles();
|
|
428
|
+
} catch {
|
|
429
|
+
staged = [];
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
try {
|
|
433
|
+
staged = await getStagedFiles();
|
|
434
|
+
} catch {
|
|
435
|
+
staged = [];
|
|
436
|
+
}
|
|
437
|
+
if (staged.length === 0 && opts.autoStage) {
|
|
438
|
+
try {
|
|
439
|
+
const changed = await getChangedFiles();
|
|
440
|
+
if (changed.length > 0) {
|
|
441
|
+
try {
|
|
442
|
+
await stageFiles(changed);
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
staged = await getStagedFiles();
|
|
447
|
+
} catch {
|
|
448
|
+
staged = [];
|
|
449
|
+
}
|
|
355
450
|
}
|
|
451
|
+
} catch {
|
|
356
452
|
}
|
|
357
|
-
} catch {
|
|
358
453
|
}
|
|
359
454
|
}
|
|
360
|
-
const { stat, diff: stagedDiff } = await getStagedDiff();
|
|
455
|
+
const { stat, diff: stagedDiff } = commitScope ? await getScopedStagedDiff(commitScope) : await getStagedDiff();
|
|
361
456
|
let generatedByLlm = false;
|
|
362
457
|
if (wantGenerate && staged.length > 0) {
|
|
363
458
|
const g = await generateCommitFromDiff(api, stat, stagedDiff);
|
|
@@ -400,9 +495,19 @@ var plugin = {
|
|
|
400
495
|
if (staged.length === 0) {
|
|
401
496
|
return {
|
|
402
497
|
ok: false,
|
|
403
|
-
error:
|
|
498
|
+
error: 'Nothing staged. Pass files (exact paths) or paths (pathspec globs) to scope this commit, stage with git add beforehand, or set extensions["git-autocommit"].autoStage=true to allow staging every changed file (legacy whole-tree behavior).'
|
|
404
499
|
};
|
|
405
500
|
}
|
|
501
|
+
let scopeWarning = null;
|
|
502
|
+
if (commitScope) {
|
|
503
|
+
const scopedSet = new Set(commitScope);
|
|
504
|
+
const foreign = staged.filter((f) => !scopedSet.has(f));
|
|
505
|
+
if (foreign.length > 0) {
|
|
506
|
+
const preview = foreign.slice(0, 10).join(", ");
|
|
507
|
+
const suffix = foreign.length > 10 ? ` and ${foreign.length - 10} more` : "";
|
|
508
|
+
scopeWarning = `\u26A0 Scope guard: ${foreign.length} staged file(s) outside the requested scope (${preview}${suffix}) were left uncommitted and remain staged for their owner.`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
406
511
|
const worktreeWarn = await simultaneousEditWarning();
|
|
407
512
|
const externalChanges = await externalChangesSinceStage();
|
|
408
513
|
let externalWarning = null;
|
|
@@ -411,7 +516,7 @@ var plugin = {
|
|
|
411
516
|
const suffix = externalChanges.length > 10 ? ` and ${externalChanges.length - 10} more` : "";
|
|
412
517
|
externalWarning = `\u26A0 External changes detected since staging: ${preview}${suffix}. Another agent may be modifying files concurrently. These unstaged changes will NOT be included in this commit, but they indicate simultaneous edits. Review carefully.`;
|
|
413
518
|
}
|
|
414
|
-
const warning = [worktreeWarn, externalWarning].filter(Boolean).join("\n") || void 0;
|
|
519
|
+
const warning = [worktreeWarn, scopeWarning, externalWarning].filter(Boolean).join("\n") || void 0;
|
|
415
520
|
if (dryRun) {
|
|
416
521
|
return {
|
|
417
522
|
ok: true,
|
|
@@ -428,16 +533,20 @@ ${stagedDiff}
|
|
|
428
533
|
\`\`\``
|
|
429
534
|
};
|
|
430
535
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
536
|
+
if (commitScope && !dryRun) {
|
|
537
|
+
const drifted = await scopedPathsDrifted(commitScope);
|
|
538
|
+
if (drifted.length > 0) {
|
|
539
|
+
const preview = drifted.slice(0, 10).join(", ");
|
|
540
|
+
const suffix = drifted.length > 10 ? ` and ${drifted.length - 10} more` : "";
|
|
541
|
+
return {
|
|
542
|
+
ok: false,
|
|
543
|
+
error: `Working tree changed after staging for: ${preview}${suffix}. A scoped commit takes working-tree content, so committing now could include changes that were never staged or previewed. Re-run the tool to re-stage the current content.`
|
|
544
|
+
};
|
|
545
|
+
}
|
|
437
546
|
}
|
|
438
547
|
let hash = "";
|
|
439
548
|
try {
|
|
440
|
-
hash = await commitWithMessage(msg);
|
|
549
|
+
hash = await commitWithMessage(msg, void 0, commitScope);
|
|
441
550
|
} catch (err) {
|
|
442
551
|
return {
|
|
443
552
|
ok: false,
|
|
@@ -457,7 +566,7 @@ ${stagedDiff}
|
|
|
457
566
|
commitType: type,
|
|
458
567
|
scope: String(scope ?? ""),
|
|
459
568
|
/* v8 ignore next -- staged is always an array here; the : [] fallback is defensive. */
|
|
460
|
-
files: Array.isArray(staged) ? staged : [],
|
|
569
|
+
files: Array.isArray(staged) ? commitScope ?? staged : [],
|
|
461
570
|
warning: warning ?? null
|
|
462
571
|
});
|
|
463
572
|
} catch (_err) {
|
|
@@ -466,7 +575,7 @@ ${stagedDiff}
|
|
|
466
575
|
ok: true,
|
|
467
576
|
hash,
|
|
468
577
|
message: msg,
|
|
469
|
-
stagedFiles: staged,
|
|
578
|
+
stagedFiles: commitScope ?? staged,
|
|
470
579
|
type,
|
|
471
580
|
scope: scope ?? null,
|
|
472
581
|
generatedByLlm,
|
|
@@ -474,10 +583,10 @@ ${stagedDiff}
|
|
|
474
583
|
diff: `
|
|
475
584
|
## Staged diff
|
|
476
585
|
|
|
477
|
-
${
|
|
586
|
+
${stat}
|
|
478
587
|
|
|
479
588
|
\`\`\`diff
|
|
480
|
-
${
|
|
589
|
+
${stagedDiff}
|
|
481
590
|
\`\`\``
|
|
482
591
|
};
|
|
483
592
|
} catch (err) {
|
|
@@ -489,7 +598,7 @@ ${preCommitDiff}
|
|
|
489
598
|
}
|
|
490
599
|
});
|
|
491
600
|
api.log.info("git-autocommit plugin loaded", {
|
|
492
|
-
version: "0.
|
|
601
|
+
version: "0.3.0",
|
|
493
602
|
conventionalCommits: opts.conventionalCommits
|
|
494
603
|
});
|
|
495
604
|
},
|
package/dist/index.js
CHANGED
|
@@ -4370,14 +4370,14 @@ function readCostTrackerConfig(raw) {
|
|
|
4370
4370
|
};
|
|
4371
4371
|
}
|
|
4372
4372
|
var modelKeyCache = new runtime_exports.BoundedMap({ max: 256 });
|
|
4373
|
-
function estimateCost(model,
|
|
4373
|
+
function estimateCost(model, freshTokens, completionTokens, cachedTokens = 0) {
|
|
4374
4374
|
let key = modelKeyCache.get(model);
|
|
4375
4375
|
if (!key) {
|
|
4376
4376
|
key = model.toLowerCase();
|
|
4377
4377
|
modelKeyCache.set(model, key);
|
|
4378
4378
|
}
|
|
4379
4379
|
const pricing = pricingOverrides[key] ?? bundledFromRegistry[key] ?? PRICING[key] ?? DEFAULT_PRICING;
|
|
4380
|
-
const inputCost =
|
|
4380
|
+
const inputCost = freshTokens / 1e6 * pricing.input + cachedTokens / 1e6 * (pricing.cacheRead ?? pricing.input);
|
|
4381
4381
|
const outputCost = completionTokens / 1e6 * pricing.output;
|
|
4382
4382
|
return inputCost + outputCost;
|
|
4383
4383
|
}
|
|
@@ -4411,12 +4411,17 @@ var plugin14 = {
|
|
|
4411
4411
|
},
|
|
4412
4412
|
pricingOverrides: {
|
|
4413
4413
|
type: "object",
|
|
4414
|
-
description: "Per-model pricing overrides in USD per 1M tokens.
|
|
4414
|
+
description: "Per-model pricing overrides in USD per 1M tokens. Values are { input, output, cacheRead? }.",
|
|
4415
4415
|
additionalProperties: {
|
|
4416
4416
|
type: "object",
|
|
4417
4417
|
properties: {
|
|
4418
4418
|
input: { type: "number", minimum: 0, description: "Cost per 1M input tokens in USD" },
|
|
4419
|
-
output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" }
|
|
4419
|
+
output: { type: "number", minimum: 0, description: "Cost per 1M output tokens in USD" },
|
|
4420
|
+
cacheRead: {
|
|
4421
|
+
type: "number",
|
|
4422
|
+
minimum: 0,
|
|
4423
|
+
description: "Cost per 1M prompt-cache read tokens in USD"
|
|
4424
|
+
}
|
|
4420
4425
|
},
|
|
4421
4426
|
required: ["input", "output"],
|
|
4422
4427
|
additionalProperties: false
|
|
@@ -4449,7 +4454,12 @@ var plugin14 = {
|
|
|
4449
4454
|
const input = v["input"];
|
|
4450
4455
|
const output = v["output"];
|
|
4451
4456
|
if (typeof input !== "number" || typeof output !== "number") continue;
|
|
4452
|
-
|
|
4457
|
+
const cacheRead = v["cacheRead"];
|
|
4458
|
+
pricingOverrides[model.toLowerCase()] = {
|
|
4459
|
+
input,
|
|
4460
|
+
output,
|
|
4461
|
+
...typeof cacheRead === "number" ? { cacheRead } : {}
|
|
4462
|
+
};
|
|
4453
4463
|
}
|
|
4454
4464
|
}
|
|
4455
4465
|
if (api.modelsRegistry) {
|
|
@@ -4464,7 +4474,8 @@ var plugin14 = {
|
|
|
4464
4474
|
if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
|
|
4465
4475
|
bundledFromRegistry[modelId.toLowerCase()] = {
|
|
4466
4476
|
input: cost.input,
|
|
4467
|
-
output: cost.output
|
|
4477
|
+
output: cost.output,
|
|
4478
|
+
...typeof cost.cache_read === "number" ? { cacheRead: cost.cache_read } : {}
|
|
4468
4479
|
};
|
|
4469
4480
|
hydrated += 1;
|
|
4470
4481
|
}
|
|
@@ -4491,10 +4502,12 @@ var plugin14 = {
|
|
|
4491
4502
|
api.onEvent("provider.response", async (payload) => {
|
|
4492
4503
|
const usage = payload.usage;
|
|
4493
4504
|
const model = payload.ctx?.model ?? "unknown";
|
|
4494
|
-
const
|
|
4505
|
+
const cachedTokens = usage.cacheRead ?? 0;
|
|
4506
|
+
const freshTokens = (usage.input ?? 0) + (usage.cacheWrite ?? 0);
|
|
4507
|
+
const promptTokens = freshTokens + cachedTokens;
|
|
4495
4508
|
const completionTokens = usage.output ?? 0;
|
|
4496
4509
|
const totalTokens = promptTokens + completionTokens;
|
|
4497
|
-
const costUsd = estimateCost(model,
|
|
4510
|
+
const costUsd = estimateCost(model, freshTokens, completionTokens, cachedTokens);
|
|
4498
4511
|
const record = {
|
|
4499
4512
|
promptTokens,
|
|
4500
4513
|
completionTokens,
|
|
@@ -8412,9 +8425,15 @@ function unquotePorcelainPath(raw) {
|
|
|
8412
8425
|
return Buffer.from(bytes).toString("utf8");
|
|
8413
8426
|
}
|
|
8414
8427
|
function parsePorcelainLine(line) {
|
|
8415
|
-
const
|
|
8428
|
+
const twoColumn = /^[MADRCUTX?! ]{2} /.test(line);
|
|
8429
|
+
const oneColumnTrimmed = !twoColumn && /^[MADRCUTX?!] /.test(line);
|
|
8430
|
+
if (!twoColumn && !oneColumnTrimmed) {
|
|
8431
|
+
const bodyAny = line.slice(3);
|
|
8432
|
+
return bodyAny ? unquotePorcelainPath(bodyAny.trim()) : null;
|
|
8433
|
+
}
|
|
8434
|
+
const body = oneColumnTrimmed ? line.slice(2) : line.slice(3);
|
|
8416
8435
|
if (!body) return null;
|
|
8417
|
-
const status = line.slice(0, 2);
|
|
8436
|
+
const status = oneColumnTrimmed ? ` ${line.slice(0, 1)}` : line.slice(0, 2);
|
|
8418
8437
|
if (status.includes("R") || status.includes("C")) {
|
|
8419
8438
|
const arrow = body.lastIndexOf(" -> ");
|
|
8420
8439
|
if (arrow !== -1) return unquotePorcelainPath(body.slice(arrow + 4).trim());
|
|
@@ -8430,20 +8449,38 @@ async function getStagedFiles(cwd) {
|
|
|
8430
8449
|
const output = await runGit3(["diff", "--cached", "--name-only"], cwd);
|
|
8431
8450
|
return output ? output.split("\n").filter(Boolean) : [];
|
|
8432
8451
|
}
|
|
8452
|
+
async function getScopedStagedFiles(paths, cwd) {
|
|
8453
|
+
const output = await runGit3(["diff", "--cached", "--name-only", "--", ...paths], cwd);
|
|
8454
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
8455
|
+
}
|
|
8433
8456
|
async function stageFiles(files, cwd) {
|
|
8434
|
-
if (!files || !Array.isArray(files)) return;
|
|
8435
|
-
const
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8457
|
+
if (!files || !Array.isArray(files) || files.length === 0) return;
|
|
8458
|
+
const hasPattern = files.some((f) => /[*?[\]]/.test(f));
|
|
8459
|
+
if (!hasPattern) {
|
|
8460
|
+
const existing = files.filter((f) => {
|
|
8461
|
+
try {
|
|
8462
|
+
return existsSync2(f);
|
|
8463
|
+
} catch {
|
|
8464
|
+
return false;
|
|
8465
|
+
}
|
|
8466
|
+
});
|
|
8467
|
+
if (existing.length === 0) throw new Error("No files exist to stage");
|
|
8468
|
+
await runGit3(["add", "--", ...existing], cwd);
|
|
8469
|
+
return;
|
|
8470
|
+
}
|
|
8471
|
+
await runGit3(["add", "--", ...files], cwd);
|
|
8472
|
+
}
|
|
8473
|
+
async function commitWithMessage(message, cwd, paths) {
|
|
8474
|
+
const scoped = paths && paths.length > 0 ? ["--only", "--", ...paths] : [];
|
|
8475
|
+
return await runGit3(["commit", "-m", message, ...scoped], cwd, GIT_COMMIT_TIMEOUT_MS);
|
|
8444
8476
|
}
|
|
8445
|
-
async function
|
|
8446
|
-
|
|
8477
|
+
async function scopedPathsDrifted(paths, cwd) {
|
|
8478
|
+
try {
|
|
8479
|
+
const out = await runGit3(["diff", "--name-only", "--", ...paths], cwd);
|
|
8480
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
8481
|
+
} catch {
|
|
8482
|
+
return [];
|
|
8483
|
+
}
|
|
8447
8484
|
}
|
|
8448
8485
|
async function getWorktrees(cwd) {
|
|
8449
8486
|
try {
|
|
@@ -8486,6 +8523,17 @@ async function getStagedDiff(cwd) {
|
|
|
8486
8523
|
return { stat: "(unavailable)", diff: "(unavailable)" };
|
|
8487
8524
|
}
|
|
8488
8525
|
}
|
|
8526
|
+
async function getScopedStagedDiff(paths, cwd) {
|
|
8527
|
+
try {
|
|
8528
|
+
const stat8 = await runGit3(["diff", "--cached", "--stat", "--", ...paths], cwd);
|
|
8529
|
+
const diff = await runGit3(["diff", "--cached", "--", ...paths], cwd);
|
|
8530
|
+
const MAX_DIFF = 2e4;
|
|
8531
|
+
const truncated = diff.length > MAX_DIFF ? diff.slice(0, MAX_DIFF) + "\n\n... (diff truncated)" : diff;
|
|
8532
|
+
return { stat: stat8 || "(no stat)", diff: truncated || "(clean)" };
|
|
8533
|
+
} catch {
|
|
8534
|
+
return { stat: "(unavailable)", diff: "(unavailable)" };
|
|
8535
|
+
}
|
|
8536
|
+
}
|
|
8489
8537
|
async function externalChangesSinceStage(cwd) {
|
|
8490
8538
|
try {
|
|
8491
8539
|
const out = await runGit3(["status", "--porcelain"], cwd);
|
|
@@ -8557,7 +8605,7 @@ function extractJsonObject2(text) {
|
|
|
8557
8605
|
}
|
|
8558
8606
|
var plugin26 = {
|
|
8559
8607
|
name: "git-autocommit",
|
|
8560
|
-
version: "0.
|
|
8608
|
+
version: "0.3.0",
|
|
8561
8609
|
description: "AI-powered git staging and conventional commit message generation",
|
|
8562
8610
|
apiVersion: API_VERSION18,
|
|
8563
8611
|
capabilities: { tools: true, llm: true },
|
|
@@ -8571,7 +8619,11 @@ var plugin26 = {
|
|
|
8571
8619
|
type: "object",
|
|
8572
8620
|
properties: {
|
|
8573
8621
|
conventionalCommits: { type: "boolean", default: true },
|
|
8574
|
-
autoStage: {
|
|
8622
|
+
autoStage: {
|
|
8623
|
+
type: "boolean",
|
|
8624
|
+
default: false,
|
|
8625
|
+
description: "When the index is empty and no files/paths were given, stage every changed file before committing (legacy whole-tree behavior). Default false: the tool returns an instructive error instead, so a commit never absorbs unrelated concurrently staged work."
|
|
8626
|
+
},
|
|
8575
8627
|
defaultType: { type: "string", default: "feat" },
|
|
8576
8628
|
useLlm: {
|
|
8577
8629
|
type: "boolean",
|
|
@@ -8601,14 +8653,19 @@ var plugin26 = {
|
|
|
8601
8653
|
};
|
|
8602
8654
|
api.tools.register({
|
|
8603
8655
|
name: "git_autocommit",
|
|
8604
|
-
description:
|
|
8656
|
+
description: 'Stage files and create a git commit with an AI-generated conventional commit message. Pass files for exact paths, or paths (git pathspec globs like "**/package.json", "website/**") to stage only matching changed files. Commits are fenced to the staged scope \u2014 unrelated concurrently staged files are left in the index, not absorbed.',
|
|
8605
8657
|
inputSchema: {
|
|
8606
8658
|
type: "object",
|
|
8607
8659
|
properties: {
|
|
8608
8660
|
files: {
|
|
8609
8661
|
type: "array",
|
|
8610
8662
|
items: { type: "string" },
|
|
8611
|
-
description: "Specific files to stage.
|
|
8663
|
+
description: "Specific files to stage and commit. The commit is fenced to exactly these paths."
|
|
8664
|
+
},
|
|
8665
|
+
paths: {
|
|
8666
|
+
type: "array",
|
|
8667
|
+
items: { type: "string" },
|
|
8668
|
+
description: 'Git pathspec globs limiting what this commit may include (e.g. ["**/package.json", "CHANGELOG.md", "website/**"] for a release). Only changed files matching these patterns are staged and committed.'
|
|
8612
8669
|
},
|
|
8613
8670
|
type: {
|
|
8614
8671
|
type: "string",
|
|
@@ -8662,7 +8719,52 @@ var plugin26 = {
|
|
|
8662
8719
|
}
|
|
8663
8720
|
files = rawFiles;
|
|
8664
8721
|
}
|
|
8665
|
-
|
|
8722
|
+
let pathspecs;
|
|
8723
|
+
const rawPaths = input["paths"];
|
|
8724
|
+
if (rawPaths !== void 0) {
|
|
8725
|
+
if (!Array.isArray(rawPaths)) {
|
|
8726
|
+
return { ok: false, error: "paths must be an array of pathspec patterns" };
|
|
8727
|
+
}
|
|
8728
|
+
pathspecs = rawPaths.filter((p) => typeof p === "string" && p.length > 0);
|
|
8729
|
+
if (pathspecs.length === 0) {
|
|
8730
|
+
return { ok: false, error: "paths must contain at least one non-empty pattern" };
|
|
8731
|
+
}
|
|
8732
|
+
if (files && files.length > 0) {
|
|
8733
|
+
return {
|
|
8734
|
+
ok: false,
|
|
8735
|
+
error: "Pass either files (exact paths) or paths (pathspec globs), not both \u2014 the other would be silently ignored."
|
|
8736
|
+
};
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
let commitScope;
|
|
8740
|
+
let staged = [];
|
|
8741
|
+
if (pathspecs) {
|
|
8742
|
+
try {
|
|
8743
|
+
await stageFiles(pathspecs);
|
|
8744
|
+
} catch (err) {
|
|
8745
|
+
return {
|
|
8746
|
+
ok: false,
|
|
8747
|
+
error: `Failed to stage files matching paths: ${err instanceof Error ? err.message : String(err)}`
|
|
8748
|
+
};
|
|
8749
|
+
}
|
|
8750
|
+
try {
|
|
8751
|
+
staged = await getScopedStagedFiles(pathspecs);
|
|
8752
|
+
} catch {
|
|
8753
|
+
staged = [];
|
|
8754
|
+
}
|
|
8755
|
+
if (staged.length === 0) {
|
|
8756
|
+
return {
|
|
8757
|
+
ok: false,
|
|
8758
|
+
error: "No changed files match the given paths \u2014 refusing to commit anything else."
|
|
8759
|
+
};
|
|
8760
|
+
}
|
|
8761
|
+
commitScope = staged;
|
|
8762
|
+
try {
|
|
8763
|
+
staged = await getStagedFiles();
|
|
8764
|
+
} catch {
|
|
8765
|
+
staged = commitScope;
|
|
8766
|
+
}
|
|
8767
|
+
} else if (files && files.length > 0) {
|
|
8666
8768
|
try {
|
|
8667
8769
|
await stageFiles(files);
|
|
8668
8770
|
} catch (err) {
|
|
@@ -8671,31 +8773,37 @@ var plugin26 = {
|
|
|
8671
8773
|
error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}`
|
|
8672
8774
|
};
|
|
8673
8775
|
}
|
|
8674
|
-
|
|
8675
|
-
let staged = [];
|
|
8676
|
-
try {
|
|
8677
|
-
staged = await getStagedFiles();
|
|
8678
|
-
} catch {
|
|
8679
|
-
staged = [];
|
|
8680
|
-
}
|
|
8681
|
-
if (staged.length === 0) {
|
|
8776
|
+
commitScope = files;
|
|
8682
8777
|
try {
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8778
|
+
staged = await getStagedFiles();
|
|
8779
|
+
} catch {
|
|
8780
|
+
staged = [];
|
|
8781
|
+
}
|
|
8782
|
+
} else {
|
|
8783
|
+
try {
|
|
8784
|
+
staged = await getStagedFiles();
|
|
8785
|
+
} catch {
|
|
8786
|
+
staged = [];
|
|
8787
|
+
}
|
|
8788
|
+
if (staged.length === 0 && opts.autoStage) {
|
|
8789
|
+
try {
|
|
8790
|
+
const changed = await getChangedFiles();
|
|
8791
|
+
if (changed.length > 0) {
|
|
8792
|
+
try {
|
|
8793
|
+
await stageFiles(changed);
|
|
8794
|
+
} catch {
|
|
8795
|
+
}
|
|
8796
|
+
try {
|
|
8797
|
+
staged = await getStagedFiles();
|
|
8798
|
+
} catch {
|
|
8799
|
+
staged = [];
|
|
8800
|
+
}
|
|
8693
8801
|
}
|
|
8802
|
+
} catch {
|
|
8694
8803
|
}
|
|
8695
|
-
} catch {
|
|
8696
8804
|
}
|
|
8697
8805
|
}
|
|
8698
|
-
const { stat: stat8, diff: stagedDiff } = await getStagedDiff();
|
|
8806
|
+
const { stat: stat8, diff: stagedDiff } = commitScope ? await getScopedStagedDiff(commitScope) : await getStagedDiff();
|
|
8699
8807
|
let generatedByLlm = false;
|
|
8700
8808
|
if (wantGenerate && staged.length > 0) {
|
|
8701
8809
|
const g = await generateCommitFromDiff(api, stat8, stagedDiff);
|
|
@@ -8738,9 +8846,19 @@ var plugin26 = {
|
|
|
8738
8846
|
if (staged.length === 0) {
|
|
8739
8847
|
return {
|
|
8740
8848
|
ok: false,
|
|
8741
|
-
error:
|
|
8849
|
+
error: 'Nothing staged. Pass files (exact paths) or paths (pathspec globs) to scope this commit, stage with git add beforehand, or set extensions["git-autocommit"].autoStage=true to allow staging every changed file (legacy whole-tree behavior).'
|
|
8742
8850
|
};
|
|
8743
8851
|
}
|
|
8852
|
+
let scopeWarning = null;
|
|
8853
|
+
if (commitScope) {
|
|
8854
|
+
const scopedSet = new Set(commitScope);
|
|
8855
|
+
const foreign = staged.filter((f) => !scopedSet.has(f));
|
|
8856
|
+
if (foreign.length > 0) {
|
|
8857
|
+
const preview = foreign.slice(0, 10).join(", ");
|
|
8858
|
+
const suffix = foreign.length > 10 ? ` and ${foreign.length - 10} more` : "";
|
|
8859
|
+
scopeWarning = `\u26A0 Scope guard: ${foreign.length} staged file(s) outside the requested scope (${preview}${suffix}) were left uncommitted and remain staged for their owner.`;
|
|
8860
|
+
}
|
|
8861
|
+
}
|
|
8744
8862
|
const worktreeWarn = await simultaneousEditWarning();
|
|
8745
8863
|
const externalChanges = await externalChangesSinceStage();
|
|
8746
8864
|
let externalWarning = null;
|
|
@@ -8749,7 +8867,7 @@ var plugin26 = {
|
|
|
8749
8867
|
const suffix = externalChanges.length > 10 ? ` and ${externalChanges.length - 10} more` : "";
|
|
8750
8868
|
externalWarning = `\u26A0 External changes detected since staging: ${preview}${suffix}. Another agent may be modifying files concurrently. These unstaged changes will NOT be included in this commit, but they indicate simultaneous edits. Review carefully.`;
|
|
8751
8869
|
}
|
|
8752
|
-
const warning = [worktreeWarn, externalWarning].filter(Boolean).join("\n") || void 0;
|
|
8870
|
+
const warning = [worktreeWarn, scopeWarning, externalWarning].filter(Boolean).join("\n") || void 0;
|
|
8753
8871
|
if (dryRun) {
|
|
8754
8872
|
return {
|
|
8755
8873
|
ok: true,
|
|
@@ -8766,16 +8884,20 @@ ${stagedDiff}
|
|
|
8766
8884
|
\`\`\``
|
|
8767
8885
|
};
|
|
8768
8886
|
}
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8887
|
+
if (commitScope && !dryRun) {
|
|
8888
|
+
const drifted = await scopedPathsDrifted(commitScope);
|
|
8889
|
+
if (drifted.length > 0) {
|
|
8890
|
+
const preview = drifted.slice(0, 10).join(", ");
|
|
8891
|
+
const suffix = drifted.length > 10 ? ` and ${drifted.length - 10} more` : "";
|
|
8892
|
+
return {
|
|
8893
|
+
ok: false,
|
|
8894
|
+
error: `Working tree changed after staging for: ${preview}${suffix}. A scoped commit takes working-tree content, so committing now could include changes that were never staged or previewed. Re-run the tool to re-stage the current content.`
|
|
8895
|
+
};
|
|
8896
|
+
}
|
|
8775
8897
|
}
|
|
8776
8898
|
let hash = "";
|
|
8777
8899
|
try {
|
|
8778
|
-
hash = await commitWithMessage(msg);
|
|
8900
|
+
hash = await commitWithMessage(msg, void 0, commitScope);
|
|
8779
8901
|
} catch (err) {
|
|
8780
8902
|
return {
|
|
8781
8903
|
ok: false,
|
|
@@ -8795,7 +8917,7 @@ ${stagedDiff}
|
|
|
8795
8917
|
commitType: type,
|
|
8796
8918
|
scope: String(scope ?? ""),
|
|
8797
8919
|
/* v8 ignore next -- staged is always an array here; the : [] fallback is defensive. */
|
|
8798
|
-
files: Array.isArray(staged) ? staged : [],
|
|
8920
|
+
files: Array.isArray(staged) ? commitScope ?? staged : [],
|
|
8799
8921
|
warning: warning ?? null
|
|
8800
8922
|
});
|
|
8801
8923
|
} catch (_err) {
|
|
@@ -8804,7 +8926,7 @@ ${stagedDiff}
|
|
|
8804
8926
|
ok: true,
|
|
8805
8927
|
hash,
|
|
8806
8928
|
message: msg,
|
|
8807
|
-
stagedFiles: staged,
|
|
8929
|
+
stagedFiles: commitScope ?? staged,
|
|
8808
8930
|
type,
|
|
8809
8931
|
scope: scope ?? null,
|
|
8810
8932
|
generatedByLlm,
|
|
@@ -8812,10 +8934,10 @@ ${stagedDiff}
|
|
|
8812
8934
|
diff: `
|
|
8813
8935
|
## Staged diff
|
|
8814
8936
|
|
|
8815
|
-
${
|
|
8937
|
+
${stat8}
|
|
8816
8938
|
|
|
8817
8939
|
\`\`\`diff
|
|
8818
|
-
${
|
|
8940
|
+
${stagedDiff}
|
|
8819
8941
|
\`\`\``
|
|
8820
8942
|
};
|
|
8821
8943
|
} catch (err) {
|
|
@@ -8827,7 +8949,7 @@ ${preCommitDiff}
|
|
|
8827
8949
|
}
|
|
8828
8950
|
});
|
|
8829
8951
|
api.log.info("git-autocommit plugin loaded", {
|
|
8830
|
-
version: "0.
|
|
8952
|
+
version: "0.3.0",
|
|
8831
8953
|
conventionalCommits: opts.conventionalCommits
|
|
8832
8954
|
});
|
|
8833
8955
|
},
|
|
@@ -22673,9 +22795,10 @@ var plugin60 = {
|
|
|
22673
22795
|
var todo_listener_default = plugin60;
|
|
22674
22796
|
|
|
22675
22797
|
// src/todo-tracker/index.ts
|
|
22676
|
-
import * as fsp from "node:fs/promises";
|
|
22677
22798
|
import { randomUUID } from "node:crypto";
|
|
22799
|
+
import * as fsp from "node:fs/promises";
|
|
22678
22800
|
import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core/utils";
|
|
22801
|
+
import { nowIso } from "@wrongstack/primitives";
|
|
22679
22802
|
function deriveFilePath(api) {
|
|
22680
22803
|
const raw = api.config.extensions?.["todo-tracker"];
|
|
22681
22804
|
const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
|
|
@@ -22720,9 +22843,6 @@ var state56 = {
|
|
|
22720
22843
|
/** Most recent mutation for /diag plugins visibility. */
|
|
22721
22844
|
lastMutation: null
|
|
22722
22845
|
};
|
|
22723
|
-
function nowIso() {
|
|
22724
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
22725
|
-
}
|
|
22726
22846
|
function ensureFile() {
|
|
22727
22847
|
if (!state56.file) {
|
|
22728
22848
|
state56.file = {
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* todo-tracker plugin — Persistent, project-scoped todo backlog that
|
|
3
|
+
* survives across sessions.
|
|
4
|
+
*
|
|
5
|
+
* Why a separate plugin from the built-in `todo` tool?
|
|
6
|
+
* - The built-in `todo` tool mutates `ctx.todos`, which is
|
|
7
|
+
* session-scoped and auto-clears when all items complete.
|
|
8
|
+
* - todo-tracker writes to disk (`~/.wrongstack/projects/<slug>/todo-tracker.json`)
|
|
9
|
+
* and survives across sessions. Items are explicit add/complete;
|
|
10
|
+
* no auto-clear.
|
|
11
|
+
*
|
|
12
|
+
* Use cases:
|
|
13
|
+
* - Backlog of work the user wants to track over days/weeks
|
|
14
|
+
* - Items the LLM noticed but didn't finish — pull them into a fresh
|
|
15
|
+
* session via `todo_tracker_pull` (the LLM then registers them with
|
|
16
|
+
* the session's `ctx.todos` via the built-in `todo` tool)
|
|
17
|
+
* - Per-project scratchpad that survives `wstack resume <id>`
|
|
18
|
+
*
|
|
19
|
+
* Tools registered:
|
|
20
|
+
* - todo_tracker_list : List items, filterable by status/tag/priority
|
|
21
|
+
* - todo_tracker_add : Append a new item
|
|
22
|
+
* - todo_tracker_complete : Mark an item completed (idempotent)
|
|
23
|
+
* - todo_tracker_drop : Mark an item dropped (idempotent)
|
|
24
|
+
* - todo_tracker_remove : Permanently delete by id
|
|
25
|
+
* - todo_tracker_pull : Return pending items for LLM to promote
|
|
26
|
+
* into the session's ctx.todos via the
|
|
27
|
+
* built-in `todo` tool
|
|
28
|
+
* - todo_tracker_status : Counters + last update timestamp
|
|
29
|
+
*/
|
|
1
30
|
import type { Plugin } from '@wrongstack/core/types';
|
|
2
31
|
declare const plugin: Plugin;
|
|
3
32
|
export default plugin;
|
package/dist/todo-tracker.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/todo-tracker/index.ts
|
|
2
|
-
import * as fsp from "node:fs/promises";
|
|
3
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fsp from "node:fs/promises";
|
|
4
4
|
import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
|
|
5
|
+
import { nowIso } from "@wrongstack/primitives";
|
|
5
6
|
function deriveFilePath(api) {
|
|
6
7
|
const raw = api.config.extensions?.["todo-tracker"];
|
|
7
8
|
const explicit = typeof raw?.["filePath"] === "string" ? raw["filePath"] : null;
|
|
@@ -46,9 +47,6 @@ var state = {
|
|
|
46
47
|
/** Most recent mutation for /diag plugins visibility. */
|
|
47
48
|
lastMutation: null
|
|
48
49
|
};
|
|
49
|
-
function nowIso() {
|
|
50
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
51
|
-
}
|
|
52
50
|
function ensureFile() {
|
|
53
51
|
if (!state.file) {
|
|
54
52
|
state.file = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.310.1",
|
|
4
4
|
"description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -303,9 +303,10 @@
|
|
|
303
303
|
"vitest": "^4.1.11"
|
|
304
304
|
},
|
|
305
305
|
"dependencies": {
|
|
306
|
-
"@wrongstack/
|
|
307
|
-
"@wrongstack/
|
|
308
|
-
"@wrongstack/
|
|
306
|
+
"@wrongstack/core": "0.310.1",
|
|
307
|
+
"@wrongstack/plugin-sdk": "0.310.1",
|
|
308
|
+
"@wrongstack/primitives": "0.310.1",
|
|
309
|
+
"@wrongstack/tools": "0.310.1"
|
|
309
310
|
},
|
|
310
311
|
"scripts": {
|
|
311
312
|
"build": "node ../../scripts/build-package.mjs",
|