@zhijiewang/openharness 2.4.0 → 2.8.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 (49) hide show
  1. package/README.md +2 -2
  2. package/dist/Tool.d.ts +2 -0
  3. package/dist/commands/ai.d.ts +6 -0
  4. package/dist/commands/ai.js +244 -0
  5. package/dist/commands/git.d.ts +6 -0
  6. package/dist/commands/git.js +167 -0
  7. package/dist/commands/index.d.ts +10 -31
  8. package/dist/commands/index.js +22 -1052
  9. package/dist/commands/info.d.ts +8 -0
  10. package/dist/commands/info.js +671 -0
  11. package/dist/commands/session.d.ts +6 -0
  12. package/dist/commands/session.js +214 -0
  13. package/dist/commands/settings.d.ts +6 -0
  14. package/dist/commands/settings.js +187 -0
  15. package/dist/commands/skills.d.ts +6 -0
  16. package/dist/commands/skills.js +117 -0
  17. package/dist/commands/types.d.ts +36 -0
  18. package/dist/commands/types.js +5 -0
  19. package/dist/components/InitWizard.js +61 -61
  20. package/dist/harness/config.d.ts +2 -0
  21. package/dist/harness/hooks.js +9 -6
  22. package/dist/harness/memory.js +28 -1
  23. package/dist/harness/plugins.d.ts +2 -0
  24. package/dist/harness/plugins.js +44 -11
  25. package/dist/harness/session-db.js +3 -1
  26. package/dist/harness/skill-registry.d.ts +21 -0
  27. package/dist/harness/skill-registry.js +35 -0
  28. package/dist/lsp/client.js +2 -1
  29. package/dist/main.js +10 -2
  30. package/dist/mcp/client.js +2 -1
  31. package/dist/mcp/server-mode.d.ts +10 -0
  32. package/dist/mcp/server-mode.js +17 -0
  33. package/dist/providers/anthropic.js +7 -8
  34. package/dist/providers/fallback.js +2 -3
  35. package/dist/providers/openai.js +3 -2
  36. package/dist/query/index.js +30 -6
  37. package/dist/query/tools.js +11 -0
  38. package/dist/query/types.d.ts +4 -0
  39. package/dist/renderer/layout-sections.d.ts +56 -0
  40. package/dist/renderer/layout-sections.js +462 -0
  41. package/dist/renderer/layout.d.ts +4 -2
  42. package/dist/renderer/layout.js +25 -500
  43. package/dist/repl.js +3 -1
  44. package/dist/services/SkillExtractor.js +2 -0
  45. package/dist/tools/SkillTool/index.js +26 -2
  46. package/dist/tools/TodoWriteTool/index.d.ts +37 -0
  47. package/dist/tools/TodoWriteTool/index.js +78 -0
  48. package/dist/tools.js +2 -0
  49. package/package.json +1 -1
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Session commands — /clear, /compact, /export, /history, /browse, /resume, /fork, /pin, /unpin
3
+ */
4
+ import { existsSync, mkdirSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { getContextWindow } from "../harness/cost.js";
8
+ import { createSession, listSessions, loadSession, saveSession } from "../harness/session.js";
9
+ import { compressMessages } from "../query/index.js";
10
+ function setPinned(args, ctx, pinned) {
11
+ const idx = parseInt(args.trim(), 10);
12
+ if (Number.isNaN(idx) || idx < 1 || idx > ctx.messages.length) {
13
+ return { output: `Usage: /${pinned ? "pin" : "unpin"} <message-number> (1-${ctx.messages.length})`, handled: true };
14
+ }
15
+ const updatedMessages = ctx.messages.map((m, i) => (i === idx - 1 ? { ...m, meta: { ...m.meta, pinned } } : m));
16
+ return {
17
+ output: `Message #${idx} ${pinned ? "pinned" : "unpinned"}.`,
18
+ handled: true,
19
+ compactedMessages: updatedMessages,
20
+ };
21
+ }
22
+ export function registerSessionCommands(register) {
23
+ register("clear", "Clear conversation history", () => {
24
+ return { output: "Conversation cleared.", handled: true, clearMessages: true };
25
+ });
26
+ register("compact", "Compress conversation history (optional: focus keyword or message number)", (args, ctx) => {
27
+ const focus = args.trim();
28
+ const before = ctx.messages.length;
29
+ const targetTokens = Math.floor(getContextWindow(ctx.model) * 0.6);
30
+ if (focus && /^\d+$/.test(focus)) {
31
+ const cutoff = parseInt(focus, 10);
32
+ if (cutoff < 1 || cutoff >= before) {
33
+ return { output: `Invalid: use 1-${before - 1}`, handled: true };
34
+ }
35
+ const kept = ctx.messages.slice(cutoff);
36
+ return {
37
+ output: `Compacted: removed first ${cutoff} messages, kept ${kept.length}.`,
38
+ handled: true,
39
+ compactedMessages: kept,
40
+ };
41
+ }
42
+ if (focus) {
43
+ const focusLower = focus.toLowerCase();
44
+ const preserved = ctx.messages.filter((m) => m.content.toLowerCase().includes(focusLower) || m.meta?.pinned);
45
+ const others = ctx.messages.filter((m) => !m.content.toLowerCase().includes(focusLower) && !m.meta?.pinned);
46
+ const compactedOthers = compressMessages(others, targetTokens);
47
+ const merged = [...compactedOthers, ...preserved].sort((a, b) => a.timestamp - b.timestamp);
48
+ return {
49
+ output: `Compacted with focus "${focus}": ${before} → ${merged.length} messages (preserved ${preserved.length} matching).`,
50
+ handled: true,
51
+ compactedMessages: merged,
52
+ };
53
+ }
54
+ const compacted = compressMessages(ctx.messages, targetTokens);
55
+ const dropped = before - compacted.length;
56
+ return {
57
+ output: `Compacted: ${before} → ${compacted.length} messages (dropped ${dropped} older turns).`,
58
+ handled: true,
59
+ compactedMessages: compacted,
60
+ };
61
+ });
62
+ register("export", "Export conversation to file", (_args, ctx) => {
63
+ const lines = ctx.messages
64
+ .filter((m) => m.role === "user" || m.role === "assistant")
65
+ .map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
66
+ .join("\n\n");
67
+ const filename = `.oh/export-${ctx.sessionId}.md`;
68
+ try {
69
+ mkdirSync(dirname(filename), { recursive: true });
70
+ const { writeFileSync } = require("node:fs");
71
+ writeFileSync(filename, lines);
72
+ return { output: `Exported to ${filename}`, handled: true };
73
+ }
74
+ catch {
75
+ return { output: `Export failed. Content:\n\n${lines.slice(0, 500)}`, handled: true };
76
+ }
77
+ });
78
+ register("history", "List recent sessions or search across them", (args) => {
79
+ const parts = args.trim().split(/\s+/);
80
+ const sessionDir = join(homedir(), ".oh", "sessions");
81
+ if (parts[0] === "search" && parts[1]) {
82
+ const term = parts.slice(1).join(" ").toLowerCase();
83
+ const sessions = listSessions(sessionDir);
84
+ const matches = [];
85
+ for (const s of sessions) {
86
+ try {
87
+ const full = loadSession(s.id, sessionDir);
88
+ const hit = full.messages.find((m) => typeof m.content === "string" && m.content.toLowerCase().includes(term));
89
+ if (hit) {
90
+ const date = new Date(s.updatedAt).toLocaleDateString();
91
+ matches.push(` ${s.id} ${date} ${s.model || "?"}`);
92
+ }
93
+ }
94
+ catch {
95
+ /* skip */
96
+ }
97
+ }
98
+ if (matches.length === 0)
99
+ return { output: `No sessions matching "${term}".`, handled: true };
100
+ return { output: `Sessions matching "${term}":\n${matches.join("\n")}`, handled: true };
101
+ }
102
+ const n = parseInt(parts[0] ?? "10", 10) || 10;
103
+ const sessions = listSessions(sessionDir).slice(0, n);
104
+ if (sessions.length === 0)
105
+ return { output: "No saved sessions.", handled: true };
106
+ const lines = sessions.map((s) => {
107
+ const date = new Date(s.updatedAt).toLocaleDateString();
108
+ const cost = s.cost > 0 ? ` $${s.cost.toFixed(4)}` : "";
109
+ return ` ${s.id} ${date} ${String(s.messages).padStart(3)} msgs ${(s.model || "?").slice(0, 24)}${cost}`;
110
+ });
111
+ return { output: `Recent sessions (use /resume <id> to continue):\n${lines.join("\n")}`, handled: true };
112
+ });
113
+ register("browse", "Open interactive session browser", () => {
114
+ return { output: "__OPEN_SESSION_BROWSER__", handled: true };
115
+ });
116
+ register("resume", "Resume a saved session by ID", (args) => {
117
+ const id = args.trim();
118
+ if (!id)
119
+ return { output: "Usage: /resume <session-id>", handled: true };
120
+ const sessionDir = join(homedir(), ".oh", "sessions");
121
+ try {
122
+ loadSession(id, sessionDir);
123
+ return { output: `Resuming session ${id}...`, handled: true, resumeSessionId: id };
124
+ }
125
+ catch {
126
+ return { output: `Session not found: ${id}`, handled: true };
127
+ }
128
+ });
129
+ register("fork", "Fork current session (create a branch you can resume later)", (_args, ctx) => {
130
+ const forked = createSession("", "");
131
+ forked.messages = [...ctx.messages];
132
+ saveSession(forked);
133
+ return {
134
+ output: `Session forked as ${forked.id}. Resume later with: oh --resume ${forked.id}`,
135
+ handled: true,
136
+ };
137
+ });
138
+ register("pin", "Pin a message (survives /compact)", (args, ctx) => setPinned(args, ctx, true));
139
+ register("unpin", "Unpin a message", (args, ctx) => setPinned(args, ctx, false));
140
+ register("rebuild-sessions", "Rebuild session search index", () => {
141
+ import("../harness/session-db.js")
142
+ .then(({ openSessionDb, rebuildIndex, closeSessionDb }) => {
143
+ const db = openSessionDb();
144
+ const count = rebuildIndex(db);
145
+ closeSessionDb(db);
146
+ console.log(`Rebuilt session search index: ${count} sessions indexed.`);
147
+ })
148
+ .catch((err) => {
149
+ console.log(`Failed to rebuild index: ${err.message}`);
150
+ });
151
+ return { output: "Rebuilding session search index...", handled: true };
152
+ });
153
+ register("add-dir", "Add an additional working directory", (args) => {
154
+ const dir = args.trim();
155
+ if (!dir) {
156
+ return {
157
+ output: "Usage: /add-dir <path>\n\nAdds a directory to the session's working directories, allowing the AI to access files in multiple locations.",
158
+ handled: true,
159
+ };
160
+ }
161
+ const resolved = resolve(dir);
162
+ if (!existsSync(resolved)) {
163
+ return { output: `Directory not found: ${resolved}`, handled: true };
164
+ }
165
+ return { output: `Added working directory: ${resolved}`, handled: true };
166
+ });
167
+ register("listen", "Listen to stdin for input", () => {
168
+ return {
169
+ output: "Listening mode enabled. Paste or pipe input, then press Ctrl+D (EOF) to submit.",
170
+ handled: true,
171
+ };
172
+ });
173
+ register("truncate", "Remove messages from the end of conversation", (args, ctx) => {
174
+ const countStr = args.trim();
175
+ const count = parseInt(countStr, 10);
176
+ if (!countStr || Number.isNaN(count) || count < 1) {
177
+ return {
178
+ output: `Usage: /truncate <count>\n\nRemove the last <count> messages from the conversation.\nCurrent message count: ${ctx.messages.length}`,
179
+ handled: true,
180
+ };
181
+ }
182
+ if (count >= ctx.messages.length) {
183
+ return { output: "Cannot truncate all messages. Use /clear instead.", handled: true };
184
+ }
185
+ const kept = ctx.messages.slice(0, ctx.messages.length - count);
186
+ return {
187
+ output: `Truncated ${count} message(s). ${kept.length} remaining.`,
188
+ handled: true,
189
+ compactedMessages: kept,
190
+ };
191
+ });
192
+ register("search", "Search current conversation", (args, ctx) => {
193
+ const term = args.trim().toLowerCase();
194
+ if (!term) {
195
+ return {
196
+ output: "Usage: /search <term>\n\nSearch through all messages in the current conversation.",
197
+ handled: true,
198
+ };
199
+ }
200
+ const matches = [];
201
+ for (let i = 0; i < ctx.messages.length; i++) {
202
+ const msg = ctx.messages[i];
203
+ if (typeof msg.content === "string" && msg.content.toLowerCase().includes(term)) {
204
+ const preview = msg.content.slice(0, 80).replace(/\n/g, " ");
205
+ matches.push(` #${i + 1} [${msg.role}]: ${preview}...`);
206
+ }
207
+ }
208
+ if (matches.length === 0) {
209
+ return { output: `No messages matching "${term}".`, handled: true };
210
+ }
211
+ return { output: `Found ${matches.length} message(s) matching "${term}":\n${matches.join("\n")}`, handled: true };
212
+ });
213
+ }
214
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Settings commands — /theme, /companion, /fast, /keys, /effort, /sandbox, /permissions, /allowed-tools
3
+ */
4
+ import type { CommandHandler } from "./types.js";
5
+ export declare function registerSettingsCommands(register: (name: string, description: string, handler: CommandHandler) => void): void;
6
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Settings commands — /theme, /companion, /fast, /keys, /effort, /sandbox, /permissions, /allowed-tools
3
+ */
4
+ import { readOhConfig } from "../harness/config.js";
5
+ import { loadKeybindings } from "../harness/keybindings.js";
6
+ export function registerSettingsCommands(register) {
7
+ register("theme", "Switch theme (dark/light)", (args) => {
8
+ const theme = args.trim().toLowerCase();
9
+ if (theme !== "dark" && theme !== "light") {
10
+ return { output: "Usage: /theme dark or /theme light", handled: true };
11
+ }
12
+ return { output: `__SWITCH_THEME__:${theme}`, handled: true };
13
+ });
14
+ register("companion", "Toggle companion visibility (off/on)", (args) => {
15
+ const arg = args.trim().toLowerCase();
16
+ if (arg === "off")
17
+ return { output: "__COMPANION_OFF__", handled: true };
18
+ if (arg === "on")
19
+ return { output: "__COMPANION_ON__", handled: true };
20
+ return { output: "Usage: /companion off or /companion on", handled: true };
21
+ });
22
+ register("fast", "Toggle fast mode (optimized for speed)", () => {
23
+ return { output: "", handled: true, toggleFastMode: true };
24
+ });
25
+ register("keys", "Show keyboard shortcuts", () => {
26
+ const bindings = loadKeybindings();
27
+ const shortcuts = [
28
+ "Keyboard Shortcuts:",
29
+ "",
30
+ " Navigation:",
31
+ " ↑ / ↓ Input history",
32
+ " Tab Cycle autocomplete suggestions",
33
+ " Escape Cancel / clear autocomplete",
34
+ " Ctrl+C Abort current request / exit",
35
+ " Scroll wheel Scroll through messages",
36
+ "",
37
+ " Editing:",
38
+ " Alt+Enter Insert newline (multi-line input)",
39
+ " Ctrl+A Move cursor to start of line",
40
+ " Ctrl+E Move cursor to end of line",
41
+ "",
42
+ " Display:",
43
+ " Ctrl+K Toggle code block expansion",
44
+ " Ctrl+O Toggle thinking block expansion",
45
+ " Tab (in output) Expand/collapse tool call output",
46
+ "",
47
+ " Custom keybindings (~/.oh/keybindings.json):",
48
+ ];
49
+ for (const b of bindings) {
50
+ shortcuts.push(` ${b.key.padEnd(18)} ${b.action}`);
51
+ }
52
+ shortcuts.push("", " Session:", " /vim Toggle Vim mode", " /browse Interactive session browser", " /theme dark|light Switch theme");
53
+ return { output: shortcuts.join("\n"), handled: true };
54
+ });
55
+ register("effort", "Set reasoning effort level (low/medium/high/max)", (args) => {
56
+ const level = args.trim().toLowerCase();
57
+ const valid = ["low", "medium", "high", "max"];
58
+ if (!valid.includes(level)) {
59
+ return {
60
+ output: `Usage: /effort <${valid.join("|")}>\n\nlow — fast, minimal reasoning\nmedium — balanced (default)\nhigh — thorough reasoning\nmax — maximum depth (Opus only)`,
61
+ handled: true,
62
+ };
63
+ }
64
+ return { output: `Effort level set to: ${level}`, handled: true };
65
+ });
66
+ register("sandbox", "Show sandbox status and restrictions", () => {
67
+ const { sandboxStatus } = require("../harness/sandbox.js");
68
+ return { output: `${sandboxStatus()}\n\nConfigure in .oh/config.yaml under sandbox:`, handled: true };
69
+ });
70
+ register("permissions", "View or change permission mode", (args, ctx) => {
71
+ const mode = args.trim().toLowerCase();
72
+ if (!mode) {
73
+ return {
74
+ output: `Current permission mode: ${ctx.permissionMode}\n\nAvailable modes:\n ask Prompt for medium/high risk (default)\n trust Auto-approve everything\n deny Only low-risk read-only\n acceptEdits Auto-approve file edits\n plan Read-only mode\n auto Auto-approve, block dangerous bash\n bypassPermissions CI/CD only`,
75
+ handled: true,
76
+ };
77
+ }
78
+ const valid = ["ask", "trust", "deny", "acceptedits", "plan", "auto", "bypasspermissions"];
79
+ if (!valid.includes(mode)) {
80
+ return { output: `Unknown mode: ${mode}. Valid: ${valid.join(", ")}`, handled: true };
81
+ }
82
+ return {
83
+ output: `Permission mode set to: ${mode}\n(Note: takes effect for new tool calls in this session)`,
84
+ handled: true,
85
+ };
86
+ });
87
+ register("allowed-tools", "View tool permission rules", () => {
88
+ const config = readOhConfig();
89
+ const rules = config?.toolPermissions;
90
+ if (!rules || rules.length === 0) {
91
+ return {
92
+ output: 'No custom tool permission rules configured.\n\nAdd rules to .oh/config.yaml:\n\ntoolPermissions:\n - tool: Bash\n action: ask\n pattern: "^rm .*"',
93
+ handled: true,
94
+ };
95
+ }
96
+ const lines = rules.map((r) => {
97
+ const parts = [` ${r.tool}: ${r.action}`];
98
+ if (r.pattern)
99
+ parts.push(`(pattern: ${r.pattern})`);
100
+ return parts.join(" ");
101
+ });
102
+ return { output: `Tool permission rules:\n${lines.join("\n")}`, handled: true };
103
+ });
104
+ register("vim", "Toggle Vim mode", () => {
105
+ return { output: "__TOGGLE_VIM__", handled: true };
106
+ });
107
+ register("login", "Set API key for current provider", (args, ctx) => {
108
+ const key = args.trim();
109
+ if (!key) {
110
+ const envHint = ctx.providerName === "anthropic"
111
+ ? "ANTHROPIC_API_KEY"
112
+ : ctx.providerName === "openai"
113
+ ? "OPENAI_API_KEY"
114
+ : `${ctx.providerName.toUpperCase()}_API_KEY`;
115
+ return {
116
+ output: `Usage: /login <api-key>\n\nAlternatively, set the ${envHint} environment variable.\nCurrent provider: ${ctx.providerName}`,
117
+ handled: true,
118
+ };
119
+ }
120
+ return {
121
+ output: `API key set for ${ctx.providerName}. (Takes effect for new requests in this session.)`,
122
+ handled: true,
123
+ };
124
+ });
125
+ register("logout", "Clear API key for current provider", (_args, ctx) => {
126
+ return {
127
+ output: `API key cleared for ${ctx.providerName}. Set via environment variable or /login to re-authenticate.`,
128
+ handled: true,
129
+ };
130
+ });
131
+ register("terminal-setup", "Terminal configuration hints", () => {
132
+ const lines = [
133
+ "Terminal Setup Hints:",
134
+ "",
135
+ " Recommended terminal: Windows Terminal, iTerm2, Alacritty, or Kitty",
136
+ " Font: Use a Nerd Font (e.g., FiraCode Nerd Font) for icon support",
137
+ " Minimum size: 80x24 characters",
138
+ "",
139
+ " Environment variables:",
140
+ " TERM_PROGRAM Your terminal emulator",
141
+ " COLORTERM Set to 'truecolor' for full color support",
142
+ " FORCE_COLOR=1 Force color output in CI environments",
143
+ "",
144
+ " Shell: bash or zsh recommended. Fish is supported but less tested.",
145
+ "",
146
+ " If you see broken characters, ensure your terminal supports UTF-8.",
147
+ ];
148
+ return { output: lines.join("\n"), handled: true };
149
+ });
150
+ register("verbose", "Toggle verbose mode", () => {
151
+ return {
152
+ output: "Verbose mode toggled. Set OH_VERBOSE=1 in your environment for persistent verbose output.",
153
+ handled: true,
154
+ };
155
+ });
156
+ register("quiet", "Toggle quiet/minimal output mode", () => {
157
+ return { output: "Quiet mode toggled. Minimal output will be shown.", handled: true };
158
+ });
159
+ register("provider", "Show or switch provider", (args, ctx) => {
160
+ const provider = args.trim();
161
+ if (!provider) {
162
+ const lines = [
163
+ `Current provider: ${ctx.providerName}`,
164
+ `Current model: ${ctx.model}`,
165
+ "",
166
+ "Available providers:",
167
+ " anthropic — Claude models (requires ANTHROPIC_API_KEY)",
168
+ " openai — GPT models (requires OPENAI_API_KEY)",
169
+ " ollama — Local models via Ollama",
170
+ " openrouter — Multi-provider gateway (requires OPENROUTER_API_KEY)",
171
+ "",
172
+ "Switch with: /provider <name>",
173
+ "Or restart: oh --model <provider>/<model>",
174
+ ];
175
+ return { output: lines.join("\n"), handled: true };
176
+ }
177
+ const valid = ["anthropic", "openai", "ollama", "openrouter"];
178
+ if (!valid.includes(provider)) {
179
+ return { output: `Unknown provider: ${provider}. Valid: ${valid.join(", ")}`, handled: true };
180
+ }
181
+ return {
182
+ output: `Provider switching requires a session restart.\nRun: oh --model ${provider}/<model-name>`,
183
+ handled: true,
184
+ };
185
+ });
186
+ }
187
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Skill management commands — /skill-create, /skill-delete, /skill-edit, /skill-search, /skill-install
3
+ */
4
+ import type { CommandHandler } from "./types.js";
5
+ export declare function registerSkillCommands(register: (name: string, description: string, handler: CommandHandler) => void): void;
6
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Skill management commands — /skill-create, /skill-delete, /skill-edit, /skill-search, /skill-install
3
+ */
4
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ export function registerSkillCommands(register) {
7
+ register("skill-create", "Create a new skill file", (args) => {
8
+ const name = args.trim();
9
+ if (!name)
10
+ return { output: "Usage: /skill-create <name>", handled: true };
11
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
12
+ return { output: "Error: Invalid skill name.", handled: true };
13
+ }
14
+ const dir = join(process.cwd(), ".oh", "skills");
15
+ mkdirSync(dir, { recursive: true });
16
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
17
+ const filePath = join(dir, `${slug}.md`);
18
+ if (existsSync(filePath)) {
19
+ return { output: `Skill "${slug}" already exists at ${filePath}`, handled: true };
20
+ }
21
+ const template = `---
22
+ name: ${slug}
23
+ description: Describe what this skill does
24
+ trigger: ${slug}
25
+ ---
26
+
27
+ # ${name}
28
+
29
+ ## When to Use
30
+ Describe when this skill should be triggered.
31
+
32
+ ## Procedure
33
+ 1. Step one
34
+ 2. Step two
35
+ 3. Step three
36
+
37
+ ## Pitfalls
38
+ - Common mistakes to avoid
39
+
40
+ ## Verification
41
+ How to confirm the skill worked correctly.
42
+ `;
43
+ writeFileSync(filePath, template);
44
+ return { output: `Created skill: ${filePath}\nEdit the file to customize it.`, handled: true };
45
+ });
46
+ register("skill-delete", "Delete a skill file", (args) => {
47
+ const name = args.trim();
48
+ if (!name)
49
+ return { output: "Usage: /skill-delete <name>", handled: true };
50
+ const { findSkill } = require("../harness/plugins.js");
51
+ const skill = findSkill(name);
52
+ if (!skill)
53
+ return { output: `Skill "${name}" not found.`, handled: true };
54
+ try {
55
+ const { unlinkSync } = require("node:fs");
56
+ unlinkSync(skill.filePath);
57
+ return { output: `Deleted skill: ${skill.filePath}`, handled: true };
58
+ }
59
+ catch (err) {
60
+ return { output: `Error deleting skill: ${err.message}`, handled: true };
61
+ }
62
+ });
63
+ register("skill-edit", "Show skill file path for editing", (args) => {
64
+ const name = args.trim();
65
+ if (!name)
66
+ return { output: "Usage: /skill-edit <name>", handled: true };
67
+ const { findSkill } = require("../harness/plugins.js");
68
+ const skill = findSkill(name);
69
+ if (!skill)
70
+ return { output: `Skill "${name}" not found.`, handled: true };
71
+ return { output: `Skill file: ${skill.filePath}\nEdit this file to update the skill.`, handled: true };
72
+ });
73
+ register("skill-search", "Search the skills registry", (args) => {
74
+ const query = args.trim();
75
+ if (!query)
76
+ return { output: "Usage: /skill-search <query>", handled: true };
77
+ import("../harness/skill-registry.js").then(async ({ fetchRegistry, searchRegistry }) => {
78
+ try {
79
+ const registry = await fetchRegistry();
80
+ const results = searchRegistry(registry, query);
81
+ if (results.length === 0) {
82
+ console.log(`No skills found matching "${query}".`);
83
+ }
84
+ else {
85
+ const lines = results.map((s) => ` ${s.name.padEnd(20)} ${s.description} [${s.tags.join(", ")}]`);
86
+ console.log(`Found ${results.length} skill(s):\n${lines.join("\n")}\n\nInstall: /skill-install <name>`);
87
+ }
88
+ }
89
+ catch (err) {
90
+ console.log(`Registry search failed: ${err.message}`);
91
+ }
92
+ });
93
+ return { output: "Searching skills registry...", handled: true };
94
+ });
95
+ register("skill-install", "Install a skill from the registry", (args) => {
96
+ const name = args.trim();
97
+ if (!name)
98
+ return { output: "Usage: /skill-install <name>", handled: true };
99
+ import("../harness/skill-registry.js").then(async ({ fetchRegistry, installSkill }) => {
100
+ try {
101
+ const registry = await fetchRegistry();
102
+ const skill = registry.skills.find((s) => s.name.toLowerCase() === name.toLowerCase());
103
+ if (!skill) {
104
+ console.log(`Skill "${name}" not found in registry. Try /skill-search first.`);
105
+ return;
106
+ }
107
+ const path = await installSkill(skill);
108
+ console.log(`Installed skill "${skill.name}" to ${path}`);
109
+ }
110
+ catch (err) {
111
+ console.log(`Installation failed: ${err.message}`);
112
+ }
113
+ });
114
+ return { output: `Installing skill "${name}"...`, handled: true };
115
+ });
116
+ }
117
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared types for the slash command system.
3
+ */
4
+ import type { Message } from "../types/message.js";
5
+ export type CommandResult = {
6
+ /** Text output to display */
7
+ output: string;
8
+ /** If true, don't send to LLM */
9
+ handled: boolean;
10
+ /** If set, clear messages */
11
+ clearMessages?: boolean;
12
+ /** If set, update model */
13
+ newModel?: string;
14
+ /** If set, replace messages with compacted version */
15
+ compactedMessages?: Message[];
16
+ /** If true, open the cybergotchi setup UI */
17
+ openCybergotchiSetup?: boolean;
18
+ /** If set, resume this session ID */
19
+ resumeSessionId?: string;
20
+ /** If set, prepend this text to the user's prompt before sending to LLM */
21
+ prependToPrompt?: string;
22
+ /** If set, toggle fast mode */
23
+ toggleFastMode?: boolean;
24
+ };
25
+ export type CommandHandler = (args: string, context: CommandContext) => CommandResult;
26
+ export type CommandContext = {
27
+ messages: Message[];
28
+ model: string;
29
+ providerName: string;
30
+ permissionMode: string;
31
+ totalCost: number;
32
+ totalInputTokens: number;
33
+ totalOutputTokens: number;
34
+ sessionId: string;
35
+ };
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared types for the slash command system.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map