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

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 (192) hide show
  1. package/README.md +8 -4
  2. package/assets/eikons/ares.eikon +367 -0
  3. package/assets/eikons/default.eikon +398 -0
  4. package/assets/eikons/mono.eikon +395 -0
  5. package/db.worker.js +81 -0
  6. package/highlights-eq9cgrbb.scm +604 -0
  7. package/highlights-ghv9g403.scm +205 -0
  8. package/highlights-hk7bwhj4.scm +284 -0
  9. package/highlights-r812a2qc.scm +150 -0
  10. package/highlights-x6tmsnaa.scm +115 -0
  11. package/index.js +4151 -0
  12. package/injections-73j83es3.scm +27 -0
  13. package/package.json +14 -64
  14. package/parser.worker.js +8 -0
  15. package/tree-sitter-3jzf13jk.wasm +0 -0
  16. package/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  17. package/tree-sitter-markdown-411r6y9b.wasm +0 -0
  18. package/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  19. package/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  20. package/tree-sitter-zig-e78zbjpm.wasm +0 -0
  21. package/scripts/postinstall.ts +0 -29
  22. package/src/app/gateway.tsx +0 -83
  23. package/src/app/gatewayEvents.ts +0 -203
  24. package/src/app/launch.ts +0 -41
  25. package/src/app/skin.tsx +0 -31
  26. package/src/app/spawnHistory.ts +0 -75
  27. package/src/app/tabs.ts +0 -23
  28. package/src/app/turnReducer.ts +0 -390
  29. package/src/app/useAppKeys.ts +0 -268
  30. package/src/app/useAtRefPopover.ts +0 -99
  31. package/src/app/useInputHistory.ts +0 -66
  32. package/src/app/useSession.ts +0 -102
  33. package/src/app/useSlashCommands.ts +0 -70
  34. package/src/app/useSlashPopover.ts +0 -48
  35. package/src/app.tsx +0 -917
  36. package/src/commands/slash.ts +0 -151
  37. package/src/components/avatar/AnimatedAvatar.tsx +0 -66
  38. package/src/components/avatar/eikon.ts +0 -144
  39. package/src/components/avatar/states/error.ts +0 -1155
  40. package/src/components/avatar/states/idle.ts +0 -1155
  41. package/src/components/avatar/states/index.ts +0 -30
  42. package/src/components/avatar/states/listening.ts +0 -1155
  43. package/src/components/avatar/states/speaking.ts +0 -1155
  44. package/src/components/avatar/states/thinking.ts +0 -1155
  45. package/src/components/avatar/states/working.ts +0 -1155
  46. package/src/components/chat/AtRefPopover.tsx +0 -54
  47. package/src/components/chat/CodeBlock.tsx +0 -67
  48. package/src/components/chat/Composer.tsx +0 -347
  49. package/src/components/chat/DiffBlock.tsx +0 -116
  50. package/src/components/chat/ErrorBlock.tsx +0 -70
  51. package/src/components/chat/MediaChip.tsx +0 -114
  52. package/src/components/chat/MessageItem.tsx +0 -282
  53. package/src/components/chat/MessageList.tsx +0 -114
  54. package/src/components/chat/PromptCard.tsx +0 -359
  55. package/src/components/chat/SlashPopover.tsx +0 -158
  56. package/src/components/chat/ThoughtCloud.tsx +0 -185
  57. package/src/components/chat/TypingIndicator.tsx +0 -25
  58. package/src/components/chat/tool/Subagent.tsx +0 -75
  59. package/src/components/chat/tool/frame.tsx +0 -69
  60. package/src/components/chat/tool/index.tsx +0 -65
  61. package/src/components/chat/tool/preview.ts +0 -57
  62. package/src/components/sidebar/ContextGauge.tsx +0 -102
  63. package/src/components/sidebar/Sidebar.tsx +0 -143
  64. package/src/components/tabs/TabBar.tsx +0 -50
  65. package/src/components/ui/FileLink.tsx +0 -52
  66. package/src/config/index.ts +0 -156
  67. package/src/config/lane.ts +0 -161
  68. package/src/config/models.ts +0 -95
  69. package/src/config/rules.ts +0 -80
  70. package/src/config/schema.ts +0 -308
  71. package/src/dialogs/alert.tsx +0 -52
  72. package/src/dialogs/chafa.tsx +0 -72
  73. package/src/dialogs/confirm.tsx +0 -58
  74. package/src/dialogs/curator.tsx +0 -153
  75. package/src/dialogs/eikon-picker.tsx +0 -95
  76. package/src/dialogs/help.tsx +0 -80
  77. package/src/dialogs/history.tsx +0 -92
  78. package/src/dialogs/info.tsx +0 -115
  79. package/src/dialogs/keys.tsx +0 -170
  80. package/src/dialogs/logs.tsx +0 -42
  81. package/src/dialogs/message.tsx +0 -38
  82. package/src/dialogs/model-picker.tsx +0 -123
  83. package/src/dialogs/new-profile.tsx +0 -69
  84. package/src/dialogs/new-task.tsx +0 -103
  85. package/src/dialogs/profile.tsx +0 -55
  86. package/src/dialogs/rollback.tsx +0 -190
  87. package/src/dialogs/spawn-history.tsx +0 -80
  88. package/src/dialogs/text-prompt.tsx +0 -68
  89. package/src/dialogs/theme-picker.tsx +0 -50
  90. package/src/home/index.ts +0 -23
  91. package/src/home/store.ts +0 -267
  92. package/src/index.tsx +0 -113
  93. package/src/keys/catalog.ts +0 -115
  94. package/src/keys/chord.ts +0 -125
  95. package/src/keys/conflicts.ts +0 -48
  96. package/src/keys/context.tsx +0 -112
  97. package/src/keys/index.ts +0 -5
  98. package/src/keys/list.ts +0 -94
  99. package/src/keys/oc-compat.ts +0 -87
  100. package/src/tabs/Agents.tsx +0 -607
  101. package/src/tabs/Analytics.tsx +0 -154
  102. package/src/tabs/Chat.tsx +0 -50
  103. package/src/tabs/Config.tsx +0 -605
  104. package/src/tabs/Context.tsx +0 -599
  105. package/src/tabs/Cron.tsx +0 -294
  106. package/src/tabs/Env.tsx +0 -227
  107. package/src/tabs/Kanban.tsx +0 -367
  108. package/src/tabs/Memory.tsx +0 -294
  109. package/src/tabs/Sessions.tsx +0 -786
  110. package/src/tabs/Skills.tsx +0 -507
  111. package/src/tabs/Toolsets.tsx +0 -266
  112. package/src/theme/builtin.ts +0 -78
  113. package/src/theme/context.tsx +0 -106
  114. package/src/theme/index.ts +0 -4
  115. package/src/theme/resolve.ts +0 -134
  116. package/src/theme/syntax.ts +0 -31
  117. package/src/theme/themes/aura.json +0 -69
  118. package/src/theme/themes/ayu.json +0 -80
  119. package/src/theme/themes/carbonfox.json +0 -248
  120. package/src/theme/themes/catppuccin-frappe.json +0 -233
  121. package/src/theme/themes/catppuccin-macchiato.json +0 -233
  122. package/src/theme/themes/catppuccin.json +0 -112
  123. package/src/theme/themes/cobalt2.json +0 -228
  124. package/src/theme/themes/cursor.json +0 -249
  125. package/src/theme/themes/dracula.json +0 -219
  126. package/src/theme/themes/everforest.json +0 -241
  127. package/src/theme/themes/flexoki.json +0 -237
  128. package/src/theme/themes/github.json +0 -233
  129. package/src/theme/themes/gruvbox.json +0 -242
  130. package/src/theme/themes/kanagawa.json +0 -77
  131. package/src/theme/themes/lucent-orng.json +0 -237
  132. package/src/theme/themes/material.json +0 -235
  133. package/src/theme/themes/matrix.json +0 -77
  134. package/src/theme/themes/mercury.json +0 -252
  135. package/src/theme/themes/monokai.json +0 -221
  136. package/src/theme/themes/nightowl.json +0 -221
  137. package/src/theme/themes/nord.json +0 -223
  138. package/src/theme/themes/one-dark.json +0 -84
  139. package/src/theme/themes/opencode.json +0 -245
  140. package/src/theme/themes/orng.json +0 -249
  141. package/src/theme/themes/osaka-jade.json +0 -93
  142. package/src/theme/themes/palenight.json +0 -222
  143. package/src/theme/themes/rosepine.json +0 -234
  144. package/src/theme/themes/solarized.json +0 -223
  145. package/src/theme/themes/synthwave84.json +0 -226
  146. package/src/theme/themes/tokyonight.json +0 -243
  147. package/src/theme/themes/vercel.json +0 -245
  148. package/src/theme/themes/vesper.json +0 -218
  149. package/src/theme/themes/zenburn.json +0 -223
  150. package/src/theme/types.ts +0 -119
  151. package/src/types/message.ts +0 -97
  152. package/src/ui/ChafaImage.tsx +0 -64
  153. package/src/ui/Splash.tsx +0 -118
  154. package/src/ui/borders.ts +0 -28
  155. package/src/ui/command.tsx +0 -104
  156. package/src/ui/dialog-select.tsx +0 -164
  157. package/src/ui/dialog.tsx +0 -102
  158. package/src/ui/fmt.ts +0 -82
  159. package/src/ui/kv.tsx +0 -28
  160. package/src/ui/shell.tsx +0 -45
  161. package/src/ui/spinner.tsx +0 -59
  162. package/src/ui/splash-art.ts +0 -123
  163. package/src/ui/table.tsx +0 -117
  164. package/src/ui/ticker.tsx +0 -90
  165. package/src/ui/toast.tsx +0 -130
  166. package/src/utils/categorical.ts +0 -77
  167. package/src/utils/chafa.ts +0 -173
  168. package/src/utils/clipboard.ts +0 -67
  169. package/src/utils/context-segments.ts +0 -317
  170. package/src/utils/control.ts +0 -495
  171. package/src/utils/drop.ts +0 -25
  172. package/src/utils/editor.ts +0 -33
  173. package/src/utils/fuzzy.ts +0 -45
  174. package/src/utils/gateway-client.ts +0 -253
  175. package/src/utils/gateway-types.ts +0 -282
  176. package/src/utils/git.ts +0 -57
  177. package/src/utils/hermes-analytics.ts +0 -134
  178. package/src/utils/hermes-home.ts +0 -821
  179. package/src/utils/hermes-kanban.ts +0 -154
  180. package/src/utils/hermes-profiles.ts +0 -217
  181. package/src/utils/interpolate.ts +0 -31
  182. package/src/utils/math-unicode.ts +0 -818
  183. package/src/utils/memory-activity.ts +0 -140
  184. package/src/utils/open-file.ts +0 -13
  185. package/src/utils/paths.ts +0 -52
  186. package/src/utils/perf.ts +0 -235
  187. package/src/utils/preferences.ts +0 -150
  188. package/src/utils/sessions-db.ts +0 -396
  189. package/src/utils/subagent-tree.ts +0 -146
  190. package/src/utils/terminal-reset.ts +0 -129
  191. package/src/utils/tips.ts +0 -67
  192. 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
- ];