@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.
- package/README.md +838 -0
- package/dist/auto-doc.d.ts +8 -0
- package/dist/auto-doc.js +175 -13
- package/dist/auto-escalate.d.ts +45 -0
- package/dist/auto-escalate.js +190 -0
- package/dist/branch-guard.d.ts +33 -0
- package/dist/branch-guard.js +228 -0
- package/dist/changelog-writer.d.ts +73 -0
- package/dist/changelog-writer.js +369 -0
- package/dist/checkpoint.d.ts +55 -0
- package/dist/checkpoint.js +305 -0
- package/dist/commit-validator.d.ts +33 -0
- package/dist/commit-validator.js +315 -0
- package/dist/config-validator.d.ts +48 -0
- package/dist/config-validator.js +347 -0
- package/dist/context-pins.d.ts +45 -0
- package/dist/context-pins.js +240 -0
- package/dist/cost-tracker.d.ts +40 -1
- package/dist/cost-tracker.js +105 -4
- package/dist/dep-guard.d.ts +65 -0
- package/dist/dep-guard.js +316 -0
- package/dist/diff-summary.d.ts +36 -0
- package/dist/diff-summary.js +235 -0
- package/dist/error-lens.d.ts +67 -0
- package/dist/error-lens.js +280 -0
- package/dist/format-on-save.d.ts +35 -0
- package/dist/format-on-save.js +219 -0
- package/dist/git-autocommit.js +186 -26
- package/dist/import-organizer.d.ts +52 -0
- package/dist/import-organizer.js +274 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +10151 -1628
- package/dist/injection-shield.d.ts +49 -0
- package/dist/injection-shield.js +205 -0
- package/dist/lint-gate.d.ts +33 -0
- package/dist/lint-gate.js +394 -0
- package/dist/llm-cache.d.ts +56 -0
- package/dist/llm-cache.js +251 -0
- package/dist/loop-breaker.d.ts +43 -0
- package/dist/loop-breaker.js +241 -0
- package/dist/model-router.d.ts +69 -0
- package/dist/model-router.js +198 -0
- package/dist/notify-hub.d.ts +45 -0
- package/dist/notify-hub.js +304 -0
- package/dist/path-guard.d.ts +54 -0
- package/dist/path-guard.js +235 -0
- package/dist/prompt-firewall.d.ts +57 -0
- package/dist/prompt-firewall.js +290 -0
- package/dist/secret-scanner.d.ts +34 -0
- package/dist/secret-scanner.js +409 -0
- package/dist/semver-bump.js +45 -0
- package/dist/session-recap.d.ts +50 -0
- package/dist/session-recap.js +421 -0
- package/dist/shell-check.js +52 -4
- package/dist/spec-linker.d.ts +51 -0
- package/dist/spec-linker.js +541 -0
- package/dist/template-engine.js +19 -1
- package/dist/test-runner-gate.d.ts +37 -0
- package/dist/test-runner-gate.js +356 -0
- package/dist/todo-listener.d.ts +37 -0
- package/dist/todo-listener.js +216 -0
- package/dist/todo-tracker.d.ts +5 -0
- package/dist/todo-tracker.js +441 -0
- package/dist/token-budget.d.ts +40 -0
- package/dist/token-budget.js +254 -0
- package/dist/token-throttle.d.ts +54 -0
- package/dist/token-throttle.js +203 -0
- package/package.json +116 -12
- package/dist/json-path.d.ts +0 -18
- package/dist/json-path.js +0 -15
- package/dist/web-search.d.ts +0 -19
- package/dist/web-search.js +0 -15
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// src/session-recap/index.ts
|
|
2
|
+
var state = {
|
|
3
|
+
recapsPublished: 0,
|
|
4
|
+
recapsErrored: 0,
|
|
5
|
+
recapsSkipped: 0,
|
|
6
|
+
aiSummariesWritten: 0,
|
|
7
|
+
aiSummaryErrors: 0,
|
|
8
|
+
stopInvocations: 0,
|
|
9
|
+
totalInputTokens: 0,
|
|
10
|
+
totalOutputTokens: 0,
|
|
11
|
+
perModel: /* @__PURE__ */ new Map(),
|
|
12
|
+
toolCounts: /* @__PURE__ */ new Map(),
|
|
13
|
+
commitCount: 0,
|
|
14
|
+
startedAt: null,
|
|
15
|
+
lastActivityAt: null,
|
|
16
|
+
stopHookUnregister: null,
|
|
17
|
+
eventUnsubscribers: []
|
|
18
|
+
};
|
|
19
|
+
var DEFAULTS = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
subjectPrefix: "session recap: ",
|
|
22
|
+
includeTranscriptTail: 3,
|
|
23
|
+
maxBodyChars: 8e3,
|
|
24
|
+
aiSummary: false
|
|
25
|
+
};
|
|
26
|
+
function readConfig(raw) {
|
|
27
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
28
|
+
const r = raw;
|
|
29
|
+
return {
|
|
30
|
+
enabled: r["enabled"] !== false,
|
|
31
|
+
subjectPrefix: typeof r["subjectPrefix"] === "string" ? r["subjectPrefix"] : DEFAULTS.subjectPrefix,
|
|
32
|
+
includeTranscriptTail: typeof r["includeTranscriptTail"] === "number" && r["includeTranscriptTail"] >= 0 ? r["includeTranscriptTail"] : DEFAULTS.includeTranscriptTail,
|
|
33
|
+
maxBodyChars: typeof r["maxBodyChars"] === "number" && r["maxBodyChars"] > 0 ? r["maxBodyChars"] : DEFAULTS.maxBodyChars,
|
|
34
|
+
aiSummary: r["aiSummary"] === true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function touchActivity() {
|
|
38
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
39
|
+
if (state.startedAt === null) state.startedAt = now;
|
|
40
|
+
state.lastActivityAt = now;
|
|
41
|
+
}
|
|
42
|
+
function bumpModelUsage(model, inputTokens, outputTokens) {
|
|
43
|
+
let m = state.perModel.get(model);
|
|
44
|
+
if (!m) {
|
|
45
|
+
m = { inputTokens: 0, outputTokens: 0, invocations: 0 };
|
|
46
|
+
state.perModel.set(model, m);
|
|
47
|
+
}
|
|
48
|
+
m.inputTokens += inputTokens;
|
|
49
|
+
m.outputTokens += outputTokens;
|
|
50
|
+
m.invocations += 1;
|
|
51
|
+
state.totalInputTokens += inputTokens;
|
|
52
|
+
state.totalOutputTokens += outputTokens;
|
|
53
|
+
}
|
|
54
|
+
function bumpToolCount(name) {
|
|
55
|
+
state.toolCounts.set(name, (state.toolCounts.get(name) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
function formatDuration(startedAt, lastActivityAt) {
|
|
58
|
+
if (!startedAt) return "0s";
|
|
59
|
+
const start = Date.parse(startedAt);
|
|
60
|
+
const end = lastActivityAt ? Date.parse(lastActivityAt) : Date.now();
|
|
61
|
+
const ms = Math.max(0, end - start);
|
|
62
|
+
const sec = Math.floor(ms / 1e3);
|
|
63
|
+
if (sec < 60) return `${sec}s`;
|
|
64
|
+
const min = Math.floor(sec / 60);
|
|
65
|
+
const remSec = sec % 60;
|
|
66
|
+
if (min < 60) return `${min}m${remSec}s`;
|
|
67
|
+
const hr = Math.floor(min / 60);
|
|
68
|
+
const remMin = min % 60;
|
|
69
|
+
return `${hr}h${remMin}m`;
|
|
70
|
+
}
|
|
71
|
+
function topN(map, n) {
|
|
72
|
+
return [...map.entries()].sort((a, b) => {
|
|
73
|
+
const av = a[1];
|
|
74
|
+
const bv = b[1];
|
|
75
|
+
if (typeof av === "number" && typeof bv === "number") return bv - av;
|
|
76
|
+
return 0;
|
|
77
|
+
}).slice(0, n);
|
|
78
|
+
}
|
|
79
|
+
async function readTranscriptTail(transcriptPath, n) {
|
|
80
|
+
if (!transcriptPath || n <= 0) return [];
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
const { readFile } = await import('fs/promises');
|
|
84
|
+
raw = await readFile(transcriptPath, "utf-8");
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
89
|
+
const tail = lines.slice(-n);
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const l of tail) {
|
|
92
|
+
try {
|
|
93
|
+
out.push(JSON.parse(l));
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function truncate(s, max) {
|
|
100
|
+
return s.length > max ? s.slice(0, max) + `
|
|
101
|
+
|
|
102
|
+
[truncated ${s.length - max} chars]` : s;
|
|
103
|
+
}
|
|
104
|
+
var plugin = {
|
|
105
|
+
name: "session-recap",
|
|
106
|
+
version: "0.1.0",
|
|
107
|
+
description: "Stop hook that posts a one-page session summary (tokens, tools, commits, last activity) to the project mailbox",
|
|
108
|
+
apiVersion: "^0.1.10",
|
|
109
|
+
capabilities: { tools: true, hooks: true },
|
|
110
|
+
defaultConfig: { ...DEFAULTS },
|
|
111
|
+
configSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
115
|
+
subjectPrefix: {
|
|
116
|
+
type: "string",
|
|
117
|
+
default: DEFAULTS.subjectPrefix,
|
|
118
|
+
description: "Prepended to the broadcast subject."
|
|
119
|
+
},
|
|
120
|
+
includeTranscriptTail: {
|
|
121
|
+
type: "number",
|
|
122
|
+
minimum: 0,
|
|
123
|
+
maximum: 50,
|
|
124
|
+
default: 3,
|
|
125
|
+
description: "Number of last transcript events to include in the recap body."
|
|
126
|
+
},
|
|
127
|
+
maxBodyChars: {
|
|
128
|
+
type: "number",
|
|
129
|
+
minimum: 500,
|
|
130
|
+
default: 8e3,
|
|
131
|
+
description: "Hard cap on the recap body size (chars)."
|
|
132
|
+
},
|
|
133
|
+
aiSummary: {
|
|
134
|
+
type: "boolean",
|
|
135
|
+
default: false,
|
|
136
|
+
description: 'Prepend an LLM-written natural-language summary (api.llm) to the recap. Provider/model follow extensions["session-recap"].llm, then the session default.'
|
|
137
|
+
},
|
|
138
|
+
llm: {
|
|
139
|
+
type: "object",
|
|
140
|
+
description: "Optional { provider, model } override for the AI summary."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
setup(api) {
|
|
145
|
+
state.recapsPublished = 0;
|
|
146
|
+
state.recapsErrored = 0;
|
|
147
|
+
state.recapsSkipped = 0;
|
|
148
|
+
state.aiSummariesWritten = 0;
|
|
149
|
+
state.aiSummaryErrors = 0;
|
|
150
|
+
state.stopInvocations = 0;
|
|
151
|
+
state.totalInputTokens = 0;
|
|
152
|
+
state.totalOutputTokens = 0;
|
|
153
|
+
state.perModel.clear();
|
|
154
|
+
state.toolCounts.clear();
|
|
155
|
+
state.commitCount = 0;
|
|
156
|
+
state.startedAt = null;
|
|
157
|
+
state.lastActivityAt = null;
|
|
158
|
+
state.stopHookUnregister = null;
|
|
159
|
+
for (const off of state.eventUnsubscribers) {
|
|
160
|
+
try {
|
|
161
|
+
off();
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
state.eventUnsubscribers = [];
|
|
166
|
+
const cfg = readConfig(api.config.extensions?.["session-recap"]);
|
|
167
|
+
const mailbox = api.mailbox;
|
|
168
|
+
if (api.onEvent) {
|
|
169
|
+
const offUsage = api.onEvent("provider.response", (payload) => {
|
|
170
|
+
touchActivity();
|
|
171
|
+
const p = payload;
|
|
172
|
+
const model = p?.model ?? "unknown";
|
|
173
|
+
const input = p?.usage?.input ?? 0;
|
|
174
|
+
const output = p?.usage?.output ?? 0;
|
|
175
|
+
bumpModelUsage(model, input, output);
|
|
176
|
+
});
|
|
177
|
+
state.eventUnsubscribers.push(offUsage);
|
|
178
|
+
}
|
|
179
|
+
if (api.onPattern) {
|
|
180
|
+
const offTool = api.onPattern("tool.*", (eventName, payload) => {
|
|
181
|
+
touchActivity();
|
|
182
|
+
const p = payload;
|
|
183
|
+
const toolName = p?.tool ?? p?.name ?? eventName;
|
|
184
|
+
if (typeof toolName === "string") bumpToolCount(toolName);
|
|
185
|
+
if (toolName === "git_autocommit" || toolName.startsWith("git ")) ;
|
|
186
|
+
});
|
|
187
|
+
state.eventUnsubscribers.push(offTool);
|
|
188
|
+
const offResult = api.onPattern("tool.result", (_event, payload) => {
|
|
189
|
+
const p = payload;
|
|
190
|
+
if (p?.tool === "git_autocommit" && p.isError === false) {
|
|
191
|
+
state.commitCount += 1;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
state.eventUnsubscribers.push(offResult);
|
|
195
|
+
}
|
|
196
|
+
const stopHook = async (input) => {
|
|
197
|
+
if (!cfg.enabled) return;
|
|
198
|
+
touchActivity();
|
|
199
|
+
state.stopInvocations += 1;
|
|
200
|
+
if (!mailbox) {
|
|
201
|
+
state.recapsSkipped += 1;
|
|
202
|
+
api.log.warn(
|
|
203
|
+
"session-recap: no mailbox available on api \u2014 recap disabled. Add `mailbox` to the setupPlugins() call to enable cross-session summaries."
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const transcriptPath = api.session?.transcriptPath;
|
|
208
|
+
const tailEvents = await readTranscriptTail(transcriptPath, cfg.includeTranscriptTail);
|
|
209
|
+
const duration = formatDuration(state.startedAt, state.lastActivityAt);
|
|
210
|
+
const recap = {
|
|
211
|
+
session: {
|
|
212
|
+
id: input.sessionId ?? null,
|
|
213
|
+
cwd: input.cwd ?? null,
|
|
214
|
+
startedAt: state.startedAt,
|
|
215
|
+
endedAt: state.lastActivityAt,
|
|
216
|
+
duration
|
|
217
|
+
},
|
|
218
|
+
tokens: {
|
|
219
|
+
total: { input: state.totalInputTokens, output: state.totalOutputTokens },
|
|
220
|
+
perModel: topN(state.perModel, 10).map(([model, u]) => ({
|
|
221
|
+
model,
|
|
222
|
+
input: u.inputTokens,
|
|
223
|
+
output: u.outputTokens,
|
|
224
|
+
invocations: u.invocations
|
|
225
|
+
}))
|
|
226
|
+
},
|
|
227
|
+
tools: {
|
|
228
|
+
totalCalls: [...state.toolCounts.values()].reduce((a, b) => a + b, 0),
|
|
229
|
+
uniqueTools: state.toolCounts.size,
|
|
230
|
+
top: topN(state.toolCounts, 5)
|
|
231
|
+
},
|
|
232
|
+
commits: state.commitCount,
|
|
233
|
+
transcriptTail: tailEvents.flatMap((e) => {
|
|
234
|
+
const entry = {};
|
|
235
|
+
if (e.type !== void 0) entry.type = e.type;
|
|
236
|
+
if (e.ts !== void 0) entry.ts = e.ts;
|
|
237
|
+
if (e.role !== void 0) entry.role = e.role;
|
|
238
|
+
if (typeof e.content === "string") entry.preview = e.content.slice(0, 200);
|
|
239
|
+
return [entry];
|
|
240
|
+
})
|
|
241
|
+
};
|
|
242
|
+
const subject = `${cfg.subjectPrefix}${recap.session.id ?? "session"} \u2014 ${duration}, ${recap.tools.totalCalls} tool calls, ${recap.tokens.total.input + recap.tokens.total.output} tokens`.slice(
|
|
243
|
+
0,
|
|
244
|
+
200
|
|
245
|
+
);
|
|
246
|
+
let aiSummary = null;
|
|
247
|
+
if (cfg.aiSummary && api.llm) {
|
|
248
|
+
try {
|
|
249
|
+
const topTools = recap.tools.top.map(([n, c]) => `${n}\xD7${c}`).join(", ") || "none";
|
|
250
|
+
const result = await api.llm.complete(
|
|
251
|
+
`Summarize this coding-agent session in 2-3 sentences for a teammate catching up. Focus on what was worked on and the scale of activity. Be concrete and terse.
|
|
252
|
+
|
|
253
|
+
Duration: ${duration}
|
|
254
|
+
Tool calls: ${recap.tools.totalCalls} (top: ${topTools})
|
|
255
|
+
Commits: ${recap.commits}
|
|
256
|
+
Tokens: ${recap.tokens.total.input} in / ${recap.tokens.total.output} out
|
|
257
|
+
` + (recap.transcriptTail.length > 0 ? `Recent activity: ${recap.transcriptTail.map((e) => e.preview ?? e.type ?? "").filter(Boolean).join(" | ").slice(0, 500)}` : ""),
|
|
258
|
+
{ system: "You write concise engineering session recaps.", maxTokens: 200 }
|
|
259
|
+
);
|
|
260
|
+
const text = result.text.trim();
|
|
261
|
+
if (text) {
|
|
262
|
+
aiSummary = text;
|
|
263
|
+
state.aiSummariesWritten += 1;
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
state.aiSummaryErrors += 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const recapWithSummary = aiSummary ? { summary: aiSummary, ...recap } : recap;
|
|
270
|
+
const bodyPrefix = aiSummary ? `${aiSummary}
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
` : "";
|
|
274
|
+
const body = truncate(
|
|
275
|
+
bodyPrefix + JSON.stringify(recapWithSummary, null, 2),
|
|
276
|
+
cfg.maxBodyChars
|
|
277
|
+
);
|
|
278
|
+
try {
|
|
279
|
+
const result = await mailbox.send({
|
|
280
|
+
from: "plugin:session-recap",
|
|
281
|
+
to: "*",
|
|
282
|
+
type: "status",
|
|
283
|
+
subject,
|
|
284
|
+
body,
|
|
285
|
+
priority: "low"
|
|
286
|
+
});
|
|
287
|
+
state.recapsPublished += 1;
|
|
288
|
+
api.log.info("session-recap: published session summary", {
|
|
289
|
+
messageId: result.id ?? null,
|
|
290
|
+
duration,
|
|
291
|
+
toolCalls: recap.tools.totalCalls,
|
|
292
|
+
tokensIn: recap.tokens.total.input,
|
|
293
|
+
tokensOut: recap.tokens.total.output
|
|
294
|
+
});
|
|
295
|
+
} catch (err) {
|
|
296
|
+
state.recapsErrored += 1;
|
|
297
|
+
api.log.warn("session-recap: mailbox.send failed", {
|
|
298
|
+
error: err instanceof Error ? err.message : String(err)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
state.stopHookUnregister = api.registerHook("Stop", void 0, stopHook);
|
|
303
|
+
api.tools.register({
|
|
304
|
+
name: "session_recap_status",
|
|
305
|
+
description: "Reports session-recap state: config, accumulated metrics (tokens, tool calls, commits), and last recap status.",
|
|
306
|
+
inputSchema: { type: "object", properties: {} },
|
|
307
|
+
permission: "auto",
|
|
308
|
+
category: "Diagnostics",
|
|
309
|
+
mutating: false,
|
|
310
|
+
async execute() {
|
|
311
|
+
return {
|
|
312
|
+
ok: true,
|
|
313
|
+
enabled: cfg.enabled,
|
|
314
|
+
subjectPrefix: cfg.subjectPrefix,
|
|
315
|
+
includeTranscriptTail: cfg.includeTranscriptTail,
|
|
316
|
+
maxBodyChars: cfg.maxBodyChars,
|
|
317
|
+
mailboxAvailable: Boolean(mailbox),
|
|
318
|
+
aiSummary: cfg.aiSummary,
|
|
319
|
+
llmAvailable: Boolean(api.llm),
|
|
320
|
+
counters: {
|
|
321
|
+
stopInvocations: state.stopInvocations,
|
|
322
|
+
recapsPublished: state.recapsPublished,
|
|
323
|
+
recapsErrored: state.recapsErrored,
|
|
324
|
+
recapsSkipped: state.recapsSkipped,
|
|
325
|
+
aiSummariesWritten: state.aiSummariesWritten,
|
|
326
|
+
aiSummaryErrors: state.aiSummaryErrors
|
|
327
|
+
},
|
|
328
|
+
metrics: {
|
|
329
|
+
totalInputTokens: state.totalInputTokens,
|
|
330
|
+
totalOutputTokens: state.totalOutputTokens,
|
|
331
|
+
perModel: topN(state.perModel, 10).map(([model, u]) => ({
|
|
332
|
+
model,
|
|
333
|
+
input: u.inputTokens,
|
|
334
|
+
output: u.outputTokens,
|
|
335
|
+
invocations: u.invocations
|
|
336
|
+
})),
|
|
337
|
+
toolCalls: {
|
|
338
|
+
total: [...state.toolCounts.values()].reduce((a, b) => a + b, 0),
|
|
339
|
+
uniqueTools: state.toolCounts.size,
|
|
340
|
+
top: topN(state.toolCounts, 5)
|
|
341
|
+
},
|
|
342
|
+
commits: state.commitCount
|
|
343
|
+
},
|
|
344
|
+
timing: {
|
|
345
|
+
startedAt: state.startedAt,
|
|
346
|
+
lastActivityAt: state.lastActivityAt,
|
|
347
|
+
duration: formatDuration(state.startedAt, state.lastActivityAt)
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
api.log.info("session-recap plugin loaded", {
|
|
353
|
+
version: "0.1.0",
|
|
354
|
+
enabled: cfg.enabled,
|
|
355
|
+
mailboxAvailable: Boolean(mailbox)
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
teardown(api) {
|
|
359
|
+
if (state.stopHookUnregister) {
|
|
360
|
+
try {
|
|
361
|
+
state.stopHookUnregister();
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
state.stopHookUnregister = null;
|
|
365
|
+
}
|
|
366
|
+
for (const off of state.eventUnsubscribers) {
|
|
367
|
+
try {
|
|
368
|
+
off();
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
state.eventUnsubscribers = [];
|
|
373
|
+
const final = {
|
|
374
|
+
recapsPublished: state.recapsPublished,
|
|
375
|
+
recapsErrored: state.recapsErrored,
|
|
376
|
+
recapsSkipped: state.recapsSkipped,
|
|
377
|
+
aiSummariesWritten: state.aiSummariesWritten,
|
|
378
|
+
totalInputTokens: state.totalInputTokens,
|
|
379
|
+
totalOutputTokens: state.totalOutputTokens,
|
|
380
|
+
toolCalls: [...state.toolCounts.values()].reduce((a, b) => a + b, 0),
|
|
381
|
+
commits: state.commitCount
|
|
382
|
+
};
|
|
383
|
+
state.recapsPublished = 0;
|
|
384
|
+
state.recapsErrored = 0;
|
|
385
|
+
state.recapsSkipped = 0;
|
|
386
|
+
state.aiSummariesWritten = 0;
|
|
387
|
+
state.aiSummaryErrors = 0;
|
|
388
|
+
state.stopInvocations = 0;
|
|
389
|
+
state.totalInputTokens = 0;
|
|
390
|
+
state.totalOutputTokens = 0;
|
|
391
|
+
state.perModel.clear();
|
|
392
|
+
state.toolCounts.clear();
|
|
393
|
+
state.commitCount = 0;
|
|
394
|
+
state.startedAt = null;
|
|
395
|
+
state.lastActivityAt = null;
|
|
396
|
+
api.log.info("session-recap: teardown complete", { final });
|
|
397
|
+
},
|
|
398
|
+
async health() {
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
message: `session-recap: ${state.stopInvocations} stop(s), ${state.recapsPublished} recap(s) published (${state.aiSummariesWritten} with AI summary), ${state.recapsErrored} error(s), ${state.totalInputTokens + state.totalOutputTokens} tokens observed`,
|
|
402
|
+
counters: {
|
|
403
|
+
stopInvocations: state.stopInvocations,
|
|
404
|
+
recapsPublished: state.recapsPublished,
|
|
405
|
+
recapsErrored: state.recapsErrored,
|
|
406
|
+
recapsSkipped: state.recapsSkipped,
|
|
407
|
+
aiSummariesWritten: state.aiSummariesWritten,
|
|
408
|
+
aiSummaryErrors: state.aiSummaryErrors
|
|
409
|
+
},
|
|
410
|
+
metrics: {
|
|
411
|
+
totalInputTokens: state.totalInputTokens,
|
|
412
|
+
totalOutputTokens: state.totalOutputTokens,
|
|
413
|
+
toolCalls: [...state.toolCounts.values()].reduce((a, b) => a + b, 0),
|
|
414
|
+
commits: state.commitCount
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var session_recap_default = plugin;
|
|
420
|
+
|
|
421
|
+
export { session_recap_default as default };
|
package/dist/shell-check.js
CHANGED
|
@@ -4,6 +4,14 @@ import { join } from 'path';
|
|
|
4
4
|
|
|
5
5
|
// src/shell-check/index.ts
|
|
6
6
|
var API_VERSION = "^0.1.10";
|
|
7
|
+
var state = {
|
|
8
|
+
/** Per-session invocation count. */
|
|
9
|
+
invocationCount: 0,
|
|
10
|
+
/** Total issues found across all runs this session (success or fail). */
|
|
11
|
+
totalIssues: 0,
|
|
12
|
+
/** Most recent run summary, surfaced by health(). */
|
|
13
|
+
lastRun: null
|
|
14
|
+
};
|
|
7
15
|
function runShellCheck(files, severity, cwd) {
|
|
8
16
|
if (!existsSync("shellcheck")) {
|
|
9
17
|
try {
|
|
@@ -92,6 +100,9 @@ var plugin = {
|
|
|
92
100
|
}
|
|
93
101
|
},
|
|
94
102
|
setup(api) {
|
|
103
|
+
state.invocationCount = 0;
|
|
104
|
+
state.totalIssues = 0;
|
|
105
|
+
state.lastRun = null;
|
|
95
106
|
api.tools.register({
|
|
96
107
|
name: "shellcheck",
|
|
97
108
|
description: "Run shellcheck analysis on shell script files. Pass `files` for specific files, or `directory` (optionally with `pattern`) to recursively scan for .sh files. Returns issues with file, line, column, severity, code, and message.",
|
|
@@ -130,10 +141,12 @@ var plugin = {
|
|
|
130
141
|
category: "Code Quality",
|
|
131
142
|
mutating: true,
|
|
132
143
|
async execute(input) {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
144
|
+
const inp = input;
|
|
145
|
+
const files = inp.files;
|
|
146
|
+
const directory = inp.directory ?? ".";
|
|
147
|
+
const pattern = inp.pattern ?? "";
|
|
148
|
+
const severity = inp.severity ?? "warning";
|
|
149
|
+
state.invocationCount += 1;
|
|
137
150
|
let checkFiles;
|
|
138
151
|
let scannedDirectories = false;
|
|
139
152
|
if (files && files.length > 0) {
|
|
@@ -143,6 +156,13 @@ var plugin = {
|
|
|
143
156
|
scannedDirectories = true;
|
|
144
157
|
}
|
|
145
158
|
if (checkFiles.length === 0) {
|
|
159
|
+
state.lastRun = {
|
|
160
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
161
|
+
filesChecked: 0,
|
|
162
|
+
issues: 0,
|
|
163
|
+
severity,
|
|
164
|
+
mode: scannedDirectories ? "directory" : "files"
|
|
165
|
+
};
|
|
146
166
|
return {
|
|
147
167
|
ok: true,
|
|
148
168
|
filesScanned: 0,
|
|
@@ -171,6 +191,14 @@ var plugin = {
|
|
|
171
191
|
const styleCount = issues.filter((i) => i.level === "style").length;
|
|
172
192
|
api.metrics.counter("issues_found", issues.length, { severity });
|
|
173
193
|
api.metrics.histogram("issues_per_file", issues.length / Math.max(checkFiles.length, 1));
|
|
194
|
+
state.totalIssues += issues.length;
|
|
195
|
+
state.lastRun = {
|
|
196
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
197
|
+
filesChecked: checkFiles.length,
|
|
198
|
+
issues: issues.length,
|
|
199
|
+
severity,
|
|
200
|
+
mode: scannedDirectories ? "directory" : "files"
|
|
201
|
+
};
|
|
174
202
|
return {
|
|
175
203
|
ok: true,
|
|
176
204
|
mode: scannedDirectories ? "directory" : "files",
|
|
@@ -190,6 +218,26 @@ var plugin = {
|
|
|
190
218
|
}
|
|
191
219
|
});
|
|
192
220
|
api.log.info("shell-check plugin loaded", { version: "0.2.0" });
|
|
221
|
+
},
|
|
222
|
+
teardown(api) {
|
|
223
|
+
const finalInvocations = state.invocationCount;
|
|
224
|
+
const finalIssues = state.totalIssues;
|
|
225
|
+
state.invocationCount = 0;
|
|
226
|
+
state.totalIssues = 0;
|
|
227
|
+
state.lastRun = null;
|
|
228
|
+
api.log.info("shell-check: teardown complete", {
|
|
229
|
+
invocations: finalInvocations,
|
|
230
|
+
totalIssues: finalIssues
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
async health() {
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
message: state.lastRun === null ? `shell-check: ${state.invocationCount} run(s) this session` : `shell-check: last run checked ${state.lastRun.filesChecked} file(s), ${state.lastRun.issues} issue(s) at ${state.lastRun.when}`,
|
|
237
|
+
invocationCount: state.invocationCount,
|
|
238
|
+
totalIssues: state.totalIssues,
|
|
239
|
+
lastRun: state.lastRun
|
|
240
|
+
};
|
|
193
241
|
}
|
|
194
242
|
};
|
|
195
243
|
var shell_check_default = plugin;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* spec-linker plugin — markdown link auditor for plugin references.
|
|
5
|
+
*
|
|
6
|
+
* Two hooks:
|
|
7
|
+
* 1. **PostToolUse** on `write|edit` — READ-ONLY. Scans the saved
|
|
8
|
+
* file for unlinked plugin references and surfaces them to the
|
|
9
|
+
* LLM via `additionalContext`. The LLM decides whether to fix
|
|
10
|
+
* the file in a follow-up edit.
|
|
11
|
+
*
|
|
12
|
+
* 2. **PreToolUse** on `write` (NOT `edit`) — AUTO-FIX. When the
|
|
13
|
+
* `autoFix` config is `true`, scans the would-be content and
|
|
14
|
+
* returns a `modifiedInput.content` where each unlinked plugin
|
|
15
|
+
* reference is wrapped in a markdown link. The tool executor
|
|
16
|
+
* then writes the fixed content instead of the original.
|
|
17
|
+
*
|
|
18
|
+
* Why `write` only and not `edit`? The `edit` tool's input shape
|
|
19
|
+
* is `{ path, old_string, new_string }` — `new_string` is a small
|
|
20
|
+
* patch, not the whole file. To auto-fix `edit` cleanly we'd have
|
|
21
|
+
* to either:
|
|
22
|
+
* - parse the file, find where `old_string` lives, substitute
|
|
23
|
+
* `new_string` with the auto-fixed version, and re-derive
|
|
24
|
+
* the new `old_string` (a hard string-diff problem), or
|
|
25
|
+
* - reject `edit` and force `write` (bad UX).
|
|
26
|
+
* Both are too complex for the win. `write` is the common case
|
|
27
|
+
* for new files; `edit` stays read-only and the PostToolUse
|
|
28
|
+
* context tells the LLM what to fix.
|
|
29
|
+
*
|
|
30
|
+
* The plugin catalog is sourced from `../catalog.js` (single source
|
|
31
|
+
* of truth — adding a new plugin to the catalog table there is
|
|
32
|
+
* enough for this plugin to start detecting it).
|
|
33
|
+
*
|
|
34
|
+
* Config (`config.extensions['spec-linker']`):
|
|
35
|
+
*
|
|
36
|
+
* ```jsonc
|
|
37
|
+
* {
|
|
38
|
+
* "enabled": true,
|
|
39
|
+
* "fileGlobs": ["**\/*.md", "**\/*.mdx"],
|
|
40
|
+
* "maxReferences": 8,
|
|
41
|
+
* "autoFix": false // when true, PreToolUse on `write` wraps unlinked
|
|
42
|
+
* // references in markdown links via modifiedInput
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
declare const plugin: Plugin;
|
|
50
|
+
|
|
51
|
+
export { plugin as default };
|