@synapta/skills 0.1.1 → 0.2.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 (354) hide show
  1. package/dist/index.js +11 -4
  2. package/package.json +3 -4
  3. package/skills/ATTRIBUTION.md +80 -0
  4. package/skills/accessibility-audit/SKILL.md +325 -0
  5. package/skills/accessibility-audit/reference/wcag-checklist.md +103 -0
  6. package/skills/apns-notifier/SKILL.md +86 -0
  7. package/skills/approval-policy-enforcer/SKILL.md +66 -0
  8. package/skills/apps-sdk-builder/LICENSE.txt +201 -0
  9. package/skills/apps-sdk-builder/SKILL.md +328 -0
  10. package/skills/apps-sdk-builder/agents/openai.yaml +13 -0
  11. package/skills/apps-sdk-builder/references/app-archetypes.md +132 -0
  12. package/skills/apps-sdk-builder/references/apps-sdk-docs-workflow.md +135 -0
  13. package/skills/apps-sdk-builder/references/interactive-state-sync-patterns.md +113 -0
  14. package/skills/apps-sdk-builder/references/repo-contract-and-validation.md +93 -0
  15. package/skills/apps-sdk-builder/references/search-fetch-standard.md +67 -0
  16. package/skills/apps-sdk-builder/references/upstream-example-workflow.md +79 -0
  17. package/skills/apps-sdk-builder/references/window-openai-patterns.md +79 -0
  18. package/skills/apps-sdk-builder/scripts/scaffold_node_ext_apps.mjs +606 -0
  19. package/skills/architecture-selector/SKILL.md +64 -0
  20. package/skills/backlog-planner/SKILL.md +68 -0
  21. package/skills/carplay-entitlement-checker/SKILL.md +82 -0
  22. package/skills/concept-deepener/SKILL.md +86 -0
  23. package/skills/concept-discovery/SKILL.md +517 -0
  24. package/skills/concept-discovery/assets/sample-analysis.json +81 -0
  25. package/skills/concept-discovery/expected_outputs/sample-enum-dictionary.md +25 -0
  26. package/skills/concept-discovery/expected_outputs/sample-page-user-list.md +83 -0
  27. package/skills/concept-discovery/expected_outputs/sample-prd-readme.md +43 -0
  28. package/skills/concept-discovery/references/framework-patterns.md +228 -0
  29. package/skills/concept-discovery/references/prd-quality-checklist.md +65 -0
  30. package/skills/concept-discovery/scripts/codebase_analyzer.py +732 -0
  31. package/skills/concept-discovery/scripts/prd_scaffolder.py +435 -0
  32. package/skills/dast-zap/SKILL.md +453 -0
  33. package/skills/dast-zap/assets/.gitkeep +9 -0
  34. package/skills/dast-zap/assets/github_action.yml +207 -0
  35. package/skills/dast-zap/assets/gitlab_ci.yml +226 -0
  36. package/skills/dast-zap/assets/zap_automation.yaml +196 -0
  37. package/skills/dast-zap/assets/zap_context.xml +192 -0
  38. package/skills/dast-zap/references/EXAMPLE.md +40 -0
  39. package/skills/dast-zap/references/api_testing_guide.md +475 -0
  40. package/skills/dast-zap/references/authentication_guide.md +431 -0
  41. package/skills/dast-zap/references/false_positive_handling.md +427 -0
  42. package/skills/dast-zap/references/owasp_mapping.md +255 -0
  43. package/skills/dep-sbom-scan/SKILL.md +466 -0
  44. package/skills/deploy-cloudflare/SKILL.md +930 -0
  45. package/skills/deploy-docker/SKILL.md +55 -0
  46. package/skills/deploy-fly/SKILL.md +228 -0
  47. package/skills/deploy-k8s/SKILL.md +108 -0
  48. package/skills/deploy-k8s/assets/logo.png +0 -0
  49. package/skills/deploy-k8s/docs/README.md +29 -0
  50. package/skills/deploy-k8s/docs/SUMMARY.md +56 -0
  51. package/skills/deploy-k8s/docs/advanced/token-efficiency.md +61 -0
  52. package/skills/deploy-k8s/docs/architecture/multi-tenancy.md +96 -0
  53. package/skills/deploy-k8s/docs/architecture/storage-and-state.md +102 -0
  54. package/skills/deploy-k8s/docs/architecture/workload-patterns.md +87 -0
  55. package/skills/deploy-k8s/docs/book.json +16 -0
  56. package/skills/deploy-k8s/docs/community/changelog.md +34 -0
  57. package/skills/deploy-k8s/docs/community/contributing.md +67 -0
  58. package/skills/deploy-k8s/docs/core-concepts/failure-modes.md +153 -0
  59. package/skills/deploy-k8s/docs/core-concepts/philosophy.md +83 -0
  60. package/skills/deploy-k8s/docs/core-concepts/workflow.md +124 -0
  61. package/skills/deploy-k8s/docs/examples/bad-patterns.md +47 -0
  62. package/skills/deploy-k8s/docs/examples/do-dont-checklist.md +37 -0
  63. package/skills/deploy-k8s/docs/examples/good-patterns.md +49 -0
  64. package/skills/deploy-k8s/docs/failure-modes/api-drift.md +104 -0
  65. package/skills/deploy-k8s/docs/failure-modes/fragile-rollouts.md +99 -0
  66. package/skills/deploy-k8s/docs/failure-modes/insecure-workload-defaults.md +80 -0
  67. package/skills/deploy-k8s/docs/failure-modes/network-exposure.md +98 -0
  68. package/skills/deploy-k8s/docs/failure-modes/privilege-sprawl.md +91 -0
  69. package/skills/deploy-k8s/docs/failure-modes/resource-starvation.md +85 -0
  70. package/skills/deploy-k8s/docs/getting-started/installation.md +152 -0
  71. package/skills/deploy-k8s/docs/getting-started/quick-start.md +115 -0
  72. package/skills/deploy-k8s/docs/guides/helm-patterns.md +71 -0
  73. package/skills/deploy-k8s/docs/guides/kustomize-patterns.md +65 -0
  74. package/skills/deploy-k8s/docs/guides/observability.md +67 -0
  75. package/skills/deploy-k8s/docs/guides/security-hardening.md +59 -0
  76. package/skills/deploy-k8s/docs/guides/validation-and-policy.md +66 -0
  77. package/skills/deploy-k8s/docs/integrations/mcp-integration.md +52 -0
  78. package/skills/deploy-k8s/docs/package-lock.json +2892 -0
  79. package/skills/deploy-k8s/docs/package.json +13 -0
  80. package/skills/deploy-k8s/references/api-drift.md +298 -0
  81. package/skills/deploy-k8s/references/conditional/aks-patterns.md +70 -0
  82. package/skills/deploy-k8s/references/conditional/eks-patterns.md +79 -0
  83. package/skills/deploy-k8s/references/conditional/gitops-controllers.md +71 -0
  84. package/skills/deploy-k8s/references/conditional/gke-patterns.md +74 -0
  85. package/skills/deploy-k8s/references/conditional/observability-stacks.md +80 -0
  86. package/skills/deploy-k8s/references/conditional/openshift-patterns.md +67 -0
  87. package/skills/deploy-k8s/references/daemonset-operator-patterns.md +155 -0
  88. package/skills/deploy-k8s/references/deployment-patterns.md +146 -0
  89. package/skills/deploy-k8s/references/do-dont-patterns.md +87 -0
  90. package/skills/deploy-k8s/references/examples-bad.md +282 -0
  91. package/skills/deploy-k8s/references/examples-good.md +440 -0
  92. package/skills/deploy-k8s/references/fragile-rollouts.md +303 -0
  93. package/skills/deploy-k8s/references/helm-patterns.md +203 -0
  94. package/skills/deploy-k8s/references/insecure-workload-defaults.md +300 -0
  95. package/skills/deploy-k8s/references/job-patterns.md +120 -0
  96. package/skills/deploy-k8s/references/kustomize-patterns.md +239 -0
  97. package/skills/deploy-k8s/references/multi-tenancy.md +343 -0
  98. package/skills/deploy-k8s/references/network-exposure.md +481 -0
  99. package/skills/deploy-k8s/references/observability.md +302 -0
  100. package/skills/deploy-k8s/references/privilege-sprawl.md +273 -0
  101. package/skills/deploy-k8s/references/resource-starvation.md +374 -0
  102. package/skills/deploy-k8s/references/security-hardening.md +209 -0
  103. package/skills/deploy-k8s/references/stateful-patterns.md +130 -0
  104. package/skills/deploy-k8s/references/storage-and-state.md +330 -0
  105. package/skills/deploy-k8s/references/validation-and-policy.md +242 -0
  106. package/skills/deploy-railway/SKILL.md +235 -0
  107. package/skills/deploy-railway/references/analyze-db-mongo.md +84 -0
  108. package/skills/deploy-railway/references/analyze-db-mysql.md +254 -0
  109. package/skills/deploy-railway/references/analyze-db-postgres.md +479 -0
  110. package/skills/deploy-railway/references/analyze-db-redis.md +208 -0
  111. package/skills/deploy-railway/references/analyze-db.md +344 -0
  112. package/skills/deploy-railway/references/configure.md +309 -0
  113. package/skills/deploy-railway/references/deploy.md +195 -0
  114. package/skills/deploy-railway/references/operate.md +214 -0
  115. package/skills/deploy-railway/references/request.md +248 -0
  116. package/skills/deploy-railway/references/setup.md +312 -0
  117. package/skills/deploy-railway/scripts/analyze-mongo.py +1549 -0
  118. package/skills/deploy-railway/scripts/analyze-mysql.py +1195 -0
  119. package/skills/deploy-railway/scripts/analyze-postgres.py +3058 -0
  120. package/skills/deploy-railway/scripts/analyze-redis.py +1090 -0
  121. package/skills/deploy-railway/scripts/dal.py +671 -0
  122. package/skills/deploy-railway/scripts/enable-pg-stats.py +170 -0
  123. package/skills/deploy-railway/scripts/pg-extensions.py +370 -0
  124. package/skills/deploy-railway/scripts/railway-api.sh +52 -0
  125. package/skills/deploy-ssh/SKILL.md +91 -0
  126. package/skills/deploy-vercel/SKILL.md +304 -0
  127. package/skills/deploy-vercel/resources/deploy-codex.sh +301 -0
  128. package/skills/deploy-vercel/resources/deploy.sh +301 -0
  129. package/skills/docs-runbooks/SKILL.md +399 -0
  130. package/skills/drive-status-renderer/SKILL.md +62 -0
  131. package/skills/iac-scan/SKILL.md +680 -0
  132. package/skills/iac-scan/assets/.gitkeep +9 -0
  133. package/skills/iac-scan/assets/checkov_config.yaml +94 -0
  134. package/skills/iac-scan/assets/github_actions.yml +199 -0
  135. package/skills/iac-scan/assets/gitlab_ci.yml +218 -0
  136. package/skills/iac-scan/assets/pre_commit_config.yaml +92 -0
  137. package/skills/iac-scan/references/EXAMPLE.md +40 -0
  138. package/skills/iac-scan/references/compliance_mapping.md +237 -0
  139. package/skills/iac-scan/references/custom_policies.md +460 -0
  140. package/skills/iac-scan/references/suppression_guide.md +431 -0
  141. package/skills/incident-briefing/SKILL.md +66 -0
  142. package/skills/incident-triage/SKILL.md +481 -0
  143. package/{LICENSE → skills/mcp-builder/LICENSE.txt} +15 -14
  144. package/skills/mcp-builder/SKILL.md +244 -0
  145. package/skills/mcp-builder/reference/evaluation.md +602 -0
  146. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  147. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  148. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  149. package/skills/mcp-builder/scripts/connections.py +151 -0
  150. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  151. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  152. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  153. package/skills/mobile-pairing/SKILL.md +52 -0
  154. package/skills/ops-sre/SKILL.md +297 -0
  155. package/skills/playwright-qa/LICENSE.txt +201 -0
  156. package/skills/playwright-qa/NOTICE.txt +14 -0
  157. package/skills/playwright-qa/SKILL.md +156 -0
  158. package/skills/playwright-qa/agents/openai.yaml +6 -0
  159. package/skills/playwright-qa/assets/playwright-small.svg +3 -0
  160. package/skills/playwright-qa/assets/playwright.png +0 -0
  161. package/skills/playwright-qa/references/cli.md +116 -0
  162. package/skills/playwright-qa/references/workflows.md +95 -0
  163. package/skills/playwright-qa/scripts/playwright_cli.sh +25 -0
  164. package/skills/release-publish/SKILL.md +85 -0
  165. package/skills/repo-bootstrap/SKILL.md +92 -0
  166. package/skills/repo-bootstrap/assets/example-workflows/validate-agents.yml +89 -0
  167. package/skills/repo-bootstrap/assets/root-thin.md +141 -0
  168. package/skills/repo-bootstrap/assets/root-verbose.md +149 -0
  169. package/skills/repo-bootstrap/assets/scoped/backend-go.md +107 -0
  170. package/skills/repo-bootstrap/assets/scoped/backend-php.md +94 -0
  171. package/skills/repo-bootstrap/assets/scoped/backend-python.md +84 -0
  172. package/skills/repo-bootstrap/assets/scoped/backend-typescript.md +89 -0
  173. package/skills/repo-bootstrap/assets/scoped/claude-code-skill.md +101 -0
  174. package/skills/repo-bootstrap/assets/scoped/cli.md +83 -0
  175. package/skills/repo-bootstrap/assets/scoped/concourse.md +196 -0
  176. package/skills/repo-bootstrap/assets/scoped/ddev.md +68 -0
  177. package/skills/repo-bootstrap/assets/scoped/docker.md +160 -0
  178. package/skills/repo-bootstrap/assets/scoped/documentation.md +98 -0
  179. package/skills/repo-bootstrap/assets/scoped/examples.md +96 -0
  180. package/skills/repo-bootstrap/assets/scoped/frontend-typescript.md +88 -0
  181. package/skills/repo-bootstrap/assets/scoped/github-actions.md +174 -0
  182. package/skills/repo-bootstrap/assets/scoped/gitlab-ci.md +174 -0
  183. package/skills/repo-bootstrap/assets/scoped/oro-bundle.md +209 -0
  184. package/skills/repo-bootstrap/assets/scoped/oro-project.md +170 -0
  185. package/skills/repo-bootstrap/assets/scoped/python-modern.md +170 -0
  186. package/skills/repo-bootstrap/assets/scoped/resources.md +96 -0
  187. package/skills/repo-bootstrap/assets/scoped/skill-repo.md +139 -0
  188. package/skills/repo-bootstrap/assets/scoped/symfony.md +168 -0
  189. package/skills/repo-bootstrap/assets/scoped/testing.md +87 -0
  190. package/skills/repo-bootstrap/assets/scoped/typo3-docs.md +103 -0
  191. package/skills/repo-bootstrap/assets/scoped/typo3-extension.md +133 -0
  192. package/skills/repo-bootstrap/assets/scoped/typo3-project.md +137 -0
  193. package/skills/repo-bootstrap/assets/scoped/typo3-testing.md +80 -0
  194. package/skills/repo-bootstrap/checkpoints.yaml +279 -0
  195. package/skills/repo-bootstrap/evals/evals.json +385 -0
  196. package/skills/repo-bootstrap/references/ai-contribution-guidelines.md +63 -0
  197. package/skills/repo-bootstrap/references/ai-tool-compatibility.md +223 -0
  198. package/skills/repo-bootstrap/references/directory-coverage.md +82 -0
  199. package/skills/repo-bootstrap/references/examples/coding-agent-cli/AGENTS.md +70 -0
  200. package/skills/repo-bootstrap/references/examples/coding-agent-cli/go.mod +3 -0
  201. package/skills/repo-bootstrap/references/examples/coding-agent-cli/scripts-AGENTS.md +389 -0
  202. package/skills/repo-bootstrap/references/examples/express-api-ts/.env.example +13 -0
  203. package/skills/repo-bootstrap/references/examples/express-api-ts/AGENTS.md +91 -0
  204. package/skills/repo-bootstrap/references/examples/express-api-ts/package.json +33 -0
  205. package/skills/repo-bootstrap/references/examples/express-api-ts/pnpm-lock.yaml +3 -0
  206. package/skills/repo-bootstrap/references/examples/express-api-ts/src/AGENTS.md +91 -0
  207. package/skills/repo-bootstrap/references/examples/express-api-ts/src/config.ts +28 -0
  208. package/skills/repo-bootstrap/references/examples/express-api-ts/src/controllers/userController.ts +74 -0
  209. package/skills/repo-bootstrap/references/examples/express-api-ts/src/index.ts +26 -0
  210. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/errorHandler.ts +45 -0
  211. package/skills/repo-bootstrap/references/examples/express-api-ts/src/middleware/requestLogger.ts +18 -0
  212. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/health.ts +18 -0
  213. package/skills/repo-bootstrap/references/examples/express-api-ts/src/routes/users.ts +13 -0
  214. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/errors.ts +40 -0
  215. package/skills/repo-bootstrap/references/examples/express-api-ts/src/utils/logger.ts +14 -0
  216. package/skills/repo-bootstrap/references/examples/express-api-ts/tsconfig.json +24 -0
  217. package/skills/repo-bootstrap/references/examples/fastapi-app/.env.example +19 -0
  218. package/skills/repo-bootstrap/references/examples/fastapi-app/AGENTS.md +92 -0
  219. package/skills/repo-bootstrap/references/examples/fastapi-app/pyproject.toml +88 -0
  220. package/skills/repo-bootstrap/references/examples/fastapi-app/src/AGENTS.md +85 -0
  221. package/skills/repo-bootstrap/references/examples/fastapi-app/src/__init__.py +3 -0
  222. package/skills/repo-bootstrap/references/examples/fastapi-app/src/config.py +49 -0
  223. package/skills/repo-bootstrap/references/examples/fastapi-app/src/main.py +66 -0
  224. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/__init__.py +13 -0
  225. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/item.py +43 -0
  226. package/skills/repo-bootstrap/references/examples/fastapi-app/src/models/user.py +40 -0
  227. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/__init__.py +5 -0
  228. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/health.py +20 -0
  229. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/items.py +61 -0
  230. package/skills/repo-bootstrap/references/examples/fastapi-app/src/routes/users.py +55 -0
  231. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/__init__.py +6 -0
  232. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/item_service.py +77 -0
  233. package/skills/repo-bootstrap/references/examples/fastapi-app/src/services/user_service.py +69 -0
  234. package/skills/repo-bootstrap/references/examples/fastapi-app/uv.lock +4 -0
  235. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/.scopes +3 -0
  236. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/AGENTS.md +86 -0
  237. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/package.json +20 -0
  238. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/admin/src/App.tsx +5 -0
  239. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/cmd/api/main.go +7 -0
  240. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/go.mod +2 -0
  241. package/skills/repo-bootstrap/references/examples/go-api-with-react-admin/main.go +7 -0
  242. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/.scopes +3 -0
  243. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/AGENTS.md +89 -0
  244. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/go.mod +2 -0
  245. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/AGENTS.md +90 -0
  246. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/package.json +17 -0
  247. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/App.tsx +1 -0
  248. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Button.tsx +1 -0
  249. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Footer.tsx +1 -0
  250. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Header.tsx +1 -0
  251. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/internal/web/src/Sidebar.tsx +1 -0
  252. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/main.go +7 -0
  253. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package-lock.json +0 -0
  254. package/skills/repo-bootstrap/references/examples/go-with-internal-web-tsx/package.json +12 -0
  255. package/skills/repo-bootstrap/references/examples/ldap-selfservice/AGENTS.md +70 -0
  256. package/skills/repo-bootstrap/references/examples/ldap-selfservice/go.mod +3 -0
  257. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-AGENTS.md +371 -0
  258. package/skills/repo-bootstrap/references/examples/ldap-selfservice/internal-web-AGENTS.md +448 -0
  259. package/skills/repo-bootstrap/references/examples/php-with-frontend/.scopes +3 -0
  260. package/skills/repo-bootstrap/references/examples/php-with-frontend/AGENTS.md +91 -0
  261. package/skills/repo-bootstrap/references/examples/php-with-frontend/composer.json +8 -0
  262. package/skills/repo-bootstrap/references/examples/php-with-frontend/package.json +15 -0
  263. package/skills/repo-bootstrap/references/examples/php-with-frontend/pnpm-lock.yaml +0 -0
  264. package/skills/repo-bootstrap/references/examples/php-with-frontend/src/Controller.php +3 -0
  265. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/AGENTS.md +92 -0
  266. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/package.json +26 -0
  267. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/App.tsx +3 -0
  268. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Button.tsx +10 -0
  269. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Footer.tsx +9 -0
  270. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/Header.tsx +9 -0
  271. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/src/main.tsx +3 -0
  272. package/skills/repo-bootstrap/references/examples/php-with-frontend/web/tsconfig.json +13 -0
  273. package/skills/repo-bootstrap/references/examples/pnpm-workspace/AGENTS.md +75 -0
  274. package/skills/repo-bootstrap/references/examples/pnpm-workspace/package.json +7 -0
  275. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/package.json +11 -0
  276. package/skills/repo-bootstrap/references/examples/pnpm-workspace/packages/web/src/index.ts +11 -0
  277. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-lock.yaml +42 -0
  278. package/skills/repo-bootstrap/references/examples/pnpm-workspace/pnpm-workspace.yaml +2 -0
  279. package/skills/repo-bootstrap/references/examples/simple-ldap-go/AGENTS.md +70 -0
  280. package/skills/repo-bootstrap/references/examples/simple-ldap-go/examples-AGENTS.md +45 -0
  281. package/skills/repo-bootstrap/references/examples/simple-ldap-go/go.mod +3 -0
  282. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/AGENTS.md +70 -0
  283. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/Classes-AGENTS.md +392 -0
  284. package/skills/repo-bootstrap/references/examples/t3x-rte-ckeditor-image/composer.json +8 -0
  285. package/skills/repo-bootstrap/references/feedback-memory-schema.md +135 -0
  286. package/skills/repo-bootstrap/references/git-hooks-setup.md +79 -0
  287. package/skills/repo-bootstrap/references/output-structure.md +124 -0
  288. package/skills/repo-bootstrap/references/scripts-guide.md +175 -0
  289. package/skills/repo-bootstrap/references/verification-guide.md +137 -0
  290. package/skills/repo-bootstrap/scripts/analyze-git-history.sh +315 -0
  291. package/skills/repo-bootstrap/scripts/check-freshness.sh +230 -0
  292. package/skills/repo-bootstrap/scripts/detect-golden-samples.sh +161 -0
  293. package/skills/repo-bootstrap/scripts/detect-heuristics.sh +93 -0
  294. package/skills/repo-bootstrap/scripts/detect-project.sh +486 -0
  295. package/skills/repo-bootstrap/scripts/detect-scopes.sh +330 -0
  296. package/skills/repo-bootstrap/scripts/detect-utilities.sh +133 -0
  297. package/skills/repo-bootstrap/scripts/extract-adrs.sh +194 -0
  298. package/skills/repo-bootstrap/scripts/extract-agent-configs.sh +331 -0
  299. package/skills/repo-bootstrap/scripts/extract-architecture-rules.sh +522 -0
  300. package/skills/repo-bootstrap/scripts/extract-ci-commands.sh +385 -0
  301. package/skills/repo-bootstrap/scripts/extract-ci-rules.sh +384 -0
  302. package/skills/repo-bootstrap/scripts/extract-commands.sh +358 -0
  303. package/skills/repo-bootstrap/scripts/extract-documentation.sh +308 -0
  304. package/skills/repo-bootstrap/scripts/extract-github-rulesets.sh +96 -0
  305. package/skills/repo-bootstrap/scripts/extract-github-settings.sh +88 -0
  306. package/skills/repo-bootstrap/scripts/extract-ide-settings.sh +228 -0
  307. package/skills/repo-bootstrap/scripts/extract-platform-files.sh +290 -0
  308. package/skills/repo-bootstrap/scripts/extract-quality-configs.sh +442 -0
  309. package/skills/repo-bootstrap/scripts/generate-agents.sh +2424 -0
  310. package/skills/repo-bootstrap/scripts/generate-file-map.sh +153 -0
  311. package/skills/repo-bootstrap/scripts/lib/config-root.sh +211 -0
  312. package/skills/repo-bootstrap/scripts/lib/summary.sh +244 -0
  313. package/skills/repo-bootstrap/scripts/lib/template.sh +397 -0
  314. package/skills/repo-bootstrap/scripts/validate-structure.sh +324 -0
  315. package/skills/repo-bootstrap/scripts/verify-commands.sh +615 -0
  316. package/skills/repo-bootstrap/scripts/verify-content.sh +302 -0
  317. package/skills/schema-api-contracts/SKILL.md +56 -0
  318. package/skills/secret-hygiene/SKILL.md +511 -0
  319. package/skills/secret-hygiene/assets/.gitkeep +9 -0
  320. package/skills/secret-hygiene/assets/config-balanced.toml +81 -0
  321. package/skills/secret-hygiene/assets/config-custom.toml +178 -0
  322. package/skills/secret-hygiene/assets/config-strict.toml +48 -0
  323. package/skills/secret-hygiene/assets/github-action.yml +181 -0
  324. package/skills/secret-hygiene/assets/gitlab-ci.yml +257 -0
  325. package/skills/secret-hygiene/assets/precommit-config.yaml +70 -0
  326. package/skills/secret-hygiene/references/EXAMPLE.md +40 -0
  327. package/skills/secret-hygiene/references/compliance_mapping.md +538 -0
  328. package/skills/secret-hygiene/references/detection_rules.md +276 -0
  329. package/skills/secret-hygiene/references/false_positives.md +598 -0
  330. package/skills/secret-hygiene/references/remediation_guide.md +530 -0
  331. package/skills/stack-selector/SKILL.md +56 -0
  332. package/skills/telegram-control/SKILL.md +110 -0
  333. package/skills/telegram-control/references/architecture.md +184 -0
  334. package/skills/telegram-control/references/convex.md +173 -0
  335. package/skills/telegram-control/references/error_handling.md +212 -0
  336. package/skills/telegram-control/references/initial_setup.md +165 -0
  337. package/skills/telegram-control/references/telegram_api.md +156 -0
  338. package/skills/telegram-control/scripts/cancel_message.ts +53 -0
  339. package/skills/telegram-control/scripts/list_scheduled.ts +103 -0
  340. package/skills/telegram-control/scripts/logger.ts +121 -0
  341. package/skills/telegram-control/scripts/proxy-util.ts +11 -0
  342. package/skills/telegram-control/scripts/schedule_message.ts +216 -0
  343. package/skills/telegram-control/scripts/send_message.ts +115 -0
  344. package/skills/telegram-control/scripts/setup.ts +185 -0
  345. package/skills/telegram-control/scripts/types.ts +75 -0
  346. package/skills/telegram-control/scripts/view_history.ts +74 -0
  347. package/skills/test-strategy/SKILL.md +352 -0
  348. package/skills/threat-model/SKILL.md +303 -0
  349. package/skills/threat-model/examples/example-output.md +196 -0
  350. package/skills/threat-model/template.md +96 -0
  351. package/skills/ts-lint/SKILL.md +80 -0
  352. package/skills/ui-flow/SKILL.md +668 -0
  353. package/skills/voice-command-router/SKILL.md +51 -0
  354. package/skills/widget-live-activity-sync/SKILL.md +66 -0
@@ -0,0 +1,671 @@
1
+ #!/usr/bin/env python3
2
+ """Shared Railway infrastructure helpers for database analysis scripts."""
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from datetime import datetime, timezone
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+ from dataclasses import dataclass
11
+
12
+ LOG_LINES_DEFAULT = 1000 # Number of log lines to fetch via API
13
+
14
+
15
+ class ProgressTimer:
16
+ """Track elapsed time for progress messages."""
17
+ def __init__(self):
18
+ self.start_time = None
19
+ self.step_start_time = None
20
+
21
+ def start(self):
22
+ """Start the overall timer."""
23
+ self.start_time = datetime.now()
24
+ self.step_start_time = self.start_time
25
+
26
+ def step_elapsed(self) -> str:
27
+ """Get elapsed time since last step, then reset step timer."""
28
+ if self.step_start_time is None:
29
+ return ""
30
+ now = datetime.now()
31
+ elapsed = (now - self.step_start_time).total_seconds()
32
+ self.step_start_time = now
33
+ if elapsed < 0.1:
34
+ return ""
35
+ return f" ({elapsed:.1f}s)"
36
+
37
+ def total_elapsed(self) -> str:
38
+ """Get total elapsed time."""
39
+ if self.start_time is None:
40
+ return ""
41
+ elapsed = (datetime.now() - self.start_time).total_seconds()
42
+ return f" (total: {elapsed:.1f}s)"
43
+
44
+
45
+ # Global timer instance
46
+ _progress_timer = ProgressTimer()
47
+
48
+
49
+ @dataclass
50
+ class RailwayContext:
51
+ """Explicit Railway IDs that bypass railway link."""
52
+ project_id: Optional[str] = None
53
+ environment_id: Optional[str] = None
54
+ service_id: Optional[str] = None
55
+
56
+ def ssh_flags(self) -> List[str]:
57
+ """Return CLI flags for railway ssh."""
58
+ flags = ["--native"]
59
+ if self.project_id:
60
+ flags.extend(["--project", self.project_id])
61
+ if self.environment_id:
62
+ flags.extend(["--environment", self.environment_id])
63
+ if self.service_id:
64
+ flags.extend(["--service", self.service_id])
65
+ return flags
66
+
67
+ def logs_flags(self) -> List[str]:
68
+ """Return CLI flags for railway logs."""
69
+ flags = []
70
+ if self.environment_id:
71
+ flags.extend(["--environment", self.environment_id])
72
+ return flags
73
+
74
+
75
+ # Global context — set once at startup, used by all CLI calls
76
+ _ctx = RailwayContext()
77
+
78
+
79
+ def _init_context(args) -> None:
80
+ """Initialize global context from CLI args or railway config."""
81
+ global _ctx
82
+ if args.environment_id and args.service_id:
83
+ _ctx = RailwayContext(
84
+ project_id=getattr(args, 'project_id', None),
85
+ environment_id=args.environment_id,
86
+ service_id=args.service_id,
87
+ )
88
+ else:
89
+ railway_status = get_railway_status()
90
+ if railway_status:
91
+ _ctx = RailwayContext(
92
+ project_id=railway_status.get("projectId"),
93
+ environment_id=railway_status.get("environmentId"),
94
+ service_id=railway_status.get("serviceId"),
95
+ )
96
+
97
+
98
+ def progress(step: int, total: int, message: str, quiet: bool = False):
99
+ """Print progress message to stderr with elapsed time."""
100
+ if not quiet:
101
+ # Show elapsed time from previous step (before current step message)
102
+ elapsed = _progress_timer.step_elapsed()
103
+ if elapsed:
104
+ print(f" done{elapsed}", file=sys.stderr, flush=True)
105
+ print(f" [{step}/{total}] {message}", file=sys.stderr, flush=True)
106
+
107
+
108
+ def run_railway_command(args: List[str], timeout: int = 30) -> Tuple[int, str, str]:
109
+ """Run a railway CLI command and return (returncode, stdout, stderr)."""
110
+ try:
111
+ result = subprocess.run(
112
+ ["railway"] + args,
113
+ capture_output=True,
114
+ text=True,
115
+ timeout=timeout
116
+ )
117
+ return result.returncode, result.stdout, result.stderr
118
+ except subprocess.TimeoutExpired:
119
+ return 124, "", "Command timed out"
120
+ except FileNotFoundError:
121
+ return 127, "", "railway CLI not found"
122
+
123
+
124
+ def _cli_fatal_error(returncode: int, stderr: str) -> Optional[str]:
125
+ """Return a friendly error string if the CLI itself is broken, else None.
126
+
127
+ These errors are unrecoverable — retrying won't help.
128
+ """
129
+ if returncode == 127 or "railway CLI not found" in stderr:
130
+ return (
131
+ "Railway CLI not found. "
132
+ "Install it with: npm i -g @railway/cli "
133
+ "or brew install railway"
134
+ )
135
+ lower = stderr.lower()
136
+ if "unknown flag" in lower or "flag provided but not defined" in lower:
137
+ return (
138
+ "Railway CLI is outdated — the --native SSH flag is not supported. "
139
+ "Update it with: npm i -g @railway/cli@latest "
140
+ "or brew upgrade railway"
141
+ )
142
+ return None
143
+
144
+
145
+ def run_ssh_query(service: str, command: str, timeout: int = 60,
146
+ max_attempts: int = 3) -> Tuple[int, str, str]:
147
+ """Run a command via railway ssh, retrying up to max_attempts times.
148
+
149
+ Passes the command as a single argument after '--'. Railway ssh
150
+ interprets it through a shell on the remote end, so pipes, env vars,
151
+ and redirects all work without an explicit sh -c wrapper.
152
+
153
+ Retries on non-zero exit code or empty stdout (covers transient errors
154
+ like 'exec request failed on channel 0'). Never retries when the CLI
155
+ itself is missing or outdated — those errors are unrecoverable.
156
+ """
157
+ flags = _ctx.ssh_flags()
158
+ # Only pass --service <name> if context didn't already provide --service <id>
159
+ if not _ctx.service_id:
160
+ flags += ["--service", service]
161
+ args = ["ssh"] + flags + ["--", command]
162
+ last_code, last_stdout, last_stderr = 1, "", ""
163
+ for attempt in range(1, max_attempts + 1):
164
+ last_code, last_stdout, last_stderr = run_railway_command(args, timeout)
165
+ if last_code == 0 and last_stdout.strip():
166
+ return last_code, last_stdout, last_stderr
167
+ fatal = _cli_fatal_error(last_code, last_stderr)
168
+ if fatal:
169
+ return last_code, last_stdout, fatal
170
+ if attempt < max_attempts:
171
+ print(
172
+ f" SSH attempt {attempt}/{max_attempts} failed "
173
+ f"({last_stderr.strip() or 'empty response'}), retrying...",
174
+ file=sys.stderr, flush=True,
175
+ )
176
+ return last_code, last_stdout, last_stderr
177
+
178
+
179
+ def run_psql_query(service: str, query: str, timeout: int = 60) -> Tuple[int, str]:
180
+ """Run a psql query via railway ssh and return (returncode, output).
181
+
182
+ Normalizes query whitespace and suppresses psql warnings (e.g. collation
183
+ version mismatch) that would otherwise pollute stdout.
184
+ """
185
+ query = " ".join(query.split())
186
+ command = f'''PAGER='' psql $DATABASE_URL -P pager=off -t -A -c "{query}" 2>/dev/null'''
187
+ code, stdout, stderr = run_ssh_query(service, command, timeout)
188
+ if code != 0:
189
+ return code, stderr or stdout
190
+ return 0, stdout
191
+
192
+
193
+ def get_railway_status() -> Optional[Dict[str, Any]]:
194
+ """Get environment and service IDs from Railway config file.
195
+
196
+ Reads directly from ~/.railway/config.json instead of calling CLI (~15s saved).
197
+ """
198
+ config_path = os.path.expanduser("~/.railway/config.json")
199
+ if not os.path.exists(config_path):
200
+ return None
201
+
202
+ try:
203
+ with open(config_path, "r") as f:
204
+ config = json.load(f)
205
+
206
+ # Get linked project for current directory
207
+ cwd = os.getcwd()
208
+ projects = config.get("projects", {})
209
+
210
+ # Find project config for current directory or parent
211
+ project_config = None
212
+ check_path = cwd
213
+ while check_path != "/":
214
+ if check_path in projects:
215
+ project_config = projects[check_path]
216
+ break
217
+ check_path = os.path.dirname(check_path)
218
+
219
+ if not project_config:
220
+ return None
221
+
222
+ return {
223
+ "projectId": project_config.get("project"),
224
+ "environmentId": project_config.get("environment"),
225
+ "serviceId": project_config.get("service"),
226
+ "serviceName": project_config.get("name", ""),
227
+ }
228
+ except (json.JSONDecodeError, IOError):
229
+ return None
230
+
231
+
232
+ def get_deployment_status(service: str, service_id: Optional[str] = None) -> str:
233
+ """Get deployment status for service.
234
+
235
+ Uses direct API call if service_id provided (~1s), falls back to CLI (~15s).
236
+ """
237
+ # Fast path: use API directly if we have service_id
238
+ if service_id:
239
+ script_dir = os.path.dirname(os.path.abspath(__file__))
240
+ api_script = os.path.join(script_dir, "railway-api.sh")
241
+
242
+ if os.path.exists(api_script):
243
+ query = '''query svc($id: String!) {
244
+ service(id: $id) {
245
+ deployments(first: 1) {
246
+ edges { node { status } }
247
+ }
248
+ }
249
+ }'''
250
+ try:
251
+ result = subprocess.run(
252
+ [api_script, query, json.dumps({"id": service_id})],
253
+ capture_output=True,
254
+ text=True,
255
+ timeout=10
256
+ )
257
+ if result.returncode == 0:
258
+ data = json.loads(result.stdout)
259
+ edges = data.get("data", {}).get("service", {}).get("deployments", {}).get("edges", [])
260
+ if edges:
261
+ return edges[0].get("node", {}).get("status", "UNKNOWN")
262
+ except (subprocess.TimeoutExpired, json.JSONDecodeError):
263
+ pass
264
+
265
+ # Fallback: use CLI (slow, ~15s)
266
+ code, stdout, stderr = run_railway_command(
267
+ ["service", "status", "--service", service, "--json"]
268
+ )
269
+ if code != 0:
270
+ return "UNKNOWN"
271
+ try:
272
+ data = json.loads(stdout)
273
+ status = data.get("status", "UNKNOWN")
274
+ if data.get("stopped"):
275
+ return f"{status} (stopped)"
276
+ return status
277
+ except json.JSONDecodeError:
278
+ return "UNKNOWN"
279
+
280
+
281
+ def get_all_metrics_from_api(environment_id: str, service_id: str, hours: int = 24) -> Optional[Dict[str, Any]]:
282
+ """Get disk, CPU, memory, and network usage from Railway metrics API.
283
+
284
+ Fetches time-series data and computes trend analysis including
285
+ min/max/avg, spike detection, and directional trends.
286
+
287
+ Args:
288
+ hours: Hours of history to fetch (default: 24, max: 168)
289
+ """
290
+ from datetime import timedelta
291
+
292
+ start_date = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()
293
+ script_dir = os.path.dirname(os.path.abspath(__file__))
294
+ api_script = os.path.join(script_dir, "railway-api.sh")
295
+
296
+ if not os.path.exists(api_script):
297
+ return None
298
+
299
+ query = '''query metrics($environmentId: String!, $serviceId: String, $startDate: DateTime!, $measurements: [MetricMeasurement!]!) {
300
+ metrics(environmentId: $environmentId, serviceId: $serviceId, startDate: $startDate, measurements: $measurements) {
301
+ measurement values { ts value }
302
+ }
303
+ }'''
304
+
305
+ variables = json.dumps({
306
+ "environmentId": environment_id,
307
+ "serviceId": service_id,
308
+ "startDate": start_date,
309
+ "measurements": [
310
+ "DISK_USAGE_GB", "CPU_USAGE", "MEMORY_USAGE_GB",
311
+ "MEMORY_LIMIT_GB", "CPU_LIMIT",
312
+ "NETWORK_RX_GB", "NETWORK_TX_GB",
313
+ ]
314
+ })
315
+
316
+ try:
317
+ result = subprocess.run(
318
+ [api_script, query, variables],
319
+ capture_output=True,
320
+ text=True,
321
+ timeout=30
322
+ )
323
+ if result.returncode != 0:
324
+ return None
325
+
326
+ data = json.loads(result.stdout)
327
+ metrics = data.get("data", {}).get("metrics", [])
328
+
329
+ combined = {"disk_usage": None, "cpu_memory": {}, "metrics_history": None}
330
+
331
+ # Raw time series keyed by measurement name
332
+ raw_series: Dict[str, List[Dict[str, Any]]] = {}
333
+
334
+ for metric in metrics:
335
+ measurement = metric.get("measurement")
336
+ values = metric.get("values", [])
337
+ if values:
338
+ raw_series[measurement] = values
339
+ latest = values[-1].get("value", 0)
340
+ if measurement == "DISK_USAGE_GB":
341
+ combined["disk_usage"] = {
342
+ "used_gb": round(latest, 2),
343
+ "used": f"{round(latest, 1)} GB"
344
+ }
345
+ elif measurement == "CPU_USAGE":
346
+ combined["cpu_memory"]["cpu_percent"] = round(latest, 1)
347
+ elif measurement == "MEMORY_USAGE_GB":
348
+ combined["cpu_memory"]["memory_gb"] = round(latest, 2)
349
+ elif measurement == "MEMORY_LIMIT_GB":
350
+ combined["cpu_memory"]["memory_limit_gb"] = round(latest, 2)
351
+ elif measurement == "CPU_LIMIT":
352
+ combined["cpu_memory"]["cpu_limit"] = round(latest, 1)
353
+
354
+ if not combined["cpu_memory"]:
355
+ combined["cpu_memory"] = None
356
+
357
+ # Build time-series history with trend analysis
358
+ if raw_series:
359
+ combined["metrics_history"] = _build_metrics_history(raw_series, hours=hours)
360
+
361
+ return combined
362
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
363
+ pass
364
+
365
+ return None
366
+
367
+
368
+ def _analyze_window(values: List[Dict[str, Any]], nums: List[float], d: int,
369
+ unit: str) -> Dict[str, Any]:
370
+ """Analyze a single time window of metric data.
371
+
372
+ Returns summary stats, trend, spike detection, and downsampled series.
373
+ """
374
+ if not nums:
375
+ return {}
376
+
377
+ avg_val = sum(nums) / len(nums)
378
+ min_val = min(nums)
379
+ max_val = max(nums)
380
+
381
+ entry: Dict[str, Any] = {
382
+ "unit": unit,
383
+ "current": round(nums[-1], d),
384
+ "min": round(min_val, d),
385
+ "max": round(max_val, d),
386
+ "avg": round(avg_val, d),
387
+ "samples": len(nums),
388
+ }
389
+
390
+ # Trend: compare first quarter avg to last quarter avg
391
+ q_size = max(len(nums) // 4, 1)
392
+ first_q = nums[:q_size]
393
+ last_q = nums[-q_size:]
394
+ first_avg = sum(first_q) / len(first_q)
395
+ last_avg = sum(last_q) / len(last_q)
396
+
397
+ if first_avg > 0:
398
+ change_pct = round(((last_avg - first_avg) / first_avg) * 100, 1)
399
+ elif last_avg > 0:
400
+ change_pct = 100.0
401
+ else:
402
+ change_pct = 0.0
403
+
404
+ if change_pct > 10:
405
+ trend_dir = "increasing"
406
+ elif change_pct < -10:
407
+ trend_dir = "decreasing"
408
+ else:
409
+ trend_dir = "stable"
410
+
411
+ entry["trend"] = {
412
+ "direction": trend_dir,
413
+ "change_pct": change_pct,
414
+ "first_quarter_avg": round(first_avg, d),
415
+ "last_quarter_avg": round(last_avg, d),
416
+ }
417
+
418
+ # Spike detection
419
+ if len(nums) >= 10:
420
+ variance = sum((x - avg_val) ** 2 for x in nums) / len(nums)
421
+ stddev = variance ** 0.5
422
+ threshold = avg_val + 2 * stddev
423
+ if stddev > 0 and threshold > 0:
424
+ spikes = []
425
+ for v in values:
426
+ val = v.get("value")
427
+ if val is not None and val > threshold:
428
+ spikes.append({"ts": v["ts"], "value": round(val, d)})
429
+ if spikes:
430
+ entry["spikes"] = {
431
+ "count": len(spikes),
432
+ "threshold": round(threshold, d),
433
+ "peaks": spikes[:10],
434
+ }
435
+
436
+ # Compact time series: downsample to ~48 points
437
+ series_points = []
438
+ for v in values:
439
+ ts = v.get("ts")
440
+ val = v.get("value")
441
+ if ts is not None and val is not None:
442
+ series_points.append({"ts": ts, "value": round(val, d)})
443
+
444
+ if len(series_points) > 48:
445
+ step = len(series_points) / 48
446
+ downsampled = []
447
+ for i in range(48):
448
+ idx = int(i * step)
449
+ downsampled.append(series_points[idx])
450
+ downsampled.append(series_points[-1])
451
+ entry["series"] = downsampled
452
+ else:
453
+ entry["series"] = series_points
454
+
455
+ return entry
456
+
457
+
458
+ def _build_metrics_history(raw_series: Dict[str, List[Dict[str, Any]]], hours: int = 168) -> Dict[str, Any]:
459
+ """Build multi-window time-series history with trend analysis.
460
+
461
+ Always produces a full-window analysis. If the window is > 24h, also
462
+ produces a 24h short-window analysis from the tail of the data so the
463
+ LLM can compare long-term vs short-term trends.
464
+ """
465
+ from datetime import timedelta
466
+
467
+ metric_info = {
468
+ "CPU_USAGE": {"name": "cpu", "unit": "vCPU", "decimals": 2},
469
+ "MEMORY_USAGE_GB": {"name": "memory", "unit": "GB", "decimals": 2},
470
+ "MEMORY_LIMIT_GB": {"name": "memory_limit", "unit": "GB", "decimals": 2},
471
+ "CPU_LIMIT": {"name": "cpu_limit", "unit": "vCPU", "decimals": 2},
472
+ "DISK_USAGE_GB": {"name": "disk", "unit": "GB", "decimals": 2},
473
+ "NETWORK_RX_GB": {"name": "network_rx", "unit": "GB", "decimals": 3},
474
+ "NETWORK_TX_GB": {"name": "network_tx", "unit": "GB", "decimals": 3},
475
+ }
476
+
477
+ # Determine the 24h cutoff timestamp
478
+ now_ts = int(datetime.now(timezone.utc).timestamp())
479
+ cutoff_24h = now_ts - (24 * 3600)
480
+
481
+ produce_short_window = hours > 24
482
+
483
+ full_window: Dict[str, Any] = {}
484
+ short_window: Dict[str, Any] = {}
485
+
486
+ for measurement, values in raw_series.items():
487
+ info = metric_info.get(measurement)
488
+ if not info or len(values) < 2:
489
+ continue
490
+
491
+ nums = [v["value"] for v in values if v.get("value") is not None]
492
+ if not nums:
493
+ continue
494
+
495
+ d = info["decimals"]
496
+ name = info["name"]
497
+
498
+ # Full window analysis
499
+ full_window[name] = _analyze_window(values, nums, d, info["unit"])
500
+
501
+ # Short window (last 24h) analysis
502
+ if produce_short_window:
503
+ recent_values = [v for v in values if v.get("ts", 0) >= cutoff_24h]
504
+ recent_nums = [v["value"] for v in recent_values if v.get("value") is not None]
505
+ if len(recent_nums) >= 2:
506
+ short_window[name] = _analyze_window(recent_values, recent_nums, d, info["unit"])
507
+
508
+ # Build the result with named windows
509
+ windows: Dict[str, Any] = {}
510
+
511
+ # Label the full window
512
+ if hours >= 168:
513
+ full_label = "7d"
514
+ elif hours >= 72:
515
+ full_label = f"{hours // 24}d"
516
+ else:
517
+ full_label = f"{hours}h"
518
+
519
+ windows[full_label] = {
520
+ "window_hours": hours,
521
+ "metrics": full_window,
522
+ }
523
+
524
+ if produce_short_window and short_window:
525
+ windows["24h"] = {
526
+ "window_hours": 24,
527
+ "metrics": short_window,
528
+ }
529
+
530
+ return {"windows": windows}
531
+
532
+
533
+ def info(msg: str) -> None:
534
+ """Print an [INFO] message to stdout."""
535
+ print(f"[INFO] {msg}")
536
+
537
+
538
+ def error(msg: str) -> None:
539
+ """Print an [ERROR] message to stderr and exit."""
540
+ print(f"[ERROR] {msg}", file=sys.stderr)
541
+ sys.exit(1)
542
+
543
+
544
+ def confirm_with_user(prompt: str) -> bool:
545
+ """Get confirmation directly from the terminal.
546
+
547
+ Reads from /dev/tty to ensure it's an actual user at a terminal,
548
+ not piped input. This prevents automated scripts from bypassing confirmation.
549
+ """
550
+ try:
551
+ with open('/dev/tty', 'r') as tty:
552
+ print(prompt, end=' ', flush=True)
553
+ response = tty.readline().strip().lower()
554
+ return response in ('y', 'yes')
555
+ except (OSError, IOError):
556
+ print("\n[ERROR] This command requires interactive terminal confirmation.")
557
+ print("It cannot be run with piped input or in non-interactive mode.")
558
+ print("Please run this command directly in a terminal.")
559
+ return False
560
+
561
+
562
+ def _safe_int(val: Any, default: int = 0) -> int:
563
+ """Safely convert a value to int, returning default on failure."""
564
+ try:
565
+ return int(val)
566
+ except (ValueError, TypeError):
567
+ return default
568
+
569
+
570
+ def _safe_float(val: Any, default: float = 0.0) -> float:
571
+ """Safely convert a value to float, returning default on failure."""
572
+ try:
573
+ return float(val)
574
+ except (ValueError, TypeError):
575
+ return default
576
+
577
+
578
+ def _format_uptime(seconds: int) -> str:
579
+ """Format uptime seconds into a human-readable string."""
580
+ if seconds <= 0:
581
+ return "N/A"
582
+ days = seconds // 86400
583
+ hours = (seconds % 86400) // 3600
584
+ minutes = (seconds % 3600) // 60
585
+ parts = []
586
+ if days > 0:
587
+ parts.append(f"{days}d")
588
+ if hours > 0:
589
+ parts.append(f"{hours}h")
590
+ if minutes > 0 and days == 0:
591
+ parts.append(f"{minutes}m")
592
+ return " ".join(parts) if parts else "< 1m"
593
+
594
+
595
+ def _trend_indicator(metrics_history: Optional[Dict[str, Any]], metric_name: str) -> str:
596
+ """Return a compact trend string like ' (^ +15.2% 24h)' for use in summary rows."""
597
+ if not metrics_history:
598
+ return ""
599
+ windows = metrics_history.get("windows", {})
600
+ window_data = windows.get("24h") or next(iter(windows.values()), None) if windows else None
601
+ if not window_data or not window_data.get("metrics"):
602
+ return ""
603
+ m = window_data["metrics"].get(metric_name)
604
+ if not m or "trend" not in m:
605
+ return ""
606
+ t = m["trend"]
607
+ direction = t.get("direction", "stable")
608
+ change = t.get("change_pct", 0)
609
+ window_label = "24h" if "24h" in windows else next(iter(windows), "")
610
+ arrow = {"increasing": "^", "decreasing": "v", "stable": "~"}.get(direction, "")
611
+ if direction == "stable":
612
+ return f" ({arrow} stable {window_label})"
613
+ return f" ({arrow} {change:+.1f}% {window_label})"
614
+
615
+
616
+ def get_recent_logs(service: str, lines: int = LOG_LINES_DEFAULT,
617
+ environment_id: Optional[str] = None,
618
+ service_id: Optional[str] = None) -> List[str]:
619
+ """Get recent logs for LLM analysis.
620
+
621
+ Uses API if environment_id and service_id provided (~3s),
622
+ retries once with longer timeout on failure,
623
+ falls back to CLI (~27s for 100 lines).
624
+ """
625
+ # Fast path: use API directly
626
+ if environment_id and service_id:
627
+ script_dir = os.path.dirname(os.path.abspath(__file__))
628
+ api_script = os.path.join(script_dir, "railway-api.sh")
629
+
630
+ if os.path.exists(api_script):
631
+ # Use environmentLogs API with service filter
632
+ query = f'''query {{
633
+ environmentLogs(
634
+ environmentId: "{environment_id}",
635
+ beforeLimit: {lines},
636
+ filter: "@service:{service_id}"
637
+ ) {{
638
+ timestamp
639
+ message
640
+ }}
641
+ }}'''
642
+ # Try API twice: first with 15s timeout, retry with 30s
643
+ for attempt_timeout in [15, 30]:
644
+ try:
645
+ result = subprocess.run(
646
+ [api_script, query],
647
+ capture_output=True,
648
+ text=True,
649
+ timeout=attempt_timeout
650
+ )
651
+ if result.returncode == 0:
652
+ data = json.loads(result.stdout)
653
+ logs_data = data.get("data", {}).get("environmentLogs", [])
654
+ if logs_data:
655
+ return [f"{log['timestamp']} {log['message']}" for log in logs_data]
656
+ except (subprocess.TimeoutExpired, json.JSONDecodeError):
657
+ pass
658
+
659
+ # Fallback: use CLI (slow, ~27s)
660
+ code, stdout, stderr = run_railway_command(
661
+ ["logs"] + _ctx.logs_flags() + ["--service", service, "--lines", str(lines)],
662
+ timeout=30
663
+ )
664
+ if code != 0:
665
+ return []
666
+
667
+ logs = []
668
+ for line in stdout.strip().split("\n"):
669
+ if line.strip():
670
+ logs.append(line.strip())
671
+ return logs