@zhijiewang/openharness 2.30.1 → 2.32.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 CHANGED
@@ -773,6 +773,14 @@ Create `.oh/RULES.md` in any repo (or run `oh init`):
773
773
 
774
774
  Rules load automatically into every session.
775
775
 
776
+ openHarness also reads any of the following project-instruction files if present (additive, parent-first):
777
+
778
+ - `CLAUDE.md` (Anthropic convention) — and hierarchical `CLAUDE.md` from parent dirs, plus `~/.claude/CLAUDE.md` for user-global
779
+ - `AGENTS.md` ([agents.md cross-tool standard](https://agents.md/), used by Codex / Cursor / Copilot / Cline / Aider) — same parent-first walk
780
+ - `CLAUDE.local.md` (gitignored personal overrides)
781
+
782
+ If a repo has `AGENTS.md` already configured for another agent, openHarness picks it up unchanged — no migration step needed.
783
+
776
784
  ## Skills & Plugins
777
785
 
778
786
  ### Skills
package/README.zh-CN.md CHANGED
@@ -772,6 +772,14 @@ description: 专注的代码审查模式
772
772
 
773
773
  规则会自动加载到每次会话中。
774
774
 
775
+ openHarness 还会自动读取以下项目指令文件(如果存在,按父目录优先合并加载):
776
+
777
+ - `CLAUDE.md`(Anthropic 约定)—— 含从父目录到项目根的层级 `CLAUDE.md` 文件,以及全局 `~/.claude/CLAUDE.md`
778
+ - `AGENTS.md`([agents.md 跨工具标准](https://agents.md/),被 Codex / Cursor / Copilot / Cline / Aider 共同采用)—— 同样的父目录优先扫描
779
+ - `CLAUDE.local.md`(gitignore 的个人覆盖)
780
+
781
+ 如果仓库已为其他 agent 配置了 `AGENTS.md`,openHarness 直接读取,无需迁移。
782
+
775
783
  ## 技能与插件
776
784
 
777
785
  ### 技能
@@ -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, /sandbox, /permissions, /allowed-tools
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
  */
@@ -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, /sandbox, /permissions, /allowed-tools
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
  */
@@ -10,7 +10,6 @@ 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
14
  import { getVerificationConfig, invalidateVerificationCache } from "../harness/verification.js";
16
15
  import { normalizeMcpConfig } from "../mcp/config-normalize.js";
@@ -76,7 +75,6 @@ export function registerInfoCommands(register, getCommandMap) {
76
75
  "fast",
77
76
  "keys",
78
77
  "effort",
79
- "sandbox",
80
78
  "permissions",
81
79
  "allowed-tools",
82
80
  "login",
@@ -752,13 +750,12 @@ export function registerInfoCommands(register, getCommandMap) {
752
750
  return { output: lines.join("\n"), handled: true };
753
751
  });
754
752
  register("reload-plugins", "Hot-reload plugins, skills, hooks, MCP servers and config without restarting the session.", async () => {
755
- // Invalidate every cached source — config, hooks, sandbox, verification.
753
+ // Invalidate every cached source — config, hooks, verification.
756
754
  // Skills + plugins aren't cached (each discoverSkills/discoverPlugins call
757
755
  // reads fresh) but we still re-run them for the report so the user sees
758
756
  // a count consistent with the new on-disk state.
759
757
  invalidateConfigCache();
760
758
  invalidateHookCache();
761
- invalidateSandboxCache();
762
759
  invalidateVerificationCache();
763
760
  // Tear down + reconnect MCP servers (the live connections aren't
764
761
  // cache-driven; they're long-lived sockets that need an explicit
@@ -780,7 +777,7 @@ export function registerInfoCommands(register, getCommandMap) {
780
777
  const mcpServers = connectedMcpServers().length;
781
778
  const lines = [
782
779
  "Hot reload complete:",
783
- " - config + hooks + sandbox + verification: caches invalidated",
780
+ " - config + hooks + verification: caches invalidated",
784
781
  ` - hook events configured: ${hookEvents}`,
785
782
  ` - MCP servers connected: ${mcpServers}${mcpError ? ` (error: ${mcpError})` : ""}`,
786
783
  ` - MCP tools loaded: ${mcpTools}`,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Settings commands — /theme, /companion, /fast, /keys, /keybindings, /effort, /sandbox, /permissions, /allowed-tools, /trust
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, /sandbox, /permissions, /allowed-tools, /trust
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";
@@ -8,7 +8,6 @@ import { dirname, join } from "node:path";
8
8
  import { readApprovalLog } from "../harness/approvals.js";
9
9
  import { readOhConfig } from "../harness/config.js";
10
10
  import { loadKeybindings } from "../harness/keybindings.js";
11
- import { sandboxStatus } from "../harness/sandbox.js";
12
11
  import { isTrusted, listTrusted, trust } from "../harness/trust.js";
13
12
  const KEYBINDINGS_TEMPLATE = `[
14
13
  { "key": "ctrl+d", "action": "/diff" },
@@ -136,9 +135,6 @@ export function registerSettingsCommands(register) {
136
135
  }
137
136
  return { output: `Effort level set to: ${level}`, handled: true };
138
137
  });
139
- register("sandbox", "Show sandbox status and restrictions", () => {
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) {
@@ -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[];
@@ -31,10 +31,10 @@ export type MemoryEntry = {
31
31
  export declare function loadMemories(): MemoryEntry[];
32
32
  /** Build a system prompt section from loaded memories (capped at MEMORY_PROMPT_MAX_CHARS) */
33
33
  export declare function memoriesToPrompt(memories: MemoryEntry[]): string;
34
- /** A single CLAUDE.md source with its resolved content (imports inlined). */
34
+ /** A single project-instructions source with its resolved content (imports inlined). */
35
35
  export type ClaudeMdEntry = {
36
36
  path: string;
37
- source: "project" | "project-local" | "user" | "claude-dir";
37
+ source: "project" | "project-local" | "user" | "claude-dir" | "agents-md";
38
38
  content: string;
39
39
  };
40
40
  /**
@@ -47,11 +47,17 @@ export type ClaudeMdEntry = {
47
47
  */
48
48
  export declare function resolveClaudeMdImports(content: string, baseDir: string, hopsLeft?: number): string;
49
49
  /**
50
- * Load the hierarchical CLAUDE.md set in the order Anthropic documents:
51
- * 1. `./.claude/CLAUDE.md` (project, checked in)
50
+ * Load hierarchical project-instruction files. Order:
51
+ * 1. `./.claude/CLAUDE.md` (project, Anthropic convention)
52
52
  * 2. `./CLAUDE.md` (project, checked in)
53
- * 3. `./CLAUDE.local.md` (project, gitignored)
54
- * 4. `~/.claude/CLAUDE.md` (user-global)
53
+ * 3. `./AGENTS.md` (project, AGENTS.md cross-tool standard — agents.md)
54
+ * 4. `./CLAUDE.local.md` (project, gitignored)
55
+ * 5. `~/.claude/CLAUDE.md` (user-global)
56
+ *
57
+ * AGENTS.md is read alongside CLAUDE.md so OH "just works" in the 60k+ repos
58
+ * already configured for cross-agent compatibility (Codex / Cursor / Copilot
59
+ * / Cline / Aider all read AGENTS.md). Both layers concatenate into the
60
+ * system prompt; if both files exist, both contribute.
55
61
  *
56
62
  * Each file is read, `@imports` are resolved, and the results are returned in
57
63
  * load order. Missing files are skipped. The caller can format these into the
@@ -61,8 +67,11 @@ export declare function resolveClaudeMdImports(content: string, baseDir: string,
61
67
  */
62
68
  export declare function loadClaudeMdHierarchy(root?: string): ClaudeMdEntry[];
63
69
  /**
64
- * Render loaded CLAUDE.md entries as a system-prompt block. Empty when no
65
- * CLAUDE.md files exist — caller should concatenate alongside `memoriesToPrompt`.
70
+ * Render loaded project-instruction entries as a system-prompt block. Empty
71
+ * when no source files exist — caller should concatenate alongside
72
+ * `memoriesToPrompt`. Header is generic ("Project instructions") since both
73
+ * CLAUDE.md and AGENTS.md feed in; the per-entry `source:` comment
74
+ * disambiguates which file each section came from.
66
75
  */
67
76
  export declare function claudeMdToPrompt(entries: ClaudeMdEntry[]): string;
68
77
  /** Save a memory entry to the project memory directory */
@@ -130,11 +130,17 @@ function readClaudeMdIfExists(path, source) {
130
130
  }
131
131
  }
132
132
  /**
133
- * Load the hierarchical CLAUDE.md set in the order Anthropic documents:
134
- * 1. `./.claude/CLAUDE.md` (project, checked in)
133
+ * Load hierarchical project-instruction files. Order:
134
+ * 1. `./.claude/CLAUDE.md` (project, Anthropic convention)
135
135
  * 2. `./CLAUDE.md` (project, checked in)
136
- * 3. `./CLAUDE.local.md` (project, gitignored)
137
- * 4. `~/.claude/CLAUDE.md` (user-global)
136
+ * 3. `./AGENTS.md` (project, AGENTS.md cross-tool standard — agents.md)
137
+ * 4. `./CLAUDE.local.md` (project, gitignored)
138
+ * 5. `~/.claude/CLAUDE.md` (user-global)
139
+ *
140
+ * AGENTS.md is read alongside CLAUDE.md so OH "just works" in the 60k+ repos
141
+ * already configured for cross-agent compatibility (Codex / Cursor / Copilot
142
+ * / Cline / Aider all read AGENTS.md). Both layers concatenate into the
143
+ * system prompt; if both files exist, both contribute.
138
144
  *
139
145
  * Each file is read, `@imports` are resolved, and the results are returned in
140
146
  * load order. Missing files are skipped. The caller can format these into the
@@ -146,6 +152,7 @@ export function loadClaudeMdHierarchy(root = ".") {
146
152
  const candidates = [
147
153
  [join(root, ".claude", "CLAUDE.md"), "claude-dir"],
148
154
  [join(root, "CLAUDE.md"), "project"],
155
+ [join(root, "AGENTS.md"), "agents-md"],
149
156
  [join(root, "CLAUDE.local.md"), "project-local"],
150
157
  [join(homedir(), ".claude", "CLAUDE.md"), "user"],
151
158
  ];
@@ -162,13 +169,16 @@ export function loadClaudeMdHierarchy(root = ".") {
162
169
  return entries;
163
170
  }
164
171
  /**
165
- * Render loaded CLAUDE.md entries as a system-prompt block. Empty when no
166
- * CLAUDE.md files exist — caller should concatenate alongside `memoriesToPrompt`.
172
+ * Render loaded project-instruction entries as a system-prompt block. Empty
173
+ * when no source files exist — caller should concatenate alongside
174
+ * `memoriesToPrompt`. Header is generic ("Project instructions") since both
175
+ * CLAUDE.md and AGENTS.md feed in; the per-entry `source:` comment
176
+ * disambiguates which file each section came from.
167
177
  */
168
178
  export function claudeMdToPrompt(entries) {
169
179
  if (entries.length === 0)
170
180
  return "";
171
- const parts = ["# Project instructions (CLAUDE.md)"];
181
+ const parts = ["# Project instructions"];
172
182
  for (const e of entries) {
173
183
  parts.push(`<!-- source: ${e.source} (${e.path}) -->`);
174
184
  parts.push(e.content.trim());
@@ -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
@@ -2,10 +2,14 @@
2
2
  * Rules system — load project and global rules into agent context.
3
3
  * Discovery order:
4
4
  * 1. ~/.oh/global-rules/*.md
5
- * 2. CLAUDE.md files from parent directories down to project root (hierarchical)
5
+ * 2. CLAUDE.md / AGENTS.md files from parent directories down to project root (hierarchical)
6
6
  * 3. .oh/RULES.md
7
7
  * 4. .oh/rules/*.md
8
8
  * 5. CLAUDE.local.md (gitignored personal overrides)
9
+ *
10
+ * AGENTS.md (https://agents.md/) is read alongside CLAUDE.md at every level
11
+ * of the hierarchy so OH "just works" in repos already configured for the
12
+ * cross-tool standard (Codex, Cursor, Copilot, Cline, Aider all read it).
9
13
  */
10
14
  export declare function loadRules(projectPath?: string): string[];
11
15
  export declare function loadRulesAsPrompt(projectPath?: string): string;
@@ -2,10 +2,14 @@
2
2
  * Rules system — load project and global rules into agent context.
3
3
  * Discovery order:
4
4
  * 1. ~/.oh/global-rules/*.md
5
- * 2. CLAUDE.md files from parent directories down to project root (hierarchical)
5
+ * 2. CLAUDE.md / AGENTS.md files from parent directories down to project root (hierarchical)
6
6
  * 3. .oh/RULES.md
7
7
  * 4. .oh/rules/*.md
8
8
  * 5. CLAUDE.local.md (gitignored personal overrides)
9
+ *
10
+ * AGENTS.md (https://agents.md/) is read alongside CLAUDE.md at every level
11
+ * of the hierarchy so OH "just works" in repos already configured for the
12
+ * cross-tool standard (Codex, Cursor, Copilot, Cline, Aider all read it).
9
13
  */
10
14
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
11
15
  import { homedir } from "node:os";
@@ -13,10 +17,13 @@ import { dirname, join, parse as parsePath, resolve } from "node:path";
13
17
  import { gitRoot as getGitRoot } from "../git/index.js";
14
18
  const OH_HOME = join(homedir(), ".oh");
15
19
  /**
16
- * Walk from git root (or home) down to `projectRoot`, collecting CLAUDE.md files.
17
- * Returns them in parent-first order so more specific rules override general ones.
20
+ * Walk from git root (or home) down to `projectRoot`, collecting CLAUDE.md
21
+ * and AGENTS.md files at every level. Returns them in parent-first order so
22
+ * more specific rules override general ones. Within a single directory,
23
+ * CLAUDE.md is read before AGENTS.md (Anthropic-specific guidance first,
24
+ * then the cross-tool standard layer).
18
25
  */
19
- function loadClaudeMdFiles(projectRoot) {
26
+ function loadHierarchicalInstructionFiles(projectRoot) {
20
27
  const gitRootDir = getGitRoot(projectRoot);
21
28
  const stopAt = gitRootDir ? resolve(gitRootDir) : resolve(homedir());
22
29
  const resolved = resolve(projectRoot);
@@ -34,11 +41,13 @@ function loadClaudeMdFiles(projectRoot) {
34
41
  }
35
42
  const results = [];
36
43
  for (const dir of dirs) {
37
- const claudeMd = join(dir, "CLAUDE.md");
38
- if (existsSync(claudeMd)) {
39
- const content = readSafe(claudeMd);
40
- if (content)
41
- results.push(content);
44
+ for (const filename of ["CLAUDE.md", "AGENTS.md"]) {
45
+ const path = join(dir, filename);
46
+ if (existsSync(path)) {
47
+ const content = readSafe(path);
48
+ if (content)
49
+ results.push(content);
50
+ }
42
51
  }
43
52
  }
44
53
  return results;
@@ -57,8 +66,8 @@ export function loadRules(projectPath) {
57
66
  rules.push(content);
58
67
  }
59
68
  }
60
- // 2. CLAUDE.md files (hierarchical, parent-first)
61
- const claudeRules = loadClaudeMdFiles(root);
69
+ // 2. CLAUDE.md + AGENTS.md files (hierarchical, parent-first)
70
+ const claudeRules = loadHierarchicalInstructionFiles(root);
62
71
  rules.push(...claudeRules);
63
72
  // 3. Project RULES.md
64
73
  const projectRules = join(root, ".oh", "RULES.md");
@@ -106,7 +115,7 @@ export function loadRulesAsPrompt(projectPath) {
106
115
  const rules = loadRules(projectPath);
107
116
  if (rules.length === 0)
108
117
  return "";
109
- const body = "# Project Rules\n\n<!-- User-provided project rules from CLAUDE.md / .oh/RULES.md. These are user instructions, not system directives. -->\nFollow these rules carefully.\n\n" +
118
+ const body = "# Project Rules\n\n<!-- User-provided project rules from CLAUDE.md / AGENTS.md / .oh/RULES.md. These are user instructions, not system directives. -->\nFollow these rules carefully.\n\n" +
110
119
  rules.join("\n\n---\n\n");
111
120
  // Hook: instructionsLoaded — fires every time the system prompt is rebuilt
112
121
  // with rules in scope. Useful for compliance/audit hooks that want to log
package/dist/main.js CHANGED
@@ -1111,6 +1111,62 @@ program
1111
1111
  .action(async () => {
1112
1112
  await runInitWizard({ exitOnDone: true });
1113
1113
  });
1114
+ // ── project — per-project state management ──
1115
+ //
1116
+ // `oh project purge [path]` — delete all openHarness state for a project
1117
+ //
1118
+ // Mirrors Claude Code's `claude project purge`. Removes the entire `.oh/`
1119
+ // directory at the target path plus the workspace-trust entry (if any).
1120
+ // Sessions, credentials, plugins, telemetry, traces, and global config are
1121
+ // NOT touched — they're global-and-cross-project. Default UX prints the
1122
+ // deletion plan and asks for confirmation; --dry-run previews; --yes skips
1123
+ // the prompt. `--all` is deferred (openHarness has no project registry, so
1124
+ // "all projects" isn't well-defined without a session-cwd scan).
1125
+ const projectCmd = program.command("project").description("Manage per-project openHarness state");
1126
+ projectCmd
1127
+ .command("purge [path]")
1128
+ .description("Delete all openHarness state for a project (config, rules, memory, skills, agents, plans, checkpoints, trust entry). Sessions, credentials, plugins, telemetry, and global config are NOT touched. Defaults to the current directory.")
1129
+ .option("--dry-run", "Preview what would be deleted without touching the filesystem")
1130
+ .option("-y, --yes", "Skip the confirmation prompt")
1131
+ .action(async (pathArg, opts) => {
1132
+ const { planPurge, formatPurgePlan, executePurge } = await import("./harness/project-purge.js");
1133
+ const target = pathArg ?? process.cwd();
1134
+ if (!existsSync(target)) {
1135
+ process.stderr.write(`Error: path does not exist: ${target}\n`);
1136
+ process.exit(1);
1137
+ }
1138
+ const plan = planPurge(target);
1139
+ console.log(formatPurgePlan(plan));
1140
+ if (plan.entries.length === 0) {
1141
+ return;
1142
+ }
1143
+ if (opts.dryRun) {
1144
+ console.log("\n(dry-run — no files were deleted)");
1145
+ return;
1146
+ }
1147
+ if (!opts.yes) {
1148
+ const readline = await import("node:readline/promises");
1149
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1150
+ try {
1151
+ const answer = (await rl.question("\nProceed with deletion? [y/N] ")).trim();
1152
+ if (!/^y(es)?$/i.test(answer)) {
1153
+ console.log("Aborted.");
1154
+ return;
1155
+ }
1156
+ }
1157
+ finally {
1158
+ rl.close();
1159
+ }
1160
+ }
1161
+ const result = executePurge(plan);
1162
+ console.log(`\nDeleted ${result.deleted} of ${plan.entries.length} target(s).`);
1163
+ if (result.errors.length > 0) {
1164
+ console.log(`${result.errors.length} error(s):`);
1165
+ for (const err of result.errors)
1166
+ console.log(` ⚠ ${err}`);
1167
+ process.exit(1);
1168
+ }
1169
+ });
1114
1170
  // ── auth (audit B6) — provider-agnostic credential management ──
1115
1171
  //
1116
1172
  // `oh auth login [provider] --key <value>` — set API key for a provider
@@ -59,10 +59,9 @@ export const FileReadTool = {
59
59
  return { output: `Error: ${filePath} is a directory, not a file.`, isError: true };
60
60
  }
61
61
  const ext = path.extname(filePath).toLowerCase();
62
- // Image files: return as base64
62
+ // Image files: return as base64 (auto-downscaled if oversized)
63
63
  if (IMAGE_EXTENSIONS.has(ext)) {
64
- const buffer = await fs.readFile(filePath);
65
- const base64 = buffer.toString("base64");
64
+ const raw = await fs.readFile(filePath);
66
65
  const mimeTypes = {
67
66
  ".png": "image/png",
68
67
  ".jpg": "image/jpeg",
@@ -72,7 +71,11 @@ export const FileReadTool = {
72
71
  ".bmp": "image/bmp",
73
72
  ".svg": "image/svg+xml",
74
73
  };
75
- return { output: `__IMAGE__:${mimeTypes[ext] ?? "image/png"}:${base64}`, isError: false };
74
+ const mediaType = mimeTypes[ext] ?? "image/png";
75
+ const { downscaleIfLarge } = await import("../../utils/image-downscale.js");
76
+ const { buffer } = await downscaleIfLarge(raw, mediaType);
77
+ const base64 = buffer.toString("base64");
78
+ return { output: `__IMAGE__:${mediaType}:${base64}`, isError: false };
76
79
  }
77
80
  // PDF files: extract text per page (basic extraction)
78
81
  if (ext === ".pdf") {
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { z } from "zod";
4
+ import { downscaleIfLarge } from "../../utils/image-downscale.js";
4
5
  const SUPPORTED_TYPES = {
5
6
  ".png": "image/png",
6
7
  ".jpg": "image/jpeg",
@@ -37,7 +38,11 @@ export const ImageReadTool = {
37
38
  };
38
39
  }
39
40
  try {
40
- const buffer = await fs.readFile(filePath);
41
+ const raw = await fs.readFile(filePath);
42
+ // Auto-downscale to ≤2000px on the longest dimension. PDFs and
43
+ // missing-sharp installs pass through unchanged. Aspect + format
44
+ // preserved by sharp.
45
+ const { buffer } = await downscaleIfLarge(raw, mediaType);
41
46
  const base64 = buffer.toString("base64");
42
47
  return {
43
48
  output: `${IMAGE_PREFIX}:${mediaType}:${base64}`,
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Image auto-downscale — bound the longest dimension to a fixed maximum
3
+ * before encoding the image as base64 for the model.
4
+ *
5
+ * Why: most providers reject or downsample images above ~1568-2048px on
6
+ * the longest side. Shipping a 4000px screenshot wastes input tokens, can
7
+ * exceed the request size limit, and historically broke the session
8
+ * outright when an oversized image landed in the conversation history.
9
+ *
10
+ * The function is a no-op for images already within bounds, for formats
11
+ * sharp doesn't process (PDF, SVG), and when sharp itself isn't installed
12
+ * (it's an `optionalDependency` so unsupported platforms still install).
13
+ * Any sharp error returns the original buffer unchanged — we never break a
14
+ * tool call over a downscale failure.
15
+ */
16
+ /** @internal Test-only reset of the lazy sharp cache. */
17
+ export declare function _resetSharpCacheForTest(): void;
18
+ export type DownscaleResult = {
19
+ /** The (possibly resized) buffer to encode. */
20
+ buffer: Buffer;
21
+ /** True if a resize actually happened; false for passthrough. */
22
+ downscaled: boolean;
23
+ /** Set when sharp wasn't available — caller may want to surface a one-time hint. */
24
+ reason?: "sharp-unavailable" | "unsupported-format" | "within-bounds" | "sharp-error";
25
+ };
26
+ /**
27
+ * Downscale `buffer` so its longest dimension is ≤ `maxDimension` (default 2000).
28
+ * Aspect ratio preserved. Format preserved (PNG stays PNG, JPEG stays JPEG, etc.).
29
+ *
30
+ * Pure pass-through for: PDF, SVG, BMP (sharp doesn't handle reliably),
31
+ * already-small images, missing sharp, and any sharp error.
32
+ */
33
+ export declare function downscaleIfLarge(buffer: Buffer, mediaType: string, maxDimension?: number): Promise<DownscaleResult>;
34
+ //# sourceMappingURL=image-downscale.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Image auto-downscale — bound the longest dimension to a fixed maximum
3
+ * before encoding the image as base64 for the model.
4
+ *
5
+ * Why: most providers reject or downsample images above ~1568-2048px on
6
+ * the longest side. Shipping a 4000px screenshot wastes input tokens, can
7
+ * exceed the request size limit, and historically broke the session
8
+ * outright when an oversized image landed in the conversation history.
9
+ *
10
+ * The function is a no-op for images already within bounds, for formats
11
+ * sharp doesn't process (PDF, SVG), and when sharp itself isn't installed
12
+ * (it's an `optionalDependency` so unsupported platforms still install).
13
+ * Any sharp error returns the original buffer unchanged — we never break a
14
+ * tool call over a downscale failure.
15
+ */
16
+ const DEFAULT_MAX_DIMENSION = 2000;
17
+ const SHARP_SUPPORTED_TYPES = new Set([
18
+ "image/png",
19
+ "image/jpeg",
20
+ "image/jpg",
21
+ "image/gif",
22
+ "image/webp",
23
+ "image/avif",
24
+ "image/tiff",
25
+ ]);
26
+ let _sharpModule;
27
+ /** Lazy-load sharp; cache the result so we don't pay the import cost per image. */
28
+ async function getSharp() {
29
+ if (_sharpModule !== undefined)
30
+ return _sharpModule;
31
+ try {
32
+ const mod = (await import("sharp"));
33
+ _sharpModule = (mod.default ?? mod);
34
+ return _sharpModule;
35
+ }
36
+ catch {
37
+ _sharpModule = null;
38
+ return null;
39
+ }
40
+ }
41
+ /** @internal Test-only reset of the lazy sharp cache. */
42
+ export function _resetSharpCacheForTest() {
43
+ _sharpModule = undefined;
44
+ }
45
+ /**
46
+ * Downscale `buffer` so its longest dimension is ≤ `maxDimension` (default 2000).
47
+ * Aspect ratio preserved. Format preserved (PNG stays PNG, JPEG stays JPEG, etc.).
48
+ *
49
+ * Pure pass-through for: PDF, SVG, BMP (sharp doesn't handle reliably),
50
+ * already-small images, missing sharp, and any sharp error.
51
+ */
52
+ export async function downscaleIfLarge(buffer, mediaType, maxDimension = DEFAULT_MAX_DIMENSION) {
53
+ if (!SHARP_SUPPORTED_TYPES.has(mediaType)) {
54
+ return { buffer, downscaled: false, reason: "unsupported-format" };
55
+ }
56
+ const sharp = await getSharp();
57
+ if (!sharp) {
58
+ return { buffer, downscaled: false, reason: "sharp-unavailable" };
59
+ }
60
+ try {
61
+ const pipeline = sharp(buffer);
62
+ const meta = await pipeline.metadata();
63
+ const w = meta.width ?? 0;
64
+ const h = meta.height ?? 0;
65
+ if (w === 0 || h === 0) {
66
+ // Animated GIFs can report 0 here; pass through rather than mangle.
67
+ return { buffer, downscaled: false, reason: "unsupported-format" };
68
+ }
69
+ if (Math.max(w, h) <= maxDimension) {
70
+ return { buffer, downscaled: false, reason: "within-bounds" };
71
+ }
72
+ // `fit: "inside"` + `withoutEnlargement: true` resizes proportionally
73
+ // so the longest side equals maxDimension, with no upscaling.
74
+ const out = await pipeline
75
+ .resize({
76
+ width: maxDimension,
77
+ height: maxDimension,
78
+ fit: "inside",
79
+ withoutEnlargement: true,
80
+ })
81
+ .toBuffer();
82
+ return { buffer: out, downscaled: true };
83
+ }
84
+ catch {
85
+ // Corrupt image, unsupported subformat, etc. — never fail the tool over this.
86
+ return { buffer, downscaled: false, reason: "sharp-error" };
87
+ }
88
+ }
89
+ //# sourceMappingURL=image-downscale.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.30.1",
3
+ "version": "2.32.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,7 +63,6 @@
63
63
  "@types/react": "^18.3.0",
64
64
  "c8": "^11.0.0",
65
65
  "husky": "^9.1.7",
66
- "sharp": "^0.34.5",
67
66
  "tsx": "^4.19.0",
68
67
  "typescript": "^5.8.0"
69
68
  },
@@ -92,6 +91,7 @@
92
91
  },
93
92
  "homepage": "https://github.com/zhijiewong/openharness#readme",
94
93
  "optionalDependencies": {
95
- "@napi-rs/keyring": "^1.2.0"
94
+ "@napi-rs/keyring": "^1.2.0",
95
+ "sharp": "^0.34.5"
96
96
  }
97
97
  }
@@ -1,34 +0,0 @@
1
- /**
2
- * Sandbox — filesystem and network restrictions for tool execution.
3
- *
4
- * Limits what tools can access:
5
- * - File tools: only write to allowed paths
6
- * - Web tools: only access allowed domains
7
- * - Bash: restricted commands (no curl/wget by default)
8
- *
9
- * Reduces permission prompts while maintaining security.
10
- */
11
- export type SandboxConfig = {
12
- enabled: boolean;
13
- /** Paths tools can write to (glob-style, relative to cwd) */
14
- allowedPaths: string[];
15
- /** Domains WebFetch/WebSearch can access */
16
- allowedDomains: string[];
17
- /** Block all network access */
18
- blockNetwork: boolean;
19
- /** Commands blocked in Bash (default: curl, wget) */
20
- blockedCommands: string[];
21
- };
22
- /** Get the current sandbox config */
23
- export declare function getSandboxConfig(): SandboxConfig;
24
- /** Reset cached config */
25
- export declare function invalidateSandboxCache(): void;
26
- /** Check if a file path is allowed for writing */
27
- export declare function isPathAllowed(filePath: string): boolean;
28
- /** Check if a domain is allowed for network access */
29
- export declare function isDomainAllowed(url: string): boolean;
30
- /** Check if a bash command is allowed */
31
- export declare function isCommandAllowed(command: string): boolean;
32
- /** Get a human-readable sandbox status */
33
- export declare function sandboxStatus(): string;
34
- //# sourceMappingURL=sandbox.d.ts.map
@@ -1,104 +0,0 @@
1
- /**
2
- * Sandbox — filesystem and network restrictions for tool execution.
3
- *
4
- * Limits what tools can access:
5
- * - File tools: only write to allowed paths
6
- * - Web tools: only access allowed domains
7
- * - Bash: restricted commands (no curl/wget by default)
8
- *
9
- * Reduces permission prompts while maintaining security.
10
- */
11
- import { relative, resolve } from "node:path";
12
- import { readOhConfig } from "./config.js";
13
- const DEFAULT_SANDBOX = {
14
- enabled: false,
15
- allowedPaths: ["."], // current directory
16
- allowedDomains: [], // empty = all allowed
17
- blockNetwork: false,
18
- blockedCommands: ["curl", "wget"],
19
- };
20
- // ── Sandbox Manager ──
21
- let _config = null;
22
- /** Get the current sandbox config */
23
- export function getSandboxConfig() {
24
- if (_config)
25
- return _config;
26
- const ohConfig = readOhConfig();
27
- if (ohConfig?.sandbox) {
28
- _config = {
29
- ...DEFAULT_SANDBOX,
30
- ...ohConfig.sandbox,
31
- };
32
- }
33
- else {
34
- _config = DEFAULT_SANDBOX;
35
- }
36
- return _config;
37
- }
38
- /** Reset cached config */
39
- export function invalidateSandboxCache() {
40
- _config = null;
41
- }
42
- /** Check if a file path is allowed for writing */
43
- export function isPathAllowed(filePath) {
44
- const config = getSandboxConfig();
45
- if (!config.enabled)
46
- return true;
47
- const resolved = resolve(filePath);
48
- const cwd = process.cwd();
49
- for (const allowed of config.allowedPaths) {
50
- const allowedResolved = resolve(cwd, allowed);
51
- // Check if the file is within the allowed directory
52
- const rel = relative(allowedResolved, resolved);
53
- if (!rel.startsWith("..") && !rel.startsWith("/"))
54
- return true;
55
- }
56
- return false;
57
- }
58
- /** Check if a domain is allowed for network access */
59
- export function isDomainAllowed(url) {
60
- const config = getSandboxConfig();
61
- if (!config.enabled)
62
- return true;
63
- if (config.blockNetwork)
64
- return false;
65
- if (config.allowedDomains.length === 0)
66
- return true;
67
- try {
68
- const hostname = new URL(url).hostname.toLowerCase();
69
- return config.allowedDomains.some((d) => hostname === d.toLowerCase() || hostname.endsWith(`.${d.toLowerCase()}`));
70
- }
71
- catch {
72
- return false;
73
- }
74
- }
75
- /** Check if a bash command is allowed */
76
- export function isCommandAllowed(command) {
77
- const config = getSandboxConfig();
78
- if (!config.enabled)
79
- return true;
80
- const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
81
- return !config.blockedCommands.includes(firstWord);
82
- }
83
- /** Get a human-readable sandbox status */
84
- export function sandboxStatus() {
85
- const config = getSandboxConfig();
86
- if (!config.enabled)
87
- return "Sandbox: disabled";
88
- const lines = ["Sandbox: enabled"];
89
- lines.push(` Allowed paths: ${config.allowedPaths.join(", ") || "none"}`);
90
- if (config.blockNetwork) {
91
- lines.push(" Network: blocked");
92
- }
93
- else if (config.allowedDomains.length > 0) {
94
- lines.push(` Allowed domains: ${config.allowedDomains.join(", ")}`);
95
- }
96
- else {
97
- lines.push(" Network: unrestricted");
98
- }
99
- if (config.blockedCommands.length > 0) {
100
- lines.push(` Blocked commands: ${config.blockedCommands.join(", ")}`);
101
- }
102
- return lines.join("\n");
103
- }
104
- //# sourceMappingURL=sandbox.js.map