@zhijiewang/openharness 1.2.0 → 1.4.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/dist/agents/roles.d.ts +4 -2
- package/dist/agents/roles.js +69 -5
- package/dist/commands/index.js +110 -34
- package/dist/harness/config.d.ts +12 -1
- package/dist/harness/config.js +5 -0
- package/dist/harness/hooks.d.ts +19 -4
- package/dist/harness/hooks.js +82 -23
- package/dist/harness/marketplace.d.ts +62 -0
- package/dist/harness/marketplace.js +242 -0
- package/dist/harness/plugins.d.ts +1 -1
- package/dist/harness/plugins.js +15 -1
- package/dist/harness/rules.js +32 -4
- package/dist/harness/submit-handler.js +18 -2
- package/dist/main.js +1 -0
- package/dist/query/compress.js +5 -1
- package/dist/query/tools.js +7 -0
- package/dist/tools/AgentTool/index.js +5 -1
- package/dist/tools/DiagnosticsTool/index.d.ts +3 -3
- package/dist/tools/DiagnosticsTool/index.js +37 -8
- package/dist/tools/MonitorTool/index.d.ts +21 -0
- package/dist/tools/MonitorTool/index.js +114 -0
- package/dist/tools/PowerShellTool/index.d.ts +15 -0
- package/dist/tools/PowerShellTool/index.js +32 -0
- package/dist/tools.js +4 -0
- package/dist/types/permissions.js +42 -2
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Marketplace — discover, install, and manage plugins from curated registries.
|
|
3
|
+
*
|
|
4
|
+
* Marketplaces are JSON files listing available plugins with versions and sources.
|
|
5
|
+
* Plugins are downloaded and cached to ~/.oh/plugins/cache/ for security and versioning.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by Claude Code's marketplace model.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'node:fs';
|
|
10
|
+
import { join, basename } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
const MARKETPLACE_DIR = join(homedir(), '.oh', 'marketplaces');
|
|
14
|
+
const PLUGIN_CACHE_DIR = join(homedir(), '.oh', 'plugins', 'cache');
|
|
15
|
+
const INSTALLED_PLUGINS_FILE = join(homedir(), '.oh', 'plugins', 'installed.json');
|
|
16
|
+
// ── Marketplace Management ──
|
|
17
|
+
/** Add a marketplace from a URL, GitHub repo, or local path */
|
|
18
|
+
export function addMarketplace(nameOrUrl) {
|
|
19
|
+
mkdirSync(MARKETPLACE_DIR, { recursive: true });
|
|
20
|
+
// Fetch marketplace.json
|
|
21
|
+
let data;
|
|
22
|
+
let marketplaceName;
|
|
23
|
+
if (nameOrUrl.startsWith('http')) {
|
|
24
|
+
// URL
|
|
25
|
+
try {
|
|
26
|
+
data = execSync(`curl -sL "${nameOrUrl}/marketplace.json"`, { encoding: 'utf-8', timeout: 10_000 });
|
|
27
|
+
marketplaceName = new URL(nameOrUrl).hostname;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (nameOrUrl.includes('/') && !nameOrUrl.startsWith('.')) {
|
|
34
|
+
// GitHub repo (owner/repo format)
|
|
35
|
+
try {
|
|
36
|
+
const url = `https://raw.githubusercontent.com/${nameOrUrl}/main/marketplace.json`;
|
|
37
|
+
data = execSync(`curl -sL "${url}"`, { encoding: 'utf-8', timeout: 10_000 });
|
|
38
|
+
marketplaceName = nameOrUrl.replace('/', '-');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (existsSync(join(nameOrUrl, 'marketplace.json'))) {
|
|
45
|
+
// Local path
|
|
46
|
+
data = readFileSync(join(nameOrUrl, 'marketplace.json'), 'utf-8');
|
|
47
|
+
marketplaceName = basename(nameOrUrl);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const marketplace = JSON.parse(data);
|
|
54
|
+
if (!marketplace.plugins || !Array.isArray(marketplace.plugins))
|
|
55
|
+
return null;
|
|
56
|
+
marketplace.name = marketplace.name ?? marketplaceName;
|
|
57
|
+
writeFileSync(join(MARKETPLACE_DIR, `${marketplace.name}.json`), JSON.stringify(marketplace, null, 2));
|
|
58
|
+
return marketplace;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Remove a marketplace */
|
|
65
|
+
export function removeMarketplace(name) {
|
|
66
|
+
const path = join(MARKETPLACE_DIR, `${name}.json`);
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
rmSync(path);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** List all configured marketplaces */
|
|
78
|
+
export function listMarketplaces() {
|
|
79
|
+
if (!existsSync(MARKETPLACE_DIR))
|
|
80
|
+
return [];
|
|
81
|
+
return readdirSync(MARKETPLACE_DIR)
|
|
82
|
+
.filter(f => f.endsWith('.json'))
|
|
83
|
+
.map(f => {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(readFileSync(join(MARKETPLACE_DIR, f), 'utf-8'));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.filter((m) => m !== null);
|
|
92
|
+
}
|
|
93
|
+
/** Search all marketplaces for plugins matching a query */
|
|
94
|
+
export function searchMarketplace(query) {
|
|
95
|
+
const q = query.toLowerCase();
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const mp of listMarketplaces()) {
|
|
98
|
+
for (const plugin of mp.plugins) {
|
|
99
|
+
if (plugin.name.toLowerCase().includes(q) ||
|
|
100
|
+
plugin.description.toLowerCase().includes(q) ||
|
|
101
|
+
plugin.keywords?.some(k => k.toLowerCase().includes(q))) {
|
|
102
|
+
results.push({ ...plugin, marketplace: mp.name });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
// ── Plugin Installation ──
|
|
109
|
+
/** Install a plugin from a marketplace */
|
|
110
|
+
export function installPlugin(pluginName, marketplaceName) {
|
|
111
|
+
// Find the plugin in marketplaces
|
|
112
|
+
const marketplaces = listMarketplaces();
|
|
113
|
+
let entry = null;
|
|
114
|
+
let fromMarketplace = '';
|
|
115
|
+
for (const mp of marketplaces) {
|
|
116
|
+
if (marketplaceName && mp.name !== marketplaceName)
|
|
117
|
+
continue;
|
|
118
|
+
const found = mp.plugins.find(p => p.name === pluginName);
|
|
119
|
+
if (found) {
|
|
120
|
+
entry = found;
|
|
121
|
+
fromMarketplace = mp.name;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!entry)
|
|
126
|
+
return null;
|
|
127
|
+
// Download to cache
|
|
128
|
+
const cacheDir = join(PLUGIN_CACHE_DIR, entry.name, entry.version);
|
|
129
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
130
|
+
try {
|
|
131
|
+
switch (entry.source.type) {
|
|
132
|
+
case 'github': {
|
|
133
|
+
// Clone the repo to cache
|
|
134
|
+
execSync(`git clone --depth 1 "https://github.com/${entry.source.repo}.git" "${cacheDir}"`, { stdio: 'pipe', timeout: 30_000 });
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'npm': {
|
|
138
|
+
// Install npm package to cache
|
|
139
|
+
execSync(`npm pack "${entry.source.package}" --pack-destination "${cacheDir}"`, { stdio: 'pipe', timeout: 30_000 });
|
|
140
|
+
// Extract the tarball
|
|
141
|
+
const tgz = readdirSync(cacheDir).find(f => f.endsWith('.tgz'));
|
|
142
|
+
if (tgz) {
|
|
143
|
+
execSync(`tar xzf "${join(cacheDir, tgz)}" -C "${cacheDir}" --strip-components=1`, { stdio: 'pipe' });
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'url': {
|
|
148
|
+
execSync(`curl -sL "${entry.source.url}" -o "${join(cacheDir, 'plugin.tar.gz')}"`, { stdio: 'pipe', timeout: 30_000 });
|
|
149
|
+
execSync(`tar xzf "${join(cacheDir, 'plugin.tar.gz')}" -C "${cacheDir}"`, { stdio: 'pipe' });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Clean up failed install
|
|
156
|
+
try {
|
|
157
|
+
rmSync(cacheDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
catch { /* ignore */ }
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
// Record installation
|
|
163
|
+
const installed = {
|
|
164
|
+
name: entry.name,
|
|
165
|
+
version: entry.version,
|
|
166
|
+
marketplace: fromMarketplace,
|
|
167
|
+
installedAt: Date.now(),
|
|
168
|
+
cachePath: cacheDir,
|
|
169
|
+
};
|
|
170
|
+
saveInstalledPlugin(installed);
|
|
171
|
+
return installed;
|
|
172
|
+
}
|
|
173
|
+
/** Uninstall a plugin */
|
|
174
|
+
export function uninstallPlugin(name) {
|
|
175
|
+
const installed = getInstalledPlugins();
|
|
176
|
+
const plugin = installed.find(p => p.name === name);
|
|
177
|
+
if (!plugin)
|
|
178
|
+
return false;
|
|
179
|
+
// Remove from cache
|
|
180
|
+
try {
|
|
181
|
+
rmSync(plugin.cachePath, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ }
|
|
184
|
+
// Remove from installed list
|
|
185
|
+
const remaining = installed.filter(p => p.name !== name);
|
|
186
|
+
saveInstalledPluginList(remaining);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
/** Get all installed plugins */
|
|
190
|
+
export function getInstalledPlugins() {
|
|
191
|
+
if (!existsSync(INSTALLED_PLUGINS_FILE))
|
|
192
|
+
return [];
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(readFileSync(INSTALLED_PLUGINS_FILE, 'utf-8'));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function saveInstalledPlugin(plugin) {
|
|
201
|
+
const installed = getInstalledPlugins();
|
|
202
|
+
// Replace existing version
|
|
203
|
+
const idx = installed.findIndex(p => p.name === plugin.name);
|
|
204
|
+
if (idx >= 0)
|
|
205
|
+
installed[idx] = plugin;
|
|
206
|
+
else
|
|
207
|
+
installed.push(plugin);
|
|
208
|
+
saveInstalledPluginList(installed);
|
|
209
|
+
}
|
|
210
|
+
function saveInstalledPluginList(plugins) {
|
|
211
|
+
const dir = join(homedir(), '.oh', 'plugins');
|
|
212
|
+
mkdirSync(dir, { recursive: true });
|
|
213
|
+
writeFileSync(INSTALLED_PLUGINS_FILE, JSON.stringify(plugins, null, 2));
|
|
214
|
+
}
|
|
215
|
+
// ── Formatting ──
|
|
216
|
+
/** Format marketplace entries for display */
|
|
217
|
+
export function formatMarketplaceSearch(results) {
|
|
218
|
+
if (results.length === 0)
|
|
219
|
+
return 'No plugins found.';
|
|
220
|
+
const lines = [`Found ${results.length} plugin(s):\n`];
|
|
221
|
+
for (const r of results) {
|
|
222
|
+
lines.push(` ${r.name}@${r.version} [${r.marketplace}]`);
|
|
223
|
+
lines.push(` ${r.description}`);
|
|
224
|
+
if (r.author)
|
|
225
|
+
lines.push(` by ${r.author}`);
|
|
226
|
+
lines.push('');
|
|
227
|
+
}
|
|
228
|
+
lines.push('Install with: /plugin install <name>');
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
/** Format installed plugins for display */
|
|
232
|
+
export function formatInstalledPlugins(plugins) {
|
|
233
|
+
if (plugins.length === 0)
|
|
234
|
+
return 'No plugins installed from marketplaces.';
|
|
235
|
+
const lines = [`Installed Plugins (${plugins.length}):\n`];
|
|
236
|
+
for (const p of plugins) {
|
|
237
|
+
const age = Math.round((Date.now() - p.installedAt) / (1000 * 60 * 60 * 24));
|
|
238
|
+
lines.push(` ${p.name}@${p.version} [${p.marketplace}] ${age}d ago`);
|
|
239
|
+
}
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=marketplace.js.map
|
|
@@ -42,7 +42,7 @@ export type AgentTeamConfig = {
|
|
|
42
42
|
tools?: string[];
|
|
43
43
|
}>;
|
|
44
44
|
};
|
|
45
|
-
/** Discover all available skills from project + global dirs */
|
|
45
|
+
/** Discover all available skills from project + global dirs + installed plugins */
|
|
46
46
|
export declare function discoverSkills(): SkillMetadata[];
|
|
47
47
|
/** Find a skill by name (case-insensitive) */
|
|
48
48
|
export declare function findSkill(name: string): SkillMetadata | null;
|
package/dist/harness/plugins.js
CHANGED
|
@@ -67,11 +67,25 @@ function loadSkillsFromDir(dir, source) {
|
|
|
67
67
|
})
|
|
68
68
|
.filter((s) => s !== null);
|
|
69
69
|
}
|
|
70
|
-
/** Discover all available skills from project + global dirs */
|
|
70
|
+
/** Discover all available skills from project + global dirs + installed plugins */
|
|
71
71
|
export function discoverSkills() {
|
|
72
72
|
const skills = [];
|
|
73
73
|
skills.push(...loadSkillsFromDir(PROJECT_SKILLS_DIR, 'project'));
|
|
74
74
|
skills.push(...loadSkillsFromDir(GLOBAL_SKILLS_DIR, 'global'));
|
|
75
|
+
// Load skills from installed marketplace plugins (namespaced as plugin-name:skill-name)
|
|
76
|
+
try {
|
|
77
|
+
const { getInstalledPlugins } = require('./marketplace.js');
|
|
78
|
+
for (const plugin of getInstalledPlugins()) {
|
|
79
|
+
const pluginSkillsDir = join(plugin.cachePath, 'skills');
|
|
80
|
+
const pluginSkills = loadSkillsFromDir(pluginSkillsDir, 'plugin');
|
|
81
|
+
// Namespace: prefix skill name with plugin name
|
|
82
|
+
for (const skill of pluginSkills) {
|
|
83
|
+
skill.name = `${plugin.name}:${skill.name}`;
|
|
84
|
+
}
|
|
85
|
+
skills.push(...pluginSkills);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { /* marketplace module may not be loaded yet */ }
|
|
75
89
|
return skills;
|
|
76
90
|
}
|
|
77
91
|
/** Find a skill by name (case-insensitive) */
|
package/dist/harness/rules.js
CHANGED
|
@@ -65,13 +65,28 @@ export function loadRules(projectPath) {
|
|
|
65
65
|
if (content)
|
|
66
66
|
rules.push(content);
|
|
67
67
|
}
|
|
68
|
-
// 4. Project rules/*.md
|
|
68
|
+
// 4. Project rules/*.md (with optional path-scoped filtering)
|
|
69
69
|
const rulesDir = join(root, ".oh", "rules");
|
|
70
70
|
if (existsSync(rulesDir)) {
|
|
71
71
|
for (const file of readdirSync(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
72
|
+
const raw = readSafe(join(rulesDir, file));
|
|
73
|
+
if (!raw)
|
|
74
|
+
continue;
|
|
75
|
+
// Check for paths frontmatter: only include if matching current context
|
|
76
|
+
const pathsMatch = raw.match(/^---\n[\s\S]*?^paths:\s*(.+)$/m);
|
|
77
|
+
if (pathsMatch) {
|
|
78
|
+
// Path-scoped rule — strip frontmatter and only include if glob matches
|
|
79
|
+
const pattern = pathsMatch[1].trim();
|
|
80
|
+
const fmEnd = raw.indexOf('---', raw.indexOf('---') + 3);
|
|
81
|
+
const content = fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : raw;
|
|
82
|
+
if (content && matchesPathGlob(root, pattern)) {
|
|
83
|
+
rules.push(content);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// No paths restriction — always include
|
|
88
|
+
rules.push(raw);
|
|
89
|
+
}
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
// 5. CLAUDE.local.md (personal overrides, typically gitignored)
|
|
@@ -110,4 +125,17 @@ function readSafe(path) {
|
|
|
110
125
|
return "";
|
|
111
126
|
}
|
|
112
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if any file in the project matches a glob pattern.
|
|
130
|
+
* Simple implementation: checks if the pattern directory exists.
|
|
131
|
+
* For `src/api/**`, checks if `src/api/` exists.
|
|
132
|
+
*/
|
|
133
|
+
function matchesPathGlob(root, pattern) {
|
|
134
|
+
// Extract the directory portion before any wildcard
|
|
135
|
+
const dirPart = pattern.split('*')[0].replace(/\/+$/, '');
|
|
136
|
+
if (!dirPart)
|
|
137
|
+
return true; // Pattern like "**/*.ts" matches everything
|
|
138
|
+
const fullDir = join(root, dirPart);
|
|
139
|
+
return existsSync(fullDir);
|
|
140
|
+
}
|
|
113
141
|
//# sourceMappingURL=rules.js.map
|
|
@@ -69,14 +69,30 @@ export async function handleUserInput(input, ctx) {
|
|
|
69
69
|
}
|
|
70
70
|
// Normal prompt — add user message
|
|
71
71
|
messages = [...messages, createUserMessage(input)];
|
|
72
|
-
// Resolve @mentions
|
|
72
|
+
// Resolve @mentions — local files first, then MCP resources
|
|
73
73
|
let resolvedInput = input;
|
|
74
|
-
const mentionPattern = /@(\w[\w
|
|
74
|
+
const mentionPattern = /@([\w][\w./-]*)/g;
|
|
75
75
|
const mentions = [...input.matchAll(mentionPattern)].map(m => m[1]);
|
|
76
76
|
const companionName = ctx.companionConfig?.soul?.name?.toLowerCase();
|
|
77
77
|
for (const mention of mentions) {
|
|
78
78
|
if (companionName && mention.toLowerCase() === companionName)
|
|
79
79
|
continue;
|
|
80
|
+
// Try local file first (supports paths like @src/main.ts, @README.md)
|
|
81
|
+
try {
|
|
82
|
+
const { existsSync, readFileSync } = await import('node:fs');
|
|
83
|
+
const { resolve } = await import('node:path');
|
|
84
|
+
const filePath = resolve(process.cwd(), mention);
|
|
85
|
+
if (existsSync(filePath)) {
|
|
86
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
87
|
+
const truncated = content.length > 10_000
|
|
88
|
+
? content.slice(0, 10_000) + '\n[...truncated]'
|
|
89
|
+
: content;
|
|
90
|
+
resolvedInput += `\n\n[File @${mention}]:\n${truncated}`;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* ignore */ }
|
|
95
|
+
// Fall back to MCP resource
|
|
80
96
|
try {
|
|
81
97
|
const content = await resolveMcpMention(mention);
|
|
82
98
|
if (content)
|
package/dist/main.js
CHANGED
|
@@ -233,6 +233,7 @@ program
|
|
|
233
233
|
.option("--fork <id>", "Fork (branch) from an existing session")
|
|
234
234
|
.option("--light", "Use light theme")
|
|
235
235
|
.option("--output-format <format>", "Output format for -p mode (text, json, stream-json)", "text")
|
|
236
|
+
.option("--json-schema <schema>", "Constrain output to match a JSON schema (headless mode)")
|
|
236
237
|
.action(async (opts) => {
|
|
237
238
|
// Load saved config as defaults (env vars + CLI flags override)
|
|
238
239
|
const savedConfig = readOhConfig();
|
package/dist/query/compress.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createUserMessage } from "../types/message.js";
|
|
6
6
|
import { defaultEstimateTokens } from "../providers/base.js";
|
|
7
|
+
import { emitHook } from "../harness/hooks.js";
|
|
7
8
|
const DEFAULT_KEEP_LAST = 10;
|
|
8
9
|
/**
|
|
9
10
|
* Semantic importance scoring for messages.
|
|
@@ -61,6 +62,7 @@ export function estimateMessagesTokens(messages, estimateTokens = (t) => Math.ce
|
|
|
61
62
|
export function compressMessages(messages, targetTokens) {
|
|
62
63
|
if (messages.length <= 2)
|
|
63
64
|
return messages;
|
|
65
|
+
emitHook("preCompact", {});
|
|
64
66
|
const result = [...messages];
|
|
65
67
|
const keepLast = DEFAULT_KEEP_LAST;
|
|
66
68
|
// MicroCompact: Truncate long tool results and assistant messages
|
|
@@ -114,12 +116,14 @@ export function compressMessages(messages, targetTokens) {
|
|
|
114
116
|
validCallIds.add(tc.id);
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
|
-
|
|
119
|
+
const filtered = result.filter((msg) => {
|
|
118
120
|
if (msg.role !== "tool")
|
|
119
121
|
return true;
|
|
120
122
|
return (msg.toolResults?.length ?? 0) > 0 &&
|
|
121
123
|
msg.toolResults.every((tr) => validCallIds.has(tr.callId));
|
|
122
124
|
});
|
|
125
|
+
emitHook("postCompact", {});
|
|
126
|
+
return filtered;
|
|
123
127
|
}
|
|
124
128
|
/**
|
|
125
129
|
* LLM-assisted summarization of older messages.
|
package/dist/query/tools.js
CHANGED
|
@@ -85,6 +85,13 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
|
|
|
85
85
|
toolArgs: JSON.stringify(toolCall.arguments).slice(0, 1000),
|
|
86
86
|
toolOutput: result.output.slice(0, 1000),
|
|
87
87
|
});
|
|
88
|
+
// Emit fileChanged hook for file-modifying tools
|
|
89
|
+
if (!result.isError && ['Edit', 'Write', 'MultiEdit'].includes(tool.name)) {
|
|
90
|
+
const filePaths = getAffectedFiles(tool.name, parsed.data);
|
|
91
|
+
for (const fp of filePaths) {
|
|
92
|
+
emitHook("fileChanged", { filePath: fp, toolName: tool.name });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
88
95
|
// Verification loop: auto-run lint/typecheck after file-modifying tools
|
|
89
96
|
let verificationSuffix = '';
|
|
90
97
|
if (!result.isError && ['Edit', 'Write', 'MultiEdit'].includes(tool.name)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createWorktree, removeWorktree, hasWorktreeChanges, isGitRepo } from "../../git/index.js";
|
|
3
|
+
import { emitHook } from "../../harness/hooks.js";
|
|
3
4
|
const inputSchema = z.object({
|
|
4
5
|
prompt: z.string(),
|
|
5
6
|
description: z.string().optional(),
|
|
@@ -80,9 +81,11 @@ export const AgentTool = {
|
|
|
80
81
|
maxTurns: 20,
|
|
81
82
|
abortSignal: context.abortSignal,
|
|
82
83
|
};
|
|
84
|
+
const agentId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
85
|
+
emitHook("subagentStart", { agentId, toolName: input.subagent_type ?? 'general' });
|
|
83
86
|
// Background execution: start agent and return immediately
|
|
84
87
|
if (input.run_in_background) {
|
|
85
|
-
const bgId =
|
|
88
|
+
const bgId = agentId;
|
|
86
89
|
const runAgent = async () => {
|
|
87
90
|
let finalText = "";
|
|
88
91
|
const originalCwd = process.cwd();
|
|
@@ -190,6 +193,7 @@ export const AgentTool = {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
}
|
|
196
|
+
emitHook("subagentStop", { agentId });
|
|
193
197
|
return { output: finalText || "(sub-agent completed with no text output)", isError: false };
|
|
194
198
|
},
|
|
195
199
|
prompt() {
|
|
@@ -2,17 +2,17 @@ import { z } from "zod";
|
|
|
2
2
|
import type { Tool } from "../../Tool.js";
|
|
3
3
|
declare const inputSchema: z.ZodObject<{
|
|
4
4
|
file_path: z.ZodString;
|
|
5
|
-
action: z.ZodDefault<z.ZodEnum<["diagnostics", "definition", "references"]>>;
|
|
5
|
+
action: z.ZodDefault<z.ZodEnum<["diagnostics", "definition", "references", "hover"]>>;
|
|
6
6
|
line: z.ZodOptional<z.ZodNumber>;
|
|
7
7
|
character: z.ZodOptional<z.ZodNumber>;
|
|
8
8
|
}, "strip", z.ZodTypeAny, {
|
|
9
|
-
action: "diagnostics" | "definition" | "references";
|
|
10
9
|
file_path: string;
|
|
10
|
+
action: "diagnostics" | "definition" | "references" | "hover";
|
|
11
11
|
line?: number | undefined;
|
|
12
12
|
character?: number | undefined;
|
|
13
13
|
}, {
|
|
14
14
|
file_path: string;
|
|
15
|
-
action?: "diagnostics" | "definition" | "references" | undefined;
|
|
15
|
+
action?: "diagnostics" | "definition" | "references" | "hover" | undefined;
|
|
16
16
|
line?: number | undefined;
|
|
17
17
|
character?: number | undefined;
|
|
18
18
|
}>;
|
|
@@ -2,8 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { LspClient } from "../../lsp/client.js";
|
|
3
3
|
const inputSchema = z.object({
|
|
4
4
|
file_path: z.string().describe("Absolute path to the file to check"),
|
|
5
|
-
action: z.enum(["diagnostics", "definition", "references"]).default("diagnostics")
|
|
6
|
-
.describe("Action: diagnostics (errors/warnings), definition (go-to-def), references (find-refs)"),
|
|
5
|
+
action: z.enum(["diagnostics", "definition", "references", "hover"]).default("diagnostics")
|
|
6
|
+
.describe("Action: diagnostics (errors/warnings), definition (go-to-def), references (find-refs), hover (type info)"),
|
|
7
7
|
line: z.number().optional().describe("Line number (0-indexed) for definition/references"),
|
|
8
8
|
character: z.number().optional().describe("Column number (0-indexed) for definition/references"),
|
|
9
9
|
});
|
|
@@ -16,6 +16,12 @@ function getLspCommand(filePath) {
|
|
|
16
16
|
if (filePath.endsWith('.py')) {
|
|
17
17
|
return { command: 'pylsp', args: [] };
|
|
18
18
|
}
|
|
19
|
+
if (filePath.endsWith('.go')) {
|
|
20
|
+
return { command: 'gopls', args: ['serve'] };
|
|
21
|
+
}
|
|
22
|
+
if (filePath.endsWith('.rs')) {
|
|
23
|
+
return { command: 'rust-analyzer', args: [] };
|
|
24
|
+
}
|
|
19
25
|
return null;
|
|
20
26
|
}
|
|
21
27
|
async function getClient(filePath, workingDir) {
|
|
@@ -84,6 +90,28 @@ export const DiagnosticsTool = {
|
|
|
84
90
|
const lines = refs.map(r => `${r.uri.replace('file://', '')}:${r.range.start.line + 1}:${r.range.start.character}`);
|
|
85
91
|
return { output: `${refs.length} reference(s):\n${lines.join('\n')}`, isError: false };
|
|
86
92
|
}
|
|
93
|
+
if (input.action === "hover") {
|
|
94
|
+
if (input.line === undefined || input.character === undefined) {
|
|
95
|
+
return { output: "line and character are required for hover.", isError: true };
|
|
96
|
+
}
|
|
97
|
+
await client.openFile(input.file_path);
|
|
98
|
+
// Hover uses textDocument/hover which returns MarkupContent
|
|
99
|
+
try {
|
|
100
|
+
const result = await client.send('textDocument/hover', {
|
|
101
|
+
textDocument: { uri: `file://${input.file_path.replace(/\\/g, '/')}` },
|
|
102
|
+
position: { line: input.line, character: input.character },
|
|
103
|
+
});
|
|
104
|
+
if (!result || !result.contents)
|
|
105
|
+
return { output: "No hover information.", isError: false };
|
|
106
|
+
const content = typeof result.contents === 'string'
|
|
107
|
+
? result.contents
|
|
108
|
+
: result.contents.value ?? JSON.stringify(result.contents);
|
|
109
|
+
return { output: content, isError: false };
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { output: "Hover not supported by this language server.", isError: false };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
87
115
|
return { output: `Unknown action: ${input.action}`, isError: true };
|
|
88
116
|
}
|
|
89
117
|
catch (err) {
|
|
@@ -94,15 +122,16 @@ export const DiagnosticsTool = {
|
|
|
94
122
|
}
|
|
95
123
|
},
|
|
96
124
|
prompt() {
|
|
97
|
-
return `Get code intelligence from the language server. Actions:
|
|
125
|
+
return `Get code intelligence from the language server. Supports TypeScript, JavaScript, Python, Go, and Rust. Actions:
|
|
98
126
|
- diagnostics: Get errors and warnings for a file
|
|
99
|
-
- definition: Go to definition of a symbol at a given position
|
|
100
|
-
- references: Find all references to a symbol at a given position
|
|
127
|
+
- definition: Go to definition of a symbol at a given position
|
|
128
|
+
- references: Find all references to a symbol at a given position
|
|
129
|
+
- hover: Get type information and documentation for a symbol
|
|
101
130
|
Parameters:
|
|
102
131
|
- file_path (string, required): Absolute path to the file
|
|
103
|
-
- action (string): "diagnostics" | "definition" | "references" (default: diagnostics)
|
|
104
|
-
- line (number, optional): 0-indexed line for definition/references
|
|
105
|
-
- character (number, optional): 0-indexed column for definition/references`;
|
|
132
|
+
- action (string): "diagnostics" | "definition" | "references" | "hover" (default: diagnostics)
|
|
133
|
+
- line (number, optional): 0-indexed line for definition/references/hover
|
|
134
|
+
- character (number, optional): 0-indexed column for definition/references/hover`;
|
|
106
135
|
},
|
|
107
136
|
};
|
|
108
137
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Tool } from "../../Tool.js";
|
|
3
|
+
declare const inputSchema: z.ZodObject<{
|
|
4
|
+
command: z.ZodString;
|
|
5
|
+
pattern: z.ZodOptional<z.ZodString>;
|
|
6
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
maxLines: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
command: string;
|
|
10
|
+
pattern?: string | undefined;
|
|
11
|
+
timeout?: number | undefined;
|
|
12
|
+
maxLines?: number | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
command: string;
|
|
15
|
+
pattern?: string | undefined;
|
|
16
|
+
timeout?: number | undefined;
|
|
17
|
+
maxLines?: number | undefined;
|
|
18
|
+
}>;
|
|
19
|
+
export declare const MonitorTool: Tool<typeof inputSchema>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|