echoes-vault-opencode 1.0.33 → 1.0.35

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 +113 -143
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.35",
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,150 +1,120 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { createSignal } from "solid-js"
3
2
  import type { TuiPlugin } from "@opencode-ai/plugin/tui"
4
-
5
- type VaultStatus = "uninitialized" | "idle" | "working" | "saved"
6
-
7
- const KV = "echoes_vault_state"
8
-
9
- const STATUS_META: Record<VaultStatus, { label: string; description: string; colorKey: string }> = {
10
- uninitialized: {
11
- label: "Vault not initialized",
12
- description: 'Run /echoes-init to create EchoesVault',
13
- colorKey: "error",
14
- },
15
- idle: {
16
- label: "Vault idle",
17
- description: 'Run /echoes-start to restore context from vault',
18
- colorKey: "warning",
19
- },
20
- working: {
21
- label: "Vault active",
22
- description: 'Run /echoes-end before closing to save session memory',
23
- colorKey: "success",
24
- },
25
- saved: {
26
- label: "Session saved",
27
- description: 'Memory committed to EchoesVault',
28
- colorKey: "accent",
29
- },
30
- }
31
-
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"
3
+ import { createSignal, onMount, onCleanup } from "@opentui/solid"
4
+ import * as fs from "node:fs/promises"
5
+ import * as path from "node:path"
6
+
7
+ // Тип для нашего внутреннего состояния
8
+ type VaultStatus = {
9
+ title: string
10
+ desc: string
11
+ color: string
37
12
  }
38
13
 
39
14
  const tui: TuiPlugin = async (api) => {
40
- 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
- })
92
-
93
- api.slots.register({
94
- slots: {
95
- sidebar_content() {
96
- const status = computeStatus(getInit(), getStarted(), getEnded())
97
- const meta = STATUS_META[status]
98
- const color = (theme as any)[meta.colorKey] ?? theme.text
99
-
100
- return (
101
- <box flexDirection="column">
102
- <box border="all" borderStyle="single" paddingX={1} paddingY={0}>
103
- <text fg={theme.text}>
104
- <b>EchoesVault</b>
105
- </text>
106
- </box>
107
-
108
- <box paddingX={1} paddingY={0} marginTop={1}>
109
- <text fg={color}>
110
- <b>{meta.label}</b>
111
- </text>
112
- </box>
113
-
114
- <box paddingX={1} paddingY={0}>
115
- <text fg={theme.textMuted}>{meta.description}</text>
116
- </box>
117
-
118
- <box paddingX={1} paddingY={0} marginTop={1}>
119
- <text fg={theme.textMuted}>
120
- <b>Commands</b>
121
- </text>
122
- </box>
123
-
124
- <box paddingX={1} paddingY={0}>
125
- <text fg={theme.text}>/echoes-init</text>
126
- <text fg={theme.textMuted}> Create vault</text>
127
- </box>
128
-
129
- <box paddingX={1} paddingY={0}>
130
- <text fg={theme.text}>/echoes-start</text>
131
- <text fg={theme.textMuted}> Restore context</text>
132
- </box>
133
-
134
- <box paddingX={1} paddingY={0}>
135
- <text fg={theme.text}>/echoes-end</text>
136
- <text fg={theme.textMuted}> Save session</text>
137
- </box>
138
-
139
- <box paddingX={1} paddingY={0}>
140
- <text fg={theme.text}>/echoes-status</text>
141
- <text fg={theme.textMuted}> Vault health</text>
142
- </box>
143
- </box>
144
- )
145
- },
146
- },
147
- })
15
+ const theme = api.theme.current
16
+
17
+ api.slots.register({
18
+ slots: {
19
+ sidebar_content() {
20
+ // Создаем реактивный сигнал
21
+ const [status, setStatus] = createSignal<VaultStatus>({
22
+ title: "Checking...",
23
+ desc: "Verifying vault state...",
24
+ color: theme.textMuted,
25
+ })
26
+
27
+ onMount(async () => {
28
+ // Функция проверки статуса
29
+ const checkStatus = async () => {
30
+ // В OpenCode TUI обычно рабочая директория — это process.cwd()
31
+ const cwd = process.cwd()
32
+ const vaultPath = path.join(cwd, "EchoesVault")
33
+ const statePath = path.join(vaultPath, ".state.json")
34
+
35
+ // СТАТУС 1: Хранилище не активировано (папки нет)
36
+ try {
37
+ await fs.access(vaultPath)
38
+ } catch {
39
+ setStatus({
40
+ title: "Not Initialized",
41
+ desc: "Run /echoes-init to create the vault.",
42
+ color: theme.error,
43
+ })
44
+ return
45
+ }
46
+
47
+ // Читаем стейт
48
+ let stateData: any = null
49
+ try {
50
+ const raw = await fs.readFile(statePath, "utf-8")
51
+ stateData = JSON.parse(raw)
52
+ } catch {
53
+ // Файл стейта отсутствует (например, только что сделали init, но еще не start)
54
+ }
55
+
56
+ const session = stateData?.session
57
+
58
+ // СТАТУС 4: Данные сохранены в этой сессии
59
+ if (session?.saved) {
60
+ setStatus({
61
+ title: "Saved",
62
+ desc: "Vault remembered the data.",
63
+ color: theme.accent,
64
+ })
65
+ return
66
+ }
67
+
68
+ // СТАТУС 3: Хранилище работает (вызван /echoes-start)
69
+ if (session?.started) {
70
+ setStatus({
71
+ title: "Active",
72
+ desc: "Working. Call /echoes-end before closing to save.",
73
+ color: theme.success,
74
+ })
75
+ return
76
+ }
77
+
78
+ // СТАТУС 2: Необходимо вызвать /echoes-start
79
+ setStatus({
80
+ title: "Idle",
81
+ desc: "Call /echoes-start to load knowledge.",
82
+ color: theme.warning, // Используем warning, так как это призыв к действию
83
+ })
84
+ }
85
+
86
+ // Запускаем проверку сразу
87
+ await checkStatus()
88
+
89
+ // Запускаем поллинг каждые 2 секунды для обновления UI
90
+ const interval = setInterval(checkStatus, 2000)
91
+
92
+ // Чистим интервал при размонтировании компонента
93
+ onCleanup(() => clearInterval(interval))
94
+ })
95
+
96
+ return (
97
+ <box flexDirection="column">
98
+ <text fg={theme.text}>
99
+ <b>EchoesVault</b>
100
+ </text>
101
+
102
+ {/* Отступ для красоты */}
103
+ <text> </text>
104
+
105
+ {/* Вывод реактивного статуса */}
106
+ <text fg={status().color}>
107
+ <b>• {status().title}</b>
108
+ </text>
109
+
110
+ <text fg={theme.textMuted}>
111
+ {status().desc}
112
+ </text>
113
+ </box>
114
+ )
115
+ },
116
+ },
117
+ })
148
118
  }
149
119
 
150
- export default { id: "echoes-vault-sidebar", tui }
120
+ export default { id: "echoes-vault-ui", tui }