@wrongstack/plugins 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/auto-doc.d.ts +13 -0
- package/dist/auto-doc.js +239 -0
- package/dist/cost-tracker.d.ts +14 -0
- package/dist/cost-tracker.js +209 -0
- package/dist/cron.d.ts +14 -0
- package/dist/cron.js +223 -0
- package/dist/file-watcher.d.ts +14 -0
- package/dist/file-watcher.js +192 -0
- package/dist/git-autocommit.d.ts +14 -0
- package/dist/git-autocommit.js +305 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +2503 -0
- package/dist/json-path.d.ts +15 -0
- package/dist/json-path.js +311 -0
- package/dist/semver-bump.d.ts +14 -0
- package/dist/semver-bump.js +347 -0
- package/dist/shell-check.d.ts +13 -0
- package/dist/shell-check.js +225 -0
- package/dist/template-engine.d.ts +15 -0
- package/dist/template-engine.js +267 -0
- package/dist/web-search.d.ts +13 -0
- package/dist/web-search.js +210 -0
- package/package.json +75 -0
package/dist/cron.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// src/cron/index.ts
|
|
2
|
+
var API_VERSION = "^0.1.10";
|
|
3
|
+
function formatNextRun(intervalMs) {
|
|
4
|
+
const ms = isNaN(intervalMs) || intervalMs <= 0 ? 6e4 : intervalMs;
|
|
5
|
+
return new Date(Date.now() + ms).toISOString();
|
|
6
|
+
}
|
|
7
|
+
var plugin = {
|
|
8
|
+
name: "cron",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
description: "Schedules recurring tasks using beforeIteration/afterIteration extension hooks",
|
|
11
|
+
apiVersion: API_VERSION,
|
|
12
|
+
capabilities: { tools: true },
|
|
13
|
+
defaultConfig: {
|
|
14
|
+
maxConcurrentJobs: 5,
|
|
15
|
+
timezone: "UTC",
|
|
16
|
+
persistSchedules: false
|
|
17
|
+
},
|
|
18
|
+
configSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
maxConcurrentJobs: { type: "number", default: 5 },
|
|
22
|
+
timezone: { type: "string", default: "UTC" },
|
|
23
|
+
persistSchedules: { type: "boolean", default: false }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
setup(api) {
|
|
27
|
+
const state = {
|
|
28
|
+
jobs: /* @__PURE__ */ new Map(),
|
|
29
|
+
timers: /* @__PURE__ */ new Map(),
|
|
30
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31
|
+
};
|
|
32
|
+
const maxConcurrent = api.config.extensions?.["cron"]?.["maxConcurrentJobs"] ?? 5;
|
|
33
|
+
function scheduleNextRun(name) {
|
|
34
|
+
const job = state.jobs.get(name);
|
|
35
|
+
if (!job || !job.enabled) return;
|
|
36
|
+
const existing = state.timers.get(name);
|
|
37
|
+
if (existing) clearTimeout(existing);
|
|
38
|
+
const delay = Math.max(0, new Date(job.nextRun).getTime() - Date.now());
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
job.runCount++;
|
|
41
|
+
job.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
42
|
+
job.nextRun = formatNextRun(job.intervalMs);
|
|
43
|
+
api.emitCustom("cron:job_fired", {
|
|
44
|
+
name,
|
|
45
|
+
action: job.action,
|
|
46
|
+
runCount: job.runCount,
|
|
47
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
48
|
+
});
|
|
49
|
+
api.metrics.counter("cron_job_fired", 1, { job: name });
|
|
50
|
+
api.metrics.histogram("cron_job_interval_ms", job.intervalMs, { job: name });
|
|
51
|
+
scheduleNextRun(name);
|
|
52
|
+
}, delay);
|
|
53
|
+
state.timers.set(name, timer);
|
|
54
|
+
}
|
|
55
|
+
function cancelJob(name) {
|
|
56
|
+
const timer = state.timers.get(name);
|
|
57
|
+
if (timer) {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
state.timers.delete(name);
|
|
60
|
+
}
|
|
61
|
+
state.jobs.delete(name);
|
|
62
|
+
}
|
|
63
|
+
api.extensions.register({
|
|
64
|
+
name: "cron-iteration-hooks",
|
|
65
|
+
owner: "cron",
|
|
66
|
+
beforeIteration: async (_ctx, _idx) => {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
let activeJobs = 0;
|
|
69
|
+
const promises = [];
|
|
70
|
+
for (const [name, job] of state.jobs) {
|
|
71
|
+
if (!job.enabled) continue;
|
|
72
|
+
if (activeJobs >= maxConcurrent) break;
|
|
73
|
+
if (new Date(job.nextRun).getTime() <= now) {
|
|
74
|
+
activeJobs++;
|
|
75
|
+
promises.push(
|
|
76
|
+
(async () => {
|
|
77
|
+
await api.session.append({
|
|
78
|
+
type: "cron:scheduled_trigger",
|
|
79
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
80
|
+
jobName: name,
|
|
81
|
+
action: job.action,
|
|
82
|
+
runCount: job.runCount + 1
|
|
83
|
+
});
|
|
84
|
+
api.emitCustom("cron:job_due", {
|
|
85
|
+
name,
|
|
86
|
+
action: job.action,
|
|
87
|
+
dueAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
88
|
+
});
|
|
89
|
+
})()
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await Promise.all(promises);
|
|
94
|
+
},
|
|
95
|
+
afterIteration: async (_ctx, _idx) => {
|
|
96
|
+
for (const job of state.jobs.values()) {
|
|
97
|
+
if (!job.enabled) continue;
|
|
98
|
+
if (new Date(job.nextRun).getTime() <= Date.now()) {
|
|
99
|
+
job.nextRun = formatNextRun(job.intervalMs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
api.tools.register({
|
|
105
|
+
name: "cron_schedule",
|
|
106
|
+
description: "Schedule a recurring action to fire at a fixed interval (in milliseconds). The action is emitted as a custom event for downstream handlers.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
name: { type: "string", description: "Unique name for this cron job" },
|
|
111
|
+
intervalMs: { type: "number", description: "Interval between runs in milliseconds (minimum 1000)" },
|
|
112
|
+
action: { type: "string", description: "Action identifier or description of what to run" },
|
|
113
|
+
enabled: { type: "boolean", default: true }
|
|
114
|
+
},
|
|
115
|
+
required: ["name", "intervalMs", "action"]
|
|
116
|
+
},
|
|
117
|
+
permission: "confirm",
|
|
118
|
+
mutating: false,
|
|
119
|
+
async execute(input) {
|
|
120
|
+
const name = input["name"];
|
|
121
|
+
const intervalMs = Math.max(1e3, Number(input["intervalMs"]));
|
|
122
|
+
const action = input["action"];
|
|
123
|
+
const enabled = input["enabled"] ?? true;
|
|
124
|
+
if (!name || typeof name !== "string" || name.trim() === "") {
|
|
125
|
+
return { ok: false, error: "name is required and must be a non-empty string" };
|
|
126
|
+
}
|
|
127
|
+
if (isNaN(intervalMs)) {
|
|
128
|
+
return { ok: false, error: "intervalMs must be a number >= 1000" };
|
|
129
|
+
}
|
|
130
|
+
if (state.jobs.has(name)) {
|
|
131
|
+
return { ok: false, error: `Cron job '${name}' already exists. Use cron_cancel first.` };
|
|
132
|
+
}
|
|
133
|
+
if (state.jobs.size >= maxConcurrent) {
|
|
134
|
+
return { ok: false, error: `Maximum concurrent jobs (${maxConcurrent}) reached.` };
|
|
135
|
+
}
|
|
136
|
+
const job = {
|
|
137
|
+
name,
|
|
138
|
+
intervalMs,
|
|
139
|
+
action,
|
|
140
|
+
enabled,
|
|
141
|
+
lastRun: null,
|
|
142
|
+
nextRun: formatNextRun(intervalMs),
|
|
143
|
+
runCount: 0
|
|
144
|
+
};
|
|
145
|
+
state.jobs.set(name, job);
|
|
146
|
+
scheduleNextRun(name);
|
|
147
|
+
api.metrics.gauge("cron_active_jobs", state.jobs.size);
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
name,
|
|
151
|
+
intervalMs,
|
|
152
|
+
nextRun: job.nextRun,
|
|
153
|
+
message: `Scheduled '${name}' every ${intervalMs}ms.`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
api.tools.register({
|
|
158
|
+
name: "cron_list",
|
|
159
|
+
description: "List all registered cron jobs with their intervals, next run times, and execution counts.",
|
|
160
|
+
inputSchema: { type: "object", properties: {} },
|
|
161
|
+
permission: "auto",
|
|
162
|
+
mutating: false,
|
|
163
|
+
async execute() {
|
|
164
|
+
const jobs = Array.from(state.jobs.values()).map((j) => ({
|
|
165
|
+
name: j.name,
|
|
166
|
+
intervalMs: j.intervalMs,
|
|
167
|
+
action: j.action,
|
|
168
|
+
enabled: j.enabled,
|
|
169
|
+
lastRun: j.lastRun,
|
|
170
|
+
nextRun: j.nextRun,
|
|
171
|
+
runCount: j.runCount,
|
|
172
|
+
overdue: new Date(j.nextRun).getTime() < Date.now()
|
|
173
|
+
}));
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
count: jobs.length,
|
|
177
|
+
maxConcurrent,
|
|
178
|
+
jobs
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
api.tools.register({
|
|
183
|
+
name: "cron_cancel",
|
|
184
|
+
description: "Cancel and remove a cron job by name.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
name: { type: "string", description: "Name of the cron job to cancel" }
|
|
189
|
+
},
|
|
190
|
+
required: ["name"]
|
|
191
|
+
},
|
|
192
|
+
permission: "auto",
|
|
193
|
+
mutating: false,
|
|
194
|
+
async execute(input) {
|
|
195
|
+
const name = input["name"];
|
|
196
|
+
if (!state.jobs.has(name)) {
|
|
197
|
+
return { ok: false, error: `No cron job named '${name}'` };
|
|
198
|
+
}
|
|
199
|
+
cancelJob(name);
|
|
200
|
+
api.metrics.gauge("cron_active_jobs", state.jobs.size);
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
name,
|
|
204
|
+
message: `Cancelled cron job '${name}'.`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
api.log.info("cron plugin loaded", { version: "0.1.0", maxConcurrent });
|
|
209
|
+
},
|
|
210
|
+
teardown(api) {
|
|
211
|
+
const { jobs, timers } = api._state ?? { jobs: /* @__PURE__ */ new Map(), timers: /* @__PURE__ */ new Map() };
|
|
212
|
+
for (const name of jobs.keys()) {
|
|
213
|
+
const timer = timers.get(name);
|
|
214
|
+
if (timer) clearTimeout(timer);
|
|
215
|
+
}
|
|
216
|
+
jobs.clear();
|
|
217
|
+
timers.clear();
|
|
218
|
+
api.log.info("cron plugin unloaded");
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var cron_default = plugin;
|
|
222
|
+
|
|
223
|
+
export { cron_default as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* file-watcher plugin — Watches project files and triggers actions on changes.
|
|
5
|
+
*
|
|
6
|
+
* Tools registered:
|
|
7
|
+
* - watch_start: Start watching paths for file changes
|
|
8
|
+
* - watch_stop: Stop a watch by ID
|
|
9
|
+
* - watch_list: List all active watches
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
declare const plugin: Plugin;
|
|
13
|
+
|
|
14
|
+
export { plugin as default };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
|
|
3
|
+
// src/file-watcher/index.ts
|
|
4
|
+
var API_VERSION = "^0.1.10";
|
|
5
|
+
var watchIdCounter = 0;
|
|
6
|
+
function nextId() {
|
|
7
|
+
return `watch_${++watchIdCounter}_${Date.now().toString(36)}`;
|
|
8
|
+
}
|
|
9
|
+
var plugin = {
|
|
10
|
+
name: "file-watcher",
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
description: "Watches project files and emits events when changes occur (add, change, delete)",
|
|
13
|
+
apiVersion: API_VERSION,
|
|
14
|
+
capabilities: { tools: true },
|
|
15
|
+
defaultConfig: {
|
|
16
|
+
debounceMs: 500,
|
|
17
|
+
watchOnStartup: [],
|
|
18
|
+
autoUnwatchOnExit: true
|
|
19
|
+
},
|
|
20
|
+
configSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
debounceMs: { type: "number", default: 500 },
|
|
24
|
+
watchOnStartup: { type: "array", items: { type: "string" }, default: [] },
|
|
25
|
+
autoUnwatchOnExit: { type: "boolean", default: true }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
setup(api) {
|
|
29
|
+
const watches = /* @__PURE__ */ new Map();
|
|
30
|
+
let debounceMs = api.config.extensions?.["file-watcher"]?.["debounceMs"] ?? 500;
|
|
31
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
32
|
+
function debounceEvent(key, fn, ms) {
|
|
33
|
+
const existing = debounceTimers.get(key);
|
|
34
|
+
if (existing) clearTimeout(existing);
|
|
35
|
+
debounceTimers.set(key, setTimeout(() => {
|
|
36
|
+
debounceTimers.delete(key);
|
|
37
|
+
fn();
|
|
38
|
+
}, ms));
|
|
39
|
+
}
|
|
40
|
+
function safeWatchDir(dirPath, recursive, handle) {
|
|
41
|
+
try {
|
|
42
|
+
const watcher = watch(dirPath, { recursive }, (eventType, filename) => {
|
|
43
|
+
if (!filename) return;
|
|
44
|
+
const fullPath = `${dirPath}/${filename}`;
|
|
45
|
+
const key = `${handle.id}:${fullPath}:${eventType}`;
|
|
46
|
+
debounceEvent(key, () => {
|
|
47
|
+
api.emitCustom("file-watcher:changed", {
|
|
48
|
+
watchId: handle.id,
|
|
49
|
+
path: fullPath,
|
|
50
|
+
event: eventType,
|
|
51
|
+
filename,
|
|
52
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
53
|
+
});
|
|
54
|
+
api.metrics.counter("file_change", 1, { event: eventType ?? "unknown" });
|
|
55
|
+
api.log.debug(`file-watcher: ${eventType} ${fullPath} (watch=${handle.id})`);
|
|
56
|
+
}, debounceMs);
|
|
57
|
+
});
|
|
58
|
+
watcher.on("error", (err) => {
|
|
59
|
+
api.log.warn(`file-watcher: error on ${dirPath}: ${err}`);
|
|
60
|
+
});
|
|
61
|
+
handle.watcher = watcher;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
api.log.warn(`file-watcher: could not watch ${dirPath}: ${err}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
api.tools.register({
|
|
67
|
+
name: "watch_start",
|
|
68
|
+
description: "Start watching one or more file paths for changes (add, change, delete). Returns a watch ID for stopping the watch later.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
paths: {
|
|
73
|
+
type: "array",
|
|
74
|
+
items: { type: "string" },
|
|
75
|
+
description: "File or directory paths to watch"
|
|
76
|
+
},
|
|
77
|
+
events: {
|
|
78
|
+
type: "array",
|
|
79
|
+
items: { type: "string" },
|
|
80
|
+
default: ["change", "add", "delete"],
|
|
81
|
+
description: "Event types to watch for"
|
|
82
|
+
},
|
|
83
|
+
recursive: {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
default: true,
|
|
86
|
+
description: "Watch directories recursively"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: ["paths"]
|
|
90
|
+
},
|
|
91
|
+
permission: "confirm",
|
|
92
|
+
mutating: false,
|
|
93
|
+
async execute(input) {
|
|
94
|
+
const rawPaths = input["paths"];
|
|
95
|
+
if (!rawPaths || typeof rawPaths !== "object" || !Array.isArray(rawPaths)) {
|
|
96
|
+
return { ok: false, error: "paths must be an array of file/directory paths", watchId: null };
|
|
97
|
+
}
|
|
98
|
+
const paths = rawPaths;
|
|
99
|
+
if (paths.length === 0) {
|
|
100
|
+
return { ok: false, error: "paths array is empty \u2014 provide at least one path", watchId: null };
|
|
101
|
+
}
|
|
102
|
+
const events = input["events"] ?? ["change", "add", "delete"];
|
|
103
|
+
const recursive = input["recursive"] ?? true;
|
|
104
|
+
const id = nextId();
|
|
105
|
+
const handle = {
|
|
106
|
+
id,
|
|
107
|
+
paths,
|
|
108
|
+
recursive,
|
|
109
|
+
events,
|
|
110
|
+
watcher: { close: () => {
|
|
111
|
+
} },
|
|
112
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
113
|
+
};
|
|
114
|
+
for (const p of paths) {
|
|
115
|
+
safeWatchDir(p, recursive, handle);
|
|
116
|
+
}
|
|
117
|
+
watches.set(id, handle);
|
|
118
|
+
api.metrics.gauge("active_watches", watches.size);
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
watchId: id,
|
|
122
|
+
paths,
|
|
123
|
+
events,
|
|
124
|
+
recursive,
|
|
125
|
+
message: `Started watching ${paths.length} path(s). Use watch_stop to cancel.`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
api.tools.register({
|
|
130
|
+
name: "watch_stop",
|
|
131
|
+
description: "Stop a file watch by its ID. Releases all resources.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
watchId: { type: "string", description: "Watch ID returned by watch_start" }
|
|
136
|
+
},
|
|
137
|
+
required: ["watchId"]
|
|
138
|
+
},
|
|
139
|
+
permission: "auto",
|
|
140
|
+
mutating: false,
|
|
141
|
+
async execute(input) {
|
|
142
|
+
const watchId = input["watchId"];
|
|
143
|
+
const handle = watches.get(watchId);
|
|
144
|
+
if (!handle) {
|
|
145
|
+
return { ok: false, error: `No active watch with ID: ${watchId}` };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
handle.watcher.close();
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
watches.delete(watchId);
|
|
152
|
+
api.metrics.gauge("active_watches", watches.size);
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
watchId,
|
|
156
|
+
message: `Stopped watch ${watchId}. ${watches.size} watch(es) remaining.`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
api.tools.register({
|
|
161
|
+
name: "watch_list",
|
|
162
|
+
description: "List all currently active file watches with their IDs, paths, and creation times.",
|
|
163
|
+
inputSchema: { type: "object", properties: {} },
|
|
164
|
+
permission: "auto",
|
|
165
|
+
mutating: false,
|
|
166
|
+
async execute() {
|
|
167
|
+
const list = Array.from(watches.values()).map((w) => ({
|
|
168
|
+
id: w.id,
|
|
169
|
+
paths: w.paths,
|
|
170
|
+
events: w.events,
|
|
171
|
+
recursive: w.recursive,
|
|
172
|
+
createdAt: w.createdAt,
|
|
173
|
+
age: `${Date.now() - new Date(w.createdAt).getTime()}ms`
|
|
174
|
+
}));
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
count: list.length,
|
|
178
|
+
watches: list
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
api.log.info("file-watcher plugin loaded", { version: "0.1.0" });
|
|
183
|
+
},
|
|
184
|
+
teardown(api) {
|
|
185
|
+
{
|
|
186
|
+
api.log.info("file-watcher: teardown complete");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var file_watcher_default = plugin;
|
|
191
|
+
|
|
192
|
+
export { file_watcher_default as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* git-autocommit plugin — AI-powered git staging and commit message generation.
|
|
5
|
+
*
|
|
6
|
+
* Tools registered:
|
|
7
|
+
* - git_autocommit: Generate and create a commit with AI-written messages
|
|
8
|
+
* - git_stage: Stage specific files for commit
|
|
9
|
+
* - git_status_summary: Show a summary of current git status
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
declare const plugin: Plugin;
|
|
13
|
+
|
|
14
|
+
export { plugin as default };
|