@cluesmith/codev 2.0.2 → 2.0.3

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 (209) hide show
  1. package/dashboard/dist/assets/{index-b38SaXk5.js → index-UsH9ixz1.js} +20 -20
  2. package/dashboard/dist/assets/index-UsH9ixz1.js.map +1 -0
  3. package/dashboard/dist/index.html +1 -1
  4. package/dist/agent-farm/cli.js +1 -1
  5. package/dist/agent-farm/cli.js.map +1 -1
  6. package/dist/agent-farm/commands/architect.d.ts +1 -1
  7. package/dist/agent-farm/commands/architect.js +3 -3
  8. package/dist/agent-farm/commands/architect.js.map +1 -1
  9. package/dist/agent-farm/commands/attach.js +1 -1
  10. package/dist/agent-farm/commands/attach.js.map +1 -1
  11. package/dist/agent-farm/commands/cleanup.js +6 -6
  12. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  13. package/dist/agent-farm/commands/open.js +5 -5
  14. package/dist/agent-farm/commands/open.js.map +1 -1
  15. package/dist/agent-farm/commands/send.js +5 -5
  16. package/dist/agent-farm/commands/send.js.map +1 -1
  17. package/dist/agent-farm/commands/shell.js +5 -5
  18. package/dist/agent-farm/commands/shell.js.map +1 -1
  19. package/dist/agent-farm/commands/spawn-worktree.d.ts +1 -1
  20. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/spawn-worktree.js +8 -8
  22. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  23. package/dist/agent-farm/commands/spawn.js +3 -3
  24. package/dist/agent-farm/commands/spawn.js.map +1 -1
  25. package/dist/agent-farm/commands/start.d.ts +4 -4
  26. package/dist/agent-farm/commands/start.js +16 -16
  27. package/dist/agent-farm/commands/start.js.map +1 -1
  28. package/dist/agent-farm/commands/status.d.ts +1 -1
  29. package/dist/agent-farm/commands/status.js +16 -16
  30. package/dist/agent-farm/commands/status.js.map +1 -1
  31. package/dist/agent-farm/commands/stop.d.ts +4 -4
  32. package/dist/agent-farm/commands/stop.js +9 -9
  33. package/dist/agent-farm/commands/stop.js.map +1 -1
  34. package/dist/agent-farm/db/index.d.ts.map +1 -1
  35. package/dist/agent-farm/db/index.js +82 -7
  36. package/dist/agent-farm/db/index.js.map +1 -1
  37. package/dist/agent-farm/db/schema.d.ts +2 -2
  38. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  39. package/dist/agent-farm/db/schema.js +21 -4
  40. package/dist/agent-farm/db/schema.js.map +1 -1
  41. package/dist/agent-farm/lib/tower-client.d.ts +20 -20
  42. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  43. package/dist/agent-farm/lib/tower-client.js +25 -25
  44. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  45. package/dist/agent-farm/lib/tunnel-client.d.ts +12 -2
  46. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  47. package/dist/agent-farm/lib/tunnel-client.js +59 -1
  48. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  49. package/dist/agent-farm/servers/tower-instances.d.ts +18 -18
  50. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  51. package/dist/agent-farm/servers/tower-instances.js +89 -89
  52. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  53. package/dist/agent-farm/servers/tower-routes.d.ts +1 -1
  54. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  55. package/dist/agent-farm/servers/tower-routes.js +184 -162
  56. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  57. package/dist/agent-farm/servers/tower-server.js +23 -19
  58. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  59. package/dist/agent-farm/servers/tower-terminals.d.ts +27 -29
  60. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  61. package/dist/agent-farm/servers/tower-terminals.js +95 -116
  62. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  63. package/dist/agent-farm/servers/tower-tunnel.d.ts +2 -2
  64. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  65. package/dist/agent-farm/servers/tower-tunnel.js +12 -12
  66. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  67. package/dist/agent-farm/servers/tower-types.d.ts +8 -10
  68. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
  69. package/dist/agent-farm/servers/tower-utils.d.ts +9 -9
  70. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/tower-utils.js +18 -18
  72. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  73. package/dist/agent-farm/servers/tower-websocket.d.ts +2 -2
  74. package/dist/agent-farm/servers/tower-websocket.js +14 -14
  75. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  76. package/dist/agent-farm/types.d.ts +2 -2
  77. package/dist/agent-farm/types.d.ts.map +1 -1
  78. package/dist/agent-farm/utils/config.d.ts +1 -1
  79. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  80. package/dist/agent-farm/utils/config.js +16 -16
  81. package/dist/agent-farm/utils/config.js.map +1 -1
  82. package/dist/agent-farm/utils/file-tabs.d.ts +3 -3
  83. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -1
  84. package/dist/agent-farm/utils/file-tabs.js +9 -9
  85. package/dist/agent-farm/utils/file-tabs.js.map +1 -1
  86. package/dist/agent-farm/utils/gate-status.d.ts +2 -2
  87. package/dist/agent-farm/utils/gate-status.d.ts.map +1 -1
  88. package/dist/agent-farm/utils/gate-status.js +3 -3
  89. package/dist/agent-farm/utils/gate-status.js.map +1 -1
  90. package/dist/agent-farm/utils/index.d.ts +0 -1
  91. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  92. package/dist/agent-farm/utils/index.js +0 -1
  93. package/dist/agent-farm/utils/index.js.map +1 -1
  94. package/dist/agent-farm/utils/notifications.d.ts +4 -4
  95. package/dist/agent-farm/utils/notifications.d.ts.map +1 -1
  96. package/dist/agent-farm/utils/notifications.js +18 -18
  97. package/dist/agent-farm/utils/notifications.js.map +1 -1
  98. package/dist/commands/adopt.d.ts +2 -2
  99. package/dist/commands/adopt.d.ts.map +1 -1
  100. package/dist/commands/adopt.js +13 -3
  101. package/dist/commands/adopt.js.map +1 -1
  102. package/dist/commands/consult/index.d.ts +1 -1
  103. package/dist/commands/consult/index.d.ts.map +1 -1
  104. package/dist/commands/consult/index.js +52 -51
  105. package/dist/commands/consult/index.js.map +1 -1
  106. package/dist/commands/doctor.js +6 -6
  107. package/dist/commands/doctor.js.map +1 -1
  108. package/dist/commands/import.js +4 -4
  109. package/dist/commands/import.js.map +1 -1
  110. package/dist/commands/init.d.ts +2 -2
  111. package/dist/commands/init.d.ts.map +1 -1
  112. package/dist/commands/init.js +13 -3
  113. package/dist/commands/init.js.map +1 -1
  114. package/dist/commands/porch/index.d.ts +6 -6
  115. package/dist/commands/porch/index.d.ts.map +1 -1
  116. package/dist/commands/porch/index.js +37 -37
  117. package/dist/commands/porch/index.js.map +1 -1
  118. package/dist/commands/porch/next.d.ts +1 -1
  119. package/dist/commands/porch/next.d.ts.map +1 -1
  120. package/dist/commands/porch/next.js +43 -40
  121. package/dist/commands/porch/next.js.map +1 -1
  122. package/dist/commands/porch/notify.d.ts +11 -0
  123. package/dist/commands/porch/notify.d.ts.map +1 -0
  124. package/dist/commands/porch/notify.js +30 -0
  125. package/dist/commands/porch/notify.js.map +1 -0
  126. package/dist/commands/porch/plan.d.ts +1 -1
  127. package/dist/commands/porch/plan.d.ts.map +1 -1
  128. package/dist/commands/porch/plan.js +3 -3
  129. package/dist/commands/porch/plan.js.map +1 -1
  130. package/dist/commands/porch/prompts.d.ts +1 -1
  131. package/dist/commands/porch/prompts.d.ts.map +1 -1
  132. package/dist/commands/porch/prompts.js +13 -13
  133. package/dist/commands/porch/prompts.js.map +1 -1
  134. package/dist/commands/porch/protocol.d.ts +1 -1
  135. package/dist/commands/porch/protocol.d.ts.map +1 -1
  136. package/dist/commands/porch/protocol.js +6 -6
  137. package/dist/commands/porch/protocol.js.map +1 -1
  138. package/dist/commands/porch/state.d.ts +6 -6
  139. package/dist/commands/porch/state.d.ts.map +1 -1
  140. package/dist/commands/porch/state.js +11 -11
  141. package/dist/commands/porch/state.js.map +1 -1
  142. package/dist/commands/update.d.ts.map +1 -1
  143. package/dist/commands/update.js +10 -1
  144. package/dist/commands/update.js.map +1 -1
  145. package/dist/lib/scaffold.d.ts +13 -0
  146. package/dist/lib/scaffold.d.ts.map +1 -1
  147. package/dist/lib/scaffold.js +34 -0
  148. package/dist/lib/scaffold.js.map +1 -1
  149. package/dist/lib/skeleton.d.ts +7 -7
  150. package/dist/lib/skeleton.d.ts.map +1 -1
  151. package/dist/lib/skeleton.js +10 -10
  152. package/dist/lib/skeleton.js.map +1 -1
  153. package/dist/terminal/pty-manager.d.ts +1 -1
  154. package/dist/terminal/pty-manager.d.ts.map +1 -1
  155. package/dist/terminal/pty-manager.js +3 -3
  156. package/dist/terminal/pty-manager.js.map +1 -1
  157. package/package.json +1 -1
  158. package/templates/open.html +13 -13
  159. package/templates/tower.html +54 -54
  160. package/templates/vendor/marked.min.js +6 -0
  161. package/templates/vendor/prism-bash.min.js +1 -0
  162. package/templates/vendor/prism-css.min.js +1 -0
  163. package/templates/vendor/prism-javascript.min.js +1 -0
  164. package/templates/vendor/prism-json.min.js +1 -0
  165. package/templates/vendor/prism-markdown.min.js +1 -0
  166. package/templates/vendor/prism-markup.min.js +1 -0
  167. package/templates/vendor/prism-python.min.js +1 -0
  168. package/templates/vendor/prism-tomorrow.min.css +1 -0
  169. package/templates/vendor/prism-typescript.min.js +1 -0
  170. package/templates/vendor/prism-yaml.min.js +1 -0
  171. package/templates/vendor/prism.min.js +1 -0
  172. package/templates/vendor/purify.min.js +3 -0
  173. package/dashboard/dist/assets/index-b38SaXk5.js.map +0 -1
  174. package/dist/agent-farm/hq-connector.d.ts +0 -19
  175. package/dist/agent-farm/hq-connector.d.ts.map +0 -1
  176. package/dist/agent-farm/hq-connector.js +0 -351
  177. package/dist/agent-farm/hq-connector.js.map +0 -1
  178. package/dist/agent-farm/utils/deps.d.ts +0 -51
  179. package/dist/agent-farm/utils/deps.d.ts.map +0 -1
  180. package/dist/agent-farm/utils/deps.js +0 -162
  181. package/dist/agent-farm/utils/deps.js.map +0 -1
  182. package/dist/agent-farm/utils/gate-watcher.d.ts +0 -38
  183. package/dist/agent-farm/utils/gate-watcher.d.ts.map +0 -1
  184. package/dist/agent-farm/utils/gate-watcher.js +0 -122
  185. package/dist/agent-farm/utils/gate-watcher.js.map +0 -1
  186. package/dist/agent-farm/utils/session.d.ts +0 -32
  187. package/dist/agent-farm/utils/session.d.ts.map +0 -1
  188. package/dist/agent-farm/utils/session.js +0 -57
  189. package/dist/agent-farm/utils/session.js.map +0 -1
  190. package/dist/lib/projectlist-parser.d.ts +0 -70
  191. package/dist/lib/projectlist-parser.d.ts.map +0 -1
  192. package/dist/lib/projectlist-parser.js +0 -200
  193. package/dist/lib/projectlist-parser.js.map +0 -1
  194. package/templates/dashboard/css/dialogs.css +0 -149
  195. package/templates/dashboard/css/files.css +0 -558
  196. package/templates/dashboard/css/layout.css +0 -133
  197. package/templates/dashboard/css/projects.css +0 -501
  198. package/templates/dashboard/css/statusbar.css +0 -23
  199. package/templates/dashboard/css/tabs.css +0 -314
  200. package/templates/dashboard/css/utilities.css +0 -50
  201. package/templates/dashboard/css/variables.css +0 -45
  202. package/templates/dashboard/index.html +0 -149
  203. package/templates/dashboard/js/dialogs.js +0 -368
  204. package/templates/dashboard/js/files.js +0 -448
  205. package/templates/dashboard/js/main.js +0 -476
  206. package/templates/dashboard/js/projects.js +0 -544
  207. package/templates/dashboard/js/state.js +0 -91
  208. package/templates/dashboard/js/tabs.js +0 -518
  209. package/templates/dashboard/js/utils.js +0 -191
@@ -18,10 +18,10 @@ import { execSync } from 'node:child_process';
18
18
  import { homedir, tmpdir } from 'node:os';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { parseJsonBody, isRequestAllowed } from '../utils/server-utils.js';
21
- import { isRateLimited, normalizeProjectPath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
21
+ import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
22
22
  import { handleTunnelEndpoint } from './tower-tunnel.js';
23
- import { getKnownProjectPaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
24
- import { getProjectTerminals, getTerminalManager, getProjectTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteProjectTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForProject, } from './tower-terminals.js';
23
+ import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, stopInstance, } from './tower-instances.js';
24
+ import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, deleteWorkspaceTerminalSessions, saveFileTab, deleteFileTab, getTerminalsForWorkspace, } from './tower-terminals.js';
25
25
  const __filename = fileURLToPath(import.meta.url);
26
26
  const __dirname = path.dirname(__filename);
27
27
  // ============================================================================
@@ -36,14 +36,14 @@ async function readBody(req) {
36
36
  }
37
37
  const ROUTES = {
38
38
  'GET /health': (_req, res) => handleHealthCheck(res),
39
- 'GET /api/projects': (_req, res) => handleListProjects(res),
39
+ 'GET /api/workspaces': (_req, res) => handleListWorkspaces(res),
40
40
  'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
41
41
  'GET /api/terminals': (_req, res) => handleTerminalList(res),
42
42
  'GET /api/status': (_req, res) => handleStatus(res),
43
43
  'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx),
44
44
  'POST /api/notify': (req, res, _url, ctx) => handleNotify(req, res, ctx),
45
45
  'GET /api/browse': (_req, res, url) => handleBrowse(res, url),
46
- 'POST /api/create': (req, res, _url, ctx) => handleCreateProject(req, res, ctx),
46
+ 'POST /api/create': (req, res, _url, ctx) => handleCreateWorkspace(req, res, ctx),
47
47
  'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
48
48
  'POST /api/stop': (req, res) => handleStopInstance(req, res),
49
49
  'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
@@ -89,19 +89,19 @@ export async function handleRequest(req, res, ctx) {
89
89
  await handleTunnelEndpoint(req, res, tunnelSub);
90
90
  return;
91
91
  }
92
- // Project API: /api/projects/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
93
- const projectApiMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/(activate|deactivate|status)$/);
94
- if (projectApiMatch) {
95
- return await handleProjectAction(req, res, ctx, projectApiMatch);
92
+ // Workspace API: /api/workspaces/:encodedPath/activate|deactivate|status (Spec 0090 Phase 1)
93
+ const workspaceApiMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/(activate|deactivate|status)$/);
94
+ if (workspaceApiMatch) {
95
+ return await handleWorkspaceAction(req, res, ctx, workspaceApiMatch);
96
96
  }
97
97
  // Terminal-specific routes: /api/terminals/:id/* (Spec 0090 Phase 2)
98
98
  const terminalRouteMatch = url.pathname.match(/^\/api\/terminals\/([^/]+)(\/.*)?$/);
99
99
  if (terminalRouteMatch) {
100
100
  return await handleTerminalRoutes(req, res, url, terminalRouteMatch);
101
101
  }
102
- // Project routes: /project/:base64urlPath/* (Spec 0090 Phase 4)
103
- if (url.pathname.startsWith('/project/')) {
104
- return await handleProjectRoutes(req, res, ctx, url);
102
+ // Workspace routes: /workspace/:base64urlPath/* (Spec 0090 Phase 4)
103
+ if (url.pathname.startsWith('/workspace/')) {
104
+ return await handleWorkspaceRoutes(req, res, ctx, url);
105
105
  }
106
106
  // 404 for everything else
107
107
  res.writeHead(404, { 'Content-Type': 'text/plain' });
@@ -123,59 +123,59 @@ async function handleHealthCheck(res) {
123
123
  res.end(JSON.stringify({
124
124
  status: 'healthy',
125
125
  uptime: process.uptime(),
126
- activeProjects: activeCount,
127
- totalProjects: instances.length,
126
+ activeWorkspaces: activeCount,
127
+ totalWorkspaces: instances.length,
128
128
  memoryUsage: process.memoryUsage().heapUsed,
129
129
  timestamp: new Date().toISOString(),
130
130
  }));
131
131
  }
132
- async function handleListProjects(res) {
132
+ async function handleListWorkspaces(res) {
133
133
  const instances = await getInstances();
134
- const projects = instances.map((i) => ({
135
- path: i.projectPath,
136
- name: i.projectName,
134
+ const workspaces = instances.map((i) => ({
135
+ path: i.workspacePath,
136
+ name: i.workspaceName,
137
137
  active: i.running,
138
138
  proxyUrl: i.proxyUrl,
139
139
  terminals: i.terminals.length,
140
140
  }));
141
141
  res.writeHead(200, { 'Content-Type': 'application/json' });
142
- res.end(JSON.stringify({ projects }));
142
+ res.end(JSON.stringify({ workspaces }));
143
143
  }
144
- async function handleProjectAction(req, res, ctx, match) {
144
+ async function handleWorkspaceAction(req, res, ctx, match) {
145
145
  const [, encodedPath, action] = match;
146
- let projectPath;
146
+ let workspacePath;
147
147
  try {
148
- projectPath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
149
- if (!projectPath || (!projectPath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(projectPath))) {
148
+ workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
149
+ if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
150
150
  throw new Error('Invalid path');
151
151
  }
152
- projectPath = normalizeProjectPath(projectPath);
152
+ workspacePath = normalizeWorkspacePath(workspacePath);
153
153
  }
154
154
  catch {
155
155
  res.writeHead(400, { 'Content-Type': 'application/json' });
156
- res.end(JSON.stringify({ error: 'Invalid project path encoding' }));
156
+ res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
157
157
  return;
158
158
  }
159
- // GET /api/projects/:path/status
159
+ // GET /api/workspaces/:path/status
160
160
  if (req.method === 'GET' && action === 'status') {
161
161
  const instances = await getInstances();
162
- const instance = instances.find((i) => i.projectPath === projectPath);
162
+ const instance = instances.find((i) => i.workspacePath === workspacePath);
163
163
  if (!instance) {
164
164
  res.writeHead(404, { 'Content-Type': 'application/json' });
165
- res.end(JSON.stringify({ error: 'Project not found' }));
165
+ res.end(JSON.stringify({ error: 'Workspace not found' }));
166
166
  return;
167
167
  }
168
168
  res.writeHead(200, { 'Content-Type': 'application/json' });
169
169
  res.end(JSON.stringify({
170
- path: instance.projectPath,
171
- name: instance.projectName,
170
+ path: instance.workspacePath,
171
+ name: instance.workspaceName,
172
172
  active: instance.running,
173
173
  terminals: instance.terminals,
174
174
  gateStatus: instance.gateStatus,
175
175
  }));
176
176
  return;
177
177
  }
178
- // POST /api/projects/:path/activate
178
+ // POST /api/workspaces/:path/activate
179
179
  if (req.method === 'POST' && action === 'activate') {
180
180
  // Rate limiting: 10 activations per minute per client
181
181
  const clientIp = req.socket.remoteAddress || '127.0.0.1';
@@ -184,7 +184,7 @@ async function handleProjectAction(req, res, ctx, match) {
184
184
  res.end(JSON.stringify({ error: 'Too many activations, try again later' }));
185
185
  return;
186
186
  }
187
- const result = await launchInstance(projectPath);
187
+ const result = await launchInstance(workspacePath);
188
188
  if (result.success) {
189
189
  res.writeHead(200, { 'Content-Type': 'application/json' });
190
190
  res.end(JSON.stringify({ success: true, adopted: result.adopted }));
@@ -195,17 +195,17 @@ async function handleProjectAction(req, res, ctx, match) {
195
195
  }
196
196
  return;
197
197
  }
198
- // POST /api/projects/:path/deactivate
198
+ // POST /api/workspaces/:path/deactivate
199
199
  if (req.method === 'POST' && action === 'deactivate') {
200
- const knownPaths = getKnownProjectPaths();
201
- const resolvedPath = fs.existsSync(projectPath) ? fs.realpathSync(projectPath) : projectPath;
202
- const isKnown = knownPaths.some((p) => p === projectPath || p === resolvedPath);
200
+ const knownPaths = getKnownWorkspacePaths();
201
+ const resolvedPath = fs.existsSync(workspacePath) ? fs.realpathSync(workspacePath) : workspacePath;
202
+ const isKnown = knownPaths.some((p) => p === workspacePath || p === resolvedPath);
203
203
  if (!isKnown) {
204
204
  res.writeHead(404, { 'Content-Type': 'application/json' });
205
- res.end(JSON.stringify({ ok: false, error: 'Project not found' }));
205
+ res.end(JSON.stringify({ ok: false, error: 'Workspace not found' }));
206
206
  return;
207
207
  }
208
- const result = await stopInstance(projectPath);
208
+ const result = await stopInstance(workspacePath);
209
209
  res.writeHead(200, { 'Content-Type': 'application/json' });
210
210
  res.end(JSON.stringify(result));
211
211
  return;
@@ -224,7 +224,7 @@ async function handleTerminalCreate(req, res, ctx) {
224
224
  const env = typeof body.env === 'object' && body.env !== null ? body.env : undefined;
225
225
  const label = typeof body.label === 'string' ? body.label : undefined;
226
226
  // Optional session persistence via shellper
227
- const projectPath = typeof body.projectPath === 'string' ? body.projectPath : null;
227
+ const workspacePath = typeof body.workspacePath === 'string' ? body.workspacePath : null;
228
228
  const termType = typeof body.type === 'string' && ['builder', 'shell'].includes(body.type) ? body.type : null;
229
229
  const roleId = typeof body.roleId === 'string' ? body.roleId : null;
230
230
  const requestPersistence = body.persistent === true;
@@ -260,16 +260,16 @@ async function handleTerminalCreate(req, res, ctx) {
260
260
  }
261
261
  info = session;
262
262
  persistent = true;
263
- if (projectPath && termType && roleId) {
264
- const entry = getProjectTerminalsEntry(normalizeProjectPath(projectPath));
263
+ if (workspacePath && termType && roleId) {
264
+ const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
265
265
  if (termType === 'builder') {
266
266
  entry.builders.set(roleId, session.id);
267
267
  }
268
268
  else {
269
269
  entry.shells.set(roleId, session.id);
270
270
  }
271
- saveTerminalSession(session.id, projectPath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
272
- ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for project ${projectPath}`);
271
+ saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
272
+ ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
273
273
  }
274
274
  }
275
275
  catch (shellperErr) {
@@ -281,16 +281,16 @@ async function handleTerminalCreate(req, res, ctx) {
281
281
  if (!info) {
282
282
  info = await manager.createSession({ command, args, cols, rows, cwd, env, label });
283
283
  persistent = false;
284
- if (projectPath && termType && roleId) {
285
- const entry = getProjectTerminalsEntry(normalizeProjectPath(projectPath));
284
+ if (workspacePath && termType && roleId) {
285
+ const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
286
286
  if (termType === 'builder') {
287
287
  entry.builders.set(roleId, info.id);
288
288
  }
289
289
  else {
290
290
  entry.shells.set(roleId, info.id);
291
291
  }
292
- saveTerminalSession(info.id, projectPath, termType, roleId, info.pid);
293
- ctx.log('WARN', `Terminal ${info.id} for ${projectPath} is non-persistent (shellper unavailable)`);
292
+ saveTerminalSession(info.id, workspacePath, termType, roleId, info.pid);
293
+ ctx.log('WARN', `Terminal ${info.id} for ${workspacePath} is non-persistent (shellper unavailable)`);
294
294
  }
295
295
  }
296
296
  res.writeHead(201, { 'Content-Type': 'application/json' });
@@ -429,7 +429,7 @@ async function handleNotify(req, res, ctx) {
429
429
  const type = typeof body.type === 'string' ? body.type : 'info';
430
430
  const title = typeof body.title === 'string' ? body.title : '';
431
431
  const messageBody = typeof body.body === 'string' ? body.body : '';
432
- const project = typeof body.project === 'string' ? body.project : undefined;
432
+ const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
433
433
  if (!title || !messageBody) {
434
434
  res.writeHead(400, { 'Content-Type': 'application/json' });
435
435
  res.end(JSON.stringify({ success: false, error: 'Missing title or body' }));
@@ -440,7 +440,7 @@ async function handleNotify(req, res, ctx) {
440
440
  type,
441
441
  title,
442
442
  body: messageBody,
443
- project,
443
+ workspace,
444
444
  });
445
445
  ctx.log('INFO', `Notification broadcast: ${title}`);
446
446
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -458,19 +458,19 @@ async function handleBrowse(res, url) {
458
458
  res.end(JSON.stringify({ suggestions: [], error: err.message }));
459
459
  }
460
460
  }
461
- async function handleCreateProject(req, res, ctx) {
461
+ async function handleCreateWorkspace(req, res, ctx) {
462
462
  const body = await parseJsonBody(req);
463
463
  const parentPath = body.parent;
464
- const projectName = body.name;
465
- if (!parentPath || !projectName) {
464
+ const workspaceName = body.name;
465
+ if (!parentPath || !workspaceName) {
466
466
  res.writeHead(400, { 'Content-Type': 'application/json' });
467
467
  res.end(JSON.stringify({ success: false, error: 'Missing parent or name' }));
468
468
  return;
469
469
  }
470
- // Validate project name
471
- if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
470
+ // Validate workspace name
471
+ if (!/^[a-zA-Z0-9_-]+$/.test(workspaceName)) {
472
472
  res.writeHead(400, { 'Content-Type': 'application/json' });
473
- res.end(JSON.stringify({ success: false, error: 'Invalid project name' }));
473
+ res.end(JSON.stringify({ success: false, error: 'Invalid workspace name' }));
474
474
  return;
475
475
  }
476
476
  // Expand ~ to home directory
@@ -484,77 +484,77 @@ async function handleCreateProject(req, res, ctx) {
484
484
  res.end(JSON.stringify({ success: false, error: `Parent directory does not exist: ${parentPath}` }));
485
485
  return;
486
486
  }
487
- const projectPath = path.join(expandedParent, projectName);
488
- // Check if project already exists
489
- if (fs.existsSync(projectPath)) {
487
+ const workspacePath = path.join(expandedParent, workspaceName);
488
+ // Check if workspace already exists
489
+ if (fs.existsSync(workspacePath)) {
490
490
  res.writeHead(400, { 'Content-Type': 'application/json' });
491
- res.end(JSON.stringify({ success: false, error: `Directory already exists: ${projectPath}` }));
491
+ res.end(JSON.stringify({ success: false, error: `Directory already exists: ${workspacePath}` }));
492
492
  return;
493
493
  }
494
494
  try {
495
495
  // Run codev init (it creates the directory)
496
- execSync(`codev init --yes "${projectName}"`, {
496
+ execSync(`codev init --yes "${workspaceName}"`, {
497
497
  cwd: expandedParent,
498
498
  stdio: 'pipe',
499
499
  timeout: 60000,
500
500
  });
501
501
  // Launch the instance
502
- const launchResult = await launchInstance(projectPath);
502
+ const launchResult = await launchInstance(workspacePath);
503
503
  if (!launchResult.success) {
504
504
  res.writeHead(500, { 'Content-Type': 'application/json' });
505
505
  res.end(JSON.stringify({ success: false, error: launchResult.error }));
506
506
  return;
507
507
  }
508
508
  res.writeHead(200, { 'Content-Type': 'application/json' });
509
- res.end(JSON.stringify({ success: true, projectPath }));
509
+ res.end(JSON.stringify({ success: true, workspacePath }));
510
510
  }
511
511
  catch (err) {
512
512
  // Clean up on failure
513
513
  try {
514
- if (fs.existsSync(projectPath)) {
515
- fs.rmSync(projectPath, { recursive: true });
514
+ if (fs.existsSync(workspacePath)) {
515
+ fs.rmSync(workspacePath, { recursive: true });
516
516
  }
517
517
  }
518
518
  catch {
519
519
  // Ignore cleanup errors
520
520
  }
521
521
  res.writeHead(500, { 'Content-Type': 'application/json' });
522
- res.end(JSON.stringify({ success: false, error: `Failed to create project: ${err.message}` }));
522
+ res.end(JSON.stringify({ success: false, error: `Failed to create workspace: ${err.message}` }));
523
523
  }
524
524
  }
525
525
  async function handleLaunchInstance(req, res) {
526
526
  const body = await parseJsonBody(req);
527
- let projectPath = body.projectPath;
528
- if (!projectPath) {
527
+ let workspacePath = body.workspacePath;
528
+ if (!workspacePath) {
529
529
  res.writeHead(400, { 'Content-Type': 'application/json' });
530
- res.end(JSON.stringify({ success: false, error: 'Missing projectPath' }));
530
+ res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
531
531
  return;
532
532
  }
533
533
  // Expand ~ to home directory
534
- if (projectPath.startsWith('~')) {
535
- projectPath = projectPath.replace('~', homedir());
534
+ if (workspacePath.startsWith('~')) {
535
+ workspacePath = workspacePath.replace('~', homedir());
536
536
  }
537
537
  // Reject relative paths — tower daemon CWD is unpredictable
538
- if (!path.isAbsolute(projectPath)) {
538
+ if (!path.isAbsolute(workspacePath)) {
539
539
  res.writeHead(400, { 'Content-Type': 'application/json' });
540
540
  res.end(JSON.stringify({
541
541
  success: false,
542
- error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../project or ~/Development/project).`,
542
+ error: `Relative paths are not supported. Use an absolute path (e.g., /Users/.../workspace or ~/Development/workspace).`,
543
543
  }));
544
544
  return;
545
545
  }
546
546
  // Normalize path (resolve .. segments, trailing slashes)
547
- projectPath = path.resolve(projectPath);
548
- const result = await launchInstance(projectPath);
547
+ workspacePath = path.resolve(workspacePath);
548
+ const result = await launchInstance(workspacePath);
549
549
  res.writeHead(result.success ? 200 : 400, { 'Content-Type': 'application/json' });
550
550
  res.end(JSON.stringify(result));
551
551
  }
552
552
  async function handleStopInstance(req, res) {
553
553
  const body = await parseJsonBody(req);
554
- const targetPath = body.projectPath;
554
+ const targetPath = body.workspacePath;
555
555
  if (!targetPath) {
556
556
  res.writeHead(400, { 'Content-Type': 'application/json' });
557
- res.end(JSON.stringify({ success: false, error: 'Missing projectPath' }));
557
+ res.end(JSON.stringify({ success: false, error: 'Missing workspacePath' }));
558
558
  return;
559
559
  }
560
560
  const result = await stopInstance(targetPath);
@@ -578,52 +578,52 @@ function handleDashboard(res, ctx) {
578
578
  }
579
579
  }
580
580
  // ============================================================================
581
- // Project-scoped route handler
581
+ // Workspace-scoped route handler
582
582
  // ============================================================================
583
- async function handleProjectRoutes(req, res, ctx, url) {
583
+ async function handleWorkspaceRoutes(req, res, ctx, url) {
584
584
  const pathParts = url.pathname.split('/');
585
- // ['', 'project', base64urlPath, ...rest]
585
+ // ['', 'workspace', base64urlPath, ...rest]
586
586
  const encodedPath = pathParts[2];
587
587
  const subPath = pathParts.slice(3).join('/');
588
588
  if (!encodedPath) {
589
589
  res.writeHead(400, { 'Content-Type': 'application/json' });
590
- res.end(JSON.stringify({ error: 'Missing project path' }));
590
+ res.end(JSON.stringify({ error: 'Missing workspace path' }));
591
591
  return;
592
592
  }
593
593
  // Decode Base64URL (RFC 4648)
594
- let projectPath;
594
+ let workspacePath;
595
595
  try {
596
- projectPath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
596
+ workspacePath = Buffer.from(encodedPath, 'base64url').toString('utf-8');
597
597
  // Support both POSIX (/) and Windows (C:\) paths
598
- if (!projectPath || (!projectPath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(projectPath))) {
599
- throw new Error('Invalid project path');
598
+ if (!workspacePath || (!workspacePath.startsWith('/') && !/^[A-Za-z]:[\\/]/.test(workspacePath))) {
599
+ throw new Error('Invalid workspace path');
600
600
  }
601
601
  // Normalize to resolve symlinks (e.g. /var/folders → /private/var/folders on macOS)
602
- projectPath = normalizeProjectPath(projectPath);
602
+ workspacePath = normalizeWorkspacePath(workspacePath);
603
603
  }
604
604
  catch {
605
605
  res.writeHead(400, { 'Content-Type': 'application/json' });
606
- res.end(JSON.stringify({ error: 'Invalid project path encoding' }));
606
+ res.end(JSON.stringify({ error: 'Invalid workspace path encoding' }));
607
607
  return;
608
608
  }
609
609
  // Phase 4 (Spec 0090): Tower handles everything directly
610
610
  const isApiCall = subPath.startsWith('api/') || subPath === 'api';
611
611
  const isWsPath = subPath.startsWith('ws/') || subPath === 'ws';
612
- // Tunnel endpoints are tower-level, not project-scoped, but the React
612
+ // Tunnel endpoints are tower-level, not workspace-scoped, but the React
613
613
  // dashboard uses relative paths (./api/tunnel/...) which resolve to
614
- // /project/<encoded>/api/tunnel/... in project context. Handle here by
614
+ // /workspace/<encoded>/api/tunnel/... in workspace context. Handle here by
615
615
  // extracting the tunnel sub-path and dispatching to handleTunnelEndpoint().
616
616
  if (subPath.startsWith('api/tunnel/')) {
617
617
  const tunnelSub = subPath.slice('api/tunnel/'.length); // e.g. "status", "connect", "disconnect"
618
618
  await handleTunnelEndpoint(req, res, tunnelSub);
619
619
  return;
620
620
  }
621
- // GET /file?path=<relative-path> — Read project file by path (for StatusPanel project list)
621
+ // GET /file?path=<relative-path> — Read workspace file by path (for StatusPanel workspace list)
622
622
  if (req.method === 'GET' && subPath === 'file' && url.searchParams.has('path')) {
623
623
  const relPath = url.searchParams.get('path');
624
- const fullPath = path.resolve(projectPath, relPath);
625
- // Security: ensure resolved path stays within project directory
626
- if (!fullPath.startsWith(projectPath + path.sep) && fullPath !== projectPath) {
624
+ const fullPath = path.resolve(workspacePath, relPath);
625
+ // Security: ensure resolved path stays within workspace directory
626
+ if (!fullPath.startsWith(workspacePath + path.sep) && fullPath !== workspacePath) {
627
627
  res.writeHead(403, { 'Content-Type': 'text/plain' });
628
628
  res.end('Forbidden');
629
629
  return;
@@ -643,7 +643,7 @@ async function handleProjectRoutes(req, res, ctx, url) {
643
643
  // 1. Not an API call
644
644
  // 2. Not a WebSocket path
645
645
  // 3. React dashboard is available
646
- // 4. Project doesn't need to be running for static files
646
+ // 4. Workspace doesn't need to be running for static files
647
647
  if (!isApiCall && !isWsPath && ctx.hasReactDashboard) {
648
648
  // Determine which static file to serve
649
649
  let staticPath;
@@ -664,61 +664,61 @@ async function handleProjectRoutes(req, res, ctx, url) {
664
664
  return;
665
665
  }
666
666
  }
667
- // Phase 4 (Spec 0090): Handle project APIs directly instead of proxying to dashboard-server
667
+ // Phase 4 (Spec 0090): Handle workspace APIs directly instead of proxying to dashboard-server
668
668
  if (isApiCall) {
669
669
  const apiPath = subPath.replace(/^api\/?/, '');
670
- // GET /api/state - Return project state (architect, builders, shells)
670
+ // GET /api/state - Return workspace state (architect, builders, shells)
671
671
  if (req.method === 'GET' && (apiPath === 'state' || apiPath === '')) {
672
- return handleProjectState(res, projectPath);
672
+ return handleWorkspaceState(res, workspacePath);
673
673
  }
674
674
  // POST /api/tabs/shell - Create a new shell terminal
675
675
  if (req.method === 'POST' && apiPath === 'tabs/shell') {
676
- return handleProjectShellCreate(res, ctx, projectPath);
676
+ return handleWorkspaceShellCreate(res, ctx, workspacePath);
677
677
  }
678
678
  // POST /api/tabs/file - Create a file tab (Spec 0092)
679
679
  if (req.method === 'POST' && apiPath === 'tabs/file') {
680
- return handleProjectFileTabCreate(req, res, ctx, projectPath);
680
+ return handleWorkspaceFileTabCreate(req, res, ctx, workspacePath);
681
681
  }
682
682
  // GET /api/file/:id - Get file content as JSON (Spec 0092)
683
683
  const fileGetMatch = apiPath.match(/^file\/([^/]+)$/);
684
684
  if (req.method === 'GET' && fileGetMatch) {
685
- return handleProjectFileGet(res, ctx, projectPath, fileGetMatch[1]);
685
+ return handleWorkspaceFileGet(res, ctx, workspacePath, fileGetMatch[1]);
686
686
  }
687
687
  // GET /api/file/:id/raw - Get raw file content (for images/video) (Spec 0092)
688
688
  const fileRawMatch = apiPath.match(/^file\/([^/]+)\/raw$/);
689
689
  if (req.method === 'GET' && fileRawMatch) {
690
- return handleProjectFileRaw(res, ctx, projectPath, fileRawMatch[1]);
690
+ return handleWorkspaceFileRaw(res, ctx, workspacePath, fileRawMatch[1]);
691
691
  }
692
692
  // POST /api/file/:id/save - Save file content (Spec 0092)
693
693
  const fileSaveMatch = apiPath.match(/^file\/([^/]+)\/save$/);
694
694
  if (req.method === 'POST' && fileSaveMatch) {
695
- return handleProjectFileSave(req, res, ctx, projectPath, fileSaveMatch[1]);
695
+ return handleWorkspaceFileSave(req, res, ctx, workspacePath, fileSaveMatch[1]);
696
696
  }
697
697
  // DELETE /api/tabs/:id - Delete a terminal or file tab
698
698
  const deleteMatch = apiPath.match(/^tabs\/(.+)$/);
699
699
  if (req.method === 'DELETE' && deleteMatch) {
700
- return handleProjectTabDelete(res, ctx, projectPath, deleteMatch[1]);
700
+ return handleWorkspaceTabDelete(res, ctx, workspacePath, deleteMatch[1]);
701
701
  }
702
- // POST /api/stop - Stop all terminals for project
702
+ // POST /api/stop - Stop all terminals for workspace
703
703
  if (req.method === 'POST' && apiPath === 'stop') {
704
- return handleProjectStopAll(res, projectPath);
704
+ return handleWorkspaceStopAll(res, workspacePath);
705
705
  }
706
- // GET /api/files - Return project directory tree for file browser (Spec 0092)
706
+ // GET /api/files - Return workspace directory tree for file browser (Spec 0092)
707
707
  if (req.method === 'GET' && apiPath === 'files') {
708
- return handleProjectFiles(res, url, projectPath);
708
+ return handleWorkspaceFiles(res, url, workspacePath);
709
709
  }
710
710
  // GET /api/git/status - Return git status for file browser (Spec 0092)
711
711
  if (req.method === 'GET' && apiPath === 'git/status') {
712
- return handleProjectGitStatus(res, ctx, projectPath);
712
+ return handleWorkspaceGitStatus(res, ctx, workspacePath);
713
713
  }
714
714
  // GET /api/files/recent - Return recently opened file tabs (Spec 0092)
715
715
  if (req.method === 'GET' && apiPath === 'files/recent') {
716
- return handleProjectRecentFiles(res, projectPath);
716
+ return handleWorkspaceRecentFiles(res, workspacePath);
717
717
  }
718
718
  // GET /api/annotate/:tabId/* — Serve rich annotator template and sub-APIs
719
719
  const annotateMatch = apiPath.match(/^annotate\/([^/]+)(\/(.*))?$/);
720
720
  if (annotateMatch) {
721
- return handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch);
721
+ return handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch);
722
722
  }
723
723
  // POST /api/paste-image - Upload pasted image to temp file (Issue #252)
724
724
  if (req.method === 'POST' && apiPath === 'paste-image') {
@@ -794,23 +794,23 @@ async function handleProjectRoutes(req, res, ctx, url) {
794
794
  res.end('Not found');
795
795
  }
796
796
  // ============================================================================
797
- // Project API sub-handlers
797
+ // Workspace API sub-handlers
798
798
  // ============================================================================
799
- async function handleProjectState(res, projectPath) {
800
- // Refresh cache via getTerminalsForProject (handles SQLite sync
799
+ async function handleWorkspaceState(res, workspacePath) {
800
+ // Refresh cache via getTerminalsForWorkspace (handles SQLite sync
801
801
  // and shellper reconnection in one place)
802
- const encodedPath = Buffer.from(projectPath).toString('base64url');
803
- const proxyUrl = `/project/${encodedPath}/`;
804
- const { gateStatus } = await getTerminalsForProject(projectPath, proxyUrl);
802
+ const encodedPath = Buffer.from(workspacePath).toString('base64url');
803
+ const proxyUrl = `/workspace/${encodedPath}/`;
804
+ const { gateStatus } = await getTerminalsForWorkspace(workspacePath, proxyUrl);
805
805
  // Now read from the refreshed cache
806
- const entry = getProjectTerminalsEntry(projectPath);
806
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
807
807
  const manager = getTerminalManager();
808
808
  const state = {
809
809
  architect: null,
810
810
  builders: [],
811
811
  utils: [],
812
812
  annotations: [],
813
- projectName: path.basename(projectPath),
813
+ workspaceName: path.basename(workspacePath),
814
814
  gateStatus,
815
815
  };
816
816
  // Add architect if exists
@@ -870,10 +870,10 @@ async function handleProjectState(res, projectPath) {
870
870
  res.writeHead(200, { 'Content-Type': 'application/json' });
871
871
  res.end(JSON.stringify(state));
872
872
  }
873
- async function handleProjectShellCreate(res, ctx, projectPath) {
873
+ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
874
874
  try {
875
875
  const manager = getTerminalManager();
876
- const shellId = getNextShellId(projectPath);
876
+ const shellId = getNextShellId(workspacePath);
877
877
  const shellCmd = process.env.SHELL || '/bin/bash';
878
878
  const shellArgs = [];
879
879
  let shellCreated = false;
@@ -889,7 +889,7 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
889
889
  sessionId,
890
890
  command: shellCmd,
891
891
  args: shellArgs,
892
- cwd: projectPath,
892
+ cwd: workspacePath,
893
893
  env: shellEnv,
894
894
  cols: 200,
895
895
  rows: 50,
@@ -899,15 +899,15 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
899
899
  const shellperInfo = shellperManager.getSessionInfo(sessionId);
900
900
  const session = manager.createSessionRaw({
901
901
  label: `Shell ${shellId.replace('shell-', '')}`,
902
- cwd: projectPath,
902
+ cwd: workspacePath,
903
903
  });
904
904
  const ptySession = manager.getSession(session.id);
905
905
  if (ptySession) {
906
906
  ptySession.attachShellper(client, replayData, shellperInfo.pid, sessionId);
907
907
  }
908
- const entry = getProjectTerminalsEntry(projectPath);
908
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
909
909
  entry.shells.set(shellId, session.id);
910
- saveTerminalSession(session.id, projectPath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
910
+ saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime);
911
911
  shellCreated = true;
912
912
  res.writeHead(200, { 'Content-Type': 'application/json' });
913
913
  res.end(JSON.stringify({
@@ -928,14 +928,14 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
928
928
  const session = await manager.createSession({
929
929
  command: shellCmd,
930
930
  args: shellArgs,
931
- cwd: projectPath,
931
+ cwd: workspacePath,
932
932
  label: `Shell ${shellId.replace('shell-', '')}`,
933
933
  env: process.env,
934
934
  });
935
- const entry = getProjectTerminalsEntry(projectPath);
935
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
936
936
  entry.shells.set(shellId, session.id);
937
- saveTerminalSession(session.id, projectPath, 'shell', shellId, session.pid);
938
- ctx.log('WARN', `Shell ${shellId} for ${projectPath} is non-persistent (shellper unavailable)`);
937
+ saveTerminalSession(session.id, workspacePath, 'shell', shellId, session.pid);
938
+ ctx.log('WARN', `Shell ${shellId} for ${workspacePath} is non-persistent (shellper unavailable)`);
939
939
  res.writeHead(200, { 'Content-Type': 'application/json' });
940
940
  res.end(JSON.stringify({
941
941
  id: shellId,
@@ -952,7 +952,7 @@ async function handleProjectShellCreate(res, ctx, projectPath) {
952
952
  res.end(JSON.stringify({ error: err.message }));
953
953
  }
954
954
  }
955
- async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
955
+ async function handleWorkspaceFileTabCreate(req, res, ctx, workspacePath) {
956
956
  try {
957
957
  const body = await readBody(req);
958
958
  const { path: filePath, line, terminalId } = JSON.parse(body || '{}');
@@ -973,12 +973,12 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
973
973
  fullPath = path.join(session.cwd, filePath);
974
974
  }
975
975
  else {
976
- ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to project root`);
977
- fullPath = path.join(projectPath, filePath);
976
+ ctx.log('WARN', `Terminal session ${terminalId} not found, falling back to workspace root`);
977
+ fullPath = path.join(workspacePath, filePath);
978
978
  }
979
979
  }
980
980
  else {
981
- fullPath = path.join(projectPath, filePath);
981
+ fullPath = path.join(workspacePath, filePath);
982
982
  }
983
983
  // Security: symlink-aware containment check
984
984
  // For non-existent files, resolve the parent directory to handle
@@ -995,23 +995,23 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
995
995
  resolvedPath = path.resolve(fullPath);
996
996
  }
997
997
  }
998
- let normalizedProject;
998
+ let normalizedWorkspace;
999
999
  try {
1000
- normalizedProject = fs.realpathSync(projectPath);
1000
+ normalizedWorkspace = fs.realpathSync(workspacePath);
1001
1001
  }
1002
1002
  catch {
1003
- normalizedProject = path.resolve(projectPath);
1003
+ normalizedWorkspace = path.resolve(workspacePath);
1004
1004
  }
1005
- const isWithinProject = resolvedPath.startsWith(normalizedProject + path.sep)
1006
- || resolvedPath === normalizedProject;
1007
- if (!isWithinProject) {
1005
+ const isWithinWorkspace = resolvedPath.startsWith(normalizedWorkspace + path.sep)
1006
+ || resolvedPath === normalizedWorkspace;
1007
+ if (!isWithinWorkspace) {
1008
1008
  res.writeHead(403, { 'Content-Type': 'application/json' });
1009
- res.end(JSON.stringify({ error: 'Path outside project' }));
1009
+ res.end(JSON.stringify({ error: 'Path outside workspace' }));
1010
1010
  return;
1011
1011
  }
1012
1012
  // Non-existent files still create a tab (spec 0101: file viewer shows "File not found")
1013
1013
  const fileExists = fs.existsSync(fullPath);
1014
- const entry = getProjectTerminalsEntry(projectPath);
1014
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1015
1015
  // Check if already open
1016
1016
  for (const [id, tab] of entry.fileTabs) {
1017
1017
  if (tab.path === fullPath) {
@@ -1024,7 +1024,7 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
1024
1024
  const id = `file-${crypto.randomUUID()}`;
1025
1025
  const createdAt = Date.now();
1026
1026
  entry.fileTabs.set(id, { id, path: fullPath, createdAt });
1027
- saveFileTab(id, projectPath, fullPath, createdAt);
1027
+ saveFileTab(id, workspacePath, fullPath, createdAt);
1028
1028
  ctx.log('INFO', `Created file tab: ${id} for ${path.basename(fullPath)}`);
1029
1029
  res.writeHead(200, { 'Content-Type': 'application/json' });
1030
1030
  res.end(JSON.stringify({ id, existing: false, line, notFound: !fileExists }));
@@ -1035,8 +1035,8 @@ async function handleProjectFileTabCreate(req, res, ctx, projectPath) {
1035
1035
  res.end(JSON.stringify({ error: err.message }));
1036
1036
  }
1037
1037
  }
1038
- function handleProjectFileGet(res, ctx, projectPath, tabId) {
1039
- const entry = getProjectTerminalsEntry(projectPath);
1038
+ function handleWorkspaceFileGet(res, ctx, workspacePath, tabId) {
1039
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1040
1040
  const tab = entry.fileTabs.get(tabId);
1041
1041
  if (!tab) {
1042
1042
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1083,8 +1083,8 @@ function handleProjectFileGet(res, ctx, projectPath, tabId) {
1083
1083
  res.end(JSON.stringify({ error: err.message }));
1084
1084
  }
1085
1085
  }
1086
- function handleProjectFileRaw(res, ctx, projectPath, tabId) {
1087
- const entry = getProjectTerminalsEntry(projectPath);
1086
+ function handleWorkspaceFileRaw(res, ctx, workspacePath, tabId) {
1087
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1088
1088
  const tab = entry.fileTabs.get(tabId);
1089
1089
  if (!tab) {
1090
1090
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1107,8 +1107,8 @@ function handleProjectFileRaw(res, ctx, projectPath, tabId) {
1107
1107
  res.end(JSON.stringify({ error: err.message }));
1108
1108
  }
1109
1109
  }
1110
- async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
1111
- const entry = getProjectTerminalsEntry(projectPath);
1110
+ async function handleWorkspaceFileSave(req, res, ctx, workspacePath, tabId) {
1111
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1112
1112
  const tab = entry.fileTabs.get(tabId);
1113
1113
  if (!tab) {
1114
1114
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1134,8 +1134,8 @@ async function handleProjectFileSave(req, res, ctx, projectPath, tabId) {
1134
1134
  res.end(JSON.stringify({ error: err.message }));
1135
1135
  }
1136
1136
  }
1137
- async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
1138
- const entry = getProjectTerminalsEntry(projectPath);
1137
+ async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
1138
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1139
1139
  const manager = getTerminalManager();
1140
1140
  // Check if it's a file tab first (Spec 0092, write-through: in-memory + SQLite)
1141
1141
  if (tabId.startsWith('file-')) {
@@ -1185,8 +1185,8 @@ async function handleProjectTabDelete(res, ctx, projectPath, tabId) {
1185
1185
  res.end(JSON.stringify({ error: 'Tab not found' }));
1186
1186
  }
1187
1187
  }
1188
- async function handleProjectStopAll(res, projectPath) {
1189
- const entry = getProjectTerminalsEntry(projectPath);
1188
+ async function handleWorkspaceStopAll(res, workspacePath) {
1189
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1190
1190
  const manager = getTerminalManager();
1191
1191
  // Kill all terminals (disable shellper auto-restart if applicable)
1192
1192
  if (entry.architect) {
@@ -1199,13 +1199,13 @@ async function handleProjectStopAll(res, projectPath) {
1199
1199
  await killTerminalWithShellper(manager, terminalId);
1200
1200
  }
1201
1201
  // Clear registry
1202
- getProjectTerminals().delete(projectPath);
1202
+ getWorkspaceTerminals().delete(workspacePath);
1203
1203
  // TICK-001: Delete all terminal sessions from SQLite
1204
- deleteProjectTerminalSessions(projectPath);
1204
+ deleteWorkspaceTerminalSessions(workspacePath);
1205
1205
  res.writeHead(200, { 'Content-Type': 'application/json' });
1206
1206
  res.end(JSON.stringify({ ok: true }));
1207
1207
  }
1208
- function handleProjectFiles(res, url, projectPath) {
1208
+ function handleWorkspaceFiles(res, url, workspacePath) {
1209
1209
  const maxDepth = parseInt(url.searchParams.get('depth') || '3', 10);
1210
1210
  const ignore = new Set(['.git', 'node_modules', '.builders', 'dist', '.agent-farm', '.next', '.cache', '__pycache__']);
1211
1211
  function readTree(dir, depth) {
@@ -1226,7 +1226,7 @@ function handleProjectFiles(res, url, projectPath) {
1226
1226
  })
1227
1227
  .map(e => {
1228
1228
  const fullPath = path.join(dir, e.name);
1229
- const relativePath = path.relative(projectPath, fullPath);
1229
+ const relativePath = path.relative(workspacePath, fullPath);
1230
1230
  if (e.isDirectory()) {
1231
1231
  return { name: e.name, path: relativePath, type: 'directory', children: readTree(fullPath, depth - 1) };
1232
1232
  }
@@ -1237,15 +1237,15 @@ function handleProjectFiles(res, url, projectPath) {
1237
1237
  return [];
1238
1238
  }
1239
1239
  }
1240
- const tree = readTree(projectPath, maxDepth);
1240
+ const tree = readTree(workspacePath, maxDepth);
1241
1241
  res.writeHead(200, { 'Content-Type': 'application/json' });
1242
1242
  res.end(JSON.stringify(tree));
1243
1243
  }
1244
- function handleProjectGitStatus(res, ctx, projectPath) {
1244
+ function handleWorkspaceGitStatus(res, ctx, workspacePath) {
1245
1245
  try {
1246
1246
  // Get git status in porcelain format for parsing
1247
1247
  const result = execSync('git status --porcelain', {
1248
- cwd: projectPath,
1248
+ cwd: workspacePath,
1249
1249
  encoding: 'utf-8',
1250
1250
  timeout: 5000,
1251
1251
  });
@@ -1282,8 +1282,8 @@ function handleProjectGitStatus(res, ctx, projectPath) {
1282
1282
  res.end(JSON.stringify({ modified: [], staged: [], untracked: [], error: err.message }));
1283
1283
  }
1284
1284
  }
1285
- function handleProjectRecentFiles(res, projectPath) {
1286
- const entry = getProjectTerminalsEntry(projectPath);
1285
+ function handleWorkspaceRecentFiles(res, workspacePath) {
1286
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1287
1287
  // Get all file tabs sorted by creation time (most recent first)
1288
1288
  const recentFiles = Array.from(entry.fileTabs.values())
1289
1289
  .sort((a, b) => b.createdAt - a.createdAt)
@@ -1292,15 +1292,15 @@ function handleProjectRecentFiles(res, projectPath) {
1292
1292
  id: tab.id,
1293
1293
  path: tab.path,
1294
1294
  name: path.basename(tab.path),
1295
- relativePath: path.relative(projectPath, tab.path),
1295
+ relativePath: path.relative(workspacePath, tab.path),
1296
1296
  }));
1297
1297
  res.writeHead(200, { 'Content-Type': 'application/json' });
1298
1298
  res.end(JSON.stringify(recentFiles));
1299
1299
  }
1300
- function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
1300
+ function handleWorkspaceAnnotate(req, res, ctx, url, workspacePath, annotateMatch) {
1301
1301
  const tabId = annotateMatch[1];
1302
1302
  const subRoute = annotateMatch[3] || '';
1303
- const entry = getProjectTerminalsEntry(projectPath);
1303
+ const entry = getWorkspaceTerminalsEntry(workspacePath);
1304
1304
  const tab = entry.fileTabs.get(tabId);
1305
1305
  if (!tab) {
1306
1306
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -1387,6 +1387,28 @@ function handleProjectAnnotate(req, res, ctx, url, projectPath, annotateMatch) {
1387
1387
  }
1388
1388
  return;
1389
1389
  }
1390
+ // Sub-route: GET /vendor/* — serve bundled vendor libraries (PrismJS, marked, DOMPurify)
1391
+ if (req.method === 'GET' && subRoute.startsWith('vendor/')) {
1392
+ const vendorFile = subRoute.slice('vendor/'.length);
1393
+ // Security: only allow known file extensions and no path traversal
1394
+ if (vendorFile.includes('..') || vendorFile.includes('/') || !/\.(js|css)$/.test(vendorFile)) {
1395
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
1396
+ res.end('Bad request');
1397
+ return;
1398
+ }
1399
+ const vendorPath = path.resolve(__dirname, `../../../templates/vendor/${vendorFile}`);
1400
+ try {
1401
+ const content = fs.readFileSync(vendorPath);
1402
+ const contentType = vendorFile.endsWith('.css') ? 'text/css' : 'application/javascript';
1403
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' });
1404
+ res.end(content);
1405
+ }
1406
+ catch {
1407
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
1408
+ res.end('Not found');
1409
+ }
1410
+ return;
1411
+ }
1390
1412
  // Default: serve the annotator HTML template
1391
1413
  if (req.method === 'GET' && (subRoute === '' || subRoute === undefined)) {
1392
1414
  try {