@qoren/cli 0.1.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 (171) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +162 -0
  3. package/dist/args.d.ts +5 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +23 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/commands/account.d.ts +4 -0
  8. package/dist/commands/account.d.ts.map +1 -0
  9. package/dist/commands/account.js +154 -0
  10. package/dist/commands/account.js.map +1 -0
  11. package/dist/commands/agent.d.ts +27 -0
  12. package/dist/commands/agent.d.ts.map +1 -0
  13. package/dist/commands/agent.js +968 -0
  14. package/dist/commands/agent.js.map +1 -0
  15. package/dist/commands/api.d.ts +4 -0
  16. package/dist/commands/api.d.ts.map +1 -0
  17. package/dist/commands/api.js +66 -0
  18. package/dist/commands/api.js.map +1 -0
  19. package/dist/commands/approvals.d.ts +11 -0
  20. package/dist/commands/approvals.d.ts.map +1 -0
  21. package/dist/commands/approvals.js +140 -0
  22. package/dist/commands/approvals.js.map +1 -0
  23. package/dist/commands/auth.d.ts +4 -0
  24. package/dist/commands/auth.d.ts.map +1 -0
  25. package/dist/commands/auth.js +215 -0
  26. package/dist/commands/auth.js.map +1 -0
  27. package/dist/commands/clients.d.ts +62 -0
  28. package/dist/commands/clients.d.ts.map +1 -0
  29. package/dist/commands/clients.js +311 -0
  30. package/dist/commands/clients.js.map +1 -0
  31. package/dist/commands/console.d.ts +4 -0
  32. package/dist/commands/console.d.ts.map +1 -0
  33. package/dist/commands/console.js +26 -0
  34. package/dist/commands/console.js.map +1 -0
  35. package/dist/commands/customAgent.d.ts +42 -0
  36. package/dist/commands/customAgent.d.ts.map +1 -0
  37. package/dist/commands/customAgent.js +449 -0
  38. package/dist/commands/customAgent.js.map +1 -0
  39. package/dist/commands/designer.d.ts +32 -0
  40. package/dist/commands/designer.d.ts.map +1 -0
  41. package/dist/commands/designer.js +682 -0
  42. package/dist/commands/designer.js.map +1 -0
  43. package/dist/commands/env.d.ts +15 -0
  44. package/dist/commands/env.d.ts.map +1 -0
  45. package/dist/commands/env.js +245 -0
  46. package/dist/commands/env.js.map +1 -0
  47. package/dist/commands/interactive.d.ts +4 -0
  48. package/dist/commands/interactive.d.ts.map +1 -0
  49. package/dist/commands/interactive.js +30 -0
  50. package/dist/commands/interactive.js.map +1 -0
  51. package/dist/commands/jobs.d.ts +4 -0
  52. package/dist/commands/jobs.d.ts.map +1 -0
  53. package/dist/commands/jobs.js +59 -0
  54. package/dist/commands/jobs.js.map +1 -0
  55. package/dist/commands/secrets.d.ts +15 -0
  56. package/dist/commands/secrets.d.ts.map +1 -0
  57. package/dist/commands/secrets.js +164 -0
  58. package/dist/commands/secrets.js.map +1 -0
  59. package/dist/commands/webhook.d.ts +4 -0
  60. package/dist/commands/webhook.d.ts.map +1 -0
  61. package/dist/commands/webhook.js +275 -0
  62. package/dist/commands/webhook.js.map +1 -0
  63. package/dist/config.d.ts +38 -0
  64. package/dist/config.d.ts.map +1 -0
  65. package/dist/config.js +117 -0
  66. package/dist/config.js.map +1 -0
  67. package/dist/context.d.ts +52 -0
  68. package/dist/context.d.ts.map +1 -0
  69. package/dist/context.js +163 -0
  70. package/dist/context.js.map +1 -0
  71. package/dist/duration.d.ts +9 -0
  72. package/dist/duration.d.ts.map +1 -0
  73. package/dist/duration.js +30 -0
  74. package/dist/duration.js.map +1 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +27 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/issues.d.ts +10 -0
  80. package/dist/issues.d.ts.map +1 -0
  81. package/dist/issues.js +45 -0
  82. package/dist/issues.js.map +1 -0
  83. package/dist/jobProgress.d.ts +12 -0
  84. package/dist/jobProgress.d.ts.map +1 -0
  85. package/dist/jobProgress.js +56 -0
  86. package/dist/jobProgress.js.map +1 -0
  87. package/dist/loopback.d.ts +28 -0
  88. package/dist/loopback.d.ts.map +1 -0
  89. package/dist/loopback.js +110 -0
  90. package/dist/loopback.js.map +1 -0
  91. package/dist/migrate/apply.d.ts +29 -0
  92. package/dist/migrate/apply.d.ts.map +1 -0
  93. package/dist/migrate/apply.js +135 -0
  94. package/dist/migrate/apply.js.map +1 -0
  95. package/dist/migrate/bundle.d.ts +89 -0
  96. package/dist/migrate/bundle.d.ts.map +1 -0
  97. package/dist/migrate/bundle.js +681 -0
  98. package/dist/migrate/bundle.js.map +1 -0
  99. package/dist/migrate/tar.d.ts +9 -0
  100. package/dist/migrate/tar.d.ts.map +1 -0
  101. package/dist/migrate/tar.js +89 -0
  102. package/dist/migrate/tar.js.map +1 -0
  103. package/dist/output.d.ts +46 -0
  104. package/dist/output.d.ts.map +1 -0
  105. package/dist/output.js +152 -0
  106. package/dist/output.js.map +1 -0
  107. package/dist/program.d.ts +17 -0
  108. package/dist/program.d.ts.map +1 -0
  109. package/dist/program.js +89 -0
  110. package/dist/program.js.map +1 -0
  111. package/dist/runtimes.d.ts +11 -0
  112. package/dist/runtimes.d.ts.map +1 -0
  113. package/dist/runtimes.js +20 -0
  114. package/dist/runtimes.js.map +1 -0
  115. package/dist/slug.d.ts +3 -0
  116. package/dist/slug.d.ts.map +1 -0
  117. package/dist/slug.js +15 -0
  118. package/dist/slug.js.map +1 -0
  119. package/dist/taskChain.d.ts +20 -0
  120. package/dist/taskChain.d.ts.map +1 -0
  121. package/dist/taskChain.js +40 -0
  122. package/dist/taskChain.js.map +1 -0
  123. package/dist/taskDelivery.d.ts +33 -0
  124. package/dist/taskDelivery.d.ts.map +1 -0
  125. package/dist/taskDelivery.js +67 -0
  126. package/dist/taskDelivery.js.map +1 -0
  127. package/dist/tui/app.d.ts +5 -0
  128. package/dist/tui/app.d.ts.map +1 -0
  129. package/dist/tui/app.js +130 -0
  130. package/dist/tui/app.js.map +1 -0
  131. package/dist/tui/index.d.ts +12 -0
  132. package/dist/tui/index.d.ts.map +1 -0
  133. package/dist/tui/index.js +34 -0
  134. package/dist/tui/index.js.map +1 -0
  135. package/dist/tui/model.d.ts +110 -0
  136. package/dist/tui/model.d.ts.map +1 -0
  137. package/dist/tui/model.js +257 -0
  138. package/dist/tui/model.js.map +1 -0
  139. package/dist/tui/overlay.d.ts +66 -0
  140. package/dist/tui/overlay.d.ts.map +1 -0
  141. package/dist/tui/overlay.js +216 -0
  142. package/dist/tui/overlay.js.map +1 -0
  143. package/dist/tui/panes/account.d.ts +5 -0
  144. package/dist/tui/panes/account.d.ts.map +1 -0
  145. package/dist/tui/panes/account.js +108 -0
  146. package/dist/tui/panes/account.js.map +1 -0
  147. package/dist/tui/panes/agents.d.ts +9 -0
  148. package/dist/tui/panes/agents.d.ts.map +1 -0
  149. package/dist/tui/panes/agents.js +340 -0
  150. package/dist/tui/panes/agents.js.map +1 -0
  151. package/dist/tui/panes/chat.d.ts +14 -0
  152. package/dist/tui/panes/chat.d.ts.map +1 -0
  153. package/dist/tui/panes/chat.js +169 -0
  154. package/dist/tui/panes/chat.js.map +1 -0
  155. package/dist/tui/panes/environments.d.ts +6 -0
  156. package/dist/tui/panes/environments.d.ts.map +1 -0
  157. package/dist/tui/panes/environments.js +364 -0
  158. package/dist/tui/panes/environments.js.map +1 -0
  159. package/dist/tui/panes/jobs.d.ts +5 -0
  160. package/dist/tui/panes/jobs.d.ts.map +1 -0
  161. package/dist/tui/panes/jobs.js +104 -0
  162. package/dist/tui/panes/jobs.js.map +1 -0
  163. package/dist/tui/store.d.ts +40 -0
  164. package/dist/tui/store.d.ts.map +1 -0
  165. package/dist/tui/store.js +124 -0
  166. package/dist/tui/store.js.map +1 -0
  167. package/dist/tui/ui.d.ts +95 -0
  168. package/dist/tui/ui.d.ts.map +1 -0
  169. package/dist/tui/ui.js +144 -0
  170. package/dist/tui/ui.js.map +1 -0
  171. package/package.json +51 -0
@@ -0,0 +1,681 @@
1
+ export const RUNTIMES = ["hermes", "openclaw", "codex"];
2
+ const decoder = new TextDecoder();
3
+ const encoder = new TextEncoder();
4
+ const MAX_FILE_BYTES = 256 * 1024;
5
+ /** Variables the platform sets itself, or that would fight the ones it sets. */
6
+ const PLATFORM_MANAGED = new Set([
7
+ "HOME",
8
+ "HERMES_HOME",
9
+ "OPENCLAW_HOME",
10
+ "OPENCLAW_STATE_DIR",
11
+ "OPENCLAW_CONFIG_PATH",
12
+ "CODEX_HOME",
13
+ "HTTP_PROXY",
14
+ "HTTPS_PROXY",
15
+ "OPENROUTER_API_KEY",
16
+ "QOREN_MCP_TOKEN",
17
+ ]);
18
+ /** Model-provider keys. Qoren meters models through its own OpenRouter key;
19
+ * an operator who wants their own goes through bring-your-own-key instead. */
20
+ const PROVIDER_KEYS = new Set([
21
+ "OPENAI_API_KEY",
22
+ "ANTHROPIC_API_KEY",
23
+ "GOOGLE_API_KEY",
24
+ "GEMINI_API_KEY",
25
+ "XAI_API_KEY",
26
+ "GROQ_API_KEY",
27
+ "MISTRAL_API_KEY",
28
+ "DEEPSEEK_API_KEY",
29
+ "OPENROUTER_API_KEY",
30
+ ]);
31
+ // ------------------------------------------------------------------ bundle
32
+ export function parseBundle(entries) {
33
+ let manifest = null;
34
+ const files = new Map();
35
+ let secretsEnv = null;
36
+ for (const entry of entries) {
37
+ const path = entry.path.replace(/^\.\//, "");
38
+ const text = () => decoder.decode(entry.data);
39
+ if (path === "bundle/manifest.json") {
40
+ manifest = JSON.parse(text());
41
+ }
42
+ else if (path === "bundle/secrets.env") {
43
+ secretsEnv = text();
44
+ }
45
+ else if (path.startsWith("bundle/files/")) {
46
+ files.set(path.slice("bundle/files/".length), text());
47
+ }
48
+ }
49
+ if (!manifest) {
50
+ throw new Error("Not a Qoren export: bundle/manifest.json is missing. Produce one with `curl -fsSL https://qoren.sh/migrate.sh | sh`.");
51
+ }
52
+ if (manifest.format !== "qoren-export/1") {
53
+ throw new Error(`Unsupported export format "${manifest.format}". Update the CLI, or re-run the export script.`);
54
+ }
55
+ if (!RUNTIMES.includes(manifest.runtime)) {
56
+ throw new Error(`Unknown runtime "${manifest.runtime}" in the export.`);
57
+ }
58
+ return { manifest, files, secretsEnv };
59
+ }
60
+ // -------------------------------------------------------------------- plan
61
+ export function planImport(bundle) {
62
+ const notes = [];
63
+ const { manifest, files } = bundle;
64
+ const secrets = bundle.secretsEnv ? parseDotenv(bundle.secretsEnv) : {};
65
+ const base = {
66
+ runtime: manifest.runtime,
67
+ model: manifest.model?.trim() ? manifest.model.trim() : null,
68
+ soulMd: "",
69
+ agentsMd: "",
70
+ mcpServers: [],
71
+ scheduledTasks: [],
72
+ workspaceFiles: [],
73
+ runtimeFiles: [],
74
+ envNames: [],
75
+ secrets: {},
76
+ notes,
77
+ };
78
+ const plan = manifest.runtime === "hermes"
79
+ ? planHermes(base, files)
80
+ : manifest.runtime === "openclaw"
81
+ ? planOpenClaw(base, files)
82
+ : planCodex(base, files);
83
+ // Environment: the names the source defined, minus what Qoren provides.
84
+ const names = (manifest.envNames ?? []).filter((n) => /^[A-Z_][A-Z0-9_]*$/i.test(n));
85
+ const providerKeys = names.filter((n) => PROVIDER_KEYS.has(n));
86
+ plan.envNames = names.filter((n) => !PLATFORM_MANAGED.has(n) && !PROVIDER_KEYS.has(n));
87
+ if (providerKeys.length > 0) {
88
+ notes.push(`Model provider keys were not carried over (${providerKeys.join(", ")}): Qoren meters models through its own key. To use your own, add it under Settings, Bring your own key.`);
89
+ }
90
+ for (const name of plan.envNames) {
91
+ if (secrets[name] !== undefined)
92
+ plan.secrets[name] = secrets[name];
93
+ }
94
+ const missing = plan.envNames.filter((n) => plan.secrets[n] === undefined);
95
+ if (missing.length > 0) {
96
+ notes.push(`Values for ${missing.join(", ")} are not in the bundle. Add them in the vault and attach them with qoren agent create --secret, or re-export with --with-secrets.`);
97
+ }
98
+ const messaging = names.filter((n) => /^(TELEGRAM|DISCORD|SLACK|WHATSAPP|SIGNAL|MATRIX)_/i.test(n));
99
+ if (messaging.length > 0) {
100
+ notes.push(`Messaging credentials were found (${messaging.join(", ")}). Connect the channel from the agent's Settings > Connections in the console; a bot token alone is not enough to bring a gateway up.`);
101
+ }
102
+ if ((manifest.skipped ?? []).length > 0) {
103
+ notes.push(`The export skipped ${manifest.skipped.length} file(s) that were binary or too large; see manifest.json in the bundle.`);
104
+ }
105
+ if (!plan.soulMd.trim()) {
106
+ plan.soulMd = `# ${capitalize(manifest.runtime)} agent\n\nMigrated from ${manifest.host ?? "a self-hosted install"} on ${manifest.exportedAt ?? "an unknown date"}. No persona file was found in the export; describe who this agent is here.\n`;
107
+ notes.push("No persona file (SOUL.md or IDENTITY.md) was in the export, so a placeholder was written. Edit it from the agent's Memory tab.");
108
+ }
109
+ return plan;
110
+ }
111
+ // ------------------------------------------------------------------ hermes
112
+ function planHermes(plan, files) {
113
+ plan.soulMd = files.get("SOUL.md") ?? "";
114
+ plan.agentsMd = files.get("AGENTS.md") ?? "";
115
+ const config = files.get("config.yaml");
116
+ if (config) {
117
+ plan.mcpServers = mcpFromYaml(config);
118
+ plan.model ??= hermesModel(config);
119
+ }
120
+ plan.scheduledTasks = tasksFromJobsJson(files.get("cron/jobs.json"));
121
+ // Memory and skills live under HERMES_HOME on both sides.
122
+ for (const [path, content] of files) {
123
+ if (path.startsWith("memory/") || path.startsWith("skills/") || path.startsWith("sessions/")) {
124
+ pushRuntimeFile(plan, path, content);
125
+ }
126
+ }
127
+ return plan;
128
+ }
129
+ function hermesModel(yaml) {
130
+ const block = /^model:\s*\n((?:[ \t]+.*\n?)*)/m.exec(yaml);
131
+ if (!block)
132
+ return null;
133
+ const line = /^\s+default:\s*["']?([^"'\s#]+)/m.exec(block[1] ?? "");
134
+ return line?.[1] ?? null;
135
+ }
136
+ // ---------------------------------------------------------------- openclaw
137
+ function planOpenClaw(plan, files) {
138
+ const soul = files.get("workspace/SOUL.md")?.trim() ?? "";
139
+ const rawIdentity = files.get("workspace/IDENTITY.md")?.trim() ?? "";
140
+ // An IDENTITY.md Qoren wrote holds only the agent's name (the persona is in
141
+ // SOUL.md, see OpenClawPersona in magentic-core), so it adds nothing to fold.
142
+ const identity = rawIdentity.includes("<!-- magentic:identity-meta") ? "" : rawIdentity;
143
+ // Older Qoren agents, and OpenClaw's own bootstrap ritual, can put persona
144
+ // text in both files, so the two are folded into one document rather than
145
+ // one being dropped.
146
+ plan.soulMd =
147
+ soul && identity && soul !== identity
148
+ ? `${soul}\n\n${identity}\n`
149
+ : `${soul || identity}\n`;
150
+ if (!soul && !identity)
151
+ plan.soulMd = "";
152
+ plan.agentsMd = files.get("workspace/AGENTS.md") ?? "";
153
+ const config = files.get("openclaw.json");
154
+ if (config) {
155
+ const parsed = parseLenientJson(config);
156
+ if (parsed) {
157
+ plan.mcpServers = mcpFromOpenClaw(parsed);
158
+ plan.model ??= openClawModel(parsed);
159
+ }
160
+ else {
161
+ plan.notes.push("openclaw.json could not be parsed, so MCP servers and the model were not read from it.");
162
+ }
163
+ }
164
+ plan.scheduledTasks = tasksFromJobsJson(files.get("cron/jobs.json"));
165
+ const persona = new Set(["workspace/SOUL.md", "workspace/IDENTITY.md", "workspace/AGENTS.md"]);
166
+ for (const [path, content] of files) {
167
+ if (persona.has(path))
168
+ continue;
169
+ if (path.startsWith("workspace/")) {
170
+ // OpenClaw's workspace is the agent's workspace on Qoren too: seed it
171
+ // through the template so it is there before the first turn.
172
+ pushWorkspaceFile(plan, path, content);
173
+ }
174
+ else if (path.startsWith("skills/") || path.startsWith("sessions/")) {
175
+ pushRuntimeFile(plan, path, content);
176
+ }
177
+ // agents/<id>/* is runtime-managed identity for the old install; Qoren
178
+ // registers its own agent, so those files are deliberately not carried.
179
+ }
180
+ return plan;
181
+ }
182
+ function openClawModel(config) {
183
+ const agents = asRecord(config.agents);
184
+ const defaults = asRecord(agents?.defaults);
185
+ const model = asRecord(defaults?.model) ?? asRecord(config.model);
186
+ const primary = model?.primary;
187
+ if (typeof primary === "string" && primary.trim())
188
+ return primary.trim();
189
+ if (typeof defaults?.model === "string")
190
+ return defaults.model;
191
+ return null;
192
+ }
193
+ function mcpFromOpenClaw(config) {
194
+ const mcp = asRecord(config.mcp);
195
+ const servers = asRecord(mcp?.servers) ?? asRecord(config.mcpServers);
196
+ if (!servers)
197
+ return [];
198
+ const out = [];
199
+ for (const [name, raw] of Object.entries(servers)) {
200
+ const server = asRecord(raw);
201
+ if (!server)
202
+ continue;
203
+ const built = mcpFromRecord(name, server);
204
+ if (built)
205
+ out.push(built);
206
+ }
207
+ return out;
208
+ }
209
+ // ------------------------------------------------------------------- codex
210
+ function planCodex(plan, files) {
211
+ // Codex has one instructions file. Qoren writes the persona there, so the
212
+ // whole AGENTS.md becomes the persona rather than being split in two.
213
+ plan.soulMd = files.get("AGENTS.md") ?? "";
214
+ plan.agentsMd = "";
215
+ const config = files.get("config.toml");
216
+ if (config) {
217
+ const toml = parseMinimalToml(config);
218
+ plan.mcpServers = mcpFromToml(toml);
219
+ if (!plan.model) {
220
+ const model = toml.get("model");
221
+ if (typeof model === "string" && model.trim())
222
+ plan.model = model.trim();
223
+ }
224
+ }
225
+ for (const [path, content] of files) {
226
+ if (path.startsWith("skills/") ||
227
+ path.startsWith("memories/") ||
228
+ path.startsWith("rules/") ||
229
+ path.startsWith("prompts/") ||
230
+ path.startsWith("sessions/") ||
231
+ path === "auth.json") {
232
+ pushRuntimeFile(plan, path, content);
233
+ }
234
+ }
235
+ if (files.has("auth.json")) {
236
+ plan.notes.push("auth.json was carried over: the agent will run on your ChatGPT sign-in once deployed. Run qoren agent chatgpt-logout to drop it.");
237
+ }
238
+ return plan;
239
+ }
240
+ function mcpFromToml(toml) {
241
+ const out = [];
242
+ for (const [key, value] of toml) {
243
+ const match = /^mcp_servers\.(.+)$/.exec(key);
244
+ if (!match)
245
+ continue;
246
+ const server = asRecord(value);
247
+ if (!server)
248
+ continue;
249
+ const built = mcpFromRecord(match[1], server);
250
+ if (built)
251
+ out.push(built);
252
+ }
253
+ return out;
254
+ }
255
+ // ---------------------------------------------------------- shared helpers
256
+ function pushRuntimeFile(plan, path, content) {
257
+ if (!isSafePath(path)) {
258
+ plan.notes.push(`Skipped ${path}: the path is not one Qoren will write.`);
259
+ return;
260
+ }
261
+ if (encoder.encode(content).length > MAX_FILE_BYTES) {
262
+ plan.notes.push(`Skipped ${path}: larger than 256 KB.`);
263
+ return;
264
+ }
265
+ plan.runtimeFiles.push({ path, content });
266
+ }
267
+ function pushWorkspaceFile(plan, path, content) {
268
+ if (!isSafePath(path)) {
269
+ plan.notes.push(`Skipped ${path}: the path is not one Qoren will write.`);
270
+ return;
271
+ }
272
+ if (encoder.encode(content).length > MAX_FILE_BYTES) {
273
+ plan.notes.push(`Skipped ${path}: larger than 256 KB.`);
274
+ return;
275
+ }
276
+ plan.workspaceFiles.push({
277
+ path,
278
+ description: `Migrated from the previous install (${path}).`,
279
+ content,
280
+ });
281
+ }
282
+ /** Mirrors WorkspaceFile.IsSafePath on the server: relative, no parent
283
+ * segments, and a conservative character set (no spaces). */
284
+ export function isSafePath(path) {
285
+ if (!path || path.startsWith("/"))
286
+ return false;
287
+ const segments = path.split(/[\\/]/);
288
+ if (segments.some((s) => s === "" || s === "." || s === ".."))
289
+ return false;
290
+ return /^[A-Za-z0-9/._-]+$/.test(path);
291
+ }
292
+ /** The server accepts names of letters, digits, `_` and `-`. */
293
+ function isValidMcpName(name) {
294
+ return /^[A-Za-z0-9_-]{1,64}$/.test(name);
295
+ }
296
+ function mcpFromRecord(name, server) {
297
+ if (!isValidMcpName(name))
298
+ return null;
299
+ if (server.enabled === false || server.disabled === true)
300
+ return null;
301
+ const command = str(server.command);
302
+ const url = str(server.url);
303
+ const base = { name, enabled: true };
304
+ const tools = asRecord(server.tools);
305
+ const include = strList(tools?.include) ?? strList(server.enabled_tools);
306
+ const exclude = strList(tools?.exclude) ?? strList(server.disabled_tools);
307
+ const toolFields = {
308
+ ...(include ? { toolInclude: include } : {}),
309
+ ...(exclude ? { toolExclude: exclude } : {}),
310
+ };
311
+ if (command) {
312
+ return {
313
+ ...base,
314
+ transport: "stdio",
315
+ command,
316
+ ...(strList(server.args) ? { args: strList(server.args) } : {}),
317
+ ...(strMap(server.env) ? { env: strMap(server.env) } : {}),
318
+ ...toolFields,
319
+ };
320
+ }
321
+ if (url) {
322
+ return {
323
+ ...base,
324
+ transport: "http",
325
+ url,
326
+ ...(strMap(server.headers) ? { headers: strMap(server.headers) } : {}),
327
+ ...toolFields,
328
+ };
329
+ }
330
+ return null;
331
+ }
332
+ /** Hermes' `mcp_servers:` block. Hermes writes it as a map of name to a map of
333
+ * command/args/env or url/headers, and `tools: {include, exclude}`. */
334
+ export function mcpFromYaml(yaml) {
335
+ const tree = parseMinimalYaml(yaml);
336
+ const servers = asRecord(tree.mcp_servers);
337
+ if (!servers)
338
+ return [];
339
+ const out = [];
340
+ for (const [name, raw] of Object.entries(servers)) {
341
+ const server = asRecord(raw);
342
+ if (!server)
343
+ continue;
344
+ const built = mcpFromRecord(name, server);
345
+ if (built)
346
+ out.push(built);
347
+ }
348
+ return out;
349
+ }
350
+ /** Both Hermes and OpenClaw keep scheduled jobs in a `cron/jobs.json` whose
351
+ * shape has shifted between versions; read the fields that matter and skip
352
+ * what cannot be understood rather than refusing the whole file. */
353
+ export function tasksFromJobsJson(json) {
354
+ if (!json)
355
+ return [];
356
+ const parsed = parseLenientJson(json);
357
+ if (!parsed)
358
+ return [];
359
+ const list = Array.isArray(parsed.jobs)
360
+ ? parsed.jobs
361
+ : Array.isArray(parsed)
362
+ ? parsed
363
+ : [];
364
+ const out = [];
365
+ for (const raw of list) {
366
+ const job = asRecord(raw);
367
+ if (!job)
368
+ continue;
369
+ if (job.enabled === false)
370
+ continue;
371
+ const scheduleRaw = job.schedule ?? job.cron;
372
+ const schedule = typeof scheduleRaw === "string"
373
+ ? scheduleRaw
374
+ : (str(asRecord(scheduleRaw)?.expr) ?? str(asRecord(scheduleRaw)?.cron));
375
+ const payload = asRecord(job.payload);
376
+ const prompt = str(job.prompt) ??
377
+ str(job.message) ??
378
+ str(payload?.message) ??
379
+ str(payload?.text) ??
380
+ str(payload?.prompt);
381
+ if (!schedule || !prompt)
382
+ continue;
383
+ if (!looksLikeCron(schedule))
384
+ continue;
385
+ const name = str(job.name) ?? str(job.id) ?? `job-${out.length + 1}`;
386
+ out.push({ name: name.slice(0, 80), schedule, prompt });
387
+ }
388
+ return out;
389
+ }
390
+ function looksLikeCron(value) {
391
+ const parts = value.trim().split(/\s+/);
392
+ return parts.length === 5 || parts.length === 6;
393
+ }
394
+ export function parseDotenv(text) {
395
+ const out = {};
396
+ for (const rawLine of text.split(/\r?\n/)) {
397
+ const line = rawLine.trim();
398
+ if (!line || line.startsWith("#"))
399
+ continue;
400
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
401
+ if (!match)
402
+ continue;
403
+ let value = match[2].trim();
404
+ if ((value.startsWith('"') && value.endsWith('"')) ||
405
+ (value.startsWith("'") && value.endsWith("'"))) {
406
+ value = value.slice(1, -1);
407
+ }
408
+ else {
409
+ value = value.replace(/\s+#.*$/, "");
410
+ }
411
+ out[match[1]] = value;
412
+ }
413
+ return out;
414
+ }
415
+ /** JSON, or the JSON-with-comments-and-trailing-commas that OpenClaw accepts. */
416
+ export function parseLenientJson(text) {
417
+ const attempt = (source) => {
418
+ try {
419
+ const value = JSON.parse(source);
420
+ return typeof value === "object" && value !== null
421
+ ? value
422
+ : null;
423
+ }
424
+ catch {
425
+ return null;
426
+ }
427
+ };
428
+ return (attempt(text) ??
429
+ attempt(text
430
+ .replace(/\/\*[\s\S]*?\*\//g, "")
431
+ .replace(/^\s*\/\/.*$/gm, "")
432
+ .replace(/,(\s*[}\]])/g, "$1")));
433
+ }
434
+ // A YAML subset: block maps, block sequences of scalars, inline `[a, b]`
435
+ // lists, and quoted or bare scalars. Enough for a Hermes config.yaml, which
436
+ // Hermes itself writes with a real emitter, so the shapes are regular.
437
+ export function parseMinimalYaml(text) {
438
+ const lines = text
439
+ .split(/\r?\n/)
440
+ .map((raw) => ({
441
+ indent: raw.length - raw.trimStart().length,
442
+ body: stripYamlComment(raw).trim(),
443
+ }))
444
+ .filter((l) => l.body !== "");
445
+ let i = 0;
446
+ const parseList = (indent) => {
447
+ const list = [];
448
+ while (i < lines.length && lines[i].indent === indent && lines[i].body.startsWith("- ")) {
449
+ list.push(yamlScalar(lines[i].body.slice(2).trim()));
450
+ i++;
451
+ }
452
+ // Anything nested deeper than a scalar item is beyond this subset.
453
+ while (i < lines.length && lines[i].indent > indent)
454
+ i++;
455
+ return list;
456
+ };
457
+ const parseMap = (indent) => {
458
+ const map = {};
459
+ while (i < lines.length && lines[i].indent === indent) {
460
+ const body = lines[i].body;
461
+ const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body);
462
+ if (!match) {
463
+ i++;
464
+ continue;
465
+ }
466
+ const key = match[1].replace(/^["']|["']$/g, "");
467
+ const rest = (match[2] ?? "").trim();
468
+ i++;
469
+ if (rest !== "") {
470
+ map[key] = yamlScalar(rest);
471
+ continue;
472
+ }
473
+ const next = lines[i];
474
+ if (!next || next.indent < indent) {
475
+ map[key] = null;
476
+ }
477
+ else if (next.body.startsWith("- ") && next.indent >= indent) {
478
+ map[key] = parseList(next.indent);
479
+ }
480
+ else if (next.indent > indent) {
481
+ map[key] = parseMap(next.indent);
482
+ }
483
+ else {
484
+ map[key] = null;
485
+ }
486
+ }
487
+ // Skip anything indented oddly relative to this block.
488
+ while (i < lines.length && lines[i].indent > indent)
489
+ i++;
490
+ return map;
491
+ };
492
+ return lines.length === 0 ? {} : parseMap(lines[0].indent);
493
+ }
494
+ function stripYamlComment(line) {
495
+ let inString = null;
496
+ for (let i = 0; i < line.length; i++) {
497
+ const c = line[i];
498
+ if (inString) {
499
+ if (c === inString)
500
+ inString = null;
501
+ }
502
+ else if (c === '"' || c === "'") {
503
+ inString = c;
504
+ }
505
+ else if (c === "#" && (i === 0 || /\s/.test(line[i - 1]))) {
506
+ return line.slice(0, i);
507
+ }
508
+ }
509
+ return line;
510
+ }
511
+ function yamlScalar(text) {
512
+ if (text.startsWith("[") && text.endsWith("]")) {
513
+ const inner = text.slice(1, -1).trim();
514
+ return inner ? splitTopLevel(inner).map((s) => yamlScalar(s.trim())) : [];
515
+ }
516
+ if (text.startsWith("{") && text.endsWith("}")) {
517
+ const record = {};
518
+ for (const part of splitTopLevel(text.slice(1, -1))) {
519
+ const kv = /^("[^"]*"|'[^']*'|[^:]+?)\s*:\s*(.+)$/.exec(part.trim());
520
+ if (kv)
521
+ record[kv[1].replace(/^["']|["']$/g, "")] = yamlScalar(kv[2].trim());
522
+ }
523
+ return record;
524
+ }
525
+ if ((text.startsWith('"') && text.endsWith('"')) ||
526
+ (text.startsWith("'") && text.endsWith("'"))) {
527
+ return text.slice(1, -1);
528
+ }
529
+ if (text === "true")
530
+ return true;
531
+ if (text === "false")
532
+ return false;
533
+ if (text === "null" || text === "~")
534
+ return null;
535
+ if (/^-?\d+(\.\d+)?$/.test(text))
536
+ return Number(text);
537
+ return text;
538
+ }
539
+ // A TOML subset: `[table.sub]` headers, `key = "string"`, `key = ["a", "b"]`,
540
+ // `key = { A = "1" }` inline tables, numbers and booleans. Nested tables come
541
+ // back keyed by their dotted header, e.g. `mcp_servers.github`.
542
+ export function parseMinimalToml(text) {
543
+ const out = new Map();
544
+ let table = null;
545
+ for (const rawLine of text.split(/\r?\n/)) {
546
+ const line = stripTomlComment(rawLine).trim();
547
+ if (!line)
548
+ continue;
549
+ const header = /^\[([^\]]+)\]$/.exec(line);
550
+ if (header) {
551
+ table = header[1].trim().replace(/"/g, "");
552
+ if (!out.has(table))
553
+ out.set(table, {});
554
+ continue;
555
+ }
556
+ const kv = /^([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.+)$/.exec(line);
557
+ if (!kv)
558
+ continue;
559
+ const key = kv[1].replace(/^"|"$/g, "");
560
+ const value = tomlValue(kv[2].trim());
561
+ if (table === null) {
562
+ out.set(key, value);
563
+ }
564
+ else {
565
+ const record = out.get(table);
566
+ record[key] = value;
567
+ }
568
+ }
569
+ return out;
570
+ }
571
+ function stripTomlComment(line) {
572
+ let inString = null;
573
+ for (let i = 0; i < line.length; i++) {
574
+ const c = line[i];
575
+ if (inString) {
576
+ if (c === "\\")
577
+ i++;
578
+ else if (c === inString)
579
+ inString = null;
580
+ }
581
+ else if (c === '"' || c === "'") {
582
+ inString = c;
583
+ }
584
+ else if (c === "#") {
585
+ return line.slice(0, i);
586
+ }
587
+ }
588
+ return line;
589
+ }
590
+ function tomlValue(text) {
591
+ if (text.startsWith('"') && text.endsWith('"')) {
592
+ return text.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
593
+ }
594
+ if (text.startsWith("'") && text.endsWith("'"))
595
+ return text.slice(1, -1);
596
+ if (text.startsWith("[") && text.endsWith("]")) {
597
+ return splitTopLevel(text.slice(1, -1)).map((s) => tomlValue(s.trim()));
598
+ }
599
+ if (text.startsWith("{") && text.endsWith("}")) {
600
+ const record = {};
601
+ for (const part of splitTopLevel(text.slice(1, -1))) {
602
+ const kv = /^([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.+)$/.exec(part.trim());
603
+ if (kv)
604
+ record[kv[1].replace(/^"|"$/g, "")] = tomlValue(kv[2].trim());
605
+ }
606
+ return record;
607
+ }
608
+ if (text === "true")
609
+ return true;
610
+ if (text === "false")
611
+ return false;
612
+ if (/^-?\d+(\.\d+)?$/.test(text))
613
+ return Number(text);
614
+ return text;
615
+ }
616
+ /** Split on commas that are not inside quotes or nested brackets. */
617
+ function splitTopLevel(text) {
618
+ const parts = [];
619
+ let depth = 0;
620
+ let inString = null;
621
+ let current = "";
622
+ for (let i = 0; i < text.length; i++) {
623
+ const c = text[i];
624
+ if (inString) {
625
+ current += c;
626
+ if (c === "\\") {
627
+ current += text[i + 1] ?? "";
628
+ i++;
629
+ }
630
+ else if (c === inString)
631
+ inString = null;
632
+ continue;
633
+ }
634
+ if (c === '"' || c === "'")
635
+ inString = c;
636
+ else if (c === "[" || c === "{")
637
+ depth++;
638
+ else if (c === "]" || c === "}")
639
+ depth--;
640
+ if (c === "," && depth === 0) {
641
+ parts.push(current);
642
+ current = "";
643
+ continue;
644
+ }
645
+ current += c;
646
+ }
647
+ if (current.trim())
648
+ parts.push(current);
649
+ return parts;
650
+ }
651
+ function asRecord(value) {
652
+ return typeof value === "object" && value !== null && !Array.isArray(value)
653
+ ? value
654
+ : null;
655
+ }
656
+ function str(value) {
657
+ return typeof value === "string" && value.trim() ? value : null;
658
+ }
659
+ function strList(value) {
660
+ if (!Array.isArray(value))
661
+ return null;
662
+ const list = value.filter((v) => typeof v === "string" && v.trim() !== "");
663
+ return list.length > 0 ? list : null;
664
+ }
665
+ function strMap(value) {
666
+ const record = asRecord(value);
667
+ if (!record)
668
+ return null;
669
+ const out = {};
670
+ for (const [k, v] of Object.entries(record)) {
671
+ if (typeof v === "string")
672
+ out[k] = v;
673
+ else if (typeof v === "number" || typeof v === "boolean")
674
+ out[k] = String(v);
675
+ }
676
+ return Object.keys(out).length > 0 ? out : null;
677
+ }
678
+ function capitalize(text) {
679
+ return text.charAt(0).toUpperCase() + text.slice(1);
680
+ }
681
+ //# sourceMappingURL=bundle.js.map