@zhijiewang/openharness 2.21.0 → 2.22.1
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/harness/api-key-helper.d.ts +32 -0
- package/dist/harness/api-key-helper.js +70 -0
- package/dist/harness/config.d.ts +25 -0
- package/dist/harness/credentials.d.ts +6 -4
- package/dist/harness/credentials.js +15 -4
- package/dist/harness/hooks.d.ts +11 -1
- package/dist/harness/hooks.js +10 -0
- package/dist/lsp/client.d.ts +9 -0
- package/dist/lsp/client.js +50 -0
- package/dist/main.js +176 -63
- package/dist/mcp/elicitation.d.ts +66 -0
- package/dist/mcp/elicitation.js +88 -0
- package/dist/mcp/roots.d.ts +36 -0
- package/dist/mcp/roots.js +56 -0
- package/dist/mcp/transport.js +45 -3
- package/dist/providers/index.d.ts +25 -1
- package/dist/providers/index.js +27 -2
- package/dist/query/index.js +1 -1
- package/dist/query/tools.d.ts +2 -2
- package/dist/query/tools.js +68 -4
- package/dist/query/types.d.ts +10 -0
- package/dist/tools/DiagnosticsTool/index.js +10 -22
- package/dist/utils/install-method.d.ts +42 -0
- package/dist/utils/install-method.js +110 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the configured `apiKeyHelper` script and return its trimmed stdout as
|
|
3
|
+
* the API key (audit B8). Mirrors Claude Code's `apiKeyHelper`.
|
|
4
|
+
*
|
|
5
|
+
* Invocation:
|
|
6
|
+
* - shell: true (so `helper-script.sh` and pipelines work without an explicit shell)
|
|
7
|
+
* - 5s timeout (helper should be fast — it's invoked at credential-fetch time)
|
|
8
|
+
* - OH_PROVIDER env var set so a single helper can dispatch by provider
|
|
9
|
+
* - stderr captured and surfaced on failure
|
|
10
|
+
*
|
|
11
|
+
* Failure modes — all return undefined (caller falls through to legacy config):
|
|
12
|
+
* - non-zero exit code
|
|
13
|
+
* - timeout
|
|
14
|
+
* - empty stdout
|
|
15
|
+
* - spawn error (helper not found, permission denied, etc.)
|
|
16
|
+
*
|
|
17
|
+
* Failures are logged via `debug("config", ...)` so users can opt into
|
|
18
|
+
* visibility with `--debug config` without polluting normal output.
|
|
19
|
+
*/
|
|
20
|
+
export interface RunApiKeyHelperOptions {
|
|
21
|
+
/** Provider name passed to the helper as `OH_PROVIDER`. */
|
|
22
|
+
provider: string;
|
|
23
|
+
/** Spawn timeout in ms. Defaults to 5_000. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execute `command` via the user's shell with `OH_PROVIDER` set, return the
|
|
28
|
+
* trimmed stdout on success, undefined on any failure. Pure side-effect-only —
|
|
29
|
+
* no caching here; resolveApiKey owns lifetime.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runApiKeyHelper(command: string, opts: RunApiKeyHelperOptions): string | undefined;
|
|
32
|
+
//# sourceMappingURL=api-key-helper.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the configured `apiKeyHelper` script and return its trimmed stdout as
|
|
3
|
+
* the API key (audit B8). Mirrors Claude Code's `apiKeyHelper`.
|
|
4
|
+
*
|
|
5
|
+
* Invocation:
|
|
6
|
+
* - shell: true (so `helper-script.sh` and pipelines work without an explicit shell)
|
|
7
|
+
* - 5s timeout (helper should be fast — it's invoked at credential-fetch time)
|
|
8
|
+
* - OH_PROVIDER env var set so a single helper can dispatch by provider
|
|
9
|
+
* - stderr captured and surfaced on failure
|
|
10
|
+
*
|
|
11
|
+
* Failure modes — all return undefined (caller falls through to legacy config):
|
|
12
|
+
* - non-zero exit code
|
|
13
|
+
* - timeout
|
|
14
|
+
* - empty stdout
|
|
15
|
+
* - spawn error (helper not found, permission denied, etc.)
|
|
16
|
+
*
|
|
17
|
+
* Failures are logged via `debug("config", ...)` so users can opt into
|
|
18
|
+
* visibility with `--debug config` without polluting normal output.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { debug } from "../utils/debug.js";
|
|
22
|
+
/**
|
|
23
|
+
* Execute `command` via the user's shell with `OH_PROVIDER` set, return the
|
|
24
|
+
* trimmed stdout on success, undefined on any failure. Pure side-effect-only —
|
|
25
|
+
* no caching here; resolveApiKey owns lifetime.
|
|
26
|
+
*/
|
|
27
|
+
export function runApiKeyHelper(command, opts) {
|
|
28
|
+
const timeout = opts.timeoutMs ?? 5_000;
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync(command, {
|
|
31
|
+
shell: true,
|
|
32
|
+
timeout,
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
env: { ...process.env, OH_PROVIDER: opts.provider },
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
});
|
|
37
|
+
if (result.error) {
|
|
38
|
+
debug("config", "apiKeyHelper spawn failed", { provider: opts.provider, err: result.error.message });
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (result.signal === "SIGTERM") {
|
|
42
|
+
debug("config", "apiKeyHelper timed out", { provider: opts.provider, timeoutMs: timeout });
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
const stderr = (result.stderr ?? "").toString().trim().slice(0, 500);
|
|
47
|
+
debug("config", "apiKeyHelper non-zero exit", {
|
|
48
|
+
provider: opts.provider,
|
|
49
|
+
exit: result.status,
|
|
50
|
+
stderr,
|
|
51
|
+
});
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const out = (result.stdout ?? "").toString().trim();
|
|
55
|
+
if (!out) {
|
|
56
|
+
debug("config", "apiKeyHelper produced empty stdout", { provider: opts.provider });
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
debug("config", "apiKeyHelper resolved", { provider: opts.provider, length: out.length });
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
debug("config", "apiKeyHelper threw", {
|
|
64
|
+
provider: opts.provider,
|
|
65
|
+
err: err instanceof Error ? err.message : String(err),
|
|
66
|
+
});
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=api-key-helper.js.map
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -80,6 +80,19 @@ export type HooksConfig = {
|
|
|
80
80
|
worktreeCreate?: HookDef[];
|
|
81
81
|
/** Fires after ExitWorktreeTool successfully removes a git worktree. */
|
|
82
82
|
worktreeRemove?: HookDef[];
|
|
83
|
+
/**
|
|
84
|
+
* Fires when an MCP server issues an `elicitation/create` request — before
|
|
85
|
+
* any decision is made. Hook can return `permissionDecision: "allow"` to
|
|
86
|
+
* accept (sends `{action: "accept", content: {}}` to the server) or `"deny"`
|
|
87
|
+
* to decline. No decision falls through to the interactive handler (REPL)
|
|
88
|
+
* or, if absent, to a fail-safe `decline`.
|
|
89
|
+
*/
|
|
90
|
+
elicitation?: HookDef[];
|
|
91
|
+
/**
|
|
92
|
+
* Fires after the elicitation response has been decided — symmetric to
|
|
93
|
+
* `elicitation`. Useful for audit trails that want the request/response pair.
|
|
94
|
+
*/
|
|
95
|
+
elicitationResult?: HookDef[];
|
|
83
96
|
/** Fires once per system-prompt build after CLAUDE.md / global-rules / project RULES.md / user profile have been concatenated. Useful for audit trails. */
|
|
84
97
|
instructionsLoaded?: HookDef[];
|
|
85
98
|
};
|
|
@@ -124,6 +137,18 @@ export type OhConfig = {
|
|
|
124
137
|
* Claude Code's `disableAllHooks` setting.
|
|
125
138
|
*/
|
|
126
139
|
disableAllHooks?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Script invoked at credential-fetch time to produce an API key on stdout.
|
|
142
|
+
* Avoids storing keys in plaintext config or the encrypted store. Inserted
|
|
143
|
+
* between the encrypted-store and legacy-config steps in `resolveApiKey`.
|
|
144
|
+
* Mirrors Claude Code's `apiKeyHelper`.
|
|
145
|
+
*
|
|
146
|
+
* The configured command runs through the user's shell with a 5s timeout;
|
|
147
|
+
* stderr is captured and surfaced on failure. The provider name is passed
|
|
148
|
+
* via the `OH_PROVIDER` env var so a single helper can dispatch by provider
|
|
149
|
+
* (`if [ "$OH_PROVIDER" = "anthropic" ]; then ... fi`).
|
|
150
|
+
*/
|
|
151
|
+
apiKeyHelper?: string;
|
|
127
152
|
toolPermissions?: ToolPermissionRule[];
|
|
128
153
|
statusLineFormat?: string;
|
|
129
154
|
/** Verification loops — auto-run lint/typecheck after file edits */
|
|
@@ -15,10 +15,12 @@ export declare function deleteCredential(key: string): void;
|
|
|
15
15
|
/** List credential keys (not values) */
|
|
16
16
|
export declare function listCredentials(): string[];
|
|
17
17
|
/**
|
|
18
|
-
* Get API key for a provider, checking:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Get API key for a provider, checking in priority order:
|
|
19
|
+
* 1. Environment variable
|
|
20
|
+
* 2. Encrypted credential store
|
|
21
|
+
* 3. `apiKeyHelper` config script (audit B8) — runs the configured command
|
|
22
|
+
* with `OH_PROVIDER` set; trimmed stdout is the key. Failures fall through.
|
|
23
|
+
* 4. Config file (legacy plaintext, with migration into the encrypted store)
|
|
22
24
|
*/
|
|
23
25
|
export declare function resolveApiKey(provider: string, configApiKey?: string): string | undefined;
|
|
24
26
|
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -10,6 +10,8 @@ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir, hostname, userInfo } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { runApiKeyHelper } from "./api-key-helper.js";
|
|
14
|
+
import { readOhConfig } from "./config.js";
|
|
13
15
|
const CRED_DIR = join(homedir(), ".oh");
|
|
14
16
|
const CRED_PATH = join(CRED_DIR, "credentials.enc");
|
|
15
17
|
const ALGORITHM = "aes-256-gcm";
|
|
@@ -74,10 +76,12 @@ export function listCredentials() {
|
|
|
74
76
|
return Object.keys(loadStore());
|
|
75
77
|
}
|
|
76
78
|
/**
|
|
77
|
-
* Get API key for a provider, checking:
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
79
|
+
* Get API key for a provider, checking in priority order:
|
|
80
|
+
* 1. Environment variable
|
|
81
|
+
* 2. Encrypted credential store
|
|
82
|
+
* 3. `apiKeyHelper` config script (audit B8) — runs the configured command
|
|
83
|
+
* with `OH_PROVIDER` set; trimmed stdout is the key. Failures fall through.
|
|
84
|
+
* 4. Config file (legacy plaintext, with migration into the encrypted store)
|
|
81
85
|
*/
|
|
82
86
|
export function resolveApiKey(provider, configApiKey) {
|
|
83
87
|
// Environment variable names by provider
|
|
@@ -93,6 +97,13 @@ export function resolveApiKey(provider, configApiKey) {
|
|
|
93
97
|
const stored = getCredential(`${provider}-api-key`);
|
|
94
98
|
if (stored)
|
|
95
99
|
return stored;
|
|
100
|
+
// apiKeyHelper script — let users plug in 1Password / pass / vault / etc.
|
|
101
|
+
const cfg = readOhConfig();
|
|
102
|
+
if (cfg?.apiKeyHelper) {
|
|
103
|
+
const fromHelper = runApiKeyHelper(cfg.apiKeyHelper, { provider });
|
|
104
|
+
if (fromHelper)
|
|
105
|
+
return fromHelper;
|
|
106
|
+
}
|
|
96
107
|
// Legacy config (migrate on use)
|
|
97
108
|
if (configApiKey) {
|
|
98
109
|
// Auto-migrate to encrypted store
|
package/dist/harness/hooks.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - prompt: LLM yes/no check via provider.complete()
|
|
11
11
|
*/
|
|
12
12
|
import type { HookDef, HooksConfig } from "./config.js";
|
|
13
|
-
export type HookEvent = "sessionStart" | "sessionEnd" | "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "userPromptExpansion" | "permissionRequest" | "permissionDenied" | "fileChanged" | "cwdChanged" | "subagentStart" | "subagentStop" | "preCompact" | "postCompact" | "configChange" | "notification" | "turnStart" | "turnStop" | "taskCreated" | "taskCompleted" | "worktreeCreate" | "worktreeRemove" | "instructionsLoaded";
|
|
13
|
+
export type HookEvent = "sessionStart" | "sessionEnd" | "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "userPromptExpansion" | "permissionRequest" | "permissionDenied" | "fileChanged" | "cwdChanged" | "subagentStart" | "subagentStop" | "preCompact" | "postCompact" | "configChange" | "notification" | "turnStart" | "turnStop" | "taskCreated" | "taskCompleted" | "worktreeCreate" | "worktreeRemove" | "elicitation" | "elicitationResult" | "instructionsLoaded";
|
|
14
14
|
export type HookContext = {
|
|
15
15
|
toolName?: string;
|
|
16
16
|
toolArgs?: string;
|
|
@@ -66,6 +66,16 @@ export type HookContext = {
|
|
|
66
66
|
worktreeParent?: string;
|
|
67
67
|
/** For worktreeRemove: whether `force: true` was passed to skip the dirty-state check */
|
|
68
68
|
worktreeForced?: string;
|
|
69
|
+
/** For elicitation/elicitationResult: the MCP server that issued the elicitation request */
|
|
70
|
+
elicitationServer?: string;
|
|
71
|
+
/** For elicitation/elicitationResult: human-readable message the server wants to show (capped at 500 chars) */
|
|
72
|
+
elicitationMessage?: string;
|
|
73
|
+
/** For elicitation: JSON-stringified `requestedSchema` from the server (capped at 2000 chars) */
|
|
74
|
+
elicitationSchema?: string;
|
|
75
|
+
/** For elicitationResult: the final action ("accept" | "decline" | "cancel") */
|
|
76
|
+
elicitationAction?: string;
|
|
77
|
+
/** For elicitationResult: JSON-stringified content payload returned to the server (when action="accept") */
|
|
78
|
+
elicitationContent?: string;
|
|
69
79
|
/** For instructionsLoaded: count of rules concatenated (as a string for env-var parity) */
|
|
70
80
|
rulesCount?: string;
|
|
71
81
|
/** For instructionsLoaded: total character length of the loaded rules */
|
package/dist/harness/hooks.js
CHANGED
|
@@ -90,6 +90,16 @@ function buildEnv(event, ctx) {
|
|
|
90
90
|
env.OH_WORKTREE_PARENT = ctx.worktreeParent;
|
|
91
91
|
if (ctx.worktreeForced !== undefined)
|
|
92
92
|
env.OH_WORKTREE_FORCED = ctx.worktreeForced;
|
|
93
|
+
if (ctx.elicitationServer !== undefined)
|
|
94
|
+
env.OH_ELICITATION_SERVER = ctx.elicitationServer;
|
|
95
|
+
if (ctx.elicitationMessage !== undefined)
|
|
96
|
+
env.OH_ELICITATION_MESSAGE = ctx.elicitationMessage;
|
|
97
|
+
if (ctx.elicitationSchema !== undefined)
|
|
98
|
+
env.OH_ELICITATION_SCHEMA = ctx.elicitationSchema;
|
|
99
|
+
if (ctx.elicitationAction !== undefined)
|
|
100
|
+
env.OH_ELICITATION_ACTION = ctx.elicitationAction;
|
|
101
|
+
if (ctx.elicitationContent !== undefined)
|
|
102
|
+
env.OH_ELICITATION_CONTENT = ctx.elicitationContent;
|
|
93
103
|
return env;
|
|
94
104
|
}
|
|
95
105
|
/**
|
package/dist/lsp/client.d.ts
CHANGED
|
@@ -51,7 +51,16 @@ export declare class LspClient {
|
|
|
51
51
|
getDefinition(filePath: string, line: number, character: number): Promise<Location[]>;
|
|
52
52
|
/** Find references */
|
|
53
53
|
getReferences(filePath: string, line: number, character: number): Promise<Location[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Hover at a position — returns the text content of the LSP hover response,
|
|
56
|
+
* or `null` if the server returned no hover information / doesn't support
|
|
57
|
+
* the `textDocument/hover` capability. The LSP `MarkupContent` envelope is
|
|
58
|
+
* unwrapped so callers see plain text or markdown.
|
|
59
|
+
*/
|
|
60
|
+
getHover(filePath: string, line: number, character: number): Promise<string | null>;
|
|
54
61
|
private guessLanguage;
|
|
62
|
+
/** @internal Exposed for unit tests of the hover-content unwrapper. */
|
|
63
|
+
static unwrapHoverContents(result: unknown): string | null;
|
|
55
64
|
disconnect(): void;
|
|
56
65
|
}
|
|
57
66
|
export {};
|
package/dist/lsp/client.js
CHANGED
|
@@ -4,6 +4,38 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
|
+
/**
|
|
8
|
+
* Unwrap a `textDocument/hover` result into a plain string. LSP allows three
|
|
9
|
+
* `contents` shapes: a bare string, a `{ kind, value }` envelope, or an
|
|
10
|
+
* array of either. Returns null when nothing is hoverable. Pure — exposed
|
|
11
|
+
* via `LspClient.unwrapHoverContents` for unit tests.
|
|
12
|
+
*/
|
|
13
|
+
function unwrapHoverContents(result) {
|
|
14
|
+
if (!result || typeof result !== "object")
|
|
15
|
+
return null;
|
|
16
|
+
const r = result;
|
|
17
|
+
if (!r.contents)
|
|
18
|
+
return null;
|
|
19
|
+
const c = r.contents;
|
|
20
|
+
if (typeof c === "string")
|
|
21
|
+
return c;
|
|
22
|
+
if (Array.isArray(c)) {
|
|
23
|
+
const parts = c.map((item) => {
|
|
24
|
+
if (typeof item === "string")
|
|
25
|
+
return item;
|
|
26
|
+
if (item && typeof item === "object" && typeof item.value === "string") {
|
|
27
|
+
return item.value;
|
|
28
|
+
}
|
|
29
|
+
return "";
|
|
30
|
+
});
|
|
31
|
+
const joined = parts.filter(Boolean).join("\n");
|
|
32
|
+
return joined || null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof c === "object" && typeof c.value === "string") {
|
|
35
|
+
return c.value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
7
39
|
export class LspClient {
|
|
8
40
|
proc;
|
|
9
41
|
nextId = 1;
|
|
@@ -150,6 +182,20 @@ export class LspClient {
|
|
|
150
182
|
});
|
|
151
183
|
return result ?? [];
|
|
152
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Hover at a position — returns the text content of the LSP hover response,
|
|
187
|
+
* or `null` if the server returned no hover information / doesn't support
|
|
188
|
+
* the `textDocument/hover` capability. The LSP `MarkupContent` envelope is
|
|
189
|
+
* unwrapped so callers see plain text or markdown.
|
|
190
|
+
*/
|
|
191
|
+
async getHover(filePath, line, character) {
|
|
192
|
+
const uri = `file://${filePath.replace(/\\/g, "/")}`;
|
|
193
|
+
const result = await this.send("textDocument/hover", {
|
|
194
|
+
textDocument: { uri },
|
|
195
|
+
position: { line, character },
|
|
196
|
+
});
|
|
197
|
+
return unwrapHoverContents(result);
|
|
198
|
+
}
|
|
153
199
|
guessLanguage(path) {
|
|
154
200
|
if (path.endsWith(".ts") || path.endsWith(".tsx"))
|
|
155
201
|
return "typescript";
|
|
@@ -165,6 +211,10 @@ export class LspClient {
|
|
|
165
211
|
return "java";
|
|
166
212
|
return "plaintext";
|
|
167
213
|
}
|
|
214
|
+
/** @internal Exposed for unit tests of the hover-content unwrapper. */
|
|
215
|
+
static unwrapHoverContents(result) {
|
|
216
|
+
return unwrapHoverContents(result);
|
|
217
|
+
}
|
|
168
218
|
disconnect() {
|
|
169
219
|
this.send("shutdown", {})
|
|
170
220
|
.then(() => {
|