echoes-vault-opencode 1.2.3 → 2.0.0
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/EchoesProtocol.md +1097 -0
- package/README.md +150 -99
- package/index.ts +176 -429
- package/package.json +17 -6
- package/prompts/commands/echoes-end.md +9 -16
- package/prompts/commands/echoes-init.md +7 -31
- package/prompts/commands/echoes-start.md +6 -25
- package/prompts/commands/echoes-status.md +4 -32
- package/runtime.ts +157 -0
- package/scripts/echoes_vault.py +2454 -0
- package/tui.tsx +75 -75
- package/prompts/skills/echoes-append-to-daily-log.md +0 -22
- package/prompts/skills/echoes-create-or-update-page.md +0 -22
- package/prompts/skills/echoes-search-vault-pages.md +0 -19
package/index.ts
CHANGED
|
@@ -2,192 +2,31 @@ import type { Plugin } from "@opencode-ai/plugin"
|
|
|
2
2
|
import { tool } from "@opencode-ai/plugin"
|
|
3
3
|
import * as fs from "node:fs/promises"
|
|
4
4
|
import * as path from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
import { runEchoes } from "./runtime.ts"
|
|
5
7
|
|
|
6
|
-
const
|
|
7
|
-
const d = new Date()
|
|
8
|
-
const year = d.getFullYear()
|
|
9
|
-
const month = String(d.getMonth() + 1).padStart(2, "0")
|
|
10
|
-
const day = String(d.getDate()).padStart(2, "0")
|
|
11
|
-
return `${year}-${month}-${day}`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type VaultPaths = {
|
|
15
|
-
vault: string
|
|
16
|
-
raw: string
|
|
17
|
-
pages: string
|
|
18
|
-
daily: string
|
|
19
|
-
assets: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type VaultStats = {
|
|
23
|
-
totalPages: number
|
|
24
|
-
totalDailyLogs: number
|
|
25
|
-
deprecatedPages: number
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type EchoesState = {
|
|
29
|
-
version: number
|
|
30
|
-
pluginVersion: string
|
|
31
|
-
initialized: boolean
|
|
32
|
-
session: {
|
|
33
|
-
started: boolean
|
|
34
|
-
saved: boolean
|
|
35
|
-
lastStart: string | null
|
|
36
|
-
lastSave: string | null
|
|
37
|
-
}
|
|
38
|
-
stats: VaultStats
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const STATE_FILENAME = ".opencode/echoes-state.json"
|
|
42
|
-
|
|
43
|
-
const getPluginVersion = async (): Promise<string> => {
|
|
44
|
-
try {
|
|
45
|
-
const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "package.json")
|
|
46
|
-
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"))
|
|
47
|
-
return pkg.version || "0.0.0"
|
|
48
|
-
} catch {
|
|
49
|
-
return "0.0.0"
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const defaultState = (): EchoesState => ({
|
|
54
|
-
version: 1,
|
|
55
|
-
pluginVersion: "0.0.0",
|
|
56
|
-
initialized: false,
|
|
57
|
-
session: {
|
|
58
|
-
started: false,
|
|
59
|
-
saved: false,
|
|
60
|
-
lastStart: null,
|
|
61
|
-
lastSave: null,
|
|
62
|
-
},
|
|
63
|
-
stats: {
|
|
64
|
-
totalPages: 0,
|
|
65
|
-
totalDailyLogs: 0,
|
|
66
|
-
deprecatedPages: 0,
|
|
67
|
-
},
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const readState = async (directory: string): Promise<EchoesState> => {
|
|
71
|
-
try {
|
|
72
|
-
const raw = await fs.readFile(path.join(directory, STATE_FILENAME), "utf-8")
|
|
73
|
-
return JSON.parse(raw) as EchoesState
|
|
74
|
-
} catch {
|
|
75
|
-
return defaultState()
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const writeState = async (directory: string, state: EchoesState): Promise<void> => {
|
|
80
|
-
const filePath = path.join(directory, STATE_FILENAME)
|
|
81
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
82
|
-
await fs.writeFile(filePath, JSON.stringify(state, null, 2))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const resolveVaultPaths = (directory: string): VaultPaths => {
|
|
86
|
-
const vault = path.join(directory, "EchoesVault")
|
|
87
|
-
return {
|
|
88
|
-
vault,
|
|
89
|
-
raw: path.join(vault, "raw"),
|
|
90
|
-
pages: path.join(vault, "pages"),
|
|
91
|
-
daily: path.join(vault, "daily"),
|
|
92
|
-
assets: path.join(vault, "assets"),
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const ensureVaultDirs = async (paths: VaultPaths): Promise<void> => {
|
|
97
|
-
await fs.mkdir(paths.raw, { recursive: true })
|
|
98
|
-
await fs.mkdir(paths.pages, { recursive: true })
|
|
99
|
-
await fs.mkdir(paths.daily, { recursive: true })
|
|
100
|
-
await fs.mkdir(paths.assets, { recursive: true })
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const collectStats = async (vaultPaths: VaultPaths): Promise<VaultStats> => {
|
|
104
|
-
let totalPages = 0
|
|
105
|
-
let totalDailyLogs = 0
|
|
106
|
-
let deprecatedPages = 0
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const pageFiles = (await fs.readdir(vaultPaths.pages)).filter((f) => f.endsWith(".md"))
|
|
110
|
-
totalPages = pageFiles.length
|
|
111
|
-
for (const file of pageFiles) {
|
|
112
|
-
const content = await fs.readFile(path.join(vaultPaths.pages, file), "utf-8")
|
|
113
|
-
if (content.includes("DEPRECATED")) {
|
|
114
|
-
deprecatedPages++
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} catch { /* pages dir may not exist yet */ }
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const dailyFiles = (await fs.readdir(vaultPaths.daily)).filter((f) => f.endsWith(".md"))
|
|
121
|
-
totalDailyLogs = dailyFiles.length
|
|
122
|
-
} catch { /* daily dir may not exist yet */ }
|
|
123
|
-
|
|
124
|
-
return { totalPages, totalDailyLogs, deprecatedPages }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const updateStats = async (directory: string, vaultPaths: VaultPaths): Promise<void> => {
|
|
128
|
-
const st = await readState(directory)
|
|
129
|
-
st.stats = await collectStats(vaultPaths)
|
|
130
|
-
await writeState(directory, st)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const DEFAULT_INDEX = `# EchoesVault Index
|
|
8
|
+
const PLUGIN_ROOT = path.dirname(fileURLToPath(import.meta.url))
|
|
134
9
|
|
|
135
|
-
|
|
10
|
+
const parseCommandFrontmatter = (
|
|
11
|
+
command: string,
|
|
12
|
+
): { template: string; description?: string; agent?: string } => {
|
|
13
|
+
const match = command.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
14
|
+
if (!match) return { template: command }
|
|
136
15
|
|
|
137
|
-
This index tracks all structured pages in the vault.
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
const sanitizeFilename = (name: string): string => {
|
|
141
|
-
const cleaned = name.replace(/\.\./g, "").replace(/[\/\\]/g, "")
|
|
142
|
-
return cleaned || "untitled"
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Normalizes any user/LLM-supplied page name to a safe `*.md` filename,
|
|
146
|
-
// ensuring we never produce duplicates like `foo.md.md`.
|
|
147
|
-
const toPageFilename = (name: string): string => {
|
|
148
|
-
const safe = sanitizeFilename(name)
|
|
149
|
-
return safe.endsWith(".md") ? safe : `${safe}.md`
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const toPageSlug = (filename: string): string => filename.replace(/\.md$/, "")
|
|
153
|
-
|
|
154
|
-
const ensureCommands = async (directory: string, commands: Record<string, string>): Promise<void> => {
|
|
155
|
-
const cmdDir = path.join(directory, ".opencode", "commands")
|
|
156
|
-
await fs.mkdir(cmdDir, { recursive: true })
|
|
157
|
-
for (const [name, content] of Object.entries(commands)) {
|
|
158
|
-
const cmdFile = path.join(cmdDir, name)
|
|
159
|
-
try {
|
|
160
|
-
await fs.access(cmdFile)
|
|
161
|
-
} catch {
|
|
162
|
-
await fs.writeFile(cmdFile, content)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const ensureSkills = async (directory: string, skills: Record<string, string>): Promise<void> => {
|
|
168
|
-
for (const [name, content] of Object.entries(skills)) {
|
|
169
|
-
const skillDir = path.join(directory, ".opencode", "skills", name)
|
|
170
|
-
const skillFile = path.join(skillDir, "SKILL.md")
|
|
171
|
-
await fs.mkdir(skillDir, { recursive: true })
|
|
172
|
-
try {
|
|
173
|
-
await fs.access(skillFile)
|
|
174
|
-
} catch {
|
|
175
|
-
await fs.writeFile(skillFile, content)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const parseCommandFrontmatter = (cmd: string): { template: string; description?: string; agent?: string } => {
|
|
181
|
-
const match = cmd.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
|
182
|
-
if (!match) return { template: cmd }
|
|
183
16
|
const frontmatter: Record<string, string> = {}
|
|
184
17
|
for (const line of match[1].split("\n")) {
|
|
185
18
|
const [key, ...rest] = line.split(":")
|
|
186
19
|
if (key && rest.length) frontmatter[key.trim()] = rest.join(":").trim()
|
|
187
20
|
}
|
|
188
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
template: match[2],
|
|
23
|
+
description: frontmatter.description,
|
|
24
|
+
agent: frontmatter.agent,
|
|
25
|
+
}
|
|
189
26
|
}
|
|
190
27
|
|
|
28
|
+
const displayOutput = (output: string): string => output.trimEnd()
|
|
29
|
+
|
|
191
30
|
const statusActions = {
|
|
192
31
|
"echoes-init": "init",
|
|
193
32
|
"echoes-start": "start",
|
|
@@ -196,319 +35,227 @@ const statusActions = {
|
|
|
196
35
|
|
|
197
36
|
type StatusAction = (typeof statusActions)[keyof typeof statusActions]
|
|
198
37
|
|
|
199
|
-
const OpenCodeEchoes: Plugin = async ({ directory }) => {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
const readPromptFile = async (relativePath: string): Promise<string> => {
|
|
203
|
-
return await fs.readFile(path.join(pluginDir, "prompts", relativePath), "utf-8")
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const ECHOES_INIT = await readPromptFile("commands/echoes-init.md")
|
|
207
|
-
const ECHOES_START = await readPromptFile("commands/echoes-start.md")
|
|
208
|
-
const ECHOES_END = await readPromptFile("commands/echoes-end.md")
|
|
209
|
-
const ECHOES_STATUS = await readPromptFile("commands/echoes-status.md")
|
|
210
|
-
|
|
211
|
-
const APPEND_TO_DAILY_LOG = await readPromptFile("skills/echoes-append-to-daily-log.md")
|
|
212
|
-
const SEARCH_VAULT_PAGES = await readPromptFile("skills/echoes-search-vault-pages.md")
|
|
213
|
-
const CREATE_OR_UPDATE_PAGE = await readPromptFile("skills/echoes-create-or-update-page.md")
|
|
38
|
+
const OpenCodeEchoes: Plugin = async ({ directory, worktree }) => {
|
|
39
|
+
const readPromptFile = async (relativePath: string): Promise<string> =>
|
|
40
|
+
await fs.readFile(path.join(PLUGIN_ROOT, "prompts", relativePath), "utf-8")
|
|
214
41
|
|
|
215
42
|
const commands: Record<string, string> = {
|
|
216
|
-
"echoes-init.md":
|
|
217
|
-
"echoes-start.md":
|
|
218
|
-
"echoes-end.md":
|
|
219
|
-
"echoes-status.md":
|
|
220
|
-
}
|
|
221
|
-
const skills: Record<string, string> = {
|
|
222
|
-
"echoes-append-to-daily-log": APPEND_TO_DAILY_LOG,
|
|
223
|
-
"echoes-search-vault-pages": SEARCH_VAULT_PAGES,
|
|
224
|
-
"echoes-create-or-update-page": CREATE_OR_UPDATE_PAGE,
|
|
43
|
+
"echoes-init.md": await readPromptFile("commands/echoes-init.md"),
|
|
44
|
+
"echoes-start.md": await readPromptFile("commands/echoes-start.md"),
|
|
45
|
+
"echoes-end.md": await readPromptFile("commands/echoes-end.md"),
|
|
46
|
+
"echoes-status.md": await readPromptFile("commands/echoes-status.md"),
|
|
225
47
|
}
|
|
226
48
|
|
|
227
|
-
|
|
228
|
-
const indexFile = path.join(paths.vault, "index.md")
|
|
229
|
-
|
|
230
|
-
await ensureVaultDirs(paths)
|
|
231
|
-
try {
|
|
232
|
-
await fs.access(indexFile)
|
|
233
|
-
} catch {
|
|
234
|
-
await fs.writeFile(indexFile, DEFAULT_INDEX)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
await ensureCommands(directory, commands)
|
|
238
|
-
await ensureSkills(directory, skills)
|
|
239
|
-
|
|
240
|
-
// Sidebar status can change only through the matching user-invoked command.
|
|
49
|
+
// Lifecycle tools are one-shot capabilities granted only by the matching explicit slash command.
|
|
241
50
|
const authorizedStatusActions = new Map<string, StatusAction>()
|
|
51
|
+
const fallbackWorkspace = worktree || directory
|
|
242
52
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
53
|
+
const requireAuthorization = (sessionID: string, expected: StatusAction): void => {
|
|
54
|
+
if (authorizedStatusActions.get(sessionID) !== expected) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`EchoesVault ${expected} requires the explicit /echoes-${expected} command from the user.`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
247
60
|
|
|
248
61
|
return {
|
|
249
62
|
config: async (input) => {
|
|
250
|
-
input.command
|
|
251
|
-
for (const [name,
|
|
252
|
-
const { template, description, agent } = parseCommandFrontmatter(
|
|
253
|
-
input.command[name.replace(
|
|
63
|
+
input.command ||= {}
|
|
64
|
+
for (const [name, command] of Object.entries(commands)) {
|
|
65
|
+
const { template, description, agent } = parseCommandFrontmatter(command)
|
|
66
|
+
input.command[name.replace(/\.md$/, "")] = { template, description, agent }
|
|
254
67
|
}
|
|
255
68
|
},
|
|
69
|
+
|
|
256
70
|
"command.execute.before": async (input) => {
|
|
257
71
|
const command = input.command.replace(/^\//, "") as keyof typeof statusActions
|
|
258
72
|
const action = statusActions[command]
|
|
259
|
-
if (action)
|
|
260
|
-
authorizedStatusActions.set(input.sessionID, action)
|
|
261
|
-
}
|
|
73
|
+
if (action) authorizedStatusActions.set(input.sessionID, action)
|
|
262
74
|
},
|
|
75
|
+
|
|
263
76
|
tool: {
|
|
77
|
+
echoes_activate_vault: tool({
|
|
78
|
+
description:
|
|
79
|
+
"Initialize or explicitly migrate EchoesVault through the shared portable runtime. Available only after /echoes-init.",
|
|
80
|
+
args: {},
|
|
81
|
+
async execute(_args, ctx) {
|
|
82
|
+
requireAuthorization(ctx.sessionID, "init")
|
|
83
|
+
const output = displayOutput(
|
|
84
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "init", {
|
|
85
|
+
signal: ctx.abort,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
authorizedStatusActions.delete(ctx.sessionID)
|
|
89
|
+
return output
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
echoes_start_session: tool({
|
|
94
|
+
description:
|
|
95
|
+
"Restore the generated index and three most recent EchoesVault session entries. Available only after /echoes-start.",
|
|
96
|
+
args: {},
|
|
97
|
+
async execute(_args, ctx) {
|
|
98
|
+
requireAuthorization(ctx.sessionID, "start")
|
|
99
|
+
const output = displayOutput(
|
|
100
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "start", {
|
|
101
|
+
args: ["--recent", "3"],
|
|
102
|
+
signal: ctx.abort,
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
authorizedStatusActions.delete(ctx.sessionID)
|
|
106
|
+
return output
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
echoes_vault_status: tool({
|
|
111
|
+
description:
|
|
112
|
+
"Inspect EchoesVault protocol, storage, metadata, index, Git readiness, conflicts, and scale without modifying files.",
|
|
113
|
+
args: {},
|
|
114
|
+
async execute(_args, ctx) {
|
|
115
|
+
return displayOutput(
|
|
116
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "status", {
|
|
117
|
+
args: ["--format", "card"],
|
|
118
|
+
signal: ctx.abort,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
|
|
264
124
|
commit_memory_to_echoes_vault: tool({
|
|
265
125
|
description:
|
|
266
|
-
"
|
|
126
|
+
"Finalize an EchoesVault session with one daily summary and optional curated pages. Available only after explicit /echoes-end.",
|
|
267
127
|
args: {
|
|
268
128
|
dailySummary: tool.schema
|
|
269
129
|
.string()
|
|
270
|
-
.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
newPages: tool.schema
|
|
130
|
+
.min(1)
|
|
131
|
+
.describe("Dense final outcomes, unresolved blockers, and next steps; never a transcript."),
|
|
132
|
+
pages: tool.schema
|
|
274
133
|
.array(
|
|
275
134
|
tool.schema.object({
|
|
276
|
-
filename: tool.schema
|
|
277
|
-
.string()
|
|
278
|
-
.describe("Filename without .md extension (e.g. 'architecture-decisions')"),
|
|
135
|
+
filename: tool.schema.string().min(1).describe("Safe page filename ending in .md."),
|
|
279
136
|
content: tool.schema
|
|
280
137
|
.string()
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.optional()
|
|
285
|
-
.describe("Array of new knowledge base pages to create in EchoesVault/pages/"),
|
|
286
|
-
indexAppends: tool.schema
|
|
287
|
-
.array(tool.schema.string())
|
|
288
|
-
.optional()
|
|
289
|
-
.describe("Lines to append to the end of EchoesVault/index.md"),
|
|
290
|
-
indexUpdates: tool.schema
|
|
291
|
-
.array(
|
|
292
|
-
tool.schema.object({
|
|
293
|
-
oldLine: tool.schema
|
|
294
|
-
.string()
|
|
295
|
-
.describe("The exact line to find and replace in the index"),
|
|
296
|
-
newLine: tool.schema
|
|
138
|
+
.min(1)
|
|
139
|
+
.describe("Complete page with type, stack, status, and summary frontmatter."),
|
|
140
|
+
expectedSha256: tool.schema
|
|
297
141
|
.string()
|
|
298
|
-
.
|
|
299
|
-
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Fresh current hash, required when replacing an existing page."),
|
|
144
|
+
}),
|
|
300
145
|
)
|
|
301
|
-
.optional()
|
|
302
|
-
.describe("Lines to find and replace in place within EchoesVault/index.md"),
|
|
146
|
+
.optional(),
|
|
303
147
|
},
|
|
304
148
|
async execute(args, ctx) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const dailyFile = path.join(paths.daily, `${today}.md`)
|
|
317
|
-
const timestamp = new Date().toISOString()
|
|
318
|
-
const header = `## Session — ${timestamp}\n\n`
|
|
319
|
-
await fs.appendFile(dailyFile, header + args.dailySummary + "\n\n")
|
|
320
|
-
|
|
321
|
-
let pagesCreated = 0
|
|
322
|
-
if (args.newPages && args.newPages.length > 0) {
|
|
323
|
-
for (const page of args.newPages) {
|
|
324
|
-
const fileName = toPageFilename(page.filename)
|
|
325
|
-
const pageFile = path.join(paths.pages, fileName)
|
|
326
|
-
await fs.writeFile(pageFile, page.content.trim() + "\n")
|
|
327
|
-
pagesCreated++
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const idxFile = path.join(paths.vault, "index.md")
|
|
332
|
-
|
|
333
|
-
let indexContent = ""
|
|
334
|
-
try {
|
|
335
|
-
indexContent = await fs.readFile(idxFile, "utf-8")
|
|
336
|
-
} catch {
|
|
337
|
-
indexContent = DEFAULT_INDEX
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (args.indexUpdates && args.indexUpdates.length > 0) {
|
|
341
|
-
for (const upd of args.indexUpdates) {
|
|
342
|
-
if (indexContent.includes(upd.oldLine)) {
|
|
343
|
-
indexContent = indexContent.replaceAll(upd.oldLine, upd.newLine)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (args.indexAppends && args.indexAppends.length > 0) {
|
|
349
|
-
const toAppend = args.indexAppends.join("\n")
|
|
350
|
-
indexContent = indexContent.trimEnd() + "\n" + toAppend + "\n"
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
await fs.writeFile(idxFile, indexContent)
|
|
354
|
-
|
|
355
|
-
const st = await readState(directory)
|
|
356
|
-
st.session.saved = true
|
|
357
|
-
st.session.lastSave = new Date().toISOString()
|
|
358
|
-
st.stats = await collectStats(paths)
|
|
359
|
-
await writeState(directory, st)
|
|
149
|
+
requireAuthorization(ctx.sessionID, "end")
|
|
150
|
+
const output = displayOutput(
|
|
151
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "end", {
|
|
152
|
+
args: ["--confirm-explicit-user-end", "--payload", "-"],
|
|
153
|
+
payload: {
|
|
154
|
+
dailySummary: args.dailySummary,
|
|
155
|
+
pages: args.pages ?? [],
|
|
156
|
+
},
|
|
157
|
+
signal: ctx.abort,
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
360
160
|
authorizedStatusActions.delete(ctx.sessionID)
|
|
361
|
-
|
|
362
|
-
return [
|
|
363
|
-
`✅ Memory committed to EchoesVault.`,
|
|
364
|
-
`- Daily log: EchoesVault/daily/${today}.md`,
|
|
365
|
-
`- Pages created: ${pagesCreated}`,
|
|
366
|
-
`- Index: updated`,
|
|
367
|
-
].join("\n")
|
|
161
|
+
return output
|
|
368
162
|
},
|
|
369
163
|
}),
|
|
164
|
+
|
|
370
165
|
echoes_append_to_daily_log: tool({
|
|
371
166
|
description:
|
|
372
|
-
"
|
|
167
|
+
"Write one intermediate technical milestone to a unique EchoesVault daily entry without ending the session.",
|
|
373
168
|
args: {
|
|
374
169
|
logEntry: tool.schema
|
|
375
170
|
.string()
|
|
376
|
-
.
|
|
377
|
-
|
|
378
|
-
),
|
|
171
|
+
.min(1)
|
|
172
|
+
.describe("Concise Markdown facts, decisions, blockers, or next steps."),
|
|
379
173
|
},
|
|
380
|
-
async execute(args,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return `✅ Scratchpad note saved to EchoesVault/daily/${today}.md`
|
|
174
|
+
async execute(args, ctx) {
|
|
175
|
+
return displayOutput(
|
|
176
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "append", {
|
|
177
|
+
args: ["--payload", "-"],
|
|
178
|
+
payload: { entry: args.logEntry },
|
|
179
|
+
signal: ctx.abort,
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
389
182
|
},
|
|
390
183
|
}),
|
|
184
|
+
|
|
391
185
|
echoes_search_vault_pages: tool({
|
|
392
186
|
description:
|
|
393
|
-
"Search
|
|
187
|
+
"Search EchoesVault knowledge pages with a narrow keyword or phrase before reading a relevant page.",
|
|
394
188
|
args: {
|
|
395
|
-
query: tool.schema
|
|
396
|
-
|
|
397
|
-
.describe("Specific keyword or short phrase to search for across the pages/ directory."),
|
|
189
|
+
query: tool.schema.string().min(1).describe("Specific keyword or short phrase."),
|
|
190
|
+
limit: tool.schema.number().int().min(1).max(500).optional(),
|
|
398
191
|
},
|
|
399
|
-
async execute(args,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
if (results.length === 0) {
|
|
426
|
-
return `No results found for "${args.query}" in EchoesVault/pages/.`
|
|
427
|
-
}
|
|
428
|
-
return results.join("\n")
|
|
192
|
+
async execute(args, ctx) {
|
|
193
|
+
const runtimeArgs = [args.query]
|
|
194
|
+
if (args.limit !== undefined) runtimeArgs.push("--limit", String(args.limit))
|
|
195
|
+
return displayOutput(
|
|
196
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "search", {
|
|
197
|
+
args: runtimeArgs,
|
|
198
|
+
signal: ctx.abort,
|
|
199
|
+
}),
|
|
200
|
+
)
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
|
|
204
|
+
echoes_hash_vault_page: tool({
|
|
205
|
+
description:
|
|
206
|
+
"Calculate the current SHA-256 of an EchoesVault page before replacing that existing page.",
|
|
207
|
+
args: {
|
|
208
|
+
filename: tool.schema.string().min(1).describe("Existing page filename."),
|
|
209
|
+
},
|
|
210
|
+
async execute(args, ctx) {
|
|
211
|
+
return displayOutput(
|
|
212
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "hash", {
|
|
213
|
+
args: [args.filename],
|
|
214
|
+
signal: ctx.abort,
|
|
215
|
+
}),
|
|
216
|
+
)
|
|
429
217
|
},
|
|
430
218
|
}),
|
|
219
|
+
|
|
431
220
|
echoes_create_or_update_page: tool({
|
|
432
221
|
description:
|
|
433
|
-
"
|
|
222
|
+
"Create a validated EchoesVault page or replace one using its fresh expected SHA-256; the runtime regenerates the index.",
|
|
434
223
|
args: {
|
|
435
|
-
filename: tool.schema
|
|
436
|
-
.string()
|
|
437
|
-
.describe("Exact filename without paths (e.g. 'auth-architecture.md')."),
|
|
224
|
+
filename: tool.schema.string().min(1).describe("Exact page filename without paths."),
|
|
438
225
|
content: tool.schema
|
|
439
226
|
.string()
|
|
440
|
-
.
|
|
441
|
-
|
|
227
|
+
.min(1)
|
|
228
|
+
.describe("Complete Markdown page with all required frontmatter."),
|
|
229
|
+
expectedSha256: tool.schema
|
|
442
230
|
.string()
|
|
443
231
|
.optional()
|
|
444
|
-
.describe("
|
|
232
|
+
.describe("Fresh current hash, required when replacing an existing page."),
|
|
445
233
|
},
|
|
446
|
-
async execute(args,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
indexContent = await fs.readFile(idxFile, "utf-8")
|
|
459
|
-
} catch {
|
|
460
|
-
indexContent = DEFAULT_INDEX
|
|
461
|
-
}
|
|
462
|
-
const link = `[[${toPageSlug(fileName)}]]`
|
|
463
|
-
if (!indexContent.includes(link)) {
|
|
464
|
-
indexContent = indexContent.trimEnd() + "\n" + args.indexDescription + "\n"
|
|
465
|
-
await fs.writeFile(idxFile, indexContent)
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
await updateStats(directory, paths)
|
|
470
|
-
|
|
471
|
-
const action = existed ? "updated" : "created"
|
|
472
|
-
const parts = [`✅ Page ${action}: EchoesVault/pages/${fileName}`]
|
|
473
|
-
if (!existed && args.indexDescription) {
|
|
474
|
-
parts.push(`📑 Index: synced`)
|
|
475
|
-
}
|
|
476
|
-
return parts.join("\n")
|
|
234
|
+
async execute(args, ctx) {
|
|
235
|
+
return displayOutput(
|
|
236
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "upsert", {
|
|
237
|
+
args: ["--payload", "-"],
|
|
238
|
+
payload: {
|
|
239
|
+
filename: args.filename,
|
|
240
|
+
content: args.content,
|
|
241
|
+
...(args.expectedSha256 ? { expectedSha256: args.expectedSha256 } : {}),
|
|
242
|
+
},
|
|
243
|
+
signal: ctx.abort,
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
477
246
|
},
|
|
478
247
|
}),
|
|
479
|
-
echoes_activate_vault: tool({
|
|
480
|
-
description:
|
|
481
|
-
"Mark the EchoesVault as activated. This succeeds only after the user explicitly runs /echoes-init.",
|
|
482
|
-
args: {},
|
|
483
|
-
async execute(_args, ctx) {
|
|
484
|
-
if (authorizedStatusActions.get(ctx.sessionID) !== "init") {
|
|
485
|
-
return "Vault status was not changed. Only an explicit user /echoes-init command can activate EchoesVault."
|
|
486
|
-
}
|
|
487
248
|
|
|
488
|
-
|
|
489
|
-
st.initialized = true
|
|
490
|
-
st.stats = await collectStats(paths)
|
|
491
|
-
await writeState(directory, st)
|
|
492
|
-
authorizedStatusActions.delete(ctx.sessionID)
|
|
493
|
-
return "EchoesVault activated."
|
|
494
|
-
},
|
|
495
|
-
}),
|
|
496
|
-
echoes_start_session: tool({
|
|
249
|
+
echoes_hydrate_vault: tool({
|
|
497
250
|
description:
|
|
498
|
-
"
|
|
251
|
+
"Rebuild only ignored local EchoesVault index/state files for an already initialized checkout.",
|
|
499
252
|
args: {},
|
|
500
253
|
async execute(_args, ctx) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
st.session.started = true
|
|
507
|
-
st.session.saved = false
|
|
508
|
-
st.session.lastStart = new Date().toISOString()
|
|
509
|
-
await writeState(directory, st)
|
|
510
|
-
authorizedStatusActions.delete(ctx.sessionID)
|
|
511
|
-
return "EchoesVault session started."
|
|
254
|
+
return displayOutput(
|
|
255
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "hydrate", {
|
|
256
|
+
signal: ctx.abort,
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
512
259
|
},
|
|
513
260
|
}),
|
|
514
261
|
},
|