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.
- package/index.ts +76 -0
- package/package.json +1 -1
- 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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
86
|
+
const state = readVaultState(dir)
|
|
87
|
+
const status = getStatus(state)
|
|
97
88
|
const meta = STATUS_META[status]
|
|
98
|
-
const
|
|
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>
|