echoes-vault-opencode 1.0.32 → 1.0.34

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 +76 -0
  2. package/package.json +1 -1
  3. package/tui.tsx +139 -10
package/index.ts CHANGED
@@ -58,6 +58,72 @@ const toPageFilename = (name: string): string => {
58
58
 
59
59
  const toPageSlug = (filename: string): string => filename.replace(/\.md$/, "")
60
60
 
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
+
61
127
  const ECHOES_INIT_COMMAND = `---
62
128
  description: Initialize EchoesVault — create directory structure and index.md
63
129
  agent: build
@@ -317,6 +383,9 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
317
383
  await ensureCommands(directory)
318
384
  await ensureSkills(directory)
319
385
 
386
+ // Reset session state on every plugin init (new OpenCode session)
387
+ await resetSession(paths.vault)
388
+
320
389
  return {
321
390
  config: async (input) => {
322
391
  const cmds: Record<string, string> = {
@@ -416,6 +485,8 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
416
485
 
417
486
  await fs.writeFile(idxFile, indexContent)
418
487
 
488
+ await markSaved(paths.vault)
489
+
419
490
  return [
420
491
  `✅ Memory committed to EchoesVault.`,
421
492
  `- Daily log: EchoesVault/daily/${today}.md`,
@@ -441,6 +512,9 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
441
512
  const timestamp = new Date().toISOString()
442
513
  const entry = `### Scratchpad — ${timestamp}\n\n${args.logEntry}\n\n`
443
514
  await fs.appendFile(dailyFile, entry)
515
+
516
+ await markStarted(paths.vault)
517
+
444
518
  return `✅ Scratchpad note saved to EchoesVault/daily/${today}.md`
445
519
  },
446
520
  }),
@@ -455,6 +529,7 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
455
529
  async execute(args, _ctx) {
456
530
  await ensureVaultDirs(paths)
457
531
  const results: string[] = []
532
+ await markStarted(paths.vault)
458
533
  try {
459
534
  const files = (await fs.readdir(paths.pages)).filter((f) =>
460
535
  f.endsWith(".md")
@@ -501,6 +576,7 @@ const OpenCodeEchoes: Plugin = async ({ directory }) => {
501
576
  },
502
577
  async execute(args, _ctx) {
503
578
  await ensureVaultDirs(paths)
579
+ await markStarted(paths.vault)
504
580
  const fileName = toPageFilename(args.filename)
505
581
  const pageFile = path.join(paths.pages, fileName)
506
582
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echoes-vault-opencode",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
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,30 +1,159 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
+ import { readFileSync } from "node:fs"
3
+ import { join } from "node:path"
2
4
  import type { TuiPlugin } from "@opencode-ai/plugin/tui"
3
5
 
6
+ type SessionState = {
7
+ started: boolean
8
+ saved: boolean
9
+ lastStart: string | null
10
+ lastSave: string | null
11
+ }
12
+
13
+ type VaultState = {
14
+ version: number
15
+ initialized: boolean
16
+ session: SessionState
17
+ }
18
+
19
+ type VaultStatus = "uninitialized" | "idle" | "working" | "saved"
20
+
21
+ function readVaultState(dir: string): VaultState | null {
22
+ try {
23
+ return JSON.parse(readFileSync(join(dir, "EchoesVault", ".state.json"), "utf-8"))
24
+ } catch {
25
+ return null
26
+ }
27
+ }
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
+
4
79
  const tui: TuiPlugin = async (api) => {
5
80
  const theme = api.theme.current
81
+ const dir = api.state.path.directory
6
82
 
7
83
  api.slots.register({
8
84
  slots: {
9
85
  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)
95
+
10
96
  return (
11
- <box flexDirection="column">
97
+ <box flexDirection="column">
98
+ <box border="all" borderStyle="single" paddingX={1} paddingY={0}>
12
99
  <text fg={theme.text}>
13
- <b>EchoesVault Controls</b>
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>
14
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>
15
147
 
16
- <text fg={theme.secondary}>• Start Session</text>
17
- <text fg={theme.accent}>• End Session</text>
18
- <text fg={theme.error}>• Init</text>
19
- <text fg={theme.warning}>• Status</text>
20
- <text fg={theme.success}>• Success</text>
21
- <text fg={theme.info}>• Info</text>
22
- <text fg={theme.textMuted}>• Muted</text>
148
+ <box paddingX={1} paddingY={0}>
149
+ <text fg={theme.text}>/echoes-status</text>
150
+ <text fg={theme.textMuted}> Vault health</text>
23
151
  </box>
152
+ </box>
24
153
  )
25
154
  },
26
155
  },
27
156
  })
28
157
  }
29
158
 
30
- export default { id: "echoes-vault-ui", tui }
159
+ export default { id: "echoes-vault-sidebar", tui }