@zhijiewang/openharness 2.0.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/DeferredTool.js +3 -1
- package/dist/Tool.d.ts +1 -1
- package/dist/agents/roles.js +58 -62
- package/dist/commands/cybergotchi.d.ts +1 -1
- package/dist/commands/cybergotchi.js +30 -30
- package/dist/commands/index.js +360 -122
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +6 -6
- package/dist/components/CompanionFooter.d.ts +1 -1
- package/dist/components/CompanionFooter.js +6 -8
- package/dist/components/CybergotchiBubble.js +5 -5
- package/dist/components/CybergotchiPanel.d.ts +1 -1
- package/dist/components/CybergotchiPanel.js +7 -7
- package/dist/components/CybergotchiPanelConnected.js +2 -2
- package/dist/components/CybergotchiSetup.js +26 -24
- package/dist/components/CybergotchiSprite.d.ts +1 -1
- package/dist/components/CybergotchiSprite.js +8 -12
- package/dist/components/DiffView.d.ts +1 -1
- package/dist/components/DiffView.js +10 -10
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/InitWizard.js +65 -33
- package/dist/components/Markdown.js +2 -4
- package/dist/components/Messages.js +4 -4
- package/dist/components/PermissionPrompt.d.ts +1 -1
- package/dist/components/PermissionPrompt.js +15 -17
- package/dist/components/REPL.d.ts +1 -1
- package/dist/components/REPL.js +74 -49
- package/dist/components/Spinner.js +2 -2
- package/dist/components/TextInput.js +35 -29
- package/dist/components/ToolCallDisplay.js +3 -5
- package/dist/cybergotchi/bones.d.ts +1 -1
- package/dist/cybergotchi/bones.js +8 -8
- package/dist/cybergotchi/config.d.ts +2 -2
- package/dist/cybergotchi/config.js +13 -13
- package/dist/cybergotchi/events.d.ts +5 -5
- package/dist/cybergotchi/events.js +7 -7
- package/dist/cybergotchi/needs.d.ts +2 -2
- package/dist/cybergotchi/needs.js +7 -9
- package/dist/cybergotchi/personality.d.ts +2 -2
- package/dist/cybergotchi/personality.js +2 -2
- package/dist/cybergotchi/species.d.ts +1 -1
- package/dist/cybergotchi/species.js +145 -217
- package/dist/cybergotchi/speech.d.ts +2 -2
- package/dist/cybergotchi/speech.js +43 -43
- package/dist/cybergotchi/types.d.ts +4 -4
- package/dist/cybergotchi/types.js +26 -26
- package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
- package/dist/cybergotchi/useCybergotchi.js +29 -25
- package/dist/git/index.js +11 -9
- package/dist/harness/checkpoints.js +29 -21
- package/dist/harness/config.d.ts +12 -2
- package/dist/harness/config.js +15 -9
- package/dist/harness/context-warning.d.ts +1 -1
- package/dist/harness/context-warning.js +1 -1
- package/dist/harness/cost.js +1 -1
- package/dist/harness/credentials.js +13 -13
- package/dist/harness/hooks.js +7 -5
- package/dist/harness/keybindings.js +20 -18
- package/dist/harness/marketplace.d.ts +3 -3
- package/dist/harness/marketplace.js +55 -42
- package/dist/harness/memory.d.ts +23 -5
- package/dist/harness/memory.js +142 -41
- package/dist/harness/onboarding.js +30 -10
- package/dist/harness/plugins.d.ts +9 -1
- package/dist/harness/plugins.js +54 -30
- package/dist/harness/rules.js +12 -7
- package/dist/harness/sandbox.d.ts +34 -0
- package/dist/harness/sandbox.js +104 -0
- package/dist/harness/session-db.d.ts +55 -0
- package/dist/harness/session-db.js +165 -0
- package/dist/harness/session.d.ts +1 -1
- package/dist/harness/session.js +34 -15
- package/dist/harness/store.d.ts +3 -3
- package/dist/harness/store.js +6 -4
- package/dist/harness/submit-handler.d.ts +4 -4
- package/dist/harness/submit-handler.js +57 -21
- package/dist/harness/telemetry.d.ts +1 -1
- package/dist/harness/telemetry.js +23 -19
- package/dist/harness/traces.d.ts +2 -2
- package/dist/harness/traces.js +44 -33
- package/dist/harness/verification.d.ts +1 -1
- package/dist/harness/verification.js +50 -44
- package/dist/lsp/client.js +44 -40
- package/dist/main.js +100 -59
- package/dist/mcp/DeferredMcpTool.d.ts +4 -4
- package/dist/mcp/DeferredMcpTool.js +9 -5
- package/dist/mcp/McpTool.d.ts +4 -4
- package/dist/mcp/McpTool.js +8 -4
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +21 -21
- package/dist/mcp/loader.d.ts +1 -1
- package/dist/mcp/loader.js +17 -12
- package/dist/mcp/registry.d.ts +3 -3
- package/dist/mcp/registry.js +97 -97
- package/dist/mcp/schema.d.ts +1 -1
- package/dist/mcp/schema.js +16 -16
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +21 -21
- package/dist/mcp/types.d.ts +3 -3
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +10 -9
- package/dist/providers/base.d.ts +1 -1
- package/dist/providers/index.js +10 -3
- package/dist/providers/llamacpp.d.ts +2 -2
- package/dist/providers/llamacpp.js +1 -3
- package/dist/providers/ollama.d.ts +2 -2
- package/dist/providers/ollama.js +3 -4
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.js +3 -5
- package/dist/providers/openrouter.d.ts +2 -2
- package/dist/providers/router.d.ts +1 -1
- package/dist/providers/router.js +7 -7
- package/dist/query/compress.d.ts +2 -2
- package/dist/query/compress.js +22 -21
- package/dist/query/context-manager.d.ts +2 -2
- package/dist/query/context-manager.js +8 -11
- package/dist/query/errors.js +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +30 -22
- package/dist/query/tools.js +15 -12
- package/dist/query/types.d.ts +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/remote/auth.d.ts +2 -2
- package/dist/remote/auth.js +8 -8
- package/dist/remote/server.d.ts +3 -3
- package/dist/remote/server.js +60 -60
- package/dist/renderer/cells.js +9 -9
- package/dist/renderer/colors.js +24 -6
- package/dist/renderer/diff.d.ts +2 -2
- package/dist/renderer/diff.js +27 -19
- package/dist/renderer/differ.d.ts +1 -1
- package/dist/renderer/differ.js +9 -9
- package/dist/renderer/image.js +19 -19
- package/dist/renderer/index.d.ts +6 -6
- package/dist/renderer/index.js +163 -93
- package/dist/renderer/input.js +66 -48
- package/dist/renderer/layout.d.ts +6 -6
- package/dist/renderer/layout.js +163 -124
- package/dist/renderer/markdown.d.ts +2 -2
- package/dist/renderer/markdown.js +173 -54
- package/dist/renderer/session-browser.d.ts +2 -2
- package/dist/renderer/session-browser.js +19 -21
- package/dist/repl.d.ts +5 -5
- package/dist/repl.js +300 -198
- package/dist/sdk/index.d.ts +8 -7
- package/dist/sdk/index.js +59 -42
- package/dist/services/AgentDispatcher.d.ts +3 -3
- package/dist/services/AgentDispatcher.js +33 -29
- package/dist/services/CronExecutor.d.ts +4 -4
- package/dist/services/CronExecutor.js +12 -8
- package/dist/services/EvaluatorLoop.d.ts +3 -3
- package/dist/services/EvaluatorLoop.js +29 -21
- package/dist/services/MetaHarness.d.ts +1 -1
- package/dist/services/MetaHarness.js +41 -33
- package/dist/services/PipelineExecutor.d.ts +1 -1
- package/dist/services/PipelineExecutor.js +23 -25
- package/dist/services/SkillExtractor.d.ts +43 -0
- package/dist/services/SkillExtractor.js +143 -0
- package/dist/services/StreamingToolExecutor.d.ts +2 -2
- package/dist/services/StreamingToolExecutor.js +11 -7
- package/dist/services/a2a.d.ts +8 -8
- package/dist/services/a2a.js +44 -34
- package/dist/services/agent-messaging.d.ts +33 -15
- package/dist/services/agent-messaging.js +65 -13
- package/dist/services/cron.js +16 -16
- package/dist/tools/AgentTool/index.d.ts +5 -2
- package/dist/tools/AgentTool/index.js +35 -15
- package/dist/tools/AskUserTool/index.js +1 -1
- package/dist/tools/BashTool/index.d.ts +2 -2
- package/dist/tools/BashTool/index.js +18 -10
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/CronTool/index.js +30 -12
- package/dist/tools/DiagnosticsTool/index.js +28 -22
- package/dist/tools/EnterPlanModeTool/index.js +93 -14
- package/dist/tools/EnterWorktreeTool/index.js +7 -3
- package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
- package/dist/tools/ExitPlanModeTool/index.js +20 -5
- package/dist/tools/ExitWorktreeTool/index.js +11 -4
- package/dist/tools/FileEditTool/index.js +3 -5
- package/dist/tools/FileReadTool/index.js +16 -10
- package/dist/tools/FileWriteTool/index.js +2 -2
- package/dist/tools/GlobTool/index.js +5 -9
- package/dist/tools/GrepTool/index.d.ts +2 -2
- package/dist/tools/GrepTool/index.js +14 -9
- package/dist/tools/ImageReadTool/index.js +2 -2
- package/dist/tools/KillProcessTool/index.js +11 -7
- package/dist/tools/LSTool/index.js +3 -3
- package/dist/tools/MemoryTool/index.d.ts +11 -11
- package/dist/tools/MemoryTool/index.js +28 -14
- package/dist/tools/MonitorTool/index.d.ts +2 -2
- package/dist/tools/MonitorTool/index.js +24 -19
- package/dist/tools/MultiEditTool/index.js +9 -5
- package/dist/tools/NotebookEditTool/index.js +3 -3
- package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
- package/dist/tools/ParallelAgentTool/index.js +12 -6
- package/dist/tools/PipelineTool/index.d.ts +4 -4
- package/dist/tools/PipelineTool/index.js +3 -3
- package/dist/tools/PowerShellTool/index.js +10 -6
- package/dist/tools/RemoteTriggerTool/index.js +8 -4
- package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
- package/dist/tools/ScheduleWakeupTool/index.js +115 -0
- package/dist/tools/SendMessageTool/index.js +25 -7
- package/dist/tools/SessionSearchTool/index.d.ts +15 -0
- package/dist/tools/SessionSearchTool/index.js +36 -0
- package/dist/tools/SkillTool/index.d.ts +3 -0
- package/dist/tools/SkillTool/index.js +39 -9
- package/dist/tools/TaskCreateTool/index.d.ts +2 -2
- package/dist/tools/TaskCreateTool/index.js +2 -2
- package/dist/tools/TaskGetTool/index.js +2 -2
- package/dist/tools/TaskListTool/index.js +3 -5
- package/dist/tools/TaskOutputTool/index.js +2 -2
- package/dist/tools/TaskStopTool/index.js +3 -3
- package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
- package/dist/tools/TaskUpdateTool/index.js +2 -2
- package/dist/tools/ToolSearchTool/index.js +9 -6
- package/dist/tools/WebFetchTool/index.js +1 -1
- package/dist/tools/WebSearchTool/index.js +2 -6
- package/dist/tools.js +31 -30
- package/dist/types/permissions.js +15 -9
- package/dist/utils/bash-safety.d.ts +1 -1
- package/dist/utils/bash-safety.js +64 -54
- package/dist/utils/diff-algorithm.d.ts +3 -3
- package/dist/utils/diff-algorithm.js +7 -7
- package/dist/utils/fs.js +3 -3
- package/dist/utils/safe-env.js +1 -1
- package/dist/utils/theme-data.d.ts +1 -1
- package/dist/utils/theme-data.js +1 -1
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/tool-summary.d.ts +1 -1
- package/dist/utils/tool-summary.js +27 -9
- package/package.json +10 -3
package/dist/commands/index.js
CHANGED
|
@@ -4,20 +4,18 @@
|
|
|
4
4
|
* Commands are processed in the REPL before being sent to the LLM.
|
|
5
5
|
* If input starts with /, it's treated as a command.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { dirname } from "node:path";
|
|
9
|
-
import { isGitRepo, gitDiff, gitUndo, gitCommit, gitLog, gitBranch } from "../git/index.js";
|
|
10
|
-
import { handleCybergotchiCommand } from "./cybergotchi.js";
|
|
11
|
-
import { connectedMcpServers } from "../mcp/loader.js";
|
|
12
|
-
import { listSessions, loadSession, createSession, saveSession } from "../harness/session.js";
|
|
13
|
-
import { readOhConfig } from "../harness/config.js";
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
8
|
import { homedir } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import {
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { gitBranch, gitCommit, gitDiff, gitLog, gitUndo, isGitRepo, isInMergeOrRebase } from "../git/index.js";
|
|
11
|
+
import { readOhConfig } from "../harness/config.js";
|
|
12
|
+
import { estimateMessageTokens } from "../harness/context-warning.js";
|
|
17
13
|
import { getContextWindow } from "../harness/cost.js";
|
|
18
14
|
import { loadKeybindings } from "../harness/keybindings.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
15
|
+
import { createSession, listSessions, loadSession, saveSession } from "../harness/session.js";
|
|
16
|
+
import { connectedMcpServers } from "../mcp/loader.js";
|
|
17
|
+
import { compressMessages } from "../query/index.js";
|
|
18
|
+
import { handleCybergotchiCommand } from "./cybergotchi.js";
|
|
21
19
|
const commands = new Map();
|
|
22
20
|
function register(name, description, handler) {
|
|
23
21
|
commands.set(name, { description, handler });
|
|
@@ -25,12 +23,25 @@ function register(name, description, handler) {
|
|
|
25
23
|
// ── Register all commands ──
|
|
26
24
|
register("help", "Show available commands", () => {
|
|
27
25
|
const categories = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
Session: ["clear", "compact", "export", "history", "browse", "resume", "fork", "pin", "unpin"],
|
|
27
|
+
Git: ["diff", "undo", "rewind", "commit", "log"],
|
|
28
|
+
Info: [
|
|
29
|
+
"help",
|
|
30
|
+
"cost",
|
|
31
|
+
"status",
|
|
32
|
+
"config",
|
|
33
|
+
"files",
|
|
34
|
+
"model",
|
|
35
|
+
"memory",
|
|
36
|
+
"doctor",
|
|
37
|
+
"context",
|
|
38
|
+
"mcp",
|
|
39
|
+
"mcp-registry",
|
|
40
|
+
"init",
|
|
41
|
+
],
|
|
42
|
+
Settings: ["theme", "vim", "companion", "fast", "keys", "effort", "sandbox", "permissions", "allowed-tools"],
|
|
43
|
+
AI: ["plan", "review", "roles", "agents", "plugins", "btw", "loop"],
|
|
44
|
+
Pet: ["cybergotchi"],
|
|
34
45
|
};
|
|
35
46
|
const lines = [];
|
|
36
47
|
for (const [category, names] of Object.entries(categories)) {
|
|
@@ -40,13 +51,13 @@ register("help", "Show available commands", () => {
|
|
|
40
51
|
if (cmd)
|
|
41
52
|
lines.push(` /${name.padEnd(12)} ${cmd.description}`);
|
|
42
53
|
}
|
|
43
|
-
lines.push(
|
|
54
|
+
lines.push("");
|
|
44
55
|
}
|
|
45
56
|
// Include any uncategorized commands
|
|
46
57
|
const categorized = new Set(Object.values(categories).flat());
|
|
47
|
-
const uncategorized = [...commands.keys()].filter(n => !categorized.has(n));
|
|
58
|
+
const uncategorized = [...commands.keys()].filter((n) => !categorized.has(n));
|
|
48
59
|
if (uncategorized.length > 0) {
|
|
49
|
-
lines.push(
|
|
60
|
+
lines.push("Other:");
|
|
50
61
|
for (const name of uncategorized) {
|
|
51
62
|
const cmd = commands.get(name);
|
|
52
63
|
lines.push(` /${name.padEnd(12)} ${cmd.description}`);
|
|
@@ -79,7 +90,7 @@ register("status", "Show session status", (_args, ctx) => {
|
|
|
79
90
|
}
|
|
80
91
|
const mcp = connectedMcpServers();
|
|
81
92
|
if (mcp.length > 0) {
|
|
82
|
-
lines.push(`MCP servers: ${mcp.join(
|
|
93
|
+
lines.push(`MCP servers: ${mcp.join(", ")}`);
|
|
83
94
|
}
|
|
84
95
|
return { output: lines.join("\n"), handled: true };
|
|
85
96
|
});
|
|
@@ -114,15 +125,15 @@ register("rewind", "Restore files from checkpoint (interactive picker or last)",
|
|
|
114
125
|
const cp = checkpoints[i];
|
|
115
126
|
const age = Math.round((Date.now() - cp.timestamp) / 60_000);
|
|
116
127
|
lines.push(` ${i + 1}. [${age}m ago] ${cp.description}`);
|
|
117
|
-
lines.push(` Files: ${cp.files.join(
|
|
128
|
+
lines.push(` Files: ${cp.files.join(", ")}`);
|
|
118
129
|
}
|
|
119
|
-
lines.push(
|
|
120
|
-
lines.push(
|
|
121
|
-
lines.push(
|
|
122
|
-
return { output: lines.join(
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push("Usage: /rewind <number> to restore a specific checkpoint");
|
|
132
|
+
lines.push(" /rewind last to restore the most recent");
|
|
133
|
+
return { output: lines.join("\n"), handled: true };
|
|
123
134
|
}
|
|
124
135
|
// /rewind last — restore most recent
|
|
125
|
-
if (idx ===
|
|
136
|
+
if (idx === "last") {
|
|
126
137
|
const cp = rewindLastCheckpoint();
|
|
127
138
|
if (!cp)
|
|
128
139
|
return { output: "No checkpoints.", handled: true };
|
|
@@ -133,7 +144,7 @@ register("rewind", "Restore files from checkpoint (interactive picker or last)",
|
|
|
133
144
|
}
|
|
134
145
|
// /rewind <n> — restore specific checkpoint
|
|
135
146
|
const num = parseInt(idx, 10);
|
|
136
|
-
if (isNaN(num) || num < 1 || num > checkpoints.length) {
|
|
147
|
+
if (Number.isNaN(num) || num < 1 || num > checkpoints.length) {
|
|
137
148
|
return { output: `Invalid checkpoint number. Use 1-${checkpoints.length}.`, handled: true };
|
|
138
149
|
}
|
|
139
150
|
// Rewind to specific checkpoint (restore all from that point)
|
|
@@ -175,13 +186,15 @@ register("history", "List recent sessions or search across them", (args) => {
|
|
|
175
186
|
for (const s of sessions) {
|
|
176
187
|
try {
|
|
177
188
|
const full = loadSession(s.id, sessionDir);
|
|
178
|
-
const hit = full.messages.find(m => typeof m.content === "string" && m.content.toLowerCase().includes(term));
|
|
189
|
+
const hit = full.messages.find((m) => typeof m.content === "string" && m.content.toLowerCase().includes(term));
|
|
179
190
|
if (hit) {
|
|
180
191
|
const date = new Date(s.updatedAt).toLocaleDateString();
|
|
181
192
|
matches.push(` ${s.id} ${date} ${s.model || "?"}`);
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
|
-
catch {
|
|
195
|
+
catch {
|
|
196
|
+
/* skip */
|
|
197
|
+
}
|
|
185
198
|
}
|
|
186
199
|
if (matches.length === 0)
|
|
187
200
|
return { output: `No sessions matching "${term}".`, handled: true };
|
|
@@ -191,7 +204,7 @@ register("history", "List recent sessions or search across them", (args) => {
|
|
|
191
204
|
const sessions = listSessions(sessionDir).slice(0, n);
|
|
192
205
|
if (sessions.length === 0)
|
|
193
206
|
return { output: "No saved sessions.", handled: true };
|
|
194
|
-
const lines = sessions.map(s => {
|
|
207
|
+
const lines = sessions.map((s) => {
|
|
195
208
|
const date = new Date(s.updatedAt).toLocaleDateString();
|
|
196
209
|
const cost = s.cost > 0 ? ` $${s.cost.toFixed(4)}` : "";
|
|
197
210
|
return ` ${s.id} ${date} ${String(s.messages).padStart(3)} msgs ${(s.model || "?").slice(0, 24)}${cost}`;
|
|
@@ -200,7 +213,7 @@ register("history", "List recent sessions or search across them", (args) => {
|
|
|
200
213
|
});
|
|
201
214
|
register("theme", "Switch theme (dark/light)", (args) => {
|
|
202
215
|
const theme = args.trim().toLowerCase();
|
|
203
|
-
if (theme !==
|
|
216
|
+
if (theme !== "dark" && theme !== "light") {
|
|
204
217
|
return { output: "Usage: /theme dark or /theme light", handled: true };
|
|
205
218
|
}
|
|
206
219
|
return { output: `__SWITCH_THEME__:${theme}`, handled: true };
|
|
@@ -244,7 +257,7 @@ register("files", "List files in context", (_args, ctx) => {
|
|
|
244
257
|
}
|
|
245
258
|
if (files.size === 0)
|
|
246
259
|
return { output: "No files in context yet.", handled: true };
|
|
247
|
-
return { output: `Files in context:\n${[...files].map(f => ` ${f}`).join("\n")}`, handled: true };
|
|
260
|
+
return { output: `Files in context:\n${[...files].map((f) => ` ${f}`).join("\n")}`, handled: true };
|
|
248
261
|
});
|
|
249
262
|
register("model", "Switch model (e.g., /model llama3.2 or /model ollama/llama3.2)", (args, ctx) => {
|
|
250
263
|
const model = args.trim();
|
|
@@ -269,9 +282,37 @@ register("model", "Switch model (e.g., /model llama3.2 or /model ollama/llama3.2
|
|
|
269
282
|
const modelName = model.includes("/") ? model.split("/").slice(1).join("/") : model;
|
|
270
283
|
return { output: `Switched to ${modelName}.`, handled: true, newModel: modelName };
|
|
271
284
|
});
|
|
272
|
-
register("compact", "Compress conversation history", (
|
|
285
|
+
register("compact", "Compress conversation history (optional: focus keyword or message number)", (args, ctx) => {
|
|
286
|
+
const focus = args.trim();
|
|
273
287
|
const before = ctx.messages.length;
|
|
274
288
|
const targetTokens = Math.floor(getContextWindow(ctx.model) * 0.6);
|
|
289
|
+
if (focus && /^\d+$/.test(focus)) {
|
|
290
|
+
// Numeric: compact messages 1-N, keep N+1 onwards
|
|
291
|
+
const cutoff = parseInt(focus, 10);
|
|
292
|
+
if (cutoff < 1 || cutoff >= before) {
|
|
293
|
+
return { output: `Invalid: use 1-${before - 1}`, handled: true };
|
|
294
|
+
}
|
|
295
|
+
const kept = ctx.messages.slice(cutoff);
|
|
296
|
+
return {
|
|
297
|
+
output: `Compacted: removed first ${cutoff} messages, kept ${kept.length}.`,
|
|
298
|
+
handled: true,
|
|
299
|
+
compactedMessages: kept,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (focus) {
|
|
303
|
+
// Keyword focus: compress but preserve messages containing the keyword
|
|
304
|
+
const focusLower = focus.toLowerCase();
|
|
305
|
+
const preserved = ctx.messages.filter((m) => m.content.toLowerCase().includes(focusLower) || m.meta?.pinned);
|
|
306
|
+
const others = ctx.messages.filter((m) => !m.content.toLowerCase().includes(focusLower) && !m.meta?.pinned);
|
|
307
|
+
const compactedOthers = compressMessages(others, targetTokens);
|
|
308
|
+
const merged = [...compactedOthers, ...preserved].sort((a, b) => a.timestamp - b.timestamp);
|
|
309
|
+
return {
|
|
310
|
+
output: `Compacted with focus "${focus}": ${before} → ${merged.length} messages (preserved ${preserved.length} matching).`,
|
|
311
|
+
handled: true,
|
|
312
|
+
compactedMessages: merged,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
// Default: compress everything
|
|
275
316
|
const compacted = compressMessages(ctx.messages, targetTokens);
|
|
276
317
|
const dropped = before - compacted.length;
|
|
277
318
|
return {
|
|
@@ -282,8 +323,8 @@ register("compact", "Compress conversation history", (_args, ctx) => {
|
|
|
282
323
|
});
|
|
283
324
|
register("export", "Export conversation to file", (_args, ctx) => {
|
|
284
325
|
const lines = ctx.messages
|
|
285
|
-
.filter(m => m.role === "user" || m.role === "assistant")
|
|
286
|
-
.map(m => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
|
|
326
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
327
|
+
.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
|
|
287
328
|
.join("\n\n");
|
|
288
329
|
const filename = `.oh/export-${ctx.sessionId}.md`;
|
|
289
330
|
try {
|
|
@@ -327,7 +368,7 @@ register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
|
327
368
|
const term = args.trim().toLowerCase();
|
|
328
369
|
let files;
|
|
329
370
|
try {
|
|
330
|
-
files = readdirSync(memDir).filter(f => f.endsWith(".md"));
|
|
371
|
+
files = readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
331
372
|
}
|
|
332
373
|
catch {
|
|
333
374
|
return { output: "Could not read .oh/memory/", handled: true };
|
|
@@ -341,11 +382,13 @@ register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
|
341
382
|
try {
|
|
342
383
|
const content = readFileSync(join(memDir, file), "utf-8");
|
|
343
384
|
if (content.toLowerCase().includes(term)) {
|
|
344
|
-
const firstLine = content.split("\n").find(l => l.trim() && !l.startsWith("---")) ?? file;
|
|
385
|
+
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("---")) ?? file;
|
|
345
386
|
matches.push(` ${file.padEnd(30)} ${firstLine.slice(0, 50)}`);
|
|
346
387
|
}
|
|
347
388
|
}
|
|
348
|
-
catch {
|
|
389
|
+
catch {
|
|
390
|
+
/* skip */
|
|
391
|
+
}
|
|
349
392
|
}
|
|
350
393
|
if (matches.length === 0)
|
|
351
394
|
return { output: `No memories matching "${term}".`, handled: true };
|
|
@@ -369,11 +412,11 @@ register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
|
369
412
|
});
|
|
370
413
|
register("companion", "Toggle companion visibility (off/on)", (args) => {
|
|
371
414
|
const arg = args.trim().toLowerCase();
|
|
372
|
-
if (arg ===
|
|
373
|
-
return { output:
|
|
374
|
-
if (arg ===
|
|
375
|
-
return { output:
|
|
376
|
-
return { output:
|
|
415
|
+
if (arg === "off")
|
|
416
|
+
return { output: "__COMPANION_OFF__", handled: true };
|
|
417
|
+
if (arg === "on")
|
|
418
|
+
return { output: "__COMPANION_ON__", handled: true };
|
|
419
|
+
return { output: "Usage: /companion off or /companion on", handled: true };
|
|
377
420
|
});
|
|
378
421
|
register("cybergotchi", "Manage your cybergotchi — feed · pet · rest · status · rename · reset", (args) => {
|
|
379
422
|
return handleCybergotchiCommand(args);
|
|
@@ -384,32 +427,35 @@ register("roles", "List available agent specialization roles", () => {
|
|
|
384
427
|
const lines = ["Available agent roles:\n"];
|
|
385
428
|
for (const role of roles) {
|
|
386
429
|
lines.push(` ${role.id.padEnd(18)} ${role.name}`);
|
|
387
|
-
lines.push(` ${
|
|
430
|
+
lines.push(` ${"".padEnd(18)} ${role.description}`);
|
|
388
431
|
if (role.suggestedTools?.length) {
|
|
389
|
-
lines.push(` ${
|
|
432
|
+
lines.push(` ${"".padEnd(18)} Tools: ${role.suggestedTools.join(", ")}`);
|
|
390
433
|
}
|
|
391
|
-
lines.push(
|
|
434
|
+
lines.push("");
|
|
392
435
|
}
|
|
393
436
|
lines.push("Usage: Agent({ subagent_type: 'code-reviewer', prompt: '...' })");
|
|
394
437
|
return { output: lines.join("\n"), handled: true };
|
|
395
438
|
});
|
|
396
439
|
register("agents", "Discover running openHarness agents on this machine", () => {
|
|
397
|
-
const { discoverAgents } = require(
|
|
440
|
+
const { discoverAgents } = require("../services/a2a.js");
|
|
398
441
|
const agents = discoverAgents();
|
|
399
442
|
if (agents.length === 0) {
|
|
400
|
-
return {
|
|
443
|
+
return {
|
|
444
|
+
output: "No other openHarness agents running on this machine.\n\nOther oh sessions will appear here automatically via the A2A protocol.",
|
|
445
|
+
handled: true,
|
|
446
|
+
};
|
|
401
447
|
}
|
|
402
448
|
const lines = [`Running Agents (${agents.length}):\n`];
|
|
403
449
|
for (const agent of agents) {
|
|
404
450
|
const age = Math.round((Date.now() - agent.registeredAt) / 60_000);
|
|
405
451
|
lines.push(` ${agent.name}`);
|
|
406
452
|
lines.push(` ID: ${agent.id}`);
|
|
407
|
-
lines.push(` Provider: ${agent.provider ??
|
|
408
|
-
lines.push(` Dir: ${agent.workingDir ??
|
|
409
|
-
lines.push(` Endpoint: ${agent.endpoint.type}${agent.endpoint.port ?
|
|
453
|
+
lines.push(` Provider: ${agent.provider ?? "unknown"} / ${agent.model ?? "unknown"}`);
|
|
454
|
+
lines.push(` Dir: ${agent.workingDir ?? "unknown"}`);
|
|
455
|
+
lines.push(` Endpoint: ${agent.endpoint.type}${agent.endpoint.port ? `:${agent.endpoint.port}` : ""}`);
|
|
410
456
|
lines.push(` Uptime: ${age}m`);
|
|
411
|
-
lines.push(` Caps: ${agent.capabilities.map((c) => c.name).join(
|
|
412
|
-
lines.push(
|
|
457
|
+
lines.push(` Caps: ${agent.capabilities.map((c) => c.name).join(", ")}`);
|
|
458
|
+
lines.push("");
|
|
413
459
|
}
|
|
414
460
|
lines.push("Send messages with: Agent({ prompt: 'ask the other agent...', allowed_tools: ['SendMessage'] })");
|
|
415
461
|
return { output: lines.join("\n"), handled: true };
|
|
@@ -447,6 +493,65 @@ register("keys", "Show keyboard shortcuts", () => {
|
|
|
447
493
|
shortcuts.push("", " Session:", " /vim Toggle Vim mode", " /browse Interactive session browser", " /theme dark|light Switch theme");
|
|
448
494
|
return { output: shortcuts.join("\n"), handled: true };
|
|
449
495
|
});
|
|
496
|
+
register("sandbox", "Show sandbox status and restrictions", () => {
|
|
497
|
+
const { sandboxStatus } = require("../harness/sandbox.js");
|
|
498
|
+
return { output: `${sandboxStatus()}\n\nConfigure in .oh/config.yaml under sandbox:`, handled: true };
|
|
499
|
+
});
|
|
500
|
+
register("effort", "Set reasoning effort level (low/medium/high/max)", (args) => {
|
|
501
|
+
const level = args.trim().toLowerCase();
|
|
502
|
+
const valid = ["low", "medium", "high", "max"];
|
|
503
|
+
if (!valid.includes(level)) {
|
|
504
|
+
return {
|
|
505
|
+
output: `Usage: /effort <${valid.join("|")}>\n\nlow — fast, minimal reasoning\nmedium — balanced (default)\nhigh — thorough reasoning\nmax — maximum depth (Opus only)`,
|
|
506
|
+
handled: true,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return { output: `Effort level set to: ${level}`, handled: true };
|
|
510
|
+
});
|
|
511
|
+
register("btw", "Ask a side question (ephemeral, no tools, not saved to history)", (args) => {
|
|
512
|
+
if (!args.trim()) {
|
|
513
|
+
return { output: "Usage: /btw <your question>", handled: true };
|
|
514
|
+
}
|
|
515
|
+
// Side questions are answered directly without tools or history
|
|
516
|
+
// The output is shown but NOT added to conversation history
|
|
517
|
+
return {
|
|
518
|
+
output: `[btw] ${args.trim()}`,
|
|
519
|
+
handled: false,
|
|
520
|
+
prependToPrompt: `[Side question — answer briefly without using any tools. This is ephemeral and not part of the main conversation.]\n\n${args.trim()}`,
|
|
521
|
+
};
|
|
522
|
+
});
|
|
523
|
+
register("loop", "Run a prompt repeatedly with self-paced timing", (args) => {
|
|
524
|
+
const input = args.trim();
|
|
525
|
+
if (!input) {
|
|
526
|
+
return {
|
|
527
|
+
output: "Usage: /loop [interval] <prompt or /command>\n\nExamples:\n /loop check if the build passed\n /loop 5m /review\n\nOmit the interval to let the model self-pace via ScheduleWakeup.",
|
|
528
|
+
handled: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// Check for optional interval prefix like "5m", "30s", "2h"
|
|
532
|
+
const intervalMatch = input.match(/^(\d+)(s|m|h)\s+(.+)$/);
|
|
533
|
+
let intervalMs = null;
|
|
534
|
+
let prompt;
|
|
535
|
+
if (intervalMatch) {
|
|
536
|
+
const [, num, unit, rest] = intervalMatch;
|
|
537
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000 };
|
|
538
|
+
intervalMs = parseInt(num, 10) * multipliers[unit];
|
|
539
|
+
prompt = rest;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
prompt = input;
|
|
543
|
+
}
|
|
544
|
+
const mode = intervalMs
|
|
545
|
+
? `Fixed interval: ${intervalMatch[1]}${intervalMatch[2]}`
|
|
546
|
+
: "Dynamic (model self-paces via ScheduleWakeup)";
|
|
547
|
+
return {
|
|
548
|
+
output: `[loop] ${mode}\nPrompt: ${prompt}`,
|
|
549
|
+
handled: false,
|
|
550
|
+
prependToPrompt: intervalMs
|
|
551
|
+
? `You are in LOOP MODE (fixed interval: ${intervalMs / 1000}s). Execute this task, then use ScheduleWakeup with delaySeconds=${intervalMs / 1000} to schedule the next iteration.\n\nTask: ${prompt}`
|
|
552
|
+
: `You are in LOOP MODE (dynamic pacing). Execute this task, then use ScheduleWakeup to schedule the next iteration at an appropriate interval. Choose your delay based on what you're waiting for. Omit the ScheduleWakeup call to end the loop.\n\nTask: ${prompt}`,
|
|
553
|
+
};
|
|
554
|
+
});
|
|
450
555
|
register("plan", "Enter plan mode", (_args, _ctx) => {
|
|
451
556
|
const task = _args.trim();
|
|
452
557
|
if (!task) {
|
|
@@ -455,7 +560,7 @@ register("plan", "Enter plan mode", (_args, _ctx) => {
|
|
|
455
560
|
return {
|
|
456
561
|
output: `[plan mode] ${task}`,
|
|
457
562
|
handled: false,
|
|
458
|
-
prependToPrompt: `You are in PLAN MODE. Do NOT write any code yet.
|
|
563
|
+
prependToPrompt: `You are in PLAN MODE. Do NOT write any code yet.\n\n1. Call EnterPlanMode to create a plan file in .oh/plans/\n2. Write your detailed implementation plan to that file (files to create/modify, key functions/types, data flow, edge cases)\n3. When the plan is complete, call ExitPlanMode to signal readiness for review\n\nTask: ${task}`,
|
|
459
564
|
};
|
|
460
565
|
});
|
|
461
566
|
register("review", "Review recent code changes", () => {
|
|
@@ -538,39 +643,45 @@ register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
|
538
643
|
const ohDir = join(homedir(), ".oh");
|
|
539
644
|
if (existsSync(ohDir)) {
|
|
540
645
|
const sessionsDir = join(ohDir, "sessions");
|
|
541
|
-
const sessCount = existsSync(sessionsDir)
|
|
646
|
+
const sessCount = existsSync(sessionsDir)
|
|
647
|
+
? readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length
|
|
648
|
+
: 0;
|
|
542
649
|
lines.push(` Sessions: ${sessCount} saved`);
|
|
543
650
|
if (sessCount > 80)
|
|
544
651
|
issues.push(`${sessCount} saved sessions. Consider cleaning old ones.`);
|
|
545
652
|
// Memory stats
|
|
546
653
|
const memDir = join(ohDir, "memory");
|
|
547
|
-
const memCount = existsSync(memDir) ? readdirSync(memDir).filter(f => f.endsWith(
|
|
654
|
+
const memCount = existsSync(memDir) ? readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
548
655
|
lines.push(` Memories: ${memCount} global`);
|
|
549
656
|
// Cron stats
|
|
550
657
|
const cronDir = join(ohDir, "crons");
|
|
551
|
-
const cronCount = existsSync(cronDir) ? readdirSync(cronDir).filter(f => f.endsWith(
|
|
658
|
+
const cronCount = existsSync(cronDir) ? readdirSync(cronDir).filter((f) => f.endsWith(".json")).length : 0;
|
|
552
659
|
lines.push(` Cron tasks: ${cronCount}`);
|
|
553
660
|
}
|
|
554
661
|
}
|
|
555
|
-
catch {
|
|
662
|
+
catch {
|
|
663
|
+
/* ignore */
|
|
664
|
+
}
|
|
556
665
|
// Project-level stats
|
|
557
666
|
try {
|
|
558
667
|
const projMemDir = join(".oh", "memory");
|
|
559
|
-
const projMemCount = existsSync(projMemDir) ? readdirSync(projMemDir).filter(f => f.endsWith(
|
|
668
|
+
const projMemCount = existsSync(projMemDir) ? readdirSync(projMemDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
560
669
|
if (projMemCount > 0)
|
|
561
670
|
lines.push(` Project mems: ${projMemCount}`);
|
|
562
671
|
const skillsDir = join(".oh", "skills");
|
|
563
|
-
const skillCount = existsSync(skillsDir) ? readdirSync(skillsDir).filter(f => f.endsWith(
|
|
672
|
+
const skillCount = existsSync(skillsDir) ? readdirSync(skillsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
564
673
|
if (skillCount > 0)
|
|
565
674
|
lines.push(` Skills: ${skillCount}`);
|
|
566
675
|
}
|
|
567
|
-
catch {
|
|
676
|
+
catch {
|
|
677
|
+
/* ignore */
|
|
678
|
+
}
|
|
568
679
|
// Global config
|
|
569
680
|
const globalCfg = existsSync(join(homedir(), ".oh", "config.yaml"));
|
|
570
681
|
lines.push(` Global config: ${globalCfg ? "~/.oh/config.yaml ✓" : "not set (optional)"}`);
|
|
571
682
|
// Verification config
|
|
572
683
|
try {
|
|
573
|
-
const { getVerificationConfig } = require(
|
|
684
|
+
const { getVerificationConfig } = require("../harness/verification.js");
|
|
574
685
|
const vCfg = getVerificationConfig();
|
|
575
686
|
if (vCfg?.enabled) {
|
|
576
687
|
lines.push(` Verification: ✓ (${vCfg.rules.length} rules, mode: ${vCfg.mode})`);
|
|
@@ -579,13 +690,15 @@ register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
|
579
690
|
lines.push(` Verification: off (no rules detected)`);
|
|
580
691
|
}
|
|
581
692
|
}
|
|
582
|
-
catch {
|
|
693
|
+
catch {
|
|
694
|
+
/* ignore */
|
|
695
|
+
}
|
|
583
696
|
// Tools
|
|
584
697
|
lines.push("");
|
|
585
|
-
lines.push(` Tools: ${ctx.messages.length > 0 ?
|
|
698
|
+
lines.push(` Tools: ${ctx.messages.length > 0 ? "ready" : "loaded"}`);
|
|
586
699
|
// Node.js version
|
|
587
700
|
lines.push(` Node.js: ${process.version}`);
|
|
588
|
-
const [major] = process.version.slice(1).split(
|
|
701
|
+
const [major] = process.version.slice(1).split(".").map(Number);
|
|
589
702
|
if (major && major < 18)
|
|
590
703
|
issues.push(`Node.js ${process.version} is below minimum (18+). Upgrade Node.js.`);
|
|
591
704
|
// Issues summary
|
|
@@ -604,25 +717,58 @@ register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
|
604
717
|
});
|
|
605
718
|
register("context", "Show context window usage breakdown", (_args, ctx) => {
|
|
606
719
|
const ctxWindow = getContextWindow(ctx.model);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
for (
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
720
|
+
// Categorize messages by type
|
|
721
|
+
let userTokens = 0, assistantTokens = 0, toolTokens = 0, systemTokens = 0;
|
|
722
|
+
for (const msg of ctx.messages) {
|
|
723
|
+
const tokens = Math.ceil((msg.content?.length ?? 0) / 4);
|
|
724
|
+
switch (msg.role) {
|
|
725
|
+
case "user":
|
|
726
|
+
userTokens += tokens;
|
|
727
|
+
break;
|
|
728
|
+
case "assistant":
|
|
729
|
+
assistantTokens += tokens;
|
|
730
|
+
break;
|
|
731
|
+
case "tool":
|
|
732
|
+
toolTokens += tokens;
|
|
733
|
+
break;
|
|
734
|
+
case "system":
|
|
735
|
+
systemTokens += tokens;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
615
738
|
}
|
|
739
|
+
const totalTokens = userTokens + assistantTokens + toolTokens + systemTokens;
|
|
740
|
+
const freeTokens = ctxWindow - totalTokens;
|
|
616
741
|
const usage = totalTokens / ctxWindow;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
742
|
+
// Visual bar (30 chars wide)
|
|
743
|
+
const barWidth = 30;
|
|
744
|
+
const filled = Math.round(usage * barWidth);
|
|
745
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
746
|
+
const pct = (n) => `${((n / ctxWindow) * 100).toFixed(1)}%`;
|
|
747
|
+
const pad = (s, n) => s.padEnd(n);
|
|
748
|
+
const lines = [
|
|
749
|
+
`Context Window (${ctxWindow.toLocaleString()} tokens):`,
|
|
750
|
+
"",
|
|
751
|
+
` ${pad("User messages:", 20)} ${userTokens.toLocaleString().padStart(8)} tokens (${pct(userTokens)})`,
|
|
752
|
+
` ${pad("Assistant:", 20)} ${assistantTokens.toLocaleString().padStart(8)} tokens (${pct(assistantTokens)})`,
|
|
753
|
+
` ${pad("Tool results:", 20)} ${toolTokens.toLocaleString().padStart(8)} tokens (${pct(toolTokens)})`,
|
|
754
|
+
` ${pad("System/info:", 20)} ${systemTokens.toLocaleString().padStart(8)} tokens (${pct(systemTokens)})`,
|
|
755
|
+
"",
|
|
756
|
+
` ${pad("Total used:", 20)} ${totalTokens.toLocaleString().padStart(8)} tokens (${pct(totalTokens)})`,
|
|
757
|
+
` ${pad("Free:", 20)} ${freeTokens.toLocaleString().padStart(8)} tokens (${pct(freeTokens)})`,
|
|
758
|
+
"",
|
|
759
|
+
` ${bar} ${Math.round(usage * 100)}%`,
|
|
760
|
+
"",
|
|
761
|
+
` Messages: ${ctx.messages.length} | Compress at: ${Math.round(ctxWindow * 0.8).toLocaleString()} (80%)`,
|
|
762
|
+
];
|
|
763
|
+
return { output: lines.join("\n"), handled: true };
|
|
621
764
|
});
|
|
622
765
|
register("mcp", "Show MCP server status", () => {
|
|
623
766
|
const mcp = connectedMcpServers();
|
|
624
767
|
if (mcp.length === 0) {
|
|
625
|
-
return {
|
|
768
|
+
return {
|
|
769
|
+
output: "No MCP servers connected.\nConfigure in .oh/config.yaml under mcpServers.\nRun /mcp-registry to browse available servers.",
|
|
770
|
+
handled: true,
|
|
771
|
+
};
|
|
626
772
|
}
|
|
627
773
|
const lines = [`MCP Servers (${mcp.length} connected):\n`];
|
|
628
774
|
for (const name of mcp) {
|
|
@@ -632,11 +778,11 @@ register("mcp", "Show MCP server status", () => {
|
|
|
632
778
|
return { output: lines.join("\n"), handled: true };
|
|
633
779
|
});
|
|
634
780
|
register("mcp-registry", "Browse and add MCP servers from the curated registry", (args) => {
|
|
635
|
-
const { searchRegistry, formatRegistry, generateConfigBlock, MCP_REGISTRY } = require(
|
|
781
|
+
const { searchRegistry, formatRegistry, generateConfigBlock, MCP_REGISTRY } = require("../mcp/registry.js");
|
|
636
782
|
const query = args.trim();
|
|
637
783
|
if (!query) {
|
|
638
784
|
// Show full registry
|
|
639
|
-
const output = `MCP Server Registry (${MCP_REGISTRY.length} servers)\n${
|
|
785
|
+
const output = `MCP Server Registry (${MCP_REGISTRY.length} servers)\n${"─".repeat(50)}\n\n${formatRegistry()}\n\nUsage:\n /mcp-registry <name> Show install config for a server\n /mcp-registry <keyword> Search by name, description, or category`;
|
|
640
786
|
return { output, handled: true };
|
|
641
787
|
}
|
|
642
788
|
// Search or show specific server
|
|
@@ -649,10 +795,10 @@ register("mcp-registry", "Browse and add MCP servers from the curated registry",
|
|
|
649
795
|
const entry = results[0];
|
|
650
796
|
const config = generateConfigBlock(entry);
|
|
651
797
|
const envNote = entry.envVars?.length
|
|
652
|
-
? `\n\nRequired environment variables:\n${entry.envVars.map((v) => ` - ${v}`).join(
|
|
653
|
-
:
|
|
798
|
+
? `\n\nRequired environment variables:\n${entry.envVars.map((v) => ` - ${v}`).join("\n")}`
|
|
799
|
+
: "";
|
|
654
800
|
return {
|
|
655
|
-
output: `${entry.name} — ${entry.description}\nPackage: ${entry.package}\nRisk: ${entry.riskLevel ??
|
|
801
|
+
output: `${entry.name} — ${entry.description}\nPackage: ${entry.package}\nRisk: ${entry.riskLevel ?? "medium"}${envNote}\n\nAdd to .oh/config.yaml under mcpServers:\n\n${config}`,
|
|
656
802
|
handled: true,
|
|
657
803
|
};
|
|
658
804
|
}
|
|
@@ -661,13 +807,13 @@ register("mcp-registry", "Browse and add MCP servers from the curated registry",
|
|
|
661
807
|
});
|
|
662
808
|
function setPinned(args, ctx, pinned) {
|
|
663
809
|
const idx = parseInt(args.trim(), 10);
|
|
664
|
-
if (isNaN(idx) || idx < 1 || idx > ctx.messages.length) {
|
|
665
|
-
return { output: `Usage: /${pinned ?
|
|
810
|
+
if (Number.isNaN(idx) || idx < 1 || idx > ctx.messages.length) {
|
|
811
|
+
return { output: `Usage: /${pinned ? "pin" : "unpin"} <message-number> (1-${ctx.messages.length})`, handled: true };
|
|
666
812
|
}
|
|
667
813
|
// Immutable update — replace message with updated meta
|
|
668
|
-
const updatedMessages = ctx.messages.map((m, i) => i === idx - 1 ? { ...m, meta: { ...m.meta, pinned } } : m);
|
|
814
|
+
const updatedMessages = ctx.messages.map((m, i) => (i === idx - 1 ? { ...m, meta: { ...m.meta, pinned } } : m));
|
|
669
815
|
return {
|
|
670
|
-
output: `Message #${idx} ${pinned ?
|
|
816
|
+
output: `Message #${idx} ${pinned ? "pinned" : "unpinned"}.`,
|
|
671
817
|
handled: true,
|
|
672
818
|
compactedMessages: updatedMessages,
|
|
673
819
|
};
|
|
@@ -675,52 +821,64 @@ function setPinned(args, ctx, pinned) {
|
|
|
675
821
|
register("pin", "Pin a message (survives /compact)", (args, ctx) => setPinned(args, ctx, true));
|
|
676
822
|
register("unpin", "Unpin a message", (args, ctx) => setPinned(args, ctx, false));
|
|
677
823
|
register("plugins", "Manage plugins: list, search, install, uninstall, marketplace", (args) => {
|
|
678
|
-
const { discoverPlugins, discoverSkills } = require(
|
|
679
|
-
const { searchMarketplace, installPlugin, uninstallPlugin, getInstalledPlugins, listMarketplaces, addMarketplace, removeMarketplace, formatMarketplaceSearch, formatInstalledPlugins, } = require(
|
|
824
|
+
const { discoverPlugins, discoverSkills } = require("../harness/plugins.js");
|
|
825
|
+
const { searchMarketplace, installPlugin, uninstallPlugin, getInstalledPlugins, listMarketplaces, addMarketplace, removeMarketplace, formatMarketplaceSearch, formatInstalledPlugins, } = require("../harness/marketplace.js");
|
|
680
826
|
const parts = args.trim().split(/\s+/);
|
|
681
|
-
const subcommand = parts[0] ??
|
|
682
|
-
const rest = parts.slice(1).join(
|
|
827
|
+
const subcommand = parts[0] ?? "";
|
|
828
|
+
const rest = parts.slice(1).join(" ");
|
|
683
829
|
// /plugins marketplace add <source>
|
|
684
|
-
if (subcommand ===
|
|
830
|
+
if (subcommand === "marketplace") {
|
|
685
831
|
const action = parts[1];
|
|
686
|
-
const source = parts.slice(2).join(
|
|
687
|
-
if (action ===
|
|
832
|
+
const source = parts.slice(2).join(" ");
|
|
833
|
+
if (action === "add" && source) {
|
|
688
834
|
const mp = addMarketplace(source);
|
|
689
835
|
if (mp)
|
|
690
836
|
return { output: `Added marketplace "${mp.name}" (${mp.plugins.length} plugins)`, handled: true };
|
|
691
837
|
return { output: `Failed to add marketplace from "${source}"`, handled: true };
|
|
692
838
|
}
|
|
693
|
-
if (action ===
|
|
694
|
-
return {
|
|
839
|
+
if (action === "remove" && source) {
|
|
840
|
+
return {
|
|
841
|
+
output: removeMarketplace(source) ? `Removed marketplace "${source}"` : `Marketplace "${source}" not found`,
|
|
842
|
+
handled: true,
|
|
843
|
+
};
|
|
695
844
|
}
|
|
696
845
|
// List marketplaces
|
|
697
846
|
const mps = listMarketplaces();
|
|
698
847
|
if (mps.length === 0) {
|
|
699
|
-
return {
|
|
848
|
+
return {
|
|
849
|
+
output: "No marketplaces configured.\n\nAdd one:\n /plugins marketplace add owner/repo\n /plugins marketplace add https://example.com/plugins",
|
|
850
|
+
handled: true,
|
|
851
|
+
};
|
|
700
852
|
}
|
|
701
853
|
const lines = [`Marketplaces (${mps.length}):\n`];
|
|
702
854
|
for (const mp of mps) {
|
|
703
855
|
lines.push(` ${mp.name} — ${mp.plugins.length} plugins`);
|
|
704
856
|
}
|
|
705
|
-
return { output: lines.join(
|
|
857
|
+
return { output: lines.join("\n"), handled: true };
|
|
706
858
|
}
|
|
707
859
|
// /plugins search <query>
|
|
708
|
-
if (subcommand ===
|
|
709
|
-
const query = rest ||
|
|
710
|
-
const results = searchMarketplace(query ===
|
|
860
|
+
if (subcommand === "search") {
|
|
861
|
+
const query = rest || "all";
|
|
862
|
+
const results = searchMarketplace(query === "all" ? "" : query);
|
|
711
863
|
return { output: formatMarketplaceSearch(results), handled: true };
|
|
712
864
|
}
|
|
713
865
|
// /plugins install <name>
|
|
714
|
-
if (subcommand ===
|
|
715
|
-
const [name, marketplace] = rest.split(
|
|
866
|
+
if (subcommand === "install" && rest) {
|
|
867
|
+
const [name, marketplace] = rest.split("@");
|
|
716
868
|
const result = installPlugin(name, marketplace);
|
|
717
869
|
if (result) {
|
|
718
|
-
return {
|
|
870
|
+
return {
|
|
871
|
+
output: `Installed ${result.name}@${result.version} from ${result.marketplace}\nCached at: ${result.cachePath}`,
|
|
872
|
+
handled: true,
|
|
873
|
+
};
|
|
719
874
|
}
|
|
720
|
-
return {
|
|
875
|
+
return {
|
|
876
|
+
output: `Failed to install "${rest}". Is it listed in a marketplace?\nRun /plugins search ${name} to check.`,
|
|
877
|
+
handled: true,
|
|
878
|
+
};
|
|
721
879
|
}
|
|
722
880
|
// /plugins uninstall <name>
|
|
723
|
-
if (subcommand ===
|
|
881
|
+
if (subcommand === "uninstall" && rest) {
|
|
724
882
|
return { output: uninstallPlugin(rest) ? `Uninstalled "${rest}"` : `Plugin "${rest}" not found`, handled: true };
|
|
725
883
|
}
|
|
726
884
|
// /plugins (no args) — show everything
|
|
@@ -730,33 +888,110 @@ register("plugins", "Manage plugins: list, search, install, uninstall, marketpla
|
|
|
730
888
|
const lines = [];
|
|
731
889
|
if (marketplacePlugins.length > 0) {
|
|
732
890
|
lines.push(formatInstalledPlugins(marketplacePlugins));
|
|
733
|
-
lines.push(
|
|
891
|
+
lines.push("");
|
|
734
892
|
}
|
|
735
893
|
if (plugins.length > 0) {
|
|
736
894
|
lines.push(`Local Plugins (${plugins.length}):`);
|
|
737
895
|
for (const p of plugins) {
|
|
738
|
-
lines.push(` ${p.name}@${p.version} — ${p.description ||
|
|
896
|
+
lines.push(` ${p.name}@${p.version} — ${p.description || "no description"}`);
|
|
739
897
|
}
|
|
740
|
-
lines.push(
|
|
898
|
+
lines.push("");
|
|
741
899
|
}
|
|
742
900
|
if (skills.length > 0) {
|
|
743
901
|
lines.push(`Skills (${skills.length}):`);
|
|
744
902
|
for (const s of skills) {
|
|
745
|
-
lines.push(` ${s.source}:${s.name} — ${s.description ||
|
|
903
|
+
lines.push(` ${s.source}:${s.name} — ${s.description || ""}`);
|
|
746
904
|
}
|
|
747
|
-
lines.push(
|
|
905
|
+
lines.push("");
|
|
748
906
|
}
|
|
749
907
|
if (lines.length === 0) {
|
|
750
|
-
lines.push(
|
|
908
|
+
lines.push("No plugins or skills installed.");
|
|
909
|
+
}
|
|
910
|
+
lines.push("");
|
|
911
|
+
lines.push("Commands:");
|
|
912
|
+
lines.push(" /plugins search <query> Search marketplaces");
|
|
913
|
+
lines.push(" /plugins install <name> Install from marketplace");
|
|
914
|
+
lines.push(" /plugins uninstall <name> Remove a plugin");
|
|
915
|
+
lines.push(" /plugins marketplace add <src> Add a marketplace");
|
|
916
|
+
lines.push(" /plugins marketplace List marketplaces");
|
|
917
|
+
return { output: lines.join("\n"), handled: true };
|
|
918
|
+
});
|
|
919
|
+
// ── Project Init ──
|
|
920
|
+
register("init", "Initialize project with .oh/ config", () => {
|
|
921
|
+
const ohDir = join(process.cwd(), ".oh");
|
|
922
|
+
if (existsSync(ohDir)) {
|
|
923
|
+
return { output: ".oh/ directory already exists. Project is already initialized.", handled: true };
|
|
924
|
+
}
|
|
925
|
+
mkdirSync(ohDir, { recursive: true });
|
|
926
|
+
const rulesPath = join(ohDir, "RULES.md");
|
|
927
|
+
if (!existsSync(rulesPath)) {
|
|
928
|
+
writeFileSync(rulesPath, `# Project Rules
|
|
929
|
+
|
|
930
|
+
<!-- Add project-specific instructions here. These are loaded into every session. -->
|
|
931
|
+
<!-- Examples: coding conventions, testing requirements, deployment guidelines. -->
|
|
932
|
+
`);
|
|
933
|
+
}
|
|
934
|
+
const configPath = join(ohDir, "config.yaml");
|
|
935
|
+
if (!existsSync(configPath)) {
|
|
936
|
+
writeFileSync(configPath, `# OpenHarness project config
|
|
937
|
+
# provider: ollama
|
|
938
|
+
# model: llama3
|
|
939
|
+
# permissionMode: ask
|
|
940
|
+
`);
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
output: `Initialized .oh/ with:\n .oh/RULES.md — project rules\n .oh/config.yaml — project config\n\nEdit these files to customize your project.`,
|
|
944
|
+
handled: true,
|
|
945
|
+
};
|
|
946
|
+
});
|
|
947
|
+
// ── Permissions ──
|
|
948
|
+
register("permissions", "View or change permission mode", (args, ctx) => {
|
|
949
|
+
const mode = args.trim().toLowerCase();
|
|
950
|
+
if (!mode) {
|
|
951
|
+
return {
|
|
952
|
+
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`,
|
|
953
|
+
handled: true,
|
|
954
|
+
};
|
|
751
955
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
956
|
+
const valid = ["ask", "trust", "deny", "acceptedits", "plan", "auto", "bypasspermissions"];
|
|
957
|
+
if (!valid.includes(mode)) {
|
|
958
|
+
return { output: `Unknown mode: ${mode}. Valid: ${valid.join(", ")}`, handled: true };
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
output: `Permission mode set to: ${mode}\n(Note: takes effect for new tool calls in this session)`,
|
|
962
|
+
handled: true,
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
register("allowed-tools", "View tool permission rules", () => {
|
|
966
|
+
const config = readOhConfig();
|
|
967
|
+
const rules = config?.toolPermissions;
|
|
968
|
+
if (!rules || rules.length === 0) {
|
|
969
|
+
return {
|
|
970
|
+
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 .*"',
|
|
971
|
+
handled: true,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
const lines = rules.map((r) => {
|
|
975
|
+
const parts = [` ${r.tool}: ${r.action}`];
|
|
976
|
+
if (r.pattern)
|
|
977
|
+
parts.push(`(pattern: ${r.pattern})`);
|
|
978
|
+
return parts.join(" ");
|
|
979
|
+
});
|
|
980
|
+
return { output: `Tool permission rules:\n${lines.join("\n")}`, handled: true };
|
|
981
|
+
});
|
|
982
|
+
register("rebuild-sessions", "Rebuild session search index", () => {
|
|
983
|
+
// Fire async rebuild, return immediately with status
|
|
984
|
+
import("../harness/session-db.js")
|
|
985
|
+
.then(({ openSessionDb, rebuildIndex, closeSessionDb }) => {
|
|
986
|
+
const db = openSessionDb();
|
|
987
|
+
const count = rebuildIndex(db);
|
|
988
|
+
closeSessionDb(db);
|
|
989
|
+
console.log(`Rebuilt session search index: ${count} sessions indexed.`);
|
|
990
|
+
})
|
|
991
|
+
.catch((err) => {
|
|
992
|
+
console.log(`Failed to rebuild index: ${err.message}`);
|
|
993
|
+
});
|
|
994
|
+
return { output: "Rebuilding session search index...", handled: true };
|
|
760
995
|
});
|
|
761
996
|
// ── Command Parser ──
|
|
762
997
|
/**
|
|
@@ -771,7 +1006,10 @@ export function processSlashCommand(input, context) {
|
|
|
771
1006
|
const args = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
|
|
772
1007
|
// Resolve aliases
|
|
773
1008
|
const aliases = {
|
|
774
|
-
h:
|
|
1009
|
+
h: "help",
|
|
1010
|
+
c: "commit",
|
|
1011
|
+
m: "model",
|
|
1012
|
+
s: "status",
|
|
775
1013
|
};
|
|
776
1014
|
const resolved = aliases[name] ?? name;
|
|
777
1015
|
const cmd = commands.get(resolved);
|