@zhijiewang/openharness 2.1.0 → 2.3.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.
- 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 +288 -132
- 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 +3 -3
- 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.js +15 -15
- 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 +25 -23
- 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 +39 -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 +114 -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 +1 -1
- package/dist/query/context-manager.js +5 -5
- package/dist/query/errors.js +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +42 -24
- package/dist/query/tools.js +15 -12
- package/dist/query/types.d.ts +3 -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 +311 -198
- package/dist/sdk/index.d.ts +5 -5
- package/dist/sdk/index.js +32 -26
- 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 +34 -32
- 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 +163 -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 +25 -39
- 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.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 +5 -5
- package/dist/tools/MemoryTool/index.js +28 -14
- 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.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();
|
|
@@ -275,7 +288,7 @@ register("compact", "Compress conversation history (optional: focus keyword or m
|
|
|
275
288
|
const targetTokens = Math.floor(getContextWindow(ctx.model) * 0.6);
|
|
276
289
|
if (focus && /^\d+$/.test(focus)) {
|
|
277
290
|
// Numeric: compact messages 1-N, keep N+1 onwards
|
|
278
|
-
const cutoff = parseInt(focus);
|
|
291
|
+
const cutoff = parseInt(focus, 10);
|
|
279
292
|
if (cutoff < 1 || cutoff >= before) {
|
|
280
293
|
return { output: `Invalid: use 1-${before - 1}`, handled: true };
|
|
281
294
|
}
|
|
@@ -289,8 +302,8 @@ register("compact", "Compress conversation history (optional: focus keyword or m
|
|
|
289
302
|
if (focus) {
|
|
290
303
|
// Keyword focus: compress but preserve messages containing the keyword
|
|
291
304
|
const focusLower = focus.toLowerCase();
|
|
292
|
-
const preserved = ctx.messages.filter(m => m.content.toLowerCase().includes(focusLower) || m.meta?.pinned);
|
|
293
|
-
const others = ctx.messages.filter(m => !m.content.toLowerCase().includes(focusLower) && !m.meta?.pinned);
|
|
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);
|
|
294
307
|
const compactedOthers = compressMessages(others, targetTokens);
|
|
295
308
|
const merged = [...compactedOthers, ...preserved].sort((a, b) => a.timestamp - b.timestamp);
|
|
296
309
|
return {
|
|
@@ -310,8 +323,8 @@ register("compact", "Compress conversation history (optional: focus keyword or m
|
|
|
310
323
|
});
|
|
311
324
|
register("export", "Export conversation to file", (_args, ctx) => {
|
|
312
325
|
const lines = ctx.messages
|
|
313
|
-
.filter(m => m.role === "user" || m.role === "assistant")
|
|
314
|
-
.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}`)
|
|
315
328
|
.join("\n\n");
|
|
316
329
|
const filename = `.oh/export-${ctx.sessionId}.md`;
|
|
317
330
|
try {
|
|
@@ -355,7 +368,7 @@ register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
|
355
368
|
const term = args.trim().toLowerCase();
|
|
356
369
|
let files;
|
|
357
370
|
try {
|
|
358
|
-
files = readdirSync(memDir).filter(f => f.endsWith(".md"));
|
|
371
|
+
files = readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
359
372
|
}
|
|
360
373
|
catch {
|
|
361
374
|
return { output: "Could not read .oh/memory/", handled: true };
|
|
@@ -369,11 +382,13 @@ register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
|
369
382
|
try {
|
|
370
383
|
const content = readFileSync(join(memDir, file), "utf-8");
|
|
371
384
|
if (content.toLowerCase().includes(term)) {
|
|
372
|
-
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;
|
|
373
386
|
matches.push(` ${file.padEnd(30)} ${firstLine.slice(0, 50)}`);
|
|
374
387
|
}
|
|
375
388
|
}
|
|
376
|
-
catch {
|
|
389
|
+
catch {
|
|
390
|
+
/* skip */
|
|
391
|
+
}
|
|
377
392
|
}
|
|
378
393
|
if (matches.length === 0)
|
|
379
394
|
return { output: `No memories matching "${term}".`, handled: true };
|
|
@@ -397,11 +412,11 @@ register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
|
397
412
|
});
|
|
398
413
|
register("companion", "Toggle companion visibility (off/on)", (args) => {
|
|
399
414
|
const arg = args.trim().toLowerCase();
|
|
400
|
-
if (arg ===
|
|
401
|
-
return { output:
|
|
402
|
-
if (arg ===
|
|
403
|
-
return { output:
|
|
404
|
-
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 };
|
|
405
420
|
});
|
|
406
421
|
register("cybergotchi", "Manage your cybergotchi — feed · pet · rest · status · rename · reset", (args) => {
|
|
407
422
|
return handleCybergotchiCommand(args);
|
|
@@ -412,32 +427,35 @@ register("roles", "List available agent specialization roles", () => {
|
|
|
412
427
|
const lines = ["Available agent roles:\n"];
|
|
413
428
|
for (const role of roles) {
|
|
414
429
|
lines.push(` ${role.id.padEnd(18)} ${role.name}`);
|
|
415
|
-
lines.push(` ${
|
|
430
|
+
lines.push(` ${"".padEnd(18)} ${role.description}`);
|
|
416
431
|
if (role.suggestedTools?.length) {
|
|
417
|
-
lines.push(` ${
|
|
432
|
+
lines.push(` ${"".padEnd(18)} Tools: ${role.suggestedTools.join(", ")}`);
|
|
418
433
|
}
|
|
419
|
-
lines.push(
|
|
434
|
+
lines.push("");
|
|
420
435
|
}
|
|
421
436
|
lines.push("Usage: Agent({ subagent_type: 'code-reviewer', prompt: '...' })");
|
|
422
437
|
return { output: lines.join("\n"), handled: true };
|
|
423
438
|
});
|
|
424
439
|
register("agents", "Discover running openHarness agents on this machine", () => {
|
|
425
|
-
const { discoverAgents } = require(
|
|
440
|
+
const { discoverAgents } = require("../services/a2a.js");
|
|
426
441
|
const agents = discoverAgents();
|
|
427
442
|
if (agents.length === 0) {
|
|
428
|
-
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
|
+
};
|
|
429
447
|
}
|
|
430
448
|
const lines = [`Running Agents (${agents.length}):\n`];
|
|
431
449
|
for (const agent of agents) {
|
|
432
450
|
const age = Math.round((Date.now() - agent.registeredAt) / 60_000);
|
|
433
451
|
lines.push(` ${agent.name}`);
|
|
434
452
|
lines.push(` ID: ${agent.id}`);
|
|
435
|
-
lines.push(` Provider: ${agent.provider ??
|
|
436
|
-
lines.push(` Dir: ${agent.workingDir ??
|
|
437
|
-
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}` : ""}`);
|
|
438
456
|
lines.push(` Uptime: ${age}m`);
|
|
439
|
-
lines.push(` Caps: ${agent.capabilities.map((c) => c.name).join(
|
|
440
|
-
lines.push(
|
|
457
|
+
lines.push(` Caps: ${agent.capabilities.map((c) => c.name).join(", ")}`);
|
|
458
|
+
lines.push("");
|
|
441
459
|
}
|
|
442
460
|
lines.push("Send messages with: Agent({ prompt: 'ask the other agent...', allowed_tools: ['SendMessage'] })");
|
|
443
461
|
return { output: lines.join("\n"), handled: true };
|
|
@@ -476,14 +494,17 @@ register("keys", "Show keyboard shortcuts", () => {
|
|
|
476
494
|
return { output: shortcuts.join("\n"), handled: true };
|
|
477
495
|
});
|
|
478
496
|
register("sandbox", "Show sandbox status and restrictions", () => {
|
|
479
|
-
const { sandboxStatus } = require(
|
|
480
|
-
return { output: sandboxStatus()
|
|
497
|
+
const { sandboxStatus } = require("../harness/sandbox.js");
|
|
498
|
+
return { output: `${sandboxStatus()}\n\nConfigure in .oh/config.yaml under sandbox:`, handled: true };
|
|
481
499
|
});
|
|
482
500
|
register("effort", "Set reasoning effort level (low/medium/high/max)", (args) => {
|
|
483
501
|
const level = args.trim().toLowerCase();
|
|
484
|
-
const valid = [
|
|
502
|
+
const valid = ["low", "medium", "high", "max"];
|
|
485
503
|
if (!valid.includes(level)) {
|
|
486
|
-
return {
|
|
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
|
+
};
|
|
487
508
|
}
|
|
488
509
|
return { output: `Effort level set to: ${level}`, handled: true };
|
|
489
510
|
});
|
|
@@ -499,6 +520,38 @@ register("btw", "Ask a side question (ephemeral, no tools, not saved to history)
|
|
|
499
520
|
prependToPrompt: `[Side question — answer briefly without using any tools. This is ephemeral and not part of the main conversation.]\n\n${args.trim()}`,
|
|
500
521
|
};
|
|
501
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
|
+
});
|
|
502
555
|
register("plan", "Enter plan mode", (_args, _ctx) => {
|
|
503
556
|
const task = _args.trim();
|
|
504
557
|
if (!task) {
|
|
@@ -507,7 +560,7 @@ register("plan", "Enter plan mode", (_args, _ctx) => {
|
|
|
507
560
|
return {
|
|
508
561
|
output: `[plan mode] ${task}`,
|
|
509
562
|
handled: false,
|
|
510
|
-
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}`,
|
|
511
564
|
};
|
|
512
565
|
});
|
|
513
566
|
register("review", "Review recent code changes", () => {
|
|
@@ -590,39 +643,45 @@ register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
|
590
643
|
const ohDir = join(homedir(), ".oh");
|
|
591
644
|
if (existsSync(ohDir)) {
|
|
592
645
|
const sessionsDir = join(ohDir, "sessions");
|
|
593
|
-
const sessCount = existsSync(sessionsDir)
|
|
646
|
+
const sessCount = existsSync(sessionsDir)
|
|
647
|
+
? readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length
|
|
648
|
+
: 0;
|
|
594
649
|
lines.push(` Sessions: ${sessCount} saved`);
|
|
595
650
|
if (sessCount > 80)
|
|
596
651
|
issues.push(`${sessCount} saved sessions. Consider cleaning old ones.`);
|
|
597
652
|
// Memory stats
|
|
598
653
|
const memDir = join(ohDir, "memory");
|
|
599
|
-
const memCount = existsSync(memDir) ? readdirSync(memDir).filter(f => f.endsWith(
|
|
654
|
+
const memCount = existsSync(memDir) ? readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
600
655
|
lines.push(` Memories: ${memCount} global`);
|
|
601
656
|
// Cron stats
|
|
602
657
|
const cronDir = join(ohDir, "crons");
|
|
603
|
-
const cronCount = existsSync(cronDir) ? readdirSync(cronDir).filter(f => f.endsWith(
|
|
658
|
+
const cronCount = existsSync(cronDir) ? readdirSync(cronDir).filter((f) => f.endsWith(".json")).length : 0;
|
|
604
659
|
lines.push(` Cron tasks: ${cronCount}`);
|
|
605
660
|
}
|
|
606
661
|
}
|
|
607
|
-
catch {
|
|
662
|
+
catch {
|
|
663
|
+
/* ignore */
|
|
664
|
+
}
|
|
608
665
|
// Project-level stats
|
|
609
666
|
try {
|
|
610
667
|
const projMemDir = join(".oh", "memory");
|
|
611
|
-
const projMemCount = existsSync(projMemDir) ? readdirSync(projMemDir).filter(f => f.endsWith(
|
|
668
|
+
const projMemCount = existsSync(projMemDir) ? readdirSync(projMemDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
612
669
|
if (projMemCount > 0)
|
|
613
670
|
lines.push(` Project mems: ${projMemCount}`);
|
|
614
671
|
const skillsDir = join(".oh", "skills");
|
|
615
|
-
const skillCount = existsSync(skillsDir) ? readdirSync(skillsDir).filter(f => f.endsWith(
|
|
672
|
+
const skillCount = existsSync(skillsDir) ? readdirSync(skillsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
616
673
|
if (skillCount > 0)
|
|
617
674
|
lines.push(` Skills: ${skillCount}`);
|
|
618
675
|
}
|
|
619
|
-
catch {
|
|
676
|
+
catch {
|
|
677
|
+
/* ignore */
|
|
678
|
+
}
|
|
620
679
|
// Global config
|
|
621
680
|
const globalCfg = existsSync(join(homedir(), ".oh", "config.yaml"));
|
|
622
681
|
lines.push(` Global config: ${globalCfg ? "~/.oh/config.yaml ✓" : "not set (optional)"}`);
|
|
623
682
|
// Verification config
|
|
624
683
|
try {
|
|
625
|
-
const { getVerificationConfig } = require(
|
|
684
|
+
const { getVerificationConfig } = require("../harness/verification.js");
|
|
626
685
|
const vCfg = getVerificationConfig();
|
|
627
686
|
if (vCfg?.enabled) {
|
|
628
687
|
lines.push(` Verification: ✓ (${vCfg.rules.length} rules, mode: ${vCfg.mode})`);
|
|
@@ -631,13 +690,15 @@ register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
|
631
690
|
lines.push(` Verification: off (no rules detected)`);
|
|
632
691
|
}
|
|
633
692
|
}
|
|
634
|
-
catch {
|
|
693
|
+
catch {
|
|
694
|
+
/* ignore */
|
|
695
|
+
}
|
|
635
696
|
// Tools
|
|
636
697
|
lines.push("");
|
|
637
|
-
lines.push(` Tools: ${ctx.messages.length > 0 ?
|
|
698
|
+
lines.push(` Tools: ${ctx.messages.length > 0 ? "ready" : "loaded"}`);
|
|
638
699
|
// Node.js version
|
|
639
700
|
lines.push(` Node.js: ${process.version}`);
|
|
640
|
-
const [major] = process.version.slice(1).split(
|
|
701
|
+
const [major] = process.version.slice(1).split(".").map(Number);
|
|
641
702
|
if (major && major < 18)
|
|
642
703
|
issues.push(`Node.js ${process.version} is below minimum (18+). Upgrade Node.js.`);
|
|
643
704
|
// Issues summary
|
|
@@ -661,16 +722,16 @@ register("context", "Show context window usage breakdown", (_args, ctx) => {
|
|
|
661
722
|
for (const msg of ctx.messages) {
|
|
662
723
|
const tokens = Math.ceil((msg.content?.length ?? 0) / 4);
|
|
663
724
|
switch (msg.role) {
|
|
664
|
-
case
|
|
725
|
+
case "user":
|
|
665
726
|
userTokens += tokens;
|
|
666
727
|
break;
|
|
667
|
-
case
|
|
728
|
+
case "assistant":
|
|
668
729
|
assistantTokens += tokens;
|
|
669
730
|
break;
|
|
670
|
-
case
|
|
731
|
+
case "tool":
|
|
671
732
|
toolTokens += tokens;
|
|
672
733
|
break;
|
|
673
|
-
case
|
|
734
|
+
case "system":
|
|
674
735
|
systemTokens += tokens;
|
|
675
736
|
break;
|
|
676
737
|
}
|
|
@@ -681,30 +742,33 @@ register("context", "Show context window usage breakdown", (_args, ctx) => {
|
|
|
681
742
|
// Visual bar (30 chars wide)
|
|
682
743
|
const barWidth = 30;
|
|
683
744
|
const filled = Math.round(usage * barWidth);
|
|
684
|
-
const bar =
|
|
745
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
685
746
|
const pct = (n) => `${((n / ctxWindow) * 100).toFixed(1)}%`;
|
|
686
747
|
const pad = (s, n) => s.padEnd(n);
|
|
687
748
|
const lines = [
|
|
688
749
|
`Context Window (${ctxWindow.toLocaleString()} tokens):`,
|
|
689
|
-
|
|
690
|
-
` ${pad(
|
|
691
|
-
` ${pad(
|
|
692
|
-
` ${pad(
|
|
693
|
-
` ${pad(
|
|
694
|
-
|
|
695
|
-
` ${pad(
|
|
696
|
-
` ${pad(
|
|
697
|
-
|
|
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
|
+
"",
|
|
698
759
|
` ${bar} ${Math.round(usage * 100)}%`,
|
|
699
|
-
|
|
760
|
+
"",
|
|
700
761
|
` Messages: ${ctx.messages.length} | Compress at: ${Math.round(ctxWindow * 0.8).toLocaleString()} (80%)`,
|
|
701
762
|
];
|
|
702
|
-
return { output: lines.join(
|
|
763
|
+
return { output: lines.join("\n"), handled: true };
|
|
703
764
|
});
|
|
704
765
|
register("mcp", "Show MCP server status", () => {
|
|
705
766
|
const mcp = connectedMcpServers();
|
|
706
767
|
if (mcp.length === 0) {
|
|
707
|
-
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
|
+
};
|
|
708
772
|
}
|
|
709
773
|
const lines = [`MCP Servers (${mcp.length} connected):\n`];
|
|
710
774
|
for (const name of mcp) {
|
|
@@ -714,11 +778,11 @@ register("mcp", "Show MCP server status", () => {
|
|
|
714
778
|
return { output: lines.join("\n"), handled: true };
|
|
715
779
|
});
|
|
716
780
|
register("mcp-registry", "Browse and add MCP servers from the curated registry", (args) => {
|
|
717
|
-
const { searchRegistry, formatRegistry, generateConfigBlock, MCP_REGISTRY } = require(
|
|
781
|
+
const { searchRegistry, formatRegistry, generateConfigBlock, MCP_REGISTRY } = require("../mcp/registry.js");
|
|
718
782
|
const query = args.trim();
|
|
719
783
|
if (!query) {
|
|
720
784
|
// Show full registry
|
|
721
|
-
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`;
|
|
722
786
|
return { output, handled: true };
|
|
723
787
|
}
|
|
724
788
|
// Search or show specific server
|
|
@@ -731,10 +795,10 @@ register("mcp-registry", "Browse and add MCP servers from the curated registry",
|
|
|
731
795
|
const entry = results[0];
|
|
732
796
|
const config = generateConfigBlock(entry);
|
|
733
797
|
const envNote = entry.envVars?.length
|
|
734
|
-
? `\n\nRequired environment variables:\n${entry.envVars.map((v) => ` - ${v}`).join(
|
|
735
|
-
:
|
|
798
|
+
? `\n\nRequired environment variables:\n${entry.envVars.map((v) => ` - ${v}`).join("\n")}`
|
|
799
|
+
: "";
|
|
736
800
|
return {
|
|
737
|
-
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}`,
|
|
738
802
|
handled: true,
|
|
739
803
|
};
|
|
740
804
|
}
|
|
@@ -743,13 +807,13 @@ register("mcp-registry", "Browse and add MCP servers from the curated registry",
|
|
|
743
807
|
});
|
|
744
808
|
function setPinned(args, ctx, pinned) {
|
|
745
809
|
const idx = parseInt(args.trim(), 10);
|
|
746
|
-
if (isNaN(idx) || idx < 1 || idx > ctx.messages.length) {
|
|
747
|
-
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 };
|
|
748
812
|
}
|
|
749
813
|
// Immutable update — replace message with updated meta
|
|
750
|
-
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));
|
|
751
815
|
return {
|
|
752
|
-
output: `Message #${idx} ${pinned ?
|
|
816
|
+
output: `Message #${idx} ${pinned ? "pinned" : "unpinned"}.`,
|
|
753
817
|
handled: true,
|
|
754
818
|
compactedMessages: updatedMessages,
|
|
755
819
|
};
|
|
@@ -757,52 +821,64 @@ function setPinned(args, ctx, pinned) {
|
|
|
757
821
|
register("pin", "Pin a message (survives /compact)", (args, ctx) => setPinned(args, ctx, true));
|
|
758
822
|
register("unpin", "Unpin a message", (args, ctx) => setPinned(args, ctx, false));
|
|
759
823
|
register("plugins", "Manage plugins: list, search, install, uninstall, marketplace", (args) => {
|
|
760
|
-
const { discoverPlugins, discoverSkills } = require(
|
|
761
|
-
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");
|
|
762
826
|
const parts = args.trim().split(/\s+/);
|
|
763
|
-
const subcommand = parts[0] ??
|
|
764
|
-
const rest = parts.slice(1).join(
|
|
827
|
+
const subcommand = parts[0] ?? "";
|
|
828
|
+
const rest = parts.slice(1).join(" ");
|
|
765
829
|
// /plugins marketplace add <source>
|
|
766
|
-
if (subcommand ===
|
|
830
|
+
if (subcommand === "marketplace") {
|
|
767
831
|
const action = parts[1];
|
|
768
|
-
const source = parts.slice(2).join(
|
|
769
|
-
if (action ===
|
|
832
|
+
const source = parts.slice(2).join(" ");
|
|
833
|
+
if (action === "add" && source) {
|
|
770
834
|
const mp = addMarketplace(source);
|
|
771
835
|
if (mp)
|
|
772
836
|
return { output: `Added marketplace "${mp.name}" (${mp.plugins.length} plugins)`, handled: true };
|
|
773
837
|
return { output: `Failed to add marketplace from "${source}"`, handled: true };
|
|
774
838
|
}
|
|
775
|
-
if (action ===
|
|
776
|
-
return {
|
|
839
|
+
if (action === "remove" && source) {
|
|
840
|
+
return {
|
|
841
|
+
output: removeMarketplace(source) ? `Removed marketplace "${source}"` : `Marketplace "${source}" not found`,
|
|
842
|
+
handled: true,
|
|
843
|
+
};
|
|
777
844
|
}
|
|
778
845
|
// List marketplaces
|
|
779
846
|
const mps = listMarketplaces();
|
|
780
847
|
if (mps.length === 0) {
|
|
781
|
-
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
|
+
};
|
|
782
852
|
}
|
|
783
853
|
const lines = [`Marketplaces (${mps.length}):\n`];
|
|
784
854
|
for (const mp of mps) {
|
|
785
855
|
lines.push(` ${mp.name} — ${mp.plugins.length} plugins`);
|
|
786
856
|
}
|
|
787
|
-
return { output: lines.join(
|
|
857
|
+
return { output: lines.join("\n"), handled: true };
|
|
788
858
|
}
|
|
789
859
|
// /plugins search <query>
|
|
790
|
-
if (subcommand ===
|
|
791
|
-
const query = rest ||
|
|
792
|
-
const results = searchMarketplace(query ===
|
|
860
|
+
if (subcommand === "search") {
|
|
861
|
+
const query = rest || "all";
|
|
862
|
+
const results = searchMarketplace(query === "all" ? "" : query);
|
|
793
863
|
return { output: formatMarketplaceSearch(results), handled: true };
|
|
794
864
|
}
|
|
795
865
|
// /plugins install <name>
|
|
796
|
-
if (subcommand ===
|
|
797
|
-
const [name, marketplace] = rest.split(
|
|
866
|
+
if (subcommand === "install" && rest) {
|
|
867
|
+
const [name, marketplace] = rest.split("@");
|
|
798
868
|
const result = installPlugin(name, marketplace);
|
|
799
869
|
if (result) {
|
|
800
|
-
return {
|
|
870
|
+
return {
|
|
871
|
+
output: `Installed ${result.name}@${result.version} from ${result.marketplace}\nCached at: ${result.cachePath}`,
|
|
872
|
+
handled: true,
|
|
873
|
+
};
|
|
801
874
|
}
|
|
802
|
-
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
|
+
};
|
|
803
879
|
}
|
|
804
880
|
// /plugins uninstall <name>
|
|
805
|
-
if (subcommand ===
|
|
881
|
+
if (subcommand === "uninstall" && rest) {
|
|
806
882
|
return { output: uninstallPlugin(rest) ? `Uninstalled "${rest}"` : `Plugin "${rest}" not found`, handled: true };
|
|
807
883
|
}
|
|
808
884
|
// /plugins (no args) — show everything
|
|
@@ -812,33 +888,110 @@ register("plugins", "Manage plugins: list, search, install, uninstall, marketpla
|
|
|
812
888
|
const lines = [];
|
|
813
889
|
if (marketplacePlugins.length > 0) {
|
|
814
890
|
lines.push(formatInstalledPlugins(marketplacePlugins));
|
|
815
|
-
lines.push(
|
|
891
|
+
lines.push("");
|
|
816
892
|
}
|
|
817
893
|
if (plugins.length > 0) {
|
|
818
894
|
lines.push(`Local Plugins (${plugins.length}):`);
|
|
819
895
|
for (const p of plugins) {
|
|
820
|
-
lines.push(` ${p.name}@${p.version} — ${p.description ||
|
|
896
|
+
lines.push(` ${p.name}@${p.version} — ${p.description || "no description"}`);
|
|
821
897
|
}
|
|
822
|
-
lines.push(
|
|
898
|
+
lines.push("");
|
|
823
899
|
}
|
|
824
900
|
if (skills.length > 0) {
|
|
825
901
|
lines.push(`Skills (${skills.length}):`);
|
|
826
902
|
for (const s of skills) {
|
|
827
|
-
lines.push(` ${s.source}:${s.name} — ${s.description ||
|
|
903
|
+
lines.push(` ${s.source}:${s.name} — ${s.description || ""}`);
|
|
828
904
|
}
|
|
829
|
-
lines.push(
|
|
905
|
+
lines.push("");
|
|
830
906
|
}
|
|
831
907
|
if (lines.length === 0) {
|
|
832
|
-
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
|
+
};
|
|
955
|
+
}
|
|
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 };
|
|
833
959
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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 };
|
|
842
995
|
});
|
|
843
996
|
// ── Command Parser ──
|
|
844
997
|
/**
|
|
@@ -853,7 +1006,10 @@ export function processSlashCommand(input, context) {
|
|
|
853
1006
|
const args = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
|
|
854
1007
|
// Resolve aliases
|
|
855
1008
|
const aliases = {
|
|
856
|
-
h:
|
|
1009
|
+
h: "help",
|
|
1010
|
+
c: "commit",
|
|
1011
|
+
m: "model",
|
|
1012
|
+
s: "status",
|
|
857
1013
|
};
|
|
858
1014
|
const resolved = aliases[name] ?? name;
|
|
859
1015
|
const cmd = commands.get(resolved);
|