@zhijiewang/openharness 2.0.0 → 2.3.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 +4 -4
- package/dist/DeferredTool.js +3 -1
- package/dist/Tool.d.ts +1 -1
- package/dist/agents/roles.js +58 -62
- package/dist/commands/cybergotchi.d.ts +1 -1
- package/dist/commands/cybergotchi.js +30 -30
- package/dist/commands/index.js +360 -122
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +6 -6
- package/dist/components/CompanionFooter.d.ts +1 -1
- package/dist/components/CompanionFooter.js +6 -8
- package/dist/components/CybergotchiBubble.js +5 -5
- package/dist/components/CybergotchiPanel.d.ts +1 -1
- package/dist/components/CybergotchiPanel.js +7 -7
- package/dist/components/CybergotchiPanelConnected.js +2 -2
- package/dist/components/CybergotchiSetup.js +26 -24
- package/dist/components/CybergotchiSprite.d.ts +1 -1
- package/dist/components/CybergotchiSprite.js +8 -12
- package/dist/components/DiffView.d.ts +1 -1
- package/dist/components/DiffView.js +10 -10
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/InitWizard.js +65 -33
- package/dist/components/Markdown.js +2 -4
- package/dist/components/Messages.js +4 -4
- package/dist/components/PermissionPrompt.d.ts +1 -1
- package/dist/components/PermissionPrompt.js +15 -17
- package/dist/components/REPL.d.ts +1 -1
- package/dist/components/REPL.js +74 -49
- package/dist/components/Spinner.js +2 -2
- package/dist/components/TextInput.js +35 -29
- package/dist/components/ToolCallDisplay.js +3 -5
- package/dist/cybergotchi/bones.d.ts +1 -1
- package/dist/cybergotchi/bones.js +8 -8
- package/dist/cybergotchi/config.d.ts +2 -2
- package/dist/cybergotchi/config.js +13 -13
- package/dist/cybergotchi/events.d.ts +5 -5
- package/dist/cybergotchi/events.js +7 -7
- package/dist/cybergotchi/needs.d.ts +2 -2
- package/dist/cybergotchi/needs.js +7 -9
- package/dist/cybergotchi/personality.d.ts +2 -2
- package/dist/cybergotchi/personality.js +2 -2
- package/dist/cybergotchi/species.d.ts +1 -1
- package/dist/cybergotchi/species.js +145 -217
- package/dist/cybergotchi/speech.d.ts +2 -2
- package/dist/cybergotchi/speech.js +43 -43
- package/dist/cybergotchi/types.d.ts +4 -4
- package/dist/cybergotchi/types.js +26 -26
- package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
- package/dist/cybergotchi/useCybergotchi.js +29 -25
- package/dist/git/index.js +11 -9
- package/dist/harness/checkpoints.js +29 -21
- package/dist/harness/config.d.ts +12 -2
- package/dist/harness/config.js +15 -9
- package/dist/harness/context-warning.d.ts +1 -1
- package/dist/harness/context-warning.js +1 -1
- package/dist/harness/cost.js +1 -1
- package/dist/harness/credentials.js +13 -13
- package/dist/harness/hooks.js +7 -5
- package/dist/harness/keybindings.js +20 -18
- package/dist/harness/marketplace.d.ts +3 -3
- package/dist/harness/marketplace.js +55 -42
- package/dist/harness/memory.d.ts +23 -5
- package/dist/harness/memory.js +142 -41
- package/dist/harness/onboarding.js +30 -10
- package/dist/harness/plugins.d.ts +9 -1
- package/dist/harness/plugins.js +54 -30
- package/dist/harness/rules.js +12 -7
- package/dist/harness/sandbox.d.ts +34 -0
- package/dist/harness/sandbox.js +104 -0
- package/dist/harness/session-db.d.ts +55 -0
- package/dist/harness/session-db.js +165 -0
- package/dist/harness/session.d.ts +1 -1
- package/dist/harness/session.js +34 -15
- package/dist/harness/store.d.ts +3 -3
- package/dist/harness/store.js +6 -4
- package/dist/harness/submit-handler.d.ts +4 -4
- package/dist/harness/submit-handler.js +57 -21
- package/dist/harness/telemetry.d.ts +1 -1
- package/dist/harness/telemetry.js +23 -19
- package/dist/harness/traces.d.ts +2 -2
- package/dist/harness/traces.js +44 -33
- package/dist/harness/verification.d.ts +1 -1
- package/dist/harness/verification.js +50 -44
- package/dist/lsp/client.js +44 -40
- package/dist/main.js +100 -59
- package/dist/mcp/DeferredMcpTool.d.ts +4 -4
- package/dist/mcp/DeferredMcpTool.js +9 -5
- package/dist/mcp/McpTool.d.ts +4 -4
- package/dist/mcp/McpTool.js +8 -4
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +21 -21
- package/dist/mcp/loader.d.ts +1 -1
- package/dist/mcp/loader.js +17 -12
- package/dist/mcp/registry.d.ts +3 -3
- package/dist/mcp/registry.js +97 -97
- package/dist/mcp/schema.d.ts +1 -1
- package/dist/mcp/schema.js +16 -16
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +21 -21
- package/dist/mcp/types.d.ts +3 -3
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +10 -9
- package/dist/providers/base.d.ts +1 -1
- package/dist/providers/index.js +10 -3
- package/dist/providers/llamacpp.d.ts +2 -2
- package/dist/providers/llamacpp.js +1 -3
- package/dist/providers/ollama.d.ts +2 -2
- package/dist/providers/ollama.js +3 -4
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.js +3 -5
- package/dist/providers/openrouter.d.ts +2 -2
- package/dist/providers/router.d.ts +1 -1
- package/dist/providers/router.js +7 -7
- package/dist/query/compress.d.ts +2 -2
- package/dist/query/compress.js +22 -21
- package/dist/query/context-manager.d.ts +2 -2
- package/dist/query/context-manager.js +8 -11
- package/dist/query/errors.js +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +30 -22
- package/dist/query/tools.js +15 -12
- package/dist/query/types.d.ts +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/remote/auth.d.ts +2 -2
- package/dist/remote/auth.js +8 -8
- package/dist/remote/server.d.ts +3 -3
- package/dist/remote/server.js +60 -60
- package/dist/renderer/cells.js +9 -9
- package/dist/renderer/colors.js +24 -6
- package/dist/renderer/diff.d.ts +2 -2
- package/dist/renderer/diff.js +27 -19
- package/dist/renderer/differ.d.ts +1 -1
- package/dist/renderer/differ.js +9 -9
- package/dist/renderer/image.js +19 -19
- package/dist/renderer/index.d.ts +6 -6
- package/dist/renderer/index.js +163 -93
- package/dist/renderer/input.js +66 -48
- package/dist/renderer/layout.d.ts +6 -6
- package/dist/renderer/layout.js +163 -124
- package/dist/renderer/markdown.d.ts +2 -2
- package/dist/renderer/markdown.js +173 -54
- package/dist/renderer/session-browser.d.ts +2 -2
- package/dist/renderer/session-browser.js +19 -21
- package/dist/repl.d.ts +5 -5
- package/dist/repl.js +300 -198
- package/dist/sdk/index.d.ts +8 -7
- package/dist/sdk/index.js +59 -42
- package/dist/services/AgentDispatcher.d.ts +3 -3
- package/dist/services/AgentDispatcher.js +33 -29
- package/dist/services/CronExecutor.d.ts +4 -4
- package/dist/services/CronExecutor.js +12 -8
- package/dist/services/EvaluatorLoop.d.ts +3 -3
- package/dist/services/EvaluatorLoop.js +29 -21
- package/dist/services/MetaHarness.d.ts +1 -1
- package/dist/services/MetaHarness.js +41 -33
- package/dist/services/PipelineExecutor.d.ts +1 -1
- package/dist/services/PipelineExecutor.js +23 -25
- package/dist/services/SkillExtractor.d.ts +43 -0
- package/dist/services/SkillExtractor.js +143 -0
- package/dist/services/StreamingToolExecutor.d.ts +2 -2
- package/dist/services/StreamingToolExecutor.js +11 -7
- package/dist/services/a2a.d.ts +8 -8
- package/dist/services/a2a.js +44 -34
- package/dist/services/agent-messaging.d.ts +33 -15
- package/dist/services/agent-messaging.js +65 -13
- package/dist/services/cron.js +16 -16
- package/dist/tools/AgentTool/index.d.ts +5 -2
- package/dist/tools/AgentTool/index.js +35 -15
- package/dist/tools/AskUserTool/index.js +1 -1
- package/dist/tools/BashTool/index.d.ts +2 -2
- package/dist/tools/BashTool/index.js +18 -10
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/CronTool/index.js +30 -12
- package/dist/tools/DiagnosticsTool/index.js +28 -22
- package/dist/tools/EnterPlanModeTool/index.js +93 -14
- package/dist/tools/EnterWorktreeTool/index.js +7 -3
- package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
- package/dist/tools/ExitPlanModeTool/index.js +20 -5
- package/dist/tools/ExitWorktreeTool/index.js +11 -4
- package/dist/tools/FileEditTool/index.js +3 -5
- package/dist/tools/FileReadTool/index.js +16 -10
- package/dist/tools/FileWriteTool/index.js +2 -2
- package/dist/tools/GlobTool/index.js +5 -9
- package/dist/tools/GrepTool/index.d.ts +2 -2
- package/dist/tools/GrepTool/index.js +14 -9
- package/dist/tools/ImageReadTool/index.js +2 -2
- package/dist/tools/KillProcessTool/index.js +11 -7
- package/dist/tools/LSTool/index.js +3 -3
- package/dist/tools/MemoryTool/index.d.ts +11 -11
- package/dist/tools/MemoryTool/index.js +28 -14
- package/dist/tools/MonitorTool/index.d.ts +2 -2
- package/dist/tools/MonitorTool/index.js +24 -19
- package/dist/tools/MultiEditTool/index.js +9 -5
- package/dist/tools/NotebookEditTool/index.js +3 -3
- package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
- package/dist/tools/ParallelAgentTool/index.js +12 -6
- package/dist/tools/PipelineTool/index.d.ts +4 -4
- package/dist/tools/PipelineTool/index.js +3 -3
- package/dist/tools/PowerShellTool/index.js +10 -6
- package/dist/tools/RemoteTriggerTool/index.js +8 -4
- package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
- package/dist/tools/ScheduleWakeupTool/index.js +115 -0
- package/dist/tools/SendMessageTool/index.js +25 -7
- package/dist/tools/SessionSearchTool/index.d.ts +15 -0
- package/dist/tools/SessionSearchTool/index.js +36 -0
- package/dist/tools/SkillTool/index.d.ts +3 -0
- package/dist/tools/SkillTool/index.js +39 -9
- package/dist/tools/TaskCreateTool/index.d.ts +2 -2
- package/dist/tools/TaskCreateTool/index.js +2 -2
- package/dist/tools/TaskGetTool/index.js +2 -2
- package/dist/tools/TaskListTool/index.js +3 -5
- package/dist/tools/TaskOutputTool/index.js +2 -2
- package/dist/tools/TaskStopTool/index.js +3 -3
- package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
- package/dist/tools/TaskUpdateTool/index.js +2 -2
- package/dist/tools/ToolSearchTool/index.js +9 -6
- package/dist/tools/WebFetchTool/index.js +1 -1
- package/dist/tools/WebSearchTool/index.js +2 -6
- package/dist/tools.js +31 -30
- package/dist/types/permissions.js +15 -9
- package/dist/utils/bash-safety.d.ts +1 -1
- package/dist/utils/bash-safety.js +64 -54
- package/dist/utils/diff-algorithm.d.ts +3 -3
- package/dist/utils/diff-algorithm.js +7 -7
- package/dist/utils/fs.js +3 -3
- package/dist/utils/safe-env.js +1 -1
- package/dist/utils/theme-data.d.ts +1 -1
- package/dist/utils/theme-data.js +1 -1
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/tool-summary.d.ts +1 -1
- package/dist/utils/tool-summary.js +27 -9
- package/package.json +10 -3
package/dist/harness/rules.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* 4. .oh/rules/*.md
|
|
8
8
|
* 5. CLAUDE.local.md (gitignored personal overrides)
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { join, resolve, dirname, parse as parsePath } from "node:path";
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
11
|
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join, parse as parsePath, resolve } from "node:path";
|
|
13
13
|
import { gitRoot as getGitRoot } from "../git/index.js";
|
|
14
14
|
const OH_HOME = join(homedir(), ".oh");
|
|
15
15
|
/**
|
|
@@ -49,7 +49,9 @@ export function loadRules(projectPath) {
|
|
|
49
49
|
// 1. Global rules
|
|
50
50
|
const globalDir = join(OH_HOME, "global-rules");
|
|
51
51
|
if (existsSync(globalDir)) {
|
|
52
|
-
for (const file of readdirSync(globalDir)
|
|
52
|
+
for (const file of readdirSync(globalDir)
|
|
53
|
+
.filter((f) => f.endsWith(".md"))
|
|
54
|
+
.sort()) {
|
|
53
55
|
const content = readSafe(join(globalDir, file));
|
|
54
56
|
if (content)
|
|
55
57
|
rules.push(content);
|
|
@@ -68,7 +70,9 @@ export function loadRules(projectPath) {
|
|
|
68
70
|
// 4. Project rules/*.md (with optional path-scoped filtering)
|
|
69
71
|
const rulesDir = join(root, ".oh", "rules");
|
|
70
72
|
if (existsSync(rulesDir)) {
|
|
71
|
-
for (const file of readdirSync(rulesDir)
|
|
73
|
+
for (const file of readdirSync(rulesDir)
|
|
74
|
+
.filter((f) => f.endsWith(".md"))
|
|
75
|
+
.sort()) {
|
|
72
76
|
const raw = readSafe(join(rulesDir, file));
|
|
73
77
|
if (!raw)
|
|
74
78
|
continue;
|
|
@@ -77,7 +81,7 @@ export function loadRules(projectPath) {
|
|
|
77
81
|
if (pathsMatch) {
|
|
78
82
|
// Path-scoped rule — strip frontmatter and only include if glob matches
|
|
79
83
|
const pattern = pathsMatch[1].trim();
|
|
80
|
-
const fmEnd = raw.indexOf(
|
|
84
|
+
const fmEnd = raw.indexOf("---", raw.indexOf("---") + 3);
|
|
81
85
|
const content = fmEnd > 0 ? raw.slice(fmEnd + 3).trim() : raw;
|
|
82
86
|
if (content && matchesPathGlob(root, pattern)) {
|
|
83
87
|
rules.push(content);
|
|
@@ -102,7 +106,8 @@ export function loadRulesAsPrompt(projectPath) {
|
|
|
102
106
|
const rules = loadRules(projectPath);
|
|
103
107
|
if (rules.length === 0)
|
|
104
108
|
return "";
|
|
105
|
-
return "# 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" +
|
|
109
|
+
return ("# 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" +
|
|
110
|
+
rules.join("\n\n---\n\n"));
|
|
106
111
|
}
|
|
107
112
|
export function createRulesFile(projectPath) {
|
|
108
113
|
const root = projectPath ?? process.cwd();
|
|
@@ -132,7 +137,7 @@ function readSafe(path) {
|
|
|
132
137
|
*/
|
|
133
138
|
function matchesPathGlob(root, pattern) {
|
|
134
139
|
// Extract the directory portion before any wildcard
|
|
135
|
-
const dirPart = pattern.split(
|
|
140
|
+
const dirPart = pattern.split("*")[0].replace(/\/+$/, "");
|
|
136
141
|
if (!dirPart)
|
|
137
142
|
return true; // Pattern like "**/*.ts" matches everything
|
|
138
143
|
const fullDir = join(root, dirPart);
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite FTS5-based session search index.
|
|
3
|
+
* Provides fast full-text search over session content using BM25 ranking.
|
|
4
|
+
*/
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import type { Session } from "./session.js";
|
|
7
|
+
export type SessionIndexEntry = {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
content: string;
|
|
10
|
+
toolsUsed: string[];
|
|
11
|
+
model: string;
|
|
12
|
+
messageCount: number;
|
|
13
|
+
cost: number;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
};
|
|
17
|
+
export type SessionSearchResult = {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
snippet: string;
|
|
20
|
+
model: string;
|
|
21
|
+
messageCount: number;
|
|
22
|
+
cost: number;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
rank: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Opens or creates a SQLite DB with FTS5 virtual table for session search.
|
|
28
|
+
*/
|
|
29
|
+
export declare function openSessionDb(dbPath?: string): Database.Database;
|
|
30
|
+
/**
|
|
31
|
+
* Closes the SQLite database connection.
|
|
32
|
+
*/
|
|
33
|
+
export declare function closeSessionDb(db: Database.Database): void;
|
|
34
|
+
/**
|
|
35
|
+
* Upserts a session index entry using delete+insert pattern.
|
|
36
|
+
*/
|
|
37
|
+
export declare function indexSession(db: Database.Database, entry: SessionIndexEntry): void;
|
|
38
|
+
/**
|
|
39
|
+
* Searches sessions using FTS5 with BM25 ranking.
|
|
40
|
+
* Returns results with snippets showing matching context.
|
|
41
|
+
*/
|
|
42
|
+
export declare function searchSessions(db: Database.Database, query: string, limit?: number): SessionSearchResult[];
|
|
43
|
+
/**
|
|
44
|
+
* Converts a Session object to a SessionIndexEntry for indexing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sessionToIndexEntry(session: Session): SessionIndexEntry;
|
|
47
|
+
/**
|
|
48
|
+
* Rebuilds the FTS5 index from session JSON files on disk.
|
|
49
|
+
*/
|
|
50
|
+
export declare function rebuildIndex(db: Database.Database, sessionsDir?: string): number;
|
|
51
|
+
/** Get a shared DB connection (opens once, reuses thereafter) */
|
|
52
|
+
export declare function getSessionDb(): Database.Database;
|
|
53
|
+
/** Close the singleton connection (call on process exit) */
|
|
54
|
+
export declare function closeGlobalSessionDb(): void;
|
|
55
|
+
//# sourceMappingURL=session-db.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite FTS5-based session search index.
|
|
3
|
+
* Provides fast full-text search over session content using BM25 ranking.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import Database from "better-sqlite3";
|
|
9
|
+
const DEFAULT_DB_PATH = join(homedir(), ".oh", "sessions.db");
|
|
10
|
+
const DEFAULT_SESSION_DIR = join(homedir(), ".oh", "sessions");
|
|
11
|
+
/**
|
|
12
|
+
* Opens or creates a SQLite DB with FTS5 virtual table for session search.
|
|
13
|
+
*/
|
|
14
|
+
export function openSessionDb(dbPath) {
|
|
15
|
+
const path = dbPath ?? DEFAULT_DB_PATH;
|
|
16
|
+
const dir = dirname(path);
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
const db = new Database(path);
|
|
19
|
+
// Enable WAL mode for better concurrent read performance
|
|
20
|
+
db.pragma("journal_mode = WAL");
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
|
|
23
|
+
session_id, content, tools_used, model,
|
|
24
|
+
message_count UNINDEXED, cost UNINDEXED,
|
|
25
|
+
created_at UNINDEXED, updated_at UNINDEXED
|
|
26
|
+
);
|
|
27
|
+
`);
|
|
28
|
+
return db;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Closes the SQLite database connection.
|
|
32
|
+
*/
|
|
33
|
+
export function closeSessionDb(db) {
|
|
34
|
+
try {
|
|
35
|
+
db.close();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* skip */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Upserts a session index entry using delete+insert pattern.
|
|
43
|
+
*/
|
|
44
|
+
export function indexSession(db, entry) {
|
|
45
|
+
const del = db.prepare("DELETE FROM sessions_fts WHERE session_id = ?");
|
|
46
|
+
const ins = db.prepare("INSERT INTO sessions_fts (session_id, content, tools_used, model, message_count, cost, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
47
|
+
const upsert = db.transaction(() => {
|
|
48
|
+
del.run(entry.sessionId);
|
|
49
|
+
ins.run(entry.sessionId, entry.content, entry.toolsUsed.join(" "), entry.model, entry.messageCount, entry.cost, entry.createdAt, entry.updatedAt);
|
|
50
|
+
});
|
|
51
|
+
upsert();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Searches sessions using FTS5 with BM25 ranking.
|
|
55
|
+
* Returns results with snippets showing matching context.
|
|
56
|
+
*/
|
|
57
|
+
export function searchSessions(db, query, limit = 20) {
|
|
58
|
+
const stmt = db.prepare(`
|
|
59
|
+
SELECT
|
|
60
|
+
session_id,
|
|
61
|
+
snippet(sessions_fts, 1, '>>>', '<<<', '...', 64) AS snippet,
|
|
62
|
+
model,
|
|
63
|
+
CAST(message_count AS INTEGER) AS message_count,
|
|
64
|
+
CAST(cost AS REAL) AS cost,
|
|
65
|
+
CAST(updated_at AS INTEGER) AS updated_at,
|
|
66
|
+
rank
|
|
67
|
+
FROM sessions_fts
|
|
68
|
+
WHERE sessions_fts MATCH ?
|
|
69
|
+
ORDER BY rank
|
|
70
|
+
LIMIT ?
|
|
71
|
+
`);
|
|
72
|
+
try {
|
|
73
|
+
const rows = stmt.all(query, limit);
|
|
74
|
+
return rows.map((row) => ({
|
|
75
|
+
sessionId: row.session_id,
|
|
76
|
+
snippet: row.snippet,
|
|
77
|
+
model: row.model,
|
|
78
|
+
messageCount: row.message_count,
|
|
79
|
+
cost: row.cost,
|
|
80
|
+
updatedAt: row.updated_at,
|
|
81
|
+
rank: row.rank,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
// Only swallow FTS5 syntax errors; rethrow DB corruption or other issues
|
|
86
|
+
if (err instanceof Error && (err.message.includes("fts5") || err.message.includes("syntax"))) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Converts a Session object to a SessionIndexEntry for indexing.
|
|
94
|
+
*/
|
|
95
|
+
export function sessionToIndexEntry(session) {
|
|
96
|
+
// Concatenate user + assistant message text
|
|
97
|
+
const contentParts = [];
|
|
98
|
+
const toolsSet = new Set();
|
|
99
|
+
for (const msg of session.messages) {
|
|
100
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
101
|
+
if (msg.content) {
|
|
102
|
+
contentParts.push(msg.content);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Dedupe tool names from toolCalls
|
|
106
|
+
if (msg.toolCalls) {
|
|
107
|
+
for (const tc of msg.toolCalls) {
|
|
108
|
+
toolsSet.add(tc.toolName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
sessionId: session.id,
|
|
114
|
+
content: contentParts.join(" "),
|
|
115
|
+
toolsUsed: Array.from(toolsSet),
|
|
116
|
+
model: session.model,
|
|
117
|
+
messageCount: session.messages.length,
|
|
118
|
+
cost: session.totalCost,
|
|
119
|
+
createdAt: session.createdAt,
|
|
120
|
+
updatedAt: session.updatedAt,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Rebuilds the FTS5 index from session JSON files on disk.
|
|
125
|
+
*/
|
|
126
|
+
export function rebuildIndex(db, sessionsDir) {
|
|
127
|
+
const dir = sessionsDir ?? DEFAULT_SESSION_DIR;
|
|
128
|
+
if (!existsSync(dir))
|
|
129
|
+
return 0;
|
|
130
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
try {
|
|
134
|
+
const raw = readFileSync(join(dir, file), "utf-8");
|
|
135
|
+
const session = JSON.parse(raw);
|
|
136
|
+
const entry = sessionToIndexEntry(session);
|
|
137
|
+
indexSession(db, entry);
|
|
138
|
+
count++;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* skip invalid/corrupt files */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
// ── Singleton Connection ──
|
|
147
|
+
let _singletonDb = null;
|
|
148
|
+
/** Get a shared DB connection (opens once, reuses thereafter) */
|
|
149
|
+
export function getSessionDb() {
|
|
150
|
+
if (!_singletonDb) {
|
|
151
|
+
_singletonDb = openSessionDb();
|
|
152
|
+
}
|
|
153
|
+
return _singletonDb;
|
|
154
|
+
}
|
|
155
|
+
/** Close the singleton connection (call on process exit) */
|
|
156
|
+
export function closeGlobalSessionDb() {
|
|
157
|
+
if (_singletonDb) {
|
|
158
|
+
try {
|
|
159
|
+
_singletonDb.close();
|
|
160
|
+
}
|
|
161
|
+
catch { /* ignore */ }
|
|
162
|
+
_singletonDb = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=session-db.js.map
|
|
@@ -43,7 +43,7 @@ export declare function getLastSessionId(dir?: string): string | null;
|
|
|
43
43
|
* Captures the last user message, recent assistant activity,
|
|
44
44
|
* and a brief summary for context reconstruction on wake.
|
|
45
45
|
*/
|
|
46
|
-
export declare function buildHibernateState(messages: Message[]): Session[
|
|
46
|
+
export declare function buildHibernateState(messages: Message[]): Session["hibernate"];
|
|
47
47
|
/**
|
|
48
48
|
* Generate a wake-up context message for a resumed session.
|
|
49
49
|
* Tells the LLM what happened in the previous session.
|
package/dist/harness/session.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session persistence — save and resume conversations.
|
|
3
3
|
*/
|
|
4
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, unlinkSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
8
|
const DEFAULT_SESSION_DIR = join(homedir(), ".oh", "sessions");
|
|
9
9
|
export function createSession(provider, model, extras) {
|
|
10
10
|
return {
|
|
@@ -27,13 +27,28 @@ export function saveSession(session, dir) {
|
|
|
27
27
|
const path = join(sessionDir, `${session.id}.json`);
|
|
28
28
|
session.updatedAt = Date.now();
|
|
29
29
|
writeFileSync(path, JSON.stringify(session, null, 2));
|
|
30
|
+
// Index session for FTS5 search (fire-and-forget, singleton connection)
|
|
31
|
+
import("./session-db.js")
|
|
32
|
+
.then(({ getSessionDb, indexSession: idx, sessionToIndexEntry }) => {
|
|
33
|
+
try {
|
|
34
|
+
idx(getSessionDb(), sessionToIndexEntry(session));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* session search is optional */
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.catch(() => {
|
|
41
|
+
/* ignore if session-db unavailable */
|
|
42
|
+
});
|
|
30
43
|
// Evict old sessions (with lock to prevent concurrent eviction)
|
|
31
44
|
if (!_evicting) {
|
|
32
45
|
_evicting = true;
|
|
33
46
|
try {
|
|
34
47
|
evictOldSessions(sessionDir);
|
|
35
48
|
}
|
|
36
|
-
catch {
|
|
49
|
+
catch {
|
|
50
|
+
/* ignore */
|
|
51
|
+
}
|
|
37
52
|
_evicting = false;
|
|
38
53
|
}
|
|
39
54
|
return path;
|
|
@@ -81,23 +96,23 @@ export function buildHibernateState(messages) {
|
|
|
81
96
|
if (messages.length === 0)
|
|
82
97
|
return undefined;
|
|
83
98
|
// Find last user message
|
|
84
|
-
const lastUser = [...messages].reverse().find(m => m.role ===
|
|
85
|
-
const lastAssistant = [...messages].reverse().find(m => m.role ===
|
|
99
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
100
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
|
|
86
101
|
// Build a brief summary from the last few exchanges
|
|
87
102
|
const recentMsgs = messages.slice(-6);
|
|
88
103
|
const summaryParts = [];
|
|
89
104
|
for (const m of recentMsgs) {
|
|
90
|
-
if (m.role ===
|
|
105
|
+
if (m.role === "user") {
|
|
91
106
|
summaryParts.push(`User: ${m.content.slice(0, 100)}`);
|
|
92
107
|
}
|
|
93
|
-
else if (m.role ===
|
|
108
|
+
else if (m.role === "assistant" && m.content) {
|
|
94
109
|
summaryParts.push(`Assistant: ${m.content.slice(0, 100)}`);
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
return {
|
|
98
113
|
lastUserMessage: lastUser?.content.slice(0, 200),
|
|
99
114
|
pendingTask: lastAssistant?.content.slice(0, 200),
|
|
100
|
-
summary: summaryParts.join(
|
|
115
|
+
summary: summaryParts.join("\n"),
|
|
101
116
|
};
|
|
102
117
|
}
|
|
103
118
|
/**
|
|
@@ -105,7 +120,7 @@ export function buildHibernateState(messages) {
|
|
|
105
120
|
* Tells the LLM what happened in the previous session.
|
|
106
121
|
*/
|
|
107
122
|
export function buildWakeContext(session) {
|
|
108
|
-
const parts = [
|
|
123
|
+
const parts = ["[Session Resumed]"];
|
|
109
124
|
if (session.workingDir) {
|
|
110
125
|
parts.push(`Previous working directory: ${session.workingDir}`);
|
|
111
126
|
if (session.workingDir !== process.cwd()) {
|
|
@@ -122,8 +137,8 @@ export function buildWakeContext(session) {
|
|
|
122
137
|
parts.push(`\nLast user request: ${session.hibernate.lastUserMessage}`);
|
|
123
138
|
}
|
|
124
139
|
parts.push(`\nSession has ${session.messages.length} messages and cost $${session.totalCost.toFixed(4)} so far.`);
|
|
125
|
-
parts.push(
|
|
126
|
-
return parts.join(
|
|
140
|
+
parts.push("Continue where you left off. If the user's last request was incomplete, acknowledge that and ask how to proceed.");
|
|
141
|
+
return parts.join("\n");
|
|
127
142
|
}
|
|
128
143
|
/** Maximum number of sessions to keep on disk. */
|
|
129
144
|
const MAX_SESSIONS = 100;
|
|
@@ -139,7 +154,8 @@ export function evictOldSessions(dir, maxSessions = MAX_SESSIONS) {
|
|
|
139
154
|
if (files.length <= maxSessions)
|
|
140
155
|
return 0;
|
|
141
156
|
// Sort by modification time (oldest first)
|
|
142
|
-
const withStats = files
|
|
157
|
+
const withStats = files
|
|
158
|
+
.map((f) => {
|
|
143
159
|
const path = join(sessionDir, f);
|
|
144
160
|
try {
|
|
145
161
|
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
@@ -148,13 +164,16 @@ export function evictOldSessions(dir, maxSessions = MAX_SESSIONS) {
|
|
|
148
164
|
catch {
|
|
149
165
|
return { path, updatedAt: 0 };
|
|
150
166
|
}
|
|
151
|
-
})
|
|
167
|
+
})
|
|
168
|
+
.sort((a, b) => a.updatedAt - b.updatedAt);
|
|
152
169
|
const toRemove = withStats.slice(0, files.length - maxSessions);
|
|
153
170
|
for (const { path } of toRemove) {
|
|
154
171
|
try {
|
|
155
172
|
unlinkSync(path);
|
|
156
173
|
}
|
|
157
|
-
catch {
|
|
174
|
+
catch {
|
|
175
|
+
/* ignore */
|
|
176
|
+
}
|
|
158
177
|
}
|
|
159
178
|
return toRemove.length;
|
|
160
179
|
}
|
package/dist/harness/store.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Simple reactive store inspired by Zustand but without React dependency.
|
|
5
5
|
* State is modified via setState() which notifies subscribers.
|
|
6
6
|
*/
|
|
7
|
-
import type { Message } from
|
|
8
|
-
import type { Session } from
|
|
7
|
+
import type { Message } from "../types/message.js";
|
|
8
|
+
import type { Session } from "./session.js";
|
|
9
9
|
export type REPLState = {
|
|
10
10
|
messages: Message[];
|
|
11
11
|
loading: boolean;
|
|
@@ -14,7 +14,7 @@ export type REPLState = {
|
|
|
14
14
|
inputCursor: number;
|
|
15
15
|
inputHistory: string[];
|
|
16
16
|
historyIndex: number;
|
|
17
|
-
vimMode:
|
|
17
|
+
vimMode: "normal" | "insert" | null;
|
|
18
18
|
fastMode: boolean;
|
|
19
19
|
companionVisible: boolean;
|
|
20
20
|
acSuggestions: string[];
|
package/dist/harness/store.js
CHANGED
|
@@ -8,8 +8,8 @@ export function createInitialState(overrides) {
|
|
|
8
8
|
return {
|
|
9
9
|
messages: [],
|
|
10
10
|
loading: false,
|
|
11
|
-
currentModel:
|
|
12
|
-
inputText:
|
|
11
|
+
currentModel: "",
|
|
12
|
+
inputText: "",
|
|
13
13
|
inputCursor: 0,
|
|
14
14
|
inputHistory: [],
|
|
15
15
|
historyIndex: -1,
|
|
@@ -42,14 +42,16 @@ export function createStore(initial) {
|
|
|
42
42
|
return {
|
|
43
43
|
getState: () => state,
|
|
44
44
|
setState(partial) {
|
|
45
|
-
const updates = typeof partial ===
|
|
45
|
+
const updates = typeof partial === "function" ? partial(state) : partial;
|
|
46
46
|
state = { ...state, ...updates };
|
|
47
47
|
for (const fn of subscribers)
|
|
48
48
|
fn(state);
|
|
49
49
|
},
|
|
50
50
|
subscribe(fn) {
|
|
51
51
|
subscribers.add(fn);
|
|
52
|
-
return () => {
|
|
52
|
+
return () => {
|
|
53
|
+
subscribers.delete(fn);
|
|
54
|
+
};
|
|
53
55
|
},
|
|
54
56
|
};
|
|
55
57
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Shared submit/input handler — processes user input before sending to LLM.
|
|
3
3
|
* Used by both cell renderer REPL and Ink REPL.
|
|
4
4
|
*/
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
7
|
-
import type {
|
|
8
|
-
import type { CostTracker } from
|
|
5
|
+
import type { CompanionConfig } from "../cybergotchi/types.js";
|
|
6
|
+
import type { Message } from "../types/message.js";
|
|
7
|
+
import type { PermissionMode } from "../types/permissions.js";
|
|
8
|
+
import type { CostTracker } from "./cost.js";
|
|
9
9
|
export type SubmitContext = {
|
|
10
10
|
messages: Message[];
|
|
11
11
|
currentModel: string;
|