appostle-installer 0.0.22 → 0.0.24

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
@@ -3528,10 +3528,14 @@ var MutableDaemonConfigSchema = z11.object({
3528
3528
  icon: z11.string().optional()
3529
3529
  }).passthrough().optional(),
3530
3530
  chrome: z11.object({
3531
- // When false, agents on this host won't get the --chrome flag,
3532
- // preventing the client's Chrome browser from being controlled.
3533
- // Useful on headless servers where browser tools should target
3534
- // 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.
3535
3539
  enabled: z11.boolean()
3536
3540
  }).passthrough().optional()
3537
3541
  }).passthrough();
@@ -3540,7 +3544,8 @@ var MutableDaemonConfigPatchSchema = z11.object({
3540
3544
  identity: z11.object({
3541
3545
  icon: z11.string().optional()
3542
3546
  }).partial().passthrough().optional(),
3543
- chrome: MutableDaemonConfigSchema.shape.chrome
3547
+ chrome: MutableDaemonConfigSchema.shape.chrome,
3548
+ playwright: MutableDaemonConfigSchema.shape.playwright
3544
3549
  }).partial().passthrough();
3545
3550
  var AgentStatusSchema = z11.enum(AGENT_LIFECYCLE_STATUSES);
3546
3551
  var AgentModeSchema = z11.object({
@@ -7694,6 +7699,9 @@ var PersistedConfigSchema = z14.object({
7694
7699
  }).strict().optional(),
7695
7700
  chrome: z14.object({
7696
7701
  enabled: z14.boolean().optional()
7702
+ }).strict().optional(),
7703
+ playwright: z14.object({
7704
+ enabled: z14.boolean().optional()
7697
7705
  }).strict().optional()
7698
7706
  }).strict().transform(({ allowedHosts, ...daemon }) => {
7699
7707
  const hostnames = daemon.hostnames ?? allowedHosts;
@@ -18514,6 +18522,7 @@ var ClaudeAgentClient = class {
18514
18522
  runtimeSettings: this.runtimeSettings,
18515
18523
  launchEnv: launchContext?.env,
18516
18524
  chromeEnabled: launchContext?.chromeEnabled,
18525
+ playwrightEnabled: launchContext?.playwrightEnabled,
18517
18526
  logger: this.logger,
18518
18527
  queryFactory: this.queryFactory
18519
18528
  });
@@ -18532,6 +18541,7 @@ var ClaudeAgentClient = class {
18532
18541
  handle,
18533
18542
  launchEnv: launchContext?.env,
18534
18543
  chromeEnabled: launchContext?.chromeEnabled,
18544
+ playwrightEnabled: launchContext?.playwrightEnabled,
18535
18545
  logger: this.logger,
18536
18546
  queryFactory: this.queryFactory
18537
18547
  });
@@ -18846,6 +18856,7 @@ var ClaudeAgentSession = class {
18846
18856
  this.config = config;
18847
18857
  this.launchEnv = options.launchEnv;
18848
18858
  this.chromeEnabled = options.chromeEnabled ?? true;
18859
+ this.playwrightEnabled = options.playwrightEnabled ?? true;
18849
18860
  this.defaults = options.defaults;
18850
18861
  this.runtimeSettings = options.runtimeSettings;
18851
18862
  this.logger = options.logger;
@@ -19609,7 +19620,8 @@ var ClaudeAgentSession = class {
19609
19620
  } else {
19610
19621
  const homeMcps = loadHomeMcpServers(this.logger);
19611
19622
  if (homeMcps) {
19612
- 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);
19613
19625
  }
19614
19626
  }
19615
19627
  if (this.config.model) {
@@ -31527,6 +31539,330 @@ function removeClaudeProfile(username, logger) {
31527
31539
  import { z as z34 } from "zod";
31528
31540
  import { getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
31529
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
+
31530
31866
  // ../server/src/server/agent/handoff-mcp.ts
31531
31867
  import { createSdkMcpServer, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
31532
31868
  import { z as z33 } from "zod";
@@ -32499,8 +32835,8 @@ function isVoicePermissionAllowed(request) {
32499
32835
  }
32500
32836
 
32501
32837
  // ../server/src/server/file-explorer/service.ts
32502
- import { promises as fs8 } from "fs";
32503
- import path14 from "path";
32838
+ import { promises as fs9 } from "fs";
32839
+ import path15 from "path";
32504
32840
 
32505
32841
  // ../server/src/server/path-utils.ts
32506
32842
  import { homedir as homedir2 } from "node:os";
@@ -32547,14 +32883,14 @@ async function listDirectoryEntries({
32547
32883
  relativePath = "."
32548
32884
  }) {
32549
32885
  const directoryPath = await resolveScopedPath({ root, relativePath });
32550
- const stats = await fs8.stat(directoryPath);
32886
+ const stats = await fs9.stat(directoryPath);
32551
32887
  if (!stats.isDirectory()) {
32552
32888
  throw new Error("Requested path is not a directory");
32553
32889
  }
32554
- const dirents = await fs8.readdir(directoryPath, { withFileTypes: true });
32890
+ const dirents = await fs9.readdir(directoryPath, { withFileTypes: true });
32555
32891
  const entriesWithNulls = await Promise.all(
32556
32892
  dirents.map(async (dirent) => {
32557
- const targetPath = path14.join(directoryPath, dirent.name);
32893
+ const targetPath = path15.join(directoryPath, dirent.name);
32558
32894
  const kind = dirent.isDirectory() ? "directory" : "file";
32559
32895
  try {
32560
32896
  return await buildEntryPayload({
@@ -32589,18 +32925,18 @@ async function readExplorerFile({
32589
32925
  relativePath
32590
32926
  }) {
32591
32927
  const filePath = await resolveScopedPath({ root, relativePath });
32592
- const stats = await fs8.stat(filePath);
32928
+ const stats = await fs9.stat(filePath);
32593
32929
  if (!stats.isFile()) {
32594
32930
  throw new Error("Requested path is not a file");
32595
32931
  }
32596
- const ext = path14.extname(filePath).toLowerCase();
32932
+ const ext = path15.extname(filePath).toLowerCase();
32597
32933
  const basePayload = {
32598
32934
  path: normalizeRelativePath({ root, targetPath: filePath }),
32599
32935
  size: stats.size,
32600
32936
  modifiedAt: stats.mtime.toISOString()
32601
32937
  };
32602
32938
  if (ext in IMAGE_MIME_TYPES) {
32603
- const buffer2 = await fs8.readFile(filePath);
32939
+ const buffer2 = await fs9.readFile(filePath);
32604
32940
  return {
32605
32941
  ...basePayload,
32606
32942
  kind: "image",
@@ -32609,7 +32945,7 @@ async function readExplorerFile({
32609
32945
  mimeType: IMAGE_MIME_TYPES[ext]
32610
32946
  };
32611
32947
  }
32612
- const buffer = await fs8.readFile(filePath);
32948
+ const buffer = await fs9.readFile(filePath);
32613
32949
  if (isLikelyBinary(buffer)) {
32614
32950
  return {
32615
32951
  ...basePayload,
@@ -32631,34 +32967,34 @@ async function writeTextFile({
32631
32967
  relativePath,
32632
32968
  content
32633
32969
  }) {
32634
- const ext = path14.extname(relativePath).toLowerCase();
32970
+ const ext = path15.extname(relativePath).toLowerCase();
32635
32971
  if (ext in IMAGE_MIME_TYPES) {
32636
32972
  throw new Error(`Refusing to write '${relativePath}': binary/image file`);
32637
32973
  }
32638
32974
  const filePath = await resolveScopedPath({ root, relativePath });
32639
32975
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
32640
- await fs8.writeFile(tempPath, content, "utf8");
32641
- await fs8.rename(tempPath, filePath);
32976
+ await fs9.writeFile(tempPath, content, "utf8");
32977
+ await fs9.rename(tempPath, filePath);
32642
32978
  }
32643
32979
  async function deleteFile({ root, relativePath }) {
32644
- const ext = path14.extname(relativePath).toLowerCase();
32980
+ const ext = path15.extname(relativePath).toLowerCase();
32645
32981
  if (ext !== ".md") {
32646
32982
  throw new Error(`Refusing to delete '${relativePath}': only .md files allowed`);
32647
32983
  }
32648
32984
  const filePath = await resolveScopedPath({ root, relativePath });
32649
- const stats = await fs8.stat(filePath);
32985
+ const stats = await fs9.stat(filePath);
32650
32986
  if (!stats.isFile()) {
32651
32987
  throw new Error("Requested path is not a file");
32652
32988
  }
32653
- await fs8.unlink(filePath);
32989
+ await fs9.unlink(filePath);
32654
32990
  }
32655
32991
  async function deleteEntry({ root, relativePath }) {
32656
32992
  const entryPath = await resolveScopedPath({ root, relativePath });
32657
- await fs8.rm(entryPath, { recursive: true, force: false });
32993
+ await fs9.rm(entryPath, { recursive: true, force: false });
32658
32994
  }
32659
32995
  async function createDirectory({ root, relativePath }) {
32660
32996
  const dirPath = await resolveScopedPath({ root, relativePath });
32661
- await fs8.mkdir(dirPath, { recursive: true });
32997
+ await fs9.mkdir(dirPath, { recursive: true });
32662
32998
  }
32663
32999
  async function moveEntry({
32664
33000
  root,
@@ -32667,22 +33003,22 @@ async function moveEntry({
32667
33003
  }) {
32668
33004
  const src = await resolveScopedPath({ root, relativePath: sourcePath });
32669
33005
  const dest = await resolveScopedPath({ root, relativePath: destinationPath });
32670
- await fs8.rename(src, dest);
33006
+ await fs9.rename(src, dest);
32671
33007
  }
32672
33008
  async function getDownloadableFileInfo({ root, relativePath }) {
32673
33009
  const filePath = await resolveScopedPath({ root, relativePath });
32674
- const stats = await fs8.stat(filePath);
33010
+ const stats = await fs9.stat(filePath);
32675
33011
  if (!stats.isFile()) {
32676
33012
  throw new Error("Requested path is not a file");
32677
33013
  }
32678
- const ext = path14.extname(filePath).toLowerCase();
33014
+ const ext = path15.extname(filePath).toLowerCase();
32679
33015
  let mimeType = "application/octet-stream";
32680
33016
  if (ext in IMAGE_MIME_TYPES) {
32681
33017
  mimeType = IMAGE_MIME_TYPES[ext] ?? mimeType;
32682
33018
  } else if (ext in FONT_MIME_TYPES) {
32683
33019
  mimeType = FONT_MIME_TYPES[ext] ?? mimeType;
32684
33020
  } else {
32685
- const handle = await fs8.open(filePath, "r");
33021
+ const handle = await fs9.open(filePath, "r");
32686
33022
  const sample = Buffer.alloc(8192);
32687
33023
  try {
32688
33024
  const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
@@ -32697,23 +33033,23 @@ async function getDownloadableFileInfo({ root, relativePath }) {
32697
33033
  return {
32698
33034
  path: normalizeRelativePath({ root, targetPath: filePath }),
32699
33035
  absolutePath: filePath,
32700
- fileName: path14.basename(filePath),
33036
+ fileName: path15.basename(filePath),
32701
33037
  mimeType,
32702
33038
  size: stats.size
32703
33039
  };
32704
33040
  }
32705
33041
  async function resolveScopedPath({ root, relativePath = "." }) {
32706
- const normalizedRoot = path14.resolve(root);
33042
+ const normalizedRoot = path15.resolve(root);
32707
33043
  const requestedPath = resolvePathFromBase(normalizedRoot, relativePath);
32708
- const relative = path14.relative(normalizedRoot, requestedPath);
32709
- if (relative !== "" && (relative.startsWith("..") || path14.isAbsolute(relative))) {
33044
+ const relative = path15.relative(normalizedRoot, requestedPath);
33045
+ if (relative !== "" && (relative.startsWith("..") || path15.isAbsolute(relative))) {
32710
33046
  throw new Error("Access outside of workspace is not allowed");
32711
33047
  }
32712
- const realRoot = await fs8.realpath(normalizedRoot);
33048
+ const realRoot = await fs9.realpath(normalizedRoot);
32713
33049
  try {
32714
- const realPath = await fs8.realpath(requestedPath);
32715
- const realRelative = path14.relative(realRoot, realPath);
32716
- 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))) {
32717
33053
  throw new Error("Access outside of workspace is not allowed");
32718
33054
  }
32719
33055
  return requestedPath;
@@ -32730,7 +33066,7 @@ async function buildEntryPayload({
32730
33066
  name,
32731
33067
  kind
32732
33068
  }) {
32733
- const stats = await fs8.stat(targetPath);
33069
+ const stats = await fs9.stat(targetPath);
32734
33070
  return {
32735
33071
  name,
32736
33072
  path: normalizeRelativePath({ root, targetPath }),
@@ -32744,10 +33080,10 @@ function isMissingEntryError(error) {
32744
33080
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
32745
33081
  }
32746
33082
  function normalizeRelativePath({ root, targetPath }) {
32747
- const normalizedRoot = path14.resolve(root);
32748
- const normalizedTarget = path14.resolve(targetPath);
32749
- const relative = path14.relative(normalizedRoot, normalizedTarget);
32750
- 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("/");
32751
33087
  }
32752
33088
  function textMimeTypeForExtension(ext) {
32753
33089
  return TEXT_MIME_TYPES[ext] ?? DEFAULT_TEXT_MIME_TYPE;
@@ -33100,342 +33436,18 @@ async function getProjectIcon(projectDir) {
33100
33436
  }
33101
33437
 
33102
33438
  // ../server/src/utils/path.ts
33103
- import os6 from "os";
33439
+ import os7 from "os";
33104
33440
  function expandTilde(path29) {
33105
33441
  if (path29.startsWith("~/")) {
33106
- const homeDir3 = process.env.HOME || os6.homedir();
33442
+ const homeDir3 = process.env.HOME || os7.homedir();
33107
33443
  return path29.replace("~", homeDir3);
33108
33444
  }
33109
33445
  if (path29 === "~") {
33110
- return process.env.HOME || os6.homedir();
33446
+ return process.env.HOME || os7.homedir();
33111
33447
  }
33112
33448
  return path29;
33113
33449
  }
33114
33450
 
33115
- // ../server/src/server/skills/scanner.ts
33116
- import fs9 from "node:fs/promises";
33117
- import os7 from "node:os";
33118
- import path15 from "node:path";
33119
- var NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/i;
33120
- function homeDir() {
33121
- return process.env.HOME || os7.homedir();
33122
- }
33123
- function codexHomeDir() {
33124
- return process.env.CODEX_HOME || path15.join(homeDir(), ".codex");
33125
- }
33126
- function resolveScopeDir(provider, scope, workspaceRoot) {
33127
- if (scope === "codex-prompts") {
33128
- if (provider !== "codex") {
33129
- throw new Error(`Scope "codex-prompts" is only valid for provider "codex"`);
33130
- }
33131
- return path15.join(codexHomeDir(), "prompts");
33132
- }
33133
- if (scope === "project") {
33134
- if (!workspaceRoot) {
33135
- throw new Error(`workspaceRoot is required for scope "project"`);
33136
- }
33137
- const dotDir = provider === "claude" ? ".claude" : ".codex";
33138
- return path15.join(workspaceRoot, dotDir, "skills");
33139
- }
33140
- if (provider === "claude") {
33141
- return path15.join(homeDir(), ".claude", "skills");
33142
- }
33143
- return path15.join(codexHomeDir(), "skills");
33144
- }
33145
- function allowedRoots(workspaceRoot) {
33146
- const roots = [
33147
- path15.join(homeDir(), ".claude", "skills"),
33148
- path15.join(codexHomeDir(), "skills"),
33149
- path15.join(codexHomeDir(), "prompts")
33150
- ];
33151
- if (workspaceRoot) {
33152
- roots.push(path15.join(workspaceRoot, ".claude", "skills"));
33153
- roots.push(path15.join(workspaceRoot, ".codex", "skills"));
33154
- }
33155
- return roots.map((r) => path15.resolve(r));
33156
- }
33157
- function isInsideAllowedRoot(absPath, workspaceRoot) {
33158
- const resolved = path15.resolve(absPath);
33159
- for (const root of allowedRoots(workspaceRoot)) {
33160
- const rel = path15.relative(root, resolved);
33161
- if (rel === "" || !rel.startsWith("..") && !path15.isAbsolute(rel)) {
33162
- return true;
33163
- }
33164
- }
33165
- return false;
33166
- }
33167
- var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
33168
- function parseSkillFile(text) {
33169
- const match = FRONTMATTER_RE.exec(text);
33170
- if (!match) {
33171
- return { rawFrontmatterLines: [], body: text, hadFrontmatter: false };
33172
- }
33173
- const yamlBlock = match[1] ?? "";
33174
- const body = match[2] ?? "";
33175
- return {
33176
- rawFrontmatterLines: yamlBlock.split(/\r?\n/),
33177
- body,
33178
- hadFrontmatter: true
33179
- };
33180
- }
33181
- function readDescription(text) {
33182
- const parsed = parseSkillFile(text);
33183
- if (!parsed.hadFrontmatter) return "";
33184
- for (const line of parsed.rawFrontmatterLines) {
33185
- const m = /^description\s*:\s*(.*)$/.exec(line);
33186
- if (m) {
33187
- const value = (m[1] ?? "").trim();
33188
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
33189
- return value.slice(1, -1);
33190
- }
33191
- return value;
33192
- }
33193
- }
33194
- return "";
33195
- }
33196
- function findOwnedSpans(lines) {
33197
- const spans = [];
33198
- let i = 0;
33199
- while (i < lines.length) {
33200
- const line = lines[i] ?? "";
33201
- const keyMatch = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line);
33202
- if (!keyMatch) {
33203
- i++;
33204
- continue;
33205
- }
33206
- const key = keyMatch[1];
33207
- const value = keyMatch[2];
33208
- if (key !== "description" && key !== "argument-hint" && key !== "allowed-tools") {
33209
- i++;
33210
- continue;
33211
- }
33212
- let end = i + 1;
33213
- if (value === "") {
33214
- while (end < lines.length) {
33215
- const next = lines[end] ?? "";
33216
- if (/^\s+-\s+/.test(next) || /^\s*$/.test(next)) {
33217
- end++;
33218
- } else {
33219
- break;
33220
- }
33221
- }
33222
- }
33223
- spans.push({ key, startLine: i, endLine: end });
33224
- i = end;
33225
- }
33226
- return spans;
33227
- }
33228
- function emitFrontmatterValue(key, value) {
33229
- if (key === "allowed-tools") {
33230
- if (!Array.isArray(value) || value.length === 0) return [];
33231
- return [`allowed-tools:`, ...value.map((v) => ` - ${v}`)];
33232
- }
33233
- const text = typeof value === "string" ? value : "";
33234
- if (text.length === 0) return [];
33235
- const needsQuote = /[:#\n]/.test(text) || text.startsWith(" ") || text.endsWith(" ");
33236
- const formatted = needsQuote ? `"${text.replace(/"/g, '\\"')}"` : text;
33237
- return [`${key}: ${formatted}`];
33238
- }
33239
- function rewriteFrontmatter(originalLines, next) {
33240
- const spans = findOwnedSpans(originalLines);
33241
- const ownedKeys = new Set(spans.map((s) => s.key));
33242
- const replacements = /* @__PURE__ */ new Map();
33243
- if (next.description !== void 0) {
33244
- replacements.set("description", emitFrontmatterValue("description", next.description));
33245
- }
33246
- if (next.argumentHint !== void 0) {
33247
- replacements.set("argument-hint", emitFrontmatterValue("argument-hint", next.argumentHint));
33248
- }
33249
- if (next.allowedTools !== void 0) {
33250
- replacements.set("allowed-tools", emitFrontmatterValue("allowed-tools", next.allowedTools));
33251
- }
33252
- const out = [];
33253
- let i = 0;
33254
- while (i < originalLines.length) {
33255
- const span = spans.find((s) => s.startLine === i);
33256
- if (span) {
33257
- if (replacements.has(span.key)) {
33258
- out.push(...replacements.get(span.key) ?? []);
33259
- } else {
33260
- for (let j = span.startLine; j < span.endLine; j++) {
33261
- out.push(originalLines[j] ?? "");
33262
- }
33263
- }
33264
- i = span.endLine;
33265
- continue;
33266
- }
33267
- out.push(originalLines[i] ?? "");
33268
- i++;
33269
- }
33270
- for (const key of ["description", "argument-hint", "allowed-tools"]) {
33271
- if (replacements.has(key) && !ownedKeys.has(key)) {
33272
- out.push(...replacements.get(key) ?? []);
33273
- }
33274
- }
33275
- return out;
33276
- }
33277
- async function listSkills(args) {
33278
- const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
33279
- let entries;
33280
- try {
33281
- entries = await fs9.readdir(dir, { withFileTypes: true });
33282
- } catch {
33283
- return [];
33284
- }
33285
- const results = [];
33286
- if (args.scope === "codex-prompts") {
33287
- for (const entry of entries) {
33288
- if (!entry.isFile()) continue;
33289
- if (!entry.name.endsWith(".md")) continue;
33290
- const name = entry.name.slice(0, -".md".length);
33291
- if (!name) continue;
33292
- const fullPath = path15.join(dir, entry.name);
33293
- const stat5 = await safeStat(fullPath);
33294
- if (!stat5) continue;
33295
- const description = await readDescriptionSafely(fullPath);
33296
- results.push({
33297
- id: `${args.provider}:${args.scope}:${name}`,
33298
- provider: args.provider,
33299
- scope: args.scope,
33300
- name,
33301
- path: fullPath,
33302
- description,
33303
- modifiedAt: stat5.mtime.toISOString(),
33304
- size: stat5.size
33305
- });
33306
- }
33307
- } else {
33308
- for (const entry of entries) {
33309
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
33310
- const skillDir = path15.join(dir, entry.name);
33311
- const skillPath = path15.join(skillDir, "SKILL.md");
33312
- const stat5 = await safeStat(skillPath);
33313
- if (!stat5) continue;
33314
- const description = await readDescriptionSafely(skillPath);
33315
- results.push({
33316
- id: `${args.provider}:${args.scope}:${entry.name}`,
33317
- provider: args.provider,
33318
- scope: args.scope,
33319
- name: entry.name,
33320
- path: skillPath,
33321
- description,
33322
- modifiedAt: stat5.mtime.toISOString(),
33323
- size: stat5.size
33324
- });
33325
- }
33326
- }
33327
- results.sort((a, b) => a.name.localeCompare(b.name));
33328
- return results;
33329
- }
33330
- async function safeStat(filePath) {
33331
- try {
33332
- const s = await fs9.stat(filePath);
33333
- return { mtime: s.mtime, size: s.size };
33334
- } catch {
33335
- return null;
33336
- }
33337
- }
33338
- async function readDescriptionSafely(filePath) {
33339
- try {
33340
- const text = await fs9.readFile(filePath, "utf8");
33341
- return readDescription(text);
33342
- } catch {
33343
- return "";
33344
- }
33345
- }
33346
- async function createSkill(args) {
33347
- if (!NAME_REGEX.test(args.name)) {
33348
- throw new Error(
33349
- `Invalid skill name "${args.name}". Use letters, digits, dot, underscore, dash.`
33350
- );
33351
- }
33352
- if (args.name.includes("/") || args.name.includes("\\") || args.name.includes("..")) {
33353
- throw new Error(`Skill name must not contain path separators or "..".`);
33354
- }
33355
- const dir = resolveScopeDir(args.provider, args.scope, args.workspaceRoot);
33356
- await fs9.mkdir(dir, { recursive: true });
33357
- if (args.scope === "codex-prompts") {
33358
- const filePath2 = path15.join(dir, `${args.name}.md`);
33359
- try {
33360
- await fs9.access(filePath2);
33361
- throw new Error(`A prompt named "${args.name}" already exists at ${filePath2}`);
33362
- } catch (err) {
33363
- if (err && typeof err === "object" && "code" in err && err.code !== "ENOENT") {
33364
- throw err;
33365
- }
33366
- if (err instanceof Error && err.message.includes("already exists")) {
33367
- throw err;
33368
- }
33369
- }
33370
- const initial2 = buildStarterPrompt(args.name);
33371
- await fs9.writeFile(filePath2, initial2, "utf8");
33372
- return { path: filePath2 };
33373
- }
33374
- const skillDir = path15.join(dir, args.name);
33375
- let dirExists = false;
33376
- try {
33377
- const stat5 = await fs9.stat(skillDir);
33378
- dirExists = stat5.isDirectory();
33379
- } catch {
33380
- dirExists = false;
33381
- }
33382
- if (dirExists) {
33383
- throw new Error(`A skill named "${args.name}" already exists at ${skillDir}`);
33384
- }
33385
- await fs9.mkdir(skillDir, { recursive: true });
33386
- const filePath = path15.join(skillDir, "SKILL.md");
33387
- const initial = buildStarterSkill(args.name);
33388
- await fs9.writeFile(filePath, initial, "utf8");
33389
- return { path: filePath };
33390
- }
33391
- function buildStarterSkill(name) {
33392
- return `---
33393
- name: ${name}
33394
- description: ""
33395
- ---
33396
-
33397
- # ${name}
33398
-
33399
- Describe when this skill should be used and what it does. The body of this
33400
- file is loaded into the agent's context when the skill is invoked.
33401
- `;
33402
- }
33403
- function buildStarterPrompt(name) {
33404
- return `---
33405
- description: ""
33406
- argument-hint: ""
33407
- ---
33408
-
33409
- # ${name}
33410
-
33411
- Body of the prompt. Use \`$1\`, \`$2\`, ... or \`$ARGUMENTS\` for parameter expansion.
33412
- `;
33413
- }
33414
- async function writeSkillFrontmatter(args, workspaceRoot) {
33415
- if (!path15.isAbsolute(args.path)) {
33416
- throw new Error(`writeSkillFrontmatter expects an absolute path; got "${args.path}"`);
33417
- }
33418
- if (!isInsideAllowedRoot(args.path, workspaceRoot)) {
33419
- throw new Error(`Path "${args.path}" is not inside an allowlisted skill root`);
33420
- }
33421
- let original;
33422
- try {
33423
- original = await fs9.readFile(args.path, "utf8");
33424
- } catch (err) {
33425
- throw new Error(
33426
- `Failed to read skill file: ${err instanceof Error ? err.message : String(err)}`
33427
- );
33428
- }
33429
- const parsed = parseSkillFile(original);
33430
- const newLines = rewriteFrontmatter(parsed.rawFrontmatterLines, args.frontmatter);
33431
- const nextFrontmatter = ["---", ...newLines, "---"].join("\n");
33432
- const nextContent = parsed.hadFrontmatter ? `${nextFrontmatter}
33433
- ${parsed.body}` : `${nextFrontmatter}
33434
-
33435
- ${original}`;
33436
- await fs9.writeFile(args.path, nextContent, "utf8");
33437
- }
33438
-
33439
33451
  // ../server/src/utils/directory-suggestions.ts
33440
33452
  import { readdir as readdir2, realpath, stat as stat3 } from "node:fs/promises";
33441
33453
  import path16 from "node:path";
@@ -47275,6 +47287,7 @@ function loadConfig(appostleHome, options) {
47275
47287
  const mcpEnabled = options?.cli?.mcpEnabled ?? persisted.daemon?.mcp?.enabled ?? true;
47276
47288
  const mcpInjectIntoAgents = options?.cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false;
47277
47289
  const chromeEnabled = persisted.daemon?.chrome?.enabled ?? true;
47290
+ const playwrightEnabled = persisted.daemon?.playwright?.enabled ?? true;
47278
47291
  const daemonIcon = persisted.daemon?.identity?.icon;
47279
47292
  const relayEnabled = options?.cli?.relayEnabled ?? parseBooleanEnv(env.APPOSTLE_RELAY_ENABLED) ?? persisted.daemon?.relay?.enabled ?? true;
47280
47293
  const relayEndpoint = env.APPOSTLE_RELAY_ENDPOINT ?? persisted.daemon?.relay?.endpoint ?? DEFAULT_RELAY_ENDPOINT;
@@ -47305,6 +47318,7 @@ function loadConfig(appostleHome, options) {
47305
47318
  mcpEnabled,
47306
47319
  mcpInjectIntoAgents,
47307
47320
  chromeEnabled,
47321
+ playwrightEnabled,
47308
47322
  mcpDebug: env.MCP_DEBUG === "1",
47309
47323
  daemonIcon,
47310
47324
  agentStoragePath: path22.join(appostleHome, "agents"),