echoes-vault-opencode 1.2.3 → 2.0.1
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 +178 -430
- 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 +176 -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 { resolveEchoesWorkspace, 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
|
|
134
|
-
|
|
135
|
-
Welcome to the EchoesVault knowledge base.
|
|
8
|
+
const PLUGIN_ROOT = path.dirname(fileURLToPath(import.meta.url))
|
|
136
9
|
|
|
137
|
-
|
|
138
|
-
|
|
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 }
|
|
139
15
|
|
|
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,228 @@ 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":
|
|
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"),
|
|
220
47
|
}
|
|
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,
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const paths = resolveVaultPaths(directory)
|
|
228
|
-
const indexFile = path.join(paths.vault, "index.md")
|
|
229
48
|
|
|
230
|
-
|
|
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>()
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
51
|
+
const workspaceFor = (contextDirectory?: string, contextWorktree?: string): string =>
|
|
52
|
+
resolveEchoesWorkspace(contextDirectory, contextWorktree, directory, worktree)
|
|
53
|
+
|
|
54
|
+
const requireAuthorization = (sessionID: string, expected: StatusAction): void => {
|
|
55
|
+
if (authorizedStatusActions.get(sessionID) !== expected) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`EchoesVault ${expected} requires the explicit /echoes-${expected} command from the user.`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
247
61
|
|
|
248
62
|
return {
|
|
249
63
|
config: async (input) => {
|
|
250
|
-
input.command
|
|
251
|
-
for (const [name,
|
|
252
|
-
const { template, description, agent } = parseCommandFrontmatter(
|
|
253
|
-
input.command[name.replace(
|
|
64
|
+
input.command ||= {}
|
|
65
|
+
for (const [name, command] of Object.entries(commands)) {
|
|
66
|
+
const { template, description, agent } = parseCommandFrontmatter(command)
|
|
67
|
+
input.command[name.replace(/\.md$/, "")] = { template, description, agent }
|
|
254
68
|
}
|
|
255
69
|
},
|
|
70
|
+
|
|
256
71
|
"command.execute.before": async (input) => {
|
|
257
72
|
const command = input.command.replace(/^\//, "") as keyof typeof statusActions
|
|
258
73
|
const action = statusActions[command]
|
|
259
|
-
if (action)
|
|
260
|
-
authorizedStatusActions.set(input.sessionID, action)
|
|
261
|
-
}
|
|
74
|
+
if (action) authorizedStatusActions.set(input.sessionID, action)
|
|
262
75
|
},
|
|
76
|
+
|
|
263
77
|
tool: {
|
|
78
|
+
echoes_activate_vault: tool({
|
|
79
|
+
description:
|
|
80
|
+
"Initialize or explicitly migrate EchoesVault through the shared portable runtime. Available only after /echoes-init.",
|
|
81
|
+
args: {},
|
|
82
|
+
async execute(_args, ctx) {
|
|
83
|
+
requireAuthorization(ctx.sessionID, "init")
|
|
84
|
+
const output = displayOutput(
|
|
85
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "init", {
|
|
86
|
+
signal: ctx.abort,
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
authorizedStatusActions.delete(ctx.sessionID)
|
|
90
|
+
return output
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
|
|
94
|
+
echoes_start_session: tool({
|
|
95
|
+
description:
|
|
96
|
+
"Restore the generated index and three most recent EchoesVault session entries. Available only after /echoes-start.",
|
|
97
|
+
args: {},
|
|
98
|
+
async execute(_args, ctx) {
|
|
99
|
+
requireAuthorization(ctx.sessionID, "start")
|
|
100
|
+
const output = displayOutput(
|
|
101
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "start", {
|
|
102
|
+
args: ["--recent", "3"],
|
|
103
|
+
signal: ctx.abort,
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
authorizedStatusActions.delete(ctx.sessionID)
|
|
107
|
+
return output
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
echoes_vault_status: tool({
|
|
112
|
+
description:
|
|
113
|
+
"Inspect EchoesVault protocol, storage, metadata, index, Git readiness, conflicts, and scale without modifying files.",
|
|
114
|
+
args: {},
|
|
115
|
+
async execute(_args, ctx) {
|
|
116
|
+
return displayOutput(
|
|
117
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "status", {
|
|
118
|
+
args: ["--format", "card"],
|
|
119
|
+
signal: ctx.abort,
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
|
|
264
125
|
commit_memory_to_echoes_vault: tool({
|
|
265
126
|
description:
|
|
266
|
-
"
|
|
127
|
+
"Finalize an EchoesVault session with one daily summary and optional curated pages. Available only after explicit /echoes-end.",
|
|
267
128
|
args: {
|
|
268
129
|
dailySummary: tool.schema
|
|
269
130
|
.string()
|
|
270
|
-
.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
newPages: tool.schema
|
|
131
|
+
.min(1)
|
|
132
|
+
.describe("Dense final outcomes, unresolved blockers, and next steps; never a transcript."),
|
|
133
|
+
pages: tool.schema
|
|
274
134
|
.array(
|
|
275
135
|
tool.schema.object({
|
|
276
|
-
filename: tool.schema
|
|
277
|
-
.string()
|
|
278
|
-
.describe("Filename without .md extension (e.g. 'architecture-decisions')"),
|
|
136
|
+
filename: tool.schema.string().min(1).describe("Safe page filename ending in .md."),
|
|
279
137
|
content: tool.schema
|
|
280
138
|
.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
|
|
139
|
+
.min(1)
|
|
140
|
+
.describe("Complete page with type, stack, status, and summary frontmatter."),
|
|
141
|
+
expectedSha256: tool.schema
|
|
297
142
|
.string()
|
|
298
|
-
.
|
|
299
|
-
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Fresh current hash, required when replacing an existing page."),
|
|
145
|
+
}),
|
|
300
146
|
)
|
|
301
|
-
.optional()
|
|
302
|
-
.describe("Lines to find and replace in place within EchoesVault/index.md"),
|
|
147
|
+
.optional(),
|
|
303
148
|
},
|
|
304
149
|
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)
|
|
150
|
+
requireAuthorization(ctx.sessionID, "end")
|
|
151
|
+
const output = displayOutput(
|
|
152
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "end", {
|
|
153
|
+
args: ["--confirm-explicit-user-end", "--payload", "-"],
|
|
154
|
+
payload: {
|
|
155
|
+
dailySummary: args.dailySummary,
|
|
156
|
+
pages: args.pages ?? [],
|
|
157
|
+
},
|
|
158
|
+
signal: ctx.abort,
|
|
159
|
+
}),
|
|
160
|
+
)
|
|
360
161
|
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")
|
|
162
|
+
return output
|
|
368
163
|
},
|
|
369
164
|
}),
|
|
165
|
+
|
|
370
166
|
echoes_append_to_daily_log: tool({
|
|
371
167
|
description:
|
|
372
|
-
"
|
|
168
|
+
"Write one intermediate technical milestone to a unique EchoesVault daily entry without ending the session.",
|
|
373
169
|
args: {
|
|
374
170
|
logEntry: tool.schema
|
|
375
171
|
.string()
|
|
376
|
-
.
|
|
377
|
-
|
|
378
|
-
),
|
|
172
|
+
.min(1)
|
|
173
|
+
.describe("Concise Markdown facts, decisions, blockers, or next steps."),
|
|
379
174
|
},
|
|
380
|
-
async execute(args,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return `✅ Scratchpad note saved to EchoesVault/daily/${today}.md`
|
|
175
|
+
async execute(args, ctx) {
|
|
176
|
+
return displayOutput(
|
|
177
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "append", {
|
|
178
|
+
args: ["--payload", "-"],
|
|
179
|
+
payload: { entry: args.logEntry },
|
|
180
|
+
signal: ctx.abort,
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
389
183
|
},
|
|
390
184
|
}),
|
|
185
|
+
|
|
391
186
|
echoes_search_vault_pages: tool({
|
|
392
187
|
description:
|
|
393
|
-
"Search
|
|
188
|
+
"Search EchoesVault knowledge pages with a narrow keyword or phrase before reading a relevant page.",
|
|
394
189
|
args: {
|
|
395
|
-
query: tool.schema
|
|
396
|
-
|
|
397
|
-
.describe("Specific keyword or short phrase to search for across the pages/ directory."),
|
|
190
|
+
query: tool.schema.string().min(1).describe("Specific keyword or short phrase."),
|
|
191
|
+
limit: tool.schema.number().int().min(1).max(500).optional(),
|
|
398
192
|
},
|
|
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")
|
|
193
|
+
async execute(args, ctx) {
|
|
194
|
+
const runtimeArgs = [args.query]
|
|
195
|
+
if (args.limit !== undefined) runtimeArgs.push("--limit", String(args.limit))
|
|
196
|
+
return displayOutput(
|
|
197
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "search", {
|
|
198
|
+
args: runtimeArgs,
|
|
199
|
+
signal: ctx.abort,
|
|
200
|
+
}),
|
|
201
|
+
)
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
|
|
205
|
+
echoes_hash_vault_page: tool({
|
|
206
|
+
description:
|
|
207
|
+
"Calculate the current SHA-256 of an EchoesVault page before replacing that existing page.",
|
|
208
|
+
args: {
|
|
209
|
+
filename: tool.schema.string().min(1).describe("Existing page filename."),
|
|
210
|
+
},
|
|
211
|
+
async execute(args, ctx) {
|
|
212
|
+
return displayOutput(
|
|
213
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "hash", {
|
|
214
|
+
args: [args.filename],
|
|
215
|
+
signal: ctx.abort,
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
429
218
|
},
|
|
430
219
|
}),
|
|
220
|
+
|
|
431
221
|
echoes_create_or_update_page: tool({
|
|
432
222
|
description:
|
|
433
|
-
"
|
|
223
|
+
"Create a validated EchoesVault page or replace one using its fresh expected SHA-256; the runtime regenerates the index.",
|
|
434
224
|
args: {
|
|
435
|
-
filename: tool.schema
|
|
436
|
-
.string()
|
|
437
|
-
.describe("Exact filename without paths (e.g. 'auth-architecture.md')."),
|
|
225
|
+
filename: tool.schema.string().min(1).describe("Exact page filename without paths."),
|
|
438
226
|
content: tool.schema
|
|
439
227
|
.string()
|
|
440
|
-
.
|
|
441
|
-
|
|
228
|
+
.min(1)
|
|
229
|
+
.describe("Complete Markdown page with all required frontmatter."),
|
|
230
|
+
expectedSha256: tool.schema
|
|
442
231
|
.string()
|
|
443
232
|
.optional()
|
|
444
|
-
.describe("
|
|
233
|
+
.describe("Fresh current hash, required when replacing an existing page."),
|
|
445
234
|
},
|
|
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")
|
|
235
|
+
async execute(args, ctx) {
|
|
236
|
+
return displayOutput(
|
|
237
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "upsert", {
|
|
238
|
+
args: ["--payload", "-"],
|
|
239
|
+
payload: {
|
|
240
|
+
filename: args.filename,
|
|
241
|
+
content: args.content,
|
|
242
|
+
...(args.expectedSha256 ? { expectedSha256: args.expectedSha256 } : {}),
|
|
243
|
+
},
|
|
244
|
+
signal: ctx.abort,
|
|
245
|
+
}),
|
|
246
|
+
)
|
|
477
247
|
},
|
|
478
248
|
}),
|
|
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
249
|
|
|
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({
|
|
250
|
+
echoes_hydrate_vault: tool({
|
|
497
251
|
description:
|
|
498
|
-
"
|
|
252
|
+
"Rebuild only ignored local EchoesVault index/state files for an already initialized checkout.",
|
|
499
253
|
args: {},
|
|
500
254
|
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."
|
|
255
|
+
return displayOutput(
|
|
256
|
+
await runEchoes(workspaceFor(ctx.directory, ctx.worktree), "hydrate", {
|
|
257
|
+
signal: ctx.abort,
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
512
260
|
},
|
|
513
261
|
}),
|
|
514
262
|
},
|