@kiran_nandi_123/conxa 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/README.md +44 -0
- package/bin/conxa.js +7 -0
- package/lib/browser.js +129 -0
- package/lib/cli.js +398 -0
- package/lib/config.js +48 -0
- package/lib/package.json +16 -0
- package/lib/resolver/cache.js +89 -0
- package/lib/resolver/git.js +76 -0
- package/lib/resolver/installed.js +65 -0
- package/lib/resolver/registry.js +88 -0
- package/lib/run.js +367 -0
- package/lib/runtime.js +6 -0
- package/lib/search.js +51 -0
- package/lib/server.js +516 -0
- package/package.json +23 -0
package/lib/runtime.js
ADDED
package/lib/search.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* In-process search over the resolver chain.
|
|
4
|
+
*
|
|
5
|
+
* No database, no FTS, no background indexing. On every call we ask each
|
|
6
|
+
* resolver for its top matches and merge by (plugin_id, slug). Designed for
|
|
7
|
+
* <100-plugin scale; swap behind this same interface when scale demands a real
|
|
8
|
+
* index.
|
|
9
|
+
*
|
|
10
|
+
* Resolver order: installed > cache > registry. Each resolver exposes
|
|
11
|
+
* search(query, limit) and getManifest(plugin_id).
|
|
12
|
+
*/
|
|
13
|
+
const installed = require("./resolver/installed");
|
|
14
|
+
const cache = require("./resolver/cache");
|
|
15
|
+
const registry = require("./resolver/registry");
|
|
16
|
+
|
|
17
|
+
const _SOURCE_PRIORITY = { "installed": 3, "cache": 2 };
|
|
18
|
+
|
|
19
|
+
function _key(item) {
|
|
20
|
+
const slug = item.slug || (item.skills && item.skills[0]) || "";
|
|
21
|
+
return `${item.plugin_id || ""}::${slug}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function search(query, limit) {
|
|
25
|
+
const cap = Math.max(1, Math.min(50, Number(limit) || 20));
|
|
26
|
+
const remoteResults = await registry.search(query, cap);
|
|
27
|
+
const local = [
|
|
28
|
+
...installed.search(query, cap),
|
|
29
|
+
...cache.search(query, cap),
|
|
30
|
+
];
|
|
31
|
+
const merged = new Map();
|
|
32
|
+
for (const item of [...local, ...remoteResults]) {
|
|
33
|
+
const k = _key(item);
|
|
34
|
+
const existing = merged.get(k);
|
|
35
|
+
if (!existing) { merged.set(k, item); continue; }
|
|
36
|
+
const a = _SOURCE_PRIORITY[existing.source] || 1;
|
|
37
|
+
const b = _SOURCE_PRIORITY[item.source] || 1;
|
|
38
|
+
if (b > a) merged.set(k, item);
|
|
39
|
+
}
|
|
40
|
+
return Array.from(merged.values()).slice(0, cap);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function getManifest(pluginId) {
|
|
44
|
+
return (
|
|
45
|
+
(await installed.getManifest(pluginId)) ||
|
|
46
|
+
(await cache.getManifest(pluginId)) ||
|
|
47
|
+
(await registry.getManifest(pluginId))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { search, getManifest };
|
package/lib/server.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
4
|
+
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = require("@modelcontextprotocol/sdk/types.js");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
// Write PID so bootstrap.js can detect a running server (prevents duplicate forks)
|
|
11
|
+
const _PID_FILE = path.join(os.homedir(), ".conxa", "runtime", "server.pid");
|
|
12
|
+
try { fs.writeFileSync(_PID_FILE, String(process.pid)); } catch (_) {}
|
|
13
|
+
const _cleanPid = () => { try { fs.unlinkSync(_PID_FILE); } catch (_) {} };
|
|
14
|
+
process.on("exit", _cleanPid);
|
|
15
|
+
process.on("SIGINT", () => process.exit(0));
|
|
16
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
17
|
+
|
|
18
|
+
const { getPluginConfig, getPluginDir, getAuthJson, getRegistry } = require("./config");
|
|
19
|
+
const { getCachedBrowser, isAuthenticated, getAuthContext, gracefulShutdown } = require("./browser");
|
|
20
|
+
const {
|
|
21
|
+
appendRecoveryEvent, interpolate, enrichStepsWithRecovery,
|
|
22
|
+
waitForUrlState, runPlan, runSkill, checkRetryBudget, clearRetryBudget,
|
|
23
|
+
} = require("./run");
|
|
24
|
+
|
|
25
|
+
// ─── Registry helpers ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
// Returns { slug → { pluginSlug, skill, skillDir } } for fast lookup
|
|
28
|
+
function buildSkillIndex() {
|
|
29
|
+
const registry = getRegistry();
|
|
30
|
+
const index = {};
|
|
31
|
+
for (const [pluginSlug, entry] of Object.entries(registry)) {
|
|
32
|
+
const pluginDir = getPluginDir(pluginSlug);
|
|
33
|
+
for (const skill of (entry.skills || [])) {
|
|
34
|
+
const key = `${pluginSlug}:${skill.slug}`;
|
|
35
|
+
index[key] = { pluginSlug, skill, skillDir: path.join(pluginDir, skill.path || `skills/${skill.slug}`) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return index;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveSkill(pluginSlug, skillSlug, index) {
|
|
42
|
+
// Exact match with plugin prefix
|
|
43
|
+
if (pluginSlug) {
|
|
44
|
+
const key = `${pluginSlug}:${skillSlug}`;
|
|
45
|
+
if (index[key]) return index[key];
|
|
46
|
+
// Try slug normalization
|
|
47
|
+
for (const [k, v] of Object.entries(index)) {
|
|
48
|
+
if (v.pluginSlug === pluginSlug && (v.skill.slug === skillSlug || v.skill.slug.replace(/-/g, "_") === skillSlug.replace(/-/g, "_")))
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Slug-only: match across all plugins
|
|
53
|
+
for (const v of Object.values(index)) {
|
|
54
|
+
if (v.skill.slug === skillSlug || v.skill.slug.replace(/-/g, "_") === skillSlug.replace(/-/g, "_"))
|
|
55
|
+
return v;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── MCP server ───────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const server = new Server(
|
|
63
|
+
{ name: "conxa", version: "1.0.0" },
|
|
64
|
+
{ capabilities: { tools: {} } },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
68
|
+
const tools = [
|
|
69
|
+
{
|
|
70
|
+
name: "list_skills",
|
|
71
|
+
description: "List all installed Conxa skills across all plugins. Pass plugin slug to filter. Call once to plan, then call execute_plan immediately.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
plugin: { type: "string", description: "Optional plugin slug to filter skills" },
|
|
76
|
+
},
|
|
77
|
+
required: [],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "execute_plan",
|
|
82
|
+
description: "Run one or more skills in sequence in a single browser session. Auth is 100% automatic. On failure: L4 (vision) and L5 (intent) data are returned. Fix execution.json or pass step_overrides, then retry with resume_from.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
skills: {
|
|
87
|
+
type: "array",
|
|
88
|
+
description: "Ordered list of skills to run",
|
|
89
|
+
items: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
plugin: { type: "string", description: "Plugin slug (optional if skill slug is unique across all plugins)" },
|
|
93
|
+
slug: { type: "string", description: "Skill slug from list_skills" },
|
|
94
|
+
inputs: { type: "object", description: "Input values for this skill" },
|
|
95
|
+
},
|
|
96
|
+
required: ["slug"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
resume_from: {
|
|
100
|
+
type: "integer",
|
|
101
|
+
description: "0-based step index to resume from. Use after fixing execution.json.",
|
|
102
|
+
},
|
|
103
|
+
step_overrides: {
|
|
104
|
+
type: "object",
|
|
105
|
+
description: "Per-skill, per-step overrides. Keyed by skill slug then 0-based step index. Non-persistent.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["skills"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "read_skill_files",
|
|
113
|
+
description: "DEBUG ONLY — inspect raw execution steps and recovery data for a skill.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
slug: { type: "string" },
|
|
118
|
+
plugin: { type: "string", description: "Plugin slug (optional)" },
|
|
119
|
+
},
|
|
120
|
+
required: ["slug"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "search_registry",
|
|
125
|
+
description: "Search installed, cached, and registry plugins. Returns lightweight metadata only — no execution steps or images. Runtime handles ranking internally.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
query: { type: "string", description: "Search query (matches name, description, tags, slug)" },
|
|
130
|
+
limit: { type: "integer", description: "Max results (default 20, capped at 50)" },
|
|
131
|
+
},
|
|
132
|
+
required: ["query"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "get_skill_metadata",
|
|
137
|
+
description: "Fetch the manifest (plugin.json) for an un-installed plugin. Used to preview before install_plugin.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
plugin_id: { type: "string", description: "Plugin id from search_registry (e.g. 'acme/hr')" },
|
|
142
|
+
},
|
|
143
|
+
required: ["plugin_id"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "install_plugin",
|
|
148
|
+
description: "Install a plugin into ~/.conxa/plugins/. plugin_ref is a plugin_id, owner/repo, owner/repo@version, or absolute path.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
plugin_ref: { type: "string" },
|
|
153
|
+
version: { type: "string", description: "Optional version override" },
|
|
154
|
+
},
|
|
155
|
+
required: ["plugin_ref"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "uninstall_plugin",
|
|
160
|
+
description: "Remove an installed plugin and its data from ~/.conxa/plugins/.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: { slug: { type: "string", description: "Installed plugin slug" } },
|
|
164
|
+
required: ["slug"],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
console.error(`[ListTools] Registering ${tools.length} tools`);
|
|
169
|
+
return { tools };
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
173
|
+
const { name, arguments: args } = request.params;
|
|
174
|
+
const skillIndex = buildSkillIndex();
|
|
175
|
+
|
|
176
|
+
// ── list_skills ───────────────────────────────────────────────────────────
|
|
177
|
+
if (name === "list_skills") {
|
|
178
|
+
const filterPlugin = args && args.plugin ? String(args.plugin) : null;
|
|
179
|
+
const registry = getRegistry();
|
|
180
|
+
const skills = [];
|
|
181
|
+
for (const [pluginSlug, entry] of Object.entries(registry)) {
|
|
182
|
+
if (filterPlugin && pluginSlug !== filterPlugin) continue;
|
|
183
|
+
const pluginDir = getPluginDir(pluginSlug);
|
|
184
|
+
for (const skill of (entry.skills || [])) {
|
|
185
|
+
const skillDir = path.join(pluginDir, skill.path || `skills/${skill.slug}`);
|
|
186
|
+
const iPath = path.join(skillDir, "input.json");
|
|
187
|
+
const mdPath = path.join(skillDir, "SKILL.md");
|
|
188
|
+
let requiredInputs = [], inputProps = {}, description = skill.slug;
|
|
189
|
+
if (fs.existsSync(iPath)) {
|
|
190
|
+
try {
|
|
191
|
+
const s = JSON.parse(fs.readFileSync(iPath, "utf8"));
|
|
192
|
+
requiredInputs = s.required || [];
|
|
193
|
+
inputProps = s.properties || {};
|
|
194
|
+
description = s.description || description;
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
}
|
|
197
|
+
// Fall back to first line of SKILL.md as description
|
|
198
|
+
if (description === skill.slug && fs.existsSync(mdPath)) {
|
|
199
|
+
try {
|
|
200
|
+
const firstLine = fs.readFileSync(mdPath, "utf8").split("\n").find(l => l.startsWith("# "));
|
|
201
|
+
if (firstLine) description = firstLine.replace(/^#\s*/, "").trim();
|
|
202
|
+
} catch (_) {}
|
|
203
|
+
}
|
|
204
|
+
skills.push({ plugin: pluginSlug, slug: skill.slug, description, required_inputs: requiredInputs, inputs: inputProps });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.error(`[list_skills] Returning ${skills.length} skills`);
|
|
208
|
+
return { content: [{ type: "text", text: [
|
|
209
|
+
"RULES: auth automatic, no confirmations, no read_skill_files in normal flow.",
|
|
210
|
+
"FLOW: list_skills → ask for any missing inputs → execute_plan({ skills: [{ plugin, slug, inputs }] })",
|
|
211
|
+
"",
|
|
212
|
+
"SKILLS:",
|
|
213
|
+
JSON.stringify(skills, null, 2),
|
|
214
|
+
].join("\n") }] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── search_registry ──────────────────────────────────────────────────────
|
|
218
|
+
if (name === "search_registry") {
|
|
219
|
+
const query = String((args && args.query) || "").trim();
|
|
220
|
+
const limit = Number.isInteger(args && args.limit) ? args.limit : 20;
|
|
221
|
+
if (!query) return { content: [{ type: "text", text: "search_registry: query is required" }] };
|
|
222
|
+
const search = require("./search");
|
|
223
|
+
const results = await search.search(query, limit);
|
|
224
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── get_skill_metadata ───────────────────────────────────────────────────
|
|
228
|
+
if (name === "get_skill_metadata") {
|
|
229
|
+
const pluginId = String((args && args.plugin_id) || "").trim();
|
|
230
|
+
if (!pluginId) return { content: [{ type: "text", text: "get_skill_metadata: plugin_id is required" }] };
|
|
231
|
+
const search = require("./search");
|
|
232
|
+
const manifest = await search.getManifest(pluginId);
|
|
233
|
+
if (!manifest) return { content: [{ type: "text", text: `Plugin not found: ${pluginId}` }] };
|
|
234
|
+
return { content: [{ type: "text", text: JSON.stringify(manifest, null, 2) }] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── install_plugin ───────────────────────────────────────────────────────
|
|
238
|
+
if (name === "install_plugin") {
|
|
239
|
+
const ref = String((args && args.plugin_ref) || "").trim();
|
|
240
|
+
const version = (args && args.version) ? String(args.version) : null;
|
|
241
|
+
if (!ref) return { content: [{ type: "text", text: "install_plugin: plugin_ref is required" }] };
|
|
242
|
+
const cli = require("./cli");
|
|
243
|
+
try {
|
|
244
|
+
const installRef = version && !ref.includes("@") ? `${ref}@${version}` : ref;
|
|
245
|
+
const entry = await cli.install(installRef);
|
|
246
|
+
return { content: [{ type: "text", text: `Installed ${entry.slug} v${entry.version}. Skills: ${(entry.skills || []).map(s => s.slug).join(", ")}` }] };
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return { content: [{ type: "text", text: `install_plugin failed: ${e.message}` }] };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── uninstall_plugin ─────────────────────────────────────────────────────
|
|
253
|
+
if (name === "uninstall_plugin") {
|
|
254
|
+
const slug = String((args && args.slug) || "").trim();
|
|
255
|
+
if (!slug) return { content: [{ type: "text", text: "uninstall_plugin: slug is required" }] };
|
|
256
|
+
const cli = require("./cli");
|
|
257
|
+
try {
|
|
258
|
+
cli.uninstall(slug);
|
|
259
|
+
return { content: [{ type: "text", text: `Uninstalled ${slug}` }] };
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return { content: [{ type: "text", text: `uninstall_plugin failed: ${e.message}` }] };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── read_skill_files ─────────────────────────────────────────────────────
|
|
266
|
+
if (name === "read_skill_files") {
|
|
267
|
+
const slugArg = args && args.slug ? String(args.slug) : "";
|
|
268
|
+
const pluginArg = args && args.plugin ? String(args.plugin) : null;
|
|
269
|
+
const resolved = resolveSkill(pluginArg, slugArg, skillIndex);
|
|
270
|
+
if (!resolved) return { content: [{ type: "text", text: `Skill not found: ${slugArg}. Use list_skills.` }] };
|
|
271
|
+
const { skillDir } = resolved;
|
|
272
|
+
const execPath = path.join(skillDir, "execution.json");
|
|
273
|
+
const recPath = path.join(skillDir, "recovery.json");
|
|
274
|
+
const mdPath = path.join(skillDir, "SKILL.md");
|
|
275
|
+
const iPath = path.join(skillDir, "input.json");
|
|
276
|
+
const inputSchema = fs.existsSync(iPath) ? JSON.parse(fs.readFileSync(iPath, "utf8")) : null;
|
|
277
|
+
const requiredInputs = inputSchema && inputSchema.required ? inputSchema.required : [];
|
|
278
|
+
const rawExecution = fs.existsSync(execPath) ? JSON.parse(fs.readFileSync(execPath, "utf8")) : null;
|
|
279
|
+
const rawRecovery = fs.existsSync(recPath) ? JSON.parse(fs.readFileSync(recPath, "utf8")) : null;
|
|
280
|
+
const rawSteps = Array.isArray(rawExecution) ? rawExecution
|
|
281
|
+
: (rawExecution && Array.isArray(rawExecution.steps)) ? rawExecution.steps : [];
|
|
282
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
283
|
+
plugin: resolved.pluginSlug,
|
|
284
|
+
slug: resolved.skill.slug,
|
|
285
|
+
skill_md: fs.existsSync(mdPath) ? fs.readFileSync(mdPath, "utf8") : null,
|
|
286
|
+
required_inputs: requiredInputs,
|
|
287
|
+
instruction: requiredInputs.length > 0
|
|
288
|
+
? `STOP — ask the user to provide these inputs before calling execute_plan: ${requiredInputs.join(", ")}`
|
|
289
|
+
: "No inputs required. You may call execute_plan directly.",
|
|
290
|
+
execution: enrichStepsWithRecovery(rawSteps, rawRecovery),
|
|
291
|
+
recovery: rawRecovery,
|
|
292
|
+
}, null, 2) }] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── execute_plan ─────────────────────────────────────────────────────────
|
|
296
|
+
if (name === "execute_plan") {
|
|
297
|
+
const skillRuns = (args && Array.isArray(args.skills)) ? args.skills : [];
|
|
298
|
+
if (skillRuns.length === 0)
|
|
299
|
+
return { content: [{ type: "text", text: "execute_plan: provide { skills: [{ slug, inputs }] }" }] };
|
|
300
|
+
|
|
301
|
+
const resumeFrom = (Number.isInteger(args.resume_from) && args.resume_from > 0) ? args.resume_from : 0;
|
|
302
|
+
const overrides = (args.step_overrides && typeof args.step_overrides === "object") ? args.step_overrides : {};
|
|
303
|
+
const isFlatOverrides = Object.keys(overrides).length > 0
|
|
304
|
+
&& Object.keys(overrides).every(k => /^\d+$/.test(k));
|
|
305
|
+
|
|
306
|
+
// Resolve each skill run
|
|
307
|
+
const resolved = [];
|
|
308
|
+
for (const run of skillRuns) {
|
|
309
|
+
const slug = String(run.slug || "");
|
|
310
|
+
const pluginArg = run.plugin ? String(run.plugin) : null;
|
|
311
|
+
const inputs = (run.inputs && typeof run.inputs === "object") ? run.inputs : {};
|
|
312
|
+
const found = resolveSkill(pluginArg, slug, skillIndex);
|
|
313
|
+
if (!found) return { content: [{ type: "text", text: `Skill not found: ${slug}. Call list_skills.` }] };
|
|
314
|
+
const { skillDir, pluginSlug } = found;
|
|
315
|
+
const rawExec = fs.existsSync(path.join(skillDir, "execution.json"))
|
|
316
|
+
? JSON.parse(fs.readFileSync(path.join(skillDir, "execution.json"), "utf8")) : null;
|
|
317
|
+
const rawRec = fs.existsSync(path.join(skillDir, "recovery.json"))
|
|
318
|
+
? JSON.parse(fs.readFileSync(path.join(skillDir, "recovery.json"), "utf8")) : null;
|
|
319
|
+
const rawSteps = Array.isArray(rawExec) ? rawExec : (rawExec && Array.isArray(rawExec.steps)) ? rawExec.steps : [];
|
|
320
|
+
const slugOv = isFlatOverrides
|
|
321
|
+
? (resolved.length === 0 ? overrides : {})
|
|
322
|
+
: (overrides[slug] && typeof overrides[slug] === "object" ? overrides[slug] : {});
|
|
323
|
+
const enriched = enrichStepsWithRecovery(rawSteps, rawRec).map((s, idx) => {
|
|
324
|
+
const ov = slugOv[String(idx)] ?? slugOv[idx];
|
|
325
|
+
return (ov && typeof ov === "object") ? { ...s, ...ov } : s;
|
|
326
|
+
});
|
|
327
|
+
resolved.push({ steps: enriched, inputs, slug, skillDir, pluginSlug });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Determine which plugin to use for auth (first skill's plugin)
|
|
331
|
+
const primaryPlugin = resolved[0].pluginSlug;
|
|
332
|
+
|
|
333
|
+
// ── Layer 0: Retry budget gate ────────────────────────────────────────
|
|
334
|
+
if (resumeFrom > 0) {
|
|
335
|
+
if (!checkRetryBudget(resolved[0].slug, resumeFrom)) {
|
|
336
|
+
return { content: [{ type: "text", text: `Retry budget exhausted (5 attempts at step ${resumeFrom}). Fix the root cause in execution.json before retrying from step 0.` }] };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Acquire browser (cached per-plugin slug) ──────────────────────────
|
|
341
|
+
let _browser, _context;
|
|
342
|
+
try {
|
|
343
|
+
({ browser: _browser, context: _context } = await getCachedBrowser(primaryPlugin));
|
|
344
|
+
} catch (authErr) {
|
|
345
|
+
return { content: [{ type: "text", text: String(authErr) }] };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const runtimeLog = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
349
|
+
const page = await _context.newPage();
|
|
350
|
+
|
|
351
|
+
page.on("console", msg => {
|
|
352
|
+
if (["error", "warning"].includes(msg.type()) && runtimeLog.consoleErrors.length < 50)
|
|
353
|
+
runtimeLog.consoleErrors.push({ type: msg.type(), text: msg.text() });
|
|
354
|
+
});
|
|
355
|
+
page.on("pageerror", err => {
|
|
356
|
+
if (runtimeLog.pageErrors.length < 20) runtimeLog.pageErrors.push(err.message);
|
|
357
|
+
});
|
|
358
|
+
page.on("requestfailed", req => {
|
|
359
|
+
if (runtimeLog.failedRequests.length < 30)
|
|
360
|
+
runtimeLog.failedRequests.push({ url: req.url(), failure: req.failure()?.errorText });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ── Resume state verification ─────────────────────────────────────────
|
|
364
|
+
if (resumeFrom > 0 && resolved.length > 0) {
|
|
365
|
+
const { steps: firstSteps, inputs: firstInputs } = resolved[0];
|
|
366
|
+
for (let i = resumeFrom - 1; i >= 0; i--) {
|
|
367
|
+
if (firstSteps[i] && firstSteps[i].type === "navigate") {
|
|
368
|
+
try { await page.goto(interpolate(firstSteps[i].url || "", firstInputs), { timeout: 30000, waitUntil: "domcontentloaded" }); }
|
|
369
|
+
catch (_) {}
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const resumeStep = firstSteps[resumeFrom];
|
|
374
|
+
if (resumeStep?.url_state?.before?.url_pattern) {
|
|
375
|
+
try { await waitForUrlState(page, resumeStep.url_state.before); }
|
|
376
|
+
catch (_) {
|
|
377
|
+
const actual = page.url(), expected = resumeStep.url_state.before.url_pattern;
|
|
378
|
+
await page.close().catch(() => {});
|
|
379
|
+
return { content: [{ type: "text", text: `Session state diverged. Expected URL pattern: ${expected}, got: ${actual}. Restart from step 0 or fix execution.json.` }] };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Execute skills ────────────────────────────────────────────────────
|
|
385
|
+
try {
|
|
386
|
+
for (let si = 0; si < resolved.length; si++) {
|
|
387
|
+
const { steps, inputs, slug, skillDir } = resolved[si];
|
|
388
|
+
const startAt = si === 0 ? resumeFrom : 0;
|
|
389
|
+
console.error(`[execute_plan] Running ${slug} (${steps.length} steps, starting at ${startAt})...`);
|
|
390
|
+
try {
|
|
391
|
+
await runPlan(page, steps, inputs, startAt, slug);
|
|
392
|
+
} catch (runErr) {
|
|
393
|
+
runErr.skillSlug = slug;
|
|
394
|
+
runErr.skillDir = skillDir;
|
|
395
|
+
throw runErr;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Success ───────────────────────────────────────────────────────────
|
|
400
|
+
const authJson = getAuthJson(primaryPlugin);
|
|
401
|
+
const state = await _context.storageState();
|
|
402
|
+
fs.mkdirSync(path.dirname(authJson), { recursive: true });
|
|
403
|
+
fs.writeFileSync(authJson, JSON.stringify(state, null, 2));
|
|
404
|
+
const shot = await page.screenshot({ type: "png" }).catch(() => null);
|
|
405
|
+
const url = page.url();
|
|
406
|
+
await page.close().catch(() => {});
|
|
407
|
+
for (const r of resolved) {
|
|
408
|
+
clearRetryBudget(r.slug);
|
|
409
|
+
appendRecoveryEvent({ event: "run_success", slug: r.slug, steps_executed: r.steps.length });
|
|
410
|
+
}
|
|
411
|
+
console.error(`[execute_plan] Done. URL: ${url}`);
|
|
412
|
+
const content = [{ type: "text", text: `Done. URL: ${url}` }];
|
|
413
|
+
if (shot) content.push({ type: "image", data: shot.toString("base64"), mimeType: "image/png" });
|
|
414
|
+
return { content };
|
|
415
|
+
|
|
416
|
+
} catch (err) {
|
|
417
|
+
const url = page.url();
|
|
418
|
+
const failedAt = typeof err.failedAt === "number" ? err.failedAt : null;
|
|
419
|
+
const failedDir = err.skillDir || null;
|
|
420
|
+
const failedSlug = err.skillSlug || null;
|
|
421
|
+
|
|
422
|
+
const failShot = await page.screenshot({ type: "png" }).catch(() => null);
|
|
423
|
+
|
|
424
|
+
let visualRefData = null, visualRefMime = null;
|
|
425
|
+
if (failedDir && failedAt !== null) {
|
|
426
|
+
const visualDir = path.join(failedDir, "visuals");
|
|
427
|
+
const stepNum = failedAt + 1;
|
|
428
|
+
for (const ext of [".jpg", ".jpeg", ".png"]) {
|
|
429
|
+
const candidate = path.join(visualDir, `Image_${stepNum}${ext}`);
|
|
430
|
+
if (fs.existsSync(candidate)) {
|
|
431
|
+
visualRefData = fs.readFileSync(candidate).toString("base64");
|
|
432
|
+
visualRefMime = ext === ".png" ? "image/png" : "image/jpeg";
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let pageStructure = null, viewport = null, scrollY = null;
|
|
439
|
+
try {
|
|
440
|
+
viewport = page.viewportSize();
|
|
441
|
+
scrollY = await page.evaluate(() => window.scrollY).catch(() => null);
|
|
442
|
+
pageStructure = await page.evaluate(() => {
|
|
443
|
+
const seen = new Set();
|
|
444
|
+
return Array.from(document.querySelectorAll(
|
|
445
|
+
'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="menuitem"], [role="option"]'
|
|
446
|
+
)).map(el => {
|
|
447
|
+
const text = (el.innerText || el.value || el.getAttribute("aria-label") || el.getAttribute("placeholder") || "").trim().slice(0, 80);
|
|
448
|
+
const tag = el.tagName.toLowerCase();
|
|
449
|
+
const type = el.getAttribute("type") || "";
|
|
450
|
+
const role = el.getAttribute("role") || "";
|
|
451
|
+
const id = el.id || undefined;
|
|
452
|
+
const dt = el.getAttribute("data-testid") || el.getAttribute("data-test") || undefined;
|
|
453
|
+
const key = `${tag}|${type}|${text}`;
|
|
454
|
+
if (!text && !type && !id && !dt) return null;
|
|
455
|
+
if (seen.has(key)) return null;
|
|
456
|
+
seen.add(key);
|
|
457
|
+
return { tag, type: type || undefined, role: role || undefined, text: text || undefined, id: id || undefined, "data-testid": dt || undefined };
|
|
458
|
+
}).filter(Boolean).slice(0, 250);
|
|
459
|
+
});
|
|
460
|
+
} catch (_) {}
|
|
461
|
+
|
|
462
|
+
await page.close().catch(() => {});
|
|
463
|
+
|
|
464
|
+
appendRecoveryEvent({
|
|
465
|
+
event: "terminal_failure", slug: failedSlug, step_index: failedAt,
|
|
466
|
+
error: err.message,
|
|
467
|
+
console_errors_count: runtimeLog.consoleErrors.length,
|
|
468
|
+
failed_requests_count: runtimeLog.failedRequests.length,
|
|
469
|
+
});
|
|
470
|
+
console.error(`[execute_plan] Failed: ${err.message}`);
|
|
471
|
+
|
|
472
|
+
const resumeHint = failedAt !== null
|
|
473
|
+
? `\nFix the selector, then call execute_plan with resume_from: ${failedAt}.`
|
|
474
|
+
: "";
|
|
475
|
+
|
|
476
|
+
const content = [
|
|
477
|
+
{ type: "text", text: `Execution failed at step ${failedAt !== null ? failedAt + 1 : "?"}: ${err.message}\nPage URL: ${url}${resumeHint}` },
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
content.push({ type: "text", text: "\nLayer 4 — vision recovery" });
|
|
481
|
+
if (err.preShot) {
|
|
482
|
+
content.push({ type: "text", text: "Pre-step screenshot (page state BEFORE the failed action):" });
|
|
483
|
+
content.push({ type: "image", data: err.preShot.toString("base64"), mimeType: "image/png" });
|
|
484
|
+
}
|
|
485
|
+
if (visualRefData) {
|
|
486
|
+
content.push({ type: "text", text: `Reference image — red box marks where step ${failedAt + 1} should interact:` });
|
|
487
|
+
content.push({ type: "image", data: visualRefData, mimeType: visualRefMime });
|
|
488
|
+
}
|
|
489
|
+
if (failShot) {
|
|
490
|
+
content.push({ type: "text", text: "Current page at failure:" });
|
|
491
|
+
content.push({ type: "image", data: failShot.toString("base64"), mimeType: "image/png" });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const l5 = ["\nLayer 5 — intent recovery"];
|
|
495
|
+
if (viewport) l5.push(`viewport: ${JSON.stringify(viewport)}, scrollY: ${scrollY}`);
|
|
496
|
+
if (pageStructure && pageStructure.length > 0)
|
|
497
|
+
l5.push(`Interactive elements (${pageStructure.length}):\n${JSON.stringify(pageStructure, null, 2)}`);
|
|
498
|
+
if (runtimeLog.consoleErrors.length > 0) l5.push(`Console errors:\n${JSON.stringify(runtimeLog.consoleErrors, null, 2)}`);
|
|
499
|
+
if (runtimeLog.pageErrors.length > 0) l5.push(`Page errors:\n${JSON.stringify(runtimeLog.pageErrors, null, 2)}`);
|
|
500
|
+
if (runtimeLog.failedRequests.length > 0) l5.push(`Failed requests:\n${JSON.stringify(runtimeLog.failedRequests,null, 2)}`);
|
|
501
|
+
content.push({ type: "text", text: l5.join("\n") });
|
|
502
|
+
|
|
503
|
+
return { content };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// ─── MCP server start ─────────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
const transport = new StdioServerTransport();
|
|
513
|
+
server.connect(transport);
|
|
514
|
+
|
|
515
|
+
process.on("SIGINT", gracefulShutdown);
|
|
516
|
+
process.on("SIGTERM", gracefulShutdown);
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kiran_nandi_123/conxa",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Conxa CLI — install and manage shared-runtime automation plugins",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"conxa": "bin/conxa.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "lib/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
|
+
"playwright": "^1.45.0"
|
|
22
|
+
}
|
|
23
|
+
}
|