@lucas-bur/pix 0.3.0 → 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 +61 -50
- 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
4
4
|
import { Clock, Context, Data, Effect, Layer, Option } from "effect";
|
|
5
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
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
|
});
|
|
@@ -135,6 +133,26 @@ var ResetIndex = class extends Effect.Service()("ResetIndex", {
|
|
|
135
133
|
})
|
|
136
134
|
}) {};
|
|
137
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
|
|
138
156
|
//#region src/commands/index-cmd.ts
|
|
139
157
|
/** CLI command: pix index [--force] [--verbose] [--json] */
|
|
140
158
|
const indexCommand = Command.make("index", {
|
|
@@ -142,18 +160,11 @@ const indexCommand = Command.make("index", {
|
|
|
142
160
|
verbose: Options.boolean("verbose").pipe(Options.withDefault(false)),
|
|
143
161
|
json: Options.boolean("json").pipe(Options.withDefault(false))
|
|
144
162
|
}, ({ force, verbose, json }) => Effect.gen(function* () {
|
|
145
|
-
if (force) yield* Effect.logInfo(
|
|
146
|
-
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.");
|
|
147
165
|
const startTime = Date.now();
|
|
148
166
|
const result = yield* IndexProject.index().pipe(Effect.either);
|
|
149
|
-
if (result._tag === "Left")
|
|
150
|
-
const error = result.left;
|
|
151
|
-
const message = error.message ?? String(error);
|
|
152
|
-
yield* Effect.sync(() => {
|
|
153
|
-
console.log(JSON.stringify({ error: message }));
|
|
154
|
-
});
|
|
155
|
-
return yield* Effect.fail(error);
|
|
156
|
-
}
|
|
167
|
+
if (result._tag === "Left") return yield* Effect.fail(result.left);
|
|
157
168
|
const duration = `${((Date.now() - startTime) / 1e3).toFixed(1)}s`;
|
|
158
169
|
if (json) return yield* Effect.sync(() => {
|
|
159
170
|
console.log(JSON.stringify({
|
|
@@ -163,7 +174,9 @@ const indexCommand = Command.make("index", {
|
|
|
163
174
|
}));
|
|
164
175
|
});
|
|
165
176
|
yield* Effect.logInfo(`Indexed ${result.right.stats.chunks} chunks from ${result.right.stats.files} files in ${duration}.`);
|
|
166
|
-
}))
|
|
177
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
178
|
+
console.log(formatError(error));
|
|
179
|
+
}))));
|
|
167
180
|
//#endregion
|
|
168
181
|
//#region src/commands/init.ts
|
|
169
182
|
/** CLI command: pix init [--json] */
|
|
@@ -174,7 +187,9 @@ const initCommand = Command.make("init", { json: Options.boolean("json").pipe(Op
|
|
|
174
187
|
});
|
|
175
188
|
yield* Effect.logInfo("Created .pix/config.json with default settings.");
|
|
176
189
|
yield* Effect.logInfo("Reminder: Add `.pix` to your `.gitignore` file to avoid committing the index.");
|
|
177
|
-
}))
|
|
190
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
191
|
+
console.log(formatError(error));
|
|
192
|
+
}))));
|
|
178
193
|
//#endregion
|
|
179
194
|
//#region src/commands/query.ts
|
|
180
195
|
const DEFAULT_TOP_K = 5;
|
|
@@ -234,7 +249,23 @@ const queryCommand = Command.make("query", {
|
|
|
234
249
|
console.log(formatResult(result));
|
|
235
250
|
console.log("---");
|
|
236
251
|
});
|
|
237
|
-
}))
|
|
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
|
+
};
|
|
238
269
|
//#endregion
|
|
239
270
|
//#region src/commands/reset.ts
|
|
240
271
|
/** CLI command: pix reset [--json] */
|
|
@@ -259,21 +290,11 @@ const resetCommand = Command.make("reset", { json: Options.boolean("json").pipe(
|
|
|
259
290
|
if (result.deletedChunks) parts.push("chunks.jsonl");
|
|
260
291
|
if (result.deletedVectors) parts.push("vectors.bin");
|
|
261
292
|
yield* Effect.logInfo(`Deleted: ${parts.join(", ")}`);
|
|
262
|
-
yield* Effect.logInfo(`Freed: ${formatBytes
|
|
293
|
+
yield* Effect.logInfo(`Freed: ${formatBytes(result.freedBytes)}`);
|
|
263
294
|
yield* Effect.logInfo(`Time: ${elapsedMs}ms`);
|
|
264
|
-
}))
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (bytes === 0) return "0 B";
|
|
268
|
-
const units = [
|
|
269
|
-
"B",
|
|
270
|
-
"KB",
|
|
271
|
-
"MB",
|
|
272
|
-
"GB"
|
|
273
|
-
];
|
|
274
|
-
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
275
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
276
|
-
};
|
|
295
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
296
|
+
console.log(formatError(error));
|
|
297
|
+
}))));
|
|
277
298
|
//#endregion
|
|
278
299
|
//#region src/commands/status.ts
|
|
279
300
|
/** CLI command: pix status [--json] */
|
|
@@ -288,19 +309,9 @@ const statusCommand = Command.make("status", { json: Options.boolean("json").pip
|
|
|
288
309
|
yield* Effect.logInfo(`Total lines: ${result.totalLines.toLocaleString()}`);
|
|
289
310
|
yield* Effect.logInfo(`Index size: ${formatBytes(result.byteSize)}`);
|
|
290
311
|
yield* Effect.logInfo(`Last indexed: ${lastIndexStr}`);
|
|
291
|
-
}))
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (bytes === 0) return "0 B";
|
|
295
|
-
const units = [
|
|
296
|
-
"B",
|
|
297
|
-
"KB",
|
|
298
|
-
"MB",
|
|
299
|
-
"GB"
|
|
300
|
-
];
|
|
301
|
-
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
302
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
|
303
|
-
};
|
|
312
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
313
|
+
console.log(formatError(error));
|
|
314
|
+
}))));
|
|
304
315
|
//#endregion
|
|
305
316
|
//#region src/cli.ts
|
|
306
317
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -314,10 +325,10 @@ const pix = Command.make("pix", {}, () => Effect.gen(function* () {
|
|
|
314
325
|
queryCommand,
|
|
315
326
|
resetCommand
|
|
316
327
|
]));
|
|
317
|
-
const cli = Command.run(pix, {
|
|
328
|
+
const cli = (args) => Command.run(pix, {
|
|
318
329
|
name: "pix",
|
|
319
330
|
version: VERSION
|
|
320
|
-
});
|
|
331
|
+
})(args).pipe(Effect.provide(CliConfig.layer({ showTypes: false })));
|
|
321
332
|
//#endregion
|
|
322
333
|
//#region src/services/chunker.ts
|
|
323
334
|
const MIN_CHUNK_CHARS = 20;
|
|
@@ -587,7 +598,7 @@ const make = Effect.gen(function* () {
|
|
|
587
598
|
const model = readModelFromChunks(lines);
|
|
588
599
|
const totalLines = countTotalLines(lines);
|
|
589
600
|
const vectorsStat = yield* fs.stat(VECTORS_FILE).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
590
|
-
const byteSize = vectorsStat && "size" in vectorsStat ? vectorsStat.size : 0;
|
|
601
|
+
const byteSize = vectorsStat && "size" in vectorsStat ? Number(vectorsStat.size) : 0;
|
|
591
602
|
return {
|
|
592
603
|
chunks,
|
|
593
604
|
files,
|
|
@@ -603,13 +614,13 @@ const make = Effect.gen(function* () {
|
|
|
603
614
|
let freedBytes = 0;
|
|
604
615
|
if (yield* fs.exists(CHUNKS_FILE)) {
|
|
605
616
|
const stat = yield* fs.stat(CHUNKS_FILE);
|
|
606
|
-
freedBytes += stat && "size" in stat ? stat.size : 0;
|
|
617
|
+
freedBytes += stat && "size" in stat ? Number(stat.size) : 0;
|
|
607
618
|
yield* fs.remove(CHUNKS_FILE);
|
|
608
619
|
deletedChunks = true;
|
|
609
620
|
}
|
|
610
621
|
if (yield* fs.exists(VECTORS_FILE)) {
|
|
611
622
|
const stat = yield* fs.stat(VECTORS_FILE);
|
|
612
|
-
freedBytes += stat && "size" in stat ? stat.size : 0;
|
|
623
|
+
freedBytes += stat && "size" in stat ? Number(stat.size) : 0;
|
|
613
624
|
yield* fs.remove(VECTORS_FILE);
|
|
614
625
|
deletedVectors = true;
|
|
615
626
|
}
|
|
@@ -634,6 +645,6 @@ const ChunkerLayer = ChunkerLive.pipe(Layer.provide(ServicesLayer));
|
|
|
634
645
|
const InfraLayer = Layer.mergeAll(ServicesLayer, ChunkerLayer).pipe(Layer.provide(NodeContext.layer));
|
|
635
646
|
const UseCaseLayer = Layer.mergeAll(InitProject.Default, GetStatus.Default, QueryProject.Default, IndexProject.Default, ResetIndex.Default);
|
|
636
647
|
const AppLayer = Layer.merge(UseCaseLayer.pipe(Layer.provide(InfraLayer)), NodeContext.layer);
|
|
637
|
-
cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain);
|
|
648
|
+
cli(process.argv).pipe(Effect.provide(AppLayer), NodeRuntime.runMain({ disableErrorReporting: true }));
|
|
638
649
|
//#endregion
|
|
639
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"
|