harnessforce 1.0.1

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 (42) hide show
  1. package/dist/commands/format.d.ts +23 -0
  2. package/dist/commands/format.d.ts.map +1 -0
  3. package/dist/commands/format.js +107 -0
  4. package/dist/commands/format.js.map +1 -0
  5. package/dist/commands/model.d.ts +15 -0
  6. package/dist/commands/model.d.ts.map +1 -0
  7. package/dist/commands/model.js +169 -0
  8. package/dist/commands/model.js.map +1 -0
  9. package/dist/commands/registry.d.ts +38 -0
  10. package/dist/commands/registry.d.ts.map +1 -0
  11. package/dist/commands/registry.js +1383 -0
  12. package/dist/commands/registry.js.map +1 -0
  13. package/dist/commands/skill.d.ts +10 -0
  14. package/dist/commands/skill.d.ts.map +1 -0
  15. package/dist/commands/skill.js +63 -0
  16. package/dist/commands/skill.js.map +1 -0
  17. package/dist/commands/tool.d.ts +9 -0
  18. package/dist/commands/tool.d.ts.map +1 -0
  19. package/dist/commands/tool.js +35 -0
  20. package/dist/commands/tool.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +239 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/ui/agent-astro.png +0 -0
  26. package/dist/ui/app.d.ts +19 -0
  27. package/dist/ui/app.d.ts.map +1 -0
  28. package/dist/ui/app.js +384 -0
  29. package/dist/ui/app.js.map +1 -0
  30. package/dist/ui/greeting.d.ts +7 -0
  31. package/dist/ui/greeting.d.ts.map +1 -0
  32. package/dist/ui/greeting.js +109 -0
  33. package/dist/ui/greeting.js.map +1 -0
  34. package/dist/ui/markdown.d.ts +9 -0
  35. package/dist/ui/markdown.d.ts.map +1 -0
  36. package/dist/ui/markdown.js +98 -0
  37. package/dist/ui/markdown.js.map +1 -0
  38. package/dist/ui/status-bar.d.ts +10 -0
  39. package/dist/ui/status-bar.d.ts.map +1 -0
  40. package/dist/ui/status-bar.js +7 -0
  41. package/dist/ui/status-bar.js.map +1 -0
  42. package/package.json +45 -0
@@ -0,0 +1,1383 @@
1
+ /**
2
+ * Slash command registry for the Harnessforce 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, estimateMessagesTokens, createSessionManager, sessionCostTracker, restoreLastVersion, getLastEditedFile, loadHooks, openInEditor, getTodos, } from "harnessforce-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 setKeyCommand = {
50
+ name: "set-key",
51
+ description: "Save your OpenRouter API key (persists to ~/.harnessforce/models.yaml)",
52
+ type: "local",
53
+ execute: async (args) => {
54
+ const key = args.trim();
55
+ if (!key) {
56
+ return "Usage: /set-key sk-or-your-key-here\n\nGet a key at https://openrouter.ai/keys";
57
+ }
58
+ try {
59
+ const { writeFileSync, mkdirSync, existsSync } = await import("node:fs");
60
+ const { join } = await import("node:path");
61
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
62
+ const configDir = join(home, ".harnessforce");
63
+ const configPath = join(configDir, "models.yaml");
64
+ if (!existsSync(configDir))
65
+ mkdirSync(configDir, { recursive: true });
66
+ // Always write a clean OpenRouter config with the key as a direct value
67
+ const content = [
68
+ `default_model: "openrouter:anthropic/claude-opus-4.6"`,
69
+ `providers:`,
70
+ ` openrouter:`,
71
+ ` type: gateway`,
72
+ ` base_url: "https://openrouter.ai/api/v1"`,
73
+ ` api_key: "${key}"`,
74
+ ` models:`,
75
+ ` - anthropic/claude-4.6-sonnet-20260217`,
76
+ ` - anthropic/claude-opus-4.6`,
77
+ ` - anthropic/claude-haiku-4`,
78
+ ` - openai/gpt-5.4`,
79
+ ` - openai/gpt-5.4-pro`,
80
+ ` - google/gemini-3.1-pro-preview`,
81
+ ` - x-ai/grok-4.20-beta`,
82
+ ` - deepseek/deepseek-v3.2`,
83
+ ` - meta-llama/llama-4-maverick`,
84
+ ` - qwen/qwen3.6-plus-preview:free`,
85
+ ``,
86
+ ].join("\n");
87
+ writeFileSync(configPath, content, "utf-8");
88
+ // Also set in current process
89
+ process.env.OPENROUTER_API_KEY = key;
90
+ return `API key saved. Restart harnessforce to connect.`;
91
+ }
92
+ catch (err) {
93
+ return `Error saving key: ${err.message}`;
94
+ }
95
+ },
96
+ };
97
+ const modelListCommand = {
98
+ name: "model-list",
99
+ description: "List all available models",
100
+ type: "local",
101
+ execute: async () => {
102
+ ensureConfigFile();
103
+ const config = readConfig();
104
+ const registry = new ModelRegistry(config);
105
+ const models = registry.listModels();
106
+ if (models.length === 0) {
107
+ return "No models configured. Run `harnessforce provider:add` to get started.";
108
+ }
109
+ const defaultId = config.defaultModel;
110
+ const lines = models.map((m) => {
111
+ const isDefault = m.id === defaultId ? " (default)" : "";
112
+ const typeLabel = m.type.toUpperCase().padEnd(7);
113
+ return ` ${typeLabel} ${m.provider.padEnd(12)} ${m.model}${isDefault}`;
114
+ });
115
+ return `Available models:\n\n${lines.join("\n")}\n`;
116
+ },
117
+ };
118
+ const skillListCommand = {
119
+ name: "skill-list",
120
+ description: "List all loaded skills",
121
+ type: "local",
122
+ execute: async (_args, ctx) => {
123
+ const skills = loadSkills(ctx.skillsDir);
124
+ if (skills.length === 0) {
125
+ return "No skills loaded. Create a .md file in the skills/ directory to add one.";
126
+ }
127
+ const maxName = Math.max(...skills.map((s) => s.name.length));
128
+ const lines = skills.map((s) => ` ${s.name.padEnd(maxName + 2)}${s.description}`);
129
+ return `Loaded skills:\n\n${lines.join("\n")}\n`;
130
+ },
131
+ };
132
+ const skillAddCommand = {
133
+ name: "skill-add",
134
+ description: "Create a new skill file from template",
135
+ type: "local",
136
+ execute: async (args, ctx) => {
137
+ const name = args.trim();
138
+ if (!name) {
139
+ return "Usage: /skill:add <name>";
140
+ }
141
+ const template = `---
142
+ name: ${name}
143
+ description: TODO — describe what this skill does
144
+ trigger: when the user asks to ${name.replace(/-/g, " ")}
145
+ ---
146
+
147
+ # ${name} Skill
148
+
149
+ ## Instructions
150
+
151
+ TODO — write the skill instructions here.
152
+
153
+ ## Steps
154
+
155
+ 1. First, ...
156
+ 2. Then, ...
157
+ 3. Finally, ...
158
+ `;
159
+ const filePath = writeSkill(ctx.skillsDir, `${name}.md`, template);
160
+ return `Created skill template at ${filePath}\nEdit the file to customize the skill.`;
161
+ },
162
+ };
163
+ const toolListCommand = {
164
+ name: "tool-list",
165
+ description: "List all tools with descriptions",
166
+ type: "local",
167
+ execute: async () => {
168
+ const tools = allTools;
169
+ if (tools.length === 0) {
170
+ return "No tools loaded.";
171
+ }
172
+ const maxName = Math.max(...tools.map((t) => t.name.length));
173
+ const lines = tools.map((t) => {
174
+ const desc = "description" in t && typeof t.description === "string"
175
+ ? t.description
176
+ : "";
177
+ const short = desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
178
+ return ` ${t.name.padEnd(maxName + 2)}${short}`;
179
+ });
180
+ return `Available tools (${tools.length}):\n\n${lines.join("\n")}\n`;
181
+ },
182
+ };
183
+ const orgCommand = {
184
+ name: "org",
185
+ description: "Show current org or switch with /org <alias>",
186
+ type: "local",
187
+ execute: async (args, ctx) => {
188
+ if (!args.trim()) {
189
+ return ctx.org ? `Current org: ${ctx.org}` : "No org connected. Use /org <alias> to set one.";
190
+ }
191
+ // Just report — actual org switching would need more wiring
192
+ return `Org set to: ${args.trim()}`;
193
+ },
194
+ };
195
+ const orgLoginCommand = {
196
+ name: "org-login",
197
+ description: "Authenticate a new Salesforce org (opens browser)",
198
+ type: "local",
199
+ execute: async (args) => {
200
+ const alias = args.trim();
201
+ try {
202
+ const { execSync } = await import("node:child_process");
203
+ const aliasFlag = alias ? ` --alias ${alias}` : "";
204
+ execSync(`sf org login web${aliasFlag}`, {
205
+ stdio: "inherit",
206
+ timeout: 120_000,
207
+ });
208
+ return alias
209
+ ? `Authenticated org "${alias}". Use /org ${alias} to switch to it.`
210
+ : "Authenticated new org. Use /org-list to see all orgs.";
211
+ }
212
+ catch (err) {
213
+ return `Login failed: ${err.message}`;
214
+ }
215
+ },
216
+ };
217
+ const quitCommand = {
218
+ name: "quit",
219
+ description: "Exit the CLI",
220
+ type: "local",
221
+ execute: async () => {
222
+ process.exit(0);
223
+ },
224
+ };
225
+ const exitCommand = {
226
+ name: "exit",
227
+ description: "Exit the CLI",
228
+ type: "local",
229
+ execute: async () => {
230
+ process.exit(0);
231
+ },
232
+ };
233
+ const clearCommand = {
234
+ name: "clear",
235
+ description: "Clear message history",
236
+ type: "local",
237
+ execute: async (_args, ctx) => {
238
+ if (ctx.clearMessages)
239
+ ctx.clearMessages();
240
+ return "Message history cleared.";
241
+ },
242
+ };
243
+ const statusCommand = {
244
+ name: "status",
245
+ description: "Show current session info",
246
+ type: "local",
247
+ execute: async (_args, ctx) => {
248
+ ensureConfigFile();
249
+ const config = readConfig();
250
+ const skills = loadSkills(ctx.skillsDir);
251
+ const toolCount = allTools.length;
252
+ const lines = [
253
+ `Model: ${ctx.model ?? config.defaultModel}`,
254
+ `Org: ${ctx.org ?? "none"}`,
255
+ `Tools: ${toolCount}`,
256
+ `Skills: ${skills.length}`,
257
+ `CWD: ${process.cwd()}`,
258
+ ];
259
+ return lines.join("\n");
260
+ },
261
+ };
262
+ const doctorCommand = {
263
+ name: "doctor",
264
+ description: "Check prerequisites (sf CLI, node, API key, etc.)",
265
+ type: "local",
266
+ execute: async () => {
267
+ const checks = [];
268
+ // Node version
269
+ const nodeVersion = process.version;
270
+ checks.push(` Node.js ${nodeVersion} OK`);
271
+ // sf CLI
272
+ try {
273
+ const sfVersion = execSync("sf --version 2>/dev/null", { encoding: "utf-8" }).trim().split("\n")[0];
274
+ checks.push(` sf CLI ${sfVersion} OK`);
275
+ }
276
+ catch {
277
+ checks.push(` sf CLI NOT FOUND`);
278
+ }
279
+ // Anthropic API key
280
+ const hasKey = !!process.env.ANTHROPIC_API_KEY;
281
+ checks.push(` API Key ${hasKey ? "set" : "MISSING (set ANTHROPIC_API_KEY)"} ${hasKey ? "OK" : "WARN"}`);
282
+ // Python
283
+ try {
284
+ const pyVersion = execSync("python3 --version 2>/dev/null", { encoding: "utf-8" }).trim();
285
+ checks.push(` Python ${pyVersion} OK`);
286
+ }
287
+ catch {
288
+ checks.push(` Python NOT FOUND`);
289
+ }
290
+ // Robot Framework
291
+ try {
292
+ execSync("robot --version 2>/dev/null", { encoding: "utf-8" });
293
+ checks.push(` Robot FW installed OK`);
294
+ }
295
+ catch {
296
+ checks.push(` Robot FW NOT FOUND (optional)`);
297
+ }
298
+ return `Harnessforce Doctor\n\n${checks.join("\n")}\n`;
299
+ },
300
+ };
301
+ const rollbackCommand = {
302
+ name: "rollback",
303
+ description: "Restore from latest snapshot",
304
+ type: "local",
305
+ execute: async () => {
306
+ try {
307
+ const result = await rollbackToLatest();
308
+ return result.success
309
+ ? `Rollback successful: ${result.data ?? "restored from latest snapshot"}`
310
+ : `Rollback failed: ${result.error ?? "unknown error"}`;
311
+ }
312
+ catch (err) {
313
+ return `Rollback error: ${err.message}`;
314
+ }
315
+ },
316
+ };
317
+ const initCommand = {
318
+ name: "init",
319
+ description: "Run first-time setup (scaffold .harnessforce/, check deps)",
320
+ type: "local",
321
+ execute: async (_args, ctx) => {
322
+ const { mkdirSync, existsSync, writeFileSync } = await import("node:fs");
323
+ const { join } = await import("node:path");
324
+ const cwd = process.cwd();
325
+ const vfDir = join(cwd, ".harnessforce");
326
+ const skillsDir = join(cwd, "skills");
327
+ const steps = [];
328
+ if (!existsSync(vfDir)) {
329
+ mkdirSync(vfDir, { recursive: true });
330
+ steps.push("Created .harnessforce/ directory");
331
+ }
332
+ else {
333
+ steps.push(".harnessforce/ already exists");
334
+ }
335
+ if (!existsSync(skillsDir)) {
336
+ mkdirSync(skillsDir, { recursive: true });
337
+ steps.push("Created skills/ directory");
338
+ }
339
+ else {
340
+ steps.push("skills/ already exists");
341
+ }
342
+ const configPath = join(vfDir, "config.json");
343
+ if (!existsSync(configPath)) {
344
+ writeFileSync(configPath, JSON.stringify({ initialized: true }, null, 2));
345
+ steps.push("Created .harnessforce/config.json");
346
+ }
347
+ ensureConfigFile();
348
+ steps.push("Ensured model config at ~/.harnessforce/config.json");
349
+ return `Harnessforce initialized:\n\n ${steps.join("\n ")}\n`;
350
+ },
351
+ };
352
+ // ---------------------------------------------------------------------------
353
+ // Salesforce local commands
354
+ // ---------------------------------------------------------------------------
355
+ const orgListCommand = {
356
+ name: "org-list",
357
+ description: "List all authenticated Salesforce orgs",
358
+ type: "local",
359
+ execute: async () => {
360
+ try {
361
+ const result = await runSfCommand("org", ["list"]);
362
+ if (!result.success)
363
+ return `Error: ${result.raw}`;
364
+ return formatOrgInfo(result.data);
365
+ }
366
+ catch (err) {
367
+ return `Error listing orgs: ${err.message}`;
368
+ }
369
+ },
370
+ };
371
+ const orgOpenCommand = {
372
+ name: "org-open",
373
+ description: "Open the default org in the browser",
374
+ type: "local",
375
+ execute: async (args) => {
376
+ try {
377
+ const sfArgs = ["open", "--url-only"];
378
+ if (args.trim())
379
+ sfArgs.push("--path", args.trim());
380
+ const result = await runSfCommand("org", sfArgs);
381
+ if (!result.success)
382
+ return `Error: ${result.raw}`;
383
+ const url = typeof result.data === "object" && result.data !== null && "url" in result.data
384
+ ? result.data.url
385
+ : result.raw.trim();
386
+ if (url) {
387
+ try {
388
+ execSync(`open "${url}" 2>/dev/null || xdg-open "${url}" 2>/dev/null`, { stdio: "ignore" });
389
+ }
390
+ catch { /* browser open is best-effort */ }
391
+ return `Opened: ${url}`;
392
+ }
393
+ return `Org URL:\n${result.raw}`;
394
+ }
395
+ catch (err) {
396
+ return `Error opening org: ${err.message}`;
397
+ }
398
+ },
399
+ };
400
+ const orgLimitsCommand = {
401
+ name: "org-limits",
402
+ description: "Show org API limits (remaining / max)",
403
+ type: "local",
404
+ execute: async () => {
405
+ try {
406
+ const result = await runSfCommand("org", ["list", "limits"]);
407
+ if (!result.success)
408
+ return `Error: ${result.raw}`;
409
+ const limits = Array.isArray(result.data) ? result.data : [];
410
+ if (limits.length === 0)
411
+ return "No limits data returned.";
412
+ const headers = ["Limit", "Remaining", "Max"];
413
+ const rows = limits.map((l) => [
414
+ l.name ?? "",
415
+ String(l.remaining ?? ""),
416
+ String(l.max ?? ""),
417
+ ]);
418
+ return formatTable(headers, rows);
419
+ }
420
+ catch (err) {
421
+ return `Error fetching limits: ${err.message}`;
422
+ }
423
+ },
424
+ };
425
+ const describeCommand = {
426
+ name: "describe",
427
+ description: "Describe a Salesforce object's fields",
428
+ type: "local",
429
+ execute: async (args) => {
430
+ const objectName = args.trim();
431
+ if (!objectName)
432
+ return "Usage: /describe <ObjectName>";
433
+ try {
434
+ const result = await runSfCommand("sobject", ["describe", "--sobject", objectName]);
435
+ if (!result.success)
436
+ return `Error: ${result.raw}`;
437
+ const data = result.data;
438
+ const fields = data?.fields ?? [];
439
+ const label = data?.label ?? objectName;
440
+ return `${label} (${objectName}) — ${fields.length} fields\n\n${formatFieldList(fields)}`;
441
+ }
442
+ catch (err) {
443
+ return `Error describing ${objectName}: ${err.message}`;
444
+ }
445
+ },
446
+ };
447
+ const metadataCommand = {
448
+ name: "metadata",
449
+ description: "List metadata types or components of a given type",
450
+ type: "local",
451
+ execute: async (args) => {
452
+ try {
453
+ if (args.trim()) {
454
+ const result = await runSfCommand("org", ["list", "metadata", "--metadata-type", args.trim()]);
455
+ if (!result.success)
456
+ return `Error: ${result.raw}`;
457
+ const items = Array.isArray(result.data) ? result.data : [];
458
+ if (items.length === 0)
459
+ return `No ${args.trim()} components found.`;
460
+ const headers = ["Full Name", "Type", "Last Modified"];
461
+ const rows = items.map((m) => [
462
+ m.fullName ?? "",
463
+ m.type ?? args.trim(),
464
+ m.lastModifiedDate ?? "",
465
+ ]);
466
+ return formatTable(headers, rows);
467
+ }
468
+ else {
469
+ const result = await runSfCommand("org", ["list", "metadata-types"]);
470
+ if (!result.success)
471
+ return `Error: ${result.raw}`;
472
+ const types = Array.isArray(result.data)
473
+ ? result.data
474
+ : result.data?.metadataObjects ?? [];
475
+ if (types.length === 0)
476
+ return "No metadata types returned.";
477
+ const headers = ["Type", "Suffix", "Directory"];
478
+ const rows = types.map((t) => [
479
+ t.xmlName ?? t.name ?? "",
480
+ t.suffix ?? "",
481
+ t.directoryName ?? "",
482
+ ]);
483
+ return formatTable(headers, rows);
484
+ }
485
+ }
486
+ catch (err) {
487
+ return `Error listing metadata: ${err.message}`;
488
+ }
489
+ },
490
+ };
491
+ const retrieveCommand = {
492
+ name: "retrieve",
493
+ description: "Retrieve metadata from the org",
494
+ type: "local",
495
+ execute: async (args) => {
496
+ if (!args.trim())
497
+ return "Usage: /retrieve <metadata>, e.g. /retrieve ApexClass:MyClass";
498
+ try {
499
+ const result = await runSfCommand("project", ["retrieve", "start", "--metadata", args.trim()]);
500
+ if (!result.success)
501
+ return `Error: ${result.raw}`;
502
+ const files = result.data?.files ?? [];
503
+ if (Array.isArray(files) && files.length > 0) {
504
+ const headers = ["Component", "Type", "Path"];
505
+ const rows = files.map((f) => [
506
+ f.fullName ?? "",
507
+ f.type ?? "",
508
+ f.filePath ?? "",
509
+ ]);
510
+ return `Retrieved ${files.length} component${files.length === 1 ? "" : "s"}:\n\n${formatTable(headers, rows)}`;
511
+ }
512
+ return `Retrieve complete.\n${result.raw}`;
513
+ }
514
+ catch (err) {
515
+ return `Error retrieving metadata: ${err.message}`;
516
+ }
517
+ },
518
+ };
519
+ const queryCommand = {
520
+ name: "query",
521
+ description: "Run a SOQL query against the default org",
522
+ type: "local",
523
+ execute: async (args) => {
524
+ if (!args.trim())
525
+ return "Usage: /query <SOQL>\nExample: /query SELECT Id, Name FROM Account LIMIT 10";
526
+ try {
527
+ const result = await runSfCommand("data", ["query", "--query", args.trim()]);
528
+ if (!result.success)
529
+ return `Error: ${result.raw}`;
530
+ return formatQueryResults(result.data);
531
+ }
532
+ catch (err) {
533
+ return `Error running query: ${err.message}`;
534
+ }
535
+ },
536
+ };
537
+ const queryDcCommand = {
538
+ name: "query-dc",
539
+ description: "Run a Data Cloud ANSI SQL query",
540
+ type: "prompt",
541
+ getPrompt: (args) => `Run this Data Cloud ANSI SQL query using the dc_query tool: ${args}`,
542
+ };
543
+ const insertCommand = {
544
+ name: "insert",
545
+ description: "Insert a record: /insert ObjectName Field=Value Field=Value",
546
+ type: "local",
547
+ execute: async (args) => {
548
+ const parts = args.trim().split(/\s+/);
549
+ if (parts.length < 2)
550
+ return "Usage: /insert <ObjectName> Field=Value Field=Value ...";
551
+ const objectName = parts[0];
552
+ const values = parts.slice(1).join(" ");
553
+ try {
554
+ const result = await runSfCommand("data", ["create", "record", "--sobject", objectName, "--values", values]);
555
+ if (!result.success)
556
+ return `Error: ${result.raw}`;
557
+ const id = result.data?.id ?? "";
558
+ return id ? `Record created: ${objectName} ${id}` : `Record created.\n${result.raw}`;
559
+ }
560
+ catch (err) {
561
+ return `Error inserting record: ${err.message}`;
562
+ }
563
+ },
564
+ };
565
+ const agentPreviewCommand = {
566
+ name: "agent-preview",
567
+ description: "Preview an Agentforce agent with an utterance",
568
+ type: "local",
569
+ execute: async (args) => {
570
+ const parts = args.trim().split(/\s+/);
571
+ if (parts.length < 2)
572
+ return "Usage: /agent:preview <AgentName> <utterance>";
573
+ const agentName = parts[0];
574
+ const utterance = parts.slice(1).join(" ");
575
+ try {
576
+ const start = await runSfCommand("agent", ["preview", "start", "--name", agentName]);
577
+ if (!start.success)
578
+ return `Error starting preview: ${start.raw}`;
579
+ const send = await runSfCommand("agent", ["preview", "send", "--message", utterance]);
580
+ await runSfCommand("agent", ["preview", "end"]);
581
+ if (!send.success)
582
+ return `Error sending message: ${send.raw}`;
583
+ const output = send.data?.output ?? send.data?.message ?? send.raw;
584
+ return `Agent response:\n\n${typeof output === "string" ? output : JSON.stringify(output, null, 2)}`;
585
+ }
586
+ catch (err) {
587
+ try {
588
+ await runSfCommand("agent", ["preview", "end"]);
589
+ }
590
+ catch { /* best-effort cleanup */ }
591
+ return `Error in agent preview: ${err.message}`;
592
+ }
593
+ },
594
+ };
595
+ const dcObjectsCommand = {
596
+ name: "dc-objects",
597
+ description: "List all Data Cloud objects",
598
+ type: "prompt",
599
+ getPrompt: () => "Use the dc_list_objects tool to list all Data Cloud objects",
600
+ };
601
+ const dcDescribeCommand = {
602
+ name: "dc-describe",
603
+ description: "Describe a Data Cloud table",
604
+ type: "prompt",
605
+ getPrompt: (args) => `Use the dc_describe tool to describe this Data Cloud table: ${args}`,
606
+ };
607
+ const deployStatusCommand = {
608
+ name: "deploy-status",
609
+ description: "Check the status of the most recent deployment",
610
+ type: "local",
611
+ execute: async () => {
612
+ try {
613
+ const result = await runSfCommand("project", ["deploy", "report"]);
614
+ if (!result.success)
615
+ return `Error: ${result.raw}`;
616
+ const data = result.data;
617
+ const status = data?.status ?? "Unknown";
618
+ const components = data?.numberComponentsDeployed ?? "?";
619
+ const errors = data?.numberComponentErrors ?? 0;
620
+ return `Deploy Status: ${status}\nComponents deployed: ${components}\nErrors: ${errors}${errors > 0 ? `\n\n${result.raw}` : ""}`;
621
+ }
622
+ catch (err) {
623
+ return `Error checking deploy status: ${err.message}`;
624
+ }
625
+ },
626
+ };
627
+ const deployCancelCommand = {
628
+ name: "deploy-cancel",
629
+ description: "Cancel the most recent deployment",
630
+ type: "local",
631
+ execute: async () => {
632
+ try {
633
+ const result = await runSfCommand("project", ["deploy", "cancel"]);
634
+ if (!result.success)
635
+ return `Error: ${result.raw}`;
636
+ return "Deploy cancelled.";
637
+ }
638
+ catch (err) {
639
+ return `Error cancelling deploy: ${err.message}`;
640
+ }
641
+ },
642
+ };
643
+ const testCoverageCommand = {
644
+ name: "test-coverage",
645
+ description: "Run Apex tests and show code coverage",
646
+ type: "local",
647
+ execute: async () => {
648
+ try {
649
+ const result = await runSfCommand("apex", [
650
+ "run", "test", "--code-coverage", "--result-format", "json",
651
+ ]);
652
+ if (!result.success)
653
+ return `Error: ${result.raw}`;
654
+ const data = result.data;
655
+ const coverage = data?.coverage?.coverage ?? data?.codeCoverage ?? [];
656
+ if (Array.isArray(coverage) && coverage.length > 0) {
657
+ const headers = ["Class", "Coverage %", "Lines Covered", "Lines Missed"];
658
+ const rows = coverage.map((c) => {
659
+ const covered = c.numLinesCovered ?? 0;
660
+ const uncovered = c.numLinesUncovered ?? 0;
661
+ const total = covered + uncovered;
662
+ const pct = total > 0 ? Math.round((covered / total) * 100) : 0;
663
+ return [c.name ?? "", `${pct}%`, String(covered), String(uncovered)];
664
+ });
665
+ return formatTable(headers, rows);
666
+ }
667
+ return `Test run complete.\n${result.raw}`;
668
+ }
669
+ catch (err) {
670
+ return `Error running tests: ${err.message}`;
671
+ }
672
+ },
673
+ };
674
+ const logsCommand = {
675
+ name: "logs",
676
+ description: "View debug logs: /logs [logId] or /logs [count]",
677
+ type: "local",
678
+ execute: async (args) => {
679
+ try {
680
+ const trimmed = args.trim();
681
+ if (trimmed && /^[a-zA-Z0-9]{15,18}$/.test(trimmed)) {
682
+ const result = await runSfCommand("apex", ["get", "log", "--log-id", trimmed]);
683
+ if (!result.success)
684
+ return `Error: ${result.raw}`;
685
+ return result.raw;
686
+ }
687
+ const count = trimmed && /^\d+$/.test(trimmed) ? trimmed : "5";
688
+ const result = await runSfCommand("apex", ["list", "log", "--number", count]);
689
+ if (!result.success)
690
+ return `Error: ${result.raw}`;
691
+ const logs = Array.isArray(result.data) ? result.data : [];
692
+ if (logs.length === 0)
693
+ return "No debug logs found.";
694
+ const headers = ["Id", "Application", "Operation", "Status", "Size"];
695
+ const rows = logs.map((l) => [
696
+ l.Id ?? "",
697
+ l.Application ?? "",
698
+ l.Operation ?? "",
699
+ l.Status ?? "",
700
+ String(l.LogLength ?? ""),
701
+ ]);
702
+ return formatTable(headers, rows);
703
+ }
704
+ catch (err) {
705
+ return `Error fetching logs: ${err.message}`;
706
+ }
707
+ },
708
+ };
709
+ // ---------------------------------------------------------------------------
710
+ // Salesforce prompt commands
711
+ // ---------------------------------------------------------------------------
712
+ const apexCommand = {
713
+ name: "apex",
714
+ description: "Execute or generate Apex code",
715
+ type: "prompt",
716
+ 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}`,
717
+ };
718
+ const lwcCommand = {
719
+ name: "lwc",
720
+ description: "Create or modify a Lightning Web Component",
721
+ type: "prompt",
722
+ getPrompt: (args) => `Create or modify a Lightning Web Component. Generate the JS, HTML, CSS, and meta.xml files: ${args}`,
723
+ };
724
+ const flowCommand = {
725
+ name: "flow",
726
+ description: "Create or describe a Salesforce Flow",
727
+ type: "prompt",
728
+ getPrompt: (args) => `Create or describe a Salesforce Flow. Generate the Flow XML metadata: ${args}`,
729
+ };
730
+ const triggerCommand = {
731
+ name: "trigger",
732
+ description: "Create an Apex trigger with handler class pattern",
733
+ type: "prompt",
734
+ getPrompt: (args) => `Create an Apex trigger with handler class pattern (one trigger per object) and test class: ${args}`,
735
+ };
736
+ const agentBuildCommand = {
737
+ name: "agent-build",
738
+ description: "Build a complete Agentforce agent end-to-end",
739
+ type: "prompt",
740
+ getPrompt: (args) => `Build a complete Agentforce agent end-to-end. Use the agentforce-build skill. Requirements: ${args}`,
741
+ };
742
+ const agentTestCommand = {
743
+ name: "agent-test",
744
+ description: "Test an Agentforce agent",
745
+ type: "prompt",
746
+ getPrompt: (args) => `Test an Agentforce agent. Use the agentforce-test skill. Target: ${args}`,
747
+ };
748
+ const agentDeployCommand = {
749
+ name: "agent-deploy",
750
+ description: "Publish and activate an Agentforce agent bundle",
751
+ type: "prompt",
752
+ getPrompt: (args) => `Publish and activate an Agentforce agent bundle. Deploy dependencies first, then publish, then activate: ${args}`,
753
+ };
754
+ const dcSetupCommand = {
755
+ name: "dc-setup",
756
+ description: "Set up Data Cloud configuration",
757
+ type: "prompt",
758
+ getPrompt: (args) => `Set up Data Cloud configuration. Use the data-cloud-setup skill: ${args}`,
759
+ };
760
+ const testGenerateCommand = {
761
+ name: "test-generate",
762
+ description: "Generate comprehensive Apex test classes",
763
+ type: "prompt",
764
+ getPrompt: (args) => `Generate comprehensive Apex test classes for the specified class. Include positive, negative, bulk, and governor limit test cases: ${args}`,
765
+ };
766
+ const exportCommand = {
767
+ name: "export",
768
+ description: "Run a SOQL query and save results to CSV",
769
+ type: "prompt",
770
+ getPrompt: (args) => `Run this SOQL query and save the results to a CSV file: ${args}`,
771
+ };
772
+ const debugCommand = {
773
+ name: "debug",
774
+ description: "Analyze a Salesforce error or debug log",
775
+ type: "prompt",
776
+ getPrompt: (args) => `Analyze this Salesforce error or debug log and explain the root cause with a fix: ${args}`,
777
+ };
778
+ const governorCommand = {
779
+ name: "governor",
780
+ description: "Analyze Apex code for governor limit risks",
781
+ type: "prompt",
782
+ getPrompt: (args) => `Analyze the specified Apex code for governor limit risks. Check for SOQL in loops, DML in loops, CPU time issues: ${args}`,
783
+ };
784
+ const scaffoldCommand = {
785
+ name: "scaffold",
786
+ description: "Scaffold a full-stack app connected to Salesforce",
787
+ type: "prompt",
788
+ getPrompt: (args) => `Scaffold a full-stack application connected to Salesforce. Use the app-scaffold skill: ${args}`,
789
+ };
790
+ const connectedAppCommand = {
791
+ name: "connected-app",
792
+ description: "Create a Connected App in Salesforce",
793
+ type: "prompt",
794
+ getPrompt: (args) => `Create a Connected App in Salesforce. Use the connected-app-setup skill: ${args}`,
795
+ };
796
+ const setupCommand = {
797
+ name: "setup",
798
+ description: "Configure a Salesforce org setting",
799
+ type: "prompt",
800
+ getPrompt: (args) => `Configure this Salesforce org setting. If it requires the Setup UI, use browser automation tools: ${args}`,
801
+ };
802
+ // ---------------------------------------------------------------------------
803
+ // Extended Salesforce commands
804
+ // ---------------------------------------------------------------------------
805
+ const scratchCreateCommand = {
806
+ name: "scratch-create",
807
+ description: "Create a new scratch org from a definition file",
808
+ type: "local",
809
+ execute: async (args) => {
810
+ const parts = args.trim().split(/\s+/);
811
+ const defFile = parts[0];
812
+ if (!defFile)
813
+ return "Usage: /scratch-create <definition-file> [alias]";
814
+ const alias = parts[1];
815
+ try {
816
+ const sfArgs = ["create", "scratch", "--definition-file", defFile];
817
+ if (alias)
818
+ sfArgs.push("--alias", alias);
819
+ const result = await runSfCommand("org", sfArgs, { timeout: 300_000 });
820
+ if (!result.success)
821
+ return `Error: ${result.raw}`;
822
+ const data = result.data;
823
+ const orgId = data?.orgId ?? data?.id ?? "";
824
+ const username = data?.username ?? "";
825
+ return `Scratch org created${alias ? ` (${alias})` : ""}${orgId ? `\nOrg ID: ${orgId}` : ""}${username ? `\nUsername: ${username}` : ""}`;
826
+ }
827
+ catch (err) {
828
+ return `Error creating scratch org: ${err.message}`;
829
+ }
830
+ },
831
+ };
832
+ const scratchDeleteCommand = {
833
+ name: "scratch-delete",
834
+ description: "Delete a scratch org",
835
+ type: "local",
836
+ execute: async (args) => {
837
+ const targetOrg = args.trim();
838
+ if (!targetOrg)
839
+ return "Usage: /scratch-delete <alias-or-username>";
840
+ try {
841
+ const result = await runSfCommand("org", [
842
+ "delete", "scratch", "--target-org", targetOrg, "--no-prompt",
843
+ ]);
844
+ if (!result.success)
845
+ return `Error: ${result.raw}`;
846
+ return `Scratch org "${targetOrg}" deleted.`;
847
+ }
848
+ catch (err) {
849
+ return `Error deleting scratch org: ${err.message}`;
850
+ }
851
+ },
852
+ };
853
+ const packageCreateCommand = {
854
+ name: "package-create",
855
+ description: "Create a new Salesforce package",
856
+ type: "local",
857
+ execute: async (args) => {
858
+ const parts = args.trim().split(/\s+/);
859
+ const name = parts[0];
860
+ if (!name)
861
+ return "Usage: /package-create <name> [Managed|Unlocked]";
862
+ const packageType = parts[1] ?? "Unlocked";
863
+ try {
864
+ const result = await runSfCommand("package", [
865
+ "create", "--name", name, "--package-type", packageType,
866
+ ]);
867
+ if (!result.success)
868
+ return `Error: ${result.raw}`;
869
+ return `Package "${name}" created (${packageType}).`;
870
+ }
871
+ catch (err) {
872
+ return `Error creating package: ${err.message}`;
873
+ }
874
+ },
875
+ };
876
+ const packageVersionCommand = {
877
+ name: "package-version",
878
+ description: "Create a new package version",
879
+ type: "local",
880
+ execute: async (args) => {
881
+ const pkg = args.trim();
882
+ if (!pkg)
883
+ return "Usage: /package-version <package-id-or-alias>";
884
+ try {
885
+ const result = await runSfCommand("package", [
886
+ "version", "create", "--package", pkg,
887
+ ], { timeout: 600_000 });
888
+ if (!result.success)
889
+ return `Error: ${result.raw}`;
890
+ const data = result.data;
891
+ const versionId = data?.SubscriberPackageVersionId ?? data?.id ?? "";
892
+ return `Package version created.${versionId ? `\nVersion ID: ${versionId}` : ""}`;
893
+ }
894
+ catch (err) {
895
+ return `Error creating package version: ${err.message}`;
896
+ }
897
+ },
898
+ };
899
+ const coverageCommand = {
900
+ name: "coverage",
901
+ description: "Run Apex tests and show code coverage (alias for /test-coverage)",
902
+ type: "local",
903
+ execute: async () => {
904
+ try {
905
+ const result = await runSfCommand("apex", [
906
+ "run", "test", "--code-coverage", "--result-format", "json",
907
+ ], { timeout: 300_000 });
908
+ if (!result.success)
909
+ return `Error: ${result.raw}`;
910
+ const data = result.data;
911
+ const coverage = data?.coverage?.coverage ?? data?.codeCoverage ?? [];
912
+ if (Array.isArray(coverage) && coverage.length > 0) {
913
+ const headers = ["Class", "Coverage %", "Lines Covered", "Lines Missed"];
914
+ const rows = coverage.map((c) => {
915
+ const covered = c.numLinesCovered ?? 0;
916
+ const uncovered = c.numLinesUncovered ?? 0;
917
+ const total = covered + uncovered;
918
+ const pct = total > 0 ? Math.round((covered / total) * 100) : 0;
919
+ return [c.name ?? "", `${pct}%`, String(covered), String(uncovered)];
920
+ });
921
+ return formatTable(headers, rows);
922
+ }
923
+ return `Test run complete.\n${result.raw}`;
924
+ }
925
+ catch (err) {
926
+ return `Error running tests: ${err.message}`;
927
+ }
928
+ },
929
+ };
930
+ const deployHistoryCommand = {
931
+ name: "deploy-history",
932
+ description: "Show the most recent deployment report",
933
+ type: "local",
934
+ execute: async () => {
935
+ try {
936
+ const result = await runSfCommand("project", ["deploy", "report"]);
937
+ if (!result.success)
938
+ return `Error: ${result.raw}`;
939
+ const data = result.data;
940
+ const status = data?.status ?? "Unknown";
941
+ const components = data?.numberComponentsDeployed ?? "?";
942
+ const errors = data?.numberComponentErrors ?? 0;
943
+ return `Deploy Status: ${status}\nComponents deployed: ${components}\nErrors: ${errors}${errors > 0 ? `\n\n${result.raw}` : ""}`;
944
+ }
945
+ catch (err) {
946
+ return `Error checking deploy status: ${err.message}`;
947
+ }
948
+ },
949
+ };
950
+ const orgDiffCommand = {
951
+ name: "org-diff",
952
+ description: "Compare metadata between two orgs",
953
+ type: "prompt",
954
+ getPrompt: (args) => `Compare metadata between these two orgs: ${args}`,
955
+ };
956
+ const dataExportCommand = {
957
+ name: "data-export",
958
+ description: "Run a SOQL query and export results as CSV",
959
+ type: "local",
960
+ execute: async (args) => {
961
+ if (!args.trim())
962
+ return "Usage: /data-export <SOQL>\nExample: /data-export SELECT Id, Name FROM Account LIMIT 100";
963
+ try {
964
+ const result = await runSfCommand("data", ["query", "--query", args.trim(), "--result-format", "csv"], { skipJson: true });
965
+ if (!result.success)
966
+ return `Error: ${result.raw}`;
967
+ return result.raw;
968
+ }
969
+ catch (err) {
970
+ return `Error exporting data: ${err.message}`;
971
+ }
972
+ },
973
+ };
974
+ const limitsWatchCommand = {
975
+ name: "limits-watch",
976
+ description: "Show current org limits (alias for /org-limits)",
977
+ type: "local",
978
+ execute: async () => {
979
+ try {
980
+ const result = await runSfCommand("org", ["list", "limits"]);
981
+ if (!result.success)
982
+ return `Error: ${result.raw}`;
983
+ const limits = Array.isArray(result.data) ? result.data : [];
984
+ if (limits.length === 0)
985
+ return "No limits data returned.";
986
+ const headers = ["Limit", "Remaining", "Max"];
987
+ const rows = limits.map((l) => [
988
+ l.name ?? "",
989
+ String(l.remaining ?? ""),
990
+ String(l.max ?? ""),
991
+ ]);
992
+ return formatTable(headers, rows);
993
+ }
994
+ catch (err) {
995
+ return `Error fetching limits: ${err.message}`;
996
+ }
997
+ },
998
+ };
999
+ const sandboxCreateCommand = {
1000
+ name: "sandbox-create",
1001
+ description: "Create a new Salesforce sandbox",
1002
+ type: "local",
1003
+ execute: async (args) => {
1004
+ const parts = args.trim().split(/\s+/);
1005
+ const name = parts[0];
1006
+ if (!name)
1007
+ return "Usage: /sandbox-create <name> [definition-file]";
1008
+ const defFile = parts[1];
1009
+ try {
1010
+ const sfArgs = ["create", "sandbox", "--name", name];
1011
+ if (defFile)
1012
+ sfArgs.push("--definition-file", defFile);
1013
+ const result = await runSfCommand("org", sfArgs, { timeout: 300_000 });
1014
+ if (!result.success)
1015
+ return `Error: ${result.raw}`;
1016
+ return `Sandbox "${name}" creation started.`;
1017
+ }
1018
+ catch (err) {
1019
+ return `Error creating sandbox: ${err.message}`;
1020
+ }
1021
+ },
1022
+ };
1023
+ // ---------------------------------------------------------------------------
1024
+ // Built-in prompt commands
1025
+ // ---------------------------------------------------------------------------
1026
+ const commitCommand = {
1027
+ name: "commit",
1028
+ description: "Review staged changes and create a git commit",
1029
+ type: "prompt",
1030
+ 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.",
1031
+ };
1032
+ const diffCommand = {
1033
+ name: "diff",
1034
+ description: "Show and explain the current git diff",
1035
+ type: "prompt",
1036
+ 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.",
1037
+ };
1038
+ const deployCommand = {
1039
+ name: "deploy",
1040
+ description: "Deploy the current Salesforce project to the default org",
1041
+ type: "prompt",
1042
+ 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.",
1043
+ };
1044
+ const testCommand = {
1045
+ name: "test",
1046
+ description: "Run all Apex tests and report results",
1047
+ type: "prompt",
1048
+ 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.",
1049
+ };
1050
+ const compactCommand = {
1051
+ name: "compact",
1052
+ description: "Summarize older messages to free up context space",
1053
+ type: "local",
1054
+ execute: async () => {
1055
+ const tokensBefore = estimateMessagesTokens([]);
1056
+ // Note: actual compaction requires access to the conversation messages
1057
+ // which are in the LangGraph checkpointer. For now, report status.
1058
+ return `Context compaction available.\nEstimated tokens: ${tokensBefore}\nTo free context, start a new session or use a model with a larger context window.`;
1059
+ },
1060
+ };
1061
+ const rememberCommand = {
1062
+ name: "remember",
1063
+ description: "Save what you learned in this conversation to memory",
1064
+ type: "prompt",
1065
+ getPrompt: (args) => {
1066
+ const extra = args.trim() ? `\n\nSpecifically, remember: ${args}` : "";
1067
+ return `Review this conversation and save any important learnings, corrections, or project-specific knowledge to .harnessforce/agent.md. Create the file if it doesn't exist. Use a structured format with headers and bullet points.${extra}`;
1068
+ },
1069
+ };
1070
+ const threadsCommand = {
1071
+ name: "threads",
1072
+ description: "List previous conversation sessions",
1073
+ type: "local",
1074
+ execute: async () => {
1075
+ const manager = createSessionManager();
1076
+ const sessions = await manager.list();
1077
+ if (sessions.length === 0) {
1078
+ return "No saved sessions found.";
1079
+ }
1080
+ const lines = sessions.map((s) => {
1081
+ const started = new Date(s.startedAt).toLocaleString();
1082
+ const last = new Date(s.lastMessageAt).toLocaleString();
1083
+ return ` ${s.id.slice(0, 8)}... ${s.messageCount} msgs started ${started} last ${last}`;
1084
+ });
1085
+ return `Saved sessions (${sessions.length}):\n\n${lines.join("\n")}\n`;
1086
+ },
1087
+ };
1088
+ const resumeCommand = {
1089
+ name: "resume",
1090
+ description: "Resume a previous conversation session",
1091
+ type: "local",
1092
+ execute: async () => {
1093
+ const manager = createSessionManager();
1094
+ const sessions = await manager.list();
1095
+ if (sessions.length === 0)
1096
+ return "No previous sessions found.";
1097
+ const lines = sessions.slice(0, 10).map((s, i) => ` ${i + 1}. ${s.id.slice(0, 8)}... (${s.messageCount} messages, ${new Date(s.lastMessageAt).toLocaleString()})`);
1098
+ return `Previous sessions:\n\n${lines.join("\n")}\n\nUse: harnessforce --resume <session-id> to resume.`;
1099
+ },
1100
+ };
1101
+ const costCommand = {
1102
+ name: "cost",
1103
+ description: "Show token usage and estimated cost for this session",
1104
+ type: "local",
1105
+ execute: async () => {
1106
+ return sessionCostTracker.getUsageSummary();
1107
+ },
1108
+ };
1109
+ const undoCommand = {
1110
+ name: "undo",
1111
+ description: "Restore the previous version of the last edited file",
1112
+ type: "local",
1113
+ execute: async () => {
1114
+ const lastFile = getLastEditedFile();
1115
+ if (!lastFile)
1116
+ return "No recent file edits to undo.";
1117
+ const restored = await restoreLastVersion(lastFile);
1118
+ if (restored)
1119
+ return `Restored previous version of ${lastFile}`;
1120
+ return `No history found for ${lastFile}`;
1121
+ },
1122
+ };
1123
+ const hooksCommand = {
1124
+ name: "hooks",
1125
+ description: "List configured hooks from .harnessforce/settings.json",
1126
+ type: "local",
1127
+ execute: async () => {
1128
+ const hooks = loadHooks();
1129
+ if (hooks.length === 0)
1130
+ return "No hooks configured.\nAdd hooks to .harnessforce/settings.json";
1131
+ const lines = hooks.map((h) => ` ${h.event}: ${h.command} ${(h.args || []).join(" ")}`);
1132
+ return `Configured hooks:\n\n${lines.join("\n")}`;
1133
+ },
1134
+ };
1135
+ // ---------------------------------------------------------------------------
1136
+ // Editor, Output Style, Todos commands
1137
+ // ---------------------------------------------------------------------------
1138
+ const editorCommand = {
1139
+ name: "editor",
1140
+ description: "Open current input in an external editor ($VISUAL / $EDITOR)",
1141
+ type: "local",
1142
+ execute: async (_args, ctx) => {
1143
+ const result = await openInEditor("");
1144
+ if (result === null)
1145
+ return "Editor cancelled (empty result).";
1146
+ return `Editor content:\n${result}`;
1147
+ // Note: ideally this would submit the content as a message,
1148
+ // but for now just show what was typed
1149
+ },
1150
+ };
1151
+ /** Available output styles. */
1152
+ const OUTPUT_STYLES = ["default", "explanatory", "learning"];
1153
+ let currentOutputStyle = "default";
1154
+ export function getOutputStyle() {
1155
+ return currentOutputStyle;
1156
+ }
1157
+ const outputStyleCommand = {
1158
+ name: "output-style",
1159
+ description: "Show or switch output style (default, explanatory, learning)",
1160
+ type: "local",
1161
+ execute: async (args) => {
1162
+ const style = args.trim().toLowerCase();
1163
+ if (!style) {
1164
+ const lines = OUTPUT_STYLES.map((s) => ` ${s === currentOutputStyle ? "* " : " "}${s}`);
1165
+ return `Output styles:\n\n${lines.join("\n")}\n`;
1166
+ }
1167
+ if (!OUTPUT_STYLES.includes(style)) {
1168
+ return `Unknown style "${style}". Available: ${OUTPUT_STYLES.join(", ")}`;
1169
+ }
1170
+ currentOutputStyle = style;
1171
+ return `Output style switched to: ${style}`;
1172
+ },
1173
+ };
1174
+ const todosCommand = {
1175
+ name: "todos",
1176
+ description: "Show the current todo list from the session",
1177
+ type: "local",
1178
+ execute: async () => {
1179
+ const todos = getTodos();
1180
+ if (todos.length === 0)
1181
+ return "No todos. The agent can create todos with the write_todos tool.";
1182
+ const lines = todos.map((t) => {
1183
+ const icon = t.status === "completed"
1184
+ ? "\u2713"
1185
+ : t.status === "in_progress"
1186
+ ? "\u25C9"
1187
+ : "\u2610";
1188
+ return `${icon} [${t.id}] ${t.title}`;
1189
+ });
1190
+ return lines.join("\n");
1191
+ },
1192
+ };
1193
+ // ---------------------------------------------------------------------------
1194
+ // Plan / Approve, Version, Changelog, Feedback, Tokens, Reload
1195
+ // ---------------------------------------------------------------------------
1196
+ const planCommand = {
1197
+ name: "plan",
1198
+ description: "Enter plan mode — agent explores but can't make changes",
1199
+ type: "local",
1200
+ execute: async (_args, ctx) => {
1201
+ if (ctx.setPermissionMode)
1202
+ ctx.setPermissionMode("plan");
1203
+ return "Entered plan mode. Agent can explore but won't make changes.\nUse /approve to exit plan mode and execute.";
1204
+ },
1205
+ };
1206
+ const approveCommand = {
1207
+ name: "approve",
1208
+ description: "Exit plan mode and let the agent execute changes",
1209
+ type: "local",
1210
+ execute: async (_args, ctx) => {
1211
+ if (ctx.setPermissionMode)
1212
+ ctx.setPermissionMode("default");
1213
+ return "Plan approved. Agent can now make changes.";
1214
+ },
1215
+ };
1216
+ const versionCommand = {
1217
+ name: "version",
1218
+ description: "Show the current Harnessforce version",
1219
+ type: "local",
1220
+ execute: async () => `Harnessforce v${process.env.npm_package_version ?? "0.7.4"}`,
1221
+ };
1222
+ const changelogCommand = {
1223
+ name: "changelog",
1224
+ description: "Open the changelog / release notes in the browser",
1225
+ type: "local",
1226
+ execute: async () => {
1227
+ try {
1228
+ (await import("node:child_process")).execSync("open https://github.com/skyrmionz/harnessforce/releases");
1229
+ }
1230
+ catch { }
1231
+ return "Opened changelog in browser.";
1232
+ },
1233
+ };
1234
+ const feedbackCommand = {
1235
+ name: "feedback",
1236
+ description: "Open GitHub Issues to submit feedback",
1237
+ type: "local",
1238
+ execute: async () => {
1239
+ try {
1240
+ (await import("node:child_process")).execSync("open https://github.com/skyrmionz/harnessforce/issues/new");
1241
+ }
1242
+ catch { }
1243
+ return "Opened GitHub Issues in browser.";
1244
+ },
1245
+ };
1246
+ const tokensCommand = {
1247
+ name: "tokens",
1248
+ description: "Show token usage and estimated cost (alias for /cost)",
1249
+ type: "local",
1250
+ execute: async () => sessionCostTracker.getUsageSummary(),
1251
+ };
1252
+ const reloadCommand = {
1253
+ name: "reload",
1254
+ description: "Reload configuration from ~/.harnessforce/models.yaml",
1255
+ type: "local",
1256
+ execute: async () => {
1257
+ ensureConfigFile();
1258
+ return "Configuration reloaded from ~/.harnessforce/models.yaml";
1259
+ },
1260
+ };
1261
+ // ---------------------------------------------------------------------------
1262
+ // Registry
1263
+ // ---------------------------------------------------------------------------
1264
+ const builtInCommands = [
1265
+ helpCommand,
1266
+ setKeyCommand,
1267
+ modelCommand,
1268
+ modelListCommand,
1269
+ skillListCommand,
1270
+ skillAddCommand,
1271
+ toolListCommand,
1272
+ orgCommand,
1273
+ orgLoginCommand,
1274
+ quitCommand,
1275
+ exitCommand,
1276
+ clearCommand,
1277
+ statusCommand,
1278
+ doctorCommand,
1279
+ rollbackCommand,
1280
+ initCommand,
1281
+ commitCommand,
1282
+ diffCommand,
1283
+ deployCommand,
1284
+ testCommand,
1285
+ compactCommand,
1286
+ rememberCommand,
1287
+ threadsCommand,
1288
+ resumeCommand,
1289
+ costCommand,
1290
+ undoCommand,
1291
+ hooksCommand,
1292
+ editorCommand,
1293
+ outputStyleCommand,
1294
+ todosCommand,
1295
+ planCommand,
1296
+ approveCommand,
1297
+ versionCommand,
1298
+ changelogCommand,
1299
+ feedbackCommand,
1300
+ tokensCommand,
1301
+ reloadCommand,
1302
+ // Salesforce local commands
1303
+ orgListCommand,
1304
+ orgOpenCommand,
1305
+ orgLimitsCommand,
1306
+ describeCommand,
1307
+ metadataCommand,
1308
+ retrieveCommand,
1309
+ queryCommand,
1310
+ queryDcCommand,
1311
+ insertCommand,
1312
+ agentPreviewCommand,
1313
+ dcObjectsCommand,
1314
+ dcDescribeCommand,
1315
+ deployStatusCommand,
1316
+ deployCancelCommand,
1317
+ testCoverageCommand,
1318
+ logsCommand,
1319
+ // Extended Salesforce commands
1320
+ scratchCreateCommand,
1321
+ scratchDeleteCommand,
1322
+ packageCreateCommand,
1323
+ packageVersionCommand,
1324
+ coverageCommand,
1325
+ deployHistoryCommand,
1326
+ orgDiffCommand,
1327
+ dataExportCommand,
1328
+ limitsWatchCommand,
1329
+ sandboxCreateCommand,
1330
+ // Salesforce prompt commands
1331
+ apexCommand,
1332
+ lwcCommand,
1333
+ flowCommand,
1334
+ triggerCommand,
1335
+ agentBuildCommand,
1336
+ agentTestCommand,
1337
+ agentDeployCommand,
1338
+ dcSetupCommand,
1339
+ testGenerateCommand,
1340
+ exportCommand,
1341
+ debugCommand,
1342
+ governorCommand,
1343
+ scaffoldCommand,
1344
+ connectedAppCommand,
1345
+ setupCommand,
1346
+ ];
1347
+ /**
1348
+ * Get all registered slash commands, including skill-based prompt commands.
1349
+ */
1350
+ export function getCommands(skillsDir = "./skills") {
1351
+ const skills = loadSkills(skillsDir);
1352
+ const skillCommands = skillsToCommands(skills);
1353
+ return [...builtInCommands, ...skillCommands];
1354
+ }
1355
+ /**
1356
+ * Find a command by name (with or without leading slash).
1357
+ */
1358
+ export function findCommand(input, skillsDir = "./skills") {
1359
+ const name = input.startsWith("/") ? input.slice(1) : input;
1360
+ // Split to separate command name from args
1361
+ const cmdName = name.split(/\s+/)[0].toLowerCase();
1362
+ const commands = getCommands(skillsDir);
1363
+ return commands.find((c) => c.name.toLowerCase() === cmdName);
1364
+ }
1365
+ /**
1366
+ * Convert loaded skills into prompt-type slash commands.
1367
+ */
1368
+ function skillsToCommands(skills) {
1369
+ return skills.map((skill) => ({
1370
+ name: skill.name,
1371
+ description: skill.description || skill.trigger || `Run the ${skill.name} skill`,
1372
+ type: "prompt",
1373
+ getPrompt: (args) => {
1374
+ let prompt = `Use the "${skill.name}" skill.\n\n`;
1375
+ prompt += `Skill instructions:\n${skill.content}\n`;
1376
+ if (args.trim()) {
1377
+ prompt += `\nUser context: ${args}`;
1378
+ }
1379
+ return prompt;
1380
+ },
1381
+ }));
1382
+ }
1383
+ //# sourceMappingURL=registry.js.map