@zhijiewang/openharness 2.30.0 → 2.31.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/commands/ai.js +4 -4
- package/dist/commands/git.js +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/info.js +4 -8
- package/dist/commands/session.js +1 -2
- package/dist/commands/settings.d.ts +1 -1
- package/dist/commands/settings.js +1 -5
- package/dist/commands/skills.js +2 -5
- package/dist/components/InitWizard.js +1 -1
- package/dist/harness/config.d.ts +0 -8
- package/dist/harness/config.js +3 -7
- package/dist/harness/plugins.js +1 -1
- package/dist/harness/project-purge.d.ts +56 -0
- package/dist/harness/project-purge.js +198 -0
- package/dist/harness/telemetry.js +18 -12
- package/dist/harness/traces.d.ts +24 -1
- package/dist/harness/traces.js +72 -8
- package/dist/main.js +56 -0
- package/dist/providers/anthropic.js +4 -1
- package/dist/repl.js +1 -1
- package/dist/services/AgentDispatcher.js +15 -28
- package/dist/services/StreamingToolExecutor.js +97 -11
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
- package/dist/tools/FileReadTool/index.js +7 -4
- package/dist/tools/GrepTool/index.d.ts +2 -2
- package/dist/tools/ImageReadTool/index.js +6 -1
- package/dist/tools/PowerShellTool/index.js +11 -2
- package/dist/utils/image-downscale.d.ts +34 -0
- package/dist/utils/image-downscale.js +89 -0
- package/package.json +3 -3
- package/dist/harness/sandbox.d.ts +0 -34
- package/dist/harness/sandbox.js +0 -104
package/dist/commands/ai.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI commands — /plan, /review, /roles, /agents, /plugins, /btw, /loop
|
|
3
3
|
*/
|
|
4
|
+
import { listRoles } from "../agents/roles.js";
|
|
4
5
|
import { gitDiff, isGitRepo } from "../git/index.js";
|
|
6
|
+
import { addMarketplace, formatInstalledPlugins, formatMarketplaceSearch, getInstalledPlugins, installPlugin, listMarketplaces, removeMarketplace, searchMarketplace, uninstallPlugin, } from "../harness/marketplace.js";
|
|
7
|
+
import { discoverPlugins, discoverSkills } from "../harness/plugins.js";
|
|
8
|
+
import { discoverAgents } from "../services/a2a.js";
|
|
5
9
|
import { handleCybergotchiCommand } from "./cybergotchi.js";
|
|
6
10
|
export function registerAICommands(register) {
|
|
7
11
|
register("btw", "Ask a side question (ephemeral, no tools, not saved to history)", (args) => {
|
|
@@ -71,7 +75,6 @@ export function registerAICommands(register) {
|
|
|
71
75
|
};
|
|
72
76
|
});
|
|
73
77
|
register("roles", "List available agent specialization roles", () => {
|
|
74
|
-
const { listRoles } = require("../agents/roles.js");
|
|
75
78
|
const roles = listRoles();
|
|
76
79
|
const lines = ["Available agent roles:\n"];
|
|
77
80
|
for (const role of roles) {
|
|
@@ -86,7 +89,6 @@ export function registerAICommands(register) {
|
|
|
86
89
|
return { output: lines.join("\n"), handled: true };
|
|
87
90
|
});
|
|
88
91
|
register("agents", "Discover running openHarness agents on this machine", () => {
|
|
89
|
-
const { discoverAgents } = require("../services/a2a.js");
|
|
90
92
|
const agents = discoverAgents();
|
|
91
93
|
if (agents.length === 0) {
|
|
92
94
|
return {
|
|
@@ -110,8 +112,6 @@ export function registerAICommands(register) {
|
|
|
110
112
|
return { output: lines.join("\n"), handled: true };
|
|
111
113
|
});
|
|
112
114
|
const pluginsHandler = (args) => {
|
|
113
|
-
const { discoverPlugins, discoverSkills } = require("../harness/plugins.js");
|
|
114
|
-
const { searchMarketplace, installPlugin, uninstallPlugin, getInstalledPlugins, listMarketplaces, addMarketplace, removeMarketplace, formatMarketplaceSearch, formatInstalledPlugins, } = require("../harness/marketplace.js");
|
|
115
115
|
const parts = args.trim().split(/\s+/);
|
|
116
116
|
const subcommand = parts[0] ?? "";
|
|
117
117
|
const rest = parts.slice(1).join(" ");
|
package/dist/commands/git.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
import { gitBranch, gitCommit, gitDiff, gitLog, gitUndo, isGitRepo } from "../git/index.js";
|
|
6
|
+
import { checkpointCount, listCheckpoints, rewindLastCheckpoint } from "../harness/checkpoints.js";
|
|
6
7
|
export function registerGitCommands(register) {
|
|
7
8
|
register("diff", "Show uncommitted git changes", () => {
|
|
8
9
|
if (!isGitRepo()) {
|
|
@@ -22,7 +23,6 @@ export function registerGitCommands(register) {
|
|
|
22
23
|
};
|
|
23
24
|
});
|
|
24
25
|
register("rewind", "Restore files from checkpoint (interactive picker or last)", (args) => {
|
|
25
|
-
const { rewindLastCheckpoint, listCheckpoints, checkpointCount } = require("../harness/checkpoints.js");
|
|
26
26
|
const checkpoints = listCheckpoints();
|
|
27
27
|
if (checkpoints.length === 0) {
|
|
28
28
|
return { output: "No checkpoints available. Checkpoints are created before file modifications.", handled: true };
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* session.ts — /clear, /compact, /export, /history, /browse, /resume, /fork, /pin, /unpin
|
|
9
9
|
* git.ts — /diff, /undo, /rewind, /commit, /log
|
|
10
10
|
* info.ts — /help, /cost, /status, /config, /files, /model, /memory, /doctor, /context, /mcp, /init
|
|
11
|
-
* settings.ts — /theme, /companion, /fast, /keys, /effort, /
|
|
11
|
+
* settings.ts — /theme, /companion, /fast, /keys, /effort, /permissions, /allowed-tools
|
|
12
12
|
* ai.ts — /plan, /review, /roles, /agents, /plugins, /btw, /loop, /cybergotchi
|
|
13
13
|
* skills.ts — /skill-create, /skill-delete, /skill-edit, /skill-search, /skill-install
|
|
14
14
|
*/
|
package/dist/commands/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* session.ts — /clear, /compact, /export, /history, /browse, /resume, /fork, /pin, /unpin
|
|
9
9
|
* git.ts — /diff, /undo, /rewind, /commit, /log
|
|
10
10
|
* info.ts — /help, /cost, /status, /config, /files, /model, /memory, /doctor, /context, /mcp, /init
|
|
11
|
-
* settings.ts — /theme, /companion, /fast, /keys, /effort, /
|
|
11
|
+
* settings.ts — /theme, /companion, /fast, /keys, /effort, /permissions, /allowed-tools
|
|
12
12
|
* ai.ts — /plan, /review, /roles, /agents, /plugins, /btw, /loop, /cybergotchi
|
|
13
13
|
* skills.ts — /skill-create, /skill-delete, /skill-edit, /skill-search, /skill-install
|
|
14
14
|
*/
|
package/dist/commands/info.js
CHANGED
|
@@ -10,12 +10,12 @@ import { estimateMessageTokens } from "../harness/context-warning.js";
|
|
|
10
10
|
import { getContextWindow } from "../harness/cost.js";
|
|
11
11
|
import { getHooks, invalidateHookCache } from "../harness/hooks.js";
|
|
12
12
|
import { discoverPlugins, discoverSkills } from "../harness/plugins.js";
|
|
13
|
-
import { invalidateSandboxCache } from "../harness/sandbox.js";
|
|
14
13
|
import { formatTrace, listTracedSessions, loadTrace } from "../harness/traces.js";
|
|
15
|
-
import { invalidateVerificationCache } from "../harness/verification.js";
|
|
14
|
+
import { getVerificationConfig, invalidateVerificationCache } from "../harness/verification.js";
|
|
16
15
|
import { normalizeMcpConfig } from "../mcp/config-normalize.js";
|
|
17
16
|
import { connectedMcpServers, disconnectMcpClients, loadMcpTools } from "../mcp/loader.js";
|
|
18
17
|
import { getAuthStatus } from "../mcp/oauth.js";
|
|
18
|
+
import { formatRegistry, generateConfigBlock, MCP_REGISTRY, searchRegistry } from "../mcp/registry.js";
|
|
19
19
|
import { getRouteSelection } from "../providers/router.js";
|
|
20
20
|
import { formatHooksReport } from "./hooks-report.js";
|
|
21
21
|
import { mcpLoginHandler, mcpLogoutHandler } from "./mcp-auth.js";
|
|
@@ -75,7 +75,6 @@ export function registerInfoCommands(register, getCommandMap) {
|
|
|
75
75
|
"fast",
|
|
76
76
|
"keys",
|
|
77
77
|
"effort",
|
|
78
|
-
"sandbox",
|
|
79
78
|
"permissions",
|
|
80
79
|
"allowed-tools",
|
|
81
80
|
"login",
|
|
@@ -326,7 +325,6 @@ export function registerInfoCommands(register, getCommandMap) {
|
|
|
326
325
|
const globalCfg = existsSync(join(homedir(), ".oh", "config.yaml"));
|
|
327
326
|
lines.push(` Global config: ${globalCfg ? "~/.oh/config.yaml ✓" : "not set (optional)"}`);
|
|
328
327
|
try {
|
|
329
|
-
const { getVerificationConfig } = require("../harness/verification.js");
|
|
330
328
|
const vCfg = getVerificationConfig();
|
|
331
329
|
if (vCfg?.enabled) {
|
|
332
330
|
lines.push(` Verification: ✓ (${vCfg.rules.length} rules, mode: ${vCfg.mode})`);
|
|
@@ -478,7 +476,6 @@ export function registerInfoCommands(register, getCommandMap) {
|
|
|
478
476
|
return { output: lines.join("\n"), handled: true };
|
|
479
477
|
});
|
|
480
478
|
register("mcp-registry", "Browse and add MCP servers from the curated registry", (args) => {
|
|
481
|
-
const { searchRegistry, formatRegistry, generateConfigBlock, MCP_REGISTRY } = require("../mcp/registry.js");
|
|
482
479
|
const query = args.trim();
|
|
483
480
|
if (!query) {
|
|
484
481
|
const output = `MCP Server Registry (${MCP_REGISTRY.length} servers)\n${"─".repeat(50)}\n\n${formatRegistry()}\n\nUsage:\n /mcp-registry <name> Show install config for a server\n /mcp-registry <keyword> Search by name, description, or category`;
|
|
@@ -753,13 +750,12 @@ export function registerInfoCommands(register, getCommandMap) {
|
|
|
753
750
|
return { output: lines.join("\n"), handled: true };
|
|
754
751
|
});
|
|
755
752
|
register("reload-plugins", "Hot-reload plugins, skills, hooks, MCP servers and config without restarting the session.", async () => {
|
|
756
|
-
// Invalidate every cached source — config, hooks,
|
|
753
|
+
// Invalidate every cached source — config, hooks, verification.
|
|
757
754
|
// Skills + plugins aren't cached (each discoverSkills/discoverPlugins call
|
|
758
755
|
// reads fresh) but we still re-run them for the report so the user sees
|
|
759
756
|
// a count consistent with the new on-disk state.
|
|
760
757
|
invalidateConfigCache();
|
|
761
758
|
invalidateHookCache();
|
|
762
|
-
invalidateSandboxCache();
|
|
763
759
|
invalidateVerificationCache();
|
|
764
760
|
// Tear down + reconnect MCP servers (the live connections aren't
|
|
765
761
|
// cache-driven; they're long-lived sockets that need an explicit
|
|
@@ -781,7 +777,7 @@ export function registerInfoCommands(register, getCommandMap) {
|
|
|
781
777
|
const mcpServers = connectedMcpServers().length;
|
|
782
778
|
const lines = [
|
|
783
779
|
"Hot reload complete:",
|
|
784
|
-
" - config + hooks +
|
|
780
|
+
" - config + hooks + verification: caches invalidated",
|
|
785
781
|
` - hook events configured: ${hookEvents}`,
|
|
786
782
|
` - MCP servers connected: ${mcpServers}${mcpError ? ` (error: ${mcpError})` : ""}`,
|
|
787
783
|
` - MCP tools loaded: ${mcpTools}`,
|
package/dist/commands/session.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Session commands — /clear, /compact, /copy, /export, /history, /browse, /resume, /fork, /pin, /unpin
|
|
3
3
|
*/
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { getContextWindow } from "../harness/cost.js";
|
|
@@ -134,7 +134,6 @@ export function registerSessionCommands(register) {
|
|
|
134
134
|
const body = asJson ? JSON.stringify(ctx.messages, null, 2) : formatMessagesAsMarkdown(ctx.messages);
|
|
135
135
|
try {
|
|
136
136
|
mkdirSync(dirname(filename), { recursive: true });
|
|
137
|
-
const { writeFileSync } = require("node:fs");
|
|
138
137
|
writeFileSync(filename, body);
|
|
139
138
|
return { output: `Exported ${ctx.messages.length} messages to ${filename}`, handled: true };
|
|
140
139
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings commands — /theme, /companion, /fast, /keys, /keybindings, /effort, /
|
|
2
|
+
* Settings commands — /theme, /companion, /fast, /keys, /keybindings, /effort, /permissions, /allowed-tools, /trust
|
|
3
3
|
*/
|
|
4
4
|
import type { CommandHandler } from "./types.js";
|
|
5
5
|
export declare function registerSettingsCommands(register: (name: string, description: string, handler: CommandHandler) => void): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings commands — /theme, /companion, /fast, /keys, /keybindings, /effort, /
|
|
2
|
+
* Settings commands — /theme, /companion, /fast, /keys, /keybindings, /effort, /permissions, /allowed-tools, /trust
|
|
3
3
|
*/
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -135,10 +135,6 @@ export function registerSettingsCommands(register) {
|
|
|
135
135
|
}
|
|
136
136
|
return { output: `Effort level set to: ${level}`, handled: true };
|
|
137
137
|
});
|
|
138
|
-
register("sandbox", "Show sandbox status and restrictions", () => {
|
|
139
|
-
const { sandboxStatus } = require("../harness/sandbox.js");
|
|
140
|
-
return { output: `${sandboxStatus()}\n\nConfigure in .oh/config.yaml under sandbox:`, handled: true };
|
|
141
|
-
});
|
|
142
138
|
register("permissions", "View or change permission mode (or 'log' for approval history)", (args, ctx) => {
|
|
143
139
|
const trimmed = args.trim();
|
|
144
140
|
if (!trimmed) {
|
package/dist/commands/skills.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill management commands — /skills, /skill-create, /skill-delete, /skill-edit, /skill-search, /skill-install
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { discoverSkills } from "../harness/plugins.js";
|
|
6
|
+
import { discoverSkills, findSkill } from "../harness/plugins.js";
|
|
7
7
|
export function registerSkillCommands(register) {
|
|
8
8
|
register("skills", "List all available skills", () => {
|
|
9
9
|
const skills = discoverSkills();
|
|
@@ -76,12 +76,10 @@ How to confirm the skill worked correctly.
|
|
|
76
76
|
const name = args.trim();
|
|
77
77
|
if (!name)
|
|
78
78
|
return { output: "Usage: /skill-delete <name>", handled: true };
|
|
79
|
-
const { findSkill } = require("../harness/plugins.js");
|
|
80
79
|
const skill = findSkill(name);
|
|
81
80
|
if (!skill)
|
|
82
81
|
return { output: `Skill "${name}" not found.`, handled: true };
|
|
83
82
|
try {
|
|
84
|
-
const { unlinkSync } = require("node:fs");
|
|
85
83
|
unlinkSync(skill.filePath);
|
|
86
84
|
return { output: `Deleted skill: ${skill.filePath}`, handled: true };
|
|
87
85
|
}
|
|
@@ -93,7 +91,6 @@ How to confirm the skill worked correctly.
|
|
|
93
91
|
const name = args.trim();
|
|
94
92
|
if (!name)
|
|
95
93
|
return { output: "Usage: /skill-edit <name>", handled: true };
|
|
96
|
-
const { findSkill } = require("../harness/plugins.js");
|
|
97
94
|
const skill = findSkill(name);
|
|
98
95
|
if (!skill)
|
|
99
96
|
return { output: `Skill "${name}" not found.`, handled: true };
|
|
@@ -15,6 +15,7 @@ import { Box, Text, useInput } from "ink";
|
|
|
15
15
|
import TextInput from "ink-text-input";
|
|
16
16
|
import { useCallback, useState } from "react";
|
|
17
17
|
import { writeOhConfig } from "../harness/config.js";
|
|
18
|
+
import { MCP_REGISTRY } from "../mcp/registry.js";
|
|
18
19
|
import CybergotchiSetup from "./CybergotchiSetup.js";
|
|
19
20
|
const PROVIDERS = [
|
|
20
21
|
{
|
|
@@ -125,7 +126,6 @@ export default function InitWizard({ onDone }) {
|
|
|
125
126
|
let mcpServers;
|
|
126
127
|
if (selectedMcp.size > 0) {
|
|
127
128
|
try {
|
|
128
|
-
const { MCP_REGISTRY } = require("../mcp/registry.js");
|
|
129
129
|
mcpServers = [...selectedMcp]
|
|
130
130
|
.map((name) => MCP_REGISTRY.find((e) => e.name === name))
|
|
131
131
|
.filter(Boolean)
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -208,14 +208,6 @@ export type OhConfig = {
|
|
|
208
208
|
enabled?: boolean;
|
|
209
209
|
endpoint?: string;
|
|
210
210
|
};
|
|
211
|
-
/** Sandbox — filesystem and network restrictions */
|
|
212
|
-
sandbox?: {
|
|
213
|
-
enabled?: boolean;
|
|
214
|
-
allowedPaths?: string[];
|
|
215
|
-
allowedDomains?: string[];
|
|
216
|
-
blockNetwork?: boolean;
|
|
217
|
-
blockedCommands?: string[];
|
|
218
|
-
};
|
|
219
211
|
/** Remote server security settings */
|
|
220
212
|
remote?: {
|
|
221
213
|
tokens?: string[];
|
package/dist/harness/config.js
CHANGED
|
@@ -127,13 +127,9 @@ export function appendToolPermission(toolName, action = "allow", root) {
|
|
|
127
127
|
}
|
|
128
128
|
export function writeOhConfig(cfg, root) {
|
|
129
129
|
invalidateConfigCache();
|
|
130
|
-
// Emit configChange hook (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
/* ignore */
|
|
136
|
-
}
|
|
130
|
+
// Emit configChange hook (dynamic import to avoid circular dependency
|
|
131
|
+
// with hooks.ts, which imports readOhConfig from this module).
|
|
132
|
+
import("./hooks.js").then((m) => m.emitHook("configChange", {})).catch(() => { });
|
|
137
133
|
const p = configPath(root);
|
|
138
134
|
mkdirSync(join(root ?? ".", ".oh"), { recursive: true });
|
|
139
135
|
if (cfg.provider === "llamacpp" || cfg.provider === "lmstudio") {
|
package/dist/harness/plugins.js
CHANGED
|
@@ -14,6 +14,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { dirname, join, relative } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { getInstalledPlugins } from "./marketplace.js";
|
|
17
18
|
const PROJECT_SKILLS_DIR = join(".oh", "skills");
|
|
18
19
|
const GLOBAL_SKILLS_DIR = join(homedir(), ".oh", "skills");
|
|
19
20
|
// Claude Code ecosystem mirror paths (Anthropic convention)
|
|
@@ -178,7 +179,6 @@ export function discoverSkills() {
|
|
|
178
179
|
skills.push(...loadSkillsFromDir(CC_GLOBAL_SKILLS_DIR, "global"));
|
|
179
180
|
// Load skills from installed marketplace plugins (namespaced as plugin-name:skill-name)
|
|
180
181
|
try {
|
|
181
|
-
const { getInstalledPlugins } = require("./marketplace.js");
|
|
182
182
|
for (const plugin of getInstalledPlugins()) {
|
|
183
183
|
const pluginSkillsDir = join(plugin.cachePath, "skills");
|
|
184
184
|
const pluginSkills = loadSkillsFromDir(pluginSkillsDir, "plugin");
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oh project purge` core logic — extracted from the CLI command for testability.
|
|
3
|
+
*
|
|
4
|
+
* Deletes per-project openHarness state at a target directory:
|
|
5
|
+
* 1. The entire `.oh/` directory at that path (config, RULES.md, memory/,
|
|
6
|
+
* skills/, agents/, output-styles/, plans/, checkpoints/, exports).
|
|
7
|
+
* 2. The workspace-trust entry for that path in `~/.oh/trusted-dirs.json`,
|
|
8
|
+
* if present.
|
|
9
|
+
*
|
|
10
|
+
* What it does NOT touch (these are global-and-cross-project):
|
|
11
|
+
* - `~/.oh/sessions/` session transcripts (may span projects)
|
|
12
|
+
* - `~/.oh/credentials.enc` global API keys
|
|
13
|
+
* - `~/.oh/memory/` (etc.) global counterparts of project state
|
|
14
|
+
* - `~/.oh/plugins/`, marketplaces installed plugins
|
|
15
|
+
* - `~/.oh/telemetry/`, traces/ global observability data
|
|
16
|
+
* - `~/.oh/approvals.log` append-only audit log
|
|
17
|
+
* - `~/.oh/keybindings.json`,
|
|
18
|
+
* `~/.oh/config.yaml` global config
|
|
19
|
+
*
|
|
20
|
+
* Mirrors Claude Code's `claude project purge` UX surface (--dry-run, --yes,
|
|
21
|
+
* default plan + confirm). `--all` and `--interactive` are deferred — openHarness
|
|
22
|
+
* has no project registry, so `--all` would need a session-cwd scan, and
|
|
23
|
+
* `--dry-run` already covers the spec for `--interactive`.
|
|
24
|
+
*/
|
|
25
|
+
export type PurgeEntry = {
|
|
26
|
+
/** Filesystem path that will be removed. */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Human-readable label shown in the plan. */
|
|
29
|
+
label: string;
|
|
30
|
+
/** Cumulative size in bytes. 0 when the entry is metadata-only (e.g. a trust-store entry). */
|
|
31
|
+
bytes: number;
|
|
32
|
+
/** When false, this entry is reported but doesn't currently exist on disk. */
|
|
33
|
+
exists: boolean;
|
|
34
|
+
/** When true, removal is via JSON edit instead of `rmSync`. Used for the trust-store entry. */
|
|
35
|
+
jsonEdit?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type PurgePlan = {
|
|
38
|
+
projectPath: string;
|
|
39
|
+
entries: PurgeEntry[];
|
|
40
|
+
totalBytes: number;
|
|
41
|
+
};
|
|
42
|
+
/** Format bytes as a short human string (e.g. `1.2 MB`, `342 B`). */
|
|
43
|
+
export declare function formatBytes(bytes: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build the list of things `purge` would delete, without touching the filesystem.
|
|
46
|
+
* Inspects the `.oh/` directory at `projectPath` and looks for a trust-store entry.
|
|
47
|
+
*/
|
|
48
|
+
export declare function planPurge(projectPath: string): PurgePlan;
|
|
49
|
+
/** Render a plan as a multi-line string for display. */
|
|
50
|
+
export declare function formatPurgePlan(plan: PurgePlan): string;
|
|
51
|
+
/** Execute the plan. Returns the count of successfully removed entries and any errors. */
|
|
52
|
+
export declare function executePurge(plan: PurgePlan): {
|
|
53
|
+
deleted: number;
|
|
54
|
+
errors: string[];
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=project-purge.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oh project purge` core logic — extracted from the CLI command for testability.
|
|
3
|
+
*
|
|
4
|
+
* Deletes per-project openHarness state at a target directory:
|
|
5
|
+
* 1. The entire `.oh/` directory at that path (config, RULES.md, memory/,
|
|
6
|
+
* skills/, agents/, output-styles/, plans/, checkpoints/, exports).
|
|
7
|
+
* 2. The workspace-trust entry for that path in `~/.oh/trusted-dirs.json`,
|
|
8
|
+
* if present.
|
|
9
|
+
*
|
|
10
|
+
* What it does NOT touch (these are global-and-cross-project):
|
|
11
|
+
* - `~/.oh/sessions/` session transcripts (may span projects)
|
|
12
|
+
* - `~/.oh/credentials.enc` global API keys
|
|
13
|
+
* - `~/.oh/memory/` (etc.) global counterparts of project state
|
|
14
|
+
* - `~/.oh/plugins/`, marketplaces installed plugins
|
|
15
|
+
* - `~/.oh/telemetry/`, traces/ global observability data
|
|
16
|
+
* - `~/.oh/approvals.log` append-only audit log
|
|
17
|
+
* - `~/.oh/keybindings.json`,
|
|
18
|
+
* `~/.oh/config.yaml` global config
|
|
19
|
+
*
|
|
20
|
+
* Mirrors Claude Code's `claude project purge` UX surface (--dry-run, --yes,
|
|
21
|
+
* default plan + confirm). `--all` and `--interactive` are deferred — openHarness
|
|
22
|
+
* has no project registry, so `--all` would need a session-cwd scan, and
|
|
23
|
+
* `--dry-run` already covers the spec for `--interactive`.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join, resolve } from "node:path";
|
|
28
|
+
/**
|
|
29
|
+
* Path to the workspace-trust file. Resolved per-call so `OH_TRUST_FILE`
|
|
30
|
+
* env-var overrides (used by tests) take effect without re-importing.
|
|
31
|
+
*/
|
|
32
|
+
function trustFilePath() {
|
|
33
|
+
return process.env.OH_TRUST_FILE ?? join(homedir(), ".oh", "trusted-dirs.json");
|
|
34
|
+
}
|
|
35
|
+
/** Walk a directory and return the cumulative size in bytes. Errors swallowed. */
|
|
36
|
+
function dirSize(path) {
|
|
37
|
+
let total = 0;
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return 0;
|
|
41
|
+
const stats = statSync(path);
|
|
42
|
+
if (stats.isFile())
|
|
43
|
+
return stats.size;
|
|
44
|
+
if (!stats.isDirectory())
|
|
45
|
+
return 0;
|
|
46
|
+
for (const entry of readdirSync(path)) {
|
|
47
|
+
total += dirSize(join(path, entry));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* permission errors etc. — best-effort sizing */
|
|
52
|
+
}
|
|
53
|
+
return total;
|
|
54
|
+
}
|
|
55
|
+
/** Format bytes as a short human string (e.g. `1.2 MB`, `342 B`). */
|
|
56
|
+
export function formatBytes(bytes) {
|
|
57
|
+
if (bytes < 1024)
|
|
58
|
+
return `${bytes} B`;
|
|
59
|
+
if (bytes < 1024 * 1024)
|
|
60
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
61
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
62
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
63
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
64
|
+
}
|
|
65
|
+
/** Normalize a directory the same way `harness/trust.ts` does. Lowercase on Windows. */
|
|
66
|
+
function normalizeForTrust(dir) {
|
|
67
|
+
const abs = resolve(dir);
|
|
68
|
+
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the list of things `purge` would delete, without touching the filesystem.
|
|
72
|
+
* Inspects the `.oh/` directory at `projectPath` and looks for a trust-store entry.
|
|
73
|
+
*/
|
|
74
|
+
export function planPurge(projectPath) {
|
|
75
|
+
const project = resolve(projectPath);
|
|
76
|
+
const ohDir = join(project, ".oh");
|
|
77
|
+
const entries = [];
|
|
78
|
+
if (existsSync(ohDir)) {
|
|
79
|
+
// Group sub-paths so the plan is informative without listing every file.
|
|
80
|
+
const knownChildren = [
|
|
81
|
+
{ rel: "config.yaml", label: "Project config (config.yaml)" },
|
|
82
|
+
{ rel: "RULES.md", label: "Project rules (RULES.md)" },
|
|
83
|
+
{ rel: "memory", label: "Memories (.oh/memory/)" },
|
|
84
|
+
{ rel: "skills", label: "Skills (.oh/skills/)" },
|
|
85
|
+
{ rel: "agents", label: "Agent roles (.oh/agents/)" },
|
|
86
|
+
{ rel: "output-styles", label: "Output styles (.oh/output-styles/)" },
|
|
87
|
+
{ rel: "plans", label: "Plans (.oh/plans/)" },
|
|
88
|
+
{ rel: "checkpoints", label: "Checkpoints (.oh/checkpoints/)" },
|
|
89
|
+
];
|
|
90
|
+
for (const child of knownChildren) {
|
|
91
|
+
const path = join(ohDir, child.rel);
|
|
92
|
+
if (existsSync(path)) {
|
|
93
|
+
entries.push({ path, label: child.label, bytes: dirSize(path), exists: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Anything else under .oh/ that we didn't enumerate (export-*, etc.).
|
|
97
|
+
try {
|
|
98
|
+
const explicit = new Set(knownChildren.map((c) => c.rel));
|
|
99
|
+
for (const name of readdirSync(ohDir)) {
|
|
100
|
+
if (explicit.has(name))
|
|
101
|
+
continue;
|
|
102
|
+
const path = join(ohDir, name);
|
|
103
|
+
entries.push({
|
|
104
|
+
path,
|
|
105
|
+
label: `Other .oh/ entry (${name})`,
|
|
106
|
+
bytes: dirSize(path),
|
|
107
|
+
exists: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* directory unreadable — caught later when we try to remove */
|
|
113
|
+
}
|
|
114
|
+
// Finally, add the .oh dir itself so it's removed after children are reported.
|
|
115
|
+
entries.push({ path: ohDir, label: ".oh/ directory", bytes: 0, exists: true });
|
|
116
|
+
}
|
|
117
|
+
// Workspace-trust entry, if any.
|
|
118
|
+
const trustFile = trustFilePath();
|
|
119
|
+
if (existsSync(trustFile)) {
|
|
120
|
+
try {
|
|
121
|
+
const raw = readFileSync(trustFile, "utf8");
|
|
122
|
+
const parsed = JSON.parse(raw);
|
|
123
|
+
if (Array.isArray(parsed.trusted)) {
|
|
124
|
+
const target = normalizeForTrust(project);
|
|
125
|
+
const isTrusted = parsed.trusted.some((p) => typeof p === "string" && normalizeForTrust(p) === target);
|
|
126
|
+
if (isTrusted) {
|
|
127
|
+
entries.push({
|
|
128
|
+
path: trustFile,
|
|
129
|
+
label: "Workspace-trust entry (~/.oh/trusted-dirs.json)",
|
|
130
|
+
bytes: 0,
|
|
131
|
+
exists: true,
|
|
132
|
+
jsonEdit: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
/* malformed trust file — nothing to remove */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const totalBytes = entries.reduce((sum, e) => sum + e.bytes, 0);
|
|
142
|
+
return { projectPath: project, entries, totalBytes };
|
|
143
|
+
}
|
|
144
|
+
/** Render a plan as a multi-line string for display. */
|
|
145
|
+
export function formatPurgePlan(plan) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
lines.push(`Purge plan for ${plan.projectPath}`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
if (plan.entries.length === 0) {
|
|
150
|
+
lines.push(" (nothing to delete — no .oh/ directory and no trust entry)");
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
for (const entry of plan.entries) {
|
|
154
|
+
const size = entry.bytes > 0 ? ` [${formatBytes(entry.bytes)}]` : "";
|
|
155
|
+
lines.push(` - ${entry.label}${size}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(`Total: ${plan.entries.length} target(s), ${formatBytes(plan.totalBytes)}`);
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push("Not touched (global state): ~/.oh/sessions/, credentials, plugins,");
|
|
161
|
+
lines.push(" telemetry, traces, approvals.log, keybindings, global config.");
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
/** Execute the plan. Returns the count of successfully removed entries and any errors. */
|
|
165
|
+
export function executePurge(plan) {
|
|
166
|
+
let deleted = 0;
|
|
167
|
+
const errors = [];
|
|
168
|
+
for (const entry of plan.entries) {
|
|
169
|
+
if (entry.jsonEdit) {
|
|
170
|
+
// Trust entry — JSON edit, not file delete.
|
|
171
|
+
try {
|
|
172
|
+
const raw = readFileSync(entry.path, "utf8");
|
|
173
|
+
const parsed = JSON.parse(raw);
|
|
174
|
+
if (Array.isArray(parsed.trusted)) {
|
|
175
|
+
const target = normalizeForTrust(plan.projectPath);
|
|
176
|
+
const filtered = parsed.trusted.filter((p) => typeof p === "string" && normalizeForTrust(p) !== target);
|
|
177
|
+
writeFileSync(entry.path, JSON.stringify({ trusted: filtered }, null, 2));
|
|
178
|
+
deleted++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
errors.push(`${entry.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (existsSync(entry.path)) {
|
|
188
|
+
rmSync(entry.path, { recursive: true, force: true });
|
|
189
|
+
deleted++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
errors.push(`${entry.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { deleted, errors };
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=project-purge.js.map
|
|
@@ -15,10 +15,15 @@ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from
|
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { readOhConfig } from "./config.js";
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Telemetry directory. Resolved on each access so `OH_TELEMETRY_DIR` env-var
|
|
20
|
+
* overrides (used by tests) take effect even after this module loads.
|
|
21
|
+
*/
|
|
22
|
+
function telemetryDir() {
|
|
23
|
+
return process.env.OH_TELEMETRY_DIR ?? join(homedir(), ".oh", "telemetry");
|
|
24
|
+
}
|
|
19
25
|
// ── State ──
|
|
20
26
|
let _enabled;
|
|
21
|
-
let _sessionFile = null;
|
|
22
27
|
function isEnabled() {
|
|
23
28
|
if (_enabled !== undefined)
|
|
24
29
|
return _enabled;
|
|
@@ -26,12 +31,13 @@ function isEnabled() {
|
|
|
26
31
|
_enabled = config?.telemetry?.enabled === true;
|
|
27
32
|
return _enabled;
|
|
28
33
|
}
|
|
34
|
+
/** Resolve the JSONL path for a sessionId. Stateless — was previously a module-level
|
|
35
|
+
* singleton that ignored `sessionId` after the first call, causing multi-session
|
|
36
|
+
* processes (e.g. `--resume`) to write every session into the first one's file. */
|
|
29
37
|
function getSessionFile(sessionId) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
_sessionFile = join(TELEMETRY_DIR, `${sessionId}.jsonl`);
|
|
34
|
-
return _sessionFile;
|
|
38
|
+
const dir = telemetryDir();
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
return join(dir, `${sessionId}.jsonl`);
|
|
35
41
|
}
|
|
36
42
|
// ── Public API ──
|
|
37
43
|
/** Record a telemetry event (no-op if telemetry disabled) */
|
|
@@ -84,7 +90,7 @@ export function recordError(sessionId, category) {
|
|
|
84
90
|
}
|
|
85
91
|
/** Read local telemetry events for a session */
|
|
86
92
|
export function readSessionEvents(sessionId) {
|
|
87
|
-
const file = join(
|
|
93
|
+
const file = join(telemetryDir(), `${sessionId}.jsonl`);
|
|
88
94
|
if (!existsSync(file))
|
|
89
95
|
return [];
|
|
90
96
|
try {
|
|
@@ -99,15 +105,16 @@ export function readSessionEvents(sessionId) {
|
|
|
99
105
|
}
|
|
100
106
|
/** Get aggregate stats across all sessions */
|
|
101
107
|
export function getAggregateStats() {
|
|
102
|
-
|
|
108
|
+
const dir = telemetryDir();
|
|
109
|
+
if (!existsSync(dir))
|
|
103
110
|
return { totalSessions: 0, totalEvents: 0, toolUsage: {}, errorCategories: {} };
|
|
104
|
-
const files = readdirSync(
|
|
111
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
105
112
|
const toolUsage = {};
|
|
106
113
|
const errorCategories = {};
|
|
107
114
|
let totalEvents = 0;
|
|
108
115
|
for (const file of files) {
|
|
109
116
|
try {
|
|
110
|
-
const lines = readFileSync(join(
|
|
117
|
+
const lines = readFileSync(join(dir, file), "utf-8").split("\n").filter(Boolean);
|
|
111
118
|
totalEvents += lines.length;
|
|
112
119
|
for (const line of lines) {
|
|
113
120
|
const event = JSON.parse(line);
|
|
@@ -128,6 +135,5 @@ export function getAggregateStats() {
|
|
|
128
135
|
/** Reset telemetry cache (for testing or config changes) */
|
|
129
136
|
export function resetTelemetry() {
|
|
130
137
|
_enabled = undefined;
|
|
131
|
-
_sessionFile = null;
|
|
132
138
|
}
|
|
133
139
|
//# sourceMappingURL=telemetry.js.map
|
package/dist/harness/traces.d.ts
CHANGED
|
@@ -32,13 +32,36 @@ export declare class SessionTracer {
|
|
|
32
32
|
private activeSpans;
|
|
33
33
|
private spanCounter;
|
|
34
34
|
private otlp?;
|
|
35
|
+
/**
|
|
36
|
+
* Pending spans that have ended but not yet been POSTed to OTLP. Drained
|
|
37
|
+
* by a microtask-debounced flush (one POST per microtask boundary even if
|
|
38
|
+
* many spans end in the same tick) and by the public `flush()` method.
|
|
39
|
+
*/
|
|
40
|
+
private otlpBuffer;
|
|
41
|
+
private otlpFlushScheduled;
|
|
42
|
+
/** In-flight fetches so `flush()` can await any POSTs already on the wire. */
|
|
43
|
+
private otlpInFlight;
|
|
35
44
|
constructor(sessionId: string, otlp?: OTLPConfig);
|
|
36
45
|
/** Start a new span. Returns the span ID. */
|
|
37
46
|
startSpan(name: string, attributes?: Record<string, unknown>, parentSpanId?: string): string;
|
|
38
47
|
/** End a span and record it. */
|
|
39
48
|
endSpan(spanId: string, status?: "ok" | "error", extraAttributes?: Record<string, unknown>): TraceSpan | null;
|
|
40
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Buffer the span for OTLP shipping. The actual POST is deferred to a
|
|
51
|
+
* microtask so multiple spans ending in the same tick coalesce into a
|
|
52
|
+
* single batch POST instead of one fetch each. Errors are swallowed —
|
|
53
|
+
* telemetry must never crash the agent.
|
|
54
|
+
*/
|
|
41
55
|
private shipSpanOTLP;
|
|
56
|
+
/** Send whatever is in `otlpBuffer` as a single fire-and-forget POST. The
|
|
57
|
+
* returned promise is tracked in `otlpInFlight` so `flush()` can await it. */
|
|
58
|
+
private drainOTLPBuffer;
|
|
59
|
+
/**
|
|
60
|
+
* Drain any pending OTLP buffer and await every in-flight POST. Call this at
|
|
61
|
+
* session end so spans aren't dropped on `process.exit`. No-op when OTLP is
|
|
62
|
+
* not configured. Errors are swallowed (already, by `drainOTLPBuffer`).
|
|
63
|
+
*/
|
|
64
|
+
flush(): Promise<void>;
|
|
42
65
|
/** Get all completed spans */
|
|
43
66
|
getSpans(): TraceSpan[];
|
|
44
67
|
/** Get a summary of the trace */
|