@lucas-bur/pix 0.2.1 → 0.4.0
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/README.md +36 -20
- package/dist/index.mjs +123 -38
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# @lucas-bur/pix
|
|
2
2
|
|
|
3
|
-
[](https://github.com/Lucas-Bur/pix/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/Lucas-Bur/pix)
|
|
5
5
|
[](https://www.npmjs.com/package/@lucas-bur/pix)
|
|
6
6
|
[](https://www.npmjs.com/package/@lucas-bur/pix)
|
|
7
|
-
[](https://github.com/fallow-rs/fallow)
|
|
8
7
|
|
|
9
|
-
Lightweight local semantic project indexer
|
|
10
|
-
|
|
11
|
-
Zero external services, 100% local + offline. Installs as a devDependency and provides agent-ready structured JSON output.
|
|
12
|
-
|
|
13
|
-
## Status
|
|
14
|
-
|
|
15
|
-
MVP in development. See [CONTEXT.md](./CONTEXT.md) for architecture decisions and [.scratch/pix-mvp/PRD.md](./.scratch/pix-mvp/PRD.md) for the product requirements.
|
|
8
|
+
Lightweight local semantic project indexer. Zero external services, 100% local and offline. Installs as a devDependency and provides agent-ready structured JSON output.
|
|
16
9
|
|
|
17
10
|
## Quick Start
|
|
18
11
|
|
|
@@ -23,23 +16,46 @@ pix index
|
|
|
23
16
|
pix query "authentication middleware"
|
|
24
17
|
```
|
|
25
18
|
|
|
26
|
-
##
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
| Command | Description | JSON flag |
|
|
22
|
+
| -------------------- | ------------------------------------------- | --------- |
|
|
23
|
+
| `pix init` | Create `.pix/config.json` with defaults | `--json` |
|
|
24
|
+
| `pix index` | Scan, chunk, embed, and store project files | `--json` |
|
|
25
|
+
| `pix query "<text>"` | Semantic search via cosine similarity | `--json` |
|
|
26
|
+
| `pix status` | Show index statistics | `--json` |
|
|
27
|
+
| `pix reset` | Delete index files (chunks + vectors) | `--json` |
|
|
28
|
+
|
|
29
|
+
All commands support `--json` for structured output on stdout — ideal for piping to AI agents.
|
|
30
|
+
|
|
31
|
+
## Agent-Ready Output
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
```bash
|
|
34
|
+
$ pix status --json
|
|
35
|
+
{"chunks":59,"files":37,"model":"Xenova/all-MiniLM-L6-v2","lastIndex":1715030400000,"totalLines":1260,"byteSize":16128}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Errors use the same structured format:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "error": true, "code": "CONFIG_MISSING", "message": "No .pix/config.json found" }
|
|
42
|
+
```
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
## Architecture
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
- `fallow audit --summary` — Check only changed files (used in pre-commit hook)
|
|
46
|
+
pix follows hexagonal architecture (ports and adapters) with three layers:
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
- **Domain** (`src/domain/`) — Pure types, entities, port declarations
|
|
49
|
+
- **Application** (`src/application/`) — Use cases orchestrating business logic
|
|
50
|
+
- **Infrastructure** (`src/services/`) — Concrete adapters (ONNX, filesystem, ffmpeg scanning)
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
See [CONTEXT.md](./CONTEXT.md) for architecture decisions and [docs/adr/](./docs/adr/) for decision records.
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
2. `fallow audit --summary` — Audits changed files for quality issues
|
|
54
|
+
## Quality
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
- `vp check` — Format, lint, type-check
|
|
57
|
+
- `vp test` — Unit and integration tests
|
|
58
|
+
- `vp run lint:fallow` — Dead code, duplication, complexity analysis
|
|
43
59
|
|
|
44
60
|
## License
|
|
45
61
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
4
|
-
import { Context, Data, Effect, Layer, Option } from "effect";
|
|
5
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
4
|
+
import { Clock, Context, Data, Effect, Layer, Option } from "effect";
|
|
5
|
+
import { Args, CliConfig, Command, Options } from "@effect/cli";
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
import { FileSystem } from "@effect/platform";
|
|
8
8
|
import { env } from "@huggingface/transformers";
|
|
@@ -68,15 +68,13 @@ var IndexProject = class extends Effect.Service()("IndexProject", {
|
|
|
68
68
|
const embeddings = yield* embedder.batch(texts);
|
|
69
69
|
yield* vectorStore.store(allChunks, embeddings);
|
|
70
70
|
const dims = embeddings[0]?.dims ?? 384;
|
|
71
|
-
const byteSize = embeddings.length * dims * 4;
|
|
72
|
-
yield* Effect.logInfo(`Indexed ${totalChunks} chunks from ${totalFiles} files.`);
|
|
73
71
|
return {
|
|
74
72
|
success: true,
|
|
75
73
|
stats: {
|
|
76
74
|
chunks: totalChunks,
|
|
77
75
|
files: totalFiles,
|
|
78
76
|
totalLines,
|
|
79
|
-
byteSize
|
|
77
|
+
byteSize: embeddings.length * dims * 4
|
|
80
78
|
}
|
|
81
79
|
};
|
|
82
80
|
});
|
|
@@ -124,6 +122,37 @@ var QueryProject = class extends Effect.Service()("QueryProject", {
|
|
|
124
122
|
})
|
|
125
123
|
}) {};
|
|
126
124
|
//#endregion
|
|
125
|
+
//#region src/application/reset-index.ts
|
|
126
|
+
/** Use case: reset the project index. Depends on VectorStore via Effect tag. */
|
|
127
|
+
var ResetIndex = class extends Effect.Service()("ResetIndex", {
|
|
128
|
+
accessors: true,
|
|
129
|
+
effect: Effect.gen(function* () {
|
|
130
|
+
const store = yield* VectorStore;
|
|
131
|
+
const reset = () => store.reset();
|
|
132
|
+
return { reset };
|
|
133
|
+
})
|
|
134
|
+
}) {};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/lib/error-format.ts
|
|
137
|
+
const errorCodes = {
|
|
138
|
+
ConfigError: "CONFIG_MISSING",
|
|
139
|
+
PlatformError: "PLATFORM_ERROR"
|
|
140
|
+
};
|
|
141
|
+
const messageFromError = (error) => {
|
|
142
|
+
if (typeof error === "string") return error;
|
|
143
|
+
if (error && typeof error === "object" && "message" in error) return String(error.message);
|
|
144
|
+
return "Unknown error";
|
|
145
|
+
};
|
|
146
|
+
const codeFromError = (error) => {
|
|
147
|
+
if (error && typeof error === "object" && "_tag" in error) return errorCodes[String(error._tag)] ?? "UNKNOWN";
|
|
148
|
+
return "UNKNOWN";
|
|
149
|
+
};
|
|
150
|
+
const formatError = (error) => JSON.stringify({
|
|
151
|
+
error: true,
|
|
152
|
+
code: codeFromError(error),
|
|
153
|
+
message: messageFromError(error)
|
|
154
|
+
});
|
|
155
|
+
//#endregion
|
|
127
156
|
//#region src/commands/index-cmd.ts
|
|
128
157
|
/** CLI command: pix index [--force] [--verbose] [--json] */
|
|
129
158
|
const indexCommand = Command.make("index", {
|
|
@@ -131,18 +160,11 @@ const indexCommand = Command.make("index", {
|
|
|
131
160
|
verbose: Options.boolean("verbose").pipe(Options.withDefault(false)),
|
|
132
161
|
json: Options.boolean("json").pipe(Options.withDefault(false))
|
|
133
162
|
}, ({ force, verbose, json }) => Effect.gen(function* () {
|
|
134
|
-
if (force) yield* Effect.logInfo(
|
|
135
|
-
if (verbose) yield* Effect.logInfo(
|
|
163
|
+
if (force && !json) yield* Effect.logInfo("--force is currently not implemented and only a placeholder.");
|
|
164
|
+
if (verbose && !json) yield* Effect.logInfo("--verbose is currently not implemented and only a placeholder.");
|
|
136
165
|
const startTime = Date.now();
|
|
137
166
|
const result = yield* IndexProject.index().pipe(Effect.either);
|
|
138
|
-
if (result._tag === "Left")
|
|
139
|
-
const error = result.left;
|
|
140
|
-
const message = error.message ?? String(error);
|
|
141
|
-
yield* Effect.sync(() => {
|
|
142
|
-
console.log(JSON.stringify({ error: message }));
|
|
143
|
-
});
|
|
144
|
-
return yield* Effect.fail(error);
|
|
145
|
-
}
|
|
167
|
+
if (result._tag === "Left") return yield* Effect.fail(result.left);
|
|
146
168
|
const duration = `${((Date.now() - startTime) / 1e3).toFixed(1)}s`;
|
|
147
169
|
if (json) return yield* Effect.sync(() => {
|
|
148
170
|
console.log(JSON.stringify({
|
|
@@ -152,7 +174,9 @@ const indexCommand = Command.make("index", {
|
|
|
152
174
|
}));
|
|
153
175
|
});
|
|
154
176
|
yield* Effect.logInfo(`Indexed ${result.right.stats.chunks} chunks from ${result.right.stats.files} files in ${duration}.`);
|
|
155
|
-
}))
|
|
177
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
178
|
+
console.log(formatError(error));
|
|
179
|
+
}))));
|
|
156
180
|
//#endregion
|
|
157
181
|
//#region src/commands/init.ts
|
|
158
182
|
/** CLI command: pix init [--json] */
|
|
@@ -163,7 +187,9 @@ const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Op
|
|
|
163
187
|
});
|
|
164
188
|
yield* Effect.logInfo("Created .pix/config.json with default settings.");
|
|
165
189
|
yield* Effect.logInfo("Reminder: Add `.pix` to your `.gitignore` file to avoid committing the index.");
|
|
166
|
-
}))
|
|
190
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
191
|
+
console.log(formatError(error));
|
|
192
|
+
}))));
|
|
167
193
|
//#endregion
|
|
168
194
|
//#region src/commands/query.ts
|
|
169
195
|
const DEFAULT_TOP_K = 5;
|
|
@@ -223,7 +249,52 @@ const queryCommand = Command.make("query", {
|
|
|
223
249
|
console.log(formatResult(result));
|
|
224
250
|
console.log("---");
|
|
225
251
|
});
|
|
226
|
-
}))
|
|
252
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
253
|
+
console.log(formatError(error));
|
|
254
|
+
}))));
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/lib/format.ts
|
|
257
|
+
/** Format byte count as human-readable string (e.g. "1.5 MB") */
|
|
258
|
+
const formatBytes = (bytes) => {
|
|
259
|
+
if (bytes === 0) return "0 B";
|
|
260
|
+
const units = [
|
|
261
|
+
"B",
|
|
262
|
+
"KB",
|
|
263
|
+
"MB",
|
|
264
|
+
"GB"
|
|
265
|
+
];
|
|
266
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
267
|
+
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
|
|
268
|
+
};
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/commands/reset.ts
|
|
271
|
+
/** CLI command: pix reset [--json] */
|
|
272
|
+
const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(Options.withDefault(false)) }, ({ json }) => Effect.gen(function* () {
|
|
273
|
+
const start = yield* Clock.currentTimeMillis;
|
|
274
|
+
const result = yield* ResetIndex.reset();
|
|
275
|
+
const elapsedMs = (yield* Clock.currentTimeMillis) - start;
|
|
276
|
+
if (json) return yield* Effect.sync(() => {
|
|
277
|
+
console.log(JSON.stringify({
|
|
278
|
+
status: "ok",
|
|
279
|
+
deletedChunks: result.deletedChunks,
|
|
280
|
+
deletedVectors: result.deletedVectors,
|
|
281
|
+
freedBytes: result.freedBytes,
|
|
282
|
+
elapsedMs
|
|
283
|
+
}));
|
|
284
|
+
});
|
|
285
|
+
if (!result.deletedChunks && !result.deletedVectors) {
|
|
286
|
+
yield* Effect.logInfo("Nothing to reset.");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const parts = [];
|
|
290
|
+
if (result.deletedChunks) parts.push("chunks.jsonl");
|
|
291
|
+
if (result.deletedVectors) parts.push("vectors.bin");
|
|
292
|
+
yield* Effect.logInfo(`Deleted: ${parts.join(", ")}`);
|
|
293
|
+
yield* Effect.logInfo(`Freed: ${formatBytes(result.freedBytes)}`);
|
|
294
|
+
yield* Effect.logInfo(`Time: ${elapsedMs}ms`);
|
|
295
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
296
|
+
console.log(formatError(error));
|
|
297
|
+
}))));
|
|
227
298
|
//#endregion
|
|
228
299
|
//#region src/commands/status.ts
|
|
229
300
|
/** CLI command: pix status [--json] */
|
|
@@ -238,19 +309,9 @@ const statusCommand = Command.make("status", { json: Options.boolean("json").pip
|
|
|
238
309
|
yield* Effect.logInfo(`Total lines: ${result.totalLines.toLocaleString()}`);
|
|
239
310
|
yield* Effect.logInfo(`Index size: ${formatBytes(result.byteSize)}`);
|
|
240
311
|
yield* Effect.logInfo(`Last indexed: ${lastIndexStr}`);
|
|
241
|
-
}))
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (bytes === 0) return "0 B";
|
|
245
|
-
const units = [
|
|
246
|
-
"B",
|
|
247
|
-
"KB",
|
|
248
|
-
"MB",
|
|
249
|
-
"GB"
|
|
250
|
-
];
|
|
251
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
252
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
253
|
-
};
|
|
312
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
313
|
+
console.log(formatError(error));
|
|
314
|
+
}))));
|
|
254
315
|
//#endregion
|
|
255
316
|
//#region src/cli.ts
|
|
256
317
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -261,12 +322,13 @@ const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
|
261
322
|
initCommand,
|
|
262
323
|
statusCommand,
|
|
263
324
|
indexCommand,
|
|
264
|
-
queryCommand
|
|
325
|
+
queryCommand,
|
|
326
|
+
resetCommand
|
|
265
327
|
]));
|
|
266
|
-
const cli = Command.run(pix, {
|
|
328
|
+
const cli = (args) => Command.run(pix, {
|
|
267
329
|
name: "pix",
|
|
268
330
|
version: VERSION
|
|
269
|
-
});
|
|
331
|
+
})(args).pipe(Effect.provide(CliConfig.layer({ showTypes: false })));
|
|
270
332
|
//#endregion
|
|
271
333
|
//#region src/services/chunker.ts
|
|
272
334
|
const MIN_CHUNK_CHARS = 20;
|
|
@@ -536,7 +598,7 @@ const make = Effect.gen(function* () {
|
|
|
536
598
|
const model = readModelFromChunks(lines);
|
|
537
599
|
const totalLines = countTotalLines(lines);
|
|
538
600
|
const vectorsStat = yield* fs.stat(VECTORS_FILE).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
539
|
-
const byteSize = vectorsStat && "size" in vectorsStat ? vectorsStat.size : 0;
|
|
601
|
+
const byteSize = vectorsStat && "size" in vectorsStat ? Number(vectorsStat.size) : 0;
|
|
540
602
|
return {
|
|
541
603
|
chunks,
|
|
542
604
|
files,
|
|
@@ -546,10 +608,33 @@ const make = Effect.gen(function* () {
|
|
|
546
608
|
byteSize
|
|
547
609
|
};
|
|
548
610
|
});
|
|
611
|
+
const reset = () => Effect.gen(function* () {
|
|
612
|
+
let deletedChunks = false;
|
|
613
|
+
let deletedVectors = false;
|
|
614
|
+
let freedBytes = 0;
|
|
615
|
+
if (yield* fs.exists(CHUNKS_FILE)) {
|
|
616
|
+
const stat = yield* fs.stat(CHUNKS_FILE);
|
|
617
|
+
freedBytes += stat && "size" in stat ? Number(stat.size) : 0;
|
|
618
|
+
yield* fs.remove(CHUNKS_FILE);
|
|
619
|
+
deletedChunks = true;
|
|
620
|
+
}
|
|
621
|
+
if (yield* fs.exists(VECTORS_FILE)) {
|
|
622
|
+
const stat = yield* fs.stat(VECTORS_FILE);
|
|
623
|
+
freedBytes += stat && "size" in stat ? Number(stat.size) : 0;
|
|
624
|
+
yield* fs.remove(VECTORS_FILE);
|
|
625
|
+
deletedVectors = true;
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
deletedChunks,
|
|
629
|
+
deletedVectors,
|
|
630
|
+
freedBytes
|
|
631
|
+
};
|
|
632
|
+
});
|
|
549
633
|
return {
|
|
550
634
|
store,
|
|
551
635
|
search,
|
|
552
|
-
getStats
|
|
636
|
+
getStats,
|
|
637
|
+
reset
|
|
553
638
|
};
|
|
554
639
|
});
|
|
555
640
|
const VectorStoreLive = Layer.effect(VectorStore, make);
|
|
@@ -558,8 +643,8 @@ const VectorStoreLive = Layer.effect(VectorStore, make);
|
|
|
558
643
|
const ServicesLayer = Layer.mergeAll(ConfigStoreLive, ScannerLive, OnnxEmbedderLive, VectorStoreLive);
|
|
559
644
|
const ChunkerLayer = ChunkerLive.pipe(Layer.provide(ServicesLayer));
|
|
560
645
|
const InfraLayer = Layer.mergeAll(ServicesLayer, ChunkerLayer).pipe(Layer.provide(NodeContext.layer));
|
|
561
|
-
const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default);
|
|
646
|
+
const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default, ResetIndex.Default);
|
|
562
647
|
const AppLayer = Layer.merge(UseCaseLayer.pipe(Layer.provide(InfraLayer)), NodeContext.layer);
|
|
563
|
-
cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain);
|
|
648
|
+
cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain({ disableErrorReporting: true }));
|
|
564
649
|
//#endregion
|
|
565
650
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lucas-bur/pix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Lightweight local semantic project indexer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@types/node": "^25.5.0",
|
|
60
60
|
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
61
61
|
"@vitest/coverage-v8": "^4.1.6",
|
|
62
|
+
"effect-memfs": "^0.8.0",
|
|
62
63
|
"fallow": "^2.65.0",
|
|
63
64
|
"typescript": "^6.0.2",
|
|
64
65
|
"vite-plus": "^0.1.14"
|