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
package/cli/ask.js ADDED
@@ -0,0 +1,208 @@
1
+ const { execute } = require("./executor");
2
+
3
+ async function localLLMCompletion(query, config, baseUrl, model, apiKey) {
4
+ const namespaces = [...new Set(config.commands.map((c) => c.namespace))];
5
+ const allDefs = [];
6
+
7
+ for (const ns of namespaces) {
8
+ const resources = [
9
+ ...new Set(
10
+ config.commands
11
+ .filter((c) => c.namespace === ns)
12
+ .map((c) => c.resource),
13
+ ),
14
+ ];
15
+ for (const res of resources) {
16
+ const actions = config.commands
17
+ .filter((c) => c.namespace === ns && c.resource === res)
18
+ .map((c) => c.action);
19
+ for (const act of actions) {
20
+ const cmd = config.commands.find(
21
+ (c) => c.namespace === ns && c.resource === res && c.action === act,
22
+ );
23
+ if (!cmd) continue;
24
+ const argList = (cmd.args || [])
25
+ .map((a) => `--${a.name}${a.required ? " (required)" : ""}`)
26
+ .join(" ");
27
+ allDefs.push(
28
+ `- ${ns} ${res} ${act} ${argList} : ${cmd.description || "no desc"}`,
29
+ );
30
+ }
31
+ }
32
+ }
33
+
34
+ const systemPrompt = `You are an AI CLI assistant router.
35
+ Available commands:
36
+ ${allDefs.join("\n")}
37
+
38
+ The user wants to accomplish a goal. Map their goal into a sequence of CLI commands to run.
39
+ Respond STRICTLY with a valid JSON array of steps. For example:
40
+ [
41
+ { "command": "aws.instances.list", "args": { "region": "us-east-1" } },
42
+ { "command": "ai.summarize.text", "args": { "text": "{{step.0.data.summary}}" } }
43
+ ]
44
+ Do not wrap it in markdown. Do not include any other text.`;
45
+
46
+ const r = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Authorization: `Bearer ${apiKey}`,
51
+ },
52
+ body: JSON.stringify({
53
+ model,
54
+ messages: [
55
+ { role: "system", content: systemPrompt },
56
+ { role: "user", content: query },
57
+ ],
58
+ temperature: 0,
59
+ }),
60
+ });
61
+
62
+ if (!r.ok) {
63
+ const txt = await r.text();
64
+ throw new Error(`Local LLM Error ${r.status}: ${txt}`);
65
+ }
66
+
67
+ const data = await r.json();
68
+ const content =
69
+ data.choices &&
70
+ data.choices[0] &&
71
+ data.choices[0].message &&
72
+ data.choices[0].message.content;
73
+ if (!content) throw new Error("Invalid response format from local LLM");
74
+
75
+ let jsonStr = content.trim();
76
+ if (jsonStr.startsWith("```json"))
77
+ jsonStr = jsonStr
78
+ .replace(/^```json/, "")
79
+ .replace(/```$/, "")
80
+ .trim();
81
+ else if (jsonStr.startsWith("```"))
82
+ jsonStr = jsonStr.replace(/^```/, "").replace(/```$/, "").trim();
83
+
84
+ const steps = JSON.parse(jsonStr);
85
+ if (!Array.isArray(steps)) throw new Error("LLM did not return a JSON array");
86
+ return steps;
87
+ }
88
+
89
+ async function remoteLLMCompletion(query, serverUrl) {
90
+ const r = await fetch(`${serverUrl}/api/ask`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ query }),
94
+ });
95
+
96
+ if (!r.ok) {
97
+ const errorBody = await r.json().catch(() => ({}));
98
+ throw new Error(
99
+ `Server LLM completion failed: ${errorBody.error || r.statusText}`,
100
+ );
101
+ }
102
+
103
+ const data = await r.json();
104
+ return data.steps;
105
+ }
106
+
107
+ async function handleAskCommand({
108
+ positional,
109
+ config,
110
+ context,
111
+ humanMode,
112
+ output,
113
+ outputError,
114
+ }) {
115
+ if (positional.length < 2) {
116
+ outputError({
117
+ code: 85,
118
+ type: "invalid_argument",
119
+ message: 'Usage: supercli ask "<your natural language query>"',
120
+ recoverable: false,
121
+ });
122
+ return;
123
+ }
124
+
125
+ const query = positional.slice(1).join(" ");
126
+ const hasLocalLLM = !!process.env.OPENAI_BASE_URL;
127
+ const hasServerLLM = context.server && config.features && config.features.ask;
128
+
129
+ if (!hasLocalLLM && !hasServerLLM) {
130
+ outputError({
131
+ code: 105,
132
+ type: "integration_error",
133
+ message:
134
+ "The 'ask' feature is not configured. Export OPENAI_BASE_URL locally or ensure the SUPERCLI_SERVER has it configured.",
135
+ recoverable: false,
136
+ });
137
+ return;
138
+ }
139
+
140
+ if (humanMode) {
141
+ console.log(
142
+ `\n 🤖 Thinking... (${hasLocalLLM ? "local" : "server"} resolution)`,
143
+ );
144
+ }
145
+
146
+ try {
147
+ let steps = [];
148
+ if (hasLocalLLM) {
149
+ steps = await localLLMCompletion(
150
+ query,
151
+ config,
152
+ process.env.OPENAI_BASE_URL,
153
+ process.env.OPENAI_MODEL || "gpt-3.5-turbo",
154
+ process.env.OPENAI_API_KEY || "dummy",
155
+ );
156
+ } else {
157
+ steps = await remoteLLMCompletion(query, context.server);
158
+ }
159
+
160
+ if (humanMode) {
161
+ console.log(` 📋 Plan:`);
162
+ steps.forEach((s, i) => {
163
+ const argStr = Object.entries(s.args || {})
164
+ .map(([k, v]) => `--${k}="${v}"`)
165
+ .join(" ");
166
+ console.log(` ${i + 1}. ${s.command.replace(/\./g, " ")} ${argStr}`);
167
+ });
168
+ console.log(`\n ▶ Executing...`);
169
+ }
170
+
171
+ // Map to a virtual workflow command and execute
172
+ const virtualCommand = {
173
+ namespace: "system",
174
+ resource: "ai",
175
+ action: "ask",
176
+ type: "workflow",
177
+ adapterConfig: { steps },
178
+ };
179
+
180
+ const start = Date.now();
181
+ const result = await execute(virtualCommand, {}, context);
182
+ const duration = Date.now() - start;
183
+
184
+ const envelope = {
185
+ version: "1.0",
186
+ command: "ask",
187
+ duration_ms: duration,
188
+ data: result,
189
+ };
190
+
191
+ if (!humanMode) {
192
+ output(envelope);
193
+ } else {
194
+ console.log(`\n ✅ Success (${duration}ms)\n`);
195
+ console.log(JSON.stringify(envelope.data, null, 2));
196
+ console.log("");
197
+ }
198
+ } catch (err) {
199
+ outputError({
200
+ code: 105,
201
+ type: "integration_error",
202
+ message: err.message,
203
+ recoverable: true,
204
+ });
205
+ }
206
+ }
207
+
208
+ module.exports = { handleAskCommand };
package/cli/config.js ADDED
@@ -0,0 +1,133 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const os = require("os")
4
+
5
+ const CACHE_DIR = path.join(os.homedir(), ".supercli")
6
+ const CACHE_FILE = path.join(CACHE_DIR, "config.json")
7
+
8
+ function ensureCacheDir() {
9
+ if (!fs.existsSync(CACHE_DIR)) {
10
+ fs.mkdirSync(CACHE_DIR, { recursive: true })
11
+ }
12
+ }
13
+
14
+ function readCache() {
15
+ try {
16
+ if (fs.existsSync(CACHE_FILE)) {
17
+ const raw = fs.readFileSync(CACHE_FILE, "utf-8")
18
+ return JSON.parse(raw)
19
+ }
20
+ } catch (e) {
21
+ // Corrupted cache, ignore
22
+ }
23
+ return null
24
+ }
25
+
26
+ function writeCache(config) {
27
+ ensureCacheDir()
28
+ const data = {
29
+ ...config,
30
+ fetchedAt: Date.now()
31
+ }
32
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2))
33
+ return data
34
+ }
35
+
36
+ function ttlValid(cache) {
37
+ if (!cache || !cache.fetchedAt) return false
38
+ const ttl = (cache.ttl || 3600) * 1000
39
+ return (Date.now() - cache.fetchedAt) < ttl
40
+ }
41
+
42
+ async function fetchRemoteConfig(server) {
43
+ if (!server) throw new Error("SUPERCLI_SERVER is not configured")
44
+ const url = `${server}/api/config`
45
+ const r = await fetch(url)
46
+ if (!r.ok) throw new Error(`Failed to fetch config: ${r.status} ${r.statusText}`)
47
+ return r.json()
48
+ }
49
+
50
+ function emptyConfig() {
51
+ return {
52
+ version: "1",
53
+ ttl: 3600,
54
+ mcp_servers: [],
55
+ specs: [],
56
+ commands: []
57
+ }
58
+ }
59
+
60
+ async function loadConfig() {
61
+ const cache = readCache()
62
+ if (cache) return cache
63
+ return emptyConfig()
64
+ }
65
+
66
+ async function syncConfig(server) {
67
+ const config = await fetchRemoteConfig(server)
68
+
69
+ if (!Array.isArray(config.mcp_servers)) {
70
+ try {
71
+ const mcpRes = await fetch(`${server}/api/mcp?format=json`)
72
+ config.mcp_servers = mcpRes.ok ? await mcpRes.json() : []
73
+ } catch {
74
+ config.mcp_servers = []
75
+ }
76
+ }
77
+
78
+ if (!Array.isArray(config.specs)) {
79
+ try {
80
+ const specsRes = await fetch(`${server}/api/specs?format=json`)
81
+ config.specs = specsRes.ok ? await specsRes.json() : []
82
+ } catch {
83
+ config.specs = []
84
+ }
85
+ }
86
+
87
+ if (!Array.isArray(config.commands)) config.commands = []
88
+ return writeCache(config)
89
+ }
90
+
91
+ async function setMcpServer(name, url) {
92
+ const cfg = readCache() || emptyConfig()
93
+ const servers = Array.isArray(cfg.mcp_servers) ? cfg.mcp_servers.slice() : []
94
+ const idx = servers.findIndex(s => s && s.name === name)
95
+ const next = { name, url }
96
+ if (idx >= 0) servers[idx] = next
97
+ else servers.push(next)
98
+ cfg.mcp_servers = servers.sort((a, b) => a.name.localeCompare(b.name))
99
+ return writeCache(cfg)
100
+ }
101
+
102
+ async function removeMcpServer(name) {
103
+ const cfg = readCache() || emptyConfig()
104
+ const servers = Array.isArray(cfg.mcp_servers) ? cfg.mcp_servers : []
105
+ const next = servers.filter(s => s && s.name !== name)
106
+ const removed = next.length !== servers.length
107
+ cfg.mcp_servers = next
108
+ writeCache(cfg)
109
+ return removed
110
+ }
111
+
112
+ async function listMcpServers() {
113
+ const cfg = await loadConfig()
114
+ return Array.isArray(cfg.mcp_servers) ? cfg.mcp_servers : []
115
+ }
116
+
117
+ async function showConfig() {
118
+ const cache = readCache()
119
+ if (!cache) {
120
+ return { cached: false, message: "No config cached. Set SUPERCLI_SERVER and run: supercli sync" }
121
+ }
122
+ return {
123
+ version: cache.version,
124
+ ttl: cache.ttl,
125
+ fetchedAt: new Date(cache.fetchedAt).toISOString(),
126
+ commands: cache.commands ? cache.commands.length : 0,
127
+ mcp_servers: cache.mcp_servers ? cache.mcp_servers.length : 0,
128
+ specs: cache.specs ? cache.specs.length : 0,
129
+ cacheFile: CACHE_FILE
130
+ }
131
+ }
132
+
133
+ module.exports = { loadConfig, syncConfig, showConfig, setMcpServer, removeMcpServer, listMcpServers }
@@ -0,0 +1,117 @@
1
+ const path = require("path")
2
+
3
+ // Adapter registry — lazy-loaded
4
+ const ADAPTERS = {
5
+ openapi: () => require("./adapters/openapi"),
6
+ mcp: () => require("./adapters/mcp"),
7
+ http: () => require("./adapters/http")
8
+ }
9
+
10
+ async function execute(cmd, flags, context) {
11
+ // Workflow commands: execute steps sequentially
12
+ const steps = cmd.type === "workflow" ? (cmd.steps || (cmd.adapterConfig && cmd.adapterConfig.steps) || []) : null
13
+ if (cmd.type === "workflow" && steps && steps.length > 0) {
14
+ return executeWorkflow(cmd, flags, context, steps)
15
+ }
16
+
17
+ const adapterName = cmd.adapter
18
+
19
+ if (!ADAPTERS[adapterName]) {
20
+ try {
21
+ const custom = require(path.resolve("adapters", adapterName))
22
+ return custom.execute(cmd, flags, context)
23
+ } catch (e) {
24
+ throw Object.assign(new Error(`Unknown adapter: ${adapterName}`), {
25
+ code: 110,
26
+ type: "internal_error",
27
+ recoverable: false
28
+ })
29
+ }
30
+ }
31
+
32
+ const adapter = ADAPTERS[adapterName]()
33
+ return adapter.execute(cmd, flags, context)
34
+ }
35
+
36
+ async function executeWorkflow(workflow, flags, context, steps) {
37
+ const results = []
38
+ let prevOutput = null
39
+
40
+ for (const stepDef of steps) {
41
+ let stepCommandString = ""
42
+ let stepArgs = {}
43
+
44
+ if (typeof stepDef === "string") {
45
+ stepCommandString = stepDef
46
+ } else if (typeof stepDef === "object") {
47
+ stepCommandString = stepDef.command
48
+ stepArgs = stepDef.args || {}
49
+ }
50
+
51
+ let parts = stepCommandString.includes(".") ? stepCommandString.split(".") : stepCommandString.split(" ")
52
+ parts = parts.filter(p => !p.startsWith("-"))
53
+ if (parts.length < 3) {
54
+ throw Object.assign(new Error(`Invalid workflow step: ${stepCommandString}`), {
55
+ code: 85, type: "invalid_argument", recoverable: false
56
+ })
57
+ }
58
+ const [ns, resource, action] = parts
59
+
60
+ let stepCmd = null
61
+ if (context.config && Array.isArray(context.config.commands)) {
62
+ stepCmd = context.config.commands.find(c => c.namespace === ns && c.resource === resource && c.action === action)
63
+ }
64
+
65
+ if (!stepCmd) {
66
+ if (!context.server) {
67
+ throw Object.assign(new Error(`Workflow step command not found in local config and server is unavailable: ${stepCommandString}`), {
68
+ code: 92, type: "resource_not_found", recoverable: false
69
+ })
70
+ }
71
+ const r = await fetch(`${context.server}/api/command/${ns}/${resource}/${action}`)
72
+ if (!r.ok) {
73
+ throw Object.assign(new Error(`Workflow step command not found: ${stepCommandString}`), {
74
+ code: 92, type: "resource_not_found", recoverable: false
75
+ })
76
+ }
77
+ stepCmd = await r.json()
78
+ }
79
+
80
+ // Merge flags + explicit step args + previous output as context
81
+ const mergedFlags = { ...flags, ...stepArgs }
82
+ if (prevOutput && typeof prevOutput === "object") {
83
+ for (const [k, v] of Object.entries(prevOutput)) {
84
+ if (mergedFlags[k] === undefined && typeof v !== "object") {
85
+ mergedFlags[k] = v
86
+ }
87
+ }
88
+ }
89
+
90
+ // Note: Template replacement (e.g. "{{step.0.data.summary}}") handling should ideally be done here.
91
+ // For simplicity, doing a basic string replace for known vars on string arguments.
92
+ for (const [k, v] of Object.entries(mergedFlags)) {
93
+ if (typeof v === "string") {
94
+ mergedFlags[k] = v.replace(/\{\{args\.([^}]+)\}\}/g, (_, path) => flags[path] || "")
95
+ mergedFlags[k] = mergedFlags[k].replace(/\{\{step\.(\d+)\.([^}]+)\}\}/g, (_, idx, path) => {
96
+ const res = results[Number(idx)]
97
+ if (!res) return ""
98
+ // Simple dot path resolution for data...
99
+ let val = res
100
+ for (const p of path.split(".")) {
101
+ if (val && typeof val === "object") val = val[p]
102
+ else { val = ""; break; }
103
+ }
104
+ return val || ""
105
+ })
106
+ }
107
+ }
108
+
109
+ const result = await module.exports.execute(stepCmd, mergedFlags, context)
110
+ results.push({ step: stepCommandString, result })
111
+ prevOutput = result
112
+ }
113
+
114
+ return { workflow: workflow.namespace + "." + workflow.resource + "." + workflow.action, steps: results }
115
+ }
116
+
117
+ module.exports = { execute }
@@ -0,0 +1,46 @@
1
+ function buildCapabilities(config, hasServer) {
2
+ const commands = {
3
+ help: { description: "List namespaces and commands" },
4
+ config: { subcommands: ["show"] },
5
+ mcp: { subcommands: ["list", "add", "remove"], description: "Manage local MCP server registry" },
6
+ commands: { description: "List all commands" },
7
+ inspect: { description: "Inspect command details", usage: "supercli inspect <ns> <res> <act>" },
8
+ plan: { description: "Create execution plan", usage: "supercli plan <ns> <res> <act> [--args]" },
9
+ execute: { description: "Execute a stored plan", usage: "supercli execute <plan_id>" },
10
+ skills: {
11
+ description: "Skill discovery and SKILL.md generation",
12
+ subcommands: ["list", "get", "teach"]
13
+ }
14
+ }
15
+ if (hasServer) commands.sync = { description: "Sync local config from SUPERCLI_SERVER" }
16
+ if (config.features?.ask || process.env.OPENAI_BASE_URL) commands.ask = { description: "Execute natural language queries", usage: "supercli ask \"<query>\"" }
17
+
18
+ return {
19
+ version: "1.0",
20
+ name: "supercli",
21
+ description: "Config-driven, AI-friendly dynamic CLI",
22
+ commands,
23
+ namespaces: [...new Set((config.commands || []).map(c => c.namespace))],
24
+ total_commands: (config.commands || []).length,
25
+ output_formats: ["json", "human", "compact"],
26
+ flags: {
27
+ "--json": "Force JSON output",
28
+ "--human": "Force human-readable output",
29
+ "--compact": "Compressed JSON for token optimization",
30
+ "--schema": "Show input/output schema for a command",
31
+ "--help-json": "Machine-readable capability discovery",
32
+ "--show-dag": "Include execution DAG in output",
33
+ "--format": "Output format for selected commands"
34
+ },
35
+ exit_codes: {
36
+ "0": "success",
37
+ "82": "validation_error",
38
+ "85": "invalid_argument",
39
+ "92": "resource_not_found",
40
+ "105": "integration_error",
41
+ "110": "internal_error"
42
+ }
43
+ }
44
+ }
45
+
46
+ module.exports = { buildCapabilities }
@@ -0,0 +1,72 @@
1
+ async function handleMcpRegistryCommand(options) {
2
+ const {
3
+ positional,
4
+ flags,
5
+ humanMode,
6
+ output,
7
+ outputHumanTable,
8
+ outputError,
9
+ setMcpServer,
10
+ removeMcpServer,
11
+ listMcpServers,
12
+ } = options;
13
+
14
+ const subcommand = positional[1];
15
+ if (subcommand === "list") {
16
+ const servers = await listMcpServers();
17
+ if (humanMode) {
18
+ console.log("\n ⚡ Local MCP Servers\n");
19
+ outputHumanTable(servers, [
20
+ { key: "name", label: "Name" },
21
+ { key: "url", label: "URL" },
22
+ ]);
23
+ console.log("");
24
+ } else {
25
+ output({ mcp_servers: servers });
26
+ }
27
+ return true;
28
+ }
29
+
30
+ if (subcommand === "add") {
31
+ const name = positional[2];
32
+ const url = flags.url;
33
+ if (!name || !url) {
34
+ outputError({
35
+ code: 85,
36
+ type: "invalid_argument",
37
+ message: "Usage: supercli mcp add <name> --url <mcp_url>",
38
+ recoverable: false,
39
+ });
40
+ return true;
41
+ }
42
+ await setMcpServer(name, url);
43
+ output({ ok: true, message: `MCP server '${name}' saved locally` });
44
+ return true;
45
+ }
46
+
47
+ if (subcommand === "remove") {
48
+ const name = positional[2];
49
+ if (!name) {
50
+ outputError({
51
+ code: 85,
52
+ type: "invalid_argument",
53
+ message: "Usage: supercli mcp remove <name>",
54
+ recoverable: false,
55
+ });
56
+ return true;
57
+ }
58
+ const removed = await removeMcpServer(name);
59
+ output({ ok: true, removed });
60
+ return true;
61
+ }
62
+
63
+ outputError({
64
+ code: 85,
65
+ type: "invalid_argument",
66
+ message: "Unknown mcp subcommand. Use: list, add, remove",
67
+ recoverable: false,
68
+ });
69
+ return true;
70
+ }
71
+
72
+ module.exports = { handleMcpRegistryCommand };
@@ -0,0 +1,32 @@
1
+ const { createPlan } = require("./planner")
2
+
3
+ function buildLocalPlan(cmd, args) {
4
+ return {
5
+ ...createPlan(cmd, args),
6
+ persisted: false,
7
+ execution_mode: "local"
8
+ }
9
+ }
10
+
11
+ function annotateServerPlan(plan) {
12
+ return {
13
+ ...plan,
14
+ persisted: true,
15
+ execution_mode: "server"
16
+ }
17
+ }
18
+
19
+ function outputHumanPlan(plan) {
20
+ console.log(`\n ⚡ Execution Plan: ${plan.plan_id}\n`)
21
+ console.log(` Command: ${plan.command}`)
22
+ console.log(` Risk: ${plan.risk_level}`)
23
+ console.log(` Side effects: ${plan.side_effects ? "yes" : "no"}`)
24
+ console.log(` Persisted: ${plan.persisted ? "yes" : "no"}`)
25
+ console.log("\n Steps:")
26
+ plan.steps.forEach((s, i) => console.log(` ${i + 1}. [${s.type}] ${s.description || s.method || ""} ${s.url || ""}`))
27
+ if (plan.persisted) console.log(`\n Execute: supercli execute ${plan.plan_id}`)
28
+ else console.log("\n Execute: local plan preview only (set SUPERCLI_SERVER for persisted execute)")
29
+ console.log("")
30
+ }
31
+
32
+ module.exports = { buildLocalPlan, annotateServerPlan, outputHumanPlan }
package/cli/planner.js ADDED
@@ -0,0 +1,67 @@
1
+ const crypto = require("crypto")
2
+
3
+ function generatePlanId() {
4
+ return "plan_" + crypto.randomBytes(6).toString("hex")
5
+ }
6
+
7
+ function createPlan(cmd, args) {
8
+ const steps = []
9
+
10
+ // Step 1: Resolve command
11
+ steps.push({
12
+ step: 1,
13
+ type: "resolve_command",
14
+ description: "Resolve command definition from config cache"
15
+ })
16
+
17
+ // Step 2: Validate args
18
+ steps.push({
19
+ step: 2,
20
+ type: "validate_args",
21
+ description: "Validate input arguments against schema"
22
+ })
23
+
24
+ // Step 3: Adapter request
25
+ const adapterStep = { step: 3, type: "adapter_request", adapter: cmd.adapter }
26
+ if (cmd.adapter === "openapi") {
27
+ adapterStep.method = (cmd.adapterConfig && cmd.adapterConfig.method) || "GET"
28
+ adapterStep.operationId = cmd.adapterConfig && cmd.adapterConfig.operationId
29
+ adapterStep.description = `Call OpenAPI operation: ${adapterStep.operationId}`
30
+ } else if (cmd.adapter === "http") {
31
+ adapterStep.method = (cmd.adapterConfig && cmd.adapterConfig.method) || "GET"
32
+ adapterStep.url = cmd.adapterConfig && cmd.adapterConfig.url
33
+ adapterStep.description = `HTTP ${adapterStep.method} ${adapterStep.url || "(dynamic)"}`
34
+ } else if (cmd.adapter === "mcp") {
35
+ adapterStep.tool = cmd.adapterConfig && cmd.adapterConfig.tool
36
+ adapterStep.description = `Call MCP tool: ${adapterStep.tool}`
37
+ } else {
38
+ adapterStep.description = `Execute via ${cmd.adapter} adapter`
39
+ }
40
+ steps.push(adapterStep)
41
+
42
+ // Step 4: Transform output
43
+ steps.push({
44
+ step: 4,
45
+ type: "transform_output",
46
+ description: "Normalize response into structured output envelope"
47
+ })
48
+
49
+ // Determine side effects and risk
50
+ const isMutation = !!(cmd.mutation)
51
+ const riskLevel = cmd.risk_level || (isMutation ? "medium" : "safe")
52
+
53
+ return {
54
+ plan_id: generatePlanId(),
55
+ command: `${cmd.namespace}.${cmd.resource}.${cmd.action}`,
56
+ args,
57
+ steps,
58
+ side_effects: isMutation,
59
+ risk_level: riskLevel,
60
+ estimated_duration_ms: cmd.adapter === "http" || cmd.adapter === "openapi" ? 200 : 100,
61
+ status: "planned",
62
+ created_at: new Date().toISOString(),
63
+ expires_at: new Date(Date.now() + 5 * 60 * 1000).toISOString()
64
+ }
65
+ }
66
+
67
+ module.exports = { createPlan }