vibora 1.9.1 → 1.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/README.md CHANGED
@@ -5,37 +5,34 @@
5
5
 
6
6
  The Vibe Engineer's Cockpit. Vibora marries basic project management with actual software development by embedding terminals directly into the workflow.
7
7
 
8
+ Available in English and Chinese. Works with [z.ai](https://z.ai) for Claude Code proxy integration.
9
+
8
10
  ## Philosophy
9
11
 
10
12
  - **Terminal-first AI agent orchestration** — Agents (Claude Code, Codex, etc.) run in terminals as-is. No abstraction layer, no wrapper APIs, no feature parity maintenance as agents evolve.
11
13
  - **Opinionated with few opinions** — Provides structure without dictating workflow.
12
14
  - **Git worktree isolation** — Tasks create isolated git worktrees, with architecture supporting evolution toward more general task types.
13
15
 
14
- ## Requirements
15
-
16
- - [Bun](https://bun.sh/) — JavaScript runtime
17
- - [dtach](https://github.com/crigler/dtach) — Terminal session persistence
18
-
19
- ## Tech Stack
20
-
21
- - **Frontend**: React 19, TanStack Router & Query, xterm.js, Tailwind CSS, shadcn/ui (v4 with baseui support)
22
- - **Backend**: Hono.js on Bun, SQLite with Drizzle ORM, WebSocket for terminal I/O
23
-
24
16
  ## Quick Start
25
17
 
26
- Run the latest vibora with a single command:
18
+ Requires [Node.js](https://nodejs.org/). Run the latest vibora with a single command:
27
19
 
28
20
  ```bash
29
- bunx vibora@latest up
21
+ npx vibora@latest up
30
22
  ```
31
23
 
32
- This starts the vibora server as a daemon. Open http://localhost:3333 in your browser.
24
+ This starts the vibora server as a daemon. Open http://localhost:3333 in your browser. The `up` command will help you install any missing dependencies.
33
25
 
34
26
  ```bash
35
- bunx vibora@latest down # Stop the server
36
- bunx vibora@latest status # Check if running
27
+ npx vibora@latest down # Stop the server
28
+ npx vibora@latest status # Check if running
37
29
  ```
38
30
 
31
+ ## Tech Stack
32
+
33
+ - **Frontend**: React 19, TanStack Router & Query, xterm.js, Tailwind CSS, shadcn/ui (v4 with baseui support)
34
+ - **Backend**: Hono.js on Bun, SQLite with Drizzle ORM, WebSocket for terminal I/O
35
+
39
36
  ## Configuration
40
37
 
41
38
  Settings are stored in `.vibora/settings.json`.
package/bin/vibora.js CHANGED
@@ -230,6 +230,26 @@ class ViboraClient {
230
230
  async resetConfig(key) {
231
231
  return this.fetch(`/api/config/${key}`, { method: "DELETE" });
232
232
  }
233
+ async getNotifications() {
234
+ return this.fetch("/api/config/notifications");
235
+ }
236
+ async updateNotifications(updates) {
237
+ return this.fetch("/api/config/notifications", {
238
+ method: "PUT",
239
+ body: JSON.stringify(updates)
240
+ });
241
+ }
242
+ async testNotification(channel) {
243
+ return this.fetch(`/api/config/notifications/test/${channel}`, {
244
+ method: "POST"
245
+ });
246
+ }
247
+ async sendNotification(title, message) {
248
+ return this.fetch("/api/config/notifications/send", {
249
+ method: "POST",
250
+ body: JSON.stringify({ title, message })
251
+ });
252
+ }
233
253
  }
234
254
 
235
255
  // cli/src/utils/output.ts
@@ -237,25 +257,6 @@ var prettyOutput = false;
237
257
  function setPrettyOutput(value) {
238
258
  prettyOutput = value;
239
259
  }
240
- function isPrettyOutput() {
241
- return prettyOutput;
242
- }
243
- function prettyLog(type, message) {
244
- const prefixes = {
245
- success: "\u2713",
246
- info: "\u2192",
247
- error: "\u2717",
248
- warning: "\u26A0"
249
- };
250
- console.log(`${prefixes[type]} ${message}`);
251
- }
252
- function outputSuccess(message) {
253
- if (prettyOutput) {
254
- prettyLog("success", message);
255
- } else {
256
- output({ message });
257
- }
258
- }
259
260
  function output(data) {
260
261
  const response = {
261
262
  success: true,
@@ -277,7 +278,6 @@ function outputError(error) {
277
278
 
278
279
  // cli/src/commands/current-task.ts
279
280
  var STATUS_MAP = {
280
- done: "DONE",
281
281
  review: "IN_REVIEW",
282
282
  cancel: "CANCELED",
283
283
  "in-progress": "IN_PROGRESS"
@@ -332,7 +332,7 @@ async function handleCurrentTaskCommand(action, rest, flags) {
332
332
  }
333
333
  const newStatus = STATUS_MAP[action];
334
334
  if (!newStatus) {
335
- throw new CliError("INVALID_ACTION", `Unknown action: ${action}. Valid actions: done, review, cancel, in-progress, pr, linear`, ExitCodes.INVALID_ARGS);
335
+ throw new CliError("INVALID_ACTION", `Unknown action: ${action}. Valid actions: review, cancel, in-progress, pr, linear`, ExitCodes.INVALID_ARGS);
336
336
  }
337
337
  const task = await findCurrentTask(client, pathOverride);
338
338
  const updatedTask = await client.moveTask(task.id, newStatus);
@@ -341,7 +341,7 @@ async function handleCurrentTaskCommand(action, rest, flags) {
341
341
 
342
342
  // cli/src/commands/tasks.ts
343
343
  import { basename } from "path";
344
- var VALID_STATUSES = ["IN_PROGRESS", "IN_REVIEW", "DONE", "CANCELED"];
344
+ var VALID_STATUSES = ["IN_PROGRESS", "IN_REVIEW", "CANCELED"];
345
345
  async function handleTasksCommand(action, positional, flags) {
346
346
  const client = new ViboraClient(flags.url, flags.port);
347
347
  switch (action) {
@@ -823,200 +823,78 @@ async function handleHealthCommand(flags) {
823
823
  output(health);
824
824
  }
825
825
 
826
- // cli/src/commands/hooks.ts
827
- import * as fs from "fs";
828
- import * as path from "path";
829
- import * as os from "os";
830
- import { execSync as execSync2 } from "child_process";
831
- import { fileURLToPath as fileURLToPath2 } from "url";
832
- function getClaudeSettingsPath(global) {
833
- if (global) {
834
- return path.join(os.homedir(), ".claude", "settings.json");
835
- }
836
- return path.join(process.cwd(), ".claude", "settings.json");
837
- }
838
- function readClaudeSettings(settingsPath) {
839
- if (!fs.existsSync(settingsPath)) {
840
- return {};
841
- }
842
- try {
843
- const content = fs.readFileSync(settingsPath, "utf-8");
844
- return JSON.parse(content);
845
- } catch {
846
- return {};
847
- }
848
- }
849
- function writeClaudeSettings(settingsPath, settings) {
850
- const dir = path.dirname(settingsPath);
851
- if (!fs.existsSync(dir)) {
852
- fs.mkdirSync(dir, { recursive: true });
853
- }
854
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
855
- }
856
- function getViboraHookPath() {
857
- const currentFile = fileURLToPath2(import.meta.url);
858
- const scriptDir = path.dirname(currentFile);
859
- const possiblePaths = [
860
- path.join(scriptDir, "..", "scripts", "vibora-plan-complete-hook"),
861
- path.join(scriptDir, "..", "..", "scripts", "vibora-plan-complete-hook"),
862
- "vibora-plan-complete-hook"
863
- ];
864
- for (const p of possiblePaths) {
865
- if (p === "vibora-plan-complete-hook") {
866
- try {
867
- execSync2("which vibora-plan-complete-hook", { stdio: "pipe" });
868
- return "vibora-plan-complete-hook";
869
- } catch {
870
- continue;
871
- }
872
- } else if (fs.existsSync(p)) {
873
- return path.resolve(p);
874
- }
875
- }
876
- return "vibora-plan-complete-hook";
877
- }
878
- function installStopHook(global) {
879
- const settingsPath = getClaudeSettingsPath(global);
880
- const settings = readClaudeSettings(settingsPath);
881
- const hookCommand = getViboraHookPath();
882
- if (!settings.hooks) {
883
- settings.hooks = {};
884
- }
885
- const existingStopHooks = settings.hooks.Stop || [];
886
- const hasViboraHook = existingStopHooks.some((hook) => hook.hooks.some((h) => h.type === "command" && h.command?.includes("vibora-plan-complete-hook")));
887
- if (hasViboraHook) {
888
- return { settingsPath, hookCommand };
889
- }
890
- settings.hooks.Stop = [
891
- ...existingStopHooks,
892
- {
893
- hooks: [
894
- {
895
- type: "command",
896
- command: hookCommand
897
- }
898
- ]
826
+ // cli/src/commands/notifications.ts
827
+ var VALID_CHANNELS = ["sound", "slack", "discord", "pushover"];
828
+ async function handleNotificationsCommand(action, positional, flags) {
829
+ const client = new ViboraClient(flags.url, flags.port);
830
+ switch (action) {
831
+ case "status":
832
+ case undefined: {
833
+ const settings = await client.getNotifications();
834
+ output(settings);
835
+ break;
899
836
  }
900
- ];
901
- writeClaudeSettings(settingsPath, settings);
902
- return { settingsPath, hookCommand };
903
- }
904
- function uninstallStopHook(global) {
905
- const settingsPath = getClaudeSettingsPath(global);
906
- const settings = readClaudeSettings(settingsPath);
907
- if (!settings.hooks?.Stop) {
908
- return { settingsPath, removed: false };
909
- }
910
- const originalLength = settings.hooks.Stop.length;
911
- settings.hooks.Stop = settings.hooks.Stop.filter((hook) => !hook.hooks.some((h) => h.type === "command" && h.command?.includes("vibora-plan-complete-hook")));
912
- const removed = settings.hooks.Stop.length < originalLength;
913
- if (removed) {
914
- if (settings.hooks.Stop.length === 0) {
915
- delete settings.hooks.Stop;
837
+ case "enable": {
838
+ const updated = await client.updateNotifications({ enabled: true });
839
+ output(updated);
840
+ break;
916
841
  }
917
- if (Object.keys(settings.hooks).length === 0) {
918
- delete settings.hooks;
842
+ case "disable": {
843
+ const updated = await client.updateNotifications({ enabled: false });
844
+ output(updated);
845
+ break;
919
846
  }
920
- writeClaudeSettings(settingsPath, settings);
921
- }
922
- return { settingsPath, removed };
923
- }
924
- function checkStopHook(global) {
925
- const settingsPath = getClaudeSettingsPath(global);
926
- const settings = readClaudeSettings(settingsPath);
927
- if (!settings.hooks?.Stop) {
928
- return { installed: false, settingsPath };
929
- }
930
- for (const hook of settings.hooks.Stop) {
931
- for (const h of hook.hooks) {
932
- if (h.type === "command" && h.command?.includes("vibora-plan-complete-hook")) {
933
- return { installed: true, settingsPath, hookCommand: h.command };
847
+ case "test": {
848
+ const [channel] = positional;
849
+ if (!channel) {
850
+ throw new CliError("MISSING_CHANNEL", `Channel is required. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
934
851
  }
935
- }
936
- }
937
- return { installed: false, settingsPath };
938
- }
939
- async function handleHooksCommand(action, _rest, flags) {
940
- const global = flags.global === "true" || flags.g === "true";
941
- switch (action) {
942
- case "install": {
943
- const { settingsPath, hookCommand } = installStopHook(global);
944
- if (isPrettyOutput()) {
945
- prettyLog("success", `Installed Vibora Stop hook`);
946
- prettyLog("info", ` Settings: ${settingsPath}`);
947
- prettyLog("info", ` Command: ${hookCommand}`);
948
- prettyLog("info", "");
949
- prettyLog("info", "The hook will automatically transition tasks to IN_REVIEW");
950
- prettyLog("info", "when Claude Code finishes in a Vibora worktree.");
951
- } else {
952
- outputSuccess({
953
- action: "install",
954
- settingsPath,
955
- hookCommand,
956
- message: "Stop hook installed successfully"
957
- });
852
+ if (!VALID_CHANNELS.includes(channel)) {
853
+ throw new CliError("INVALID_CHANNEL", `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
958
854
  }
855
+ const result = await client.testNotification(channel);
856
+ output(result);
959
857
  break;
960
858
  }
961
- case "uninstall": {
962
- const { settingsPath, removed } = uninstallStopHook(global);
963
- if (isPrettyOutput()) {
964
- if (removed) {
965
- prettyLog("success", `Removed Vibora Stop hook from ${settingsPath}`);
966
- } else {
967
- prettyLog("info", "Vibora Stop hook was not installed");
968
- }
969
- } else {
970
- outputSuccess({
971
- action: "uninstall",
972
- settingsPath,
973
- removed
974
- });
859
+ case "set": {
860
+ const [channel, key, value] = positional;
861
+ if (!channel) {
862
+ throw new CliError("MISSING_CHANNEL", `Channel is required. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
975
863
  }
976
- break;
977
- }
978
- case "status": {
979
- const { installed, settingsPath, hookCommand } = checkStopHook(global);
980
- if (isPrettyOutput()) {
981
- if (installed) {
982
- prettyLog("success", "Vibora Stop hook is installed");
983
- prettyLog("info", ` Settings: ${settingsPath}`);
984
- prettyLog("info", ` Command: ${hookCommand}`);
985
- } else {
986
- prettyLog("info", "Vibora Stop hook is not installed");
987
- prettyLog("info", ` Settings: ${settingsPath}`);
988
- prettyLog("info", "");
989
- prettyLog("info", 'Run "vibora hooks install" to install it.');
990
- }
991
- } else {
992
- outputSuccess({
993
- action: "status",
994
- installed,
995
- settingsPath,
996
- hookCommand
997
- });
864
+ if (!VALID_CHANNELS.includes(channel)) {
865
+ throw new CliError("INVALID_CHANNEL", `Invalid channel: ${channel}. Valid: ${VALID_CHANNELS.join(", ")}`, ExitCodes.INVALID_ARGS);
998
866
  }
867
+ if (!key) {
868
+ throw new CliError("MISSING_KEY", "Setting key is required", ExitCodes.INVALID_ARGS);
869
+ }
870
+ if (value === undefined) {
871
+ throw new CliError("MISSING_VALUE", "Setting value is required", ExitCodes.INVALID_ARGS);
872
+ }
873
+ const update = buildChannelUpdate(channel, key, value);
874
+ const updated = await client.updateNotifications(update);
875
+ output(updated);
999
876
  break;
1000
877
  }
1001
878
  default:
1002
- if (isPrettyOutput()) {
1003
- console.log(`Usage: vibora hooks <action> [--global]
1004
-
1005
- Actions:
1006
- install Install the Stop hook for auto task transitions
1007
- uninstall Remove the Stop hook
1008
- status Check if the Stop hook is installed
1009
-
1010
- Options:
1011
- --global Use global Claude settings (~/.claude/settings.json)
1012
- Default is project-local (.claude/settings.json)
879
+ throw new CliError("UNKNOWN_ACTION", `Unknown action: ${action}. Valid: status, enable, disable, test, set`, ExitCodes.INVALID_ARGS);
880
+ }
881
+ }
882
+ function buildChannelUpdate(channel, key, value) {
883
+ const parsedValue = value === "true" ? true : value === "false" ? false : value;
884
+ const channelConfig = { [key]: parsedValue };
885
+ return { [channel]: channelConfig };
886
+ }
1013
887
 
1014
- The Stop hook automatically transitions tasks from IN_PROGRESS to IN_REVIEW
1015
- when Claude Code finishes in a Vibora worktree.`);
1016
- } else {
1017
- throw new CliError("INVALID_ACTION", `Invalid hooks action: ${action}. Use install, uninstall, or status.`, ExitCodes.INVALID_ARGS);
1018
- }
888
+ // cli/src/commands/notify.ts
889
+ async function handleNotifyCommand(positional, flags) {
890
+ const client = new ViboraClient(flags.url, flags.port);
891
+ const title = flags.title || positional[0];
892
+ const message = flags.message || positional.slice(1).join(" ") || positional[0];
893
+ if (!title) {
894
+ throw new CliError("MISSING_TITLE", "Title is required. Usage: vibora notify <title> [message] or --title=<title> --message=<message>", ExitCodes.INVALID_ARGS);
1019
895
  }
896
+ const result = await client.sendNotification(title, message || title);
897
+ output(result);
1020
898
  }
1021
899
 
1022
900
  // cli/src/index.ts
@@ -1093,9 +971,14 @@ Commands:
1093
971
  config get <key> Get a config value
1094
972
  config set <key> <value> Set a config value
1095
973
 
1096
- hooks install Install Claude Code Stop hook
1097
- hooks uninstall Remove Claude Code Stop hook
1098
- hooks status Check if Stop hook is installed
974
+ notifications Show notification settings
975
+ notifications enable Enable notifications
976
+ notifications disable Disable notifications
977
+ notifications test <ch> Test a channel (sound, slack, discord, pushover)
978
+ notifications set <ch> <key> <value>
979
+ Set a channel config (e.g., slack webhookUrl <url>)
980
+
981
+ notify <title> [message] Send a notification to all enabled channels
1099
982
 
1100
983
  health Check server health
1101
984
 
@@ -1157,9 +1040,13 @@ Examples:
1157
1040
  await handleHealthCommand(flags);
1158
1041
  break;
1159
1042
  }
1160
- case "hooks": {
1161
- const [action, ...hooksRest] = rest;
1162
- await handleHooksCommand(action, hooksRest, flags);
1043
+ case "notifications": {
1044
+ const [action, ...notificationsRest] = rest;
1045
+ await handleNotificationsCommand(action, notificationsRest, flags);
1046
+ break;
1047
+ }
1048
+ case "notify": {
1049
+ await handleNotifyCommand(rest, flags);
1163
1050
  break;
1164
1051
  }
1165
1052
  default: