Reference
Detector API v4
Everything @wellactually/sdk/v4 exports, for detector.ts and the files it imports.
The types are the source of truth, and your editor shows them on hover.
import { findUp, findWord, lineAt, matchesGlob, matchesGlobs, mentioned, onCommand, onDelete, onOutput, onPrompt, onRead, onWrite } from "@wellactually/sdk/v4";
import type { CommandEvent, DeleteEvent, File, Finding, OutputEvent, PromptEvent, ReadEvent, RemovedLine, TextFinding, WriteEvent, WrittenLine } from "@wellactually/sdk/v4";Handlers#
Each registers a handler.
Call them at the top level of detector.ts, as often as you need.
| Function | Runs | Takes |
|---|---|---|
onWrite(options) | After the agent wrote or edited a file. | { globs?, detect(event: WriteEvent) } |
onRead(options) | After the agent read a file. | { globs?, detect(event: ReadEvent) } |
onDelete(options) | After the agent deleted a file. | { globs?, detect(event: DeleteEvent) } |
onPrompt(options) | When you sent a message, or the agent briefed a subagent. | { detect(event: PromptEvent) } |
onCommand(options) | Before a shell command runs. | { detect(event: CommandEvent) } |
onOutput(options) | After a shell command ran. | { detect(event: OutputEvent) } |
detect returns findings: usually a generator, written *detect(event) { ... }, or any other iterable, or nothing.
globs are patterns for event.file.relativePath: **, *, ?, {a,b}, and ! in front to take files out.
Only onWrite, onRead and onDelete take globs.
Events#
Every event has these fields:
| Field | Type | What it is |
|---|---|---|
event | "write", "read", "delete", "prompt", "command" or "output" | Which event it is. |
text | string | The file's content, the prompt, the command line, or what the command printed. |
lines | string[] | text split into lines. A finding's line counts in here. |
project | string or null | The nearest folder upwards with a .wellactually.json or a .git, not the home folder. Often null, so do not rely on it. |
And each kind adds its own:
| Event | Adds |
|---|---|
WriteEvent | file: File, written: WrittenLine[], removed: RemovedLine[] |
ReadEvent | file: File |
DeleteEvent | file: File as it was before it was deleted, removed: RemovedLine[] with every line of it |
PromptEvent | cwd: string |
CommandEvent | cwd: string, command: string (the same as text) |
OutputEvent | cwd: string, command: string (the command that printed text) |
cwd is the folder the session runs in, as an absolute path.
File#
| Field | Type | What it is |
|---|---|---|
path | string | Absolute path, with forward slashes. |
relativePath | string | The path from the project root. Globs match this. |
name | string | The file name, config_loader.dart. |
ext | string | The extension with its dot, lower case, .dart. Empty when there is none. |
content | string | The whole file, after the write, or as it was before it was deleted. |
lines | string[] | content split into lines. |
isNew | boolean | The write created the file. Always false on a read and a deletion. |
WrittenLine#
| Field | Type | What it is |
|---|---|---|
line | number | The 1-based line in the file. |
text | string | The line as it is in the file, untrimmed, comments and strings included. |
A handler of onWrite may also yield a WrittenLine as it is.
It becomes a finding on its line.
RemovedLine#
| Field | Type | What it is |
|---|---|---|
line | number | The 1-based line it had in the file before the change. |
text | string | The line as it was, untrimmed. |
removed | true | Always true. It makes a removed line that is yielded as it is a finding on that line. |
A line the agent changed is removed as it was and written as it is now.
removed is empty when the host cannot tell what the file was before, as for a new file.
A handler of onWrite or onDelete may yield a RemovedLine as it is, and the agent is shown "removed line 3: ..." with its text.
Because a deletion has removed too, one helper serves both:
function* removedTests(event: WriteEvent | DeleteEvent) {
for (const removed of event.removed) {
if (removed.text.includes("test(")) {
yield removed;
}
}
}
onWrite({ globs: ["test/**"], detect: removedTests });
onDelete({ globs: ["test/**"], detect: removedTests });Finding#
What onWrite, onRead, onDelete and onOutput yield.
Nothing in it is required.
| Field | Type | What it is |
|---|---|---|
line | number | The 1-based line of the event's text. Without it, a file handler's finding points at the file. |
endLine | number | The last line, when the finding spans several. Needs line. |
match | string | Text that occurs word for word in the event, on the named lines, and does not span lines. At most 300 characters. |
file | string | Another file the finding is about, from the project root or absolute. It exists, or occurs word for word in the event. At most 300 characters. |
depth | "pointer" or "full" | "pointer" shows only the summary. Leave it out for "full". |
removed | boolean | line and endLine name removed lines, by the line each had before, and match occurs in them. Every line from line to endLine is in event.removed. Needs line. |
TextFinding is what onPrompt and onCommand yield: the same without line and endLine.
A detail that is wrong is left out and the finding still fires.
Findings explains each field with examples.
Helpers#
findUp(name, from)#
findUp(name: string, from: string): string | nullThe nearest file called name, in from or a folder above it, as an absolute path, or null.
findUp("pubspec.yaml", event.cwd); // for a prompt, a command or an output
findUp("pubspec.yaml", path.dirname(event.file.path)); // for a filementioned(event, names)#
mentioned(event: { text: string }, names: readonly string[]): TextFinding[]A pointer finding for the first of names that the event's text mentions, or an empty list.
A name counts where it stands alone, in any case, and with or without the separators people put inside names: rxdart also finds rx_dart, rx-dart and RxDart.
A space in a name stands for any whitespace.
Pass it on with yield*.
yield* mentioned(event, ["late keyword", "late final", "LateInitializationError"]);findWord(text, word, from?)#
findWord(text: string, word: string, from?: number): numberThe index of word in text where it stands alone, or -1.
findWord("isolate", "late") is -1, and findWord("late final x", "late") is 0.
lineAt(text, offset)#
lineAt(text: string, offset: number): numberThe 1-based line of a character offset, for a detector that searches the whole event.text with a regular expression.
for (const match of event.text.matchAll(/Future\.delayed\(/g)) {
yield { line: lineAt(event.text, match.index), match: "Future.delayed(" };
}matchesGlob(path, pattern) and matchesGlobs(path, globs)#
matchesGlob(path: string, pattern: string): boolean
matchesGlobs(path: string, globs: readonly string[]): booleanThe glob matching the handlers use, for a detector that decides about other paths itself, such as the files readdirSync lists.
A pattern without a slash matches the file name in any folder.
The EVENTS constant#
["write", "read", "delete", "prompt", "command", "output"], the names of the events.
node:fs and node:path#
A detector may import node:fs and node:path.
fs has readFileSync, existsSync, statSync and readdirSync, read-only and synchronous.
See Reading project files.
Versions of the API#
| Version | What it is |
|---|---|
| 1 | One detect(ctx) function. No longer run. |
| 2 | Absolute paths, relativePath, isNew, project, and reading files. No longer run. |
| 3 | A handler per event, typed events, globs on the file handlers, findings without text of their own, and the output event. Still run. |
| 4 | The lines a write removed, event.removed, and onDelete for a deleted file. Current. |
A detector says which version it is written against by its import path, @wellactually/sdk/v4.
A detector on version 3 keeps running as it was: it never sees removed lines or deletions.
Detector API versions lists what each version added, and everything version 3 offers.