@duckcodeailabs/dql-cli 1.4.4 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +123 -0
  2. package/README.md +72 -0
  3. package/{apps-api.d.ts → dist/apps-api.d.ts} +27 -3
  4. package/dist/apps-api.d.ts.map +1 -0
  5. package/{apps-api.js → dist/apps-api.js} +512 -8
  6. package/dist/apps-api.js.map +1 -0
  7. package/{apps-api.test.js → dist/apps-api.test.js} +6 -0
  8. package/{apps-api.test.js.map → dist/apps-api.test.js.map} +1 -1
  9. package/{args.test.js → dist/args.test.js} +8 -0
  10. package/{args.test.js.map → dist/args.test.js.map} +1 -1
  11. package/dist/assets/dql-notebook/assets/index-DZ2X3-OY.js +862 -0
  12. package/dist/assets/dql-notebook/assets/index-R3UrqjLQ.css +1 -0
  13. package/{assets → dist/assets}/dql-notebook/index.html +2 -2
  14. package/dist/block-studio-import.d.ts +59 -0
  15. package/dist/block-studio-import.d.ts.map +1 -0
  16. package/dist/block-studio-import.js +398 -0
  17. package/dist/block-studio-import.js.map +1 -0
  18. package/dist/block-studio-import.test.d.ts +2 -0
  19. package/dist/block-studio-import.test.d.ts.map +1 -0
  20. package/dist/block-studio-import.test.js +110 -0
  21. package/dist/block-studio-import.test.js.map +1 -0
  22. package/dist/commands/agent.d.ts.map +1 -0
  23. package/{commands → dist/commands}/agent.js +98 -5
  24. package/dist/commands/agent.js.map +1 -0
  25. package/{commands → dist/commands}/app.d.ts.map +1 -1
  26. package/{commands → dist/commands}/app.js +6 -0
  27. package/dist/commands/app.js.map +1 -0
  28. package/dist/commands/import.d.ts +3 -0
  29. package/dist/commands/import.d.ts.map +1 -0
  30. package/dist/commands/import.js +50 -0
  31. package/dist/commands/import.js.map +1 -0
  32. package/{commands → dist/commands}/init.d.ts.map +1 -1
  33. package/{commands → dist/commands}/init.js +8 -5
  34. package/dist/commands/init.js.map +1 -0
  35. package/{commands → dist/commands}/init.test.js +84 -24
  36. package/dist/commands/init.test.js.map +1 -0
  37. package/{commands → dist/commands}/migrate.d.ts.map +1 -1
  38. package/{commands → dist/commands}/migrate.js +5 -0
  39. package/dist/commands/migrate.js.map +1 -0
  40. package/dist/commands/validate.d.ts.map +1 -0
  41. package/dist/commands/validate.js +163 -0
  42. package/dist/commands/validate.js.map +1 -0
  43. package/dist/commands/validate.test.d.ts +2 -0
  44. package/dist/commands/validate.test.d.ts.map +1 -0
  45. package/dist/commands/validate.test.js +55 -0
  46. package/dist/commands/validate.test.js.map +1 -0
  47. package/{index.js → dist/index.js} +6 -1
  48. package/dist/index.js.map +1 -0
  49. package/{llm → dist/llm}/index.d.ts.map +1 -1
  50. package/{llm → dist/llm}/index.js +4 -3
  51. package/{llm → dist/llm}/index.js.map +1 -1
  52. package/{llm → dist/llm}/providers/dql-agent-provider.d.ts +1 -1
  53. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -0
  54. package/dist/llm/providers/dql-agent-provider.js +287 -0
  55. package/dist/llm/providers/dql-agent-provider.js.map +1 -0
  56. package/{llm → dist/llm}/types.d.ts +3 -1
  57. package/dist/llm/types.d.ts.map +1 -0
  58. package/{local-runtime.d.ts.map → dist/local-runtime.d.ts.map} +1 -1
  59. package/{local-runtime.js → dist/local-runtime.js} +567 -50
  60. package/dist/local-runtime.js.map +1 -0
  61. package/{schedule → dist/schedule}/runner.d.ts.map +1 -1
  62. package/{schedule → dist/schedule}/runner.js +4 -0
  63. package/{schedule → dist/schedule}/runner.js.map +1 -1
  64. package/dist/settings/provider-settings.d.ts +33 -0
  65. package/dist/settings/provider-settings.d.ts.map +1 -0
  66. package/dist/settings/provider-settings.js +91 -0
  67. package/dist/settings/provider-settings.js.map +1 -0
  68. package/package.json +31 -20
  69. package/apps-api.d.ts.map +0 -1
  70. package/apps-api.js.map +0 -1
  71. package/assets/dql-notebook/assets/index-DUTeFz5j.js +0 -858
  72. package/assets/dql-notebook/assets/index-DrhoZmtv.css +0 -1
  73. package/commands/agent.d.ts.map +0 -1
  74. package/commands/agent.js.map +0 -1
  75. package/commands/app.js.map +0 -1
  76. package/commands/init.js.map +0 -1
  77. package/commands/init.test.js.map +0 -1
  78. package/commands/migrate.js.map +0 -1
  79. package/commands/validate.d.ts.map +0 -1
  80. package/commands/validate.js +0 -116
  81. package/commands/validate.js.map +0 -1
  82. package/index.js.map +0 -1
  83. package/llm/providers/dql-agent-provider.d.ts.map +0 -1
  84. package/llm/providers/dql-agent-provider.js +0 -99
  85. package/llm/providers/dql-agent-provider.js.map +0 -1
  86. package/llm/types.d.ts.map +0 -1
  87. package/local-runtime.js.map +0 -1
  88. /package/{apps-api.test.d.ts → dist/apps-api.test.d.ts} +0 -0
  89. /package/{apps-api.test.d.ts.map → dist/apps-api.test.d.ts.map} +0 -0
  90. /package/{args.d.ts → dist/args.d.ts} +0 -0
  91. /package/{args.d.ts.map → dist/args.d.ts.map} +0 -0
  92. /package/{args.js → dist/args.js} +0 -0
  93. /package/{args.js.map → dist/args.js.map} +0 -0
  94. /package/{args.test.d.ts → dist/args.test.d.ts} +0 -0
  95. /package/{args.test.d.ts.map → dist/args.test.d.ts.map} +0 -0
  96. /package/{assets → dist/assets}/dql-notebook/assets/codemirror-DJYUkPr1.js +0 -0
  97. /package/{assets → dist/assets}/dql-notebook/assets/react-CRB3T2We.js +0 -0
  98. /package/{assets → dist/assets}/notebook-browser/app.js +0 -0
  99. /package/{assets → dist/assets}/notebook-browser/index.html +0 -0
  100. /package/{assets → dist/assets}/notebook-browser/styles.css +0 -0
  101. /package/{block-templates.d.ts → dist/block-templates.d.ts} +0 -0
  102. /package/{block-templates.d.ts.map → dist/block-templates.d.ts.map} +0 -0
  103. /package/{block-templates.js → dist/block-templates.js} +0 -0
  104. /package/{block-templates.js.map → dist/block-templates.js.map} +0 -0
  105. /package/{commands → dist/commands}/agent.d.ts +0 -0
  106. /package/{commands → dist/commands}/app.d.ts +0 -0
  107. /package/{commands → dist/commands}/build.d.ts +0 -0
  108. /package/{commands → dist/commands}/build.d.ts.map +0 -0
  109. /package/{commands → dist/commands}/build.js +0 -0
  110. /package/{commands → dist/commands}/build.js.map +0 -0
  111. /package/{commands → dist/commands}/build.test.d.ts +0 -0
  112. /package/{commands → dist/commands}/build.test.d.ts.map +0 -0
  113. /package/{commands → dist/commands}/build.test.js +0 -0
  114. /package/{commands → dist/commands}/build.test.js.map +0 -0
  115. /package/{commands → dist/commands}/certify.d.ts +0 -0
  116. /package/{commands → dist/commands}/certify.d.ts.map +0 -0
  117. /package/{commands → dist/commands}/certify.js +0 -0
  118. /package/{commands → dist/commands}/certify.js.map +0 -0
  119. /package/{commands → dist/commands}/compile.d.ts +0 -0
  120. /package/{commands → dist/commands}/compile.d.ts.map +0 -0
  121. /package/{commands → dist/commands}/compile.js +0 -0
  122. /package/{commands → dist/commands}/compile.js.map +0 -0
  123. /package/{commands → dist/commands}/compile.test.d.ts +0 -0
  124. /package/{commands → dist/commands}/compile.test.d.ts.map +0 -0
  125. /package/{commands → dist/commands}/compile.test.js +0 -0
  126. /package/{commands → dist/commands}/compile.test.js.map +0 -0
  127. /package/{commands → dist/commands}/diff.d.ts +0 -0
  128. /package/{commands → dist/commands}/diff.d.ts.map +0 -0
  129. /package/{commands → dist/commands}/diff.js +0 -0
  130. /package/{commands → dist/commands}/diff.js.map +0 -0
  131. /package/{commands → dist/commands}/doctor.d.ts +0 -0
  132. /package/{commands → dist/commands}/doctor.d.ts.map +0 -0
  133. /package/{commands → dist/commands}/doctor.js +0 -0
  134. /package/{commands → dist/commands}/doctor.js.map +0 -0
  135. /package/{commands → dist/commands}/doctor.test.d.ts +0 -0
  136. /package/{commands → dist/commands}/doctor.test.d.ts.map +0 -0
  137. /package/{commands → dist/commands}/doctor.test.js +0 -0
  138. /package/{commands → dist/commands}/doctor.test.js.map +0 -0
  139. /package/{commands → dist/commands}/fmt.d.ts +0 -0
  140. /package/{commands → dist/commands}/fmt.d.ts.map +0 -0
  141. /package/{commands → dist/commands}/fmt.js +0 -0
  142. /package/{commands → dist/commands}/fmt.js.map +0 -0
  143. /package/{commands → dist/commands}/info.d.ts +0 -0
  144. /package/{commands → dist/commands}/info.d.ts.map +0 -0
  145. /package/{commands → dist/commands}/info.js +0 -0
  146. /package/{commands → dist/commands}/info.js.map +0 -0
  147. /package/{commands → dist/commands}/init.d.ts +0 -0
  148. /package/{commands → dist/commands}/init.test.d.ts +0 -0
  149. /package/{commands → dist/commands}/init.test.d.ts.map +0 -0
  150. /package/{commands → dist/commands}/lineage.d.ts +0 -0
  151. /package/{commands → dist/commands}/lineage.d.ts.map +0 -0
  152. /package/{commands → dist/commands}/lineage.js +0 -0
  153. /package/{commands → dist/commands}/lineage.js.map +0 -0
  154. /package/{commands → dist/commands}/mcp.d.ts +0 -0
  155. /package/{commands → dist/commands}/mcp.d.ts.map +0 -0
  156. /package/{commands → dist/commands}/mcp.js +0 -0
  157. /package/{commands → dist/commands}/mcp.js.map +0 -0
  158. /package/{commands → dist/commands}/migrate.d.ts +0 -0
  159. /package/{commands → dist/commands}/new.d.ts +0 -0
  160. /package/{commands → dist/commands}/new.d.ts.map +0 -0
  161. /package/{commands → dist/commands}/new.js +0 -0
  162. /package/{commands → dist/commands}/new.js.map +0 -0
  163. /package/{commands → dist/commands}/new.test.d.ts +0 -0
  164. /package/{commands → dist/commands}/new.test.d.ts.map +0 -0
  165. /package/{commands → dist/commands}/new.test.js +0 -0
  166. /package/{commands → dist/commands}/new.test.js.map +0 -0
  167. /package/{commands → dist/commands}/notebook.d.ts +0 -0
  168. /package/{commands → dist/commands}/notebook.d.ts.map +0 -0
  169. /package/{commands → dist/commands}/notebook.js +0 -0
  170. /package/{commands → dist/commands}/notebook.js.map +0 -0
  171. /package/{commands → dist/commands}/parse.d.ts +0 -0
  172. /package/{commands → dist/commands}/parse.d.ts.map +0 -0
  173. /package/{commands → dist/commands}/parse.js +0 -0
  174. /package/{commands → dist/commands}/parse.js.map +0 -0
  175. /package/{commands → dist/commands}/preview.d.ts +0 -0
  176. /package/{commands → dist/commands}/preview.d.ts.map +0 -0
  177. /package/{commands → dist/commands}/preview.js +0 -0
  178. /package/{commands → dist/commands}/preview.js.map +0 -0
  179. /package/{commands → dist/commands}/schedule.d.ts +0 -0
  180. /package/{commands → dist/commands}/schedule.d.ts.map +0 -0
  181. /package/{commands → dist/commands}/schedule.js +0 -0
  182. /package/{commands → dist/commands}/schedule.js.map +0 -0
  183. /package/{commands → dist/commands}/semantic.d.ts +0 -0
  184. /package/{commands → dist/commands}/semantic.d.ts.map +0 -0
  185. /package/{commands → dist/commands}/semantic.js +0 -0
  186. /package/{commands → dist/commands}/semantic.js.map +0 -0
  187. /package/{commands → dist/commands}/serve.d.ts +0 -0
  188. /package/{commands → dist/commands}/serve.d.ts.map +0 -0
  189. /package/{commands → dist/commands}/serve.js +0 -0
  190. /package/{commands → dist/commands}/serve.js.map +0 -0
  191. /package/{commands → dist/commands}/slack.d.ts +0 -0
  192. /package/{commands → dist/commands}/slack.d.ts.map +0 -0
  193. /package/{commands → dist/commands}/slack.js +0 -0
  194. /package/{commands → dist/commands}/slack.js.map +0 -0
  195. /package/{commands → dist/commands}/sync.d.ts +0 -0
  196. /package/{commands → dist/commands}/sync.d.ts.map +0 -0
  197. /package/{commands → dist/commands}/sync.js +0 -0
  198. /package/{commands → dist/commands}/sync.js.map +0 -0
  199. /package/{commands → dist/commands}/sync.test.d.ts +0 -0
  200. /package/{commands → dist/commands}/sync.test.d.ts.map +0 -0
  201. /package/{commands → dist/commands}/sync.test.js +0 -0
  202. /package/{commands → dist/commands}/sync.test.js.map +0 -0
  203. /package/{commands → dist/commands}/test.d.ts +0 -0
  204. /package/{commands → dist/commands}/test.d.ts.map +0 -0
  205. /package/{commands → dist/commands}/test.js +0 -0
  206. /package/{commands → dist/commands}/test.js.map +0 -0
  207. /package/{commands → dist/commands}/validate.d.ts +0 -0
  208. /package/{commands → dist/commands}/verify.d.ts +0 -0
  209. /package/{commands → dist/commands}/verify.d.ts.map +0 -0
  210. /package/{commands → dist/commands}/verify.js +0 -0
  211. /package/{commands → dist/commands}/verify.js.map +0 -0
  212. /package/{digest.d.ts → dist/digest.d.ts} +0 -0
  213. /package/{digest.d.ts.map → dist/digest.d.ts.map} +0 -0
  214. /package/{digest.js → dist/digest.js} +0 -0
  215. /package/{digest.js.map → dist/digest.js.map} +0 -0
  216. /package/{git-service.d.ts → dist/git-service.d.ts} +0 -0
  217. /package/{git-service.d.ts.map → dist/git-service.d.ts.map} +0 -0
  218. /package/{git-service.js → dist/git-service.js} +0 -0
  219. /package/{git-service.js.map → dist/git-service.js.map} +0 -0
  220. /package/{governance-runtime.d.ts → dist/governance-runtime.d.ts} +0 -0
  221. /package/{governance-runtime.d.ts.map → dist/governance-runtime.d.ts.map} +0 -0
  222. /package/{governance-runtime.js → dist/governance-runtime.js} +0 -0
  223. /package/{governance-runtime.js.map → dist/governance-runtime.js.map} +0 -0
  224. /package/{index.d.ts → dist/index.d.ts} +0 -0
  225. /package/{index.d.ts.map → dist/index.d.ts.map} +0 -0
  226. /package/{llm → dist/llm}/index.d.ts +0 -0
  227. /package/{llm → dist/llm}/providers/claude-agent-sdk.d.ts +0 -0
  228. /package/{llm → dist/llm}/providers/claude-agent-sdk.d.ts.map +0 -0
  229. /package/{llm → dist/llm}/providers/claude-agent-sdk.js +0 -0
  230. /package/{llm → dist/llm}/providers/claude-agent-sdk.js.map +0 -0
  231. /package/{llm → dist/llm}/providers/claude-code.d.ts +0 -0
  232. /package/{llm → dist/llm}/providers/claude-code.d.ts.map +0 -0
  233. /package/{llm → dist/llm}/providers/claude-code.js +0 -0
  234. /package/{llm → dist/llm}/providers/claude-code.js.map +0 -0
  235. /package/{llm → dist/llm}/tools.d.ts +0 -0
  236. /package/{llm → dist/llm}/tools.d.ts.map +0 -0
  237. /package/{llm → dist/llm}/tools.js +0 -0
  238. /package/{llm → dist/llm}/tools.js.map +0 -0
  239. /package/{llm → dist/llm}/types.js +0 -0
  240. /package/{llm → dist/llm}/types.js.map +0 -0
  241. /package/{local-runtime.d.ts → dist/local-runtime.d.ts} +0 -0
  242. /package/{local-runtime.test.d.ts → dist/local-runtime.test.d.ts} +0 -0
  243. /package/{local-runtime.test.d.ts.map → dist/local-runtime.test.d.ts.map} +0 -0
  244. /package/{local-runtime.test.js → dist/local-runtime.test.js} +0 -0
  245. /package/{local-runtime.test.js.map → dist/local-runtime.test.js.map} +0 -0
  246. /package/{metricflow.d.ts → dist/metricflow.d.ts} +0 -0
  247. /package/{metricflow.d.ts.map → dist/metricflow.d.ts.map} +0 -0
  248. /package/{metricflow.js → dist/metricflow.js} +0 -0
  249. /package/{metricflow.js.map → dist/metricflow.js.map} +0 -0
  250. /package/{metricflow.test.d.ts → dist/metricflow.test.d.ts} +0 -0
  251. /package/{metricflow.test.d.ts.map → dist/metricflow.test.d.ts.map} +0 -0
  252. /package/{metricflow.test.js → dist/metricflow.test.js} +0 -0
  253. /package/{metricflow.test.js.map → dist/metricflow.test.js.map} +0 -0
  254. /package/{open-browser.d.ts → dist/open-browser.d.ts} +0 -0
  255. /package/{open-browser.d.ts.map → dist/open-browser.d.ts.map} +0 -0
  256. /package/{open-browser.js → dist/open-browser.js} +0 -0
  257. /package/{open-browser.js.map → dist/open-browser.js.map} +0 -0
  258. /package/{schedule → dist/schedule}/alerts.d.ts +0 -0
  259. /package/{schedule → dist/schedule}/alerts.d.ts.map +0 -0
  260. /package/{schedule → dist/schedule}/alerts.js +0 -0
  261. /package/{schedule → dist/schedule}/alerts.js.map +0 -0
  262. /package/{schedule → dist/schedule}/discovery.d.ts +0 -0
  263. /package/{schedule → dist/schedule}/discovery.d.ts.map +0 -0
  264. /package/{schedule → dist/schedule}/discovery.js +0 -0
  265. /package/{schedule → dist/schedule}/discovery.js.map +0 -0
  266. /package/{schedule → dist/schedule}/notifiers/email.d.ts +0 -0
  267. /package/{schedule → dist/schedule}/notifiers/email.d.ts.map +0 -0
  268. /package/{schedule → dist/schedule}/notifiers/email.js +0 -0
  269. /package/{schedule → dist/schedule}/notifiers/email.js.map +0 -0
  270. /package/{schedule → dist/schedule}/notifiers/file.d.ts +0 -0
  271. /package/{schedule → dist/schedule}/notifiers/file.d.ts.map +0 -0
  272. /package/{schedule → dist/schedule}/notifiers/file.js +0 -0
  273. /package/{schedule → dist/schedule}/notifiers/file.js.map +0 -0
  274. /package/{schedule → dist/schedule}/notifiers/index.d.ts +0 -0
  275. /package/{schedule → dist/schedule}/notifiers/index.d.ts.map +0 -0
  276. /package/{schedule → dist/schedule}/notifiers/index.js +0 -0
  277. /package/{schedule → dist/schedule}/notifiers/index.js.map +0 -0
  278. /package/{schedule → dist/schedule}/notifiers/slack.d.ts +0 -0
  279. /package/{schedule → dist/schedule}/notifiers/slack.d.ts.map +0 -0
  280. /package/{schedule → dist/schedule}/notifiers/slack.js +0 -0
  281. /package/{schedule → dist/schedule}/notifiers/slack.js.map +0 -0
  282. /package/{schedule → dist/schedule}/runner.d.ts +0 -0
  283. /package/{schedule → dist/schedule}/runs.d.ts +0 -0
  284. /package/{schedule → dist/schedule}/runs.d.ts.map +0 -0
  285. /package/{schedule → dist/schedule}/runs.js +0 -0
  286. /package/{schedule → dist/schedule}/runs.js.map +0 -0
  287. /package/{schedule → dist/schedule}/service.d.ts +0 -0
  288. /package/{schedule → dist/schedule}/service.d.ts.map +0 -0
  289. /package/{schedule → dist/schedule}/service.js +0 -0
  290. /package/{schedule → dist/schedule}/service.js.map +0 -0
  291. /package/{schedule → dist/schedule}/types.d.ts +0 -0
  292. /package/{schedule → dist/schedule}/types.d.ts.map +0 -0
  293. /package/{schedule → dist/schedule}/types.js +0 -0
  294. /package/{schedule → dist/schedule}/types.js.map +0 -0
  295. /package/{semantic-import.d.ts → dist/semantic-import.d.ts} +0 -0
  296. /package/{semantic-import.d.ts.map → dist/semantic-import.d.ts.map} +0 -0
  297. /package/{semantic-import.js → dist/semantic-import.js} +0 -0
  298. /package/{semantic-import.js.map → dist/semantic-import.js.map} +0 -0
  299. /package/{semantic-import.test.d.ts → dist/semantic-import.test.d.ts} +0 -0
  300. /package/{semantic-import.test.d.ts.map → dist/semantic-import.test.d.ts.map} +0 -0
  301. /package/{semantic-import.test.js → dist/semantic-import.test.js} +0 -0
  302. /package/{semantic-import.test.js.map → dist/semantic-import.test.js.map} +0 -0
@@ -4,9 +4,9 @@
4
4
  * dispatcher — returns `true` if the request was handled, `false` otherwise.
5
5
  */
6
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
7
- import { join, dirname } from 'node:path';
7
+ import { join, dirname, relative, basename } from 'node:path';
8
8
  import { loadAppDocument, findAppDocuments, loadDashboardDocument, findDashboardsForApp, parseAppDocument, parseDashboardDocument, suggestAppId, } from '@duckcodeailabs/dql-core';
9
- import { defaultPersonaRegistry, personaFromMember, } from '@duckcodeailabs/dql-project';
9
+ import { defaultPersonaRegistry, defaultLocalAppsDbPath, LocalAppStorage, personaFromMember, } from '@duckcodeailabs/dql-project';
10
10
  export async function handleAppsApi(ctx) {
11
11
  const { req, res, path, projectRoot } = ctx;
12
12
  // ── Apps ────────────────────────────────────────────────────────────────
@@ -40,8 +40,166 @@ export async function handleAppsApi(ctx) {
40
40
  }
41
41
  return true;
42
42
  }
43
+ let m = path.match(/^\/api\/apps\/([^/]+)\/editor\/catalog$/);
44
+ if (m && req.method === 'GET') {
45
+ const appId = decodeURIComponent(m[1]);
46
+ const app = loadAppById(projectRoot, appId)?.app;
47
+ if (!app) {
48
+ sendJson(res, 404, { error: `App "${appId}" not found` });
49
+ return true;
50
+ }
51
+ const domain = ctx.url.searchParams.get('domain') ?? app.domain;
52
+ const certifiedOnly = ctx.url.searchParams.get('certifiedOnly') !== 'false';
53
+ const blocks = collectBlockCandidates(projectRoot)
54
+ .filter((block) => !certifiedOnly || block.status === 'certified')
55
+ .filter((block) => !domain || block.domain === domain || appAllowsExecute(app, block.domain))
56
+ .sort((a, b) => {
57
+ const aDomain = a.domain === app.domain ? 0 : 1;
58
+ const bDomain = b.domain === app.domain ? 0 : 1;
59
+ return aDomain - bDomain || a.name.localeCompare(b.name);
60
+ });
61
+ sendJson(res, 200, {
62
+ appId,
63
+ defaultDomain: app.domain,
64
+ domains: unique(blocks.map((block) => block.domain)),
65
+ blocks,
66
+ });
67
+ return true;
68
+ }
69
+ m = path.match(/^\/api\/apps\/([^/]+)\/dashboards$/);
70
+ if (m && req.method === 'POST') {
71
+ const appId = decodeURIComponent(m[1]);
72
+ try {
73
+ const body = await readJson(req);
74
+ const result = createDashboardForApp(projectRoot, appId, body);
75
+ if (!result.ok) {
76
+ sendJson(res, 400, { error: result.error });
77
+ return true;
78
+ }
79
+ sendJson(res, 201, result);
80
+ }
81
+ catch (err) {
82
+ sendJson(res, 500, { error: err.message });
83
+ }
84
+ return true;
85
+ }
86
+ m = path.match(/^\/api\/apps\/([^/]+)\/notebooks$/);
87
+ if (m && req.method === 'POST') {
88
+ const appId = decodeURIComponent(m[1]);
89
+ try {
90
+ const body = await readJson(req);
91
+ const result = attachNotebookToApp(projectRoot, appId, body);
92
+ if (!result.ok) {
93
+ sendJson(res, 400, { error: result.error });
94
+ return true;
95
+ }
96
+ sendJson(res, 200, loadAppById(projectRoot, appId) ?? result);
97
+ }
98
+ catch (err) {
99
+ sendJson(res, 500, { error: err.message });
100
+ }
101
+ return true;
102
+ }
103
+ m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins$/);
104
+ if (m) {
105
+ const appId = decodeURIComponent(m[1]);
106
+ if (req.method === 'GET') {
107
+ const dashboardId = ctx.url.searchParams.get('dashboardId') ?? undefined;
108
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
109
+ try {
110
+ sendJson(res, 200, { pins: storage.listAiPins(appId, dashboardId) });
111
+ }
112
+ finally {
113
+ storage.close();
114
+ }
115
+ return true;
116
+ }
117
+ if (req.method === 'POST') {
118
+ try {
119
+ const body = await readJson(req);
120
+ const created = createAiPinTile(projectRoot, appId, body);
121
+ if (!created.ok) {
122
+ sendJson(res, 400, { error: created.error });
123
+ return true;
124
+ }
125
+ sendJson(res, 201, created);
126
+ }
127
+ catch (err) {
128
+ sendJson(res, 500, { error: err.message });
129
+ }
130
+ return true;
131
+ }
132
+ }
133
+ m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins\/([^/]+)\/refresh$/);
134
+ if (m && req.method === 'POST') {
135
+ const pinId = decodeURIComponent(m[2]);
136
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
137
+ try {
138
+ const pin = storage.getAiPin(pinId);
139
+ if (!pin) {
140
+ sendJson(res, 404, { error: `AI pin "${pinId}" not found` });
141
+ return true;
142
+ }
143
+ if (!pin.sql) {
144
+ const updated = storage.updateAiPinResult(pinId, pin.result, 'Pin has no SQL to refresh.');
145
+ sendJson(res, 400, { error: 'Pin has no SQL to refresh.', pin: updated });
146
+ return true;
147
+ }
148
+ if (!ctx.executeSql) {
149
+ const updated = storage.updateAiPinResult(pinId, pin.result, 'This host cannot execute AI pin SQL.');
150
+ sendJson(res, 400, { error: 'This host cannot execute AI pin SQL.', pin: updated });
151
+ return true;
152
+ }
153
+ const result = await ctx.executeSql(pin.sql);
154
+ const updated = storage.updateAiPinResult(pinId, result);
155
+ sendJson(res, 200, { ok: true, pin: updated });
156
+ }
157
+ catch (err) {
158
+ const pin = storage.updateAiPinResult(pinId, undefined, err instanceof Error ? err.message : String(err));
159
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err), pin });
160
+ }
161
+ finally {
162
+ storage.close();
163
+ }
164
+ return true;
165
+ }
166
+ m = path.match(/^\/api\/apps\/([^/]+)\/ai-pins\/([^/]+)\/promote$/);
167
+ if (m && req.method === 'POST') {
168
+ const appId = decodeURIComponent(m[1]);
169
+ const pinId = decodeURIComponent(m[2]);
170
+ try {
171
+ const result = promoteAiPinToDraftBlock(projectRoot, appId, pinId);
172
+ if (!result.ok) {
173
+ sendJson(res, 400, { error: result.error });
174
+ return true;
175
+ }
176
+ sendJson(res, 200, result);
177
+ }
178
+ catch (err) {
179
+ sendJson(res, 500, { error: err.message });
180
+ }
181
+ return true;
182
+ }
183
+ m = path.match(/^\/api\/apps\/([^/]+)\/dashboards\/([^/]+)\/layout$/);
184
+ if (m && req.method === 'PATCH') {
185
+ const appId = decodeURIComponent(m[1]);
186
+ const dashboardId = decodeURIComponent(m[2]);
187
+ try {
188
+ const body = await readJson(req);
189
+ const result = patchDashboardLayout(projectRoot, appId, dashboardId, body);
190
+ if (!result.ok) {
191
+ sendJson(res, 400, { error: result.error });
192
+ return true;
193
+ }
194
+ sendJson(res, 200, result);
195
+ }
196
+ catch (err) {
197
+ sendJson(res, 500, { error: err.message });
198
+ }
199
+ return true;
200
+ }
43
201
  // /api/apps/:id — single App with dashboards summary
44
- let m = path.match(/^\/api\/apps\/([^/]+)$/);
202
+ m = path.match(/^\/api\/apps\/([^/]+)$/);
45
203
  if (m && req.method === 'GET') {
46
204
  const id = m[1];
47
205
  const result = loadAppById(projectRoot, id);
@@ -129,7 +287,6 @@ export async function handleAppsApi(ctx) {
129
287
  }
130
288
  return false;
131
289
  }
132
- // ---- Helpers ----
133
290
  function collectAppsList(projectRoot) {
134
291
  const out = [];
135
292
  for (const p of findAppDocuments(projectRoot)) {
@@ -147,9 +304,15 @@ function collectAppsList(projectRoot) {
147
304
  id: document.id,
148
305
  name: document.name,
149
306
  domain: document.domain,
307
+ subdomain: document.subdomain,
308
+ groups: document.groups ?? [],
150
309
  description: document.description,
151
- audience: audienceFromTags(document.tags ?? []),
152
- status: dashboards.length > 0 ? 'ready' : 'empty',
310
+ audience: document.audience ?? audienceFromTags(document.tags ?? []),
311
+ lifecycle: document.lifecycle ?? 'draft',
312
+ certification: document.lifecycle === 'certified' ? 'certified' : 'uncertified',
313
+ status: document.lifecycle === 'review' ? 'review' : dashboards.length > 0 ? 'ready' : 'empty',
314
+ storage: document.visibility === 'private' ? 'mine' : document.visibility === 'template' ? 'template' : 'shared',
315
+ visibility: document.visibility ?? 'shared',
153
316
  owners: document.owners,
154
317
  tags: document.tags ?? [],
155
318
  members: document.members.length,
@@ -157,6 +320,9 @@ function collectAppsList(projectRoot) {
157
320
  policies: document.policies.length,
158
321
  schedules: (document.schedules ?? []).length,
159
322
  dashboards,
323
+ notebooks: listAppNotebookRefs(projectRoot, document, appDir),
324
+ drafts: listAppDrafts(projectRoot, appDir),
325
+ aiPins: countAiPins(projectRoot, document.id),
160
326
  homepage: document.homepage,
161
327
  });
162
328
  }
@@ -224,6 +390,12 @@ export function createAppPackage(projectRoot, input) {
224
390
  return { ok: false, error: `App already exists: ${id}` };
225
391
  const owner = cleanString(input.owners?.[0]) || `${process.env.USER ?? 'owner'}@local`;
226
392
  const audience = cleanString(input.audience);
393
+ const subdomain = cleanString(input.subdomain);
394
+ const groups = normalizeTags(input.groups ?? []);
395
+ const visibility = input.visibility === 'private' || input.visibility === 'template' ? input.visibility : 'shared';
396
+ const lifecycle = input.lifecycle === 'certified' || input.lifecycle === 'review' || input.lifecycle === 'deprecated'
397
+ ? input.lifecycle
398
+ : 'draft';
227
399
  const tags = normalizeTags([...(input.tags ?? []), audience ? `audience:${slugify(audience)}` : '']);
228
400
  const selectedIds = Array.from(new Set((input.selectedBlockIds ?? []).map(cleanString).filter(Boolean)));
229
401
  const blocks = collectBlockCandidates(projectRoot);
@@ -235,7 +407,12 @@ export function createAppPackage(projectRoot, input) {
235
407
  id,
236
408
  name,
237
409
  description: cleanString(input.purpose) || `${name} consumption surface for ${domain}`,
410
+ visibility,
238
411
  domain,
412
+ subdomain: subdomain || undefined,
413
+ groups,
414
+ audience: audience || undefined,
415
+ lifecycle,
239
416
  owners: [owner],
240
417
  tags,
241
418
  members: [
@@ -283,6 +460,11 @@ export function createAppPackage(projectRoot, input) {
283
460
  title: `${name} Overview`,
284
461
  description: cleanString(input.purpose) || `Starter dashboard for ${name}`,
285
462
  domain,
463
+ subdomain: subdomain || undefined,
464
+ groups,
465
+ audience: audience || undefined,
466
+ visibility,
467
+ lifecycle,
286
468
  tags,
287
469
  },
288
470
  layout: {
@@ -374,10 +556,26 @@ function normalizeVizType(chartType) {
374
556
  return 'line';
375
557
  if (normalized === 'bar')
376
558
  return 'bar';
559
+ if (normalized === 'grouped_bar')
560
+ return 'grouped_bar';
561
+ if (normalized === 'stacked_bar')
562
+ return 'stacked_bar';
377
563
  if (normalized === 'area')
378
564
  return 'area';
379
565
  if (normalized === 'pie')
380
566
  return 'pie';
567
+ if (normalized === 'donut')
568
+ return 'donut';
569
+ if (normalized === 'scatter')
570
+ return 'scatter';
571
+ if (normalized === 'heatmap')
572
+ return 'heatmap';
573
+ if (normalized === 'histogram')
574
+ return 'histogram';
575
+ if (normalized === 'waterfall')
576
+ return 'waterfall';
577
+ if (normalized === 'gauge')
578
+ return 'gauge';
381
579
  if (normalized === 'pivot')
382
580
  return 'pivot';
383
581
  if (normalized === 'map')
@@ -393,9 +591,15 @@ function appReadme(app, audience, blocks) {
393
591
  app.description ?? '',
394
592
  '',
395
593
  `- Domain: ${app.domain}`,
594
+ ...(app.subdomain ? [`- Subdomain: ${app.subdomain}`] : []),
595
+ ...(app.groups?.length ? [`- Groups: ${app.groups.join(', ')}`] : []),
396
596
  `- Audience: ${audience || 'not specified'}`,
597
+ `- Visibility: ${app.visibility}`,
598
+ `- Lifecycle: ${app.lifecycle}`,
397
599
  `- Owners: ${app.owners.join(', ')}`,
398
600
  `- Starter dashboard: dashboards/overview.dqld`,
601
+ `- Supporting notebooks: notebooks/`,
602
+ `- Draft blocks: drafts/`,
399
603
  '',
400
604
  '## Selected Certified Blocks',
401
605
  '',
@@ -486,18 +690,213 @@ function cleanString(value) {
486
690
  function normalizeTags(values) {
487
691
  return Array.from(new Set(values.map((value) => cleanString(value)).filter(Boolean)));
488
692
  }
693
+ function unique(values) {
694
+ return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a.localeCompare(b));
695
+ }
489
696
  function slugify(value) {
490
697
  return value
491
698
  .toLowerCase()
492
699
  .replace(/[^a-z0-9]+/g, '-')
493
700
  .replace(/^-+|-+$/g, '');
494
701
  }
702
+ function titleFromPath(path) {
703
+ return basename(path)
704
+ .replace(/\.(dqlnb|dql)$/i, '')
705
+ .replace(/[_-]+/g, ' ')
706
+ .replace(/\s+/g, ' ')
707
+ .trim()
708
+ .replace(/\b\w/g, (char) => char.toUpperCase()) || path;
709
+ }
495
710
  function audienceFromTags(tags) {
496
711
  const tag = tags.find((value) => value.startsWith('audience:'));
497
712
  if (!tag)
498
713
  return undefined;
499
714
  return tag.slice('audience:'.length).replace(/-/g, ' ');
500
715
  }
716
+ function appAllowsExecute(app, domain) {
717
+ return (app.policies ?? []).some((policy) => {
718
+ if (policy.enabled === false)
719
+ return false;
720
+ if (policy.domain !== '*' && policy.domain !== domain)
721
+ return false;
722
+ return policy.accessLevel === 'execute' || policy.accessLevel === 'admin';
723
+ });
724
+ }
725
+ function createDashboardForApp(projectRoot, appId, input) {
726
+ const loaded = loadAppById(projectRoot, appId);
727
+ if (!loaded)
728
+ return { ok: false, error: `App "${appId}" not found` };
729
+ const title = cleanString(input.title) || 'New tab';
730
+ const id = slugify(cleanString(input.id) || title) || `tab-${Date.now()}`;
731
+ if (!/^[a-z0-9][a-z0-9_-]*$/i.test(id))
732
+ return { ok: false, error: 'dashboard id must be folder-safe' };
733
+ const appDir = join(projectRoot, 'apps', appId);
734
+ const dashboardPath = join(appDir, 'dashboards', `${id}.dqld`);
735
+ if (existsSync(dashboardPath))
736
+ return { ok: false, error: `Dashboard already exists: ${id}` };
737
+ const dashboard = {
738
+ version: 1,
739
+ id,
740
+ metadata: {
741
+ title,
742
+ description: cleanString(input.description) || `${title} dashboard tab`,
743
+ domain: loaded.app.domain,
744
+ subdomain: loaded.app.subdomain,
745
+ groups: loaded.app.groups ?? [],
746
+ audience: loaded.app.audience,
747
+ visibility: loaded.app.visibility ?? 'shared',
748
+ lifecycle: loaded.app.lifecycle ?? 'draft',
749
+ tags: loaded.app.tags ?? [],
750
+ },
751
+ layout: {
752
+ kind: 'grid',
753
+ cols: 12,
754
+ rowHeight: 80,
755
+ items: [],
756
+ },
757
+ };
758
+ mkdirSync(dirname(dashboardPath), { recursive: true });
759
+ writeFileSync(dashboardPath, JSON.stringify(dashboard, null, 2) + '\n', 'utf-8');
760
+ return { ok: true, dashboard, path: relative(projectRoot, dashboardPath) };
761
+ }
762
+ function patchDashboardLayout(projectRoot, appId, dashboardId, input) {
763
+ const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
764
+ if (!loaded)
765
+ return { ok: false, error: `Dashboard "${dashboardId}" not found in app "${appId}"` };
766
+ const next = {
767
+ ...loaded.dashboard,
768
+ layout: input.layout
769
+ ? input.layout
770
+ : {
771
+ ...loaded.dashboard.layout,
772
+ items: input.items ?? loaded.dashboard.layout.items,
773
+ },
774
+ };
775
+ const written = writeDashboard(projectRoot, appId, dashboardId, next);
776
+ if (!written.ok)
777
+ return written;
778
+ return { ok: true, dashboard: next, path: relative(projectRoot, written.path) };
779
+ }
780
+ function createAiPinTile(projectRoot, appId, input) {
781
+ const dashboardId = cleanString(input.dashboardId);
782
+ if (!dashboardId)
783
+ return { ok: false, error: 'dashboardId is required' };
784
+ const loaded = loadDashboardForApp(projectRoot, appId, dashboardId);
785
+ if (!loaded)
786
+ return { ok: false, error: `Dashboard "${dashboardId}" not found in app "${appId}"` };
787
+ const title = cleanString(input.title) || 'AI result';
788
+ const tileId = cleanString(input.tileId) || nextTileId(loaded.dashboard, slugify(title) || 'ai-pin');
789
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
790
+ try {
791
+ const pin = storage.createAiPin({
792
+ appId,
793
+ dashboardId,
794
+ tileId,
795
+ title,
796
+ answer: cleanString(input.answer) || title,
797
+ sql: cleanString(input.sql) || undefined,
798
+ sourceTier: cleanString(input.sourceTier) || undefined,
799
+ certification: input.certification === 'certified' ? 'certified' : 'ai_generated',
800
+ reviewStatus: input.reviewStatus,
801
+ refreshCadence: input.refreshCadence === 'daily' ? 'daily' : 'none',
802
+ chartConfig: input.chartConfig,
803
+ result: input.result,
804
+ citations: Array.isArray(input.citations) ? input.citations : [],
805
+ });
806
+ const tile = {
807
+ i: tileId,
808
+ ...nextTilePosition(loaded.dashboard),
809
+ aiPin: { id: pin.id },
810
+ viz: { type: normalizeVizTypeFromChart(input.chartConfig) },
811
+ title,
812
+ };
813
+ const dashboard = {
814
+ ...loaded.dashboard,
815
+ layout: {
816
+ ...loaded.dashboard.layout,
817
+ items: [...loaded.dashboard.layout.items, tile],
818
+ },
819
+ };
820
+ const written = writeDashboard(projectRoot, appId, dashboardId, dashboard);
821
+ if (!written.ok) {
822
+ return { ok: false, error: written.error };
823
+ }
824
+ return { ok: true, pin, dashboard, tile };
825
+ }
826
+ finally {
827
+ storage.close();
828
+ }
829
+ }
830
+ function promoteAiPinToDraftBlock(projectRoot, appId, pinId) {
831
+ const loaded = loadAppById(projectRoot, appId);
832
+ if (!loaded)
833
+ return { ok: false, error: `App "${appId}" not found` };
834
+ const storage = new LocalAppStorage(defaultLocalAppsDbPath(projectRoot));
835
+ try {
836
+ const pin = storage.getAiPin(pinId);
837
+ if (!pin)
838
+ return { ok: false, error: `AI pin "${pinId}" not found` };
839
+ if (!pin.sql)
840
+ return { ok: false, error: 'AI pin has no SQL to promote' };
841
+ const blockName = slugify(pin.title) || pin.id;
842
+ const draftDir = join(projectRoot, 'apps', appId, 'drafts');
843
+ const blockPath = join(draftDir, `${blockName}.dql`);
844
+ mkdirSync(draftDir, { recursive: true });
845
+ const source = [
846
+ `block "${blockName}" {`,
847
+ ` domain = "${escapeDqlString(loaded.app.domain)}"`,
848
+ ' type = "custom"',
849
+ ' status = "review"',
850
+ ` owner = "${escapeDqlString(loaded.app.owners[0] ?? `${process.env.USER ?? 'analyst'}@local`)}"`,
851
+ ` description = "${escapeDqlString(pin.answer.slice(0, 240))}"`,
852
+ ' tags = ["ai-generated", "needs-review"]',
853
+ '',
854
+ ' query = """',
855
+ pin.sql,
856
+ ' """',
857
+ '',
858
+ ' visualization {',
859
+ ` chart = "${escapeDqlString(String(pin.chartConfig?.chart ?? 'table'))}"`,
860
+ ' }',
861
+ '}',
862
+ '',
863
+ ].join('\n');
864
+ writeFileSync(blockPath, source, 'utf-8');
865
+ const updated = storage.markAiPinPromoted(pinId, relative(projectRoot, blockPath));
866
+ return { ok: true, pin: updated, blockPath: relative(projectRoot, blockPath) };
867
+ }
868
+ finally {
869
+ storage.close();
870
+ }
871
+ }
872
+ function nextTilePosition(dashboard) {
873
+ const maxY = dashboard.layout.items.reduce((value, item) => Math.max(value, item.y + item.h), 0);
874
+ return { x: 0, y: maxY, w: 6, h: 3 };
875
+ }
876
+ function nextTileId(dashboard, base) {
877
+ const used = new Set(dashboard.layout.items.map((item) => item.i));
878
+ if (!used.has(base))
879
+ return base;
880
+ for (let i = 2; i < 1000; i++) {
881
+ const candidate = `${base}-${i}`;
882
+ if (!used.has(candidate))
883
+ return candidate;
884
+ }
885
+ return `${base}-${Date.now()}`;
886
+ }
887
+ function normalizeVizTypeFromChart(chartConfig) {
888
+ const chart = String(chartConfig?.chart ?? '').toLowerCase().replace(/-/g, '_');
889
+ if (chart === 'single_value' || chart === 'kpi' || chart === 'line' || chart === 'bar' || chart === 'area'
890
+ || chart === 'grouped_bar' || chart === 'stacked_bar' || chart === 'pie' || chart === 'donut'
891
+ || chart === 'scatter' || chart === 'heatmap' || chart === 'histogram' || chart === 'waterfall'
892
+ || chart === 'gauge' || chart === 'pivot' || chart === 'map' || chart === 'funnel') {
893
+ return chart;
894
+ }
895
+ return 'table';
896
+ }
897
+ function escapeDqlString(value) {
898
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ');
899
+ }
501
900
  function loadAppById(projectRoot, id) {
502
901
  for (const p of findAppDocuments(projectRoot)) {
503
902
  const { document } = loadAppDocument(p);
@@ -516,10 +915,115 @@ function loadAppById(projectRoot, id) {
516
915
  });
517
916
  }
518
917
  }
519
- return { app: document, dashboards };
918
+ return {
919
+ app: document,
920
+ dashboards,
921
+ notebooks: listAppNotebookRefs(projectRoot, document, appDir),
922
+ drafts: listAppDrafts(projectRoot, appDir),
923
+ aiPins: listAiPins(projectRoot, document.id),
924
+ };
520
925
  }
521
926
  return null;
522
927
  }
928
+ function attachNotebookToApp(projectRoot, appId, input) {
929
+ const notebookPath = cleanString(input.path).replaceAll('\\', '/');
930
+ if (!notebookPath)
931
+ return { ok: false, error: 'path is required' };
932
+ if (notebookPath.startsWith('/') || notebookPath.includes('..')) {
933
+ return { ok: false, error: 'notebook path must be project-relative' };
934
+ }
935
+ if (!existsSync(join(projectRoot, notebookPath))) {
936
+ return { ok: false, error: `Notebook not found: ${notebookPath}` };
937
+ }
938
+ for (const appJsonPath of findAppDocuments(projectRoot)) {
939
+ const { document } = loadAppDocument(appJsonPath);
940
+ if (!document || document.id !== appId)
941
+ continue;
942
+ const role = input.role === 'source' || input.role === 'analysis' ? input.role : 'supporting';
943
+ const visibility = input.visibility === 'private' || input.visibility === 'template' ? input.visibility : 'shared';
944
+ const next = {
945
+ ...document,
946
+ notebooks: [
947
+ ...(document.notebooks ?? []).filter((notebook) => notebook.path !== notebookPath),
948
+ {
949
+ path: notebookPath,
950
+ title: cleanString(input.title) || titleFromPath(notebookPath),
951
+ role,
952
+ visibility,
953
+ },
954
+ ],
955
+ };
956
+ const { document: validated, errors } = parseAppDocument(JSON.stringify(next), appJsonPath);
957
+ if (!validated)
958
+ return { ok: false, error: errors.map((e) => e.message).join('; ') };
959
+ writeFileSync(appJsonPath, JSON.stringify(validated, null, 2) + '\n', 'utf-8');
960
+ return { ok: true, path: relative(projectRoot, appJsonPath) };
961
+ }
962
+ return { ok: false, error: `App "${appId}" not found` };
963
+ }
964
+ function listAppNotebookRefs(projectRoot, app, appDir) {
965
+ const byPath = new Map();
966
+ for (const notebook of app.notebooks ?? []) {
967
+ byPath.set(notebook.path, {
968
+ path: notebook.path,
969
+ title: notebook.title,
970
+ role: notebook.role,
971
+ visibility: notebook.visibility ?? 'shared',
972
+ });
973
+ }
974
+ const notebooksDir = join(appDir, 'notebooks');
975
+ for (const file of scanFiles(notebooksDir, '.dqlnb')) {
976
+ const rel = relative(projectRoot, file).replaceAll('\\', '/');
977
+ if (byPath.has(rel))
978
+ continue;
979
+ byPath.set(rel, {
980
+ path: rel,
981
+ title: titleFromPath(rel),
982
+ role: 'supporting',
983
+ visibility: app.visibility ?? 'shared',
984
+ });
985
+ }
986
+ return Array.from(byPath.values()).sort((a, b) => (a.title ?? a.path).localeCompare(b.title ?? b.path));
987
+ }
988
+ function listAppDrafts(projectRoot, appDir) {
989
+ return scanFiles(join(appDir, 'drafts'), '.dql').map((file) => {
990
+ const source = readFileSync(file, 'utf-8');
991
+ const path = relative(projectRoot, file).replaceAll('\\', '/');
992
+ return {
993
+ path,
994
+ name: matchString(source, /block\s+"([^"]+)"/) ?? titleFromPath(path),
995
+ reviewStatus: matchString(source, /status\s*=\s*"([^"]+)"/) ?? 'review',
996
+ };
997
+ }).sort((a, b) => a.name.localeCompare(b.name));
998
+ }
999
+ function scanFiles(root, extension) {
1000
+ if (!existsSync(root))
1001
+ return [];
1002
+ const out = [];
1003
+ for (const entry of readdirSyncSafe(root)) {
1004
+ const full = join(root, entry.name);
1005
+ if (entry.isDirectory())
1006
+ out.push(...scanFiles(full, extension));
1007
+ else if (entry.isFile() && entry.name.endsWith(extension))
1008
+ out.push(full);
1009
+ }
1010
+ return out.sort();
1011
+ }
1012
+ function countAiPins(projectRoot, appId) {
1013
+ return listAiPins(projectRoot, appId).length;
1014
+ }
1015
+ function listAiPins(projectRoot, appId) {
1016
+ const dbPath = defaultLocalAppsDbPath(projectRoot);
1017
+ if (!existsSync(dbPath))
1018
+ return [];
1019
+ const storage = new LocalAppStorage(dbPath);
1020
+ try {
1021
+ return storage.listAiPins(appId);
1022
+ }
1023
+ finally {
1024
+ storage.close();
1025
+ }
1026
+ }
523
1027
  function listDashboardsFor(projectRoot, id) {
524
1028
  const result = loadAppById(projectRoot, id);
525
1029
  return result?.dashboards ?? null;
@@ -539,7 +1043,7 @@ function loadDashboardForApp(projectRoot, appId, dashboardId) {
539
1043
  }
540
1044
  return null;
541
1045
  }
542
- async function writeDashboard(projectRoot, appId, dashboardId, payload) {
1046
+ function writeDashboard(projectRoot, appId, dashboardId, payload) {
543
1047
  // Validate against the dashboard schema before touching disk.
544
1048
  const { document, errors } = parseDashboardDocument(JSON.stringify(payload), '<incoming>');
545
1049
  if (!document) {