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 +12 -15
- package/bin/vibora.js +95 -208
- package/dist/assets/index-BiapsD8F.css +1 -0
- package/dist/assets/index-C4ahV9ZS.js +116 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/index.js +369 -95
- package/dist/assets/index-BxbgLbxS.css +0 -1
- package/dist/assets/index-C7bo3ECQ.css +0 -1
- package/dist/assets/index-CCtJOkVu.js +0 -116
- package/dist/assets/index-QutiQrzr.js +0 -45
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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:
|
|
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", "
|
|
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/
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
902
|
-
|
|
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
|
-
|
|
918
|
-
|
|
842
|
+
case "disable": {
|
|
843
|
+
const updated = await client.updateNotifications({ enabled: false });
|
|
844
|
+
output(updated);
|
|
845
|
+
break;
|
|
919
846
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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 "
|
|
962
|
-
const
|
|
963
|
-
if (
|
|
964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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 "
|
|
1161
|
-
const [action, ...
|
|
1162
|
-
await
|
|
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:
|