echoes-vault-opencode 1.0.34 → 1.0.36

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 +75 -75
  2. package/package.json +1 -1
  3. package/tui.tsx +51 -129
package/index.ts CHANGED
@@ -19,6 +19,45 @@ type VaultPaths = {
19
19
  assets: string
20
20
  }
21
21
 
22
+ type EchoesState = {
23
+ version: number
24
+ initialized: boolean
25
+ session: {
26
+ started: boolean
27
+ saved: boolean
28
+ lastStart: string | null
29
+ lastSave: string | null
30
+ }
31
+ }
32
+
33
+ const STATE_FILENAME = ".opencode/echoes-state.json"
34
+
35
+ const defaultState = (): EchoesState => ({
36
+ version: 1,
37
+ initialized: false,
38
+ session: {
39
+ started: false,
40
+ saved: false,
41
+ lastStart: null,
42
+ lastSave: null,
43
+ },
44
+ })
45
+
46
+ const readState = async (directory: string): Promise<EchoesState> => {
47
+ try {
48
+ const raw = await fs.readFile(path.join(directory, STATE_FILENAME), "utf-8")
49
+ return JSON.parse(raw) as EchoesState
50
+ } catch {
51
+ return defaultState()
52
+ }
53
+ }
54
+
55
+ const writeState = async (directory: string, state: EchoesState): Promise<void> => {
56
+ const filePath = path.join(directory, STATE_FILENAME)
57
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
58
+ await fs.writeFile(filePath, JSON.stringify(state, null, 2))
59
+ }
60
+
22
61
  const resolveVaultPaths = (directory: string): VaultPaths => {
23
62
  const vault = path.join(directory, "EchoesVault")
24
63
  return {
@@ -58,72 +97,6 @@ const toPageFilename = (name: string): string => {
58
97
 
59
98
  const toPageSlug = (filename: string): string => filename.replace(/\.md$/, "")
60
99
 
61
- // --------------- State file management ---------------
62
-
63
- type SessionState = {
64
- started: boolean
65
- saved: boolean
66
- lastStart: string | null
67
- lastSave: string | null
68
- }
69
-
70
- type VaultState = {
71
- version: number
72
- initialized: boolean
73
- session: SessionState
74
- }
75
-
76
- const STATE_FILE = ".state.json"
77
-
78
- const defaultState = (): VaultState => ({
79
- version: 1,
80
- initialized: true,
81
- session: {
82
- started: false,
83
- saved: false,
84
- lastStart: null,
85
- lastSave: null,
86
- },
87
- })
88
-
89
- async function readState(vaultDir: string): Promise<VaultState> {
90
- try {
91
- const raw = await fs.readFile(path.join(vaultDir, STATE_FILE), "utf-8")
92
- return JSON.parse(raw) as VaultState
93
- } catch {
94
- return defaultState()
95
- }
96
- }
97
-
98
- async function writeState(vaultDir: string, state: VaultState): Promise<void> {
99
- await fs.mkdir(vaultDir, { recursive: true })
100
- await fs.writeFile(path.join(vaultDir, STATE_FILE), JSON.stringify(state, null, 2))
101
- }
102
-
103
- async function markStarted(vaultDir: string): Promise<void> {
104
- const state = await readState(vaultDir)
105
- state.initialized = true
106
- state.session.started = true
107
- state.session.lastStart = new Date().toISOString()
108
- await writeState(vaultDir, state)
109
- }
110
-
111
- async function markSaved(vaultDir: string): Promise<void> {
112
- const state = await readState(vaultDir)
113
- state.session.saved = true
114
- state.session.lastSave = new Date().toISOString()
115
- await writeState(vaultDir, state)
116
- }
117
-
118
- async function resetSession(vaultDir: string): Promise<VaultState> {
119
- const state = await readState(vaultDir)
120
- state.session.started = false
121
- state.session.saved = false
122
- // preserve timestamps for display
123
- await writeState(vaultDir, state)
124
- return state
125
- }
126
-
127
100
  const ECHOES_INIT_COMMAND = `---
128
101
  description: Initialize EchoesVault — create directory structure and index.md
129
102
  agent: build
@@ -157,7 +130,9 @@ You are an AI developer agent equipped with persistent memory. Your memory is a
157
130
 
158
131
  ## \ud83d\ude80 ACTION
159
132
 
160
- The memory system has been initialized. Use your file reading tool to read the current \`EchoesVault/index.md\`.
133
+ **Step 0:** Call the \`echoes_activate_vault\` tool immediately to register the vault as activated in the status tracker.
134
+
135
+ Then use your file reading tool to read the current \`EchoesVault/index.md\`.
161
136
  If the index is empty or missing, acknowledge the initialization of a fresh vault. Otherwise, acknowledge your understanding of these rules with a brief message and list the key concepts already present in the index.
162
137
  `
163
138
 
@@ -188,6 +163,7 @@ Here is the concatenated work log from our LAST 3 SESSIONS (\`EchoesVault/daily/
188
163
  </recent_logs>
189
164
 
190
165
  ## ACTION
166
+ 0. **Register:** Call the \`echoes_start_session\` tool immediately to mark this session as started in the status tracker.
191
167
  1. **Restore:** Analyze the \`<recent_logs>\` to understand the current trajectory. Briefly summarize where we left off and what our immediate next steps should be today.
192
168
  2. **Linting:** Briefly review the \`<index>\`. Do you spot any duplicate concepts, obvious contradictions, or orphan topics that should be merged? If so, propose a quick refactoring plan. If the index is clean, simply say: "Index is healthy. Ready to code."
193
169
  `
@@ -383,8 +359,10 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
383
359
  await ensureCommands(directory)
384
360
  await ensureSkills(directory)
385
361
 
386
- // Reset session state on every plugin init (new OpenCode session)
387
- await resetSession(paths.vault)
362
+ const state = await readState(directory)
363
+ state.session.started = false
364
+ state.session.saved = false
365
+ await writeState(directory, state)
388
366
 
389
367
  return {
390
368
  config: async (input) => {
@@ -485,7 +463,10 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
485
463
 
486
464
  await fs.writeFile(idxFile, indexContent)
487
465
 
488
- await markSaved(paths.vault)
466
+ const st = await readState(directory)
467
+ st.session.saved = true
468
+ st.session.lastSave = new Date().toISOString()
469
+ await writeState(directory, st)
489
470
 
490
471
  return [
491
472
  `✅ Memory committed to EchoesVault.`,
@@ -512,9 +493,6 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
512
493
  const timestamp = new Date().toISOString()
513
494
  const entry = `### Scratchpad — ${timestamp}\n\n${args.logEntry}\n\n`
514
495
  await fs.appendFile(dailyFile, entry)
515
-
516
- await markStarted(paths.vault)
517
-
518
496
  return `✅ Scratchpad note saved to EchoesVault/daily/${today}.md`
519
497
  },
520
498
  }),
@@ -529,7 +507,6 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
529
507
  async execute(args, _ctx) {
530
508
  await ensureVaultDirs(paths)
531
509
  const results: string[] = []
532
- await markStarted(paths.vault)
533
510
  try {
534
511
  const files = (await fs.readdir(paths.pages)).filter((f) =>
535
512
  f.endsWith(".md")
@@ -576,7 +553,6 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
576
553
  },
577
554
  async execute(args, _ctx) {
578
555
  await ensureVaultDirs(paths)
579
- await markStarted(paths.vault)
580
556
  const fileName = toPageFilename(args.filename)
581
557
  const pageFile = path.join(paths.pages, fileName)
582
558
 
@@ -606,6 +582,30 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
606
582
  return parts.join("\n")
607
583
  },
608
584
  }),
585
+ echoes_activate_vault: tool({
586
+ description:
587
+ "Mark the EchoesVault as activated. Called automatically during /echoes-init to register the vault in the status tracker.",
588
+ args: {},
589
+ async execute(_args, _ctx) {
590
+ const st = await readState(directory)
591
+ st.initialized = true
592
+ await writeState(directory, st)
593
+ return "EchoesVault activated."
594
+ },
595
+ }),
596
+ echoes_start_session: tool({
597
+ description:
598
+ "Mark the current EchoesVault session as started. Called automatically during /echoes-start to update the status tracker.",
599
+ args: {},
600
+ async execute(_args, _ctx) {
601
+ const st = await readState(directory)
602
+ st.session.started = true
603
+ st.session.saved = false
604
+ st.session.lastStart = new Date().toISOString()
605
+ await writeState(directory, st)
606
+ return "EchoesVault session started."
607
+ },
608
+ }),
609
609
  },
610
610
  }
611
611
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoes-vault-opencode",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
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,154 +1,76 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { readFileSync } from "node:fs"
3
- import { join } from "node:path"
4
2
  import type { TuiPlugin } from "@opencode-ai/plugin/tui"
3
+ import { createSignal, onCleanup } from "solid-js"
4
+ import * as fs from "node:fs"
5
+ import * as path from "node:path"
5
6
 
6
- type SessionState = {
7
- started: boolean
8
- saved: boolean
9
- lastStart: string | null
10
- lastSave: string | null
11
- }
12
-
13
- type VaultState = {
7
+ type EchoesState = {
14
8
  version: number
15
9
  initialized: boolean
16
- session: SessionState
10
+ session: {
11
+ started: boolean
12
+ saved: boolean
13
+ lastStart: string | null
14
+ lastSave: string | null
15
+ }
17
16
  }
18
17
 
19
- type VaultStatus = "uninitialized" | "idle" | "working" | "saved"
18
+ const STATE_PATH = path.join(process.cwd(), ".opencode", "echoes-state.json")
20
19
 
21
- function readVaultState(dir: string): VaultState | null {
20
+ const readState = (): EchoesState | null => {
22
21
  try {
23
- return JSON.parse(readFileSync(join(dir, "EchoesVault", ".state.json"), "utf-8"))
22
+ const raw = fs.readFileSync(STATE_PATH, "utf-8")
23
+ return JSON.parse(raw) as EchoesState
24
24
  } catch {
25
25
  return null
26
26
  }
27
27
  }
28
28
 
29
- function getStatus(state: VaultState | null): VaultStatus {
30
- if (!state || !state.initialized) return "uninitialized"
31
- if (state.session.saved) return "saved"
32
- if (state.session.started) return "working"
33
- return "idle"
34
- }
35
-
36
- function timeAgo(iso: string | null): string {
37
- if (!iso) return ""
38
- const diff = Date.now() - new Date(iso).getTime()
39
- const mins = Math.floor(diff / 60000)
40
- if (mins < 1) return "just now"
41
- if (mins < 60) return `${mins}m ago`
42
- const hours = Math.floor(mins / 60)
43
- if (hours < 24) return `${hours}h ago`
44
- const days = Math.floor(hours / 24)
45
- return `${days}d ago`
46
- }
47
-
48
- function formatTime(iso: string | null): string {
49
- if (!iso) return ""
50
- return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
51
- }
52
-
53
- const STATUS_META: Record<VaultStatus, { label: string; description: string }> = {
54
- uninitialized: {
55
- label: "Vault not initialized",
56
- description: 'Run /echoes-init to create EchoesVault',
57
- },
58
- idle: {
59
- label: "Vault idle",
60
- description: 'Run /echoes-start to restore context',
61
- },
62
- working: {
63
- label: "Vault active",
64
- description: 'Run /echoes-end before closing to save session',
65
- },
66
- saved: {
67
- label: "Session saved",
68
- description: 'Memory committed to EchoesVault',
69
- },
70
- }
71
-
72
- const STATUS_COLOR: Record<VaultStatus, string> = {
73
- uninitialized: "error",
74
- idle: "warning",
75
- working: "success",
76
- saved: "accent",
77
- }
78
-
79
29
  const tui: TuiPlugin = async (api) => {
80
30
  const theme = api.theme.current
81
- const dir = api.state.path.directory
82
31
 
83
32
  api.slots.register({
84
33
  slots: {
85
34
  sidebar_content() {
86
- const state = readVaultState(dir)
87
- const status = getStatus(state)
88
- const meta = STATUS_META[status]
89
- const colorKey = STATUS_COLOR[status]
90
- const color = (theme as any)[colorKey] ?? theme.text
91
-
92
- const startedAgo = timeAgo(state?.session.lastStart ?? null)
93
- const savedTime = formatTime(state?.session.lastSave ?? null)
94
- const savedAgo = timeAgo(state?.session.lastSave ?? null)
35
+ const [state, setState] = createSignal<EchoesState | null>(readState())
36
+
37
+ const interval = setInterval(() => setState(readState()), 3000)
38
+ onCleanup(() => clearInterval(interval))
39
+
40
+ const statusColor = () => {
41
+ const s = state()
42
+ if (!s || !s.initialized) return theme.error
43
+ if (s.session.saved) return theme.accent
44
+ if (s.session.started) return theme.success
45
+ return theme.warning
46
+ }
47
+
48
+ const statusLabel = () => {
49
+ const s = state()
50
+ if (!s || !s.initialized) return "Not Activated"
51
+ if (s.session.saved) return "Memory Saved"
52
+ if (s.session.started) return "Active"
53
+ return "Session Not Started"
54
+ }
55
+
56
+ const statusDesc = () => {
57
+ const s = state()
58
+ if (!s || !s.initialized)
59
+ return "Run /echoes-init to activate vault"
60
+ if (s.session.saved)
61
+ return "Vault memorized session data"
62
+ if (s.session.started)
63
+ return "Run /echoes-end before closing"
64
+ return "Run /echoes-start to load context"
65
+ }
95
66
 
96
67
  return (
97
68
  <box flexDirection="column">
98
- <box border="all" borderStyle="single" paddingX={1} paddingY={0}>
99
- <text fg={theme.text}>
100
- <b>EchoesVault</b>
101
- </text>
102
- </box>
103
-
104
- <box paddingX={1} paddingY={0} marginTop={1}>
105
- <text fg={color}>
106
- <b>{meta.label}</b>
107
- </text>
108
- </box>
109
-
110
- <box paddingX={1} paddingY={0}>
111
- <text fg={theme.textMuted}>{meta.description}</text>
112
- </box>
113
-
114
- {startedAgo && (
115
- <box paddingX={1} paddingY={0}>
116
- <text fg={theme.textMuted}>Started {startedAgo}</text>
117
- </box>
118
- )}
119
-
120
- {savedTime && (
121
- <box paddingX={1} paddingY={0}>
122
- <text fg={theme.textMuted}>Last save {savedTime}</text>
123
- {savedAgo && <text fg={theme.textMuted}> ({savedAgo})</text>}
124
- </box>
125
- )}
126
-
127
- <box paddingX={1} paddingY={0} marginTop={1}>
128
- <text fg={theme.textMuted}>
129
- <b>Commands</b>
130
- </text>
131
- </box>
132
-
133
- <box paddingX={1} paddingY={0}>
134
- <text fg={theme.text}>/echoes-init</text>
135
- <text fg={theme.textMuted}> Create vault</text>
136
- </box>
137
-
138
- <box paddingX={1} paddingY={0}>
139
- <text fg={theme.text}>/echoes-start</text>
140
- <text fg={theme.textMuted}> Restore context</text>
141
- </box>
142
-
143
- <box paddingX={1} paddingY={0}>
144
- <text fg={theme.text}>/echoes-end</text>
145
- <text fg={theme.textMuted}> Save session</text>
146
- </box>
147
-
148
- <box paddingX={1} paddingY={0}>
149
- <text fg={theme.text}>/echoes-status</text>
150
- <text fg={theme.textMuted}> Vault health</text>
151
- </box>
69
+ <text fg={theme.text}>
70
+ <b>EchoesVault</b>
71
+ </text>
72
+ <text fg={statusColor()}>{statusLabel()}</text>
73
+ <text fg={theme.textMuted}>{statusDesc()}</text>
152
74
  </box>
153
75
  )
154
76
  },
@@ -156,4 +78,4 @@ const tui: TuiPlugin = async (api) => {
156
78
  })
157
79
  }
158
80
 
159
- export default { id: "echoes-vault-sidebar", tui }
81
+ export default { id: "echoes-vault-ui", tui }