webmux 0.10.0 → 0.11.0

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.
@@ -7399,10 +7399,12 @@ function parseLifecycleHooks(raw) {
7399
7399
  function parseAutoName(raw) {
7400
7400
  if (!isRecord(raw))
7401
7401
  return null;
7402
- if (typeof raw.model !== "string" || !raw.model.trim())
7402
+ const provider = raw.provider;
7403
+ if (provider !== "claude" && provider !== "codex")
7403
7404
  return null;
7404
7405
  return {
7405
- model: raw.model.trim(),
7406
+ provider,
7407
+ ...typeof raw.model === "string" && raw.model.trim() ? { model: raw.model.trim() } : {},
7406
7408
  ...typeof raw.system_prompt === "string" && raw.system_prompt.trim() ? { systemPrompt: raw.system_prompt.trim() } : {}
7407
7409
  };
7408
7410
  }
@@ -9751,7 +9753,11 @@ function buildErrorMessage(name, exitCode, stdout, stderr) {
9751
9753
  return `${name} hook failed (exit ${exitCode})`;
9752
9754
  }
9753
9755
  function hasDirenv() {
9754
- return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
9756
+ try {
9757
+ return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
9758
+ } catch {
9759
+ return false;
9760
+ }
9755
9761
  }
9756
9762
 
9757
9763
  class BunLifecycleHookRunner {
@@ -9853,47 +9859,16 @@ class BunPortProbe {
9853
9859
  }
9854
9860
 
9855
9861
  // backend/src/services/auto-name-service.ts
9856
- var BRANCH_NAME_SCHEMA = {
9857
- type: "object",
9858
- properties: {
9859
- branch_name: {
9860
- type: "string",
9861
- description: "A lowercase kebab-case git branch name with no prefix"
9862
- }
9863
- },
9864
- required: ["branch_name"],
9865
- additionalProperties: false
9866
- };
9867
- var GEMINI_BRANCH_NAME_SCHEMA = {
9868
- ...BRANCH_NAME_SCHEMA,
9869
- propertyOrdering: ["branch_name"]
9870
- };
9871
9862
  var DEFAULT_SYSTEM_PROMPT = [
9872
9863
  "Generate a concise git branch name from the task description.",
9873
9864
  "Return only the branch name.",
9874
9865
  "Use lowercase kebab-case.",
9875
9866
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
9876
9867
  ].join(" ");
9877
- function isRecord4(value) {
9878
- return typeof value === "object" && value !== null && !Array.isArray(value);
9879
- }
9880
9868
  function buildPrompt(task) {
9881
9869
  return `Task description:
9882
9870
  ${task.trim()}`;
9883
9871
  }
9884
- function parseBranchNamePayload(raw) {
9885
- if (!isRecord4(raw) || typeof raw.branch_name !== "string") {
9886
- throw new Error("Auto-name response did not include branch_name");
9887
- }
9888
- return raw.branch_name;
9889
- }
9890
- function parseJsonText(text) {
9891
- try {
9892
- return JSON.parse(text);
9893
- } catch {
9894
- throw new Error(`Auto-name response was not valid JSON: ${text}`);
9895
- }
9896
- }
9897
9872
  function normalizeGeneratedBranchName(raw) {
9898
9873
  let branch = raw.trim();
9899
9874
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -9913,231 +9888,84 @@ function normalizeGeneratedBranchName(raw) {
9913
9888
  }
9914
9889
  return branch;
9915
9890
  }
9916
- function resolveAutoNameModel(modelSpec) {
9917
- const trimmed = modelSpec.trim();
9918
- const slashIndex = trimmed.indexOf("/");
9919
- if (slashIndex > 0) {
9920
- const provider = trimmed.slice(0, slashIndex);
9921
- const model = trimmed.slice(slashIndex + 1).trim().replace(/^models\//, "");
9922
- if (!model) {
9923
- throw new Error(`Invalid auto_name model: ${modelSpec}`);
9924
- }
9925
- if (provider === "anthropic" || provider === "google" || provider === "openai") {
9926
- return { provider, model };
9927
- }
9928
- if (provider === "gemini") {
9929
- return { provider: "google", model };
9930
- }
9931
- }
9932
- if (trimmed.startsWith("claude-")) {
9933
- return { provider: "anthropic", model: trimmed };
9934
- }
9935
- if (trimmed.startsWith("gemini-") || trimmed.startsWith("models/gemini-")) {
9936
- return { provider: "google", model: trimmed.replace(/^models\//, "") };
9937
- }
9938
- if (/^(gpt-|chatgpt-|o\d)/.test(trimmed)) {
9939
- return { provider: "openai", model: trimmed };
9940
- }
9941
- throw new Error(`Unsupported auto_name model provider for ${modelSpec}. Use an anthropic/, gemini/, google/, or openai/ prefix, or a known model name.`);
9942
- }
9943
9891
  function getSystemPrompt(config) {
9944
9892
  return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
9945
9893
  }
9946
- function extractAnthropicText(raw) {
9947
- if (!isRecord4(raw) || !Array.isArray(raw.content))
9948
- return null;
9949
- for (const item of raw.content) {
9950
- if (!isRecord4(item))
9951
- continue;
9952
- if (item.type === "text" && typeof item.text === "string" && item.text.trim()) {
9953
- return item.text;
9954
- }
9955
- }
9956
- return null;
9894
+ async function defaultSpawn(args) {
9895
+ const proc = Bun.spawn(args, {
9896
+ stdout: "pipe",
9897
+ stderr: "pipe"
9898
+ });
9899
+ const [stdout, stderr, exitCode] = await Promise.all([
9900
+ new Response(proc.stdout).text(),
9901
+ new Response(proc.stderr).text(),
9902
+ proc.exited
9903
+ ]);
9904
+ return { exitCode, stdout, stderr };
9957
9905
  }
9958
- function extractGoogleText(raw) {
9959
- if (!isRecord4(raw) || !Array.isArray(raw.candidates))
9960
- return null;
9961
- for (const candidate of raw.candidates) {
9962
- if (!isRecord4(candidate) || !isRecord4(candidate.content) || !Array.isArray(candidate.content.parts))
9963
- continue;
9964
- for (const part of candidate.content.parts) {
9965
- if (isRecord4(part) && typeof part.text === "string" && part.text.trim()) {
9966
- return part.text;
9967
- }
9968
- }
9906
+ function buildClaudeArgs(model, systemPrompt, prompt) {
9907
+ const args = [
9908
+ "claude",
9909
+ "-p",
9910
+ "--system-prompt",
9911
+ systemPrompt,
9912
+ "--output-format",
9913
+ "text",
9914
+ "--no-session-persistence"
9915
+ ];
9916
+ if (model) {
9917
+ args.push("--model", model);
9969
9918
  }
9970
- return null;
9919
+ args.push(prompt);
9920
+ return args;
9971
9921
  }
9972
- function extractOpenAiText(raw) {
9973
- if (!isRecord4(raw))
9974
- return null;
9975
- if (typeof raw.output_text === "string" && raw.output_text.trim()) {
9976
- return raw.output_text;
9977
- }
9978
- if (!Array.isArray(raw.output))
9979
- return null;
9980
- for (const item of raw.output) {
9981
- if (!isRecord4(item) || !Array.isArray(item.content))
9982
- continue;
9983
- for (const content of item.content) {
9984
- if (!isRecord4(content))
9985
- continue;
9986
- if (typeof content.text === "string" && content.text.trim()) {
9987
- return content.text;
9988
- }
9989
- }
9990
- }
9991
- return null;
9922
+ function escapeTomlString(s) {
9923
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
9992
9924
  }
9993
- async function readErrorBody(response) {
9994
- const text = (await response.text()).trim();
9995
- return text || `HTTP ${response.status}`;
9925
+ function buildCodexArgs(model, systemPrompt, prompt) {
9926
+ const args = [
9927
+ "codex",
9928
+ "-c",
9929
+ `developer_instructions="${escapeTomlString(systemPrompt)}"`,
9930
+ "exec",
9931
+ "--ephemeral"
9932
+ ];
9933
+ if (model) {
9934
+ args.push("-m", model);
9935
+ }
9936
+ args.push(prompt);
9937
+ return args;
9996
9938
  }
9997
9939
 
9998
9940
  class AutoNameService {
9999
- fetchImpl;
10000
- anthropicApiKey;
10001
- geminiApiKey;
10002
- openaiApiKey;
9941
+ spawnImpl;
10003
9942
  constructor(deps = {}) {
10004
- this.fetchImpl = deps.fetchImpl ?? fetch;
10005
- this.anthropicApiKey = deps.anthropicApiKey ?? Bun.env.ANTHROPIC_API_KEY;
10006
- this.geminiApiKey = deps.geminiApiKey ?? Bun.env.GEMINI_API_KEY;
10007
- this.openaiApiKey = deps.openaiApiKey ?? Bun.env.OPENAI_API_KEY;
9943
+ this.spawnImpl = deps.spawnImpl ?? defaultSpawn;
10008
9944
  }
10009
9945
  async generateBranchName(config, task) {
10010
9946
  const prompt = task.trim();
10011
9947
  if (!prompt) {
10012
9948
  throw new Error("Auto-name requires a prompt");
10013
9949
  }
10014
- const resolved = resolveAutoNameModel(config.model);
10015
- const branchName = resolved.provider === "anthropic" ? await this.generateWithAnthropic(resolved.model, getSystemPrompt(config), prompt) : resolved.provider === "google" ? await this.generateWithGoogle(resolved.model, getSystemPrompt(config), prompt) : await this.generateWithOpenAI(resolved.model, getSystemPrompt(config), prompt);
10016
- return normalizeGeneratedBranchName(branchName);
10017
- }
10018
- async generateWithAnthropic(model, systemPrompt, task) {
10019
- if (!this.anthropicApiKey) {
10020
- throw new Error("ANTHROPIC_API_KEY is required for auto_name with Anthropic models");
10021
- }
10022
- const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
10023
- method: "POST",
10024
- headers: {
10025
- "content-type": "application/json",
10026
- "x-api-key": this.anthropicApiKey,
10027
- "anthropic-version": "2023-06-01"
10028
- },
10029
- body: JSON.stringify({
10030
- model,
10031
- system: systemPrompt,
10032
- max_tokens: 64,
10033
- messages: [{ role: "user", content: buildPrompt(task) }],
10034
- output_config: {
10035
- format: {
10036
- type: "json_schema",
10037
- schema: BRANCH_NAME_SCHEMA
10038
- }
10039
- }
10040
- })
10041
- });
10042
- if (!response.ok) {
10043
- throw new Error(`Anthropic auto-name request failed: ${await readErrorBody(response)}`);
10044
- }
10045
- const json = await response.json();
10046
- if (isRecord4(json) && json.stop_reason === "refusal") {
10047
- throw new Error("Anthropic auto-name request was refused");
10048
- }
10049
- if (isRecord4(json) && json.stop_reason === "max_tokens") {
10050
- throw new Error("Anthropic auto-name response hit max_tokens before completing");
10051
- }
10052
- const text = extractAnthropicText(json);
10053
- if (!text) {
10054
- throw new Error("Anthropic auto-name response did not include text");
10055
- }
10056
- return parseBranchNamePayload(parseJsonText(text));
10057
- }
10058
- async generateWithGoogle(model, systemPrompt, task) {
10059
- if (!this.geminiApiKey) {
10060
- throw new Error("GEMINI_API_KEY is required for auto_name with Gemini models");
10061
- }
10062
- const response = await this.fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
10063
- method: "POST",
10064
- headers: {
10065
- "content-type": "application/json",
10066
- "x-goog-api-key": this.geminiApiKey
10067
- },
10068
- body: JSON.stringify({
10069
- systemInstruction: {
10070
- parts: [{ text: systemPrompt }]
10071
- },
10072
- contents: [
10073
- {
10074
- role: "user",
10075
- parts: [{ text: buildPrompt(task) }]
10076
- }
10077
- ],
10078
- generationConfig: {
10079
- responseMimeType: "application/json",
10080
- responseJsonSchema: GEMINI_BRANCH_NAME_SCHEMA
10081
- }
10082
- })
10083
- });
10084
- if (!response.ok) {
10085
- throw new Error(`Google auto-name request failed: ${await readErrorBody(response)}`);
10086
- }
10087
- const json = await response.json();
10088
- const text = extractGoogleText(json);
10089
- if (!text) {
10090
- throw new Error("Google auto-name response did not include text");
10091
- }
10092
- return parseBranchNamePayload(parseJsonText(text));
10093
- }
10094
- async generateWithOpenAI(model, systemPrompt, task) {
10095
- if (!this.openaiApiKey) {
10096
- throw new Error("OPENAI_API_KEY is required for auto_name with OpenAI models");
10097
- }
10098
- const response = await this.fetchImpl("https://api.openai.com/v1/responses", {
10099
- method: "POST",
10100
- headers: {
10101
- "content-type": "application/json",
10102
- authorization: `Bearer ${this.openaiApiKey}`
10103
- },
10104
- body: JSON.stringify({
10105
- model,
10106
- input: [
10107
- { role: "system", content: systemPrompt },
10108
- { role: "user", content: buildPrompt(task) }
10109
- ],
10110
- max_output_tokens: 64,
10111
- text: {
10112
- format: {
10113
- type: "json_schema",
10114
- name: "branch_name_response",
10115
- strict: true,
10116
- schema: BRANCH_NAME_SCHEMA
10117
- }
10118
- }
10119
- })
10120
- });
10121
- if (!response.ok) {
10122
- throw new Error(`OpenAI auto-name request failed: ${await readErrorBody(response)}`);
9950
+ const systemPrompt = getSystemPrompt(config);
9951
+ const userPrompt = buildPrompt(prompt);
9952
+ const args = config.provider === "claude" ? buildClaudeArgs(config.model, systemPrompt, userPrompt) : buildCodexArgs(config.model, systemPrompt, userPrompt);
9953
+ const cli = config.provider === "claude" ? "claude" : "codex";
9954
+ let result;
9955
+ try {
9956
+ result = await this.spawnImpl(args);
9957
+ } catch {
9958
+ throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
10123
9959
  }
10124
- const json = await response.json();
10125
- if (isRecord4(json) && Array.isArray(json.output)) {
10126
- for (const item of json.output) {
10127
- if (!isRecord4(item) || !Array.isArray(item.content))
10128
- continue;
10129
- for (const content of item.content) {
10130
- if (isRecord4(content) && content.type === "refusal" && typeof content.refusal === "string") {
10131
- throw new Error(`OpenAI auto-name request was refused: ${content.refusal}`);
10132
- }
10133
- }
10134
- }
9960
+ if (result.exitCode !== 0) {
9961
+ const detail = result.stderr.trim() || `exit ${result.exitCode}`;
9962
+ throw new Error(`${cli} failed: ${detail}`);
10135
9963
  }
10136
- const text = extractOpenAiText(json);
10137
- if (!text) {
10138
- throw new Error("OpenAI auto-name response did not include text");
9964
+ const output = result.stdout.trim();
9965
+ if (!output) {
9966
+ throw new Error(`${cli} returned empty output`);
10139
9967
  }
10140
- return parseBranchNamePayload(parseJsonText(text));
9968
+ return normalizeGeneratedBranchName(output);
10141
9969
  }
10142
9970
  }
10143
9971