neural-loom 0.2.70 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/cache/.previewinfo +1 -1
  4. package/.next/cache/.rscinfo +1 -1
  5. package/.next/cache/.tsbuildinfo +1 -1
  6. package/.next/diagnostics/route-bundle-stats.json +11 -11
  7. package/.next/fallback-build-manifest.json +3 -3
  8. package/.next/next-server.js.nft.json +1 -1
  9. package/.next/prerender-manifest.json +3 -3
  10. package/.next/required-server-files.js +20 -20
  11. package/.next/required-server-files.json +20 -20
  12. package/.next/server/app/_global-error.html +1 -1
  13. package/.next/server/app/_global-error.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.html +1 -1
  20. package/.next/server/app/_not-found.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  25. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  26. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/.next/server/app/api/configs/route.js.nft.json +1 -1
  28. package/.next/server/app/api/context/aider/route.js.nft.json +1 -1
  29. package/.next/server/app/api/context/claude/route.js.nft.json +1 -1
  30. package/.next/server/app/api/context/route.js.nft.json +1 -1
  31. package/.next/server/app/api/context/ssh/route.js.nft.json +1 -1
  32. package/.next/server/app/api/files/route.js +1 -1
  33. package/.next/server/app/api/files/route.js.nft.json +1 -1
  34. package/.next/server/app/api/recents/route.js.nft.json +1 -1
  35. package/.next/server/app/api/sessions/inject/route.js.nft.json +1 -1
  36. package/.next/server/app/api/sessions/input/route.js.nft.json +1 -1
  37. package/.next/server/app/api/sessions/launch/route.js.nft.json +1 -1
  38. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  39. package/.next/server/app/api/sessions/scope/route.js.nft.json +1 -1
  40. package/.next/server/app/api/sessions/start/route.js.nft.json +1 -1
  41. package/.next/server/app/api/sessions/stats/route.js.nft.json +1 -1
  42. package/.next/server/app/api/sessions/stop/route.js.nft.json +1 -1
  43. package/.next/server/app/api/sessions/upload-image/route.js.nft.json +1 -1
  44. package/.next/server/app/api/sql/connections/route.js.nft.json +1 -1
  45. package/.next/server/app/api/sql/mcp/route.js +1 -1
  46. package/.next/server/app/api/sql/mcp/route.js.nft.json +1 -1
  47. package/.next/server/app/api/sql/query/route.js +1 -1
  48. package/.next/server/app/api/sql/query/route.js.nft.json +1 -1
  49. package/.next/server/app/api/sql/schema/route.js.nft.json +1 -1
  50. package/.next/server/app/api/telemetry/route.js +1 -1
  51. package/.next/server/app/api/telemetry/route.js.nft.json +1 -1
  52. package/.next/server/app/index.html +1 -1
  53. package/.next/server/app/index.rsc +2 -2
  54. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  55. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  56. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  57. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  58. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  59. package/.next/server/app/page/react-loadable-manifest.json +2 -2
  60. package/.next/server/app/page_client-reference-manifest.js +1 -1
  61. package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js +3 -0
  62. package/.next/server/chunks/[root-of-the-server]__0.hmw1f._.js.map +1 -0
  63. package/.next/server/chunks/[root-of-the-server]__00-sqou._.js +2 -2
  64. package/.next/server/chunks/[root-of-the-server]__00-sqou._.js.map +1 -1
  65. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js +4 -4
  66. package/.next/server/chunks/[root-of-the-server]__00l5btc._.js.map +1 -1
  67. package/.next/server/chunks/[root-of-the-server]__06men8g._.js +4 -4
  68. package/.next/server/chunks/[root-of-the-server]__06men8g._.js.map +1 -1
  69. package/.next/server/chunks/[root-of-the-server]__097vtn1._.js +2 -2
  70. package/.next/server/chunks/[root-of-the-server]__097vtn1._.js.map +1 -1
  71. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js +2 -2
  72. package/.next/server/chunks/[root-of-the-server]__0_v2.a8._.js.map +1 -1
  73. package/.next/server/chunks/{[root-of-the-server]__0.ptwor._.js → [root-of-the-server]__0ajln56._.js} +7 -7
  74. package/.next/server/chunks/[root-of-the-server]__0ajln56._.js.map +1 -0
  75. package/.next/server/chunks/{[root-of-the-server]__0co.g5k._.js → [root-of-the-server]__0auh-2z._.js} +5 -5
  76. package/.next/server/chunks/[root-of-the-server]__0auh-2z._.js.map +1 -0
  77. package/.next/server/chunks/[root-of-the-server]__0e03ite._.js +2 -2
  78. package/.next/server/chunks/[root-of-the-server]__0e03ite._.js.map +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js +2 -2
  80. package/.next/server/chunks/[root-of-the-server]__0fxtx7z._.js.map +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js +2 -2
  82. package/.next/server/chunks/[root-of-the-server]__0h.~hv6._.js.map +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__0i83~95._.js +1 -1
  84. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +19 -19
  85. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__0s1axek._.js +5 -5
  87. package/.next/server/chunks/[root-of-the-server]__0s1axek._.js.map +1 -1
  88. package/.next/server/chunks/[root-of-the-server]__0t713sv._.js +1 -1
  89. package/.next/server/chunks/[root-of-the-server]__0t713sv._.js.map +1 -1
  90. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +5 -5
  91. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__0viil6b._.js +2 -2
  93. package/.next/server/chunks/[root-of-the-server]__0viil6b._.js.map +1 -1
  94. package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js +3 -0
  95. package/.next/server/chunks/[root-of-the-server]__0ykd4~u._.js.map +1 -0
  96. package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js +1 -1
  97. package/.next/server/chunks/[root-of-the-server]__11h_nxt._.js.map +1 -1
  98. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js +2 -2
  99. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_016qk~0.js.map +1 -1
  100. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js +1 -1
  101. package/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ev3h.z.js.map +1 -1
  102. package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
  103. package/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js.map +1 -0
  104. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
  105. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js.map +1 -0
  106. package/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js.map +1 -0
  107. package/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
  108. package/.next/server/middleware/middleware-manifest.json +32 -0
  109. package/.next/server/middleware-build-manifest.js +3 -3
  110. package/.next/server/middleware-manifest.json +30 -2
  111. package/.next/server/pages/404.html +1 -1
  112. package/.next/server/pages/500.html +1 -1
  113. package/.next/server/server-reference-manifest.js +1 -1
  114. package/.next/server/server-reference-manifest.json +1 -1
  115. package/.next/static/04j4ht7eCOOXSLQ5Wixq0/_clientMiddlewareManifest.js +6 -0
  116. package/.next/static/chunks/{0fxouf2n_5adf.js → 0uyuco-451j.n.js} +2 -2
  117. package/.next/static/chunks/{0r3x2gcn0sde..js → 14~-ojwsxf~oe.js} +4 -4
  118. package/.next/trace +1 -1
  119. package/.next/trace-build +1 -1
  120. package/README.md +18 -0
  121. package/bin/cli.js +12 -2
  122. package/package.json +9 -6
  123. package/src/app/api/files/route.ts +30 -0
  124. package/src/app/api/git/route.ts +46 -31
  125. package/src/app/api/sessions/stats/route.ts +40 -22
  126. package/src/app/api/sql/query/route.ts +2 -15
  127. package/src/app/api/telemetry/route.ts +2 -1
  128. package/src/app/components/TerminalConsole.tsx +103 -39
  129. package/src/lib/agents/AgentRunner.ts +41 -5
  130. package/src/lib/agents/AiderRunner.ts +20 -36
  131. package/src/lib/agents/ClaudeRunner.ts +8 -62
  132. package/src/lib/agents/DockerRunner.ts +18 -54
  133. package/src/lib/agents/MockRunner.ts +3 -1
  134. package/src/lib/agents/SSHRunner.ts +8 -54
  135. package/src/lib/agents/SessionPersister.ts +11 -1
  136. package/src/lib/agents/ShellRunner.ts +10 -46
  137. package/src/lib/agents/WebSocketServer.ts +17 -2
  138. package/src/lib/agents/pricing.ts +45 -0
  139. package/src/lib/agents/runtime.ts +92 -0
  140. package/src/lib/gitSafe.ts +15 -0
  141. package/src/lib/security.ts +76 -0
  142. package/src/lib/sql/McpServer.ts +2 -11
  143. package/src/lib/sql/adapters/MssqlAdapter.ts +16 -1
  144. package/src/lib/sql/adapters/MysqlAdapter.ts +23 -1
  145. package/src/lib/sql/adapters/PostgresAdapter.ts +22 -1
  146. package/src/lib/sql/adapters/SqliteAdapter.ts +21 -10
  147. package/src/lib/sql/sqlGuard.ts +20 -0
  148. package/src/lib/workspaceParser.ts +23 -6
  149. package/src/middleware.ts +27 -0
  150. package/.next/dev/build-manifest.json +0 -50
  151. package/.next/dev/fallback-build-manifest.json +0 -37
  152. package/.next/dev/logs/next-development.log +0 -3737
  153. package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js +0 -2396
  154. package/.next/dev/server/chunks/[root-of-the-server]__11uf~fd._.js.map +0 -20
  155. package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js +0 -32
  156. package/.next/dev/server/chunks/ssr/[externals]__0.o~_~l._.js.map +0 -5
  157. package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js +0 -8
  158. package/.next/dev/server/chunks/ssr/[externals]_next_dist_shared_lib_no-fallback-error_external_0fgm~bn.js.map +0 -5
  159. package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js +0 -285
  160. package/.next/dev/server/chunks/ssr/[root-of-the-server]__0je5zm2._.js.map +0 -9
  161. package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js +0 -6621
  162. package/.next/dev/server/chunks/ssr/node_modules_01ed9g5._.js.map +0 -76
  163. package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js +0 -4399
  164. package/.next/dev/server/chunks/ssr/node_modules_129w7gn._.js.map +0 -35
  165. package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js +0 -55
  166. package/.next/dev/server/chunks/ssr/node_modules_next_12i4inu._.js.map +0 -7
  167. package/.next/dev/server/middleware-build-manifest.js +0 -50
  168. package/.next/dev/server/next-font-manifest.js +0 -1
  169. package/.next/dev/server/next-font-manifest.json +0 -11
  170. package/.next/dev/server/pages/_app/build-manifest.json +0 -23
  171. package/.next/dev/server/pages/_app/client-build-manifest.json +0 -5
  172. package/.next/dev/server/pages/_app/next-font-manifest.json +0 -6
  173. package/.next/dev/server/pages/_app/pages-manifest.json +0 -3
  174. package/.next/dev/server/pages/_app/react-loadable-manifest.json +0 -1
  175. package/.next/dev/server/pages/_app.js +0 -4
  176. package/.next/dev/server/pages/_app.js.map +0 -5
  177. package/.next/dev/server/pages/_document/next-font-manifest.json +0 -6
  178. package/.next/dev/server/pages/_document/pages-manifest.json +0 -3
  179. package/.next/dev/server/pages/_document/react-loadable-manifest.json +0 -1
  180. package/.next/dev/server/pages/_document.js +0 -5
  181. package/.next/dev/server/pages/_document.js.map +0 -5
  182. package/.next/dev/server/pages/_error/build-manifest.json +0 -22
  183. package/.next/dev/server/pages/_error/client-build-manifest.json +0 -5
  184. package/.next/dev/server/pages/_error/next-font-manifest.json +0 -6
  185. package/.next/dev/server/pages/_error/pages-manifest.json +0 -3
  186. package/.next/dev/server/pages/_error/react-loadable-manifest.json +0 -1
  187. package/.next/dev/server/pages/_error.js +0 -8
  188. package/.next/dev/server/pages/_error.js.map +0 -5
  189. package/.next/dev/server/pages-manifest.json +0 -5
  190. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js +0 -23
  191. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0j~flwh._.js.map +0 -6
  192. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js +0 -23
  193. package/.next/dev/static/chunks/[next]_entry_page-loader_ts_0rqw6yo._.js.map +0 -6
  194. package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js +0 -476
  195. package/.next/dev/static/chunks/[root-of-the-server]__01mw43t._.js.map +0 -6
  196. package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js +0 -476
  197. package/.next/dev/static/chunks/[root-of-the-server]__0c0okpg._.js.map +0 -6
  198. package/.next/dev/static/chunks/node_modules_0lx093h._.js +0 -1343
  199. package/.next/dev/static/chunks/node_modules_0lx093h._.js.map +0 -13
  200. package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js +0 -8
  201. package/.next/dev/static/chunks/node_modules_next_app_0jt-zj..js.map +0 -6
  202. package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js +0 -2334
  203. package/.next/dev/static/chunks/node_modules_next_dist_0rt-2cr._.js.map +0 -26
  204. package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js +0 -2165
  205. package/.next/dev/static/chunks/node_modules_next_dist_0u_w_5s._.js.map +0 -25
  206. package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js +0 -4091
  207. package/.next/dev/static/chunks/node_modules_next_dist_client_0pe1dg-._.js.map +0 -49
  208. package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js +0 -6064
  209. package/.next/dev/static/chunks/node_modules_next_dist_compiled_0o6l_m6._.js.map +0 -17
  210. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js +0 -6535
  211. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0ek5eq7._.js.map +0 -72
  212. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js +0 -6294
  213. package/.next/dev/static/chunks/node_modules_next_dist_shared_lib_0n1wg2c._.js.map +0 -70
  214. package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js +0 -15381
  215. package/.next/dev/static/chunks/node_modules_react-dom_0bruynb._.js.map +0 -9
  216. package/.next/dev/static/chunks/pages/_app.js +0 -13
  217. package/.next/dev/static/chunks/pages/_error.js +0 -12
  218. package/.next/dev/static/chunks/pages__app_0-bg2z-._.js.map +0 -11
  219. package/.next/dev/static/chunks/pages__app_07xvfw~._.js +0 -5
  220. package/.next/dev/static/chunks/pages__error_0.i_zsq._.js.map +0 -11
  221. package/.next/dev/static/chunks/pages__error_07xvfw~._.js +0 -5
  222. package/.next/dev/static/chunks/turbopack-pages__app_0-bg2z-._.js +0 -2236
  223. package/.next/dev/static/chunks/turbopack-pages__error_0.i_zsq._.js +0 -2236
  224. package/.next/server/chunks/[externals]__048y_op._.js +0 -3
  225. package/.next/server/chunks/[externals]__048y_op._.js.map +0 -1
  226. package/.next/server/chunks/[root-of-the-server]__0.ptwor._.js.map +0 -1
  227. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js +0 -3
  228. package/.next/server/chunks/[root-of-the-server]__08r0s-s._.js.map +0 -1
  229. package/.next/server/chunks/[root-of-the-server]__0co.g5k._.js.map +0 -1
  230. package/.next/static/llmJsGQafolZV5_sqEw4l/_clientMiddlewareManifest.js +0 -1
  231. /package/.next/static/{llmJsGQafolZV5_sqEw4l → 04j4ht7eCOOXSLQ5Wixq0}/_buildManifest.js +0 -0
  232. /package/.next/static/{llmJsGQafolZV5_sqEw4l → 04j4ht7eCOOXSLQ5Wixq0}/_ssgManifest.js +0 -0
package/bin/cli.js CHANGED
@@ -140,10 +140,20 @@ process.chdir(packageRoot);
140
140
  // Next.js provides a cli entry point in next/dist/bin/next
141
141
  const nextBin = require.resolve('next/dist/bin/next');
142
142
 
143
+ // Bind to loopback by default so the dashboard (which exposes shell, filesystem
144
+ // and DB access) is not reachable from the local network. Opt in with
145
+ // NEURAL_LOOM_ALLOW_LAN=1, or override the interface with NEURAL_LOOM_HOST.
146
+ const allowLan = ['1', 'true', 'yes'].includes((process.env.NEURAL_LOOM_ALLOW_LAN || '').toLowerCase());
147
+ const bindHost = process.env.NEURAL_LOOM_HOST || (allowLan ? '0.0.0.0' : '127.0.0.1');
148
+ const port = process.env.PORT || '9600';
149
+
143
150
  console.log("🚀 Starting NeuralLoom AI Orchestrator dashboard...");
144
- console.log(`🔗 Open http://localhost:${process.env.PORT || 9600} in your browser to begin.`);
151
+ console.log(`🔗 Open http://localhost:${port} in your browser to begin.`);
152
+ if (allowLan || process.env.NEURAL_LOOM_HOST) {
153
+ console.log(`🌐 Binding to ${bindHost} (network access enabled).`);
154
+ }
145
155
 
146
- const nextProcess = spawn(process.execPath, [nextBin, 'start', '-p', process.env.PORT || '9600'], {
156
+ const nextProcess = spawn(process.execPath, [nextBin, 'start', '-p', port, '-H', bindHost], {
147
157
  stdio: 'inherit',
148
158
  env: {
149
159
  ...process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neural-loom",
3
- "version": "0.2.70",
3
+ "version": "0.3.0",
4
4
  "bin": {
5
5
  "neural-loom": "./bin/cli.js"
6
6
  },
@@ -14,10 +14,12 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "dev": "next dev",
17
+ "dev": "next dev -H 127.0.0.1",
18
18
  "build": "next build",
19
- "start": "next start",
20
- "lint": "eslint"
19
+ "start": "next start -H 127.0.0.1",
20
+ "lint": "eslint .",
21
+ "test": "node --test --experimental-strip-types \"test/**/*.test.ts\"",
22
+ "prepublishOnly": "next build"
21
23
  },
22
24
  "dependencies": {
23
25
  "@monaco-editor/react": "^4.7.0",
@@ -25,6 +27,7 @@
25
27
  "mssql": "^12.5.5",
26
28
  "next": "16.2.6",
27
29
  "node-pty": "^1.1.0",
30
+ "pidusage": "^3.0.2",
28
31
  "react": "19.2.4",
29
32
  "react-dom": "19.2.4",
30
33
  "ws": "^8.21.0",
@@ -33,11 +36,11 @@
33
36
  },
34
37
  "devDependencies": {
35
38
  "@types/node": "^20",
36
- "@types/react": "^19",
39
+ "@types/react": "19.2.17",
37
40
  "@types/react-dom": "^19",
38
41
  "@types/ws": "^8.18.1",
39
42
  "eslint": "^9",
40
43
  "eslint-config-next": "16.2.6",
41
- "typescript": "^5"
44
+ "typescript": "5.9.3"
42
45
  }
43
46
  }
@@ -2,6 +2,24 @@ import { NextResponse } from "next/server";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import os from "os";
5
+ import { getConfigDir } from "@/lib/config";
6
+
7
+ // NeuralLoom's runtime/secret store (.ai/neural-loom, .claude/, the resolved
8
+ // config dir) holds credentials, SSH keys and process state. The generic file
9
+ // API must never read or mutate it, regardless of caller.
10
+ function isProtectedPath(targetPath: string): boolean {
11
+ const norm = path.resolve(targetPath).replace(/\\/g, "/").toLowerCase();
12
+ const segments = norm.split("/");
13
+ if (segments.includes(".ai") || segments.includes(".claude")) return true;
14
+ const cfg = getConfigDir().replace(/\\/g, "/").toLowerCase();
15
+ return norm === cfg || norm.startsWith(cfg + "/");
16
+ }
17
+
18
+ const protectedResponse = () =>
19
+ NextResponse.json(
20
+ { error: "Access to NeuralLoom's runtime/config directory is not permitted." },
21
+ { status: 403 }
22
+ );
5
23
 
6
24
  // Helper to recursively generate a tree nodes list for UI representation
7
25
  interface TreeNode {
@@ -229,6 +247,9 @@ export async function GET(request: Request) {
229
247
  }
230
248
 
231
249
  const filePath = filePathRaw.replace(/\\/g, "/");
250
+ if (isProtectedPath(filePath)) {
251
+ return protectedResponse();
252
+ }
232
253
  if (!fs.existsSync(filePath)) {
233
254
  return NextResponse.json({ error: "File does not exist" }, { status: 404 });
234
255
  }
@@ -252,6 +273,15 @@ export async function POST(request: Request) {
252
273
  const oldPath = rawOldPath ? rawOldPath.replace(/\\/g, "/") : undefined;
253
274
  const newPath = rawNewPath ? rawNewPath.replace(/\\/g, "/") : undefined;
254
275
 
276
+ // Refuse any mutation that touches the runtime/secret store.
277
+ if (
278
+ (filePath && isProtectedPath(filePath)) ||
279
+ (oldPath && isProtectedPath(oldPath)) ||
280
+ (newPath && isProtectedPath(newPath))
281
+ ) {
282
+ return protectedResponse();
283
+ }
284
+
255
285
  if (action === "write") {
256
286
  if (!filePath || content === undefined) {
257
287
  return NextResponse.json({ error: "Missing 'path' or 'content' in body" }, { status: 400 });
@@ -1,10 +1,22 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { promisify } from "util";
3
- import { exec } from "child_process";
3
+ import { execFile } from "child_process";
4
4
  import fs from "fs";
5
5
  import path from "path";
6
+ import { isValidBranchName } from "@/lib/gitSafe";
6
7
 
7
- const execPromise = promisify(exec);
8
+ const execFilePromise = promisify(execFile);
9
+
10
+ // Run git without a shell. Arguments are passed as an array, so values like file
11
+ // names, branch names and commit messages cannot break out into shell commands.
12
+ async function git(args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
13
+ const { stdout, stderr } = await execFilePromise("git", args, {
14
+ cwd,
15
+ maxBuffer: 1024 * 1024 * 20,
16
+ windowsHide: true,
17
+ });
18
+ return { stdout: stdout.toString(), stderr: stderr.toString() };
19
+ }
8
20
 
9
21
  export async function GET(request: Request) {
10
22
  try {
@@ -17,13 +29,13 @@ export async function GET(request: Request) {
17
29
 
18
30
  // Check if it's a git repo
19
31
  try {
20
- await execPromise("git rev-parse --is-inside-work-tree", { cwd: targetRoot });
32
+ await git(["rev-parse", "--is-inside-work-tree"], targetRoot);
21
33
  } catch {
22
34
  return NextResponse.json({ success: true, isGit: false, files: [] });
23
35
  }
24
36
 
25
37
  if (action === "status") {
26
- const { stdout } = await execPromise("git status --porcelain", { cwd: targetRoot });
38
+ const { stdout } = await git(["status", "--porcelain"], targetRoot);
27
39
  const lines = stdout.split(/\r?\n/).filter(Boolean);
28
40
  const files = lines.map(line => {
29
41
  // porcelain format: XY path or XY "path"
@@ -49,7 +61,7 @@ export async function GET(request: Request) {
49
61
  // Get branch name
50
62
  let branch = "";
51
63
  try {
52
- const { stdout: branchOut } = await execPromise("git rev-parse --abbrev-ref HEAD", { cwd: targetRoot });
64
+ const { stdout: branchOut } = await git(["rev-parse", "--abbrev-ref", "HEAD"], targetRoot);
53
65
  branch = branchOut.trim();
54
66
  } catch {}
55
67
 
@@ -57,7 +69,7 @@ export async function GET(request: Request) {
57
69
  let ahead = 0;
58
70
  let behind = 0;
59
71
  try {
60
- const { stdout: syncOut } = await execPromise("git rev-list --left-right --count HEAD...@{u}", { cwd: targetRoot });
72
+ const { stdout: syncOut } = await git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], targetRoot);
61
73
  const parts = syncOut.trim().split(/\s+/);
62
74
  if (parts.length === 2) {
63
75
  ahead = parseInt(parts[0], 10) || 0;
@@ -79,7 +91,7 @@ export async function GET(request: Request) {
79
91
  }
80
92
 
81
93
  // Check status to see if it is untracked
82
- const { stdout: statusOut } = await execPromise("git status --porcelain -- " + JSON.stringify(file), { cwd: targetRoot });
94
+ const { stdout: statusOut } = await git(["status", "--porcelain", "--", file], targetRoot);
83
95
  const isUntracked = statusOut.startsWith("??");
84
96
 
85
97
  if (isUntracked) {
@@ -88,7 +100,7 @@ export async function GET(request: Request) {
88
100
  return NextResponse.json({ success: true, diff: diffLines });
89
101
  } else {
90
102
  try {
91
- const { stdout: diffOut } = await execPromise(`git diff --no-color -- ${JSON.stringify(file)}`, { cwd: targetRoot });
103
+ const { stdout: diffOut } = await git(["diff", "--no-color", "--", file], targetRoot);
92
104
  return NextResponse.json({ success: true, diff: diffOut });
93
105
  } catch {
94
106
  return NextResponse.json({ success: true, diff: `Binary file or diff unavailable.` });
@@ -97,7 +109,7 @@ export async function GET(request: Request) {
97
109
  }
98
110
 
99
111
  if (action === "branches") {
100
- const { stdout } = await execPromise("git branch --format=\"%(refname:short)|%(HEAD)\"", { cwd: targetRoot });
112
+ const { stdout } = await git(["branch", "--format=%(refname:short)|%(HEAD)"], targetRoot);
101
113
  const branches = stdout.split(/\r?\n/).filter(Boolean).map(line => {
102
114
  const [name, active] = line.split("|");
103
115
  return {
@@ -110,7 +122,10 @@ export async function GET(request: Request) {
110
122
 
111
123
  if (action === "log") {
112
124
  try {
113
- const { stdout } = await execPromise("git log --graph --abbrev-commit --decorate --format=format:\"%h||%p||%s||%an||%ar\" --all -n 100", { cwd: targetRoot });
125
+ const { stdout } = await git(
126
+ ["log", "--graph", "--abbrev-commit", "--decorate", "--format=format:%h||%p||%s||%an||%ar", "--all", "-n", "100"],
127
+ targetRoot
128
+ );
114
129
  const lines = stdout.split(/\r?\n/);
115
130
  const logItems = lines.map(line => {
116
131
  const parts = line.split("||");
@@ -119,7 +134,7 @@ export async function GET(request: Request) {
119
134
  const hashMatch = firstPart.match(/([0-9a-f]{7,40})$/);
120
135
  const hash = hashMatch ? hashMatch[1] : "";
121
136
  const graph = firstPart.substring(0, firstPart.length - hash.length);
122
-
137
+
123
138
  return {
124
139
  isCommit: true,
125
140
  graph,
@@ -142,7 +157,7 @@ export async function GET(request: Request) {
142
157
  }
143
158
  });
144
159
  return NextResponse.json({ success: true, log: logItems });
145
- } catch (err) {
160
+ } catch {
146
161
  return NextResponse.json({ success: true, log: [] });
147
162
  }
148
163
  }
@@ -163,25 +178,25 @@ export async function POST(request: Request) {
163
178
 
164
179
  // Check if it's a git repo
165
180
  try {
166
- await execPromise("git rev-parse --is-inside-work-tree", { cwd: targetRoot });
181
+ await git(["rev-parse", "--is-inside-work-tree"], targetRoot);
167
182
  } catch {
168
183
  return NextResponse.json({ error: "Not a git repository" }, { status: 400 });
169
184
  }
170
185
 
171
186
  if (action === "stage") {
172
187
  if (file) {
173
- await execPromise(`git add ${JSON.stringify(file)}`, { cwd: targetRoot });
188
+ await git(["add", "--", file], targetRoot);
174
189
  } else {
175
- await execPromise("git add -A", { cwd: targetRoot });
190
+ await git(["add", "-A"], targetRoot);
176
191
  }
177
192
  return NextResponse.json({ success: true });
178
193
  }
179
194
 
180
195
  if (action === "unstage") {
181
196
  if (file) {
182
- await execPromise(`git reset HEAD -- ${JSON.stringify(file)}`, { cwd: targetRoot });
197
+ await git(["reset", "HEAD", "--", file], targetRoot);
183
198
  } else {
184
- await execPromise("git reset HEAD -- .", { cwd: targetRoot });
199
+ await git(["reset", "HEAD", "--", "."], targetRoot);
185
200
  }
186
201
  return NextResponse.json({ success: true });
187
202
  }
@@ -192,7 +207,7 @@ export async function POST(request: Request) {
192
207
  }
193
208
  const absolutePath = path.resolve(targetRoot, file).replace(/\\/g, "/");
194
209
  // Check status to see if it is untracked
195
- const { stdout: statusOut } = await execPromise("git status --porcelain -- " + JSON.stringify(file), { cwd: targetRoot });
210
+ const { stdout: statusOut } = await git(["status", "--porcelain", "--", file], targetRoot);
196
211
  const isUntracked = statusOut.startsWith("??");
197
212
 
198
213
  if (isUntracked) {
@@ -200,7 +215,7 @@ export async function POST(request: Request) {
200
215
  fs.unlinkSync(absolutePath);
201
216
  }
202
217
  } else {
203
- await execPromise(`git checkout -- ${JSON.stringify(file)}`, { cwd: targetRoot });
218
+ await git(["checkout", "--", file], targetRoot);
204
219
  }
205
220
  return NextResponse.json({ success: true });
206
221
  }
@@ -209,7 +224,7 @@ export async function POST(request: Request) {
209
224
  if (!message || !message.trim()) {
210
225
  return NextResponse.json({ error: "Commit message cannot be empty" }, { status: 400 });
211
226
  }
212
- const { stdout } = await execPromise(`git commit -m ${JSON.stringify(message.trim())}`, { cwd: targetRoot });
227
+ const { stdout } = await git(["commit", "-m", message.trim()], targetRoot);
213
228
  return NextResponse.json({ success: true, output: stdout });
214
229
  }
215
230
 
@@ -231,40 +246,40 @@ export async function POST(request: Request) {
231
246
  }
232
247
 
233
248
  if (action === "pull") {
234
- const { stdout, stderr } = await execPromise("git pull", { cwd: targetRoot });
249
+ const { stdout, stderr } = await git(["pull"], targetRoot);
235
250
  return NextResponse.json({ success: true, output: stdout + stderr });
236
251
  }
237
252
 
238
253
  if (action === "push") {
239
- const { stdout, stderr } = await execPromise("git push", { cwd: targetRoot });
254
+ const { stdout, stderr } = await git(["push"], targetRoot);
240
255
  return NextResponse.json({ success: true, output: stdout + stderr });
241
256
  }
242
257
 
243
258
  if (action === "checkout") {
244
259
  const { branchName } = body;
245
- if (!branchName) {
246
- return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
260
+ if (!isValidBranchName(branchName)) {
261
+ return NextResponse.json({ error: "Missing or invalid branchName parameter" }, { status: 400 });
247
262
  }
248
- const { stdout, stderr } = await execPromise(`git checkout ${JSON.stringify(branchName)}`, { cwd: targetRoot });
263
+ const { stdout, stderr } = await git(["checkout", branchName], targetRoot);
249
264
  return NextResponse.json({ success: true, output: stdout + stderr });
250
265
  }
251
266
 
252
267
  if (action === "create_branch") {
253
268
  const { branchName } = body;
254
- if (!branchName) {
255
- return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
269
+ if (!isValidBranchName(branchName)) {
270
+ return NextResponse.json({ error: "Missing or invalid branchName parameter" }, { status: 400 });
256
271
  }
257
- const { stdout, stderr } = await execPromise(`git checkout -b ${JSON.stringify(branchName)}`, { cwd: targetRoot });
272
+ const { stdout, stderr } = await git(["checkout", "-b", branchName], targetRoot);
258
273
  return NextResponse.json({ success: true, output: stdout + stderr });
259
274
  }
260
275
 
261
276
  if (action === "delete_branch") {
262
277
  const { branchName, force } = body;
263
- if (!branchName) {
264
- return NextResponse.json({ error: "Missing branchName parameter" }, { status: 400 });
278
+ if (!isValidBranchName(branchName)) {
279
+ return NextResponse.json({ error: "Missing or invalid branchName parameter" }, { status: 400 });
265
280
  }
266
281
  const flag = force ? "-D" : "-d";
267
- const { stdout, stderr } = await execPromise(`git branch ${flag} ${JSON.stringify(branchName)}`, { cwd: targetRoot });
282
+ const { stdout, stderr } = await git(["branch", flag, branchName], targetRoot);
268
283
  return NextResponse.json({ success: true, output: stdout + stderr });
269
284
  }
270
285
 
@@ -1,10 +1,24 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { SessionManager } from "@/lib/agents/SessionManager";
3
3
  import { promisify } from "util";
4
- import { exec } from "child_process";
4
+ import { execFile } from "child_process";
5
5
  import os from "os";
6
6
 
7
- const execPromise = promisify(exec);
7
+ const execFilePromise = promisify(execFile);
8
+
9
+ // Real per-process stats for the PTY. Returns null when unavailable (pidusage
10
+ // not installed, or no live pid) so callers report honestly instead of faking it.
11
+ async function getProcessStats(pid: number | undefined): Promise<{ cpu: number; memory: number } | null> {
12
+ if (!pid) return null;
13
+ try {
14
+ /* eslint-disable @typescript-eslint/no-require-imports */
15
+ const pidusage = eval("require")("pidusage");
16
+ const stat = await pidusage(pid);
17
+ return { cpu: stat.cpu, memory: stat.memory };
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
8
22
 
9
23
  export async function GET(request: Request) {
10
24
  try {
@@ -21,42 +35,45 @@ export async function GET(request: Request) {
21
35
  return NextResponse.json({ error: "Session not found" }, { status: 404 });
22
36
  }
23
37
 
24
- // Default response metrics
38
+ // Default response metrics. `available` reports whether these are real
39
+ // measurements (vs. a placeholder when telemetry can't be collected).
25
40
  let cpu = "0.0%";
26
41
  let memory = "0.0MiB";
27
42
  let memoryPercent = "0.0%";
43
+ let available = false;
28
44
 
29
45
  if (session.status === "running") {
30
46
  const isDocker = session.type === "claude-docker" || session.type === "aider-docker";
31
47
  if (isDocker) {
32
48
  try {
33
- // Query docker stats
49
+ // Real container stats from the Docker daemon.
34
50
  const containerName = `neural-loom-session-${id}`;
35
- const { stdout } = await execPromise(`docker stats ${containerName} --no-stream --format "{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}"`);
36
- const parts = stdout.trim().split(",");
51
+ const { stdout } = await execFilePromise("docker", [
52
+ "stats",
53
+ containerName,
54
+ "--no-stream",
55
+ "--format",
56
+ "{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}",
57
+ ]);
58
+ const parts = stdout.toString().trim().split(",");
37
59
  if (parts.length >= 3) {
38
60
  cpu = parts[0].trim();
39
- memory = parts[1].trim().split(" / ")[0]; // get the used part, e.g. "142.2MiB"
61
+ memory = parts[1].trim().split(" / ")[0]; // used part, e.g. "142.2MiB"
40
62
  memoryPercent = parts[2].trim();
63
+ available = true;
41
64
  }
42
65
  } catch {
43
- // Docker stats not available/running, fallback to simulated telemetry
44
- const simulatedCpu = (Math.random() * 3 + 1).toFixed(1) + "%";
45
- const simulatedMem = (130 + Math.random() * 20).toFixed(1) + "MiB";
46
- const simulatedMemPerc = (1.5 + Math.random() * 0.5).toFixed(2) + "%";
47
- cpu = simulatedCpu;
48
- memory = simulatedMem;
49
- memoryPercent = simulatedMemPerc;
66
+ // Docker daemon/container not reachable leave defaults, available=false.
50
67
  }
51
68
  } else {
52
- // Local runner telemetry fallback
53
- const simulatedCpu = (Math.random() * 5 + 2).toFixed(1) + "%";
54
- const memBytes = process.memoryUsage().rss;
55
- const simulatedMem = (memBytes / (1024 * 1024)).toFixed(1) + "MiB";
56
- const simulatedMemPerc = ((memBytes / os.totalmem()) * 100).toFixed(2) + "%";
57
- cpu = simulatedCpu;
58
- memory = simulatedMem;
59
- memoryPercent = simulatedMemPerc;
69
+ // Real per-process stats for the local/SSH PTY process.
70
+ const stats = await getProcessStats(session.getPid());
71
+ if (stats) {
72
+ cpu = stats.cpu.toFixed(1) + "%";
73
+ memory = (stats.memory / (1024 * 1024)).toFixed(1) + "MiB";
74
+ memoryPercent = ((stats.memory / os.totalmem()) * 100).toFixed(2) + "%";
75
+ available = true;
76
+ }
60
77
  }
61
78
  }
62
79
 
@@ -68,6 +85,7 @@ export async function GET(request: Request) {
68
85
  cpu,
69
86
  memory,
70
87
  memoryPercent,
88
+ available,
71
89
  });
72
90
  } catch (error) {
73
91
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -1,19 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { ConnectionManager } from "@/lib/sql/ConnectionManager";
3
-
4
- const isMutation = (sql: string): boolean => {
5
- const clean = sql.trim().toLowerCase();
6
- return (
7
- clean.startsWith("insert") ||
8
- clean.startsWith("update") ||
9
- clean.startsWith("delete") ||
10
- clean.startsWith("drop") ||
11
- clean.startsWith("alter") ||
12
- clean.startsWith("create") ||
13
- clean.startsWith("truncate") ||
14
- clean.startsWith("replace")
15
- );
16
- };
3
+ import { isReadOnlyViolation } from "@/lib/sql/sqlGuard";
17
4
 
18
5
  export async function POST(request: Request) {
19
6
  try {
@@ -33,7 +20,7 @@ export async function POST(request: Request) {
33
20
 
34
21
  const { adapter, profile } = active;
35
22
 
36
- if (profile.readOnly && isMutation(sql)) {
23
+ if (profile.readOnly && isReadOnlyViolation(sql)) {
37
24
  return NextResponse.json({
38
25
  error: "This connection profile is configured as Read-Only. Modifications are disabled."
39
26
  }, { status: 403 });
@@ -1,10 +1,11 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import { getConfigDir } from "@/lib/config";
4
5
 
5
6
  export async function GET() {
6
7
  try {
7
- const historyFile = path.join(process.cwd(), ".ai", "neural-loom", "token-history.json");
8
+ const historyFile = path.join(getConfigDir(), "token-history.json");
8
9
  if (!fs.existsSync(historyFile)) {
9
10
  return NextResponse.json({ success: true, history: [] });
10
11
  }