appostle-installer 0.0.21 → 0.0.23

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
@@ -3234,7 +3234,9 @@ var BrandEntrySchema = z10.object({
3234
3234
  variables: z10.array(BrandVariableSchema).optional().default([]),
3235
3235
  path: z10.string(),
3236
3236
  modifiedAt: z10.string(),
3237
- size: z10.number()
3237
+ size: z10.number(),
3238
+ /** Raw markdown body (frontmatter stripped). Optional, fed by the scanner. */
3239
+ body: z10.string().optional()
3238
3240
  });
3239
3241
  var BrandFrontmatterSchema = z10.object({
3240
3242
  description: z10.string().optional(),
@@ -3411,6 +3413,22 @@ var BrandGenerateWireframeResponseSchema = z10.object({
3411
3413
  error: z10.string().nullable()
3412
3414
  })
3413
3415
  });
3416
+ var BrandAnalyzePhotographyRequestSchema = z10.object({
3417
+ type: z10.literal("brands/analyze-photography"),
3418
+ requestId: z10.string(),
3419
+ workspaceRoot: z10.string(),
3420
+ brandPath: z10.string(),
3421
+ brief: z10.string(),
3422
+ referenceDescriptions: z10.array(z10.string()).optional()
3423
+ });
3424
+ var BrandAnalyzePhotographyResponseSchema = z10.object({
3425
+ type: z10.literal("brands/analyze-photography/response"),
3426
+ payload: z10.object({
3427
+ requestId: z10.string(),
3428
+ updatedCount: z10.number(),
3429
+ error: z10.string().nullable()
3430
+ })
3431
+ });
3414
3432
  var RightFontEntrySchema = z10.object({
3415
3433
  id: z10.string(),
3416
3434
  name: z10.string(),
@@ -3510,10 +3528,14 @@ var MutableDaemonConfigSchema = z11.object({
3510
3528
  icon: z11.string().optional()
3511
3529
  }).passthrough().optional(),
3512
3530
  chrome: z11.object({
3513
- // When false, agents on this host won't get the --chrome flag,
3514
- // preventing the client's Chrome browser from being controlled.
3515
- // Useful on headless servers where browser tools should target
3516
- // a local headless Chrome instead of the remote client's browser.
3531
+ // When false, agents on this host won't have the claude-in-chrome
3532
+ // tools injected. Set to false on hosts where the client's browser
3533
+ // should not be controlled (e.g. headless/server-only hosts).
3534
+ enabled: z11.boolean()
3535
+ }).passthrough().optional(),
3536
+ playwright: z11.object({
3537
+ // When false, agents on this host won't have the Playwright MCP
3538
+ // server injected.
3517
3539
  enabled: z11.boolean()
3518
3540
  }).passthrough().optional()
3519
3541
  }).passthrough();
@@ -3522,7 +3544,8 @@ var MutableDaemonConfigPatchSchema = z11.object({
3522
3544
  identity: z11.object({
3523
3545
  icon: z11.string().optional()
3524
3546
  }).partial().passthrough().optional(),
3525
- chrome: MutableDaemonConfigSchema.shape.chrome
3547
+ chrome: MutableDaemonConfigSchema.shape.chrome,
3548
+ playwright: MutableDaemonConfigSchema.shape.playwright
3526
3549
  }).partial().passthrough();
3527
3550
  var AgentStatusSchema = z11.enum(AGENT_LIFECYCLE_STATUSES);
3528
3551
  var AgentModeSchema = z11.object({
@@ -5532,6 +5555,7 @@ var SessionInboundMessageSchema = z11.discriminatedUnion("type", [
5532
5555
  BrandGenerateLayoutRequestSchema,
5533
5556
  BrandLayoutQaNextRequestSchema,
5534
5557
  BrandGenerateWireframeRequestSchema,
5558
+ BrandAnalyzePhotographyRequestSchema,
5535
5559
  RightFontLibraryRequestSchema,
5536
5560
  GoogleFontsCatalogRequestSchema,
5537
5561
  GoogleFontsDownloadRequestSchema,
@@ -7168,6 +7192,7 @@ var SessionOutboundMessageSchema = z11.discriminatedUnion("type", [
7168
7192
  BrandGenerateLayoutResponseSchema,
7169
7193
  BrandLayoutQaNextResponseSchema,
7170
7194
  BrandGenerateWireframeResponseSchema,
7195
+ BrandAnalyzePhotographyResponseSchema,
7171
7196
  RightFontLibraryResponseSchema,
7172
7197
  GoogleFontsCatalogResponseSchema,
7173
7198
  GoogleFontsDownloadResponseSchema,
@@ -7377,7 +7402,7 @@ import { exec } from "node:child_process";
7377
7402
  import { promisify as promisify3 } from "util";
7378
7403
  import { join as join14, resolve as resolve9, sep as sep2 } from "path";
7379
7404
  import { homedir as homedir5, hostname as osHostname } from "node:os";
7380
- import { z as z39 } from "zod";
7405
+ import { z as z40 } from "zod";
7381
7406
 
7382
7407
  // ../server/src/server/persisted-config.ts
7383
7408
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
@@ -7674,6 +7699,9 @@ var PersistedConfigSchema = z14.object({
7674
7699
  }).strict().optional(),
7675
7700
  chrome: z14.object({
7676
7701
  enabled: z14.boolean().optional()
7702
+ }).strict().optional(),
7703
+ playwright: z14.object({
7704
+ enabled: z14.boolean().optional()
7677
7705
  }).strict().optional()
7678
7706
  }).strict().transform(({ allowedHosts, ...daemon }) => {
7679
7707
  const hostnames = daemon.hostnames ?? allowedHosts;
@@ -18494,6 +18522,7 @@ var ClaudeAgentClient = class {
18494
18522
  runtimeSettings: this.runtimeSettings,
18495
18523
  launchEnv: launchContext?.env,
18496
18524
  chromeEnabled: launchContext?.chromeEnabled,
18525
+ playwrightEnabled: launchContext?.playwrightEnabled,
18497
18526
  logger: this.logger,
18498
18527
  queryFactory: this.queryFactory
18499
18528
  });
@@ -18512,6 +18541,7 @@ var ClaudeAgentClient = class {
18512
18541
  handle,
18513
18542
  launchEnv: launchContext?.env,
18514
18543
  chromeEnabled: launchContext?.chromeEnabled,
18544
+ playwrightEnabled: launchContext?.playwrightEnabled,
18515
18545
  logger: this.logger,
18516
18546
  queryFactory: this.queryFactory
18517
18547
  });
@@ -18826,6 +18856,7 @@ var ClaudeAgentSession = class {
18826
18856
  this.config = config;
18827
18857
  this.launchEnv = options.launchEnv;
18828
18858
  this.chromeEnabled = options.chromeEnabled ?? true;
18859
+ this.playwrightEnabled = options.playwrightEnabled ?? true;
18829
18860
  this.defaults = options.defaults;
18830
18861
  this.runtimeSettings = options.runtimeSettings;
18831
18862
  this.logger = options.logger;
@@ -19589,7 +19620,8 @@ var ClaudeAgentSession = class {
19589
19620
  } else {
19590
19621
  const homeMcps = loadHomeMcpServers(this.logger);
19591
19622
  if (homeMcps) {
19592
- base.mcpServers = this.normalizeMcpServers(homeMcps);
19623
+ const filtered = this.playwrightEnabled ? homeMcps : Object.fromEntries(Object.entries(homeMcps).filter(([k]) => k !== "playwright"));
19624
+ base.mcpServers = this.normalizeMcpServers(filtered);
19593
19625
  }
19594
19626
  }
19595
19627
  if (this.config.model) {
@@ -20727,6 +20759,12 @@ ${error.stack ?? ""}` : JSON.stringify(error);
20727
20759
  buildStructuredToolResult(server, tool4, output, input) {
20728
20760
  const normalizedServer = server.toLowerCase();
20729
20761
  const normalizedTool = tool4.toLowerCase();
20762
+ if (normalizedTool === "mcp__playwright__browser_take_screenshot" || normalizedTool === "browser_take_screenshot") {
20763
+ const imageOutput = this.tryInlinePlaywrightScreenshot(output, input);
20764
+ if (imageOutput) {
20765
+ return { output: imageOutput };
20766
+ }
20767
+ }
20730
20768
  if (normalizedServer.includes("bash") || normalizedServer.includes("shell") || normalizedServer.includes("command") || normalizedTool.includes("bash") || normalizedTool.includes("shell") || normalizedTool.includes("command") || input && (typeof input.command === "string" || Array.isArray(input.command))) {
20731
20769
  const command = this.extractCommandText(input ?? {}) ?? "command";
20732
20770
  return {
@@ -20768,8 +20806,46 @@ ${error.stack ?? ""}` : JSON.stringify(error);
20768
20806
  };
20769
20807
  }
20770
20808
  }
20809
+ if (normalizedServer === "playwright" && normalizedTool === "browser_take_screenshot") {
20810
+ const imageDetail = this.tryReadPlaywrightScreenshot(output, input);
20811
+ if (imageDetail) return imageDetail;
20812
+ }
20771
20813
  return void 0;
20772
20814
  }
20815
+ /**
20816
+ * Playwright MCP's `browser_take_screenshot` saves the image to a local file
20817
+ * and returns a markdown link (`[Screenshot of viewport](./file.png)`).
20818
+ * Extract the path, resolve it against the agent CWD, read the bytes, and
20819
+ * return an image ToolCallDetail so the client renders it inline.
20820
+ */
20821
+ tryReadPlaywrightScreenshot(output, input) {
20822
+ let filePath;
20823
+ if (input && typeof input.filename === "string") {
20824
+ filePath = input.filename;
20825
+ }
20826
+ if (!filePath) {
20827
+ const match = output.match(/\[Screenshot[^\]]*\]\(([^)]+)\)/i);
20828
+ if (match?.[1]) {
20829
+ filePath = match[1];
20830
+ }
20831
+ }
20832
+ if (!filePath) return void 0;
20833
+ const cwd = this.config.cwd;
20834
+ if (!cwd) return void 0;
20835
+ const abs = path9.isAbsolute(filePath) ? filePath : path9.resolve(cwd, filePath);
20836
+ try {
20837
+ const buf = fs4.readFileSync(abs);
20838
+ const ext = path9.extname(abs).toLowerCase();
20839
+ const mime = ext === ".jpeg" || ext === ".jpg" ? "image/jpeg" : "image/png";
20840
+ return {
20841
+ type: "image",
20842
+ toolName: "mcp__playwright__browser_take_screenshot",
20843
+ images: [{ mimeType: mime, data: buf.toString("base64") }]
20844
+ };
20845
+ } catch {
20846
+ return void 0;
20847
+ }
20848
+ }
20773
20849
  mapPartialEvent(event, options) {
20774
20850
  if (event.type === "content_block_start") {
20775
20851
  const block = isClaudeContentChunk2(event.content_block) ? event.content_block : null;
@@ -20942,6 +21018,44 @@ ${error.stack ?? ""}` : JSON.stringify(error);
20942
21018
  }
20943
21019
  return input;
20944
21020
  }
21021
+ /**
21022
+ * Playwright MCP returns `### Result\n- [Screenshot of viewport](./foo.png)`
21023
+ * and writes the file to disk instead of inlining the image. Resolve that
21024
+ * path against the agent cwd and turn the file into an Anthropic-style
21025
+ * image content block array, so downstream `extractImageToolDetail`
21026
+ * promotes the tool call to a renderable `image` detail.
21027
+ *
21028
+ * Returns `null` when the path cannot be resolved/read safely; the caller
21029
+ * then falls back to the default text output behavior.
21030
+ */
21031
+ tryInlinePlaywrightScreenshot(output, input) {
21032
+ const cwd = this.config.cwd;
21033
+ if (!cwd) return null;
21034
+ let relPath;
21035
+ if (input && typeof input.filename === "string" && input.filename.length > 0) {
21036
+ relPath = input.filename;
21037
+ } else {
21038
+ const match = output.match(/\[Screenshot[^\]]*\]\(([^)]+)\)/);
21039
+ if (match?.[1]) {
21040
+ relPath = match[1];
21041
+ }
21042
+ }
21043
+ if (!relPath) return null;
21044
+ const resolved = path9.resolve(cwd, relPath);
21045
+ const cwdReal = path9.resolve(cwd);
21046
+ if (!resolved.startsWith(cwdReal + path9.sep) && resolved !== cwdReal) {
21047
+ return null;
21048
+ }
21049
+ let buf;
21050
+ try {
21051
+ buf = fs4.readFileSync(resolved);
21052
+ } catch {
21053
+ return null;
21054
+ }
21055
+ const ext = path9.extname(resolved).toLowerCase();
21056
+ const mimeType = ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".webp" ? "image/webp" : ext === ".gif" ? "image/gif" : "image/png";
21057
+ return [{ type: "image", mimeType, data: buf.toString("base64") }];
21058
+ }
20945
21059
  areToolInputsEqual(left, right) {
20946
21060
  if (!left) {
20947
21061
  return false;
@@ -31425,6 +31539,330 @@ function removeClaudeProfile(username, logger) {
31425
31539
  import { z as z34 } from "zod";
31426
31540
  import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
31427
31541
 
31542
+ // ../server/src/server/skills/scanner.ts
31543
+ import fs8 from "node:fs/promises";
31544
+ import os6 from "node:os";
31545
+ import path14 from "node:path";
31546
+ var NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/i;
31547
+ function homeDir() {
31548
+ return process.env.HOME || os6.homedir();
31549
+ }
31550
+ function codexHomeDir() {
31551
+ return process.env.CODEX_HOME || path14.join(homeDir(), ".codex");
31552
+ }
31553
+ function resolveScopeDir(provider, scope, workspaceRoot) {
31554
+ if (scope === "codex-prompts") {
31555
+ if (provider !== "codex") {
31556
+ throw new Error(`Scope "codex-prompts" is only valid for provider "codex"`);
31557
+ }
31558
+ return path14.join(codexHomeDir(), "prompts");
31559
+ }
31560
+ if (scope === "project") {
31561
+ if (!workspaceRoot) {
31562
+ throw new Error(`workspaceRoot is required for scope "project"`);
31563
+ }
31564
+ const dotDir = provider === "claude" ? ".claude" : ".codex";
31565
+ return path14.join(workspaceRoot, dotDir, "skills");
31566
+ }
31567
+ if (provider === "claude") {
31568
+ return path14.join(homeDir(), ".claude", "skills");
31569
+ }
31570
+ return path14.join(codexHomeDir(), "skills");
31571
+ }
31572
+ function allowedRoots(workspaceRoot) {
31573
+ const roots = [
31574
+ path14.join(homeDir(), ".claude", "skills"),
31575
+ path14.join(codexHomeDir(), "skills"),
31576
+ path14.join(codexHomeDir(), "prompts")
31577
+ ];
31578
+ if (workspaceRoot) {
31579
+ roots.push(path14.join(workspaceRoot, ".claude", "skills"));
31580
+ roots.push(path14.join(workspaceRoot, ".codex", "skills"));
31581
+ }
31582
+ return roots.map((r) => path14.resolve(r));
31583
+ }
31584
+ function isInsideAllowedRoot(absPath, workspaceRoot) {
31585
+ const resolved = path14.resolve(absPath);
31586
+ for (const root of allowedRoots(workspaceRoot)) {
31587
+ const rel = path14.relative(root, resolved);
31588
+ if (rel === "" || !rel.startsWith("..") && !path14.isAbsolute(rel)) {
31589
+ return true;
31590
+ }
31591
+ }
31592
+ return false;
31593
+ }
31594
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
31595
+ function parseSkillFile(text) {
31596
+ const match = FRONTMATTER_RE.exec(text);
31597
+ if (!match) {
31598
+ return { rawFrontmatterLines: [], body: text, hadFrontmatter: false };
31599
+ }
31600
+ const yamlBlock = match[1] ?? "";
31601
+ const body = match[2] ?? "";
31602
+ return {
31603
+ rawFrontmatterLines: yamlBlock.split(/\r?\n/),
31604
+ body,
31605
+ hadFrontmatter: true
31606
+ };
31607
+ }
31608
+ function readDescription(text) {
31609
+ const parsed = parseSkillFile(text);
31610
+ if (!parsed.hadFrontmatter) return "";
31611
+ for (const line of parsed.rawFrontmatterLines) {
31612
+ const m = /^description\s*:\s*(.*)$/.exec(line);
31613
+ if (m) {
31614
+ const value = (m[1] ?? "").trim();
31615
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
31616
+ return value.slice(1, -1);
31617
+ }
31618
+ return value;
31619
+ }
31620
+ }
31621
+ return "";
31622
+ }
31623
+ function findOwnedSpans(lines) {
31624
+ const spans = [];
31625
+ let i = 0;
31626
+ while (i < lines.length) {
31627
+ const line = lines[i] ?? "";
31628
+ const keyMatch = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line);
31629
+ if (!keyMatch) {
31630
+ i++;
31631
+ continue;
31632
+ }
31633
+ const key = keyMatch[1];
31634
+ const value = keyMatch[2];
31635
+ if (key !== "description" && key !== "argument-hint" && key !== "allowed-tools") {
31636
+ i++;
31637
+ continue;
31638
+ }
31639
+ let end = i + 1;
31640
+ if (value === "") {
31641
+ while (end < lines.length) {
31642
+ const next = lines[end] ?? "";
31643
+ if (/^\s+-\s+/.test(next) || /^\s*$/.test(next)) {
31644
+ end++;
31645
+ } else {
31646
+ break;
31647
+ }
31648
+ }
31649
+ }
31650
+ spans.push({ key, startLine: i, endLine: end });
31651
+ i = end;
31652
+ }
31653
+ return spans;
31654
+ }
31655
+ function emitFrontmatterValue(key, value) {
31656
+ if (key === "allowed-tools") {
31657
+ if (!Array.isArray(value) || value.length === 0) return [];
31658
+ return [`allowed-tools:`, ...value.map((v) => ` - ${v}`)];
31659
+ }
31660
+ const text = typeof value === "string" ? value : "";
31661
+ if (text.length === 0) return [];
31662
+ const needsQuote = /[:#\n]/.test(text) || text.startsWith(" ") || text.endsWith(" ");
31663
+ const formatted = needsQuote ? `"${text.replace(/"/g, '\\"')}"` : text;
31664
+ return [`${key}: ${formatted}`];
31665
+ }
31666
+ function rewriteFrontmatter(originalLines, next) {
31667
+ const spans = findOwnedSpans(originalLines);
31668
+ const ownedKeys = new Set(spans.map((s) => s.key));
31669
+ const replacements = /* @__PURE__ */ new Map();
31670
+ if (next.description !== void 0) {
31671
+ replacements.set("description", emitFrontmatterValue("description", next.description));
31672
+ }
31673
+ if (next.argumentHint !== void 0) {
31674
+ replacements.set("argument-hint", emitFrontmatterValue("argument-hint", next.argumentHint));
31675
+ }
31676
+ if (next.allowedTools !== void 0) {
31677
+ replacements.set("allowed-tools", emitFrontmatterValue("allowed-tools", next.allowedTools));
31678
+ }
31679
+ const out = [];
31680
+ let i = 0;
31681
+ while (i < originalLines.length) {
31682
+ const span = spans.find((s) => s.startLine === i);
31683
+ if (span) {
31684
+ if (replacements.has(span.key)) {
31685
+ out.push(...replacements.get(span.key) ?? []);
31686
+ } else {
31687
+ for (let j = span.startLine; j < span.endLine; j++) {
31688
+ out.push(originalLines[j] ?? "");
31689
+ }
31690
+ }
31691
+ i = span.endLine;
31692
+ continue;
31693
+ }
31694
+ out.push(originalLines[i] ?? "");
31695
+ i++;
31696
+ }
31697
+ for (const key of ["description", "argument-hint", "allowed-tools"]) {
31698
+ if (replacements.has(key) && !ownedKeys.has(key)) {
31699
+ out.push(...replacements.get(key) ?? []);
31700
+ }
31701
+ }
31702
+ return out;
31703
+ }
31704
+ async function listSkills(args) {
31705
+ const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
31706
+ let entries;
31707
+ try {
31708
+ entries = await fs8.readdir(dir, { withFileTypes: true });
31709
+ } catch {
31710
+ return [];
31711
+ }
31712
+ const results = [];
31713
+ if (args.scope === "codex-prompts") {
31714
+ for (const entry of entries) {
31715
+ if (!entry.isFile()) continue;
31716
+ if (!entry.name.endsWith(".md")) continue;
31717
+ const name = entry.name.slice(0, -".md".length);
31718
+ if (!name) continue;
31719
+ const fullPath = path14.join(dir, entry.name);
31720
+ const stat5 = await safeStat(fullPath);
31721
+ if (!stat5) continue;
31722
+ const description = await readDescriptionSafely(fullPath);
31723
+ results.push({
31724
+ id: `${args.provider}:${args.scope}:${name}`,
31725
+ provider: args.provider,
31726
+ scope: args.scope,
31727
+ name,
31728
+ path: fullPath,
31729
+ description,
31730
+ modifiedAt: stat5.mtime.toISOString(),
31731
+ size: stat5.size
31732
+ });
31733
+ }
31734
+ } else {
31735
+ for (const entry of entries) {
31736
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
31737
+ const skillDir = path14.join(dir, entry.name);
31738
+ const skillPath = path14.join(skillDir, "SKILL.md");
31739
+ const stat5 = await safeStat(skillPath);
31740
+ if (!stat5) continue;
31741
+ const description = await readDescriptionSafely(skillPath);
31742
+ results.push({
31743
+ id: `${args.provider}:${args.scope}:${entry.name}`,
31744
+ provider: args.provider,
31745
+ scope: args.scope,
31746
+ name: entry.name,
31747
+ path: skillPath,
31748
+ description,
31749
+ modifiedAt: stat5.mtime.toISOString(),
31750
+ size: stat5.size
31751
+ });
31752
+ }
31753
+ }
31754
+ results.sort((a, b) => a.name.localeCompare(b.name));
31755
+ return results;
31756
+ }
31757
+ async function safeStat(filePath) {
31758
+ try {
31759
+ const s = await fs8.stat(filePath);
31760
+ return { mtime: s.mtime, size: s.size };
31761
+ } catch {
31762
+ return null;
31763
+ }
31764
+ }
31765
+ async function readDescriptionSafely(filePath) {
31766
+ try {
31767
+ const text = await fs8.readFile(filePath, "utf8");
31768
+ return readDescription(text);
31769
+ } catch {
31770
+ return "";
31771
+ }
31772
+ }
31773
+ async function createSkill(args) {
31774
+ if (!NAME_REGEX.test(args.name)) {
31775
+ throw new Error(
31776
+ `Invalid skill name "${args.name}". Use letters, digits, dot, underscore, dash.`
31777
+ );
31778
+ }
31779
+ if (args.name.includes("/") || args.name.includes("\\") || args.name.includes("..")) {
31780
+ throw new Error(`Skill name must not contain path separators or "..".`);
31781
+ }
31782
+ const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
31783
+ await fs8.mkdir(dir, { recursive: true });
31784
+ if (args.scope === "codex-prompts") {
31785
+ const filePath2 = path14.join(dir, `${args.name}.md`);
31786
+ try {
31787
+ await fs8.access(filePath2);
31788
+ throw new Error(`A prompt named "${args.name}" already exists at ${filePath2}`);
31789
+ } catch (err) {
31790
+ if (err && typeof err === "object" && "code" in err && err.code !== "ENOENT") {
31791
+ throw err;
31792
+ }
31793
+ if (err instanceof Error && err.message.includes("already exists")) {
31794
+ throw err;
31795
+ }
31796
+ }
31797
+ const initial2 = buildStarterPrompt(args.name);
31798
+ await fs8.writeFile(filePath2, initial2, "utf8");
31799
+ return { path: filePath2 };
31800
+ }
31801
+ const skillDir = path14.join(dir, args.name);
31802
+ let dirExists = false;
31803
+ try {
31804
+ const stat5 = await fs8.stat(skillDir);
31805
+ dirExists = stat5.isDirectory();
31806
+ } catch {
31807
+ dirExists = false;
31808
+ }
31809
+ if (dirExists) {
31810
+ throw new Error(`A skill named "${args.name}" already exists at ${skillDir}`);
31811
+ }
31812
+ await fs8.mkdir(skillDir, { recursive: true });
31813
+ const filePath = path14.join(skillDir, "SKILL.md");
31814
+ const initial = buildStarterSkill(args.name);
31815
+ await fs8.writeFile(filePath, initial, "utf8");
31816
+ return { path: filePath };
31817
+ }
31818
+ function buildStarterSkill(name) {
31819
+ return `---
31820
+ name: ${name}
31821
+ description: ""
31822
+ ---
31823
+
31824
+ # ${name}
31825
+
31826
+ Describe when this skill should be used and what it does. The body of this
31827
+ file is loaded into the agent's context when the skill is invoked.
31828
+ `;
31829
+ }
31830
+ function buildStarterPrompt(name) {
31831
+ return `---
31832
+ description: ""
31833
+ argument-hint: ""
31834
+ ---
31835
+
31836
+ # ${name}
31837
+
31838
+ Body of the prompt. Use \`$1\`, \`$2\`, ... or \`$ARGUMENTS\` for parameter expansion.
31839
+ `;
31840
+ }
31841
+ async function writeSkillFrontmatter(args, workspaceRoot) {
31842
+ if (!path14.isAbsolute(args.path)) {
31843
+ throw new Error(`writeSkillFrontmatter expects an absolute path; got "${args.path}"`);
31844
+ }
31845
+ if (!isInsideAllowedRoot(args.path, workspaceRoot)) {
31846
+ throw new Error(`Path "${args.path}" is not inside an allowlisted skill root`);
31847
+ }
31848
+ let original;
31849
+ try {
31850
+ original = await fs8.readFile(args.path, "utf8");
31851
+ } catch (err) {
31852
+ throw new Error(
31853
+ `Failed to read skill file: ${err instanceof Error ? err.message : String(err)}`
31854
+ );
31855
+ }
31856
+ const parsed = parseSkillFile(original);
31857
+ const newLines = rewriteFrontmatter(parsed.rawFrontmatterLines, args.frontmatter);
31858
+ const nextFrontmatter = ["---", ...newLines, "---"].join("\n");
31859
+ const nextContent = parsed.hadFrontmatter ? `${nextFrontmatter}
31860
+ ${parsed.body}` : `${nextFrontmatter}
31861
+
31862
+ ${original}`;
31863
+ await fs8.writeFile(args.path, nextContent, "utf8");
31864
+ }
31865
+
31428
31866
  // ../server/src/server/agent/handoff-mcp.ts
31429
31867
  import { createSdkMcpServer, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
31430
31868
  import { z as z33 } from "zod";
@@ -32397,8 +32835,8 @@ function isVoicePermissionAllowed(request) {
32397
32835
  }
32398
32836
 
32399
32837
  // ../server/src/server/file-explorer/service.ts
32400
- import { promises as fs8 } from "fs";
32401
- import path14 from "path";
32838
+ import { promises as fs9 } from "fs";
32839
+ import path15 from "path";
32402
32840
 
32403
32841
  // ../server/src/server/path-utils.ts
32404
32842
  import { homedir as homedir2 } from "node:os";
@@ -32445,14 +32883,14 @@ async function listDirectoryEntries({
32445
32883
  relativePath = "."
32446
32884
  }) {
32447
32885
  const directoryPath = await resolveScopedPath({ root, relativePath });
32448
- const stats = await fs8.stat(directoryPath);
32886
+ const stats = await fs9.stat(directoryPath);
32449
32887
  if (!stats.isDirectory()) {
32450
32888
  throw new Error("Requested path is not a directory");
32451
32889
  }
32452
- const dirents = await fs8.readdir(directoryPath, { withFileTypes: true });
32890
+ const dirents = await fs9.readdir(directoryPath, { withFileTypes: true });
32453
32891
  const entriesWithNulls = await Promise.all(
32454
32892
  dirents.map(async (dirent) => {
32455
- const targetPath = path14.join(directoryPath, dirent.name);
32893
+ const targetPath = path15.join(directoryPath, dirent.name);
32456
32894
  const kind = dirent.isDirectory() ? "directory" : "file";
32457
32895
  try {
32458
32896
  return await buildEntryPayload({
@@ -32487,18 +32925,18 @@ async function readExplorerFile({
32487
32925
  relativePath
32488
32926
  }) {
32489
32927
  const filePath = await resolveScopedPath({ root, relativePath });
32490
- const stats = await fs8.stat(filePath);
32928
+ const stats = await fs9.stat(filePath);
32491
32929
  if (!stats.isFile()) {
32492
32930
  throw new Error("Requested path is not a file");
32493
32931
  }
32494
- const ext = path14.extname(filePath).toLowerCase();
32932
+ const ext = path15.extname(filePath).toLowerCase();
32495
32933
  const basePayload = {
32496
32934
  path: normalizeRelativePath({ root, targetPath: filePath }),
32497
32935
  size: stats.size,
32498
32936
  modifiedAt: stats.mtime.toISOString()
32499
32937
  };
32500
32938
  if (ext in IMAGE_MIME_TYPES) {
32501
- const buffer2 = await fs8.readFile(filePath);
32939
+ const buffer2 = await fs9.readFile(filePath);
32502
32940
  return {
32503
32941
  ...basePayload,
32504
32942
  kind: "image",
@@ -32507,7 +32945,7 @@ async function readExplorerFile({
32507
32945
  mimeType: IMAGE_MIME_TYPES[ext]
32508
32946
  };
32509
32947
  }
32510
- const buffer = await fs8.readFile(filePath);
32948
+ const buffer = await fs9.readFile(filePath);
32511
32949
  if (isLikelyBinary(buffer)) {
32512
32950
  return {
32513
32951
  ...basePayload,
@@ -32529,34 +32967,34 @@ async function writeTextFile({
32529
32967
  relativePath,
32530
32968
  content
32531
32969
  }) {
32532
- const ext = path14.extname(relativePath).toLowerCase();
32970
+ const ext = path15.extname(relativePath).toLowerCase();
32533
32971
  if (ext in IMAGE_MIME_TYPES) {
32534
32972
  throw new Error(`Refusing to write '${relativePath}': binary/image file`);
32535
32973
  }
32536
32974
  const filePath = await resolveScopedPath({ root, relativePath });
32537
32975
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32538
- await fs8.writeFile(tempPath, content, "utf8");
32539
- await fs8.rename(tempPath, filePath);
32976
+ await fs9.writeFile(tempPath, content, "utf8");
32977
+ await fs9.rename(tempPath, filePath);
32540
32978
  }
32541
32979
  async function deleteFile({ root, relativePath }) {
32542
- const ext = path14.extname(relativePath).toLowerCase();
32980
+ const ext = path15.extname(relativePath).toLowerCase();
32543
32981
  if (ext !== ".md") {
32544
32982
  throw new Error(`Refusing to delete '${relativePath}': only .md files allowed`);
32545
32983
  }
32546
32984
  const filePath = await resolveScopedPath({ root, relativePath });
32547
- const stats = await fs8.stat(filePath);
32985
+ const stats = await fs9.stat(filePath);
32548
32986
  if (!stats.isFile()) {
32549
32987
  throw new Error("Requested path is not a file");
32550
32988
  }
32551
- await fs8.unlink(filePath);
32989
+ await fs9.unlink(filePath);
32552
32990
  }
32553
32991
  async function deleteEntry({ root, relativePath }) {
32554
32992
  const entryPath = await resolveScopedPath({ root, relativePath });
32555
- await fs8.rm(entryPath, { recursive: true, force: false });
32993
+ await fs9.rm(entryPath, { recursive: true, force: false });
32556
32994
  }
32557
32995
  async function createDirectory({ root, relativePath }) {
32558
32996
  const dirPath = await resolveScopedPath({ root, relativePath });
32559
- await fs8.mkdir(dirPath, { recursive: true });
32997
+ await fs9.mkdir(dirPath, { recursive: true });
32560
32998
  }
32561
32999
  async function moveEntry({
32562
33000
  root,
@@ -32565,22 +33003,22 @@ async function moveEntry({
32565
33003
  }) {
32566
33004
  const src = await resolveScopedPath({ root, relativePath: sourcePath });
32567
33005
  const dest = await resolveScopedPath({ root, relativePath: destinationPath });
32568
- await fs8.rename(src, dest);
33006
+ await fs9.rename(src, dest);
32569
33007
  }
32570
33008
  async function getDownloadableFileInfo({ root, relativePath }) {
32571
33009
  const filePath = await resolveScopedPath({ root, relativePath });
32572
- const stats = await fs8.stat(filePath);
33010
+ const stats = await fs9.stat(filePath);
32573
33011
  if (!stats.isFile()) {
32574
33012
  throw new Error("Requested path is not a file");
32575
33013
  }
32576
- const ext = path14.extname(filePath).toLowerCase();
33014
+ const ext = path15.extname(filePath).toLowerCase();
32577
33015
  let mimeType = "application/octet-stream";
32578
33016
  if (ext in IMAGE_MIME_TYPES) {
32579
33017
  mimeType = IMAGE_MIME_TYPES[ext] ?? mimeType;
32580
33018
  } else if (ext in FONT_MIME_TYPES) {
32581
33019
  mimeType = FONT_MIME_TYPES[ext] ?? mimeType;
32582
33020
  } else {
32583
- const handle = await fs8.open(filePath, "r");
33021
+ const handle = await fs9.open(filePath, "r");
32584
33022
  const sample = Buffer.alloc(8192);
32585
33023
  try {
32586
33024
  const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
@@ -32595,23 +33033,23 @@ async function getDownloadableFileInfo({ root, relativePath }) {
32595
33033
  return {
32596
33034
  path: normalizeRelativePath({ root, targetPath: filePath }),
32597
33035
  absolutePath: filePath,
32598
- fileName: path14.basename(filePath),
33036
+ fileName: path15.basename(filePath),
32599
33037
  mimeType,
32600
33038
  size: stats.size
32601
33039
  };
32602
33040
  }
32603
33041
  async function resolveScopedPath({ root, relativePath = "." }) {
32604
- const normalizedRoot = path14.resolve(root);
33042
+ const normalizedRoot = path15.resolve(root);
32605
33043
  const requestedPath = resolvePathFromBase(normalizedRoot, relativePath);
32606
- const relative = path14.relative(normalizedRoot, requestedPath);
32607
- if (relative !== "" && (relative.startsWith("..") || path14.isAbsolute(relative))) {
33044
+ const relative = path15.relative(normalizedRoot, requestedPath);
33045
+ if (relative !== "" && (relative.startsWith("..") || path15.isAbsolute(relative))) {
32608
33046
  throw new Error("Access outside of workspace is not allowed");
32609
33047
  }
32610
- const realRoot = await fs8.realpath(normalizedRoot);
33048
+ const realRoot = await fs9.realpath(normalizedRoot);
32611
33049
  try {
32612
- const realPath = await fs8.realpath(requestedPath);
32613
- const realRelative = path14.relative(realRoot, realPath);
32614
- if (realRelative !== "" && (realRelative.startsWith("..") || path14.isAbsolute(realRelative))) {
33050
+ const realPath = await fs9.realpath(requestedPath);
33051
+ const realRelative = path15.relative(realRoot, realPath);
33052
+ if (realRelative !== "" && (realRelative.startsWith("..") || path15.isAbsolute(realRelative))) {
32615
33053
  throw new Error("Access outside of workspace is not allowed");
32616
33054
  }
32617
33055
  return requestedPath;
@@ -32628,7 +33066,7 @@ async function buildEntryPayload({
32628
33066
  name,
32629
33067
  kind
32630
33068
  }) {
32631
- const stats = await fs8.stat(targetPath);
33069
+ const stats = await fs9.stat(targetPath);
32632
33070
  return {
32633
33071
  name,
32634
33072
  path: normalizeRelativePath({ root, targetPath }),
@@ -32642,10 +33080,10 @@ function isMissingEntryError(error) {
32642
33080
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
32643
33081
  }
32644
33082
  function normalizeRelativePath({ root, targetPath }) {
32645
- const normalizedRoot = path14.resolve(root);
32646
- const normalizedTarget = path14.resolve(targetPath);
32647
- const relative = path14.relative(normalizedRoot, normalizedTarget);
32648
- return relative === "" ? "." : relative.split(path14.sep).join("/");
33083
+ const normalizedRoot = path15.resolve(root);
33084
+ const normalizedTarget = path15.resolve(targetPath);
33085
+ const relative = path15.relative(normalizedRoot, normalizedTarget);
33086
+ return relative === "" ? "." : relative.split(path15.sep).join("/");
32649
33087
  }
32650
33088
  function textMimeTypeForExtension(ext) {
32651
33089
  return TEXT_MIME_TYPES[ext] ?? DEFAULT_TEXT_MIME_TYPE;
@@ -32998,342 +33436,18 @@ async function getProjectIcon(projectDir) {
32998
33436
  }
32999
33437
 
33000
33438
  // ../server/src/utils/path.ts
33001
- import os6 from "os";
33439
+ import os7 from "os";
33002
33440
  function expandTilde(path29) {
33003
33441
  if (path29.startsWith("~/")) {
33004
- const homeDir3 = process.env.HOME || os6.homedir();
33442
+ const homeDir3 = process.env.HOME || os7.homedir();
33005
33443
  return path29.replace("~", homeDir3);
33006
33444
  }
33007
33445
  if (path29 === "~") {
33008
- return process.env.HOME || os6.homedir();
33446
+ return process.env.HOME || os7.homedir();
33009
33447
  }
33010
33448
  return path29;
33011
33449
  }
33012
33450
 
33013
- // ../server/src/server/skills/scanner.ts
33014
- import fs9 from "node:fs/promises";
33015
- import os7 from "node:os";
33016
- import path15 from "node:path";
33017
- var NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/i;
33018
- function homeDir() {
33019
- return process.env.HOME || os7.homedir();
33020
- }
33021
- function codexHomeDir() {
33022
- return process.env.CODEX_HOME || path15.join(homeDir(), ".codex");
33023
- }
33024
- function resolveScopeDir(provider, scope, workspaceRoot) {
33025
- if (scope === "codex-prompts") {
33026
- if (provider !== "codex") {
33027
- throw new Error(`Scope "codex-prompts" is only valid for provider "codex"`);
33028
- }
33029
- return path15.join(codexHomeDir(), "prompts");
33030
- }
33031
- if (scope === "project") {
33032
- if (!workspaceRoot) {
33033
- throw new Error(`workspaceRoot is required for scope "project"`);
33034
- }
33035
- const dotDir = provider === "claude" ? ".claude" : ".codex";
33036
- return path15.join(workspaceRoot, dotDir, "skills");
33037
- }
33038
- if (provider === "claude") {
33039
- return path15.join(homeDir(), ".claude", "skills");
33040
- }
33041
- return path15.join(codexHomeDir(), "skills");
33042
- }
33043
- function allowedRoots(workspaceRoot) {
33044
- const roots = [
33045
- path15.join(homeDir(), ".claude", "skills"),
33046
- path15.join(codexHomeDir(), "skills"),
33047
- path15.join(codexHomeDir(), "prompts")
33048
- ];
33049
- if (workspaceRoot) {
33050
- roots.push(path15.join(workspaceRoot, ".claude", "skills"));
33051
- roots.push(path15.join(workspaceRoot, ".codex", "skills"));
33052
- }
33053
- return roots.map((r) => path15.resolve(r));
33054
- }
33055
- function isInsideAllowedRoot(absPath, workspaceRoot) {
33056
- const resolved = path15.resolve(absPath);
33057
- for (const root of allowedRoots(workspaceRoot)) {
33058
- const rel = path15.relative(root, resolved);
33059
- if (rel === "" || !rel.startsWith("..") && !path15.isAbsolute(rel)) {
33060
- return true;
33061
- }
33062
- }
33063
- return false;
33064
- }
33065
- var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
33066
- function parseSkillFile(text) {
33067
- const match = FRONTMATTER_RE.exec(text);
33068
- if (!match) {
33069
- return { rawFrontmatterLines: [], body: text, hadFrontmatter: false };
33070
- }
33071
- const yamlBlock = match[1] ?? "";
33072
- const body = match[2] ?? "";
33073
- return {
33074
- rawFrontmatterLines: yamlBlock.split(/\r?\n/),
33075
- body,
33076
- hadFrontmatter: true
33077
- };
33078
- }
33079
- function readDescription(text) {
33080
- const parsed = parseSkillFile(text);
33081
- if (!parsed.hadFrontmatter) return "";
33082
- for (const line of parsed.rawFrontmatterLines) {
33083
- const m = /^description\s*:\s*(.*)$/.exec(line);
33084
- if (m) {
33085
- const value = (m[1] ?? "").trim();
33086
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
33087
- return value.slice(1, -1);
33088
- }
33089
- return value;
33090
- }
33091
- }
33092
- return "";
33093
- }
33094
- function findOwnedSpans(lines) {
33095
- const spans = [];
33096
- let i = 0;
33097
- while (i < lines.length) {
33098
- const line = lines[i] ?? "";
33099
- const keyMatch = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line);
33100
- if (!keyMatch) {
33101
- i++;
33102
- continue;
33103
- }
33104
- const key = keyMatch[1];
33105
- const value = keyMatch[2];
33106
- if (key !== "description" && key !== "argument-hint" && key !== "allowed-tools") {
33107
- i++;
33108
- continue;
33109
- }
33110
- let end = i + 1;
33111
- if (value === "") {
33112
- while (end < lines.length) {
33113
- const next = lines[end] ?? "";
33114
- if (/^\s+-\s+/.test(next) || /^\s*$/.test(next)) {
33115
- end++;
33116
- } else {
33117
- break;
33118
- }
33119
- }
33120
- }
33121
- spans.push({ key, startLine: i, endLine: end });
33122
- i = end;
33123
- }
33124
- return spans;
33125
- }
33126
- function emitFrontmatterValue(key, value) {
33127
- if (key === "allowed-tools") {
33128
- if (!Array.isArray(value) || value.length === 0) return [];
33129
- return [`allowed-tools:`, ...value.map((v) => ` - ${v}`)];
33130
- }
33131
- const text = typeof value === "string" ? value : "";
33132
- if (text.length === 0) return [];
33133
- const needsQuote = /[:#\n]/.test(text) || text.startsWith(" ") || text.endsWith(" ");
33134
- const formatted = needsQuote ? `"${text.replace(/"/g, '\\"')}"` : text;
33135
- return [`${key}: ${formatted}`];
33136
- }
33137
- function rewriteFrontmatter(originalLines, next) {
33138
- const spans = findOwnedSpans(originalLines);
33139
- const ownedKeys = new Set(spans.map((s) => s.key));
33140
- const replacements = /* @__PURE__ */ new Map();
33141
- if (next.description !== void 0) {
33142
- replacements.set("description", emitFrontmatterValue("description", next.description));
33143
- }
33144
- if (next.argumentHint !== void 0) {
33145
- replacements.set("argument-hint", emitFrontmatterValue("argument-hint", next.argumentHint));
33146
- }
33147
- if (next.allowedTools !== void 0) {
33148
- replacements.set("allowed-tools", emitFrontmatterValue("allowed-tools", next.allowedTools));
33149
- }
33150
- const out = [];
33151
- let i = 0;
33152
- while (i < originalLines.length) {
33153
- const span = spans.find((s) => s.startLine === i);
33154
- if (span) {
33155
- if (replacements.has(span.key)) {
33156
- out.push(...replacements.get(span.key) ?? []);
33157
- } else {
33158
- for (let j = span.startLine; j < span.endLine; j++) {
33159
- out.push(originalLines[j] ?? "");
33160
- }
33161
- }
33162
- i = span.endLine;
33163
- continue;
33164
- }
33165
- out.push(originalLines[i] ?? "");
33166
- i++;
33167
- }
33168
- for (const key of ["description", "argument-hint", "allowed-tools"]) {
33169
- if (replacements.has(key) && !ownedKeys.has(key)) {
33170
- out.push(...replacements.get(key) ?? []);
33171
- }
33172
- }
33173
- return out;
33174
- }
33175
- async function listSkills(args) {
33176
- const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
33177
- let entries;
33178
- try {
33179
- entries = await fs9.readdir(dir, { withFileTypes: true });
33180
- } catch {
33181
- return [];
33182
- }
33183
- const results = [];
33184
- if (args.scope === "codex-prompts") {
33185
- for (const entry of entries) {
33186
- if (!entry.isFile()) continue;
33187
- if (!entry.name.endsWith(".md")) continue;
33188
- const name = entry.name.slice(0, -".md".length);
33189
- if (!name) continue;
33190
- const fullPath = path15.join(dir, entry.name);
33191
- const stat5 = await safeStat(fullPath);
33192
- if (!stat5) continue;
33193
- const description = await readDescriptionSafely(fullPath);
33194
- results.push({
33195
- id: `${args.provider}:${args.scope}:${name}`,
33196
- provider: args.provider,
33197
- scope: args.scope,
33198
- name,
33199
- path: fullPath,
33200
- description,
33201
- modifiedAt: stat5.mtime.toISOString(),
33202
- size: stat5.size
33203
- });
33204
- }
33205
- } else {
33206
- for (const entry of entries) {
33207
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
33208
- const skillDir = path15.join(dir, entry.name);
33209
- const skillPath = path15.join(skillDir, "SKILL.md");
33210
- const stat5 = await safeStat(skillPath);
33211
- if (!stat5) continue;
33212
- const description = await readDescriptionSafely(skillPath);
33213
- results.push({
33214
- id: `${args.provider}:${args.scope}:${entry.name}`,
33215
- provider: args.provider,
33216
- scope: args.scope,
33217
- name: entry.name,
33218
- path: skillPath,
33219
- description,
33220
- modifiedAt: stat5.mtime.toISOString(),
33221
- size: stat5.size
33222
- });
33223
- }
33224
- }
33225
- results.sort((a, b) => a.name.localeCompare(b.name));
33226
- return results;
33227
- }
33228
- async function safeStat(filePath) {
33229
- try {
33230
- const s = await fs9.stat(filePath);
33231
- return { mtime: s.mtime, size: s.size };
33232
- } catch {
33233
- return null;
33234
- }
33235
- }
33236
- async function readDescriptionSafely(filePath) {
33237
- try {
33238
- const text = await fs9.readFile(filePath, "utf8");
33239
- return readDescription(text);
33240
- } catch {
33241
- return "";
33242
- }
33243
- }
33244
- async function createSkill(args) {
33245
- if (!NAME_REGEX.test(args.name)) {
33246
- throw new Error(
33247
- `Invalid skill name "${args.name}". Use letters, digits, dot, underscore, dash.`
33248
- );
33249
- }
33250
- if (args.name.includes("/") || args.name.includes("\\") || args.name.includes("..")) {
33251
- throw new Error(`Skill name must not contain path separators or "..".`);
33252
- }
33253
- const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
33254
- await fs9.mkdir(dir, { recursive: true });
33255
- if (args.scope === "codex-prompts") {
33256
- const filePath2 = path15.join(dir, `${args.name}.md`);
33257
- try {
33258
- await fs9.access(filePath2);
33259
- throw new Error(`A prompt named "${args.name}" already exists at ${filePath2}`);
33260
- } catch (err) {
33261
- if (err && typeof err === "object" && "code" in err && err.code !== "ENOENT") {
33262
- throw err;
33263
- }
33264
- if (err instanceof Error && err.message.includes("already exists")) {
33265
- throw err;
33266
- }
33267
- }
33268
- const initial2 = buildStarterPrompt(args.name);
33269
- await fs9.writeFile(filePath2, initial2, "utf8");
33270
- return { path: filePath2 };
33271
- }
33272
- const skillDir = path15.join(dir, args.name);
33273
- let dirExists = false;
33274
- try {
33275
- const stat5 = await fs9.stat(skillDir);
33276
- dirExists = stat5.isDirectory();
33277
- } catch {
33278
- dirExists = false;
33279
- }
33280
- if (dirExists) {
33281
- throw new Error(`A skill named "${args.name}" already exists at ${skillDir}`);
33282
- }
33283
- await fs9.mkdir(skillDir, { recursive: true });
33284
- const filePath = path15.join(skillDir, "SKILL.md");
33285
- const initial = buildStarterSkill(args.name);
33286
- await fs9.writeFile(filePath, initial, "utf8");
33287
- return { path: filePath };
33288
- }
33289
- function buildStarterSkill(name) {
33290
- return `---
33291
- name: ${name}
33292
- description: ""
33293
- ---
33294
-
33295
- # ${name}
33296
-
33297
- Describe when this skill should be used and what it does. The body of this
33298
- file is loaded into the agent's context when the skill is invoked.
33299
- `;
33300
- }
33301
- function buildStarterPrompt(name) {
33302
- return `---
33303
- description: ""
33304
- argument-hint: ""
33305
- ---
33306
-
33307
- # ${name}
33308
-
33309
- Body of the prompt. Use \`$1\`, \`$2\`, ... or \`$ARGUMENTS\` for parameter expansion.
33310
- `;
33311
- }
33312
- async function writeSkillFrontmatter(args, workspaceRoot) {
33313
- if (!path15.isAbsolute(args.path)) {
33314
- throw new Error(`writeSkillFrontmatter expects an absolute path; got "${args.path}"`);
33315
- }
33316
- if (!isInsideAllowedRoot(args.path, workspaceRoot)) {
33317
- throw new Error(`Path "${args.path}" is not inside an allowlisted skill root`);
33318
- }
33319
- let original;
33320
- try {
33321
- original = await fs9.readFile(args.path, "utf8");
33322
- } catch (err) {
33323
- throw new Error(
33324
- `Failed to read skill file: ${err instanceof Error ? err.message : String(err)}`
33325
- );
33326
- }
33327
- const parsed = parseSkillFile(original);
33328
- const newLines = rewriteFrontmatter(parsed.rawFrontmatterLines, args.frontmatter);
33329
- const nextFrontmatter = ["---", ...newLines, "---"].join("\n");
33330
- const nextContent = parsed.hadFrontmatter ? `${nextFrontmatter}
33331
- ${parsed.body}` : `${nextFrontmatter}
33332
-
33333
- ${original}`;
33334
- await fs9.writeFile(args.path, nextContent, "utf8");
33335
- }
33336
-
33337
33451
  // ../server/src/utils/directory-suggestions.ts
33338
33452
  import { readdir as readdir2, realpath, stat as stat3 } from "node:fs/promises";
33339
33453
  import path16 from "node:path";
@@ -34972,10 +35086,12 @@ async function readBrandsFromDir(scope, dir) {
34972
35086
  continue;
34973
35087
  }
34974
35088
  let parsed;
35089
+ let body = "";
34975
35090
  try {
34976
35091
  const text = await fs11.readFile(fullPath, "utf8");
34977
- const { rawFrontmatterLines, hadFrontmatter } = parseBrandFile(text);
35092
+ const { rawFrontmatterLines, body: parsedBody, hadFrontmatter } = parseBrandFile(text);
34978
35093
  parsed = hadFrontmatter ? parseFrontmatter2(rawFrontmatterLines) : { description: "", variables: [] };
35094
+ body = parsedBody;
34979
35095
  } catch {
34980
35096
  parsed = { description: "", variables: [] };
34981
35097
  }
@@ -34987,7 +35103,8 @@ async function readBrandsFromDir(scope, dir) {
34987
35103
  variables: parsed.variables,
34988
35104
  path: fullPath,
34989
35105
  modifiedAt: stat5.mtime.toISOString(),
34990
- size: stat5.size
35106
+ size: stat5.size,
35107
+ body
34991
35108
  });
34992
35109
  }
34993
35110
  results.sort((a, b) => a.name.localeCompare(b.name));
@@ -35426,11 +35543,132 @@ async function generateAndApplyBrandTokens(options) {
35426
35543
  return { generatedCount: acceptedUpdates.size };
35427
35544
  }
35428
35545
 
35546
+ // ../server/src/server/brand/photography-analyzer.ts
35547
+ import { z as z38 } from "zod";
35548
+ var PhotographyResponseSchema = z38.object({
35549
+ dna: z38.string().min(1),
35550
+ dials: z38.record(z38.string(), z38.string())
35551
+ });
35552
+ function buildPrompt3(args) {
35553
+ const dialLines = args.selectVars.map((v) => {
35554
+ const options = (v.options ?? []).join(", ");
35555
+ return `- ${v.key} (${v.label || v.key}): current="${v.value || "(unset)"}" options=[${options}]`;
35556
+ }).join("\n");
35557
+ const refSection = args.referenceDescriptions.length > 0 ? [
35558
+ "",
35559
+ "Reference image filenames/descriptions provided by the user:",
35560
+ ...args.referenceDescriptions.map((r) => `- ${r}`)
35561
+ ].join("\n") : "";
35562
+ return [
35563
+ "You are a photography director analyzing a brand's photographic identity.",
35564
+ "The user has provided a brief describing the visual direction they want.",
35565
+ "Your job is to:",
35566
+ "1. Write a rich, specific Style DNA (2-3 sentences describing the photographic",
35567
+ " identity in concrete visual terms \u2014 lighting quality, tonal mood, depth",
35568
+ " characteristics, color rendering. No marketing fluff.)",
35569
+ "2. Pre-dial every camera control to match that style direction.",
35570
+ " ONLY use values that appear in the options list for each dial.",
35571
+ "",
35572
+ "User brief:",
35573
+ args.brief,
35574
+ refSection,
35575
+ "",
35576
+ "Camera dials (key, current value, valid options):",
35577
+ dialLines,
35578
+ "",
35579
+ "Return JSON only with the shape:",
35580
+ '{ "dna": "Rich style DNA description...", "dials": { "key": "value", ... } }',
35581
+ "",
35582
+ "Rules:",
35583
+ "- The dna field must be a non-empty string, 2-3 sentences.",
35584
+ "- Every value in dials must be one of the listed options for that key.",
35585
+ "- Include ALL dial keys in dials, even if you keep the current value.",
35586
+ "- Do not invent new keys. Do not output anything outside the JSON."
35587
+ ].filter((line) => line !== "").join("\n");
35588
+ }
35589
+ async function analyzeAndApplyPhotographyStyle(options) {
35590
+ const { agentManager, workspaceRoot, brandPath, brief, referenceDescriptions, logger } = options;
35591
+ const brands = await listBrands({ workspaceRoot });
35592
+ const photoBrand = brands.find((b) => b.path === brandPath) ?? null;
35593
+ if (!photoBrand) {
35594
+ throw new Error(`Brand file not found at ${brandPath}`);
35595
+ }
35596
+ const allVars = photoBrand.variables;
35597
+ const selectVars = allVars.filter(
35598
+ (v) => v.type === "select" && v.options && v.options.length > 0
35599
+ );
35600
+ if (selectVars.length === 0) {
35601
+ throw new Error("No camera dials found in photography brand file");
35602
+ }
35603
+ const validOptions = /* @__PURE__ */ new Map();
35604
+ for (const v of selectVars) {
35605
+ validOptions.set(v.key, new Set(v.options ?? []));
35606
+ }
35607
+ const agentPrompt = buildPrompt3({
35608
+ brief,
35609
+ referenceDescriptions: referenceDescriptions ?? [],
35610
+ selectVars
35611
+ });
35612
+ let response;
35613
+ try {
35614
+ response = await generateStructuredAgentResponseWithFallback({
35615
+ manager: agentManager,
35616
+ cwd: workspaceRoot,
35617
+ prompt: agentPrompt,
35618
+ schema: PhotographyResponseSchema,
35619
+ schemaName: "PhotographyAnalysis",
35620
+ maxRetries: 2,
35621
+ providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
35622
+ agentConfigOverrides: {
35623
+ title: "Photography style analyzer",
35624
+ internal: true
35625
+ }
35626
+ });
35627
+ } catch (error) {
35628
+ if (error instanceof StructuredAgentResponseError || error instanceof StructuredAgentFallbackError) {
35629
+ logger.warn({ err: error, brandPath }, "Structured photography analysis failed");
35630
+ throw new Error("Photography analysis failed \u2014 the agent did not return valid JSON");
35631
+ }
35632
+ throw error;
35633
+ }
35634
+ const acceptedUpdates = /* @__PURE__ */ new Map();
35635
+ for (const [key, value] of Object.entries(response.dials)) {
35636
+ if (typeof value !== "string") continue;
35637
+ const trimmed = value.trim();
35638
+ const opts = validOptions.get(key);
35639
+ if (!opts) continue;
35640
+ if (!opts.has(trimmed)) continue;
35641
+ acceptedUpdates.set(key, trimmed);
35642
+ }
35643
+ acceptedUpdates.set("photo.dna", response.dna);
35644
+ acceptedUpdates.set("photo.brief", brief);
35645
+ const nextVariables = allVars.map((v) => {
35646
+ const update = acceptedUpdates.get(v.key);
35647
+ if (update === void 0) return v;
35648
+ return { ...v, value: update };
35649
+ });
35650
+ await writeBrandFrontmatter(
35651
+ {
35652
+ path: brandPath,
35653
+ frontmatter: {
35654
+ description: photoBrand.description,
35655
+ variables: nextVariables
35656
+ }
35657
+ },
35658
+ workspaceRoot
35659
+ );
35660
+ logger.info(
35661
+ { brandPath, updatedCount: acceptedUpdates.size },
35662
+ "photography-analyzer: applied style analysis"
35663
+ );
35664
+ return { updatedCount: acceptedUpdates.size };
35665
+ }
35666
+
35429
35667
  // ../server/src/server/brand/layout-generator.ts
35430
35668
  import { promises as fs12 } from "node:fs";
35431
35669
  import path20 from "node:path";
35432
35670
  import { fileURLToPath as fileURLToPath2 } from "node:url";
35433
- import { z as z38 } from "zod";
35671
+ import { z as z39 } from "zod";
35434
35672
  var QA_FILENAME = "layout-qa.md";
35435
35673
  var PROMPT_FILENAME = "layout-prompt.md";
35436
35674
  var MAX_LOOKUP_LEVELS = 10;
@@ -35465,34 +35703,34 @@ async function writeRoleFile(workspaceRoot, content) {
35465
35703
  await fs12.mkdir(path20.dirname(filePath), { recursive: true });
35466
35704
  await fs12.writeFile(filePath, content, "utf8");
35467
35705
  }
35468
- var QaNextResponseSchema = z38.object({
35469
- done: z38.boolean(),
35470
- question: z38.string().optional(),
35471
- options: z38.array(z38.string()).optional(),
35472
- roleDocument: z38.string().optional()
35473
- });
35474
- var RefinementResponseSchema = z38.object({
35475
- roleDocument: z38.string()
35476
- });
35477
- var WireframeBlockSchema = z38.object({
35478
- type: z38.enum(["headline", "subheadline", "text", "image", "cta", "spacer"]),
35479
- widthFraction: z38.number(),
35480
- heightVh: z38.number()
35481
- });
35482
- var WireframeSectionSchema = z38.object({
35483
- type: z38.enum(["hero", "features", "content", "cta", "footer", "divider"]),
35484
- widthMode: z38.enum(["full-bleed", "contained"]),
35485
- heightVh: z38.number(),
35486
- arrangement: z38.enum(["single-column", "split", "asymmetric-grid", "bento", "stacked"]),
35487
- blocks: z38.array(WireframeBlockSchema),
35488
- bg: z38.enum(["neutral", "alt", "accent", "image"]),
35489
- gapAfterVh: z38.number()
35490
- });
35491
- var WireframeDataSchema = z38.object({
35492
- viewport: z38.object({ width: z38.number(), height: z38.number() }),
35493
- maxWidth: z38.number(),
35494
- pageBg: z38.string(),
35495
- sections: z38.array(WireframeSectionSchema)
35706
+ var QaNextResponseSchema = z39.object({
35707
+ done: z39.boolean(),
35708
+ question: z39.string().optional(),
35709
+ options: z39.array(z39.string()).optional(),
35710
+ roleDocument: z39.string().optional()
35711
+ });
35712
+ var RefinementResponseSchema = z39.object({
35713
+ roleDocument: z39.string()
35714
+ });
35715
+ var WireframeBlockSchema = z39.object({
35716
+ type: z39.enum(["headline", "subheadline", "text", "image", "cta", "spacer"]),
35717
+ widthFraction: z39.number(),
35718
+ heightVh: z39.number()
35719
+ });
35720
+ var WireframeSectionSchema = z39.object({
35721
+ type: z39.enum(["hero", "features", "content", "cta", "footer", "divider"]),
35722
+ widthMode: z39.enum(["full-bleed", "contained"]),
35723
+ heightVh: z39.number(),
35724
+ arrangement: z39.enum(["single-column", "split", "asymmetric-grid", "bento", "stacked"]),
35725
+ blocks: z39.array(WireframeBlockSchema),
35726
+ bg: z39.enum(["neutral", "alt", "accent", "image"]),
35727
+ gapAfterVh: z39.number()
35728
+ });
35729
+ var WireframeDataSchema = z39.object({
35730
+ viewport: z39.object({ width: z39.number(), height: z39.number() }),
35731
+ maxWidth: z39.number(),
35732
+ pageBg: z39.string(),
35733
+ sections: z39.array(WireframeSectionSchema)
35496
35734
  });
35497
35735
  function buildFullBrandContext(allBrands) {
35498
35736
  const lines = [];
@@ -37167,7 +37405,7 @@ var MIN_STREAMING_SEGMENT_DURATION_MS = 1e3;
37167
37405
  var MIN_STREAMING_SEGMENT_BYTES = Math.round(
37168
37406
  PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS
37169
37407
  );
37170
- var AgentIdSchema2 = z39.string().uuid();
37408
+ var AgentIdSchema2 = z40.string().uuid();
37171
37409
  var VOICE_INTERRUPT_CONFIRMATION_MS = 500;
37172
37410
  var VoiceFeatureUnavailableError = class extends Error {
37173
37411
  constructor(context) {
@@ -38548,6 +38786,9 @@ var Session = class _Session {
38548
38786
  case "brands/generate-wireframe":
38549
38787
  await this.handleBrandGenerateWireframeRequest(msg);
38550
38788
  break;
38789
+ case "brands/analyze-photography":
38790
+ await this.handleBrandAnalyzePhotographyRequest(msg);
38791
+ break;
38551
38792
  case "rightfont/library":
38552
38793
  await this.handleRightFontLibraryRequest(msg);
38553
38794
  break;
@@ -40207,8 +40448,8 @@ var Session = class _Session {
40207
40448
  }
40208
40449
  async generateCommitMessage(cwd) {
40209
40450
  const files = await listUncommittedFiles(cwd);
40210
- const schema = z39.object({
40211
- message: z39.string().min(1).max(100).describe(
40451
+ const schema = z40.object({
40452
+ message: z40.string().min(1).max(100).describe(
40212
40453
  "Short feature-level summary, lowercase, imperative mood, no trailing period, no filename references."
40213
40454
  )
40214
40455
  });
@@ -40253,9 +40494,9 @@ var Session = class _Session {
40253
40494
  },
40254
40495
  { appostleHome: this.appostleHome }
40255
40496
  );
40256
- const schema = z39.object({
40257
- title: z39.string().min(1).max(72),
40258
- body: z39.string().min(1)
40497
+ const schema = z40.object({
40498
+ title: z40.string().min(1).max(72),
40499
+ body: z40.string().min(1)
40259
40500
  });
40260
40501
  const fileList = diff.structured && diff.structured.length > 0 ? [
40261
40502
  "Files changed:",
@@ -45747,6 +45988,38 @@ ${details}`.trim());
45747
45988
  });
45748
45989
  }
45749
45990
  }
45991
+ async handleBrandAnalyzePhotographyRequest(request) {
45992
+ const { requestId, workspaceRoot, brandPath, brief, referenceDescriptions } = request;
45993
+ try {
45994
+ const result = await analyzeAndApplyPhotographyStyle({
45995
+ agentManager: this.agentManager,
45996
+ workspaceRoot: expandTilde(workspaceRoot),
45997
+ brandPath: expandTilde(brandPath),
45998
+ brief,
45999
+ referenceDescriptions,
46000
+ logger: this.sessionLogger
46001
+ });
46002
+ this.emit({
46003
+ type: "brands/analyze-photography/response",
46004
+ payload: {
46005
+ requestId,
46006
+ updatedCount: result.updatedCount,
46007
+ error: null
46008
+ }
46009
+ });
46010
+ } catch (error) {
46011
+ const message = error instanceof Error ? error.message : String(error);
46012
+ this.sessionLogger.error({ err: error, brandPath }, "Failed to analyze photography style");
46013
+ this.emit({
46014
+ type: "brands/analyze-photography/response",
46015
+ payload: {
46016
+ requestId,
46017
+ updatedCount: 0,
46018
+ error: message
46019
+ }
46020
+ });
46021
+ }
46022
+ }
45750
46023
  async handleRightFontLibraryRequest(request) {
45751
46024
  const { requestId, libraryPath } = request;
45752
46025
  try {
@@ -46308,7 +46581,7 @@ import webpush from "web-push";
46308
46581
  import webpush2 from "web-push";
46309
46582
 
46310
46583
  // ../server/src/server/speech/providers/local/sherpa/model-catalog.ts
46311
- import { z as z40 } from "zod";
46584
+ import { z as z41 } from "zod";
46312
46585
  var SHERPA_ONNX_MODEL_CATALOG = {
46313
46586
  "zipformer-bilingual-zh-en-2023-02-20": {
46314
46587
  kind: "stt-online",
@@ -46401,7 +46674,7 @@ function buildAliasMap(modelIds) {
46401
46674
  }
46402
46675
  function createAliasedModelIdSchema(params) {
46403
46676
  const validIds = new Set(params.modelIds);
46404
- return z40.string().trim().toLowerCase().refine(
46677
+ return z41.string().trim().toLowerCase().refine(
46405
46678
  (value) => validIds.has(value) || Object.prototype.hasOwnProperty.call(params.aliases, value),
46406
46679
  {
46407
46680
  message: "Invalid model id"
@@ -46432,20 +46705,20 @@ import { v4 as uuidv410 } from "uuid";
46432
46705
  import { v4 as uuidv411 } from "uuid";
46433
46706
 
46434
46707
  // ../server/src/server/speech/providers/openai/config.ts
46435
- import { z as z41 } from "zod";
46708
+ import { z as z42 } from "zod";
46436
46709
  var DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
46437
46710
  var DEFAULT_OPENAI_TTS_MODEL = "tts-1";
46438
- var OpenAiTtsVoiceSchema = z41.enum(["alloy", "echo", "fable", "onyx", "nova", "shimmer"]);
46439
- var OpenAiTtsModelSchema = z41.enum(["tts-1", "tts-1-hd"]);
46440
- var NumberLikeSchema = z41.union([z41.number(), z41.string().trim().min(1)]);
46441
- var OptionalFiniteNumberSchema = NumberLikeSchema.pipe(z41.coerce.number().finite()).optional();
46442
- var OptionalTrimmedStringSchema = z41.string().trim().optional().transform((value) => value && value.length > 0 ? value : void 0);
46443
- var OpenAiSpeechResolutionSchema = z41.object({
46711
+ var OpenAiTtsVoiceSchema = z42.enum(["alloy", "echo", "fable", "onyx", "nova", "shimmer"]);
46712
+ var OpenAiTtsModelSchema = z42.enum(["tts-1", "tts-1-hd"]);
46713
+ var NumberLikeSchema = z42.union([z42.number(), z42.string().trim().min(1)]);
46714
+ var OptionalFiniteNumberSchema = NumberLikeSchema.pipe(z42.coerce.number().finite()).optional();
46715
+ var OptionalTrimmedStringSchema = z42.string().trim().optional().transform((value) => value && value.length > 0 ? value : void 0);
46716
+ var OpenAiSpeechResolutionSchema = z42.object({
46444
46717
  apiKey: OptionalTrimmedStringSchema,
46445
46718
  sttConfidenceThreshold: OptionalFiniteNumberSchema,
46446
46719
  sttModel: OptionalTrimmedStringSchema,
46447
- ttsVoice: z41.string().trim().toLowerCase().pipe(OpenAiTtsVoiceSchema).default("alloy"),
46448
- ttsModel: z41.string().trim().toLowerCase().pipe(OpenAiTtsModelSchema).default(DEFAULT_OPENAI_TTS_MODEL),
46720
+ ttsVoice: z42.string().trim().toLowerCase().pipe(OpenAiTtsVoiceSchema).default("alloy"),
46721
+ ttsModel: z42.string().trim().toLowerCase().pipe(OpenAiTtsModelSchema).default(DEFAULT_OPENAI_TTS_MODEL),
46449
46722
  realtimeTranscriptionModel: OptionalTrimmedStringSchema.default(
46450
46723
  DEFAULT_OPENAI_REALTIME_TRANSCRIPTION_MODEL
46451
46724
  )
@@ -46490,177 +46763,177 @@ import { v4 } from "uuid";
46490
46763
  import OpenAI2 from "openai";
46491
46764
 
46492
46765
  // ../server/src/server/agent/agent-storage.ts
46493
- import { z as z42 } from "zod";
46494
- var SERIALIZABLE_CONFIG_SCHEMA = z42.object({
46495
- title: z42.string().nullable().optional(),
46496
- modeId: z42.string().nullable().optional(),
46497
- model: z42.string().nullable().optional(),
46498
- thinkingOptionId: z42.string().nullable().optional(),
46499
- featureValues: z42.record(z42.unknown()).nullable().optional(),
46500
- extra: z42.record(z42.any()).nullable().optional(),
46501
- systemPrompt: z42.string().nullable().optional(),
46502
- mcpServers: z42.record(z42.any()).nullable().optional()
46766
+ import { z as z43 } from "zod";
46767
+ var SERIALIZABLE_CONFIG_SCHEMA = z43.object({
46768
+ title: z43.string().nullable().optional(),
46769
+ modeId: z43.string().nullable().optional(),
46770
+ model: z43.string().nullable().optional(),
46771
+ thinkingOptionId: z43.string().nullable().optional(),
46772
+ featureValues: z43.record(z43.unknown()).nullable().optional(),
46773
+ extra: z43.record(z43.any()).nullable().optional(),
46774
+ systemPrompt: z43.string().nullable().optional(),
46775
+ mcpServers: z43.record(z43.any()).nullable().optional()
46503
46776
  }).nullable().optional();
46504
- var PERSISTENCE_HANDLE_SCHEMA = z42.object({
46505
- provider: z42.string(),
46506
- sessionId: z42.string(),
46507
- nativeHandle: z42.any().optional(),
46508
- metadata: z42.record(z42.any()).optional()
46777
+ var PERSISTENCE_HANDLE_SCHEMA = z43.object({
46778
+ provider: z43.string(),
46779
+ sessionId: z43.string(),
46780
+ nativeHandle: z43.any().optional(),
46781
+ metadata: z43.record(z43.any()).optional()
46509
46782
  }).nullable().optional();
46510
- var STORED_AGENT_SCHEMA = z42.object({
46511
- id: z42.string(),
46512
- provider: z42.string(),
46513
- cwd: z42.string(),
46514
- createdAt: z42.string(),
46515
- updatedAt: z42.string(),
46516
- lastActivityAt: z42.string().optional(),
46517
- lastUserMessageAt: z42.string().nullable().optional(),
46518
- title: z42.string().nullable().optional(),
46519
- labels: z42.record(z42.string()).default({}),
46783
+ var STORED_AGENT_SCHEMA = z43.object({
46784
+ id: z43.string(),
46785
+ provider: z43.string(),
46786
+ cwd: z43.string(),
46787
+ createdAt: z43.string(),
46788
+ updatedAt: z43.string(),
46789
+ lastActivityAt: z43.string().optional(),
46790
+ lastUserMessageAt: z43.string().nullable().optional(),
46791
+ title: z43.string().nullable().optional(),
46792
+ labels: z43.record(z43.string()).default({}),
46520
46793
  lastStatus: AgentStatusSchema.default("closed"),
46521
- lastModeId: z42.string().nullable().optional(),
46794
+ lastModeId: z43.string().nullable().optional(),
46522
46795
  config: SERIALIZABLE_CONFIG_SCHEMA,
46523
- runtimeInfo: z42.object({
46524
- provider: z42.string(),
46525
- sessionId: z42.string().nullable(),
46526
- model: z42.string().nullable().optional(),
46527
- thinkingOptionId: z42.string().nullable().optional(),
46528
- modeId: z42.string().nullable().optional(),
46529
- extra: z42.record(z42.unknown()).optional()
46796
+ runtimeInfo: z43.object({
46797
+ provider: z43.string(),
46798
+ sessionId: z43.string().nullable(),
46799
+ model: z43.string().nullable().optional(),
46800
+ thinkingOptionId: z43.string().nullable().optional(),
46801
+ modeId: z43.string().nullable().optional(),
46802
+ extra: z43.record(z43.unknown()).optional()
46530
46803
  }).optional(),
46531
- features: z42.array(AgentFeatureSchema).optional(),
46804
+ features: z43.array(AgentFeatureSchema).optional(),
46532
46805
  persistence: PERSISTENCE_HANDLE_SCHEMA,
46533
- lastError: z42.string().nullable().optional(),
46534
- requiresAttention: z42.boolean().optional(),
46535
- attentionReason: z42.enum(["finished", "error", "permission"]).nullable().optional(),
46536
- attentionTimestamp: z42.string().nullable().optional(),
46537
- internal: z42.boolean().optional(),
46538
- archivedAt: z42.string().nullable().optional(),
46806
+ lastError: z43.string().nullable().optional(),
46807
+ requiresAttention: z43.boolean().optional(),
46808
+ attentionReason: z43.enum(["finished", "error", "permission"]).nullable().optional(),
46809
+ attentionTimestamp: z43.string().nullable().optional(),
46810
+ internal: z43.boolean().optional(),
46811
+ archivedAt: z43.string().nullable().optional(),
46539
46812
  // Fork lineage (optional for backward compat with pre-fork records).
46540
- parentAgentId: z42.string().optional(),
46541
- forkedFromMessageUuid: z42.string().optional(),
46813
+ parentAgentId: z43.string().optional(),
46814
+ forkedFromMessageUuid: z43.string().optional(),
46542
46815
  // Multi-tenant session isolation: the auth-server user-id that created
46543
46816
  // (and therefore owns) this agent + the additive ACL of other users
46544
46817
  // granted access. Both optional/null-default for backward compatibility
46545
46818
  // with pre-Phase-2c records — those load with `null` owner and stay
46546
46819
  // visible to every connecting user (matches today's single-tenant
46547
46820
  // behavior). Set on new agents at create time via Session.ownerUserId.
46548
- ownerUserId: z42.string().nullable().optional(),
46549
- sharedWithUserIds: z42.array(z42.string()).default([]),
46821
+ ownerUserId: z43.string().nullable().optional(),
46822
+ sharedWithUserIds: z43.array(z43.string()).default([]),
46550
46823
  // Owner's display username — needed on resume to route to the correct
46551
46824
  // `CLAUDE_CONFIG_DIR` via `ensureClaudeProfile(username)` for agents
46552
46825
  // created by a shared (non-owner) user. Without this, a resumed shared-
46553
46826
  // user agent would silently fall back to the daemon owner's Claude
46554
46827
  // profile/subscription. Optional — pre-Phase-3 records rehydrate without
46555
46828
  // per-user profile routing.
46556
- ownerUsername: z42.string().optional()
46829
+ ownerUsername: z43.string().optional()
46557
46830
  });
46558
46831
 
46559
46832
  // ../server/src/server/agent/mcp-server.ts
46560
46833
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
46561
- import { z as z43 } from "zod";
46562
- var TerminalSummarySchema = z43.object({
46563
- id: z43.string(),
46564
- name: z43.string(),
46565
- cwd: z43.string()
46834
+ import { z as z44 } from "zod";
46835
+ var TerminalSummarySchema = z44.object({
46836
+ id: z44.string(),
46837
+ name: z44.string(),
46838
+ cwd: z44.string()
46566
46839
  });
46567
- var WorktreeSummarySchema = z43.object({
46568
- path: z43.string(),
46569
- createdAt: z43.string(),
46570
- branchName: z43.string().optional(),
46571
- head: z43.string().optional()
46840
+ var WorktreeSummarySchema = z44.object({
46841
+ path: z44.string(),
46842
+ createdAt: z44.string(),
46843
+ branchName: z44.string().optional(),
46844
+ head: z44.string().optional()
46572
46845
  });
46573
46846
 
46574
46847
  // ../server/src/server/loop-service.ts
46575
- import { z as z44 } from "zod";
46848
+ import { z as z45 } from "zod";
46576
46849
  var MAX_VERIFY_OUTPUT_BYTES = 64 * 1024;
46577
- var LoopVerifyPromptSchema = z44.object({
46578
- passed: z44.boolean(),
46579
- reason: z44.string().min(1)
46580
- });
46581
- var LoopLogEntrySchema2 = z44.object({
46582
- seq: z44.number().int().positive(),
46583
- timestamp: z44.string(),
46584
- iteration: z44.number().int().positive().nullable(),
46585
- source: z44.enum(["loop", "worker", "verifier", "verify-check"]),
46586
- level: z44.enum(["info", "error"]),
46587
- text: z44.string()
46588
- });
46589
- var LoopVerifyCheckResultSchema2 = z44.object({
46590
- command: z44.string(),
46591
- exitCode: z44.number().int(),
46592
- passed: z44.boolean(),
46593
- stdout: z44.string(),
46594
- stderr: z44.string(),
46595
- startedAt: z44.string(),
46596
- completedAt: z44.string()
46597
- });
46598
- var LoopVerifyPromptResultSchema2 = z44.object({
46599
- passed: z44.boolean(),
46600
- reason: z44.string(),
46601
- verifierAgentId: z44.string().nullable(),
46602
- startedAt: z44.string(),
46603
- completedAt: z44.string()
46604
- });
46605
- var LoopIterationRecordSchema2 = z44.object({
46606
- index: z44.number().int().positive(),
46607
- workerAgentId: z44.string().nullable(),
46608
- workerStartedAt: z44.string(),
46609
- workerCompletedAt: z44.string().nullable(),
46610
- verifierAgentId: z44.string().nullable(),
46611
- status: z44.enum(["running", "succeeded", "failed", "stopped"]),
46612
- workerOutcome: z44.enum(["completed", "failed", "canceled"]).nullable(),
46613
- failureReason: z44.string().nullable(),
46614
- verifyChecks: z44.array(LoopVerifyCheckResultSchema2),
46850
+ var LoopVerifyPromptSchema = z45.object({
46851
+ passed: z45.boolean(),
46852
+ reason: z45.string().min(1)
46853
+ });
46854
+ var LoopLogEntrySchema2 = z45.object({
46855
+ seq: z45.number().int().positive(),
46856
+ timestamp: z45.string(),
46857
+ iteration: z45.number().int().positive().nullable(),
46858
+ source: z45.enum(["loop", "worker", "verifier", "verify-check"]),
46859
+ level: z45.enum(["info", "error"]),
46860
+ text: z45.string()
46861
+ });
46862
+ var LoopVerifyCheckResultSchema2 = z45.object({
46863
+ command: z45.string(),
46864
+ exitCode: z45.number().int(),
46865
+ passed: z45.boolean(),
46866
+ stdout: z45.string(),
46867
+ stderr: z45.string(),
46868
+ startedAt: z45.string(),
46869
+ completedAt: z45.string()
46870
+ });
46871
+ var LoopVerifyPromptResultSchema2 = z45.object({
46872
+ passed: z45.boolean(),
46873
+ reason: z45.string(),
46874
+ verifierAgentId: z45.string().nullable(),
46875
+ startedAt: z45.string(),
46876
+ completedAt: z45.string()
46877
+ });
46878
+ var LoopIterationRecordSchema2 = z45.object({
46879
+ index: z45.number().int().positive(),
46880
+ workerAgentId: z45.string().nullable(),
46881
+ workerStartedAt: z45.string(),
46882
+ workerCompletedAt: z45.string().nullable(),
46883
+ verifierAgentId: z45.string().nullable(),
46884
+ status: z45.enum(["running", "succeeded", "failed", "stopped"]),
46885
+ workerOutcome: z45.enum(["completed", "failed", "canceled"]).nullable(),
46886
+ failureReason: z45.string().nullable(),
46887
+ verifyChecks: z45.array(LoopVerifyCheckResultSchema2),
46615
46888
  verifyPrompt: LoopVerifyPromptResultSchema2.nullable()
46616
46889
  });
46617
- var LoopRecordSchema2 = z44.object({
46618
- id: z44.string(),
46619
- name: z44.string().nullable(),
46620
- prompt: z44.string(),
46621
- cwd: z44.string(),
46622
- provider: z44.string(),
46623
- model: z44.string().nullable(),
46624
- workerProvider: z44.string().nullable(),
46625
- workerModel: z44.string().nullable(),
46626
- verifierProvider: z44.string().nullable(),
46627
- verifierModel: z44.string().nullable(),
46628
- verifyPrompt: z44.string().nullable(),
46629
- verifyChecks: z44.array(z44.string()),
46630
- archive: z44.boolean(),
46631
- sleepMs: z44.number().int().nonnegative(),
46632
- maxIterations: z44.number().int().positive().nullable(),
46633
- maxTimeMs: z44.number().int().positive().nullable(),
46634
- status: z44.enum(["running", "succeeded", "failed", "stopped"]),
46635
- createdAt: z44.string(),
46636
- updatedAt: z44.string(),
46637
- startedAt: z44.string(),
46638
- completedAt: z44.string().nullable(),
46639
- stopRequestedAt: z44.string().nullable(),
46640
- iterations: z44.array(LoopIterationRecordSchema2),
46641
- logs: z44.array(LoopLogEntrySchema2),
46642
- nextLogSeq: z44.number().int().positive(),
46643
- activeIteration: z44.number().int().positive().nullable(),
46644
- activeWorkerAgentId: z44.string().nullable(),
46645
- activeVerifierAgentId: z44.string().nullable()
46646
- });
46647
- var StoredLoopsSchema = z44.array(LoopRecordSchema2);
46890
+ var LoopRecordSchema2 = z45.object({
46891
+ id: z45.string(),
46892
+ name: z45.string().nullable(),
46893
+ prompt: z45.string(),
46894
+ cwd: z45.string(),
46895
+ provider: z45.string(),
46896
+ model: z45.string().nullable(),
46897
+ workerProvider: z45.string().nullable(),
46898
+ workerModel: z45.string().nullable(),
46899
+ verifierProvider: z45.string().nullable(),
46900
+ verifierModel: z45.string().nullable(),
46901
+ verifyPrompt: z45.string().nullable(),
46902
+ verifyChecks: z45.array(z45.string()),
46903
+ archive: z45.boolean(),
46904
+ sleepMs: z45.number().int().nonnegative(),
46905
+ maxIterations: z45.number().int().positive().nullable(),
46906
+ maxTimeMs: z45.number().int().positive().nullable(),
46907
+ status: z45.enum(["running", "succeeded", "failed", "stopped"]),
46908
+ createdAt: z45.string(),
46909
+ updatedAt: z45.string(),
46910
+ startedAt: z45.string(),
46911
+ completedAt: z45.string().nullable(),
46912
+ stopRequestedAt: z45.string().nullable(),
46913
+ iterations: z45.array(LoopIterationRecordSchema2),
46914
+ logs: z45.array(LoopLogEntrySchema2),
46915
+ nextLogSeq: z45.number().int().positive(),
46916
+ activeIteration: z45.number().int().positive().nullable(),
46917
+ activeWorkerAgentId: z45.string().nullable(),
46918
+ activeVerifierAgentId: z45.string().nullable()
46919
+ });
46920
+ var StoredLoopsSchema = z45.array(LoopRecordSchema2);
46648
46921
 
46649
46922
  // ../server/src/server/quest/store.ts
46650
- import { z as z45 } from "zod";
46651
- var StoredQuestsSchema = z45.array(QuestRecordSchema);
46923
+ import { z as z46 } from "zod";
46924
+ var StoredQuestsSchema = z46.array(QuestRecordSchema);
46652
46925
 
46653
46926
  // ../server/src/server/quest/quest-metadata-generator.ts
46654
- import { z as z46 } from "zod";
46927
+ import { z as z47 } from "zod";
46655
46928
 
46656
46929
  // ../server/src/server/quest/orchestrator-mcp.ts
46657
46930
  import { createSdkMcpServer as createSdkMcpServer2, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
46658
- import { z as z47 } from "zod";
46931
+ import { z as z48 } from "zod";
46659
46932
  var HANDOFF_INPUT_SHAPE = {
46660
- rolePath: z47.string().min(1).describe(
46933
+ rolePath: z48.string().min(1).describe(
46661
46934
  "Absolute filesystem path to the role .md file the worker should adopt as its system prompt. Must be one of the role file paths listed in your team roster \u2014 do not invent paths."
46662
46935
  ),
46663
- task: z47.string().min(1).describe(
46936
+ task: z48.string().min(1).describe(
46664
46937
  "Concrete, self-contained instruction for the worker. The worker has its own toolbelt and reads the role at rolePath as its system prompt \u2014 write the task as a normal user message describing what to do, what inputs to read, and where to write outputs. Reference the hivemind doc by absolute path if relevant."
46665
46938
  )
46666
46939
  };
@@ -46715,13 +46988,13 @@ var execFileAsync3 = promisify4(execFile3);
46715
46988
  var MAX_VERIFY_OUTPUT_BYTES2 = 64 * 1024;
46716
46989
 
46717
46990
  // ../server/src/shared/connection-offer.ts
46718
- import { z as z48 } from "zod";
46719
- var ConnectionOfferV2Schema = z48.object({
46720
- v: z48.literal(2),
46721
- serverId: z48.string().min(1),
46722
- daemonPublicKeyB64: z48.string().min(1),
46723
- relay: z48.object({
46724
- endpoint: z48.string().min(1)
46991
+ import { z as z49 } from "zod";
46992
+ var ConnectionOfferV2Schema = z49.object({
46993
+ v: z49.literal(2),
46994
+ serverId: z49.string().min(1),
46995
+ daemonPublicKeyB64: z49.string().min(1),
46996
+ relay: z49.object({
46997
+ endpoint: z49.string().min(1)
46725
46998
  })
46726
46999
  });
46727
47000
 
@@ -46755,21 +47028,21 @@ function isRelayClientWebSocketUrl(url) {
46755
47028
 
46756
47029
  // ../server/src/server/config.ts
46757
47030
  import path22 from "node:path";
46758
- import { z as z52 } from "zod";
47031
+ import { z as z53 } from "zod";
46759
47032
 
46760
47033
  // ../server/src/server/speech/speech-config-resolver.ts
46761
- import { z as z51 } from "zod";
47034
+ import { z as z52 } from "zod";
46762
47035
 
46763
47036
  // ../server/src/server/speech/providers/local/config.ts
46764
47037
  import path21 from "node:path";
46765
- import { z as z49 } from "zod";
47038
+ import { z as z50 } from "zod";
46766
47039
  var DEFAULT_LOCAL_MODELS_SUBDIR = path21.join("models", "local-speech");
46767
- var NumberLikeSchema2 = z49.union([z49.number(), z49.string().trim().min(1)]);
46768
- var OptionalFiniteNumberSchema2 = NumberLikeSchema2.pipe(z49.coerce.number().finite()).optional();
46769
- var OptionalIntegerSchema = NumberLikeSchema2.pipe(z49.coerce.number().int()).optional();
46770
- var LocalSpeechResolutionSchema = z49.object({
46771
- includeProviderConfig: z49.boolean(),
46772
- modelsDir: z49.string().trim().min(1),
47040
+ var NumberLikeSchema2 = z50.union([z50.number(), z50.string().trim().min(1)]);
47041
+ var OptionalFiniteNumberSchema2 = NumberLikeSchema2.pipe(z50.coerce.number().finite()).optional();
47042
+ var OptionalIntegerSchema = NumberLikeSchema2.pipe(z50.coerce.number().int()).optional();
47043
+ var LocalSpeechResolutionSchema = z50.object({
47044
+ includeProviderConfig: z50.boolean(),
47045
+ modelsDir: z50.string().trim().min(1),
46773
47046
  dictationLocalSttModel: LocalSttModelIdSchema.default(DEFAULT_LOCAL_STT_MODEL),
46774
47047
  voiceLocalSttModel: LocalSttModelIdSchema.default(DEFAULT_LOCAL_STT_MODEL),
46775
47048
  voiceLocalTtsModel: LocalTtsModelIdSchema.default(DEFAULT_LOCAL_TTS_MODEL),
@@ -46825,17 +47098,17 @@ function resolveLocalSpeechConfig(params) {
46825
47098
  }
46826
47099
 
46827
47100
  // ../server/src/server/speech/speech-types.ts
46828
- import { z as z50 } from "zod";
46829
- var SpeechProviderIdSchema2 = z50.enum(["openai", "local"]);
46830
- var RequestedSpeechProviderSchema = z50.object({
47101
+ import { z as z51 } from "zod";
47102
+ var SpeechProviderIdSchema2 = z51.enum(["openai", "local"]);
47103
+ var RequestedSpeechProviderSchema = z51.object({
46831
47104
  provider: SpeechProviderIdSchema2,
46832
- explicit: z50.boolean(),
46833
- enabled: z50.boolean().optional()
47105
+ explicit: z51.boolean(),
47106
+ enabled: z51.boolean().optional()
46834
47107
  });
46835
47108
 
46836
47109
  // ../server/src/server/speech/speech-config-resolver.ts
46837
- var OptionalSpeechProviderSchema = z51.string().trim().toLowerCase().pipe(SpeechProviderIdSchema2).optional();
46838
- var OptionalBooleanFlagSchema = z51.union([z51.boolean(), z51.string().trim().toLowerCase()]).optional().transform((value) => {
47110
+ var OptionalSpeechProviderSchema = z52.string().trim().toLowerCase().pipe(SpeechProviderIdSchema2).optional();
47111
+ var OptionalBooleanFlagSchema = z52.union([z52.boolean(), z52.string().trim().toLowerCase()]).optional().transform((value) => {
46839
47112
  if (typeof value === "boolean") {
46840
47113
  return value;
46841
47114
  }
@@ -46850,7 +47123,7 @@ var OptionalBooleanFlagSchema = z51.union([z51.boolean(), z51.string().trim().to
46850
47123
  }
46851
47124
  return void 0;
46852
47125
  });
46853
- var RequestedSpeechProvidersSchema = z51.object({
47126
+ var RequestedSpeechProvidersSchema = z52.object({
46854
47127
  dictationStt: OptionalSpeechProviderSchema.default("local"),
46855
47128
  voiceTurnDetection: OptionalSpeechProviderSchema.default("local"),
46856
47129
  voiceStt: OptionalSpeechProviderSchema.default("local"),
@@ -46959,9 +47232,9 @@ function parseBooleanEnv(value) {
46959
47232
  }
46960
47233
  return void 0;
46961
47234
  }
46962
- var OptionalVoiceLlmProviderSchema = z52.union([z52.string(), z52.null(), z52.undefined()]).transform(
47235
+ var OptionalVoiceLlmProviderSchema = z53.union([z53.string(), z53.null(), z53.undefined()]).transform(
46963
47236
  (value) => typeof value === "string" ? value.trim().toLowerCase() : null
46964
- ).pipe(z52.union([AgentProviderSchema, z52.null()]));
47237
+ ).pipe(z53.union([AgentProviderSchema, z53.null()]));
46965
47238
  function parseOptionalVoiceLlmProvider(value) {
46966
47239
  const parsed = OptionalVoiceLlmProviderSchema.safeParse(value);
46967
47240
  return parsed.success ? parsed.data : null;
@@ -47014,6 +47287,7 @@ function loadConfig(appostleHome, options) {
47014
47287
  const mcpEnabled = options?.cli?.mcpEnabled ?? persisted.daemon?.mcp?.enabled ?? true;
47015
47288
  const mcpInjectIntoAgents = options?.cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false;
47016
47289
  const chromeEnabled = persisted.daemon?.chrome?.enabled ?? true;
47290
+ const playwrightEnabled = persisted.daemon?.playwright?.enabled ?? true;
47017
47291
  const daemonIcon = persisted.daemon?.identity?.icon;
47018
47292
  const relayEnabled = options?.cli?.relayEnabled ?? parseBooleanEnv(env.APPOSTLE_RELAY_ENABLED) ?? persisted.daemon?.relay?.enabled ?? true;
47019
47293
  const relayEndpoint = env.APPOSTLE_RELAY_ENDPOINT ?? persisted.daemon?.relay?.endpoint ?? DEFAULT_RELAY_ENDPOINT;
@@ -47044,6 +47318,7 @@ function loadConfig(appostleHome, options) {
47044
47318
  mcpEnabled,
47045
47319
  mcpInjectIntoAgents,
47046
47320
  chromeEnabled,
47321
+ playwrightEnabled,
47047
47322
  mcpDebug: env.MCP_DEBUG === "1",
47048
47323
  daemonIcon,
47049
47324
  agentStoragePath: path22.join(appostleHome, "agents"),
@@ -47335,6 +47610,9 @@ function createEncryptedTransport(base, daemonPublicKeyB64, logger, staticKeyPai
47335
47610
  throw new Error("Encrypted channel not ready");
47336
47611
  }
47337
47612
  void channel.send(normalizeTransportPayload(data)).catch((error) => {
47613
+ if (closed) {
47614
+ console.error("[relay-e2ee] send failed after transport close:", error);
47615
+ }
47338
47616
  emitError(error);
47339
47617
  });
47340
47618
  },
@@ -50937,6 +51215,20 @@ var DaemonClient = class {
50937
51215
  timeout: 12e4
50938
51216
  });
50939
51217
  }
51218
+ async brandsAnalyzePhotography(options) {
51219
+ return this.sendCorrelatedSessionRequest({
51220
+ requestId: options.requestId,
51221
+ message: {
51222
+ type: "brands/analyze-photography",
51223
+ workspaceRoot: options.workspaceRoot,
51224
+ brandPath: options.brandPath,
51225
+ brief: options.brief,
51226
+ ...options.referenceDescriptions ? { referenceDescriptions: options.referenceDescriptions } : {}
51227
+ },
51228
+ responseType: "brands/analyze-photography/response",
51229
+ timeout: 12e4
51230
+ });
51231
+ }
50940
51232
  onTerminalStreamEvent(handler) {
50941
51233
  this.terminalStreamListeners.add(handler);
50942
51234
  return () => {