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.
- package/index.ts +75 -75
- package/package.json +1 -1
- 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
|
-
|
|
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,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
|
|
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:
|
|
10
|
+
session: {
|
|
11
|
+
started: boolean
|
|
12
|
+
saved: boolean
|
|
13
|
+
lastStart: string | null
|
|
14
|
+
lastSave: string | null
|
|
15
|
+
}
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
const STATE_PATH = path.join(process.cwd(), ".opencode", "echoes-state.json")
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
const readState = (): EchoesState | null => {
|
|
22
21
|
try {
|
|
23
|
-
|
|
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 =
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
</
|
|
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-
|
|
81
|
+
export default { id: "echoes-vault-ui", tui }
|