opencodekit 0.23.2 → 0.23.3

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.
@@ -114,23 +114,23 @@ Feedback mode examples:
114
114
  // --- Feedback mode: observation #id marked helpful/harmful ---
115
115
  if (args.id !== undefined && args.feedback !== undefined) {
116
116
  const obsId = Number(args.id);
117
- if (!Number.isInteger(obsId)) return "id must be an integer.";
117
+ if (!Number.isInteger(obsId)) return "id must be an integer.";
118
118
  const fb = String(args.feedback);
119
119
  if (fb !== "helpful" && fb !== "harmful")
120
- return 'feedback must be "helpful" or "harmful".';
120
+ return 'feedback must be "helpful" or "harmful".';
121
121
 
122
122
  const result = recordFeedback(obsId, fb, args.reason as string | undefined);
123
- if (!result.success) return `❌ ${result.error ?? "Failed to record feedback."}`;
123
+ if (!result.success) return `${result.error ?? "Failed to record feedback."}`;
124
124
 
125
125
  return [
126
- `✅ Observation #${obsId} marked as ${fb}.`,
126
+ `Observation #${obsId} marked as ${fb}.`,
127
127
  `- Feedback: ${result.helpfulCount} helpful / ${result.harmfulCount} harmful`,
128
128
  ].join("\n");
129
129
  }
130
130
 
131
131
  // --- Create mode: store a new observation ---
132
132
  if (!args.type || !args.title) {
133
- return "Provide type+title to create an observation, or id+feedback to give feedback.";
133
+ return "Provide type+title to create an observation, or id+feedback to give feedback.";
134
134
  }
135
135
 
136
136
  const obsType = args.type as ObservationType;
@@ -200,7 +200,7 @@ Feedback mode examples:
200
200
 
201
201
  const warnings =
202
202
  validation.issues.length > 0
203
- ? `\n⚠️ Warnings: ${validation.issues.map((i) => i.message).join("; ")}`
203
+ ? `\nWarnings: ${validation.issues.map((i) => i.message).join("; ")}`
204
204
  : "";
205
205
 
206
206
  return `${TYPE_ICONS[obsType] ?? "📌"} Observation #${id} stored [${obsType}] "${args.title}" (confidence: ${confidence}, source: ${source})${warnings}`;
@@ -454,9 +454,7 @@ export const SessionSummaryPlugin: Plugin = async ({ client, directory }) => {
454
454
  addRead(summary, normalized, "Code navigation");
455
455
  break;
456
456
  case "grep":
457
- case "srcwalk_search":
458
457
  case "glob":
459
- case "srcwalk_files":
460
458
  // Search tools — not tracking individual files
461
459
  break;
462
460
  }
@@ -5,9 +5,7 @@
5
5
  * Wraps available CLI tools (rg/grep, find, cat, ls) into unified srcwalk_* tools.
6
6
  *
7
7
  * Tools (matching pikit srcwalk extension surface):
8
- * - srcwalk_search — Search symbols/text/regex in codebase
9
8
  * - srcwalk_read — Read files with optional section/range
10
- * - srcwalk_files — Find files by glob pattern
11
9
  * - srcwalk_deps — Show imports and dependents for a file
12
10
  * - srcwalk_map — Directory tree overview
13
11
  * - srcwalk_callers — Reverse call graph (grep-based)
@@ -23,7 +21,6 @@ import path from "node:path";
23
21
  import { tool } from "@opencode-ai/plugin/tool";
24
22
  import type { Plugin } from "@opencode-ai/plugin";
25
23
 
26
- const BIN_RG = "rg";
27
24
  const TIMEOUT_MS = 15_000;
28
25
  const MAX_BUFFER = 5 * 1024 * 1024;
29
26
 
@@ -55,17 +52,25 @@ function run(
55
52
  }
56
53
  }
57
54
 
58
- function hasRg(): boolean {
59
- try {
60
- execFileSync(BIN_RG, ["--version"], { encoding: "utf-8", timeout: 1000, stdio: "ignore" });
61
- return true;
62
- } catch {
63
- return false;
64
- }
65
- }
66
-
67
55
  function plural(n: number, word: string): string {
68
- return `${n} ${word}${n !== 1 ? "s" : ""}`;
56
+ if (n === 1) return `${n} ${word}`;
57
+ if (
58
+ word.endsWith("ch") ||
59
+ word.endsWith("s") ||
60
+ word.endsWith("sh") ||
61
+ word.endsWith("x") ||
62
+ word.endsWith("z")
63
+ ) {
64
+ return `${n} ${word}es`;
65
+ }
66
+ if (
67
+ word.endsWith("y") &&
68
+ word.length > 1 &&
69
+ !["a", "e", "i", "o", "u"].includes(word[word.length - 2])
70
+ ) {
71
+ return `${n} ${word.slice(0, -1)}ies`;
72
+ }
73
+ return `${n} ${word}s`;
69
74
  }
70
75
 
71
76
  // ---------------------------------------------------------------------------
@@ -73,68 +78,6 @@ function plural(n: number, word: string): string {
73
78
  // ---------------------------------------------------------------------------
74
79
 
75
80
  const srcwalkTools = {
76
- // -----------------------------------------------------------------------
77
- // srcwalk_search: Search codebase
78
- // -----------------------------------------------------------------------
79
- srcwalk_search: tool({
80
- description: `Search for symbols, text, or regex patterns in code.\n\nUses ripgrep (rg) when available, falls back to grep.\nSupports symbol search (definitions first), content search, and regex.`,
81
- args: {
82
- query: tool.schema.string().describe("Search query or regex pattern"),
83
- scope: tool.schema.string().optional().describe("Subdirectory scope (default: project root)"),
84
- kind: tool.schema.string().optional().describe("Search type: text (default), regex, content"),
85
- context: tool.schema.number().optional().describe("Lines of context before/after each match"),
86
- limit: tool.schema.number().optional().describe("Max results (default: 30)"),
87
- },
88
- execute: async (args, context) => {
89
- const query = String(args.query ?? "").trim();
90
- if (!query) return "❌ query is required.";
91
- const scope = args.scope ? String(args.scope) : context.directory;
92
- const limit = Math.min(args.limit ?? 30, 100);
93
- const ctxLines = args.context ?? 0;
94
- const kind = String(args.kind ?? "text");
95
-
96
- const searchDir = path.resolve(context.directory, scope);
97
-
98
- if (hasRg()) {
99
- const rgArgs = ["--no-heading", "--line-number", "--color", "never"];
100
- rgArgs.push("--max-count", String(limit * 2));
101
- if (ctxLines > 0) {
102
- rgArgs.push("-C", String(ctxLines));
103
- }
104
- if (kind === "regex") {
105
- rgArgs.push("-E", "rust");
106
- } else {
107
- rgArgs.push("-F"); // literal
108
- }
109
- rgArgs.push(query, searchDir);
110
-
111
- const result = run(BIN_RG, rgArgs);
112
- if (result.code !== 0 && result.stderr) {
113
- // Try plain grep fallback
114
- } else {
115
- const lines = result.stdout.split("\n").filter(Boolean).slice(0, limit);
116
- if (lines.length === 0) return "No matches found.";
117
- return lines.join("\n");
118
- }
119
- }
120
-
121
- // Fallback to grep
122
- const grepArgs = ["-rn", "--color=never"];
123
- if (ctxLines > 0) grepArgs.push("-C", String(ctxLines));
124
- if (kind === "regex") {
125
- grepArgs.push("-E");
126
- } else {
127
- grepArgs.push("-F");
128
- }
129
- grepArgs.push(query, searchDir);
130
-
131
- const result = run("grep", grepArgs);
132
- const lines = result.stdout.split("\n").filter(Boolean).slice(0, limit);
133
- if (lines.length === 0) return "No matches found.";
134
- return lines.join("\n");
135
- },
136
- }),
137
-
138
81
  // -----------------------------------------------------------------------
139
82
  // srcwalk_read: Read files
140
83
  // -----------------------------------------------------------------------
@@ -233,84 +176,6 @@ const srcwalkTools = {
233
176
  },
234
177
  }),
235
178
 
236
- // -----------------------------------------------------------------------
237
- // srcwalk_files: Find files
238
- // -----------------------------------------------------------------------
239
- srcwalk_files: tool({
240
- description: `Find files by glob pattern. Returns matched file paths with size estimates, grouped by directory. Respects .gitignore.`,
241
- args: {
242
- pattern: tool.schema.string().describe("Glob pattern (e.g. '*.ts', 'src/**/*.ts')"),
243
- scope: tool.schema
244
- .string()
245
- .optional()
246
- .describe("Directory to search (default: project root)"),
247
- },
248
- execute: async (args, context) => {
249
- const pattern = String(args.pattern ?? "").trim();
250
- if (!pattern) return "❌ pattern is required.";
251
- const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
252
-
253
- // Use find with simple glob pattern
254
- const findArgs: string[] = [scopeDir, "-type", "f"];
255
-
256
- // Convert simple glob to -name pattern
257
- if (pattern.includes("**")) {
258
- // find handles ** naturally with -path
259
- findArgs.push("-path", `*/${pattern.replace(/\*\*/g, "*")}`);
260
- } else if (pattern.includes("*")) {
261
- findArgs.push("-name", pattern);
262
- } else if (pattern.includes(".")) {
263
- findArgs.push("-name", pattern);
264
- } else {
265
- findArgs.push("-name", `*${pattern}*`);
266
- }
267
-
268
- // Exclude common dirs
269
- for (const dir of [
270
- ".git",
271
- "node_modules",
272
- "dist",
273
- "build",
274
- "coverage",
275
- ".next",
276
- ".opencode",
277
- ]) {
278
- findArgs.push("-not", "-path", `*/${dir}/*`);
279
- }
280
-
281
- const result = run("find", findArgs);
282
- const files = result.stdout.split("\n").filter(Boolean).slice(0, 200);
283
-
284
- if (files.length === 0) return `No files matching "${pattern}" found.`;
285
-
286
- // Group by directory
287
- const groups: Record<string, string[]> = {};
288
- for (const f of files) {
289
- const dir = path.dirname(f);
290
- if (!groups[dir]) groups[dir] = [];
291
- groups[dir].push(path.basename(f));
292
- }
293
-
294
- const lines: string[] = [`# Files matching "${pattern}" (${plural(files.length, "file")})\n`];
295
- for (const [dir, files] of Object.entries(groups).sort()) {
296
- const relDir = path.relative(context.directory, dir) || ".";
297
- lines.push(`${relDir}/ (${plural(files.length, "file")})`);
298
- for (const f of files) {
299
- const fp = path.join(dir, f);
300
- try {
301
- const size = statSync(fp).size;
302
- const tokenEst = Math.ceil(size / 4);
303
- lines.push(` ${f} (~${tokenEst.toLocaleString()} tokens)`);
304
- } catch {
305
- lines.push(` ${f}`);
306
- }
307
- }
308
- lines.push("");
309
- }
310
- return lines.join("\n");
311
- },
312
- }),
313
-
314
179
  // -----------------------------------------------------------------------
315
180
  // srcwalk_deps: Import analysis
316
181
  // -----------------------------------------------------------------------
@@ -419,7 +284,7 @@ const srcwalkTools = {
419
284
  },
420
285
  execute: async (args, context) => {
421
286
  const symbol = String(args.symbol ?? "").trim();
422
- if (!symbol) return "symbol is required.";
287
+ if (!symbol) return "symbol is required.";
423
288
  const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
424
289
  const depth = Math.min(args.depth ?? 1, 3);
425
290
 
@@ -483,7 +348,7 @@ const srcwalkTools = {
483
348
  },
484
349
  execute: async (args, context) => {
485
350
  const symbol = String(args.symbol ?? "").trim();
486
- if (!symbol) return "symbol is required.";
351
+ if (!symbol) return "symbol is required.";
487
352
  const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
488
353
 
489
354
  // Find the function definition
@@ -598,7 +463,7 @@ const srcwalkTools = {
598
463
  },
599
464
  execute: async (args, context) => {
600
465
  const symbol = String(args.symbol ?? "").trim();
601
- if (!symbol) return "symbol is required.";
466
+ if (!symbol) return "symbol is required.";
602
467
  const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
603
468
 
604
469
  // Get callers
@@ -705,7 +570,7 @@ const srcwalkTools = {
705
570
  },
706
571
  execute: async (args, context) => {
707
572
  const symbol = String(args.symbol ?? "").trim();
708
- if (!symbol) return "symbol is required.";
573
+ if (!symbol) return "symbol is required.";
709
574
  const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
710
575
 
711
576
  // Count usages per directory
@@ -28,7 +28,7 @@ tools: []
28
28
 
29
29
  ## Choose The Right Navigation Tool
30
30
 
31
- - Use `srcwalk_search`, `srcwalk_read`, `srcwalk_files`, `srcwalk_deps` for symbol search, file reading, glob finds, and blast-radius checks
31
+ - Use `srcwalk_read`, `srcwalk_deps`, `grep`, `csearch` for file reading, blast-radius checks, pattern search, and multi-keyword chunk search
32
32
  - Use `srcwalk_callers`, `srcwalk_callees`, `srcwalk_flow`, `srcwalk_impact`, `srcwalk_map` for call graphs, orientation slices, impact triage, and repo maps — these are first-class Pi tools, no separate skill load needed
33
33
  - All tools are backed by the installed `srcwalk` binary via the `srcwalk.ts` extension
34
34
 
@@ -46,7 +46,7 @@ glob("*.ts") → read(file1) → "too big" → grep("functionName") → read(fil
46
46
  grep("functionName", path: "src/") → read(exact_file, offset: line-10, limit: 30)
47
47
  ```
48
48
 
49
- Start with search (`srcwalk_search` or grep fallback) to locate, then read only what you need.
49
+ Start with `grep` or `csearch` to locate, then read only what you need.
50
50
 
51
51
  ### Pattern 2: Multi-Symbol Search
52
52
 
@@ -79,9 +79,8 @@ Steps:
79
79
 
80
80
  ### Pattern 5: Context Locality
81
81
 
82
- When editing a file, search results from the same directory/package are more likely relevant. Pass context when available:
83
- - In grep: use `path: "src/same-module/"` to scope
84
- - In srcwalk_search: pass `context` param to boost nearby results
82
+ When editing a file, search results from the same directory/package are more likely relevant. Use `path` to scope grep results:
83
+ - `grep({ pattern: "...", path: "src/same-module/" })`
85
84
 
86
85
  ### Pattern 6: Outline Before Deep Read
87
86
 
@@ -98,7 +97,7 @@ This gives you structure and line ranges. Then read only the section you need.
98
97
  **Right**: Start from the entry point, follow function calls:
99
98
 
100
99
  ```
101
- 1. `srcwalk_search(query: "entryPoint")` → find where it is defined
100
+ 1. `grep({ pattern: "entryPoint" })` → find where it is defined
102
101
  2. `srcwalk_callees({ symbol: "entryPoint", scope: "src" })` or `srcwalk_flow` for call graph orientation
103
102
  3. `srcwalk_read(section: "line-range")` → drill into the interesting callee
104
103
  ```
@@ -109,8 +108,9 @@ All navigation tools are native srcwalk_* tools. Available tools:
109
108
 
110
109
  | Task | Tool | Notes |
111
110
  |---|---|---|
112
- | `grep` + `read` | `srcwalk_search(expand: 2)` | Returns definitions with inline source — no second read needed |
113
- | `glob` | `srcwalk_files` | Adds token estimates per file |
111
+ | Pattern search | `grep` | Exact/regex symbol search, multi-pattern, scoped |
112
+ | Multi-keyword chunk search | `csearch` | Find code by multiple keywords, returns ranked function/class chunks |
113
+ | Find files by glob | `glob` | Built-in glob file discovery |
114
114
  | Large file read | `srcwalk_read` | Auto-outlines, shows structure |
115
115
  | Direct callers | `srcwalk_callers` | Structural call-site evidence |
116
116
  | Transitive callers | `srcwalk_callers(depth: N)` | Multi-hop BFS up to 5 |
@@ -120,7 +120,7 @@ All navigation tools are native srcwalk_* tools. Available tools:
120
120
  | Heuristic triage | `srcwalk_impact` | Follow up with callers for proof |
121
121
  | Repo shape | `srcwalk_map` | Token budgets + directory skeleton |
122
122
 
123
- **IMPORTANT**: Prefer `srcwalk_*` tools over built-in grep/glob/read for code navigation. Their expanded search results often include full source do NOT re-read files already shown in search output.
123
+ **IMPORTANT**: Use `grep` for exact-pattern searches, `csearch` for multi-keyword chunk discovery, and `srcwalk_*` tools for structural navigation (call graphs, deps, file reads).
124
124
 
125
125
  ## Cost Awareness
126
126
 
@@ -137,6 +137,6 @@ Every tool call has a token cost. Efficient navigation means:
137
137
  |---|---|
138
138
  | Read entire large file | Use outline first, then section read |
139
139
  | Search → read same code again | Work from search results directly |
140
- | Trace calls one-by-one | `srcwalk_callers` / `srcwalk_callees` or multi-symbol `srcwalk_search` |
140
+ | Trace calls one-by-one | `srcwalk_callers` / `srcwalk_callees` or multi-pattern `grep` |
141
141
  | Explore randomly | Start from entry point, follow calls |
142
142
  | Forget to check blast radius | Always check before signature changes |
@@ -5,7 +5,7 @@ version: 1.0.0
5
5
  tags: [review, code-quality, verification]
6
6
  dependencies: [verification-before-completion]
7
7
  agent_types: [reviewer]
8
- tools: [srcwalk_search, srcwalk_deps, bash]
8
+ tools: [grep, srcwalk_deps, bash]
9
9
  ---
10
10
 
11
11
  # Code Review & Quality
@@ -5,7 +5,7 @@ version: 1.0.0
5
5
  tags: [debugging, workflow, verification]
6
6
  dependencies: [test-driven-development, verification-before-completion]
7
7
  agent_types: [worker, reviewer]
8
- tools: [bash, srcwalk_search, srcwalk_deps]
8
+ tools: [bash, grep, srcwalk_deps]
9
9
  ---
10
10
 
11
11
  # Debugging & Error Recovery
@@ -5,7 +5,7 @@ version: 1.0.0
5
5
  tags: [architecture, code-quality, ousterhout]
6
6
  dependencies: []
7
7
  agent_types: [planner, worker, reviewer]
8
- tools: [srcwalk_search, srcwalk_deps]
8
+ tools: [grep, srcwalk_deps]
9
9
  ---
10
10
 
11
11
  # Deep Module Design
@@ -5,7 +5,7 @@ version: 1.0.0
5
5
  tags: [workflow, planning, agent-coordination]
6
6
  dependencies: [spec-driven-development]
7
7
  agent_types: [planner]
8
- tools: [TaskCreate, TaskUpdate, memory, srcwalk_search]
8
+ tools: [TaskCreate, TaskUpdate, memory, grep]
9
9
  ---
10
10
 
11
11
  # Planning & Task Breakdown
@@ -6,7 +6,7 @@ version: 2.1.0
6
6
  tags: [code-intelligence, search, cli, srcwalk]
7
7
  dependencies: []
8
8
  agent_types: [planner, worker, reviewer, explorer]
9
- tools: [bash, srcwalk_search, srcwalk_read, srcwalk_files, srcwalk_deps, srcwalk_map, srcwalk_callers, srcwalk_callees, srcwalk_flow, srcwalk_impact]
9
+ tools: [bash, srcwalk_read, srcwalk_deps, srcwalk_map, srcwalk_callers, srcwalk_callees, srcwalk_flow, srcwalk_impact]
10
10
  ---
11
11
 
12
12
  # Srcwalk — Code Navigation
@@ -41,9 +41,7 @@ Do not pipe, truncate, or summarize `srcwalk guide`.
41
41
 
42
42
  | Tool | Srcwalk command | Purpose |
43
43
  |---|---|---|
44
- | `srcwalk_search` | `srcwalk discover` / `srcwalk trace callers` | AST-aware symbol/content/regex/callers search |
45
44
  | `srcwalk_read` | `srcwalk <path>` | Smart file reading: outline or full with sections |
46
- | `srcwalk_files` | `srcwalk discover --as file` | Glob file finding with token estimates, grouped by dir |
47
45
  | `srcwalk_deps` | `srcwalk deps` + exact import scan | Blast-radius: importers + dep-aware dependents (v1.0.0) |
48
46
 
49
47
  ### Extended analysis tools
@@ -65,9 +63,9 @@ Do not pipe, truncate, or summarize `srcwalk guide`.
65
63
  | Jump to exact line | `srcwalk_read({ path: "file:42" })` |
66
64
  | Read a line range | `srcwalk_read({ path: "file:44-89" })` — v1.0.0 shortcut |
67
65
  | Read by symbol name | `srcwalk_read({ section: "symbolName" })` |
68
- | Find definition/usages/text/glob | `srcwalk_search` |
69
- | Find files by glob | `srcwalk_files` |
70
- | Multi-symbol search | `srcwalk_search({ query: "A, B, C" })` |
66
+ | Find patterns and symbols | `grep` (exact), `csearch` (multi-keyword) |
67
+ | Find files by glob | `glob` |
68
+ | Multi-symbol search | `grep({ pattern: "A|B|C" })` |
71
69
  | Who directly calls this? | `srcwalk_callers` |
72
70
  | Who reaches this transitively? | `srcwalk_callers({ depth: 2 })` |
73
71
  | What does this call? | `srcwalk_callees` |
@@ -82,7 +80,7 @@ Do not pipe, truncate, or summarize `srcwalk guide`.
82
80
 
83
81
  ```
84
82
  srcwalk_map({ scope: "." })
85
- srcwalk_search({ query: "likely_symbol", scope: "src" })
83
+ grep({ pattern: "likely_symbol", path: "src/" })
86
84
  srcwalk_read({ path: "src/file.ts:42" }) // jump to line
87
85
  srcwalk_read({ path: "src/file.ts:44-89" }) // range shortcut (v1.0.0)
88
86
  ```
@@ -100,9 +98,9 @@ Prefer outline/section reads before `full: true`.
100
98
  ### Find and drill into symbols
101
99
 
102
100
  ```
103
- srcwalk_search({ query: "handleAuth", scope: "src" })
104
- srcwalk_search({ query: "A, B, C", scope: "src" }) // multi-symbol
105
- srcwalk_search({ query: "handleAuth", expand: 2 }) // inline source
101
+ grep({ pattern: "handleAuth", path: "src/" })
102
+ grep({ pattern: "A|B|C", path: "src/" }) // multi-symbol
103
+ csearch({ query: "auth token login middleware" }) // multi-keyword chunk search
106
104
  ```
107
105
 
108
106
  ### Trace call graph
@@ -123,7 +121,7 @@ srcwalk_callees({ symbol: "handleAuth", depth: 2, scope: "src" }) // trans
123
121
  srcwalk_flow({ symbol: "handleAuth", scope: "src" })
124
122
  ```
125
123
 
126
- Use `srcwalk_search({ kind: "callers" })` for quick single-hop. Use `srcwalk_callers` when you need depth, filters, or aggregation.
124
+ Use `grep` for quick single-hop searches. Use `srcwalk_callers` when you need depth, filters, or aggregation.
127
125
 
128
126
  > Note: `--count-by` and `--depth` are mutually exclusive in `srcwalk_callers` — use one or the other, not both.
129
127
 
@@ -143,8 +141,7 @@ srcwalk_impact({ symbol: "handleAuth", scope: "src" }) // heuristic; follow up
143
141
 
144
142
  ## Critical Rules
145
143
 
146
- - **Do NOT** use built-in `read`/`grep`/`find` when srcwalk_* tools can answer
147
- - **Do NOT** re-read files already shown in expanded `srcwalk_search` results
144
+ - **Do NOT** use built-in `read`/`find` when srcwalk_* tools can answer; `grep` is preferred for text searches
148
145
  - `srcwalk_impact` is heuristic, not proof — verify with `srcwalk_callers` or exact reads
149
146
  - `srcwalk_flow` may collapse nested/fluent chains — drill with `srcwalk_callees({ detailed: true })` when inner calls matter
150
147
  - Follow `> Next:` footers in output — they suggest the best next command
@@ -5,7 +5,7 @@ version: 1.0.0
5
5
  tags: [architecture, domain-driven-design, ai-workflow]
6
6
  dependencies: []
7
7
  agent_types: [planner, scout]
8
- tools: [srcwalk_search, grep, memory]
8
+ tools: [grep, memory]
9
9
  ---
10
10
 
11
11
  # Ubiquitous Language