@volc-emr/emr-cli 0.1.0-beta.0 → 0.1.0-beta.2
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/README.md +81 -269
- package/dist/agent/agent.js +14 -3
- package/dist/agent/executor.js +55 -8
- package/dist/agent/llmPlanner.js +4 -17
- package/dist/commands/config.js +57 -0
- package/dist/commands/init.js +102 -0
- package/dist/commands/run.js +41 -0
- package/dist/core/agent.js +30 -0
- package/dist/core/executor.js +36 -0
- package/dist/core/llm-planner.js +131 -0
- package/dist/core/planner.js +12 -0
- package/dist/core/registry.js +11 -0
- package/dist/core/tool.js +2 -0
- package/dist/core/types.js +2 -0
- package/dist/index.js +8 -174
- package/dist/integrations/volc/client.js +53 -0
- package/dist/integrations/volc/createClusterMemory.js +56 -0
- package/dist/integrations/volc/emr.js +177 -0
- package/dist/integrations/volc/tools/createCluster.js +335 -0
- package/dist/integrations/volc/tools/deleteCluster.js +15 -0
- package/dist/integrations/volc/tools/findClustersToCleanup.js +18 -0
- package/dist/integrations/volc/tools/index.js +15 -0
- package/dist/integrations/volc/tools/listClusters.js +68 -0
- package/dist/runtime/logger.js +118 -4
- package/dist/services/ecsApi.js +159 -0
- package/dist/services/emrApi.js +0 -22
- package/dist/shared/config.js +73 -0
- package/dist/shared/confirm.js +92 -0
- package/dist/shared/llm.js +64 -0
- package/dist/shared/logger.js +122 -0
- package/dist/shared/memory.js +4 -0
- package/dist/shared/prompt.js +9 -0
- package/dist/tools/ecs/createCluster.js +335 -0
- package/dist/tools/ecs/deleteCluster.js +127 -0
- package/dist/tools/ecs/findClustersToCleanup.js +32 -0
- package/dist/tools/ecs/index.js +13 -0
- package/dist/tools/ecs/listClusters.js +68 -0
- package/dist/tools/emr/deleteCluster.js +12 -3
- package/dist/tools/emr/findClustersToCleanup.js +16 -2
- package/dist/tools/emr/index.js +0 -2
- package/dist/tools/registry.js +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerInitCommand = registerInitCommand;
|
|
4
|
+
const config_1 = require("../runtime/config");
|
|
5
|
+
const confirm_1 = require("../runtime/confirm");
|
|
6
|
+
const logger_1 = require("../runtime/logger");
|
|
7
|
+
function registerInitCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("init")
|
|
10
|
+
.description("Interactive wizard to initialize local credentials")
|
|
11
|
+
.option("--access-key <ak>", "Non-interactive access key")
|
|
12
|
+
.option("--secret-key <sk>", "Non-interactive secret key")
|
|
13
|
+
.option("--region <region>", "Default region", "cn-beijing")
|
|
14
|
+
.option("--llm-endpoint <url>", "Optional LLM endpoint (OpenAI compatible)")
|
|
15
|
+
.option("--llm-api-key <key>", "Optional LLM API key")
|
|
16
|
+
.option("--llm-model <model>", "Optional LLM model name")
|
|
17
|
+
.option("-f, --force", "Overwrite existing config without asking")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const existing = (0, config_1.readLocalConfig)();
|
|
20
|
+
const hasExisting = !!(existing.accessKey || existing.secretKey);
|
|
21
|
+
if (hasExisting && !opts.force) {
|
|
22
|
+
logger_1.ui.info(`Detected existing config: ${(0, config_1.configFilePath)()}`);
|
|
23
|
+
const ok = await (0, confirm_1.confirm)("Overwrite existing credentials?");
|
|
24
|
+
if (!ok) {
|
|
25
|
+
logger_1.ui.warn("Cancelled. No changes made.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const needInteractive = !opts.accessKey || !opts.secretKey;
|
|
30
|
+
const session = needInteractive ? (0, confirm_1.createPromptSession)() : null;
|
|
31
|
+
try {
|
|
32
|
+
const accessKey = opts.accessKey ||
|
|
33
|
+
(await session.ask("VOLC Access Key", {
|
|
34
|
+
defaultValue: existing.accessKey
|
|
35
|
+
}));
|
|
36
|
+
if (!accessKey) {
|
|
37
|
+
logger_1.ui.error("Access key is required.");
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const secretKey = opts.secretKey ||
|
|
42
|
+
(await session.ask("VOLC Secret Key", { silent: true }));
|
|
43
|
+
if (!secretKey) {
|
|
44
|
+
logger_1.ui.error("Secret key is required.");
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const region = opts.region ||
|
|
49
|
+
(session
|
|
50
|
+
? await session.ask("Region", {
|
|
51
|
+
defaultValue: existing.region || "cn-beijing"
|
|
52
|
+
})
|
|
53
|
+
: existing.region || "cn-beijing") ||
|
|
54
|
+
"cn-beijing";
|
|
55
|
+
const llmEndpoint = opts.llmEndpoint ??
|
|
56
|
+
(session
|
|
57
|
+
? await session.ask("LLM endpoint (empty to skip)", {
|
|
58
|
+
defaultValue: existing.llm?.endpoint || ""
|
|
59
|
+
})
|
|
60
|
+
: existing.llm?.endpoint || "");
|
|
61
|
+
let llmApiKey = opts.llmApiKey;
|
|
62
|
+
let llmModel = opts.llmModel;
|
|
63
|
+
if (llmEndpoint && session) {
|
|
64
|
+
llmApiKey =
|
|
65
|
+
llmApiKey ??
|
|
66
|
+
(await session.ask("LLM API key", { silent: true }));
|
|
67
|
+
llmModel =
|
|
68
|
+
llmModel ??
|
|
69
|
+
(await session.ask("LLM model", {
|
|
70
|
+
defaultValue: existing.llm?.model || ""
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
const payload = { accessKey, secretKey, region };
|
|
74
|
+
if (llmEndpoint) {
|
|
75
|
+
payload.llm = {
|
|
76
|
+
endpoint: llmEndpoint,
|
|
77
|
+
apiKey: llmApiKey || existing.llm?.apiKey,
|
|
78
|
+
model: llmModel || existing.llm?.model
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const file = (0, config_1.writeLocalConfig)(payload);
|
|
82
|
+
logger_1.ui.success(`Saved to ${file}`);
|
|
83
|
+
logger_1.ui.kv("accessKey", `${accessKey.slice(0, 4)}****`);
|
|
84
|
+
logger_1.ui.kv("secretKey", "********");
|
|
85
|
+
logger_1.ui.kv("region", region);
|
|
86
|
+
if (llmEndpoint) {
|
|
87
|
+
logger_1.ui.kv("llm.endpoint", llmEndpoint);
|
|
88
|
+
if (payload.llm.apiKey)
|
|
89
|
+
logger_1.ui.kv("llm.apiKey", "********");
|
|
90
|
+
if (payload.llm.model)
|
|
91
|
+
logger_1.ui.kv("llm.model", payload.llm.model);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
logger_1.ui.kv("llm", "(not configured)");
|
|
95
|
+
}
|
|
96
|
+
logger_1.ui.dim(`Tip: run \`emr-cli run "列出集群"\` to verify.`);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
session?.close();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRunCommand = registerRunCommand;
|
|
4
|
+
const agent_1 = require("../agent/agent");
|
|
5
|
+
const registry_1 = require("../tools/registry");
|
|
6
|
+
const memory_1 = require("../runtime/memory");
|
|
7
|
+
const config_1 = require("../runtime/config");
|
|
8
|
+
const logger_1 = require("../runtime/logger");
|
|
9
|
+
function registerRunCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command("run <task>")
|
|
12
|
+
.description("Plan and execute a natural language EMR task")
|
|
13
|
+
.option("--dry-run", "Only show plan")
|
|
14
|
+
.option("-y, --yes", "Auto approve high-risk tools (skip confirm)")
|
|
15
|
+
.option("--region <region>", "Override region, default cn-beijing")
|
|
16
|
+
.action(async (task, opts) => {
|
|
17
|
+
if (opts.accessKey)
|
|
18
|
+
process.env.VOLC_ACCESSKEY = opts.accessKey;
|
|
19
|
+
if (opts.secretKey)
|
|
20
|
+
process.env.VOLC_SECRETKEY = opts.secretKey;
|
|
21
|
+
if (opts.region)
|
|
22
|
+
process.env.VOLC_REGION = opts.region;
|
|
23
|
+
try {
|
|
24
|
+
const creds = (0, config_1.resolveCredentials)();
|
|
25
|
+
const cfg = (0, config_1.readLocalConfig)();
|
|
26
|
+
logger_1.ui.dim(`region=${creds.region} llm=${cfg.llm?.endpoint ? "configured" : "not configured"}`);
|
|
27
|
+
const agent = new agent_1.Agent({
|
|
28
|
+
api: registry_1.apiRegistry,
|
|
29
|
+
memory: memory_1.memory
|
|
30
|
+
});
|
|
31
|
+
await agent.run(task, {
|
|
32
|
+
dryRun: opts.dryRun,
|
|
33
|
+
autoApprove: opts.yes
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger_1.ui.error(err?.message || String(err));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Agent = void 0;
|
|
4
|
+
const planner_1 = require("./planner");
|
|
5
|
+
const executor_1 = require("./executor");
|
|
6
|
+
const logger_1 = require("../shared/logger");
|
|
7
|
+
class Agent {
|
|
8
|
+
constructor(ctx) {
|
|
9
|
+
this.ctx = ctx;
|
|
10
|
+
}
|
|
11
|
+
async run(task, options = {}) {
|
|
12
|
+
logger_1.ui.section(`Task`);
|
|
13
|
+
logger_1.ui.info(task);
|
|
14
|
+
const plan = await (0, planner_1.planTask)(task);
|
|
15
|
+
logger_1.ui.section(`Plan (${plan.length} step${plan.length > 1 ? "s" : ""})`);
|
|
16
|
+
plan.forEach((step, i) => {
|
|
17
|
+
logger_1.ui.raw(` ${i + 1}. ${step.tool}`);
|
|
18
|
+
if (step.input && Object.keys(step.input).length > 0) {
|
|
19
|
+
logger_1.ui.dim(` ${JSON.stringify(step.input)}`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (options.dryRun) {
|
|
23
|
+
logger_1.ui.divider();
|
|
24
|
+
logger_1.ui.info("dry-run mode: no Tool will be executed");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await (0, executor_1.executePlan)(plan, this.ctx, { autoApprove: options.autoApprove });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.Agent = Agent;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executePlan = executePlan;
|
|
4
|
+
const registry_1 = require("./registry");
|
|
5
|
+
const confirm_1 = require("../shared/confirm");
|
|
6
|
+
const logger_1 = require("../shared/logger");
|
|
7
|
+
async function executePlan(plan, ctx, options = {}) {
|
|
8
|
+
logger_1.ui.section(`Execute (${plan.length} step${plan.length > 1 ? "s" : ""})`);
|
|
9
|
+
for (const [i, step] of plan.entries()) {
|
|
10
|
+
const tool = registry_1.toolList.find((t) => t.name === step.tool);
|
|
11
|
+
if (!tool) {
|
|
12
|
+
(0, logger_1.logStep)(i, "fail", step.tool, { error: "Tool not found" });
|
|
13
|
+
throw new Error(`Tool not found: ${step.tool}`);
|
|
14
|
+
}
|
|
15
|
+
if (tool.riskLevel === "high") {
|
|
16
|
+
const ok = options.autoApprove
|
|
17
|
+
? true
|
|
18
|
+
: await (0, confirm_1.confirm)(`⚠ 执行高风险 Tool: ${tool.name} ${JSON.stringify(step.input)}`);
|
|
19
|
+
if (!ok) {
|
|
20
|
+
(0, logger_1.logStep)(i, "skip", tool.name, { reason: "user cancelled" });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
(0, logger_1.logStep)(i, "run", tool.name, { input: step.input });
|
|
25
|
+
try {
|
|
26
|
+
const result = await tool.execute(step.input, ctx);
|
|
27
|
+
(0, logger_1.logStep)(i, "done", tool.name, { output: result });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
(0, logger_1.logStep)(i, "fail", tool.name, { error: err?.message || String(err) });
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
logger_1.ui.divider();
|
|
35
|
+
logger_1.ui.success("Execute finished");
|
|
36
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.llmPlanTask = llmPlanTask;
|
|
4
|
+
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
5
|
+
const registry_1 = require("./registry");
|
|
6
|
+
const tools_1 = require("../integrations/volc/tools");
|
|
7
|
+
const llm_1 = require("../shared/llm");
|
|
8
|
+
function buildToolCatalog() {
|
|
9
|
+
return registry_1.toolList.map((t) => ({
|
|
10
|
+
name: t.name,
|
|
11
|
+
description: t.description,
|
|
12
|
+
riskLevel: t.riskLevel || "low",
|
|
13
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(t.input, t.name)
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
function buildSystemPrompt() {
|
|
17
|
+
const catalog = buildToolCatalog();
|
|
18
|
+
return [
|
|
19
|
+
"You are a CLI Agent Planner for Volcengine EMR.",
|
|
20
|
+
"You MUST NOT execute anything directly.",
|
|
21
|
+
"Your only job is to output an ordered plan of tool calls.",
|
|
22
|
+
"",
|
|
23
|
+
"Available tools (JSON catalog):",
|
|
24
|
+
JSON.stringify(catalog, null, 2),
|
|
25
|
+
"",
|
|
26
|
+
"Response format:",
|
|
27
|
+
'You MUST output ONLY a JSON object with shape {"steps":[{"tool":"<name>","input":{...}}]}.',
|
|
28
|
+
"Do NOT include any prose, markdown fences, or explanations.",
|
|
29
|
+
"The very first character of your reply MUST be `{` and the last MUST be `}`.",
|
|
30
|
+
"Rules:",
|
|
31
|
+
"- Only use tools listed above.",
|
|
32
|
+
"- `input` MUST conform to each tool's inputSchema.",
|
|
33
|
+
"- For destructive actions (riskLevel=high), only plan them if the user task clearly asks for it.",
|
|
34
|
+
"- If the task is ambiguous, return at most one safe, read-only step (e.g. listClusters)."
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
function buildUserPrompt(task, context) {
|
|
38
|
+
const ctx = context ? `\n\nContext:\n${JSON.stringify(context, null, 2)}` : "";
|
|
39
|
+
return `Task: ${task}${ctx}`;
|
|
40
|
+
}
|
|
41
|
+
function extractJsonObject(raw) {
|
|
42
|
+
const stripped = raw
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
45
|
+
.replace(/```\s*$/i, "")
|
|
46
|
+
.trim();
|
|
47
|
+
if (stripped.startsWith("{"))
|
|
48
|
+
return stripped;
|
|
49
|
+
const start = stripped.indexOf("{");
|
|
50
|
+
if (start === -1)
|
|
51
|
+
return stripped;
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let inString = false;
|
|
54
|
+
let escape = false;
|
|
55
|
+
for (let i = start; i < stripped.length; i++) {
|
|
56
|
+
const ch = stripped[i];
|
|
57
|
+
if (inString) {
|
|
58
|
+
if (escape) {
|
|
59
|
+
escape = false;
|
|
60
|
+
}
|
|
61
|
+
else if (ch === "\\") {
|
|
62
|
+
escape = true;
|
|
63
|
+
}
|
|
64
|
+
else if (ch === '"') {
|
|
65
|
+
inString = false;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === '"') {
|
|
70
|
+
inString = true;
|
|
71
|
+
}
|
|
72
|
+
else if (ch === "{") {
|
|
73
|
+
depth++;
|
|
74
|
+
}
|
|
75
|
+
else if (ch === "}") {
|
|
76
|
+
depth--;
|
|
77
|
+
if (depth === 0)
|
|
78
|
+
return stripped.slice(start, i + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return stripped.slice(start);
|
|
82
|
+
}
|
|
83
|
+
function tryParsePlan(raw) {
|
|
84
|
+
const jsonText = extractJsonObject(raw);
|
|
85
|
+
const parsed = JSON.parse(jsonText);
|
|
86
|
+
const steps = Array.isArray(parsed) ? parsed : parsed?.steps;
|
|
87
|
+
if (!Array.isArray(steps)) {
|
|
88
|
+
throw new Error("LLM plan has no `steps` array");
|
|
89
|
+
}
|
|
90
|
+
return steps.map((s, i) => {
|
|
91
|
+
if (!s || typeof s.tool !== "string") {
|
|
92
|
+
throw new Error(`Step ${i} is missing "tool"`);
|
|
93
|
+
}
|
|
94
|
+
return { tool: s.tool, input: s.input ?? {} };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function validatePlanAgainstTools(steps) {
|
|
98
|
+
return steps.map((step) => {
|
|
99
|
+
const tool = registry_1.toolList.find((t) => t.name === step.tool);
|
|
100
|
+
if (!tool)
|
|
101
|
+
throw new Error(`LLM chose unknown tool: ${step.tool}`);
|
|
102
|
+
const result = tool.input.safeParse(step.input ?? {});
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
throw new Error(`LLM produced invalid input for ${step.tool}: ${result.error.message}`);
|
|
105
|
+
}
|
|
106
|
+
return { tool: step.tool, input: result.data };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function llmPlanTask(task, llm) {
|
|
110
|
+
const messages = [
|
|
111
|
+
{ role: "system", content: buildSystemPrompt() },
|
|
112
|
+
{ role: "user", content: buildUserPrompt(task) }
|
|
113
|
+
];
|
|
114
|
+
const content = await (0, llm_1.chatCompletion)(llm, { messages });
|
|
115
|
+
const steps = tryParsePlan(content);
|
|
116
|
+
const validated = validatePlanAgainstTools(steps);
|
|
117
|
+
const expanded = [];
|
|
118
|
+
for (const step of validated) {
|
|
119
|
+
expanded.push(step);
|
|
120
|
+
if (step.tool === "findClustersToCleanup") {
|
|
121
|
+
const found = await tools_1.emrApi.findClustersToCleanup(step.input || {});
|
|
122
|
+
for (const c of found) {
|
|
123
|
+
expanded.push({
|
|
124
|
+
tool: "deleteCluster",
|
|
125
|
+
input: { ClusterId: c.ClusterId }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return expanded;
|
|
131
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planTask = planTask;
|
|
4
|
+
const config_1 = require("../shared/config");
|
|
5
|
+
const llm_planner_1 = require("./llm-planner");
|
|
6
|
+
async function planTask(task, _options = {}) {
|
|
7
|
+
const llm = (0, config_1.resolveLlmConfig)();
|
|
8
|
+
if (!llm) {
|
|
9
|
+
throw new Error("LLM is not configured. Run `volc-emr-agent config set-llm` or set VOLC_LLM_ENDPOINT first.");
|
|
10
|
+
}
|
|
11
|
+
return (0, llm_planner_1.llmPlanTask)(task, llm);
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apiRegistry = exports.toolList = exports.tools = void 0;
|
|
4
|
+
const tools_1 = require("../integrations/volc/tools");
|
|
5
|
+
exports.tools = {
|
|
6
|
+
...tools_1.emrTools
|
|
7
|
+
};
|
|
8
|
+
exports.toolList = Object.values(exports.tools);
|
|
9
|
+
exports.apiRegistry = {
|
|
10
|
+
emr: tools_1.emrApi
|
|
11
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -2,181 +2,15 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const config_1 = require("./runtime/config");
|
|
9
|
-
const confirm_1 = require("./runtime/confirm");
|
|
5
|
+
const config_1 = require("./commands/config");
|
|
6
|
+
const init_1 = require("./commands/init");
|
|
7
|
+
const run_1 = require("./commands/run");
|
|
10
8
|
const program = new commander_1.Command();
|
|
11
9
|
program
|
|
12
|
-
.name("
|
|
13
|
-
.description("Volcengine EMR CLI Agent (
|
|
10
|
+
.name("emr-cli")
|
|
11
|
+
.description("Volcengine EMR CLI Agent (Tool-first, Plan/Execute, LLM-driven)")
|
|
14
12
|
.version("0.1.0");
|
|
15
|
-
program
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.option("--dry-run", "Only show plan")
|
|
19
|
-
.option("-y, --yes", "Auto approve high-risk tools (skip confirm)")
|
|
20
|
-
.option("--region <region>", "Override region, default cn-beijing")
|
|
21
|
-
.action(async (task, opts) => {
|
|
22
|
-
if (opts.accessKey)
|
|
23
|
-
process.env.VOLC_ACCESSKEY = opts.accessKey;
|
|
24
|
-
if (opts.secretKey)
|
|
25
|
-
process.env.VOLC_SECRETKEY = opts.secretKey;
|
|
26
|
-
if (opts.region)
|
|
27
|
-
process.env.VOLC_REGION = opts.region;
|
|
28
|
-
(0, config_1.resolveCredentials)();
|
|
29
|
-
const agent = new agent_1.Agent({
|
|
30
|
-
api: registry_1.apiRegistry,
|
|
31
|
-
memory: memory_1.memory
|
|
32
|
-
});
|
|
33
|
-
await agent.run(task, {
|
|
34
|
-
dryRun: opts.dryRun,
|
|
35
|
-
autoApprove: opts.yes
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
program
|
|
39
|
-
.command("init")
|
|
40
|
-
.description("Interactive wizard to initialize local credentials")
|
|
41
|
-
.option("--access-key <ak>", "Non-interactive access key")
|
|
42
|
-
.option("--secret-key <sk>", "Non-interactive secret key")
|
|
43
|
-
.option("--region <region>", "Default region", "cn-beijing")
|
|
44
|
-
.option("--llm-endpoint <url>", "Optional LLM endpoint (OpenAI compatible)")
|
|
45
|
-
.option("--llm-api-key <key>", "Optional LLM API key")
|
|
46
|
-
.option("--llm-model <model>", "Optional LLM model name")
|
|
47
|
-
.option("-f, --force", "Overwrite existing config without asking")
|
|
48
|
-
.action(async (opts) => {
|
|
49
|
-
const existing = (0, config_1.readLocalConfig)();
|
|
50
|
-
const hasExisting = !!(existing.accessKey || existing.secretKey);
|
|
51
|
-
if (hasExisting && !opts.force) {
|
|
52
|
-
console.log(`Detected existing config: ${(0, config_1.configFilePath)()}`);
|
|
53
|
-
const ok = await (0, confirm_1.confirm)("Overwrite existing credentials?");
|
|
54
|
-
if (!ok) {
|
|
55
|
-
console.log("Cancelled. No changes made.");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const needInteractive = !opts.accessKey || !opts.secretKey;
|
|
60
|
-
const session = needInteractive ? (0, confirm_1.createPromptSession)() : null;
|
|
61
|
-
try {
|
|
62
|
-
const accessKey = opts.accessKey ||
|
|
63
|
-
(await session.ask("VOLC Access Key", {
|
|
64
|
-
defaultValue: existing.accessKey
|
|
65
|
-
}));
|
|
66
|
-
if (!accessKey) {
|
|
67
|
-
console.error("Access key is required.");
|
|
68
|
-
process.exitCode = 1;
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const secretKey = opts.secretKey ||
|
|
72
|
-
(await session.ask("VOLC Secret Key", { silent: true }));
|
|
73
|
-
if (!secretKey) {
|
|
74
|
-
console.error("Secret key is required.");
|
|
75
|
-
process.exitCode = 1;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const region = opts.region ||
|
|
79
|
-
(session
|
|
80
|
-
? await session.ask("Region", {
|
|
81
|
-
defaultValue: existing.region || "cn-beijing"
|
|
82
|
-
})
|
|
83
|
-
: existing.region || "cn-beijing") ||
|
|
84
|
-
"cn-beijing";
|
|
85
|
-
const llmEndpoint = opts.llmEndpoint ??
|
|
86
|
-
(session
|
|
87
|
-
? await session.ask("LLM endpoint (empty to skip)", {
|
|
88
|
-
defaultValue: existing.llm?.endpoint || ""
|
|
89
|
-
})
|
|
90
|
-
: existing.llm?.endpoint || "");
|
|
91
|
-
let llmApiKey = opts.llmApiKey;
|
|
92
|
-
let llmModel = opts.llmModel;
|
|
93
|
-
if (llmEndpoint && session) {
|
|
94
|
-
llmApiKey =
|
|
95
|
-
llmApiKey ??
|
|
96
|
-
(await session.ask("LLM API key", { silent: true }));
|
|
97
|
-
llmModel =
|
|
98
|
-
llmModel ??
|
|
99
|
-
(await session.ask("LLM model", {
|
|
100
|
-
defaultValue: existing.llm?.model || ""
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
const payload = { accessKey, secretKey, region };
|
|
104
|
-
if (llmEndpoint) {
|
|
105
|
-
payload.llm = {
|
|
106
|
-
endpoint: llmEndpoint,
|
|
107
|
-
apiKey: llmApiKey || existing.llm?.apiKey,
|
|
108
|
-
model: llmModel || existing.llm?.model
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const file = (0, config_1.writeLocalConfig)(payload);
|
|
112
|
-
console.log(`\n✓ Saved to ${file}`);
|
|
113
|
-
console.log({
|
|
114
|
-
accessKey: `${accessKey.slice(0, 4)}****`,
|
|
115
|
-
secretKey: "********",
|
|
116
|
-
region,
|
|
117
|
-
llm: llmEndpoint
|
|
118
|
-
? {
|
|
119
|
-
endpoint: llmEndpoint,
|
|
120
|
-
apiKey: payload.llm.apiKey ? "********" : undefined,
|
|
121
|
-
model: payload.llm.model || undefined
|
|
122
|
-
}
|
|
123
|
-
: "(not configured)"
|
|
124
|
-
});
|
|
125
|
-
console.log("\nTip: run `volc-emr-agent run \"列出集群\"` to verify.");
|
|
126
|
-
}
|
|
127
|
-
finally {
|
|
128
|
-
session?.close();
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
const config = program.command("config").description("Manage local credentials");
|
|
132
|
-
config
|
|
133
|
-
.command("set-credentials")
|
|
134
|
-
.description("Save AK/SK/region to local config (~/.volc-emr/config.json)")
|
|
135
|
-
.requiredOption("--access-key <ak>")
|
|
136
|
-
.requiredOption("--secret-key <sk>")
|
|
137
|
-
.option("--region <region>", "default cn-beijing", "cn-beijing")
|
|
138
|
-
.action((opts) => {
|
|
139
|
-
const file = (0, config_1.writeLocalConfig)({
|
|
140
|
-
accessKey: opts.accessKey,
|
|
141
|
-
secretKey: opts.secretKey,
|
|
142
|
-
region: opts.region
|
|
143
|
-
});
|
|
144
|
-
console.log(`Saved: ${file}`);
|
|
145
|
-
});
|
|
146
|
-
config
|
|
147
|
-
.command("set-llm")
|
|
148
|
-
.description("Save LLM endpoint config to local config")
|
|
149
|
-
.requiredOption("--endpoint <url>", "OpenAI-compatible Chat Completions URL")
|
|
150
|
-
.option("--api-key <key>")
|
|
151
|
-
.option("--model <model>")
|
|
152
|
-
.action((opts) => {
|
|
153
|
-
const file = (0, config_1.writeLocalConfig)({
|
|
154
|
-
llm: {
|
|
155
|
-
endpoint: opts.endpoint,
|
|
156
|
-
apiKey: opts.apiKey,
|
|
157
|
-
model: opts.model
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
console.log(`Saved LLM config: ${file}`);
|
|
161
|
-
});
|
|
162
|
-
config
|
|
163
|
-
.command("show")
|
|
164
|
-
.description("Show current local config (secrets are masked)")
|
|
165
|
-
.action(() => {
|
|
166
|
-
const cfg = (0, config_1.readLocalConfig)();
|
|
167
|
-
const masked = {
|
|
168
|
-
...cfg,
|
|
169
|
-
accessKey: cfg.accessKey ? `${cfg.accessKey.slice(0, 4)}****` : undefined,
|
|
170
|
-
secretKey: cfg.secretKey ? "********" : undefined,
|
|
171
|
-
llm: cfg.llm
|
|
172
|
-
? {
|
|
173
|
-
endpoint: cfg.llm.endpoint,
|
|
174
|
-
apiKey: cfg.llm.apiKey ? "********" : undefined,
|
|
175
|
-
model: cfg.llm.model
|
|
176
|
-
}
|
|
177
|
-
: undefined
|
|
178
|
-
};
|
|
179
|
-
console.log("file:", (0, config_1.configFilePath)());
|
|
180
|
-
console.log(masked);
|
|
181
|
-
});
|
|
13
|
+
(0, run_1.registerRunCommand)(program);
|
|
14
|
+
(0, init_1.registerInitCommand)(program);
|
|
15
|
+
(0, config_1.registerConfigCommand)(program);
|
|
182
16
|
program.parse();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getEmrService = getEmrService;
|
|
4
|
+
exports.callEmr = callEmr;
|
|
5
|
+
const openapi_1 = require("@volcengine/openapi");
|
|
6
|
+
const config_1 = require("../../shared/config");
|
|
7
|
+
const EMR_API_VERSION = "2023-08-15";
|
|
8
|
+
let cachedService = null;
|
|
9
|
+
let cachedKey = null;
|
|
10
|
+
function buildService(creds) {
|
|
11
|
+
const service = new openapi_1.Service({
|
|
12
|
+
host: `emr.${creds.region}.volcengineapi.com`,
|
|
13
|
+
serviceName: "emr",
|
|
14
|
+
region: creds.region,
|
|
15
|
+
accessKeyId: creds.accessKey,
|
|
16
|
+
secretKey: creds.secretKey,
|
|
17
|
+
defaultVersion: EMR_API_VERSION
|
|
18
|
+
});
|
|
19
|
+
return service;
|
|
20
|
+
}
|
|
21
|
+
function getEmrService(cli = {}) {
|
|
22
|
+
const creds = (0, config_1.resolveCredentials)(cli);
|
|
23
|
+
const key = `${creds.region}:${creds.accessKey}`;
|
|
24
|
+
if (cachedService && cachedKey === key)
|
|
25
|
+
return cachedService;
|
|
26
|
+
cachedService = buildService(creds);
|
|
27
|
+
cachedKey = key;
|
|
28
|
+
return cachedService;
|
|
29
|
+
}
|
|
30
|
+
async function callEmr(action, body = {}, options = {}) {
|
|
31
|
+
const service = getEmrService();
|
|
32
|
+
const fetchApi = service.createJSONAPI(action, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
Version: options.version || EMR_API_VERSION,
|
|
35
|
+
contentType: "json"
|
|
36
|
+
});
|
|
37
|
+
const response = await fetchApi(body);
|
|
38
|
+
if (response?.ResponseMetadata?.Error) {
|
|
39
|
+
const err = response.ResponseMetadata.Error;
|
|
40
|
+
let message = err?.Message || err?.CodeN || err?.Code || "Volcengine EMR error";
|
|
41
|
+
if (typeof message === "string" && /\{\d+\}/.test(message)) {
|
|
42
|
+
try {
|
|
43
|
+
const bodyPreview = JSON.stringify(body);
|
|
44
|
+
message = `${message} (body=${bodyPreview})`;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* ignore */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`[${action}] ${message}`);
|
|
51
|
+
}
|
|
52
|
+
return response?.Result;
|
|
53
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readCreateClusterMemory = readCreateClusterMemory;
|
|
7
|
+
exports.writeCreateClusterMemory = writeCreateClusterMemory;
|
|
8
|
+
exports.clearCreateClusterMemory = clearCreateClusterMemory;
|
|
9
|
+
exports.createClusterMemoryPath = createClusterMemoryPath;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const MEMORY_DIR = process.env.VOLC_EMR_CONFIG_DIR || path_1.default.join(os_1.default.homedir(), ".volc-emr");
|
|
14
|
+
const MEMORY_FILE = path_1.default.join(MEMORY_DIR, "create-cluster-memory.json");
|
|
15
|
+
function readCreateClusterMemory() {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs_1.default.existsSync(MEMORY_FILE))
|
|
18
|
+
return {};
|
|
19
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(MEMORY_FILE, "utf-8"));
|
|
20
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeCreateClusterMemory(patch) {
|
|
27
|
+
fs_1.default.mkdirSync(MEMORY_DIR, { recursive: true, mode: 0o700 });
|
|
28
|
+
const prev = readCreateClusterMemory();
|
|
29
|
+
const merged = { ...prev };
|
|
30
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
31
|
+
if (v === undefined || v === null)
|
|
32
|
+
continue;
|
|
33
|
+
if (typeof v === "string" && !v.trim())
|
|
34
|
+
continue;
|
|
35
|
+
if (Array.isArray(v) && v.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
merged[k] = v;
|
|
38
|
+
}
|
|
39
|
+
merged.updatedAt = Date.now();
|
|
40
|
+
fs_1.default.writeFileSync(MEMORY_FILE, JSON.stringify(merged, null, 2), {
|
|
41
|
+
mode: 0o600
|
|
42
|
+
});
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
45
|
+
function clearCreateClusterMemory() {
|
|
46
|
+
try {
|
|
47
|
+
if (fs_1.default.existsSync(MEMORY_FILE))
|
|
48
|
+
fs_1.default.unlinkSync(MEMORY_FILE);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function createClusterMemoryPath() {
|
|
55
|
+
return MEMORY_FILE;
|
|
56
|
+
}
|