@offlocal/mcp 0.3.0 → 0.3.1
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/detect.js +2 -2
- package/dist/tools/deploy.js +6 -2
- package/dist/tools/providers.js +9 -1
- package/dist/tools/status.js +18 -3
- package/dist/zip.js +18 -3
- package/package.json +1 -1
package/dist/detect.js
CHANGED
|
@@ -313,6 +313,8 @@ function pickNodeFramework(deps) {
|
|
|
313
313
|
return "remix";
|
|
314
314
|
if ("astro" in deps)
|
|
315
315
|
return "astro";
|
|
316
|
+
if ("vite" in deps)
|
|
317
|
+
return "vite";
|
|
316
318
|
if ("fastify" in deps)
|
|
317
319
|
return "fastify";
|
|
318
320
|
if ("hono" in deps)
|
|
@@ -321,8 +323,6 @@ function pickNodeFramework(deps) {
|
|
|
321
323
|
return "express";
|
|
322
324
|
if ("koa" in deps)
|
|
323
325
|
return "koa";
|
|
324
|
-
if ("vite" in deps)
|
|
325
|
-
return "vite";
|
|
326
326
|
return "node";
|
|
327
327
|
}
|
|
328
328
|
function nodePort(framework) {
|
package/dist/tools/deploy.js
CHANGED
|
@@ -72,13 +72,17 @@ export async function deployHandler(input) {
|
|
|
72
72
|
`| ${ICON.spark} Framework | ${summary.framework} (${summary.buildSource}) |\n` +
|
|
73
73
|
`| Port | ${summary.port} |\n` +
|
|
74
74
|
`| Bundle | ${summary.bundle.fileCount} files · ${summary.bundle.bytes} bytes |\n\n` +
|
|
75
|
+
(bundle.warnings.length
|
|
76
|
+
? bundle.warnings.map((w) => `${ICON.warn} ${w}`).join("\n") + "\n\n"
|
|
77
|
+
: "") +
|
|
75
78
|
`———\n` +
|
|
76
|
-
`${ICON.arrow} **Next (you, the agent):** tell the user it's building (2–
|
|
79
|
+
`${ICON.arrow} **Next (you, the agent):** tell the user it's building (usually 2–8 min) and you'll ` +
|
|
77
80
|
`report back when it's live. Then:\n` +
|
|
78
81
|
` 1. Sleep 45 seconds.\n` +
|
|
79
82
|
` 2. \`offlocal_status({ deploymentId: "${summary.deploymentId}" })\`.\n` +
|
|
80
83
|
` 3. Sleep the \`pollAfterSeconds\` it returns, then call status again — exactly once per interval.\n` +
|
|
81
|
-
` 4. Repeat until \`live\` or \`failed\`.
|
|
84
|
+
` 4. Repeat until \`live\` or \`failed\`. **Slow is normal — don't panic or declare failure on a slow deploy.** ` +
|
|
85
|
+
`The status tool tells you if it's genuinely stuck; only then should you stop and suggest a retry.\n` +
|
|
82
86
|
`Keep the **projectId** from the live status for any database step.`,
|
|
83
87
|
},
|
|
84
88
|
],
|
package/dist/tools/providers.js
CHANGED
|
@@ -110,7 +110,10 @@ export async function connectHandler(input) {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
export const ProvisionInputShape = {
|
|
113
|
-
provider: z
|
|
113
|
+
provider: z
|
|
114
|
+
.string()
|
|
115
|
+
.min(1)
|
|
116
|
+
.describe("The CONNECTED external provider to provision on: supabase, planetscale, digitalocean, or railway. For Offlocal's own managed Postgres, use offlocal_create_managed_database instead — not this."),
|
|
114
117
|
type: z
|
|
115
118
|
.enum(["project", "database", "deployment", "env_vars", "branch"])
|
|
116
119
|
.describe("The kind of resource to create."),
|
|
@@ -131,6 +134,11 @@ export const ProvisionInputShape = {
|
|
|
131
134
|
};
|
|
132
135
|
export async function provisionHandler(input) {
|
|
133
136
|
const { provider, confirm, projectId, ...spec } = input;
|
|
137
|
+
if (["offlocal", "offlocal.ai"].includes(provider.toLowerCase())) {
|
|
138
|
+
return text(`${ICON.warn} "offlocal" isn't a provision provider. For an Offlocal Managed Postgres database, call ` +
|
|
139
|
+
`\`offlocal_create_managed_database({ projectId })\`. \`offlocal_provision\` is only for connected external ` +
|
|
140
|
+
`providers: **supabase, planetscale, digitalocean, railway**.`);
|
|
141
|
+
}
|
|
134
142
|
const res = await provisionResource(provider, spec, {
|
|
135
143
|
confirm: confirm ?? false,
|
|
136
144
|
projectId,
|
package/dist/tools/status.js
CHANGED
|
@@ -24,6 +24,9 @@ export async function statusHandler(input) {
|
|
|
24
24
|
const isTerminal = dep.status === "live" || dep.status === "failed";
|
|
25
25
|
const pollAfterSeconds = isTerminal ? 0 : pollIntervalFor(dep.status);
|
|
26
26
|
const etaSeconds = isTerminal ? 0 : etaFor(dep.status);
|
|
27
|
+
const secondsSinceUpdate = Math.max(0, Math.round((Date.now() - new Date(dep.updatedAt).getTime()) / 1000));
|
|
28
|
+
const STUCK_AFTER = 9 * 60;
|
|
29
|
+
const stalled = !isTerminal && secondsSinceUpdate > STUCK_AFTER;
|
|
27
30
|
const summary = {
|
|
28
31
|
deploymentId: dep.id,
|
|
29
32
|
projectId: dep.projectId,
|
|
@@ -32,6 +35,8 @@ export async function statusHandler(input) {
|
|
|
32
35
|
isTerminal,
|
|
33
36
|
pollAfterSeconds,
|
|
34
37
|
etaSeconds,
|
|
38
|
+
secondsSinceUpdate,
|
|
39
|
+
stalled,
|
|
35
40
|
liveUrl: dep.liveUrl,
|
|
36
41
|
errorType: dep.errorType,
|
|
37
42
|
errorMessage: dep.errorMessage,
|
|
@@ -61,9 +66,19 @@ export async function statusHandler(input) {
|
|
|
61
66
|
lines.push("");
|
|
62
67
|
lines.push(bar);
|
|
63
68
|
lines.push("");
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
if (stalled) {
|
|
70
|
+
lines.push(`${ICON.warn} It's been at "${phase}" for ~${Math.round(secondsSinceUpdate / 60)} min with no change — ` +
|
|
71
|
+
`longer than a normal deploy. It may be stuck.\n` +
|
|
72
|
+
`Tell the user it's taking unusually long. You can wait a little longer, but do NOT keep polling indefinitely — ` +
|
|
73
|
+
`if it doesn't move within another couple of minutes, suggest re-running \`offlocal_deploy\` (a retry usually clears a stuck deploy).`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
lines.push(`Building / rolling out — **this is normal and can take 2–8 minutes** (occasionally a bit more). Stay calm and don't treat slowness as failure:\n` +
|
|
77
|
+
`• A deploy is only failed when the status is literally **failed** — nothing else means it broke.\n` +
|
|
78
|
+
`• "starting container" can mean the container is already healthy but not yet promoted; it'll flip to **live** shortly.\n` +
|
|
79
|
+
`• Empty \`offlocal_logs\` during build / before live is expected (container logs only exist once it's running) — not an error.\n` +
|
|
80
|
+
`Sleep ${pollAfterSeconds}s, then call this exactly once. Polling faster does not help.`);
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
return {
|
|
69
84
|
content: [{ type: "text", text: lines.join("\n") }],
|
package/dist/zip.js
CHANGED
|
@@ -20,13 +20,16 @@ const IGNORED_DIRS = new Set([
|
|
|
20
20
|
".idea",
|
|
21
21
|
".vscode",
|
|
22
22
|
]);
|
|
23
|
-
const IGNORED_FILE_RE = /^(\.env(\..+)
|
|
23
|
+
const IGNORED_FILE_RE = /^(\.env(\..+)?|\.DS_Store|Thumbs\.db)$|\.(log|tsbuildinfo|exe|dll|dylib|so|msi|dmg|pdb|class|o|obj|lib)$/i;
|
|
24
24
|
const MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
25
25
|
const MAX_BUNDLE_BYTES = 250 * 1024 * 1024;
|
|
26
|
+
const WARN_BUNDLE_BYTES = 25 * 1024 * 1024;
|
|
27
|
+
const WARN_FILE_BYTES = 5 * 1024 * 1024;
|
|
26
28
|
export async function bundleDirectory(sourceDir) {
|
|
27
29
|
const zip = new AdmZip();
|
|
28
30
|
let fileCount = 0;
|
|
29
31
|
let totalBytes = 0;
|
|
32
|
+
const bigFiles = [];
|
|
30
33
|
await walk(sourceDir, async (absolute) => {
|
|
31
34
|
const rel = relative(sourceDir, absolute);
|
|
32
35
|
if (!rel)
|
|
@@ -42,14 +45,26 @@ export async function bundleDirectory(sourceDir) {
|
|
|
42
45
|
zip.addFile(zipPath, content);
|
|
43
46
|
fileCount++;
|
|
44
47
|
totalBytes += st.size;
|
|
48
|
+
if (st.size > WARN_FILE_BYTES)
|
|
49
|
+
bigFiles.push({ rel: zipPath, bytes: st.size });
|
|
45
50
|
if (totalBytes > MAX_BUNDLE_BYTES) {
|
|
46
|
-
throw new Error(`bundle exceeds ${MAX_BUNDLE_BYTES} bytes. Trim node_modules-like outputs or add a .
|
|
51
|
+
throw new Error(`bundle exceeds ${MAX_BUNDLE_BYTES} bytes. Trim node_modules-like outputs or add a .gitignore.`);
|
|
47
52
|
}
|
|
48
53
|
});
|
|
49
54
|
if (fileCount === 0) {
|
|
50
55
|
throw new Error(`no files to bundle under ${sourceDir}`);
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
const warnings = [];
|
|
58
|
+
if (totalBytes > WARN_BUNDLE_BYTES) {
|
|
59
|
+
warnings.push(`Bundle is ${mb(totalBytes)} — large for a deploy. Add a .gitignore for build output/binaries you don't need to ship.`);
|
|
60
|
+
}
|
|
61
|
+
for (const f of bigFiles.sort((a, b) => b.bytes - a.bytes).slice(0, 3)) {
|
|
62
|
+
warnings.push(`Large file bundled: ${f.rel} (${mb(f.bytes)}). Gitignore it if it isn't needed at runtime.`);
|
|
63
|
+
}
|
|
64
|
+
return { buffer: zip.toBuffer(), fileCount, byteSize: totalBytes, warnings };
|
|
65
|
+
}
|
|
66
|
+
function mb(bytes) {
|
|
67
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
53
68
|
}
|
|
54
69
|
async function walk(root, onFile) {
|
|
55
70
|
const stack = [root];
|
package/package.json
CHANGED