makaron-persona-look-cli 0.4.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/LICENSE +1 -0
- package/README.md +84 -0
- package/bin/personlib.mjs +495 -0
- package/cloudflare-worker/README.md +37 -0
- package/cloudflare-worker/schema.sql +26 -0
- package/cloudflare-worker/src/worker.mjs +206 -0
- package/cloudflare-worker/test/worker.test.mjs +111 -0
- package/cloudflare-worker/wrangler.toml +17 -0
- package/lib/remote-client.mjs +254 -0
- package/package.json +31 -0
- package/skills/makaron-persona-look/SKILL.md +33 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
|
|
3
|
+
function json(payload, status = 200) {
|
|
4
|
+
return new Response(JSON.stringify(payload), { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" } });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function error(message, status = 400) {
|
|
8
|
+
return json({ error: message }, status);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function sha256(value) {
|
|
12
|
+
const input = typeof value === "string" ? encoder.encode(value) : value;
|
|
13
|
+
const hash = await crypto.subtle.digest("SHA-256", input);
|
|
14
|
+
return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function randomId(prefix) {
|
|
18
|
+
const bytes = crypto.getRandomValues(new Uint8Array(18));
|
|
19
|
+
return `${prefix}_${[...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bearer(request) {
|
|
23
|
+
const value = request.headers.get("authorization") || "";
|
|
24
|
+
return value.startsWith("Bearer ") ? value.slice(7) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function requireAgent(request, env) {
|
|
28
|
+
const token = bearer(request);
|
|
29
|
+
if (!token) return undefined;
|
|
30
|
+
const record = await env.DB.prepare("SELECT id, name FROM agents WHERE token_hash = ? AND revoked_at IS NULL").bind(await sha256(token)).first();
|
|
31
|
+
return record || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireOwner(request, env) {
|
|
35
|
+
const token = bearer(request);
|
|
36
|
+
return Boolean(token && env.OWNER_SYNC_TOKEN && token === env.OWNER_SYNC_TOKEN);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function openEnrollmentEnabled(env) {
|
|
40
|
+
return String(env.OPEN_ENROLLMENT || "").toLowerCase() === "true";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function publicPersona(record) {
|
|
44
|
+
return { id: record.id, display_name: record.display_name, status: record.status, identity: record.identity };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function publicLook(record) {
|
|
48
|
+
return { id: record.id, display_name: record.display_name, status: record.status, ...record.look, identity_policy: "persona-is-the-only-identity-reference", brand_policy: "original-unbranded-design-only" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scorePersona(record, brief) {
|
|
52
|
+
const query = brief.toLowerCase();
|
|
53
|
+
const identity = JSON.stringify(record.identity || {}).toLowerCase();
|
|
54
|
+
let score = record.identity?.presentation === "feminine adult" ? 2 : 0;
|
|
55
|
+
if (/干净|clean|beauty|美妆/.test(query)) for (const signal of ["fine", "soft", "natural", "even", "neutral", "low-contrast"]) if (identity.includes(signal)) score += 2;
|
|
56
|
+
if (/冷感|cool|冷调|高端|luxury/.test(query)) for (const signal of ["narrow", "long", "straight", "low-contrast", "calm", "slim"]) if (identity.includes(signal)) score += 2;
|
|
57
|
+
return score;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function scoreLook(record, brief) {
|
|
61
|
+
const query = brief.toLowerCase();
|
|
62
|
+
const text = JSON.stringify(record.look || {}).toLowerCase();
|
|
63
|
+
let score = 0;
|
|
64
|
+
const signals = (pattern, entries) => {
|
|
65
|
+
if (!pattern.test(query)) return;
|
|
66
|
+
for (const [signal, weight] of entries) if (text.includes(signal)) score += weight;
|
|
67
|
+
};
|
|
68
|
+
signals(/美妆|beauty|cosmetic|skincare|护肤/, [["beauty", 4], ["skincare", 4], ["serum", 3], ["fragrance", 2], ["treatment", 2]]);
|
|
69
|
+
signals(/高端|luxury|premium|prestige/, [["luxury", 3], ["premium", 3], ["prestige", 3], ["sculptural", 2], ["metallic", 2], ["gold", 2], ["silver", 2]]);
|
|
70
|
+
signals(/干净|clean|minimal/, [["clean", 3], ["white", 2], ["minimal", 2], ["bright", 1], ["soft", 1]]);
|
|
71
|
+
signals(/冷感|cool|冷调/, [["silver", 2], ["graphite", 2], ["grey", 2], ["icy", 2], ["black", 1], ["violet", 1]]);
|
|
72
|
+
signals(/y2k|千禧/, [["y2k", 6]]);
|
|
73
|
+
signals(/学院|校园|collegiate|academic/, [["collegiate", 5], ["academic", 5], ["campus", 3]]);
|
|
74
|
+
signals(/哥特|gothic|暗黑|grunge/, [["gothic", 4], ["grunge", 4], ["dark", 2]]);
|
|
75
|
+
signals(/街头|street/, [["street", 4], ["cargo", 2], ["denim", 1]]);
|
|
76
|
+
signals(/地铁|metro|subway|transit/, [["transit", 5], ["metro", 4], ["commuter", 3], ["urban", 1]]);
|
|
77
|
+
signals(/机车夹克|皮夹克|moto|leather jacket/, [["leather", 4], ["moto", 11], ["black", 1], ["grey", 1]]);
|
|
78
|
+
signals(/中性|男女同款|unisex|androgynous|gender[- ]?neutral/, [["unisex", 5], ["gender-neutral", 5], ["oversized", 2], ["cargo", 2], ["long short", 2], ["knit", 1]]);
|
|
79
|
+
signals(/嘻哈|说唱|hip[- ]?hop|rap(?:per)?/, [["hip-hop", 5], ["rap", 5], ["street", 3], ["baggy", 2], ["cargo", 2], ["jersey", 2], ["chain", 2], ["camo", 3], ["wide", 1]]);
|
|
80
|
+
signals(/度假|海岛|resort|beach|holiday|pool|yacht|sailing|夏日/, [["resort", 5], ["beach", 5], ["island", 4], ["pool", 4], ["sailing", 4], ["summer", 3], ["linen", 2], ["swim", 2], ["nautical", 2]]);
|
|
81
|
+
signals(/通勤|office|职场|workwear|商务|上班|commute/, [["office", 4], ["commute", 4], ["workwear", 4], ["tailored", 2], ["suit", 2], ["blazer", 2]]);
|
|
82
|
+
signals(/极简轻奢|quiet luxury|成熟通勤|mature commute/, [["quiet luxury", 6], ["mature commute", 6], ["trench", 3], ["tailored", 2], ["minimal", 2], ["wool", 1]]);
|
|
83
|
+
signals(/杂志|magazine|editorial|时尚大片/, [["magazine", 4], ["editorial", 4], ["studio", 1], ["tailored", 1]]);
|
|
84
|
+
signals(/晚宴|gala|black-tie|formal/, [["gala", 4], ["evening", 3], ["formal", 3], ["gown", 2], ["soiree", 2]]);
|
|
85
|
+
signals(/红毯|red carpet/, [["red carpet", 6], ["gala", 3], ["carpet", 2], ["train", 2], ["gown", 2]]);
|
|
86
|
+
signals(/高级派对|party|cocktail|after-party/, [["party", 4], ["cocktail", 3], ["soiree", 3], ["crystal", 1], ["sequin", 1], ["gown", 1]]);
|
|
87
|
+
signals(/运动休闲|athleisure|运动|sport|sportswear|健身|球类|篮球|网球|跑步|track|训练/, [["athleisure", 5], ["sport", 4], ["track", 3], ["running", 2], ["tennis", 2], ["basketball", 2], ["technical", 1], ["fleece", 1], ["hoodie", 1]]);
|
|
88
|
+
return score;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildRenderBrief(persona, look) {
|
|
92
|
+
const identity = persona.identity || {};
|
|
93
|
+
const lock = [identity.face_shape, identity.brows, identity.eyes, identity.nose, identity.mouth, identity.skin, identity.stable_marks, identity.body_evidence].filter(Boolean).join("; ");
|
|
94
|
+
return `Original ${identity.presentation || "adult"} character. Identity lock: ${lock}. Keep this Persona facial structure and stable marks; do not use the Look reference face. Look direction: ${look.family}. ${look.silhouette}. Garments: ${look.garments}. Palette: ${look.palette}. Materials: ${look.materials}. Accessories: ${look.accessories}. Scene: ${look.scene}. Replace source logos, watermarks, wordmarks, celebrity likenesses, and recognizable brand elements with original generic design.`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function records(env, table) {
|
|
98
|
+
const result = await env.DB.prepare(`SELECT record_json FROM ${table} ORDER BY id`).all();
|
|
99
|
+
return (result.results || []).map((row) => JSON.parse(row.record_json));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function select(env, { brief, personaId, lookId }) {
|
|
103
|
+
const personas = await records(env, "persona_records");
|
|
104
|
+
const looks = await records(env, "look_records");
|
|
105
|
+
const persona = personaId ? personas.find((record) => record.id === personaId) : personas.map((record) => ({ record, score: scorePersona(record, brief || "") })).sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id))[0]?.record;
|
|
106
|
+
if (!persona) throw new Error(personaId ? `persona not found: ${personaId}` : "no Persona records available");
|
|
107
|
+
let look;
|
|
108
|
+
let lookScore;
|
|
109
|
+
if (lookId) look = looks.find((record) => record.id === lookId);
|
|
110
|
+
else if (brief) {
|
|
111
|
+
const candidates = looks.map((record) => ({ record, score: scoreLook(record, brief) })).sort((a, b) => b.score - a.score || a.record.id.localeCompare(b.record.id));
|
|
112
|
+
look = candidates[0]?.record;
|
|
113
|
+
lookScore = candidates[0]?.score;
|
|
114
|
+
}
|
|
115
|
+
if (!look) throw new Error(lookId ? `look not found: ${lookId}` : "no catalog Look matched this brief");
|
|
116
|
+
if (brief && lookScore < 6) throw new Error("no catalog Look matched this brief");
|
|
117
|
+
return { persona, look, look_score: lookScore };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function route(request, env) {
|
|
121
|
+
const url = new URL(request.url);
|
|
122
|
+
if (request.method === "POST" && url.pathname === "/v1/agents/setup") {
|
|
123
|
+
const body = await request.json().catch(() => undefined);
|
|
124
|
+
const ownerControlledEnrollment = Boolean(body?.registration_token && env.AGENT_REGISTRATION_TOKEN && body.registration_token === env.AGENT_REGISTRATION_TOKEN);
|
|
125
|
+
if (!body?.agent_name || (!openEnrollmentEnabled(env) && !ownerControlledEnrollment)) return error("open enrollment is disabled", 401);
|
|
126
|
+
const agentId = randomId("agent");
|
|
127
|
+
const agentToken = randomId("plk");
|
|
128
|
+
await env.DB.prepare("INSERT INTO agents (id, name, token_hash, created_at) VALUES (?, ?, ?, ?)").bind(agentId, String(body.agent_name || "openclaw-agent").slice(0, 120), await sha256(agentToken), new Date().toISOString()).run();
|
|
129
|
+
return json({ agent_id: agentId, agent_token: agentToken, token_delivery: "shown-once-store-locally", enrollment: openEnrollmentEnabled(env) ? "open-self-registration" : "owner-controlled" }, 201);
|
|
130
|
+
}
|
|
131
|
+
if (url.pathname.startsWith("/v1/owner/")) {
|
|
132
|
+
if (!requireOwner(request, env)) return error("owner authorization required", 401);
|
|
133
|
+
if (request.method === "POST" && url.pathname === "/v1/owner/sync/manifest") {
|
|
134
|
+
const body = await request.json().catch(() => undefined);
|
|
135
|
+
if (!body?.manifest || !Array.isArray(body.assets)) return error("manifest and assets are required");
|
|
136
|
+
const personas = body.manifest.personas || [];
|
|
137
|
+
const looks = body.manifest.looks || [];
|
|
138
|
+
const statements = [
|
|
139
|
+
env.DB.prepare("DELETE FROM persona_records"), env.DB.prepare("DELETE FROM look_records"), env.DB.prepare("DELETE FROM assets")
|
|
140
|
+
];
|
|
141
|
+
for (const record of personas) statements.push(env.DB.prepare("INSERT INTO persona_records (id, display_name, record_json) VALUES (?, ?, ?)").bind(record.id, record.display_name, JSON.stringify(record)));
|
|
142
|
+
for (const record of looks) statements.push(env.DB.prepare("INSERT INTO look_records (id, display_name, record_json) VALUES (?, ?, ?)").bind(record.id, record.display_name, JSON.stringify(record)));
|
|
143
|
+
for (const asset of body.assets) statements.push(env.DB.prepare("INSERT INTO assets (id, object_key, sha256, mime_type) VALUES (?, ?, ?, ?)").bind(asset.id, asset.object_key, asset.sha256, asset.mime_type));
|
|
144
|
+
// Keep below conservative D1 batch-size limits; a repeated owner sync is
|
|
145
|
+
// idempotent and replaces the previous metadata before asset upload.
|
|
146
|
+
for (let index = 0; index < statements.length; index += 75) {
|
|
147
|
+
await env.DB.batch(statements.slice(index, index + 75));
|
|
148
|
+
}
|
|
149
|
+
return json({ ok: true, personas: personas.length, looks: looks.length, assets: body.assets.length });
|
|
150
|
+
}
|
|
151
|
+
const assetMatch = url.pathname.match(/^\/v1\/owner\/assets\/(.+)$/);
|
|
152
|
+
if (request.method === "PUT" && assetMatch) {
|
|
153
|
+
const assetId = decodeURIComponent(assetMatch[1]);
|
|
154
|
+
const asset = await env.DB.prepare("SELECT object_key, sha256, mime_type FROM assets WHERE id = ?").bind(assetId).first();
|
|
155
|
+
if (!asset) return error("asset is not declared in the current manifest", 404);
|
|
156
|
+
const bytes = await request.arrayBuffer();
|
|
157
|
+
const declaredHash = request.headers.get("x-personlib-sha256");
|
|
158
|
+
if (!declaredHash || declaredHash !== asset.sha256 || await sha256(bytes) !== asset.sha256) return error("asset checksum mismatch", 422);
|
|
159
|
+
await env.ASSETS.put(asset.object_key, bytes, { httpMetadata: { contentType: request.headers.get("content-type") || asset.mime_type } });
|
|
160
|
+
return json({ ok: true, asset_id: assetId });
|
|
161
|
+
}
|
|
162
|
+
return error("owner endpoint not found", 404);
|
|
163
|
+
}
|
|
164
|
+
const agent = await requireAgent(request, env);
|
|
165
|
+
if (!agent) return error("agent authorization required", 401);
|
|
166
|
+
if (request.method === "GET" && url.pathname === "/v1/doctor") {
|
|
167
|
+
const count = await env.DB.prepare("SELECT COUNT(*) AS count FROM look_records").first();
|
|
168
|
+
return json({ ok: true, service: "personlib-worker", version: "1", agent_id: agent.id, d1: "ok", r2_binding: Boolean(env.ASSETS), look_records: Number(count?.count || 0) });
|
|
169
|
+
}
|
|
170
|
+
if (request.method === "POST" && url.pathname === "/v1/recommend") {
|
|
171
|
+
const body = await request.json().catch(() => undefined);
|
|
172
|
+
if (!body?.brief) return error("brief is required");
|
|
173
|
+
try {
|
|
174
|
+
const selected = await select(env, { brief: String(body.brief) });
|
|
175
|
+
return json({ request: body.brief, persona: publicPersona(selected.persona), look: publicLook(selected.look), look_match: { source: "catalog-match", score: selected.look_score }, render_brief: buildRenderBrief(selected.persona, selected.look), source_face_policy: "persona-is-the-only-identity-reference" });
|
|
176
|
+
} catch (cause) { return error(cause.message, 404); }
|
|
177
|
+
}
|
|
178
|
+
if (request.method === "POST" && url.pathname === "/v1/compose") {
|
|
179
|
+
const body = await request.json().catch(() => undefined);
|
|
180
|
+
try {
|
|
181
|
+
const selected = await select(env, { personaId: body?.persona_id, lookId: body?.look_id });
|
|
182
|
+
return json({ composition: { persona: publicPersona(selected.persona), look: publicLook(selected.look), policies: { use_persona_as_only_identity_reference: true, look_source_face: "excluded", source_logos_and_wordmarks: "replace-with-original-generic-design" }, render_brief: buildRenderBrief(selected.persona, selected.look), render_status: "brief-only-no-generation-submitted" } });
|
|
183
|
+
} catch (cause) { return error(cause.message, 404); }
|
|
184
|
+
}
|
|
185
|
+
if (request.method === "POST" && url.pathname === "/v1/preview") {
|
|
186
|
+
const body = await request.json().catch(() => undefined);
|
|
187
|
+
try {
|
|
188
|
+
const selected = await select(env, body?.brief ? { brief: String(body.brief) } : { personaId: body?.persona_id, lookId: body?.look_id });
|
|
189
|
+
const assets = [{ role: "persona", asset_id: selected.persona.source_asset_id }];
|
|
190
|
+
for (const reference of selected.persona.identity_references || []) assets.push({ role: "persona-reference", index: reference.index, asset_id: reference.asset_id });
|
|
191
|
+
assets.push({ role: "look", asset_id: selected.look.source_asset_id });
|
|
192
|
+
return json({ kind: "persona-look-reference-pack", image_status: "source-reference-pack-not-a-generated-composite", persona: publicPersona(selected.persona), look: publicLook(selected.look), render_brief: buildRenderBrief(selected.persona, selected.look), source_face_policy: "persona-is-the-only-identity-reference", assets: assets.map((asset) => ({ ...asset, url: `/v1/assets/${encodeURIComponent(asset.asset_id)}` })) });
|
|
193
|
+
} catch (cause) { return error(cause.message, 404); }
|
|
194
|
+
}
|
|
195
|
+
const assetMatch = url.pathname.match(/^\/v1\/assets\/(.+)$/);
|
|
196
|
+
if (request.method === "GET" && assetMatch) {
|
|
197
|
+
const asset = await env.DB.prepare("SELECT object_key, mime_type FROM assets WHERE id = ?").bind(decodeURIComponent(assetMatch[1])).first();
|
|
198
|
+
if (!asset) return error("asset not found", 404);
|
|
199
|
+
const object = await env.ASSETS.get(asset.object_key);
|
|
200
|
+
if (!object) return error("asset bytes are not synced", 404);
|
|
201
|
+
return new Response(object.body, { headers: { "content-type": object.httpMetadata?.contentType || asset.mime_type, "cache-control": "private, no-store" } });
|
|
202
|
+
}
|
|
203
|
+
return error("not found", 404);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default { fetch: route };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import worker from "../src/worker.mjs";
|
|
4
|
+
|
|
5
|
+
class Statement {
|
|
6
|
+
constructor(db, sql) { this.db = db; this.sql = sql; this.values = []; }
|
|
7
|
+
bind(...values) { this.values = values; return this; }
|
|
8
|
+
async first() { return this.db.execute(this.sql, this.values, "first"); }
|
|
9
|
+
async all() { return this.db.execute(this.sql, this.values, "all"); }
|
|
10
|
+
async run() { return this.db.execute(this.sql, this.values, "run"); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class FakeDB {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.agents = [];
|
|
16
|
+
this.personas = new Map();
|
|
17
|
+
this.looks = new Map();
|
|
18
|
+
this.assets = new Map();
|
|
19
|
+
}
|
|
20
|
+
prepare(sql) { return new Statement(this, sql); }
|
|
21
|
+
async batch(statements) { for (const statement of statements) await statement.run(); }
|
|
22
|
+
execute(sql, values, mode) {
|
|
23
|
+
const normalized = sql.replace(/\s+/g, " ").trim();
|
|
24
|
+
if (normalized.startsWith("INSERT INTO agents")) {
|
|
25
|
+
this.agents.push({ id: values[0], name: values[1], token_hash: values[2], created_at: values[3], revoked_at: null });
|
|
26
|
+
return { success: true };
|
|
27
|
+
}
|
|
28
|
+
if (normalized.startsWith("SELECT id, name FROM agents")) return this.agents.find((agent) => agent.token_hash === values[0] && !agent.revoked_at) || null;
|
|
29
|
+
if (normalized.startsWith("DELETE FROM persona_records")) { this.personas.clear(); return { success: true }; }
|
|
30
|
+
if (normalized.startsWith("DELETE FROM look_records")) { this.looks.clear(); return { success: true }; }
|
|
31
|
+
if (normalized.startsWith("DELETE FROM assets")) { this.assets.clear(); return { success: true }; }
|
|
32
|
+
if (normalized.startsWith("INSERT INTO persona_records")) { this.personas.set(values[0], { id: values[0], display_name: values[1], record_json: values[2] }); return { success: true }; }
|
|
33
|
+
if (normalized.startsWith("INSERT INTO look_records")) { this.looks.set(values[0], { id: values[0], display_name: values[1], record_json: values[2] }); return { success: true }; }
|
|
34
|
+
if (normalized.startsWith("INSERT INTO assets")) { this.assets.set(values[0], { id: values[0], object_key: values[1], sha256: values[2], mime_type: values[3] }); return { success: true }; }
|
|
35
|
+
if (normalized.startsWith("SELECT COUNT(*) AS count FROM look_records")) return { count: this.looks.size };
|
|
36
|
+
if (normalized.startsWith("SELECT record_json FROM persona_records")) return { results: [...this.personas.values()].sort((a, b) => a.id.localeCompare(b.id)).map(({ record_json }) => ({ record_json })) };
|
|
37
|
+
if (normalized.startsWith("SELECT record_json FROM look_records")) return { results: [...this.looks.values()].sort((a, b) => a.id.localeCompare(b.id)).map(({ record_json }) => ({ record_json })) };
|
|
38
|
+
if (normalized.startsWith("SELECT object_key, sha256, mime_type FROM assets")) {
|
|
39
|
+
const asset = this.assets.get(values[0]);
|
|
40
|
+
return asset ? { object_key: asset.object_key, sha256: asset.sha256, mime_type: asset.mime_type } : null;
|
|
41
|
+
}
|
|
42
|
+
if (normalized.startsWith("SELECT object_key, mime_type FROM assets")) {
|
|
43
|
+
const asset = this.assets.get(values[0]);
|
|
44
|
+
return asset ? { object_key: asset.object_key, mime_type: asset.mime_type } : null;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`unsupported fake SQL: ${normalized}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class FakeBucket {
|
|
51
|
+
constructor() { this.objects = new Map(); }
|
|
52
|
+
async put(key, bytes, options) { this.objects.set(key, { bytes: new Uint8Array(bytes), httpMetadata: options.httpMetadata }); }
|
|
53
|
+
async get(key) {
|
|
54
|
+
const object = this.objects.get(key);
|
|
55
|
+
return object && { body: new Blob([object.bytes]).stream(), httpMetadata: object.httpMetadata };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function digest(bytes) {
|
|
60
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
61
|
+
return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function request(path, options = {}) {
|
|
65
|
+
return new Request(`https://personlib.example${path}`, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("owner sync, self-registration, recommendation, compose, and private preview work together", async () => {
|
|
69
|
+
const env = { DB: new FakeDB(), ASSETS: new FakeBucket(), OWNER_SYNC_TOKEN: "owner-token", OPEN_ENROLLMENT: "true" };
|
|
70
|
+
const personaBytes = new TextEncoder().encode("persona-image");
|
|
71
|
+
const lookBytes = new TextEncoder().encode("look-image");
|
|
72
|
+
const personaHash = await digest(personaBytes);
|
|
73
|
+
const lookHash = await digest(lookBytes);
|
|
74
|
+
const manifest = {
|
|
75
|
+
personas: [{ id: "P-001", display_name: "Adult One", status: "private-staged", identity: { presentation: "feminine adult", face_shape: "oval", brows: "straight", eyes: "almond", nose: "straight", mouth: "soft", skin: "natural", stable_marks: "none" }, source_asset_id: "P-001:source", identity_references: [] }],
|
|
76
|
+
looks: [{ id: "L-132", display_name: "Metro Leather Transit", status: "private-staged", look: { family: "urban transit", silhouette: "moto", garments: "leather jacket", palette: "black grey", materials: "leather", accessories: "minimal", scene: "metro commuter" }, source_asset_id: "L-132:source" }]
|
|
77
|
+
};
|
|
78
|
+
const assets = [
|
|
79
|
+
{ id: "P-001:source", object_key: "personas/P-001/source.png", sha256: personaHash, mime_type: "image/png" },
|
|
80
|
+
{ id: "L-132:source", object_key: "looks/L-132/source.png", sha256: lookHash, mime_type: "image/png" }
|
|
81
|
+
];
|
|
82
|
+
let response = await worker.fetch(request("/v1/owner/sync/manifest", { method: "POST", headers: { authorization: "Bearer owner-token", "content-type": "application/json" }, body: JSON.stringify({ manifest, assets }) }), env);
|
|
83
|
+
assert.equal(response.status, 200);
|
|
84
|
+
for (const [asset, bytes] of [[assets[0], personaBytes], [assets[1], lookBytes]]) {
|
|
85
|
+
response = await worker.fetch(request(`/v1/owner/assets/${encodeURIComponent(asset.id)}`, { method: "PUT", headers: { authorization: "Bearer owner-token", "content-type": asset.mime_type, "x-personlib-sha256": asset.sha256 }, body: bytes }), env);
|
|
86
|
+
assert.equal(response.status, 200);
|
|
87
|
+
}
|
|
88
|
+
response = await worker.fetch(request("/v1/agents/setup", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ agent_name: "xiaolongxia" }) }), env);
|
|
89
|
+
assert.equal(response.status, 201);
|
|
90
|
+
const agent = await response.json();
|
|
91
|
+
assert.match(agent.agent_token, /^plk_/);
|
|
92
|
+
assert.equal(agent.enrollment, "open-self-registration");
|
|
93
|
+
const headers = { authorization: `Bearer ${agent.agent_token}`, "content-type": "application/json" };
|
|
94
|
+
response = await worker.fetch(request("/v1/doctor", { headers }), env);
|
|
95
|
+
assert.deepEqual(await response.json(), { ok: true, service: "personlib-worker", version: "1", agent_id: agent.agent_id, d1: "ok", r2_binding: true, look_records: 1 });
|
|
96
|
+
response = await worker.fetch(request("/v1/recommend", { method: "POST", headers, body: JSON.stringify({ brief: "15 秒成年女性地铁皮夹克通勤广告" }) }), env);
|
|
97
|
+
const recommendation = await response.json();
|
|
98
|
+
assert.equal(response.status, 200);
|
|
99
|
+
assert.equal(recommendation.persona.id, "P-001");
|
|
100
|
+
assert.equal(recommendation.look.id, "L-132");
|
|
101
|
+
assert.match(recommendation.render_brief, /do not use the Look reference face/i);
|
|
102
|
+
response = await worker.fetch(request("/v1/compose", { method: "POST", headers, body: JSON.stringify({ persona_id: "P-001", look_id: "L-132" }) }), env);
|
|
103
|
+
assert.equal((await response.json()).composition.policies.look_source_face, "excluded");
|
|
104
|
+
response = await worker.fetch(request("/v1/preview", { method: "POST", headers, body: JSON.stringify({ persona_id: "P-001", look_id: "L-132" }) }), env);
|
|
105
|
+
const preview = await response.json();
|
|
106
|
+
assert.equal(preview.image_status, "source-reference-pack-not-a-generated-composite");
|
|
107
|
+
assert.equal(preview.assets.length, 2);
|
|
108
|
+
response = await worker.fetch(request(preview.assets[0].url, { headers: { authorization: `Bearer ${agent.agent_token}` } }), env);
|
|
109
|
+
assert.equal(response.status, 200);
|
|
110
|
+
assert.deepEqual(new Uint8Array(await response.arrayBuffer()), personaBytes);
|
|
111
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name = "personlib-agent"
|
|
2
|
+
main = "src/worker.mjs"
|
|
3
|
+
compatibility_date = "2026-09-04"
|
|
4
|
+
|
|
5
|
+
[vars]
|
|
6
|
+
# The owner requested that any Agent can self-register. Set false to require
|
|
7
|
+
# AGENT_REGISTRATION_TOKEN as a Worker secret instead.
|
|
8
|
+
OPEN_ENROLLMENT = "true"
|
|
9
|
+
|
|
10
|
+
[[d1_databases]]
|
|
11
|
+
binding = "DB"
|
|
12
|
+
database_name = "personlib"
|
|
13
|
+
database_id = "41229d0a-101d-4361-a69d-b6f29e77ef03"
|
|
14
|
+
|
|
15
|
+
[[r2_buckets]]
|
|
16
|
+
binding = "ASSETS"
|
|
17
|
+
bucket_name = "personlib-assets"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const CONFIG_VERSION = 1;
|
|
8
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
|
+
const packageMetadata = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
10
|
+
const DEFAULT_API_URL = process.env.PERSONLIB_DEFAULT_API_URL || packageMetadata.personlib?.default_api_url || "";
|
|
11
|
+
|
|
12
|
+
function getOption(args, name) {
|
|
13
|
+
const index = args.indexOf(name);
|
|
14
|
+
return index === -1 ? undefined : args[index + 1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function has(args, name) {
|
|
18
|
+
return args.includes(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function configPath(args) {
|
|
22
|
+
return resolve(getOption(args, "--config") || process.env.PERSONLIB_REMOTE_CONFIG || join(process.env.HOME || ".", ".config", "personlib", "remote.json"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeConfig(path, config) {
|
|
26
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
28
|
+
chmodSync(path, 0o600);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readConfig(path) {
|
|
32
|
+
if (!existsSync(path)) throw new Error(`remote configuration not found: ${path}; run personlib remote setup first`);
|
|
33
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
34
|
+
if (config.schema_version !== CONFIG_VERSION || !config.endpoint || !config.agent_token) {
|
|
35
|
+
throw new Error(`invalid remote configuration: ${path}`);
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cleanEndpoint(value) {
|
|
41
|
+
if (!value) throw new Error("remote endpoint is not configured; pass --api-url https://your-worker.example during bootstrap or publish a package with personlib.default_api_url");
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value);
|
|
44
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
45
|
+
throw new Error("remote endpoint must use https");
|
|
46
|
+
}
|
|
47
|
+
return url.toString().replace(/\/$/, "");
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`invalid remote endpoint: ${value}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function request(endpoint, path, { token, method = "GET", json, body, headers = {} } = {}) {
|
|
54
|
+
const response = await fetch(`${endpoint}${path}`, {
|
|
55
|
+
method,
|
|
56
|
+
headers: {
|
|
57
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
58
|
+
...(json !== undefined ? { "content-type": "application/json" } : {}),
|
|
59
|
+
...headers
|
|
60
|
+
},
|
|
61
|
+
body: json !== undefined ? JSON.stringify(json) : body
|
|
62
|
+
});
|
|
63
|
+
const type = response.headers.get("content-type") || "";
|
|
64
|
+
const payload = type.includes("application/json") ? await response.json() : await response.text();
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const message = typeof payload === "object" && payload?.error ? payload.error : String(payload || `${response.status} ${response.statusText}`);
|
|
67
|
+
throw new Error(`remote request failed (${response.status}): ${message}`);
|
|
68
|
+
}
|
|
69
|
+
return { response, payload };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sha256(path) {
|
|
73
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mimeType(path) {
|
|
77
|
+
const extension = extname(path).toLowerCase();
|
|
78
|
+
return { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" }[extension] || "application/octet-stream";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sanitizedRemoteManifest(localManifest, root) {
|
|
82
|
+
const assets = [];
|
|
83
|
+
const asset = (id, source, objectKey) => {
|
|
84
|
+
const absolute = join(root, source.relative_path);
|
|
85
|
+
assets.push({
|
|
86
|
+
id,
|
|
87
|
+
object_key: objectKey,
|
|
88
|
+
sha256: source.sha256 || sha256(absolute),
|
|
89
|
+
mime_type: mimeType(absolute),
|
|
90
|
+
local_path: absolute
|
|
91
|
+
});
|
|
92
|
+
return id;
|
|
93
|
+
};
|
|
94
|
+
const personas = (localManifest.personas || []).filter((record) => record.status === "private-staged").map((record) => {
|
|
95
|
+
const sourceAssetId = asset(`${record.id}:source`, record.source, `personas/${record.id}/source${extname(record.source.relative_path)}`);
|
|
96
|
+
const identityReferences = (record.identity_references || []).map((reference, index) => ({
|
|
97
|
+
asset_id: asset(`${record.id}:reference:${index + 1}`, reference, `personas/${record.id}/reference-${index + 1}${extname(reference.relative_path)}`),
|
|
98
|
+
index: index + 1
|
|
99
|
+
}));
|
|
100
|
+
return {
|
|
101
|
+
id: record.id,
|
|
102
|
+
display_name: record.display_name,
|
|
103
|
+
status: record.status,
|
|
104
|
+
identity: record.identity,
|
|
105
|
+
source_asset_id: sourceAssetId,
|
|
106
|
+
identity_references: identityReferences
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const looks = (localManifest.looks || []).filter((record) => record.status === "private-staged").map((record) => ({
|
|
110
|
+
id: record.id,
|
|
111
|
+
display_name: record.display_name,
|
|
112
|
+
status: record.status,
|
|
113
|
+
look: record.look,
|
|
114
|
+
source_asset_id: asset(`${record.id}:source`, record.source, `looks/${record.id}/source${extname(record.source.relative_path)}`),
|
|
115
|
+
identity_policy: "persona-is-the-only-identity-reference",
|
|
116
|
+
brand_policy: "original-unbranded-design-only"
|
|
117
|
+
}));
|
|
118
|
+
return { schema_version: "1", personas, looks, assets };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function jsonOutput(payload, output, args) {
|
|
122
|
+
output(payload, has(args, "--json"));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function endpointForSetup(args) {
|
|
126
|
+
return cleanEndpoint(getOption(args, "--api-url") || getOption(args, "--endpoint") || process.env.PERSONLIB_API_URL || DEFAULT_API_URL);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function enroll(args) {
|
|
130
|
+
const endpoint = endpointForSetup(args);
|
|
131
|
+
const agentName = getOption(args, "--agent-name") || process.env.HOSTNAME || "openclaw-agent";
|
|
132
|
+
const legacyRegistrationToken = getOption(args, "--registration-token") || process.env.PERSONLIB_REGISTRATION_TOKEN;
|
|
133
|
+
const { payload } = await request(endpoint, "/v1/agents/setup", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
json: { agent_name: agentName, ...(legacyRegistrationToken ? { registration_token: legacyRegistrationToken } : {}) }
|
|
136
|
+
});
|
|
137
|
+
if (!payload?.agent_token || !payload?.agent_id) throw new Error("remote setup response did not contain an agent credential");
|
|
138
|
+
const path = configPath(args);
|
|
139
|
+
writeConfig(path, { schema_version: CONFIG_VERSION, endpoint, agent_id: payload.agent_id, agent_token: payload.agent_token, created_at: new Date().toISOString() });
|
|
140
|
+
return { endpoint, agent_id: payload.agent_id, config_path: path, credential: "stored-locally-not-printed", enrollment: payload.enrollment || "open-or-owner-controlled" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function setup(args, output) {
|
|
144
|
+
jsonOutput({ ok: true, remote: await enroll(args) }, output, args);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function installStep(command, args, label) {
|
|
148
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout: 180000, maxBuffer: 2 * 1024 * 1024 });
|
|
149
|
+
if (result.error || result.status !== 0) {
|
|
150
|
+
const detail = String(result.stderr || result.stdout || result.error?.message || "").slice(-800);
|
|
151
|
+
throw new Error(`${label} failed${detail ? `: ${detail}` : ""}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function runBootstrapSetup(args, { output }) {
|
|
156
|
+
const endpoint = endpointForSetup(args);
|
|
157
|
+
const packageSpec = `${packageMetadata.name}@${packageMetadata.version}`;
|
|
158
|
+
const skillDir = join(packageRoot, "skills", "makaron-persona-look");
|
|
159
|
+
const plan = {
|
|
160
|
+
global_install: ["npm", "install", "-g", packageSpec],
|
|
161
|
+
skill_install: ["npx", "-y", "skills", "add", skillDir, "--skill", "makaron-persona-look", "--copy", "--global", "--yes"],
|
|
162
|
+
enrollment: { endpoint, mode: "open-self-registration", credential: "stored-locally-not-printed" }
|
|
163
|
+
};
|
|
164
|
+
if (has(args, "--dry-run")) return jsonOutput({ ok: true, dry_run: true, setup: plan }, output, args);
|
|
165
|
+
if (!has(args, "--skip-global-install")) installStep("npm", plan.global_install.slice(1), "global CLI installation");
|
|
166
|
+
if (!has(args, "--skip-skill-install")) installStep("npx", plan.skill_install.slice(1), "Agent Skill installation");
|
|
167
|
+
const remote = await enroll(args);
|
|
168
|
+
jsonOutput({ ok: true, setup: { global_install: has(args, "--skip-global-install") ? "skipped" : "installed", skill_install: has(args, "--skip-skill-install") ? "skipped" : "installed", remote } }, output, args);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function agentConnection(args) {
|
|
172
|
+
const config = readConfig(configPath(args));
|
|
173
|
+
return { endpoint: cleanEndpoint(config.endpoint), token: config.agent_token, config };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function authenticated(args, output, path, json) {
|
|
177
|
+
const remote = agentConnection(args);
|
|
178
|
+
const { payload } = await request(remote.endpoint, path, { token: remote.token, method: json === undefined ? "GET" : "POST", json });
|
|
179
|
+
jsonOutput({ ok: true, remote: payload }, output, args);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function preview(args, output) {
|
|
183
|
+
const remote = agentConnection(args);
|
|
184
|
+
const brief = getOption(args, "--brief");
|
|
185
|
+
const personaId = getOption(args, "--persona");
|
|
186
|
+
const lookId = getOption(args, "--look");
|
|
187
|
+
if (!brief && !(personaId && lookId)) throw new Error("usage: personlib remote preview --brief TEXT | --persona P-001 --look L-001 [--output-dir PATH]");
|
|
188
|
+
const { payload } = await request(remote.endpoint, "/v1/preview", { token: remote.token, method: "POST", json: brief ? { brief } : { persona_id: personaId, look_id: lookId } });
|
|
189
|
+
const destination = getOption(args, "--output-dir");
|
|
190
|
+
if (!destination) {
|
|
191
|
+
jsonOutput({ ok: true, remote: payload }, output, args);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const absoluteDestination = resolve(destination);
|
|
195
|
+
mkdirSync(absoluteDestination, { recursive: true });
|
|
196
|
+
const downloaded = [];
|
|
197
|
+
for (const asset of payload.assets || []) {
|
|
198
|
+
const response = await fetch(`${remote.endpoint}${asset.url}`, { headers: { authorization: `Bearer ${remote.token}` } });
|
|
199
|
+
if (!response.ok) throw new Error(`remote asset download failed (${response.status}): ${asset.asset_id}`);
|
|
200
|
+
const extension = { "image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp" }[response.headers.get("content-type")] || ".bin";
|
|
201
|
+
const name = `${asset.role}${asset.index ? `-${asset.index}` : ""}${extension}`;
|
|
202
|
+
const target = join(absoluteDestination, name);
|
|
203
|
+
if (existsSync(target) && !has(args, "--overwrite")) throw new Error(`refusing to overwrite ${target}; pass --overwrite to replace it`);
|
|
204
|
+
writeFileSync(target, Buffer.from(await response.arrayBuffer()));
|
|
205
|
+
downloaded.push({ role: asset.role, filename: name });
|
|
206
|
+
}
|
|
207
|
+
writeFileSync(join(absoluteDestination, "selection.json"), `${JSON.stringify({ ...payload, assets: downloaded }, null, 2)}\n`, "utf8");
|
|
208
|
+
jsonOutput({ ok: true, retrieval: { kind: "persona-look-reference-pack", image_status: "source-reference-pack-not-a-generated-composite", output_dir: absoluteDestination, files: [...downloaded, { role: "selection", filename: "selection.json" }] } }, output, args);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function sync(args, output, { readManifest }) {
|
|
212
|
+
const root = resolve(getOption(args, "--library") || process.env.PERSONLIB_LIBRARY || "private-library");
|
|
213
|
+
const manifest = readManifest(root);
|
|
214
|
+
const payload = sanitizedRemoteManifest(manifest, root);
|
|
215
|
+
if (has(args, "--dry-run")) {
|
|
216
|
+
jsonOutput({ ok: true, dry_run: true, records: { personas: payload.personas.length, looks: payload.looks.length }, assets: { count: payload.assets.length, first_asset_id: payload.assets[0]?.id, last_asset_id: payload.assets.at(-1)?.id } }, output, args);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const endpoint = cleanEndpoint(getOption(args, "--endpoint") || process.env.PERSONLIB_REMOTE_ENDPOINT);
|
|
220
|
+
const ownerToken = process.env.PERSONLIB_OWNER_SYNC_TOKEN;
|
|
221
|
+
if (!ownerToken) throw new Error("PERSONLIB_OWNER_SYNC_TOKEN is required for owner sync");
|
|
222
|
+
await request(endpoint, "/v1/owner/sync/manifest", { token: ownerToken, method: "POST", json: { manifest: { schema_version: payload.schema_version, personas: payload.personas, looks: payload.looks }, assets: payload.assets.map(({ local_path, ...asset }) => asset) } });
|
|
223
|
+
for (const asset of payload.assets) {
|
|
224
|
+
await request(endpoint, `/v1/owner/assets/${encodeURIComponent(asset.id)}`, {
|
|
225
|
+
token: ownerToken,
|
|
226
|
+
method: "PUT",
|
|
227
|
+
body: readFileSync(asset.local_path),
|
|
228
|
+
headers: { "content-type": asset.mime_type, "x-personlib-sha256": asset.sha256 }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
jsonOutput({ ok: true, synced: { endpoint, personas: payload.personas.length, looks: payload.looks.length, assets: payload.assets.length } }, output, args);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function runRemote(args, { output, readManifest }) {
|
|
235
|
+
const subcommand = args[0];
|
|
236
|
+
if (subcommand === "setup") return setup(args.slice(1), output);
|
|
237
|
+
if (subcommand === "doctor") return authenticated(args.slice(1), output, "/v1/doctor");
|
|
238
|
+
if (subcommand === "recommend") {
|
|
239
|
+
const childArgs = args.slice(1);
|
|
240
|
+
const brief = getOption(childArgs, "--brief");
|
|
241
|
+
if (!brief) throw new Error("usage: personlib remote recommend --brief TEXT");
|
|
242
|
+
return authenticated(childArgs, output, "/v1/recommend", { brief });
|
|
243
|
+
}
|
|
244
|
+
if (subcommand === "compose") {
|
|
245
|
+
const childArgs = args.slice(1);
|
|
246
|
+
const personaId = getOption(childArgs, "--persona");
|
|
247
|
+
const lookId = getOption(childArgs, "--look");
|
|
248
|
+
if (!personaId || !lookId) throw new Error("usage: personlib remote compose --persona P-001 --look L-001");
|
|
249
|
+
return authenticated(childArgs, output, "/v1/compose", { persona_id: personaId, look_id: lookId });
|
|
250
|
+
}
|
|
251
|
+
if (subcommand === "preview") return preview(args.slice(1), output);
|
|
252
|
+
if (subcommand === "sync") return sync(args.slice(1), output, { readManifest });
|
|
253
|
+
throw new Error("usage: personlib remote <setup|doctor|recommend|compose|preview|sync>");
|
|
254
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "makaron-persona-look-cli",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Portable Persona and Look library for Makaron agents",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"personlib": "bin/personlib.mjs",
|
|
9
|
+
"makaron-persona-look-cli": "bin/personlib.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"lib",
|
|
14
|
+
"cloudflare-worker",
|
|
15
|
+
"skills",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test/smoke.mjs",
|
|
21
|
+
"test:remote": "node --test cloudflare-worker/test/worker.test.mjs test/remote-client.test.mjs",
|
|
22
|
+
"prepublishOnly": "node scripts/assert-release.mjs"
|
|
23
|
+
},
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"personlib": {
|
|
26
|
+
"default_api_url": "https://personlib-agent.bzz0309.workers.dev"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
}
|
|
31
|
+
}
|