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 +11 -1
- package/dist/commands/analytics.js +166 -8
- package/dist/commands/modules.js +29 -1
- package/dist/commands/prompt-attach.d.ts +59 -0
- package/dist/commands/prompt-attach.js +207 -0
- package/dist/commands/prompt.js +28 -18
- package/dist/completions/generate.js +9 -3
- package/dist/registry.js +47 -10
- package/llms.txt +3 -0
- package/package.json +1 -1
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
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
}
|
package/dist/commands/modules.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/prompt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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", {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: [
|
|
190
|
-
|
|
191
|
-
|
|
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: "
|
|
250
|
-
usage: "frontpage analytics [
|
|
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: [
|
|
265
|
-
|
|
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
|