@iflow-mcp/nazarlysyi-brickscope 0.1.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/cli/commands/config.js +81 -0
  4. package/dist/cli/commands/health.js +27 -0
  5. package/dist/cli/commands/identify.js +72 -0
  6. package/dist/cli/commands/mcp.js +14 -0
  7. package/dist/cli/commands/minifig.js +57 -0
  8. package/dist/cli/commands/part.js +59 -0
  9. package/dist/cli/commands/set.js +57 -0
  10. package/dist/cli/index.js +30 -0
  11. package/dist/cli/output.js +84 -0
  12. package/dist/core/brickognize/client.js +39 -0
  13. package/dist/core/brickognize/mappers.js +53 -0
  14. package/dist/core/brickognize/types.js +3 -0
  15. package/dist/core/cache/index.js +30 -0
  16. package/dist/core/cache/memory.js +15 -0
  17. package/dist/core/cache/sqlite.js +33 -0
  18. package/dist/core/cache/types.js +2 -0
  19. package/dist/core/concurrency.js +18 -0
  20. package/dist/core/config.js +43 -0
  21. package/dist/core/image.js +45 -0
  22. package/dist/core/rebrickable/client.js +91 -0
  23. package/dist/core/rebrickable/minifigDetails.js +26 -0
  24. package/dist/core/rebrickable/partDetails.js +80 -0
  25. package/dist/core/rebrickable/setDetails.js +47 -0
  26. package/dist/core/rebrickable/types.js +3 -0
  27. package/dist/core/utils/errors.js +51 -0
  28. package/dist/mcp/index.js +9 -0
  29. package/dist/mcp/server.js +56 -0
  30. package/dist/mcp/tools/batchPredict.js +55 -0
  31. package/dist/mcp/tools/cacheTools.js +24 -0
  32. package/dist/mcp/tools/health.js +19 -0
  33. package/dist/mcp/tools/minifigDetails.js +26 -0
  34. package/dist/mcp/tools/partDetails.js +83 -0
  35. package/dist/mcp/tools/predict.js +36 -0
  36. package/dist/mcp/tools/setDetails.js +30 -0
  37. package/dist/mcp/tools/shared.js +28 -0
  38. package/package.json +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brickognize MCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Brickscope
2
+
3
+ Identify LEGO parts, sets, and minifigures from images — as a **CLI tool** or an **MCP server** for AI assistants.
4
+
5
+ Powered by the [Brickognize API](https://api.brickognize.com/docs) and [Rebrickable](https://rebrickable.com/api/).
6
+
7
+ Huge thanks to [Piotr Rybak](https://brickognize.com/about) for creating the Brickognize service and making LEGO recognition accessible to everyone!
8
+
9
+ ## CLI
10
+
11
+ ```bash
12
+ npm install -g brickscope
13
+
14
+ brickscope identify photo.jpg --type part
15
+ brickscope part 3001 --color Black
16
+ brickscope set 75192
17
+ brickscope minifig fig-012805
18
+ ```
19
+
20
+ Or run without installing: `npx brickscope identify photo.jpg`
21
+
22
+ [Full CLI documentation](./docs/cli.md)
23
+
24
+ ## MCP Server
25
+
26
+ For AI assistants (Claude, Cursor, etc.), add to your MCP config:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "brickscope": {
32
+ "command": "npx",
33
+ "args": ["-y", "brickscope", "mcp"],
34
+ "env": {
35
+ "REBRICKABLE_API_KEY": "your-key-here",
36
+ "BRICKOGNIZE_CACHE": "sqlite"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ [Full MCP documentation](./docs/mcp.md)
44
+
45
+ ## Configuration
46
+
47
+ ### Config file (CLI)
48
+
49
+ ```bash
50
+ brickscope config init
51
+ ```
52
+
53
+ Creates `~/.config/brickscope/config.json` with your Rebrickable API key and cache settings.
54
+
55
+ ### Environment variables
56
+
57
+ | Variable | Default | Description |
58
+ | --------------------- | ------- | ------------------------------------------------------------------------------------------------- |
59
+ | `REBRICKABLE_API_KEY` | — | Free API key from [rebrickable.com/api](https://rebrickable.com/api/). Required for lookup tools. |
60
+ | `BRICKOGNIZE_CACHE` | `none` | Cache mode: `none`, `memory`, or `sqlite` |
61
+
62
+ Environment variables take priority over the config file.
63
+
64
+ ## Features
65
+
66
+ - **Image recognition** — identify parts, sets, minifigures, and stickers from photos
67
+ - **Batch processing** — identify multiple images in parallel
68
+ - **Part lookup** — colors, set appearances via Rebrickable
69
+ - **Set inventory** — full parts list, year, theme, piece count
70
+ - **Minifigure lookup** — details and set appearances
71
+ - **Caching** — in-memory or SQLite cache for Rebrickable API responses
72
+ - **Config file** — save API key and preferences once, use everywhere
73
+
74
+ ## Examples
75
+
76
+ See the [examples](./examples) folder for prompt templates.
77
+
78
+ ## Development
79
+
80
+ ```bash
81
+ npm install
82
+ npm run build
83
+ npm run dev # Watch mode
84
+ npm test # Unit + integration tests
85
+ npm run lint # ESLint
86
+ npm run format # Prettier
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,81 @@
1
+ import { loadConfig, saveConfig, getConfigPath } from "../../core/config.js";
2
+ import * as readline from "node:readline/promises";
3
+ export function registerConfigCommand(program) {
4
+ const config = program.command("config").description("Manage brickscope configuration");
5
+ config
6
+ .command("show")
7
+ .description("Show current configuration")
8
+ .action(() => {
9
+ const cfg = loadConfig();
10
+ const configPath = getConfigPath();
11
+ console.log(`Config file: ${configPath}`);
12
+ console.log("");
13
+ if (Object.keys(cfg).length === 0) {
14
+ console.log("No configuration set. Run 'brickscope config init' to set up.");
15
+ return;
16
+ }
17
+ if (cfg.rebrickableApiKey) {
18
+ const masked = cfg.rebrickableApiKey.slice(0, 4) + "..." + cfg.rebrickableApiKey.slice(-4);
19
+ console.log(` rebrickableApiKey: ${masked}`);
20
+ }
21
+ if (cfg.cache) {
22
+ console.log(` cache: ${cfg.cache}`);
23
+ }
24
+ });
25
+ config
26
+ .command("path")
27
+ .description("Print config file location")
28
+ .action(() => {
29
+ console.log(getConfigPath());
30
+ });
31
+ config
32
+ .command("set <key> <value>")
33
+ .description("Set a configuration value (rebrickableApiKey, cache)")
34
+ .action((key, value) => {
35
+ const cfg = loadConfig();
36
+ if (key === "rebrickableApiKey") {
37
+ cfg.rebrickableApiKey = value;
38
+ }
39
+ else if (key === "cache") {
40
+ if (!["none", "memory", "sqlite"].includes(value)) {
41
+ console.error(`Invalid cache value "${value}". Valid: none, memory, sqlite`);
42
+ process.exit(1);
43
+ }
44
+ cfg.cache = value;
45
+ }
46
+ else {
47
+ console.error(`Unknown config key "${key}". Valid keys: rebrickableApiKey, cache`);
48
+ process.exit(1);
49
+ }
50
+ saveConfig(cfg);
51
+ console.log(`Set ${key} = ${key === "rebrickableApiKey" ? "***" : value}`);
52
+ });
53
+ config
54
+ .command("init")
55
+ .description("Interactive setup wizard")
56
+ .action(async () => {
57
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
58
+ try {
59
+ const cfg = loadConfig();
60
+ const apiKey = await rl.question(`Rebrickable API key${cfg.rebrickableApiKey ? " (press Enter to keep current)" : ""}: `);
61
+ if (apiKey.trim()) {
62
+ cfg.rebrickableApiKey = apiKey.trim();
63
+ }
64
+ const cache = await rl.question(`Cache mode (none/memory/sqlite)${cfg.cache ? ` [${cfg.cache}]` : " [none]"}: `);
65
+ if (cache.trim()) {
66
+ if (!["none", "memory", "sqlite"].includes(cache.trim())) {
67
+ console.error(`Invalid cache value. Using "${cfg.cache ?? "none"}".`);
68
+ }
69
+ else {
70
+ cfg.cache = cache.trim();
71
+ }
72
+ }
73
+ saveConfig(cfg);
74
+ console.log(`\nConfiguration saved to ${getConfigPath()}`);
75
+ }
76
+ finally {
77
+ rl.close();
78
+ }
79
+ });
80
+ }
81
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,27 @@
1
+ import { checkHealth } from "../../core/brickognize/client.js";
2
+ export function registerHealthCommand(program) {
3
+ program
4
+ .command("health")
5
+ .description("Check whether the Brickognize API is online")
6
+ .option("--json", "Output raw JSON")
7
+ .action(async (opts) => {
8
+ try {
9
+ const health = await checkHealth();
10
+ if (opts.json) {
11
+ console.log(JSON.stringify(health, null, 2));
12
+ }
13
+ else {
14
+ const status = Object.values(health).every(Boolean) ? "healthy" : "degraded";
15
+ console.log(`Brickognize API: ${status}`);
16
+ for (const [key, value] of Object.entries(health)) {
17
+ console.log(` ${key}: ${value ? "ok" : "FAIL"}`);
18
+ }
19
+ }
20
+ }
21
+ catch (error) {
22
+ console.error(error instanceof Error ? error.message : "Failed to check health");
23
+ process.exit(1);
24
+ }
25
+ });
26
+ }
27
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,72 @@
1
+ import { predict } from "../../core/brickognize/client.js";
2
+ import { mapPredictionResult } from "../../core/brickognize/mappers.js";
3
+ import { resolveImage, PREDICT_ENDPOINTS } from "../../core/image.js";
4
+ import { runWithConcurrencyLimit } from "../../core/concurrency.js";
5
+ import { formatToolError } from "../../core/utils/errors.js";
6
+ import { formatPrediction } from "../output.js";
7
+ const CONCURRENCY_LIMIT = 5;
8
+ export function registerIdentifyCommand(program) {
9
+ program
10
+ .command("identify")
11
+ .description("Identify LEGO item(s) from photo(s)")
12
+ .argument("<images...>", "Path(s) to image files (JPEG, PNG, or WebP)")
13
+ .option("-t, --type <type>", "Item type: general, part, set, fig", "general")
14
+ .option("--json", "Output raw JSON")
15
+ .action(async (images, opts) => {
16
+ const type = opts.type;
17
+ const endpoint = PREDICT_ENDPOINTS[type];
18
+ if (!endpoint) {
19
+ console.error(`Unknown type "${opts.type}". Valid types: general, part, set, fig`);
20
+ process.exit(1);
21
+ }
22
+ try {
23
+ if (images.length === 1) {
24
+ const { blob, filename } = await resolveImage({ imagePath: images[0] });
25
+ const raw = await predict(endpoint, blob, filename);
26
+ const result = mapPredictionResult(raw, false);
27
+ if (opts.json) {
28
+ console.log(JSON.stringify(result, null, 2));
29
+ }
30
+ else {
31
+ console.log(formatPrediction(result));
32
+ }
33
+ }
34
+ else {
35
+ const tasks = images.map((imagePath) => async () => {
36
+ try {
37
+ const { blob, filename } = await resolveImage({ imagePath });
38
+ const raw = await predict(endpoint, blob, filename);
39
+ const result = mapPredictionResult(raw, false);
40
+ return { imagePath, status: "success", result };
41
+ }
42
+ catch (err) {
43
+ return { imagePath, status: "error", error: formatToolError(err) };
44
+ }
45
+ });
46
+ const results = await runWithConcurrencyLimit(tasks, CONCURRENCY_LIMIT);
47
+ if (opts.json) {
48
+ console.log(JSON.stringify(results, null, 2));
49
+ }
50
+ else {
51
+ for (const r of results) {
52
+ console.log(`\n--- ${r.imagePath} ---`);
53
+ if (r.status === "success") {
54
+ console.log(formatPrediction(r.result));
55
+ }
56
+ else {
57
+ console.error(`Error: ${r.error}`);
58
+ }
59
+ }
60
+ const succeeded = results.filter((r) => r.status === "success").length;
61
+ const failed = results.length - succeeded;
62
+ console.log(`\nBatch: ${succeeded}/${results.length} succeeded${failed > 0 ? `, ${failed} failed` : ""}`);
63
+ }
64
+ }
65
+ }
66
+ catch (error) {
67
+ console.error(error instanceof Error ? error.message : "Identification failed");
68
+ process.exit(1);
69
+ }
70
+ });
71
+ }
72
+ //# sourceMappingURL=identify.js.map
@@ -0,0 +1,14 @@
1
+ export function registerMcpCommand(program) {
2
+ program
3
+ .command("mcp")
4
+ .description("Start MCP server (stdio transport) for AI assistant integration")
5
+ .action(async () => {
6
+ // Dynamic import to avoid loading MCP SDK unless needed
7
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
8
+ const { createServer } = await import("../../mcp/server.js");
9
+ const server = createServer();
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ });
13
+ }
14
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,57 @@
1
+ import { fetchMinifigDetails } from "../../core/rebrickable/minifigDetails.js";
2
+ import { formatToolError } from "../../core/utils/errors.js";
3
+ import { formatMinifigDetails } from "../output.js";
4
+ export function registerMinifigCommand(program) {
5
+ program
6
+ .command("minifig")
7
+ .description("Get LEGO minifigure details and which sets contain it")
8
+ .argument("<ids...>", "Minifigure ID(s), e.g. fig-000001")
9
+ .option("--json", "Output raw JSON")
10
+ .action(async (ids, opts) => {
11
+ try {
12
+ if (ids.length === 1) {
13
+ const result = await fetchMinifigDetails(ids[0]);
14
+ if (opts.json) {
15
+ console.log(JSON.stringify(result, null, 2));
16
+ }
17
+ else {
18
+ console.log(formatMinifigDetails(result));
19
+ }
20
+ }
21
+ else {
22
+ const results = [];
23
+ for (const minifigId of ids) {
24
+ try {
25
+ const result = await fetchMinifigDetails(minifigId);
26
+ results.push({ minifigId, status: "success", result });
27
+ }
28
+ catch (err) {
29
+ results.push({ minifigId, status: "error", error: formatToolError(err) });
30
+ }
31
+ }
32
+ if (opts.json) {
33
+ console.log(JSON.stringify(results, null, 2));
34
+ }
35
+ else {
36
+ for (const r of results) {
37
+ console.log(`\n--- ${r.minifigId} ---`);
38
+ if (r.status === "success") {
39
+ console.log(formatMinifigDetails(r.result));
40
+ }
41
+ else {
42
+ console.error(`Error: ${r.error}`);
43
+ }
44
+ }
45
+ const succeeded = results.filter((r) => r.status === "success").length;
46
+ const failed = results.length - succeeded;
47
+ console.log(`\nBatch: ${succeeded}/${results.length} succeeded${failed > 0 ? `, ${failed} failed` : ""}`);
48
+ }
49
+ }
50
+ }
51
+ catch (error) {
52
+ console.error(error instanceof Error ? error.message : "Failed to fetch minifig details");
53
+ process.exit(1);
54
+ }
55
+ });
56
+ }
57
+ //# sourceMappingURL=minifig.js.map
@@ -0,0 +1,59 @@
1
+ import { fetchPartDetails } from "../../core/rebrickable/partDetails.js";
2
+ import { formatToolError } from "../../core/utils/errors.js";
3
+ import { formatPartDetails } from "../output.js";
4
+ export function registerPartCommand(program) {
5
+ program
6
+ .command("part")
7
+ .description("Get details about LEGO part(s): colors, sets containing the part")
8
+ .argument("<ids...>", "LEGO part number(s), e.g. 3001")
9
+ .option("-c, --color <name>", "Filter by color name (e.g. Black)")
10
+ .option("--json", "Output raw JSON")
11
+ .action(async (ids, opts) => {
12
+ try {
13
+ if (ids.length === 1) {
14
+ const result = await fetchPartDetails(ids[0], opts.color);
15
+ if (opts.json) {
16
+ console.log(JSON.stringify(result, null, 2));
17
+ }
18
+ else {
19
+ console.log(formatPartDetails(result));
20
+ }
21
+ }
22
+ else {
23
+ const results = [];
24
+ // Sequential due to Rebrickable rate limit
25
+ for (const partId of ids) {
26
+ try {
27
+ const result = await fetchPartDetails(partId, opts.color);
28
+ results.push({ partId, status: "success", result });
29
+ }
30
+ catch (err) {
31
+ results.push({ partId, status: "error", error: formatToolError(err) });
32
+ }
33
+ }
34
+ if (opts.json) {
35
+ console.log(JSON.stringify(results, null, 2));
36
+ }
37
+ else {
38
+ for (const r of results) {
39
+ console.log(`\n--- ${r.partId} ---`);
40
+ if (r.status === "success") {
41
+ console.log(formatPartDetails(r.result));
42
+ }
43
+ else {
44
+ console.error(`Error: ${r.error}`);
45
+ }
46
+ }
47
+ const succeeded = results.filter((r) => r.status === "success").length;
48
+ const failed = results.length - succeeded;
49
+ console.log(`\nBatch: ${succeeded}/${results.length} succeeded${failed > 0 ? `, ${failed} failed` : ""}`);
50
+ }
51
+ }
52
+ }
53
+ catch (error) {
54
+ console.error(error instanceof Error ? error.message : "Failed to fetch part details");
55
+ process.exit(1);
56
+ }
57
+ });
58
+ }
59
+ //# sourceMappingURL=part.js.map
@@ -0,0 +1,57 @@
1
+ import { fetchSetDetails } from "../../core/rebrickable/setDetails.js";
2
+ import { formatToolError } from "../../core/utils/errors.js";
3
+ import { formatSetDetails } from "../output.js";
4
+ export function registerSetCommand(program) {
5
+ program
6
+ .command("set")
7
+ .description("Get LEGO set details: inventory, year, theme, piece count")
8
+ .argument("<ids...>", 'Set number(s), e.g. 75192 (the "-1" suffix is added automatically)')
9
+ .option("--json", "Output raw JSON")
10
+ .action(async (ids, opts) => {
11
+ try {
12
+ if (ids.length === 1) {
13
+ const result = await fetchSetDetails(ids[0]);
14
+ if (opts.json) {
15
+ console.log(JSON.stringify(result, null, 2));
16
+ }
17
+ else {
18
+ console.log(formatSetDetails(result));
19
+ }
20
+ }
21
+ else {
22
+ const results = [];
23
+ for (const setId of ids) {
24
+ try {
25
+ const result = await fetchSetDetails(setId);
26
+ results.push({ setId, status: "success", result });
27
+ }
28
+ catch (err) {
29
+ results.push({ setId, status: "error", error: formatToolError(err) });
30
+ }
31
+ }
32
+ if (opts.json) {
33
+ console.log(JSON.stringify(results, null, 2));
34
+ }
35
+ else {
36
+ for (const r of results) {
37
+ console.log(`\n--- ${r.setId} ---`);
38
+ if (r.status === "success") {
39
+ console.log(formatSetDetails(r.result));
40
+ }
41
+ else {
42
+ console.error(`Error: ${r.error}`);
43
+ }
44
+ }
45
+ const succeeded = results.filter((r) => r.status === "success").length;
46
+ const failed = results.length - succeeded;
47
+ console.log(`\nBatch: ${succeeded}/${results.length} succeeded${failed > 0 ? `, ${failed} failed` : ""}`);
48
+ }
49
+ }
50
+ }
51
+ catch (error) {
52
+ console.error(error instanceof Error ? error.message : "Failed to fetch set details");
53
+ process.exit(1);
54
+ }
55
+ });
56
+ }
57
+ //# sourceMappingURL=set.js.map
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { applyConfig } from "../core/config.js";
4
+ import { initCache } from "../core/cache/index.js";
5
+ import { setCache } from "../core/rebrickable/client.js";
6
+ import { registerHealthCommand } from "./commands/health.js";
7
+ import { registerIdentifyCommand } from "./commands/identify.js";
8
+ import { registerPartCommand } from "./commands/part.js";
9
+ import { registerSetCommand } from "./commands/set.js";
10
+ import { registerMinifigCommand } from "./commands/minifig.js";
11
+ import { registerMcpCommand } from "./commands/mcp.js";
12
+ import { registerConfigCommand } from "./commands/config.js";
13
+ // Load config file values (env vars take priority)
14
+ applyConfig();
15
+ // Initialize cache from resolved config
16
+ const cache = initCache();
17
+ setCache(cache);
18
+ const program = new Command()
19
+ .name("brickscope")
20
+ .description("Identify LEGO parts, sets, and minifigures from images")
21
+ .version("0.1.0");
22
+ registerHealthCommand(program);
23
+ registerIdentifyCommand(program);
24
+ registerPartCommand(program);
25
+ registerSetCommand(program);
26
+ registerMinifigCommand(program);
27
+ registerMcpCommand(program);
28
+ registerConfigCommand(program);
29
+ program.parse();
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,84 @@
1
+ export function formatPrediction(result) {
2
+ const lines = [];
3
+ if (result.matches.length === 0) {
4
+ lines.push("No matches found.");
5
+ return lines.join("\n");
6
+ }
7
+ for (const match of result.matches) {
8
+ const score = (match.score * 100).toFixed(1);
9
+ lines.push(` ${match.id} ${match.name} (${match.type}, ${score}%)`);
10
+ for (const site of match.externalSites) {
11
+ lines.push(` ${site.name}: ${site.url}`);
12
+ }
13
+ }
14
+ if (result.predictedColors && result.predictedColors.length > 0) {
15
+ lines.push("");
16
+ lines.push("Predicted colors:");
17
+ for (const color of result.predictedColors) {
18
+ lines.push(` ${color.name} (${(color.score * 100).toFixed(1)}%)`);
19
+ }
20
+ }
21
+ return lines.join("\n");
22
+ }
23
+ export function formatPartDetails(result) {
24
+ const lines = [];
25
+ lines.push(`Part ${result.part.partNum}: ${result.part.name}`);
26
+ lines.push(` URL: ${result.part.url}`);
27
+ lines.push(` Colors: ${result.totalColors}, Set appearances: ~${result.totalSetsAppearances}`);
28
+ if (result.colorFilter) {
29
+ if (result.colorMatched) {
30
+ lines.push(` Filtered by color: "${result.colorFilter}"`);
31
+ }
32
+ else {
33
+ lines.push(` Color "${result.colorFilter}" not found, showing top 5 colors`);
34
+ }
35
+ }
36
+ lines.push("");
37
+ for (const color of result.colorDetails) {
38
+ lines.push(` ${color.colorName} (${color.numSets} sets):`);
39
+ for (const set of color.sets.slice(0, 10)) {
40
+ lines.push(` ${set.setNum} ${set.setName} (${set.year}, ${set.numParts} pcs)`);
41
+ }
42
+ if (color.sets.length > 10) {
43
+ lines.push(` ... and ${color.sets.length - 10} more`);
44
+ }
45
+ }
46
+ return lines.join("\n");
47
+ }
48
+ export function formatSetDetails(result) {
49
+ const lines = [];
50
+ lines.push(`Set ${result.set.setNum}: ${result.set.name} (${result.set.year})`);
51
+ lines.push(` Pieces: ${result.set.numParts}, Unique parts: ${result.parts.length}, Spare: ${result.spareParts.length}`);
52
+ lines.push(` URL: ${result.set.url}`);
53
+ lines.push("");
54
+ lines.push("Parts:");
55
+ for (const part of result.parts) {
56
+ lines.push(` ${part.quantity}x ${part.partNum} ${part.name} [${part.color}]`);
57
+ }
58
+ if (result.spareParts.length > 0) {
59
+ lines.push("");
60
+ lines.push("Spare parts:");
61
+ for (const part of result.spareParts) {
62
+ lines.push(` ${part.quantity}x ${part.partNum} ${part.name} [${part.color}]`);
63
+ }
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+ export function formatMinifigDetails(result) {
68
+ const lines = [];
69
+ lines.push(`Minifigure ${result.minifig.id}: ${result.minifig.name}`);
70
+ lines.push(` Parts: ${result.minifig.numParts}`);
71
+ lines.push(` URL: ${result.minifig.url}`);
72
+ lines.push("");
73
+ if (result.appearsInSets.length > 0) {
74
+ lines.push(`Appears in ${result.appearsInSets.length} set(s):`);
75
+ for (const set of result.appearsInSets) {
76
+ lines.push(` ${set.setNum} ${set.setName} (${set.numParts} pcs)`);
77
+ }
78
+ }
79
+ else {
80
+ lines.push("Does not appear in any sets.");
81
+ }
82
+ return lines.join("\n");
83
+ }
84
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1,39 @@
1
+ import { apiError, unexpectedResponse } from "../utils/errors.js";
2
+ const PARTS_PREDICT_ENDPOINT = "/predict/parts/";
3
+ const BASE_URL = "https://api.brickognize.com";
4
+ export async function checkHealth() {
5
+ const res = await fetch(`${BASE_URL}/health/`, {
6
+ signal: AbortSignal.timeout(10_000),
7
+ });
8
+ if (!res.ok) {
9
+ throw apiError(res.status, await res.text());
10
+ }
11
+ const data = await res.json();
12
+ if (typeof data !== "object" || data === null) {
13
+ throw unexpectedResponse("health endpoint did not return an object");
14
+ }
15
+ return data;
16
+ }
17
+ export async function predict(endpoint, imageBlob, filename) {
18
+ const form = new FormData();
19
+ form.append("query_image", imageBlob, filename);
20
+ // Always request color prediction for the parts endpoint specifically
21
+ const url = endpoint === PARTS_PREDICT_ENDPOINT
22
+ ? `${BASE_URL}${endpoint}?predict_color=true`
23
+ : `${BASE_URL}${endpoint}`;
24
+ const res = await fetch(url, {
25
+ method: "POST",
26
+ body: form,
27
+ signal: AbortSignal.timeout(60_000),
28
+ });
29
+ if (!res.ok) {
30
+ const body = await res.text();
31
+ throw apiError(res.status, body);
32
+ }
33
+ const data = await res.json();
34
+ if (!data || typeof data.listing_id !== "string" || !Array.isArray(data.items)) {
35
+ throw unexpectedResponse("missing listing_id or items array");
36
+ }
37
+ return data;
38
+ }
39
+ //# sourceMappingURL=client.js.map