@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.
- package/README.md +2 -2
- package/dist/Tool.d.ts +2 -0
- 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 -1052
- 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 +61 -61
- package/dist/harness/config.d.ts +2 -0
- package/dist/harness/hooks.js +9 -6
- package/dist/harness/memory.js +28 -1
- package/dist/harness/plugins.d.ts +2 -0
- package/dist/harness/plugins.js +44 -11
- package/dist/harness/session-db.js +3 -1
- package/dist/harness/skill-registry.d.ts +21 -0
- package/dist/harness/skill-registry.js +35 -0
- package/dist/lsp/client.js +2 -1
- package/dist/main.js +10 -2
- package/dist/mcp/client.js +2 -1
- package/dist/mcp/server-mode.d.ts +10 -0
- package/dist/mcp/server-mode.js +17 -0
- package/dist/providers/anthropic.js +7 -8
- package/dist/providers/fallback.js +2 -3
- package/dist/providers/openai.js +3 -2
- package/dist/query/index.js +30 -6
- package/dist/query/tools.js +11 -0
- package/dist/query/types.d.ts +4 -0
- 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/repl.js +3 -1
- package/dist/services/SkillExtractor.js +2 -0
- package/dist/tools/SkillTool/index.js +26 -2
- 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
|
@@ -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 [
|
|
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,
|
|
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: () => {
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -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) */
|
package/dist/harness/hooks.js
CHANGED
|
@@ -117,13 +117,16 @@ async function runHttpHook(url, event, ctx, timeoutMs = 10_000) {
|
|
|
117
117
|
return false;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
/**
|
|
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
|
-
|
|
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. */
|
package/dist/harness/memory.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/harness/plugins.js
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
43
|
-
function
|
|
52
|
+
/** Recursively collect all .md files from a directory tree */
|
|
53
|
+
function walkMdFiles(dir) {
|
|
44
54
|
if (!existsSync(dir))
|
|
45
55
|
return [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -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
|
package/dist/lsp/client.js
CHANGED
|
@@ -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")
|
package/dist/mcp/client.js
CHANGED
|
@@ -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)
|
|
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:
|
|
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
|
-
|
|
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
|
|
37
|
+
let _hasYielded = false;
|
|
38
38
|
for await (const event of p.provider.stream(messages, systemPrompt, tools, p.model)) {
|
|
39
|
-
|
|
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;
|
package/dist/providers/openai.js
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
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 {
|
package/dist/query/index.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
strategy = "llm-summarization";
|
|
106
|
+
state.compressionFailures = 0; // Reset on success
|
|
97
107
|
}
|
|
98
108
|
catch {
|
|
99
|
-
|
|
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 {
|
|
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;
|