@vibe80/vibe80 0.1.1
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/LICENSE +201 -0
- package/README.md +52 -0
- package/bin/vibe80.js +176 -0
- package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
- package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
- package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
- package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
- package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
- package/client/dist/assets/browser-e3WgtMs-.js +8 -0
- package/client/dist/assets/index-CgqGyssr.css +32 -0
- package/client/dist/assets/index-DnwKjoj7.js +706 -0
- package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
- package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +35 -0
- package/client/dist/index.html +14 -0
- package/client/index.html +16 -0
- package/client/package.json +34 -0
- package/client/public/favicon.ico +0 -0
- package/client/public/favicon.png +0 -0
- package/client/public/favicon.svg +35 -0
- package/client/public/pwa-192x192.png +0 -0
- package/client/public/pwa-512x512.png +0 -0
- package/client/src/App.jsx +3131 -0
- package/client/src/assets/logo_small.png +0 -0
- package/client/src/assets/vibe80_dark.svg +51 -0
- package/client/src/assets/vibe80_light.svg +50 -0
- package/client/src/components/Chat/ChatComposer.jsx +228 -0
- package/client/src/components/Chat/ChatMessages.jsx +811 -0
- package/client/src/components/Chat/ChatToolbar.jsx +109 -0
- package/client/src/components/Chat/useChatComposer.js +462 -0
- package/client/src/components/Diff/DiffPanel.jsx +129 -0
- package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
- package/client/src/components/Logs/LogsPanel.jsx +80 -0
- package/client/src/components/SessionGate/SessionGate.jsx +874 -0
- package/client/src/components/Settings/SettingsPanel.jsx +212 -0
- package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
- package/client/src/components/Topbar/Topbar.jsx +101 -0
- package/client/src/components/WorktreeTabs.css +419 -0
- package/client/src/components/WorktreeTabs.jsx +604 -0
- package/client/src/hooks/useAttachments.jsx +125 -0
- package/client/src/hooks/useBacklog.js +254 -0
- package/client/src/hooks/useChatClear.js +90 -0
- package/client/src/hooks/useChatCollapse.js +42 -0
- package/client/src/hooks/useChatCommands.js +294 -0
- package/client/src/hooks/useChatExport.js +144 -0
- package/client/src/hooks/useChatMessagesState.js +69 -0
- package/client/src/hooks/useChatSend.js +158 -0
- package/client/src/hooks/useChatSocket.js +1239 -0
- package/client/src/hooks/useDiffNavigation.js +19 -0
- package/client/src/hooks/useExplorerActions.js +1184 -0
- package/client/src/hooks/useGitIdentity.js +114 -0
- package/client/src/hooks/useLayoutMode.js +31 -0
- package/client/src/hooks/useLocalPreferences.js +131 -0
- package/client/src/hooks/useMessageSync.js +30 -0
- package/client/src/hooks/useNotifications.js +132 -0
- package/client/src/hooks/usePaneNavigation.js +67 -0
- package/client/src/hooks/usePanelState.js +13 -0
- package/client/src/hooks/useProviderSelection.js +70 -0
- package/client/src/hooks/useRepoBranchesModels.js +218 -0
- package/client/src/hooks/useRepoStatus.js +350 -0
- package/client/src/hooks/useRpcLogActions.js +19 -0
- package/client/src/hooks/useRpcLogView.js +58 -0
- package/client/src/hooks/useSessionHandoff.js +97 -0
- package/client/src/hooks/useSessionLifecycle.js +287 -0
- package/client/src/hooks/useSessionReset.js +63 -0
- package/client/src/hooks/useSessionResync.js +77 -0
- package/client/src/hooks/useTerminalSession.js +328 -0
- package/client/src/hooks/useToolbarExport.js +27 -0
- package/client/src/hooks/useTurnInterrupt.js +43 -0
- package/client/src/hooks/useVibe80Forms.js +128 -0
- package/client/src/hooks/useWorkspaceAuth.js +932 -0
- package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
- package/client/src/hooks/useWorktrees.js +396 -0
- package/client/src/i18n.jsx +87 -0
- package/client/src/index.css +5147 -0
- package/client/src/locales/en.json +37 -0
- package/client/src/locales/fr.json +321 -0
- package/client/src/main.jsx +16 -0
- package/client/vite.config.js +62 -0
- package/docs/api/asyncapi.json +1511 -0
- package/docs/api/openapi.json +3242 -0
- package/git_hooks/prepare-commit-msg +35 -0
- package/package.json +36 -0
- package/server/package.json +29 -0
- package/server/scripts/rotate-workspace-secret.js +101 -0
- package/server/src/claudeClient.js +454 -0
- package/server/src/clientEvents.js +594 -0
- package/server/src/clientFactory.js +164 -0
- package/server/src/codexClient.js +468 -0
- package/server/src/config.js +27 -0
- package/server/src/helpers.js +138 -0
- package/server/src/index.js +1641 -0
- package/server/src/middleware/auth.js +93 -0
- package/server/src/middleware/debug.js +89 -0
- package/server/src/middleware/errorTypes.js +60 -0
- package/server/src/providerLogger.js +60 -0
- package/server/src/routes/files.js +114 -0
- package/server/src/routes/git.js +183 -0
- package/server/src/routes/health.js +13 -0
- package/server/src/routes/sessions.js +407 -0
- package/server/src/routes/workspaces.js +296 -0
- package/server/src/routes/worktrees.js +993 -0
- package/server/src/runAs.js +458 -0
- package/server/src/runtimeStore.js +32 -0
- package/server/src/services/auth.js +157 -0
- package/server/src/services/claudeThreadDirectory.js +33 -0
- package/server/src/services/session.js +918 -0
- package/server/src/services/workspace.js +858 -0
- package/server/src/storage/index.js +17 -0
- package/server/src/storage/redis.js +412 -0
- package/server/src/storage/sqlite.js +649 -0
- package/server/src/worktreeManager.js +717 -0
- package/server/tests/README.md +13 -0
- package/server/tests/factories/workspaceFactory.js +13 -0
- package/server/tests/fixtures/workspaceCredentials.json +4 -0
- package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
- package/server/tests/setup/env.js +9 -0
- package/server/tests/unit/helpers.test.js +95 -0
- package/server/tests/unit/services/auth.test.js +181 -0
- package/server/tests/unit/services/workspace.test.js +115 -0
- package/server/vitest.config.js +23 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { CodexAppServerClient } from "./codexClient.js";
|
|
3
|
+
import { ClaudeCliClient } from "./claudeClient.js";
|
|
4
|
+
import { getSessionRuntime } from "./runtimeStore.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get an existing client or create a new one for the given provider.
|
|
8
|
+
* Clients are lazily initialized and cached in session.clients.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} session - The session object
|
|
11
|
+
* @param {"codex" | "claude"} provider - The provider to get or create
|
|
12
|
+
* @returns {Promise<CodexAppServerClient | ClaudeCliClient>}
|
|
13
|
+
*/
|
|
14
|
+
export async function getOrCreateClient(session, provider) {
|
|
15
|
+
const runtime = getSessionRuntime(session.sessionId);
|
|
16
|
+
if (runtime?.clients?.[provider]) {
|
|
17
|
+
return runtime.clients[provider];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultDenyGitCredentialsAccess =
|
|
21
|
+
typeof session.defaultDenyGitCredentialsAccess === "boolean"
|
|
22
|
+
? session.defaultDenyGitCredentialsAccess
|
|
23
|
+
: true;
|
|
24
|
+
|
|
25
|
+
const client =
|
|
26
|
+
provider === "claude"
|
|
27
|
+
? new ClaudeCliClient({
|
|
28
|
+
cwd: session.repoDir,
|
|
29
|
+
attachmentsDir: session.attachmentsDir,
|
|
30
|
+
repoDir: session.repoDir,
|
|
31
|
+
internetAccess: session.defaultInternetAccess,
|
|
32
|
+
denyGitCredentialsAccess: defaultDenyGitCredentialsAccess,
|
|
33
|
+
gitDir: session.gitDir || path.join(session.dir, "git"),
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
TMPDIR: path.join(session.dir, "tmp"),
|
|
37
|
+
CLAUDE_CODE_TMPDIR: path.join(session.dir, "tmp"),
|
|
38
|
+
},
|
|
39
|
+
workspaceId: session.workspaceId,
|
|
40
|
+
tmpDir: path.join(session.dir, "tmp"),
|
|
41
|
+
sessionId: session.sessionId,
|
|
42
|
+
worktreeId: "main",
|
|
43
|
+
threadId: session.threadId || null,
|
|
44
|
+
})
|
|
45
|
+
: new CodexAppServerClient({
|
|
46
|
+
cwd: session.repoDir,
|
|
47
|
+
attachmentsDir: session.attachmentsDir,
|
|
48
|
+
repoDir: session.repoDir,
|
|
49
|
+
internetAccess: session.defaultInternetAccess,
|
|
50
|
+
denyGitCredentialsAccess: defaultDenyGitCredentialsAccess,
|
|
51
|
+
gitDir: session.gitDir || path.join(session.dir, "git"),
|
|
52
|
+
threadId: session.threadId || null,
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
TMPDIR: path.join(session.dir, "tmp"),
|
|
56
|
+
},
|
|
57
|
+
workspaceId: session.workspaceId,
|
|
58
|
+
tmpDir: path.join(session.dir, "tmp"),
|
|
59
|
+
sessionId: session.sessionId,
|
|
60
|
+
worktreeId: "main",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (runtime) {
|
|
64
|
+
runtime.clients[provider] = client;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return client;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a new client for a worktree (not cached, each worktree gets its own client).
|
|
72
|
+
*
|
|
73
|
+
* @param {object} worktree - The worktree object
|
|
74
|
+
* @param {string} worktree.path - The worktree directory path
|
|
75
|
+
* @param {"codex" | "claude"} worktree.provider - The provider
|
|
76
|
+
* @param {string} [attachmentsDir] - The attachments directory
|
|
77
|
+
* @returns {CodexAppServerClient | ClaudeCliClient}
|
|
78
|
+
*/
|
|
79
|
+
export function createWorktreeClient(
|
|
80
|
+
worktree,
|
|
81
|
+
attachmentsDir,
|
|
82
|
+
repoDir,
|
|
83
|
+
internetAccess,
|
|
84
|
+
threadId,
|
|
85
|
+
gitDir,
|
|
86
|
+
options = {}
|
|
87
|
+
) {
|
|
88
|
+
const sessionDir = repoDir ? path.dirname(repoDir) : null;
|
|
89
|
+
const tmpDir = sessionDir ? path.join(sessionDir, "tmp") : null;
|
|
90
|
+
const denyGitCredentialsAccess =
|
|
91
|
+
typeof worktree.denyGitCredentialsAccess === "boolean"
|
|
92
|
+
? worktree.denyGitCredentialsAccess
|
|
93
|
+
: true;
|
|
94
|
+
const client =
|
|
95
|
+
worktree.provider === "claude"
|
|
96
|
+
? new ClaudeCliClient({
|
|
97
|
+
cwd: worktree.path,
|
|
98
|
+
attachmentsDir,
|
|
99
|
+
repoDir,
|
|
100
|
+
internetAccess,
|
|
101
|
+
denyGitCredentialsAccess,
|
|
102
|
+
gitDir,
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
...(tmpDir
|
|
106
|
+
? { TMPDIR: tmpDir, CLAUDE_CODE_TMPDIR: tmpDir }
|
|
107
|
+
: {}),
|
|
108
|
+
},
|
|
109
|
+
workspaceId: worktree.workspaceId,
|
|
110
|
+
tmpDir,
|
|
111
|
+
sessionId: worktree.sessionId,
|
|
112
|
+
worktreeId: worktree.id,
|
|
113
|
+
threadId: threadId || worktree.threadId || null,
|
|
114
|
+
forkFromThreadId:
|
|
115
|
+
options.sourceThreadId ||
|
|
116
|
+
(worktree.context === "fork" ? worktree.forkSourceThreadId || null : null),
|
|
117
|
+
})
|
|
118
|
+
: new CodexAppServerClient({
|
|
119
|
+
cwd: worktree.path,
|
|
120
|
+
attachmentsDir,
|
|
121
|
+
repoDir,
|
|
122
|
+
internetAccess,
|
|
123
|
+
denyGitCredentialsAccess,
|
|
124
|
+
gitDir,
|
|
125
|
+
threadId: threadId || worktree.threadId || null,
|
|
126
|
+
env: {
|
|
127
|
+
...process.env,
|
|
128
|
+
...(tmpDir ? { TMPDIR: tmpDir } : {}),
|
|
129
|
+
},
|
|
130
|
+
workspaceId: worktree.workspaceId,
|
|
131
|
+
tmpDir,
|
|
132
|
+
sessionId: worktree.sessionId,
|
|
133
|
+
worktreeId: worktree.id,
|
|
134
|
+
threadStartMode:
|
|
135
|
+
options.threadStartMode ||
|
|
136
|
+
(worktree.context === "fork" ? "fork" : "new"),
|
|
137
|
+
sourceThreadId:
|
|
138
|
+
options.sourceThreadId ||
|
|
139
|
+
(worktree.context === "fork" ? worktree.forkSourceThreadId || null : null),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return client;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the currently active client for the session.
|
|
147
|
+
*
|
|
148
|
+
* @param {object} session - The session object
|
|
149
|
+
* @returns {CodexAppServerClient | ClaudeCliClient | null}
|
|
150
|
+
*/
|
|
151
|
+
export function getActiveClient(session) {
|
|
152
|
+
const runtime = getSessionRuntime(session.sessionId);
|
|
153
|
+
return runtime?.clients?.[session.activeProvider] || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if a provider string is valid.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} provider
|
|
160
|
+
* @returns {provider is "codex" | "claude"}
|
|
161
|
+
*/
|
|
162
|
+
export function isValidProvider(provider) {
|
|
163
|
+
return provider === "codex" || provider === "claude";
|
|
164
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { SYSTEM_PROMPT } from "./config.js";
|
|
5
|
+
import { createProviderLogger } from "./providerLogger.js";
|
|
6
|
+
import { buildSandboxArgs, getWorkspaceHome } from "./runAs.js";
|
|
7
|
+
|
|
8
|
+
const RUN_AS_HELPER = process.env.VIBE80_RUN_AS_HELPER || "/usr/local/bin/vibe80-run-as";
|
|
9
|
+
const SUDO_PATH = process.env.VIBE80_SUDO_PATH || "sudo";
|
|
10
|
+
const isMonoUser = process.env.DEPLOYMENT_MODE === "mono_user";
|
|
11
|
+
|
|
12
|
+
export class CodexAppServerClient extends EventEmitter {
|
|
13
|
+
constructor({
|
|
14
|
+
cwd,
|
|
15
|
+
attachmentsDir,
|
|
16
|
+
repoDir,
|
|
17
|
+
internetAccess,
|
|
18
|
+
denyGitCredentialsAccess,
|
|
19
|
+
gitDir,
|
|
20
|
+
threadId,
|
|
21
|
+
env,
|
|
22
|
+
workspaceId,
|
|
23
|
+
tmpDir,
|
|
24
|
+
sessionId,
|
|
25
|
+
worktreeId,
|
|
26
|
+
threadStartMode,
|
|
27
|
+
sourceThreadId,
|
|
28
|
+
}) {
|
|
29
|
+
super();
|
|
30
|
+
this.cwd = cwd;
|
|
31
|
+
this.attachmentsDir = attachmentsDir;
|
|
32
|
+
this.repoDir = repoDir || cwd;
|
|
33
|
+
this.internetAccess = internetAccess ?? true;
|
|
34
|
+
this.denyGitCredentialsAccess = denyGitCredentialsAccess ?? true;
|
|
35
|
+
if (this.internetAccess === false && this.denyGitCredentialsAccess) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Invalid Codex configuration: denyGitCredentialsAccess must be false when internetAccess is false."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
this.gitDir = gitDir || null;
|
|
41
|
+
this.env = env || process.env;
|
|
42
|
+
this.workspaceId = workspaceId;
|
|
43
|
+
this.tmpDir = tmpDir || null;
|
|
44
|
+
this.sessionId = sessionId || null;
|
|
45
|
+
this.worktreeId = worktreeId || "main";
|
|
46
|
+
this.proc = null;
|
|
47
|
+
this.buffer = "";
|
|
48
|
+
this.stdoutLogBuffer = "";
|
|
49
|
+
this.stderrLogBuffer = "";
|
|
50
|
+
this.activeTurnIds = new Set();
|
|
51
|
+
this.restartPending = false;
|
|
52
|
+
this.restarting = false;
|
|
53
|
+
this.starting = false;
|
|
54
|
+
this.stopping = false;
|
|
55
|
+
this.stopReason = null;
|
|
56
|
+
this.lastIdleAt = Date.now();
|
|
57
|
+
this.providerLogger = createProviderLogger({
|
|
58
|
+
provider: "codex",
|
|
59
|
+
sessionId: this.sessionId,
|
|
60
|
+
worktreeId: this.worktreeId,
|
|
61
|
+
});
|
|
62
|
+
this.nextId = 1;
|
|
63
|
+
this.pending = new Map();
|
|
64
|
+
this.threadId = threadId || null;
|
|
65
|
+
this.threadStartMode =
|
|
66
|
+
threadStartMode === "fork" && !threadId ? "fork" : threadId ? "resume" : "new";
|
|
67
|
+
this.sourceThreadId = sourceThreadId || null;
|
|
68
|
+
this.ready = false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async start() {
|
|
72
|
+
if (this.starting || this.restarting) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.starting = true;
|
|
76
|
+
const codexArgs = [
|
|
77
|
+
"codex",
|
|
78
|
+
"--enable",
|
|
79
|
+
"sqlite",
|
|
80
|
+
"app-server"
|
|
81
|
+
];
|
|
82
|
+
const useLandlock = this.internetAccess;
|
|
83
|
+
const shareGitCredentials = !this.denyGitCredentialsAccess;
|
|
84
|
+
const sshDir = path.join(getWorkspaceHome(this.workspaceId), ".ssh");
|
|
85
|
+
const sandboxArgs = !isMonoUser && useLandlock
|
|
86
|
+
? buildSandboxArgs({
|
|
87
|
+
cwd: this.cwd,
|
|
88
|
+
repoDir: this.repoDir,
|
|
89
|
+
attachmentsDir: this.attachmentsDir,
|
|
90
|
+
tmpDir: this.tmpDir,
|
|
91
|
+
workspaceId: this.workspaceId,
|
|
92
|
+
internetAccess: this.internetAccess,
|
|
93
|
+
netMode: "tcp:22,53,443",
|
|
94
|
+
extraAllowRw: [
|
|
95
|
+
path.join(getWorkspaceHome(this.workspaceId), ".codex"),
|
|
96
|
+
...(shareGitCredentials
|
|
97
|
+
? [sshDir, ...(this.gitDir ? [this.gitDir] : [])]
|
|
98
|
+
: []),
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
: [];
|
|
102
|
+
const spawnCommand = isMonoUser ? codexArgs[0] : SUDO_PATH;
|
|
103
|
+
const spawnArgs = isMonoUser
|
|
104
|
+
? codexArgs.slice(1)
|
|
105
|
+
: [
|
|
106
|
+
"-n",
|
|
107
|
+
RUN_AS_HELPER,
|
|
108
|
+
"--workspace-id",
|
|
109
|
+
this.workspaceId,
|
|
110
|
+
"--cwd",
|
|
111
|
+
this.cwd,
|
|
112
|
+
...sandboxArgs,
|
|
113
|
+
"--",
|
|
114
|
+
...codexArgs,
|
|
115
|
+
];
|
|
116
|
+
this.proc = spawn(spawnCommand, spawnArgs, {
|
|
117
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
118
|
+
env: this.env,
|
|
119
|
+
cwd: isMonoUser ? this.cwd : undefined,
|
|
120
|
+
});
|
|
121
|
+
const spawnReady = new Promise((resolve, reject) => {
|
|
122
|
+
this.proc.once("spawn", resolve);
|
|
123
|
+
this.proc.once("error", (error) => {
|
|
124
|
+
const details = [
|
|
125
|
+
`Failed to spawn Codex app-server`,
|
|
126
|
+
`mode=${isMonoUser ? "mono_user" : "multi_user"}`,
|
|
127
|
+
isMonoUser ? `cmd=${codexArgs[0]}` : `sudo=${SUDO_PATH}`,
|
|
128
|
+
isMonoUser ? null : `helper=${RUN_AS_HELPER}`,
|
|
129
|
+
`workspace=${this.workspaceId}`,
|
|
130
|
+
`cwd=${this.cwd}`,
|
|
131
|
+
`error=${error?.message || error}`,
|
|
132
|
+
]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join(" ");
|
|
135
|
+
this.emit("log", details);
|
|
136
|
+
reject(new Error(details));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.proc.stdout.setEncoding("utf8");
|
|
141
|
+
this.proc.stdout.on("data", (chunk) => {
|
|
142
|
+
this.#logStreamChunk("OUT", "stdoutLogBuffer", chunk);
|
|
143
|
+
this.#handleStdout(chunk);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this.proc.stderr.setEncoding("utf8");
|
|
147
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
148
|
+
this.#logStreamChunk("ERR", "stderrLogBuffer", chunk);
|
|
149
|
+
this.emit("log", chunk.trim());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
this.proc.on("exit", (code, signal) => {
|
|
153
|
+
const stopReason = this.stopReason;
|
|
154
|
+
this.stopReason = null;
|
|
155
|
+
this.ready = false;
|
|
156
|
+
this.activeTurnIds.clear();
|
|
157
|
+
this.starting = false;
|
|
158
|
+
this.stopping = false;
|
|
159
|
+
this.restarting = false;
|
|
160
|
+
this.#flushLogBuffer("OUT", "stdoutLogBuffer");
|
|
161
|
+
this.#flushLogBuffer("ERR", "stderrLogBuffer");
|
|
162
|
+
this.providerLogger?.close?.();
|
|
163
|
+
this.emit("exit", { code, signal, reason: stopReason });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await spawnReady;
|
|
168
|
+
await this.#initialize();
|
|
169
|
+
await this.#startThread();
|
|
170
|
+
this.ready = true;
|
|
171
|
+
this.lastIdleAt = Date.now();
|
|
172
|
+
this.emit("ready", { threadId: this.threadId });
|
|
173
|
+
this.#restartIfIdle();
|
|
174
|
+
} finally {
|
|
175
|
+
this.starting = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async stop({ force = false, timeoutMs = 5000, reason = null } = {}) {
|
|
180
|
+
if (!this.proc) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.stopping = true;
|
|
184
|
+
this.stopReason = reason || null;
|
|
185
|
+
const proc = this.proc;
|
|
186
|
+
this.proc = null;
|
|
187
|
+
const exitPromise = new Promise((resolve) => {
|
|
188
|
+
proc.once("exit", resolve);
|
|
189
|
+
proc.once("close", resolve);
|
|
190
|
+
});
|
|
191
|
+
if (force) {
|
|
192
|
+
proc.kill("SIGKILL");
|
|
193
|
+
await exitPromise;
|
|
194
|
+
this.stopping = false;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
proc.kill("SIGTERM");
|
|
198
|
+
const timeout = new Promise((resolve) => {
|
|
199
|
+
setTimeout(resolve, timeoutMs);
|
|
200
|
+
});
|
|
201
|
+
await Promise.race([exitPromise, timeout]);
|
|
202
|
+
if (!proc.killed) {
|
|
203
|
+
proc.kill("SIGKILL");
|
|
204
|
+
await exitPromise;
|
|
205
|
+
}
|
|
206
|
+
this.stopping = false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getStatus() {
|
|
210
|
+
if (this.restarting) return "restarting";
|
|
211
|
+
if (this.starting) return "starting";
|
|
212
|
+
if (this.stopping) return "stopping";
|
|
213
|
+
if (!this.proc && !this.ready) return "stopped";
|
|
214
|
+
if (!this.ready) return "starting";
|
|
215
|
+
if (this.activeTurnIds.size > 0) return "busy";
|
|
216
|
+
return "idle";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
requestRestart() {
|
|
220
|
+
this.restartPending = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async restart() {
|
|
224
|
+
if (this.restarting) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.restarting = true;
|
|
228
|
+
this.restartPending = false;
|
|
229
|
+
try {
|
|
230
|
+
await this.stop();
|
|
231
|
+
} catch {
|
|
232
|
+
// ignore stop errors
|
|
233
|
+
}
|
|
234
|
+
await this.start();
|
|
235
|
+
this.restarting = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async sendTurn(text) {
|
|
239
|
+
if (!this.threadId) {
|
|
240
|
+
throw new Error("Thread not ready yet.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.#sendRequest("turn/start", {
|
|
244
|
+
threadId: this.threadId,
|
|
245
|
+
input: [{ type: "text", text }],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async interruptTurn(turnId) {
|
|
250
|
+
if (!this.threadId) {
|
|
251
|
+
throw new Error("Thread not ready yet.");
|
|
252
|
+
}
|
|
253
|
+
if (!turnId) {
|
|
254
|
+
throw new Error("Turn id is required.");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return this.#sendRequest("turn/interrupt", {
|
|
258
|
+
threadId: this.threadId,
|
|
259
|
+
turnId,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async listModels(cursor = null, limit = 100) {
|
|
264
|
+
return this.#sendRequest("model/list", { cursor, limit });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async setDefaultModel(model, reasoningEffort = null) {
|
|
268
|
+
return this.#sendRequest("setDefaultModel", { model, reasoningEffort });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async startAccountLogin(params) {
|
|
272
|
+
return this.#sendRequest("account/login/start", params);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#handleStdout(chunk) {
|
|
276
|
+
this.buffer += chunk;
|
|
277
|
+
let newlineIndex;
|
|
278
|
+
|
|
279
|
+
while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
280
|
+
const raw = this.buffer.slice(0, newlineIndex).trim();
|
|
281
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
282
|
+
|
|
283
|
+
if (!raw) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let message;
|
|
288
|
+
try {
|
|
289
|
+
message = JSON.parse(raw);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.emit("log", `Failed to parse JSON: ${raw}`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.#handleMessage(message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#handleMessage(message) {
|
|
300
|
+
if (Object.prototype.hasOwnProperty.call(message, "id")) {
|
|
301
|
+
this.emit("rpc_in", message);
|
|
302
|
+
const pending = this.pending.get(message.id);
|
|
303
|
+
if (pending) {
|
|
304
|
+
this.pending.delete(message.id);
|
|
305
|
+
if (message.error) {
|
|
306
|
+
pending.reject(new Error(message.error.message || "Unknown error"));
|
|
307
|
+
} else {
|
|
308
|
+
pending.resolve(message.result);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (message.method) {
|
|
315
|
+
this.emit("rpc_in", message);
|
|
316
|
+
if (message.method === "turn/started") {
|
|
317
|
+
const turnId = message?.params?.turn?.id;
|
|
318
|
+
if (turnId) {
|
|
319
|
+
this.activeTurnIds.add(turnId);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (message.method === "turn/completed") {
|
|
323
|
+
const turnId = message?.params?.turn?.id;
|
|
324
|
+
if (turnId) {
|
|
325
|
+
this.activeTurnIds.delete(turnId);
|
|
326
|
+
}
|
|
327
|
+
if (this.activeTurnIds.size === 0) {
|
|
328
|
+
this.lastIdleAt = Date.now();
|
|
329
|
+
}
|
|
330
|
+
this.#restartIfIdle();
|
|
331
|
+
}
|
|
332
|
+
if (message.method === "error") {
|
|
333
|
+
const turnId = message?.params?.turnId;
|
|
334
|
+
const willRetry = Boolean(message?.params?.willRetry);
|
|
335
|
+
if (turnId && !willRetry) {
|
|
336
|
+
this.activeTurnIds.delete(turnId);
|
|
337
|
+
if (this.activeTurnIds.size === 0) {
|
|
338
|
+
this.lastIdleAt = Date.now();
|
|
339
|
+
}
|
|
340
|
+
this.#restartIfIdle();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
this.emit("notification", message);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#restartIfIdle() {
|
|
348
|
+
if (!this.restartPending || this.restarting) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (this.activeTurnIds.size === 0) {
|
|
352
|
+
void this.restart();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
markActive() {
|
|
357
|
+
this.lastIdleAt = Date.now();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#sendRequest(method, params) {
|
|
361
|
+
const id = this.nextId++;
|
|
362
|
+
const payload = { jsonrpc: "2.0", id, method, params };
|
|
363
|
+
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
this.pending.set(id, { resolve, reject });
|
|
366
|
+
this.emit("rpc_out", payload);
|
|
367
|
+
const line = JSON.stringify(payload);
|
|
368
|
+
this.providerLogger?.writeLine("IN", line);
|
|
369
|
+
this.proc.stdin.write(`${line}\n`);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#logStreamChunk(prefix, bufferKey, chunk) {
|
|
374
|
+
if (!this.providerLogger) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const text = chunk == null ? "" : String(chunk);
|
|
378
|
+
this[bufferKey] += text;
|
|
379
|
+
let newlineIndex;
|
|
380
|
+
while ((newlineIndex = this[bufferKey].indexOf("\n")) !== -1) {
|
|
381
|
+
let line = this[bufferKey].slice(0, newlineIndex);
|
|
382
|
+
if (line.endsWith("\r")) {
|
|
383
|
+
line = line.slice(0, -1);
|
|
384
|
+
}
|
|
385
|
+
this.providerLogger.writeLine(prefix, line);
|
|
386
|
+
this[bufferKey] = this[bufferKey].slice(newlineIndex + 1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#flushLogBuffer(prefix, bufferKey) {
|
|
391
|
+
if (!this.providerLogger) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const leftover = this[bufferKey];
|
|
395
|
+
if (leftover) {
|
|
396
|
+
this.providerLogger.writeLine(prefix, leftover);
|
|
397
|
+
this[bufferKey] = "";
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async #initialize() {
|
|
402
|
+
await this.#sendRequest("initialize", {
|
|
403
|
+
clientInfo: {
|
|
404
|
+
name: "vibe80",
|
|
405
|
+
version: "0.1.0",
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async #startThread() {
|
|
411
|
+
const shareGitCredentials = !this.denyGitCredentialsAccess;
|
|
412
|
+
const writableRoots = [
|
|
413
|
+
this.cwd,
|
|
414
|
+
this.repoDir,
|
|
415
|
+
this.attachmentsDir,
|
|
416
|
+
shareGitCredentials ? this.gitDir : null,
|
|
417
|
+
].filter(Boolean);
|
|
418
|
+
const sandboxMode = isMonoUser
|
|
419
|
+
? "workspace-write"
|
|
420
|
+
: this.internetAccess
|
|
421
|
+
? "danger-full-access"
|
|
422
|
+
: "workspace-write";
|
|
423
|
+
|
|
424
|
+
const params = {
|
|
425
|
+
cwd: this.cwd,
|
|
426
|
+
config: {
|
|
427
|
+
// Reserved for future usage
|
|
428
|
+
// "developer_instructions": "",
|
|
429
|
+
"sandbox_workspace_write.writable_roots": writableRoots,
|
|
430
|
+
"sandbox_workspace_write.network_access": Boolean(this.internetAccess),
|
|
431
|
+
"web_search": this.internetAccess ? "live" : "disabled"
|
|
432
|
+
},
|
|
433
|
+
baseInstructions: SYSTEM_PROMPT,
|
|
434
|
+
sandbox: sandboxMode,
|
|
435
|
+
approvalPolicy: "never"
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
this.emit("thread_starting", {
|
|
439
|
+
mode:
|
|
440
|
+
this.threadStartMode === "fork"
|
|
441
|
+
? "fork"
|
|
442
|
+
: this.threadId
|
|
443
|
+
? "resume"
|
|
444
|
+
: "start",
|
|
445
|
+
threadId: this.threadStartMode === "fork" ? this.sourceThreadId : this.threadId || null,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result =
|
|
449
|
+
this.threadStartMode === "fork" && this.sourceThreadId
|
|
450
|
+
? await this.#sendRequest("thread/fork", {
|
|
451
|
+
...params,
|
|
452
|
+
threadId: this.sourceThreadId,
|
|
453
|
+
})
|
|
454
|
+
: this.threadId
|
|
455
|
+
? await this.#sendRequest("thread/resume", {
|
|
456
|
+
...params,
|
|
457
|
+
threadId: this.threadId,
|
|
458
|
+
})
|
|
459
|
+
: await this.#sendRequest("thread/start", {
|
|
460
|
+
...params,
|
|
461
|
+
includePlanTool: true,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
this.threadId = result.thread.id;
|
|
465
|
+
this.threadStartMode = "resume";
|
|
466
|
+
this.sourceThreadId = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
|
|
7
|
+
export const SYSTEM_PROMPT =
|
|
8
|
+
process.env.SYSTEM_PROMPT ||
|
|
9
|
+
"output markdown format for inline generated text;" +
|
|
10
|
+
"Reference files using relative paths when possible; " +
|
|
11
|
+
"When proposing possible next steps, use: " +
|
|
12
|
+
"<!-- vibe80:choices <question?> --> then options (one per line), end with " +
|
|
13
|
+
"<!-- /vibe80:choices --> ; When complex user input is required, output ONLY a vibe80 form: " +
|
|
14
|
+
"<!-- vibe80:form {question} --> input|textarea|radio|select|checkbox::field_id::Label::Default|Choices" +
|
|
15
|
+
"<!-- /vibe80:form --> One field per line and Use :: as choices separator; " +
|
|
16
|
+
"example form <!-- vibe80:form How r u? -->select::Anwser::Fine::very fine<!-- /vibe80:form -->" +
|
|
17
|
+
"Use <!-- vibe80:yesno <question?> --> to ask yes/no questions;" +
|
|
18
|
+
"Use <!-- vibe80:task <short_task_description> --> to notify the user about what you are doing;" +
|
|
19
|
+
"Use <!-- vibe80:fileref <filepath> --> to reference any file in the current repository";
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_GIT_AUTHOR_NAME =
|
|
22
|
+
process.env.DEFAULT_GIT_AUTHOR_NAME || "Vibe80 agent";
|
|
23
|
+
export const DEFAULT_GIT_AUTHOR_EMAIL =
|
|
24
|
+
process.env.DEFAULT_GIT_AUTHOR_EMAIL || "vibe80@example.org";
|
|
25
|
+
|
|
26
|
+
export const GIT_HOOKS_DIR = process.env.VIBE80_GIT_HOOKS_DIR
|
|
27
|
+
|| path.resolve(__dirname, "../../git_hooks");
|