@reneturcios/shelfsignal 0.0.0-stage → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rene Turcios
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 CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # ShelfSignal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **The open store index for AI agents.**
4
+
5
+ Local merchants list what they actually sell. AI agents find real food and real products nearby, score how likely each item is actually in stock, and check a validation receipt before they send a human across town. Merchants pay one flat price to get listed. Nobody takes 30% of their food.
6
+
7
+ DoorDash and the delivery apps built a closed index: they own the customer, scrape or rent the menu, charge the merchant 15-30% per order, and still cannot tell you whether the kitchen has the thing you want. ShelfSignal is the opposite shape:
8
+
9
+ - **Merchant-owned listing.** The store submits its own stuff, its own prices, its own stock. The listing belongs to the merchant, not the platform.
10
+ - **Built for agents, not ads.** One MCP server any agent can query: find stores, find items, check stock, validate the listing. No sponsored placement, ever.
11
+ - **Stock is a scored signal, not a claim.** Every item carries a last-confirmed date. The likelihood engine turns that evidence into a 0-100 score with printed reasons, so agents stop bluffing "they have it" and start saying "confirmed today, likely in stock."
12
+ - **Validation receipts.** Every listing carries checks an agent can verify: complete profile, real prices, fresh inventory, declared fulfillment. Tier: VERIFIED, LISTED, STALE, or INCOMPLETE.
13
+ - **Flat money.** $100 Starter Store, one time, listed and validated. No per-order commission. See [docs/PRICING.md](docs/PRICING.md).
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ npm test
21
+
22
+ node dist/cli.js coverage
23
+ node dist/cli.js search "birria" --city "San Francisco"
24
+ node dist/cli.js stock sf-mission-taqueria-demo "Birria tacos"
25
+ node dist/cli.js validate sf-mission-taqueria-demo
26
+ node dist/cli.js mcp # run the MCP server (stdio)
27
+ ```
28
+
29
+ ## MCP tools
30
+
31
+ | Tool | What it does |
32
+ |---|---|
33
+ | `search_stores` | Find stores by text, city, category, or type |
34
+ | `search_items` | Find a food or product across every listed store |
35
+ | `get_store` | Full listing: hours, address, fulfillment, items, stock |
36
+ | `check_stock` | Stock-likelihood verdict (score, band, reasons) for one item |
37
+ | `validate_store` | Validation receipt and tier for one listing |
38
+ | `submit_store` | Validate a merchant intake and preview its listing + receipt |
39
+ | `list_coverage` | Cities, categories, and store count in the index |
40
+
41
+ ## For merchants
42
+
43
+ You run a store in the city. For $100 you get listed where the agents look:
44
+
45
+ 1. Fill the intake (10 minutes): who you are, where you are, your hours, and the stuff you sell with real prices. Template: [examples/submission.example.json](examples/submission.example.json), field guide: [docs/INTAKE.md](docs/INTAKE.md).
46
+ 2. We validate it and your listing goes live with its first receipt.
47
+ 3. Update stock whenever it changes with one command or one message. Fresh listings rank as VERIFIED; stale ones decay honestly instead of lying to customers.
48
+
49
+ No commission. No tablet on your counter. No competing with your own listing.
50
+
51
+ ## How it works
52
+
53
+ - The registry is the source of truth: `registry/stores.json` (v0.1, git-backed, every change reviewable).
54
+ - The likelihood engine is deterministic: merchant-reported status, confirmation freshness, staple vs specialty. No model guesses.
55
+ - The management layer is freshness: listings decay, validation re-runs on every read, and merchants see exactly what an agent sees. Roadmap in [docs/MANAGEMENT.md](docs/MANAGEMENT.md).
56
+ - Full design: [docs/SPEC.md](docs/SPEC.md). Why this beats the delivery model: [docs/VS-DOORDASH.md](docs/VS-DOORDASH.md).
57
+
58
+ Bundled stores are sample data and are flagged `demo: true` in every response. Real merchants are never marked demo, and demo stores are never presented as real.
59
+
60
+ ## License
61
+
62
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { loadRegistry, registryPath, getStore, searchStores, searchItems, listCities, listCategories } from "./registry.js";
4
+ import { stockLikelihood } from "./likelihood.js";
5
+ import { validateStore } from "./validate.js";
6
+ import { processSubmission } from "./submit.js";
7
+ const [cmd, ...args] = process.argv.slice(2);
8
+ const flag = (name) => {
9
+ const i = args.indexOf(`--${name}`);
10
+ return i >= 0 ? args[i + 1] : undefined;
11
+ };
12
+ const VALUE_FLAGS = new Set(["--city", "--category", "--type", "--query"]);
13
+ const positional = [];
14
+ for (let i = 0; i < args.length; i++) {
15
+ const a = args[i];
16
+ if (VALUE_FLAGS.has(a)) {
17
+ i++;
18
+ continue;
19
+ }
20
+ if (a.startsWith("--"))
21
+ continue;
22
+ positional.push(a);
23
+ }
24
+ const out = (data) => console.log(JSON.stringify(data, null, 2));
25
+ function usage() {
26
+ console.log(`ShelfSignal: the open store index for AI agents
27
+
28
+ Usage:
29
+ shelfsignal list [--city <city>] [--category <cat>] [--type <type>] [--query <text>]
30
+ shelfsignal search <item> [--city <city>] Find a food or product across stores
31
+ shelfsignal store <store-id> Full listing for one store
32
+ shelfsignal stock <store-id> <item> Stock-likelihood verdict with reasons
33
+ shelfsignal validate <store-id> Validation receipt for a listing
34
+ shelfsignal validate-all Receipts for every listing
35
+ shelfsignal submit <submission.json> [--write] Check a merchant intake; --write adds it to the registry
36
+ shelfsignal coverage Cities, categories, store count
37
+ shelfsignal mcp Run the MCP server (stdio)
38
+ `);
39
+ }
40
+ async function main() {
41
+ switch (cmd) {
42
+ case "mcp": {
43
+ const { runMcp } = await import("./mcp.js");
44
+ await runMcp();
45
+ return;
46
+ }
47
+ case "list": {
48
+ out(searchStores(loadRegistry(), { query: flag("query"), city: flag("city"), category: flag("category"), type: flag("type") }));
49
+ return;
50
+ }
51
+ case "search": {
52
+ const query = positional.join(" ");
53
+ if (!query)
54
+ return usage();
55
+ out(searchItems(loadRegistry(), query, flag("city")).map(({ store, item }) => ({ storeId: store.id, storeName: store.name, item })));
56
+ return;
57
+ }
58
+ case "store": {
59
+ const store = getStore(loadRegistry(), positional[0] ?? "");
60
+ out(store ?? { error: `No store with id ${positional[0]}` });
61
+ return;
62
+ }
63
+ case "stock": {
64
+ const reg = loadRegistry();
65
+ const store = getStore(reg, positional[0] ?? "");
66
+ if (!store)
67
+ return out({ error: `No store with id ${positional[0]}` });
68
+ const name = positional.slice(1).join(" ");
69
+ const item = store.items.find((i) => i.name.toLowerCase() === name.toLowerCase())
70
+ ?? store.items.find((i) => i.name.toLowerCase().includes(name.toLowerCase()));
71
+ out(item ? stockLikelihood(store, item) : { error: `${store.name} does not list '${name}'`, listedItems: store.items.map((i) => i.name) });
72
+ return;
73
+ }
74
+ case "validate": {
75
+ const store = getStore(loadRegistry(), positional[0] ?? "");
76
+ out(store ? validateStore(store) : { error: `No store with id ${positional[0]}` });
77
+ return;
78
+ }
79
+ case "validate-all": {
80
+ out(loadRegistry().stores.map((s) => validateStore(s)));
81
+ return;
82
+ }
83
+ case "submit": {
84
+ const file = positional[0];
85
+ if (!file)
86
+ return usage();
87
+ const result = processSubmission(JSON.parse(readFileSync(file, "utf8")));
88
+ out(result);
89
+ if (result.ok && args.includes("--write") && result.store) {
90
+ const path = registryPath();
91
+ const reg = loadRegistry(path);
92
+ if (reg.stores.some((s) => s.id === result.store.id)) {
93
+ console.error(`Store id ${result.store.id} already exists in the registry; not overwritten.`);
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+ reg.stores.push(result.store);
98
+ writeFileSync(path, JSON.stringify(reg, null, 2) + "\n");
99
+ console.error(`Wrote ${result.store.id} to ${path}`);
100
+ }
101
+ if (!result.ok)
102
+ process.exitCode = 1;
103
+ return;
104
+ }
105
+ case "coverage": {
106
+ const reg = loadRegistry();
107
+ out({ cities: listCities(reg), categories: listCategories(reg), stores: reg.stores.length });
108
+ return;
109
+ }
110
+ default:
111
+ usage();
112
+ }
113
+ }
114
+ main().catch((err) => {
115
+ console.error(err instanceof Error ? err.message : err);
116
+ process.exit(1);
117
+ });
118
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export * from "./registry.js";
3
+ export * from "./likelihood.js";
4
+ export * from "./validate.js";
5
+ export * from "./submit.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./registry.js";
3
+ export * from "./likelihood.js";
4
+ export * from "./validate.js";
5
+ export * from "./submit.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Store, StoreItem, StockVerdict } from "./types.js";
2
+ export declare function daysBetween(fromIso: string, now: Date): number;
3
+ /**
4
+ * Deterministic stock-likelihood engine.
5
+ *
6
+ * An agent should never tell a human "they have it" on a guess. ShelfSignal
7
+ * scores the *evidence* instead: what the merchant last confirmed, how fresh
8
+ * that confirmation is, and whether the item is an everyday staple or a
9
+ * specialty item that sells out. Every verdict ships its reasons so the
10
+ * calling agent can quote them instead of bluffing.
11
+ */
12
+ export declare function stockLikelihood(store: Store, item: StoreItem, now?: Date): StockVerdict;
@@ -0,0 +1,82 @@
1
+ export function daysBetween(fromIso, now) {
2
+ const from = new Date(fromIso + (fromIso.length === 10 ? "T12:00:00Z" : ""));
3
+ if (Number.isNaN(from.getTime()))
4
+ return Number.POSITIVE_INFINITY;
5
+ return Math.max(0, Math.floor((now.getTime() - from.getTime()) / 86_400_000));
6
+ }
7
+ /**
8
+ * Deterministic stock-likelihood engine.
9
+ *
10
+ * An agent should never tell a human "they have it" on a guess. ShelfSignal
11
+ * scores the *evidence* instead: what the merchant last confirmed, how fresh
12
+ * that confirmation is, and whether the item is an everyday staple or a
13
+ * specialty item that sells out. Every verdict ships its reasons so the
14
+ * calling agent can quote them instead of bluffing.
15
+ */
16
+ export function stockLikelihood(store, item, now = new Date()) {
17
+ const reasons = [];
18
+ let score;
19
+ switch (item.stock) {
20
+ case "in_stock":
21
+ score = 90;
22
+ reasons.push("Merchant last reported this item in stock");
23
+ break;
24
+ case "low":
25
+ score = 45;
26
+ reasons.push("Merchant last reported this item running low");
27
+ break;
28
+ case "out":
29
+ score = 5;
30
+ reasons.push("Merchant last reported this item out of stock");
31
+ break;
32
+ default:
33
+ score = 35;
34
+ reasons.push("Merchant has not reported a stock status for this item");
35
+ }
36
+ const age = daysBetween(item.lastConfirmed, now);
37
+ if (!Number.isFinite(age)) {
38
+ score -= 25;
39
+ reasons.push("No confirmation date on record, freshness unknown");
40
+ }
41
+ else if (age === 0) {
42
+ reasons.push("Confirmed today");
43
+ }
44
+ else if (age <= 2) {
45
+ score -= 5;
46
+ reasons.push(`Confirmed ${age} day${age === 1 ? "" : "s"} ago`);
47
+ }
48
+ else if (age <= 7) {
49
+ score -= 15;
50
+ reasons.push(`Confirmed ${age} days ago, getting stale`);
51
+ }
52
+ else if (age <= 30) {
53
+ score -= 30;
54
+ reasons.push(`Confirmed ${age} days ago, stale signal`);
55
+ }
56
+ else {
57
+ score -= 45;
58
+ reasons.push(`Last confirmed ${age} days ago, treat as unknown`);
59
+ }
60
+ if (item.staple) {
61
+ score += 5;
62
+ reasons.push("Everyday staple item, restocked routinely");
63
+ }
64
+ if (store.tier === "starter" || store.tier === "growth" || store.tier === "pro") {
65
+ reasons.push(`Listing tier: ${store.tier}`);
66
+ }
67
+ if (store.demo)
68
+ reasons.push("DEMO listing, sample data only, not a real merchant claim");
69
+ score = Math.max(0, Math.min(100, Math.round(score)));
70
+ const band = score >= 75 ? "likely" : score >= 50 ? "probable" : score >= 25 ? "uncertain" : "unlikely";
71
+ return {
72
+ storeId: store.id,
73
+ storeName: store.name,
74
+ item: item.name,
75
+ score,
76
+ band,
77
+ reasons,
78
+ lastConfirmed: item.lastConfirmed ?? null,
79
+ demo: Boolean(store.demo),
80
+ };
81
+ }
82
+ //# sourceMappingURL=likelihood.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"likelihood.js","sourceRoot":"","sources":["../src/likelihood.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runMcp(): Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,51 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { loadRegistry, getStore, searchStores, searchItems, listCities, listCategories } from "./registry.js";
5
+ import { stockLikelihood } from "./likelihood.js";
6
+ import { validateStore } from "./validate.js";
7
+ import { processSubmission } from "./submit.js";
8
+ const text = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
9
+ export async function runMcp() {
10
+ const server = new McpServer({ name: "shelfsignal", version: "0.1.0" });
11
+ server.tool("search_stores", "Find real local stores (restaurants, groceries, bakeries, bodegas, markets) in a city. Returns merchant-submitted listings, not scraped delivery-app menus.", {
12
+ query: z.string().optional().describe("Free text matched against store names, categories, and items."),
13
+ city: z.string().optional().describe("City name, e.g. San Francisco."),
14
+ category: z.string().optional().describe("Category filter, e.g. tacos, bakery, produce."),
15
+ type: z.string().optional().describe("Store type: restaurant, grocery, bakery, bodega, cafe, pharmacy, butcher, market, retail."),
16
+ }, async (args) => text(searchStores(loadRegistry(), args)));
17
+ server.tool("search_items", "Find a specific food or product across listed stores, e.g. 'birria tacos' or 'oat milk'. Pair with check_stock before telling a human a store has it.", {
18
+ query: z.string().describe("Item or food to find."),
19
+ city: z.string().optional().describe("Limit to one city."),
20
+ }, async ({ query, city }) => text(searchItems(loadRegistry(), query, city).map(({ store, item }) => ({ storeId: store.id, storeName: store.name, neighborhood: store.neighborhood, item }))));
21
+ server.tool("get_store", "Full listing for one store by registry id: hours, address, fulfillment, and every item with its last-confirmed stock status.", { store_id: z.string().describe("Registry id, e.g. sf-mission-taqueria-demo.") }, async ({ store_id }) => {
22
+ const store = getStore(loadRegistry(), store_id);
23
+ return text(store ?? { error: `No store with id ${store_id}` });
24
+ });
25
+ server.tool("check_stock", "Score how likely a store actually has an item right now. Returns a 0-100 score, a band (likely / probable / uncertain / unlikely), and the reasons. Never present an 'uncertain' or 'unlikely' verdict to a human as available.", {
26
+ store_id: z.string().describe("Registry id of the store."),
27
+ item: z.string().describe("Item name as listed, e.g. 'Birria tacos'."),
28
+ }, async ({ store_id, item }) => {
29
+ const reg = loadRegistry();
30
+ const store = getStore(reg, store_id);
31
+ if (!store)
32
+ return text({ error: `No store with id ${store_id}` });
33
+ const found = store.items.find((i) => i.name.toLowerCase() === item.toLowerCase())
34
+ ?? store.items.find((i) => i.name.toLowerCase().includes(item.toLowerCase()));
35
+ if (!found)
36
+ return text({ error: `${store.name} does not list '${item}'`, listedItems: store.items.map((i) => i.name) });
37
+ return text(stockLikelihood(store, found));
38
+ });
39
+ server.tool("validate_store", "Run the ShelfSignal validation checks on a listing and return its receipt: tier (VERIFIED / LISTED / STALE / INCOMPLETE), score, and every check with its evidence. Use this before recommending a store you have not used before.", { store_id: z.string().describe("Registry id of the store.") }, async ({ store_id }) => {
40
+ const store = getStore(loadRegistry(), store_id);
41
+ return text(store ? validateStore(store) : { error: `No store with id ${store_id}` });
42
+ });
43
+ server.tool("submit_store", "Validate a merchant submission (the $100 Starter Store intake) and preview exactly how it would appear in the registry, with its validation receipt. Does not charge anything and does not publish until the listing is merged into the registry.", { submission: z.record(z.unknown()).describe("The merchant submission object; see examples/submission.example.json for the shape.") }, async ({ submission }) => text(processSubmission(submission)));
44
+ server.tool("list_coverage", "Which cities and categories the index currently covers, with store counts. Check coverage before promising a human you can search somewhere.", {}, async () => {
45
+ const reg = loadRegistry();
46
+ return text({ cities: listCities(reg), categories: listCategories(reg), stores: reg.stores.length });
47
+ });
48
+ const transport = new StdioServerTransport();
49
+ await server.connect(transport);
50
+ }
51
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Registry, Store } from "./types.js";
2
+ /** Registry source of truth: registry/stores.json in the repo (v0.1, git-backed). */
3
+ export declare function registryPath(): string;
4
+ export declare function loadRegistry(path?: string): Registry;
5
+ export declare function getStore(reg: Registry, id: string): Store | undefined;
6
+ export declare function searchStores(reg: Registry, opts?: {
7
+ query?: string;
8
+ city?: string;
9
+ category?: string;
10
+ type?: string;
11
+ }): Store[];
12
+ export interface ItemHit {
13
+ store: Store;
14
+ item: Store["items"][number];
15
+ }
16
+ export declare function searchItems(reg: Registry, query: string, city?: string): ItemHit[];
17
+ export declare function listCities(reg: Registry): string[];
18
+ export declare function listCategories(reg: Registry): string[];
@@ -0,0 +1,67 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** Registry source of truth: registry/stores.json in the repo (v0.1, git-backed). */
5
+ export function registryPath() {
6
+ const env = process.env.SHELFSIGNAL_REGISTRY;
7
+ if (env)
8
+ return env;
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const candidates = [
11
+ join(here, "..", "registry", "stores.json"),
12
+ join(here, "..", "..", "registry", "stores.json"),
13
+ join(process.cwd(), "registry", "stores.json"),
14
+ ];
15
+ for (const c of candidates)
16
+ if (existsSync(c))
17
+ return c;
18
+ return candidates[0];
19
+ }
20
+ export function loadRegistry(path = registryPath()) {
21
+ const raw = readFileSync(path, "utf8");
22
+ const parsed = JSON.parse(raw);
23
+ if (!Array.isArray(parsed.stores))
24
+ throw new Error(`Registry at ${path} has no stores array`);
25
+ return parsed;
26
+ }
27
+ export function getStore(reg, id) {
28
+ return reg.stores.find((s) => s.id === id);
29
+ }
30
+ const norm = (s) => s.toLowerCase().trim();
31
+ export function searchStores(reg, opts = {}) {
32
+ return reg.stores.filter((s) => {
33
+ if (opts.city && norm(s.city) !== norm(opts.city))
34
+ return false;
35
+ if (opts.type && norm(s.type) !== norm(opts.type))
36
+ return false;
37
+ if (opts.category && !s.categories.some((c) => norm(c).includes(norm(opts.category))))
38
+ return false;
39
+ if (opts.query) {
40
+ const q = norm(opts.query);
41
+ const hay = [s.name, s.type, s.city, s.neighborhood ?? "", ...s.categories, ...s.items.map((i) => i.name)].join(" ").toLowerCase();
42
+ if (!hay.includes(q))
43
+ return false;
44
+ }
45
+ return true;
46
+ });
47
+ }
48
+ export function searchItems(reg, query, city) {
49
+ const q = norm(query);
50
+ const hits = [];
51
+ for (const store of reg.stores) {
52
+ if (city && norm(store.city) !== norm(city))
53
+ continue;
54
+ for (const item of store.items) {
55
+ if (norm(item.name).includes(q) || norm(item.category).includes(q))
56
+ hits.push({ store, item });
57
+ }
58
+ }
59
+ return hits;
60
+ }
61
+ export function listCities(reg) {
62
+ return [...new Set(reg.stores.map((s) => s.city))].sort();
63
+ }
64
+ export function listCategories(reg) {
65
+ return [...new Set(reg.stores.flatMap((s) => s.categories))].sort();
66
+ }
67
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { Store } from "./types.js";
2
+ import { validateStore } from "./validate.js";
3
+ export interface SubmissionResult {
4
+ ok: boolean;
5
+ store: Store | null;
6
+ problems: string[];
7
+ receipt: ReturnType<typeof validateStore> | null;
8
+ }
9
+ /**
10
+ * Merchant intake. A store owner fills one JSON form (see
11
+ * examples/submission.example.json and docs/INTAKE.md); submit checks it the
12
+ * same way agents will see it, assigns the registry id, stamps the tier, and
13
+ * returns the validation receipt the merchant gets for their $100 starter.
14
+ * Payment itself happens outside the repo (Stripe link in docs/PRICING.md);
15
+ * nothing here charges a card.
16
+ */
17
+ export declare function processSubmission(input: unknown, now?: Date): SubmissionResult;
package/dist/submit.js ADDED
@@ -0,0 +1,67 @@
1
+ import { validateStore } from "./validate.js";
2
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
3
+ /**
4
+ * Merchant intake. A store owner fills one JSON form (see
5
+ * examples/submission.example.json and docs/INTAKE.md); submit checks it the
6
+ * same way agents will see it, assigns the registry id, stamps the tier, and
7
+ * returns the validation receipt the merchant gets for their $100 starter.
8
+ * Payment itself happens outside the repo (Stripe link in docs/PRICING.md);
9
+ * nothing here charges a card.
10
+ */
11
+ export function processSubmission(input, now = new Date()) {
12
+ const problems = [];
13
+ const raw = (input ?? {});
14
+ if (!raw.name || String(raw.name).trim().length < 2)
15
+ problems.push("name is required");
16
+ if (!raw.address || String(raw.address).trim().length < 5)
17
+ problems.push("address is required");
18
+ if (!raw.city || String(raw.city).trim().length < 2)
19
+ problems.push("city is required");
20
+ if (!raw.hours)
21
+ problems.push("hours are required");
22
+ if (!raw.type)
23
+ problems.push("type is required (restaurant, grocery, bakery, bodega, cafe, pharmacy, butcher, market, retail, other)");
24
+ if (!Array.isArray(raw.items) || raw.items.length === 0)
25
+ problems.push("at least one item is required");
26
+ if (Array.isArray(raw.items)) {
27
+ raw.items.forEach((item, idx) => {
28
+ if (!item?.name)
29
+ problems.push(`items[${idx}].name is required`);
30
+ if (item && (item.price === undefined || item.price === null))
31
+ problems.push(`items[${idx}].price is required (use the real shelf price)`);
32
+ if (item && !item.lastConfirmed)
33
+ problems.push(`items[${idx}].lastConfirmed is required (date you last confirmed it, YYYY-MM-DD)`);
34
+ });
35
+ }
36
+ if (problems.length)
37
+ return { ok: false, store: null, problems, receipt: null };
38
+ const today = now.toISOString().slice(0, 10);
39
+ const store = {
40
+ id: raw.id ?? `${slug(raw.city)}-${slug(raw.name)}`,
41
+ name: String(raw.name).trim(),
42
+ type: String(raw.type).trim(),
43
+ city: String(raw.city).trim(),
44
+ neighborhood: raw.neighborhood,
45
+ address: String(raw.address).trim(),
46
+ phone: raw.phone,
47
+ website: raw.website,
48
+ hours: String(raw.hours).trim(),
49
+ categories: Array.isArray(raw.categories) && raw.categories.length ? raw.categories.map(String) : [String(raw.type)],
50
+ fulfillment: Array.isArray(raw.fulfillment) && raw.fulfillment.length ? raw.fulfillment : ["walk_in"],
51
+ items: (raw.items ?? []).map((i) => ({
52
+ name: String(i.name),
53
+ category: String(i.category ?? raw.type),
54
+ price: i.price ?? null,
55
+ unit: i.unit,
56
+ stock: i.stock ?? "unknown",
57
+ lastConfirmed: String(i.lastConfirmed ?? today),
58
+ staple: Boolean(i.staple),
59
+ notes: i.notes,
60
+ })),
61
+ updatedAt: today,
62
+ tier: "starter",
63
+ demo: false,
64
+ };
65
+ return { ok: true, store, problems: [], receipt: validateStore(store, now) };
66
+ }
67
+ //# sourceMappingURL=submit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submit.js","sourceRoot":"","sources":["../src/submit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ export type StockStatus = "in_stock" | "low" | "out" | "unknown";
2
+ export type Fulfillment = "pickup" | "walk_in" | "own_delivery" | "curbside";
3
+ export interface StoreItem {
4
+ name: string;
5
+ category: string;
6
+ price: number | null;
7
+ unit?: string;
8
+ stock: StockStatus;
9
+ /** ISO date the merchant last confirmed this item, e.g. 2026-10-25 */
10
+ lastConfirmed: string;
11
+ staple?: boolean;
12
+ notes?: string;
13
+ }
14
+ export interface Store {
15
+ id: string;
16
+ name: string;
17
+ /** restaurant, grocery, bakery, bodega, cafe, pharmacy, butcher, market, retail, other */
18
+ type: string;
19
+ city: string;
20
+ neighborhood?: string;
21
+ address: string;
22
+ phone?: string;
23
+ website?: string;
24
+ /** free text, e.g. "Mon-Sun 8am-9pm" */
25
+ hours: string;
26
+ categories: string[];
27
+ fulfillment: Fulfillment[];
28
+ items: StoreItem[];
29
+ /** ISO date the listing itself was last updated by the merchant */
30
+ updatedAt: string;
31
+ /** starter = $100 listing tier; growth / pro reserved for later tiers */
32
+ tier: "starter" | "growth" | "pro";
33
+ /** true only for bundled sample data, never for a real merchant */
34
+ demo?: boolean;
35
+ }
36
+ export interface Registry {
37
+ version: number;
38
+ stores: Store[];
39
+ }
40
+ export type LikelihoodBand = "likely" | "probable" | "uncertain" | "unlikely";
41
+ export interface StockVerdict {
42
+ storeId: string;
43
+ storeName: string;
44
+ item: string;
45
+ score: number;
46
+ band: LikelihoodBand;
47
+ reasons: string[];
48
+ lastConfirmed: string | null;
49
+ demo: boolean;
50
+ }
51
+ export type ValidationTier = "VERIFIED" | "LISTED" | "STALE" | "INCOMPLETE";
52
+ export interface ValidationCheck {
53
+ name: string;
54
+ pass: boolean;
55
+ detail: string;
56
+ }
57
+ export interface ValidationReceipt {
58
+ storeId: string;
59
+ storeName: string;
60
+ tier: ValidationTier;
61
+ score: number;
62
+ checks: ValidationCheck[];
63
+ validatedAt: string;
64
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import type { Store, ValidationReceipt } from "./types.js";
2
+ /**
3
+ * Store validation: the receipt an agent (or a human) can check before
4
+ * trusting a listing. Same philosophy as the fleet's audit tools: a listing
5
+ * is only as good as its evidence, and the evidence is printed, not hidden.
6
+ *
7
+ * Tiers:
8
+ * VERIFIED every check passes and inventory was confirmed within 7 days
9
+ * LISTED complete profile, freshness between 8 and 30 days
10
+ * STALE complete profile but nothing confirmed in over 30 days
11
+ * INCOMPLETE profile is missing something an agent needs to act on
12
+ */
13
+ export declare function validateStore(store: Store, now?: Date): ValidationReceipt;
@@ -0,0 +1,48 @@
1
+ import { daysBetween } from "./likelihood.js";
2
+ /**
3
+ * Store validation: the receipt an agent (or a human) can check before
4
+ * trusting a listing. Same philosophy as the fleet's audit tools: a listing
5
+ * is only as good as its evidence, and the evidence is printed, not hidden.
6
+ *
7
+ * Tiers:
8
+ * VERIFIED every check passes and inventory was confirmed within 7 days
9
+ * LISTED complete profile, freshness between 8 and 30 days
10
+ * STALE complete profile but nothing confirmed in over 30 days
11
+ * INCOMPLETE profile is missing something an agent needs to act on
12
+ */
13
+ export function validateStore(store, now = new Date()) {
14
+ const checks = [];
15
+ const add = (name, pass, detail) => checks.push({ name, pass, detail });
16
+ add("name", store.name.trim().length >= 2, store.name || "missing");
17
+ add("address", store.address.trim().length >= 5, store.address || "missing");
18
+ add("city", store.city.trim().length >= 2, store.city || "missing");
19
+ add("hours", store.hours.trim().length >= 3, store.hours || "missing");
20
+ add("contact", Boolean(store.phone || store.website), store.phone || store.website || "no phone or website");
21
+ add("categories", store.categories.length >= 1, `${store.categories.length} categories`);
22
+ add("fulfillment", store.fulfillment.length >= 1, store.fulfillment.join(", ") || "none declared");
23
+ add("has_items", store.items.length >= 1, `${store.items.length} items listed`);
24
+ add("items_priced", store.items.length > 0 && store.items.every((i) => i.price !== null && i.price > 0), store.items.length ? `${store.items.filter((i) => i.price !== null && i.price > 0).length}/${store.items.length} items priced` : "no items");
25
+ const freshest = store.items.reduce((min, i) => Math.min(min, daysBetween(i.lastConfirmed, now)), Number.POSITIVE_INFINITY);
26
+ add("inventory_fresh", Number.isFinite(freshest) && freshest <= 30, Number.isFinite(freshest) ? `freshest item confirmed ${freshest}d ago` : "no confirmation dates");
27
+ const passed = checks.filter((c) => c.pass).length;
28
+ const score = Math.round((passed / checks.length) * 100);
29
+ const complete = checks.slice(0, 9).every((c) => c.pass);
30
+ let tier;
31
+ if (!complete)
32
+ tier = "INCOMPLETE";
33
+ else if (freshest <= 7)
34
+ tier = "VERIFIED";
35
+ else if (freshest <= 30)
36
+ tier = "LISTED";
37
+ else
38
+ tier = "STALE";
39
+ return {
40
+ storeId: store.id,
41
+ storeName: store.name,
42
+ tier,
43
+ score,
44
+ checks,
45
+ validatedAt: now.toISOString(),
46
+ };
47
+ }
48
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
package/docs/INTAKE.md ADDED
@@ -0,0 +1,30 @@
1
+ # Merchant intake: what to submit
2
+
3
+ Ten minutes, one form. This is everything a store needs to be listed and validated. The JSON template is `examples/submission.example.json`; a hosted form that emits the same shape is on the roadmap, and until then a store can send these answers in any format and we convert them.
4
+
5
+ ## About the store
6
+
7
+ - **name**: exactly as it appears on your sign
8
+ - **type**: restaurant, grocery, bakery, bodega, cafe, pharmacy, butcher, market, retail, or other
9
+ - **address**: street address where customers show up
10
+ - **city** and **neighborhood**
11
+ - **phone** or **website**: at least one way to reach you (agents check this before recommending you)
12
+ - **hours**: plain text is fine, e.g. "Mon-Sun 8am-9pm"
13
+ - **categories**: the words customers and agents use for you: "tacos", "sourdough", "produce"
14
+ - **fulfillment**: pickup, walk_in, own_delivery, curbside
15
+
16
+ ## Your stuff
17
+
18
+ For each item (Starter covers up to 50):
19
+
20
+ - **name**: what a customer would ask for
21
+ - **price**: the real shelf price. Listings with missing prices fail validation.
22
+ - **stock**: in_stock, low, out (be honest; agents score this, and a store that cries wolf decays)
23
+ - **lastConfirmed**: the date you last laid eyes on it, YYYY-MM-DD
24
+ - **staple**: true for everyday items you restock routinely
25
+
26
+ ## After you submit
27
+
28
+ - You get a validation receipt: your tier and the checks behind it.
29
+ - VERIFIED means complete and fresh. Keep items confirmed within 7 days to stay VERIFIED.
30
+ - Refreshing stock is one message or one command. It is always free.
@@ -0,0 +1,24 @@
1
+ # Management system
2
+
3
+ ShelfSignal's management system is built around one idea: **the registry is the business, and freshness is the operations job.**
4
+
5
+ ## Source of truth
6
+
7
+ `registry/stores.json` holds every listing. In v0.1 it lives in git: every new store, price change, and stock refresh is a diff a human reviewed. That is a feature at pilot scale (25-100 stores): zero infrastructure, full audit trail, no database to babysit.
8
+
9
+ ## The daily loop
10
+
11
+ 1. **Freshness scan**: `shelfsignal validate-all` re-scores every listing. Stores sliding from VERIFIED to LISTED (nothing confirmed in 8+ days) get a nudge list.
12
+ 2. **Merchant nudges**: one short message per stale store: "Your eggs were last confirmed 12 days ago; agents now see you as uncertain. Reply with today's stock to recover VERIFIED." The reply updates `lastConfirmed` and the affected items.
13
+ 3. **Intake queue**: new submissions run through `shelfsignal submit` before merge; failures go back to the merchant with the named problems from the validator, not a form rejection.
14
+ 4. **Agent-traffic log** (roadmap, Growth tier): record every `search_items` / `check_stock` query, aggregate weekly per store: what agents looked for, what the store had, what it missed. Misses are the merchant's best restock list and the sales proof for renewal.
15
+
16
+ ## Roles
17
+
18
+ - **Operator (Rene)**: owns the registry, Stripe link, and merchant relationships. Approves merges.
19
+ - **Merchants**: own their data. They can update or delist at any time; a delisted store leaves the registry in the next commit.
20
+ - **Agents**: read-only. An agent can validate and quote receipts; it cannot edit a listing.
21
+
22
+ ## When it outgrows git
23
+
24
+ At roughly 500 stores or the second city, move the same JSON schema into a hosted store (Postgres or a document DB) behind the same MCP tools, keep git as the export format, and give merchants the dashboard: stock board, freshness alerts, agent-traffic report. The MCP contract does not change, so agents never notice the move.
@@ -0,0 +1,26 @@
1
+ # Pricing
2
+
3
+ ## Starter Store: $100, one time
4
+
5
+ Everything a local store needs to be found by AI agents:
6
+
7
+ - Full registry listing: profile, hours, address, fulfillment, categories
8
+ - Up to 50 items with real prices and stock status
9
+ - Validation run + public receipt (VERIFIED tier when fresh)
10
+ - Stock-likelihood scoring on every item
11
+ - Agent discovery through the ShelfSignal MCP server
12
+ - Merchant update path (one command or one message to refresh stock)
13
+ - One city, one location
14
+
15
+ No commission on any sale, ever. No monthly fee to stay listed. A listing that goes stale decays in tier honestly; the merchant refreshes it for free.
16
+
17
+ ## Why flat, why $100
18
+
19
+ Delivery apps charge 15-30% forever because they can. A listing is not worth 30% of a restaurant. $100 is priced so a bodega, a bakery, or a taqueria says yes in one conversation, and so 100 stores in one city is $10,000 of revenue with zero delivery fleet, zero couriers, and zero per-order cost. The business scales by city, not by order.
20
+
21
+ ## Roadmap tiers (not sold yet)
22
+
23
+ - **Growth, $49/mo per location**: 500 items, multi-location, priority freshness reminders, menu and inventory file imports (CSV/POS exports), monthly agent-traffic report showing what agents searched for and whether the store had it.
24
+ - **Pro / City Partner**: API keys for chains and market halls, bulk onboarding, city launch partnerships with business associations.
25
+
26
+ Payment for Starter runs through a Stripe payment link. The repo never touches card data; `submit` validates the listing, Stripe validates the payment, the listing merges.
package/docs/SPEC.md ADDED
@@ -0,0 +1,47 @@
1
+ # Spec
2
+
3
+ ## Problem
4
+
5
+ AI agents are becoming the way people find things in a city ("find me birria tacos near the Mission that are actually available"). The data they need does not exist in a usable form:
6
+
7
+ - Delivery apps hold menus hostage behind closed apps, ad ranking, and 15-30% commissions, with no stock information at all.
8
+ - Map listings have hours and photos but no items, no prices, no stock.
9
+ - Merchant websites are unstructured and go stale silently.
10
+
11
+ ## Product
12
+
13
+ ShelfSignal is a merchant-submitted, agent-readable store index with three engines:
14
+
15
+ 1. **Registry** (`registry/stores.json`): the source of truth. One record per store: identity, location, hours, categories, fulfillment, and items with price, stock status, and last-confirmed date. v0.1 is git-backed so every change is a reviewable diff. A hosted registry with the same schema is the v0.3 roadmap item.
16
+ 2. **Likelihood engine** (`src/likelihood.ts`): deterministic scoring of the evidence behind "do they have it": reported status (in/low/out/unknown) adjusted by confirmation age and staple status. Output is score + band + reasons, designed to be quoted by the calling agent.
17
+ 3. **Validation engine** (`src/validate.ts`): 10 checks producing a receipt and a tier (VERIFIED / LISTED / STALE / INCOMPLETE). Agents call `validate_store` before recommending an unfamiliar store; merchants see the same receipt, so the fix list is never a mystery.
18
+
19
+ Access is dual, matching the fleet pattern: one bin runs the CLI for operators and merchants, and the MCP server (`shelfsignal mcp`, stdio) for agents.
20
+
21
+ ## Intake flow
22
+
23
+ 1. Merchant fills the intake (JSON template in `examples/`, or a form that emits the same JSON; see INTAKE.md).
24
+ 2. Merchant pays the $100 Starter via Stripe payment link.
25
+ 3. `shelfsignal submit intake.json --write` validates and appends the listing; a bad intake fails with named problems, never a silent drop.
26
+ 4. The listing's first validation receipt is sent to the merchant: this is what agents will see.
27
+ 5. Updates: the merchant re-submits changed items or sends a stock ping; `updatedAt` and item `lastConfirmed` move, freshness recovers.
28
+
29
+ ## Trust rules
30
+
31
+ - Bundled sample data is always `demo: true` and says so in every verdict.
32
+ - Scores are computed from merchant-reported evidence only; the engine never invents stock.
33
+ - An `uncertain` or `unlikely` verdict must never be presented to a human as available. The MCP tool description says this to every calling agent.
34
+ - Ranking is validation tier and freshness. There is no paid ranking and there will not be.
35
+
36
+ ## Non-goals (v0.1)
37
+
38
+ - No delivery, no couriers, no payments processing inside the repo.
39
+ - No scraping of delivery apps or map platforms. Listings are merchant-submitted, period.
40
+ - No ordering. Agents discover, validate, and direct; the transaction happens with the store. (Order-ahead intents are a roadmap item once merchants ask.)
41
+
42
+ ## Roadmap
43
+
44
+ - v0.2: intake form page, Stripe link wiring, SF pilot: first 25 real stores onboarded by hand.
45
+ - v0.3: hosted registry + merchant dashboard (stock board, freshness alerts, agent-traffic report).
46
+ - v0.4: imports (CSV, Square/Toast exports), photo menu OCR intake.
47
+ - v0.5: second city pack; business-association bundle pricing.
@@ -0,0 +1,27 @@
1
+ # ShelfSignal vs the delivery apps
2
+
3
+ The delivery apps are not a discovery problem. They are a toll booth.
4
+
5
+ | | DoorDash / Uber Eats model | ShelfSignal |
6
+ |---|---|---|
7
+ | Who owns the listing | The platform | The merchant |
8
+ | What the merchant pays | 15-30% of every order, plus ads to rank | $100 flat, one time, to get listed |
9
+ | Who owns the customer | The platform | The merchant (agents send people to the store) |
10
+ | Menu truth | Rented, scraped, or stale partner feeds | Merchant-submitted, dated, decays when stale |
11
+ | Stock truth | None. "Unavailable" discovered after ordering | Scored likelihood with reasons, before anyone moves |
12
+ | How agents reach it | No open protocol; closed apps | Open MCP server, any agent can query |
13
+ | Ranking | Pay to play | Validation tier and freshness only |
14
+ | The store's tablet farm | Yes | No |
15
+
16
+ ## The different vertical
17
+
18
+ ShelfSignal is not a delivery company and does not want to be one. Delivery is logistics; this is **agent commerce infrastructure**: the verified local inventory layer that every assistant, concierge agent, travel agent, and neighborhood bot will need before it can act in the physical city.
19
+
20
+ The fight is not "deliver food faster." The fight is: **when an AI agent looks for something in the real world, whose index does it read, and who gets paid?** If the answer stays "the delivery apps' closed, ad-ranked, stock-blind index," local stores keep paying rent to be found in their own neighborhood. ShelfSignal makes the index open, merchant-owned, validated, and cheap enough that a bodega can afford to be found by machines.
21
+
22
+ ## What $100 buys that 30% never did
23
+
24
+ - A permanent, agent-readable listing the merchant controls.
25
+ - A validation receipt that makes agents trust and recommend the store.
26
+ - Stock signals that bring in people who actually find what they came for.
27
+ - Zero per-order tax on being successful.
@@ -0,0 +1 @@
1
+ {"address":"123 Real Street, San Francisco, CA 94100","categories":["tacos","mexican"],"city":"San Francisco","fulfillment":["pickup","walk_in"],"hours":"Mon-Sun 9am-9pm","items":[{"category":"tacos","lastConfirmed":"2026-10-25","name":"Your signature item","price":12.5,"staple":false,"stock":"in_stock"},{"category":"drinks","lastConfirmed":"2026-10-25","name":"Your everyday staple","price":4.0,"staple":true,"stock":"in_stock"}],"name":"Your Store Name","neighborhood":"Your Neighborhood","phone":"(415) 555-0199","type":"restaurant","website":"https://yourstore.example.com"}
package/package.json CHANGED
@@ -1,6 +1 @@
1
- {
2
- "name": "@reneturcios/shelfsignal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {"bin":{"shelfsignal":"dist/cli.js"},"dependencies":{"@modelcontextprotocol/sdk":"^1","zod":"^3"},"description":"The open store index for AI agents: local merchants list what they sell for a flat $100 starter, agents find real food and real stock nearby, and every listing carries a validation receipt. No 30% delivery-app cut.","devDependencies":{"@types/node":"^20","tsx":"^4","typescript":"^5"},"engines":{"node":">=20"},"files":["dist","server.json","registry","examples","docs"],"homepage":"https://github.com/CloudCorpRecords/shelfsignal#readme","license":"MIT","mcpName":"io.github.CloudCorpRecords/shelfsignal","name":"@reneturcios/shelfsignal","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/CloudCorpRecords/shelfsignal.git"},"scripts":{"build":"tsc","dev":"tsx src/cli.ts","test":"tsx --test test/*.test.ts"},"type":"module","version":"0.1.0"}
@@ -0,0 +1 @@
1
+ {"stores":[{"address":"000 Demo Street, San Francisco, CA","categories":["tacos","mexican","burritos"],"city":"San Francisco","demo":true,"fulfillment":["pickup","walk_in"],"hours":"Mon-Sun 10am-10pm","id":"sf-mission-taqueria-demo","items":[{"category":"tacos","lastConfirmed":"2026-10-25","name":"Birria tacos","price":14.5,"staple":false,"stock":"in_stock"},{"category":"burritos","lastConfirmed":"2026-10-25","name":"Al pastor burrito","price":12.0,"staple":true,"stock":"in_stock"},{"category":"drinks","lastConfirmed":"2026-10-24","name":"Horchata","price":4.5,"staple":true,"stock":"low"}],"name":"Mission Taqueria (Demo)","neighborhood":"Mission District","phone":"(415) 555-0100","tier":"starter","type":"restaurant","updatedAt":"2026-10-25"},{"address":"000 Demo Avenue, San Francisco, CA","categories":["bakery","bread","pastries"],"city":"San Francisco","demo":true,"fulfillment":["walk_in","pickup"],"hours":"Tue-Sun 7am-3pm","id":"sf-sunset-bakery-demo","items":[{"category":"bread","lastConfirmed":"2026-10-25","name":"Sourdough loaf","price":9.0,"staple":true,"stock":"in_stock"},{"category":"pastries","lastConfirmed":"2026-10-25","name":"Cardamom knot","notes":"Weekends only, sells out by 10am","price":5.5,"staple":false,"stock":"out"}],"name":"Sunset Bakery (Demo)","neighborhood":"Outer Sunset","phone":"(415) 555-0101","tier":"starter","type":"bakery","updatedAt":"2026-10-25"},{"address":"000 Demo Boulevard, San Francisco, CA","categories":["groceries","snacks","drinks","pantry"],"city":"San Francisco","demo":true,"fulfillment":["walk_in"],"hours":"Mon-Sun 8am-11pm","id":"sf-richmond-bodega-demo","items":[{"category":"drinks","lastConfirmed":"2026-10-23","name":"Oat milk 64oz","price":5.99,"staple":true,"stock":"in_stock"},{"category":"pantry","lastConfirmed":"2026-10-20","name":"Farm eggs dozen","price":7.49,"staple":true,"stock":"low"},{"category":"pantry","lastConfirmed":"2026-09-28","name":"Tinned fish assortment","price":6.25,"staple":false,"stock":"unknown"}],"name":"Richmond Corner Bodega (Demo)","neighborhood":"Inner Richmond","phone":"(415) 555-0102","tier":"starter","type":"bodega","updatedAt":"2026-10-20"},{"address":"000 Demo Plaza, San Francisco, CA","categories":["produce","fruit","vegetables"],"city":"San Francisco","demo":true,"fulfillment":["walk_in"],"hours":"Tue, Thu, Sat 8am-2pm","id":"sf-ferry-market-produce-demo","items":[{"category":"vegetables","lastConfirmed":"2026-10-25","name":"Heirloom tomatoes 1lb","price":4.0,"staple":false,"stock":"in_stock","unit":"lb"},{"category":"fruit","lastConfirmed":"2026-10-25","name":"Persimmons 1lb","price":3.5,"staple":false,"stock":"in_stock","unit":"lb"}],"name":"Ferry Market Produce (Demo)","neighborhood":"Embarcadero","tier":"starter","type":"market","updatedAt":"2026-10-25","website":"https://example.com/ferry-market-produce-demo"}],"version":1}
package/server.json ADDED
@@ -0,0 +1 @@
1
+ {"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","description":"The open store index for AI agents: local merchants list what they sell for a flat $100 starter, agents find real food and real stock nearby, and every listing carries a validation receipt.","name":"io.github.CloudCorpRecords/shelfsignal","packages":[{"environmentVariables":[],"identifier":"@reneturcios/shelfsignal","registryType":"npm","transport":{"type":"stdio"},"version":"0.1.0"},{"environmentVariables":[],"identifier":"ghcr.io/cloudcorprecords/shelfsignal:0.1.0","registryType":"oci","transport":{"type":"stdio"},"version":"0.1.0"}],"repository":{"source":"github","url":"https://github.com/CloudCorpRecords/shelfsignal"},"title":"ShelfSignal","version":"0.1.0"}