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
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 }
|
package/cli/executor.js
ADDED
|
@@ -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 }
|
package/cli/help-json.js
ADDED
|
@@ -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 }
|
package/cli/mcp-local.js
ADDED
|
@@ -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 }
|