@zhijiewang/openharness 2.4.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -2
  2. package/dist/Tool.d.ts +2 -0
  3. package/dist/commands/ai.d.ts +6 -0
  4. package/dist/commands/ai.js +244 -0
  5. package/dist/commands/git.d.ts +6 -0
  6. package/dist/commands/git.js +167 -0
  7. package/dist/commands/index.d.ts +10 -31
  8. package/dist/commands/index.js +22 -1052
  9. package/dist/commands/info.d.ts +8 -0
  10. package/dist/commands/info.js +671 -0
  11. package/dist/commands/session.d.ts +6 -0
  12. package/dist/commands/session.js +214 -0
  13. package/dist/commands/settings.d.ts +6 -0
  14. package/dist/commands/settings.js +187 -0
  15. package/dist/commands/skills.d.ts +6 -0
  16. package/dist/commands/skills.js +117 -0
  17. package/dist/commands/types.d.ts +36 -0
  18. package/dist/commands/types.js +5 -0
  19. package/dist/components/InitWizard.js +61 -61
  20. package/dist/harness/config.d.ts +2 -0
  21. package/dist/harness/hooks.js +9 -6
  22. package/dist/harness/memory.js +28 -1
  23. package/dist/harness/plugins.d.ts +2 -0
  24. package/dist/harness/plugins.js +44 -11
  25. package/dist/harness/session-db.js +3 -1
  26. package/dist/harness/skill-registry.d.ts +21 -0
  27. package/dist/harness/skill-registry.js +35 -0
  28. package/dist/lsp/client.js +2 -1
  29. package/dist/main.js +10 -2
  30. package/dist/mcp/client.js +2 -1
  31. package/dist/mcp/server-mode.d.ts +10 -0
  32. package/dist/mcp/server-mode.js +17 -0
  33. package/dist/providers/anthropic.js +7 -8
  34. package/dist/providers/fallback.js +2 -3
  35. package/dist/providers/openai.js +3 -2
  36. package/dist/query/index.js +30 -6
  37. package/dist/query/tools.js +11 -0
  38. package/dist/query/types.d.ts +4 -0
  39. package/dist/renderer/layout-sections.d.ts +56 -0
  40. package/dist/renderer/layout-sections.js +462 -0
  41. package/dist/renderer/layout.d.ts +4 -2
  42. package/dist/renderer/layout.js +25 -500
  43. package/dist/repl.js +3 -1
  44. package/dist/services/SkillExtractor.js +2 -0
  45. package/dist/tools/SkillTool/index.js +26 -2
  46. package/dist/tools/TodoWriteTool/index.d.ts +37 -0
  47. package/dist/tools/TodoWriteTool/index.js +78 -0
  48. package/dist/tools.js +2 -0
  49. package/package.json +1 -1
@@ -86,12 +86,71 @@ export default function InitWizard({ onDone }) {
86
86
  const [testStatus, setTestStatus] = useState("testing");
87
87
  const [testError, setTestError] = useState("");
88
88
  const [permIdx, setPermIdx] = useState(0);
89
- const [hatchGotchi, setHatchGotchi] = useState(false);
89
+ const [_hatchGotchi, setHatchGotchi] = useState(false);
90
90
  const [showSetup, setShowSetup] = useState(false);
91
91
  const [suggestedMcp, setSuggestedMcp] = useState([]);
92
92
  const [selectedMcp, setSelectedMcp] = useState(new Set());
93
93
  const [mcpIdx, setMcpIdx] = useState(0);
94
94
  const provider = PROVIDERS[providerIdx];
95
+ // ── Connection test ──
96
+ const runTest = useCallback(async (prov, key, url) => {
97
+ setTestStatus("testing");
98
+ try {
99
+ const { createProviderInstance } = await import("../providers/index.js");
100
+ const p = createProviderInstance(prov.key, {
101
+ name: prov.key,
102
+ apiKey: key || process.env[`${prov.key.toUpperCase()}_API_KEY`],
103
+ baseUrl: url || prov.defaultBaseUrl,
104
+ defaultModel: prov.defaultModel,
105
+ });
106
+ const fetched = "fetchModels" in p && typeof p.fetchModels === "function"
107
+ ? await p.fetchModels()
108
+ : p.listModels();
109
+ const modelNames = fetched.map((m) => m.id);
110
+ setAvailableModels(modelNames.length > 0 ? modelNames : [prov.defaultModel]);
111
+ setTestStatus("ok");
112
+ setTimeout(() => setStep("model"), 600);
113
+ }
114
+ catch (err) {
115
+ setTestStatus("fail");
116
+ setTestError(err instanceof Error ? err.message : String(err));
117
+ setAvailableModels([prov.defaultModel]);
118
+ setTimeout(() => setStep("model"), 800);
119
+ }
120
+ }, []);
121
+ // ── Write final config ──
122
+ const writeFinal = useCallback(() => {
123
+ const selectedModel = availableModels.length > 0 ? (availableModels[modelIdx] ?? model) : model;
124
+ // Build MCP server configs from selected registry entries
125
+ let mcpServers;
126
+ if (selectedMcp.size > 0) {
127
+ try {
128
+ const { MCP_REGISTRY } = require("../mcp/registry.js");
129
+ mcpServers = [...selectedMcp]
130
+ .map((name) => MCP_REGISTRY.find((e) => e.name === name))
131
+ .filter(Boolean)
132
+ .map((e) => ({
133
+ name: e.name,
134
+ command: "npx",
135
+ args: ["-y", e.package, ...(e.args ?? [])],
136
+ ...(e.envVars?.length ? { env: Object.fromEntries(e.envVars.map((v) => [v, `YOUR_${v}`])) } : {}),
137
+ }));
138
+ }
139
+ catch {
140
+ /* ignore */
141
+ }
142
+ }
143
+ writeOhConfig({
144
+ provider: provider.key,
145
+ model: selectedModel || provider.defaultModel,
146
+ permissionMode: PERMISSION_MODES[permIdx].key,
147
+ ...(apiKey ? { apiKey } : {}),
148
+ ...(baseUrl ? { baseUrl } : {}),
149
+ ...(mcpServers?.length ? { mcpServers } : {}),
150
+ });
151
+ setStep("done");
152
+ setTimeout(() => onDone?.(), 1500);
153
+ }, [provider, model, availableModels, modelIdx, permIdx, apiKey, baseUrl, selectedMcp, onDone]);
95
154
  // ── Keyboard navigation ──
96
155
  useInput(useCallback((input, key) => {
97
156
  if (step === "provider") {
@@ -175,66 +234,7 @@ export default function InitWizard({ onDone }) {
175
234
  if (input === "n" || input === "N")
176
235
  writeFinal();
177
236
  }
178
- }, [step, providerIdx, provider, modelIdx, availableModels, model]));
179
- // ── Connection test ──
180
- const runTest = async (prov, key, url) => {
181
- setTestStatus("testing");
182
- try {
183
- const { createProviderInstance } = await import("../providers/index.js");
184
- const p = createProviderInstance(prov.key, {
185
- name: prov.key,
186
- apiKey: key || process.env[`${prov.key.toUpperCase()}_API_KEY`],
187
- baseUrl: url || prov.defaultBaseUrl,
188
- defaultModel: prov.defaultModel,
189
- });
190
- const fetched = "fetchModels" in p && typeof p.fetchModels === "function"
191
- ? await p.fetchModels()
192
- : p.listModels();
193
- const modelNames = fetched.map((m) => m.id);
194
- setAvailableModels(modelNames.length > 0 ? modelNames : [prov.defaultModel]);
195
- setTestStatus("ok");
196
- setTimeout(() => setStep("model"), 600);
197
- }
198
- catch (err) {
199
- setTestStatus("fail");
200
- setTestError(err instanceof Error ? err.message : String(err));
201
- setAvailableModels([prov.defaultModel]);
202
- setTimeout(() => setStep("model"), 800);
203
- }
204
- };
205
- // ── Write final config ──
206
- const writeFinal = useCallback(() => {
207
- const selectedModel = availableModels.length > 0 ? (availableModels[modelIdx] ?? model) : model;
208
- // Build MCP server configs from selected registry entries
209
- let mcpServers;
210
- if (selectedMcp.size > 0) {
211
- try {
212
- const { MCP_REGISTRY } = require("../mcp/registry.js");
213
- mcpServers = [...selectedMcp]
214
- .map((name) => MCP_REGISTRY.find((e) => e.name === name))
215
- .filter(Boolean)
216
- .map((e) => ({
217
- name: e.name,
218
- command: "npx",
219
- args: ["-y", e.package, ...(e.args ?? [])],
220
- ...(e.envVars?.length ? { env: Object.fromEntries(e.envVars.map((v) => [v, `YOUR_${v}`])) } : {}),
221
- }));
222
- }
223
- catch {
224
- /* ignore */
225
- }
226
- }
227
- writeOhConfig({
228
- provider: provider.key,
229
- model: selectedModel || provider.defaultModel,
230
- permissionMode: PERMISSION_MODES[permIdx].key,
231
- ...(apiKey ? { apiKey } : {}),
232
- ...(baseUrl ? { baseUrl } : {}),
233
- ...(mcpServers?.length ? { mcpServers } : {}),
234
- });
235
- setStep("done");
236
- setTimeout(() => onDone?.(), 1500);
237
- }, [provider, model, availableModels, modelIdx, permIdx, apiKey, baseUrl, selectedMcp]);
237
+ }, [step, provider, modelIdx, availableModels, model, suggestedMcp, mcpIdx, runTest, writeFinal]));
238
238
  // ── Render ──
239
239
  if (showSetup) {
240
240
  return (_jsx(CybergotchiSetup, { onComplete: () => {
@@ -75,6 +75,8 @@ export type OhConfig = {
75
75
  apiKey?: string;
76
76
  baseUrl?: string;
77
77
  }>;
78
+ /** Auto-commit after each file-modifying tool execution */
79
+ gitCommitPerTool?: boolean;
78
80
  /** Effort level for LLM reasoning depth */
79
81
  effortLevel?: "low" | "medium" | "high" | "max";
80
82
  /** Opt-in telemetry (default: off) */
@@ -117,13 +117,16 @@ async function runHttpHook(url, event, ctx, timeoutMs = 10_000) {
117
117
  return false;
118
118
  }
119
119
  }
120
- /** Run a prompt hook. Uses LLM to make a yes/no decision. */
120
+ /**
121
+ * Run a prompt hook. Uses LLM to make a yes/no decision.
122
+ *
123
+ * Currently a stub — prompt hooks always allow because the hook system
124
+ * runs outside the query loop and has no access to a Provider instance.
125
+ * Full implementation requires passing a Provider via HookContext so the
126
+ * hook can call provider.complete() with the prompt text.
127
+ */
121
128
  async function runPromptHook(_promptText, _ctx) {
122
- // Prompt hooks require a provider — skip if not available
123
- // This is a lightweight check; full LLM call would need provider injection
124
- // For now, prompt hooks evaluate the prompt text as a simple template
125
- // TODO: inject provider for full LLM-based prompt hooks
126
- return true; // Default allow if no LLM available
129
+ return true;
127
130
  }
128
131
  // ── Hook Execution ──
129
132
  /** Execute a single hook definition. Returns true if allowed. */
@@ -283,7 +283,34 @@ export function consolidateMemories() {
283
283
  // Refresh MEMORY.md index after pruning
284
284
  updateMemoryIndex(PROJECT_MEMORY_DIR);
285
285
  updateMemoryIndex(GLOBAL_MEMORY_DIR);
286
- return { total: all.length, pruned: prunedCount, decayed: decayedCount };
286
+ // Skill decay: prune auto-extracted skills unused for 60 days
287
+ let prunedSkills = 0;
288
+ try {
289
+ const skillsAutoDir = join(".oh", "skills", "auto");
290
+ if (existsSync(skillsAutoDir)) {
291
+ const SKILL_DECAY_MS = 60 * 24 * 60 * 60 * 1000; // 60 days
292
+ for (const file of readdirSync(skillsAutoDir).filter((f) => f.endsWith(".md"))) {
293
+ try {
294
+ const raw = readFileSync(join(skillsAutoDir, file), "utf-8");
295
+ const usedMatch = raw.match(/^timesUsed:\s*(\d+)$/m);
296
+ const extractedMatch = raw.match(/^extractedAt:\s*(\d+)$/m);
297
+ const timesUsed = usedMatch ? parseInt(usedMatch[1], 10) : 0;
298
+ const extractedAt = extractedMatch ? parseInt(extractedMatch[1], 10) : Date.now();
299
+ if (timesUsed < 2 && Date.now() - extractedAt > SKILL_DECAY_MS) {
300
+ unlinkSync(join(skillsAutoDir, file));
301
+ prunedSkills++;
302
+ }
303
+ }
304
+ catch {
305
+ /* skip unreadable skill files */
306
+ }
307
+ }
308
+ }
309
+ }
310
+ catch {
311
+ /* skill pruning is optional */
312
+ }
313
+ return { total: all.length, pruned: prunedCount + prunedSkills, decayed: decayedCount };
287
314
  }
288
315
  // ── User Profile ──
289
316
  const USER_PROFILE_FILE = "USER.md";
@@ -19,6 +19,8 @@ export type SkillMetadata = {
19
19
  content: string;
20
20
  filePath: string;
21
21
  source: "project" | "global" | "plugin";
22
+ /** When false, skill is hidden from system prompt until explicitly invoked */
23
+ invokeModel: boolean;
22
24
  };
23
25
  export type PluginManifest = {
24
26
  name: string;
@@ -10,9 +10,9 @@
10
10
  * 2. ~/.oh/skills/ (global)
11
11
  * 3. node_modules packages with "openharness-plugin" keyword
12
12
  */
13
- import { existsSync, readdirSync, readFileSync } from "node:fs";
13
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
- import { basename, join } from "node:path";
15
+ import { join, relative } from "node:path";
16
16
  const PROJECT_SKILLS_DIR = join(".oh", "skills");
17
17
  const GLOBAL_SKILLS_DIR = join(homedir(), ".oh", "skills");
18
18
  /** Parse YAML frontmatter from a skill markdown file */
@@ -34,24 +34,54 @@ function parseSkillFrontmatter(content) {
34
34
  const toolsMatch = frontmatter.match(/^tools:\s*\[(.+)\]$/m);
35
35
  if (toolsMatch)
36
36
  result.tools = toolsMatch[1].split(",").map((t) => t.trim());
37
+ // Also parse allowedTools (used by built-in skills) and merge with tools
38
+ const allowedToolsMatch = frontmatter.match(/^allowedTools:\s*\[(.+)\]$/m);
39
+ if (allowedToolsMatch) {
40
+ const allowed = allowedToolsMatch[1].split(",").map((t) => t.trim());
41
+ result.tools = result.tools ? [...new Set([...result.tools, ...allowed])] : allowed;
42
+ }
37
43
  const argsMatch = frontmatter.match(/^args:\s*\[(.+)\]$/m);
38
44
  if (argsMatch)
39
45
  result.args = argsMatch[1].split(",").map((a) => a.trim());
46
+ // invokeModel: false OR disable-model-invocation: true → hidden from system prompt
47
+ if (frontmatter.match(/^invokeModel:\s*false$/m) || frontmatter.match(/^disable-model-invocation:\s*true$/m)) {
48
+ result.invokeModel = false;
49
+ }
40
50
  return result;
41
51
  }
42
- /** Load skills from a directory */
43
- function loadSkillsFromDir(dir, source) {
52
+ /** Recursively collect all .md files from a directory tree */
53
+ function walkMdFiles(dir) {
44
54
  if (!existsSync(dir))
45
55
  return [];
46
- return readdirSync(dir)
47
- .filter((f) => f.endsWith(".md"))
48
- .map((f) => {
49
- const filePath = join(dir, f);
56
+ const results = [];
57
+ for (const entry of readdirSync(dir)) {
58
+ const full = join(dir, entry);
59
+ try {
60
+ if (statSync(full).isDirectory()) {
61
+ results.push(...walkMdFiles(full));
62
+ }
63
+ else if (entry.endsWith(".md")) {
64
+ results.push(full);
65
+ }
66
+ }
67
+ catch {
68
+ /* skip unreadable */
69
+ }
70
+ }
71
+ return results;
72
+ }
73
+ /** Load skills from a directory (recursively walks subdirectories) */
74
+ function loadSkillsFromDir(dir, source) {
75
+ const files = walkMdFiles(dir);
76
+ return files
77
+ .map((filePath) => {
50
78
  try {
51
79
  const content = readFileSync(filePath, "utf-8");
52
80
  const meta = parseSkillFrontmatter(content);
81
+ // Derive name from relative path if not in frontmatter
82
+ const relName = relative(dir, filePath).replace(/\.md$/, "").replace(/\\/g, "/");
53
83
  return {
54
- name: meta.name || basename(f, ".md"),
84
+ name: meta.name || relName,
55
85
  description: meta.description || "",
56
86
  trigger: meta.trigger,
57
87
  tools: meta.tools,
@@ -59,6 +89,7 @@ function loadSkillsFromDir(dir, source) {
59
89
  content,
60
90
  filePath,
61
91
  source,
92
+ invokeModel: meta.invokeModel ?? true,
62
93
  };
63
94
  }
64
95
  catch {
@@ -172,9 +203,11 @@ export function discoverPlugins() {
172
203
  }
173
204
  /** Build a prompt listing available skills for the LLM */
174
205
  export function skillsToPrompt(skills) {
175
- if (skills.length === 0)
206
+ // Only include skills with invokeModel !== false (hidden skills excluded from prompt)
207
+ const visible = skills.filter((s) => s.invokeModel !== false);
208
+ if (visible.length === 0)
176
209
  return "";
177
- const lines = skills.map((s) => `- ${s.name}: ${s.description}${s.trigger ? ` (auto-trigger: "${s.trigger}")` : ""}`);
210
+ const lines = visible.map((s) => `- ${s.name}: ${s.description}${s.trigger ? ` (auto-trigger: "${s.trigger}")` : ""}`);
178
211
  return `# Available Skills\nUse the Skill tool to invoke these:\n${lines.join("\n")}`;
179
212
  }
180
213
  //# sourceMappingURL=plugins.js.map
@@ -158,7 +158,9 @@ export function closeGlobalSessionDb() {
158
158
  try {
159
159
  _singletonDb.close();
160
160
  }
161
- catch { /* ignore */ }
161
+ catch {
162
+ /* ignore */
163
+ }
162
164
  _singletonDb = null;
163
165
  }
164
166
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Skills Registry — search and install community skills from a remote registry.
3
+ */
4
+ export type RegistrySkill = {
5
+ name: string;
6
+ description: string;
7
+ author: string;
8
+ version: string;
9
+ source: string;
10
+ tags: string[];
11
+ };
12
+ export type Registry = {
13
+ skills: RegistrySkill[];
14
+ };
15
+ /** Fetch the registry from remote URL */
16
+ export declare function fetchRegistry(url?: string): Promise<Registry>;
17
+ /** Search registry by query (matches name, description, tags) */
18
+ export declare function searchRegistry(registry: Registry, query: string): RegistrySkill[];
19
+ /** Install a skill from the registry to ~/.oh/skills/ */
20
+ export declare function installSkill(skill: RegistrySkill): Promise<string>;
21
+ //# sourceMappingURL=skill-registry.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Skills Registry — search and install community skills from a remote registry.
3
+ */
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+ const DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/zhijiewong/openharness/main/data/registry.json";
8
+ const GLOBAL_SKILLS_DIR = join(homedir(), ".oh", "skills");
9
+ /** Fetch the registry from remote URL */
10
+ export async function fetchRegistry(url = DEFAULT_REGISTRY_URL) {
11
+ const response = await fetch(url);
12
+ if (!response.ok)
13
+ throw new Error(`Failed to fetch registry: ${response.status}`);
14
+ return (await response.json());
15
+ }
16
+ /** Search registry by query (matches name, description, tags) */
17
+ export function searchRegistry(registry, query) {
18
+ const q = query.toLowerCase();
19
+ return registry.skills.filter((s) => s.name.toLowerCase().includes(q) ||
20
+ s.description.toLowerCase().includes(q) ||
21
+ s.tags.some((t) => t.toLowerCase().includes(q)));
22
+ }
23
+ /** Install a skill from the registry to ~/.oh/skills/ */
24
+ export async function installSkill(skill) {
25
+ const response = await fetch(skill.source);
26
+ if (!response.ok)
27
+ throw new Error(`Failed to download skill: ${response.status}`);
28
+ const content = await response.text();
29
+ mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
30
+ const slug = skill.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
31
+ const filePath = join(GLOBAL_SKILLS_DIR, `${slug}.md`);
32
+ writeFileSync(filePath, content);
33
+ return filePath;
34
+ }
35
+ //# sourceMappingURL=skill-registry.js.map
@@ -11,6 +11,7 @@ export class LspClient {
11
11
  buffer = "";
12
12
  contentLength = -1;
13
13
  diagnostics = new Map();
14
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: set via Object.assign in static factory
14
15
  ready = false;
15
16
  constructor(proc) {
16
17
  this.proc = proc;
@@ -38,7 +39,7 @@ export class LspClient {
38
39
  this.buffer = this.buffer.slice(headerEnd + 4);
39
40
  continue;
40
41
  }
41
- this.contentLength = parseInt(match[1]);
42
+ this.contentLength = parseInt(match[1], 10);
42
43
  this.buffer = this.buffer.slice(headerEnd + 4);
43
44
  }
44
45
  if (this.buffer.length < this.contentLength)
package/dist/main.js CHANGED
@@ -17,10 +17,10 @@ import { Command, Option } from "commander";
17
17
  import { render } from "ink";
18
18
  import { readOhConfig } from "./harness/config.js";
19
19
  import { emitHook } from "./harness/hooks.js";
20
- import { detectProject, projectContextToPrompt } from "./harness/onboarding.js";
21
- import { createRulesFile, loadRules, loadRulesAsPrompt } from "./harness/rules.js";
22
20
  import { loadActiveMemories, memoriesToPrompt, userProfileToPrompt } from "./harness/memory.js";
21
+ import { detectProject, projectContextToPrompt } from "./harness/onboarding.js";
23
22
  import { discoverSkills, skillsToPrompt } from "./harness/plugins.js";
23
+ import { createRulesFile, loadRules, loadRulesAsPrompt } from "./harness/rules.js";
24
24
  import { listSessions } from "./harness/session.js";
25
25
  import { connectedMcpServers, disconnectMcpClients, getMcpInstructions, loadMcpTools } from "./mcp/loader.js";
26
26
  import { getAllTools } from "./tools.js";
@@ -735,6 +735,14 @@ program
735
735
  const server = new McpServer(tools, context);
736
736
  server.start();
737
737
  });
738
+ // ── mcp-server (alias for serve, standard MCP server mode) ──
739
+ program
740
+ .command("mcp-server")
741
+ .description("Start as MCP server (stdio JSON-RPC) — alias for serve")
742
+ .action(async () => {
743
+ const { startMcpServer } = await import("./mcp/server-mode.js");
744
+ await startMcpServer();
745
+ });
738
746
  // ── schedule ──
739
747
  program
740
748
  .command("schedule")
@@ -6,6 +6,7 @@ export class McpClient {
6
6
  proc;
7
7
  nextId = 1;
8
8
  pending = new Map();
9
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: set via Object.assign in static factory
9
10
  ready = false;
10
11
  dead = false;
11
12
  cfg;
@@ -139,7 +140,7 @@ export class McpClient {
139
140
  const id = this.nextId++;
140
141
  const req = { jsonrpc: "2.0", id, method, params };
141
142
  this.pending.set(id, { resolve, reject });
142
- this.proc.stdin.write(JSON.stringify(req) + "\n");
143
+ this.proc.stdin.write(`${JSON.stringify(req)}\n`);
143
144
  });
144
145
  }
145
146
  disconnect() {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * MCP Server Mode — expose openHarness tools as an MCP server over stdio.
3
+ * Run: oh mcp-server
4
+ *
5
+ * Thin entry-point that wires getAllTools() into the McpServer class.
6
+ * Each message is a JSON-RPC 2.0 object on a single newline-delimited line.
7
+ * stdin → requests, stdout → responses, stderr → logs.
8
+ */
9
+ export declare function startMcpServer(): Promise<void>;
10
+ //# sourceMappingURL=server-mode.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * MCP Server Mode — expose openHarness tools as an MCP server over stdio.
3
+ * Run: oh mcp-server
4
+ *
5
+ * Thin entry-point that wires getAllTools() into the McpServer class.
6
+ * Each message is a JSON-RPC 2.0 object on a single newline-delimited line.
7
+ * stdin → requests, stdout → responses, stderr → logs.
8
+ */
9
+ import { getAllTools } from "../tools.js";
10
+ import { McpServer } from "./server.js";
11
+ export async function startMcpServer() {
12
+ const tools = getAllTools();
13
+ const context = { workingDir: process.cwd() };
14
+ const server = new McpServer(tools, context);
15
+ server.start();
16
+ }
17
+ //# sourceMappingURL=server-mode.js.map
@@ -87,15 +87,18 @@ export class AnthropicProvider {
87
87
  // Prompt caching: send system prompt as content blocks with cache_control.
88
88
  // Anthropic caches matching prefixes — 90% cost reduction on repeat turns.
89
89
  const systemBlocks = [{ type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } }];
90
+ // Scale max_tokens and thinking budget based on model
91
+ const isOpus = m.includes("opus");
92
+ const maxTokens = isOpus ? 16384 : 8192;
93
+ const thinkingBudget = isOpus ? 32000 : 10000;
90
94
  const body = {
91
95
  model: m,
92
- max_tokens: 8192,
96
+ max_tokens: maxTokens,
93
97
  system: systemBlocks,
94
98
  messages: this.convertMessages(messages),
95
99
  stream: true,
100
+ thinking: { type: "enabled", budget_tokens: thinkingBudget },
96
101
  };
97
- // Enable extended thinking for Claude models
98
- body.thinking = { type: "enabled", budget_tokens: 10000 };
99
102
  const anthropicTools = this.convertTools(tools);
100
103
  if (anthropicTools) {
101
104
  // Mark last tool definition as cacheable (cache covers all tools before it)
@@ -131,7 +134,6 @@ export class AnthropicProvider {
131
134
  let currentToolId = "";
132
135
  let currentToolName = "";
133
136
  let currentToolArgs = "";
134
- let _inThinkingBlock = false;
135
137
  while (true) {
136
138
  const { done, value } = await reader.read();
137
139
  if (done)
@@ -169,9 +171,7 @@ export class AnthropicProvider {
169
171
  callId: block.id,
170
172
  };
171
173
  }
172
- if (block?.type === "thinking") {
173
- _inThinkingBlock = true;
174
- }
174
+ // thinking blocks are handled via thinking_delta in content_block_delta
175
175
  break;
176
176
  }
177
177
  case "content_block_delta": {
@@ -188,7 +188,6 @@ export class AnthropicProvider {
188
188
  break;
189
189
  }
190
190
  case "content_block_stop": {
191
- _inThinkingBlock = false;
192
191
  if (currentToolId) {
193
192
  let parsedArgs = {};
194
193
  if (currentToolArgs) {
@@ -34,9 +34,9 @@ export function createFallbackProvider(primary, fallbacks) {
34
34
  for (let i = 0; i < providers.length; i++) {
35
35
  const p = providers[i];
36
36
  try {
37
- let hasYielded = false;
37
+ let _hasYielded = false;
38
38
  for await (const event of p.provider.stream(messages, systemPrompt, tools, p.model)) {
39
- hasYielded = true;
39
+ _hasYielded = true;
40
40
  yield event;
41
41
  }
42
42
  _activeFallback = i === 0 ? null : p.provider.name;
@@ -48,7 +48,6 @@ export function createFallbackProvider(primary, fallbacks) {
48
48
  throw err;
49
49
  // Pre-stream failure on primary: try next provider
50
50
  _activeFallback = null;
51
- continue;
52
51
  }
53
52
  }
54
53
  _activeFallback = null;
@@ -75,8 +75,9 @@ export class OpenAIProvider {
75
75
  if (tools?.length)
76
76
  body.tools = tools;
77
77
  // Enable reasoning for o-series models
78
- if (m.startsWith("o1") || m.startsWith("o3")) {
79
- body.reasoning_effort = "medium";
78
+ const isReasoning = m.startsWith("o1") || m.startsWith("o3") || m.startsWith("o4");
79
+ if (isReasoning) {
80
+ body.reasoning_effort = m.includes("mini") ? "medium" : "high";
80
81
  }
81
82
  let res;
82
83
  try {
@@ -29,6 +29,7 @@ export async function* query(userMessage, config, existingMessages = []) {
29
29
  systemPrompt: config.systemPrompt,
30
30
  permissionMode: config.permissionMode,
31
31
  askUserQuestion: config.askUserQuestion,
32
+ gitCommitPerTool: config.gitCommitPerTool,
32
33
  };
33
34
  const estimateTokens = makeTokenEstimator(config.provider);
34
35
  const contextManager = new ContextManager(undefined, config.model);
@@ -56,14 +57,18 @@ export async function* query(userMessage, config, existingMessages = []) {
56
57
  fullSystemPrompt += `\n\n# Suggested Skills\nThese skills match your request. Use Skill tool to load them:\n${hints}`;
57
58
  }
58
59
  }
59
- catch { /* skills optional */ }
60
+ catch {
61
+ /* skills optional */
62
+ }
60
63
  // Track memory version for live injection
61
64
  let lastMemoryVer = 0;
62
65
  try {
63
66
  const { memoryVersion } = await import("../harness/memory.js");
64
67
  lastMemoryVer = memoryVersion();
65
68
  }
66
- catch { /* ignore */ }
69
+ catch {
70
+ /* ignore */
71
+ }
67
72
  const state = {
68
73
  messages: [...existingMessages, createUserMessage(userMessage)],
69
74
  turn: 0,
@@ -85,20 +90,37 @@ export async function* query(userMessage, config, existingMessages = []) {
85
90
  return;
86
91
  }
87
92
  // Context window management
93
+ // ── Context window management with circuit breaker ──
88
94
  const contextWindow = getContextWindow(config.model);
89
95
  const estimatedTokens = estimateMessagesTokens(state.messages, estimateTokens);
90
- if (estimatedTokens > contextWindow * 0.8) {
96
+ const MAX_COMPRESSION_FAILURES = 3;
97
+ if (estimatedTokens > contextWindow * 0.8 && (state.compressionFailures ?? 0) < MAX_COMPRESSION_FAILURES) {
98
+ const tokensBefore = estimatedTokens;
99
+ let strategy = "basic";
91
100
  state.messages = compressMessages(state.messages, Math.floor(contextWindow * 0.6));
92
101
  const afterBasic = estimateMessagesTokens(state.messages, estimateTokens);
93
102
  if (afterBasic > contextWindow * 0.7 && state.messages.length > 4) {
94
103
  try {
95
104
  state.messages = await summarizeConversation(config.provider, state.messages, config.model, Math.floor(contextWindow * 0.5));
96
- yield { type: "error", message: "Context compressed with LLM summarization." };
105
+ strategy = "llm-summarization";
106
+ state.compressionFailures = 0; // Reset on success
97
107
  }
98
108
  catch {
99
- /* continue with basic compression */
109
+ state.compressionFailures = (state.compressionFailures ?? 0) + 1;
110
+ strategy = "basic-only (llm failed)";
100
111
  }
101
112
  }
113
+ const tokensAfter = estimateMessagesTokens(state.messages, estimateTokens);
114
+ yield {
115
+ type: "error",
116
+ message: `Context compressed (${strategy}): ${tokensBefore} → ${tokensAfter} tokens. Re-read any files you need.`,
117
+ };
118
+ }
119
+ else if (estimatedTokens > contextWindow * 0.8) {
120
+ yield {
121
+ type: "error",
122
+ message: "Context compression disabled (3 consecutive failures). Consider starting a new session.",
123
+ };
102
124
  }
103
125
  // ── Dynamic prompt: refresh memories if changed, inject warnings ──
104
126
  try {
@@ -116,7 +138,9 @@ export async function* query(userMessage, config, existingMessages = []) {
116
138
  lastMemoryVer = currentVer;
117
139
  }
118
140
  }
119
- catch { /* memory refresh optional */ }
141
+ catch {
142
+ /* memory refresh optional */
143
+ }
120
144
  let turnPrompt = fullSystemPrompt;
121
145
  if (config.maxCost && config.maxCost > 0) {
122
146
  const pct = state.totalCost / config.maxCost;