dirsql 0.4.12 → 0.4.14
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/dist/cli/main.js +68 -16
- package/dist/platforms.d.ts +0 -8
- package/dist/platforms.js +10 -31
- package/docs/howto/extract-from-contents.md +0 -2
- package/docs/howto/load-extension.md +0 -2
- package/docs/howto/write-a-plugin.md +15 -43
- package/docs/reference/cli.md +3 -4
- package/docs/reference/config.md +5 -15
- package/docs/reference/hooks.md +14 -52
- package/docs/reference/http-api.md +2 -11
- package/package.json +7 -11
- package/dist/cli/platform-key.d.ts +0 -1
- package/dist/cli/platform-key.js +0 -5
- package/dist/cli/resolve-binary.d.ts +0 -5
- package/dist/cli/resolve-binary.js +0 -25
- package/docs/howto/search-by-meaning.md +0 -153
package/dist/cli/main.js
CHANGED
|
@@ -1,23 +1,75 @@
|
|
|
1
|
-
// Launcher entry:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
// Launcher entry: run the CLI in-process through the napi addon.
|
|
2
|
+
//
|
|
3
|
+
// The launcher is a transparent forwarder — every argv (including any
|
|
4
|
+
// subcommand) goes straight to the core's `runCli`, which owns subcommand
|
|
5
|
+
// dispatch and clap-rejects unknown ones. Nothing is spawned: the same
|
|
6
|
+
// `@dirsql/lib-*` addon the SDK loads carries the CLI, so a package ships one
|
|
7
|
+
// copy of the core instead of two (#739).
|
|
8
|
+
import { mainInProcess } from "bin-shim";
|
|
9
|
+
import { loadNativeCore } from "../load-native-core.js";
|
|
6
10
|
import { die } from "./die.js";
|
|
7
|
-
import { resolveBinary } from "./resolve-binary.js";
|
|
8
11
|
import { withResolvedExtensions } from "./resolve-config-extensions.js";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve `runCli` off the same addon the SDK loads.
|
|
14
|
+
*
|
|
15
|
+
* bin-shim can resolve an addon itself, but its default naming is
|
|
16
|
+
* `@{scope}/lib-{platform}-{arch}`, which cannot express the ABI suffix
|
|
17
|
+
* dirsql's packages carry (`@dirsql/lib-linux-x64-gnu`). Reusing
|
|
18
|
+
* `loadNativeCore` keeps one resolution rule for the SDK and the CLI —
|
|
19
|
+
* including its dev fallback to a locally built `dirsql.node` — so a
|
|
20
|
+
* monorepo checkout and a published install take the same path.
|
|
21
|
+
*/
|
|
22
|
+
function resolveRunCli() {
|
|
23
|
+
const core = loadNativeCore();
|
|
24
|
+
if (typeof core.runCli !== "function") {
|
|
25
|
+
throw new Error("dirsql: the native addon has no callable `runCli` export; " +
|
|
26
|
+
"it was built without the `cli` feature.");
|
|
27
|
+
}
|
|
28
|
+
return core.runCli;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Keep Ctrl-C fatal while the CLI runs in-process.
|
|
32
|
+
*
|
|
33
|
+
* The core installs tokio signal handlers for `dirsql server`, and
|
|
34
|
+
* signal-hook (which tokio uses) *chains*: it runs its own actions and then
|
|
35
|
+
* the handler installed before it. CPython installs `default_int_handler` at
|
|
36
|
+
* startup, so the pip launcher gets a terminating disposition for free —
|
|
37
|
+
* bare Node leaves `SIG_DFL`, which signal-hook does not emulate, so the
|
|
38
|
+
* signal is swallowed and the process becomes SIGKILL-only (measured: a
|
|
39
|
+
* probe ignored SIGTERM for 143 seconds).
|
|
40
|
+
*
|
|
41
|
+
* Registering a JS listener first gives signal-hook a real prior handler to
|
|
42
|
+
* chain to. This cannot be fixed after the fact: `removeAllListeners` only
|
|
43
|
+
* drops JS listeners and cannot touch a disposition installed by native
|
|
44
|
+
* code, and `unregister_signal` leaves the OS handler in place. One listener
|
|
45
|
+
* is the whole remedy — no `unsafe`, no `sigaction`.
|
|
46
|
+
*/
|
|
47
|
+
function keepSignalsFatal() {
|
|
48
|
+
// While the core is running, its own handler drives shutdown and this
|
|
49
|
+
// listener is the tail of the chain; it matters for signals arriving
|
|
50
|
+
// outside that window, where exiting is exactly right.
|
|
51
|
+
process.on("SIGINT", () => process.exit(130));
|
|
52
|
+
process.on("SIGTERM", () => process.exit(143));
|
|
53
|
+
}
|
|
9
54
|
export async function main(argv = process.argv.slice(2)) {
|
|
10
|
-
|
|
11
|
-
// Resolve any package-name extensions in a TOML config here (the
|
|
55
|
+
keepSignalsFatal();
|
|
56
|
+
// Resolve any package-name extensions in a TOML config here (the core
|
|
12
57
|
// can't) and pass them as `--extension` flags; a no-op otherwise.
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
58
|
+
const resolved = await withResolvedExtensions(argv);
|
|
59
|
+
let code;
|
|
60
|
+
try {
|
|
61
|
+
code = await mainInProcess({
|
|
62
|
+
argv: resolved,
|
|
63
|
+
binaryName: "dirsql",
|
|
64
|
+
// Resolution is ours (see `resolveRunCli`), so bin-shim never consults
|
|
65
|
+
// its own naming; `from` only satisfies the shared options type.
|
|
66
|
+
from: import.meta.url,
|
|
67
|
+
runCli: resolveRunCli(),
|
|
68
|
+
});
|
|
18
69
|
}
|
|
19
|
-
|
|
20
|
-
|
|
70
|
+
catch (e) {
|
|
71
|
+
die(e instanceof Error ? e.message : String(e), 1);
|
|
72
|
+
return;
|
|
21
73
|
}
|
|
22
|
-
process.exit(
|
|
74
|
+
process.exit(code);
|
|
23
75
|
}
|
package/dist/platforms.d.ts
CHANGED
|
@@ -5,8 +5,6 @@ export interface Platform {
|
|
|
5
5
|
nodePlatform: NodeJS.Platform;
|
|
6
6
|
/** Node `process.arch` value for this target. */
|
|
7
7
|
nodeArch: NodeJS.Architecture;
|
|
8
|
-
/** CLI sub-package name (`@dirsql/cli-<slug>`). */
|
|
9
|
-
name: string;
|
|
10
8
|
/** napi library sub-package name (`@dirsql/lib-<slug>`). */
|
|
11
9
|
libName: string;
|
|
12
10
|
/** Wheel-style `os` constraint for the sub-package's package.json. */
|
|
@@ -15,14 +13,8 @@ export interface Platform {
|
|
|
15
13
|
cpu: string[];
|
|
16
14
|
/** libc constraint (Linux only). */
|
|
17
15
|
libc?: string[];
|
|
18
|
-
/** Archive extension cargo-dist emits for this target. */
|
|
19
|
-
ext: "tar.xz" | "zip";
|
|
20
|
-
/** Whether the binary has a `.exe` suffix on this platform. */
|
|
21
|
-
exe?: boolean;
|
|
22
16
|
}
|
|
23
17
|
export declare const PLATFORMS: readonly Platform[];
|
|
24
|
-
/** Node `${platform}-${arch}` → `@dirsql/cli-*` sub-package name. */
|
|
25
|
-
export declare function nodeTriples(): Record<string, string>;
|
|
26
18
|
/** Node `${platform}-${arch}` → `@dirsql/lib-*` napi sub-package name. */
|
|
27
19
|
export declare function libTriples(): Record<string, string>;
|
|
28
20
|
/**
|
package/dist/platforms.js
CHANGED
|
@@ -1,82 +1,61 @@
|
|
|
1
1
|
// Single source of truth for the target platforms `dirsql` publishes.
|
|
2
2
|
//
|
|
3
|
-
// Every target triple generates
|
|
3
|
+
// Every target triple generates ONE npm sub-package: `@dirsql/lib-<slug>`,
|
|
4
|
+
// holding the napi-rs `.node` addon. It backs both layers — the TypeScript
|
|
5
|
+
// SDK loads it via `loadNativeCore()`, and since #739 the `dirsql` CLI runs
|
|
6
|
+
// in-process through its `runCli` export rather than spawning a binary. The
|
|
7
|
+
// second family, `@dirsql/cli-<slug>`, shipped a redundant copy of the same
|
|
8
|
+
// core and is gone.
|
|
4
9
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// when a user runs the `dirsql` CLI.
|
|
8
|
-
// 2. `@dirsql/lib-<slug>` — holds the napi-rs `.node` addon used by the
|
|
9
|
-
// TypeScript SDK. Consumed at runtime by `loadNativeCore()` in
|
|
10
|
-
// `src/index.ts` when a user `import`s from `dirsql`.
|
|
10
|
+
// The sub-packages are `optionalDependencies` of the main `dirsql` package,
|
|
11
|
+
// so npm/pnpm install only the one matching the host's OS/arch.
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
// package
|
|
14
|
-
//
|
|
15
|
-
// `nodeTriples()` / `libTriples()` return `${process.platform}-${process.arch}`
|
|
16
|
-
// → sub-package-name maps for the respective layer.
|
|
13
|
+
// `libTriples()` returns a `${process.platform}-${process.arch}` →
|
|
14
|
+
// sub-package-name map.
|
|
17
15
|
export const PLATFORMS = [
|
|
18
16
|
{
|
|
19
17
|
triple: "x86_64-unknown-linux-gnu",
|
|
20
18
|
nodePlatform: "linux",
|
|
21
19
|
nodeArch: "x64",
|
|
22
|
-
name: "@dirsql/cli-linux-x64-gnu",
|
|
23
20
|
libName: "@dirsql/lib-linux-x64-gnu",
|
|
24
21
|
os: ["linux"],
|
|
25
22
|
cpu: ["x64"],
|
|
26
23
|
libc: ["glibc"],
|
|
27
|
-
ext: "tar.xz",
|
|
28
24
|
},
|
|
29
25
|
{
|
|
30
26
|
triple: "aarch64-unknown-linux-gnu",
|
|
31
27
|
nodePlatform: "linux",
|
|
32
28
|
nodeArch: "arm64",
|
|
33
|
-
name: "@dirsql/cli-linux-arm64-gnu",
|
|
34
29
|
libName: "@dirsql/lib-linux-arm64-gnu",
|
|
35
30
|
os: ["linux"],
|
|
36
31
|
cpu: ["arm64"],
|
|
37
32
|
libc: ["glibc"],
|
|
38
|
-
ext: "tar.xz",
|
|
39
33
|
},
|
|
40
34
|
{
|
|
41
35
|
triple: "x86_64-apple-darwin",
|
|
42
36
|
nodePlatform: "darwin",
|
|
43
37
|
nodeArch: "x64",
|
|
44
|
-
name: "@dirsql/cli-darwin-x64",
|
|
45
38
|
libName: "@dirsql/lib-darwin-x64",
|
|
46
39
|
os: ["darwin"],
|
|
47
40
|
cpu: ["x64"],
|
|
48
|
-
ext: "tar.xz",
|
|
49
41
|
},
|
|
50
42
|
{
|
|
51
43
|
triple: "aarch64-apple-darwin",
|
|
52
44
|
nodePlatform: "darwin",
|
|
53
45
|
nodeArch: "arm64",
|
|
54
|
-
name: "@dirsql/cli-darwin-arm64",
|
|
55
46
|
libName: "@dirsql/lib-darwin-arm64",
|
|
56
47
|
os: ["darwin"],
|
|
57
48
|
cpu: ["arm64"],
|
|
58
|
-
ext: "tar.xz",
|
|
59
49
|
},
|
|
60
50
|
{
|
|
61
51
|
triple: "x86_64-pc-windows-msvc",
|
|
62
52
|
nodePlatform: "win32",
|
|
63
53
|
nodeArch: "x64",
|
|
64
|
-
name: "@dirsql/cli-win32-x64-msvc",
|
|
65
54
|
libName: "@dirsql/lib-win32-x64-msvc",
|
|
66
55
|
os: ["win32"],
|
|
67
56
|
cpu: ["x64"],
|
|
68
|
-
ext: "zip",
|
|
69
|
-
exe: true,
|
|
70
57
|
},
|
|
71
58
|
];
|
|
72
|
-
/** Node `${platform}-${arch}` → `@dirsql/cli-*` sub-package name. */
|
|
73
|
-
export function nodeTriples() {
|
|
74
|
-
const out = {};
|
|
75
|
-
for (const p of PLATFORMS) {
|
|
76
|
-
out[`${p.nodePlatform}-${p.nodeArch}`] = p.name;
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
59
|
/** Node `${platform}-${arch}` → `@dirsql/lib-*` napi sub-package name. */
|
|
81
60
|
export function libTriples() {
|
|
82
61
|
const out = {};
|
|
@@ -79,8 +79,6 @@ SQLite value mapping is under
|
|
|
79
79
|
|
|
80
80
|
- The command re-runs on every startup and on every change to a matched
|
|
81
81
|
file. If it is expensive, [keep the index across restarts](./persist.md).
|
|
82
|
-
- The flagship use of `on-file` — computing embeddings — is
|
|
83
|
-
[Search documents by meaning](./search-by-meaning.md).
|
|
84
82
|
- Embedding `dirsql` in a program instead? The SDK's `on_file` callback
|
|
85
83
|
fills the same role in-process — see
|
|
86
84
|
[Embed `dirsql` in your application](./embed.md).
|
|
@@ -85,5 +85,3 @@ interpreter to resolve package names with
|
|
|
85
85
|
- Embedding `dirsql` in a program? The SDK constructor takes the same
|
|
86
86
|
specs via its `extensions` parameter
|
|
87
87
|
([SDK reference](../reference/sdk.md#constructor)).
|
|
88
|
-
- The payoff use case — `sqlite-vec` powering semantic search — is
|
|
89
|
-
[Search documents by meaning](./search-by-meaning.md).
|
|
@@ -24,8 +24,7 @@ dirsql-embeddings/
|
|
|
24
24
|
└── dirsql_embeddings/
|
|
25
25
|
├── __init__.py
|
|
26
26
|
├── dirsql.toml # the config fragment
|
|
27
|
-
|
|
28
|
-
└── search.py # pre-query hook
|
|
27
|
+
└── embed.py # on-file hook
|
|
29
28
|
```
|
|
30
29
|
|
|
31
30
|
The entry point maps a **source label** (the name) to the **module that
|
|
@@ -55,8 +54,7 @@ structurally **identical to a user config**. It may declare
|
|
|
55
54
|
[`[[table]]`](../reference/config.md#table) (with
|
|
56
55
|
[`on-file`](../reference/hooks.md#on-file)),
|
|
57
56
|
[`[[dirsql.extension]]`](../reference/config.md#dirsql-extension),
|
|
58
|
-
[`ignore`](../reference/config.md#dirsql-keys),
|
|
59
|
-
[`pre-query`/`post-query`](../reference/hooks.md#pre-query), and
|
|
57
|
+
[`ignore`](../reference/config.md#dirsql-keys), and
|
|
60
58
|
[`hook-timeout`](../reference/hooks.md#timeout).
|
|
61
59
|
|
|
62
60
|
There are **no plugin-specific keys and no plugin-specific restrictions**. The
|
|
@@ -96,17 +94,14 @@ matter most for a published plugin:
|
|
|
96
94
|
## Worked example: an embeddings plugin
|
|
97
95
|
|
|
98
96
|
Here is the whole plugin — vector search over a directory of notes, buildable
|
|
99
|
-
in about
|
|
100
|
-
[Search documents by meaning](./search-by-meaning.md): the
|
|
97
|
+
in about forty lines. It composes two pieces: the
|
|
101
98
|
[`sqlite-vec`](https://github.com/asg017/sqlite-vec) extension for the
|
|
102
|
-
distance math, an `on-file` command to embed each file
|
|
103
|
-
command to embed the question.
|
|
99
|
+
distance math, and an `on-file` command to embed each file.
|
|
104
100
|
|
|
105
101
|
The fragment, `src/dirsql_embeddings/dirsql.toml`:
|
|
106
102
|
|
|
107
103
|
```toml
|
|
108
104
|
[dirsql]
|
|
109
|
-
pre-query = "uv run --with model2vec python search.py {args}"
|
|
110
105
|
hook-timeout = 300 # headroom for the first-run model download
|
|
111
106
|
|
|
112
107
|
[[dirsql.extension]]
|
|
@@ -140,46 +135,23 @@ row = {"path": os.path.relpath(path, root), "text": text,
|
|
|
140
135
|
print(json.dumps([row]))
|
|
141
136
|
```
|
|
142
137
|
|
|
143
|
-
`
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
import sys
|
|
149
|
-
|
|
150
|
-
from model2vec import StaticModel
|
|
151
|
-
|
|
152
|
-
body = json.loads(sys.argv[1])
|
|
153
|
-
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
|
|
154
|
-
vector = model.encode([body["q"]])[0]
|
|
155
|
-
needle = json.dumps([round(float(x), 6) for x in vector])
|
|
156
|
-
print(
|
|
157
|
-
"SELECT path, ROUND(vec_distance_cosine(embedding, '%s'), 3) AS distance "
|
|
158
|
-
"FROM notes ORDER BY distance LIMIT 3" % needle
|
|
159
|
-
)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
::: warning The hook owns SQL safety
|
|
163
|
-
Whatever SQL `pre-query` prints is executed as-is. Here the interpolated
|
|
164
|
-
value is a numeric vector the script itself produced; never splice raw request
|
|
165
|
-
text into SQL. See [`pre-query`](../reference/hooks.md#pre-query).
|
|
166
|
-
:::
|
|
167
|
-
|
|
168
|
-
The relative `embed.py` / `search.py` above resolve against the fragment
|
|
169
|
-
directory, which is convenient during development. For a published plugin,
|
|
170
|
-
promote them to console scripts (`[project.scripts]` → `embed-file`,
|
|
171
|
-
`embed-search`) so the commands carry their own interpreter and dependencies
|
|
172
|
-
and no longer depend on `uv run --with`.
|
|
138
|
+
The relative `embed.py` above resolves against the fragment directory, which
|
|
139
|
+
is convenient during development. For a published plugin, promote it to a
|
|
140
|
+
console script (`[project.scripts]` → `embed-file`) so the command carries
|
|
141
|
+
its own interpreter and dependencies and no longer depends on
|
|
142
|
+
`uv run --with`.
|
|
173
143
|
|
|
174
144
|
Once the package is installed alongside the launcher, its `notes` table is
|
|
175
145
|
queryable with no config edits:
|
|
176
146
|
|
|
177
147
|
```bash
|
|
178
|
-
uvx --with dirsql-embeddings dirsql query
|
|
148
|
+
uvx --with dirsql-embeddings dirsql query \
|
|
149
|
+
"SELECT path FROM notes ORDER BY vec_distance_cosine(embedding, '[0.1, ...]') LIMIT 3"
|
|
179
150
|
```
|
|
180
151
|
|
|
181
152
|
The launcher discovers the installed plugin, composes its fragment, and the
|
|
182
|
-
`
|
|
153
|
+
`notes` table (with `vec_distance_cosine()` from the loaded extension) is
|
|
154
|
+
available to the query.
|
|
183
155
|
|
|
184
156
|
## SDK-style convention: expose the config
|
|
185
157
|
|
|
@@ -230,8 +202,8 @@ Discovery is deliberately narrow. Know exactly who does what:
|
|
|
230
202
|
[convention above](#sdk-style-convention-expose-the-config)).
|
|
231
203
|
- **Name collisions are a hard error.** Because fragments compose like any
|
|
232
204
|
[multiple configs](../reference/config.md#composing-multiple-configs), two
|
|
233
|
-
plugins (or a plugin and your config) defining a table of the same name
|
|
234
|
-
|
|
205
|
+
plugins (or a plugin and your config) defining a table of the same name
|
|
206
|
+
fail loudly, naming the conflict. It is never a silent
|
|
235
207
|
last-writer-wins.
|
|
236
208
|
|
|
237
209
|
::: warning Config flags are subcommand-local
|
package/docs/reference/cli.md
CHANGED
|
@@ -78,8 +78,8 @@ Config flags are subcommand-local: pass them after `server`
|
|
|
78
78
|
|
|
79
79
|
- Per-query timeout: **30 seconds**. A query exceeding it returns
|
|
80
80
|
`408 Request Timeout`.
|
|
81
|
-
-
|
|
82
|
-
|
|
81
|
+
- `on-file` command hooks default to a **30-second** timeout each,
|
|
82
|
+
overridable with the config key
|
|
83
83
|
[`[dirsql].hook-timeout`](./config.md#dirsql-keys).
|
|
84
84
|
|
|
85
85
|
### Configless mode
|
|
@@ -166,8 +166,7 @@ uses**, so behavior is identical to `POST /query` by construction:
|
|
|
166
166
|
(`--persist=/path`) so it does not swallow the SQL argument.
|
|
167
167
|
- **`--no-ignore`** is honored: path-tables in the query scan files a
|
|
168
168
|
`.gitignore` would hide. See [Skip rules](./path-tables.md#skip-rules).
|
|
169
|
-
-
|
|
170
|
-
[`post-query`](./hooks.md#post-query)) and the
|
|
169
|
+
- **`on-file` hooks** and the
|
|
171
170
|
[`[dirsql].hook-timeout`](./config.md#dirsql-keys) apply identically.
|
|
172
171
|
- The **30-second query timeout**, the **read-only rule**, and the
|
|
173
172
|
`_dirsql_*` **internal-table denial** apply identically. A rejected read
|
package/docs/reference/config.md
CHANGED
|
@@ -23,9 +23,7 @@ the config file's location. See [`--config`](./cli.md#flags).
|
|
|
23
23
|
| Key | Type | Default | Description |
|
|
24
24
|
|---|---|---|---|
|
|
25
25
|
| `ignore` | array of strings | `[]` | Glob patterns matched against root-relative paths. Matched files are skipped entirely — excluded from the initial scan and from watch events. |
|
|
26
|
-
| `
|
|
27
|
-
| `post-query` | string | none | Server-wide command hook: each successful `POST /query` result set is handed to this command (as a JSON array on stdin, and as `{args}` up to 96 KiB), and the JSON body it prints is returned instead of the bare row array. CLI server only; the SDKs ignore it. Must be non-empty. See [Command hooks](./hooks.md#post-query). |
|
|
28
|
-
| `hook-timeout` | integer (seconds) | `30` | One global per-run timeout for every command hook — `on-file`, `pre-query`, and `post-query` alike. Positive whole seconds; zero and negative values are a config error. See [Command hooks](./hooks.md#timeout). |
|
|
26
|
+
| `hook-timeout` | integer (seconds) | `30` | One global per-run timeout for every `on-file` command hook run. Positive whole seconds; zero and negative values are a config error. See [Command hooks](./hooks.md#timeout). |
|
|
29
27
|
|
|
30
28
|
The top-level `.dirsql/` directory under the root is always excluded from
|
|
31
29
|
scanning, whether or not it appears in `ignore` — it is reserved for
|
|
@@ -147,15 +145,10 @@ The configs load and merge in **argv order**:
|
|
|
147
145
|
|
|
148
146
|
- **`[[table]]`, `ignore`, and `[[dirsql.extension]]` entries accumulate** across
|
|
149
147
|
all configs, in order.
|
|
150
|
-
- **Each config's `on-file
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
wherever it lives.
|
|
155
|
-
- **`pre-query` / `post-query` hooks chain FIFO**: the request body flows through
|
|
156
|
-
each `pre-query` stage in order to the final SQL, and the result rows flow
|
|
157
|
-
through each `post-query` stage to the response. See the
|
|
158
|
-
[hook contract](./hooks.md).
|
|
148
|
+
- **Each config's `on-file` hooks run from that config file's own
|
|
149
|
+
directory**, under that config's own [`hook-timeout`](#dirsql-keys) — so a
|
|
150
|
+
relative command like `on-file = "sh ./extract.sh"` resolves against the
|
|
151
|
+
config that declared it, wherever it lives.
|
|
159
152
|
- Each config is **validated on its own** (the [parse errors](#parse-errors)
|
|
160
153
|
below apply per file). There is no cross-file merge validation, with one
|
|
161
154
|
structural exception: **two configs defining a table of the same name is an
|
|
@@ -181,7 +174,6 @@ SDKs raise/reject) when:
|
|
|
181
174
|
> `[[table]] '**/*.md' has no on-file hook, so every row would be all-NULL. Add an `on-file` hook that emits the columns, or, for stat columns with no code, query the path directly: `FROM './'``
|
|
182
175
|
|
|
183
176
|
- A `[[dirsql.extension]]` entry omits `path`, or `path` is empty.
|
|
184
|
-
- `pre-query` or `post-query` is present but empty/whitespace.
|
|
185
177
|
- `hook-timeout` is zero or negative.
|
|
186
178
|
|
|
187
179
|
## Full example
|
|
@@ -189,8 +181,6 @@ SDKs raise/reject) when:
|
|
|
189
181
|
```toml
|
|
190
182
|
[dirsql]
|
|
191
183
|
ignore = ["node_modules/**", ".git/**", "dist/**"]
|
|
192
|
-
pre-query = "uv run python to_sql.py {args}"
|
|
193
|
-
post-query = "jq -c '{results: .}'"
|
|
194
184
|
hook-timeout = 120
|
|
195
185
|
|
|
196
186
|
[[dirsql.extension]]
|
package/docs/reference/hooks.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Command hooks
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The `on-file` [config key](./config.md) (per `[[table]]`, also available as
|
|
4
|
+
the [`--on-file` flag](./cli.md#on-file-command) on `dirsql query`) runs an
|
|
5
|
+
external command under the execution contract below.
|
|
6
6
|
|
|
7
7
|
## Execution contract
|
|
8
8
|
|
|
@@ -32,8 +32,8 @@ occurrence, within whole argv tokens, in a single left-to-right pass:
|
|
|
32
32
|
`{…}` is inert.
|
|
33
33
|
- An unrecognized `{…}` is left literal.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
[
|
|
35
|
+
The available placeholders are listed under the
|
|
36
|
+
[`on-file` contract](#on-file-contract) below.
|
|
37
37
|
|
|
38
38
|
### Working directory and environment
|
|
39
39
|
|
|
@@ -62,7 +62,7 @@ compactly on one line.
|
|
|
62
62
|
|
|
63
63
|
Every hook run is bounded by a **30-second** default timeout. A run
|
|
64
64
|
exceeding it is killed and treated as a failure. One global config key
|
|
65
|
-
raises (or tightens) the bound
|
|
65
|
+
raises (or tightens) the bound:
|
|
66
66
|
|
|
67
67
|
```toml
|
|
68
68
|
[dirsql]
|
|
@@ -80,19 +80,16 @@ A hook run fails when the command:
|
|
|
80
80
|
stderr tail are reported),
|
|
81
81
|
- exceeds the timeout (killed; stderr tail reported),
|
|
82
82
|
- exits zero but prints no non-empty stdout line,
|
|
83
|
-
- or
|
|
83
|
+
- or prints output that does not parse as a JSON array of row objects.
|
|
84
84
|
|
|
85
|
-
What a failure *means
|
|
85
|
+
What a failure *means*: **per-file isolation.** The file contributes no rows
|
|
86
|
+
and is reported as skipped; the scan indexes every other file and commits.
|
|
87
|
+
The CLI names up to ten skipped files on stderr, then `... and N more`, and
|
|
88
|
+
exits `23` — distinct from `0` (clean) and `1` (the run failed), so a caller
|
|
89
|
+
can tell a partial index from a complete one. A row the table rejects under
|
|
90
|
+
`strict` counts as the same kind of failure.
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|---|---|
|
|
89
|
-
| `on-file` | **Per-file isolation.** The file contributes no rows and is reported as skipped; the scan indexes every other file and commits. The CLI names up to ten skipped files on stderr, then `... and N more`, and exits `23` — distinct from `0` (clean) and `1` (the run failed), so a caller can tell a partial index from a complete one. A row the table rejects under `strict` counts as the same kind of failure. |
|
|
90
|
-
| `pre-query` | The request returns `500 Internal Server Error` with the command's stderr tail in the JSON `error` body. |
|
|
91
|
-
| `post-query` | The request returns `500 Internal Server Error` with the command's stderr tail (or, for unparseable output, `post-query did not return valid JSON: <err>`) in the JSON `error` body. |
|
|
92
|
-
|
|
93
|
-
## Per-hook contracts
|
|
94
|
-
|
|
95
|
-
### `on-file`
|
|
92
|
+
## `on-file` contract
|
|
96
93
|
|
|
97
94
|
Runs once per file matched by the table's `glob`, at initial scan and on
|
|
98
95
|
every watched change. The command reads the file itself and prints a JSON
|
|
@@ -114,38 +111,3 @@ the path or stat metadata emits it (it has `{path}`).
|
|
|
114
111
|
|---|---|
|
|
115
112
|
| `{path}` | The matched file's **absolute** path. `on-file = "extract.py {path}"` — self-sufficient from any working directory, so the command resolves it even when the config lives outside the index. |
|
|
116
113
|
| `{root}` | The index root directory. Derive a root-relative path with `relpath({path}, {root})`. |
|
|
117
|
-
|
|
118
|
-
### `pre-query`
|
|
119
|
-
|
|
120
|
-
Runs once per `POST /query` request, before the query. The raw request body
|
|
121
|
-
goes in; plain-text SQL comes out (the stdout payload line). `dirsql` runs
|
|
122
|
-
that SQL and returns rows as usual. With no `pre-query` key, the body is
|
|
123
|
-
parsed as `{"sql": …}` instead — see [HTTP API](./http-api.md#post-query).
|
|
124
|
-
|
|
125
|
-
| Placeholder | Value |
|
|
126
|
-
|---|---|
|
|
127
|
-
| `{args}` | The raw `POST /query` request body, verbatim, as one argv token. |
|
|
128
|
-
|
|
129
|
-
**The hook owns SQL safety.** The `{args}` substitution keeps the untrusted
|
|
130
|
-
body inert *as an argv token*, but whatever SQL string the hook prints is
|
|
131
|
-
executed as-is. Validate, escape, or parameterize inside the hook.
|
|
132
|
-
|
|
133
|
-
### `post-query`
|
|
134
|
-
|
|
135
|
-
Runs once per successful `POST /query`, after the query. The result rows
|
|
136
|
-
are serialized to a JSON array and delivered two ways:
|
|
137
|
-
|
|
138
|
-
- **On stdin** — always, unbounded. This is the recommended path.
|
|
139
|
-
- **As `{args}`** — only when the serialized payload is ≤ **96 KiB**. Above
|
|
140
|
-
that, `{args}` is substituted with an **empty string** and a stderr
|
|
141
|
-
warning naming the byte size directs the operator to stdin. The full
|
|
142
|
-
payload is still on stdin — this is a fallback, not truncation.
|
|
143
|
-
|
|
144
|
-
| Placeholder | Value |
|
|
145
|
-
|---|---|
|
|
146
|
-
| `{args}` | The result rows as a JSON array, as one argv token; emptied (with a stderr warning) when the payload exceeds 96 KiB. |
|
|
147
|
-
|
|
148
|
-
The stdout payload line is parsed as JSON and returned verbatim as the
|
|
149
|
-
`200 application/json` response body. A payload that is not valid JSON
|
|
150
|
-
fails the request (`500`). With no `post-query` key, the bare row array is
|
|
151
|
-
returned — see [HTTP API](./http-api.md#post-query).
|
|
@@ -48,11 +48,11 @@ excluded from `SELECT *` results.
|
|
|
48
48
|
|
|
49
49
|
| Status | When |
|
|
50
50
|
|---|---|
|
|
51
|
-
| `200` | Query succeeded. Body: array of row objects
|
|
51
|
+
| `200` | Query succeeded. Body: array of row objects. |
|
|
52
52
|
| `400` | Malformed JSON body, missing or empty `sql` field, or a SQL error (syntax error, unknown table, or a statement SQLite classifies as a write — queries are read-only). |
|
|
53
53
|
| `405` | `GET /query`. Plain-text body `method not allowed`. |
|
|
54
54
|
| `408` | The query exceeded the 30-second per-query timeout. |
|
|
55
|
-
| `500` | Internal server fault
|
|
55
|
+
| `500` | Internal server fault. |
|
|
56
56
|
| `503` | The server is in [degraded mode](./cli.md#degraded-mode) (the config file exists but failed to load). |
|
|
57
57
|
|
|
58
58
|
All error responses (except `405`) are `application/json`:
|
|
@@ -61,15 +61,6 @@ All error responses (except `405`) are `application/json`:
|
|
|
61
61
|
{"error": "syntax error near \"SLECT\""}
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
### Hook interactions
|
|
65
|
-
|
|
66
|
-
- With [`[dirsql].pre-query`](./config.md#dirsql-keys) configured, the
|
|
67
|
-
request body is **not** parsed as `{"sql": …}`; the raw body is passed to
|
|
68
|
-
the hook, which prints the SQL to run. Hook failure → `500`.
|
|
69
|
-
- With [`[dirsql].post-query`](./config.md#dirsql-keys) configured, the
|
|
70
|
-
`200` body is whatever JSON the hook prints instead of the bare row
|
|
71
|
-
array. Hook failure or non-JSON output → `500`.
|
|
72
|
-
|
|
73
64
|
## `GET /events`
|
|
74
65
|
|
|
75
66
|
Opens a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dirsql",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"description": "Ephemeral SQL index over a local directory",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/thekevinscott/dirsql",
|
|
@@ -190,6 +190,7 @@
|
|
|
190
190
|
}
|
|
191
191
|
},
|
|
192
192
|
"dependencies": {
|
|
193
|
+
"bin-shim": "^0.2.0",
|
|
193
194
|
"smol-toml": "^1.6.1"
|
|
194
195
|
},
|
|
195
196
|
"devDependencies": {
|
|
@@ -212,15 +213,10 @@
|
|
|
212
213
|
]
|
|
213
214
|
},
|
|
214
215
|
"optionalDependencies": {
|
|
215
|
-
"@dirsql/lib-linux-x64-gnu": "0.4.
|
|
216
|
-
"@dirsql/lib-linux-arm64-gnu": "0.4.
|
|
217
|
-
"@dirsql/lib-darwin-x64": "0.4.
|
|
218
|
-
"@dirsql/lib-darwin-arm64": "0.4.
|
|
219
|
-
"@dirsql/lib-win32-x64-msvc": "0.4.
|
|
220
|
-
"@dirsql/cli-linux-x64-gnu": "0.4.12",
|
|
221
|
-
"@dirsql/cli-linux-arm64-gnu": "0.4.12",
|
|
222
|
-
"@dirsql/cli-darwin-x64": "0.4.12",
|
|
223
|
-
"@dirsql/cli-darwin-arm64": "0.4.12",
|
|
224
|
-
"@dirsql/cli-win32-x64-msvc": "0.4.12"
|
|
216
|
+
"@dirsql/lib-linux-x64-gnu": "0.4.14",
|
|
217
|
+
"@dirsql/lib-linux-arm64-gnu": "0.4.14",
|
|
218
|
+
"@dirsql/lib-darwin-x64": "0.4.14",
|
|
219
|
+
"@dirsql/lib-darwin-arm64": "0.4.14",
|
|
220
|
+
"@dirsql/lib-win32-x64-msvc": "0.4.14"
|
|
225
221
|
}
|
|
226
222
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function platformKey(): string;
|
package/dist/cli/platform-key.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** A minimal `require.resolve`-shaped function. Injectable for tests. */
|
|
2
|
-
export type Resolver = (specifier: string) => string;
|
|
3
|
-
/** Default resolver: a CJS-style `require.resolve` rooted at this ESM module. */
|
|
4
|
-
export declare function defaultResolver(): Resolver;
|
|
5
|
-
export declare function resolveBinary(key?: string, resolver?: Resolver): string;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Resolve the absolute path of the prebuilt `dirsql` binary shipped by
|
|
2
|
-
// whichever `@dirsql/cli-<triple>` optional-dependency package matched
|
|
3
|
-
// the host at `npm install` time.
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { nodeTriples } from "../platforms.js";
|
|
6
|
-
import { die } from "./die.js";
|
|
7
|
-
import { platformKey } from "./platform-key.js";
|
|
8
|
-
/** Default resolver: a CJS-style `require.resolve` rooted at this ESM module. */
|
|
9
|
-
export function defaultResolver() {
|
|
10
|
-
return createRequire(import.meta.url).resolve;
|
|
11
|
-
}
|
|
12
|
-
export function resolveBinary(key = platformKey(), resolver = defaultResolver()) {
|
|
13
|
-
const triples = nodeTriples();
|
|
14
|
-
const pkg = triples[key];
|
|
15
|
-
if (!pkg) {
|
|
16
|
-
die(`no prebuilt binary for ${key}. Build from source with \`cargo install dirsql --features cli\`.`);
|
|
17
|
-
}
|
|
18
|
-
const bin = process.platform === "win32" ? "dirsql.exe" : "dirsql";
|
|
19
|
-
try {
|
|
20
|
-
return resolver(`${pkg}/${bin}`);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
die(`${pkg} is not installed. If you ran \`npm install --no-optional\` or \`--ignore-optional\`, re-install without that flag.`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
# Search documents by meaning
|
|
2
|
-
|
|
3
|
-
Ask a question in plain language and get the closest documents back — even
|
|
4
|
-
when they share no keywords with it. Three pieces you already have compose
|
|
5
|
-
into semantic search: a SQLite vector extension for the distance math, an
|
|
6
|
-
[`on-file`](../reference/hooks.md#on-file) command to embed each file at
|
|
7
|
-
index time, and a [`pre-query`](../reference/hooks.md#pre-query) command to
|
|
8
|
-
embed each question at query time.
|
|
9
|
-
|
|
10
|
-
::: tip Just want it working?
|
|
11
|
-
[`dirsql-plugin-embeddings`](https://pypi.org/project/dirsql-plugin-embeddings/)
|
|
12
|
-
packages exactly what this guide builds, ready to install:
|
|
13
|
-
`uvx --with dirsql-plugin-embeddings dirsql server`. Keep reading to see how it's
|
|
14
|
-
built — the same three pieces, from scratch.
|
|
15
|
-
:::
|
|
16
|
-
|
|
17
|
-
## How the pieces fit
|
|
18
|
-
|
|
19
|
-
1. **[`[[dirsql.extension]]`](../reference/config.md#dirsql-extension)**
|
|
20
|
-
loads [`sqlite-vec`](https://github.com/asg017/sqlite-vec), making
|
|
21
|
-
`vec_distance_cosine()` callable in queries.
|
|
22
|
-
2. **`on-file`** runs an embedding command once per matched file; the
|
|
23
|
-
vector lands in an `embedding` column next to the text.
|
|
24
|
-
3. **`pre-query`** receives each raw `POST /query` body, embeds the
|
|
25
|
-
question, and prints the nearest-neighbor SQL to run.
|
|
26
|
-
|
|
27
|
-
`dirsql` never sees a model — both hooks are commands you own, so any
|
|
28
|
-
embedding model works. This guide uses
|
|
29
|
-
[`model2vec`](https://github.com/MinishLab/model2vec), a small, fast,
|
|
30
|
-
CPU-only embedding library (its `potion-base-8M` model downloads ~30 MB
|
|
31
|
-
from Hugging Face on first use).
|
|
32
|
-
|
|
33
|
-
## 1. The embedding scripts
|
|
34
|
-
|
|
35
|
-
Suppose short notes live in `notes/*.md`:
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
notes/pasta.md # boiling spaghetti, olive oil, garlic
|
|
39
|
-
notes/branches.md # git feature branches and pull requests
|
|
40
|
-
notes/tomatoes.md # planting tomato seedlings after the last frost
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Next to them, `embed.py` turns one file into one row carrying its text and
|
|
44
|
-
its `path`, text, and embedding (a JSON array, stored as TEXT — `sqlite-vec`
|
|
45
|
-
accepts JSON vectors directly). dirsql injects no columns, so the script emits
|
|
46
|
-
the path itself, deriving it from the `{path}`/`{root}` the hook passes in:
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
"""Embed one file's text; print a dirsql row array on stdout."""
|
|
50
|
-
import json
|
|
51
|
-
import os
|
|
52
|
-
import sys
|
|
53
|
-
|
|
54
|
-
from model2vec import StaticModel
|
|
55
|
-
|
|
56
|
-
path, root = sys.argv[1], sys.argv[2]
|
|
57
|
-
text = open(path, encoding="utf-8").read()
|
|
58
|
-
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
|
|
59
|
-
vector = model.encode([text])[0]
|
|
60
|
-
row = {"path": os.path.relpath(path, root), "text": text,
|
|
61
|
-
"embedding": json.dumps([round(float(x), 6) for x in vector])}
|
|
62
|
-
print(json.dumps([row]))
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
And `search.py` turns a `{"q": "..."}` request body into SQL, embedding the
|
|
66
|
-
question with the same model:
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
"""Turn a {"q": "..."} request body into a nearest-neighbor SQL query."""
|
|
70
|
-
import json
|
|
71
|
-
import sys
|
|
72
|
-
|
|
73
|
-
from model2vec import StaticModel
|
|
74
|
-
|
|
75
|
-
body = json.loads(sys.argv[1])
|
|
76
|
-
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
|
|
77
|
-
vector = model.encode([body["q"]])[0]
|
|
78
|
-
needle = json.dumps([round(float(x), 6) for x in vector])
|
|
79
|
-
print(
|
|
80
|
-
"SELECT path, ROUND(vec_distance_cosine(embedding, '%s'), 3) AS distance "
|
|
81
|
-
"FROM notes ORDER BY distance LIMIT 3" % needle
|
|
82
|
-
)
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
::: warning The hook owns SQL safety
|
|
86
|
-
Whatever SQL `pre-query` prints is executed as-is. Here the interpolated
|
|
87
|
-
value is a numeric vector the script itself produced; never splice raw
|
|
88
|
-
request text into SQL. See [`pre-query`](../reference/hooks.md#pre-query).
|
|
89
|
-
:::
|
|
90
|
-
|
|
91
|
-
## 2. Wire them up in `.dirsql.toml`
|
|
92
|
-
|
|
93
|
-
```toml
|
|
94
|
-
[dirsql]
|
|
95
|
-
pre-query = "uv run --with model2vec python search.py {args}"
|
|
96
|
-
hook-timeout = 300 # headroom for the first-run model download
|
|
97
|
-
|
|
98
|
-
[[dirsql.extension]]
|
|
99
|
-
path = "sqlite_vec" # Python module name; see note below
|
|
100
|
-
entrypoint = "sqlite3_vec_init"
|
|
101
|
-
|
|
102
|
-
[[table]]
|
|
103
|
-
ddl = "CREATE TABLE notes (path TEXT, text TEXT, embedding TEXT)"
|
|
104
|
-
glob = "notes/*.md"
|
|
105
|
-
on-file = "uv run --with model2vec python embed.py {path} {root}"
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
The extension is named by package: the Python launcher resolves the
|
|
109
|
-
installed `sqlite_vec` module to its bundled loadable. Naming rules per
|
|
110
|
-
runtime — and the literal-path alternative that works everywhere — are in
|
|
111
|
-
[Load a SQLite extension](./load-extension.md).
|
|
112
|
-
|
|
113
|
-
## 3. Ask questions
|
|
114
|
-
|
|
115
|
-
Run with `sqlite-vec` available to the launcher's environment. The initial
|
|
116
|
-
scan runs `embed.py` once per note, then the query argument goes straight to
|
|
117
|
-
`pre-query`, exactly as a `POST /query` body would. Pass the config with
|
|
118
|
-
[`-c`](../reference/cli.md#flags) — `dirsql` does not auto-load a
|
|
119
|
-
`.dirsql.toml` from the current directory:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
uvx --with sqlite-vec dirsql query '{"q": "how do I cook pasta?"}' -c ./.dirsql.toml
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
```json
|
|
126
|
-
[{"path":"notes/pasta.md","distance":0.315},{"path":"notes/tomatoes.md","distance":0.881},{"path":"notes/branches.md","distance":0.92}]
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
uvx --with sqlite-vec dirsql query '{"q": "reviewing code on github"}' -c ./.dirsql.toml
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
```json
|
|
134
|
-
[{"path":"notes/branches.md","distance":0.51},{"path":"notes/pasta.md","distance":1.033},{"path":"notes/tomatoes.md","distance":1.074}]
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Neither question shares a keyword with its top note — "cook" appears
|
|
138
|
-
nowhere in `pasta.md`, "github" nowhere in `branches.md`. The distance
|
|
139
|
-
ranking is doing the work.
|
|
140
|
-
|
|
141
|
-
Because `pre-query` is set, the query argument is *not* the usual
|
|
142
|
-
`{"sql": …}` — it goes to your script as-is, which decides what SQL runs
|
|
143
|
-
([hook interactions](../reference/http-api.md#hook-interactions)).
|
|
144
|
-
|
|
145
|
-
## Recomputing vs. caching
|
|
146
|
-
|
|
147
|
-
Embeddings are recomputed on every startup, because the database is a
|
|
148
|
-
derived, ephemeral view of the files
|
|
149
|
-
([how `dirsql` thinks](../explanation.md)). Once the model is cached this
|
|
150
|
-
is fast for small trees; for large ones, enable persistence so unchanged
|
|
151
|
-
files keep their stored embeddings across restarts —
|
|
152
|
-
[Keep the index across restarts](./persist.md). Editing a note re-embeds
|
|
153
|
-
just that file, automatically.
|