echoes-vault-opencode 1.1.0 → 1.1.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 (3) hide show
  1. package/index.ts +40 -0
  2. package/package.json +1 -1
  3. package/tui.tsx +58 -35
package/index.ts CHANGED
@@ -19,10 +19,17 @@ type VaultPaths = {
19
19
  assets: string
20
20
  }
21
21
 
22
+ type VaultStats = {
23
+ totalPages: number
24
+ totalDailyLogs: number
25
+ deprecatedPages: number
26
+ }
27
+
22
28
  type EchoesState = {
23
29
  version: number
24
30
  pluginVersion: string
25
31
  initialized: boolean
32
+ stats: VaultStats
26
33
  session: {
27
34
  started: boolean
28
35
  saved: boolean
@@ -47,6 +54,7 @@ const defaultState = (): EchoesState => ({
47
54
  version: 1,
48
55
  pluginVersion: "0.0.0",
49
56
  initialized: false,
57
+ stats: { totalPages: 0, totalDailyLogs: 0, deprecatedPages: 0 },
50
58
  session: {
51
59
  started: false,
52
60
  saved: false,
@@ -55,6 +63,32 @@ const defaultState = (): EchoesState => ({
55
63
  },
56
64
  })
57
65
 
66
+ const collectStats = async (vaultPaths: VaultPaths): Promise<VaultStats> => {
67
+ let totalPages = 0
68
+ let deprecatedPages = 0
69
+ let totalDailyLogs = 0
70
+ try {
71
+ const pageFiles = (await fs.readdir(vaultPaths.pages)).filter((f) => f.endsWith(".md"))
72
+ totalPages = pageFiles.length
73
+ for (const file of pageFiles) {
74
+ const content = await fs.readFile(path.join(vaultPaths.pages, file), "utf-8")
75
+ if (content.includes("> [!warning] DEPRECATED")) {
76
+ deprecatedPages++
77
+ }
78
+ }
79
+ } catch { /* pages dir may not exist */ }
80
+ try {
81
+ totalDailyLogs = (await fs.readdir(vaultPaths.daily)).filter((f) => f.endsWith(".md")).length
82
+ } catch { /* daily dir may not exist */ }
83
+ return { totalPages, totalDailyLogs, deprecatedPages }
84
+ }
85
+
86
+ const updateStats = async (directory: string, vaultPaths: VaultPaths): Promise<void> => {
87
+ const st = await readState(directory)
88
+ st.stats = await collectStats(vaultPaths)
89
+ await writeState(directory, st)
90
+ }
91
+
58
92
  const readState = async (directory: string): Promise<EchoesState> => {
59
93
  try {
60
94
  const raw = await fs.readFile(path.join(directory, STATE_FILENAME), "utf-8")
@@ -373,6 +407,7 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
373
407
 
374
408
  const state = await readState(directory)
375
409
  state.pluginVersion = await getPluginVersion()
410
+ state.stats = await collectStats(paths)
376
411
  state.session.started = false
377
412
  state.session.saved = false
378
413
  await writeState(directory, state)
@@ -479,6 +514,7 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
479
514
  const st = await readState(directory)
480
515
  st.session.saved = true
481
516
  st.session.lastSave = new Date().toISOString()
517
+ st.stats = await collectStats(paths)
482
518
  await writeState(directory, st)
483
519
 
484
520
  return [
@@ -506,6 +542,7 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
506
542
  const timestamp = new Date().toISOString()
507
543
  const entry = `### Scratchpad — ${timestamp}\n\n${args.logEntry}\n\n`
508
544
  await fs.appendFile(dailyFile, entry)
545
+ await updateStats(directory, paths)
509
546
  return `✅ Scratchpad note saved to EchoesVault/daily/${today}.md`
510
547
  },
511
548
  }),
@@ -587,6 +624,8 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
587
624
  }
588
625
  }
589
626
 
627
+ await updateStats(directory, paths)
628
+
590
629
  const action = existed ? "updated" : "created"
591
630
  const parts = [`✅ Page ${action}: EchoesVault/pages/${fileName}`]
592
631
  if (!existed && args.indexDescription) {
@@ -602,6 +641,7 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
602
641
  async execute(_args, _ctx) {
603
642
  const st = await readState(directory)
604
643
  st.initialized = true
644
+ st.stats = await collectStats(paths)
605
645
  await writeState(directory, st)
606
646
  return "EchoesVault activated."
607
647
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoes-vault-opencode",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "EchoesVault — persistent memory plugin for OpenCode. Obsidian-style knowledge base with daily logs, encyclopedia pages, and session resumption.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/tui.tsx CHANGED
@@ -1,13 +1,19 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { TuiPlugin } from "@opencode-ai/plugin/tui"
3
- import { createSignal, onCleanup } from "solid-js"
4
3
  import * as fs from "node:fs"
5
4
  import * as path from "node:path"
6
5
 
6
+ type VaultStats = {
7
+ totalPages: number
8
+ totalDailyLogs: number
9
+ deprecatedPages: number
10
+ }
11
+
7
12
  type EchoesState = {
8
13
  version: number
9
14
  pluginVersion: string
10
15
  initialized: boolean
16
+ stats: VaultStats
11
17
  session: {
12
18
  started: boolean
13
19
  saved: boolean
@@ -16,11 +22,13 @@ type EchoesState = {
16
22
  }
17
23
  }
18
24
 
19
- const STATE_PATH = path.join(process.cwd(), ".opencode", "echoes-state.json")
25
+ const CAPACITY_LIMIT = 200
26
+ const CAPACITY_WARNING = 170
20
27
 
21
28
  const readState = (): EchoesState | null => {
22
29
  try {
23
- const raw = fs.readFileSync(STATE_PATH, "utf-8")
30
+ const statePath = path.join(process.cwd(), ".opencode", "echoes-state.json")
31
+ const raw = fs.readFileSync(statePath, "utf-8")
24
32
  return JSON.parse(raw) as EchoesState
25
33
  } catch {
26
34
  return null
@@ -33,68 +41,83 @@ const tui: TuiPlugin = async (api) => {
33
41
  api.slots.register({
34
42
  slots: {
35
43
  sidebar_content() {
36
- const [state, setState] = createSignal<EchoesState | null>(readState())
44
+ const s = readState()
37
45
 
38
- const interval = setInterval(() => setState(readState()), 3000)
39
- onCleanup(() => clearInterval(interval))
40
-
41
- const statusColor = () => {
42
- const s = state()
46
+ const statusColor = (() => {
43
47
  if (!s || !s.initialized) return theme.error
44
48
  if (s.session.saved) return theme.accent
45
49
  if (s.session.started) return theme.success
46
50
  return theme.warning
47
- }
51
+ })()
48
52
 
49
- const statusLabel = () => {
50
- const s = state()
53
+ const statusLabel = (() => {
51
54
  if (!s || !s.initialized) return "Not Activated"
52
55
  if (s.session.saved) return "Memory Saved"
53
56
  if (s.session.started) return "Active"
54
57
  return "Session Not Started"
55
- }
56
-
57
- const statusDesc = () => {
58
- const s = state()
59
- if (!s || !s.initialized) return null
60
- if (s.session.saved) return "Vault memorized session data"
61
- if (s.session.started) return null
62
- return null
63
- }
58
+ })()
64
59
 
65
- const statusCmd = () => {
66
- const s = state()
60
+ const statusCmd = (() => {
67
61
  if (!s || !s.initialized) return "/echoes-init"
68
62
  if (s.session.saved) return null
69
63
  if (s.session.started) return "/echoes-end"
70
64
  return "/echoes-start"
71
- }
65
+ })()
72
66
 
73
- const statusCmdHint = () => {
74
- const s = state()
67
+ const statusCmdHint = (() => {
75
68
  if (!s || !s.initialized) return "to activate vault"
76
69
  if (s.session.saved) return null
77
70
  if (s.session.started) return "before closing"
78
71
  return "to load context"
79
- }
72
+ })()
73
+
74
+ const statusDesc = (() => {
75
+ if (!s || !s.initialized) return null
76
+ if (s.session.saved) return "Vault memorized session data"
77
+ return null
78
+ })()
79
+
80
+ const pages = s?.stats?.totalPages ?? 0
81
+
82
+ const capacityColor = (() => {
83
+ if (pages > CAPACITY_LIMIT) return theme.error
84
+ if (pages > CAPACITY_WARNING) return theme.warning
85
+ return theme.success
86
+ })()
87
+
88
+ const capacityPercent = Math.round((pages / CAPACITY_LIMIT) * 100)
80
89
 
81
90
  return (
82
91
  <box flexDirection="column">
83
92
  <box flexDirection="row">
84
- <text fg={statusColor()}><b>• </b></text>
93
+ <text fg={statusColor}><b>• </b></text>
85
94
  <text fg={theme.textMuted}><b>Echoes</b></text>
86
95
  <text fg={theme.text}><b>Vault</b></text>
87
- <text fg={theme.textMuted}> v{state()?.pluginVersion ?? ""}</text>
96
+ <text fg={theme.textMuted}> v{s?.pluginVersion ?? ""}</text>
88
97
  </box>
89
- <text fg={statusColor()}>{statusLabel()}</text>
90
- {statusDesc() && (
91
- <text fg={theme.textMuted}>{statusDesc()}</text>
98
+ <text fg={statusColor}>{statusLabel}</text>
99
+ {statusDesc && (
100
+ <text fg={theme.textMuted}>{statusDesc}</text>
92
101
  )}
93
- {statusCmd() && (
102
+ {statusCmd && (
94
103
  <box flexDirection="row">
95
104
  <text fg={theme.textMuted}>Run </text>
96
- <text fg={theme.text}>{statusCmd()}</text>
97
- <text fg={theme.textMuted}> {statusCmdHint()}</text>
105
+ <text fg={theme.text}>{statusCmd}</text>
106
+ <text fg={theme.textMuted}> {statusCmdHint}</text>
107
+ </box>
108
+ )}
109
+ {s?.initialized && (
110
+ <box flexDirection="column">
111
+ <text> </text>
112
+ <box flexDirection="row">
113
+ <text fg={capacityColor}><b>• </b></text>
114
+ <text fg={theme.text}><b>Vault Capacity</b></text>
115
+ </box>
116
+ <text fg={theme.textMuted}>Pages: {pages}</text>
117
+ <text fg={theme.textMuted}>Usage: {capacityPercent}%</text>
118
+ {pages > CAPACITY_LIMIT && (
119
+ <text fg={theme.textMuted}>Consider migrating to RAG</text>
120
+ )}
98
121
  </box>
99
122
  )}
100
123
  </box>