echoes-vault-opencode 1.0.33 → 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 +76 -67
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.33",
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,101 +1,97 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { createSignal } from "solid-js"
2
+ import { readFileSync } from "node:fs"
3
+ import { join } from "node:path"
3
4
  import type { TuiPlugin } from "@opencode-ai/plugin/tui"
4
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
+
5
19
  type VaultStatus = "uninitialized" | "idle" | "working" | "saved"
6
20
 
7
- const KV = "echoes_vault_state"
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
+ }
8
28
 
9
- const STATUS_META: Record<VaultStatus, { label: string; description: string; colorKey: string }> = {
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 }> = {
10
54
  uninitialized: {
11
55
  label: "Vault not initialized",
12
56
  description: 'Run /echoes-init to create EchoesVault',
13
- colorKey: "error",
14
57
  },
15
58
  idle: {
16
59
  label: "Vault idle",
17
- description: 'Run /echoes-start to restore context from vault',
18
- colorKey: "warning",
60
+ description: 'Run /echoes-start to restore context',
19
61
  },
20
62
  working: {
21
63
  label: "Vault active",
22
- description: 'Run /echoes-end before closing to save session memory',
23
- colorKey: "success",
64
+ description: 'Run /echoes-end before closing to save session',
24
65
  },
25
66
  saved: {
26
67
  label: "Session saved",
27
68
  description: 'Memory committed to EchoesVault',
28
- colorKey: "accent",
29
69
  },
30
70
  }
31
71
 
32
- function computeStatus(init: boolean, started: boolean, ended: boolean): VaultStatus {
33
- if (!init) return "uninitialized"
34
- if (ended) return "saved"
35
- if (started) return "working"
36
- return "idle"
72
+ const STATUS_COLOR: Record<VaultStatus, string> = {
73
+ uninitialized: "error",
74
+ idle: "warning",
75
+ working: "success",
76
+ saved: "accent",
37
77
  }
38
78
 
39
79
  const tui: TuiPlugin = async (api) => {
40
80
  const theme = api.theme.current
41
-
42
- const [getInit, setInit] = createSignal(false)
43
- const [getStarted, setStarted] = createSignal(false)
44
- const [getEnded, setEnded] = createSignal(false)
45
-
46
- try {
47
- const raw = api.kv.get<{ initialized: boolean }>(KV)
48
- if (raw?.initialized) setInit(true)
49
- } catch {}
50
-
51
- function persist() {
52
- try { api.kv.set(KV, { initialized: getInit() }) } catch {}
53
- }
54
-
55
- function handleCommand(cmd: "init" | "start" | "end") {
56
- if (cmd === "init") {
57
- setInit(true)
58
- setStarted(false)
59
- setEnded(false)
60
- } else if (cmd === "start") {
61
- setStarted(true)
62
- setEnded(false)
63
- } else if (cmd === "end") {
64
- setEnded(true)
65
- }
66
- persist()
67
- }
68
-
69
- api.event.on("session.created", () => {
70
- setStarted(false)
71
- setEnded(false)
72
- })
73
-
74
- api.event.on("session.idle", () => {
75
- if (getStarted() && !getEnded()) {
76
- setStarted(false)
77
- }
78
- })
79
-
80
- api.event.on("message.part.updated", (event: any) => {
81
- try {
82
- const part = event?.properties?.part
83
- if (!part) return
84
- const text: string = part.text ?? part.content ?? ""
85
- if (typeof text !== "string") return
86
-
87
- if (text.includes("/echoes-init")) handleCommand("init")
88
- else if (text.includes("/echoes-start")) handleCommand("start")
89
- else if (text.includes("/echoes-end")) handleCommand("end")
90
- } catch {}
91
- })
81
+ const dir = api.state.path.directory
92
82
 
93
83
  api.slots.register({
94
84
  slots: {
95
85
  sidebar_content() {
96
- const status = computeStatus(getInit(), getStarted(), getEnded())
86
+ const state = readVaultState(dir)
87
+ const status = getStatus(state)
97
88
  const meta = STATUS_META[status]
98
- const color = (theme as any)[meta.colorKey] ?? theme.text
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)
99
95
 
100
96
  return (
101
97
  <box flexDirection="column">
@@ -115,6 +111,19 @@ const tui: TuiPlugin = async (api) => {
115
111
  <text fg={theme.textMuted}>{meta.description}</text>
116
112
  </box>
117
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
+
118
127
  <box paddingX={1} paddingY={0} marginTop={1}>
119
128
  <text fg={theme.textMuted}>
120
129
  <b>Commands</b>