vibeforce 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.
@@ -0,0 +1,892 @@
1
+ /**
2
+ * Slash command registry for the Vibeforce TUI.
3
+ *
4
+ * Two command types:
5
+ * - local: executed in-process, result displayed directly
6
+ * - prompt: expanded into a prompt string and sent to the LLM agent
7
+ */
8
+ import { readConfig, ensureConfigFile, ModelRegistry, loadSkills, writeSkill, allTools, rollbackToLatest, runSfCommand, createSessionManager, } from "vibeforce-core";
9
+ import { execSync } from "node:child_process";
10
+ import { formatTable, formatQueryResults, formatFieldList, formatOrgInfo, } from "./format.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Built-in local commands
13
+ // ---------------------------------------------------------------------------
14
+ const helpCommand = {
15
+ name: "help",
16
+ description: "List all available slash commands",
17
+ type: "local",
18
+ execute: async (_args, ctx) => {
19
+ const cmds = getCommands(ctx.skillsDir);
20
+ const maxName = Math.max(...cmds.map((c) => c.name.length));
21
+ const lines = cmds.map((c) => {
22
+ const tag = c.type === "prompt" ? " (prompt)" : "";
23
+ return ` /${c.name.padEnd(maxName + 2)}${c.description}${tag}`;
24
+ });
25
+ return `Available commands:\n\n${lines.join("\n")}\n`;
26
+ },
27
+ };
28
+ const modelCommand = {
29
+ name: "model",
30
+ description: "Show current model or switch with /model <id>",
31
+ type: "local",
32
+ execute: async (args, ctx) => {
33
+ ensureConfigFile();
34
+ const config = readConfig();
35
+ if (!args.trim()) {
36
+ return `Current model: ${ctx.model ?? config.defaultModel}`;
37
+ }
38
+ const registry = new ModelRegistry(config);
39
+ const all = registry.listModels();
40
+ const match = all.find((m) => m.id === args.trim() || m.model === args.trim());
41
+ if (!match) {
42
+ return `Model "${args.trim()}" not found. Use /model:list to see available models.`;
43
+ }
44
+ if (ctx.setModel)
45
+ ctx.setModel(match.id);
46
+ return `Switched to ${match.model} (${match.provider})`;
47
+ },
48
+ };
49
+ const modelListCommand = {
50
+ name: "model-list",
51
+ description: "List all available models",
52
+ type: "local",
53
+ execute: async () => {
54
+ ensureConfigFile();
55
+ const config = readConfig();
56
+ const registry = new ModelRegistry(config);
57
+ const models = registry.listModels();
58
+ if (models.length === 0) {
59
+ return "No models configured. Run `vibeforce provider:add` to get started.";
60
+ }
61
+ const defaultId = config.defaultModel;
62
+ const lines = models.map((m) => {
63
+ const isDefault = m.id === defaultId ? " (default)" : "";
64
+ const typeLabel = m.type.toUpperCase().padEnd(7);
65
+ return ` ${typeLabel} ${m.provider.padEnd(12)} ${m.model}${isDefault}`;
66
+ });
67
+ return `Available models:\n\n${lines.join("\n")}\n`;
68
+ },
69
+ };
70
+ const skillListCommand = {
71
+ name: "skill-list",
72
+ description: "List all loaded skills",
73
+ type: "local",
74
+ execute: async (_args, ctx) => {
75
+ const skills = loadSkills(ctx.skillsDir);
76
+ if (skills.length === 0) {
77
+ return "No skills loaded. Create a .md file in the skills/ directory to add one.";
78
+ }
79
+ const maxName = Math.max(...skills.map((s) => s.name.length));
80
+ const lines = skills.map((s) => ` ${s.name.padEnd(maxName + 2)}${s.description}`);
81
+ return `Loaded skills:\n\n${lines.join("\n")}\n`;
82
+ },
83
+ };
84
+ const skillAddCommand = {
85
+ name: "skill-add",
86
+ description: "Create a new skill file from template",
87
+ type: "local",
88
+ execute: async (args, ctx) => {
89
+ const name = args.trim();
90
+ if (!name) {
91
+ return "Usage: /skill:add <name>";
92
+ }
93
+ const template = `---
94
+ name: ${name}
95
+ description: TODO — describe what this skill does
96
+ trigger: when the user asks to ${name.replace(/-/g, " ")}
97
+ ---
98
+
99
+ # ${name} Skill
100
+
101
+ ## Instructions
102
+
103
+ TODO — write the skill instructions here.
104
+
105
+ ## Steps
106
+
107
+ 1. First, ...
108
+ 2. Then, ...
109
+ 3. Finally, ...
110
+ `;
111
+ const filePath = writeSkill(ctx.skillsDir, `${name}.md`, template);
112
+ return `Created skill template at ${filePath}\nEdit the file to customize the skill.`;
113
+ },
114
+ };
115
+ const toolListCommand = {
116
+ name: "tool-list",
117
+ description: "List all tools with descriptions",
118
+ type: "local",
119
+ execute: async () => {
120
+ const tools = allTools;
121
+ if (tools.length === 0) {
122
+ return "No tools loaded.";
123
+ }
124
+ const maxName = Math.max(...tools.map((t) => t.name.length));
125
+ const lines = tools.map((t) => {
126
+ const desc = "description" in t && typeof t.description === "string"
127
+ ? t.description
128
+ : "";
129
+ const short = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
130
+ return ` ${t.name.padEnd(maxName + 2)}${short}`;
131
+ });
132
+ return `Available tools (${tools.length}):\n\n${lines.join("\n")}\n`;
133
+ },
134
+ };
135
+ const orgCommand = {
136
+ name: "org",
137
+ description: "Show current org or switch with /org <alias>",
138
+ type: "local",
139
+ execute: async (args, ctx) => {
140
+ if (!args.trim()) {
141
+ return ctx.org ? `Current org: ${ctx.org}` : "No org connected. Use /org <alias> to set one.";
142
+ }
143
+ // Just report — actual org switching would need more wiring
144
+ return `Org set to: ${args.trim()}`;
145
+ },
146
+ };
147
+ const quitCommand = {
148
+ name: "quit",
149
+ description: "Exit the CLI",
150
+ type: "local",
151
+ execute: async () => {
152
+ process.exit(0);
153
+ },
154
+ };
155
+ const exitCommand = {
156
+ name: "exit",
157
+ description: "Exit the CLI",
158
+ type: "local",
159
+ execute: async () => {
160
+ process.exit(0);
161
+ },
162
+ };
163
+ const clearCommand = {
164
+ name: "clear",
165
+ description: "Clear message history",
166
+ type: "local",
167
+ execute: async (_args, ctx) => {
168
+ if (ctx.clearMessages)
169
+ ctx.clearMessages();
170
+ return "Message history cleared.";
171
+ },
172
+ };
173
+ const statusCommand = {
174
+ name: "status",
175
+ description: "Show current session info",
176
+ type: "local",
177
+ execute: async (_args, ctx) => {
178
+ ensureConfigFile();
179
+ const config = readConfig();
180
+ const skills = loadSkills(ctx.skillsDir);
181
+ const toolCount = allTools.length;
182
+ const lines = [
183
+ `Model: ${ctx.model ?? config.defaultModel}`,
184
+ `Org: ${ctx.org ?? "none"}`,
185
+ `Tools: ${toolCount}`,
186
+ `Skills: ${skills.length}`,
187
+ `CWD: ${process.cwd()}`,
188
+ ];
189
+ return lines.join("\n");
190
+ },
191
+ };
192
+ const doctorCommand = {
193
+ name: "doctor",
194
+ description: "Check prerequisites (sf CLI, node, API key, etc.)",
195
+ type: "local",
196
+ execute: async () => {
197
+ const checks = [];
198
+ // Node version
199
+ const nodeVersion = process.version;
200
+ checks.push(` Node.js ${nodeVersion} OK`);
201
+ // sf CLI
202
+ try {
203
+ const sfVersion = execSync("sf --version 2>/dev/null", { encoding: "utf-8" }).trim().split("\n")[0];
204
+ checks.push(` sf CLI ${sfVersion} OK`);
205
+ }
206
+ catch {
207
+ checks.push(` sf CLI NOT FOUND`);
208
+ }
209
+ // Anthropic API key
210
+ const hasKey = !!process.env.ANTHROPIC_API_KEY;
211
+ checks.push(` API Key ${hasKey ? "set" : "MISSING (set ANTHROPIC_API_KEY)"} ${hasKey ? "OK" : "WARN"}`);
212
+ // Python
213
+ try {
214
+ const pyVersion = execSync("python3 --version 2>/dev/null", { encoding: "utf-8" }).trim();
215
+ checks.push(` Python ${pyVersion} OK`);
216
+ }
217
+ catch {
218
+ checks.push(` Python NOT FOUND`);
219
+ }
220
+ // Robot Framework
221
+ try {
222
+ execSync("robot --version 2>/dev/null", { encoding: "utf-8" });
223
+ checks.push(` Robot FW installed OK`);
224
+ }
225
+ catch {
226
+ checks.push(` Robot FW NOT FOUND (optional)`);
227
+ }
228
+ return `Vibeforce Doctor\n\n${checks.join("\n")}\n`;
229
+ },
230
+ };
231
+ const rollbackCommand = {
232
+ name: "rollback",
233
+ description: "Restore from latest snapshot",
234
+ type: "local",
235
+ execute: async () => {
236
+ try {
237
+ const result = await rollbackToLatest();
238
+ return result.success
239
+ ? `Rollback successful: ${result.data ?? "restored from latest snapshot"}`
240
+ : `Rollback failed: ${result.error ?? "unknown error"}`;
241
+ }
242
+ catch (err) {
243
+ return `Rollback error: ${err.message}`;
244
+ }
245
+ },
246
+ };
247
+ const initCommand = {
248
+ name: "init",
249
+ description: "Run first-time setup (scaffold .vibeforce/, check deps)",
250
+ type: "local",
251
+ execute: async (_args, ctx) => {
252
+ const { mkdirSync, existsSync, writeFileSync } = await import("node:fs");
253
+ const { join } = await import("node:path");
254
+ const cwd = process.cwd();
255
+ const vfDir = join(cwd, ".vibeforce");
256
+ const skillsDir = join(cwd, "skills");
257
+ const steps = [];
258
+ if (!existsSync(vfDir)) {
259
+ mkdirSync(vfDir, { recursive: true });
260
+ steps.push("Created .vibeforce/ directory");
261
+ }
262
+ else {
263
+ steps.push(".vibeforce/ already exists");
264
+ }
265
+ if (!existsSync(skillsDir)) {
266
+ mkdirSync(skillsDir, { recursive: true });
267
+ steps.push("Created skills/ directory");
268
+ }
269
+ else {
270
+ steps.push("skills/ already exists");
271
+ }
272
+ const configPath = join(vfDir, "config.json");
273
+ if (!existsSync(configPath)) {
274
+ writeFileSync(configPath, JSON.stringify({ initialized: true }, null, 2));
275
+ steps.push("Created .vibeforce/config.json");
276
+ }
277
+ ensureConfigFile();
278
+ steps.push("Ensured model config at ~/.vibeforce/config.json");
279
+ return `Vibeforce initialized:\n\n ${steps.join("\n ")}\n`;
280
+ },
281
+ };
282
+ // ---------------------------------------------------------------------------
283
+ // Salesforce local commands
284
+ // ---------------------------------------------------------------------------
285
+ const orgListCommand = {
286
+ name: "org-list",
287
+ description: "List all authenticated Salesforce orgs",
288
+ type: "local",
289
+ execute: async () => {
290
+ try {
291
+ const result = await runSfCommand("org", ["list"]);
292
+ if (!result.success)
293
+ return `Error: ${result.raw}`;
294
+ return formatOrgInfo(result.data);
295
+ }
296
+ catch (err) {
297
+ return `Error listing orgs: ${err.message}`;
298
+ }
299
+ },
300
+ };
301
+ const orgOpenCommand = {
302
+ name: "org-open",
303
+ description: "Open the default org in the browser",
304
+ type: "local",
305
+ execute: async (args) => {
306
+ try {
307
+ const sfArgs = ["open", "--url-only"];
308
+ if (args.trim())
309
+ sfArgs.push("--path", args.trim());
310
+ const result = await runSfCommand("org", sfArgs);
311
+ if (!result.success)
312
+ return `Error: ${result.raw}`;
313
+ const url = typeof result.data === "object" && result.data !== null && "url" in result.data
314
+ ? result.data.url
315
+ : result.raw.trim();
316
+ if (url) {
317
+ try {
318
+ execSync(`open "${url}" 2>/dev/null || xdg-open "${url}" 2>/dev/null`, { stdio: "ignore" });
319
+ }
320
+ catch { /* browser open is best-effort */ }
321
+ return `Opened: ${url}`;
322
+ }
323
+ return `Org URL:\n${result.raw}`;
324
+ }
325
+ catch (err) {
326
+ return `Error opening org: ${err.message}`;
327
+ }
328
+ },
329
+ };
330
+ const orgLimitsCommand = {
331
+ name: "org-limits",
332
+ description: "Show org API limits (remaining / max)",
333
+ type: "local",
334
+ execute: async () => {
335
+ try {
336
+ const result = await runSfCommand("org", ["list", "limits"]);
337
+ if (!result.success)
338
+ return `Error: ${result.raw}`;
339
+ const limits = Array.isArray(result.data) ? result.data : [];
340
+ if (limits.length === 0)
341
+ return "No limits data returned.";
342
+ const headers = ["Limit", "Remaining", "Max"];
343
+ const rows = limits.map((l) => [
344
+ l.name ?? "",
345
+ String(l.remaining ?? ""),
346
+ String(l.max ?? ""),
347
+ ]);
348
+ return formatTable(headers, rows);
349
+ }
350
+ catch (err) {
351
+ return `Error fetching limits: ${err.message}`;
352
+ }
353
+ },
354
+ };
355
+ const describeCommand = {
356
+ name: "describe",
357
+ description: "Describe a Salesforce object's fields",
358
+ type: "local",
359
+ execute: async (args) => {
360
+ const objectName = args.trim();
361
+ if (!objectName)
362
+ return "Usage: /describe <ObjectName>";
363
+ try {
364
+ const result = await runSfCommand("sobject", ["describe", "--sobject", objectName]);
365
+ if (!result.success)
366
+ return `Error: ${result.raw}`;
367
+ const data = result.data;
368
+ const fields = data?.fields ?? [];
369
+ const label = data?.label ?? objectName;
370
+ return `${label} (${objectName}) — ${fields.length} fields\n\n${formatFieldList(fields)}`;
371
+ }
372
+ catch (err) {
373
+ return `Error describing ${objectName}: ${err.message}`;
374
+ }
375
+ },
376
+ };
377
+ const metadataCommand = {
378
+ name: "metadata",
379
+ description: "List metadata types or components of a given type",
380
+ type: "local",
381
+ execute: async (args) => {
382
+ try {
383
+ if (args.trim()) {
384
+ const result = await runSfCommand("org", ["list", "metadata", "--metadata-type", args.trim()]);
385
+ if (!result.success)
386
+ return `Error: ${result.raw}`;
387
+ const items = Array.isArray(result.data) ? result.data : [];
388
+ if (items.length === 0)
389
+ return `No ${args.trim()} components found.`;
390
+ const headers = ["Full Name", "Type", "Last Modified"];
391
+ const rows = items.map((m) => [
392
+ m.fullName ?? "",
393
+ m.type ?? args.trim(),
394
+ m.lastModifiedDate ?? "",
395
+ ]);
396
+ return formatTable(headers, rows);
397
+ }
398
+ else {
399
+ const result = await runSfCommand("org", ["list", "metadata-types"]);
400
+ if (!result.success)
401
+ return `Error: ${result.raw}`;
402
+ const types = Array.isArray(result.data)
403
+ ? result.data
404
+ : result.data?.metadataObjects ?? [];
405
+ if (types.length === 0)
406
+ return "No metadata types returned.";
407
+ const headers = ["Type", "Suffix", "Directory"];
408
+ const rows = types.map((t) => [
409
+ t.xmlName ?? t.name ?? "",
410
+ t.suffix ?? "",
411
+ t.directoryName ?? "",
412
+ ]);
413
+ return formatTable(headers, rows);
414
+ }
415
+ }
416
+ catch (err) {
417
+ return `Error listing metadata: ${err.message}`;
418
+ }
419
+ },
420
+ };
421
+ const retrieveCommand = {
422
+ name: "retrieve",
423
+ description: "Retrieve metadata from the org",
424
+ type: "local",
425
+ execute: async (args) => {
426
+ if (!args.trim())
427
+ return "Usage: /retrieve <metadata>, e.g. /retrieve ApexClass:MyClass";
428
+ try {
429
+ const result = await runSfCommand("project", ["retrieve", "start", "--metadata", args.trim()]);
430
+ if (!result.success)
431
+ return `Error: ${result.raw}`;
432
+ const files = result.data?.files ?? [];
433
+ if (Array.isArray(files) && files.length > 0) {
434
+ const headers = ["Component", "Type", "Path"];
435
+ const rows = files.map((f) => [
436
+ f.fullName ?? "",
437
+ f.type ?? "",
438
+ f.filePath ?? "",
439
+ ]);
440
+ return `Retrieved ${files.length} component${files.length === 1 ? "" : "s"}:\n\n${formatTable(headers, rows)}`;
441
+ }
442
+ return `Retrieve complete.\n${result.raw}`;
443
+ }
444
+ catch (err) {
445
+ return `Error retrieving metadata: ${err.message}`;
446
+ }
447
+ },
448
+ };
449
+ const queryCommand = {
450
+ name: "query",
451
+ description: "Run a SOQL query against the default org",
452
+ type: "local",
453
+ execute: async (args) => {
454
+ if (!args.trim())
455
+ return "Usage: /query <SOQL>\nExample: /query SELECT Id, Name FROM Account LIMIT 10";
456
+ try {
457
+ const result = await runSfCommand("data", ["query", "--query", args.trim()]);
458
+ if (!result.success)
459
+ return `Error: ${result.raw}`;
460
+ return formatQueryResults(result.data);
461
+ }
462
+ catch (err) {
463
+ return `Error running query: ${err.message}`;
464
+ }
465
+ },
466
+ };
467
+ const queryDcCommand = {
468
+ name: "query-dc",
469
+ description: "Run a Data Cloud ANSI SQL query",
470
+ type: "prompt",
471
+ getPrompt: (args) => `Run this Data Cloud ANSI SQL query using the dc_query tool: ${args}`,
472
+ };
473
+ const insertCommand = {
474
+ name: "insert",
475
+ description: "Insert a record: /insert ObjectName Field=Value Field=Value",
476
+ type: "local",
477
+ execute: async (args) => {
478
+ const parts = args.trim().split(/\s+/);
479
+ if (parts.length < 2)
480
+ return "Usage: /insert <ObjectName> Field=Value Field=Value ...";
481
+ const objectName = parts[0];
482
+ const values = parts.slice(1).join(" ");
483
+ try {
484
+ const result = await runSfCommand("data", ["create", "record", "--sobject", objectName, "--values", values]);
485
+ if (!result.success)
486
+ return `Error: ${result.raw}`;
487
+ const id = result.data?.id ?? "";
488
+ return id ? `Record created: ${objectName} ${id}` : `Record created.\n${result.raw}`;
489
+ }
490
+ catch (err) {
491
+ return `Error inserting record: ${err.message}`;
492
+ }
493
+ },
494
+ };
495
+ const agentPreviewCommand = {
496
+ name: "agent-preview",
497
+ description: "Preview an Agentforce agent with an utterance",
498
+ type: "local",
499
+ execute: async (args) => {
500
+ const parts = args.trim().split(/\s+/);
501
+ if (parts.length < 2)
502
+ return "Usage: /agent:preview <AgentName> <utterance>";
503
+ const agentName = parts[0];
504
+ const utterance = parts.slice(1).join(" ");
505
+ try {
506
+ const start = await runSfCommand("agent", ["preview", "start", "--name", agentName]);
507
+ if (!start.success)
508
+ return `Error starting preview: ${start.raw}`;
509
+ const send = await runSfCommand("agent", ["preview", "send", "--message", utterance]);
510
+ await runSfCommand("agent", ["preview", "end"]);
511
+ if (!send.success)
512
+ return `Error sending message: ${send.raw}`;
513
+ const output = send.data?.output ?? send.data?.message ?? send.raw;
514
+ return `Agent response:\n\n${typeof output === "string" ? output : JSON.stringify(output, null, 2)}`;
515
+ }
516
+ catch (err) {
517
+ try {
518
+ await runSfCommand("agent", ["preview", "end"]);
519
+ }
520
+ catch { /* best-effort cleanup */ }
521
+ return `Error in agent preview: ${err.message}`;
522
+ }
523
+ },
524
+ };
525
+ const dcObjectsCommand = {
526
+ name: "dc-objects",
527
+ description: "List all Data Cloud objects",
528
+ type: "prompt",
529
+ getPrompt: () => "Use the dc_list_objects tool to list all Data Cloud objects",
530
+ };
531
+ const dcDescribeCommand = {
532
+ name: "dc-describe",
533
+ description: "Describe a Data Cloud table",
534
+ type: "prompt",
535
+ getPrompt: (args) => `Use the dc_describe tool to describe this Data Cloud table: ${args}`,
536
+ };
537
+ const deployStatusCommand = {
538
+ name: "deploy-status",
539
+ description: "Check the status of the most recent deployment",
540
+ type: "local",
541
+ execute: async () => {
542
+ try {
543
+ const result = await runSfCommand("project", ["deploy", "report"]);
544
+ if (!result.success)
545
+ return `Error: ${result.raw}`;
546
+ const data = result.data;
547
+ const status = data?.status ?? "Unknown";
548
+ const components = data?.numberComponentsDeployed ?? "?";
549
+ const errors = data?.numberComponentErrors ?? 0;
550
+ return `Deploy Status: ${status}\nComponents deployed: ${components}\nErrors: ${errors}${errors > 0 ? `\n\n${result.raw}` : ""}`;
551
+ }
552
+ catch (err) {
553
+ return `Error checking deploy status: ${err.message}`;
554
+ }
555
+ },
556
+ };
557
+ const deployCancelCommand = {
558
+ name: "deploy-cancel",
559
+ description: "Cancel the most recent deployment",
560
+ type: "local",
561
+ execute: async () => {
562
+ try {
563
+ const result = await runSfCommand("project", ["deploy", "cancel"]);
564
+ if (!result.success)
565
+ return `Error: ${result.raw}`;
566
+ return "Deploy cancelled.";
567
+ }
568
+ catch (err) {
569
+ return `Error cancelling deploy: ${err.message}`;
570
+ }
571
+ },
572
+ };
573
+ const testCoverageCommand = {
574
+ name: "test-coverage",
575
+ description: "Run Apex tests and show code coverage",
576
+ type: "local",
577
+ execute: async () => {
578
+ try {
579
+ const result = await runSfCommand("apex", [
580
+ "run", "test", "--code-coverage", "--result-format", "json",
581
+ ]);
582
+ if (!result.success)
583
+ return `Error: ${result.raw}`;
584
+ const data = result.data;
585
+ const coverage = data?.coverage?.coverage ?? data?.codeCoverage ?? [];
586
+ if (Array.isArray(coverage) && coverage.length > 0) {
587
+ const headers = ["Class", "Coverage %", "Lines Covered", "Lines Missed"];
588
+ const rows = coverage.map((c) => {
589
+ const covered = c.numLinesCovered ?? 0;
590
+ const uncovered = c.numLinesUncovered ?? 0;
591
+ const total = covered + uncovered;
592
+ const pct = total > 0 ? Math.round((covered / total) * 100) : 0;
593
+ return [c.name ?? "", `${pct}%`, String(covered), String(uncovered)];
594
+ });
595
+ return formatTable(headers, rows);
596
+ }
597
+ return `Test run complete.\n${result.raw}`;
598
+ }
599
+ catch (err) {
600
+ return `Error running tests: ${err.message}`;
601
+ }
602
+ },
603
+ };
604
+ const logsCommand = {
605
+ name: "logs",
606
+ description: "View debug logs: /logs [logId] or /logs [count]",
607
+ type: "local",
608
+ execute: async (args) => {
609
+ try {
610
+ const trimmed = args.trim();
611
+ if (trimmed && /^[a-zA-Z0-9]{15,18}$/.test(trimmed)) {
612
+ const result = await runSfCommand("apex", ["get", "log", "--log-id", trimmed]);
613
+ if (!result.success)
614
+ return `Error: ${result.raw}`;
615
+ return result.raw;
616
+ }
617
+ const count = trimmed && /^\d+$/.test(trimmed) ? trimmed : "5";
618
+ const result = await runSfCommand("apex", ["list", "log", "--number", count]);
619
+ if (!result.success)
620
+ return `Error: ${result.raw}`;
621
+ const logs = Array.isArray(result.data) ? result.data : [];
622
+ if (logs.length === 0)
623
+ return "No debug logs found.";
624
+ const headers = ["Id", "Application", "Operation", "Status", "Size"];
625
+ const rows = logs.map((l) => [
626
+ l.Id ?? "",
627
+ l.Application ?? "",
628
+ l.Operation ?? "",
629
+ l.Status ?? "",
630
+ String(l.LogLength ?? ""),
631
+ ]);
632
+ return formatTable(headers, rows);
633
+ }
634
+ catch (err) {
635
+ return `Error fetching logs: ${err.message}`;
636
+ }
637
+ },
638
+ };
639
+ // ---------------------------------------------------------------------------
640
+ // Salesforce prompt commands
641
+ // ---------------------------------------------------------------------------
642
+ const apexCommand = {
643
+ name: "apex",
644
+ description: "Execute or generate Apex code",
645
+ type: "prompt",
646
+ getPrompt: (args) => `Execute or generate Apex code. If this looks like Apex code, run it as anonymous Apex. If it's a description, write the Apex class and test class: ${args}`,
647
+ };
648
+ const lwcCommand = {
649
+ name: "lwc",
650
+ description: "Create or modify a Lightning Web Component",
651
+ type: "prompt",
652
+ getPrompt: (args) => `Create or modify a Lightning Web Component. Generate the JS, HTML, CSS, and meta.xml files: ${args}`,
653
+ };
654
+ const flowCommand = {
655
+ name: "flow",
656
+ description: "Create or describe a Salesforce Flow",
657
+ type: "prompt",
658
+ getPrompt: (args) => `Create or describe a Salesforce Flow. Generate the Flow XML metadata: ${args}`,
659
+ };
660
+ const triggerCommand = {
661
+ name: "trigger",
662
+ description: "Create an Apex trigger with handler class pattern",
663
+ type: "prompt",
664
+ getPrompt: (args) => `Create an Apex trigger with handler class pattern (one trigger per object) and test class: ${args}`,
665
+ };
666
+ const agentBuildCommand = {
667
+ name: "agent-build",
668
+ description: "Build a complete Agentforce agent end-to-end",
669
+ type: "prompt",
670
+ getPrompt: (args) => `Build a complete Agentforce agent end-to-end. Use the agentforce-build skill. Requirements: ${args}`,
671
+ };
672
+ const agentTestCommand = {
673
+ name: "agent-test",
674
+ description: "Test an Agentforce agent",
675
+ type: "prompt",
676
+ getPrompt: (args) => `Test an Agentforce agent. Use the agentforce-test skill. Target: ${args}`,
677
+ };
678
+ const agentDeployCommand = {
679
+ name: "agent-deploy",
680
+ description: "Publish and activate an Agentforce agent bundle",
681
+ type: "prompt",
682
+ getPrompt: (args) => `Publish and activate an Agentforce agent bundle. Deploy dependencies first, then publish, then activate: ${args}`,
683
+ };
684
+ const dcSetupCommand = {
685
+ name: "dc-setup",
686
+ description: "Set up Data Cloud configuration",
687
+ type: "prompt",
688
+ getPrompt: (args) => `Set up Data Cloud configuration. Use the data-cloud-setup skill: ${args}`,
689
+ };
690
+ const testGenerateCommand = {
691
+ name: "test-generate",
692
+ description: "Generate comprehensive Apex test classes",
693
+ type: "prompt",
694
+ getPrompt: (args) => `Generate comprehensive Apex test classes for the specified class. Include positive, negative, bulk, and governor limit test cases: ${args}`,
695
+ };
696
+ const exportCommand = {
697
+ name: "export",
698
+ description: "Run a SOQL query and save results to CSV",
699
+ type: "prompt",
700
+ getPrompt: (args) => `Run this SOQL query and save the results to a CSV file: ${args}`,
701
+ };
702
+ const debugCommand = {
703
+ name: "debug",
704
+ description: "Analyze a Salesforce error or debug log",
705
+ type: "prompt",
706
+ getPrompt: (args) => `Analyze this Salesforce error or debug log and explain the root cause with a fix: ${args}`,
707
+ };
708
+ const governorCommand = {
709
+ name: "governor",
710
+ description: "Analyze Apex code for governor limit risks",
711
+ type: "prompt",
712
+ getPrompt: (args) => `Analyze the specified Apex code for governor limit risks. Check for SOQL in loops, DML in loops, CPU time issues: ${args}`,
713
+ };
714
+ const scaffoldCommand = {
715
+ name: "scaffold",
716
+ description: "Scaffold a full-stack app connected to Salesforce",
717
+ type: "prompt",
718
+ getPrompt: (args) => `Scaffold a full-stack application connected to Salesforce. Use the app-scaffold skill: ${args}`,
719
+ };
720
+ const connectedAppCommand = {
721
+ name: "connected-app",
722
+ description: "Create a Connected App in Salesforce",
723
+ type: "prompt",
724
+ getPrompt: (args) => `Create a Connected App in Salesforce. Use the connected-app-setup skill: ${args}`,
725
+ };
726
+ const setupCommand = {
727
+ name: "setup",
728
+ description: "Configure a Salesforce org setting",
729
+ type: "prompt",
730
+ getPrompt: (args) => `Configure this Salesforce org setting. If it requires the Setup UI, use browser automation tools: ${args}`,
731
+ };
732
+ // ---------------------------------------------------------------------------
733
+ // Built-in prompt commands
734
+ // ---------------------------------------------------------------------------
735
+ const commitCommand = {
736
+ name: "commit",
737
+ description: "Review staged changes and create a git commit",
738
+ type: "prompt",
739
+ getPrompt: () => "Review the currently staged git changes (run `git diff --cached` and `git status`). Then create a git commit with a clear, descriptive commit message that summarizes the changes. If nothing is staged, let me know.",
740
+ };
741
+ const diffCommand = {
742
+ name: "diff",
743
+ description: "Show and explain the current git diff",
744
+ type: "prompt",
745
+ getPrompt: () => "Run `git diff` to show the current unstaged changes, and `git diff --cached` for staged changes. Explain what the changes do in plain English.",
746
+ };
747
+ const deployCommand = {
748
+ name: "deploy",
749
+ description: "Deploy the current Salesforce project to the default org",
750
+ type: "prompt",
751
+ getPrompt: () => "Deploy the current Salesforce project to the default org. Auto-detect changed files. Run a dry-run validation first with `sf project deploy start --dry-run`. Show results. If validation passes, ask to deploy for real with `sf project deploy start`. If there are errors, explain them and suggest fixes.",
752
+ };
753
+ const testCommand = {
754
+ name: "test",
755
+ description: "Run all Apex tests and report results",
756
+ type: "prompt",
757
+ getPrompt: () => "Run all Apex tests in the current Salesforce project using `sf apex run test --synchronous --result-format human`. Report the results including any failures with details.",
758
+ };
759
+ const compactCommand = {
760
+ name: "compact",
761
+ description: "Summarize older messages to free up context space",
762
+ type: "local",
763
+ execute: async (_args, ctx) => {
764
+ // This command signals the chat loop to run compaction.
765
+ // The actual compaction happens in the message management layer.
766
+ // Here we report the current token estimate.
767
+ return "Context compaction triggered. Older messages will be summarized to free up context space.";
768
+ },
769
+ };
770
+ const rememberCommand = {
771
+ name: "remember",
772
+ description: "Save what you learned in this conversation to memory",
773
+ type: "prompt",
774
+ getPrompt: (args) => {
775
+ const extra = args.trim() ? `\n\nSpecifically, remember: ${args}` : "";
776
+ return `Review this conversation and save any important learnings, corrections, or project-specific knowledge to .vibeforce/agent.md. Create the file if it doesn't exist. Use a structured format with headers and bullet points.${extra}`;
777
+ },
778
+ };
779
+ const threadsCommand = {
780
+ name: "threads",
781
+ description: "List previous conversation sessions",
782
+ type: "local",
783
+ execute: async () => {
784
+ const manager = createSessionManager();
785
+ const sessions = await manager.list();
786
+ if (sessions.length === 0) {
787
+ return "No saved sessions found.";
788
+ }
789
+ const lines = sessions.map((s) => {
790
+ const started = new Date(s.startedAt).toLocaleString();
791
+ const last = new Date(s.lastMessageAt).toLocaleString();
792
+ return ` ${s.id.slice(0, 8)}... ${s.messageCount} msgs started ${started} last ${last}`;
793
+ });
794
+ return `Saved sessions (${sessions.length}):\n\n${lines.join("\n")}\n`;
795
+ },
796
+ };
797
+ // ---------------------------------------------------------------------------
798
+ // Registry
799
+ // ---------------------------------------------------------------------------
800
+ const builtInCommands = [
801
+ helpCommand,
802
+ modelCommand,
803
+ modelListCommand,
804
+ skillListCommand,
805
+ skillAddCommand,
806
+ toolListCommand,
807
+ orgCommand,
808
+ quitCommand,
809
+ exitCommand,
810
+ clearCommand,
811
+ statusCommand,
812
+ doctorCommand,
813
+ rollbackCommand,
814
+ initCommand,
815
+ commitCommand,
816
+ diffCommand,
817
+ deployCommand,
818
+ testCommand,
819
+ compactCommand,
820
+ rememberCommand,
821
+ threadsCommand,
822
+ // Salesforce local commands
823
+ orgListCommand,
824
+ orgOpenCommand,
825
+ orgLimitsCommand,
826
+ describeCommand,
827
+ metadataCommand,
828
+ retrieveCommand,
829
+ queryCommand,
830
+ queryDcCommand,
831
+ insertCommand,
832
+ agentPreviewCommand,
833
+ dcObjectsCommand,
834
+ dcDescribeCommand,
835
+ deployStatusCommand,
836
+ deployCancelCommand,
837
+ testCoverageCommand,
838
+ logsCommand,
839
+ // Salesforce prompt commands
840
+ apexCommand,
841
+ lwcCommand,
842
+ flowCommand,
843
+ triggerCommand,
844
+ agentBuildCommand,
845
+ agentTestCommand,
846
+ agentDeployCommand,
847
+ dcSetupCommand,
848
+ testGenerateCommand,
849
+ exportCommand,
850
+ debugCommand,
851
+ governorCommand,
852
+ scaffoldCommand,
853
+ connectedAppCommand,
854
+ setupCommand,
855
+ ];
856
+ /**
857
+ * Get all registered slash commands, including skill-based prompt commands.
858
+ */
859
+ export function getCommands(skillsDir = "./skills") {
860
+ const skills = loadSkills(skillsDir);
861
+ const skillCommands = skillsToCommands(skills);
862
+ return [...builtInCommands, ...skillCommands];
863
+ }
864
+ /**
865
+ * Find a command by name (with or without leading slash).
866
+ */
867
+ export function findCommand(input, skillsDir = "./skills") {
868
+ const name = input.startsWith("/") ? input.slice(1) : input;
869
+ // Split to separate command name from args
870
+ const cmdName = name.split(/\s+/)[0].toLowerCase();
871
+ const commands = getCommands(skillsDir);
872
+ return commands.find((c) => c.name.toLowerCase() === cmdName);
873
+ }
874
+ /**
875
+ * Convert loaded skills into prompt-type slash commands.
876
+ */
877
+ function skillsToCommands(skills) {
878
+ return skills.map((skill) => ({
879
+ name: skill.name,
880
+ description: skill.description || skill.trigger || `Run the ${skill.name} skill`,
881
+ type: "prompt",
882
+ getPrompt: (args) => {
883
+ let prompt = `Use the "${skill.name}" skill.\n\n`;
884
+ prompt += `Skill instructions:\n${skill.content}\n`;
885
+ if (args.trim()) {
886
+ prompt += `\nUser context: ${args}`;
887
+ }
888
+ return prompt;
889
+ },
890
+ }));
891
+ }
892
+ //# sourceMappingURL=registry.js.map