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.
package/bin/webmux.js CHANGED
@@ -1768,7 +1768,7 @@ Description=webmux dashboard \u2014 ${config.projectName}
1768
1768
 
1769
1769
  [Service]
1770
1770
  Type=simple
1771
- ExecStart=${config.webmuxPath} --port ${config.port}
1771
+ ExecStart=${config.webmuxPath} serve --port ${config.port}
1772
1772
  WorkingDirectory=${config.projectDir}
1773
1773
  Restart=on-failure
1774
1774
  RestartSec=5
@@ -1791,6 +1791,7 @@ function generateLaunchdPlist(config) {
1791
1791
  <key>ProgramArguments</key>
1792
1792
  <array>
1793
1793
  <string>${config.webmuxPath}</string>
1794
+ <string>serve</string>
1794
1795
  <string>--port</string>
1795
1796
  <string>${config.port}</string>
1796
1797
  </array>
@@ -9416,10 +9417,12 @@ function parseLifecycleHooks(raw) {
9416
9417
  function parseAutoName(raw) {
9417
9418
  if (!isRecord3(raw))
9418
9419
  return null;
9419
- if (typeof raw.model !== "string" || !raw.model.trim())
9420
+ const provider = raw.provider;
9421
+ if (provider !== "claude" && provider !== "codex")
9420
9422
  return null;
9421
9423
  return {
9422
- model: raw.model.trim(),
9424
+ provider,
9425
+ ...typeof raw.model === "string" && raw.model.trim() ? { model: raw.model.trim() } : {},
9423
9426
  ...typeof raw.system_prompt === "string" && raw.system_prompt.trim() ? { systemPrompt: raw.system_prompt.trim() } : {}
9424
9427
  };
9425
9428
  }
@@ -10146,26 +10149,10 @@ class BunPortProbe {
10146
10149
  }
10147
10150
 
10148
10151
  // backend/src/services/auto-name-service.ts
10149
- function isRecord4(value) {
10150
- return typeof value === "object" && value !== null && !Array.isArray(value);
10151
- }
10152
10152
  function buildPrompt(task) {
10153
10153
  return `Task description:
10154
10154
  ${task.trim()}`;
10155
10155
  }
10156
- function parseBranchNamePayload(raw) {
10157
- if (!isRecord4(raw) || typeof raw.branch_name !== "string") {
10158
- throw new Error("Auto-name response did not include branch_name");
10159
- }
10160
- return raw.branch_name;
10161
- }
10162
- function parseJsonText(text) {
10163
- try {
10164
- return JSON.parse(text);
10165
- } catch {
10166
- throw new Error(`Auto-name response was not valid JSON: ${text}`);
10167
- }
10168
- }
10169
10156
  function normalizeGeneratedBranchName(raw) {
10170
10157
  let branch = raw.trim();
10171
10158
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -10185,251 +10172,89 @@ function normalizeGeneratedBranchName(raw) {
10185
10172
  }
10186
10173
  return branch;
10187
10174
  }
10188
- function resolveAutoNameModel(modelSpec) {
10189
- const trimmed = modelSpec.trim();
10190
- const slashIndex = trimmed.indexOf("/");
10191
- if (slashIndex > 0) {
10192
- const provider = trimmed.slice(0, slashIndex);
10193
- const model = trimmed.slice(slashIndex + 1).trim().replace(/^models\//, "");
10194
- if (!model) {
10195
- throw new Error(`Invalid auto_name model: ${modelSpec}`);
10196
- }
10197
- if (provider === "anthropic" || provider === "google" || provider === "openai") {
10198
- return { provider, model };
10199
- }
10200
- if (provider === "gemini") {
10201
- return { provider: "google", model };
10202
- }
10203
- }
10204
- if (trimmed.startsWith("claude-")) {
10205
- return { provider: "anthropic", model: trimmed };
10206
- }
10207
- if (trimmed.startsWith("gemini-") || trimmed.startsWith("models/gemini-")) {
10208
- return { provider: "google", model: trimmed.replace(/^models\//, "") };
10209
- }
10210
- if (/^(gpt-|chatgpt-|o\d)/.test(trimmed)) {
10211
- return { provider: "openai", model: trimmed };
10212
- }
10213
- throw new Error(`Unsupported auto_name model provider for ${modelSpec}. Use an anthropic/, gemini/, google/, or openai/ prefix, or a known model name.`);
10214
- }
10215
10175
  function getSystemPrompt(config) {
10216
10176
  return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
10217
10177
  }
10218
- function extractAnthropicText(raw) {
10219
- if (!isRecord4(raw) || !Array.isArray(raw.content))
10220
- return null;
10221
- for (const item of raw.content) {
10222
- if (!isRecord4(item))
10223
- continue;
10224
- if (item.type === "text" && typeof item.text === "string" && item.text.trim()) {
10225
- return item.text;
10226
- }
10227
- }
10228
- return null;
10178
+ async function defaultSpawn(args) {
10179
+ const proc = Bun.spawn(args, {
10180
+ stdout: "pipe",
10181
+ stderr: "pipe"
10182
+ });
10183
+ const [stdout, stderr, exitCode] = await Promise.all([
10184
+ new Response(proc.stdout).text(),
10185
+ new Response(proc.stderr).text(),
10186
+ proc.exited
10187
+ ]);
10188
+ return { exitCode, stdout, stderr };
10229
10189
  }
10230
- function extractGoogleText(raw) {
10231
- if (!isRecord4(raw) || !Array.isArray(raw.candidates))
10232
- return null;
10233
- for (const candidate of raw.candidates) {
10234
- if (!isRecord4(candidate) || !isRecord4(candidate.content) || !Array.isArray(candidate.content.parts))
10235
- continue;
10236
- for (const part of candidate.content.parts) {
10237
- if (isRecord4(part) && typeof part.text === "string" && part.text.trim()) {
10238
- return part.text;
10239
- }
10240
- }
10190
+ function buildClaudeArgs(model, systemPrompt, prompt) {
10191
+ const args = [
10192
+ "claude",
10193
+ "-p",
10194
+ "--system-prompt",
10195
+ systemPrompt,
10196
+ "--output-format",
10197
+ "text",
10198
+ "--no-session-persistence"
10199
+ ];
10200
+ if (model) {
10201
+ args.push("--model", model);
10241
10202
  }
10242
- return null;
10203
+ args.push(prompt);
10204
+ return args;
10243
10205
  }
10244
- function extractOpenAiText(raw) {
10245
- if (!isRecord4(raw))
10246
- return null;
10247
- if (typeof raw.output_text === "string" && raw.output_text.trim()) {
10248
- return raw.output_text;
10249
- }
10250
- if (!Array.isArray(raw.output))
10251
- return null;
10252
- for (const item of raw.output) {
10253
- if (!isRecord4(item) || !Array.isArray(item.content))
10254
- continue;
10255
- for (const content of item.content) {
10256
- if (!isRecord4(content))
10257
- continue;
10258
- if (typeof content.text === "string" && content.text.trim()) {
10259
- return content.text;
10260
- }
10261
- }
10262
- }
10263
- return null;
10206
+ function escapeTomlString(s) {
10207
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
10264
10208
  }
10265
- async function readErrorBody(response) {
10266
- const text = (await response.text()).trim();
10267
- return text || `HTTP ${response.status}`;
10209
+ function buildCodexArgs(model, systemPrompt, prompt) {
10210
+ const args = [
10211
+ "codex",
10212
+ "-c",
10213
+ `developer_instructions="${escapeTomlString(systemPrompt)}"`,
10214
+ "exec",
10215
+ "--ephemeral"
10216
+ ];
10217
+ if (model) {
10218
+ args.push("-m", model);
10219
+ }
10220
+ args.push(prompt);
10221
+ return args;
10268
10222
  }
10269
10223
 
10270
10224
  class AutoNameService {
10271
- fetchImpl;
10272
- anthropicApiKey;
10273
- geminiApiKey;
10274
- openaiApiKey;
10225
+ spawnImpl;
10275
10226
  constructor(deps2 = {}) {
10276
- this.fetchImpl = deps2.fetchImpl ?? fetch;
10277
- this.anthropicApiKey = deps2.anthropicApiKey ?? Bun.env.ANTHROPIC_API_KEY;
10278
- this.geminiApiKey = deps2.geminiApiKey ?? Bun.env.GEMINI_API_KEY;
10279
- this.openaiApiKey = deps2.openaiApiKey ?? Bun.env.OPENAI_API_KEY;
10227
+ this.spawnImpl = deps2.spawnImpl ?? defaultSpawn;
10280
10228
  }
10281
10229
  async generateBranchName(config, task) {
10282
10230
  const prompt = task.trim();
10283
10231
  if (!prompt) {
10284
10232
  throw new Error("Auto-name requires a prompt");
10285
10233
  }
10286
- const resolved = resolveAutoNameModel(config.model);
10287
- 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);
10288
- return normalizeGeneratedBranchName(branchName);
10289
- }
10290
- async generateWithAnthropic(model, systemPrompt, task) {
10291
- if (!this.anthropicApiKey) {
10292
- throw new Error("ANTHROPIC_API_KEY is required for auto_name with Anthropic models");
10293
- }
10294
- const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
10295
- method: "POST",
10296
- headers: {
10297
- "content-type": "application/json",
10298
- "x-api-key": this.anthropicApiKey,
10299
- "anthropic-version": "2023-06-01"
10300
- },
10301
- body: JSON.stringify({
10302
- model,
10303
- system: systemPrompt,
10304
- max_tokens: 64,
10305
- messages: [{ role: "user", content: buildPrompt(task) }],
10306
- output_config: {
10307
- format: {
10308
- type: "json_schema",
10309
- schema: BRANCH_NAME_SCHEMA
10310
- }
10311
- }
10312
- })
10313
- });
10314
- if (!response.ok) {
10315
- throw new Error(`Anthropic auto-name request failed: ${await readErrorBody(response)}`);
10316
- }
10317
- const json = await response.json();
10318
- if (isRecord4(json) && json.stop_reason === "refusal") {
10319
- throw new Error("Anthropic auto-name request was refused");
10320
- }
10321
- if (isRecord4(json) && json.stop_reason === "max_tokens") {
10322
- throw new Error("Anthropic auto-name response hit max_tokens before completing");
10323
- }
10324
- const text = extractAnthropicText(json);
10325
- if (!text) {
10326
- throw new Error("Anthropic auto-name response did not include text");
10327
- }
10328
- return parseBranchNamePayload(parseJsonText(text));
10329
- }
10330
- async generateWithGoogle(model, systemPrompt, task) {
10331
- if (!this.geminiApiKey) {
10332
- throw new Error("GEMINI_API_KEY is required for auto_name with Gemini models");
10333
- }
10334
- const response = await this.fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
10335
- method: "POST",
10336
- headers: {
10337
- "content-type": "application/json",
10338
- "x-goog-api-key": this.geminiApiKey
10339
- },
10340
- body: JSON.stringify({
10341
- systemInstruction: {
10342
- parts: [{ text: systemPrompt }]
10343
- },
10344
- contents: [
10345
- {
10346
- role: "user",
10347
- parts: [{ text: buildPrompt(task) }]
10348
- }
10349
- ],
10350
- generationConfig: {
10351
- responseMimeType: "application/json",
10352
- responseJsonSchema: GEMINI_BRANCH_NAME_SCHEMA
10353
- }
10354
- })
10355
- });
10356
- if (!response.ok) {
10357
- throw new Error(`Google auto-name request failed: ${await readErrorBody(response)}`);
10358
- }
10359
- const json = await response.json();
10360
- const text = extractGoogleText(json);
10361
- if (!text) {
10362
- throw new Error("Google auto-name response did not include text");
10363
- }
10364
- return parseBranchNamePayload(parseJsonText(text));
10365
- }
10366
- async generateWithOpenAI(model, systemPrompt, task) {
10367
- if (!this.openaiApiKey) {
10368
- throw new Error("OPENAI_API_KEY is required for auto_name with OpenAI models");
10369
- }
10370
- const response = await this.fetchImpl("https://api.openai.com/v1/responses", {
10371
- method: "POST",
10372
- headers: {
10373
- "content-type": "application/json",
10374
- authorization: `Bearer ${this.openaiApiKey}`
10375
- },
10376
- body: JSON.stringify({
10377
- model,
10378
- input: [
10379
- { role: "system", content: systemPrompt },
10380
- { role: "user", content: buildPrompt(task) }
10381
- ],
10382
- max_output_tokens: 64,
10383
- text: {
10384
- format: {
10385
- type: "json_schema",
10386
- name: "branch_name_response",
10387
- strict: true,
10388
- schema: BRANCH_NAME_SCHEMA
10389
- }
10390
- }
10391
- })
10392
- });
10393
- if (!response.ok) {
10394
- throw new Error(`OpenAI auto-name request failed: ${await readErrorBody(response)}`);
10234
+ const systemPrompt = getSystemPrompt(config);
10235
+ const userPrompt = buildPrompt(prompt);
10236
+ const args = config.provider === "claude" ? buildClaudeArgs(config.model, systemPrompt, userPrompt) : buildCodexArgs(config.model, systemPrompt, userPrompt);
10237
+ const cli = config.provider === "claude" ? "claude" : "codex";
10238
+ let result;
10239
+ try {
10240
+ result = await this.spawnImpl(args);
10241
+ } catch {
10242
+ throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
10395
10243
  }
10396
- const json = await response.json();
10397
- if (isRecord4(json) && Array.isArray(json.output)) {
10398
- for (const item of json.output) {
10399
- if (!isRecord4(item) || !Array.isArray(item.content))
10400
- continue;
10401
- for (const content of item.content) {
10402
- if (isRecord4(content) && content.type === "refusal" && typeof content.refusal === "string") {
10403
- throw new Error(`OpenAI auto-name request was refused: ${content.refusal}`);
10404
- }
10405
- }
10406
- }
10244
+ if (result.exitCode !== 0) {
10245
+ const detail = result.stderr.trim() || `exit ${result.exitCode}`;
10246
+ throw new Error(`${cli} failed: ${detail}`);
10407
10247
  }
10408
- const text = extractOpenAiText(json);
10409
- if (!text) {
10410
- throw new Error("OpenAI auto-name response did not include text");
10248
+ const output = result.stdout.trim();
10249
+ if (!output) {
10250
+ throw new Error(`${cli} returned empty output`);
10411
10251
  }
10412
- return parseBranchNamePayload(parseJsonText(text));
10252
+ return normalizeGeneratedBranchName(output);
10413
10253
  }
10414
10254
  }
10415
- var BRANCH_NAME_SCHEMA, GEMINI_BRANCH_NAME_SCHEMA, DEFAULT_SYSTEM_PROMPT;
10255
+ var DEFAULT_SYSTEM_PROMPT;
10416
10256
  var init_auto_name_service = __esm(() => {
10417
10257
  init_policies();
10418
- BRANCH_NAME_SCHEMA = {
10419
- type: "object",
10420
- properties: {
10421
- branch_name: {
10422
- type: "string",
10423
- description: "A lowercase kebab-case git branch name with no prefix"
10424
- }
10425
- },
10426
- required: ["branch_name"],
10427
- additionalProperties: false
10428
- };
10429
- GEMINI_BRANCH_NAME_SCHEMA = {
10430
- ...BRANCH_NAME_SCHEMA,
10431
- propertyOrdering: ["branch_name"]
10432
- };
10433
10258
  DEFAULT_SYSTEM_PROMPT = [
10434
10259
  "Generate a concise git branch name from the task description.",
10435
10260
  "Return only the branch name.",
@@ -10444,7 +10269,7 @@ import { dirname as dirname2, join as join8 } from "path";
10444
10269
  function shellQuote(value) {
10445
10270
  return `'${value.replaceAll("'", "'\\''")}'`;
10446
10271
  }
10447
- function isRecord5(value) {
10272
+ function isRecord4(value) {
10448
10273
  return typeof value === "object" && value !== null && !Array.isArray(value);
10449
10274
  }
10450
10275
  function buildAgentCtlScript() {
@@ -10712,7 +10537,7 @@ async function ensureAgentRuntimeArtifacts(input) {
10712
10537
  await chmod2(artifacts.agentCtlPath, 493);
10713
10538
  const hookSettings = buildClaudeHookSettings(artifacts);
10714
10539
  const hooks = hookSettings.hooks;
10715
- if (!isRecord5(hooks)) {
10540
+ if (!isRecord4(hooks)) {
10716
10541
  throw new Error("Invalid Claude hook settings");
10717
10542
  }
10718
10543
  await mergeClaudeSettings(artifacts.claudeSettingsPath, hooks);
@@ -12146,25 +11971,28 @@ function usage2() {
12146
11971
  webmux \u2014 Dev dashboard for managing Git worktrees
12147
11972
 
12148
11973
  Usage:
12149
- webmux Start the dashboard
11974
+ webmux serve Start the dashboard server
12150
11975
  webmux init Interactive project setup
12151
11976
  webmux service Manage webmux as a system service
11977
+ webmux update Update webmux to the latest version
12152
11978
  webmux add Create a worktree using the dashboard lifecycle
12153
11979
  webmux list List worktrees and their status
12154
11980
  webmux open Open an existing worktree session
12155
11981
  webmux close Close a worktree session without removing it
12156
11982
  webmux remove Remove a worktree
12157
11983
  webmux merge Merge a worktree into the main branch and remove it
12158
- webmux --port N Set port (default: 5111)
12159
- webmux --debug Show debug-level logs
12160
- webmux --help Show this help message
11984
+
11985
+ Options:
11986
+ --port N Set port (default: 5111)
11987
+ --debug Show debug-level logs
11988
+ --help Show this help message
12161
11989
 
12162
11990
  Environment:
12163
11991
  BACKEND_PORT Same as --port (flag takes precedence)
12164
11992
  `);
12165
11993
  }
12166
11994
  function isRootCommand(value) {
12167
- return value === "init" || value === "service" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge";
11995
+ return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge";
12168
11996
  }
12169
11997
  function parseRootArgs(args) {
12170
11998
  let port = parseInt(process.env.BACKEND_PORT || "5111", 10);
@@ -12234,6 +12062,16 @@ if (parsed.command === "service") {
12234
12062
  await service2(parsed.commandArgs);
12235
12063
  process.exit(0);
12236
12064
  }
12065
+ if (parsed.command === "update") {
12066
+ console.log("Updating webmux to the latest version...");
12067
+ const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
12068
+ stdin: "inherit",
12069
+ stdout: "inherit",
12070
+ stderr: "inherit"
12071
+ });
12072
+ const code = await proc.exited;
12073
+ process.exit(code);
12074
+ }
12237
12075
  async function loadEnvFile(path) {
12238
12076
  if (!existsSync5(path))
12239
12077
  return;
@@ -12265,6 +12103,10 @@ if (isWorktreeCommand(parsed.command)) {
12265
12103
  });
12266
12104
  process.exit(exitCode);
12267
12105
  }
12106
+ if (parsed.command === null) {
12107
+ usage2();
12108
+ process.exit(0);
12109
+ }
12268
12110
  if (!existsSync5(resolve6(process.cwd(), ".webmux.yaml"))) {
12269
12111
  console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
12270
12112
  process.exit(1);