@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.
Files changed (3) hide show
  1. package/README.md +36 -20
  2. package/dist/index.mjs +61 -50
  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
@@ -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(`--force is currently not implemented and only a placeholder.`);
146
- 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.");
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$1(result.freedBytes)}`);
293
+ yield* Effect.logInfo(`Freed: ${formatBytes(result.freedBytes)}`);
263
294
  yield* Effect.logInfo(`Time: ${elapsedMs}ms`);
264
- }));
265
- /** Format byte count as human-readable string (e.g. "1.5 MB") */
266
- const formatBytes$1 = (bytes) => {
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
- /** Format byte count as human-readable string (e.g. "1.5 MB") */
293
- const formatBytes = (bytes) => {
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.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"