oh-my-harness 0.16.0 → 0.17.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 +1 -0
- package/dist/cli/commands/config.d.ts +26 -0
- package/dist/cli/commands/config.js +81 -0
- package/dist/cli/commands/doctor.d.ts +7 -0
- package/dist/cli/commands/doctor.js +26 -1
- package/dist/cli/index.js +13 -0
- package/dist/nl/config-store.d.ts +7 -0
- package/dist/nl/config-store.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -473,6 +473,7 @@ oh-my-harness/
|
|
|
473
473
|
- [x] Pi ([pi.dev](https://pi.dev)) emitter — bridge extension (`.pi/extensions/omh-harness.ts`) reusing the same `.omh/hooks/*.sh`
|
|
474
474
|
- [x] `ask` mode — request approval before executing risky tools (Claude native prompt / Pi `ctx.ui.select`; Codex falls back to block)
|
|
475
475
|
- [x] `omh uninstall` — remove generated artifacts while preserving user content
|
|
476
|
+
- [x] `omh config` — view, reconfigure, or reset the saved AI provider (rotate expired keys, switch provider/model)
|
|
476
477
|
- [ ] Community harness.yaml registry — share and reuse configs
|
|
477
478
|
- [ ] `omh modify "change X"` — NL config editing
|
|
478
479
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ProviderConfig } from "../../nl/config-store.js";
|
|
2
|
+
export interface ConfigOptions {
|
|
3
|
+
/** Print the current provider config (masked) without changing anything. */
|
|
4
|
+
show?: boolean;
|
|
5
|
+
/** Delete the saved provider config. */
|
|
6
|
+
reset?: boolean;
|
|
7
|
+
/** Skip confirmation prompts. */
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Interactive provider setup. Injectable for tests; defaults to the clack TUI.
|
|
11
|
+
* Returns the saved config, or undefined if the user cancelled.
|
|
12
|
+
*/
|
|
13
|
+
setupRunner?: () => Promise<ProviderConfig | undefined>;
|
|
14
|
+
/** Confirmation prompt. Injectable for tests; defaults to the clack TUI. */
|
|
15
|
+
confirm?: (message: string) => Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
export interface ConfigResult {
|
|
18
|
+
exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `omh config`: view, reconfigure, or reset the saved AI provider used for
|
|
22
|
+
* natural-language mode (~/.omh/config.json). Without flags it shows the current
|
|
23
|
+
* config and launches the provider setup flow so a user can rotate an expired
|
|
24
|
+
* key or switch providers.
|
|
25
|
+
*/
|
|
26
|
+
export declare function configCommand(options?: ConfigOptions): Promise<ConfigResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadProviderConfig, deleteProviderConfig, maskApiKey, } from "../../nl/config-store.js";
|
|
3
|
+
const NO_CONFIG_MESSAGE = "No AI provider configured yet. Run `omh config` (or `omh init`) to set one up.";
|
|
4
|
+
function printSummary(config) {
|
|
5
|
+
console.log(chalk.bold("Current AI provider configuration:"));
|
|
6
|
+
console.log(` provider: ${chalk.cyan(config.provider)}`);
|
|
7
|
+
console.log(` method: ${chalk.cyan(config.method)}`);
|
|
8
|
+
if (config.method === "api") {
|
|
9
|
+
console.log(` model: ${chalk.cyan(config.model ?? "(default)")}`);
|
|
10
|
+
console.log(` api key: ${chalk.dim(maskApiKey(config.apiKey))}`);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
console.log(` command: ${chalk.cyan(config.cliCommand ?? config.provider)}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function defaultConfirm(message) {
|
|
17
|
+
const p = await import("@clack/prompts");
|
|
18
|
+
const answer = await p.confirm({ message });
|
|
19
|
+
if (p.isCancel(answer))
|
|
20
|
+
return false;
|
|
21
|
+
return answer === true;
|
|
22
|
+
}
|
|
23
|
+
async function defaultSetupRunner() {
|
|
24
|
+
const { runProviderSetup } = await import("../tui/provider-setup.js");
|
|
25
|
+
return runProviderSetup();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `omh config`: view, reconfigure, or reset the saved AI provider used for
|
|
29
|
+
* natural-language mode (~/.omh/config.json). Without flags it shows the current
|
|
30
|
+
* config and launches the provider setup flow so a user can rotate an expired
|
|
31
|
+
* key or switch providers.
|
|
32
|
+
*/
|
|
33
|
+
export async function configCommand(options = {}) {
|
|
34
|
+
// --show and --reset express contradictory intent; fail loudly rather than
|
|
35
|
+
// silently letting one win.
|
|
36
|
+
if (options.show && options.reset) {
|
|
37
|
+
console.error("`--show` and `--reset` cannot be used together.");
|
|
38
|
+
return { exitCode: 1 };
|
|
39
|
+
}
|
|
40
|
+
const existing = await loadProviderConfig();
|
|
41
|
+
// --show: read-only summary.
|
|
42
|
+
if (options.show) {
|
|
43
|
+
if (!existing) {
|
|
44
|
+
console.log(NO_CONFIG_MESSAGE);
|
|
45
|
+
return { exitCode: 0 };
|
|
46
|
+
}
|
|
47
|
+
printSummary(existing);
|
|
48
|
+
return { exitCode: 0 };
|
|
49
|
+
}
|
|
50
|
+
// --reset: delete the saved config.
|
|
51
|
+
if (options.reset) {
|
|
52
|
+
if (!existing) {
|
|
53
|
+
console.log(NO_CONFIG_MESSAGE);
|
|
54
|
+
return { exitCode: 0 };
|
|
55
|
+
}
|
|
56
|
+
const confirm = options.confirm ?? defaultConfirm;
|
|
57
|
+
const ok = options.yes ? true : await confirm("Delete the saved AI provider configuration?");
|
|
58
|
+
if (!ok) {
|
|
59
|
+
console.log("Aborted. Configuration left unchanged.");
|
|
60
|
+
return { exitCode: 0 };
|
|
61
|
+
}
|
|
62
|
+
await deleteProviderConfig();
|
|
63
|
+
console.log(chalk.green("Removed ~/.omh/config.json"));
|
|
64
|
+
return { exitCode: 0 };
|
|
65
|
+
}
|
|
66
|
+
// Default: show current config (if any), then reconfigure.
|
|
67
|
+
if (existing) {
|
|
68
|
+
printSummary(existing);
|
|
69
|
+
console.log("");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log("No AI provider configured yet. Let's set one up.\n");
|
|
73
|
+
}
|
|
74
|
+
const setupRunner = options.setupRunner ?? defaultSetupRunner;
|
|
75
|
+
const updated = await setupRunner();
|
|
76
|
+
if (!updated) {
|
|
77
|
+
// Setup was cancelled; nothing changed.
|
|
78
|
+
return { exitCode: 0 };
|
|
79
|
+
}
|
|
80
|
+
return { exitCode: 0 };
|
|
81
|
+
}
|
|
@@ -21,6 +21,13 @@ export interface DoctorResult {
|
|
|
21
21
|
* warning unless `strict` is set.
|
|
22
22
|
*/
|
|
23
23
|
inSync?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether an AI provider is configured for natural-language mode
|
|
26
|
+
* (~/.omh/config.json). Informational only — it never affects health, since
|
|
27
|
+
* the provider is global and optional. Surfaced so users can discover
|
|
28
|
+
* `omh config` to rotate an expired key or switch provider/model.
|
|
29
|
+
*/
|
|
30
|
+
providerConfigured: boolean;
|
|
24
31
|
messages: string[];
|
|
25
32
|
}
|
|
26
33
|
export declare function doctorCommand(options?: DoctorOptions): Promise<DoctorResult>;
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { parse } from "smol-toml";
|
|
4
4
|
import { OMH_HOOKS_DIR } from "../../utils/paths.js";
|
|
5
5
|
import { computeDrift, HarnessNotFoundError } from "../../core/drift.js";
|
|
6
|
+
import { loadProviderConfig } from "../../nl/config-store.js";
|
|
6
7
|
export async function doctorCommand(options = {}) {
|
|
7
8
|
const projectDir = options.projectDir ?? process.cwd();
|
|
8
9
|
const messages = [];
|
|
@@ -154,6 +155,10 @@ export async function doctorCommand(options = {}) {
|
|
|
154
155
|
}
|
|
155
156
|
// No harness.yaml → leave inSync undefined (drift not applicable).
|
|
156
157
|
}
|
|
158
|
+
// 9. AI provider config (~/.omh/config.json). Global and optional, so this is
|
|
159
|
+
// purely informational (INFO) and never affects health — it just surfaces
|
|
160
|
+
// `omh config` so users can rotate an expired key or switch provider/model.
|
|
161
|
+
const providerConfigured = await checkProviderConfig(messages);
|
|
157
162
|
const checksHealthy = Object.values(checks).every(Boolean);
|
|
158
163
|
const healthy = checksHealthy && (!options.strict || inSync !== false);
|
|
159
164
|
const exitCode = healthy ? 0 : 1;
|
|
@@ -169,5 +174,25 @@ export async function doctorCommand(options = {}) {
|
|
|
169
174
|
console.log(` ${msg}`);
|
|
170
175
|
}
|
|
171
176
|
}
|
|
172
|
-
return { healthy, exitCode, checks, inSync, messages };
|
|
177
|
+
return { healthy, exitCode, checks, inSync, providerConfigured, messages };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Inspects the global AI provider config and appends an INFO hint to `messages`.
|
|
181
|
+
* Returns whether a provider is configured. Never reads or echoes the API key.
|
|
182
|
+
*/
|
|
183
|
+
async function checkProviderConfig(messages) {
|
|
184
|
+
const config = await loadProviderConfig();
|
|
185
|
+
if (!config) {
|
|
186
|
+
messages.push("INFO: No AI provider configured for natural-language mode — run `omh config` to set one up.");
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (config.method === "api") {
|
|
190
|
+
const model = config.model ? `, ${config.model}` : "";
|
|
191
|
+
messages.push(`INFO: AI provider: ${config.provider} (api${model}). ` +
|
|
192
|
+
"If your API key expired, run `omh config` to update it or switch provider.");
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
messages.push(`INFO: AI provider: ${config.provider} (cli). Run \`omh config\` to switch provider or model.`);
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
173
198
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -87,6 +87,19 @@ export function createCli() {
|
|
|
87
87
|
process.exitCode = result.exitCode;
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
|
+
program
|
|
91
|
+
.command("config")
|
|
92
|
+
.description("View, reconfigure, or reset the saved AI provider")
|
|
93
|
+
.option("--show", "Show the current provider config (API key masked)")
|
|
94
|
+
.option("--reset", "Delete the saved provider config")
|
|
95
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
96
|
+
.action(async (options) => {
|
|
97
|
+
const { configCommand } = await import("./commands/config.js");
|
|
98
|
+
const result = await configCommand(options);
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
process.exitCode = result.exitCode;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
90
103
|
program
|
|
91
104
|
.command("stats")
|
|
92
105
|
.description("TUI dashboard for harness analytics")
|
|
@@ -9,3 +9,10 @@ export declare function getConfigDir(): string;
|
|
|
9
9
|
export declare function hasProviderConfig(): Promise<boolean>;
|
|
10
10
|
export declare function loadProviderConfig(): Promise<ProviderConfig | undefined>;
|
|
11
11
|
export declare function saveProviderConfig(config: ProviderConfig): Promise<void>;
|
|
12
|
+
export declare function deleteProviderConfig(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Masks an API key for display, keeping a short prefix and suffix so the user
|
|
15
|
+
* can recognize which key is stored without revealing it. Keys short enough
|
|
16
|
+
* that a prefix/suffix would overlap are masked entirely.
|
|
17
|
+
*/
|
|
18
|
+
export declare function maskApiKey(apiKey: string | undefined): string;
|
package/dist/nl/config-store.js
CHANGED
|
@@ -34,3 +34,17 @@ export async function saveProviderConfig(config) {
|
|
|
34
34
|
await fs.writeFile(configPath, payload, { encoding: "utf-8", mode: 0o600 });
|
|
35
35
|
await fs.chmod(configPath, 0o600);
|
|
36
36
|
}
|
|
37
|
+
export async function deleteProviderConfig() {
|
|
38
|
+
await fs.rm(getConfigPath(), { force: true });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Masks an API key for display, keeping a short prefix and suffix so the user
|
|
42
|
+
* can recognize which key is stored without revealing it. Keys short enough
|
|
43
|
+
* that a prefix/suffix would overlap are masked entirely.
|
|
44
|
+
*/
|
|
45
|
+
export function maskApiKey(apiKey) {
|
|
46
|
+
const key = apiKey?.trim() ?? "";
|
|
47
|
+
if (key.length <= 8)
|
|
48
|
+
return "****";
|
|
49
|
+
return `${key.slice(0, 3)}…${key.slice(-4)}`;
|
|
50
|
+
}
|