frontpage-host 0.1.5 → 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
@@ -108,19 +108,29 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
108
108
  ## Modules & articles
109
109
 
110
110
  - Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`.
111
+ - Wizard modules (payments/shop/ordering/instagram): after building gated markup, `frontpage modules request-setup <key> --reason "…"` once — owner finishes in the **editor status bar** (CLI cannot show it). Auto-active modules (forms/callbar/…) need markup, not request-setup.
111
112
  - Articles: `frontpage articles …` for lifecycle; bodies via sync under `content/` or `articles content get|set`. Section landing is an owned `src/pages/<section>/index.astro`.
112
113
  - Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
113
114
  - Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
114
115
  - Media in markup: `/__media/uploads/<slug>/…` from `frontpage media` (not raw blob URLs).
116
+ - Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
115
117
 
116
118
  ## Agent prompt loop
117
119
 
118
120
  ```bash
119
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>
120
125
  frontpage runs wait # wait for the agent run to finish
121
126
  # Review https://<subdomain>.frontpage.host — do not publish unless asked.
122
127
  ```
123
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
+
124
134
  Mid-task continuation is automatic. There is **no** `runs continue` in v1.
125
135
 
126
136
  ## Content / JSON-native commands
@@ -128,7 +138,7 @@ Mid-task continuation is automatic. There is **no** `runs continue` in v1.
128
138
  These already return JSON bodies (wrap further with global `--json` where supported):
129
139
 
130
140
  - `usage`, `analytics`, `seo`, `articles`, `settings`, `inbox` (default JSON)
131
- - Mutate: `modules enable|disable|archive|config`, `extensions …`
141
+ - Mutate: `modules enable|disable|archive|config|request-setup`, `extensions …`
132
142
 
133
143
  Click-edit (exact single occurrence), **not** file push:
134
144
 
@@ -1,17 +1,175 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
+ import { emitErr, emitOk, isJson } from "../runtime.js";
4
+ function takeFlag(rest, name) {
5
+ const i = rest.indexOf(name);
6
+ if (i === -1)
7
+ return undefined;
8
+ const v = rest[i + 1];
9
+ rest.splice(i, 2);
10
+ return v;
11
+ }
12
+ function parseJsonFlag(raw, label) {
13
+ if (raw === undefined)
14
+ return undefined;
15
+ try {
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ throw new Error(`${label} must be valid JSON`);
20
+ }
21
+ }
22
+ function rangeFields(work) {
23
+ const days = takeFlag(work, "--days");
24
+ const from = takeFlag(work, "--from");
25
+ const to = takeFlag(work, "--to");
26
+ const out = {};
27
+ if (days)
28
+ out.days = Number(days);
29
+ if (from)
30
+ out.from = from;
31
+ if (to)
32
+ out.to = to;
33
+ return out;
34
+ }
35
+ function printJson(data) {
36
+ console.log(JSON.stringify(data, null, 2));
37
+ }
3
38
  export async function runAnalytics(args) {
4
39
  try {
5
40
  const { slug, rest } = await resolveSlug(args);
6
- let days = "7";
7
- const daysIdx = rest.indexOf("--days");
8
- if (daysIdx >= 0 && rest[daysIdx + 1])
9
- days = rest[daysIdx + 1];
10
- const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/analytics?days=${encodeURIComponent(days)}`);
11
- console.log(JSON.stringify(data.dashboard, null, 2));
12
- return EXIT_OK;
41
+ const work = [...rest];
42
+ const sub = work[0];
43
+ const ops = new Set(["summary", "query", "funnel", "timeseries", "compare", "sql", "dashboard"]);
44
+ // Bare `analytics` or `analytics dashboard` → legacy GET dashboard (compat).
45
+ if (!sub || sub === "dashboard" || !ops.has(sub) && sub.startsWith("--")) {
46
+ if (sub === "dashboard")
47
+ work.shift();
48
+ const days = takeFlag(work, "--days") ?? "7";
49
+ const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/analytics?days=${encodeURIComponent(days)}`);
50
+ if (isJson()) {
51
+ emitOk("analytics", data);
52
+ return EXIT_OK;
53
+ }
54
+ printJson(data.dashboard);
55
+ return EXIT_OK;
56
+ }
57
+ if (!ops.has(sub)) {
58
+ console.error("Usage: frontpage analytics [dashboard|summary|query|funnel|timeseries|compare|sql] …");
59
+ return EXIT_USAGE;
60
+ }
61
+ work.shift();
62
+ const base = `/api/v1/sites/${encodeURIComponent(slug)}/analytics`;
63
+ if (sub === "summary") {
64
+ const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
65
+ const body = { op: "summary", ...rangeFields(work), ...(filters ? { filters } : {}) };
66
+ const data = await v1Post(base, body);
67
+ if (isJson())
68
+ emitOk("analytics", data);
69
+ else
70
+ printJson(data);
71
+ return EXIT_OK;
72
+ }
73
+ if (sub === "query") {
74
+ const dimension = takeFlag(work, "--dimension") ?? work.shift();
75
+ if (!dimension) {
76
+ console.error("Usage: frontpage analytics query --dimension <path|source|…> [--measure count] [--limit N] [--filters '{…}'] [--days N]");
77
+ return EXIT_USAGE;
78
+ }
79
+ const measure = takeFlag(work, "--measure");
80
+ const limit = takeFlag(work, "--limit");
81
+ const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
82
+ const body = {
83
+ op: "query",
84
+ dimension,
85
+ ...rangeFields(work),
86
+ ...(measure ? { measure } : {}),
87
+ ...(limit ? { limit: Number(limit) } : {}),
88
+ ...(filters ? { filters } : {}),
89
+ };
90
+ const data = await v1Post(base, body);
91
+ if (isJson())
92
+ emitOk("analytics", data);
93
+ else
94
+ printJson(data);
95
+ return EXIT_OK;
96
+ }
97
+ if (sub === "funnel") {
98
+ const stepsRaw = takeFlag(work, "--steps");
99
+ if (!stepsRaw) {
100
+ console.error(`Usage: frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]' [--days N]`);
101
+ return EXIT_USAGE;
102
+ }
103
+ const steps = parseJsonFlag(stepsRaw, "--steps");
104
+ const body = { op: "funnel", steps, ...rangeFields(work) };
105
+ const data = await v1Post(base, body);
106
+ if (isJson())
107
+ emitOk("analytics", data);
108
+ else
109
+ printJson(data);
110
+ return EXIT_OK;
111
+ }
112
+ if (sub === "timeseries") {
113
+ const metric = takeFlag(work, "--metric");
114
+ const interval = takeFlag(work, "--interval");
115
+ const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
116
+ const body = {
117
+ op: "timeseries",
118
+ ...rangeFields(work),
119
+ ...(metric ? { metric } : {}),
120
+ ...(interval ? { interval } : {}),
121
+ ...(filters ? { filters } : {}),
122
+ };
123
+ const data = await v1Post(base, body);
124
+ if (isJson())
125
+ emitOk("analytics", data);
126
+ else
127
+ printJson(data);
128
+ return EXIT_OK;
129
+ }
130
+ if (sub === "compare") {
131
+ const aRaw = takeFlag(work, "--a");
132
+ const bRaw = takeFlag(work, "--b");
133
+ if (!aRaw || !bRaw) {
134
+ console.error(`Usage: frontpage analytics compare --a '{"device":"mobile"}' --b '{"device":"desktop"}' [--metric uniqueVisitors] [--days N]`);
135
+ return EXIT_USAGE;
136
+ }
137
+ const a = parseJsonFlag(aRaw, "--a");
138
+ const b = parseJsonFlag(bRaw, "--b");
139
+ const metric = takeFlag(work, "--metric");
140
+ const body = {
141
+ op: "compare",
142
+ a,
143
+ b,
144
+ ...rangeFields(work),
145
+ ...(metric ? { metric } : {}),
146
+ };
147
+ const data = await v1Post(base, body);
148
+ if (isJson())
149
+ emitOk("analytics", data);
150
+ else
151
+ printJson(data);
152
+ return EXIT_OK;
153
+ }
154
+ if (sub === "sql") {
155
+ const sql = takeFlag(work, "--sql") ?? work.join(" ").trim();
156
+ if (!sql) {
157
+ console.error('Usage: frontpage analytics sql --sql "SELECT path, count(*) FROM analytics_events GROUP BY 1"');
158
+ return EXIT_USAGE;
159
+ }
160
+ const data = await v1Post(base, { op: "sql", sql });
161
+ if (isJson())
162
+ emitOk("analytics", data);
163
+ else
164
+ printJson(data);
165
+ return EXIT_OK;
166
+ }
167
+ console.error("Usage: frontpage analytics [dashboard|summary|query|funnel|timeseries|compare|sql] …");
168
+ return EXIT_USAGE;
13
169
  }
14
170
  catch (err) {
171
+ if (isJson())
172
+ return emitErr("analytics", err);
15
173
  console.error(err instanceof Error ? err.message : String(err));
16
174
  return exitCodeForError(err);
17
175
  }
@@ -36,6 +36,34 @@ export async function runModules(args) {
36
36
  console.log(data.docs);
37
37
  return EXIT_OK;
38
38
  }
39
+ if (sub === "request-setup" || sub === "request_setup") {
40
+ const key = rest[1];
41
+ if (!key) {
42
+ console.error("Usage: frontpage modules request-setup <key> --reason \"…\" [--prefill '{…}'] [--instance <id>] [--site <slug>]");
43
+ return EXIT_USAGE;
44
+ }
45
+ const work = rest.slice(2);
46
+ const instanceId = takeFlag(work, "--instance");
47
+ const reason = takeFlag(work, "--reason");
48
+ const prefillRaw = takeFlag(work, "--prefill");
49
+ if (!reason) {
50
+ console.error("Usage: frontpage modules request-setup <key> --reason \"…\" [--prefill '{…}']");
51
+ return EXIT_USAGE;
52
+ }
53
+ let prefill;
54
+ if (prefillRaw) {
55
+ try {
56
+ prefill = JSON.parse(prefillRaw);
57
+ }
58
+ catch {
59
+ console.error("--prefill must be valid JSON");
60
+ return EXIT_USAGE;
61
+ }
62
+ }
63
+ const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/modules/${encodeURIComponent(key)}`, { action: "request-setup", reason, prefill, instanceId });
64
+ console.log(JSON.stringify(data, null, 2));
65
+ return EXIT_OK;
66
+ }
39
67
  if (sub === "enable" || sub === "disable" || sub === "archive" || sub === "config") {
40
68
  const key = rest[1];
41
69
  if (!key) {
@@ -63,7 +91,7 @@ export async function runModules(args) {
63
91
  console.log(JSON.stringify(data, null, 2));
64
92
  return EXIT_OK;
65
93
  }
66
- console.error("Usage: frontpage modules [list|docs|enable|disable|archive|config] …");
94
+ console.error("Usage: frontpage modules [list|docs|request-setup|enable|disable|archive|config] …");
67
95
  return EXIT_USAGE;
68
96
  }
69
97
  catch (err) {
@@ -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
  }
@@ -23,8 +23,10 @@ _frontpage_completions() {
23
23
  tokens) COMPREPLY=( $(compgen -W "set" -- "\${cur}") ) ;;
24
24
  seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
25
25
  articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
26
- modules) COMPREPLY=( $(compgen -W "list docs enable disable archive config" -- "\${cur}") ) ;;
26
+ analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
27
+ modules) COMPREPLY=( $(compgen -W "list docs request-setup enable disable archive config" -- "\${cur}") ) ;;
27
28
  extensions) COMPREPLY=( $(compgen -W "list docs enable disable config" -- "\${cur}") ) ;;
29
+ prompt) COMPREPLY=( $(compgen -W "--path --attach --asset-url --site --json --help" -- "\${cur}") ) ;;
28
30
  *) COMPREPLY=( $(compgen -W "--site --help --json --quiet --yes" -- "\${cur}") ) ;;
29
31
  esac
30
32
  }
@@ -50,7 +52,9 @@ _frontpage() {
50
52
  fonts) _values 'subcommand' list search ;;
51
53
  tokens) _values 'subcommand' set ;;
52
54
  seo|settings) _values 'subcommand' get set ;;
53
- modules|extensions) _values 'subcommand' list docs enable disable config ;;
55
+ analytics) _values 'subcommand' dashboard summary query funnel timeseries compare sql ;;
56
+ modules) _values 'subcommand' list docs request-setup enable disable archive config ;;
57
+ extensions) _values 'subcommand' list docs enable disable config ;;
54
58
  esac
55
59
  ;;
56
60
  esac
@@ -70,7 +74,9 @@ export function generateFishCompletion() {
70
74
  `complete -c frontpage -n "__fish_seen_subcommand_from fonts" -a "list search"`,
71
75
  `complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
72
76
  `complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
73
- `complete -c frontpage -n "__fish_seen_subcommand_from modules extensions" -a "list docs enable disable config"`,
77
+ `complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
78
+ `complete -c frontpage -n "__fish_seen_subcommand_from modules" -a "list docs request-setup enable disable archive config"`,
79
+ `complete -c frontpage -n "__fish_seen_subcommand_from extensions" -a "list docs enable disable config"`,
74
80
  `complete -c frontpage -l site -d "Site subdomain slug"`,
75
81
  ];
76
82
  return `${lines.join("\n")}\n`;
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",
@@ -246,9 +258,22 @@ export const COMMANDS = [
246
258
  },
247
259
  {
248
260
  name: "analytics",
249
- summary: "Show analytics dashboard JSON",
250
- usage: "frontpage analytics [--days 7] [--site <slug>]",
261
+ summary: "Analytics dashboard or structured query ops",
262
+ usage: "frontpage analytics [dashboard|summary|query|funnel|timeseries|compare|sql] … [--site <slug>] [--json]",
251
263
  topics: ["agent"],
264
+ examples: [
265
+ "frontpage analytics --days 7",
266
+ "frontpage analytics summary --days 30",
267
+ "frontpage analytics query --dimension path --measure count --limit 10",
268
+ `frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]'`,
269
+ "frontpage analytics timeseries --metric pageviews --interval day --days 14",
270
+ `frontpage analytics compare --a '{"device":"mobile"}' --b '{"device":"desktop"}'`,
271
+ 'frontpage analytics sql --sql "SELECT path, count(*)::int AS n FROM analytics_events GROUP BY 1 ORDER BY n DESC LIMIT 10"',
272
+ ],
273
+ flags: [
274
+ "Bare analytics / dashboard = legacy GET dashboard (compat)",
275
+ "Ops use POST …/analytics with allowlisted dimensions/measures; SQL is guard-validated + RLS",
276
+ ],
252
277
  },
253
278
  {
254
279
  name: "pages",
@@ -259,10 +284,17 @@ export const COMMANDS = [
259
284
  {
260
285
  name: "modules",
261
286
  summary: "List, read docs, or mutate modules (secret masks only)",
262
- usage: "frontpage modules [list|docs|enable|disable|archive|config] <key> [--instance <id>] [--config '{…}'] [--site <slug>]",
287
+ usage: "frontpage modules [list|docs|request-setup|enable|disable|archive|config] <key> [--instance <id>] [--config '{…}'] [--site <slug>]",
263
288
  topics: ["agent", "content"],
264
- examples: ["frontpage modules docs forms", "frontpage modules list"],
265
- dangers: ["Secret values are never returned — masks only"],
289
+ examples: [
290
+ "frontpage modules docs forms",
291
+ "frontpage modules list",
292
+ 'frontpage modules request-setup payments --reason "You asked for a $50 deposit button — Stripe setup makes it live"',
293
+ ],
294
+ dangers: [
295
+ "Secret values are never returned — masks only",
296
+ "request-setup surfaces an owner prompt in the editor status bar (CLI cannot show it)",
297
+ ],
266
298
  },
267
299
  {
268
300
  name: "extensions",
@@ -447,6 +479,8 @@ export const TOPICS = {
447
479
  See also: frontpage help sync | content | AGENTS.md`,
448
480
  agent: `Agent + publish
449
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://…
450
484
  frontpage runs # latest + active
451
485
  frontpage runs wait [runId]
452
486
  frontpage runs cancel <runId>
@@ -456,10 +490,13 @@ export const TOPICS = {
456
490
  frontpage unpublish --yes # admin only; never unless asked; --yes when non-TTY
457
491
  frontpage usage
458
492
  frontpage analytics [--days 7]
493
+ frontpage analytics summary|query|funnel|timeseries|compare|sql …
459
494
  frontpage pages
460
495
  frontpage modules
496
+ frontpage modules request-setup <key> --reason "…"
461
497
  frontpage extensions
462
498
 
499
+ --attach uploads local files via media (images/PDF/text) or accepts /__media/… / https URLs.
463
500
  Mid-task continuation is automatic — there is no runs continue.
464
501
  Do not push while an agent run is active.
465
502
  Default after work: review https://<subdomain>.frontpage.host; do not publish.`,
@@ -470,7 +507,7 @@ export const TOPICS = {
470
507
  frontpage articles list|content|create-section|create-article|set-draft|…
471
508
  frontpage fonts [list|search] [--slot …] [--query …]
472
509
  frontpage tokens set --group … --key … --value …
473
- frontpage modules list|docs|enable|disable|archive|config <key>
510
+ frontpage modules list|docs|request-setup|enable|disable|archive|config <key>
474
511
  frontpage extensions list|docs|enable|disable|config <key>
475
512
  frontpage settings get|set
476
513
  frontpage edit --path / --old "…" --new "…"
package/llms.txt CHANGED
@@ -8,6 +8,9 @@
8
8
  - Fonts: frontpage fonts [--slot] [--query] — use exact value; tokens: frontpage tokens set --group --key --value (CAS)
9
9
  - Module/extension docs: frontpage modules docs <key> | frontpage extensions docs <key>
10
10
  - Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
11
+ - Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it)
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)
11
14
  - Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
12
15
  - Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
13
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.5",
3
+ "version": "0.1.7",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {