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.
- package/.env.example +14 -0
- package/README.md +173 -0
- package/cli/adapters/http.js +72 -0
- package/cli/adapters/mcp.js +193 -0
- package/cli/adapters/openapi.js +160 -0
- package/cli/ask.js +208 -0
- package/cli/config.js +133 -0
- package/cli/executor.js +117 -0
- package/cli/help-json.js +46 -0
- package/cli/mcp-local.js +72 -0
- package/cli/plan-runtime.js +32 -0
- package/cli/planner.js +67 -0
- package/cli/skills.js +240 -0
- package/cli/supercli.js +704 -0
- package/docs/features/adapters.md +25 -0
- package/docs/features/agent-friendly.md +28 -0
- package/docs/features/ask.md +32 -0
- package/docs/features/config-sync.md +22 -0
- package/docs/features/execution-plans.md +25 -0
- package/docs/features/observability.md +22 -0
- package/docs/features/skills.md +25 -0
- package/docs/features/storage.md +25 -0
- package/docs/features/workflows.md +33 -0
- package/docs/initial/AGENTS_FRIENDLY_TOOLS.md +553 -0
- package/docs/initial/agent-friendly.md +447 -0
- package/docs/initial/architecture.md +436 -0
- package/docs/initial/built-in-mcp-server.md +64 -0
- package/docs/initial/command-plan.md +532 -0
- package/docs/initial/core-features-2.md +428 -0
- package/docs/initial/core-features.md +366 -0
- package/docs/initial/dag.md +20 -0
- package/docs/initial/description.txt +9 -0
- package/docs/initial/idea.txt +564 -0
- package/docs/initial/initial-spec-details.md +726 -0
- package/docs/initial/initial-spec.md +731 -0
- package/docs/initial/mcp-local-mode.md +53 -0
- package/docs/initial/mcp-sse-mode.md +54 -0
- package/docs/initial/skills-support.md +246 -0
- package/docs/initial/storage-adapter-example.md +155 -0
- package/docs/initial/supercli-vs-gwc.md +109 -0
- package/examples/mcp-sse/install-demo.js +86 -0
- package/examples/mcp-sse/server.js +81 -0
- package/examples/mcp-stdio/install-demo.js +78 -0
- package/examples/mcp-stdio/server.js +50 -0
- package/package.json +21 -0
- package/server/app.js +59 -0
- package/server/public/app.js +18 -0
- package/server/routes/ask.js +92 -0
- package/server/routes/commands.js +126 -0
- package/server/routes/config.js +58 -0
- package/server/routes/jobs.js +122 -0
- package/server/routes/mcp.js +79 -0
- package/server/routes/plans.js +134 -0
- package/server/routes/specs.js +79 -0
- package/server/services/configService.js +88 -0
- package/server/storage/adapter.js +32 -0
- package/server/storage/file.js +64 -0
- package/server/storage/mongo.js +55 -0
- package/server/views/command-edit.ejs +110 -0
- package/server/views/commands.ejs +49 -0
- package/server/views/jobs.ejs +72 -0
- package/server/views/layout.ejs +42 -0
- package/server/views/mcp.ejs +80 -0
- package/server/views/partials/foot.ejs +5 -0
- package/server/views/partials/head.ejs +27 -0
- package/server/views/specs.ejs +91 -0
- package/tests/test-cli.js +367 -0
- package/tests/test-mcp.js +189 -0
- package/tests/test-openapi.js +101 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const http = require("http")
|
|
4
|
+
|
|
5
|
+
const PORT = Number(process.env.MCP_SSE_PORT || 8787)
|
|
6
|
+
const clients = new Set()
|
|
7
|
+
|
|
8
|
+
function summarize(text) {
|
|
9
|
+
const normalized = String(text || "").trim().replace(/\s+/g, " ")
|
|
10
|
+
if (!normalized) return ""
|
|
11
|
+
const words = normalized.split(" ")
|
|
12
|
+
if (words.length <= 12) return normalized
|
|
13
|
+
return words.slice(0, 12).join(" ") + " ..."
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function broadcast(event, data) {
|
|
17
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
|
|
18
|
+
for (const res of clients) {
|
|
19
|
+
res.write(payload)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const server = http.createServer((req, res) => {
|
|
24
|
+
if (req.method === "GET" && req.url === "/events") {
|
|
25
|
+
res.writeHead(200, {
|
|
26
|
+
"Content-Type": "text/event-stream",
|
|
27
|
+
"Cache-Control": "no-cache",
|
|
28
|
+
Connection: "keep-alive"
|
|
29
|
+
})
|
|
30
|
+
res.write("event: ready\ndata: {\"ok\":true}\n\n")
|
|
31
|
+
clients.add(res)
|
|
32
|
+
req.on("close", () => clients.delete(res))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method === "POST" && req.url === "/tool") {
|
|
37
|
+
let raw = ""
|
|
38
|
+
req.setEncoding("utf-8")
|
|
39
|
+
req.on("data", chunk => { raw += chunk })
|
|
40
|
+
req.on("end", () => {
|
|
41
|
+
let payload
|
|
42
|
+
try {
|
|
43
|
+
payload = JSON.parse(raw || "{}")
|
|
44
|
+
} catch {
|
|
45
|
+
res.writeHead(400, { "Content-Type": "application/json" })
|
|
46
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }))
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tool = payload.tool
|
|
51
|
+
const input = payload.input || {}
|
|
52
|
+
if (tool !== "summarize") {
|
|
53
|
+
res.writeHead(404, { "Content-Type": "application/json" })
|
|
54
|
+
res.end(JSON.stringify({ error: `Unknown tool: ${tool}` }))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
broadcast("tool_called", { tool, input })
|
|
59
|
+
const result = summarize(input.text)
|
|
60
|
+
const out = {
|
|
61
|
+
tool,
|
|
62
|
+
mode: "sse-http",
|
|
63
|
+
result,
|
|
64
|
+
words_in: String(input.text || "").trim() ? String(input.text).trim().split(/\s+/).length : 0,
|
|
65
|
+
words_out: result ? result.split(/\s+/).length : 0
|
|
66
|
+
}
|
|
67
|
+
broadcast("tool_done", out)
|
|
68
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
69
|
+
res.end(JSON.stringify(out))
|
|
70
|
+
})
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
res.writeHead(404, { "Content-Type": "application/json" })
|
|
75
|
+
res.end(JSON.stringify({ error: "Not found" }))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
79
|
+
console.log(`MCP SSE demo server listening on http://127.0.0.1:${PORT}`)
|
|
80
|
+
console.log("Endpoints: POST /tool, GET /events")
|
|
81
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const CACHE_DIR = path.join(os.homedir(), ".supercli");
|
|
8
|
+
const CACHE_FILE = path.join(CACHE_DIR, "config.json");
|
|
9
|
+
const ROOT = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const SERVER_SCRIPT = path.join(ROOT, "examples", "mcp-stdio", "server.js");
|
|
11
|
+
|
|
12
|
+
function ensureDir(dir) {
|
|
13
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readConfig() {
|
|
17
|
+
if (!fs.existsSync(CACHE_FILE)) {
|
|
18
|
+
return {
|
|
19
|
+
version: "1",
|
|
20
|
+
ttl: 3600,
|
|
21
|
+
mcp_servers: [],
|
|
22
|
+
specs: [],
|
|
23
|
+
commands: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
version: "1",
|
|
31
|
+
ttl: 3600,
|
|
32
|
+
mcp_servers: [],
|
|
33
|
+
specs: [],
|
|
34
|
+
commands: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function upsertCommand(config, command) {
|
|
40
|
+
const idx = config.commands.findIndex(
|
|
41
|
+
(c) =>
|
|
42
|
+
c.namespace === command.namespace &&
|
|
43
|
+
c.resource === command.resource &&
|
|
44
|
+
c.action === command.action,
|
|
45
|
+
);
|
|
46
|
+
if (idx >= 0) config.commands[idx] = command;
|
|
47
|
+
else config.commands.push(command);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function main() {
|
|
51
|
+
ensureDir(CACHE_DIR);
|
|
52
|
+
const config = readConfig();
|
|
53
|
+
if (!Array.isArray(config.commands)) config.commands = [];
|
|
54
|
+
if (!Array.isArray(config.mcp_servers)) config.mcp_servers = [];
|
|
55
|
+
if (!Array.isArray(config.specs)) config.specs = [];
|
|
56
|
+
|
|
57
|
+
upsertCommand(config, {
|
|
58
|
+
_id: "command:ai.text.summarize",
|
|
59
|
+
namespace: "ai",
|
|
60
|
+
resource: "text",
|
|
61
|
+
action: "summarize",
|
|
62
|
+
description: "Mock summarize using local stdio MCP server",
|
|
63
|
+
adapter: "mcp",
|
|
64
|
+
adapterConfig: {
|
|
65
|
+
tool: "summarize",
|
|
66
|
+
command: `node ${SERVER_SCRIPT}`,
|
|
67
|
+
},
|
|
68
|
+
args: [{ name: "text", type: "string", required: true }],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
config.fetchedAt = Date.now();
|
|
72
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(config, null, 2));
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
`Installed demo command ai.text.summarize in ${CACHE_FILE}\n`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
function summarize(text) {
|
|
4
|
+
const normalized = String(text || "").trim().replace(/\s+/g, " ")
|
|
5
|
+
if (!normalized) return ""
|
|
6
|
+
const words = normalized.split(" ")
|
|
7
|
+
if (words.length <= 12) return normalized
|
|
8
|
+
return words.slice(0, 12).join(" ") + " ..."
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const chunks = []
|
|
13
|
+
for await (const chunk of process.stdin) chunks.push(chunk)
|
|
14
|
+
|
|
15
|
+
let payload
|
|
16
|
+
try {
|
|
17
|
+
payload = JSON.parse(Buffer.concat(chunks).toString("utf-8") || "{}")
|
|
18
|
+
} catch {
|
|
19
|
+
process.stderr.write("Invalid JSON input\n")
|
|
20
|
+
process.exit(1)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tool = payload.tool
|
|
25
|
+
const input = payload.input || {}
|
|
26
|
+
|
|
27
|
+
if (tool !== "summarize") {
|
|
28
|
+
process.stderr.write(`Unknown tool: ${tool}\n`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const source = input.text || ""
|
|
34
|
+
const summary = summarize(source)
|
|
35
|
+
const out = {
|
|
36
|
+
tool,
|
|
37
|
+
mode: "stdio",
|
|
38
|
+
result: summary,
|
|
39
|
+
words_in: String(source).trim() ? String(source).trim().split(/\s+/).length : 0,
|
|
40
|
+
words_out: summary ? summary.split(/\s+/).length : 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.stdout.write(JSON.stringify(out))
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
main().catch((err) => {
|
|
48
|
+
process.stderr.write(`${err.message}\n`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "superacli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Config-driven, AI-friendly dynamic CLI",
|
|
5
|
+
"main": "server/app.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dcli": "./cli/supercli.js",
|
|
8
|
+
"scli": "./cli/supercli.js",
|
|
9
|
+
"supercli": "./cli/supercli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node server/app.js",
|
|
13
|
+
"dev": "node server/app.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"dotenv": "^17.3.1",
|
|
17
|
+
"ejs": "^3.1.9",
|
|
18
|
+
"express": "^4.18.2",
|
|
19
|
+
"mongodb": "^6.3.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/server/app.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { getStorage } = require("./storage/adapter");
|
|
4
|
+
|
|
5
|
+
const commandsRouter = require("./routes/commands");
|
|
6
|
+
const configRouter = require("./routes/config");
|
|
7
|
+
const specsRouter = require("./routes/specs");
|
|
8
|
+
const mcpRouter = require("./routes/mcp");
|
|
9
|
+
const plansRouter = require("./routes/plans");
|
|
10
|
+
const jobsRouter = require("./routes/jobs");
|
|
11
|
+
const askRouter = require("./routes/ask");
|
|
12
|
+
|
|
13
|
+
const PORT = process.env.PORT || 3000;
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
app.use(express.urlencoded({ extended: true }));
|
|
19
|
+
|
|
20
|
+
app.set("view engine", "ejs");
|
|
21
|
+
app.set("views", path.join(__dirname, "views"));
|
|
22
|
+
|
|
23
|
+
app.use("/static", express.static(path.join(__dirname, "public")));
|
|
24
|
+
|
|
25
|
+
// API routes
|
|
26
|
+
app.use("/api/config", configRouter);
|
|
27
|
+
app.use("/api/commands", commandsRouter);
|
|
28
|
+
app.use("/api/specs", specsRouter);
|
|
29
|
+
app.use("/api/mcp", mcpRouter);
|
|
30
|
+
app.use("/api/plans", plansRouter);
|
|
31
|
+
app.use("/api/jobs", jobsRouter);
|
|
32
|
+
app.use("/api/ask", askRouter);
|
|
33
|
+
|
|
34
|
+
// Tree/command endpoints under /api (config router handles them)
|
|
35
|
+
app.use("/api", configRouter);
|
|
36
|
+
|
|
37
|
+
// UI page redirects
|
|
38
|
+
app.get("/", (req, res) => res.redirect("/api/commands"));
|
|
39
|
+
app.get("/commands", (req, res) => res.redirect("/api/commands"));
|
|
40
|
+
app.get("/commands/new", (req, res) => res.redirect("/api/commands/new"));
|
|
41
|
+
app.get("/specs", (req, res) => res.redirect("/api/specs"));
|
|
42
|
+
app.get("/mcp", (req, res) => res.redirect("/api/mcp"));
|
|
43
|
+
app.get("/jobs", (req, res) => res.redirect("/api/jobs"));
|
|
44
|
+
|
|
45
|
+
async function start() {
|
|
46
|
+
try {
|
|
47
|
+
// Initialize storage singleton
|
|
48
|
+
getStorage();
|
|
49
|
+
|
|
50
|
+
app.listen(PORT, () => {
|
|
51
|
+
console.log(`SUPERCLI server running on http://localhost:${PORT}`);
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error("Failed to start:", err.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
start();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SUPERCLI Client-side utilities
|
|
2
|
+
// Highlight active nav item based on current URL
|
|
3
|
+
(function () {
|
|
4
|
+
const path = window.location.pathname;
|
|
5
|
+
if (path.startsWith("/api/commands") || path.startsWith("/commands")) {
|
|
6
|
+
const el = document.getElementById("nav-commands");
|
|
7
|
+
if (el) el.classList.add("nav-active");
|
|
8
|
+
} else if (path.startsWith("/api/specs") || path.startsWith("/specs")) {
|
|
9
|
+
const el = document.getElementById("nav-specs");
|
|
10
|
+
if (el) el.classList.add("nav-active");
|
|
11
|
+
} else if (path.startsWith("/api/mcp") || path.startsWith("/mcp")) {
|
|
12
|
+
const el = document.getElementById("nav-mcp");
|
|
13
|
+
if (el) el.classList.add("nav-active");
|
|
14
|
+
} else if (path.startsWith("/api/jobs") || path.startsWith("/jobs")) {
|
|
15
|
+
const el = document.getElementById("nav-jobs");
|
|
16
|
+
if (el) el.classList.add("nav-active");
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { Router } = require("express")
|
|
2
|
+
const configService = require("../services/configService")
|
|
3
|
+
|
|
4
|
+
const router = Router()
|
|
5
|
+
|
|
6
|
+
router.post("/", async (req, res) => {
|
|
7
|
+
const { query } = req.body
|
|
8
|
+
if (!query) return res.status(400).json({ error: "Missing query parameter" })
|
|
9
|
+
|
|
10
|
+
const baseUrl = process.env.OPENAI_BASE_URL
|
|
11
|
+
const model = process.env.OPENAI_MODEL || "gpt-3.5-turbo"
|
|
12
|
+
const apiKey = process.env.OPENAI_API_KEY || "dummy"
|
|
13
|
+
|
|
14
|
+
if (!baseUrl) {
|
|
15
|
+
return res.status(501).json({ error: "Server is not configured for LLM completions (OPENAI_BASE_URL is missing)" })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const namespaces = await configService.getNamespaces()
|
|
20
|
+
const allDefs = []
|
|
21
|
+
|
|
22
|
+
// Build a compact representation of all CLI commands
|
|
23
|
+
for (const ns of namespaces) {
|
|
24
|
+
const resources = await configService.getResources(ns)
|
|
25
|
+
for (const res of resources) {
|
|
26
|
+
const actions = await configService.getActions(ns, res)
|
|
27
|
+
for (const act of actions) {
|
|
28
|
+
const cmd = await configService.getCommand(ns, res, act)
|
|
29
|
+
if (!cmd) continue
|
|
30
|
+
const argList = (cmd.args || []).map(a => `--${a.name}${a.required ? " (required)" : ""}`).join(" ")
|
|
31
|
+
allDefs.push(`- ${ns} ${res} ${act} ${argList} : ${cmd.description || "no desc"}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const systemPrompt = `You are an AI CLI assistant router.
|
|
37
|
+
Available commands:
|
|
38
|
+
${allDefs.join("\n")}
|
|
39
|
+
|
|
40
|
+
The user wants to accomplish a goal. Map their goal into a sequence of CLI commands to run.
|
|
41
|
+
Respond STRICTLY with a valid JSON array of steps. For example:
|
|
42
|
+
[
|
|
43
|
+
{ "command": "aws.instances.list", "args": { "region": "us-east-1" } },
|
|
44
|
+
{ "command": "ai.summarize.text", "args": { "text": "{{step.0.data.summary}}" } }
|
|
45
|
+
]
|
|
46
|
+
Do not wrap it in markdown. Do not include any other text.`
|
|
47
|
+
|
|
48
|
+
const r = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"Authorization": `Bearer ${apiKey}`
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
model,
|
|
56
|
+
messages: [
|
|
57
|
+
{ role: "system", content: systemPrompt },
|
|
58
|
+
{ role: "user", content: query }
|
|
59
|
+
],
|
|
60
|
+
temperature: 0
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (!r.ok) {
|
|
65
|
+
const txt = await r.text()
|
|
66
|
+
throw new Error(`LLM Error ${r.status}: ${txt}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = await r.json()
|
|
70
|
+
const content = data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content
|
|
71
|
+
if (!content) throw new Error("Invalid response format from LLM")
|
|
72
|
+
|
|
73
|
+
// Attempt to extract JSON if the model ignored instructions and wrapped in markdown
|
|
74
|
+
let jsonStr = content.trim()
|
|
75
|
+
if (jsonStr.startsWith("```json")) {
|
|
76
|
+
jsonStr = jsonStr.replace(/^```json/, "").replace(/```$/, "").trim()
|
|
77
|
+
} else if (jsonStr.startsWith("```")) {
|
|
78
|
+
jsonStr = jsonStr.replace(/^```/, "").replace(/```$/, "").trim()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const steps = JSON.parse(jsonStr)
|
|
82
|
+
if (!Array.isArray(steps)) {
|
|
83
|
+
throw new Error("LLM did not return a JSON array")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
res.json({ steps })
|
|
87
|
+
} catch (err) {
|
|
88
|
+
res.status(500).json({ error: err.message })
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
module.exports = router
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { Router } = require("express")
|
|
2
|
+
const { getStorage } = require("../storage/adapter")
|
|
3
|
+
const { bumpVersion } = require("../services/configService")
|
|
4
|
+
|
|
5
|
+
const router = Router()
|
|
6
|
+
|
|
7
|
+
// Helper to list all commands
|
|
8
|
+
async function getAllCommands() {
|
|
9
|
+
const storage = getStorage()
|
|
10
|
+
const keys = await storage.listKeys("command:")
|
|
11
|
+
const commands = await Promise.all(keys.map(k => storage.get(k)))
|
|
12
|
+
return commands.sort((a, b) => a._id.localeCompare(b._id))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// GET /api/commands
|
|
16
|
+
router.get("/", async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const commands = await getAllCommands()
|
|
19
|
+
if (req.query.format !== "json" && req.accepts("html") && !req.xhr && !req.headers["x-requested-with"]) {
|
|
20
|
+
return res.render("commands", { commands })
|
|
21
|
+
}
|
|
22
|
+
res.json(commands)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(500).json({ error: err.message })
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// GET /api/commands/new
|
|
29
|
+
router.get("/new", async (req, res) => {
|
|
30
|
+
res.render("command-edit", { command: null })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// GET /api/commands/:id/edit
|
|
34
|
+
router.get("/:id/edit", async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
const storage = getStorage()
|
|
37
|
+
// id could be URL encoded if it's a natural key
|
|
38
|
+
const id = decodeURIComponent(req.params.id)
|
|
39
|
+
const command = await storage.get(id)
|
|
40
|
+
if (!command) return res.status(404).send("Not found")
|
|
41
|
+
res.render("command-edit", { command })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
res.status(500).send(err.message)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// POST /api/commands
|
|
48
|
+
router.post("/", async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const storage = getStorage()
|
|
51
|
+
const { namespace, resource, action, description, adapter, adapterConfig, args } = req.body
|
|
52
|
+
const key = `command:${namespace}.${resource}.${action}`
|
|
53
|
+
|
|
54
|
+
// Check if exists? Overwrite is allowed for now, acts as upsert.
|
|
55
|
+
|
|
56
|
+
const doc = {
|
|
57
|
+
_id: key, // Store the key inside the document as well
|
|
58
|
+
namespace,
|
|
59
|
+
resource,
|
|
60
|
+
action,
|
|
61
|
+
description: description || "",
|
|
62
|
+
adapter: adapter || "http",
|
|
63
|
+
adapterConfig: typeof adapterConfig === "string" ? JSON.parse(adapterConfig || "{}") : (adapterConfig || {}),
|
|
64
|
+
args: Array.isArray(args) ? args : JSON.parse(args || "[]"),
|
|
65
|
+
createdAt: new Date(),
|
|
66
|
+
updatedAt: new Date()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await storage.set(key, doc)
|
|
70
|
+
await bumpVersion()
|
|
71
|
+
|
|
72
|
+
if (req.headers["content-type"]?.includes("urlencoded")) {
|
|
73
|
+
return res.redirect("/api/commands")
|
|
74
|
+
}
|
|
75
|
+
res.status(201).json(doc)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
res.status(500).json({ error: err.message })
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// PUT /api/commands/:id
|
|
82
|
+
router.put("/:id", async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const storage = getStorage()
|
|
85
|
+
const id = decodeURIComponent(req.params.id)
|
|
86
|
+
const { namespace, resource, action, description, adapter, adapterConfig, args } = req.body
|
|
87
|
+
|
|
88
|
+
// If n/r/a changed, the ID changes, we should delete the old one.
|
|
89
|
+
const newKey = `command:${namespace}.${resource}.${action}`
|
|
90
|
+
if (newKey !== id) {
|
|
91
|
+
await storage.delete(id)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const update = {
|
|
95
|
+
_id: newKey,
|
|
96
|
+
namespace,
|
|
97
|
+
resource,
|
|
98
|
+
action,
|
|
99
|
+
description: description || "",
|
|
100
|
+
adapter: adapter || "http",
|
|
101
|
+
adapterConfig: typeof adapterConfig === "string" ? JSON.parse(adapterConfig || "{}") : (adapterConfig || {}),
|
|
102
|
+
args: Array.isArray(args) ? args : JSON.parse(args || "[]"),
|
|
103
|
+
updatedAt: new Date()
|
|
104
|
+
}
|
|
105
|
+
await storage.set(newKey, update)
|
|
106
|
+
await bumpVersion()
|
|
107
|
+
res.json({ ok: true })
|
|
108
|
+
} catch (err) {
|
|
109
|
+
res.status(500).json({ error: err.message })
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// DELETE /api/commands/:id
|
|
114
|
+
router.delete("/:id", async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const storage = getStorage()
|
|
117
|
+
const id = decodeURIComponent(req.params.id)
|
|
118
|
+
await storage.delete(id)
|
|
119
|
+
await bumpVersion()
|
|
120
|
+
res.json({ ok: true })
|
|
121
|
+
} catch (err) {
|
|
122
|
+
res.status(500).json({ error: err.message })
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
module.exports = router
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { Router } = require("express")
|
|
2
|
+
const configService = require("../services/configService")
|
|
3
|
+
|
|
4
|
+
const router = Router()
|
|
5
|
+
|
|
6
|
+
// GET /api/config — full CLI config
|
|
7
|
+
router.get("/", async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const config = await configService.getCLIConfig()
|
|
10
|
+
config.features = { ask: !!process.env.OPENAI_BASE_URL }
|
|
11
|
+
res.json(config)
|
|
12
|
+
} catch (err) {
|
|
13
|
+
res.status(500).json({ error: err.message })
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// GET /api/tree — list namespaces
|
|
18
|
+
router.get("/tree", async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const namespaces = await configService.getNamespaces()
|
|
21
|
+
res.json({ namespaces })
|
|
22
|
+
} catch (err) {
|
|
23
|
+
res.status(500).json({ error: err.message })
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// GET /api/tree/:ns — list resources in namespace
|
|
28
|
+
router.get("/tree/:ns", async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const resources = await configService.getResources(req.params.ns)
|
|
31
|
+
res.json({ resources })
|
|
32
|
+
} catch (err) {
|
|
33
|
+
res.status(500).json({ error: err.message })
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// GET /api/tree/:ns/:res — list actions
|
|
38
|
+
router.get("/tree/:ns/:res", async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const actions = await configService.getActions(req.params.ns, req.params.res)
|
|
41
|
+
res.json({ actions })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
res.status(500).json({ error: err.message })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// GET /api/command/:ns/:res/:act — full command spec
|
|
48
|
+
router.get("/command/:ns/:res/:act", async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const cmd = await configService.getCommand(req.params.ns, req.params.res, req.params.act)
|
|
51
|
+
if (!cmd) return res.status(404).json({ error: "Command not found" })
|
|
52
|
+
res.json(cmd)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
res.status(500).json({ error: err.message })
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
module.exports = router
|