stow-cli 2.0.3 → 2.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.
@@ -0,0 +1,185 @@
1
+ import {
2
+ validateBucketName,
3
+ validateFileKey
4
+ } from "./chunk-OHAFRKN5.js";
5
+ import {
6
+ formatBytes,
7
+ formatTable,
8
+ outputJson
9
+ } from "./chunk-ELSDWMEB.js";
10
+ import {
11
+ createStow
12
+ } from "./chunk-5LU25QZK.js";
13
+ import {
14
+ getApiKey,
15
+ getBaseUrl
16
+ } from "./chunk-TOADDO2F.js";
17
+
18
+ // src/commands/files.ts
19
+ async function listFiles(bucket, options) {
20
+ const stow = createStow();
21
+ const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null;
22
+ const data = await stow.listFiles({
23
+ bucket,
24
+ ...options.search ? { prefix: options.search } : {},
25
+ ...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: parsedLimit } : {}
26
+ });
27
+ if (options.json) {
28
+ outputJson(data);
29
+ return;
30
+ }
31
+ if (data.files.length === 0) {
32
+ console.log(`No files in bucket '${bucket}'.`);
33
+ return;
34
+ }
35
+ const rows = data.files.map((f) => [
36
+ f.key,
37
+ formatBytes(f.size),
38
+ f.lastModified.split("T")[0] ?? f.lastModified
39
+ ]);
40
+ console.log(formatTable(["Key", "Size", "Modified"], rows));
41
+ if (data.nextCursor) {
42
+ console.log("\n(more files available \u2014 use --limit to see more)");
43
+ }
44
+ }
45
+ async function getFile(bucket, key, options) {
46
+ const stow = createStow();
47
+ const file = await stow.getFile(key, { bucket });
48
+ if (options.json) {
49
+ outputJson(file);
50
+ return;
51
+ }
52
+ console.log(
53
+ formatTable(
54
+ ["Field", "Value"],
55
+ [
56
+ ["Key", file.key],
57
+ ["Size", formatBytes(file.size)],
58
+ ["Type", file.contentType],
59
+ ["Created", file.createdAt],
60
+ ["URL", file.url ?? "(private)"],
61
+ [
62
+ "Dimensions",
63
+ file.width && file.height ? `${file.width}\xD7${file.height}` : "\u2014"
64
+ ],
65
+ ["Duration", file.duration ? `${file.duration}s` : "\u2014"],
66
+ ["Embedding", file.embeddingStatus ?? "\u2014"]
67
+ ]
68
+ )
69
+ );
70
+ if (file.metadata && Object.keys(file.metadata).length > 0) {
71
+ console.log("\nMetadata:");
72
+ for (const [k, v] of Object.entries(file.metadata)) {
73
+ console.log(` ${k}: ${v}`);
74
+ }
75
+ }
76
+ }
77
+ async function updateFile(bucket, key, options) {
78
+ validateBucketName(bucket);
79
+ validateFileKey(key);
80
+ if (!options.metadata || options.metadata.length === 0) {
81
+ console.error("Error: At least one -m key=value pair is required.");
82
+ process.exit(1);
83
+ }
84
+ const metadata = {};
85
+ for (const pair of options.metadata) {
86
+ const idx = pair.indexOf("=");
87
+ if (idx === -1) {
88
+ console.error(`Error: Invalid metadata format '${pair}'. Use key=value.`);
89
+ process.exit(1);
90
+ }
91
+ metadata[pair.slice(0, idx)] = pair.slice(idx + 1);
92
+ }
93
+ if (options.dryRun) {
94
+ console.log(
95
+ JSON.stringify(
96
+ {
97
+ dryRun: true,
98
+ action: "updateFile",
99
+ details: { bucket, key, metadata }
100
+ },
101
+ null,
102
+ 2
103
+ )
104
+ );
105
+ return;
106
+ }
107
+ const stow = createStow();
108
+ const file = await stow.updateFileMetadata(key, metadata, { bucket });
109
+ if (options.json) {
110
+ outputJson(file);
111
+ return;
112
+ }
113
+ console.log(`Updated ${key}`);
114
+ }
115
+ async function enrichFile(bucket, key) {
116
+ const stow = createStow();
117
+ const results = await Promise.allSettled([
118
+ stow.generateTitle(key, { bucket }),
119
+ stow.generateDescription(key, { bucket }),
120
+ stow.generateAltText(key, { bucket })
121
+ ]);
122
+ const labels = ["Title", "Description", "Alt text"];
123
+ for (let i = 0; i < results.length; i++) {
124
+ const result = results[i];
125
+ const label = labels[i];
126
+ if (result.status === "fulfilled") {
127
+ console.log(` ${label}: triggered`);
128
+ } else {
129
+ console.error(` ${label}: failed \u2014 ${result.reason}`);
130
+ }
131
+ }
132
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
133
+ console.log(
134
+ `
135
+ Enriched ${key}: ${succeeded}/${results.length} tasks dispatched`
136
+ );
137
+ }
138
+ async function listMissing(bucket, type, options) {
139
+ const validTypes = ["dimensions", "embeddings", "colors"];
140
+ if (!validTypes.includes(type)) {
141
+ console.error(
142
+ `Error: Invalid type '${type}'. Must be one of: ${validTypes.join(", ")}`
143
+ );
144
+ process.exit(1);
145
+ }
146
+ const parsedLimit = options.limit ? Number.parseInt(options.limit, 10) : null;
147
+ const baseUrl = getBaseUrl();
148
+ const apiKey = getApiKey();
149
+ const params = new URLSearchParams({
150
+ bucket,
151
+ missing: type,
152
+ ...parsedLimit && Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: String(parsedLimit) } : {}
153
+ });
154
+ const res = await fetch(`${baseUrl}/files?${params}`, {
155
+ headers: { "x-api-key": apiKey }
156
+ });
157
+ if (!res.ok) {
158
+ const body = await res.json().catch(() => ({}));
159
+ throw new Error(body.error ?? `HTTP ${res.status}`);
160
+ }
161
+ const data = await res.json();
162
+ if (options.json) {
163
+ outputJson(data);
164
+ return;
165
+ }
166
+ if (data.files.length === 0) {
167
+ console.log(`No files missing ${type} in bucket '${bucket}'.`);
168
+ return;
169
+ }
170
+ const rows = data.files.map((f) => [
171
+ f.key,
172
+ formatBytes(f.size),
173
+ f.lastModified.split("T")[0] ?? f.lastModified
174
+ ]);
175
+ console.log(formatTable(["Key", "Size", "Modified"], rows));
176
+ console.log(`
177
+ ${data.files.length} files missing ${type}`);
178
+ }
179
+ export {
180
+ enrichFile,
181
+ getFile,
182
+ listFiles,
183
+ listMissing,
184
+ updateFile
185
+ };
@@ -0,0 +1,190 @@
1
+ import {
2
+ createStow
3
+ } from "./chunk-5LU25QZK.js";
4
+ import "./chunk-TOADDO2F.js";
5
+
6
+ // src/commands/mcp.ts
7
+ import { z } from "zod";
8
+ async function startMcpServer() {
9
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
10
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
11
+ const server = new McpServer({
12
+ name: "stow",
13
+ version: "2.0.4"
14
+ });
15
+ server.tool(
16
+ "stow_whoami",
17
+ "Show current user and organization",
18
+ {},
19
+ async () => {
20
+ const stow = createStow();
21
+ const result = await stow.whoami();
22
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
23
+ }
24
+ );
25
+ server.tool("stow_buckets_list", "List all buckets", {}, async () => {
26
+ const stow = createStow();
27
+ const result = await stow.listBuckets();
28
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
29
+ });
30
+ server.tool(
31
+ "stow_files_list",
32
+ "List files in a bucket",
33
+ {
34
+ bucket: z.string().describe("Bucket name"),
35
+ limit: z.number().optional().describe("Max results (default 50)")
36
+ },
37
+ async (params) => {
38
+ const stow = createStow();
39
+ const result = await stow.listFiles({
40
+ bucket: params.bucket,
41
+ limit: params.limit
42
+ });
43
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
44
+ }
45
+ );
46
+ server.tool(
47
+ "stow_files_get",
48
+ "Get file details",
49
+ {
50
+ bucket: z.string().describe("Bucket name"),
51
+ key: z.string().describe("File key")
52
+ },
53
+ async (params) => {
54
+ const stow = createStow();
55
+ const result = await stow.getFile(params.key, {
56
+ bucket: params.bucket
57
+ });
58
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
59
+ }
60
+ );
61
+ server.tool(
62
+ "stow_search_text",
63
+ "Search files by text query",
64
+ {
65
+ query: z.string().describe("Search query"),
66
+ bucket: z.string().optional().describe("Bucket name (optional)"),
67
+ limit: z.number().optional().describe("Max results (default 10)")
68
+ },
69
+ async (params) => {
70
+ const stow = createStow();
71
+ const result = await stow.search.text({
72
+ query: params.query,
73
+ bucket: params.bucket,
74
+ limit: params.limit
75
+ });
76
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
77
+ }
78
+ );
79
+ server.tool(
80
+ "stow_search_similar",
81
+ "Find visually similar files",
82
+ {
83
+ fileKey: z.string().describe("File key to find similar files for"),
84
+ bucket: z.string().optional().describe("Bucket name (optional)"),
85
+ limit: z.number().optional().describe("Max results (default 10)")
86
+ },
87
+ async (params) => {
88
+ const stow = createStow();
89
+ const result = await stow.search.similar({
90
+ fileKey: params.fileKey,
91
+ bucket: params.bucket,
92
+ limit: params.limit
93
+ });
94
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
95
+ }
96
+ );
97
+ server.tool(
98
+ "stow_search_color",
99
+ "Search files by hex color",
100
+ {
101
+ hex: z.string().describe("Hex color code (e.g. #ff0000)"),
102
+ bucket: z.string().optional().describe("Bucket name (optional)"),
103
+ limit: z.number().optional().describe("Max results (default 10)")
104
+ },
105
+ async (params) => {
106
+ const stow = createStow();
107
+ const result = await stow.search.color({
108
+ hex: params.hex,
109
+ bucket: params.bucket,
110
+ limit: params.limit
111
+ });
112
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
113
+ }
114
+ );
115
+ server.tool(
116
+ "stow_search_diverse",
117
+ "Get a diverse set of files from a bucket",
118
+ {
119
+ bucket: z.string().optional().describe("Bucket name (optional)"),
120
+ limit: z.number().optional().describe("Max results (default 10)")
121
+ },
122
+ async (params) => {
123
+ const stow = createStow();
124
+ const result = await stow.search.diverse({
125
+ bucket: params.bucket,
126
+ limit: params.limit
127
+ });
128
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
129
+ }
130
+ );
131
+ server.tool(
132
+ "stow_search_image",
133
+ "Search by image URL or existing file key",
134
+ {
135
+ url: z.string().optional().describe("Image URL to search by"),
136
+ fileKey: z.string().optional().describe("Existing file key to search by"),
137
+ bucket: z.string().optional().describe("Bucket name (optional)"),
138
+ limit: z.number().optional().describe("Max results (default 12)")
139
+ },
140
+ async (params) => {
141
+ const stow = createStow();
142
+ const input = {};
143
+ if (params.url) input.url = params.url;
144
+ if (params.fileKey) input.fileKey = params.fileKey;
145
+ const result = await stow.search.image(input, {
146
+ bucket: params.bucket,
147
+ limit: params.limit ?? 12
148
+ });
149
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
150
+ }
151
+ );
152
+ server.tool("stow_tags_list", "List all tags", {}, async () => {
153
+ const stow = createStow();
154
+ const result = await stow.tags.list();
155
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
156
+ });
157
+ server.tool(
158
+ "stow_anchors_list",
159
+ "List anchors in a bucket",
160
+ {
161
+ bucket: z.string().optional().describe("Bucket name (optional)")
162
+ },
163
+ async (params) => {
164
+ const stow = createStow();
165
+ const result = await stow.anchors.list({ bucket: params.bucket });
166
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
167
+ }
168
+ );
169
+ server.tool(
170
+ "stow_anchors_create",
171
+ "Create a text anchor for semantic search",
172
+ {
173
+ text: z.string().describe("Anchor text to embed"),
174
+ label: z.string().optional().describe("Human-readable label (optional)")
175
+ },
176
+ async (params) => {
177
+ const stow = createStow();
178
+ const result = await stow.anchors.create({
179
+ text: params.text,
180
+ label: params.label
181
+ });
182
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
183
+ }
184
+ );
185
+ const transport = new StdioServerTransport();
186
+ await server.connect(transport);
187
+ }
188
+ export {
189
+ startMcpServer
190
+ };
@@ -0,0 +1,82 @@
1
+ import {
2
+ validateInput
3
+ } from "./chunk-OHAFRKN5.js";
4
+ import {
5
+ formatTable,
6
+ outputJson
7
+ } from "./chunk-ELSDWMEB.js";
8
+ import {
9
+ createStow
10
+ } from "./chunk-5LU25QZK.js";
11
+ import "./chunk-TOADDO2F.js";
12
+
13
+ // src/commands/tags.ts
14
+ async function listTags(options) {
15
+ const stow = createStow();
16
+ const data = await stow.tags.list();
17
+ if (options.json) {
18
+ outputJson(data);
19
+ return;
20
+ }
21
+ if (data.tags.length === 0) {
22
+ console.log("No tags yet. Create one with: stow tags create <name>");
23
+ return;
24
+ }
25
+ const rows = data.tags.map((t) => [t.name, t.slug, t.color ?? "\u2014", t.id]);
26
+ console.log(formatTable(["Name", "Slug", "Color", "ID"], rows));
27
+ }
28
+ async function createTag(name, options) {
29
+ validateInput(name, "tag name");
30
+ if (options.dryRun) {
31
+ console.log(
32
+ JSON.stringify(
33
+ {
34
+ dryRun: true,
35
+ action: "createTag",
36
+ details: {
37
+ name,
38
+ color: options.color ?? null
39
+ }
40
+ },
41
+ null,
42
+ 2
43
+ )
44
+ );
45
+ return;
46
+ }
47
+ const stow = createStow();
48
+ const tag = await stow.tags.create({
49
+ name,
50
+ ...options.color ? { color: options.color } : {}
51
+ });
52
+ if (options.json) {
53
+ outputJson(tag);
54
+ return;
55
+ }
56
+ console.log(`Created tag: ${tag.name}`);
57
+ }
58
+ async function deleteTag(id, options = {}) {
59
+ validateInput(id, "tag id");
60
+ if (options.dryRun) {
61
+ console.log(
62
+ JSON.stringify(
63
+ {
64
+ dryRun: true,
65
+ action: "deleteTag",
66
+ details: { id }
67
+ },
68
+ null,
69
+ 2
70
+ )
71
+ );
72
+ return;
73
+ }
74
+ const stow = createStow();
75
+ await stow.tags.delete(id);
76
+ console.log(`Deleted tag: ${id}`);
77
+ }
78
+ export {
79
+ createTag,
80
+ deleteTag,
81
+ listTags
82
+ };
@@ -0,0 +1,126 @@
1
+ import {
2
+ validateBucketName
3
+ } from "./chunk-OHAFRKN5.js";
4
+ import {
5
+ formatBytes
6
+ } from "./chunk-ELSDWMEB.js";
7
+ import {
8
+ createStow
9
+ } from "./chunk-5LU25QZK.js";
10
+ import "./chunk-TOADDO2F.js";
11
+
12
+ // src/commands/upload.ts
13
+ import { existsSync, readFileSync, statSync } from "fs";
14
+ import { basename, resolve } from "path";
15
+ var CONTENT_TYPES = {
16
+ png: "image/png",
17
+ jpg: "image/jpeg",
18
+ jpeg: "image/jpeg",
19
+ gif: "image/gif",
20
+ webp: "image/webp",
21
+ svg: "image/svg+xml",
22
+ ico: "image/x-icon",
23
+ avif: "image/avif",
24
+ pdf: "application/pdf",
25
+ mp4: "video/mp4",
26
+ webm: "video/webm",
27
+ mov: "video/quicktime",
28
+ mp3: "audio/mpeg",
29
+ wav: "audio/wav",
30
+ ogg: "audio/ogg",
31
+ zip: "application/zip",
32
+ tar: "application/x-tar",
33
+ gz: "application/gzip",
34
+ txt: "text/plain",
35
+ json: "application/json",
36
+ xml: "application/xml",
37
+ html: "text/html",
38
+ css: "text/css",
39
+ js: "application/javascript"
40
+ };
41
+ function getContentType(filename) {
42
+ const ext = filename.toLowerCase().split(".").pop();
43
+ return CONTENT_TYPES[ext || ""] || "application/octet-stream";
44
+ }
45
+ function readFile(filePath) {
46
+ const resolvedPath = resolve(filePath);
47
+ if (!existsSync(resolvedPath)) {
48
+ console.error(`Error: File not found: ${filePath}`);
49
+ process.exit(1);
50
+ }
51
+ const buffer = readFileSync(resolvedPath);
52
+ const filename = basename(resolvedPath);
53
+ const contentType = getContentType(filename);
54
+ return { buffer, filename, contentType };
55
+ }
56
+ function printUploadResult(url, options, opts) {
57
+ if (options.quiet) {
58
+ console.log(url);
59
+ return;
60
+ }
61
+ console.error(opts?.deduped ? "Done! (deduped)" : "Done!");
62
+ console.log("");
63
+ console.log(url);
64
+ }
65
+ async function uploadDrop(filePath, options) {
66
+ const { buffer, filename, contentType } = readFile(filePath);
67
+ if (!options.quiet) {
68
+ console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`);
69
+ }
70
+ const stow = createStow();
71
+ const result = await stow.drop(buffer, {
72
+ filename,
73
+ contentType
74
+ });
75
+ printUploadResult(result.url, options);
76
+ }
77
+ async function uploadFile(filePath, options) {
78
+ if (options.bucket) {
79
+ validateBucketName(options.bucket);
80
+ }
81
+ const resolvedPath = resolve(filePath);
82
+ if (!existsSync(resolvedPath)) {
83
+ console.error(`Error: File not found: ${filePath}`);
84
+ process.exit(1);
85
+ }
86
+ const filename = basename(resolvedPath);
87
+ const contentType = getContentType(filename);
88
+ const size = statSync(resolvedPath).size;
89
+ if (options.dryRun) {
90
+ console.log(
91
+ JSON.stringify(
92
+ {
93
+ dryRun: true,
94
+ action: "upload",
95
+ details: {
96
+ file: resolvedPath,
97
+ filename,
98
+ contentType,
99
+ size,
100
+ bucket: options.bucket ?? null
101
+ }
102
+ },
103
+ null,
104
+ 2
105
+ )
106
+ );
107
+ return;
108
+ }
109
+ const buffer = readFileSync(resolvedPath);
110
+ if (!options.quiet) {
111
+ console.error(`Uploading ${filename} (${formatBytes(buffer.length)})...`);
112
+ }
113
+ const stow = createStow();
114
+ const result = await stow.uploadFile(buffer, {
115
+ ...options.bucket ? { bucket: options.bucket } : {},
116
+ filename,
117
+ contentType
118
+ });
119
+ printUploadResult(result.url ?? result.key, options, {
120
+ deduped: result.deduped
121
+ });
122
+ }
123
+ export {
124
+ uploadDrop,
125
+ uploadFile
126
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stow-cli",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Stow file storage",
6
6
  "license": "MIT",
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@howells/stow-server": "workspace:*",
33
+ "@modelcontextprotocol/sdk": "^1.27.1",
33
34
  "clipboardy": "^5.3.1",
34
35
  "commander": "^14.0.3",
35
36
  "ink": "^6.8.0",
@@ -37,7 +38,8 @@
37
38
  "ink-spinner": "^5.0.0",
38
39
  "ink-text-input": "^6.0.0",
39
40
  "open": "^11.0.0",
40
- "react": "^19.2.4"
41
+ "react": "^19.2.4",
42
+ "zod": "^4.3.6"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@stow/typescript-config": "workspace:*",