echoes-vault-opencode 1.2.2 → 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 -97
- package/index.ts +189 -408
- package/package.json +17 -6
- package/prompts/commands/echoes-end.md +9 -14
- 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,479 +2,260 @@ 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 */ }
|
|
8
|
+
const PLUGIN_ROOT = path.dirname(fileURLToPath(import.meta.url))
|
|
123
9
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
st.stats = await collectStats(vaultPaths)
|
|
130
|
-
await writeState(directory, st)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const DEFAULT_INDEX = `# EchoesVault Index
|
|
134
|
-
|
|
135
|
-
Welcome to the EchoesVault knowledge base.
|
|
136
|
-
|
|
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
|
-
}
|
|
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 }
|
|
166
15
|
|
|
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
|
|
|
191
|
-
const
|
|
192
|
-
const pluginDir = path.dirname(new URL(import.meta.url).pathname)
|
|
28
|
+
const displayOutput = (output: string): string => output.trimEnd()
|
|
193
29
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
30
|
+
const statusActions = {
|
|
31
|
+
"echoes-init": "init",
|
|
32
|
+
"echoes-start": "start",
|
|
33
|
+
"echoes-end": "end",
|
|
34
|
+
} as const
|
|
197
35
|
|
|
198
|
-
|
|
199
|
-
const ECHOES_START = await readPromptFile("commands/echoes-start.md")
|
|
200
|
-
const ECHOES_END = await readPromptFile("commands/echoes-end.md")
|
|
201
|
-
const ECHOES_STATUS = await readPromptFile("commands/echoes-status.md")
|
|
36
|
+
type StatusAction = (typeof statusActions)[keyof typeof statusActions]
|
|
202
37
|
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
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")
|
|
206
41
|
|
|
207
42
|
const commands: Record<string, string> = {
|
|
208
|
-
"echoes-init.md":
|
|
209
|
-
"echoes-start.md":
|
|
210
|
-
"echoes-end.md":
|
|
211
|
-
"echoes-status.md":
|
|
212
|
-
}
|
|
213
|
-
const skills: Record<string, string> = {
|
|
214
|
-
"echoes-append-to-daily-log": APPEND_TO_DAILY_LOG,
|
|
215
|
-
"echoes-search-vault-pages": SEARCH_VAULT_PAGES,
|
|
216
|
-
"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"),
|
|
217
47
|
}
|
|
218
48
|
|
|
219
|
-
|
|
220
|
-
const
|
|
49
|
+
// Lifecycle tools are one-shot capabilities granted only by the matching explicit slash command.
|
|
50
|
+
const authorizedStatusActions = new Map<string, StatusAction>()
|
|
51
|
+
const fallbackWorkspace = worktree || directory
|
|
221
52
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
}
|
|
227
59
|
}
|
|
228
60
|
|
|
229
|
-
await ensureCommands(directory, commands)
|
|
230
|
-
await ensureSkills(directory, skills)
|
|
231
|
-
|
|
232
|
-
const state = await readState(directory)
|
|
233
|
-
state.pluginVersion = await getPluginVersion()
|
|
234
|
-
state.session.started = false
|
|
235
|
-
state.session.saved = false
|
|
236
|
-
state.stats = await collectStats(paths)
|
|
237
|
-
await writeState(directory, state)
|
|
238
|
-
|
|
239
61
|
return {
|
|
240
62
|
config: async (input) => {
|
|
241
|
-
input.command
|
|
242
|
-
for (const [name,
|
|
243
|
-
const { template, description, agent } = parseCommandFrontmatter(
|
|
244
|
-
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 }
|
|
245
67
|
}
|
|
246
68
|
},
|
|
69
|
+
|
|
70
|
+
"command.execute.before": async (input) => {
|
|
71
|
+
const command = input.command.replace(/^\//, "") as keyof typeof statusActions
|
|
72
|
+
const action = statusActions[command]
|
|
73
|
+
if (action) authorizedStatusActions.set(input.sessionID, action)
|
|
74
|
+
},
|
|
75
|
+
|
|
247
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
|
+
|
|
248
124
|
commit_memory_to_echoes_vault: tool({
|
|
249
125
|
description:
|
|
250
|
-
"
|
|
126
|
+
"Finalize an EchoesVault session with one daily summary and optional curated pages. Available only after explicit /echoes-end.",
|
|
251
127
|
args: {
|
|
252
128
|
dailySummary: tool.schema
|
|
253
129
|
.string()
|
|
254
|
-
.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
newPages: tool.schema
|
|
130
|
+
.min(1)
|
|
131
|
+
.describe("Dense final outcomes, unresolved blockers, and next steps; never a transcript."),
|
|
132
|
+
pages: tool.schema
|
|
258
133
|
.array(
|
|
259
134
|
tool.schema.object({
|
|
260
|
-
filename: tool.schema
|
|
261
|
-
.string()
|
|
262
|
-
.describe("Filename without .md extension (e.g. 'architecture-decisions')"),
|
|
135
|
+
filename: tool.schema.string().min(1).describe("Safe page filename ending in .md."),
|
|
263
136
|
content: tool.schema
|
|
264
137
|
.string()
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.optional()
|
|
269
|
-
.describe("Array of new knowledge base pages to create in EchoesVault/pages/"),
|
|
270
|
-
indexAppends: tool.schema
|
|
271
|
-
.array(tool.schema.string())
|
|
272
|
-
.optional()
|
|
273
|
-
.describe("Lines to append to the end of EchoesVault/index.md"),
|
|
274
|
-
indexUpdates: tool.schema
|
|
275
|
-
.array(
|
|
276
|
-
tool.schema.object({
|
|
277
|
-
oldLine: tool.schema
|
|
278
|
-
.string()
|
|
279
|
-
.describe("The exact line to find and replace in the index"),
|
|
280
|
-
newLine: tool.schema
|
|
138
|
+
.min(1)
|
|
139
|
+
.describe("Complete page with type, stack, status, and summary frontmatter."),
|
|
140
|
+
expectedSha256: tool.schema
|
|
281
141
|
.string()
|
|
282
|
-
.
|
|
283
|
-
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Fresh current hash, required when replacing an existing page."),
|
|
144
|
+
}),
|
|
284
145
|
)
|
|
285
|
-
.optional()
|
|
286
|
-
.describe("Lines to find and replace in place within EchoesVault/index.md"),
|
|
146
|
+
.optional(),
|
|
287
147
|
},
|
|
288
|
-
async execute(args,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const pageFile = path.join(paths.pages, fileName)
|
|
303
|
-
await fs.writeFile(pageFile, page.content.trim() + "\n")
|
|
304
|
-
pagesCreated++
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const idxFile = path.join(paths.vault, "index.md")
|
|
309
|
-
|
|
310
|
-
let indexContent = ""
|
|
311
|
-
try {
|
|
312
|
-
indexContent = await fs.readFile(idxFile, "utf-8")
|
|
313
|
-
} catch {
|
|
314
|
-
indexContent = DEFAULT_INDEX
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (args.indexUpdates && args.indexUpdates.length > 0) {
|
|
318
|
-
for (const upd of args.indexUpdates) {
|
|
319
|
-
if (indexContent.includes(upd.oldLine)) {
|
|
320
|
-
indexContent = indexContent.replaceAll(upd.oldLine, upd.newLine)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (args.indexAppends && args.indexAppends.length > 0) {
|
|
326
|
-
const toAppend = args.indexAppends.join("\n")
|
|
327
|
-
indexContent = indexContent.trimEnd() + "\n" + toAppend + "\n"
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
await fs.writeFile(idxFile, indexContent)
|
|
331
|
-
|
|
332
|
-
const st = await readState(directory)
|
|
333
|
-
st.session.saved = true
|
|
334
|
-
st.session.lastSave = new Date().toISOString()
|
|
335
|
-
st.stats = await collectStats(paths)
|
|
336
|
-
await writeState(directory, st)
|
|
337
|
-
|
|
338
|
-
return [
|
|
339
|
-
`✅ Memory committed to EchoesVault.`,
|
|
340
|
-
`- Daily log: EchoesVault/daily/${today}.md`,
|
|
341
|
-
`- Pages created: ${pagesCreated}`,
|
|
342
|
-
`- Index: updated`,
|
|
343
|
-
].join("\n")
|
|
148
|
+
async execute(args, ctx) {
|
|
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
|
+
)
|
|
160
|
+
authorizedStatusActions.delete(ctx.sessionID)
|
|
161
|
+
return output
|
|
344
162
|
},
|
|
345
163
|
}),
|
|
164
|
+
|
|
346
165
|
echoes_append_to_daily_log: tool({
|
|
347
166
|
description:
|
|
348
|
-
"
|
|
167
|
+
"Write one intermediate technical milestone to a unique EchoesVault daily entry without ending the session.",
|
|
349
168
|
args: {
|
|
350
169
|
logEntry: tool.schema
|
|
351
170
|
.string()
|
|
352
|
-
.
|
|
353
|
-
|
|
354
|
-
),
|
|
171
|
+
.min(1)
|
|
172
|
+
.describe("Concise Markdown facts, decisions, blockers, or next steps."),
|
|
355
173
|
},
|
|
356
|
-
async execute(args,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
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
|
+
)
|
|
365
182
|
},
|
|
366
183
|
}),
|
|
184
|
+
|
|
367
185
|
echoes_search_vault_pages: tool({
|
|
368
186
|
description:
|
|
369
|
-
"Search
|
|
187
|
+
"Search EchoesVault knowledge pages with a narrow keyword or phrase before reading a relevant page.",
|
|
370
188
|
args: {
|
|
371
|
-
query: tool.schema
|
|
372
|
-
|
|
373
|
-
.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(),
|
|
374
191
|
},
|
|
375
|
-
async execute(args,
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
path.join(paths.pages, file),
|
|
385
|
-
"utf-8"
|
|
386
|
-
)
|
|
387
|
-
const lines = content.split("\n")
|
|
388
|
-
for (let i = 0; i < lines.length; i++) {
|
|
389
|
-
if (
|
|
390
|
-
lines[i].toLowerCase().includes(args.query.toLowerCase())
|
|
391
|
-
) {
|
|
392
|
-
results.push(
|
|
393
|
-
`${file}:${i + 1}: ${lines[i].trim().slice(0, 200)}`
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
} catch {
|
|
399
|
-
return "_No pages found in EchoesVault/pages/_"
|
|
400
|
-
}
|
|
401
|
-
if (results.length === 0) {
|
|
402
|
-
return `No results found for "${args.query}" in EchoesVault/pages/.`
|
|
403
|
-
}
|
|
404
|
-
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
|
+
)
|
|
405
201
|
},
|
|
406
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
|
+
)
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
|
|
407
220
|
echoes_create_or_update_page: tool({
|
|
408
221
|
description:
|
|
409
|
-
"
|
|
222
|
+
"Create a validated EchoesVault page or replace one using its fresh expected SHA-256; the runtime regenerates the index.",
|
|
410
223
|
args: {
|
|
411
|
-
filename: tool.schema
|
|
412
|
-
.string()
|
|
413
|
-
.describe("Exact filename without paths (e.g. 'auth-architecture.md')."),
|
|
224
|
+
filename: tool.schema.string().min(1).describe("Exact page filename without paths."),
|
|
414
225
|
content: tool.schema
|
|
415
226
|
.string()
|
|
416
|
-
.
|
|
417
|
-
|
|
227
|
+
.min(1)
|
|
228
|
+
.describe("Complete Markdown page with all required frontmatter."),
|
|
229
|
+
expectedSha256: tool.schema
|
|
418
230
|
.string()
|
|
419
231
|
.optional()
|
|
420
|
-
.describe("
|
|
421
|
-
},
|
|
422
|
-
async execute(args, _ctx) {
|
|
423
|
-
await ensureVaultDirs(paths)
|
|
424
|
-
const fileName = toPageFilename(args.filename)
|
|
425
|
-
const pageFile = path.join(paths.pages, fileName)
|
|
426
|
-
|
|
427
|
-
const existed = await fs.access(pageFile).then(() => true).catch(() => false)
|
|
428
|
-
await fs.writeFile(pageFile, args.content.trim() + "\n")
|
|
429
|
-
|
|
430
|
-
if (!existed && args.indexDescription) {
|
|
431
|
-
const idxFile = path.join(paths.vault, "index.md")
|
|
432
|
-
let indexContent = ""
|
|
433
|
-
try {
|
|
434
|
-
indexContent = await fs.readFile(idxFile, "utf-8")
|
|
435
|
-
} catch {
|
|
436
|
-
indexContent = DEFAULT_INDEX
|
|
437
|
-
}
|
|
438
|
-
const link = `[[${toPageSlug(fileName)}]]`
|
|
439
|
-
if (!indexContent.includes(link)) {
|
|
440
|
-
indexContent = indexContent.trimEnd() + "\n" + args.indexDescription + "\n"
|
|
441
|
-
await fs.writeFile(idxFile, indexContent)
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
await updateStats(directory, paths)
|
|
446
|
-
|
|
447
|
-
const action = existed ? "updated" : "created"
|
|
448
|
-
const parts = [`✅ Page ${action}: EchoesVault/pages/${fileName}`]
|
|
449
|
-
if (!existed && args.indexDescription) {
|
|
450
|
-
parts.push(`📑 Index: synced`)
|
|
451
|
-
}
|
|
452
|
-
return parts.join("\n")
|
|
232
|
+
.describe("Fresh current hash, required when replacing an existing page."),
|
|
453
233
|
},
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
)
|
|
465
246
|
},
|
|
466
247
|
}),
|
|
467
|
-
|
|
248
|
+
|
|
249
|
+
echoes_hydrate_vault: tool({
|
|
468
250
|
description:
|
|
469
|
-
"
|
|
251
|
+
"Rebuild only ignored local EchoesVault index/state files for an already initialized checkout.",
|
|
470
252
|
args: {},
|
|
471
|
-
async execute(_args,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return "EchoesVault session started."
|
|
253
|
+
async execute(_args, ctx) {
|
|
254
|
+
return displayOutput(
|
|
255
|
+
await runEchoes(ctx.worktree || ctx.directory || fallbackWorkspace, "hydrate", {
|
|
256
|
+
signal: ctx.abort,
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
478
259
|
},
|
|
479
260
|
}),
|
|
480
261
|
},
|