@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.
Files changed (3) hide show
  1. package/README.md +36 -20
  2. package/dist/index.mjs +123 -38
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,18 +1,11 @@
1
1
  # @lucas-bur/pix
2
2
 
3
- [![CI](https://github.com/lucas-bur/pix/actions/workflows/ci.yml/badge.svg)](https://github.com/lucas-bur/pix/actions/workflows/ci.yml)
4
- [![codecov](https://codecov.io/gh/lucas-bur/pix/branch/main/graph/badge.svg)](https://codecov.io/gh/lucas-bur/pix)
3
+ [![CI](https://github.com/Lucas-Bur/pix/actions/workflows/ci.yml/badge.svg)](https://github.com/Lucas-Bur/pix/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/Lucas-Bur/pix/branch/main/graph/badge.svg)](https://codecov.io/gh/Lucas-Bur/pix)
5
5
  [![npm version](https://img.shields.io/npm/v/@lucas-bur/pix)](https://www.npmjs.com/package/@lucas-bur/pix)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@lucas-bur/pix)](https://www.npmjs.com/package/@lucas-bur/pix)
7
- [![Code Quality](https://img.shields.io/badge/code%20quality-fallow-blue)](https://github.com/fallow-rs/fallow)
8
7
 
9
- Lightweight local semantic project indexer (short pix)
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
- ## Quality Gates
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
- This project uses [fallow](https://github.com/fallow-rs/fallow) for static analysis (dead code, duplication, complexity).
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
- ### Commands
44
+ ## Architecture
31
45
 
32
- - `vp run lint:fallow` Run fallow with JSON output (used in CI)
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
- ### Pre-commit Hook
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
- The pre-commit hook is managed by vite-plus and runs:
52
+ See [CONTEXT.md](./CONTEXT.md) for architecture decisions and [docs/adr/](./docs/adr/) for decision records.
38
53
 
39
- 1. `vp staged` — Formats, lints, and type-checks staged files
40
- 2. `fallow audit --summary` — Audits changed files for quality issues
54
+ ## Quality
41
55
 
42
- To set up hooks after cloning: `vp config`
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(`--force is currently not implemented and only a placeholder.`);
135
- if (verbose) yield* Effect.logInfo(`--verbose is currently not implemented and only a placeholder.`);
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
- /** Format byte count as human-readable string (e.g. "1.5 MB") */
243
- const formatBytes = (bytes) => {
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.2.1",
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"