kaching-cli 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 (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/assets/sdk.json +1 -0
  4. package/assets/template/.env.example +4 -0
  5. package/assets/template/AGENTS.md +54 -0
  6. package/assets/template/CLAUDE.md +1 -0
  7. package/assets/template/README.md +11 -0
  8. package/assets/template/eslint.config.mjs +18 -0
  9. package/assets/template/gitignore +42 -0
  10. package/assets/template/next.config.ts +10 -0
  11. package/assets/template/package.json +30 -0
  12. package/assets/template/postcss.config.mjs +7 -0
  13. package/assets/template/src/app/checkout/success/page.tsx +9 -0
  14. package/assets/template/src/app/favicon.ico +0 -0
  15. package/assets/template/src/app/globals.css +41 -0
  16. package/assets/template/src/app/layout.tsx +47 -0
  17. package/assets/template/src/app/not-found.tsx +13 -0
  18. package/assets/template/src/app/page.tsx +37 -0
  19. package/assets/template/src/app/products/[slug]/page.tsx +62 -0
  20. package/assets/template/src/components/cart-button.tsx +21 -0
  21. package/assets/template/src/components/cart-drawer.tsx +136 -0
  22. package/assets/template/src/components/header.tsx +26 -0
  23. package/assets/template/src/components/order-confirmation.tsx +116 -0
  24. package/assets/template/src/components/price.tsx +21 -0
  25. package/assets/template/src/components/product-card.tsx +44 -0
  26. package/assets/template/src/components/product-form.tsx +123 -0
  27. package/assets/template/src/components/providers.tsx +14 -0
  28. package/assets/template/src/lib/kaching.ts +24 -0
  29. package/assets/template/src/store.config.ts +16 -0
  30. package/assets/template/tsconfig.json +34 -0
  31. package/dist/chunk-TWKERERX.js +283 -0
  32. package/dist/index.js +503 -0
  33. package/dist/mcp-LTF62ASB.js +313 -0
  34. package/package.json +54 -0
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CliError,
4
+ KachingError,
5
+ formatMoney,
6
+ storeClient,
7
+ toMinor
8
+ } from "./chunk-TWKERERX.js";
9
+
10
+ // src/mcp.ts
11
+ import { readFileSync, statSync } from "fs";
12
+ import { basename, extname, resolve } from "path";
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { z } from "zod";
16
+ var ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
17
+ function fail(err) {
18
+ const message = err instanceof KachingError ? `${err.message} (${err.code}${err.details ? `: ${JSON.stringify(err.details)}` : ""})` : err instanceof Error ? err.message : String(err);
19
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
20
+ }
21
+ var run = (fn) => async (args) => {
22
+ try {
23
+ return ok(await fn(args));
24
+ } catch (err) {
25
+ return fail(err);
26
+ }
27
+ };
28
+ async function currency() {
29
+ return (await storeClient().store.get()).currency;
30
+ }
31
+ function describeProduct(p, cur) {
32
+ return {
33
+ id: p.id,
34
+ slug: p.slug,
35
+ title: p.title,
36
+ type: p.type,
37
+ status: p.status,
38
+ description: p.description,
39
+ images: p.images,
40
+ variants: p.variants.map((v) => ({
41
+ id: v.id,
42
+ title: v.title,
43
+ price: formatMoney(v.price, cur),
44
+ price_minor: v.price,
45
+ compare_at_price: v.compare_at_price != null ? formatMoney(v.compare_at_price, cur) : null,
46
+ options: v.options,
47
+ stock: v.track_inventory ? v.inventory_quantity : "untracked",
48
+ available: v.available,
49
+ files: v.files?.map((f) => f.filename) ?? []
50
+ }))
51
+ };
52
+ }
53
+ var MIME = {
54
+ ".jpg": "image/jpeg",
55
+ ".jpeg": "image/jpeg",
56
+ ".png": "image/png",
57
+ ".webp": "image/webp",
58
+ ".gif": "image/gif",
59
+ ".avif": "image/avif",
60
+ ".pdf": "application/pdf",
61
+ ".zip": "application/zip",
62
+ ".epub": "application/epub+zip",
63
+ ".mp3": "audio/mpeg",
64
+ ".mp4": "video/mp4"
65
+ };
66
+ function readLocal(path) {
67
+ const abs = resolve(path);
68
+ try {
69
+ statSync(abs);
70
+ } catch {
71
+ throw new CliError(`File not found: ${abs}`);
72
+ }
73
+ return { blob: new Blob([readFileSync(abs)], { type: MIME[extname(abs).toLowerCase()] ?? "application/octet-stream" }), name: basename(abs) };
74
+ }
75
+ var price = z.number().nonnegative().describe("Price in normal currency units, e.g. 24.9 for $24.90");
76
+ var variantSchema = z.object({
77
+ title: z.string().describe('Variant name shown to shoppers, e.g. "Large"'),
78
+ price,
79
+ options: z.record(z.string(), z.string()).optional().describe('Option values, e.g. {"size": "L", "color": "Black"}'),
80
+ stock: z.number().int().min(0).optional().describe("Track inventory with this quantity. Omit for unlimited."),
81
+ sku: z.string().optional()
82
+ });
83
+ async function startMcpServer() {
84
+ const dir = [process.env.KACHING_PROJECT_DIR, process.env.CLAUDE_PROJECT_DIR].find((v) => v && !v.includes("${"));
85
+ if (dir) process.chdir(dir);
86
+ const server = new McpServer({ name: "kaching", version: "0.1.0" });
87
+ server.registerTool(
88
+ "store_info",
89
+ {
90
+ description: "Get the store linked to the current project: name, currency, and whether Stripe payments are ready. Call this first. If it errors with 'No store linked', run `npx kaching-cli create .` in the project folder.",
91
+ annotations: { readOnlyHint: true }
92
+ },
93
+ run(async () => storeClient().store.get())
94
+ );
95
+ server.registerTool(
96
+ "list_products",
97
+ { description: "List all products with variants, prices and stock.", annotations: { readOnlyHint: true } },
98
+ run(async () => {
99
+ const client = storeClient();
100
+ const [{ data }, cur] = await Promise.all([client.products.list({ limit: 100 }), currency()]);
101
+ return data.map((p) => describeProduct(p, cur));
102
+ })
103
+ );
104
+ server.registerTool(
105
+ "create_product",
106
+ {
107
+ description: "Create a product. For one price use `price`; for sizes/colors pass `variants`. Digital products (downloads) use type 'digital' and then attach_file. Images can be URLs or local file paths.",
108
+ inputSchema: {
109
+ title: z.string(),
110
+ description: z.string().optional(),
111
+ type: z.enum(["physical", "digital"]).default("physical"),
112
+ price: price.optional(),
113
+ stock: z.number().int().min(0).optional().describe("Inventory for single-price products. Omit for unlimited."),
114
+ variants: z.array(variantSchema).optional(),
115
+ images: z.array(z.string()).optional().describe("Image URLs or absolute/relative local paths"),
116
+ draft: z.boolean().optional().describe("Hide from the storefront until published")
117
+ }
118
+ },
119
+ run(async (args) => {
120
+ const client = storeClient();
121
+ const cur = await currency();
122
+ if (args.price === void 0 && !args.variants?.length) throw new CliError("Provide `price` or `variants`");
123
+ let product = await client.products.create({
124
+ title: args.title,
125
+ description: args.description,
126
+ type: args.type,
127
+ status: args.draft ? "draft" : "active",
128
+ images: (args.images ?? []).filter((i) => /^https?:\/\//.test(i)),
129
+ variants: args.variants?.length ? args.variants.map((v, i) => ({
130
+ title: v.title,
131
+ price: toMinor(String(v.price), cur),
132
+ position: i,
133
+ options: v.options,
134
+ sku: v.sku,
135
+ ...v.stock !== void 0 && { track_inventory: true, inventory_quantity: v.stock }
136
+ })) : [
137
+ {
138
+ price: toMinor(String(args.price), cur),
139
+ ...args.stock !== void 0 && { track_inventory: true, inventory_quantity: args.stock }
140
+ }
141
+ ]
142
+ });
143
+ for (const path of (args.images ?? []).filter((i) => !/^https?:\/\//.test(i))) {
144
+ const { blob, name } = readLocal(path);
145
+ product = await client.products.uploadImage(product.id, blob, name);
146
+ }
147
+ return describeProduct(product, cur);
148
+ })
149
+ );
150
+ server.registerTool(
151
+ "update_product",
152
+ {
153
+ description: "Update a product's title, description, status or images (by slug or id). Use update_variant for price/stock.",
154
+ inputSchema: {
155
+ product: z.string().describe("Product slug or id"),
156
+ title: z.string().optional(),
157
+ description: z.string().optional(),
158
+ status: z.enum(["active", "draft", "archived"]).optional(),
159
+ add_images: z.array(z.string()).optional().describe("Image URLs or local paths to append")
160
+ }
161
+ },
162
+ run(async (args) => {
163
+ const client = storeClient();
164
+ const cur = await currency();
165
+ let product = await client.products.get(args.product);
166
+ const urls = (args.add_images ?? []).filter((i) => /^https?:\/\//.test(i));
167
+ if (args.title !== void 0 || args.description !== void 0 || args.status !== void 0 || urls.length) {
168
+ product = await client.products.update(product.id, {
169
+ title: args.title,
170
+ description: args.description,
171
+ status: args.status,
172
+ ...urls.length && { images: [...product.images, ...urls] }
173
+ });
174
+ }
175
+ for (const path of (args.add_images ?? []).filter((i) => !/^https?:\/\//.test(i))) {
176
+ const { blob, name } = readLocal(path);
177
+ product = await client.products.uploadImage(product.id, blob, name);
178
+ }
179
+ return describeProduct(product, cur);
180
+ })
181
+ );
182
+ server.registerTool(
183
+ "update_variant",
184
+ {
185
+ description: "Change a variant's price, sale price, stock or title. Get variant ids from list_products.",
186
+ inputSchema: {
187
+ variant_id: z.string(),
188
+ price: price.optional(),
189
+ compare_at_price: price.optional().describe("Original price to show as a sale (must be higher than price)"),
190
+ stock: z.number().int().min(0).optional(),
191
+ title: z.string().optional()
192
+ }
193
+ },
194
+ run(async (args) => {
195
+ const cur = await currency();
196
+ const product = await storeClient().variants.update(args.variant_id, {
197
+ ...args.title && { title: args.title },
198
+ ...args.price !== void 0 && { price: toMinor(String(args.price), cur) },
199
+ ...args.compare_at_price !== void 0 && { compare_at_price: toMinor(String(args.compare_at_price), cur) },
200
+ ...args.stock !== void 0 && { track_inventory: true, inventory_quantity: args.stock }
201
+ });
202
+ return describeProduct(product, cur);
203
+ })
204
+ );
205
+ server.registerTool(
206
+ "delete_product",
207
+ {
208
+ description: "Permanently delete a product. Prefer update_product with status 'archived' to hide it.",
209
+ inputSchema: { product: z.string().describe("Product slug or id") },
210
+ annotations: { destructiveHint: true }
211
+ },
212
+ run(async (args) => storeClient().products.delete(args.product))
213
+ );
214
+ server.registerTool(
215
+ "attach_file",
216
+ {
217
+ description: "Attach a downloadable file (PDF, zip, audio...) to a digital product. Buyers get expiring download links.",
218
+ inputSchema: {
219
+ product: z.string().describe("Digital product slug or id"),
220
+ path: z.string().describe("Local file path"),
221
+ variant_id: z.string().optional().describe("Defaults to the product's first variant")
222
+ }
223
+ },
224
+ run(async (args) => {
225
+ const client = storeClient();
226
+ const product = await client.products.get(args.product);
227
+ const { blob, name } = readLocal(args.path);
228
+ return client.variants.files.upload(args.variant_id ?? product.variants[0].id, blob, name);
229
+ })
230
+ );
231
+ server.registerTool(
232
+ "list_shipping_rates",
233
+ { description: "List shipping rates. With none, physical orders ship free worldwide.", annotations: { readOnlyHint: true } },
234
+ run(async () => storeClient().shippingRates.list())
235
+ );
236
+ server.registerTool(
237
+ "create_shipping_rate",
238
+ {
239
+ description: "Add a flat shipping rate for physical products. Checkout offers up to 5 rates.",
240
+ inputSchema: {
241
+ name: z.string().describe('e.g. "Standard" or "Express"'),
242
+ price: price.describe("Shipping price in normal units; 0 for free"),
243
+ countries: z.array(z.string().length(2)).optional().describe("ISO country codes, e.g. ['US','CA']. Omit for worldwide."),
244
+ min_days: z.number().int().min(0).optional(),
245
+ max_days: z.number().int().min(0).optional()
246
+ }
247
+ },
248
+ run(async (args) => {
249
+ const cur = await currency();
250
+ return storeClient().shippingRates.create({
251
+ name: args.name,
252
+ amount: toMinor(String(args.price), cur),
253
+ countries: args.countries,
254
+ ...args.min_days !== void 0 && { min_delivery_days: args.min_days, max_delivery_days: args.max_days ?? args.min_days }
255
+ });
256
+ })
257
+ );
258
+ server.registerTool(
259
+ "payments_status",
260
+ { description: "Check whether the store can accept payments (Stripe onboarding finished).", annotations: { readOnlyHint: true } },
261
+ run(async () => storeClient().payments.status())
262
+ );
263
+ server.registerTool(
264
+ "connect_payments",
265
+ {
266
+ description: "Get the Stripe onboarding link the store owner must open to receive payouts. This is the one step a human must do \u2014 give them `onboarding_url`. Pass `country` (ISO code) for EUR and other multi-country currencies.",
267
+ inputSchema: {
268
+ country: z.string().length(2).optional(),
269
+ email: z.string().optional()
270
+ }
271
+ },
272
+ run(async (args) => {
273
+ const client = storeClient();
274
+ const status = await client.payments.status().catch(() => null);
275
+ if (status?.ready) return { ready: true };
276
+ const link = await client.payments.onboard({ country: args.country, email: args.email });
277
+ return { ready: false, onboarding_url: link.start_url, note: "Valid for 7 days. Owner completes it in the browser." };
278
+ })
279
+ );
280
+ server.registerTool(
281
+ "list_orders",
282
+ {
283
+ description: "List recent orders (newest first).",
284
+ inputSchema: { unfulfilled_only: z.boolean().optional(), limit: z.number().int().min(1).max(100).optional() },
285
+ annotations: { readOnlyHint: true }
286
+ },
287
+ run(async (args) => {
288
+ const { data } = await storeClient().orders.list({
289
+ limit: args.limit ?? 20,
290
+ ...args.unfulfilled_only && { fulfillment_status: "unfulfilled" }
291
+ });
292
+ return data.map((o) => ({
293
+ id: o.id,
294
+ number: o.number,
295
+ date: o.created_at,
296
+ email: o.email,
297
+ total: formatMoney(o.total, o.currency),
298
+ fulfillment_status: o.fulfillment_status,
299
+ items: o.items.map((i) => `${i.quantity}\xD7 ${i.title}${i.variant_title ? ` (${i.variant_title})` : ""}`),
300
+ ship_to: o.shipping_address
301
+ }));
302
+ })
303
+ );
304
+ server.registerTool(
305
+ "fulfill_order",
306
+ { description: "Mark a physical order as shipped.", inputSchema: { order_id: z.string() } },
307
+ run(async (args) => storeClient().orders.fulfill(args.order_id))
308
+ );
309
+ await server.connect(new StdioServerTransport());
310
+ }
311
+ export {
312
+ startMcpServer
313
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "kaching-cli",
3
+ "version": "0.1.0",
4
+ "description": "Build a real online store from your terminal (or your coding agent) in minutes",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "kaching": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "assets",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.30.0",
18
+ "commander": "^14",
19
+ "zod": "^4.6.5"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20",
23
+ "tsup": "^8",
24
+ "typescript": "^5",
25
+ "@kaching.sh/sdk": "0.1.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/sventhaman/Kaching.git",
30
+ "directory": "packages/cli"
31
+ },
32
+ "homepage": "https://kaching.sh",
33
+ "bugs": "https://github.com/sventhaman/Kaching/issues",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "keywords": [
38
+ "kaching",
39
+ "ecommerce",
40
+ "cli",
41
+ "mcp",
42
+ "stripe",
43
+ "storefront",
44
+ "claude-code"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "scripts": {
50
+ "build": "node scripts/bundle-assets.mjs && tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }