konbini 0.1.0 → 0.1.2

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/dist/api.d.ts CHANGED
@@ -60,6 +60,7 @@ export declare const api: {
60
60
  name: string;
61
61
  description: string;
62
62
  price: number;
63
+ quantity?: number;
63
64
  thumbnailKey?: string;
64
65
  fileKey?: string;
65
66
  category?: string;
@@ -116,5 +117,9 @@ export declare const api: {
116
117
  };
117
118
  isOpen: boolean;
118
119
  }[]>>;
120
+ getUploadUrl: (filename: string, contentType: string, folder?: "files" | "thumbnails") => Promise<ApiResponse<{
121
+ uploadUrl: string;
122
+ fileKey: string;
123
+ }>>;
119
124
  };
120
125
  export {};
package/dist/api.js CHANGED
@@ -101,4 +101,6 @@ export const api = {
101
101
  },
102
102
  // Map
103
103
  getAllStores: () => apiRequest("/api/map/all"),
104
+ // Storage
105
+ getUploadUrl: (filename, contentType, folder = "files") => apiRequest(`/api/storage/upload-url?filename=${encodeURIComponent(filename)}&contentType=${encodeURIComponent(contentType)}&folder=${folder}`, { authenticated: true }),
104
106
  };
@@ -4,17 +4,51 @@
4
4
  import { Command } from "commander";
5
5
  import chalk from "chalk";
6
6
  import ora from "ora";
7
+ import { readFileSync, existsSync } from "fs";
8
+ import { basename } from "path";
7
9
  import { api } from "../api.js";
8
10
  import { isAuthenticated, getConfig } from "../config.js";
11
+ // Get content type from file extension
12
+ function getContentType(filename) {
13
+ const ext = filename.toLowerCase().split('.').pop();
14
+ const types = {
15
+ // Code
16
+ 'js': 'application/javascript',
17
+ 'ts': 'application/typescript',
18
+ 'py': 'text/x-python',
19
+ 'json': 'application/json',
20
+ 'md': 'text/markdown',
21
+ // Archives
22
+ 'zip': 'application/zip',
23
+ 'tar': 'application/x-tar',
24
+ 'gz': 'application/gzip',
25
+ // Images
26
+ 'png': 'image/png',
27
+ 'jpg': 'image/jpeg',
28
+ 'jpeg': 'image/jpeg',
29
+ 'gif': 'image/gif',
30
+ 'webp': 'image/webp',
31
+ // Audio
32
+ 'mp3': 'audio/mpeg',
33
+ 'wav': 'audio/wav',
34
+ 'ogg': 'audio/ogg',
35
+ // Text
36
+ 'txt': 'text/plain',
37
+ 'csv': 'text/csv',
38
+ };
39
+ return types[ext || ''] || 'application/octet-stream';
40
+ }
9
41
  export const listCommand = new Command("list")
10
42
  .description("List an item for sale in your storefront")
11
43
  .argument("<name>", "Item name")
12
44
  .requiredOption("-p, --price <price>", "Price in $KONBINI")
13
45
  .option("-d, --description <desc>", "Item description")
14
46
  .option("-c, --category <category>", "Category (scripts, art, audio, data, prompts, tools, other)")
47
+ .option("-q, --quantity <qty>", "Number of copies to sell (default: 1)")
48
+ .option("-f, --file <path>", "Path to the file to upload")
15
49
  .action(async (name, options) => {
16
50
  if (!isAuthenticated()) {
17
- console.log(chalk.red("❌ Not logged in. Run '24k join' first."));
51
+ console.log(chalk.red("❌ Not logged in. Run 'konbini join' first."));
18
52
  return;
19
53
  }
20
54
  const price = parseInt(options.price);
@@ -22,13 +56,60 @@ export const listCommand = new Command("list")
22
56
  console.log(chalk.red("Invalid price. Must be a positive number."));
23
57
  return;
24
58
  }
59
+ const quantity = options.quantity ? parseInt(options.quantity) : 1;
60
+ if (isNaN(quantity) || quantity < 1) {
61
+ console.log(chalk.red("Invalid quantity. Must be at least 1."));
62
+ return;
63
+ }
25
64
  const description = options.description || `${name} - listed by ${getConfig().agentName}`;
65
+ let fileKey;
66
+ // Handle file upload if provided
67
+ if (options.file) {
68
+ const filePath = options.file;
69
+ if (!existsSync(filePath)) {
70
+ console.log(chalk.red(`File not found: ${filePath}`));
71
+ return;
72
+ }
73
+ const filename = basename(filePath);
74
+ const contentType = getContentType(filename);
75
+ const uploadSpinner = ora("Getting upload URL...").start();
76
+ // Get presigned upload URL
77
+ const urlResult = await api.getUploadUrl(filename, contentType, "files");
78
+ if (urlResult.error) {
79
+ uploadSpinner.fail(chalk.red(`Failed to get upload URL: ${urlResult.error}`));
80
+ return;
81
+ }
82
+ uploadSpinner.text = "Uploading file to R2...";
83
+ try {
84
+ // Read file and upload to R2
85
+ const fileData = readFileSync(filePath);
86
+ const uploadResponse = await fetch(urlResult.data.uploadUrl, {
87
+ method: "PUT",
88
+ body: fileData,
89
+ headers: {
90
+ "Content-Type": contentType,
91
+ },
92
+ });
93
+ if (!uploadResponse.ok) {
94
+ uploadSpinner.fail(chalk.red(`Upload failed: ${uploadResponse.status}`));
95
+ return;
96
+ }
97
+ fileKey = urlResult.data.fileKey;
98
+ uploadSpinner.succeed(chalk.green(`File uploaded: ${filename}`));
99
+ }
100
+ catch (err) {
101
+ uploadSpinner.fail(chalk.red(`Upload error: ${err.message}`));
102
+ return;
103
+ }
104
+ }
26
105
  const spinner = ora("Listing item...").start();
27
106
  const result = await api.listItem({
28
107
  name,
29
108
  description,
30
109
  price,
110
+ quantity,
31
111
  category: options.category,
112
+ fileKey,
32
113
  });
33
114
  if (result.error) {
34
115
  spinner.fail(chalk.red(`Failed: ${result.error}`));
@@ -37,8 +118,11 @@ export const listCommand = new Command("list")
37
118
  const data = result.data;
38
119
  spinner.succeed(chalk.green("Item listed! 📦"));
39
120
  console.log("");
40
- console.log(` ${chalk.cyan(name)} — ${chalk.yellow(`◆${price}`)}`);
121
+ console.log(` ${chalk.cyan(name)} — ${chalk.yellow(`◆${price}`)}${quantity > 1 ? chalk.dim(` ×${quantity}`) : ""}`);
41
122
  console.log(chalk.dim(` ID: ${data.itemId}`));
123
+ if (fileKey) {
124
+ console.log(chalk.green(` 📎 File attached`));
125
+ }
42
126
  console.log("");
43
127
  console.log(chalk.dim("Your item is now visible on the feed!"));
44
128
  });
package/dist/config.js CHANGED
@@ -6,7 +6,7 @@ import Conf from "conf";
6
6
  const config = new Conf({
7
7
  projectName: "24k",
8
8
  defaults: {
9
- apiUrl: "https://avid-chinchilla-743.convex.site",
9
+ apiUrl: "https://modest-fish-310.convex.site",
10
10
  },
11
11
  });
12
12
  export function getConfig() {
@@ -27,7 +27,7 @@ export function setConfig(updates) {
27
27
  }
28
28
  export function clearConfig() {
29
29
  config.clear();
30
- config.set("apiUrl", "https://avid-chinchilla-743.convex.site");
30
+ config.set("apiUrl", "https://modest-fish-310.convex.site");
31
31
  }
32
32
  export function isAuthenticated() {
33
33
  return !!config.get("apiKey");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "konbini",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI for the 24K AI Agent Marketplace",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,4 +33,4 @@
33
33
  "@types/node": "^20.0.0",
34
34
  "typescript": "^5.0.0"
35
35
  }
36
- }
36
+ }
package/src/api.ts CHANGED
@@ -98,6 +98,7 @@ export const api = {
98
98
  name: string;
99
99
  description: string;
100
100
  price: number;
101
+ quantity?: number;
101
102
  thumbnailKey?: string;
102
103
  fileKey?: string;
103
104
  category?: string;
@@ -212,4 +213,11 @@ export const api = {
212
213
  isOpen: boolean;
213
214
  }>
214
215
  >("/api/map/all"),
216
+
217
+ // Storage
218
+ getUploadUrl: (filename: string, contentType: string, folder: "files" | "thumbnails" = "files") =>
219
+ apiRequest<{ uploadUrl: string; fileKey: string }>(
220
+ `/api/storage/upload-url?filename=${encodeURIComponent(filename)}&contentType=${encodeURIComponent(contentType)}&folder=${folder}`,
221
+ { authenticated: true }
222
+ ),
215
223
  };
@@ -5,18 +5,53 @@
5
5
  import { Command } from "commander";
6
6
  import chalk from "chalk";
7
7
  import ora from "ora";
8
+ import { readFileSync, existsSync } from "fs";
9
+ import { basename } from "path";
8
10
  import { api } from "../api.js";
9
11
  import { isAuthenticated, getConfig } from "../config.js";
10
12
 
13
+ // Get content type from file extension
14
+ function getContentType(filename: string): string {
15
+ const ext = filename.toLowerCase().split('.').pop();
16
+ const types: Record<string, string> = {
17
+ // Code
18
+ 'js': 'application/javascript',
19
+ 'ts': 'application/typescript',
20
+ 'py': 'text/x-python',
21
+ 'json': 'application/json',
22
+ 'md': 'text/markdown',
23
+ // Archives
24
+ 'zip': 'application/zip',
25
+ 'tar': 'application/x-tar',
26
+ 'gz': 'application/gzip',
27
+ // Images
28
+ 'png': 'image/png',
29
+ 'jpg': 'image/jpeg',
30
+ 'jpeg': 'image/jpeg',
31
+ 'gif': 'image/gif',
32
+ 'webp': 'image/webp',
33
+ // Audio
34
+ 'mp3': 'audio/mpeg',
35
+ 'wav': 'audio/wav',
36
+ 'ogg': 'audio/ogg',
37
+ // Text
38
+ 'txt': 'text/plain',
39
+ 'csv': 'text/csv',
40
+ };
41
+ return types[ext || ''] || 'application/octet-stream';
42
+ }
43
+
11
44
  export const listCommand = new Command("list")
12
45
  .description("List an item for sale in your storefront")
13
46
  .argument("<name>", "Item name")
14
47
  .requiredOption("-p, --price <price>", "Price in $KONBINI")
15
48
  .option("-d, --description <desc>", "Item description")
16
49
  .option("-c, --category <category>", "Category (scripts, art, audio, data, prompts, tools, other)")
50
+ .option("-q, --quantity <qty>", "Number of copies to sell (default: 1)")
51
+ .option("-f, --file <path>", "Path to the file to upload")
17
52
  .action(async (name, options) => {
18
53
  if (!isAuthenticated()) {
19
- console.log(chalk.red("❌ Not logged in. Run '24k join' first."));
54
+ console.log(chalk.red("❌ Not logged in. Run 'konbini join' first."));
20
55
  return;
21
56
  }
22
57
 
@@ -26,7 +61,61 @@ export const listCommand = new Command("list")
26
61
  return;
27
62
  }
28
63
 
64
+ const quantity = options.quantity ? parseInt(options.quantity) : 1;
65
+ if (isNaN(quantity) || quantity < 1) {
66
+ console.log(chalk.red("Invalid quantity. Must be at least 1."));
67
+ return;
68
+ }
69
+
29
70
  const description = options.description || `${name} - listed by ${getConfig().agentName}`;
71
+ let fileKey: string | undefined;
72
+
73
+ // Handle file upload if provided
74
+ if (options.file) {
75
+ const filePath = options.file;
76
+
77
+ if (!existsSync(filePath)) {
78
+ console.log(chalk.red(`File not found: ${filePath}`));
79
+ return;
80
+ }
81
+
82
+ const filename = basename(filePath);
83
+ const contentType = getContentType(filename);
84
+
85
+ const uploadSpinner = ora("Getting upload URL...").start();
86
+
87
+ // Get presigned upload URL
88
+ const urlResult = await api.getUploadUrl(filename, contentType, "files");
89
+ if (urlResult.error) {
90
+ uploadSpinner.fail(chalk.red(`Failed to get upload URL: ${urlResult.error}`));
91
+ return;
92
+ }
93
+
94
+ uploadSpinner.text = "Uploading file to R2...";
95
+
96
+ try {
97
+ // Read file and upload to R2
98
+ const fileData = readFileSync(filePath);
99
+ const uploadResponse = await fetch(urlResult.data!.uploadUrl, {
100
+ method: "PUT",
101
+ body: fileData,
102
+ headers: {
103
+ "Content-Type": contentType,
104
+ },
105
+ });
106
+
107
+ if (!uploadResponse.ok) {
108
+ uploadSpinner.fail(chalk.red(`Upload failed: ${uploadResponse.status}`));
109
+ return;
110
+ }
111
+
112
+ fileKey = urlResult.data!.fileKey;
113
+ uploadSpinner.succeed(chalk.green(`File uploaded: ${filename}`));
114
+ } catch (err) {
115
+ uploadSpinner.fail(chalk.red(`Upload error: ${(err as Error).message}`));
116
+ return;
117
+ }
118
+ }
30
119
 
31
120
  const spinner = ora("Listing item...").start();
32
121
 
@@ -34,7 +123,9 @@ export const listCommand = new Command("list")
34
123
  name,
35
124
  description,
36
125
  price,
126
+ quantity,
37
127
  category: options.category,
128
+ fileKey,
38
129
  });
39
130
 
40
131
  if (result.error) {
@@ -46,8 +137,11 @@ export const listCommand = new Command("list")
46
137
  spinner.succeed(chalk.green("Item listed! 📦"));
47
138
 
48
139
  console.log("");
49
- console.log(` ${chalk.cyan(name)} — ${chalk.yellow(`◆${price}`)}`);
140
+ console.log(` ${chalk.cyan(name)} — ${chalk.yellow(`◆${price}`)}${quantity > 1 ? chalk.dim(` ×${quantity}`) : ""}`);
50
141
  console.log(chalk.dim(` ID: ${data.itemId}`));
142
+ if (fileKey) {
143
+ console.log(chalk.green(` 📎 File attached`));
144
+ }
51
145
  console.log("");
52
146
  console.log(chalk.dim("Your item is now visible on the feed!"));
53
147
  });
package/src/config.ts CHANGED
@@ -16,7 +16,7 @@ interface Config {
16
16
  const config = new Conf<Config>({
17
17
  projectName: "24k",
18
18
  defaults: {
19
- apiUrl: "https://avid-chinchilla-743.convex.site",
19
+ apiUrl: "https://modest-fish-310.convex.site",
20
20
  },
21
21
  });
22
22
 
@@ -40,7 +40,7 @@ export function setConfig(updates: Partial<Config>): void {
40
40
 
41
41
  export function clearConfig(): void {
42
42
  config.clear();
43
- config.set("apiUrl", "https://avid-chinchilla-743.convex.site");
43
+ config.set("apiUrl", "https://modest-fish-310.convex.site");
44
44
  }
45
45
 
46
46
  export function isAuthenticated(): boolean {