herm-tui 1.0.0-dev.1 → 1.0.0-dev.2

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 (187) hide show
  1. package/highlights-eq9cgrbb.scm +604 -0
  2. package/highlights-ghv9g403.scm +205 -0
  3. package/highlights-hk7bwhj4.scm +284 -0
  4. package/highlights-r812a2qc.scm +150 -0
  5. package/highlights-x6tmsnaa.scm +115 -0
  6. package/index.js +10375 -0
  7. package/injections-73j83es3.scm +27 -0
  8. package/package.json +14 -64
  9. package/parser.worker.js +8 -0
  10. package/tree-sitter-3jzf13jk.wasm +0 -0
  11. package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  12. package/tree-sitter-markdown-411r6y9b.wasm +0 -0
  13. package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  14. package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  15. package/tree-sitter-zig-e78zbjpm.wasm +0 -0
  16. package/scripts/postinstall.ts +0 -29
  17. package/src/app/gateway.tsx +0 -83
  18. package/src/app/gatewayEvents.ts +0 -203
  19. package/src/app/launch.ts +0 -41
  20. package/src/app/skin.tsx +0 -31
  21. package/src/app/spawnHistory.ts +0 -75
  22. package/src/app/tabs.ts +0 -23
  23. package/src/app/turnReducer.ts +0 -390
  24. package/src/app/useAppKeys.ts +0 -268
  25. package/src/app/useAtRefPopover.ts +0 -99
  26. package/src/app/useInputHistory.ts +0 -66
  27. package/src/app/useSession.ts +0 -102
  28. package/src/app/useSlashCommands.ts +0 -70
  29. package/src/app/useSlashPopover.ts +0 -48
  30. package/src/app.tsx +0 -917
  31. package/src/commands/slash.ts +0 -151
  32. package/src/components/avatar/AnimatedAvatar.tsx +0 -66
  33. package/src/components/avatar/eikon.ts +0 -144
  34. package/src/components/avatar/states/error.ts +0 -1155
  35. package/src/components/avatar/states/idle.ts +0 -1155
  36. package/src/components/avatar/states/index.ts +0 -30
  37. package/src/components/avatar/states/listening.ts +0 -1155
  38. package/src/components/avatar/states/speaking.ts +0 -1155
  39. package/src/components/avatar/states/thinking.ts +0 -1155
  40. package/src/components/avatar/states/working.ts +0 -1155
  41. package/src/components/chat/AtRefPopover.tsx +0 -54
  42. package/src/components/chat/CodeBlock.tsx +0 -67
  43. package/src/components/chat/Composer.tsx +0 -347
  44. package/src/components/chat/DiffBlock.tsx +0 -116
  45. package/src/components/chat/ErrorBlock.tsx +0 -70
  46. package/src/components/chat/MediaChip.tsx +0 -114
  47. package/src/components/chat/MessageItem.tsx +0 -282
  48. package/src/components/chat/MessageList.tsx +0 -114
  49. package/src/components/chat/PromptCard.tsx +0 -359
  50. package/src/components/chat/SlashPopover.tsx +0 -158
  51. package/src/components/chat/ThoughtCloud.tsx +0 -185
  52. package/src/components/chat/TypingIndicator.tsx +0 -25
  53. package/src/components/chat/tool/Subagent.tsx +0 -75
  54. package/src/components/chat/tool/frame.tsx +0 -69
  55. package/src/components/chat/tool/index.tsx +0 -65
  56. package/src/components/chat/tool/preview.ts +0 -57
  57. package/src/components/sidebar/ContextGauge.tsx +0 -102
  58. package/src/components/sidebar/Sidebar.tsx +0 -143
  59. package/src/components/tabs/TabBar.tsx +0 -50
  60. package/src/components/ui/FileLink.tsx +0 -52
  61. package/src/config/index.ts +0 -156
  62. package/src/config/lane.ts +0 -161
  63. package/src/config/models.ts +0 -95
  64. package/src/config/rules.ts +0 -80
  65. package/src/config/schema.ts +0 -308
  66. package/src/dialogs/alert.tsx +0 -52
  67. package/src/dialogs/chafa.tsx +0 -72
  68. package/src/dialogs/confirm.tsx +0 -58
  69. package/src/dialogs/curator.tsx +0 -153
  70. package/src/dialogs/eikon-picker.tsx +0 -95
  71. package/src/dialogs/help.tsx +0 -80
  72. package/src/dialogs/history.tsx +0 -92
  73. package/src/dialogs/info.tsx +0 -115
  74. package/src/dialogs/keys.tsx +0 -170
  75. package/src/dialogs/logs.tsx +0 -42
  76. package/src/dialogs/message.tsx +0 -38
  77. package/src/dialogs/model-picker.tsx +0 -123
  78. package/src/dialogs/new-profile.tsx +0 -69
  79. package/src/dialogs/new-task.tsx +0 -103
  80. package/src/dialogs/profile.tsx +0 -55
  81. package/src/dialogs/rollback.tsx +0 -190
  82. package/src/dialogs/spawn-history.tsx +0 -80
  83. package/src/dialogs/text-prompt.tsx +0 -68
  84. package/src/dialogs/theme-picker.tsx +0 -50
  85. package/src/home/index.ts +0 -23
  86. package/src/home/store.ts +0 -267
  87. package/src/index.tsx +0 -113
  88. package/src/keys/catalog.ts +0 -115
  89. package/src/keys/chord.ts +0 -125
  90. package/src/keys/conflicts.ts +0 -48
  91. package/src/keys/context.tsx +0 -112
  92. package/src/keys/index.ts +0 -5
  93. package/src/keys/list.ts +0 -94
  94. package/src/keys/oc-compat.ts +0 -87
  95. package/src/tabs/Agents.tsx +0 -607
  96. package/src/tabs/Analytics.tsx +0 -154
  97. package/src/tabs/Chat.tsx +0 -50
  98. package/src/tabs/Config.tsx +0 -605
  99. package/src/tabs/Context.tsx +0 -599
  100. package/src/tabs/Cron.tsx +0 -294
  101. package/src/tabs/Env.tsx +0 -227
  102. package/src/tabs/Kanban.tsx +0 -367
  103. package/src/tabs/Memory.tsx +0 -294
  104. package/src/tabs/Sessions.tsx +0 -786
  105. package/src/tabs/Skills.tsx +0 -507
  106. package/src/tabs/Toolsets.tsx +0 -266
  107. package/src/theme/builtin.ts +0 -78
  108. package/src/theme/context.tsx +0 -106
  109. package/src/theme/index.ts +0 -4
  110. package/src/theme/resolve.ts +0 -134
  111. package/src/theme/syntax.ts +0 -31
  112. package/src/theme/themes/aura.json +0 -69
  113. package/src/theme/themes/ayu.json +0 -80
  114. package/src/theme/themes/carbonfox.json +0 -248
  115. package/src/theme/themes/catppuccin-frappe.json +0 -233
  116. package/src/theme/themes/catppuccin-macchiato.json +0 -233
  117. package/src/theme/themes/catppuccin.json +0 -112
  118. package/src/theme/themes/cobalt2.json +0 -228
  119. package/src/theme/themes/cursor.json +0 -249
  120. package/src/theme/themes/dracula.json +0 -219
  121. package/src/theme/themes/everforest.json +0 -241
  122. package/src/theme/themes/flexoki.json +0 -237
  123. package/src/theme/themes/github.json +0 -233
  124. package/src/theme/themes/gruvbox.json +0 -242
  125. package/src/theme/themes/kanagawa.json +0 -77
  126. package/src/theme/themes/lucent-orng.json +0 -237
  127. package/src/theme/themes/material.json +0 -235
  128. package/src/theme/themes/matrix.json +0 -77
  129. package/src/theme/themes/mercury.json +0 -252
  130. package/src/theme/themes/monokai.json +0 -221
  131. package/src/theme/themes/nightowl.json +0 -221
  132. package/src/theme/themes/nord.json +0 -223
  133. package/src/theme/themes/one-dark.json +0 -84
  134. package/src/theme/themes/opencode.json +0 -245
  135. package/src/theme/themes/orng.json +0 -249
  136. package/src/theme/themes/osaka-jade.json +0 -93
  137. package/src/theme/themes/palenight.json +0 -222
  138. package/src/theme/themes/rosepine.json +0 -234
  139. package/src/theme/themes/solarized.json +0 -223
  140. package/src/theme/themes/synthwave84.json +0 -226
  141. package/src/theme/themes/tokyonight.json +0 -243
  142. package/src/theme/themes/vercel.json +0 -245
  143. package/src/theme/themes/vesper.json +0 -218
  144. package/src/theme/themes/zenburn.json +0 -223
  145. package/src/theme/types.ts +0 -119
  146. package/src/types/message.ts +0 -97
  147. package/src/ui/ChafaImage.tsx +0 -64
  148. package/src/ui/Splash.tsx +0 -118
  149. package/src/ui/borders.ts +0 -28
  150. package/src/ui/command.tsx +0 -104
  151. package/src/ui/dialog-select.tsx +0 -164
  152. package/src/ui/dialog.tsx +0 -102
  153. package/src/ui/fmt.ts +0 -82
  154. package/src/ui/kv.tsx +0 -28
  155. package/src/ui/shell.tsx +0 -45
  156. package/src/ui/spinner.tsx +0 -59
  157. package/src/ui/splash-art.ts +0 -123
  158. package/src/ui/table.tsx +0 -117
  159. package/src/ui/ticker.tsx +0 -90
  160. package/src/ui/toast.tsx +0 -130
  161. package/src/utils/categorical.ts +0 -77
  162. package/src/utils/chafa.ts +0 -173
  163. package/src/utils/clipboard.ts +0 -67
  164. package/src/utils/context-segments.ts +0 -317
  165. package/src/utils/control.ts +0 -495
  166. package/src/utils/drop.ts +0 -25
  167. package/src/utils/editor.ts +0 -33
  168. package/src/utils/fuzzy.ts +0 -45
  169. package/src/utils/gateway-client.ts +0 -253
  170. package/src/utils/gateway-types.ts +0 -282
  171. package/src/utils/git.ts +0 -57
  172. package/src/utils/hermes-analytics.ts +0 -134
  173. package/src/utils/hermes-home.ts +0 -821
  174. package/src/utils/hermes-kanban.ts +0 -154
  175. package/src/utils/hermes-profiles.ts +0 -217
  176. package/src/utils/interpolate.ts +0 -31
  177. package/src/utils/math-unicode.ts +0 -818
  178. package/src/utils/memory-activity.ts +0 -140
  179. package/src/utils/open-file.ts +0 -13
  180. package/src/utils/paths.ts +0 -52
  181. package/src/utils/perf.ts +0 -235
  182. package/src/utils/preferences.ts +0 -150
  183. package/src/utils/sessions-db.ts +0 -396
  184. package/src/utils/subagent-tree.ts +0 -146
  185. package/src/utils/terminal-reset.ts +0 -129
  186. package/src/utils/tips.ts +0 -67
  187. package/src/utils/tokens.ts +0 -87
@@ -1,821 +0,0 @@
1
- /**
2
- * hermes-home.ts — Reader for the ~/.hermes/ directory.
3
- *
4
- * This is herm's window into the Hermes Agent's persistent state.
5
- * All reads are filesystem-based (Bun APIs), no HTTP needed.
6
- *
7
- * Every piece of extracted data carries a `source: Source` field so the
8
- * UI can generically render clickable file links without knowing paths.
9
- */
10
-
11
- import { Database } from "bun:sqlite";
12
- import { readdir, stat } from "node:fs/promises";
13
- import { openSync, readSync, closeSync, readdirSync, readFileSync } from "node:fs";
14
- import { homedir } from "os";
15
- import { parse as parseYaml } from "yaml";
16
- import { count as tokenCount } from "./tokens";
17
-
18
- // ─── Path Resolution ─────────────────────────────────────────────────
19
-
20
- const HOME = process.env.HOME || homedir();
21
- const HERMES_HOME = process.env.HERMES_HOME || `${HOME}/.hermes`;
22
-
23
- /** Resolve a path relative to ~/.hermes/ */
24
- export const hermesPath = (relative: string): string =>
25
- `${HERMES_HOME}/${relative}`;
26
-
27
- /** Detect a package-manager-owned install. Two signals, matching
28
- * hermes_cli/config.py:get_managed_system — HERMES_MANAGED env var
29
- * (systemd service sets it) or the `.managed` marker file (NixOS
30
- * activation script touches it so interactive shells see it too). */
31
- export const managedSystem = async (): Promise<string | null> => {
32
- const env = (process.env.HERMES_MANAGED ?? "").trim()
33
- if (env) {
34
- const norm = env.toLowerCase()
35
- if (norm === "1" || norm === "true" || norm === "yes" || norm === "on") return "NixOS"
36
- const names: Record<string, string> = { homebrew: "Homebrew", nix: "NixOS", nixos: "NixOS" }
37
- return names[norm] ?? env
38
- }
39
- return (await Bun.file(hermesPath(".managed")).exists()) ? "NixOS" : null
40
- }
41
-
42
- // ─── Source Provenance ────────────────────────────────────────────────
43
-
44
- /** Every piece of data extracted from ~/.hermes/ carries its origin file. */
45
- export interface Source {
46
- file: string; // absolute path
47
- relative: string; // relative to HERMES_HOME
48
- label: string; // human-friendly display name
49
- }
50
-
51
- /** Build a Source for a file relative to HERMES_HOME */
52
- export const makeSource = (
53
- relative: string,
54
- label?: string,
55
- ): Source => ({
56
- file: hermesPath(relative),
57
- relative,
58
- label: label ?? relative.split("/").pop() ?? relative,
59
- });
60
-
61
- // ─── Types ───────────────────────────────────────────────────────────
62
-
63
- /** Subset of config.yaml we care about */
64
- export interface HermesConfig {
65
- source: Source;
66
- model: {
67
- default: string;
68
- provider: string;
69
- base_url: string;
70
- };
71
- agent: {
72
- max_turns: number;
73
- reasoning_effort: string;
74
- };
75
- compression: {
76
- enabled: boolean;
77
- threshold: number;
78
- target_ratio: number;
79
- protect_last_n: number;
80
- summary_model: string;
81
- };
82
- memory: {
83
- memory_enabled: boolean;
84
- user_profile_enabled: boolean;
85
- memory_char_limit: number;
86
- user_char_limit: number;
87
- provider: string;
88
- nudge_interval: number;
89
- flush_min_turns: number;
90
- };
91
- display: {
92
- personality: string;
93
- skin: string;
94
- show_cost: boolean;
95
- };
96
- curator: {
97
- enabled: boolean;
98
- interval_hours: number;
99
- stale_after_days: number;
100
- archive_after_days: number;
101
- };
102
- gateway: {
103
- platforms: {
104
- api_server?: {
105
- enabled: boolean;
106
- host: string;
107
- port: number;
108
- };
109
- };
110
- };
111
- }
112
-
113
- /** Memory file stats */
114
- export interface MemoryFileInfo {
115
- source: Source;
116
- content: string;
117
- charCount: number;
118
- charLimit: number;
119
- usagePercent: number;
120
- entryCount: number;
121
- }
122
-
123
- /** A row from the sessions table in state.db — see sessions-db.ts. */
124
- export type { SessionRow } from "./sessions-db"
125
-
126
- /** Live session entry from sessions/sessions.json */
127
- export interface LiveSession {
128
- session_key: string;
129
- session_id: string;
130
- created_at: string;
131
- updated_at: string;
132
- display_name: string;
133
- platform: string;
134
- chat_type: string;
135
- input_tokens: number;
136
- output_tokens: number;
137
- cache_read_tokens: number;
138
- cache_write_tokens: number;
139
- total_tokens: number;
140
- last_prompt_tokens: number;
141
- estimated_cost_usd: number;
142
- cost_status: string;
143
- memory_flushed: boolean;
144
- origin?: {
145
- platform: string;
146
- chat_id: string;
147
- chat_name: string;
148
- user_id: string;
149
- user_name: string;
150
- };
151
- }
152
-
153
- /** A tool schema from the session JSON */
154
- export interface ToolInfo {
155
- name: string;
156
- descriptionLength: number;
157
- paramsLength: number;
158
- }
159
-
160
- /** Skill info from the skills directory */
161
- export interface SkillInfo {
162
- source: Source;
163
- category: string;
164
- name: string;
165
- description: string;
166
- tags: string[];
167
- /**
168
- * Token cost of this skill's index entry in the system prompt
169
- * (name + description + tags). Body content is NOT included — it
170
- * only loads on skill_view() and shows up as a tool result.
171
- */
172
- tokenEstimate: number;
173
- }
174
-
175
- /**
176
- * Read description/tags from a SKILL.md YAML frontmatter block.
177
- * Cheap — reads only the first ~2KB. Missing file / no `---` → empty.
178
- */
179
- export function readSkillFrontmatter(source: Source): { description: string; tags: string[] } {
180
- try {
181
- const fd = openSync(source.file, "r");
182
- const buf = Buffer.alloc(2048);
183
- const n = readSync(fd, buf, 0, 2048, 0);
184
- closeSync(fd);
185
- const head = buf.toString("utf-8", 0, n);
186
- if (!head.startsWith("---")) return { description: "", tags: [] };
187
- const end = head.indexOf("\n---", 3);
188
- if (end < 0) return { description: "", tags: [] };
189
- const fm = parseYaml(head.slice(3, end)) as Record<string, unknown>;
190
- const tags = Array.isArray(fm.tags) ? fm.tags.map(String) : [];
191
- return { description: String(fm.description ?? ""), tags };
192
- } catch {
193
- return { description: "", tags: [] };
194
- }
195
- }
196
-
197
- /** Per-skill telemetry sidecar record (~/.hermes/skills/.usage.json). */
198
- export interface SkillUsage {
199
- use_count: number;
200
- view_count: number;
201
- patch_count: number;
202
- last_used_at: string | null;
203
- last_viewed_at: string | null;
204
- last_patched_at: string | null;
205
- created_at: string | null;
206
- archived_at: string | null;
207
- state: "active" | "stale" | "archived";
208
- pinned: boolean;
209
- }
210
-
211
- /**
212
- * Read ~/.hermes/skills/.usage.json. Keyed by skill name.
213
- * Returns empty record on any failure — absent sidecar is the default.
214
- */
215
- export async function readSkillUsage(): Promise<Record<string, SkillUsage>> {
216
- try {
217
- const f = Bun.file(hermesPath("skills/.usage.json"));
218
- if (!(await f.exists())) return {};
219
- const raw = await f.json() as Record<string, Partial<SkillUsage>>;
220
- const out: Record<string, SkillUsage> = {};
221
- for (const [k, v] of Object.entries(raw ?? {})) {
222
- out[k] = {
223
- use_count: Number(v.use_count ?? 0),
224
- view_count: Number(v.view_count ?? 0),
225
- patch_count: Number(v.patch_count ?? 0),
226
- last_used_at: v.last_used_at ?? null,
227
- last_viewed_at: v.last_viewed_at ?? null,
228
- last_patched_at: v.last_patched_at ?? null,
229
- created_at: v.created_at ?? null,
230
- archived_at: v.archived_at ?? null,
231
- state: (v.state as SkillUsage["state"]) ?? "active",
232
- pinned: Boolean(v.pinned),
233
- };
234
- }
235
- return out;
236
- } catch {
237
- return {};
238
- }
239
- }
240
-
241
- /** Curator scheduler state (~/.hermes/skills/.curator_state). */
242
- export interface CuratorState {
243
- last_run_at: string | null;
244
- last_run_duration_seconds: number | null;
245
- last_run_summary: string | null;
246
- paused: boolean;
247
- run_count: number;
248
- }
249
-
250
- export async function readCuratorState(): Promise<CuratorState | null> {
251
- try {
252
- const f = Bun.file(hermesPath("skills/.curator_state"));
253
- if (!(await f.exists())) return null;
254
- const raw = await f.json() as Partial<CuratorState>;
255
- return {
256
- last_run_at: raw.last_run_at ?? null,
257
- last_run_duration_seconds: raw.last_run_duration_seconds ?? null,
258
- last_run_summary: raw.last_run_summary ?? null,
259
- paused: Boolean(raw.paused),
260
- run_count: Number(raw.run_count ?? 0),
261
- };
262
- } catch {
263
- return null;
264
- }
265
- }
266
-
267
- /**
268
- * Locate the newest curator run report — returns {dir, mtime} of the
269
- * directory under ~/.hermes/logs/curator/ with the latest mtime.
270
- * Returns null if none exist.
271
- */
272
- export interface CuratorReportInfo {
273
- /** Source to the REPORT.md inside the newest run dir. */
274
- source: Source;
275
- /** Raw REPORT.md body, trimmed. */
276
- content: string;
277
- /** Run dir name, e.g. "20260430-120030". */
278
- runId: string;
279
- }
280
-
281
- export async function readLatestCuratorReport(): Promise<CuratorReportInfo | null> {
282
- try {
283
- const base = `${HERMES_HOME}/logs/curator`;
284
- const entries = readdirSync(base, { withFileTypes: true }).filter(e => e.isDirectory());
285
- if (entries.length === 0) return null;
286
- // Run dirs are named YYYYMMDD-HHMMSS — lexicographic sort = chronological.
287
- entries.sort((a, b) => b.name.localeCompare(a.name));
288
- const runId = entries[0]!.name;
289
- const rel = `logs/curator/${runId}/REPORT.md`;
290
- const source = makeSource(rel);
291
- const body = await Bun.file(source.file).text();
292
- return { source, content: body.trim(), runId };
293
- } catch {
294
- return null;
295
- }
296
- }
297
-
298
- /** One curator run — id is the YYYYMMDD-HHMMSS dir name; counts come
299
- * from run.json.counts; REPORT.md is lazy-loaded on expand. */
300
- export type CuratorRun = {
301
- id: string
302
- at: number
303
- archived: number; consolidated: number; added: number
304
- before: number; after: number
305
- }
306
-
307
- export function listCuratorRuns(): CuratorRun[] {
308
- try {
309
- const base = `${HERMES_HOME}/logs/curator`;
310
- return readdirSync(base, { withFileTypes: true })
311
- .filter(e => e.isDirectory())
312
- .sort((a, b) => b.name.localeCompare(a.name))
313
- .flatMap(e => {
314
- try {
315
- const fd = openSync(`${base}/${e.name}/run.json`, "r");
316
- const buf = Buffer.alloc(8192);
317
- const n = readSync(fd, buf);
318
- closeSync(fd);
319
- const j = JSON.parse(buf.toString("utf8", 0, n));
320
- const c = j.counts ?? {};
321
- return [{
322
- id: e.name,
323
- at: Date.parse(j.started_at ?? "") / 1000 || 0,
324
- archived: c.archived_this_run ?? 0,
325
- consolidated: c.consolidated_this_run ?? 0,
326
- added: c.added_this_run ?? 0,
327
- before: c.before ?? 0, after: c.after ?? 0,
328
- }];
329
- } catch { return [] }
330
- });
331
- } catch { return [] }
332
- }
333
-
334
- export async function readCuratorReport(id: string): Promise<string> {
335
- try {
336
- return (await Bun.file(hermesPath(`logs/curator/${id}/REPORT.md`)).text()).trim();
337
- } catch { return "" }
338
- }
339
-
340
- /** Per-skill curator event, projected out of run.json so DetailPanel
341
- * can answer "what did curator do to *this* skill, and when". */
342
- export type LineageEvent =
343
- | { kind: "transition"; run: string; at: number; from: string; to: string }
344
- | { kind: "absorbed"; run: string; at: number; sources: string[] }
345
- | { kind: "merged"; run: string; at: number; into: string; reason?: string }
346
- | { kind: "pruned"; run: string; at: number; reason?: string }
347
- | { kind: "added"; run: string; at: number }
348
-
349
- export type LineageIndex = Map<string, LineageEvent[]>
350
-
351
- type RunJson = {
352
- started_at?: string
353
- archived?: string[]
354
- added?: string[]
355
- consolidated?: { name: string; into: string; reason?: string }[]
356
- pruned?: { name: string; reason?: string }[]
357
- state_transitions?: { name: string; from: string; to: string }[]
358
- }
359
-
360
- /** Build a skill→events index across every run.json. Runs: dozens,
361
- * payload: low-KB JSON each — call once per tab open. Newest-first. */
362
- export function indexCuratorLineage(): LineageIndex {
363
- const out: LineageIndex = new Map()
364
- const push = (name: string, ev: LineageEvent) => {
365
- const a = out.get(name) ?? []
366
- a.push(ev); out.set(name, a)
367
- }
368
- try {
369
- const base = hermesPath("logs/curator")
370
- for (const e of readdirSync(base, { withFileTypes: true })) {
371
- if (!e.isDirectory()) continue
372
- try {
373
- const j = JSON.parse(readFileSync(`${base}/${e.name}/run.json`, "utf8")) as RunJson
374
- const at = Date.parse(j.started_at ?? "") / 1000 || 0
375
- const run = e.name
376
- // targets absorbing 1+ sources
377
- const into = new Map<string, string[]>()
378
- for (const c of j.consolidated ?? []) {
379
- push(c.name, { kind: "merged", run, at, into: c.into, reason: c.reason })
380
- into.set(c.into, [...(into.get(c.into) ?? []), c.name])
381
- }
382
- for (const [tgt, srcs] of into)
383
- push(tgt, { kind: "absorbed", run, at, sources: srcs })
384
- for (const t of j.state_transitions ?? [])
385
- push(t.name, { kind: "transition", run, at, from: t.from, to: t.to })
386
- for (const p of j.pruned ?? [])
387
- push(p.name, { kind: "pruned", run, at, reason: p.reason })
388
- for (const n of j.added ?? [])
389
- push(n, { kind: "added", run, at })
390
- } catch {}
391
- }
392
- } catch {}
393
- for (const a of out.values()) a.sort((x, y) => y.at - x.at)
394
- return out
395
- }
396
-
397
- /**
398
- * Cron-generated hermes-agent changelog digest (tji.3). The cron job
399
- * writes ~/.hermes/herm/changelog.md as `# hermes-agent — N new
400
- * commits` + bullets. Splash shows the first bullet; the "N behind"
401
- * count already comes from session.info.update_behind, so this only
402
- * needs to surface the prose.
403
- */
404
- export type Changelog = { source: Source; headline: string; body: string }
405
-
406
- export function readChangelog(): Changelog | null {
407
- try {
408
- const source = makeSource("herm/changelog.md");
409
- // Bun.file().text() is async; splash render is sync-first-frame,
410
- // so openSync/readSync to keep the hot path synchronous.
411
- const fd = openSync(source.file, "r");
412
- const buf = Buffer.alloc(4096);
413
- const n = readSync(fd, buf);
414
- closeSync(fd);
415
- const body = buf.toString("utf8", 0, n).trim();
416
- const line = body.split("\n").find(l => /^[-*·]/.test(l.trim()));
417
- return { source, headline: line?.replace(/^[-*·]\s*/, "").trim() ?? "", body };
418
- } catch {
419
- return null;
420
- }
421
- }
422
-
423
- /** SOUL.md info */
424
- export interface SoulInfo {
425
- source: Source;
426
- charCount: number;
427
- tokenEstimate: number;
428
- /** Raw SOUL.md body. Consumed by the Context drill-down detail panel. */
429
- content: string;
430
- }
431
-
432
- /** System prompt breakdown — full text for section parsing */
433
- export interface SystemPromptInfo {
434
- source: Source;
435
- sessionId: string;
436
- text: string;
437
- totalChars: number;
438
- tokenEstimate: number;
439
- }
440
-
441
- /** Tool list with its source session file */
442
- export interface ToolsInfo {
443
- source: Source;
444
- tools: ToolInfo[];
445
- }
446
-
447
- // ─── Readers ─────────────────────────────────────────────────────────
448
-
449
- /** Read and parse config.yaml */
450
- export async function readConfig(): Promise<HermesConfig | null> {
451
- try {
452
- const file = Bun.file(hermesPath("config.yaml"));
453
- const text = await file.text();
454
- const raw = parseYaml(text);
455
- return {
456
- source: makeSource("config.yaml", "config.yaml"),
457
- model: {
458
- default: raw?.model?.default ?? "unknown",
459
- provider: raw?.model?.provider ?? "auto",
460
- base_url: raw?.model?.base_url ?? "",
461
- },
462
- agent: {
463
- max_turns: raw?.agent?.max_turns ?? 60,
464
- reasoning_effort: raw?.agent?.reasoning_effort ?? "medium",
465
- },
466
- compression: {
467
- enabled: raw?.compression?.enabled ?? true,
468
- threshold: raw?.compression?.threshold ?? 0.5,
469
- target_ratio: raw?.compression?.target_ratio ?? 0.2,
470
- protect_last_n: raw?.compression?.protect_last_n ?? 20,
471
- summary_model: raw?.compression?.summary_model ?? "",
472
- },
473
- memory: {
474
- memory_enabled: raw?.memory?.memory_enabled ?? true,
475
- user_profile_enabled: raw?.memory?.user_profile_enabled ?? true,
476
- memory_char_limit: raw?.memory?.memory_char_limit ?? 2200,
477
- user_char_limit: raw?.memory?.user_char_limit ?? 1375,
478
- provider: raw?.memory?.provider ?? "",
479
- nudge_interval: raw?.memory?.nudge_interval ?? 10,
480
- flush_min_turns: raw?.memory?.flush_min_turns ?? 6,
481
- },
482
- display: {
483
- personality: raw?.display?.personality ?? "default",
484
- skin: raw?.display?.skin ?? "default",
485
- show_cost: raw?.display?.show_cost ?? false,
486
- },
487
- curator: {
488
- enabled: raw?.curator?.enabled ?? true,
489
- interval_hours: raw?.curator?.interval_hours ?? 168,
490
- stale_after_days: raw?.curator?.stale_after_days ?? 30,
491
- archive_after_days: raw?.curator?.archive_after_days ?? 90,
492
- },
493
- gateway: {
494
- platforms: {
495
- api_server: raw?.gateway?.platforms?.api_server ?? undefined,
496
- },
497
- },
498
- };
499
- } catch {
500
- return null;
501
- }
502
- }
503
-
504
- /** Read a memory file (MEMORY.md or USER.md) with limit context */
505
- export async function readMemoryFile(
506
- filename: "MEMORY.md" | "USER.md",
507
- charLimit: number,
508
- ): Promise<MemoryFileInfo | null> {
509
- try {
510
- const relative = `memories/${filename}`;
511
- const file = Bun.file(hermesPath(relative));
512
- const content = await file.text();
513
- const entryCount = content.split("§").filter((s) => s.trim()).length;
514
- return {
515
- source: makeSource(relative, filename),
516
- content,
517
- charCount: content.length,
518
- charLimit,
519
- usagePercent:
520
- charLimit > 0 ? Math.round((content.length / charLimit) * 100) : 0,
521
- entryCount,
522
- };
523
- } catch {
524
- return null;
525
- }
526
- }
527
-
528
- /** Read sessions/sessions.json (live session index) */
529
- export async function readLiveSessions(): Promise<
530
- Record<string, LiveSession>
531
- > {
532
- try {
533
- const file = Bun.file(hermesPath("sessions/sessions.json"));
534
- const text = await file.text();
535
- return JSON.parse(text);
536
- } catch {
537
- return {};
538
- }
539
- }
540
-
541
- // ─── Session store ───────────────────────────────────────────────────
542
- // Everything session-shaped lives in sessions-db.ts now — one readonly
543
- // handle, one parent→child classification rule, shared SQL. These
544
- // aliases preserve the old hermes-home API for home/store.ts and
545
- // test/hermes-home-sessions.test.ts; new code should import from
546
- // sessions-db directly.
547
-
548
- import {
549
- roots as _roots, children as _children, lineage as _lineage,
550
- search as _search,
551
- } from "./sessions-db"
552
- export type { LineageInfo, SessionHit } from "./sessions-db"
553
- export const queryRecentSessions = _roots
554
- export const querySubagents = _children
555
- export const queryLineage = _lineage
556
- export const searchSessions = _search
557
-
558
- /** Memory provider info — what's configured and available */
559
- export interface MemoryProviderInfo {
560
- name: string;
561
- active: boolean;
562
- config: Record<string, string | number | boolean>;
563
- }
564
-
565
- // Per-provider local config/state file locations under HERMES_HOME.
566
- // This is lookup data, not an enumeration — discovery comes from
567
- // discoverMemoryProviders() below.
568
- const MEMORY_CFG_FILES: Record<string, string[]> = {
569
- mem0: ["mem0.json"],
570
- honcho: ["honcho.json"],
571
- hindsight: ["hindsight/config.json"],
572
- supermemory: ["supermemory.json"],
573
- holographic: ["holographic.db"],
574
- };
575
-
576
- /** Scan the bundled hermes-agent memory-plugin dir for provider names
577
- (mirrors plugins/memory/__init__.py discover's dir walk). User-
578
- installed providers in $HERMES_HOME/plugins/ aren't distinguished
579
- from non-memory plugins without importing them — wait for the
580
- memory.providers RPC for those. */
581
- function discoverMemoryProviders(): string[] {
582
- const names = new Set<string>(["builtin"]);
583
- try {
584
- for (const e of readdirSync(`${HERMES_HOME}/hermes-agent/plugins/memory`, { withFileTypes: true }))
585
- if (e.isDirectory() && !e.name.startsWith("_")) names.add(e.name);
586
- } catch {}
587
- return [...names];
588
- }
589
-
590
- /** Read memory provider configs from ~/.hermes/ — one entry per
591
- discovered provider, with any local config file parsed in. */
592
- export async function readMemoryProviders(
593
- activeProvider: string,
594
- ): Promise<MemoryProviderInfo[]> {
595
- const out: MemoryProviderInfo[] = [];
596
- for (const name of discoverMemoryProviders()) {
597
- if (name === "builtin") { out.push({ name, active: true, config: {} }); continue; }
598
- const cfg: Record<string, string | number | boolean> = {};
599
- for (const f of MEMORY_CFG_FILES[name] ?? []) {
600
- try {
601
- const file = Bun.file(hermesPath(f));
602
- if (f.endsWith(".json")) {
603
- const raw = await file.json();
604
- for (const [k, v] of Object.entries(raw)) {
605
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
606
- // Redact keys/tokens
607
- const lower = k.toLowerCase();
608
- if (lower.includes("key") || lower.includes("token") || lower.includes("secret")) {
609
- cfg[k] = typeof v === "string" ? `${v.slice(0, 4)}...` : v;
610
- } else {
611
- cfg[k] = v;
612
- }
613
- }
614
- }
615
- } else {
616
- const st = await file.stat();
617
- if (st) cfg["db_size"] = `${Math.round(st.size / 1024)}KB`;
618
- }
619
- } catch {}
620
- }
621
- out.push({ name, active: name === activeProvider, config: cfg });
622
- }
623
- return out;
624
- }
625
-
626
- /** Read SOUL.md */
627
- export async function readSoul(): Promise<SoulInfo | null> {
628
- try {
629
- const file = Bun.file(hermesPath("SOUL.md"));
630
- const text = await file.text();
631
- return {
632
- source: makeSource("SOUL.md"),
633
- charCount: text.length,
634
- tokenEstimate: tokenCount(text),
635
- content: text,
636
- };
637
- } catch {
638
- return null;
639
- }
640
- }
641
-
642
- /** Read tool list from the most recent session JSON */
643
- export async function readToolsFromLatestSession(): Promise<ToolsInfo | null> {
644
- try {
645
- const glob = new Bun.Glob("session_*.json");
646
- let latestPath = "";
647
- let latestTime = 0;
648
-
649
- for await (const path of glob.scan({ cwd: hermesPath("sessions") })) {
650
- const file = Bun.file(hermesPath(`sessions/${path}`));
651
- const stat = await file.stat();
652
- if (stat && stat.mtimeMs > latestTime) {
653
- latestTime = stat.mtimeMs;
654
- latestPath = path;
655
- }
656
- }
657
-
658
- if (!latestPath) return null;
659
-
660
- const relative = `sessions/${latestPath}`;
661
- const file = Bun.file(hermesPath(relative));
662
- const data = await file.json();
663
- type RawTool = { function?: { name?: string; description?: string; parameters?: unknown } };
664
- const tools: ToolInfo[] = (data.tools || []).map((t: RawTool) => ({
665
- name: t?.function?.name ?? "unknown",
666
- descriptionLength: (t?.function?.description ?? "").length,
667
- paramsLength: JSON.stringify(t?.function?.parameters ?? {}).length,
668
- }));
669
- return {
670
- source: makeSource(relative, latestPath),
671
- tools,
672
- };
673
- } catch {
674
- return null;
675
- }
676
- }
677
-
678
- /** Read system prompt from the most recent state.db session that has a full one */
679
- export function readSystemPromptInfo(): SystemPromptInfo | null {
680
- try {
681
- const db = new Database(hermesPath("state.db"), { readonly: true });
682
- // Short prompts (~700 chars) are the generic fallback without SOUL/memory/skills.
683
- const row = db
684
- .query(
685
- `SELECT id, system_prompt
686
- FROM sessions
687
- WHERE system_prompt IS NOT NULL AND length(system_prompt) > 1000
688
- ORDER BY started_at DESC LIMIT 1`,
689
- )
690
- .get() as { id: string; system_prompt: string } | null;
691
- db.close();
692
- if (!row) return null;
693
- return {
694
- source: makeSource("state.db"),
695
- sessionId: row.id,
696
- text: row.system_prompt,
697
- totalChars: row.system_prompt.length,
698
- tokenEstimate: tokenCount(row.system_prompt),
699
- };
700
- } catch {
701
- return null;
702
- }
703
- }
704
-
705
- export interface CronOutput {
706
- at: Date
707
- path: string
708
- text: string
709
- }
710
-
711
- /** Read the most recent cron output for a job, tail-truncated. */
712
- export async function readCronOutput(
713
- jobId: string,
714
- tailLines = 40,
715
- ): Promise<CronOutput | null> {
716
- const dir = hermesPath(`cron/output/${jobId}`);
717
- let entries: string[];
718
- try {
719
- entries = await readdir(dir);
720
- } catch {
721
- return null;
722
- }
723
- const md = entries.filter(f => f.endsWith(".md")).sort().reverse();
724
- if (md.length === 0) return null;
725
- const path = `${dir}/${md[0]}`;
726
- const full = await Bun.file(path).text();
727
- const lines = full.trimEnd().split("\n");
728
- const text =
729
- lines.length > tailLines
730
- ? `…(${lines.length - tailLines} earlier lines)\n` +
731
- lines.slice(-tailLines).join("\n")
732
- : full.trimEnd();
733
- const st = await stat(path);
734
- return { at: st.mtime, path, text };
735
- }
736
-
737
- // ─── Env File CRUD ──────────────────────────────────────────────────
738
-
739
- const ENV_PATH = hermesPath(".env");
740
-
741
- /** Parse ~/.hermes/.env into Record<string, string> */
742
- export async function readEnvFile(): Promise<Record<string, string>> {
743
- try {
744
- const text = await Bun.file(ENV_PATH).text();
745
- const vars: Record<string, string> = {};
746
- for (const line of text.split("\n")) {
747
- const trimmed = line.trim();
748
- if (!trimmed || trimmed.startsWith("#")) continue;
749
- const eq = trimmed.indexOf("=");
750
- if (eq < 1) continue;
751
- vars[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
752
- }
753
- return vars;
754
- } catch {
755
- return {};
756
- }
757
- }
758
-
759
- /** Update or append a KEY=VALUE in ~/.hermes/.env */
760
- export async function writeEnvVar(key: string, value: string): Promise<void> {
761
- let text = "";
762
- try {
763
- text = await Bun.file(ENV_PATH).text();
764
- } catch { /* file may not exist */ }
765
-
766
- const lines = text.split("\n");
767
- let found = false;
768
- const updated = lines.map(line => {
769
- if (line.startsWith(`${key}=`)) {
770
- found = true;
771
- return `${key}=${value}`;
772
- }
773
- return line;
774
- });
775
- if (!found) updated.push(`${key}=${value}`);
776
-
777
- await Bun.write(ENV_PATH, updated.join("\n"));
778
- }
779
-
780
- /** Remove a key from ~/.hermes/.env */
781
- export async function removeEnvVar(key: string): Promise<void> {
782
- let text = "";
783
- try {
784
- text = await Bun.file(ENV_PATH).text();
785
- } catch { return; }
786
-
787
- const lines = text.split("\n").filter(l => !l.startsWith(`${key}=`));
788
- await Bun.write(ENV_PATH, lines.join("\n"));
789
- }
790
-
791
- // ─── Provider Catalog ───────────────────────────────────────────────
792
-
793
- export const ENV_CATALOG: ReadonlyArray<{ category: string; keys: string[] }> = [
794
- {
795
- category: "LLM Providers",
796
- keys: [
797
- "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY",
798
- "DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY",
799
- "MISTRAL_API_KEY", "XAI_API_KEY", "TOGETHER_API_KEY",
800
- "FIREWORKS_API_KEY", "NOUS_API_KEY",
801
- ],
802
- },
803
- {
804
- category: "Tool API Keys",
805
- keys: [
806
- "FIRECRAWL_API_KEY", "BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID",
807
- "TAVILY_API_KEY", "EXA_API_KEY", "ELEVENLABS_API_KEY",
808
- ],
809
- },
810
- {
811
- category: "Messaging",
812
- keys: [
813
- "TELEGRAM_BOT_TOKEN", "DISCORD_BOT_TOKEN",
814
- "SLACK_BOT_TOKEN", "SLACK_APP_TOKEN",
815
- ],
816
- },
817
- {
818
- category: "Agent",
819
- keys: ["API_SERVER_KEY", "MEM0_API_KEY"],
820
- },
821
- ];