superacli 1.0.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.
Files changed (69) hide show
  1. package/.env.example +14 -0
  2. package/README.md +173 -0
  3. package/cli/adapters/http.js +72 -0
  4. package/cli/adapters/mcp.js +193 -0
  5. package/cli/adapters/openapi.js +160 -0
  6. package/cli/ask.js +208 -0
  7. package/cli/config.js +133 -0
  8. package/cli/executor.js +117 -0
  9. package/cli/help-json.js +46 -0
  10. package/cli/mcp-local.js +72 -0
  11. package/cli/plan-runtime.js +32 -0
  12. package/cli/planner.js +67 -0
  13. package/cli/skills.js +240 -0
  14. package/cli/supercli.js +704 -0
  15. package/docs/features/adapters.md +25 -0
  16. package/docs/features/agent-friendly.md +28 -0
  17. package/docs/features/ask.md +32 -0
  18. package/docs/features/config-sync.md +22 -0
  19. package/docs/features/execution-plans.md +25 -0
  20. package/docs/features/observability.md +22 -0
  21. package/docs/features/skills.md +25 -0
  22. package/docs/features/storage.md +25 -0
  23. package/docs/features/workflows.md +33 -0
  24. package/docs/initial/AGENTS_FRIENDLY_TOOLS.md +553 -0
  25. package/docs/initial/agent-friendly.md +447 -0
  26. package/docs/initial/architecture.md +436 -0
  27. package/docs/initial/built-in-mcp-server.md +64 -0
  28. package/docs/initial/command-plan.md +532 -0
  29. package/docs/initial/core-features-2.md +428 -0
  30. package/docs/initial/core-features.md +366 -0
  31. package/docs/initial/dag.md +20 -0
  32. package/docs/initial/description.txt +9 -0
  33. package/docs/initial/idea.txt +564 -0
  34. package/docs/initial/initial-spec-details.md +726 -0
  35. package/docs/initial/initial-spec.md +731 -0
  36. package/docs/initial/mcp-local-mode.md +53 -0
  37. package/docs/initial/mcp-sse-mode.md +54 -0
  38. package/docs/initial/skills-support.md +246 -0
  39. package/docs/initial/storage-adapter-example.md +155 -0
  40. package/docs/initial/supercli-vs-gwc.md +109 -0
  41. package/examples/mcp-sse/install-demo.js +86 -0
  42. package/examples/mcp-sse/server.js +81 -0
  43. package/examples/mcp-stdio/install-demo.js +78 -0
  44. package/examples/mcp-stdio/server.js +50 -0
  45. package/package.json +21 -0
  46. package/server/app.js +59 -0
  47. package/server/public/app.js +18 -0
  48. package/server/routes/ask.js +92 -0
  49. package/server/routes/commands.js +126 -0
  50. package/server/routes/config.js +58 -0
  51. package/server/routes/jobs.js +122 -0
  52. package/server/routes/mcp.js +79 -0
  53. package/server/routes/plans.js +134 -0
  54. package/server/routes/specs.js +79 -0
  55. package/server/services/configService.js +88 -0
  56. package/server/storage/adapter.js +32 -0
  57. package/server/storage/file.js +64 -0
  58. package/server/storage/mongo.js +55 -0
  59. package/server/views/command-edit.ejs +110 -0
  60. package/server/views/commands.ejs +49 -0
  61. package/server/views/jobs.ejs +72 -0
  62. package/server/views/layout.ejs +42 -0
  63. package/server/views/mcp.ejs +80 -0
  64. package/server/views/partials/foot.ejs +5 -0
  65. package/server/views/partials/head.ejs +27 -0
  66. package/server/views/specs.ejs +91 -0
  67. package/tests/test-cli.js +367 -0
  68. package/tests/test-mcp.js +189 -0
  69. package/tests/test-openapi.js +101 -0
@@ -0,0 +1,122 @@
1
+ const { Router } = require("express")
2
+ const { getStorage } = require("../storage/adapter")
3
+
4
+ const router = Router()
5
+
6
+ // POST /api/jobs — record a command execution job
7
+ router.post("/", async (req, res) => {
8
+ try {
9
+ const storage = getStorage()
10
+ const { command, args, status, duration_ms, timestamp, plan_id, error } = req.body
11
+
12
+ // Generate a unique sequential-ish ID
13
+ const jobId = `job:${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
14
+
15
+ const doc = {
16
+ _id: jobId,
17
+ command,
18
+ args: args || {},
19
+ status: status || "unknown",
20
+ duration_ms: duration_ms || 0,
21
+ plan_id: plan_id || null,
22
+ error: error || null,
23
+ timestamp: timestamp || new Date().toISOString()
24
+ }
25
+
26
+ await storage.set(jobId, doc)
27
+ res.status(201).json(doc)
28
+ } catch (err) {
29
+ res.status(500).json({ error: err.message })
30
+ }
31
+ })
32
+
33
+ // GET /api/jobs — list recent jobs
34
+ router.get("/", async (req, res) => {
35
+ try {
36
+ const storage = getStorage()
37
+ const limit = parseInt(req.query.limit) || 50
38
+ const commandQuery = req.query.command
39
+
40
+ const keys = await storage.listKeys("job:")
41
+ let jobs = await Promise.all(keys.map(k => storage.get(k)))
42
+
43
+ jobs = jobs.filter(j => !!j)
44
+ if (commandQuery) {
45
+ jobs = jobs.filter(j => j.command === commandQuery)
46
+ }
47
+
48
+ // Sort descending by timestamp
49
+ jobs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
50
+ jobs = jobs.slice(0, limit)
51
+
52
+ // If HTML request, render
53
+ if (req.query.format !== "json" && req.accepts("html") && !req.xhr && !req.headers["x-requested-with"]) {
54
+ return res.render("jobs", { jobs })
55
+ }
56
+
57
+ res.json(jobs)
58
+ } catch (err) {
59
+ res.status(500).json({ error: err.message })
60
+ }
61
+ })
62
+
63
+ // GET /api/jobs/stats — aggregate stats
64
+ router.get("/stats", async (req, res) => {
65
+ try {
66
+ const storage = getStorage()
67
+ const keys = await storage.listKeys("job:")
68
+ const jobs = await Promise.all(keys.map(k => storage.get(k)))
69
+
70
+ const total = jobs.length
71
+ let success = 0
72
+ let failed = 0
73
+ const commandStats = {}
74
+
75
+ for (const job of jobs) {
76
+ if (!job) continue
77
+ if (job.status === "success") success++
78
+ if (job.status === "failed") failed++
79
+
80
+ const cmd = job.command
81
+ if (!commandStats[cmd]) {
82
+ commandStats[cmd] = { count: 0, sum_ms: 0 }
83
+ }
84
+ commandStats[cmd].count++
85
+ commandStats[cmd].sum_ms += job.duration_ms
86
+ }
87
+
88
+ const topCommands = Object.entries(commandStats)
89
+ .map(([cmd, stats]) => ({
90
+ command: cmd,
91
+ count: stats.count,
92
+ avg_ms: Math.round(stats.sum_ms / stats.count) || 0
93
+ }))
94
+ .sort((a, b) => b.count - a.count)
95
+ .slice(0, 10)
96
+
97
+ res.json({
98
+ total,
99
+ success,
100
+ failed,
101
+ failure_rate: total > 0 ? (failed / total * 100).toFixed(1) + "%" : "0%",
102
+ top_commands: topCommands
103
+ })
104
+ } catch (err) {
105
+ res.status(500).json({ error: err.message })
106
+ }
107
+ })
108
+
109
+ // GET /api/jobs/:id — get job details
110
+ router.get("/:id", async (req, res) => {
111
+ try {
112
+ const storage = getStorage()
113
+ const id = decodeURIComponent(req.params.id)
114
+ const job = await storage.get(id.startsWith("job:") ? id : `job:${id}`)
115
+ if (!job) return res.status(404).json({ error: "Job not found" })
116
+ res.json(job)
117
+ } catch (err) {
118
+ res.status(500).json({ error: err.message })
119
+ }
120
+ })
121
+
122
+ module.exports = router
@@ -0,0 +1,79 @@
1
+ const { Router } = require("express")
2
+ const { getStorage } = require("../storage/adapter")
3
+ const { bumpVersion } = require("../services/configService")
4
+
5
+ const router = Router()
6
+
7
+ async function getAllMCPs() {
8
+ const storage = getStorage()
9
+ const keys = await storage.listKeys("mcp:")
10
+ const servers = await Promise.all(keys.map(k => storage.get(k)))
11
+ return servers.sort((a, b) => a.name.localeCompare(b.name))
12
+ }
13
+
14
+ // GET /api/mcp
15
+ router.get("/", async (req, res) => {
16
+ try {
17
+ const servers = await getAllMCPs()
18
+ if (req.query.format !== "json" && req.accepts("html") && !req.xhr && !req.headers["x-requested-with"]) {
19
+ return res.render("mcp", { servers })
20
+ }
21
+ res.json(servers)
22
+ } catch (err) {
23
+ res.status(500).json({ error: err.message })
24
+ }
25
+ })
26
+
27
+ // POST /api/mcp
28
+ router.post("/", async (req, res) => {
29
+ try {
30
+ const storage = getStorage()
31
+ const { name, url } = req.body
32
+ const key = `mcp:${name}`
33
+ const doc = { _id: key, name, url, createdAt: new Date() }
34
+ await storage.set(key, doc)
35
+ await bumpVersion()
36
+ if (req.headers["content-type"]?.includes("urlencoded")) {
37
+ return res.redirect("/api/mcp")
38
+ }
39
+ res.status(201).json(doc)
40
+ } catch (err) {
41
+ res.status(500).json({ error: err.message })
42
+ }
43
+ })
44
+
45
+ // PUT /api/mcp/:id
46
+ router.put("/:id", async (req, res) => {
47
+ try {
48
+ const storage = getStorage()
49
+ const id = decodeURIComponent(req.params.id)
50
+ const { name, url } = req.body
51
+
52
+ const newKey = `mcp:${name}`
53
+ if (newKey !== id) {
54
+ await storage.delete(id)
55
+ }
56
+
57
+ const doc = { _id: newKey, name, url }
58
+ await storage.set(newKey, doc)
59
+ await bumpVersion()
60
+ res.json({ ok: true })
61
+ } catch (err) {
62
+ res.status(500).json({ error: err.message })
63
+ }
64
+ })
65
+
66
+ // DELETE /api/mcp/:id
67
+ router.delete("/:id", async (req, res) => {
68
+ try {
69
+ const storage = getStorage()
70
+ const id = decodeURIComponent(req.params.id)
71
+ await storage.delete(id)
72
+ await bumpVersion()
73
+ res.json({ ok: true })
74
+ } catch (err) {
75
+ res.status(500).json({ error: err.message })
76
+ }
77
+ })
78
+
79
+ module.exports = router
@@ -0,0 +1,134 @@
1
+ const { Router } = require("express")
2
+ const { getStorage } = require("../storage/adapter")
3
+ const { createPlan } = require("../../cli/planner")
4
+
5
+ const router = Router()
6
+
7
+ // POST /api/plans — create a plan
8
+ router.post("/", async (req, res) => {
9
+ try {
10
+ const storage = getStorage()
11
+ const { command, args, cmd } = req.body
12
+
13
+ let planCmd = cmd
14
+ if (!planCmd) {
15
+ // Resolve command from DB
16
+ const [namespace, resource, action] = command.split(".")
17
+ planCmd = await storage.get(`command:${namespace}.${resource}.${action}`)
18
+ if (!planCmd) return res.status(404).json({ error: "Command not found" })
19
+ }
20
+
21
+ const plan = createPlan(planCmd, args || {})
22
+
23
+ // Auto-expiry for file storage logic could go here, but for now we just store it
24
+ await storage.set(`plan:${plan.plan_id}`, plan)
25
+ res.status(201).json(plan)
26
+ } catch (err) {
27
+ res.status(500).json({ error: err.message })
28
+ }
29
+ })
30
+
31
+ // GET /api/plans — list recent plans
32
+ router.get("/", async (req, res) => {
33
+ try {
34
+ const storage = getStorage()
35
+ const keys = await storage.listKeys("plan:")
36
+ const plans = await Promise.all(keys.map(k => storage.get(k)))
37
+
38
+ // Sort descending by created_at and limit to 50
39
+ const sorted = plans
40
+ .filter(p => !!p)
41
+ .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
42
+ .slice(0, 50)
43
+
44
+ res.json(sorted)
45
+ } catch (err) {
46
+ res.status(500).json({ error: err.message })
47
+ }
48
+ })
49
+
50
+ // GET /api/plans/:id — inspect plan
51
+ router.get("/:id", async (req, res) => {
52
+ try {
53
+ const storage = getStorage()
54
+ const id = decodeURIComponent(req.params.id)
55
+ const plan = await storage.get(`plan:${id}`)
56
+ if (!plan) return res.status(404).json({ error: "Plan not found" })
57
+ res.json(plan)
58
+ } catch (err) {
59
+ res.status(500).json({ error: err.message })
60
+ }
61
+ })
62
+
63
+ // POST /api/plans/:id/execute — execute a stored plan
64
+ router.post("/:id/execute", async (req, res) => {
65
+ try {
66
+ const storage = getStorage()
67
+ const id = decodeURIComponent(req.params.id)
68
+ const plan = await storage.get(`plan:${id}`)
69
+ if (!plan) return res.status(404).json({ error: "Plan not found" })
70
+ if (plan.status !== "planned") {
71
+ return res.status(400).json({ error: `Plan status is '${plan.status}', expected 'planned'` })
72
+ }
73
+
74
+ // Resolve command
75
+ const [namespace, resource, action] = plan.command.split(".")
76
+ const cmd = await storage.get(`command:${namespace}.${resource}.${action}`)
77
+ if (!cmd) return res.status(404).json({ error: "Command no longer exists" })
78
+
79
+ // Execute via adapter
80
+ const { execute } = require("../../cli/executor")
81
+ const start = Date.now()
82
+ let result, status = "success"
83
+ try {
84
+ result = await execute(cmd, plan.args || {}, { server: `http://localhost:${process.env.PORT || 3000}` })
85
+ } catch (err) {
86
+ result = { error: err.message }
87
+ status = "failed"
88
+ }
89
+ const duration = Date.now() - start
90
+
91
+ // Update plan status
92
+ plan.status = status
93
+ plan.executed_at = new Date().toISOString()
94
+ plan.duration_ms = duration
95
+ await storage.set(`plan:${id}`, plan)
96
+
97
+ // Record job
98
+ const jobId = `job:${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
99
+ await storage.set(jobId, {
100
+ _id: jobId,
101
+ command: plan.command,
102
+ plan_id: plan.plan_id,
103
+ args: plan.args,
104
+ status,
105
+ duration_ms: duration,
106
+ timestamp: new Date().toISOString()
107
+ })
108
+
109
+ res.json({
110
+ version: "1.0",
111
+ plan_id: plan.plan_id,
112
+ command: plan.command,
113
+ status,
114
+ duration_ms: duration,
115
+ data: result
116
+ })
117
+ } catch (err) {
118
+ res.status(500).json({ error: err.message })
119
+ }
120
+ })
121
+
122
+ // DELETE /api/plans/:id — cancel plan
123
+ router.delete("/:id", async (req, res) => {
124
+ try {
125
+ const storage = getStorage()
126
+ const id = decodeURIComponent(req.params.id)
127
+ await storage.delete(`plan:${id}`)
128
+ res.json({ ok: true })
129
+ } catch (err) {
130
+ res.status(500).json({ error: err.message })
131
+ }
132
+ })
133
+
134
+ module.exports = router
@@ -0,0 +1,79 @@
1
+ const { Router } = require("express")
2
+ const { getStorage } = require("../storage/adapter")
3
+ const { bumpVersion } = require("../services/configService")
4
+
5
+ const router = Router()
6
+
7
+ async function getAllSpecs() {
8
+ const storage = getStorage()
9
+ const keys = await storage.listKeys("spec:")
10
+ const specs = await Promise.all(keys.map(k => storage.get(k)))
11
+ return specs.sort((a, b) => a.name.localeCompare(b.name))
12
+ }
13
+
14
+ // GET /api/specs
15
+ router.get("/", async (req, res) => {
16
+ try {
17
+ const specs = await getAllSpecs()
18
+ if (req.query.format !== "json" && req.accepts("html") && !req.xhr && !req.headers["x-requested-with"]) {
19
+ return res.render("specs", { specs })
20
+ }
21
+ res.json(specs)
22
+ } catch (err) {
23
+ res.status(500).json({ error: err.message })
24
+ }
25
+ })
26
+
27
+ // POST /api/specs
28
+ router.post("/", async (req, res) => {
29
+ try {
30
+ const storage = getStorage()
31
+ const { name, url, auth } = req.body
32
+ const key = `spec:${name}`
33
+ const doc = { _id: key, name, url, auth: auth || "none", createdAt: new Date() }
34
+ await storage.set(key, doc)
35
+ await bumpVersion()
36
+ if (req.headers["content-type"]?.includes("urlencoded")) {
37
+ return res.redirect("/api/specs")
38
+ }
39
+ res.status(201).json(doc)
40
+ } catch (err) {
41
+ res.status(500).json({ error: err.message })
42
+ }
43
+ })
44
+
45
+ // PUT /api/specs/:id
46
+ router.put("/:id", async (req, res) => {
47
+ try {
48
+ const storage = getStorage()
49
+ const id = decodeURIComponent(req.params.id)
50
+ const { name, url, auth } = req.body
51
+
52
+ const newKey = `spec:${name}`
53
+ if (newKey !== id) {
54
+ await storage.delete(id)
55
+ }
56
+
57
+ const doc = { _id: newKey, name, url, auth: auth || "none" }
58
+ await storage.set(newKey, doc)
59
+ await bumpVersion()
60
+ res.json({ ok: true })
61
+ } catch (err) {
62
+ res.status(500).json({ error: err.message })
63
+ }
64
+ })
65
+
66
+ // DELETE /api/specs/:id
67
+ router.delete("/:id", async (req, res) => {
68
+ try {
69
+ const storage = getStorage()
70
+ const id = decodeURIComponent(req.params.id)
71
+ await storage.delete(id)
72
+ await bumpVersion()
73
+ res.json({ ok: true })
74
+ } catch (err) {
75
+ res.status(500).json({ error: err.message })
76
+ }
77
+ })
78
+
79
+ module.exports = router
@@ -0,0 +1,88 @@
1
+ const { getStorage } = require("../storage/adapter")
2
+
3
+ async function getCLIConfig() {
4
+ const storage = getStorage()
5
+ const keys = await storage.listKeys("command:")
6
+ const commands = await Promise.all(keys.map(k => storage.get(k)))
7
+ const mcpKeys = await storage.listKeys("mcp:")
8
+ const mcpServers = await Promise.all(mcpKeys.map(k => storage.get(k)))
9
+ const specKeys = await storage.listKeys("spec:")
10
+ const specs = await Promise.all(specKeys.map(k => storage.get(k)))
11
+ const version = await storage.get("settings:config_version")
12
+
13
+ return {
14
+ version: version || "1",
15
+ ttl: 3600,
16
+ mcp_servers: mcpServers
17
+ .filter(Boolean)
18
+ .map(s => ({ name: s.name, url: s.url })),
19
+ specs: specs
20
+ .filter(Boolean)
21
+ .map(s => ({ name: s.name, url: s.url, auth: s.auth || "none" })),
22
+ commands: commands.map(c => ({
23
+ _id: c._id, // this will now be the natural key
24
+ namespace: c.namespace,
25
+ resource: c.resource,
26
+ action: c.action,
27
+ description: c.description || "",
28
+ adapter: c.adapter,
29
+ adapterConfig: c.adapterConfig || {},
30
+ args: c.args || []
31
+ }))
32
+ }
33
+ }
34
+
35
+ async function bumpVersion() {
36
+ const storage = getStorage()
37
+ const current = await storage.get("settings:config_version")
38
+ const next = String(parseInt(current || "0", 10) + 1)
39
+ await storage.set("settings:config_version", next)
40
+ return next
41
+ }
42
+
43
+ async function getNamespaces() {
44
+ const storage = getStorage()
45
+ const keys = await storage.listKeys("command:")
46
+ const namespaces = new Set()
47
+ for (const k of keys) {
48
+ const parts = k.replace("command:", "").split(".")
49
+ namespaces.add(parts[0])
50
+ }
51
+ return Array.from(namespaces)
52
+ }
53
+
54
+ async function getResources(namespace) {
55
+ const storage = getStorage()
56
+ const keys = await storage.listKeys(`command:${namespace}.`)
57
+ const resources = new Set()
58
+ for (const k of keys) {
59
+ const parts = k.replace("command:", "").split(".")
60
+ resources.add(parts[1])
61
+ }
62
+ return Array.from(resources)
63
+ }
64
+
65
+ async function getActions(namespace, resource) {
66
+ const storage = getStorage()
67
+ const keys = await storage.listKeys(`command:${namespace}.${resource}.`)
68
+ const actions = new Set()
69
+ for (const k of keys) {
70
+ const parts = k.replace("command:", "").split(".")
71
+ actions.add(parts[2])
72
+ }
73
+ return Array.from(actions)
74
+ }
75
+
76
+ async function getCommand(namespace, resource, action) {
77
+ const storage = getStorage()
78
+ return storage.get(`command:${namespace}.${resource}.${action}`)
79
+ }
80
+
81
+ module.exports = {
82
+ getCLIConfig,
83
+ bumpVersion,
84
+ getNamespaces,
85
+ getResources,
86
+ getActions,
87
+ getCommand
88
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Storage Adapter Factory
3
+ * Provides a unified KV store interface (get, set, delete, listKeys)
4
+ * driven by either MongoDB or local JSON files.
5
+ */
6
+ const MongoAdapter = require("./mongo");
7
+ const FileAdapter = require("./file");
8
+
9
+ // Singleton storage instance
10
+ let currentAdapter = null;
11
+
12
+ function getStorage() {
13
+ if (currentAdapter) return currentAdapter;
14
+
15
+ const useMongo = process.env.SUPERCLI_USE_MONGO === "true";
16
+ if (useMongo) {
17
+ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost:27017";
18
+ const dbName = process.env.SUPERCLI_DB || "supercli";
19
+ currentAdapter = new MongoAdapter(mongoUrl, dbName);
20
+ console.log(
21
+ `[Storage] Initialized MongoDB adapter (${mongoUrl}/${dbName})`,
22
+ );
23
+ } else {
24
+ const storageDir = process.env.SUPERCLI_STORAGE_DIR || "./supercli_storage";
25
+ currentAdapter = new FileAdapter(storageDir);
26
+ console.log(`[Storage] Initialized File adapter (${storageDir})`);
27
+ }
28
+
29
+ return currentAdapter;
30
+ }
31
+
32
+ module.exports = { getStorage };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * FileAdapter implementation for SUPERCLI Storage using JSON files
3
+ */
4
+ const fs = require("fs").promises;
5
+ const path = require("path");
6
+
7
+ class FileAdapter {
8
+ constructor(baseDir = "./supercli_storage") {
9
+ this.baseDir = path.resolve(baseDir);
10
+ }
11
+
12
+ filePath(key) {
13
+ // Basic sanitization to avoid directory traversal inside the storage scope
14
+ const safeKey = key.replace(/[^a-zA-Z0-9_.-]/g, "_");
15
+ return path.join(this.baseDir, `${safeKey}.json`);
16
+ }
17
+
18
+ async get(key) {
19
+ try {
20
+ const data = await fs.readFile(this.filePath(key), "utf-8");
21
+ return JSON.parse(data);
22
+ } catch (e) {
23
+ if (e.code === "ENOENT") return null;
24
+ throw e;
25
+ }
26
+ }
27
+
28
+ async set(key, value) {
29
+ await fs.mkdir(this.baseDir, { recursive: true });
30
+ await fs.writeFile(
31
+ this.filePath(key),
32
+ JSON.stringify(value, null, 2),
33
+ "utf-8",
34
+ );
35
+ }
36
+
37
+ async delete(key) {
38
+ try {
39
+ await fs.unlink(this.filePath(key));
40
+ } catch (e) {
41
+ if (e.code !== "ENOENT") throw e;
42
+ }
43
+ }
44
+
45
+ async listKeys(prefix = "") {
46
+ try {
47
+ const files = await fs.readdir(this.baseDir);
48
+ // Reverse the sanitization mapping in listKeys if we can,
49
+ // but for prefix search, we just look at files matching the sanitized prefix.
50
+ const safePrefix = prefix.replace(/[^a-zA-Z0-9_.-]/g, "_");
51
+ return (
52
+ files
53
+ .filter((f) => f.endsWith(".json") && f.startsWith(safePrefix))
54
+ // Here we just strip .json. Actual key might be slightly different if it had chars we sanitized out
55
+ .map((f) => f.replace(/\.json$/, ""))
56
+ );
57
+ } catch (e) {
58
+ if (e.code === "ENOENT") return [];
59
+ throw e;
60
+ }
61
+ }
62
+ }
63
+
64
+ module.exports = FileAdapter;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * MongoAdapter implementation for SUPERCLI Storage
3
+ */
4
+ const { MongoClient } = require("mongodb");
5
+
6
+ class MongoAdapter {
7
+ constructor(uri, dbName = "supercli", collectionName = "storage") {
8
+ this.client = new MongoClient(uri);
9
+ this.dbName = dbName;
10
+ this.collectionName = collectionName;
11
+ this.connected = false;
12
+ this.collection = null;
13
+ }
14
+
15
+ async connect() {
16
+ if (!this.connected) {
17
+ await this.client.connect();
18
+ this.collection = this.client
19
+ .db(this.dbName)
20
+ .collection(this.collectionName);
21
+ this.connected = true;
22
+ }
23
+ }
24
+
25
+ async get(key) {
26
+ await this.connect();
27
+ const doc = await this.collection.findOne({ _id: key });
28
+ return doc ? doc.value : null;
29
+ }
30
+
31
+ async set(key, value) {
32
+ await this.connect();
33
+ await this.collection.updateOne(
34
+ { _id: key },
35
+ { $set: { value } },
36
+ { upsert: true },
37
+ );
38
+ }
39
+
40
+ async delete(key) {
41
+ await this.connect();
42
+ await this.collection.deleteOne({ _id: key });
43
+ }
44
+
45
+ async listKeys(prefix = "") {
46
+ await this.connect();
47
+ const query = prefix ? { _id: { $regex: `^${prefix}` } } : {};
48
+ const docs = await this.collection
49
+ .find(query, { projection: { _id: 1 } })
50
+ .toArray();
51
+ return docs.map((doc) => doc._id);
52
+ }
53
+ }
54
+
55
+ module.exports = MongoAdapter;