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