frontpage-host 0.1.6 → 0.1.7

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/AGENTS.md CHANGED
@@ -119,10 +119,18 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
119
119
 
120
120
  ```bash
121
121
  frontpage prompt "make the hero shorter"
122
+ frontpage prompt "Use this logo in the header" --attach ./logo.png
123
+ frontpage prompt "Match this brand PDF" --attach ./guide.pdf --path /
124
+ frontpage prompt "Reuse uploaded art" --attach /__media/uploads/<slug>/<file>
122
125
  frontpage runs wait # wait for the agent run to finish
123
126
  # Review https://<subdomain>.frontpage.host — do not publish unless asked.
124
127
  ```
125
128
 
129
+ - `--attach` accepts a local file (uploads via the same path as `frontpage media upload`), an existing `/__media/…` site path, or an `https://` Blob URL. Repeatable.
130
+ - `--asset-url <https://…>` adds a legacy image URL. Repeatable.
131
+ - Caps match the media proxy: images / PDF / text-code. CLI video and >4 MB binaries are not supported yet (editor attach / Phase E).
132
+ - Prompt text is still required (API). Attachments ride on the same enqueue as the editor.
133
+
126
134
  Mid-task continuation is automatic. There is **no** `runs continue` in v1.
127
135
 
128
136
  ## Content / JSON-native commands
@@ -0,0 +1,59 @@
1
+ /** Mirror ATTACH_LIMITS in src/lib/agent/attachments.ts (images/docs/text only via proxy media). */
2
+ export declare const CLI_ATTACH_LIMITS: {
3
+ readonly maxImages: 6;
4
+ readonly maxDocuments: 4;
5
+ readonly maxTextFiles: 6;
6
+ };
7
+ export type PromptAttachmentsPayload = {
8
+ images: Array<{
9
+ url: string;
10
+ name?: string;
11
+ }>;
12
+ documents: Array<{
13
+ url: string;
14
+ name?: string;
15
+ }>;
16
+ textFiles: Array<{
17
+ name: string;
18
+ content: string;
19
+ }>;
20
+ };
21
+ export type MediaUploadResult = {
22
+ kind: string;
23
+ url?: string;
24
+ key?: string;
25
+ filename?: string;
26
+ sizeBytes?: number;
27
+ content?: string;
28
+ };
29
+ export declare function emptyAttachments(): PromptAttachmentsPayload;
30
+ export declare function isHttpsUrl(s: string): boolean;
31
+ export declare function isMediaSitePath(s: string): boolean;
32
+ export declare function classifyRemoteByName(nameOrUrl: string): "image" | "document";
33
+ export declare function addHttpsRef(out: PromptAttachmentsPayload, url: string, opts?: {
34
+ name?: string;
35
+ asImageOnly?: boolean;
36
+ }): void;
37
+ export declare function addUploadResult(out: PromptAttachmentsPayload, data: MediaUploadResult): void;
38
+ export declare function uploadLocalForAttach(slug: string, filePath: string): Promise<MediaUploadResult>;
39
+ export declare function resolveSitePathUrl(slug: string, sitePath: string): Promise<{
40
+ url: string;
41
+ name: string;
42
+ type: string;
43
+ }>;
44
+ /**
45
+ * Resolve one --attach value (local path, /__media/…, or https URL) into the payload.
46
+ */
47
+ export declare function resolveAttachRef(slug: string, ref: string, out: PromptAttachmentsPayload): Promise<void>;
48
+ export declare function buildPromptAttachments(args: {
49
+ slug: string;
50
+ attachRefs: string[];
51
+ assetUrls: string[];
52
+ }): Promise<PromptAttachmentsPayload | undefined>;
53
+ /** Parse prompt argv: prompt words + --path / --attach / --asset-url flags. */
54
+ export declare function parsePromptArgs(rest: string[]): {
55
+ prompt: string;
56
+ currentPath: string;
57
+ attachRefs: string[];
58
+ assetUrls: string[];
59
+ };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * §CLI Phase C — resolve --attach / --asset-url into the agent attachments payload.
3
+ * Local files upload via the same multipart media endpoint as `frontpage media upload`.
4
+ */
5
+ import { access, readFile } from "node:fs/promises";
6
+ import { basename } from "node:path";
7
+ import { constants as fsConstants } from "node:fs";
8
+ import { v1Get, v1PostMultipart, CliHttpError, EXIT_USAGE, EXIT_UNEXPECTED } from "../http/client.js";
9
+ /** Mirror ATTACH_LIMITS in src/lib/agent/attachments.ts (images/docs/text only via proxy media). */
10
+ export const CLI_ATTACH_LIMITS = {
11
+ maxImages: 6,
12
+ maxDocuments: 4,
13
+ maxTextFiles: 6,
14
+ };
15
+ export function emptyAttachments() {
16
+ return { images: [], documents: [], textFiles: [] };
17
+ }
18
+ export function isHttpsUrl(s) {
19
+ return /^https:\/\/[^\s]+$/i.test(s) && s.length <= 2048;
20
+ }
21
+ export function isMediaSitePath(s) {
22
+ return s.startsWith("/__media/");
23
+ }
24
+ export function classifyRemoteByName(nameOrUrl) {
25
+ const base = nameOrUrl.split(/[?#]/)[0] ?? nameOrUrl;
26
+ const ext = base.includes(".") ? (base.split(".").pop() ?? "").toLowerCase() : "";
27
+ if (ext === "pdf")
28
+ return "document";
29
+ return "image";
30
+ }
31
+ function pushUniqueUrl(list, max, url, name, label) {
32
+ if (list.some((x) => x.url === url))
33
+ return;
34
+ if (list.length >= max) {
35
+ throw new CliHttpError(`Too many ${label} attachments (max ${max}).`, EXIT_USAGE, 400, "bad_request");
36
+ }
37
+ list.push(name ? { url, name } : { url });
38
+ }
39
+ function pushText(list, name, content) {
40
+ if (list.length >= CLI_ATTACH_LIMITS.maxTextFiles) {
41
+ throw new CliHttpError(`Too many text attachments (max ${CLI_ATTACH_LIMITS.maxTextFiles}).`, EXIT_USAGE, 400, "bad_request");
42
+ }
43
+ list.push({ name, content });
44
+ }
45
+ export function addHttpsRef(out, url, opts) {
46
+ if (!isHttpsUrl(url)) {
47
+ throw new CliHttpError(`Not an https URL: ${url}`, EXIT_USAGE, 400, "bad_request");
48
+ }
49
+ const kind = opts?.asImageOnly ? "image" : classifyRemoteByName(opts?.name ?? url);
50
+ if (kind === "document") {
51
+ pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, url, opts?.name, "document");
52
+ }
53
+ else {
54
+ pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, url, opts?.name, "image");
55
+ }
56
+ }
57
+ export function addUploadResult(out, data) {
58
+ const name = data.filename?.trim() || undefined;
59
+ if (data.kind === "text") {
60
+ if (typeof data.content !== "string" || !data.content) {
61
+ throw new CliHttpError("Text upload returned no content.", EXIT_UNEXPECTED, 500, "internal");
62
+ }
63
+ pushText(out.textFiles, name ?? "snippet.txt", data.content);
64
+ return;
65
+ }
66
+ if (data.kind === "image" || data.kind === "document") {
67
+ if (!data.url || !isHttpsUrl(data.url)) {
68
+ throw new CliHttpError(`Upload of ${name ?? "file"} did not return a Blob URL.`, EXIT_UNEXPECTED, 500, "internal");
69
+ }
70
+ if (data.kind === "document") {
71
+ pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, data.url, name, "document");
72
+ }
73
+ else {
74
+ pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, data.url, name, "image");
75
+ }
76
+ return;
77
+ }
78
+ throw new CliHttpError(`Unsupported upload kind "${data.kind}" for prompt attach. Use images, PDFs, or text/code files (video/large files need the editor path until Phase E).`, EXIT_USAGE, 400, "bad_request");
79
+ }
80
+ export async function uploadLocalForAttach(slug, filePath) {
81
+ try {
82
+ await access(filePath, fsConstants.R_OK);
83
+ }
84
+ catch {
85
+ throw new CliHttpError(`Cannot read file for --attach: ${filePath}`, EXIT_USAGE, 400, "bad_request");
86
+ }
87
+ const bytes = await readFile(filePath);
88
+ if (bytes.length === 0) {
89
+ throw new CliHttpError(`File is empty: ${filePath}`, EXIT_USAGE, 400, "bad_request");
90
+ }
91
+ const form = new FormData();
92
+ form.append("file", new Blob([Uint8Array.from(bytes)]), basename(filePath));
93
+ return v1PostMultipart(`/api/v1/sites/${encodeURIComponent(slug)}/media`, form);
94
+ }
95
+ export async function resolveSitePathUrl(slug, sitePath) {
96
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/media`);
97
+ const norm = (p) => p.replace(/\/+/g, "/").replace(/\/$/, "") || p;
98
+ const want = norm(sitePath);
99
+ const hit = data.assets.find((a) => a.sitePath === sitePath || norm(a.sitePath) === want);
100
+ if (!hit?.url) {
101
+ throw new CliHttpError(`No media asset found for ${sitePath}. Upload with frontpage media upload, or pass a Blob https URL.`, EXIT_USAGE, 404, "not_found");
102
+ }
103
+ return { url: hit.url, name: hit.name, type: hit.type };
104
+ }
105
+ /**
106
+ * Resolve one --attach value (local path, /__media/…, or https URL) into the payload.
107
+ */
108
+ export async function resolveAttachRef(slug, ref, out) {
109
+ const raw = ref.trim();
110
+ if (!raw) {
111
+ throw new CliHttpError("--attach requires a path or URL", EXIT_USAGE, 400, "bad_request");
112
+ }
113
+ if (/^https?:\/\//i.test(raw)) {
114
+ if (!isHttpsUrl(raw)) {
115
+ throw new CliHttpError(`--attach URLs must be https:// (got ${raw.split(":")[0]}://)`, EXIT_USAGE, 400, "bad_request");
116
+ }
117
+ addHttpsRef(out, raw);
118
+ return;
119
+ }
120
+ if (isMediaSitePath(raw)) {
121
+ const asset = await resolveSitePathUrl(slug, raw);
122
+ const kind = classifyRemoteByName(asset.name || asset.type || raw);
123
+ if (kind === "document") {
124
+ pushUniqueUrl(out.documents, CLI_ATTACH_LIMITS.maxDocuments, asset.url, asset.name, "document");
125
+ }
126
+ else {
127
+ pushUniqueUrl(out.images, CLI_ATTACH_LIMITS.maxImages, asset.url, asset.name, "image");
128
+ }
129
+ return;
130
+ }
131
+ const uploaded = await uploadLocalForAttach(slug, raw);
132
+ addUploadResult(out, uploaded);
133
+ }
134
+ export async function buildPromptAttachments(args) {
135
+ if (args.attachRefs.length === 0 && args.assetUrls.length === 0)
136
+ return undefined;
137
+ const out = emptyAttachments();
138
+ for (const url of args.assetUrls) {
139
+ addHttpsRef(out, url.trim(), { asImageOnly: true });
140
+ }
141
+ for (const ref of args.attachRefs) {
142
+ await resolveAttachRef(args.slug, ref, out);
143
+ }
144
+ if (out.images.length === 0 &&
145
+ out.documents.length === 0 &&
146
+ out.textFiles.length === 0) {
147
+ throw new CliHttpError("No attachments could be resolved from --attach / --asset-url.", EXIT_USAGE, 400, "bad_request");
148
+ }
149
+ return out;
150
+ }
151
+ /** Parse prompt argv: prompt words + --path / --attach / --asset-url flags. */
152
+ export function parsePromptArgs(rest) {
153
+ let currentPath = "/";
154
+ const promptParts = [];
155
+ const attachRefs = [];
156
+ const assetUrls = [];
157
+ for (let i = 0; i < rest.length; i++) {
158
+ const a = rest[i];
159
+ if (a === "--path") {
160
+ currentPath = rest[++i] ?? "/";
161
+ continue;
162
+ }
163
+ if (a.startsWith("--path=")) {
164
+ currentPath = a.slice("--path=".length) || "/";
165
+ continue;
166
+ }
167
+ if (a === "--attach") {
168
+ const v = rest[++i];
169
+ if (!v || v.startsWith("--")) {
170
+ throw new CliHttpError('Usage: frontpage prompt "…" --attach <file|/__media/…|https://…>', EXIT_USAGE, 400, "bad_request");
171
+ }
172
+ attachRefs.push(v);
173
+ continue;
174
+ }
175
+ if (a.startsWith("--attach=")) {
176
+ const v = a.slice("--attach=".length);
177
+ if (!v) {
178
+ throw new CliHttpError("--attach= requires a value", EXIT_USAGE, 400, "bad_request");
179
+ }
180
+ attachRefs.push(v);
181
+ continue;
182
+ }
183
+ if (a === "--asset-url") {
184
+ const v = rest[++i];
185
+ if (!v || v.startsWith("--")) {
186
+ throw new CliHttpError("Usage: frontpage prompt \"…\" --asset-url <https://…>", EXIT_USAGE, 400, "bad_request");
187
+ }
188
+ assetUrls.push(v);
189
+ continue;
190
+ }
191
+ if (a.startsWith("--asset-url=")) {
192
+ const v = a.slice("--asset-url=".length);
193
+ if (!v) {
194
+ throw new CliHttpError("--asset-url= requires a value", EXIT_USAGE, 400, "bad_request");
195
+ }
196
+ assetUrls.push(v);
197
+ continue;
198
+ }
199
+ promptParts.push(a);
200
+ }
201
+ return {
202
+ prompt: promptParts.join(" ").trim(),
203
+ currentPath,
204
+ attachRefs,
205
+ assetUrls,
206
+ };
207
+ }
@@ -1,38 +1,48 @@
1
1
  import { resolveSlug } from "../site.js";
2
2
  import { EXIT_OK, EXIT_USAGE, v1Post } from "../http/client.js";
3
3
  import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
4
+ import { buildPromptAttachments, parsePromptArgs } from "./prompt-attach.js";
4
5
  export async function runPrompt(args) {
5
6
  try {
6
7
  const { slug, rest } = await resolveSlug(args);
7
- let currentPath = "/";
8
- const promptParts = [];
9
- for (let i = 0; i < rest.length; i++) {
10
- const a = rest[i];
11
- if (a === "--path") {
12
- currentPath = rest[++i] ?? "/";
13
- continue;
14
- }
15
- if (a.startsWith("--path=")) {
16
- currentPath = a.slice("--path=".length) || "/";
17
- continue;
18
- }
19
- promptParts.push(a);
20
- }
21
- const prompt = promptParts.join(" ").trim();
8
+ const { prompt, currentPath, attachRefs, assetUrls } = parsePromptArgs(rest);
22
9
  if (!prompt) {
23
- return emitUsage("prompt", 'Usage: frontpage prompt "..." [--site <slug>] [--path /page]', EXIT_USAGE);
10
+ return emitUsage("prompt", 'Usage: frontpage prompt "..." [--site <slug>] [--path /page] [--attach <file|/__media/…|https://…>]… [--asset-url <https://…>]…', EXIT_USAGE);
24
11
  }
25
12
  if (!currentPath.startsWith("/")) {
26
13
  return emitUsage("prompt", "currentPath must start with /", EXIT_USAGE);
27
14
  }
28
- const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`, { prompt, currentPath });
15
+ const attachments = await buildPromptAttachments({ slug, attachRefs, assetUrls });
16
+ const body = { prompt, currentPath };
17
+ if (attachments) {
18
+ body.attachments = attachments;
19
+ // Legacy image list for run display / back-compat readers.
20
+ body.assetUrls = attachments.images.map((i) => i.url);
21
+ }
22
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/agent-runs`, body);
29
23
  if (isJson()) {
30
- emitOk("prompt", { slug, ...data });
24
+ emitOk("prompt", {
25
+ slug,
26
+ ...data,
27
+ attached: attachments
28
+ ? {
29
+ images: attachments.images.length,
30
+ documents: attachments.documents.length,
31
+ textFiles: attachments.textFiles.length,
32
+ }
33
+ : undefined,
34
+ });
31
35
  }
32
36
  else {
33
37
  console.log(data.runId);
34
38
  if (data.coalesced)
35
39
  console.error("(coalesced with an identical queued run)");
40
+ if (attachments) {
41
+ const n = attachments.images.length +
42
+ attachments.documents.length +
43
+ attachments.textFiles.length;
44
+ console.error(`(attached ${n} file${n === 1 ? "" : "s"})`);
45
+ }
36
46
  }
37
47
  return EXIT_OK;
38
48
  }
@@ -26,6 +26,7 @@ _frontpage_completions() {
26
26
  analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
27
27
  modules) COMPREPLY=( $(compgen -W "list docs request-setup enable disable archive config" -- "\${cur}") ) ;;
28
28
  extensions) COMPREPLY=( $(compgen -W "list docs enable disable config" -- "\${cur}") ) ;;
29
+ prompt) COMPREPLY=( $(compgen -W "--path --attach --asset-url --site --json --help" -- "\${cur}") ) ;;
29
30
  *) COMPREPLY=( $(compgen -W "--site --help --json --quiet --yes" -- "\${cur}") ) ;;
30
31
  esac
31
32
  }
package/dist/registry.js CHANGED
@@ -184,11 +184,23 @@ export const COMMANDS = [
184
184
  {
185
185
  name: "prompt",
186
186
  summary: "Enqueue an agent build",
187
- usage: 'frontpage prompt "..." [--site <slug>] [--path /] [--json]',
187
+ usage: 'frontpage prompt "..." [--site <slug>] [--path /] [--attach <file|/__media/…|https://…>]… [--asset-url <https://…>]… [--json]',
188
188
  topics: ["agent"],
189
- examples: ['frontpage prompt "make the hero shorter"'],
190
- flags: ["--path must start with /", "stdout is bare runId (or JSON with --json)"],
191
- seeAlso: ["runs", "deploy"],
189
+ examples: [
190
+ 'frontpage prompt "make the hero shorter"',
191
+ 'frontpage prompt "Use this logo in the header" --attach ./logo.png',
192
+ 'frontpage prompt "Match this PDF brand guide" --attach ./guide.pdf --path /',
193
+ 'frontpage prompt "Reuse uploaded art" --attach /__media/uploads/acme/uuid.png',
194
+ 'frontpage prompt "Use this image" --asset-url "https://….blob.vercel-storage.com/uploads/…"',
195
+ ],
196
+ flags: [
197
+ "--path must start with /",
198
+ "--attach: local file (uploads via media), /__media/… site path, or https Blob URL; repeatable",
199
+ "--asset-url: https image URL (legacy); repeatable",
200
+ "Local attach uses the same caps as frontpage media upload (images/PDF/text; no CLI video yet)",
201
+ "stdout is bare runId (or JSON with --json)",
202
+ ],
203
+ seeAlso: ["runs", "deploy", "media"],
192
204
  },
193
205
  {
194
206
  name: "runs",
@@ -467,6 +479,8 @@ export const TOPICS = {
467
479
  See also: frontpage help sync | content | AGENTS.md`,
468
480
  agent: `Agent + publish
469
481
  frontpage prompt "make the hero shorter"
482
+ frontpage prompt "Use this logo" --attach ./logo.png
483
+ frontpage prompt "…" --attach /__media/uploads/<slug>/… --asset-url https://…
470
484
  frontpage runs # latest + active
471
485
  frontpage runs wait [runId]
472
486
  frontpage runs cancel <runId>
@@ -482,6 +496,7 @@ export const TOPICS = {
482
496
  frontpage modules request-setup <key> --reason "…"
483
497
  frontpage extensions
484
498
 
499
+ --attach uploads local files via media (images/PDF/text) or accepts /__media/… / https URLs.
485
500
  Mid-task continuation is automatic — there is no runs continue.
486
501
  Do not push while an agent run is active.
487
502
  Default after work: review https://<subdomain>.frontpage.host; do not publish.`,
package/llms.txt CHANGED
@@ -10,6 +10,7 @@
10
10
  - Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
11
11
  - Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it)
12
12
  - Analytics ops: frontpage analytics summary|query|funnel|timeseries|compare|sql (bare analytics = dashboard)
13
+ - Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (media upload for locals; images/PDF/text)
13
14
  - Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
14
15
  - Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
15
16
  - Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step for agents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {