echoes-vault-opencode 1.0.35 → 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.
- package/index.ts +75 -75
- package/package.json +1 -1
- package/tui.tsx +71 -110
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
|
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.
|
|
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,120 +1,81 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPlugin } from "@opencode-ai/plugin/tui"
|
|
3
|
-
import { createSignal,
|
|
4
|
-
import * as fs from "node:fs
|
|
3
|
+
import { createSignal, onCleanup } from "solid-js"
|
|
4
|
+
import * as fs from "node:fs"
|
|
5
5
|
import * as path from "node:path"
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
type EchoesState = {
|
|
8
|
+
version: number
|
|
9
|
+
initialized: boolean
|
|
10
|
+
session: {
|
|
11
|
+
started: boolean
|
|
12
|
+
saved: boolean
|
|
13
|
+
lastStart: string | null
|
|
14
|
+
lastSave: string | null
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
const
|
|
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
|
-
}
|
|
18
|
+
const STATE_PATH = path.join(process.cwd(), ".opencode", "echoes-state.json")
|
|
77
19
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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>
|
|
20
|
+
const readState = (): EchoesState | null => {
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(STATE_PATH, "utf-8")
|
|
23
|
+
return JSON.parse(raw) as EchoesState
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
109
28
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
29
|
+
const tui: TuiPlugin = async (api) => {
|
|
30
|
+
const theme = api.theme.current
|
|
31
|
+
|
|
32
|
+
api.slots.register({
|
|
33
|
+
slots: {
|
|
34
|
+
sidebar_content() {
|
|
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
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<box flexDirection="column">
|
|
69
|
+
<text fg={theme.text}>
|
|
70
|
+
<b>EchoesVault</b>
|
|
71
|
+
</text>
|
|
72
|
+
<text fg={statusColor()}>{statusLabel()}</text>
|
|
73
|
+
<text fg={theme.textMuted}>{statusDesc()}</text>
|
|
74
|
+
</box>
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
118
79
|
}
|
|
119
80
|
|
|
120
|
-
export default { id: "echoes-vault-ui", tui }
|
|
81
|
+
export default { id: "echoes-vault-ui", tui }
|