@zhijiewang/openharness 2.5.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/ai.d.ts +6 -0
- package/dist/commands/ai.js +244 -0
- package/dist/commands/git.d.ts +6 -0
- package/dist/commands/git.js +167 -0
- package/dist/commands/index.d.ts +10 -31
- package/dist/commands/index.js +22 -1096
- package/dist/commands/info.d.ts +8 -0
- package/dist/commands/info.js +671 -0
- package/dist/commands/session.d.ts +6 -0
- package/dist/commands/session.js +214 -0
- package/dist/commands/settings.d.ts +6 -0
- package/dist/commands/settings.js +187 -0
- package/dist/commands/skills.d.ts +6 -0
- package/dist/commands/skills.js +117 -0
- package/dist/commands/types.d.ts +36 -0
- package/dist/commands/types.js +5 -0
- package/dist/components/InitWizard.js +60 -62
- package/dist/harness/hooks.js +9 -6
- package/dist/providers/anthropic.js +7 -8
- package/dist/providers/openai.js +3 -2
- package/dist/renderer/layout-sections.d.ts +56 -0
- package/dist/renderer/layout-sections.js +462 -0
- package/dist/renderer/layout.d.ts +4 -2
- package/dist/renderer/layout.js +25 -500
- package/dist/tools/TodoWriteTool/index.d.ts +37 -0
- package/dist/tools/TodoWriteTool/index.js +78 -0
- package/dist/tools.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Info commands — /help, /cost, /status, /config, /files, /model, /memory, /doctor, /context, /mcp, /mcp-registry, /init
|
|
3
|
+
*/
|
|
4
|
+
import type { CommandHandler } from "./types.js";
|
|
5
|
+
export declare function registerInfoCommands(register: (name: string, description: string, handler: CommandHandler) => void, getCommandMap: () => Map<string, {
|
|
6
|
+
description: string;
|
|
7
|
+
}>): void;
|
|
8
|
+
//# sourceMappingURL=info.d.ts.map
|
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Info commands — /help, /cost, /status, /config, /files, /model, /memory, /doctor, /context, /mcp, /mcp-registry, /init
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { gitBranch, isGitRepo, isInMergeOrRebase } from "../git/index.js";
|
|
8
|
+
import { readOhConfig } from "../harness/config.js";
|
|
9
|
+
import { estimateMessageTokens } from "../harness/context-warning.js";
|
|
10
|
+
import { getContextWindow } from "../harness/cost.js";
|
|
11
|
+
import { connectedMcpServers } from "../mcp/loader.js";
|
|
12
|
+
export function registerInfoCommands(register, getCommandMap) {
|
|
13
|
+
register("help", "Show available commands", () => {
|
|
14
|
+
const categories = {
|
|
15
|
+
Session: [
|
|
16
|
+
"clear",
|
|
17
|
+
"compact",
|
|
18
|
+
"export",
|
|
19
|
+
"history",
|
|
20
|
+
"browse",
|
|
21
|
+
"resume",
|
|
22
|
+
"fork",
|
|
23
|
+
"pin",
|
|
24
|
+
"unpin",
|
|
25
|
+
"add-dir",
|
|
26
|
+
"listen",
|
|
27
|
+
"truncate",
|
|
28
|
+
"search",
|
|
29
|
+
],
|
|
30
|
+
Git: ["diff", "undo", "rewind", "commit", "log", "review-pr", "pr-comments", "release-notes", "stash", "branch"],
|
|
31
|
+
Info: [
|
|
32
|
+
"help",
|
|
33
|
+
"cost",
|
|
34
|
+
"status",
|
|
35
|
+
"config",
|
|
36
|
+
"files",
|
|
37
|
+
"model",
|
|
38
|
+
"memory",
|
|
39
|
+
"doctor",
|
|
40
|
+
"context",
|
|
41
|
+
"mcp",
|
|
42
|
+
"mcp-registry",
|
|
43
|
+
"init",
|
|
44
|
+
"bug",
|
|
45
|
+
"feedback",
|
|
46
|
+
"upgrade",
|
|
47
|
+
"token-count",
|
|
48
|
+
"benchmark",
|
|
49
|
+
"version",
|
|
50
|
+
"api-credits",
|
|
51
|
+
"whoami",
|
|
52
|
+
"project",
|
|
53
|
+
"stats",
|
|
54
|
+
"tools",
|
|
55
|
+
],
|
|
56
|
+
Settings: [
|
|
57
|
+
"theme",
|
|
58
|
+
"vim",
|
|
59
|
+
"companion",
|
|
60
|
+
"fast",
|
|
61
|
+
"keys",
|
|
62
|
+
"effort",
|
|
63
|
+
"sandbox",
|
|
64
|
+
"permissions",
|
|
65
|
+
"allowed-tools",
|
|
66
|
+
"login",
|
|
67
|
+
"logout",
|
|
68
|
+
"terminal-setup",
|
|
69
|
+
"verbose",
|
|
70
|
+
"quiet",
|
|
71
|
+
"provider",
|
|
72
|
+
],
|
|
73
|
+
AI: ["plan", "review", "roles", "agents", "plugins", "btw", "loop", "summarize", "explain", "fix"],
|
|
74
|
+
Pet: ["cybergotchi"],
|
|
75
|
+
};
|
|
76
|
+
const commands = getCommandMap();
|
|
77
|
+
const lines = [];
|
|
78
|
+
for (const [category, names] of Object.entries(categories)) {
|
|
79
|
+
lines.push(`${category}:`);
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const cmd = commands.get(name);
|
|
82
|
+
if (cmd)
|
|
83
|
+
lines.push(` /${name.padEnd(12)} ${cmd.description}`);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
}
|
|
87
|
+
const categorized = new Set(Object.values(categories).flat());
|
|
88
|
+
const uncategorized = [...commands.keys()].filter((n) => !categorized.has(n));
|
|
89
|
+
if (uncategorized.length > 0) {
|
|
90
|
+
lines.push("Other:");
|
|
91
|
+
for (const name of uncategorized) {
|
|
92
|
+
const cmd = commands.get(name);
|
|
93
|
+
lines.push(` /${name.padEnd(12)} ${cmd.description}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { output: lines.join("\n"), handled: true };
|
|
97
|
+
});
|
|
98
|
+
register("cost", "Show session cost and token usage", (_args, ctx) => {
|
|
99
|
+
const lines = [
|
|
100
|
+
`Cost: $${ctx.totalCost.toFixed(4)}`,
|
|
101
|
+
`Tokens: ${ctx.totalInputTokens.toLocaleString()} input, ${ctx.totalOutputTokens.toLocaleString()} output`,
|
|
102
|
+
`Model: ${ctx.model}`,
|
|
103
|
+
`Session: ${ctx.sessionId}`,
|
|
104
|
+
];
|
|
105
|
+
return { output: lines.join("\n"), handled: true };
|
|
106
|
+
});
|
|
107
|
+
register("status", "Show session status", (_args, ctx) => {
|
|
108
|
+
const lines = [
|
|
109
|
+
`Model: ${ctx.model}`,
|
|
110
|
+
`Mode: ${ctx.permissionMode}`,
|
|
111
|
+
`Messages: ${ctx.messages.length}`,
|
|
112
|
+
`Cost: $${ctx.totalCost.toFixed(4)}`,
|
|
113
|
+
`Session: ${ctx.sessionId}`,
|
|
114
|
+
];
|
|
115
|
+
if (isGitRepo()) {
|
|
116
|
+
lines.push(`Git branch: ${gitBranch()}`);
|
|
117
|
+
}
|
|
118
|
+
const mcp = connectedMcpServers();
|
|
119
|
+
if (mcp.length > 0) {
|
|
120
|
+
lines.push(`MCP servers: ${mcp.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
return { output: lines.join("\n"), handled: true };
|
|
123
|
+
});
|
|
124
|
+
register("config", "Show current configuration", (_args, ctx) => {
|
|
125
|
+
const saved = readOhConfig();
|
|
126
|
+
const lines = ["Configuration:"];
|
|
127
|
+
if (saved) {
|
|
128
|
+
lines.push(` Provider: ${saved.provider}`);
|
|
129
|
+
lines.push(` Model: ${saved.model}`);
|
|
130
|
+
lines.push(` Permission: ${saved.permissionMode}`);
|
|
131
|
+
if (saved.baseUrl)
|
|
132
|
+
lines.push(` Base URL: ${saved.baseUrl}`);
|
|
133
|
+
if (saved.apiKey)
|
|
134
|
+
lines.push(` API key: ${"*".repeat(8)}...`);
|
|
135
|
+
lines.push(` Source: .oh/config.yaml`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
lines.push(` No .oh/config.yaml found — run oh init to create one`);
|
|
139
|
+
}
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push(` Active model: ${ctx.model}`);
|
|
142
|
+
lines.push(` Permission mode: ${ctx.permissionMode}`);
|
|
143
|
+
const mcp = connectedMcpServers();
|
|
144
|
+
if (mcp.length > 0)
|
|
145
|
+
lines.push(` MCP servers: ${mcp.join(", ")}`);
|
|
146
|
+
return { output: lines.join("\n"), handled: true };
|
|
147
|
+
});
|
|
148
|
+
register("files", "List files in context", (_args, ctx) => {
|
|
149
|
+
const files = new Set();
|
|
150
|
+
for (const msg of ctx.messages) {
|
|
151
|
+
if (msg.toolCalls) {
|
|
152
|
+
for (const tc of msg.toolCalls) {
|
|
153
|
+
const path = tc.arguments?.file_path ?? tc.arguments?.path;
|
|
154
|
+
if (path)
|
|
155
|
+
files.add(String(path));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (files.size === 0)
|
|
160
|
+
return { output: "No files in context yet.", handled: true };
|
|
161
|
+
return { output: `Files in context:\n${[...files].map((f) => ` ${f}`).join("\n")}`, handled: true };
|
|
162
|
+
});
|
|
163
|
+
register("model", "Switch model (e.g., /model llama3.2 or /model ollama/llama3.2)", (args, ctx) => {
|
|
164
|
+
const model = args.trim();
|
|
165
|
+
if (!model)
|
|
166
|
+
return { output: "Usage: /model <model-name> (prefix with provider/ to switch providers)", handled: true };
|
|
167
|
+
let newProviderName;
|
|
168
|
+
if (model.includes("/")) {
|
|
169
|
+
newProviderName = model.split("/")[0];
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
newProviderName = ctx.providerName;
|
|
173
|
+
}
|
|
174
|
+
if (newProviderName !== ctx.providerName) {
|
|
175
|
+
return {
|
|
176
|
+
output: `Cannot switch to '${model}': requires the '${newProviderName}' provider but current session uses '${ctx.providerName}'.\nRestart with: oh --model ${newProviderName}/${model.includes("/") ? model.split("/").slice(1).join("/") : model}`,
|
|
177
|
+
handled: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const modelName = model.includes("/") ? model.split("/").slice(1).join("/") : model;
|
|
181
|
+
return { output: `Switched to ${modelName}.`, handled: true, newModel: modelName };
|
|
182
|
+
});
|
|
183
|
+
register("memory", "View and search memories in .oh/memory/", (args) => {
|
|
184
|
+
const memDir = join(process.cwd(), ".oh", "memory");
|
|
185
|
+
if (!existsSync(memDir)) {
|
|
186
|
+
return { output: "No .oh/memory/ directory found. Memories are stored there by the AI.", handled: true };
|
|
187
|
+
}
|
|
188
|
+
const term = args.trim().toLowerCase();
|
|
189
|
+
let files;
|
|
190
|
+
try {
|
|
191
|
+
files = readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { output: "Could not read .oh/memory/", handled: true };
|
|
195
|
+
}
|
|
196
|
+
if (files.length === 0)
|
|
197
|
+
return { output: "No memories stored yet.", handled: true };
|
|
198
|
+
if (term) {
|
|
199
|
+
const matches = [];
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
try {
|
|
202
|
+
const content = readFileSync(join(memDir, file), "utf-8");
|
|
203
|
+
if (content.toLowerCase().includes(term)) {
|
|
204
|
+
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("---")) ?? file;
|
|
205
|
+
matches.push(` ${file.padEnd(30)} ${firstLine.slice(0, 50)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
/* skip */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (matches.length === 0)
|
|
213
|
+
return { output: `No memories matching "${term}".`, handled: true };
|
|
214
|
+
return { output: `Memories matching "${term}":\n${matches.join("\n")}`, handled: true };
|
|
215
|
+
}
|
|
216
|
+
const lines = [`Memories (${files.length}) — use /memory <term> to search:\n`];
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
try {
|
|
219
|
+
const content = readFileSync(join(memDir, file), "utf-8");
|
|
220
|
+
const nameLine = content.match(/^name:\s*(.+)$/m)?.[1] ?? file.replace(".md", "");
|
|
221
|
+
const typeLine = content.match(/^type:\s*(.+)$/m)?.[1] ?? "?";
|
|
222
|
+
const descLine = content.match(/^description:\s*(.+)$/m)?.[1] ?? "";
|
|
223
|
+
lines.push(` [${typeLine.padEnd(8)}] ${nameLine.padEnd(24)} ${descLine.slice(0, 40)}`);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
lines.push(` ${file}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { output: lines.join("\n"), handled: true };
|
|
230
|
+
});
|
|
231
|
+
register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
232
|
+
const lines = [];
|
|
233
|
+
const issues = [];
|
|
234
|
+
lines.push("─── Health Check ───");
|
|
235
|
+
lines.push("");
|
|
236
|
+
lines.push(` Provider: ${ctx.providerName || "⚠ not set"}`);
|
|
237
|
+
lines.push(` Model: ${ctx.model || "⚠ not set"}`);
|
|
238
|
+
lines.push(` Permission: ${ctx.permissionMode}`);
|
|
239
|
+
if (!ctx.model)
|
|
240
|
+
issues.push("No model configured. Use --model or set in .oh/config.yaml");
|
|
241
|
+
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
|
242
|
+
const hasOpenAIKey = !!process.env.OPENAI_API_KEY;
|
|
243
|
+
if (ctx.providerName === "anthropic" && !hasAnthropicKey) {
|
|
244
|
+
issues.push("ANTHROPIC_API_KEY not set. Run: export ANTHROPIC_API_KEY=sk-...");
|
|
245
|
+
}
|
|
246
|
+
if (ctx.providerName === "openai" && !hasOpenAIKey) {
|
|
247
|
+
issues.push("OPENAI_API_KEY not set. Run: export OPENAI_API_KEY=sk-...");
|
|
248
|
+
}
|
|
249
|
+
if (ctx.providerName === "ollama") {
|
|
250
|
+
lines.push(` Ollama: configured (ensure 'ollama serve' is running)`);
|
|
251
|
+
}
|
|
252
|
+
const ctxWindow = getContextWindow(ctx.model);
|
|
253
|
+
const totalTokens = estimateMessageTokens(ctx.messages);
|
|
254
|
+
const usage = ctxWindow > 0 ? Math.round((totalTokens / ctxWindow) * 100) : 0;
|
|
255
|
+
lines.push(` Context: ~${totalTokens.toLocaleString()} / ${ctxWindow.toLocaleString()} tokens (${usage}%)`);
|
|
256
|
+
if (usage > 80)
|
|
257
|
+
issues.push(`Context ${usage}% full. Consider /compact to free space.`);
|
|
258
|
+
lines.push("");
|
|
259
|
+
if (isGitRepo()) {
|
|
260
|
+
lines.push(` Git: ✓ (branch: ${gitBranch()})`);
|
|
261
|
+
if (isInMergeOrRebase()) {
|
|
262
|
+
lines.push(` ⚠ Merge/rebase in progress`);
|
|
263
|
+
issues.push("Git merge/rebase in progress. Resolve before making changes.");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
lines.push(` Git: ✗ (not a git repo)`);
|
|
268
|
+
}
|
|
269
|
+
const mcp = connectedMcpServers();
|
|
270
|
+
lines.push(` MCP servers: ${mcp.length > 0 ? mcp.join(", ") : "none"}`);
|
|
271
|
+
const cfg = readOhConfig();
|
|
272
|
+
lines.push(` Config: ${cfg ? ".oh/config.yaml ✓" : "not found"}`);
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push(` Session: ${ctx.sessionId}`);
|
|
275
|
+
lines.push(` Messages: ${ctx.messages.length}`);
|
|
276
|
+
lines.push(` Cost: $${ctx.totalCost.toFixed(4)}`);
|
|
277
|
+
try {
|
|
278
|
+
const ohDir = join(homedir(), ".oh");
|
|
279
|
+
if (existsSync(ohDir)) {
|
|
280
|
+
const sessionsDir = join(ohDir, "sessions");
|
|
281
|
+
const sessCount = existsSync(sessionsDir)
|
|
282
|
+
? readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).length
|
|
283
|
+
: 0;
|
|
284
|
+
lines.push(` Sessions: ${sessCount} saved`);
|
|
285
|
+
if (sessCount > 80)
|
|
286
|
+
issues.push(`${sessCount} saved sessions. Consider cleaning old ones.`);
|
|
287
|
+
const memDir = join(ohDir, "memory");
|
|
288
|
+
const memCount = existsSync(memDir) ? readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
289
|
+
lines.push(` Memories: ${memCount} global`);
|
|
290
|
+
const cronDir = join(ohDir, "crons");
|
|
291
|
+
const cronCount = existsSync(cronDir) ? readdirSync(cronDir).filter((f) => f.endsWith(".json")).length : 0;
|
|
292
|
+
lines.push(` Cron tasks: ${cronCount}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
/* ignore */
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const projMemDir = join(".oh", "memory");
|
|
300
|
+
const projMemCount = existsSync(projMemDir) ? readdirSync(projMemDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
301
|
+
if (projMemCount > 0)
|
|
302
|
+
lines.push(` Project mems: ${projMemCount}`);
|
|
303
|
+
const skillsDir = join(".oh", "skills");
|
|
304
|
+
const skillCount = existsSync(skillsDir) ? readdirSync(skillsDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
305
|
+
if (skillCount > 0)
|
|
306
|
+
lines.push(` Skills: ${skillCount}`);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
/* ignore */
|
|
310
|
+
}
|
|
311
|
+
const globalCfg = existsSync(join(homedir(), ".oh", "config.yaml"));
|
|
312
|
+
lines.push(` Global config: ${globalCfg ? "~/.oh/config.yaml ✓" : "not set (optional)"}`);
|
|
313
|
+
try {
|
|
314
|
+
const { getVerificationConfig } = require("../harness/verification.js");
|
|
315
|
+
const vCfg = getVerificationConfig();
|
|
316
|
+
if (vCfg?.enabled) {
|
|
317
|
+
lines.push(` Verification: ✓ (${vCfg.rules.length} rules, mode: ${vCfg.mode})`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
lines.push(` Verification: off (no rules detected)`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* ignore */
|
|
325
|
+
}
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push(` Tools: ${ctx.messages.length > 0 ? "ready" : "loaded"}`);
|
|
328
|
+
lines.push(` Node.js: ${process.version}`);
|
|
329
|
+
const [major] = process.version.slice(1).split(".").map(Number);
|
|
330
|
+
if (major && major < 18)
|
|
331
|
+
issues.push(`Node.js ${process.version} is below minimum (18+). Upgrade Node.js.`);
|
|
332
|
+
if (issues.length > 0) {
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push("─── Issues Found ───");
|
|
335
|
+
for (const issue of issues) {
|
|
336
|
+
lines.push(` ⚠ ${issue}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
lines.push("");
|
|
341
|
+
lines.push(" ✓ No issues found");
|
|
342
|
+
}
|
|
343
|
+
return { output: lines.join("\n"), handled: true };
|
|
344
|
+
});
|
|
345
|
+
register("context", "Show context window usage breakdown", (_args, ctx) => {
|
|
346
|
+
const ctxWindow = getContextWindow(ctx.model);
|
|
347
|
+
let userTokens = 0, assistantTokens = 0, toolTokens = 0, systemTokens = 0;
|
|
348
|
+
for (const msg of ctx.messages) {
|
|
349
|
+
const tokens = Math.ceil((msg.content?.length ?? 0) / 4);
|
|
350
|
+
switch (msg.role) {
|
|
351
|
+
case "user":
|
|
352
|
+
userTokens += tokens;
|
|
353
|
+
break;
|
|
354
|
+
case "assistant":
|
|
355
|
+
assistantTokens += tokens;
|
|
356
|
+
break;
|
|
357
|
+
case "tool":
|
|
358
|
+
toolTokens += tokens;
|
|
359
|
+
break;
|
|
360
|
+
case "system":
|
|
361
|
+
systemTokens += tokens;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const totalTokens = userTokens + assistantTokens + toolTokens + systemTokens;
|
|
366
|
+
const freeTokens = ctxWindow - totalTokens;
|
|
367
|
+
const usage = totalTokens / ctxWindow;
|
|
368
|
+
const barWidth = 30;
|
|
369
|
+
const filled = Math.round(usage * barWidth);
|
|
370
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
371
|
+
const pct = (n) => `${((n / ctxWindow) * 100).toFixed(1)}%`;
|
|
372
|
+
const pad = (s, n) => s.padEnd(n);
|
|
373
|
+
const lines = [
|
|
374
|
+
`Context Window (${ctxWindow.toLocaleString()} tokens):`,
|
|
375
|
+
"",
|
|
376
|
+
` ${pad("User messages:", 20)} ${userTokens.toLocaleString().padStart(8)} tokens (${pct(userTokens)})`,
|
|
377
|
+
` ${pad("Assistant:", 20)} ${assistantTokens.toLocaleString().padStart(8)} tokens (${pct(assistantTokens)})`,
|
|
378
|
+
` ${pad("Tool results:", 20)} ${toolTokens.toLocaleString().padStart(8)} tokens (${pct(toolTokens)})`,
|
|
379
|
+
` ${pad("System/info:", 20)} ${systemTokens.toLocaleString().padStart(8)} tokens (${pct(systemTokens)})`,
|
|
380
|
+
"",
|
|
381
|
+
` ${pad("Total used:", 20)} ${totalTokens.toLocaleString().padStart(8)} tokens (${pct(totalTokens)})`,
|
|
382
|
+
` ${pad("Free:", 20)} ${freeTokens.toLocaleString().padStart(8)} tokens (${pct(freeTokens)})`,
|
|
383
|
+
"",
|
|
384
|
+
` ${bar} ${Math.round(usage * 100)}%`,
|
|
385
|
+
"",
|
|
386
|
+
` Messages: ${ctx.messages.length} | Compress at: ${Math.round(ctxWindow * 0.8).toLocaleString()} (80%)`,
|
|
387
|
+
];
|
|
388
|
+
return { output: lines.join("\n"), handled: true };
|
|
389
|
+
});
|
|
390
|
+
register("mcp", "Show MCP server status", () => {
|
|
391
|
+
const mcp = connectedMcpServers();
|
|
392
|
+
if (mcp.length === 0) {
|
|
393
|
+
return {
|
|
394
|
+
output: "No MCP servers connected.\nConfigure in .oh/config.yaml under mcpServers.\nRun /mcp-registry to browse available servers.",
|
|
395
|
+
handled: true,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const lines = [`MCP Servers (${mcp.length} connected):\n`];
|
|
399
|
+
for (const name of mcp) {
|
|
400
|
+
lines.push(` ✓ ${name}`);
|
|
401
|
+
}
|
|
402
|
+
lines.push("\nRun /mcp-registry to browse and add more servers.");
|
|
403
|
+
return { output: lines.join("\n"), handled: true };
|
|
404
|
+
});
|
|
405
|
+
register("mcp-registry", "Browse and add MCP servers from the curated registry", (args) => {
|
|
406
|
+
const { searchRegistry, formatRegistry, generateConfigBlock, MCP_REGISTRY } = require("../mcp/registry.js");
|
|
407
|
+
const query = args.trim();
|
|
408
|
+
if (!query) {
|
|
409
|
+
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`;
|
|
410
|
+
return { output, handled: true };
|
|
411
|
+
}
|
|
412
|
+
const results = searchRegistry(query);
|
|
413
|
+
if (results.length === 0) {
|
|
414
|
+
return { output: `No MCP servers found matching "${query}".`, handled: true };
|
|
415
|
+
}
|
|
416
|
+
if (results.length === 1) {
|
|
417
|
+
const entry = results[0];
|
|
418
|
+
const config = generateConfigBlock(entry);
|
|
419
|
+
const envNote = entry.envVars?.length
|
|
420
|
+
? `\n\nRequired environment variables:\n${entry.envVars.map((v) => ` - ${v}`).join("\n")}`
|
|
421
|
+
: "";
|
|
422
|
+
return {
|
|
423
|
+
output: `${entry.name} — ${entry.description}\nPackage: ${entry.package}\nRisk: ${entry.riskLevel ?? "medium"}${envNote}\n\nAdd to .oh/config.yaml under mcpServers:\n\n${config}`,
|
|
424
|
+
handled: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return { output: `Found ${results.length} servers:\n\n${formatRegistry(results)}`, handled: true };
|
|
428
|
+
});
|
|
429
|
+
register("init", "Initialize project with .oh/ config", () => {
|
|
430
|
+
const ohDir = join(process.cwd(), ".oh");
|
|
431
|
+
if (existsSync(ohDir)) {
|
|
432
|
+
return { output: ".oh/ directory already exists. Project is already initialized.", handled: true };
|
|
433
|
+
}
|
|
434
|
+
mkdirSync(ohDir, { recursive: true });
|
|
435
|
+
const rulesPath = join(ohDir, "RULES.md");
|
|
436
|
+
if (!existsSync(rulesPath)) {
|
|
437
|
+
writeFileSync(rulesPath, `# Project Rules
|
|
438
|
+
|
|
439
|
+
<!-- Add project-specific instructions here. These are loaded into every session. -->
|
|
440
|
+
<!-- Examples: coding conventions, testing requirements, deployment guidelines. -->
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
const configPath = join(ohDir, "config.yaml");
|
|
444
|
+
if (!existsSync(configPath)) {
|
|
445
|
+
writeFileSync(configPath, `# OpenHarness project config
|
|
446
|
+
# provider: ollama
|
|
447
|
+
# model: llama3
|
|
448
|
+
# permissionMode: ask
|
|
449
|
+
`);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
output: `Initialized .oh/ with:\n .oh/RULES.md — project rules\n .oh/config.yaml — project config\n\nEdit these files to customize your project.`,
|
|
453
|
+
handled: true,
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
register("bug", "Report a bug or issue", () => {
|
|
457
|
+
return {
|
|
458
|
+
output: "Report issues at: https://github.com/zhijiewong/openharness/issues\n\nInclude:\n - OpenHarness version (oh --version)\n - Steps to reproduce\n - Expected vs actual behavior\n - OS and Node.js version",
|
|
459
|
+
handled: true,
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
register("feedback", "Send feedback or feature request", () => {
|
|
463
|
+
return {
|
|
464
|
+
output: "Share feedback at: https://github.com/zhijiewong/openharness/issues\n\nUse the 'enhancement' label for feature requests.\nUse the 'bug' label for bug reports.",
|
|
465
|
+
handled: true,
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
register("upgrade", "Check for updates", () => {
|
|
469
|
+
let current = "unknown";
|
|
470
|
+
try {
|
|
471
|
+
const pkgPath = join(process.cwd(), "package.json");
|
|
472
|
+
if (existsSync(pkgPath)) {
|
|
473
|
+
current = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? current;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
/* ignore */
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
output: `Current version: v${current}\n\nTo upgrade:\n npm update -g @zhijiewang/openharness\n\nOr check: https://github.com/zhijiewong/openharness/releases`,
|
|
481
|
+
handled: true,
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
register("token-count", "Count tokens in a message or file", (args, ctx) => {
|
|
485
|
+
const text = args.trim();
|
|
486
|
+
if (!text) {
|
|
487
|
+
const total = estimateMessageTokens(ctx.messages);
|
|
488
|
+
return { output: `Conversation tokens: ~${total.toLocaleString()} (estimated)`, handled: true };
|
|
489
|
+
}
|
|
490
|
+
if (existsSync(text)) {
|
|
491
|
+
try {
|
|
492
|
+
const content = readFileSync(text, "utf-8");
|
|
493
|
+
const tokens = Math.ceil(content.length / 4);
|
|
494
|
+
return {
|
|
495
|
+
output: `File: ${text}\nCharacters: ${content.length.toLocaleString()}\nEstimated tokens: ~${tokens.toLocaleString()}`,
|
|
496
|
+
handled: true,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return { output: `Could not read file: ${text}`, handled: true };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const tokens = Math.ceil(text.length / 4);
|
|
504
|
+
return { output: `Text: ${text.length} chars → ~${tokens} tokens (estimated)`, handled: true };
|
|
505
|
+
});
|
|
506
|
+
register("version", "Show version number", () => {
|
|
507
|
+
let version = "unknown";
|
|
508
|
+
try {
|
|
509
|
+
const pkgPath = join(process.cwd(), "package.json");
|
|
510
|
+
if (existsSync(pkgPath)) {
|
|
511
|
+
version = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? version;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
/* ignore */
|
|
516
|
+
}
|
|
517
|
+
return { output: `openHarness v${version}`, handled: true };
|
|
518
|
+
});
|
|
519
|
+
register("api-credits", "Check API credit balance", (_args, ctx) => {
|
|
520
|
+
const envHint = ctx.providerName === "anthropic"
|
|
521
|
+
? "ANTHROPIC_API_KEY"
|
|
522
|
+
: ctx.providerName === "openai"
|
|
523
|
+
? "OPENAI_API_KEY"
|
|
524
|
+
: `${ctx.providerName.toUpperCase()}_API_KEY`;
|
|
525
|
+
const lines = [
|
|
526
|
+
"API credit balance is not available via local CLI.",
|
|
527
|
+
"",
|
|
528
|
+
`Provider: ${ctx.providerName}`,
|
|
529
|
+
`Check your balance at your provider's dashboard.`,
|
|
530
|
+
"",
|
|
531
|
+
`Tip: Ensure ${envHint} is set in your environment.`,
|
|
532
|
+
`Session cost so far: $${ctx.totalCost.toFixed(4)}`,
|
|
533
|
+
];
|
|
534
|
+
return { output: lines.join("\n"), handled: true };
|
|
535
|
+
});
|
|
536
|
+
register("whoami", "Show current user and provider info", (_args, ctx) => {
|
|
537
|
+
const lines = [
|
|
538
|
+
`Provider: ${ctx.providerName}`,
|
|
539
|
+
`Model: ${ctx.model}`,
|
|
540
|
+
`Permission: ${ctx.permissionMode}`,
|
|
541
|
+
`Session: ${ctx.sessionId}`,
|
|
542
|
+
`Node.js: ${process.version}`,
|
|
543
|
+
`CWD: ${process.cwd()}`,
|
|
544
|
+
];
|
|
545
|
+
return { output: lines.join("\n"), handled: true };
|
|
546
|
+
});
|
|
547
|
+
register("project", "Show detected project info", () => {
|
|
548
|
+
const cwd = process.cwd();
|
|
549
|
+
const pkgPath = join(cwd, "package.json");
|
|
550
|
+
const lines = [`Project directory: ${cwd}`];
|
|
551
|
+
if (existsSync(pkgPath)) {
|
|
552
|
+
try {
|
|
553
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
554
|
+
lines.push(` Name: ${pkg.name ?? "unknown"}`);
|
|
555
|
+
lines.push(` Version: ${pkg.version ?? "unknown"}`);
|
|
556
|
+
lines.push(` Description: ${pkg.description ?? "none"}`);
|
|
557
|
+
if (pkg.type)
|
|
558
|
+
lines.push(` Type: ${pkg.type}`);
|
|
559
|
+
const deps = Object.keys(pkg.dependencies ?? {}).length;
|
|
560
|
+
const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
|
|
561
|
+
lines.push(` Dependencies: ${deps} prod, ${devDeps} dev`);
|
|
562
|
+
// Detect framework
|
|
563
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
564
|
+
const frameworks = [];
|
|
565
|
+
if (allDeps.react)
|
|
566
|
+
frameworks.push("React");
|
|
567
|
+
if (allDeps.next)
|
|
568
|
+
frameworks.push("Next.js");
|
|
569
|
+
if (allDeps.vue)
|
|
570
|
+
frameworks.push("Vue");
|
|
571
|
+
if (allDeps.express)
|
|
572
|
+
frameworks.push("Express");
|
|
573
|
+
if (allDeps.fastify)
|
|
574
|
+
frameworks.push("Fastify");
|
|
575
|
+
if (allDeps.typescript)
|
|
576
|
+
frameworks.push("TypeScript");
|
|
577
|
+
if (frameworks.length > 0)
|
|
578
|
+
lines.push(` Detected: ${frameworks.join(", ")}`);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
lines.push(" Could not parse package.json");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
lines.push(" No package.json found");
|
|
586
|
+
}
|
|
587
|
+
if (isGitRepo()) {
|
|
588
|
+
lines.push(` Git branch: ${gitBranch()}`);
|
|
589
|
+
}
|
|
590
|
+
return { output: lines.join("\n"), handled: true };
|
|
591
|
+
});
|
|
592
|
+
register("stats", "Show session statistics", (_args, ctx) => {
|
|
593
|
+
let userMsgs = 0, assistantMsgs = 0, toolMsgs = 0, systemMsgs = 0;
|
|
594
|
+
let toolCalls = 0;
|
|
595
|
+
for (const msg of ctx.messages) {
|
|
596
|
+
switch (msg.role) {
|
|
597
|
+
case "user":
|
|
598
|
+
userMsgs++;
|
|
599
|
+
break;
|
|
600
|
+
case "assistant":
|
|
601
|
+
assistantMsgs++;
|
|
602
|
+
break;
|
|
603
|
+
case "tool":
|
|
604
|
+
toolMsgs++;
|
|
605
|
+
break;
|
|
606
|
+
case "system":
|
|
607
|
+
systemMsgs++;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
if (msg.toolCalls)
|
|
611
|
+
toolCalls += msg.toolCalls.length;
|
|
612
|
+
}
|
|
613
|
+
const lines = [
|
|
614
|
+
"Session Statistics:",
|
|
615
|
+
"",
|
|
616
|
+
` Messages: ${ctx.messages.length} total`,
|
|
617
|
+
` User: ${userMsgs}`,
|
|
618
|
+
` Assistant: ${assistantMsgs}`,
|
|
619
|
+
` Tool: ${toolMsgs}`,
|
|
620
|
+
` System: ${systemMsgs}`,
|
|
621
|
+
"",
|
|
622
|
+
` Tool calls: ${toolCalls}`,
|
|
623
|
+
"",
|
|
624
|
+
` Input tokens: ${ctx.totalInputTokens.toLocaleString()}`,
|
|
625
|
+
` Output tokens: ${ctx.totalOutputTokens.toLocaleString()}`,
|
|
626
|
+
` Total cost: $${ctx.totalCost.toFixed(4)}`,
|
|
627
|
+
"",
|
|
628
|
+
` Model: ${ctx.model}`,
|
|
629
|
+
` Session ID: ${ctx.sessionId}`,
|
|
630
|
+
];
|
|
631
|
+
return { output: lines.join("\n"), handled: true };
|
|
632
|
+
});
|
|
633
|
+
register("tools", "List available tools", (_args, ctx) => {
|
|
634
|
+
const toolNames = new Set();
|
|
635
|
+
for (const msg of ctx.messages) {
|
|
636
|
+
if (msg.toolCalls) {
|
|
637
|
+
for (const tc of msg.toolCalls) {
|
|
638
|
+
if (tc.toolName)
|
|
639
|
+
toolNames.add(tc.toolName);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const mcp = connectedMcpServers();
|
|
644
|
+
const lines = ["Available Tools:"];
|
|
645
|
+
lines.push("");
|
|
646
|
+
lines.push(" Built-in: Read, Write, Edit, Bash, Glob, Grep, Agent");
|
|
647
|
+
if (mcp.length > 0) {
|
|
648
|
+
lines.push(` MCP: ${mcp.join(", ")}`);
|
|
649
|
+
}
|
|
650
|
+
if (toolNames.size > 0) {
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push(` Used this session: ${[...toolNames].join(", ")}`);
|
|
653
|
+
}
|
|
654
|
+
return { output: lines.join("\n"), handled: true };
|
|
655
|
+
});
|
|
656
|
+
register("benchmark", "Run SWE-bench benchmark suite", (args) => {
|
|
657
|
+
const task = args.trim();
|
|
658
|
+
if (!task) {
|
|
659
|
+
return {
|
|
660
|
+
output: "Usage: /benchmark <task-id or 'list'>\n\nExamples:\n /benchmark list List available tasks\n /benchmark django__django-1234 Run a specific task\n\nSee BENCHMARKS.md for results and methodology.",
|
|
661
|
+
handled: true,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
output: `[benchmark] ${task}`,
|
|
666
|
+
handled: false,
|
|
667
|
+
prependToPrompt: `You are running a SWE-bench benchmark task. Task: ${task}\n\nFollow the standard benchmark protocol: read the issue, understand the codebase, implement the fix, and verify with tests.`,
|
|
668
|
+
};
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=info.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session commands — /clear, /compact, /export, /history, /browse, /resume, /fork, /pin, /unpin
|
|
3
|
+
*/
|
|
4
|
+
import type { CommandHandler } from "./types.js";
|
|
5
|
+
export declare function registerSessionCommands(register: (name: string, description: string, handler: CommandHandler) => void): void;
|
|
6
|
+
//# sourceMappingURL=session.d.ts.map
|