appostle-installer 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/appostle.js CHANGED
@@ -2409,6 +2409,13 @@ var ChatRoomSchema = z4.object({
2409
2409
  id: z4.string(),
2410
2410
  name: z4.string(),
2411
2411
  purpose: z4.string().nullable(),
2412
+ /**
2413
+ * Auth-server user-id of the user who created the room. `null` = legacy
2414
+ * room (created before multi-tenant scoping) — visible to all users on
2415
+ * the daemon. New rooms get the creator's userId and are scoped to that
2416
+ * user only. See gitlab issue #25.
2417
+ */
2418
+ ownerUserId: z4.string().nullable().default(null),
2412
2419
  createdAt: z4.string(),
2413
2420
  updatedAt: z4.string()
2414
2421
  });
@@ -3196,6 +3203,7 @@ var RoleMoveResponseSchema = z10.object({
3196
3203
  var BrandScopeSchema = z10.literal("project");
3197
3204
  var BrandVariableTypeSchema = z10.enum([
3198
3205
  "color",
3206
+ "gradient",
3199
3207
  "font",
3200
3208
  "asset",
3201
3209
  "text",
@@ -35104,6 +35112,7 @@ ${original}`;
35104
35112
  // ../server/src/server/brand/token-generator.ts
35105
35113
  import { z as z37 } from "zod";
35106
35114
  var HEX6 = /^#[0-9a-f]{6}$/i;
35115
+ var CSS_GRADIENT = /^(linear|radial|conic)-gradient\(/i;
35107
35116
  var TokensResponseSchema = z37.object({
35108
35117
  tokens: z37.record(z37.string(), z37.string())
35109
35118
  });
@@ -35179,6 +35188,31 @@ function buildPrompt2(args) {
35179
35188
  }
35180
35189
  }
35181
35190
  const tokensToFill = args.unlockedTokens.map((v) => `- ${v.key}`).join("\n");
35191
+ const gradientSection = args.unlockedGradients.length > 0 ? [
35192
+ "",
35193
+ "Gradient tokens to fill (values must be valid CSS gradient strings,",
35194
+ "e.g. linear-gradient(135deg, #c52669 0%, #ff6b35 100%)):",
35195
+ ...args.unlockedGradients.map((v) => `- ${v.key}`),
35196
+ "",
35197
+ "Gradient key glossary:",
35198
+ "- gradient.primary \u2014 the brand's signature gradient, built from the",
35199
+ " most prominent palette colours. Used for hero backgrounds, feature",
35200
+ " highlights, and primary decorative surfaces.",
35201
+ "- gradient.accent \u2014 a secondary gradient for interactive states,",
35202
+ " hover effects, or accent surfaces. Distinct from primary but",
35203
+ " harmonious with the palette.",
35204
+ "- gradient.subtle \u2014 a very soft, low-contrast gradient for subtle",
35205
+ " background washes \u2014 card backgrounds, section tints. Should feel",
35206
+ " almost invisible; same hue family as bg-base with minimal shift.",
35207
+ "",
35208
+ "Include gradient values in the same JSON under the 'tokens' key,",
35209
+ "alongside the hex tokens. Gradient values are CSS gradient strings."
35210
+ ].join("\n") : "";
35211
+ const lockedGradientSection = args.lockedGradients.length > 0 ? [
35212
+ "",
35213
+ "Locked gradients \u2014 DO NOT include these in your output:",
35214
+ ...args.lockedGradients.map((v) => `- ${v.key}: ${v.value}`)
35215
+ ].join("\n") : "";
35182
35216
  const lockedSection = args.lockedTokens.length > 0 ? [
35183
35217
  "",
35184
35218
  "Locked tokens \u2014 DO NOT include these in your output. The user has",
@@ -35225,13 +35259,15 @@ function buildPrompt2(args) {
35225
35259
  "Tokens to fill (every value must be a #RRGGBB lowercase hex string):",
35226
35260
  tokensToFill,
35227
35261
  lockedSection,
35262
+ gradientSection,
35263
+ lockedGradientSection,
35228
35264
  directionSection,
35229
35265
  "",
35230
35266
  "Return JSON only with the shape:",
35231
- '{ "tokens": { "<token-key>": "#rrggbb", ... } }',
35267
+ '{ "tokens": { "<token-key>": "#rrggbb or css-gradient-string", ... } }',
35232
35268
  "",
35233
- "Output ONLY the unlocked tokens listed above. Do not include the locked",
35234
- "tokens, do not invent new keys, do not output anything outside the JSON.",
35269
+ "Output ONLY the unlocked tokens and gradients listed above. Do not include",
35270
+ "locked entries, do not invent new keys, do not output anything outside the JSON.",
35235
35271
  "Never use a hex value from a colour marked OFF."
35236
35272
  ].filter((line) => line !== "").join("\n");
35237
35273
  }
@@ -35245,9 +35281,12 @@ async function generateAndApplyBrandTokens(options) {
35245
35281
  const allVars = colorsBrand.variables;
35246
35282
  const paletteVars = allVars.filter((v) => v.key.startsWith("color."));
35247
35283
  const tokenVars = allVars.filter((v) => v.key.startsWith("token."));
35284
+ const gradientVars = allVars.filter((v) => v.type === "gradient");
35248
35285
  const unlockedTokens = tokenVars.filter((v) => !v.locked);
35249
35286
  const lockedTokens = tokenVars.filter((v) => v.locked);
35250
- if (unlockedTokens.length === 0) {
35287
+ const unlockedGradients = gradientVars.filter((v) => !v.locked);
35288
+ const lockedGradients = gradientVars.filter((v) => v.locked);
35289
+ if (unlockedTokens.length === 0 && unlockedGradients.length === 0) {
35251
35290
  logger.info({ brandPath }, "brand-tokens: nothing to generate (all tokens locked)");
35252
35291
  return { generatedCount: 0 };
35253
35292
  }
@@ -35258,6 +35297,8 @@ async function generateAndApplyBrandTokens(options) {
35258
35297
  paletteVars,
35259
35298
  unlockedTokens,
35260
35299
  lockedTokens,
35300
+ unlockedGradients,
35301
+ lockedGradients,
35261
35302
  mode,
35262
35303
  userPrompt: prompt
35263
35304
  });
@@ -35283,14 +35324,19 @@ async function generateAndApplyBrandTokens(options) {
35283
35324
  }
35284
35325
  throw error;
35285
35326
  }
35286
- const unlockedKeySet = new Set(unlockedTokens.map((v) => v.key));
35327
+ const unlockedTokenKeySet = new Set(unlockedTokens.map((v) => v.key));
35328
+ const unlockedGradientKeySet = new Set(unlockedGradients.map((v) => v.key));
35287
35329
  const acceptedUpdates = /* @__PURE__ */ new Map();
35288
35330
  for (const [key, value] of Object.entries(response.tokens)) {
35289
- if (!unlockedKeySet.has(key)) continue;
35290
35331
  if (typeof value !== "string") continue;
35291
35332
  const trimmed = value.trim();
35292
- if (!HEX6.test(trimmed)) continue;
35293
- acceptedUpdates.set(key, trimmed.toLowerCase());
35333
+ if (unlockedTokenKeySet.has(key)) {
35334
+ if (!HEX6.test(trimmed)) continue;
35335
+ acceptedUpdates.set(key, trimmed.toLowerCase());
35336
+ } else if (unlockedGradientKeySet.has(key)) {
35337
+ if (!CSS_GRADIENT.test(trimmed)) continue;
35338
+ acceptedUpdates.set(key, trimmed);
35339
+ }
35294
35340
  }
35295
35341
  if (acceptedUpdates.size === 0) {
35296
35342
  logger.warn(
@@ -35327,6 +35373,7 @@ import path20 from "node:path";
35327
35373
  import { fileURLToPath as fileURLToPath2 } from "node:url";
35328
35374
  import { z as z38 } from "zod";
35329
35375
  var QA_FILENAME = "layout-qa.md";
35376
+ var PROMPT_FILENAME = "layout-prompt.md";
35330
35377
  var MAX_LOOKUP_LEVELS = 10;
35331
35378
  var ROLE_FILE_RELATIVE = ".appostle/brand/assets/role/brand-layout-role.md";
35332
35379
  async function findFileUpward(filename) {
@@ -35485,61 +35532,90 @@ async function loadQaQuestions(logger) {
35485
35532
  }
35486
35533
  return EMBEDDED_QA_FALLBACK;
35487
35534
  }
35488
- var ROLE_GENERATION_INSTRUCTIONS = `You are an elite art director generating a complete design role document. This document will be injected wholesale into an AI agent's context to give it a dense, opinionated compositional voice \u2014 the way lordesign-brutalist or lordesign-soft gives a builder an entire design philosophy.
35535
+ async function loadLayoutPrompt(logger) {
35536
+ const filePath = await findFileUpward(PROMPT_FILENAME);
35537
+ if (filePath) {
35538
+ try {
35539
+ const content = await fs12.readFile(filePath, "utf8");
35540
+ logger.debug({ filePath }, "layout-generator: loaded layout prompt from disk");
35541
+ return content;
35542
+ } catch (err) {
35543
+ logger.warn(
35544
+ { err, filePath },
35545
+ "layout-generator: failed to read layout-prompt file; using embedded fallback"
35546
+ );
35547
+ }
35548
+ }
35549
+ return EMBEDDED_LAYOUT_PROMPT_FALLBACK;
35550
+ }
35551
+ function interpolateTemplate(template, vars) {
35552
+ let out = template;
35553
+ for (const [key, value] of Object.entries(vars)) {
35554
+ out = out.split(`{{${key}}}`).join(value);
35555
+ }
35556
+ return out;
35557
+ }
35558
+ var EMBEDDED_LAYOUT_PROMPT_FALLBACK = `You are an elite art director refining the layout and composition rules for a brand. This is ONLY about layout \u2014 structure, zones, grid, density, rhythm, containers, image placement, CTAs. Typography, colors, motion, animations, shadows are handled by separate brand files \u2014 do not duplicate them here.
35489
35559
 
35490
- The output must be a SINGLE cohesive markdown document (NOT JSON, NOT key-value pairs). It reads like a design manifesto \u2014 dense, opinionated, specific enough that two different builders reading it would produce nearly identical structural decisions.
35560
+ These rules must work across media \u2014 web pages, print, PDF, presentations. Use proportional language (fractions, ratios, percentages) as the primary system. CSS values are welcome as concrete examples but should not be the only expression of a rule.
35491
35561
 
35492
- ## Required sections (use these exact headings)
35562
+ Your job: update the design role document based on the user's refinement prompt. This document will be injected wholesale into an AI builder's context. If your rules are vague, the output will be generic. If your rules are specific and opinionated, the output will be distinctive.
35493
35563
 
35494
- # Brand Layout Role
35564
+ The document must be a complete markdown document covering:
35565
+ - Enemy (a specific design this must NOT resemble, with 2-3 specific layout patterns that make it wrong \u2014 and for each, the exact counter-pattern this brand uses instead)
35566
+ - Signature Anomaly (the one structural layout choice that makes this unmistakably distinctive)
35567
+ - Compositional Philosophy (one structural law + the structural spine mechanism that enforces it across every zone)
35568
+ - Intensity Dials (Density 1-10, Grid Variance 1-10 \u2014 with concrete implications for THIS brand)
35569
+ - Opening Zone / Hero Behavior (surface coverage, bleed, content placement, containment)
35570
+ - Zone Inventory (ordered list of all major zones: name, layout job, dense/airy, full-bleed/contained \u2014 this is the master reference for the rhythm)
35571
+ - Zone Variation & Flow (how zones differ, transition strategy, full-bleed vs contained \u2014 references the Zone Inventory)
35572
+ - Density Philosophy (spacing values; the oscillation rhythm derived from the Zone Inventory \u2014 name which zones are dense, which are airy, in sequence)
35573
+ - Vertical Rhythm (three values: zone padding, element gap within zones, and the one zone that breaks the rhythm and why)
35574
+ - Grid System (exact column structure, asymmetry, proportions)
35575
+ - Content Width Strategy (max-width of the primary content area, which zones break it and why)
35576
+ - Container & Card Rules (borders, corners, nesting, exact border-radius)
35577
+ - Dividers & Graphic Structure (structural vs decorative, background strategy)
35578
+ - Image Treatment \u2014 Layout Only (placement, grid relationship, overlap behavior)
35579
+ - CTA Strategy (position in layout referencing Zone Inventory, container strategy, frequency)
35580
+ - Responsive Behavior (how the layout adapts across sizes \u2014 web: breakpoints and stacking; print: scale and margin strategy)
35581
+ - Bans (at least 20 specific layout prohibitions)
35495
35582
 
35496
- ## Compositional Philosophy
35497
- The north star. 3-5 sentences describing the fundamental spatial personality.
35583
+ ## Critical rules
35498
35584
 
35499
- ## Hero Behavior
35500
- Exact rules for hero sections \u2014 dimensions, content placement, image treatment, scroll behavior, what's forbidden.
35585
+ 1. This is a REFINEMENT. The user already has a role document (shown below). Their prompt refines, evolves, or redirects \u2014 it does NOT start from scratch unless they explicitly say so.
35501
35586
 
35502
- ## Section Variation & Flow
35503
- How consecutive sections differ. Allowed arrangements. How many types cycle. Full-bleed rules.
35587
+ 2. SPECIFICITY IS MANDATORY. Every rule must be actionable at the structural level. Generic adjectives ("clean", "minimal", "modern", "elegant") are BANNED from the output.
35504
35588
 
35505
- ## Density Philosophy
35506
- Where dense, where airy, specific spacing ratios, padding rules.
35589
+ 3. THE DOCUMENT MUST BE OPINIONATED, NOT HEDGED. Every sentence must prescribe or ban \u2014 never suggest. Use "Always", "Never", "Must", "Banned", "Required".
35507
35590
 
35508
- ## Vertical Rhythm
35509
- Section height strategy, padding patterns, oscillation rules, breathing room logic.
35591
+ 4. STAY IN YOUR LANE. Do NOT include rules about typography, colors, motion/animation, or shadows \u2014 those belong in their own brand files.
35510
35592
 
35511
- ## Grid System
35512
- Column philosophy, alignment rules, breakpoint behavior, visible vs invisible structure.
35593
+ 5. THE BANS ARE THE MOST IMPORTANT SECTION. At least 20 bans. The 7 universal AI-layout cliches MUST each appear as an explicit named ban: centered headline over full-width image; three equal-width feature cards in a row; alternating left-right image/text rows with identical padding; uniform zone height and padding; every block wrapped in a card with shadow + radius; CTA with gradient fill; full-width "Why Choose Us" icon grid. Plus 3-5 brand-specific AI tells.
35513
35594
 
35514
- ## Container & Card Rules
35515
- Borders, shadows, corners, nesting rules, elevation hierarchy.
35595
+ 6. THE ENEMY IS MANDATORY. Name a specific real design and 2-3 exact layout patterns from it. Vague enemies fail.
35516
35596
 
35517
- ## Image Treatment
35518
- Aspect ratios, size constraints, cropping rules, filter/overlay rules, frequency.
35597
+ 7. THE SIGNATURE ANOMALY IS MANDATORY. One specific structural choice. Apply the removal test: if removing this single decision would make the layout indistinguishable from a generic design \u2014 it qualifies. Must appear as a constraint in at least 2 other sections.
35519
35598
 
35520
- ## CTA Strategy
35521
- Frequency per page, hierarchy, styling constraints, placement rules.
35599
+ 8. USE THE STRUCTURAL CONTEXT. The {{structuralContext}} contains sibling brand files. Do not duplicate their rules \u2014 derive layout implications from them.
35522
35600
 
35523
- ## Mobile Behavior
35524
- Breakpoint strategy, what collapses vs stacks, density changes, navigation transformation.
35525
-
35526
- ## Bans
35527
- The most powerful section. At least 20 specific prohibitions at EVERY level:
35528
- - CSS-level (specific properties, values, patterns)
35529
- - Component-level (specific UI patterns forbidden)
35530
- - Layout-level (spatial patterns forbidden)
35531
- - Content-level (content patterns forbidden)
35532
- - Interaction-level (motion/animation patterns forbidden)
35533
-
35534
- Each ban on its own line starting with "\u2022". Be ruthlessly specific \u2014 "no gradients" is vague, "no linear-gradient except single-stop overlays on hero images" is useful.
35535
-
35536
- ## Critical quality bar
35537
- - Every rule must be specific enough to resolve an ambiguous decision
35538
- - No generic advice ("keep it clean") \u2014 only actionable constraints
35539
- - The bans section alone should have 20+ items
35540
- - Rules should reference specific CSS properties, pixel values, viewport units where applicable
35541
- - The document should be 50-100 rules total across all sections`;
35542
- function buildQaSystemPrompt(questions) {
35601
+ ## Current role document
35602
+
35603
+ {{currentValues}}
35604
+
35605
+ ## Structural context (sibling brand files)
35606
+
35607
+ {{structuralContext}}
35608
+
35609
+ ## User's refinement prompt
35610
+
35611
+ {{userPrompt}}
35612
+
35613
+ ## Response format
35614
+
35615
+ Return ONLY a JSON object: { "roleDocument": "# Brand Layout Role\\n\\n## Enemy\\n..." }
35616
+
35617
+ Output ONLY the JSON object. No markdown fences. No explanation.`;
35618
+ function buildQaSystemPrompt(questions, layoutPromptSpec) {
35543
35619
  return `You are a chill creative director doing a quick vibe check on someone's layout taste.
35544
35620
 
35545
35621
  CONTEXT:
@@ -35573,10 +35649,11 @@ When done=false, return:
35573
35649
  { "done": false, "question": "Short question?", "options": ["Option A", "Option B", "Option C", "Option D"] }
35574
35650
 
35575
35651
  When done=true, return:
35576
- { "done": true, "roleDocument": "# Brand Layout Role\\n\\n## Compositional Philosophy\\n..." }
35652
+ { "done": true, "roleDocument": "# Brand Layout Role\\n\\n## Enemy\\n..." }
35653
+
35654
+ The roleDocument must be a complete markdown document following the structure and rules below. Treat the Q&A conversation as the user's intent; there is no current role document yet.
35577
35655
 
35578
- The roleDocument must be a complete markdown document following this structure:
35579
- ${ROLE_GENERATION_INSTRUCTIONS}`;
35656
+ ${layoutPromptSpec}`;
35580
35657
  }
35581
35658
  async function layoutQaNext(options) {
35582
35659
  const { agentManager, workspaceRoot, brandPath, conversation, logger } = options;
@@ -35585,7 +35662,13 @@ async function layoutQaNext(options) {
35585
35662
  if (!brand) throw new Error(`Brand file not found at ${brandPath}`);
35586
35663
  const structuralContext = buildStructuralContext(brands);
35587
35664
  const questions = await loadQaQuestions(logger);
35588
- const qaSystemPrompt = buildQaSystemPrompt(questions);
35665
+ const layoutPromptTemplate = await loadLayoutPrompt(logger);
35666
+ const layoutPromptSpec = interpolateTemplate(layoutPromptTemplate, {
35667
+ structuralContext,
35668
+ currentValues: "(No existing role document \u2014 this is initial generation from a Q&A conversation.)",
35669
+ userPrompt: "(See Q&A conversation below for the user's intent.)"
35670
+ });
35671
+ const qaSystemPrompt = buildQaSystemPrompt(questions, layoutPromptSpec);
35589
35672
  const convLines = conversation.map((m) => `${m.role === "assistant" ? "AI" : "User"}: ${m.content}`).join("\n\n");
35590
35673
  const userAnswerCount = conversation.filter((m) => m.role === "user").length;
35591
35674
  const prompt = [
@@ -35636,24 +35719,12 @@ async function generateAndApplyLayout(options) {
35636
35719
  if (!brand) throw new Error(`Brand file not found at ${brandPath}`);
35637
35720
  const structuralContext = buildStructuralContext(brands);
35638
35721
  const existingRole = await readRoleFile(workspaceRoot);
35639
- const prompt = [
35640
- ROLE_GENERATION_INSTRUCTIONS,
35641
- "",
35642
- "## Structural context (sibling brand files)",
35722
+ const layoutPromptTemplate = await loadLayoutPrompt(logger);
35723
+ const prompt = interpolateTemplate(layoutPromptTemplate, {
35643
35724
  structuralContext,
35644
- "",
35645
- existingRole ? `## Current role document
35646
-
35647
- ${existingRole}` : "(No existing role document \u2014 generate from scratch.)",
35648
- "",
35649
- "## User's refinement prompt",
35650
- userPrompt,
35651
- "",
35652
- "This is a REFINEMENT. The user already has a role document (shown above). Their prompt refines, evolves, or redirects \u2014 it does NOT start from scratch unless they explicitly say so.",
35653
- "",
35654
- 'Return ONLY a JSON object: { "roleDocument": "the complete updated markdown document" }',
35655
- "No markdown fences. No explanation."
35656
- ].join("\n");
35725
+ currentValues: existingRole ?? "(No existing role document \u2014 generate from scratch.)",
35726
+ userPrompt
35727
+ });
35657
35728
  let response;
35658
35729
  try {
35659
35730
  response = await generateStructuredAgentResponseWithFallback({
@@ -44886,7 +44957,8 @@ ${details}`.trim());
44886
44957
  try {
44887
44958
  const room = await this.chatService.createRoom({
44888
44959
  name: request.name,
44889
- purpose: request.purpose
44960
+ purpose: request.purpose,
44961
+ ownerUserId: this.ownerUserId
44890
44962
  });
44891
44963
  this.emit({
44892
44964
  type: "chat/create/response",
@@ -44902,7 +44974,7 @@ ${details}`.trim());
44902
44974
  }
44903
44975
  async handleChatListRequest(request) {
44904
44976
  try {
44905
- const rooms = await this.chatService.listRooms();
44977
+ const rooms = await this.chatService.listRooms(this.ownerUserId);
44906
44978
  this.emit({
44907
44979
  type: "chat/list/response",
44908
44980
  payload: {
@@ -44918,7 +44990,8 @@ ${details}`.trim());
44918
44990
  async handleChatInspectRequest(request) {
44919
44991
  try {
44920
44992
  const result = await this.chatService.inspectRoom({
44921
- room: request.room
44993
+ room: request.room,
44994
+ requesterUserId: this.ownerUserId
44922
44995
  });
44923
44996
  this.emit({
44924
44997
  type: "chat/inspect/response",
@@ -44935,7 +45008,8 @@ ${details}`.trim());
44935
45008
  async handleChatDeleteRequest(request) {
44936
45009
  try {
44937
45010
  const result = await this.chatService.deleteRoom({
44938
- room: request.room
45011
+ room: request.room,
45012
+ requesterUserId: this.ownerUserId
44939
45013
  });
44940
45014
  this.emit({
44941
45015
  type: "chat/delete/response",
@@ -44956,7 +45030,8 @@ ${details}`.trim());
44956
45030
  room: request.room,
44957
45031
  authorAgentId,
44958
45032
  body: request.body,
44959
- replyToMessageId: request.replyToMessageId
45033
+ replyToMessageId: request.replyToMessageId,
45034
+ requesterUserId: this.ownerUserId
44960
45035
  });
44961
45036
  this.emit({
44962
45037
  type: "chat/post/response",
@@ -44989,7 +45064,8 @@ ${details}`.trim());
44989
45064
  room: request.room,
44990
45065
  limit: request.limit,
44991
45066
  since: request.since,
44992
- authorAgentId: request.authorAgentId
45067
+ authorAgentId: request.authorAgentId,
45068
+ requesterUserId: this.ownerUserId
44993
45069
  });
44994
45070
  this.emit({
44995
45071
  type: "chat/read/response",
@@ -45008,7 +45084,8 @@ ${details}`.trim());
45008
45084
  const messages = await this.chatService.waitForMessages({
45009
45085
  room: request.room,
45010
45086
  afterMessageId: request.afterMessageId,
45011
- timeoutMs: request.timeoutMs
45087
+ timeoutMs: request.timeoutMs,
45088
+ requesterUserId: this.ownerUserId
45012
45089
  });
45013
45090
  this.emit({
45014
45091
  type: "chat/wait/response",