@zhijiewang/openharness 2.1.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 +288 -132
- 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 +3 -3
- 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.js +15 -15
- 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 +25 -23
- 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 +39 -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 +98 -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 +1 -1
- package/dist/query/context-manager.js +5 -5
- 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 +5 -5
- package/dist/sdk/index.js +32 -26
- 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 +34 -32
- 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.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);
|
package/dist/harness/sandbox.js
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Reduces permission prompts while maintaining security.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import { readOhConfig } from
|
|
11
|
+
import { relative, resolve } from "node:path";
|
|
12
|
+
import { readOhConfig } from "./config.js";
|
|
13
13
|
const DEFAULT_SANDBOX = {
|
|
14
14
|
enabled: false,
|
|
15
|
-
allowedPaths: [
|
|
15
|
+
allowedPaths: ["."], // current directory
|
|
16
16
|
allowedDomains: [], // empty = all allowed
|
|
17
17
|
blockNetwork: false,
|
|
18
|
-
blockedCommands: [
|
|
18
|
+
blockedCommands: ["curl", "wget"],
|
|
19
19
|
};
|
|
20
20
|
// ── Sandbox Manager ──
|
|
21
21
|
let _config = null;
|
|
@@ -50,7 +50,7 @@ export function isPathAllowed(filePath) {
|
|
|
50
50
|
const allowedResolved = resolve(cwd, allowed);
|
|
51
51
|
// Check if the file is within the allowed directory
|
|
52
52
|
const rel = relative(allowedResolved, resolved);
|
|
53
|
-
if (!rel.startsWith(
|
|
53
|
+
if (!rel.startsWith("..") && !rel.startsWith("/"))
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
return false;
|
|
@@ -66,7 +66,7 @@ export function isDomainAllowed(url) {
|
|
|
66
66
|
return true;
|
|
67
67
|
try {
|
|
68
68
|
const hostname = new URL(url).hostname.toLowerCase();
|
|
69
|
-
return config.allowedDomains.some(d => hostname === d.toLowerCase() || hostname.endsWith(
|
|
69
|
+
return config.allowedDomains.some((d) => hostname === d.toLowerCase() || hostname.endsWith(`.${d.toLowerCase()}`));
|
|
70
70
|
}
|
|
71
71
|
catch {
|
|
72
72
|
return false;
|
|
@@ -77,28 +77,28 @@ export function isCommandAllowed(command) {
|
|
|
77
77
|
const config = getSandboxConfig();
|
|
78
78
|
if (!config.enabled)
|
|
79
79
|
return true;
|
|
80
|
-
const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase() ??
|
|
80
|
+
const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
81
81
|
return !config.blockedCommands.includes(firstWord);
|
|
82
82
|
}
|
|
83
83
|
/** Get a human-readable sandbox status */
|
|
84
84
|
export function sandboxStatus() {
|
|
85
85
|
const config = getSandboxConfig();
|
|
86
86
|
if (!config.enabled)
|
|
87
|
-
return
|
|
88
|
-
const lines = [
|
|
89
|
-
lines.push(` Allowed paths: ${config.allowedPaths.join(
|
|
87
|
+
return "Sandbox: disabled";
|
|
88
|
+
const lines = ["Sandbox: enabled"];
|
|
89
|
+
lines.push(` Allowed paths: ${config.allowedPaths.join(", ") || "none"}`);
|
|
90
90
|
if (config.blockNetwork) {
|
|
91
|
-
lines.push(
|
|
91
|
+
lines.push(" Network: blocked");
|
|
92
92
|
}
|
|
93
93
|
else if (config.allowedDomains.length > 0) {
|
|
94
|
-
lines.push(` Allowed domains: ${config.allowedDomains.join(
|
|
94
|
+
lines.push(` Allowed domains: ${config.allowedDomains.join(", ")}`);
|
|
95
95
|
}
|
|
96
96
|
else {
|
|
97
|
-
lines.push(
|
|
97
|
+
lines.push(" Network: unrestricted");
|
|
98
98
|
}
|
|
99
99
|
if (config.blockedCommands.length > 0) {
|
|
100
|
-
lines.push(` Blocked commands: ${config.blockedCommands.join(
|
|
100
|
+
lines.push(` Blocked commands: ${config.blockedCommands.join(", ")}`);
|
|
101
101
|
}
|
|
102
|
-
return lines.join(
|
|
102
|
+
return lines.join("\n");
|
|
103
103
|
}
|
|
104
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;
|
|
@@ -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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { processSlashCommand } from "../commands/index.js";
|
|
6
|
+
import { cybergotchiEvents } from "../cybergotchi/events.js";
|
|
7
|
+
import { resolveMcpMention } from "../mcp/loader.js";
|
|
8
|
+
import { createInfoMessage, createUserMessage } from "../types/message.js";
|
|
9
9
|
/**
|
|
10
10
|
* Process user input: handle exit, companion mentions, slash commands,
|
|
11
11
|
* @mentions, and prepare the prompt for the LLM.
|
|
@@ -18,17 +18,17 @@ export async function handleUserInput(input, ctx) {
|
|
|
18
18
|
const name = ctx.companionConfig.soul.name.toLowerCase();
|
|
19
19
|
const lower = trimmed.toLowerCase();
|
|
20
20
|
if (lower.startsWith(`@${name}`) || lower.startsWith(`${name},`) || lower.startsWith(`${name} `)) {
|
|
21
|
-
cybergotchiEvents.emit(
|
|
21
|
+
cybergotchiEvents.emit("cybergotchi", { type: "userAddressed", text: trimmed });
|
|
22
22
|
return { handled: true, messages };
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
// ! Bash mode — direct shell execution, output added to context
|
|
26
|
-
if (trimmed.startsWith(
|
|
26
|
+
if (trimmed.startsWith("!") && trimmed.length > 1) {
|
|
27
27
|
const command = trimmed.slice(1).trim();
|
|
28
28
|
try {
|
|
29
|
-
const { execSync } = await import(
|
|
29
|
+
const { execSync } = await import("node:child_process");
|
|
30
30
|
const output = execSync(command, {
|
|
31
|
-
encoding:
|
|
31
|
+
encoding: "utf-8",
|
|
32
32
|
cwd: process.cwd(),
|
|
33
33
|
timeout: 30_000,
|
|
34
34
|
maxBuffer: 1024 * 1024,
|
|
@@ -37,17 +37,17 @@ export async function handleUserInput(input, ctx) {
|
|
|
37
37
|
messages = [...messages, createInfoMessage(`$ ${command}\n${output.trimEnd()}`)];
|
|
38
38
|
}
|
|
39
39
|
catch (err) {
|
|
40
|
-
const output = String(err.stdout ?? err.stderr ?? err.message ??
|
|
40
|
+
const output = String(err.stdout ?? err.stderr ?? err.message ?? "Command failed");
|
|
41
41
|
messages = [...messages, createInfoMessage(`$ ${command}\n${output.trimEnd()}`)];
|
|
42
42
|
}
|
|
43
43
|
return { handled: true, messages };
|
|
44
44
|
}
|
|
45
45
|
// Vim toggle
|
|
46
|
-
if (trimmed ===
|
|
46
|
+
if (trimmed === "/vim") {
|
|
47
47
|
return { handled: true, messages, vimToggled: true };
|
|
48
48
|
}
|
|
49
49
|
// Slash commands
|
|
50
|
-
if (trimmed.startsWith(
|
|
50
|
+
if (trimmed.startsWith("/")) {
|
|
51
51
|
const cmdCtx = {
|
|
52
52
|
messages,
|
|
53
53
|
model: ctx.currentModel,
|
|
@@ -96,43 +96,45 @@ export async function handleUserInput(input, ctx) {
|
|
|
96
96
|
const companionName = ctx.companionConfig?.soul?.name?.toLowerCase();
|
|
97
97
|
for (const match of mentions) {
|
|
98
98
|
const mention = match[1];
|
|
99
|
-
const startLine = match[2] ? parseInt(match[2]) : undefined;
|
|
100
|
-
const endLine = match[3] ? parseInt(match[3]) : startLine;
|
|
99
|
+
const startLine = match[2] ? parseInt(match[2], 10) : undefined;
|
|
100
|
+
const endLine = match[3] ? parseInt(match[3], 10) : startLine;
|
|
101
101
|
const fullRef = match[0];
|
|
102
102
|
if (companionName && mention.toLowerCase() === companionName)
|
|
103
103
|
continue;
|
|
104
104
|
// Try local file first (supports paths like @src/main.ts, @README.md#L5-10)
|
|
105
105
|
try {
|
|
106
|
-
const { existsSync, readFileSync } = await import(
|
|
107
|
-
const { resolve } = await import(
|
|
106
|
+
const { existsSync, readFileSync } = await import("node:fs");
|
|
107
|
+
const { resolve } = await import("node:path");
|
|
108
108
|
const filePath = resolve(process.cwd(), mention);
|
|
109
109
|
if (existsSync(filePath)) {
|
|
110
|
-
let content = readFileSync(filePath,
|
|
110
|
+
let content = readFileSync(filePath, "utf-8");
|
|
111
111
|
// Apply line range if specified
|
|
112
112
|
if (startLine !== undefined) {
|
|
113
|
-
const lines = content.split(
|
|
113
|
+
const lines = content.split("\n");
|
|
114
114
|
const start = Math.max(0, startLine - 1); // 1-indexed to 0-indexed
|
|
115
115
|
const end = endLine !== undefined ? endLine : start + 1;
|
|
116
|
-
content = lines.slice(start, end).join(
|
|
116
|
+
content = lines.slice(start, end).join("\n");
|
|
117
117
|
resolvedInput += `\n\n[File ${fullRef} (lines ${startLine}-${endLine ?? startLine})]:\n${content}`;
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
120
|
-
const truncated = content.length > 10_000
|
|
121
|
-
? content.slice(0, 10_000) + '\n[...truncated]'
|
|
122
|
-
: content;
|
|
120
|
+
const truncated = content.length > 10_000 ? `${content.slice(0, 10_000)}\n[...truncated]` : content;
|
|
123
121
|
resolvedInput += `\n\n[File @${mention}]:\n${truncated}`;
|
|
124
122
|
}
|
|
125
123
|
continue;
|
|
126
124
|
}
|
|
127
125
|
}
|
|
128
|
-
catch {
|
|
126
|
+
catch {
|
|
127
|
+
/* ignore */
|
|
128
|
+
}
|
|
129
129
|
// Fall back to MCP resource
|
|
130
130
|
try {
|
|
131
131
|
const content = await resolveMcpMention(mention);
|
|
132
132
|
if (content)
|
|
133
133
|
resolvedInput += `\n\n[Resource @${mention}]:\n${content.slice(0, 5000)}`;
|
|
134
134
|
}
|
|
135
|
-
catch {
|
|
135
|
+
catch {
|
|
136
|
+
/* ignore */
|
|
137
|
+
}
|
|
136
138
|
}
|
|
137
139
|
return { handled: false, messages, prompt: resolvedInput };
|
|
138
140
|
}
|