@zhijiewang/openharness 0.12.1 → 1.2.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 +741 -698
- package/dist/commands/index.js +118 -7
- package/dist/components/InitWizard.js +84 -5
- package/dist/harness/config.d.ts +17 -0
- package/dist/harness/telemetry.d.ts +59 -0
- package/dist/harness/telemetry.js +129 -0
- package/dist/main.js +40 -40
- package/dist/providers/router.d.ts +48 -0
- package/dist/providers/router.js +61 -0
- package/dist/query/compress.d.ts +5 -0
- package/dist/query/compress.js +45 -4
- package/dist/remote/auth.d.ts +25 -0
- package/dist/remote/auth.js +73 -0
- package/dist/remote/server.d.ts +18 -2
- package/dist/remote/server.js +168 -39
- package/dist/renderer/index.d.ts +7 -0
- package/dist/renderer/index.js +33 -5
- package/dist/repl.js +8 -0
- package/dist/services/PipelineExecutor.d.ts +48 -0
- package/dist/services/PipelineExecutor.js +179 -0
- package/dist/services/a2a.d.ts +119 -0
- package/dist/services/a2a.js +176 -0
- package/dist/tools/PipelineTool/index.d.ts +40 -0
- package/dist/tools/PipelineTool/index.js +53 -0
- package/dist/tools/WebFetchTool/index.js +2 -2
- package/dist/tools.js +3 -0
- package/package.json +73 -73
package/dist/commands/index.js
CHANGED
|
@@ -29,7 +29,7 @@ register("help", "Show available commands", () => {
|
|
|
29
29
|
'Git': ['diff', 'undo', 'rewind', 'commit', 'log'],
|
|
30
30
|
'Info': ['help', 'cost', 'status', 'config', 'files', 'model', 'memory', 'doctor', 'context', 'mcp', 'mcp-registry'],
|
|
31
31
|
'Settings': ['theme', 'vim', 'companion', 'fast', 'keys'],
|
|
32
|
-
'AI': ['plan', 'review', 'roles'],
|
|
32
|
+
'AI': ['plan', 'review', 'roles', 'agents', 'plugins'],
|
|
33
33
|
'Pet': ['cybergotchi'],
|
|
34
34
|
};
|
|
35
35
|
const lines = [];
|
|
@@ -354,6 +354,27 @@ register("roles", "List available agent specialization roles", () => {
|
|
|
354
354
|
lines.push("Usage: Agent({ subagent_type: 'code-reviewer', prompt: '...' })");
|
|
355
355
|
return { output: lines.join("\n"), handled: true };
|
|
356
356
|
});
|
|
357
|
+
register("agents", "Discover running openHarness agents on this machine", () => {
|
|
358
|
+
const { discoverAgents } = require('../services/a2a.js');
|
|
359
|
+
const agents = discoverAgents();
|
|
360
|
+
if (agents.length === 0) {
|
|
361
|
+
return { output: "No other openHarness agents running on this machine.\n\nOther oh sessions will appear here automatically via the A2A protocol.", handled: true };
|
|
362
|
+
}
|
|
363
|
+
const lines = [`Running Agents (${agents.length}):\n`];
|
|
364
|
+
for (const agent of agents) {
|
|
365
|
+
const age = Math.round((Date.now() - agent.registeredAt) / 60_000);
|
|
366
|
+
lines.push(` ${agent.name}`);
|
|
367
|
+
lines.push(` ID: ${agent.id}`);
|
|
368
|
+
lines.push(` Provider: ${agent.provider ?? 'unknown'} / ${agent.model ?? 'unknown'}`);
|
|
369
|
+
lines.push(` Dir: ${agent.workingDir ?? 'unknown'}`);
|
|
370
|
+
lines.push(` Endpoint: ${agent.endpoint.type}${agent.endpoint.port ? ':' + agent.endpoint.port : ''}`);
|
|
371
|
+
lines.push(` Uptime: ${age}m`);
|
|
372
|
+
lines.push(` Caps: ${agent.capabilities.map((c) => c.name).join(', ')}`);
|
|
373
|
+
lines.push('');
|
|
374
|
+
}
|
|
375
|
+
lines.push("Send messages with: Agent({ prompt: 'ask the other agent...', allowed_tools: ['SendMessage'] })");
|
|
376
|
+
return { output: lines.join("\n"), handled: true };
|
|
377
|
+
});
|
|
357
378
|
register("fast", "Toggle fast mode (optimized for speed)", () => {
|
|
358
379
|
return { output: "", handled: true, toggleFastMode: true };
|
|
359
380
|
});
|
|
@@ -473,18 +494,61 @@ register("doctor", "Run diagnostic health checks", (_args, ctx) => {
|
|
|
473
494
|
lines.push(` Session: ${ctx.sessionId}`);
|
|
474
495
|
lines.push(` Messages: ${ctx.messages.length}`);
|
|
475
496
|
lines.push(` Cost: $${ctx.totalCost.toFixed(4)}`);
|
|
476
|
-
// Disk space
|
|
497
|
+
// Disk space & storage
|
|
477
498
|
try {
|
|
478
|
-
const { statSync } = require("node:fs");
|
|
479
499
|
const ohDir = join(homedir(), ".oh");
|
|
480
500
|
if (existsSync(ohDir)) {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
501
|
+
const sessionsDir = join(ohDir, "sessions");
|
|
502
|
+
const sessCount = existsSync(sessionsDir) ? readdirSync(sessionsDir).filter(f => f.endsWith('.json')).length : 0;
|
|
503
|
+
lines.push(` Sessions: ${sessCount} saved`);
|
|
504
|
+
if (sessCount > 80)
|
|
505
|
+
issues.push(`${sessCount} saved sessions. Consider cleaning old ones.`);
|
|
506
|
+
// Memory stats
|
|
507
|
+
const memDir = join(ohDir, "memory");
|
|
508
|
+
const memCount = existsSync(memDir) ? readdirSync(memDir).filter(f => f.endsWith('.md')).length : 0;
|
|
509
|
+
lines.push(` Memories: ${memCount} global`);
|
|
510
|
+
// Cron stats
|
|
511
|
+
const cronDir = join(ohDir, "crons");
|
|
512
|
+
const cronCount = existsSync(cronDir) ? readdirSync(cronDir).filter(f => f.endsWith('.json')).length : 0;
|
|
513
|
+
lines.push(` Cron tasks: ${cronCount}`);
|
|
485
514
|
}
|
|
486
515
|
}
|
|
487
516
|
catch { /* ignore */ }
|
|
517
|
+
// Project-level stats
|
|
518
|
+
try {
|
|
519
|
+
const projMemDir = join(".oh", "memory");
|
|
520
|
+
const projMemCount = existsSync(projMemDir) ? readdirSync(projMemDir).filter(f => f.endsWith('.md')).length : 0;
|
|
521
|
+
if (projMemCount > 0)
|
|
522
|
+
lines.push(` Project mems: ${projMemCount}`);
|
|
523
|
+
const skillsDir = join(".oh", "skills");
|
|
524
|
+
const skillCount = existsSync(skillsDir) ? readdirSync(skillsDir).filter(f => f.endsWith('.md')).length : 0;
|
|
525
|
+
if (skillCount > 0)
|
|
526
|
+
lines.push(` Skills: ${skillCount}`);
|
|
527
|
+
}
|
|
528
|
+
catch { /* ignore */ }
|
|
529
|
+
// Global config
|
|
530
|
+
const globalCfg = existsSync(join(homedir(), ".oh", "config.yaml"));
|
|
531
|
+
lines.push(` Global config: ${globalCfg ? "~/.oh/config.yaml ✓" : "not set (optional)"}`);
|
|
532
|
+
// Verification config
|
|
533
|
+
try {
|
|
534
|
+
const { getVerificationConfig } = require('../harness/verification.js');
|
|
535
|
+
const vCfg = getVerificationConfig();
|
|
536
|
+
if (vCfg?.enabled) {
|
|
537
|
+
lines.push(` Verification: ✓ (${vCfg.rules.length} rules, mode: ${vCfg.mode})`);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
lines.push(` Verification: off (no rules detected)`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch { /* ignore */ }
|
|
544
|
+
// Tools
|
|
545
|
+
lines.push("");
|
|
546
|
+
lines.push(` Tools: ${ctx.messages.length > 0 ? 'ready' : 'loaded'}`);
|
|
547
|
+
// Node.js version
|
|
548
|
+
lines.push(` Node.js: ${process.version}`);
|
|
549
|
+
const [major] = process.version.slice(1).split('.').map(Number);
|
|
550
|
+
if (major && major < 18)
|
|
551
|
+
issues.push(`Node.js ${process.version} is below minimum (18+). Upgrade Node.js.`);
|
|
488
552
|
// Issues summary
|
|
489
553
|
if (issues.length > 0) {
|
|
490
554
|
lines.push("");
|
|
@@ -571,6 +635,53 @@ function setPinned(args, ctx, pinned) {
|
|
|
571
635
|
}
|
|
572
636
|
register("pin", "Pin a message (survives /compact)", (args, ctx) => setPinned(args, ctx, true));
|
|
573
637
|
register("unpin", "Unpin a message", (args, ctx) => setPinned(args, ctx, false));
|
|
638
|
+
register("plugins", "List installed plugins and discover new ones", (args) => {
|
|
639
|
+
const { discoverPlugins, discoverSkills } = require('../harness/plugins.js');
|
|
640
|
+
const query = args.trim();
|
|
641
|
+
if (query === 'search' || query.startsWith('search ')) {
|
|
642
|
+
// npm registry search
|
|
643
|
+
const keyword = query.replace(/^search\s*/, '').trim() || 'openharness-plugin';
|
|
644
|
+
return {
|
|
645
|
+
output: `To discover plugins, search npm:\n\n npm search openharness-plugin${keyword !== 'openharness-plugin' ? ' ' + keyword : ''}\n\nInstall with:\n npm install <package-name>\n\nPlugins are auto-discovered from node_modules/ if they contain openharness-plugin.json.`,
|
|
646
|
+
handled: true,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
// List installed
|
|
650
|
+
const plugins = discoverPlugins();
|
|
651
|
+
const skills = discoverSkills();
|
|
652
|
+
const lines = [];
|
|
653
|
+
if (plugins.length > 0) {
|
|
654
|
+
lines.push(`Installed Plugins (${plugins.length}):`);
|
|
655
|
+
for (const p of plugins) {
|
|
656
|
+
lines.push(` ${p.name}@${p.version} — ${p.description || 'no description'}`);
|
|
657
|
+
if (p.skills?.length)
|
|
658
|
+
lines.push(` Skills: ${p.skills.length}`);
|
|
659
|
+
if (p.mcpServers?.length)
|
|
660
|
+
lines.push(` MCP servers: ${p.mcpServers.map((s) => s.name).join(', ')}`);
|
|
661
|
+
}
|
|
662
|
+
lines.push('');
|
|
663
|
+
}
|
|
664
|
+
if (skills.length > 0) {
|
|
665
|
+
lines.push(`Available Skills (${skills.length}):`);
|
|
666
|
+
const bySource = {};
|
|
667
|
+
for (const s of skills) {
|
|
668
|
+
(bySource[s.source] ??= []).push(s);
|
|
669
|
+
}
|
|
670
|
+
for (const [source, sourceSkills] of Object.entries(bySource)) {
|
|
671
|
+
lines.push(` ${source}:`);
|
|
672
|
+
for (const s of sourceSkills) {
|
|
673
|
+
lines.push(` ${s.name} — ${s.description}${s.trigger ? ` (trigger: "${s.trigger}")` : ''}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else if (plugins.length === 0) {
|
|
678
|
+
lines.push('No plugins or skills installed.');
|
|
679
|
+
lines.push('');
|
|
680
|
+
lines.push('Create skills in .oh/skills/ or ~/.oh/skills/');
|
|
681
|
+
lines.push('Run /plugins search to find npm packages.');
|
|
682
|
+
}
|
|
683
|
+
return { output: lines.join('\n'), handled: true };
|
|
684
|
+
});
|
|
574
685
|
// ── Command Parser ──
|
|
575
686
|
/**
|
|
576
687
|
* Check if input is a slash command. If so, execute it.
|
|
@@ -30,9 +30,30 @@ const PERMISSION_MODES = [
|
|
|
30
30
|
{ key: "trust", label: "trust — auto-approve everything" },
|
|
31
31
|
{ key: "deny", label: "deny — read-only, block write/run tools" },
|
|
32
32
|
];
|
|
33
|
+
/** Auto-detect provider from environment variables */
|
|
34
|
+
function detectProviderFromEnv() {
|
|
35
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
36
|
+
return PROVIDERS.findIndex(p => p.key === "anthropic");
|
|
37
|
+
if (process.env.OPENAI_API_KEY)
|
|
38
|
+
return PROVIDERS.findIndex(p => p.key === "openai");
|
|
39
|
+
if (process.env.OPENROUTER_API_KEY)
|
|
40
|
+
return PROVIDERS.findIndex(p => p.key === "openrouter");
|
|
41
|
+
return 0; // Default to Ollama
|
|
42
|
+
}
|
|
43
|
+
/** Get the detected API key for a provider */
|
|
44
|
+
function getEnvApiKey(providerKey) {
|
|
45
|
+
const envMap = {
|
|
46
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
47
|
+
openai: 'OPENAI_API_KEY',
|
|
48
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
49
|
+
};
|
|
50
|
+
const envVar = envMap[providerKey];
|
|
51
|
+
return envVar ? (process.env[envVar] ?? '') : '';
|
|
52
|
+
}
|
|
33
53
|
export default function InitWizard({ onDone }) {
|
|
54
|
+
const detectedIdx = detectProviderFromEnv();
|
|
34
55
|
const [step, setStep] = useState("provider");
|
|
35
|
-
const [providerIdx, setProviderIdx] = useState(
|
|
56
|
+
const [providerIdx, setProviderIdx] = useState(detectedIdx);
|
|
36
57
|
const [apiKey, setApiKey] = useState("");
|
|
37
58
|
const [baseUrl, setBaseUrl] = useState("");
|
|
38
59
|
const [model, setModel] = useState("");
|
|
@@ -43,6 +64,9 @@ export default function InitWizard({ onDone }) {
|
|
|
43
64
|
const [permIdx, setPermIdx] = useState(0);
|
|
44
65
|
const [hatchGotchi, setHatchGotchi] = useState(false);
|
|
45
66
|
const [showSetup, setShowSetup] = useState(false);
|
|
67
|
+
const [suggestedMcp, setSuggestedMcp] = useState([]);
|
|
68
|
+
const [selectedMcp, setSelectedMcp] = useState(new Set());
|
|
69
|
+
const [mcpIdx, setMcpIdx] = useState(0);
|
|
46
70
|
const provider = PROVIDERS[providerIdx];
|
|
47
71
|
// ── Keyboard navigation ──
|
|
48
72
|
useInput(useCallback((input, key) => {
|
|
@@ -54,11 +78,19 @@ export default function InitWizard({ onDone }) {
|
|
|
54
78
|
if (key.return) {
|
|
55
79
|
setBaseUrl(provider.defaultBaseUrl ?? "");
|
|
56
80
|
setModel(provider.defaultModel);
|
|
81
|
+
// Auto-fill API key from environment if available
|
|
82
|
+
const envKey = getEnvApiKey(provider.key);
|
|
83
|
+
if (envKey)
|
|
84
|
+
setApiKey(envKey);
|
|
57
85
|
if (!provider.needsApiKey) {
|
|
58
|
-
// Skip API key, go straight to testing
|
|
59
86
|
runTest(provider, "", provider.defaultBaseUrl ?? "");
|
|
60
87
|
setStep("testing");
|
|
61
88
|
}
|
|
89
|
+
else if (envKey) {
|
|
90
|
+
// Have env API key — skip manual entry, go to testing
|
|
91
|
+
runTest(provider, envKey, provider.defaultBaseUrl ?? "");
|
|
92
|
+
setStep("testing");
|
|
93
|
+
}
|
|
62
94
|
else {
|
|
63
95
|
setStep("apikey");
|
|
64
96
|
}
|
|
@@ -69,8 +101,37 @@ export default function InitWizard({ onDone }) {
|
|
|
69
101
|
setPermIdx(i => Math.max(0, i - 1));
|
|
70
102
|
if (key.downArrow)
|
|
71
103
|
setPermIdx(i => Math.min(PERMISSION_MODES.length - 1, i + 1));
|
|
104
|
+
if (key.return) {
|
|
105
|
+
// Suggest popular MCP servers
|
|
106
|
+
setSuggestedMcp(['github', 'memory', 'fetch', 'sequential-thinking', 'brave-search']);
|
|
107
|
+
setMcpIdx(0);
|
|
108
|
+
setSelectedMcp(new Set());
|
|
109
|
+
setStep("mcp");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (step === "mcp") {
|
|
113
|
+
if (key.upArrow)
|
|
114
|
+
setMcpIdx(i => Math.max(0, i - 1));
|
|
115
|
+
if (key.downArrow)
|
|
116
|
+
setMcpIdx(i => Math.min(suggestedMcp.length - 1, i + 1));
|
|
117
|
+
if (input === " ") {
|
|
118
|
+
// Toggle selection
|
|
119
|
+
const name = suggestedMcp[mcpIdx];
|
|
120
|
+
if (name) {
|
|
121
|
+
setSelectedMcp(prev => {
|
|
122
|
+
const next = new Set(prev);
|
|
123
|
+
if (next.has(name))
|
|
124
|
+
next.delete(name);
|
|
125
|
+
else
|
|
126
|
+
next.add(name);
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
72
131
|
if (key.return)
|
|
73
132
|
setStep("gotchi");
|
|
133
|
+
if (input === "s" || input === "S")
|
|
134
|
+
setStep("gotchi"); // Skip
|
|
74
135
|
}
|
|
75
136
|
if (step === "model" && availableModels.length > 0) {
|
|
76
137
|
if (key.upArrow)
|
|
@@ -120,16 +181,34 @@ export default function InitWizard({ onDone }) {
|
|
|
120
181
|
// ── Write final config ──
|
|
121
182
|
const writeFinal = useCallback(() => {
|
|
122
183
|
const selectedModel = availableModels.length > 0 ? (availableModels[modelIdx] ?? model) : model;
|
|
184
|
+
// Build MCP server configs from selected registry entries
|
|
185
|
+
let mcpServers;
|
|
186
|
+
if (selectedMcp.size > 0) {
|
|
187
|
+
try {
|
|
188
|
+
const { MCP_REGISTRY } = require('../mcp/registry.js');
|
|
189
|
+
mcpServers = [...selectedMcp]
|
|
190
|
+
.map(name => MCP_REGISTRY.find((e) => e.name === name))
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.map((e) => ({
|
|
193
|
+
name: e.name,
|
|
194
|
+
command: 'npx',
|
|
195
|
+
args: ['-y', e.package, ...(e.args ?? [])],
|
|
196
|
+
...(e.envVars?.length ? { env: Object.fromEntries(e.envVars.map((v) => [v, `YOUR_${v}`])) } : {}),
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
catch { /* ignore */ }
|
|
200
|
+
}
|
|
123
201
|
writeOhConfig({
|
|
124
202
|
provider: provider.key,
|
|
125
203
|
model: selectedModel || provider.defaultModel,
|
|
126
204
|
permissionMode: PERMISSION_MODES[permIdx].key,
|
|
127
205
|
...(apiKey ? { apiKey } : {}),
|
|
128
206
|
...(baseUrl ? { baseUrl } : {}),
|
|
207
|
+
...(mcpServers?.length ? { mcpServers } : {}),
|
|
129
208
|
});
|
|
130
209
|
setStep("done");
|
|
131
210
|
setTimeout(() => onDone?.(), 1500);
|
|
132
|
-
}, [provider, model, availableModels, modelIdx, permIdx, apiKey, baseUrl]);
|
|
211
|
+
}, [provider, model, availableModels, modelIdx, permIdx, apiKey, baseUrl, selectedMcp]);
|
|
133
212
|
// ── Render ──
|
|
134
213
|
if (showSetup) {
|
|
135
214
|
return _jsx(CybergotchiSetup, { onComplete: () => { setShowSetup(false); writeFinal(); }, onSkip: () => { setShowSetup(false); writeFinal(); } });
|
|
@@ -137,7 +216,7 @@ export default function InitWizard({ onDone }) {
|
|
|
137
216
|
if (step === "done") {
|
|
138
217
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "green", children: "\u2713 OpenHarness configured!" }), _jsx(Text, { dimColor: true, children: "Config saved to .oh/config.yaml" }), _jsx(Text, { dimColor: true, children: "Run: oh" })] }));
|
|
139
218
|
}
|
|
140
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "OpenHarness Setup" }), _jsx(Text, { children: " " }), step === "provider" && (_jsxs(Box, { flexDirection: "column", children: [
|
|
219
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "OpenHarness Setup" }), _jsx(Text, { children: " " }), step === "provider" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Select provider:", detectedIdx > 0 ? _jsx(Text, { dimColor: true, children: " (auto-detected from env)" }) : ''] }), PROVIDERS.map((p, i) => (_jsxs(Text, { color: i === providerIdx ? "cyan" : undefined, children: [i === providerIdx ? "▶ " : " ", p.label] }, p.key))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate Enter select" })] })), step === "apikey" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["API key for ", _jsx(Text, { color: "cyan", children: provider.label }), ":"] }), _jsx(TextInput, { value: apiKey, onChange: setApiKey, mask: "*", onSubmit: (val) => {
|
|
141
220
|
if (!val.trim())
|
|
142
221
|
return;
|
|
143
222
|
if (provider.key === "custom") {
|
|
@@ -158,6 +237,6 @@ export default function InitWizard({ onDone }) {
|
|
|
158
237
|
const gi = start + vi;
|
|
159
238
|
return (_jsxs(Text, { color: gi === modelIdx ? "cyan" : undefined, children: [gi === modelIdx ? "▶ " : " ", m] }, m));
|
|
160
239
|
}), start + WINDOW < availableModels.length && (_jsxs(Text, { dimColor: true, children: [" \u2193 ", availableModels.length - start - WINDOW, " more"] }))] }));
|
|
161
|
-
})() : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Could not fetch model list. Enter model name:" }), _jsx(TextInput, { value: model, onChange: setModel, onSubmit: () => setStep("permission") })] })), availableModels.length > 0 && _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate Enter select" })] })), step === "permission" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Permission mode:" }), PERMISSION_MODES.map((p, i) => (_jsxs(Text, { color: i === permIdx ? "cyan" : undefined, children: [i === permIdx ? "▶ " : " ", p.label] }, p.key))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate Enter select" })] })), step === "gotchi" && (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: ["Hatch a cybergotchi companion? ", _jsx(Text, { dimColor: true, children: "(Y/n)" })] }) }))] }));
|
|
240
|
+
})() : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Could not fetch model list. Enter model name:" }), _jsx(TextInput, { value: model, onChange: setModel, onSubmit: () => setStep("permission") })] })), availableModels.length > 0 && _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate Enter select" })] })), step === "permission" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Permission mode:" }), PERMISSION_MODES.map((p, i) => (_jsxs(Text, { color: i === permIdx ? "cyan" : undefined, children: [i === permIdx ? "▶ " : " ", p.label] }, p.key))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate Enter select" })] })), step === "mcp" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Add MCP servers? ", _jsx(Text, { dimColor: true, children: "(Space to toggle, Enter to confirm, S to skip)" })] }), _jsx(Text, { children: " " }), suggestedMcp.map((name, i) => (_jsxs(Text, { color: i === mcpIdx ? "cyan" : undefined, children: [i === mcpIdx ? "▶ " : " ", selectedMcp.has(name) ? "[✓] " : "[ ] ", name] }, name))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate Space toggle Enter confirm S skip" })] })), step === "gotchi" && (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: ["Hatch a cybergotchi companion? ", _jsx(Text, { dimColor: true, children: "(Y/n)" })] }) }))] }));
|
|
162
241
|
}
|
|
163
242
|
//# sourceMappingURL=InitWizard.js.map
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -51,6 +51,23 @@ export type OhConfig = {
|
|
|
51
51
|
memory?: {
|
|
52
52
|
consolidateOnExit?: boolean;
|
|
53
53
|
};
|
|
54
|
+
/** Multi-model router — use different models for different task types */
|
|
55
|
+
modelRouter?: {
|
|
56
|
+
fast?: string;
|
|
57
|
+
balanced?: string;
|
|
58
|
+
powerful?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Opt-in telemetry (default: off) */
|
|
61
|
+
telemetry?: {
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
endpoint?: string;
|
|
64
|
+
};
|
|
65
|
+
/** Remote server security settings */
|
|
66
|
+
remote?: {
|
|
67
|
+
tokens?: string[];
|
|
68
|
+
rateLimit?: number;
|
|
69
|
+
allowedTools?: string[];
|
|
70
|
+
};
|
|
54
71
|
};
|
|
55
72
|
/** Clear cached config (call after writes or to force re-read) */
|
|
56
73
|
export declare function invalidateConfigCache(): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in telemetry — anonymous usage tracking for feature prioritization.
|
|
3
|
+
*
|
|
4
|
+
* Default: OFF. Enable via config.yaml:
|
|
5
|
+
* telemetry:
|
|
6
|
+
* enabled: true
|
|
7
|
+
*
|
|
8
|
+
* Privacy: never logs file paths, prompts, tool output, or API keys.
|
|
9
|
+
* Only tracks: tool names, durations, error categories, session metadata.
|
|
10
|
+
*
|
|
11
|
+
* Events are batched locally as JSONL in ~/.oh/telemetry/.
|
|
12
|
+
* Optional: POST to configurable endpoint on session end.
|
|
13
|
+
*/
|
|
14
|
+
export type TelemetryEvent = {
|
|
15
|
+
type: 'session_start' | 'tool_call' | 'error' | 'session_end';
|
|
16
|
+
timestamp: number;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
payload: TelemetryPayload;
|
|
19
|
+
};
|
|
20
|
+
export type TelemetryPayload = {
|
|
21
|
+
provider?: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
platform?: string;
|
|
24
|
+
toolName?: string;
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
isError?: boolean;
|
|
27
|
+
errorCategory?: string;
|
|
28
|
+
totalTurns?: number;
|
|
29
|
+
totalCost?: number;
|
|
30
|
+
totalToolCalls?: number;
|
|
31
|
+
durationMinutes?: number;
|
|
32
|
+
};
|
|
33
|
+
/** Record a telemetry event (no-op if telemetry disabled) */
|
|
34
|
+
export declare function recordEvent(event: TelemetryEvent): void;
|
|
35
|
+
/** Convenience: record a tool call event */
|
|
36
|
+
export declare function recordToolCall(sessionId: string, toolName: string, durationMs: number, isError: boolean): void;
|
|
37
|
+
/** Convenience: record session start */
|
|
38
|
+
export declare function recordSessionStart(sessionId: string, provider: string, model: string): void;
|
|
39
|
+
/** Convenience: record session end with stats */
|
|
40
|
+
export declare function recordSessionEnd(sessionId: string, stats: {
|
|
41
|
+
totalTurns: number;
|
|
42
|
+
totalCost: number;
|
|
43
|
+
totalToolCalls: number;
|
|
44
|
+
durationMinutes: number;
|
|
45
|
+
}): void;
|
|
46
|
+
/** Convenience: record an error */
|
|
47
|
+
export declare function recordError(sessionId: string, category: string): void;
|
|
48
|
+
/** Read local telemetry events for a session */
|
|
49
|
+
export declare function readSessionEvents(sessionId: string): TelemetryEvent[];
|
|
50
|
+
/** Get aggregate stats across all sessions */
|
|
51
|
+
export declare function getAggregateStats(): {
|
|
52
|
+
totalSessions: number;
|
|
53
|
+
totalEvents: number;
|
|
54
|
+
toolUsage: Record<string, number>;
|
|
55
|
+
errorCategories: Record<string, number>;
|
|
56
|
+
};
|
|
57
|
+
/** Reset telemetry cache (for testing or config changes) */
|
|
58
|
+
export declare function resetTelemetry(): void;
|
|
59
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in telemetry — anonymous usage tracking for feature prioritization.
|
|
3
|
+
*
|
|
4
|
+
* Default: OFF. Enable via config.yaml:
|
|
5
|
+
* telemetry:
|
|
6
|
+
* enabled: true
|
|
7
|
+
*
|
|
8
|
+
* Privacy: never logs file paths, prompts, tool output, or API keys.
|
|
9
|
+
* Only tracks: tool names, durations, error categories, session metadata.
|
|
10
|
+
*
|
|
11
|
+
* Events are batched locally as JSONL in ~/.oh/telemetry/.
|
|
12
|
+
* Optional: POST to configurable endpoint on session end.
|
|
13
|
+
*/
|
|
14
|
+
import { appendFileSync, mkdirSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { readOhConfig } from './config.js';
|
|
18
|
+
const TELEMETRY_DIR = join(homedir(), '.oh', 'telemetry');
|
|
19
|
+
// ── State ──
|
|
20
|
+
let _enabled;
|
|
21
|
+
let _sessionFile = null;
|
|
22
|
+
function isEnabled() {
|
|
23
|
+
if (_enabled !== undefined)
|
|
24
|
+
return _enabled;
|
|
25
|
+
const config = readOhConfig();
|
|
26
|
+
_enabled = config?.telemetry?.enabled === true;
|
|
27
|
+
return _enabled;
|
|
28
|
+
}
|
|
29
|
+
function getSessionFile(sessionId) {
|
|
30
|
+
if (_sessionFile)
|
|
31
|
+
return _sessionFile;
|
|
32
|
+
mkdirSync(TELEMETRY_DIR, { recursive: true });
|
|
33
|
+
_sessionFile = join(TELEMETRY_DIR, `${sessionId}.jsonl`);
|
|
34
|
+
return _sessionFile;
|
|
35
|
+
}
|
|
36
|
+
// ── Public API ──
|
|
37
|
+
/** Record a telemetry event (no-op if telemetry disabled) */
|
|
38
|
+
export function recordEvent(event) {
|
|
39
|
+
if (!isEnabled())
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const file = getSessionFile(event.sessionId);
|
|
43
|
+
appendFileSync(file, JSON.stringify(event) + '\n');
|
|
44
|
+
}
|
|
45
|
+
catch { /* never crash on telemetry failure */ }
|
|
46
|
+
}
|
|
47
|
+
/** Convenience: record a tool call event */
|
|
48
|
+
export function recordToolCall(sessionId, toolName, durationMs, isError) {
|
|
49
|
+
recordEvent({
|
|
50
|
+
type: 'tool_call',
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
sessionId,
|
|
53
|
+
payload: { toolName, durationMs, isError },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Convenience: record session start */
|
|
57
|
+
export function recordSessionStart(sessionId, provider, model) {
|
|
58
|
+
recordEvent({
|
|
59
|
+
type: 'session_start',
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
sessionId,
|
|
62
|
+
payload: { provider, model, platform: process.platform },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Convenience: record session end with stats */
|
|
66
|
+
export function recordSessionEnd(sessionId, stats) {
|
|
67
|
+
recordEvent({
|
|
68
|
+
type: 'session_end',
|
|
69
|
+
timestamp: Date.now(),
|
|
70
|
+
sessionId,
|
|
71
|
+
payload: stats,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Convenience: record an error */
|
|
75
|
+
export function recordError(sessionId, category) {
|
|
76
|
+
recordEvent({
|
|
77
|
+
type: 'error',
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
sessionId,
|
|
80
|
+
payload: { errorCategory: category },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Read local telemetry events for a session */
|
|
84
|
+
export function readSessionEvents(sessionId) {
|
|
85
|
+
const file = join(TELEMETRY_DIR, `${sessionId}.jsonl`);
|
|
86
|
+
if (!existsSync(file))
|
|
87
|
+
return [];
|
|
88
|
+
try {
|
|
89
|
+
return readFileSync(file, 'utf-8')
|
|
90
|
+
.split('\n')
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map(line => JSON.parse(line));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Get aggregate stats across all sessions */
|
|
99
|
+
export function getAggregateStats() {
|
|
100
|
+
if (!existsSync(TELEMETRY_DIR))
|
|
101
|
+
return { totalSessions: 0, totalEvents: 0, toolUsage: {}, errorCategories: {} };
|
|
102
|
+
const files = readdirSync(TELEMETRY_DIR).filter(f => f.endsWith('.jsonl'));
|
|
103
|
+
const toolUsage = {};
|
|
104
|
+
const errorCategories = {};
|
|
105
|
+
let totalEvents = 0;
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
try {
|
|
108
|
+
const lines = readFileSync(join(TELEMETRY_DIR, file), 'utf-8').split('\n').filter(Boolean);
|
|
109
|
+
totalEvents += lines.length;
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
const event = JSON.parse(line);
|
|
112
|
+
if (event.type === 'tool_call' && event.payload.toolName) {
|
|
113
|
+
toolUsage[event.payload.toolName] = (toolUsage[event.payload.toolName] ?? 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
if (event.type === 'error' && event.payload.errorCategory) {
|
|
116
|
+
errorCategories[event.payload.errorCategory] = (errorCategories[event.payload.errorCategory] ?? 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* skip malformed files */ }
|
|
121
|
+
}
|
|
122
|
+
return { totalSessions: files.length, totalEvents, toolUsage, errorCategories };
|
|
123
|
+
}
|
|
124
|
+
/** Reset telemetry cache (for testing or config changes) */
|
|
125
|
+
export function resetTelemetry() {
|
|
126
|
+
_enabled = undefined;
|
|
127
|
+
_sessionFile = null;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=telemetry.js.map
|
package/dist/main.js
CHANGED
|
@@ -15,13 +15,13 @@ import { join } from "node:path";
|
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
const _require = createRequire(import.meta.url);
|
|
17
17
|
const VERSION = _require('../package.json').version;
|
|
18
|
-
const BANNER = ` ___
|
|
19
|
-
/ \\
|
|
20
|
-
( ) ___ ___ ___ _ _ _ _ _ ___ _ _ ___ ___ ___
|
|
21
|
-
\`~w~\` / _ \\| _ \\| __| \\| | || | /_\\ | _ \\ \\| | __/ __/ __|
|
|
22
|
-
(( )) | (_) | _/| _|| .\` | __ |/ _ \\| / .\` | _|\\__ \\__ \\
|
|
23
|
-
))(( \\___/|_| |___|_|\\_|_||_/_/ \\_\\_|_\\_|\\_|___|___/___/
|
|
24
|
-
(( ))
|
|
18
|
+
const BANNER = ` ___
|
|
19
|
+
/ \\
|
|
20
|
+
( ) ___ ___ ___ _ _ _ _ _ ___ _ _ ___ ___ ___
|
|
21
|
+
\`~w~\` / _ \\| _ \\| __| \\| | || | /_\\ | _ \\ \\| | __/ __/ __|
|
|
22
|
+
(( )) | (_) | _/| _|| .\` | __ |/ _ \\| / .\` | _|\\__ \\__ \\
|
|
23
|
+
))(( \\___/|_| |___|_|\\_|_||_/_/ \\_\\_|_\\_|\\_|___|___/___/
|
|
24
|
+
(( ))
|
|
25
25
|
\`--\``;
|
|
26
26
|
const program = new Command();
|
|
27
27
|
program
|
|
@@ -29,39 +29,39 @@ program
|
|
|
29
29
|
.description("Open-source terminal coding agent. Works with any LLM.")
|
|
30
30
|
.version(VERSION);
|
|
31
31
|
// ── Headless run command ──
|
|
32
|
-
const DEFAULT_SYSTEM_PROMPT = `You are OpenHarness, an AI coding assistant running in the user's terminal.
|
|
33
|
-
You have access to tools for reading, writing, and searching files, running shell commands, and more.
|
|
34
|
-
|
|
35
|
-
# Tool usage
|
|
36
|
-
- Use Read (not cat/head/tail) to read files. Use Edit (not sed/awk) to modify files. Use Write only to create new files or complete rewrites. Use Grep (not grep/rg) to search content. Use Glob (not find) to find files by pattern. Use Bash only for shell commands that dedicated tools cannot handle.
|
|
37
|
-
- Read a file before editing it. Understand existing code before suggesting modifications.
|
|
38
|
-
- Prefer editing existing files over creating new ones.
|
|
39
|
-
- You can call multiple tools in a single response. Call independent tools in parallel for efficiency. Call dependent tools sequentially.
|
|
40
|
-
|
|
41
|
-
# Coding standards
|
|
42
|
-
- Do not add features, refactor code, or make improvements beyond what was asked.
|
|
43
|
-
- Do not add comments, docstrings, or type annotations to code you didn't change.
|
|
44
|
-
- Do not add error handling or validation for scenarios that can't happen.
|
|
45
|
-
- Do not create abstractions for one-time operations. Three similar lines is better than a premature abstraction.
|
|
46
|
-
- Be careful not to introduce security vulnerabilities (command injection, XSS, SQL injection, etc.).
|
|
47
|
-
- If you wrote insecure code, fix it immediately.
|
|
48
|
-
|
|
49
|
-
# Git safety
|
|
50
|
-
- NEVER run destructive git commands (push --force, reset --hard, checkout ., clean -f, branch -D) unless the user explicitly requests it.
|
|
51
|
-
- NEVER skip hooks (--no-verify) or bypass signing (--no-gpg-sign) unless the user explicitly asks.
|
|
52
|
-
- Prefer creating NEW commits over amending existing ones.
|
|
53
|
-
- Before staging, prefer adding specific files by name rather than "git add -A" which can include sensitive files.
|
|
54
|
-
- Only commit when the user explicitly asks you to.
|
|
55
|
-
|
|
56
|
-
# Careful actions
|
|
57
|
-
- For actions that are hard to reverse or affect shared systems, check with the user before proceeding.
|
|
58
|
-
- Do not use destructive actions as shortcuts. Investigate root causes rather than bypassing safety checks.
|
|
59
|
-
- If you discover unexpected state (unfamiliar files, branches, config), investigate before deleting or overwriting.
|
|
60
|
-
|
|
61
|
-
# Output style
|
|
62
|
-
- Be concise. Lead with the answer or action, not the reasoning.
|
|
63
|
-
- When referencing code, include file_path:line_number.
|
|
64
|
-
- Do not restate what the user said. Do not add trailing summaries unless asked.
|
|
32
|
+
const DEFAULT_SYSTEM_PROMPT = `You are OpenHarness, an AI coding assistant running in the user's terminal.
|
|
33
|
+
You have access to tools for reading, writing, and searching files, running shell commands, and more.
|
|
34
|
+
|
|
35
|
+
# Tool usage
|
|
36
|
+
- Use Read (not cat/head/tail) to read files. Use Edit (not sed/awk) to modify files. Use Write only to create new files or complete rewrites. Use Grep (not grep/rg) to search content. Use Glob (not find) to find files by pattern. Use Bash only for shell commands that dedicated tools cannot handle.
|
|
37
|
+
- Read a file before editing it. Understand existing code before suggesting modifications.
|
|
38
|
+
- Prefer editing existing files over creating new ones.
|
|
39
|
+
- You can call multiple tools in a single response. Call independent tools in parallel for efficiency. Call dependent tools sequentially.
|
|
40
|
+
|
|
41
|
+
# Coding standards
|
|
42
|
+
- Do not add features, refactor code, or make improvements beyond what was asked.
|
|
43
|
+
- Do not add comments, docstrings, or type annotations to code you didn't change.
|
|
44
|
+
- Do not add error handling or validation for scenarios that can't happen.
|
|
45
|
+
- Do not create abstractions for one-time operations. Three similar lines is better than a premature abstraction.
|
|
46
|
+
- Be careful not to introduce security vulnerabilities (command injection, XSS, SQL injection, etc.).
|
|
47
|
+
- If you wrote insecure code, fix it immediately.
|
|
48
|
+
|
|
49
|
+
# Git safety
|
|
50
|
+
- NEVER run destructive git commands (push --force, reset --hard, checkout ., clean -f, branch -D) unless the user explicitly requests it.
|
|
51
|
+
- NEVER skip hooks (--no-verify) or bypass signing (--no-gpg-sign) unless the user explicitly asks.
|
|
52
|
+
- Prefer creating NEW commits over amending existing ones.
|
|
53
|
+
- Before staging, prefer adding specific files by name rather than "git add -A" which can include sensitive files.
|
|
54
|
+
- Only commit when the user explicitly asks you to.
|
|
55
|
+
|
|
56
|
+
# Careful actions
|
|
57
|
+
- For actions that are hard to reverse or affect shared systems, check with the user before proceeding.
|
|
58
|
+
- Do not use destructive actions as shortcuts. Investigate root causes rather than bypassing safety checks.
|
|
59
|
+
- If you discover unexpected state (unfamiliar files, branches, config), investigate before deleting or overwriting.
|
|
60
|
+
|
|
61
|
+
# Output style
|
|
62
|
+
- Be concise. Lead with the answer or action, not the reasoning.
|
|
63
|
+
- When referencing code, include file_path:line_number.
|
|
64
|
+
- Do not restate what the user said. Do not add trailing summaries unless asked.
|
|
65
65
|
- Keep responses short and direct. If you can say it in one sentence, don't use three.`;
|
|
66
66
|
function buildSystemPrompt(model) {
|
|
67
67
|
const parts = [DEFAULT_SYSTEM_PROMPT];
|