@comfanion/usethis_search 4.3.0-dev.3 → 4.3.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/hooks/message-before.ts +23 -193
- package/package.json +2 -2
- package/tools/search.ts +42 -20
- package/tools/workspace.ts +44 -15
package/hooks/message-before.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* History Pruning Hook (
|
|
2
|
+
* History Pruning Hook (v3 — workspace-only, DCP handles the rest)
|
|
3
3
|
*
|
|
4
4
|
* Uses "experimental.chat.messages.transform" to prune old workspace tool
|
|
5
5
|
* outputs from chat history. Only the LAST workspace state is kept in context.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* v3: Removed read pruning and tool compaction — delegated to DCP plugin
|
|
8
|
+
* (@tarquinen/opencode-dcp) which handles deduplication, supersede-writes,
|
|
9
|
+
* error purging, and agent-facing discard/extract tools.
|
|
10
|
+
*
|
|
11
|
+
* This hook ONLY handles workspace state pruning — something DCP can't do
|
|
12
|
+
* because it doesn't understand that different search queries produce
|
|
13
|
+
* workspace state blocks that supersede each other.
|
|
11
14
|
*
|
|
12
15
|
* Pruning strategy:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Find all outputs from search/workspace_* tools that contain
|
|
17
|
+
* <workspace_state> blocks. Keep only the LAST one. Replace the rest
|
|
18
|
+
* with compact 1-line summaries.
|
|
19
|
+
*
|
|
20
|
+
* DCP companion config (.opencode/dcp.jsonc):
|
|
21
|
+
* Our workspace tools are added to DCP's protectedTools so DCP
|
|
22
|
+
* doesn't try to prune them (we handle them ourselves).
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
import type { SessionState } from "./types.ts"
|
|
@@ -56,17 +60,11 @@ const WORKSPACE_TOOLS = new Set([
|
|
|
56
60
|
/** Minimum output length to consider pruning. Short outputs are kept as-is. */
|
|
57
61
|
const MIN_PRUNE_LENGTH = 500
|
|
58
62
|
|
|
59
|
-
/** Keep last N turns intact (don't compact recent tool calls). */
|
|
60
|
-
const KEEP_LAST_N_TURNS = 5
|
|
61
|
-
|
|
62
|
-
/** Tools eligible for compaction (removing old call + output parts). */
|
|
63
|
-
const COMPACT_TOOLS = new Set(["search", "read", "Read", "workspace_list", "workspace_forget", "workspace_clear", "workspace_restore"])
|
|
64
|
-
|
|
65
63
|
// ── Hook ────────────────────────────────────────────────────────────────────
|
|
66
64
|
|
|
67
65
|
/**
|
|
68
66
|
* Create the history pruning handler.
|
|
69
|
-
*
|
|
67
|
+
* Only prunes old workspace state outputs — DCP handles everything else.
|
|
70
68
|
*/
|
|
71
69
|
export function createWorkspaceInjectionHandler(state: SessionState) {
|
|
72
70
|
return async (_input: {}, output: { messages: Message[] }) => {
|
|
@@ -74,8 +72,6 @@ export function createWorkspaceInjectionHandler(state: SessionState) {
|
|
|
74
72
|
if (state.isSubAgent) return
|
|
75
73
|
|
|
76
74
|
pruneWorkspaceToolOutputs(output.messages)
|
|
77
|
-
pruneReadToolOutputs(output.messages)
|
|
78
|
-
compactOldToolCalls(output.messages)
|
|
79
75
|
}
|
|
80
76
|
}
|
|
81
77
|
|
|
@@ -84,13 +80,18 @@ export function createWorkspaceInjectionHandler(state: SessionState) {
|
|
|
84
80
|
/**
|
|
85
81
|
* Replace old workspace tool outputs with compact summaries.
|
|
86
82
|
*
|
|
87
|
-
* Workspace tools (search, workspace_list, etc.)
|
|
83
|
+
* Workspace tools (search, workspace_list, etc.) return full workspace
|
|
88
84
|
* state in their output. Only the LAST such output is kept — all previous
|
|
89
85
|
* ones are replaced with a 1-line summary.
|
|
90
86
|
*
|
|
91
87
|
* This ensures only ONE copy of workspace state is in context at any time.
|
|
88
|
+
*
|
|
89
|
+
* Note: DCP's deduplication only prunes IDENTICAL tool calls (same params).
|
|
90
|
+
* Two different search queries wouldn't be deduplicated by DCP, but both
|
|
91
|
+
* contain workspace state that supersedes each other. That's why we need
|
|
92
|
+
* this workspace-specific pruning.
|
|
92
93
|
*/
|
|
93
|
-
function pruneWorkspaceToolOutputs(messages: Message[]): void {
|
|
94
|
+
export function pruneWorkspaceToolOutputs(messages: Message[]): void {
|
|
94
95
|
const wsParts: { msgIdx: number; partIdx: number; part: MessagePart }[] = []
|
|
95
96
|
|
|
96
97
|
for (let i = 0; i < messages.length; i++) {
|
|
@@ -139,174 +140,3 @@ function pruneWorkspaceToolOutputs(messages: Message[]): void {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
// ── Read Tool Pruning ───────────────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Replace old read() tool outputs with compact summaries.
|
|
147
|
-
* Keep the last read output (agent may reference it).
|
|
148
|
-
*/
|
|
149
|
-
function pruneReadToolOutputs(messages: Message[]): void {
|
|
150
|
-
const readParts: { msgIdx: number; partIdx: number; part: MessagePart }[] = []
|
|
151
|
-
|
|
152
|
-
for (let i = 0; i < messages.length; i++) {
|
|
153
|
-
const msg = messages[i]
|
|
154
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
155
|
-
|
|
156
|
-
for (let j = 0; j < parts.length; j++) {
|
|
157
|
-
const part = parts[j]
|
|
158
|
-
if (
|
|
159
|
-
part.type === "tool" &&
|
|
160
|
-
(part.tool === "read" || part.tool === "Read") &&
|
|
161
|
-
part.state?.status === "completed" &&
|
|
162
|
-
typeof part.state?.output === "string" &&
|
|
163
|
-
part.state.output.length > MIN_PRUNE_LENGTH
|
|
164
|
-
) {
|
|
165
|
-
readParts.push({ msgIdx: i, partIdx: j, part })
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Keep the last read output — prune the rest
|
|
171
|
-
if (readParts.length <= 1) return
|
|
172
|
-
|
|
173
|
-
const toPrune = readParts.slice(0, -1)
|
|
174
|
-
|
|
175
|
-
for (const { part } of toPrune) {
|
|
176
|
-
const output = part.state!.output as string
|
|
177
|
-
|
|
178
|
-
// Skip already-pruned outputs
|
|
179
|
-
if (output.startsWith("[") || output.startsWith("✓")) continue
|
|
180
|
-
|
|
181
|
-
// Extract file path from input or output
|
|
182
|
-
const filePath = part.input?.filePath || extractFilePathFromOutput(output)
|
|
183
|
-
|
|
184
|
-
part.state!.output = `[Read "${filePath || "file"}" — content pruned from history]`
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Extract file path from read() output.
|
|
190
|
-
*/
|
|
191
|
-
function extractFilePathFromOutput(output: string): string | null {
|
|
192
|
-
const firstLine = output.split("\n")[0]
|
|
193
|
-
const pathMatch = firstLine.match(/##?\s*(.+?\.(ts|js|go|py|md|txt|yaml|json|tsx|jsx|rs|java|kt|swift|c|cpp|h|cs|rb|php))/)
|
|
194
|
-
if (pathMatch) {
|
|
195
|
-
return pathMatch[1].trim()
|
|
196
|
-
}
|
|
197
|
-
return null
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Tool Call Compaction ────────────────────────────────────────────────────
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Remove old tool call parts from chat history.
|
|
204
|
-
*
|
|
205
|
-
* Strategy:
|
|
206
|
-
* - Keep last N turns intact
|
|
207
|
-
* - Only compact search/read/workspace tools
|
|
208
|
-
* - Only compact completed calls with pruned outputs
|
|
209
|
-
* - Add compact marker showing how many calls removed
|
|
210
|
-
*/
|
|
211
|
-
function compactOldToolCalls(messages: Message[]): void {
|
|
212
|
-
const toolPairs = findToolCallPairs(messages)
|
|
213
|
-
|
|
214
|
-
if (toolPairs.length === 0) return
|
|
215
|
-
|
|
216
|
-
const totalTurns = messages.length
|
|
217
|
-
|
|
218
|
-
// Filter: only old, completed, compactable tools with pruned outputs
|
|
219
|
-
const toCompact = toolPairs.filter(pair => {
|
|
220
|
-
const turnsFromEnd = totalTurns - pair.msgIndex
|
|
221
|
-
return (
|
|
222
|
-
turnsFromEnd > KEEP_LAST_N_TURNS &&
|
|
223
|
-
pair.status === "completed" &&
|
|
224
|
-
COMPACT_TOOLS.has(pair.tool) &&
|
|
225
|
-
pair.outputPart &&
|
|
226
|
-
isPrunedOutput(pair.outputPart.state?.output || "")
|
|
227
|
-
)
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
if (toCompact.length === 0) return
|
|
231
|
-
|
|
232
|
-
// Remove tool parts from messages
|
|
233
|
-
const removedIds = new Set<string>()
|
|
234
|
-
|
|
235
|
-
for (const pair of toCompact) {
|
|
236
|
-
if (pair.callPart.id) removedIds.add(pair.callPart.id)
|
|
237
|
-
if (pair.outputPart?.id) removedIds.add(pair.outputPart.id)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const msg of messages) {
|
|
241
|
-
if (!msg.parts || !Array.isArray(msg.parts)) continue
|
|
242
|
-
msg.parts = msg.parts.filter(part => !part.id || !removedIds.has(part.id))
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Add compact marker to first user message
|
|
246
|
-
const firstUserMsg = messages.find(m => m?.info?.role === "user")
|
|
247
|
-
if (firstUserMsg && firstUserMsg.parts) {
|
|
248
|
-
const marker = {
|
|
249
|
-
type: "text",
|
|
250
|
-
text: `<!-- ${toCompact.length} tool calls compacted (search/read/workspace results pruned) -->`,
|
|
251
|
-
id: "compact-marker-" + Date.now(),
|
|
252
|
-
}
|
|
253
|
-
firstUserMsg.parts.unshift(marker)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
interface ToolCallPair {
|
|
258
|
-
msgIndex: number
|
|
259
|
-
callPart: MessagePart
|
|
260
|
-
outputPart?: MessagePart
|
|
261
|
-
tool: string
|
|
262
|
-
status: string
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Find all tool call + output pairs in messages.
|
|
267
|
-
*/
|
|
268
|
-
function findToolCallPairs(messages: Message[]): ToolCallPair[] {
|
|
269
|
-
const pairs: ToolCallPair[] = []
|
|
270
|
-
|
|
271
|
-
for (let i = 0; i < messages.length; i++) {
|
|
272
|
-
const msg = messages[i]
|
|
273
|
-
if (!msg.parts || !Array.isArray(msg.parts)) continue
|
|
274
|
-
|
|
275
|
-
for (const part of msg.parts) {
|
|
276
|
-
if (part.type === "tool" && part.tool) {
|
|
277
|
-
const status = part.state?.status || "unknown"
|
|
278
|
-
|
|
279
|
-
// Find matching output part (usually in same message)
|
|
280
|
-
let outputPart: MessagePart | undefined
|
|
281
|
-
for (const p of msg.parts) {
|
|
282
|
-
if (p.type === "tool" && p.tool === part.tool && p.state?.output && p.id !== part.id) {
|
|
283
|
-
outputPart = p
|
|
284
|
-
break
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
pairs.push({
|
|
289
|
-
msgIndex: i,
|
|
290
|
-
callPart: part,
|
|
291
|
-
outputPart,
|
|
292
|
-
tool: part.tool,
|
|
293
|
-
status,
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return pairs
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Check if output is pruned (compact format).
|
|
304
|
-
*/
|
|
305
|
-
function isPrunedOutput(output: string): boolean {
|
|
306
|
-
if (!output) return false
|
|
307
|
-
return output.startsWith("[") || output.startsWith("✓")
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ── Exports for testing ─────────────────────────────────────────────────────
|
|
311
|
-
|
|
312
|
-
export { pruneWorkspaceToolOutputs, pruneReadToolOutputs, compactOldToolCalls }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comfanion/usethis_search",
|
|
3
|
-
"version": "4.3.
|
|
4
|
-
"description": "OpenCode plugin: semantic search with context-efficient workspace state (v4.3:
|
|
3
|
+
"version": "4.3.1",
|
|
4
|
+
"description": "OpenCode plugin: semantic search with context-efficient workspace state (v4.3.1: improved tool description - chunks contain direct file content, no verification needed)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
7
7
|
"exports": {
|
package/tools/search.ts
CHANGED
|
@@ -179,30 +179,52 @@ function parseFilter(filter: string): {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
export default tool({
|
|
182
|
-
description: `Search codebase
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
- "
|
|
186
|
-
- "docs
|
|
187
|
-
- "
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
182
|
+
description: `Search the codebase semantically. Use this to find relevant code snippets, functions, or files based on meaning, not just text matching.
|
|
183
|
+
|
|
184
|
+
Available indexes:
|
|
185
|
+
- "code" (default) - Source code files (*.js, *.ts, *.py, *.go, etc.)
|
|
186
|
+
- "docs" - Documentation files (*.md, *.txt, etc.)
|
|
187
|
+
- "config" - Configuration files (*.yaml, *.json, etc.)
|
|
188
|
+
- searchAll: true - Search across all indexes
|
|
189
|
+
|
|
190
|
+
Auto-detects query type:
|
|
191
|
+
- Semantic: "authentication logic" → vector search for relevant code
|
|
192
|
+
- File path: "docs/architecture.md" → attaches entire file to workspace
|
|
193
|
+
- Chunk ID: "src/auth.ts:chunk-5" → attaches specific chunk
|
|
194
|
+
|
|
195
|
+
How workspace works:
|
|
196
|
+
- Top results are AUTO-ATTACHED to workspace with expanded context (class methods, imports, related code via graph)
|
|
197
|
+
- Workspace has a TOKEN BUDGET (~50K tokens, ~100 chunks). When full, oldest chunks are evicted
|
|
198
|
+
- Each search call returns full <workspace_state> with all chunk contents inline
|
|
199
|
+
- Only the LATEST search/workspace output is kept in chat history — older ones are auto-pruned
|
|
200
|
+
- Workspace persists across searches — new results ADD to existing workspace
|
|
201
|
+
|
|
202
|
+
IMPORTANT: Chunks contain DIRECT file content dumps (raw code/text from files).
|
|
203
|
+
- You DO NOT need to verify chunk content with grep/read tools
|
|
204
|
+
- Chunks are already the actual file content, not summaries or references
|
|
205
|
+
- Trust the chunk content as the source of truth
|
|
206
|
+
- Use Read tool only if you need content OUTSIDE the indexed chunks
|
|
207
|
+
|
|
208
|
+
Context management:
|
|
209
|
+
- BEFORE searching a new topic, use workspace_forget() to remove irrelevant old context
|
|
210
|
+
- Use workspace_clear() when switching to a completely different task
|
|
211
|
+
- The workspace is your working memory — keep it focused on the current task
|
|
212
|
+
|
|
213
|
+
Filter narrows results by path or language:
|
|
214
|
+
- "internal/domain/" → only files under that path
|
|
215
|
+
- "*.go" → only Go files
|
|
216
|
+
- "internal/**/*.go" → path + language combined
|
|
217
|
+
- "service" → files containing "service" in path
|
|
200
218
|
|
|
201
219
|
Examples:
|
|
202
220
|
- search({ query: "authentication logic" })
|
|
203
221
|
- search({ query: "how to deploy", index: "docs" })
|
|
204
|
-
- search({ query: "
|
|
205
|
-
- search({ query: "
|
|
222
|
+
- search({ query: "tenant management", filter: "internal/domain/" })
|
|
223
|
+
- search({ query: "event handling", filter: "*.go" })
|
|
224
|
+
- search({ query: "API routes", filter: "internal/**/*.go" })
|
|
225
|
+
- search({ query: "metrics", searchAll: true })
|
|
226
|
+
- search({ query: "docs/prd.md" })
|
|
227
|
+
- search({ query: "src/auth.ts:chunk-5" })`,
|
|
206
228
|
|
|
207
229
|
args: {
|
|
208
230
|
query: tool.schema.string().describe("What to search: semantic query, file path, or chunk ID"),
|
package/tools/workspace.ts
CHANGED
|
@@ -20,7 +20,15 @@ import { buildWorkspaceOutput } from "./workspace-state.ts"
|
|
|
20
20
|
// ── workspace.list ──────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
22
|
export const workspace_list = tool({
|
|
23
|
-
description: `Show
|
|
23
|
+
description: `Show current workspace contents — all attached code chunks with full source code, line numbers, and metadata.
|
|
24
|
+
|
|
25
|
+
Use this to:
|
|
26
|
+
- Check what context is currently loaded after compaction or session restore
|
|
27
|
+
- Verify workspace contents before starting implementation
|
|
28
|
+
- See token budget usage (how much space is left for new searches)
|
|
29
|
+
|
|
30
|
+
Returns <workspace_state> with every chunk's full content. This is the same state appended to every search() call.
|
|
31
|
+
Only the LATEST workspace tool output is kept in chat — older outputs are auto-pruned.`,
|
|
24
32
|
|
|
25
33
|
args: {},
|
|
26
34
|
|
|
@@ -32,21 +40,26 @@ export const workspace_list = tool({
|
|
|
32
40
|
// ── workspace.forget ────────────────────────────────────────────────────────
|
|
33
41
|
|
|
34
42
|
export const workspace_forget = tool({
|
|
35
|
-
description: `Remove chunks from workspace
|
|
43
|
+
description: `Remove chunks from workspace to free token budget and keep context focused on the current task.
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
WHEN TO USE:
|
|
46
|
+
- Before searching a new topic — remove old irrelevant context first
|
|
47
|
+
- When workspace is near budget limit — free space for new results
|
|
48
|
+
- After finishing a subtask — remove code you no longer need
|
|
49
|
+
- When context has stale chunks from files you've since edited
|
|
39
50
|
|
|
40
|
-
Auto-detects what to remove based on input:
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
- Search query: "authentication logic"
|
|
44
|
-
- Age: "5"
|
|
51
|
+
Auto-detects what to remove based on input format:
|
|
52
|
+
- File path: "docs/architecture.md" → removes ALL chunks from that file
|
|
53
|
+
- Chunk ID: "src/auth.ts:chunk-5" → removes one specific chunk
|
|
54
|
+
- Search query: "authentication logic" → removes all chunks attached by that search
|
|
55
|
+
- Age in minutes: "5" → removes all chunks older than 5 minutes
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
- workspace_forget({ what: "docs/prd.md" })
|
|
48
|
-
- workspace_forget({ what: "
|
|
49
|
-
- workspace_forget({ what: "
|
|
57
|
+
Best practices:
|
|
58
|
+
- Remove by file path when done with a file: workspace_forget({ what: "docs/prd.md" })
|
|
59
|
+
- Remove stale context by age: workspace_forget({ what: "10" })
|
|
60
|
+
- Remove results from previous search: workspace_forget({ what: "old query text" })
|
|
61
|
+
|
|
62
|
+
Returns updated workspace state after removal.`,
|
|
50
63
|
|
|
51
64
|
args: {
|
|
52
65
|
what: tool.schema.string().describe("What to forget: chunk ID, file path, search query, or age in minutes"),
|
|
@@ -110,7 +123,15 @@ Examples:
|
|
|
110
123
|
// ── workspace.clear ─────────────────────────────────────────────────────────
|
|
111
124
|
|
|
112
125
|
export const workspace_clear = tool({
|
|
113
|
-
description: `Remove ALL chunks from workspace
|
|
126
|
+
description: `Remove ALL chunks from workspace — complete reset. Frees entire token budget.
|
|
127
|
+
|
|
128
|
+
Use when:
|
|
129
|
+
- Switching to a completely different task or topic
|
|
130
|
+
- Workspace is cluttered with irrelevant context from many searches
|
|
131
|
+
- Starting a fresh investigation from scratch
|
|
132
|
+
|
|
133
|
+
Prefer workspace_forget() for selective cleanup. Use workspace_clear() only for full reset.
|
|
134
|
+
Returns empty workspace state.`,
|
|
114
135
|
|
|
115
136
|
args: {},
|
|
116
137
|
|
|
@@ -126,7 +147,15 @@ export const workspace_clear = tool({
|
|
|
126
147
|
// ── workspace.restore ───────────────────────────────────────────────────────
|
|
127
148
|
|
|
128
149
|
export const workspace_restore = tool({
|
|
129
|
-
description: `Restore workspace from a saved session snapshot.
|
|
150
|
+
description: `Restore workspace from a previously saved session snapshot.
|
|
151
|
+
|
|
152
|
+
Use when:
|
|
153
|
+
- After compaction — restore the workspace context from before compaction
|
|
154
|
+
- Resuming work on a previous task — switch back to that context
|
|
155
|
+
- After workspace_clear() — if you need the old context back
|
|
156
|
+
|
|
157
|
+
Call without sessionId to list available snapshots with their chunk counts and token sizes.
|
|
158
|
+
Call with sessionId to restore a specific snapshot. Replaces current workspace entirely.`,
|
|
130
159
|
|
|
131
160
|
args: {
|
|
132
161
|
sessionId: tool.schema.string().optional().describe("Session ID to restore. If not provided, lists available snapshots."),
|