@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.
Files changed (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. 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");