superacli 1.1.20 → 1.1.21

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 (125) hide show
  1. package/cli/adapter-schema.js +1 -6
  2. package/cli/config.js +95 -0
  3. package/cli/executor.js +90 -11
  4. package/cli/server-command.js +395 -0
  5. package/cli/supercli.js +82 -1
  6. package/docs/adapters.md +159 -0
  7. package/docs/changelog-2026-04.html +15 -0
  8. package/docs/index.html +314 -98
  9. package/docs/meta-plugins.json +24424 -1247
  10. package/docs/plugins-examples.md +8 -8
  11. package/docs/plugins-how-to.md +0 -13
  12. package/docs/plugins.html +165 -61
  13. package/docs/server.md +50 -0
  14. package/graphify-out/GRAPH_REPORT.md +141 -136
  15. package/graphify-out/cache/1821936911d6eb3130499b6b45b8e6d760d6c05328131e115ac9825a5e3061cd.json +1 -0
  16. package/graphify-out/cache/20b0e1261b41286d639e7b039c8143682001981f607e26eb2a492a06f12c233a.json +1 -0
  17. package/graphify-out/cache/21dc011c08a0813ac33c0520c9cf131d8922e6007df7d9b51df740974fee5fb3.json +1 -0
  18. package/graphify-out/cache/22d3d43c24fdff310ab8445bd442fbcf4714872c6950f15b90777e5a8358dcd9.json +1 -0
  19. package/graphify-out/cache/2b8a3fbc23afe7bbc1a5ecb60d4074f39afdf6cb42120922176c280f4cc4ab4b.json +1 -0
  20. package/graphify-out/cache/37418b8b152d9c436d75361969c39bc74a76260ab7209ceabf455f80452f1654.json +1 -0
  21. package/graphify-out/cache/3b9aae57e4326f5862f8153c13b7d809a34c422996993e7b491c5ea3a66b5e04.json +1 -0
  22. package/graphify-out/cache/448f8e0c5e77cfb15fa625ed40ee0dc1622d9814d24905fe4c12c4d175131dc9.json +1 -0
  23. package/graphify-out/cache/47dd42009ceac28b7a7fec5586e565e7bc850c80be96b8329e1ed7c49efbe34f.json +1 -0
  24. package/graphify-out/cache/492712949d38efbe312c303fe717e5c93e1ecc8888e1ceb855750afc48caffdb.json +1 -0
  25. package/graphify-out/cache/494fd6af80b6a1bb7a2a5641c6ad11e2fbe2a67b5a2b3ff57fd04bb49ccad1c4.json +1 -0
  26. package/graphify-out/cache/4b31b0de656deff083119780df64d17205c45e72233987e1c495f2121c4923fd.json +1 -0
  27. package/graphify-out/cache/5a16fa8cdd687a39661321adb39186ce8cf924a79ef936e42b365380b5e3543e.json +1 -0
  28. package/graphify-out/cache/644fcb129e595cfdb4e3a9757ed9658230347f5ed0652be45a894528a2b6585b.json +1 -0
  29. package/graphify-out/cache/6b8762db2c5f51408cf49d915f3e44bcc25c7ebb32abc635307fdf2a672b370c.json +1 -0
  30. package/graphify-out/cache/6db88050060880e56dd26ec58f29e3b56a409ace48e31bc75f54cab7dc74e409.json +1 -0
  31. package/graphify-out/cache/7932d1b6a9751b2cb2f4dc6b4e5f9fd7dcc2cee82d7866ec3d655482010533c0.json +1 -0
  32. package/graphify-out/cache/8140a76ff5b70dfdd0d020aeffff27f87af39da5b237c35baa938fcc4b288ba5.json +1 -0
  33. package/graphify-out/cache/8876638878758733a412bd4ca4cbd22863322689fbba9f037d92abfa877ea3ab.json +1 -0
  34. package/graphify-out/cache/90445359c448f8135207ddcd378c8391b9274460064cf998f1ea2ad6f2173703.json +1 -0
  35. package/graphify-out/cache/b01f675bb3942d2e8f359d11c38d52f28487544f54cdef518c60b7d8dfd2b383.json +1 -0
  36. package/graphify-out/cache/bc353b40b348d54c730d8498af675f57998b14ae796b4f952cdc200147008f13.json +1 -0
  37. package/graphify-out/cache/c38b2e784b1c441121b422db0c79b4911fb525d2a3cfad10d3fd561a07900bd5.json +1 -0
  38. package/graphify-out/cache/ce9df9bb2d36ecf310a53fb34526168bdf25773edf0826c1f55accebfa3c012d.json +1 -0
  39. package/graphify-out/cache/ceaeb0f99851d313b29cb1440240db1cf835de425b9b0c1e58fa3035355c2b2b.json +1 -0
  40. package/graphify-out/cache/d8f3e78931a41cd0f367fdab1d11b634bbffb2c90280c75a80a1a53a6b2d6bc7.json +1 -0
  41. package/graphify-out/cache/f574e4ce3bcb1250ca88410519fb4fd26c5b6cd786039410880fbd6e606ab23f.json +1 -0
  42. package/graphify-out/cache/faa65636d5f779b8b1c790dde11cc6470b48125bf42a12a77651827fd1653fd7.json +1 -0
  43. package/graphify-out/graph.json +4173 -2115
  44. package/ideal-plugins.csv +124 -0
  45. package/package.json +3 -2
  46. package/plugin-scores.csv +999 -0
  47. package/plugins/beads/plugin.json +2 -2
  48. package/plugins/biome/install-guidance.json +11 -0
  49. package/plugins/biome/meta.json +13 -0
  50. package/plugins/biome/plugin.json +83 -0
  51. package/plugins/biome/skills/quickstart/SKILL.md +25 -0
  52. package/plugins/bore/install-guidance.json +11 -0
  53. package/plugins/bore/meta.json +11 -0
  54. package/plugins/bore/plugin.json +65 -0
  55. package/plugins/bore/skills/quickstart/SKILL.md +25 -0
  56. package/plugins/ccf-deadlines/install-guidance.json +11 -0
  57. package/plugins/ccf-deadlines/meta.json +11 -0
  58. package/plugins/ccf-deadlines/plugin.json +28 -0
  59. package/plugins/ccf-deadlines/skills/quickstart/SKILL.md +25 -0
  60. package/plugins/code2prompt/install-guidance.json +11 -0
  61. package/plugins/code2prompt/meta.json +12 -0
  62. package/plugins/code2prompt/plugin.json +46 -0
  63. package/plugins/code2prompt/skills/quickstart/SKILL.md +25 -0
  64. package/plugins/commiat/install-guidance.json +10 -0
  65. package/plugins/commiat/plugin.json +2 -2
  66. package/plugins/dua-cli/install-guidance.json +11 -0
  67. package/plugins/dua-cli/meta.json +11 -0
  68. package/plugins/dua-cli/plugin.json +57 -0
  69. package/plugins/dua-cli/skills/quickstart/SKILL.md +25 -0
  70. package/plugins/forever/install-guidance.json +11 -0
  71. package/plugins/forever/meta.json +11 -0
  72. package/plugins/forever/plugin.json +75 -0
  73. package/plugins/forever/skills/quickstart/SKILL.md +25 -0
  74. package/plugins/gitmoji-cli/install-guidance.json +11 -0
  75. package/plugins/gitmoji-cli/meta.json +11 -0
  76. package/plugins/gitmoji-cli/plugin.json +49 -0
  77. package/plugins/gitmoji-cli/skills/quickstart/SKILL.md +25 -0
  78. package/plugins/google-maps-scraper/plugin.json +1 -1
  79. package/plugins/gwc/install-guidance.json +10 -0
  80. package/plugins/gwc/plugin.json +2 -2
  81. package/plugins/quarto/install-guidance.json +11 -0
  82. package/plugins/quarto/meta.json +11 -0
  83. package/plugins/quarto/plugin.json +61 -0
  84. package/plugins/quarto/skills/quickstart/SKILL.md +25 -0
  85. package/plugins/readme-md-generator/install-guidance.json +11 -0
  86. package/plugins/readme-md-generator/meta.json +11 -0
  87. package/plugins/readme-md-generator/plugin.json +28 -0
  88. package/plugins/readme-md-generator/skills/quickstart/SKILL.md +25 -0
  89. package/plugins/twarc/install-guidance.json +11 -0
  90. package/plugins/twarc/meta.json +13 -0
  91. package/plugins/twarc/plugin.json +165 -0
  92. package/plugins/twarc/skills/quickstart/SKILL.md +61 -0
  93. package/plugins/vale/install-guidance.json +11 -0
  94. package/plugins/vale/meta.json +12 -0
  95. package/plugins/vale/plugin.json +50 -0
  96. package/plugins/vale/skills/quickstart/SKILL.md +25 -0
  97. package/plugins/volta/install-guidance.json +11 -0
  98. package/plugins/volta/meta.json +12 -0
  99. package/plugins/volta/plugin.json +75 -0
  100. package/plugins/volta/skills/quickstart/SKILL.md +25 -0
  101. package/plugins/wgcf/install-guidance.json +11 -0
  102. package/plugins/wgcf/meta.json +12 -0
  103. package/plugins/wgcf/plugin.json +45 -0
  104. package/plugins/wgcf/skills/quickstart/SKILL.md +25 -0
  105. package/scripts/analyze-plugins.js +130 -0
  106. package/scripts/enrich-meta-plugins.js +67 -0
  107. package/server/app.js +23 -1
  108. package/server/routes/adapters.js +356 -0
  109. package/server/routes/dashboard.js +113 -0
  110. package/server/routes/jobs.js +26 -0
  111. package/server/services/adaptersService.js +284 -0
  112. package/server/services/pluginsService.js +4 -0
  113. package/server/views/adapter-edit.ejs +226 -0
  114. package/server/views/adapter-packages.ejs +191 -0
  115. package/server/views/adapters.ejs +112 -0
  116. package/server/views/command-edit.ejs +48 -21
  117. package/server/views/commands.ejs +25 -22
  118. package/server/views/dashboard.ejs +196 -0
  119. package/server/views/layout.ejs +94 -14
  120. package/server/views/mcp.ejs +38 -35
  121. package/server/views/partials/head.ejs +88 -12
  122. package/server/views/plugins.ejs +9 -0
  123. package/server/views/specs.ejs +33 -30
  124. package/cli/adapters/builtin.js +0 -43
  125. package/index.html +0 -384
@@ -0,0 +1,356 @@
1
+ const { Router } = require("express")
2
+ const { NodeVM } = require("vm2")
3
+ const path = require("path")
4
+ const adaptersService = require("../services/adaptersService")
5
+
6
+ const router = Router()
7
+
8
+ function invalid(message) {
9
+ return Object.assign(new Error(message), {
10
+ code: 85,
11
+ type: "invalid_argument",
12
+ recoverable: false,
13
+ })
14
+ }
15
+
16
+ // UI: List adapters page
17
+ router.get("/", async (req, res, next) => {
18
+ try {
19
+ const adapters = await adaptersService.listAdapters()
20
+ if (req.headers.accept?.includes("application/json")) {
21
+ return res.json({ adapters })
22
+ }
23
+ res.render("adapters", { adapters })
24
+ } catch (err) {
25
+ next(err)
26
+ }
27
+ })
28
+
29
+ // UI: New adapter page
30
+ router.get("/new", (req, res) => {
31
+ res.render("adapter-edit", { adapter: null })
32
+ })
33
+
34
+ // UI: Edit adapter page
35
+ router.get("/:name/edit", async (req, res, next) => {
36
+ try {
37
+ const { name } = req.params
38
+ const adapter = await adaptersService.getAdapter(name)
39
+ if (!adapter) {
40
+ return res.status(404).render("error", { message: `Adapter '${name}' not found` })
41
+ }
42
+ const source = await adaptersService.getAdapterSource(name)
43
+ res.render("adapter-edit", { adapter: { ...adapter, source } })
44
+ } catch (err) {
45
+ next(err)
46
+ }
47
+ })
48
+
49
+ // UI: Adapter packages page
50
+ router.get("/:name/packages", async (req, res, next) => {
51
+ try {
52
+ const { name } = req.params
53
+ const adapter = await adaptersService.getAdapter(name)
54
+ if (!adapter) {
55
+ return res.status(404).render("error", { message: `Adapter '${name}' not found` })
56
+ }
57
+ const packages = await adaptersService.getAdapterPackages(name)
58
+ res.render("adapter-packages", { adapter, packages })
59
+ } catch (err) {
60
+ next(err)
61
+ }
62
+ })
63
+
64
+ // Get single adapter
65
+ router.get("/:name", async (req, res, next) => {
66
+ try {
67
+ const { name } = req.params
68
+ const adapter = await adaptersService.getAdapter(name)
69
+ if (!adapter) {
70
+ return res.status(404).json({ error: `Adapter '${name}' not found` })
71
+ }
72
+ res.json({ adapter })
73
+ } catch (err) {
74
+ next(err)
75
+ }
76
+ })
77
+
78
+ // Get adapter source code
79
+ router.get("/:name/source", async (req, res, next) => {
80
+ try {
81
+ const { name } = req.params
82
+ const source = await adaptersService.getAdapterSource(name)
83
+ if (!source) {
84
+ return res.status(404).json({ error: `Adapter '${name}' not found` })
85
+ }
86
+ res.json({ source })
87
+ } catch (err) {
88
+ next(err)
89
+ }
90
+ })
91
+
92
+ // Create adapter
93
+ router.post("/", async (req, res, next) => {
94
+ try {
95
+ const { name, description, execution_context, source, dependencies, timeout_ms, memory_limit_mb, allow_network } = req.body
96
+
97
+ if (!name || !source) {
98
+ throw invalid("name and source are required")
99
+ }
100
+
101
+ const adapter = await adaptersService.createAdapter({
102
+ name,
103
+ description,
104
+ execution_context,
105
+ source,
106
+ dependencies,
107
+ timeout_ms,
108
+ memory_limit_mb,
109
+ allow_network,
110
+ })
111
+
112
+ res.status(201).json({ adapter })
113
+ } catch (err) {
114
+ next(err)
115
+ }
116
+ })
117
+
118
+ // Update adapter
119
+ router.put("/:name", async (req, res, next) => {
120
+ try {
121
+ const { name } = req.params
122
+ const { description, execution_context, source, dependencies, timeout_ms, memory_limit_mb, allow_network } = req.body
123
+
124
+ const adapter = await adaptersService.updateAdapter(name, {
125
+ description,
126
+ execution_context,
127
+ source,
128
+ dependencies,
129
+ timeout_ms,
130
+ memory_limit_mb,
131
+ allow_network,
132
+ })
133
+
134
+ res.json({ adapter })
135
+ } catch (err) {
136
+ next(err)
137
+ }
138
+ })
139
+
140
+ // Delete adapter
141
+ router.delete("/:name", async (req, res, next) => {
142
+ try {
143
+ const { name } = req.params
144
+ const deleted = await adaptersService.deleteAdapter(name)
145
+ if (!deleted) {
146
+ return res.status(404).json({ error: `Adapter '${name}' not found` })
147
+ }
148
+ res.json({ ok: true, message: `Adapter '${name}' deleted` })
149
+ } catch (err) {
150
+ next(err)
151
+ }
152
+ })
153
+
154
+ // Get adapter packages
155
+ router.get("/:name/packages", async (req, res, next) => {
156
+ try {
157
+ const { name } = req.params
158
+ const packages = await adaptersService.getAdapterPackages(name)
159
+ res.json({ packages })
160
+ } catch (err) {
161
+ next(err)
162
+ }
163
+ })
164
+
165
+ // Add package to adapter
166
+ router.post("/:name/packages", async (req, res, next) => {
167
+ try {
168
+ const { name } = req.params
169
+ const { package: packageName, version, packages } = req.body
170
+
171
+ // Handle prune all (set packages array)
172
+ if (packages !== undefined) {
173
+ const result = await adaptersService.setAdapterPackages(name, packages)
174
+ res.json({ packages: result })
175
+ return
176
+ }
177
+
178
+ // Handle add single package
179
+ if (!packageName) {
180
+ throw invalid("package is required")
181
+ }
182
+
183
+ const result = await adaptersService.addAdapterPackage(name, packageName, version)
184
+ res.json({ packages: result })
185
+ } catch (err) {
186
+ next(err)
187
+ }
188
+ })
189
+
190
+ // Remove package from adapter
191
+ router.delete("/:name/packages/:package", async (req, res, next) => {
192
+ try {
193
+ const { name, package: packageName } = req.params
194
+ const packages = await adaptersService.removeAdapterPackage(name, packageName)
195
+ res.json({ packages })
196
+ } catch (err) {
197
+ next(err)
198
+ }
199
+ })
200
+
201
+ // Test run adapter (server context only)
202
+ router.post("/:name/test", async (req, res, next) => {
203
+ try {
204
+ const { name } = req.params
205
+ const { flags = {}, context = {} } = req.body
206
+
207
+ const adapter = await adaptersService.getAdapter(name)
208
+ if (!adapter) {
209
+ return res.status(404).json({ error: `Adapter '${name}' not found` })
210
+ }
211
+
212
+ if (adapter.execution_context === "cli") {
213
+ return res.status(400).json({
214
+ error: "Cannot test CLI-context adapter on server. Test locally using the CLI."
215
+ })
216
+ }
217
+
218
+ const source = await adaptersService.getAdapterSource(name)
219
+ if (!source) {
220
+ return res.status(404).json({ error: `Adapter '${name}' source not found` })
221
+ }
222
+
223
+ const startTime = Date.now()
224
+
225
+ try {
226
+ const vm = new NodeVM({
227
+ timeout: adapter.timeout_ms,
228
+ sandbox: {
229
+ console: {
230
+ log: (...args) => {},
231
+ error: (...args) => {},
232
+ warn: (...args) => {},
233
+ },
234
+ },
235
+ require: {
236
+ external: adapter.allow_network,
237
+ root: process.cwd(),
238
+ },
239
+ memoryLimit: adapter.memory_limit_mb * 1024 * 1024,
240
+ })
241
+
242
+ const script = `
243
+ ${source}
244
+ module.exports = { execute }
245
+ `
246
+
247
+ const fn = vm.run(script, "adapter.js")
248
+
249
+ if (typeof fn.execute !== "function") {
250
+ throw new Error("Adapter must export an 'execute' function")
251
+ }
252
+
253
+ const mockCmd = {
254
+ namespace: "test",
255
+ resource: "test",
256
+ action: "test",
257
+ adapter: name,
258
+ adapterConfig: {},
259
+ args: [],
260
+ }
261
+
262
+ const result = await fn.execute(mockCmd, flags, context)
263
+
264
+ const duration = Date.now() - startTime
265
+
266
+ res.json({
267
+ ok: true,
268
+ result,
269
+ duration_ms: duration,
270
+ message: "Adapter executed successfully",
271
+ })
272
+ } catch (execErr) {
273
+ const duration = Date.now() - startTime
274
+ res.json({
275
+ ok: false,
276
+ error: execErr.message,
277
+ duration_ms: duration,
278
+ message: "Adapter execution failed",
279
+ })
280
+ }
281
+ } catch (err) {
282
+ next(err)
283
+ }
284
+ })
285
+
286
+ // Execute adapter on server (for CLI delegation)
287
+ router.post("/execute", async (req, res, next) => {
288
+ try {
289
+ const { cmd, flags = {}, context = {} } = req.body
290
+
291
+ if (!cmd || !cmd.adapter) {
292
+ throw invalid("cmd.adapter is required")
293
+ }
294
+
295
+ const adapter = await adaptersService.getAdapter(cmd.adapter)
296
+ if (!adapter) {
297
+ return res.status(404).json({ error: `Adapter '${cmd.adapter}' not found` })
298
+ }
299
+
300
+ if (adapter.execution_context === "cli") {
301
+ return res.status(400).json({
302
+ error: "CLI-context adapters must be executed locally"
303
+ })
304
+ }
305
+
306
+ const source = await adaptersService.getAdapterSource(cmd.adapter)
307
+ if (!source) {
308
+ return res.status(404).json({ error: `Adapter '${cmd.adapter}' source not found` })
309
+ }
310
+
311
+ const adapterDir = path.join(process.cwd(), "supercli_storage", "adapters", cmd.adapter)
312
+ const adapterNodeModules = path.join(adapterDir, "node_modules")
313
+
314
+ const vm = new NodeVM({
315
+ timeout: adapter.timeout_ms,
316
+ sandbox: {
317
+ console: {
318
+ log: (...args) => {},
319
+ error: (...args) => {},
320
+ warn: (...args) => {},
321
+ },
322
+ },
323
+ require: {
324
+ external: true,
325
+ root: [adapterDir, adapterNodeModules],
326
+ builtin: ["*"],
327
+ resolve: (moduleName) => {
328
+ try {
329
+ return require.resolve(moduleName, { paths: [adapterNodeModules] })
330
+ } catch {
331
+ return require.resolve(moduleName)
332
+ }
333
+ }
334
+ },
335
+ memoryLimit: adapter.memory_limit_mb * 1024 * 1024,
336
+ })
337
+
338
+ const script = `
339
+ ${source}
340
+ module.exports = { execute }
341
+ `
342
+
343
+ const fn = vm.run(script, "adapter.js")
344
+
345
+ if (typeof fn.execute !== "function") {
346
+ throw invalid("Adapter must export an 'execute' function")
347
+ }
348
+
349
+ const result = await fn.execute(cmd, flags, context)
350
+ res.json(result)
351
+ } catch (err) {
352
+ next(err)
353
+ }
354
+ })
355
+
356
+ module.exports = router
@@ -0,0 +1,113 @@
1
+ const { Router } = require("express")
2
+ const { getStorage } = require("../storage/adapter")
3
+ const { listServerPlugins } = require("../services/pluginsService")
4
+
5
+ const router = Router()
6
+
7
+ async function getDashboardData() {
8
+ const storage = getStorage()
9
+
10
+ const [commandKeys, mcpKeys, specKeys, jobKeys] = await Promise.all([
11
+ storage.listKeys("command:"),
12
+ storage.listKeys("mcp:"),
13
+ storage.listKeys("spec:"),
14
+ storage.listKeys("job:"),
15
+ ])
16
+
17
+ const [commands, mcpServers, specs, allJobs] = await Promise.all([
18
+ Promise.all(commandKeys.map(k => storage.get(k))),
19
+ Promise.all(mcpKeys.map(k => storage.get(k))),
20
+ Promise.all(specKeys.map(k => storage.get(k))),
21
+ Promise.all(jobKeys.map(k => storage.get(k))),
22
+ ])
23
+
24
+ let plugins = []
25
+ try {
26
+ plugins = await listServerPlugins()
27
+ } catch (_) {}
28
+
29
+ const validJobs = allJobs.filter(Boolean)
30
+ const totalJobs = validJobs.length
31
+ const successJobs = validJobs.filter(j => j.status === "success").length
32
+ const failedJobs = validJobs.filter(j => j.status === "failed").length
33
+ const successRate = totalJobs > 0 ? ((successJobs / totalJobs) * 100).toFixed(1) : "0.0"
34
+ const failureRate = totalJobs > 0 ? ((failedJobs / totalJobs) * 100).toFixed(1) : "0.0"
35
+
36
+ const avgDuration =
37
+ totalJobs > 0
38
+ ? Math.round(validJobs.reduce((sum, j) => sum + (j.duration_ms || 0), 0) / totalJobs)
39
+ : 0
40
+
41
+ const commandStats = {}
42
+ for (const job of validJobs) {
43
+ if (!job.command) continue
44
+ if (!commandStats[job.command]) commandStats[job.command] = { count: 0, sum_ms: 0 }
45
+ commandStats[job.command].count++
46
+ commandStats[job.command].sum_ms += job.duration_ms || 0
47
+ }
48
+
49
+ const topCommands = Object.entries(commandStats)
50
+ .map(([cmd, stats]) => ({
51
+ command: cmd,
52
+ count: stats.count,
53
+ avg_ms: Math.round(stats.sum_ms / stats.count) || 0,
54
+ }))
55
+ .sort((a, b) => b.count - a.count)
56
+ .slice(0, 5)
57
+
58
+ const recentJobs = validJobs
59
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
60
+ .slice(0, 10)
61
+
62
+ // 7-day job trend
63
+ const now = Date.now()
64
+ const dayMs = 86400000
65
+ const trend = []
66
+ for (let i = 6; i >= 0; i--) {
67
+ const dayStart = now - i * dayMs
68
+ const dayEnd = dayStart + dayMs
69
+ const label = new Date(dayStart).toLocaleDateString("en-US", { weekday: "short" })
70
+ const dayJobs = validJobs.filter(j => {
71
+ const t = new Date(j.timestamp).getTime()
72
+ return t >= dayStart && t < dayEnd
73
+ })
74
+ trend.push({
75
+ label,
76
+ total: dayJobs.length,
77
+ success: dayJobs.filter(j => j.status === "success").length,
78
+ failed: dayJobs.filter(j => j.status === "failed").length,
79
+ })
80
+ }
81
+
82
+ return {
83
+ counts: {
84
+ commands: commands.filter(Boolean).length,
85
+ mcpServers: mcpServers.filter(Boolean).length,
86
+ specs: specs.filter(Boolean).length,
87
+ plugins: plugins.length,
88
+ pluginsEnabled: plugins.filter(p => p.enabled !== false).length,
89
+ },
90
+ jobs: {
91
+ total: totalJobs,
92
+ success: successJobs,
93
+ failed: failedJobs,
94
+ successRate,
95
+ failureRate,
96
+ avgDuration,
97
+ },
98
+ topCommands,
99
+ recentJobs,
100
+ trend,
101
+ }
102
+ }
103
+
104
+ router.get("/", async (req, res) => {
105
+ try {
106
+ const data = await getDashboardData()
107
+ res.render("dashboard", data)
108
+ } catch (err) {
109
+ res.status(500).send(err.message)
110
+ }
111
+ })
112
+
113
+ module.exports = router
@@ -119,4 +119,30 @@ router.get("/:id", async (req, res) => {
119
119
  }
120
120
  })
121
121
 
122
+ // DELETE /api/jobs — prune old jobs (admin mode)
123
+ router.delete("/", async (req, res) => {
124
+ try {
125
+ const storage = getStorage()
126
+ const olderThanDays = parseInt(req.query.older_than) || 7
127
+ const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000)
128
+
129
+ const keys = await storage.listKeys("job:")
130
+ const jobs = await Promise.all(keys.map(k => storage.get(k)))
131
+
132
+ let pruned = 0
133
+ for (const job of jobs) {
134
+ if (!job || !job.timestamp) continue
135
+ const jobDate = new Date(job.timestamp)
136
+ if (jobDate < cutoff) {
137
+ await storage.delete(job._id)
138
+ pruned++
139
+ }
140
+ }
141
+
142
+ res.json({ ok: true, pruned, older_than_days: olderThanDays })
143
+ } catch (err) {
144
+ res.status(500).json({ error: err.message })
145
+ }
146
+ })
147
+
122
148
  module.exports = router