agents-inc 0.164.0 → 0.165.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 (191) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +8 -9
  3. package/dist/{chunk-B6FSLXYN.js → chunk-42TNV2TK.js} +3 -3
  4. package/dist/chunk-4AAOOAIN.js +49 -0
  5. package/dist/chunk-4AAOOAIN.js.map +1 -0
  6. package/dist/{chunk-NQXO4LR3.js → chunk-4C7GL4F7.js} +16 -11
  7. package/dist/chunk-4C7GL4F7.js.map +1 -0
  8. package/dist/{chunk-Q4IRRVXK.js → chunk-57KU6SPH.js} +3 -3
  9. package/dist/{chunk-S63LXWMC.js → chunk-5M3PQNXI.js} +2 -2
  10. package/dist/{chunk-3WVH2CTK.js → chunk-5P26XBZO.js} +15 -8
  11. package/dist/chunk-5P26XBZO.js.map +1 -0
  12. package/dist/{chunk-G2JJ5BO5.js → chunk-6AIZPSEI.js} +337 -140
  13. package/dist/chunk-6AIZPSEI.js.map +1 -0
  14. package/dist/{chunk-NKGYKOB7.js → chunk-6CY6PW5X.js} +7 -8
  15. package/dist/chunk-6CY6PW5X.js.map +1 -0
  16. package/dist/{chunk-XHPWTL4I.js → chunk-6SW6SB7H.js} +3 -3
  17. package/dist/chunk-AAWZED5M.js +980 -0
  18. package/dist/chunk-AAWZED5M.js.map +1 -0
  19. package/dist/{chunk-UITEKY2R.js → chunk-AQXKAPCD.js} +30 -4
  20. package/dist/chunk-AQXKAPCD.js.map +1 -0
  21. package/dist/{chunk-X4SSJZLY.js → chunk-B5UUHBAM.js} +12 -261
  22. package/dist/chunk-B5UUHBAM.js.map +1 -0
  23. package/dist/{chunk-ARYNOWKF.js → chunk-BGU25XAY.js} +8 -8
  24. package/dist/{chunk-QGUP3QJF.js → chunk-CBMAO6OP.js} +22 -18
  25. package/dist/{chunk-QGUP3QJF.js.map → chunk-CBMAO6OP.js.map} +1 -1
  26. package/dist/{chunk-YRS3QZP2.js → chunk-CEBDGZID.js} +3 -3
  27. package/dist/{chunk-ABAZMMG4.js → chunk-CK5RM3DJ.js} +2 -2
  28. package/dist/{chunk-PHCPUQVR.js → chunk-CTUJO23Z.js} +2 -2
  29. package/dist/{chunk-QZX7SYKU.js → chunk-E6CHHXGY.js} +2 -2
  30. package/dist/{chunk-ZVXGW2IC.js → chunk-F2ZWFWTP.js} +4 -6
  31. package/dist/chunk-F2ZWFWTP.js.map +1 -0
  32. package/dist/{chunk-AVK2HEKB.js → chunk-GKPW33FP.js} +2 -6
  33. package/dist/{chunk-AVK2HEKB.js.map → chunk-GKPW33FP.js.map} +1 -1
  34. package/dist/{chunk-ECURHW3T.js → chunk-HR4YDSJK.js} +13 -12
  35. package/dist/chunk-HR4YDSJK.js.map +1 -0
  36. package/dist/{chunk-XPDVAJDH.js → chunk-IM45LLQX.js} +3599 -1558
  37. package/dist/chunk-IM45LLQX.js.map +1 -0
  38. package/dist/chunk-IVNKU4DM.js +39 -0
  39. package/dist/chunk-IVNKU4DM.js.map +1 -0
  40. package/dist/{chunk-XVAQG2CV.js → chunk-JOEA52NR.js} +10 -8
  41. package/dist/chunk-JOEA52NR.js.map +1 -0
  42. package/dist/{chunk-RGYDFBCT.js → chunk-JV4TKXEN.js} +5 -5
  43. package/dist/{chunk-4VL3CEOS.js → chunk-LXXGZAD3.js} +3 -3
  44. package/dist/chunk-MBUGQI2M.js +73 -0
  45. package/dist/chunk-MBUGQI2M.js.map +1 -0
  46. package/dist/{chunk-URXIQE7I.js → chunk-MWDY76MT.js} +2 -2
  47. package/dist/{chunk-V5GLL2JB.js → chunk-P3KJCYSR.js} +7 -7
  48. package/dist/{chunk-6MV4JOCL.js → chunk-PSX7CT6V.js} +2 -2
  49. package/dist/{chunk-B3Y3QMWI.js → chunk-TRTVOVHR.js} +4 -4
  50. package/dist/{chunk-AOTCIUGX.js → chunk-TV5Y53R2.js} +2 -2
  51. package/dist/{chunk-K5ITEA3I.js → chunk-UBSSIIWD.js} +34 -17
  52. package/dist/chunk-UBSSIIWD.js.map +1 -0
  53. package/dist/chunk-UCORXS4D.js +250 -0
  54. package/dist/chunk-UCORXS4D.js.map +1 -0
  55. package/dist/chunk-UR3RRTPO.js +159 -0
  56. package/dist/chunk-UR3RRTPO.js.map +1 -0
  57. package/dist/chunk-V5CWACR5.js +375 -0
  58. package/dist/chunk-V5CWACR5.js.map +1 -0
  59. package/dist/{chunk-LT5GXEB2.js → chunk-VPJUCVU7.js} +8 -7
  60. package/dist/chunk-VPJUCVU7.js.map +1 -0
  61. package/dist/chunk-VRXX4D6K.js +31 -0
  62. package/dist/chunk-VRXX4D6K.js.map +1 -0
  63. package/dist/{chunk-2CQNPLVF.js → chunk-WIFIAQZU.js} +63 -42
  64. package/dist/chunk-WIFIAQZU.js.map +1 -0
  65. package/dist/chunk-WSGX4IWC.js +356 -0
  66. package/dist/chunk-WSGX4IWC.js.map +1 -0
  67. package/dist/{chunk-X37UYIE3.js → chunk-WTANOSXK.js} +436 -739
  68. package/dist/chunk-WTANOSXK.js.map +1 -0
  69. package/dist/{chunk-XATLUKXK.js → chunk-X2KUI7V3.js} +6 -6
  70. package/dist/{chunk-NTF4UXGK.js → chunk-XATIKM3P.js} +73 -30
  71. package/dist/chunk-XATIKM3P.js.map +1 -0
  72. package/dist/{chunk-J27W6TXS.js → chunk-XQHZPINL.js} +8 -4
  73. package/dist/chunk-XQHZPINL.js.map +1 -0
  74. package/dist/{chunk-DSL66I4N.js → chunk-Y5SNGMB7.js} +6 -6
  75. package/dist/{chunk-7REKOTKG.js → chunk-YNOOZFIB.js} +3 -3
  76. package/dist/commands/build/marketplace.js +17 -17
  77. package/dist/commands/build/marketplace.js.map +1 -1
  78. package/dist/commands/build/plugins.js +11 -9
  79. package/dist/commands/build/plugins.js.map +1 -1
  80. package/dist/commands/compile.js +106 -27
  81. package/dist/commands/compile.js.map +1 -1
  82. package/dist/commands/doctor.js +427 -192
  83. package/dist/commands/doctor.js.map +1 -1
  84. package/dist/commands/edit.js +504 -314
  85. package/dist/commands/edit.js.map +1 -1
  86. package/dist/commands/eject.js +41 -23
  87. package/dist/commands/eject.js.map +1 -1
  88. package/dist/commands/init.js +44 -40
  89. package/dist/commands/list.js +44 -10
  90. package/dist/commands/list.js.map +1 -1
  91. package/dist/commands/new/marketplace.js +13 -10
  92. package/dist/commands/new/marketplace.js.map +1 -1
  93. package/dist/commands/search.js +12 -10
  94. package/dist/commands/search.js.map +1 -1
  95. package/dist/commands/share.js +103 -15
  96. package/dist/commands/share.js.map +1 -1
  97. package/dist/commands/uninstall.js +264 -106
  98. package/dist/commands/uninstall.js.map +1 -1
  99. package/dist/commands/update.js +50 -20
  100. package/dist/commands/update.js.map +1 -1
  101. package/dist/components/common/prompt-confirm.js +1 -1
  102. package/dist/components/common/removal-plan-confirm.js +2 -2
  103. package/dist/components/common/select-list.js +3 -3
  104. package/dist/components/common/spinner.js +6 -3
  105. package/dist/components/wizard/category-grid.js +5 -5
  106. package/dist/components/wizard/checkbox-grid.js +4 -4
  107. package/dist/components/wizard/domain-selection.js +9 -9
  108. package/dist/components/wizard/run-wizard-session.js +29 -29
  109. package/dist/components/wizard/scroll-affordance.js +2 -2
  110. package/dist/components/wizard/skill-agent-summary.js +5 -5
  111. package/dist/components/wizard/source-grid.js +6 -6
  112. package/dist/components/wizard/stack-selection.js +8 -8
  113. package/dist/components/wizard/step-agents.js +8 -8
  114. package/dist/components/wizard/step-build.js +8 -8
  115. package/dist/components/wizard/step-confirm.js +9 -9
  116. package/dist/components/wizard/step-sources.js +9 -9
  117. package/dist/components/wizard/step-stack.js +9 -9
  118. package/dist/components/wizard/summary-panel.js +8 -8
  119. package/dist/components/wizard/toast.js +2 -2
  120. package/dist/components/wizard/wizard-layout.js +12 -12
  121. package/dist/components/wizard/wizard-tabs.js +2 -2
  122. package/dist/components/wizard/wizard.js +27 -27
  123. package/dist/hooks/init.js +59 -43
  124. package/dist/hooks/init.js.map +1 -1
  125. package/dist/load-agent-defs-JHLE6MAE.js +14 -0
  126. package/dist/recompile-project-agents-ZPP5NOLZ.js +18 -0
  127. package/dist/{source-loader-GRBVNRAW.js → source-loader-RWZAYJ7E.js} +6 -4
  128. package/dist/src/agents/_templates/agent-body.liquid +55 -0
  129. package/dist/src/agents/_templates/agent.liquid +1 -55
  130. package/dist/src/agents/meta/agent-summoner/output.md +1 -1
  131. package/dist/src/agents/meta/agent-summoner/playbook.md +15 -7
  132. package/dist/stores/wizard-store.js +4 -4
  133. package/package.json +10 -9
  134. package/src/agents/_templates/agent-body.liquid +55 -0
  135. package/src/agents/_templates/agent.liquid +1 -55
  136. package/src/agents/meta/agent-summoner/output.md +1 -1
  137. package/src/agents/meta/agent-summoner/playbook.md +15 -7
  138. package/src/schemas/project-config.schema.json +1 -1
  139. package/src/schemas/project-source-config.schema.json +1 -1
  140. package/dist/chunk-2CQNPLVF.js.map +0 -1
  141. package/dist/chunk-3WVH2CTK.js.map +0 -1
  142. package/dist/chunk-7ANJQLPJ.js +0 -35
  143. package/dist/chunk-7ANJQLPJ.js.map +0 -1
  144. package/dist/chunk-ECURHW3T.js.map +0 -1
  145. package/dist/chunk-FLW25ORT.js +0 -130
  146. package/dist/chunk-FLW25ORT.js.map +0 -1
  147. package/dist/chunk-G2JJ5BO5.js.map +0 -1
  148. package/dist/chunk-J27W6TXS.js.map +0 -1
  149. package/dist/chunk-JCPANDNR.js +0 -35
  150. package/dist/chunk-JCPANDNR.js.map +0 -1
  151. package/dist/chunk-JIERYE6B.js +0 -480
  152. package/dist/chunk-JIERYE6B.js.map +0 -1
  153. package/dist/chunk-JU4ZGJ7A.js +0 -668
  154. package/dist/chunk-JU4ZGJ7A.js.map +0 -1
  155. package/dist/chunk-K5ITEA3I.js.map +0 -1
  156. package/dist/chunk-LT5GXEB2.js.map +0 -1
  157. package/dist/chunk-NKGYKOB7.js.map +0 -1
  158. package/dist/chunk-NQXO4LR3.js.map +0 -1
  159. package/dist/chunk-NTF4UXGK.js.map +0 -1
  160. package/dist/chunk-UITEKY2R.js.map +0 -1
  161. package/dist/chunk-X37UYIE3.js.map +0 -1
  162. package/dist/chunk-X4SSJZLY.js.map +0 -1
  163. package/dist/chunk-XPDVAJDH.js.map +0 -1
  164. package/dist/chunk-XVAQG2CV.js.map +0 -1
  165. package/dist/chunk-ZBPEFQ4G.js +0 -12
  166. package/dist/chunk-ZBPEFQ4G.js.map +0 -1
  167. package/dist/chunk-ZVXGW2IC.js.map +0 -1
  168. package/dist/load-agent-defs-7IM3GBEQ.js +0 -13
  169. package/dist/recompile-project-agents-VR4NLGOU.js +0 -17
  170. /package/dist/{chunk-B6FSLXYN.js.map → chunk-42TNV2TK.js.map} +0 -0
  171. /package/dist/{chunk-Q4IRRVXK.js.map → chunk-57KU6SPH.js.map} +0 -0
  172. /package/dist/{chunk-S63LXWMC.js.map → chunk-5M3PQNXI.js.map} +0 -0
  173. /package/dist/{chunk-XHPWTL4I.js.map → chunk-6SW6SB7H.js.map} +0 -0
  174. /package/dist/{chunk-ARYNOWKF.js.map → chunk-BGU25XAY.js.map} +0 -0
  175. /package/dist/{chunk-YRS3QZP2.js.map → chunk-CEBDGZID.js.map} +0 -0
  176. /package/dist/{chunk-ABAZMMG4.js.map → chunk-CK5RM3DJ.js.map} +0 -0
  177. /package/dist/{chunk-PHCPUQVR.js.map → chunk-CTUJO23Z.js.map} +0 -0
  178. /package/dist/{chunk-QZX7SYKU.js.map → chunk-E6CHHXGY.js.map} +0 -0
  179. /package/dist/{chunk-RGYDFBCT.js.map → chunk-JV4TKXEN.js.map} +0 -0
  180. /package/dist/{chunk-4VL3CEOS.js.map → chunk-LXXGZAD3.js.map} +0 -0
  181. /package/dist/{chunk-URXIQE7I.js.map → chunk-MWDY76MT.js.map} +0 -0
  182. /package/dist/{chunk-V5GLL2JB.js.map → chunk-P3KJCYSR.js.map} +0 -0
  183. /package/dist/{chunk-6MV4JOCL.js.map → chunk-PSX7CT6V.js.map} +0 -0
  184. /package/dist/{chunk-B3Y3QMWI.js.map → chunk-TRTVOVHR.js.map} +0 -0
  185. /package/dist/{chunk-AOTCIUGX.js.map → chunk-TV5Y53R2.js.map} +0 -0
  186. /package/dist/{chunk-XATLUKXK.js.map → chunk-X2KUI7V3.js.map} +0 -0
  187. /package/dist/{chunk-DSL66I4N.js.map → chunk-Y5SNGMB7.js.map} +0 -0
  188. /package/dist/{chunk-7REKOTKG.js.map → chunk-YNOOZFIB.js.map} +0 -0
  189. /package/dist/{load-agent-defs-7IM3GBEQ.js.map → load-agent-defs-JHLE6MAE.js.map} +0 -0
  190. /package/dist/{recompile-project-agents-VR4NLGOU.js.map → recompile-project-agents-ZPP5NOLZ.js.map} +0 -0
  191. /package/dist/{source-loader-GRBVNRAW.js.map → source-loader-RWZAYJ7E.js.map} +0 -0
@@ -1,105 +1,189 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ unofferablePlacementsFound
4
+ } from "../chunk-IVNKU4DM.js";
2
5
  import {
3
6
  detectProject,
4
7
  loadSource
5
- } from "../chunk-X37UYIE3.js";
6
- import "../chunk-2CQNPLVF.js";
7
- import "../chunk-B6FSLXYN.js";
8
+ } from "../chunk-WTANOSXK.js";
9
+ import "../chunk-WIFIAQZU.js";
10
+ import "../chunk-42TNV2TK.js";
11
+ import "../chunk-UR3RRTPO.js";
8
12
  import {
9
13
  filterExcludedEntries,
14
+ listAgentFilesOf,
10
15
  listAgentMdFiles,
11
16
  splitAgentsByProvenance
12
- } from "../chunk-JU4ZGJ7A.js";
17
+ } from "../chunk-V5CWACR5.js";
18
+ import {
19
+ ambiguityFinding
20
+ } from "../chunk-MBUGQI2M.js";
13
21
  import {
14
22
  BaseCommand,
15
23
  EXIT_CODES
16
- } from "../chunk-JIERYE6B.js";
17
- import "../chunk-NKGYKOB7.js";
18
- import "../chunk-PHCPUQVR.js";
24
+ } from "../chunk-AAWZED5M.js";
25
+ import "../chunk-6CY6PW5X.js";
26
+ import "../chunk-CTUJO23Z.js";
19
27
  import {
20
28
  ConfigDefaultExportError,
29
+ MarketplaceManifestRequiredError,
21
30
  MarketplaceNameRefusedError,
31
+ REGISTRY_IS_NOT_AN_INVENTORY,
22
32
  SOURCE_ENV_VAR,
33
+ agentCodec,
23
34
  agentYamlGenerationSchema,
35
+ bothSourceFoldersPresent,
24
36
  checkMatrixHealth,
37
+ compiledAgentNamesTheOtherFolder,
25
38
  configDirsInPlay,
26
39
  declaresNoContent,
27
40
  directoryExists,
28
41
  discoverLocalSkills,
29
42
  fileExists,
30
43
  findConfigLoadFailures,
44
+ folderBeingRead,
31
45
  formatZodErrors,
32
46
  getErrorMessage,
47
+ getInstalledConfigPath,
33
48
  getInstalledPluginsRegistryPath,
34
- getProjectConfigPath,
35
49
  getProjectPluginsDir,
36
50
  getStackSkillIds,
37
51
  getUserPluginsDir,
38
52
  getVerifiedPluginInstallPaths,
39
53
  glob,
54
+ hostCompilesAgent,
40
55
  installBaseDir,
56
+ invalidResult,
41
57
  isHomeDirectory,
42
58
  isLocalSource,
43
59
  isRecord,
60
+ layoutIsCurrent,
61
+ layoutIsLegacy,
44
62
  listDirectories,
45
63
  listRegisteredPluginInstalls,
46
64
  loadConfig,
47
- loadProjectConfigFromDir,
48
- loadProjectSourceConfig,
65
+ loadInstalledConfig,
49
66
  loadSkillsMatrixFromSource,
67
+ loadSourceRepoConfig,
68
+ mergeValidationResults,
50
69
  metadataValidationSchema,
51
70
  parseFrontmatter,
52
71
  parseMarketplacePluginRef,
72
+ plural,
73
+ providerInUse,
53
74
  readFile,
54
75
  readForkedFromMetadata,
55
76
  readSkillMetadata,
77
+ recreateConfigFrom,
78
+ relativeConfigPath,
56
79
  resolveInstallPaths,
57
80
  resolvePrimarySourceEntry,
81
+ resolveSourceDir,
82
+ scopeLabel,
83
+ scopeNoun,
58
84
  skillCategoriesFileSchema,
59
85
  skillRulesFileSchema,
86
+ skillsDir,
87
+ skillsPathPrefix,
88
+ sourceScopesInPlay,
60
89
  splitMetadataValidationIssues,
61
90
  stackConfigValidationSchema,
62
91
  stacksConfigSchema,
92
+ unusedSourceFolderName,
93
+ validResult,
63
94
  validateAgentFrontmatter,
64
95
  validateAllPlugins,
65
96
  validatePlugin,
66
97
  validateProjectConfig,
67
98
  validateSkillFrontmatter,
68
99
  validateSkillMetadata
69
- } from "../chunk-XPDVAJDH.js";
100
+ } from "../chunk-IM45LLQX.js";
70
101
  import {
71
102
  matrix
72
- } from "../chunk-AVK2HEKB.js";
103
+ } from "../chunk-GKPW33FP.js";
73
104
  import {
74
- CLAUDE_SRC_DIR,
105
+ CLAUDE_DIR,
75
106
  CLI_INVOKE_COMMAND,
76
107
  DEFAULT_BRANDING,
77
108
  DIRS,
78
109
  EJECT_SOURCE,
79
- LOCAL_SKILLS_PATH,
80
110
  MARKETPLACE_JSON,
81
111
  PLUGIN_MANIFEST_DIR,
82
112
  SKILLS_DIR_PATH,
83
113
  SKILL_CATEGORIES_PATH,
84
114
  SKILL_RULES_PATH,
85
115
  STACKS_FILE_PATH,
116
+ STANDARD_DIRS,
86
117
  STANDARD_FILES,
87
118
  UI_SYMBOLS,
88
119
  effectivelyExcludedSkillIds
89
- } from "../chunk-UITEKY2R.js";
120
+ } from "../chunk-AQXKAPCD.js";
90
121
  import "../chunk-OI2H6M5I.js";
91
122
 
92
123
  // src/cli/commands/doctor.ts
93
124
  import os2 from "os";
94
- import path3 from "path";
125
+ import path4 from "path";
95
126
 
96
- // src/cli/lib/source-validator.ts
127
+ // src/cli/lib/installation/layout-findings.ts
97
128
  import path from "path";
129
+ async function layoutFindingFor(scope) {
130
+ const { move } = scope;
131
+ const label = scopeLabel(scope.kind);
132
+ if (move === null) {
133
+ return { status: "pass", message: layoutIsCurrent(label, scope.relName), details: [] };
134
+ }
135
+ const whichLayout = layoutStatement(scope, move, label);
136
+ const faults = await faultsFor(scope, move);
137
+ const [worst, ...rest] = faults;
138
+ if (worst === void 0) return { ...whichLayout, details: [] };
139
+ return {
140
+ status: "fail",
141
+ message: `${label}: ${worst}`,
142
+ details: [...rest.map((fault) => `${label}: ${fault}`), whichLayout.message]
143
+ };
144
+ }
145
+ function layoutStatement(scope, move, label) {
146
+ if (!scope.legacy) return { status: "pass", message: layoutIsCurrent(label, move.to.relName) };
147
+ return { status: "warn", message: layoutIsLegacy(label, move.from.relName, move.to.relName) };
148
+ }
149
+ async function faultsFor(scope, move) {
150
+ const staleAgents = await compiledAgentsNamingTheOtherFolder(scope);
151
+ return [...rivalFolderFault(scope, move), ...staleAgents];
152
+ }
153
+ function rivalFolderFault(scope, move) {
154
+ if (!scope.both) return [];
155
+ return [
156
+ bothSourceFoldersPresent(move.from.relName, move.to.relName, folderBeingRead(scope, move))
157
+ ];
158
+ }
159
+ async function compiledAgentsNamingTheOtherFolder(scope) {
160
+ const unused = unusedSourceFolderName(scope.root, scope.provider);
161
+ if (unused === null) return [];
162
+ const agentsDir = path.join(scope.root, CLAUDE_DIR, STANDARD_DIRS.AGENTS);
163
+ const files = await listAgentMdFiles(agentsDir);
164
+ const named = await Promise.all(
165
+ files.map(async (file) => ({
166
+ file,
167
+ names: await namesFolder(path.join(agentsDir, file), unused)
168
+ }))
169
+ );
170
+ return named.filter((entry) => entry.names).map((entry) => compiledAgentNamesTheOtherFolder(entry.file, unused));
171
+ }
172
+ async function namesFolder(agentPath, folder) {
173
+ try {
174
+ return (await readFile(agentPath)).includes(folder);
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ // src/cli/lib/source-validator.ts
181
+ import path2 from "path";
98
182
  import { parse as parseYaml } from "yaml";
99
183
  import "zod";
100
184
  async function isSourceRepo(dir) {
101
- const sourceConfig = await loadProjectSourceConfig(dir);
102
- return directoryExists(path.join(dir, sourceConfig?.skillsDir ?? SKILLS_DIR_PATH));
185
+ const sourceConfig = await loadSourceRepoConfig(dir);
186
+ return directoryExists(path2.join(dir, sourceConfig?.skillsDir ?? SKILLS_DIR_PATH));
103
187
  }
104
188
  function isSnakeCase(key) {
105
189
  return /[a-z]_[a-z]/.test(key);
@@ -122,11 +206,11 @@ function checkDirNameMatchesSkillId(skillId, relPath, dirName) {
122
206
  }
123
207
  ];
124
208
  }
125
- async function checkSkillDirName(skillsDir, skillsDirRelPath, skillDir) {
126
- const dirName = path.basename(skillDir);
127
- const skillMdRelPath = path.join(skillsDirRelPath, skillDir, STANDARD_FILES.SKILL_MD);
209
+ async function checkSkillDirName(skillsDir2, skillsDirRelPath, skillDir) {
210
+ const dirName = path2.basename(skillDir);
211
+ const skillMdRelPath = path2.join(skillsDirRelPath, skillDir, STANDARD_FILES.SKILL_MD);
128
212
  try {
129
- const content = await readFile(path.join(skillsDir, skillDir, STANDARD_FILES.SKILL_MD));
213
+ const content = await readFile(path2.join(skillsDir2, skillDir, STANDARD_FILES.SKILL_MD));
130
214
  const frontmatter = parseFrontmatter(content, skillMdRelPath);
131
215
  if (frontmatter === null) {
132
216
  return [
@@ -148,67 +232,69 @@ async function checkSkillDirName(skillsDir, skillsDirRelPath, skillDir) {
148
232
  ];
149
233
  }
150
234
  }
151
- function validateSkillFilePairs(skillMdDirs, metadataDirs, skillsDir) {
235
+ function validateSkillFilePairs(skillMdDirs, metadataDirs, skillsDir2) {
152
236
  const missingMetadata = [...skillMdDirs].filter((dir) => !metadataDirs.has(dir)).map((dir) => ({
153
237
  severity: "error",
154
- file: path.join(skillsDir, dir),
238
+ file: path2.join(skillsDir2, dir),
155
239
  message: `Missing ${STANDARD_FILES.METADATA_YAML} \u2014 skill directory has ${STANDARD_FILES.SKILL_MD} but no metadata`
156
240
  }));
157
241
  const missingSkillMd = [...metadataDirs].filter((dir) => !skillMdDirs.has(dir)).map((dir) => ({
158
242
  severity: "error",
159
- file: path.join(skillsDir, dir),
243
+ file: path2.join(skillsDir2, dir),
160
244
  message: `Missing ${STANDARD_FILES.SKILL_MD} \u2014 skill directory has ${STANDARD_FILES.METADATA_YAML} but no SKILL.md`
161
245
  }));
162
246
  return [...missingMetadata, ...missingSkillMd];
163
247
  }
164
248
  async function validateSource(sourcePath, reader = "author") {
165
- const issues = [];
166
- const resolvedPath = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(sourcePath);
249
+ const resolvedPath = path2.isAbsolute(sourcePath) ? sourcePath : path2.resolve(sourcePath);
167
250
  if (!await directoryExists(resolvedPath)) {
168
- issues.push({
169
- severity: "error",
170
- file: resolvedPath,
171
- message: "Marketplace directory does not exist"
172
- });
173
- return buildResult(issues, 0);
251
+ return buildResult(
252
+ [{ severity: "error", file: resolvedPath, message: "Marketplace directory does not exist" }],
253
+ 0
254
+ );
174
255
  }
175
- const sourceProjectConfig = await loadProjectSourceConfig(resolvedPath);
256
+ const sourceProjectConfig = await loadSourceRepoConfig(resolvedPath);
176
257
  const skillsDirRelPath = sourceProjectConfig?.skillsDir ?? SKILLS_DIR_PATH;
177
- const skillsDir = path.join(resolvedPath, skillsDirRelPath);
178
- if (!await directoryExists(skillsDir)) {
179
- issues.push({
180
- severity: "error",
181
- file: skillsDir,
182
- message: "Skills directory does not exist"
183
- });
184
- return buildResult(issues, 0);
185
- }
186
- const skillMdFiles = await glob(`**/${STANDARD_FILES.SKILL_MD}`, skillsDir);
187
- const metadataFiles = await glob(`**/${STANDARD_FILES.METADATA_YAML}`, skillsDir);
188
- const skillMdDirs = new Set(skillMdFiles.map((f) => path.dirname(f)));
189
- const metadataDirs = new Set(metadataFiles.map((f) => path.dirname(f)));
190
- issues.push(...validateSkillFilePairs(skillMdDirs, metadataDirs, skillsDir));
191
- const validMetadataFiles = metadataFiles.filter((f) => skillMdDirs.has(path.dirname(f)));
192
- for (const metadataFile of validMetadataFiles) {
193
- issues.push(...await validateOneSkill(skillsDir, skillsDirRelPath, metadataFile));
258
+ const skillsDir2 = path2.join(resolvedPath, skillsDirRelPath);
259
+ if (!await directoryExists(skillsDir2)) {
260
+ return buildResult(
261
+ [{ severity: "error", file: skillsDir2, message: "Skills directory does not exist" }],
262
+ 0
263
+ );
194
264
  }
195
- issues.push(...await checkCrossReferences(resolvedPath, reader));
265
+ const skillMdFiles = await glob(`**/${STANDARD_FILES.SKILL_MD}`, skillsDir2);
266
+ const metadataFiles = await glob(`**/${STANDARD_FILES.METADATA_YAML}`, skillsDir2);
267
+ const skillMdDirs = new Set(skillMdFiles.map((f) => path2.dirname(f)));
268
+ const metadataDirs = new Set(metadataFiles.map((f) => path2.dirname(f)));
269
+ const pairIssues = validateSkillFilePairs(skillMdDirs, metadataDirs, skillsDir2);
270
+ const validMetadataFiles = metadataFiles.filter((f) => skillMdDirs.has(path2.dirname(f)));
271
+ const skillIssues = await validateSkillsInTurn(skillsDir2, skillsDirRelPath, validMetadataFiles);
272
+ const crossReferenceIssues = await checkCrossReferences(resolvedPath, reader);
196
273
  const extraIssues = await Promise.all([
197
274
  validateStacks(resolvedPath),
198
275
  validateAgents(resolvedPath),
199
276
  validateConfigFiles(resolvedPath)
200
277
  ]);
201
- issues.push(...extraIssues.flat());
202
- return buildResult(issues, validMetadataFiles.length);
278
+ return buildResult(
279
+ [...pairIssues, ...skillIssues, ...crossReferenceIssues, ...extraIssues.flat()],
280
+ validMetadataFiles.length
281
+ );
282
+ }
283
+ async function validateSkillsInTurn(skillsDir2, skillsDirRelPath, metadataFiles) {
284
+ const issues = [];
285
+ for (const metadataFile of metadataFiles) {
286
+ issues.push(...await validateOneSkill(skillsDir2, skillsDirRelPath, metadataFile));
287
+ }
288
+ return issues;
203
289
  }
204
290
  function yamlParseFailure(error) {
205
291
  return `Failed to parse YAML: ${getErrorMessage(error)}`;
206
292
  }
207
- async function validateOneSkill(skillsDir, skillsDirRelPath, metadataFile) {
208
- const relPath = path.join(skillsDirRelPath, metadataFile);
293
+ async function validateOneSkill(skillsDir2, skillsDirRelPath, metadataFile) {
294
+ const relPath = path2.join(skillsDirRelPath, metadataFile);
209
295
  let rawMetadata;
210
296
  try {
211
- rawMetadata = parseYaml(await readFile(path.join(skillsDir, metadataFile)));
297
+ rawMetadata = parseYaml(await readFile(path2.join(skillsDir2, metadataFile)));
212
298
  } catch (error) {
213
299
  return [{ severity: "error", file: relPath, message: yamlParseFailure(error) }];
214
300
  }
@@ -216,7 +302,7 @@ async function validateOneSkill(skillsDir, skillsDirRelPath, metadataFile) {
216
302
  ...checkSnakeCaseKeys(rawMetadata, relPath),
217
303
  ...checkMetadataSchema(rawMetadata, relPath),
218
304
  // Independent of metadata validity — the id lives in SKILL.md frontmatter
219
- ...await checkSkillDirName(skillsDir, skillsDirRelPath, path.dirname(metadataFile))
305
+ ...await checkSkillDirName(skillsDir2, skillsDirRelPath, path2.dirname(metadataFile))
220
306
  ];
221
307
  }
222
308
  function checkMetadataSchema(rawMetadata, relPath) {
@@ -228,7 +314,11 @@ function checkMetadataSchema(rawMetadata, relPath) {
228
314
  ...warnings.map((message) => ({ severity: "warning", file: relPath, message }))
229
315
  ];
230
316
  }
231
- var MARKETPLACE_MANIFEST_PATH = path.join(PLUGIN_MANIFEST_DIR, MARKETPLACE_JSON);
317
+ var MARKETPLACE_MANIFEST_PATH = path2.join(PLUGIN_MANIFEST_DIR, MARKETPLACE_JSON);
318
+ var MANIFEST_REQUIRED_SEVERITY = {
319
+ author: "warning",
320
+ consumer: "error"
321
+ };
232
322
  async function checkCrossReferences(resolvedPath, reader) {
233
323
  try {
234
324
  await loadSkillsMatrixFromSource({ sourceFlag: resolvedPath, skipExtraSources: true });
@@ -236,10 +326,17 @@ async function checkCrossReferences(resolvedPath, reader) {
236
326
  (healthIssue) => toSourceIssue(healthIssue, resolvedPath, reader)
237
327
  );
238
328
  } catch (error) {
239
- return [matrixLoadFailure(error)];
329
+ return [matrixLoadFailure(error, reader)];
240
330
  }
241
331
  }
242
- function matrixLoadFailure(error) {
332
+ function matrixLoadFailure(error, reader) {
333
+ if (error instanceof MarketplaceManifestRequiredError) {
334
+ return {
335
+ severity: MANIFEST_REQUIRED_SEVERITY[reader],
336
+ file: MARKETPLACE_MANIFEST_PATH,
337
+ message: getErrorMessage(error)
338
+ };
339
+ }
243
340
  if (error instanceof MarketplaceNameRefusedError) {
244
341
  return {
245
342
  severity: "error",
@@ -290,7 +387,7 @@ function consumedMarketplaceMessage(marketplacePath, details) {
290
387
  return `Marketplace '${marketplacePath}': ${details}. Nothing to fix here \u2014 the rule is that marketplace's, not this project's`;
291
388
  }
292
389
  async function validateStacks(resolvedPath) {
293
- const stacksDir = path.join(resolvedPath, DIRS.stacks);
390
+ const stacksDir = path2.join(resolvedPath, DIRS.stacks);
294
391
  if (!await directoryExists(stacksDir)) return [];
295
392
  const [skillMetaIssues, configIssues] = await Promise.all([
296
393
  validateYamlFiles({
@@ -309,7 +406,7 @@ async function validateStacks(resolvedPath) {
309
406
  return [...skillMetaIssues, ...configIssues];
310
407
  }
311
408
  async function validateAgents(resolvedPath) {
312
- const agentsDir = path.join(resolvedPath, DIRS.agents);
409
+ const agentsDir = path2.join(resolvedPath, DIRS.agents);
313
410
  if (!await directoryExists(agentsDir)) return [];
314
411
  return validateYamlFiles({
315
412
  baseDir: agentsDir,
@@ -326,33 +423,32 @@ async function validateConfigFiles(resolvedPath) {
326
423
  ]);
327
424
  return results.flat();
328
425
  }
329
- async function validateYamlFiles(opts) {
426
+ async function validateYamlFiles(scan) {
330
427
  const issues = [];
331
- const files = await glob(opts.pattern, opts.baseDir);
428
+ const files = await glob(scan.pattern, scan.baseDir);
332
429
  for (const relFile of files) {
333
- const absPath = path.join(opts.baseDir, relFile);
334
- const displayPath = path.join(opts.relBaseDir, relFile);
335
- let parsed;
336
- try {
337
- parsed = parseYaml(await readFile(absPath));
338
- } catch (error) {
339
- issues.push({ severity: "error", file: displayPath, message: yamlParseFailure(error) });
340
- continue;
341
- }
342
- const result = opts.schema.safeParse(parsed);
343
- if (result.success) continue;
344
- issues.push(
345
- ...formatZodErrors(result.error).map((message) => ({
346
- severity: "error",
347
- file: displayPath,
348
- message
349
- }))
350
- );
430
+ issues.push(...await validateYamlFile(scan, relFile));
351
431
  }
352
432
  return issues;
353
433
  }
434
+ async function validateYamlFile(scan, relFile) {
435
+ const displayPath = path2.join(scan.relBaseDir, relFile);
436
+ let parsed;
437
+ try {
438
+ parsed = parseYaml(await readFile(path2.join(scan.baseDir, relFile)));
439
+ } catch (error) {
440
+ return [{ severity: "error", file: displayPath, message: yamlParseFailure(error) }];
441
+ }
442
+ const result = scan.schema.safeParse(parsed);
443
+ if (result.success) return [];
444
+ return formatZodErrors(result.error).map((message) => ({
445
+ severity: "error",
446
+ file: displayPath,
447
+ message
448
+ }));
449
+ }
354
450
  async function validateTsConfig(resolvedPath, relConfigPath, schema) {
355
- const absPath = path.join(resolvedPath, relConfigPath);
451
+ const absPath = path2.join(resolvedPath, relConfigPath);
356
452
  if (!await fileExists(absPath)) return [];
357
453
  try {
358
454
  const loaded = await loadConfig(absPath, schema);
@@ -382,7 +478,7 @@ function buildResult(issues, skillCount) {
382
478
 
383
479
  // src/cli/lib/content-validator.ts
384
480
  import os from "os";
385
- import path2 from "path";
481
+ import path3 from "path";
386
482
  import { partition } from "remeda";
387
483
  var CWD_SOURCE_NAME = "current directory";
388
484
  var AGENT_FILE_EXTENSION = ".md";
@@ -400,8 +496,8 @@ function mergeValidations(parts) {
400
496
  function displayDir(absolutePath) {
401
497
  const home = os.homedir();
402
498
  if (absolutePath === home) return "~";
403
- if (absolutePath.startsWith(home + path2.sep)) {
404
- return `~${path2.sep}${path2.relative(home, absolutePath)}`;
499
+ if (absolutePath.startsWith(home + path3.sep)) {
500
+ return `~${path3.sep}${path3.relative(home, absolutePath)}`;
405
501
  }
406
502
  return absolutePath;
407
503
  }
@@ -422,13 +518,13 @@ async function validateProjectConfigFile(projectDir) {
422
518
  function toUnreadableConfigIssue(failure) {
423
519
  return {
424
520
  severity: "error",
425
- file: displayDir(failure.configPath),
521
+ file: `${scopeLabel(failure.scope)}'s ${displayDir(failure.configPath)}`,
426
522
  message: `${CONFIG_UNREADABLE}: ${failure.reason}`
427
523
  };
428
524
  }
429
525
  async function countExistingConfigs(projectDir) {
430
526
  const present = await Promise.all(
431
- configDirsInPlay(projectDir).map((dir) => fileExists(getProjectConfigPath(dir)))
527
+ configDirsInPlay(projectDir).map((dir) => fileExists(getInstalledConfigPath(dir)))
432
528
  );
433
529
  return present.filter(Boolean).length;
434
530
  }
@@ -456,7 +552,7 @@ async function localSourcesToValidate(projectDir, registered) {
456
552
  return [...local, { name: CWD_SOURCE_NAME, url: projectDir }];
457
553
  }
458
554
  function isCwdMarketplace(projectDir, source) {
459
- return path2.resolve(projectDir, source.url) === projectDir;
555
+ return path3.resolve(projectDir, source.url) === projectDir;
460
556
  }
461
557
  function readerFor(projectDir, source) {
462
558
  return isCwdMarketplace(projectDir, source) ? "author" : "consumer";
@@ -465,7 +561,7 @@ async function validateOneSource(source, reader) {
465
561
  const label = `${source.name} (${source.url})`;
466
562
  try {
467
563
  const result = await validateSource(source.url, reader);
468
- return { note: `${label} \u2014 ${result.skillCount} skills`, issues: result.issues };
564
+ return { note: `${label} \u2014 ${plural(result.skillCount, "skill")}`, issues: result.issues };
469
565
  } catch (error) {
470
566
  return {
471
567
  note: `${label} \u2014 failed`,
@@ -525,7 +621,7 @@ async function findPluginDirectories(pluginsDir) {
525
621
  const checks = await Promise.all(
526
622
  entries.map(async (name) => ({
527
623
  name,
528
- isPlugin: await directoryExists(path2.join(pluginsDir, name, PLUGIN_MANIFEST_DIR))
624
+ isPlugin: await directoryExists(path3.join(pluginsDir, name, PLUGIN_MANIFEST_DIR))
529
625
  }))
530
626
  );
531
627
  return checks.filter((check) => check.isPlugin).map((check) => check.name);
@@ -541,7 +637,9 @@ async function validateInstalledSkills(projectDir) {
541
637
  );
542
638
  }
543
639
  async function configuredSkillIds(projectDir) {
544
- const loaded = await Promise.all(configDirsInPlay(projectDir).map(loadProjectConfigFromDir));
640
+ const loaded = await Promise.all(
641
+ configDirsInPlay(projectDir).map((dir) => loadInstalledConfig(dir))
642
+ );
545
643
  return new Set(loaded.flatMap(configuredIdsOf));
546
644
  }
547
645
  function configuredIdsOf(loaded) {
@@ -569,10 +667,10 @@ async function listAgentFilesWithOurProvenance(agentsDir) {
569
667
  const { marked } = await splitAgentsByProvenance(agentsDir);
570
668
  return marked.map((name) => `${name}${AGENT_FILE_EXTENSION}`);
571
669
  }
572
- async function listSkillDirsWithOurProvenance(skillsDir) {
573
- const names = await listDirectories(skillsDir);
670
+ async function listSkillDirsWithOurProvenance(skillsDir2) {
671
+ const names = await listDirectories(skillsDir2);
574
672
  const judged = await Promise.all(
575
- names.map(async (name) => ({ name, ours: await carriesOurProvenance(skillsDir, name) }))
673
+ names.map(async (name) => ({ name, ours: await carriesOurProvenance(skillsDir2, name) }))
576
674
  );
577
675
  return judged.filter((entry) => entry.ours).map((entry) => entry.name);
578
676
  }
@@ -583,40 +681,40 @@ async function listAcrossScopes(dirs, listEntries) {
583
681
  async function listDisplayPaths(dir, listEntries) {
584
682
  if (!await directoryExists(dir)) return [];
585
683
  const entries = await listEntries(dir);
586
- return entries.map((entry) => displayDir(path2.join(dir, entry)));
684
+ return entries.map((entry) => displayDir(path3.join(dir, entry)));
587
685
  }
588
- async function validateSkillsDirectory(skillsDir, configuredIds) {
589
- if (!await directoryExists(skillsDir)) return NOTHING_VALIDATED;
590
- const skillDirs = await listDirectories(skillsDir);
686
+ async function validateSkillsDirectory(skillsDir2, configuredIds) {
687
+ if (!await directoryExists(skillsDir2)) return NOTHING_VALIDATED;
688
+ const skillDirs = await listDirectories(skillsDir2);
591
689
  if (skillDirs.length === 0) return NOTHING_VALIDATED;
592
690
  const judged = await Promise.all(
593
691
  skillDirs.map(async (name) => ({
594
692
  name,
595
- ours: await isOurSkillDirectory(skillsDir, name, configuredIds)
693
+ ours: await isOurSkillDirectory(skillsDir2, name, configuredIds)
596
694
  }))
597
695
  );
598
696
  const [ours, foreign] = partition(judged, (entry) => entry.ours);
599
697
  const results = await Promise.all(
600
698
  ours.map(async ({ name }) => {
601
- const skillDir = path2.join(skillsDir, name);
699
+ const skillDir = path3.join(skillsDir2, name);
602
700
  return toIssues(displayDir(skillDir), await validateInstalledSkill(skillDir));
603
701
  })
604
702
  );
605
703
  return {
606
704
  count: ours.length,
607
705
  issues: results.flat(),
608
- notes: foreign.map(({ name }) => foreignSkillNote(skillsDir, name))
706
+ notes: foreign.map(({ name }) => foreignSkillNote(skillsDir2, name))
609
707
  };
610
708
  }
611
- async function isOurSkillDirectory(skillsDir, name, configuredIds) {
709
+ async function isOurSkillDirectory(skillsDir2, name, configuredIds) {
612
710
  if (configuredIds.has(name)) return true;
613
- return carriesOurProvenance(skillsDir, name);
711
+ return carriesOurProvenance(skillsDir2, name);
614
712
  }
615
- async function carriesOurProvenance(skillsDir, name) {
616
- return await readForkedFromMetadata(path2.join(skillsDir, name)) !== null;
713
+ async function carriesOurProvenance(skillsDir2, name) {
714
+ return await readForkedFromMetadata(path3.join(skillsDir2, name)) !== null;
617
715
  }
618
- function foreignSkillNote(skillsDir, name) {
619
- return `${displayDir(path2.join(skillsDir, name))} \u2014 not installed by this CLI and named by no configuration here: not validated`;
716
+ function foreignSkillNote(skillsDir2, name) {
717
+ return `${displayDir(path3.join(skillsDir2, name))} \u2014 not installed by this CLI and named by no configuration here: not validated`;
620
718
  }
621
719
  async function validateAgentsDirectory(agentsDir) {
622
720
  if (!await directoryExists(agentsDir)) return NOTHING_VALIDATED;
@@ -624,46 +722,39 @@ async function validateAgentsDirectory(agentsDir) {
624
722
  if (agentFiles.length === 0) return NOTHING_VALIDATED;
625
723
  const results = await Promise.all(
626
724
  agentFiles.map(async (fileName) => {
627
- const agentPath = path2.join(agentsDir, fileName);
725
+ const agentPath = path3.join(agentsDir, fileName);
628
726
  return toIssues(displayDir(agentPath), await validateAgentFrontmatter(agentPath));
629
727
  })
630
728
  );
631
729
  return { count: agentFiles.length, issues: results.flat(), notes: [] };
632
730
  }
633
731
  async function validateInstalledSkill(skillDir) {
634
- const skillMdPath = path2.join(skillDir, STANDARD_FILES.SKILL_MD);
635
- const metadataPath = path2.join(skillDir, STANDARD_FILES.METADATA_YAML);
636
- const errors = [];
637
- const warnings = [];
638
- if (!await fileExists(skillMdPath)) {
639
- errors.push(`Missing ${STANDARD_FILES.SKILL_MD}`);
640
- } else {
641
- const frontmatterResult = await validateSkillFrontmatter(skillMdPath);
642
- errors.push(...frontmatterResult.errors);
643
- warnings.push(...frontmatterResult.warnings);
644
- }
645
- if (!await fileExists(metadataPath)) {
646
- errors.push(`Missing ${STANDARD_FILES.METADATA_YAML}`);
647
- } else {
648
- const metadataResult = await validateInstalledSkillMetadata(metadataPath);
649
- errors.push(...metadataResult.errors);
650
- warnings.push(...metadataResult.warnings);
651
- }
652
- return { valid: errors.length === 0, errors, warnings };
732
+ const skillMd = await validateSkillFile(
733
+ skillDir,
734
+ STANDARD_FILES.SKILL_MD,
735
+ validateSkillFrontmatter
736
+ );
737
+ const metadata = await validateSkillFile(
738
+ skillDir,
739
+ STANDARD_FILES.METADATA_YAML,
740
+ validateInstalledSkillMetadata
741
+ );
742
+ return mergeValidationResults([skillMd, metadata]);
743
+ }
744
+ async function validateSkillFile(skillDir, fileName, validate) {
745
+ const filePath = path3.join(skillDir, fileName);
746
+ if (!await fileExists(filePath)) return invalidResult(`Missing ${fileName}`);
747
+ return validate(filePath);
653
748
  }
654
749
  async function validateInstalledSkillMetadata(metadataPath) {
655
750
  const read = await readSkillMetadata(metadataPath);
656
751
  if (!read.usable) {
657
- return {
658
- valid: false,
659
- errors: [`${STANDARD_FILES.METADATA_YAML}: ${read.reason}`],
660
- warnings: []
661
- };
752
+ return invalidResult(`${STANDARD_FILES.METADATA_YAML}: ${read.reason}`);
662
753
  }
663
754
  const metadata = read.metadata;
664
755
  const result = validateSkillMetadata(metadata);
665
756
  if (result.success) {
666
- return { valid: true, errors: [], warnings: [] };
757
+ return validResult();
667
758
  }
668
759
  const { errors, warnings } = splitMetadataValidationIssues(result.error, metadata);
669
760
  return {
@@ -675,20 +766,29 @@ async function validateInstalledSkillMetadata(metadataPath) {
675
766
 
676
767
  // src/cli/commands/doctor.ts
677
768
  import { countBy, unique } from "remeda";
678
- var CONFIG_TS_REL = `${CLAUDE_SRC_DIR}/${STANDARD_FILES.CONFIG_TS}`;
679
769
  async function resolveConfigState(detected, projectDir) {
680
- if (detected?.config) return { kind: "loaded", config: detected.config };
681
- const loaded = await loadProjectConfigFromDir(projectDir);
770
+ if (detected?.config) {
771
+ return {
772
+ kind: "loaded",
773
+ config: detected.config,
774
+ inherited: inheritsTheGlobalConfig(detected.installation.projectDir, projectDir)
775
+ };
776
+ }
777
+ const loaded = await loadInstalledConfig(projectDir);
682
778
  if (!loaded) return { kind: "absent" };
683
- return declaresNoContent(loaded.config) ? { kind: "declares-nothing", config: loaded.config } : { kind: "loaded", config: loaded.config };
779
+ return declaresNoContent(loaded.config) ? { kind: "declares-nothing", config: loaded.config } : { kind: "loaded", config: loaded.config, inherited: false };
780
+ }
781
+ function inheritsTheGlobalConfig(readFrom, projectDir) {
782
+ return isHomeDirectory(readFrom) && !isHomeDirectory(projectDir);
684
783
  }
685
- function checkConfigValid(state) {
784
+ function checkConfigValid(state, projectDir) {
785
+ const theConfig = (verdict) => configRowMessage(state, projectDir, verdict);
686
786
  if (state.kind === "absent") {
687
787
  return {
688
788
  result: {
689
789
  kind: "config",
690
790
  status: "fail",
691
- message: `${CONFIG_TS_REL} not found`,
791
+ message: theConfig("not found"),
692
792
  details: [`Run '${CLI_INVOKE_COMMAND} init' to create a configuration`]
693
793
  },
694
794
  config: null
@@ -699,7 +799,7 @@ function checkConfigValid(state) {
699
799
  result: {
700
800
  kind: "config-empty",
701
801
  status: "warn",
702
- message: `${CONFIG_TS_REL} is valid but declares no skills and no agents`
802
+ message: theConfig("is valid but declares no skills and no agents")
703
803
  },
704
804
  config: state.config
705
805
  };
@@ -711,7 +811,7 @@ function checkConfigValid(state) {
711
811
  result: {
712
812
  kind: "config",
713
813
  status: "fail",
714
- message: `${CONFIG_TS_REL} has errors`,
814
+ message: theConfig("has errors"),
715
815
  details: validation.errors
716
816
  },
717
817
  config: null
@@ -722,7 +822,7 @@ function checkConfigValid(state) {
722
822
  result: {
723
823
  kind: "config",
724
824
  status: "warn",
725
- message: `${CONFIG_TS_REL} has warnings`,
825
+ message: theConfig("has warnings"),
726
826
  details: validation.warnings
727
827
  },
728
828
  config
@@ -732,11 +832,29 @@ function checkConfigValid(state) {
732
832
  result: {
733
833
  kind: "config",
734
834
  status: "pass",
735
- message: `${CONFIG_TS_REL} is valid`
835
+ message: theConfig("is valid")
736
836
  },
737
837
  config
738
838
  };
739
839
  }
840
+ function configRowMessage(state, projectDir, verdict) {
841
+ if (state.kind === "loaded" && state.inherited)
842
+ return `${theInheritedConfig()}, which ${verdict}`;
843
+ return `${theOwnConfig(projectDir)} ${verdict}`;
844
+ }
845
+ function theOwnConfig(projectDir) {
846
+ return `${scopeLabel(scopeOf(projectDir))}: ${configNamedFrom(projectDir)}`;
847
+ }
848
+ function theInheritedConfig() {
849
+ return `${scopeLabel("project")}: no ${STANDARD_FILES.CONFIG_TS}, using ${scopeNoun("global")}'s (${configNamedFrom(os2.homedir())})`;
850
+ }
851
+ function scopeOf(dir) {
852
+ return isHomeDirectory(dir) ? "global" : "project";
853
+ }
854
+ function configNamedFrom(root) {
855
+ const relPath = relativeConfigPath(root, providerInUse(root));
856
+ return isHomeDirectory(root) ? `~/${relPath}` : relPath;
857
+ }
740
858
  async function checkSkillsResolved(config, matrix2, projectDir) {
741
859
  const excludedIds = effectivelyExcludedSkillIds(config.skills);
742
860
  const stackIds = config.stack ? getStackSkillIds(config.stack) : [];
@@ -765,14 +883,14 @@ async function checkSkillsResolved(config, matrix2, projectDir) {
765
883
  return {
766
884
  kind: "skills",
767
885
  status: "fail",
768
- message: `${uniqueSkills.length - missingSkills.length}/${uniqueSkills.length} skills found`,
886
+ message: `${ratio(uniqueSkills.length - missingSkills.length, uniqueSkills.length, "skill")} found`,
769
887
  details: missingSkills.map((s) => `- ${s} (not found)`)
770
888
  };
771
889
  }
772
890
  return {
773
891
  kind: "skills",
774
892
  status: "pass",
775
- message: `${uniqueSkills.length}/${uniqueSkills.length} skills found`
893
+ message: `${ratio(uniqueSkills.length, uniqueSkills.length, "skill")} found`
776
894
  };
777
895
  }
778
896
  async function checkAgentsCompiled(config, projectDir) {
@@ -784,16 +902,28 @@ async function checkAgentsCompiled(config, projectDir) {
784
902
  message: "No agents configured"
785
903
  };
786
904
  }
787
- const projectAgentsDir = resolveInstallPaths(projectDir, "project").agentsDir;
788
- const globalAgentsDir = resolveInstallPaths(projectDir, "global").agentsDir;
905
+ const hosts = {
906
+ global: providerInUse(installBaseDir(projectDir, "global")),
907
+ project: providerInUse(installBaseDir(projectDir, "project"))
908
+ };
909
+ const dirs = {
910
+ global: resolveInstallPaths(projectDir, "global").agentsDir,
911
+ project: resolveInstallPaths(projectDir, "project").agentsDir
912
+ };
789
913
  const agentChecks = await Promise.all(
790
- agents.map(async (agent) => {
791
- const agentsDir = agent.scope === "global" ? globalAgentsDir : projectAgentsDir;
792
- const agentPath = path3.join(agentsDir, `${agent.name}.md`);
793
- return { name: agent.name, compiled: await fileExists(agentPath) };
794
- })
914
+ agents.map((agent) => agentCompileCheck(agent, hosts, dirs))
795
915
  );
796
- const missingAgents = agentChecks.filter((c) => !c.compiled).map((c) => c.name);
916
+ const unrenderable = agentChecks.filter((c) => c.state === "unrenderable");
917
+ const missingAgents = agentChecks.filter((c) => c.state === "missing").map((c) => c.name);
918
+ if (unrenderable.length === agents.length) {
919
+ const hostsLeavingThemOut = unique(unrenderable.map((a) => a.provider)).join(" or ");
920
+ return {
921
+ kind: "agents",
922
+ status: "pass",
923
+ message: `No sub-agent in this configuration is carried by ${hostsLeavingThemOut}`,
924
+ details: unrenderable.map((a) => `- ${a.name} (left out of ${a.provider})`)
925
+ };
926
+ }
797
927
  if (missingAgents.length > 0) {
798
928
  return {
799
929
  kind: "agents",
@@ -802,13 +932,24 @@ async function checkAgentsCompiled(config, projectDir) {
802
932
  details: missingAgents.map((a) => `- ${a} (missing)`)
803
933
  };
804
934
  }
935
+ const renderable = agents.length - unrenderable.length;
805
936
  return {
806
937
  kind: "agents",
807
938
  status: "pass",
808
- message: `${agents.length}/${agents.length} agents compiled`
939
+ message: `${ratio(renderable, renderable, "agent")} compiled`
809
940
  };
810
941
  }
942
+ async function agentCompileCheck(agent, hosts, agentsDirs) {
943
+ const provider = hosts[agent.scope];
944
+ if (!hostCompilesAgent(provider, agent.name)) {
945
+ return { name: agent.name, provider, state: "unrenderable" };
946
+ }
947
+ const file = `${agent.name}${agentCodec(provider).extension}`;
948
+ const compiled = await fileExists(path4.join(agentsDirs[agent.scope], file));
949
+ return { name: agent.name, provider, state: compiled ? "compiled" : "missing" };
950
+ }
811
951
  async function checkNoOrphans(config, projectDir) {
952
+ const codec = agentCodec(providerInUse(projectDir));
812
953
  const projectAgentsDir = resolveInstallPaths(projectDir, "project").agentsDir;
813
954
  const globalAgentsDir = resolveInstallPaths(projectDir, "global").agentsDir;
814
955
  const scopesShareAgentsDir = isHomeDirectory(projectDir);
@@ -823,8 +964,8 @@ async function checkNoOrphans(config, projectDir) {
823
964
  message: "No agents directory"
824
965
  };
825
966
  }
826
- const projectMdFiles = projectExists ? await listAgentMdFiles(projectAgentsDir) : [];
827
- const globalMdFiles = globalExists ? await listAgentMdFiles(globalAgentsDir) : [];
967
+ const projectMdFiles = projectExists ? await listAgentFilesOf(projectAgentsDir, codec) : [];
968
+ const globalMdFiles = globalExists ? await listAgentFilesOf(globalAgentsDir, codec) : [];
828
969
  const activeProjectAgents = new Set(
829
970
  config.agents.filter((a) => a.scope === "project" && !a.excluded).map((a) => a.name)
830
971
  );
@@ -833,8 +974,8 @@ async function checkNoOrphans(config, projectDir) {
833
974
  );
834
975
  const knownProjectDirAgents = scopesShareAgentsDir ? /* @__PURE__ */ new Set([...activeProjectAgents, ...knownGlobalAgents]) : activeProjectAgents;
835
976
  const orphanedFiles = [
836
- ...orphanedAgentNames(projectMdFiles, knownProjectDirAgents),
837
- ...orphanedAgentNames(globalMdFiles, knownGlobalAgents)
977
+ ...orphanedAgentNames(projectMdFiles, knownProjectDirAgents, codec),
978
+ ...orphanedAgentNames(globalMdFiles, knownGlobalAgents, codec)
838
979
  ];
839
980
  if (orphanedFiles.length > 0) {
840
981
  return {
@@ -850,8 +991,8 @@ async function checkNoOrphans(config, projectDir) {
850
991
  message: "No orphaned agent files"
851
992
  };
852
993
  }
853
- function orphanedAgentNames(mdFiles, knownAgents) {
854
- return mdFiles.map((fileName) => fileName.replace(/\.md$/, "")).filter((agentName) => !knownAgents.has(agentName));
994
+ function orphanedAgentNames(mdFiles, knownAgents, codec) {
995
+ return mdFiles.map((fileName) => path4.basename(fileName, codec.extension)).filter((agentName) => !knownAgents.has(agentName));
855
996
  }
856
997
  async function checkUnownedInstallation(projectDir) {
857
998
  const { skills, agents } = await listInstalledArtifacts(projectDir);
@@ -865,6 +1006,9 @@ async function checkUnownedInstallation(projectDir) {
865
1006
  details: [...skills, ...agents].map((artifact) => `- ${artifact}`)
866
1007
  };
867
1008
  }
1009
+ function ratio(found, total, noun) {
1010
+ return `${found}/${plural(total, noun)}`;
1011
+ }
868
1012
  function countedArtifacts(skills, agents) {
869
1013
  return [
870
1014
  ...skills.length > 0 ? [plural(skills.length, "skill")] : [],
@@ -882,25 +1026,31 @@ async function checkSkillsInstalled(config, projectDir) {
882
1026
  };
883
1027
  }
884
1028
  const skillChecks = await Promise.all(
885
- ejectSkills.map(async (skill) => {
886
- const baseDir = installBaseDir(projectDir, skill.scope);
887
- const skillMdPath = path3.join(baseDir, LOCAL_SKILLS_PATH, skill.id, STANDARD_FILES.SKILL_MD);
888
- return { id: skill.id, installed: await fileExists(skillMdPath) };
889
- })
1029
+ ejectSkills.map((skill) => ejectedSkillCheck(skill, projectDir))
890
1030
  );
891
- const missingSkills = skillChecks.filter((c) => !c.installed).map((c) => c.id);
1031
+ const missingSkills = skillChecks.filter((c) => !c.installed);
892
1032
  if (missingSkills.length > 0) {
893
1033
  return {
894
1034
  kind: "installed",
895
1035
  status: "warn",
896
1036
  message: `${plural(missingSkills.length, "skill")} missing from disk`,
897
- details: missingSkills.map((s) => `- ${s} (not found in ${LOCAL_SKILLS_PATH}/)`)
1037
+ details: missingSkills.map((s) => `- ${s.id} (not found in ${s.lookedIn}/)`)
898
1038
  };
899
1039
  }
900
1040
  return {
901
1041
  kind: "installed",
902
1042
  status: "pass",
903
- message: `${ejectSkills.length}/${ejectSkills.length} eject-mode skills installed`
1043
+ message: `${ratio(ejectSkills.length, ejectSkills.length, "eject-mode skill")} installed`
1044
+ };
1045
+ }
1046
+ async function ejectedSkillCheck(skill, projectDir) {
1047
+ const provider = providerInUse(installBaseDir(projectDir, skill.scope));
1048
+ const dir = skillsDir(provider, skill.scope, projectDir);
1049
+ const installed = await fileExists(path4.join(dir, skill.id, STANDARD_FILES.SKILL_MD));
1050
+ return {
1051
+ id: skill.id,
1052
+ installed,
1053
+ lookedIn: skillsPathPrefix(provider, skill.scope, projectDir)
904
1054
  };
905
1055
  }
906
1056
  async function checkPluginSkillsInstalled(config, projectDir) {
@@ -935,7 +1085,7 @@ async function checkPluginSkillsInstalled(config, projectDir) {
935
1085
  return {
936
1086
  kind: "plugins",
937
1087
  status: "pass",
938
- message: `${pluginSkills.length}/${pluginSkills.length} plugin-mode skills installed`
1088
+ message: `${ratio(pluginSkills.length, pluginSkills.length, "plugin-mode skill")} installed`
939
1089
  };
940
1090
  }
941
1091
  var MARKETPLACE_CHOSEN_BY = {
@@ -950,7 +1100,7 @@ function howItWasReached(source, isLocal) {
950
1100
  }
951
1101
  function reachedMarketplace(result) {
952
1102
  const { source, sourceOrigin } = result.sourceConfig;
953
- const skillCount = Object.keys(matrix.skills).length;
1103
+ const skillCount = result.marketplaceSkillIds.size;
954
1104
  const sourceLabel = result.isLocal ? "local" : "remote";
955
1105
  return {
956
1106
  kind: "source",
@@ -1069,9 +1219,6 @@ var SKIP_AFTER_CONFIG_ERROR = "Skipped \u2014 fix the content errors above first
1069
1219
  function skipRestatingContent(nouns) {
1070
1220
  return `Skipped \u2014 this row would only restate the ${nouns.join(" and ")} errors above`;
1071
1221
  }
1072
- function plural(count, word) {
1073
- return `${count} ${word}${count === 1 ? "" : "s"}`;
1074
- }
1075
1222
  function formatCheckName(name) {
1076
1223
  return name.padEnd(CHECK_WIDTH);
1077
1224
  }
@@ -1091,13 +1238,15 @@ function formatStatus(status) {
1091
1238
  }
1092
1239
  }
1093
1240
  }
1241
+ var DETAIL_INDENT = `${ROW_INDENT}${" ".repeat(CHECK_WIDTH)} `;
1094
1242
  function formatCheckLine(name, result) {
1095
1243
  const headerLine = `${ROW_INDENT}${formatCheckName(name)}${formatStatus(result.status)} ${result.message}`;
1096
- const detailLines = (result.details ?? []).map(
1097
- (detail) => `${ROW_INDENT}${" ".repeat(CHECK_WIDTH)} ${detail}`
1098
- );
1244
+ const detailLines = (result.details ?? []).flatMap(indentedDetailLines);
1099
1245
  return [headerLine, ...detailLines];
1100
1246
  }
1247
+ function indentedDetailLines(detail) {
1248
+ return detail.split("\n").filter((line) => line !== "").map((line) => `${DETAIL_INDENT}${line}`);
1249
+ }
1101
1250
  function formatSummary(results) {
1102
1251
  const counts = countBy(results, (r) => r.status);
1103
1252
  const parts = [
@@ -1126,14 +1275,6 @@ var TIPS = [
1126
1275
  status: "warn",
1127
1276
  tip: ` Tip: Nothing is configured yet \u2014 run '${CLI_INVOKE_COMMAND} init' to choose skills and sub-agents`
1128
1277
  },
1129
- {
1130
- // The one remedy that applies to a config that cannot be read. `init` does not clear such a
1131
- // file — it refuses it — so the tip above would send the reader in a circle; this is the same
1132
- // way out `edit` and `init` themselves name, worded the same way.
1133
- kind: "content-config",
1134
- status: "fail",
1135
- tip: ` Tip: There is no automatic repair \u2014 recreate the configuration: '${CLI_INVOKE_COMMAND} uninstall' still works on a config it cannot read, then '${CLI_INVOKE_COMMAND} init'`
1136
- },
1137
1278
  {
1138
1279
  // Printed beside the config tip above, which says how to get a configuration back and
1139
1280
  // nothing about the files that outlived the old one. Both halves are named with what they
@@ -1163,6 +1304,49 @@ function formatTips(results) {
1163
1304
  (t) => t.tip
1164
1305
  );
1165
1306
  }
1307
+ async function recreateConfigTips(contentResults, projectDir) {
1308
+ if (!failedContentKinds(contentResults).has(CONFIG_CHECK.kind)) return [];
1309
+ const failures = await findConfigLoadFailures(projectDir).catch(() => []);
1310
+ return failures.map(
1311
+ (failure) => ` Tip: There is no automatic repair \u2014 recreate the configuration by running ${recreateConfigFrom(failure.scopeRoot)}`
1312
+ );
1313
+ }
1314
+ var LAYOUT_ROW_NAME = "Layout";
1315
+ function toLayoutResult(finding) {
1316
+ return {
1317
+ kind: "layout",
1318
+ status: finding.status,
1319
+ message: finding.message,
1320
+ details: finding.details
1321
+ };
1322
+ }
1323
+ async function registeredProjectsOnTheOldFolder(home) {
1324
+ const loaded = await loadInstalledConfig(home).catch(() => null);
1325
+ return (loaded?.config.projects ?? []).filter(isOnTheLegacySourceFolder);
1326
+ }
1327
+ function isOnTheLegacySourceFolder(dir) {
1328
+ return resolveSourceDir(dir, providerInUse(dir)).legacy;
1329
+ }
1330
+ var PLACEMENTS_ROW_NAME = "Placements Offered";
1331
+ async function checkOfferedPlacements(projectDir) {
1332
+ const findings = await everythingThatWouldStopACommandActing(projectDir);
1333
+ if (findings.length === 0) {
1334
+ return { kind: "placements", status: "pass", message: "Every configured skill is placeable" };
1335
+ }
1336
+ return {
1337
+ kind: "placements",
1338
+ status: "fail",
1339
+ message: `${plural(findings.length, "finding")} would stop a command acting here`,
1340
+ details: findings.map((finding) => `- ${finding}`)
1341
+ };
1342
+ }
1343
+ async function everythingThatWouldStopACommandActing(projectDir) {
1344
+ const [unofferable, ambiguous] = await Promise.all([
1345
+ unofferablePlacementsFound(projectDir),
1346
+ ambiguityFinding(projectDir)
1347
+ ]);
1348
+ return ambiguous === null ? unofferable : [...unofferable, ambiguous];
1349
+ }
1166
1350
  function skippedResult(kind) {
1167
1351
  return { kind, status: "skip", message: "Skipped (config invalid)" };
1168
1352
  }
@@ -1202,7 +1386,7 @@ var Doctor = class _Doctor extends BaseCommand {
1202
1386
  const contentResults = await this.runContentChecks(projectDir);
1203
1387
  const operationalResults = await this.runOperationalChecks(projectDir, contentResults);
1204
1388
  const results = [...contentResults, ...operationalResults];
1205
- this.printResults(results);
1389
+ this.printResults(results, await recreateConfigTips(contentResults, projectDir));
1206
1390
  if (results.some((r) => r.status === "fail")) {
1207
1391
  this.exit(EXIT_CODES.ERROR);
1208
1392
  }
@@ -1265,12 +1449,18 @@ var Doctor = class _Doctor extends BaseCommand {
1265
1449
  * at all, not merely no usable one.
1266
1450
  */
1267
1451
  async isUninstalledSourceRepo(projectDir) {
1268
- if (await fileExists(getProjectConfigPath(projectDir))) return false;
1452
+ if (await fileExists(getInstalledConfigPath(projectDir))) return false;
1269
1453
  return isSourceRepo(projectDir);
1270
1454
  }
1271
1455
  async runAllChecks(projectDir, configState, failedContent) {
1272
- const { result: configResult, config } = checkConfigValid(configState);
1456
+ const { result: configResult, config } = checkConfigValid(configState, projectDir);
1273
1457
  this.logCheck("Config Valid", configResult);
1458
+ const layoutResults = await this.reportLayout(projectDir);
1459
+ const placementsResult = await safeCheck(
1460
+ "placements",
1461
+ () => checkOfferedPlacements(projectDir)
1462
+ );
1463
+ this.logCheck(PLACEMENTS_ROW_NAME, placementsResult);
1274
1464
  const sourceResult = await safeCheck("source", () => checkSourceReachable(projectDir));
1275
1465
  const filteredConfig = config ? filterExcludedEntries(config) : null;
1276
1466
  const skillsResult = await this.resolveSkillsCheck(
@@ -1291,6 +1481,8 @@ var Doctor = class _Doctor extends BaseCommand {
1291
1481
  this.logCheck("Marketplace Reachable", sourceResult);
1292
1482
  return [
1293
1483
  configResult,
1484
+ ...layoutResults,
1485
+ placementsResult,
1294
1486
  skillsResult,
1295
1487
  agentsResult,
1296
1488
  orphansResult,
@@ -1299,6 +1491,49 @@ var Doctor = class _Doctor extends BaseCommand {
1299
1491
  sourceResult
1300
1492
  ];
1301
1493
  }
1494
+ /**
1495
+ * One `Layout` row per scope in play, and — at the home directory — the registered projects
1496
+ * still on the old folder.
1497
+ *
1498
+ * Per scope rather than per machine: a project and the global installation can be on different
1499
+ * layouts, and one row for the two can only be right about one of them.
1500
+ *
1501
+ * Printed right under `Config Valid`, because every row below it is read out of a config file
1502
+ * whose folder this row is about — a scope with two rival folders has a config the rest of the
1503
+ * report may have read from either one.
1504
+ */
1505
+ async reportLayout(projectDir) {
1506
+ const scopes = await sourceScopesInPlay(projectDir);
1507
+ const rows = await Promise.all(
1508
+ scopes.map(async (scope) => toLayoutResult(await layoutFindingFor(scope)))
1509
+ );
1510
+ const withRegistry = await this.appendRegistryList(projectDir, rows);
1511
+ for (const row of withRegistry) {
1512
+ this.logCheck(LAYOUT_ROW_NAME, row);
1513
+ }
1514
+ return withRegistry;
1515
+ }
1516
+ /**
1517
+ * The registered projects still on the old folder, added to the LAST row, and only at HOME.
1518
+ *
1519
+ * At home because that is where the registry lives, and the move a legacy project needs is made
1520
+ * by hand in that project — a list of other people's projects printed inside one of them is a
1521
+ * list nobody standing there can act on. The caveat travels with it: the registry holds a
1522
+ * fraction of the installations on a machine, so a list printed without it reads as an
1523
+ * inventory.
1524
+ */
1525
+ async appendRegistryList(projectDir, rows) {
1526
+ const last = rows.at(-1);
1527
+ if (!isHomeDirectory(projectDir) || last === void 0) return rows;
1528
+ const legacyProjects = await registeredProjectsOnTheOldFolder(projectDir);
1529
+ if (legacyProjects.length === 0) return rows;
1530
+ const detailed = {
1531
+ ...last,
1532
+ status: last.status === "fail" ? "fail" : "warn",
1533
+ details: [...last.details ?? [], REGISTRY_IS_NOT_AN_INVENTORY, ...legacyProjects]
1534
+ };
1535
+ return [...rows.slice(0, -1), detailed];
1536
+ }
1302
1537
  /**
1303
1538
  * The orphan row, and which question it can answer. With a config it names the compiled
1304
1539
  * agents that config does not; with no config at all every installed file is unowned and it
@@ -1342,10 +1577,10 @@ var Doctor = class _Doctor extends BaseCommand {
1342
1577
  this.log(line);
1343
1578
  }
1344
1579
  }
1345
- printResults(results) {
1580
+ printResults(results, recreateTips) {
1346
1581
  this.log("");
1347
1582
  this.log(formatSummary(results));
1348
- const tips = formatTips(results);
1583
+ const tips = [...formatTips(results), ...recreateTips];
1349
1584
  if (tips.length > 0) {
1350
1585
  this.log("");
1351
1586
  for (const tip of tips) {