stow-cli 2.0.4 → 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.
- package/README.md +363 -0
- package/dist/buckets-ESAOL6CH.js +115 -0
- package/dist/chunk-OHAFRKN5.js +40 -0
- package/dist/chunk-XJDK2CBE.js +328 -0
- package/dist/cli.js +66 -366
- package/dist/delete-4JSVNETO.js +34 -0
- package/dist/describe-UFMXNNUB.js +79 -0
- package/dist/files-TDIGJDN3.js +185 -0
- package/dist/mcp-RZT4TJEX.js +190 -0
- package/dist/tags-MCFL5M2J.js +82 -0
- package/dist/upload-5TAWJU5N.js +126 -0
- package/package.json +4 -2
|
@@ -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
|
+
"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:*",
|