@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.
Files changed (235) hide show
  1. package/README.md +4 -4
  2. package/dist/DeferredTool.js +3 -1
  3. package/dist/Tool.d.ts +1 -1
  4. package/dist/agents/roles.js +58 -62
  5. package/dist/commands/cybergotchi.d.ts +1 -1
  6. package/dist/commands/cybergotchi.js +30 -30
  7. package/dist/commands/index.js +360 -122
  8. package/dist/components/App.d.ts +1 -1
  9. package/dist/components/App.js +6 -6
  10. package/dist/components/CompanionFooter.d.ts +1 -1
  11. package/dist/components/CompanionFooter.js +6 -8
  12. package/dist/components/CybergotchiBubble.js +5 -5
  13. package/dist/components/CybergotchiPanel.d.ts +1 -1
  14. package/dist/components/CybergotchiPanel.js +7 -7
  15. package/dist/components/CybergotchiPanelConnected.js +2 -2
  16. package/dist/components/CybergotchiSetup.js +26 -24
  17. package/dist/components/CybergotchiSprite.d.ts +1 -1
  18. package/dist/components/CybergotchiSprite.js +8 -12
  19. package/dist/components/DiffView.d.ts +1 -1
  20. package/dist/components/DiffView.js +10 -10
  21. package/dist/components/ErrorBoundary.d.ts +1 -1
  22. package/dist/components/ErrorBoundary.js +1 -1
  23. package/dist/components/InitWizard.js +65 -33
  24. package/dist/components/Markdown.js +2 -4
  25. package/dist/components/Messages.js +4 -4
  26. package/dist/components/PermissionPrompt.d.ts +1 -1
  27. package/dist/components/PermissionPrompt.js +15 -17
  28. package/dist/components/REPL.d.ts +1 -1
  29. package/dist/components/REPL.js +74 -49
  30. package/dist/components/Spinner.js +2 -2
  31. package/dist/components/TextInput.js +35 -29
  32. package/dist/components/ToolCallDisplay.js +3 -5
  33. package/dist/cybergotchi/bones.d.ts +1 -1
  34. package/dist/cybergotchi/bones.js +8 -8
  35. package/dist/cybergotchi/config.d.ts +2 -2
  36. package/dist/cybergotchi/config.js +13 -13
  37. package/dist/cybergotchi/events.d.ts +5 -5
  38. package/dist/cybergotchi/events.js +7 -7
  39. package/dist/cybergotchi/needs.d.ts +2 -2
  40. package/dist/cybergotchi/needs.js +7 -9
  41. package/dist/cybergotchi/personality.d.ts +2 -2
  42. package/dist/cybergotchi/personality.js +2 -2
  43. package/dist/cybergotchi/species.d.ts +1 -1
  44. package/dist/cybergotchi/species.js +145 -217
  45. package/dist/cybergotchi/speech.d.ts +2 -2
  46. package/dist/cybergotchi/speech.js +43 -43
  47. package/dist/cybergotchi/types.d.ts +4 -4
  48. package/dist/cybergotchi/types.js +26 -26
  49. package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
  50. package/dist/cybergotchi/useCybergotchi.js +29 -25
  51. package/dist/git/index.js +11 -9
  52. package/dist/harness/checkpoints.js +29 -21
  53. package/dist/harness/config.d.ts +12 -2
  54. package/dist/harness/config.js +15 -9
  55. package/dist/harness/context-warning.d.ts +1 -1
  56. package/dist/harness/context-warning.js +1 -1
  57. package/dist/harness/cost.js +1 -1
  58. package/dist/harness/credentials.js +13 -13
  59. package/dist/harness/hooks.js +7 -5
  60. package/dist/harness/keybindings.js +20 -18
  61. package/dist/harness/marketplace.d.ts +3 -3
  62. package/dist/harness/marketplace.js +55 -42
  63. package/dist/harness/memory.d.ts +23 -5
  64. package/dist/harness/memory.js +142 -41
  65. package/dist/harness/onboarding.js +30 -10
  66. package/dist/harness/plugins.d.ts +9 -1
  67. package/dist/harness/plugins.js +54 -30
  68. package/dist/harness/rules.js +12 -7
  69. package/dist/harness/sandbox.d.ts +34 -0
  70. package/dist/harness/sandbox.js +104 -0
  71. package/dist/harness/session-db.d.ts +55 -0
  72. package/dist/harness/session-db.js +165 -0
  73. package/dist/harness/session.d.ts +1 -1
  74. package/dist/harness/session.js +34 -15
  75. package/dist/harness/store.d.ts +3 -3
  76. package/dist/harness/store.js +6 -4
  77. package/dist/harness/submit-handler.d.ts +4 -4
  78. package/dist/harness/submit-handler.js +57 -21
  79. package/dist/harness/telemetry.d.ts +1 -1
  80. package/dist/harness/telemetry.js +23 -19
  81. package/dist/harness/traces.d.ts +2 -2
  82. package/dist/harness/traces.js +44 -33
  83. package/dist/harness/verification.d.ts +1 -1
  84. package/dist/harness/verification.js +50 -44
  85. package/dist/lsp/client.js +44 -40
  86. package/dist/main.js +100 -59
  87. package/dist/mcp/DeferredMcpTool.d.ts +4 -4
  88. package/dist/mcp/DeferredMcpTool.js +9 -5
  89. package/dist/mcp/McpTool.d.ts +4 -4
  90. package/dist/mcp/McpTool.js +8 -4
  91. package/dist/mcp/client.d.ts +2 -2
  92. package/dist/mcp/client.js +21 -21
  93. package/dist/mcp/loader.d.ts +1 -1
  94. package/dist/mcp/loader.js +17 -12
  95. package/dist/mcp/registry.d.ts +3 -3
  96. package/dist/mcp/registry.js +97 -97
  97. package/dist/mcp/schema.d.ts +1 -1
  98. package/dist/mcp/schema.js +16 -16
  99. package/dist/mcp/server.d.ts +1 -1
  100. package/dist/mcp/server.js +21 -21
  101. package/dist/mcp/types.d.ts +3 -3
  102. package/dist/providers/anthropic.d.ts +2 -2
  103. package/dist/providers/anthropic.js +10 -9
  104. package/dist/providers/base.d.ts +1 -1
  105. package/dist/providers/index.js +10 -3
  106. package/dist/providers/llamacpp.d.ts +2 -2
  107. package/dist/providers/llamacpp.js +1 -3
  108. package/dist/providers/ollama.d.ts +2 -2
  109. package/dist/providers/ollama.js +3 -4
  110. package/dist/providers/openai.d.ts +2 -2
  111. package/dist/providers/openai.js +3 -5
  112. package/dist/providers/openrouter.d.ts +2 -2
  113. package/dist/providers/router.d.ts +1 -1
  114. package/dist/providers/router.js +7 -7
  115. package/dist/query/compress.d.ts +2 -2
  116. package/dist/query/compress.js +22 -21
  117. package/dist/query/context-manager.d.ts +2 -2
  118. package/dist/query/context-manager.js +8 -11
  119. package/dist/query/errors.js +1 -1
  120. package/dist/query/index.d.ts +1 -1
  121. package/dist/query/index.js +30 -22
  122. package/dist/query/tools.js +15 -12
  123. package/dist/query/types.d.ts +1 -1
  124. package/dist/query.d.ts +1 -1
  125. package/dist/query.js +1 -1
  126. package/dist/remote/auth.d.ts +2 -2
  127. package/dist/remote/auth.js +8 -8
  128. package/dist/remote/server.d.ts +3 -3
  129. package/dist/remote/server.js +60 -60
  130. package/dist/renderer/cells.js +9 -9
  131. package/dist/renderer/colors.js +24 -6
  132. package/dist/renderer/diff.d.ts +2 -2
  133. package/dist/renderer/diff.js +27 -19
  134. package/dist/renderer/differ.d.ts +1 -1
  135. package/dist/renderer/differ.js +9 -9
  136. package/dist/renderer/image.js +19 -19
  137. package/dist/renderer/index.d.ts +6 -6
  138. package/dist/renderer/index.js +163 -93
  139. package/dist/renderer/input.js +66 -48
  140. package/dist/renderer/layout.d.ts +6 -6
  141. package/dist/renderer/layout.js +163 -124
  142. package/dist/renderer/markdown.d.ts +2 -2
  143. package/dist/renderer/markdown.js +173 -54
  144. package/dist/renderer/session-browser.d.ts +2 -2
  145. package/dist/renderer/session-browser.js +19 -21
  146. package/dist/repl.d.ts +5 -5
  147. package/dist/repl.js +300 -198
  148. package/dist/sdk/index.d.ts +8 -7
  149. package/dist/sdk/index.js +59 -42
  150. package/dist/services/AgentDispatcher.d.ts +3 -3
  151. package/dist/services/AgentDispatcher.js +33 -29
  152. package/dist/services/CronExecutor.d.ts +4 -4
  153. package/dist/services/CronExecutor.js +12 -8
  154. package/dist/services/EvaluatorLoop.d.ts +3 -3
  155. package/dist/services/EvaluatorLoop.js +29 -21
  156. package/dist/services/MetaHarness.d.ts +1 -1
  157. package/dist/services/MetaHarness.js +41 -33
  158. package/dist/services/PipelineExecutor.d.ts +1 -1
  159. package/dist/services/PipelineExecutor.js +23 -25
  160. package/dist/services/SkillExtractor.d.ts +43 -0
  161. package/dist/services/SkillExtractor.js +143 -0
  162. package/dist/services/StreamingToolExecutor.d.ts +2 -2
  163. package/dist/services/StreamingToolExecutor.js +11 -7
  164. package/dist/services/a2a.d.ts +8 -8
  165. package/dist/services/a2a.js +44 -34
  166. package/dist/services/agent-messaging.d.ts +33 -15
  167. package/dist/services/agent-messaging.js +65 -13
  168. package/dist/services/cron.js +16 -16
  169. package/dist/tools/AgentTool/index.d.ts +5 -2
  170. package/dist/tools/AgentTool/index.js +35 -15
  171. package/dist/tools/AskUserTool/index.js +1 -1
  172. package/dist/tools/BashTool/index.d.ts +2 -2
  173. package/dist/tools/BashTool/index.js +18 -10
  174. package/dist/tools/CronTool/index.d.ts +2 -2
  175. package/dist/tools/CronTool/index.js +30 -12
  176. package/dist/tools/DiagnosticsTool/index.js +28 -22
  177. package/dist/tools/EnterPlanModeTool/index.js +93 -14
  178. package/dist/tools/EnterWorktreeTool/index.js +7 -3
  179. package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
  180. package/dist/tools/ExitPlanModeTool/index.js +20 -5
  181. package/dist/tools/ExitWorktreeTool/index.js +11 -4
  182. package/dist/tools/FileEditTool/index.js +3 -5
  183. package/dist/tools/FileReadTool/index.js +16 -10
  184. package/dist/tools/FileWriteTool/index.js +2 -2
  185. package/dist/tools/GlobTool/index.js +5 -9
  186. package/dist/tools/GrepTool/index.d.ts +2 -2
  187. package/dist/tools/GrepTool/index.js +14 -9
  188. package/dist/tools/ImageReadTool/index.js +2 -2
  189. package/dist/tools/KillProcessTool/index.js +11 -7
  190. package/dist/tools/LSTool/index.js +3 -3
  191. package/dist/tools/MemoryTool/index.d.ts +11 -11
  192. package/dist/tools/MemoryTool/index.js +28 -14
  193. package/dist/tools/MonitorTool/index.d.ts +2 -2
  194. package/dist/tools/MonitorTool/index.js +24 -19
  195. package/dist/tools/MultiEditTool/index.js +9 -5
  196. package/dist/tools/NotebookEditTool/index.js +3 -3
  197. package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
  198. package/dist/tools/ParallelAgentTool/index.js +12 -6
  199. package/dist/tools/PipelineTool/index.d.ts +4 -4
  200. package/dist/tools/PipelineTool/index.js +3 -3
  201. package/dist/tools/PowerShellTool/index.js +10 -6
  202. package/dist/tools/RemoteTriggerTool/index.js +8 -4
  203. package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
  204. package/dist/tools/ScheduleWakeupTool/index.js +115 -0
  205. package/dist/tools/SendMessageTool/index.js +25 -7
  206. package/dist/tools/SessionSearchTool/index.d.ts +15 -0
  207. package/dist/tools/SessionSearchTool/index.js +36 -0
  208. package/dist/tools/SkillTool/index.d.ts +3 -0
  209. package/dist/tools/SkillTool/index.js +39 -9
  210. package/dist/tools/TaskCreateTool/index.d.ts +2 -2
  211. package/dist/tools/TaskCreateTool/index.js +2 -2
  212. package/dist/tools/TaskGetTool/index.js +2 -2
  213. package/dist/tools/TaskListTool/index.js +3 -5
  214. package/dist/tools/TaskOutputTool/index.js +2 -2
  215. package/dist/tools/TaskStopTool/index.js +3 -3
  216. package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
  217. package/dist/tools/TaskUpdateTool/index.js +2 -2
  218. package/dist/tools/ToolSearchTool/index.js +9 -6
  219. package/dist/tools/WebFetchTool/index.js +1 -1
  220. package/dist/tools/WebSearchTool/index.js +2 -6
  221. package/dist/tools.js +31 -30
  222. package/dist/types/permissions.js +15 -9
  223. package/dist/utils/bash-safety.d.ts +1 -1
  224. package/dist/utils/bash-safety.js +64 -54
  225. package/dist/utils/diff-algorithm.d.ts +3 -3
  226. package/dist/utils/diff-algorithm.js +7 -7
  227. package/dist/utils/fs.js +3 -3
  228. package/dist/utils/safe-env.js +1 -1
  229. package/dist/utils/theme-data.d.ts +1 -1
  230. package/dist/utils/theme-data.js +1 -1
  231. package/dist/utils/theme.d.ts +1 -1
  232. package/dist/utils/theme.js +1 -1
  233. package/dist/utils/tool-summary.d.ts +1 -1
  234. package/dist/utils/tool-summary.js +27 -9
  235. package/package.json +10 -3
@@ -7,9 +7,9 @@
7
7
  * 4. .oh/rules/*.md
8
8
  * 5. CLAUDE.local.md (gitignored personal overrides)
9
9
  */
10
- import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
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).filter((f) => f.endsWith(".md")).sort()) {
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).filter((f) => f.endsWith(".md")).sort()) {
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('---', raw.indexOf('---') + 3);
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" + rules.join("\n\n---\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('*')[0].replace(/\/+$/, '');
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['hibernate'];
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.
@@ -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 { /* ignore */ }
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 === 'user');
85
- const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
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 === 'user') {
105
+ if (m.role === "user") {
91
106
  summaryParts.push(`User: ${m.content.slice(0, 100)}`);
92
107
  }
93
- else if (m.role === 'assistant' && m.content) {
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('\n'),
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 = ['[Session Resumed]'];
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('Continue where you left off. If the user\'s last request was incomplete, acknowledge that and ask how to proceed.');
126
- return parts.join('\n');
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.map((f) => {
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
- }).sort((a, b) => a.updatedAt - b.updatedAt);
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 { /* ignore */ }
174
+ catch {
175
+ /* ignore */
176
+ }
158
177
  }
159
178
  return toRemove.length;
160
179
  }
@@ -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 '../types/message.js';
8
- import type { Session } from './session.js';
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: 'normal' | 'insert' | null;
17
+ vimMode: "normal" | "insert" | null;
18
18
  fastMode: boolean;
19
19
  companionVisible: boolean;
20
20
  acSuggestions: string[];
@@ -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 === 'function' ? partial(state) : 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 () => { subscribers.delete(fn); };
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 { Message } from '../types/message.js';
6
- import type { PermissionMode } from '../types/permissions.js';
7
- import type { CompanionConfig } from '../cybergotchi/types.js';
8
- import type { CostTracker } from './cost.js';
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;