@uluops/setup 0.2.0 → 0.6.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 (253) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -89
  3. package/assets/auto-tracker-save.mjs +142 -0
  4. package/assets/claude-code/agents/anxiety-reader-agent.md +464 -0
  5. package/assets/{agents → claude-code/agents}/api-contract-validator-agent.md +9 -228
  6. package/assets/{agents → claude-code/agents}/aristotle-analyst-agent.md +51 -4
  7. package/assets/{agents → claude-code/agents}/aristotle-explorer-agent.md +6 -2
  8. package/assets/{agents → claude-code/agents}/aristotle-forecaster-agent.md +15 -230
  9. package/assets/{agents → claude-code/agents}/aristotle-validator-agent.md +12 -252
  10. package/assets/{agents → claude-code/agents}/assumption-excavator-agent.md +21 -247
  11. package/assets/{agents → claude-code/agents}/code-auditor-agent.md +12 -255
  12. package/assets/{agents → claude-code/agents}/code-optimizer-agent.md +15 -236
  13. package/assets/{agents → claude-code/agents}/code-validator-agent.md +31 -300
  14. package/assets/claude-code/agents/docs-validator-agent.md +472 -0
  15. package/assets/{agents → claude-code/agents}/frontend-validator-agent.md +15 -258
  16. package/assets/{agents → claude-code/agents}/mcp-validator-agent.md +8 -252
  17. package/assets/{agents → claude-code/agents}/pre-implementation-architect-agent.md +8 -224
  18. package/assets/{agents → claude-code/agents}/prompt-engineer-agent.md +57 -290
  19. package/assets/{agents → claude-code/agents}/prompt-pattern-analyzer-agent.md +10 -225
  20. package/assets/{agents → claude-code/agents}/prompt-quality-validator-agent.md +11 -249
  21. package/assets/{agents → claude-code/agents}/public-interface-validator-agent.md +15 -268
  22. package/assets/claude-code/agents/release-readiness-agent.md +495 -0
  23. package/assets/{agents → claude-code/agents}/security-analyst-agent.md +236 -480
  24. package/assets/{agents → claude-code/agents}/test-architect-agent.md +16 -259
  25. package/assets/{agents → claude-code/agents}/type-safety-validator-agent.md +23 -266
  26. package/assets/{agents → claude-code/agents}/workflow-synthesis-agent.md +23 -226
  27. package/assets/claude-code/commands/agents/anxiety-reader.md +157 -0
  28. package/assets/{commands → claude-code/commands}/agents/api-contract.md +156 -135
  29. package/assets/{commands → claude-code/commands}/agents/architect.md +156 -135
  30. package/assets/claude-code/commands/agents/aristotle-analyst.md +157 -0
  31. package/assets/claude-code/commands/agents/aristotle-explorer.md +157 -0
  32. package/assets/claude-code/commands/agents/aristotle-forecaster.md +157 -0
  33. package/assets/claude-code/commands/agents/aristotle-validator.md +157 -0
  34. package/assets/{commands → claude-code/commands}/agents/assumption-excavator.md +49 -6
  35. package/assets/{commands → claude-code/commands}/agents/audit.md +156 -136
  36. package/assets/{commands → claude-code/commands}/agents/docs-validate.md +156 -133
  37. package/assets/{commands → claude-code/commands}/agents/frontend.md +156 -135
  38. package/assets/{commands → claude-code/commands}/agents/mcp-validate.md +156 -136
  39. package/assets/{commands → claude-code/commands}/agents/optimize.md +156 -133
  40. package/assets/{commands → claude-code/commands}/agents/pattern-analyzer.md +150 -126
  41. package/assets/{commands → claude-code/commands}/agents/prompt-quality.md +155 -134
  42. package/assets/claude-code/commands/agents/prompt-validate.md +155 -0
  43. package/assets/{commands → claude-code/commands}/agents/public-interface.md +156 -134
  44. package/assets/{commands → claude-code/commands}/agents/release.md +156 -135
  45. package/assets/{commands → claude-code/commands}/agents/security.md +156 -137
  46. package/assets/{commands → claude-code/commands}/agents/test-review.md +156 -136
  47. package/assets/{commands → claude-code/commands}/agents/type-safety.md +156 -135
  48. package/assets/{commands → claude-code/commands}/agents/validate.md +156 -134
  49. package/assets/claude-code/commands/agents/workflow-synthesis.md +157 -0
  50. package/assets/claude-code/commands/pipelines/aristotle.md +143 -0
  51. package/assets/claude-code/commands/pipelines/ship.md +188 -0
  52. package/assets/claude-code/commands/workflows/post-implementation.md +60 -0
  53. package/assets/claude-code/commands/workflows/pre-implementation.md +46 -0
  54. package/assets/claude-code/commands/workflows/prompt-audit.md +44 -0
  55. package/assets/codex/agents/anxiety-reader-agent.toml +462 -0
  56. package/assets/codex/agents/api-contract-validator-agent.toml +738 -0
  57. package/assets/codex/agents/aristotle-analyst-agent.toml +750 -0
  58. package/assets/codex/agents/aristotle-explorer-agent.toml +155 -0
  59. package/assets/codex/agents/aristotle-forecaster-agent.toml +449 -0
  60. package/assets/codex/agents/aristotle-validator-agent.toml +424 -0
  61. package/assets/codex/agents/assumption-excavator-agent.toml +1126 -0
  62. package/assets/codex/agents/code-auditor-agent.toml +815 -0
  63. package/assets/codex/agents/code-optimizer-agent.toml +652 -0
  64. package/assets/codex/agents/code-validator-agent.toml +573 -0
  65. package/assets/codex/agents/docs-validator-agent.toml +468 -0
  66. package/assets/codex/agents/frontend-validator-agent.toml +598 -0
  67. package/assets/codex/agents/mcp-validator-agent.toml +580 -0
  68. package/assets/codex/agents/pre-implementation-architect-agent.toml +817 -0
  69. package/assets/codex/agents/prompt-engineer-agent.toml +922 -0
  70. package/assets/codex/agents/prompt-pattern-analyzer-agent.toml +689 -0
  71. package/assets/codex/agents/prompt-quality-validator-agent.toml +777 -0
  72. package/assets/codex/agents/public-interface-validator-agent.toml +695 -0
  73. package/assets/codex/agents/release-readiness-agent.toml +491 -0
  74. package/assets/codex/agents/security-analyst-agent.toml +847 -0
  75. package/assets/codex/agents/test-architect-agent.toml +615 -0
  76. package/assets/codex/agents/type-safety-validator-agent.toml +686 -0
  77. package/assets/codex/agents/workflow-synthesis-agent.toml +631 -0
  78. package/assets/gemini-cli/agents/anxiety-reader-agent.md +470 -0
  79. package/assets/gemini-cli/agents/api-contract-validator-agent.md +747 -0
  80. package/assets/gemini-cli/agents/aristotle-analyst-agent.md +758 -0
  81. package/assets/gemini-cli/agents/aristotle-explorer-agent.md +163 -0
  82. package/assets/gemini-cli/agents/aristotle-forecaster-agent.md +457 -0
  83. package/assets/gemini-cli/agents/aristotle-validator-agent.md +432 -0
  84. package/assets/gemini-cli/agents/assumption-excavator-agent.md +1134 -0
  85. package/assets/gemini-cli/agents/code-auditor-agent.md +827 -0
  86. package/assets/gemini-cli/agents/code-optimizer-agent.md +661 -0
  87. package/assets/gemini-cli/agents/code-validator-agent.md +582 -0
  88. package/assets/gemini-cli/agents/docs-validator-agent.md +477 -0
  89. package/assets/gemini-cli/agents/frontend-validator-agent.md +610 -0
  90. package/assets/gemini-cli/agents/mcp-validator-agent.md +589 -0
  91. package/assets/gemini-cli/agents/pre-implementation-architect-agent.md +826 -0
  92. package/assets/gemini-cli/agents/prompt-engineer-agent.md +931 -0
  93. package/assets/gemini-cli/agents/prompt-pattern-analyzer-agent.md +698 -0
  94. package/assets/gemini-cli/agents/prompt-quality-validator-agent.md +786 -0
  95. package/assets/gemini-cli/agents/public-interface-validator-agent.md +707 -0
  96. package/assets/gemini-cli/agents/release-readiness-agent.md +500 -0
  97. package/assets/gemini-cli/agents/security-analyst-agent.md +859 -0
  98. package/assets/gemini-cli/agents/test-architect-agent.md +624 -0
  99. package/assets/gemini-cli/agents/type-safety-validator-agent.md +695 -0
  100. package/assets/gemini-cli/agents/workflow-synthesis-agent.md +639 -0
  101. package/assets/gemini-cli/commands/agents/anxiety-reader.toml +155 -0
  102. package/assets/gemini-cli/commands/agents/api-contract.toml +154 -0
  103. package/assets/gemini-cli/commands/agents/architect.toml +154 -0
  104. package/assets/gemini-cli/commands/agents/aristotle-analyst.toml +155 -0
  105. package/assets/gemini-cli/commands/agents/aristotle-explorer.toml +155 -0
  106. package/assets/gemini-cli/commands/agents/aristotle-forecaster.toml +155 -0
  107. package/assets/gemini-cli/commands/agents/aristotle-validator.toml +155 -0
  108. package/assets/gemini-cli/commands/agents/assumption-excavator.toml +155 -0
  109. package/assets/gemini-cli/commands/agents/audit.toml +154 -0
  110. package/assets/gemini-cli/commands/agents/docs-validate.toml +154 -0
  111. package/assets/gemini-cli/commands/agents/frontend.toml +154 -0
  112. package/assets/gemini-cli/commands/agents/mcp-validate.toml +154 -0
  113. package/assets/gemini-cli/commands/agents/optimize.toml +154 -0
  114. package/assets/gemini-cli/commands/agents/pattern-analyzer.toml +148 -0
  115. package/assets/gemini-cli/commands/agents/prompt-quality.toml +153 -0
  116. package/assets/gemini-cli/commands/agents/prompt-validate.toml +153 -0
  117. package/assets/gemini-cli/commands/agents/public-interface.toml +154 -0
  118. package/assets/gemini-cli/commands/agents/release.toml +154 -0
  119. package/assets/gemini-cli/commands/agents/security.toml +154 -0
  120. package/assets/gemini-cli/commands/agents/test-review.toml +154 -0
  121. package/assets/gemini-cli/commands/agents/type-safety.toml +154 -0
  122. package/assets/gemini-cli/commands/agents/validate.toml +154 -0
  123. package/assets/gemini-cli/commands/agents/workflow-synthesis.toml +155 -0
  124. package/assets/gemini-cli/commands/pipelines/aristotle.toml +139 -0
  125. package/assets/gemini-cli/commands/pipelines/ship.toml +184 -0
  126. package/assets/gemini-cli/commands/workflows/post-implementation.toml +56 -0
  127. package/assets/gemini-cli/commands/workflows/pre-implementation.toml +42 -0
  128. package/assets/gemini-cli/commands/workflows/prompt-audit.toml +40 -0
  129. package/assets/opencode/agents/anxiety-reader-agent.md +472 -0
  130. package/assets/opencode/agents/api-contract-validator-agent.md +749 -0
  131. package/assets/opencode/agents/aristotle-analyst-agent.md +760 -0
  132. package/assets/opencode/agents/aristotle-explorer-agent.md +164 -0
  133. package/assets/opencode/agents/aristotle-forecaster-agent.md +459 -0
  134. package/assets/opencode/agents/aristotle-validator-agent.md +434 -0
  135. package/assets/opencode/agents/assumption-excavator-agent.md +1136 -0
  136. package/assets/opencode/agents/code-auditor-agent.md +826 -0
  137. package/assets/opencode/agents/code-optimizer-agent.md +663 -0
  138. package/assets/opencode/agents/code-validator-agent.md +584 -0
  139. package/assets/opencode/agents/docs-validator-agent.md +479 -0
  140. package/assets/opencode/agents/frontend-validator-agent.md +609 -0
  141. package/assets/opencode/agents/mcp-validator-agent.md +591 -0
  142. package/assets/opencode/agents/pre-implementation-architect-agent.md +828 -0
  143. package/assets/opencode/agents/prompt-engineer-agent.md +933 -0
  144. package/assets/opencode/agents/prompt-pattern-analyzer-agent.md +700 -0
  145. package/assets/opencode/agents/prompt-quality-validator-agent.md +788 -0
  146. package/assets/opencode/agents/public-interface-validator-agent.md +706 -0
  147. package/assets/opencode/agents/release-readiness-agent.md +502 -0
  148. package/assets/opencode/agents/security-analyst-agent.md +858 -0
  149. package/assets/opencode/agents/test-architect-agent.md +626 -0
  150. package/assets/opencode/agents/type-safety-validator-agent.md +697 -0
  151. package/assets/opencode/agents/workflow-synthesis-agent.md +641 -0
  152. package/dist/cli.js +22 -380
  153. package/dist/commands/helpers.d.ts +73 -0
  154. package/dist/commands/helpers.js +274 -0
  155. package/dist/commands/setup.d.ts +13 -0
  156. package/dist/commands/setup.js +93 -0
  157. package/dist/commands/uninstall.d.ts +3 -0
  158. package/dist/commands/uninstall.js +126 -0
  159. package/dist/commands/verify.d.ts +1 -0
  160. package/dist/commands/verify.js +28 -0
  161. package/dist/harnesses/claude-code.d.ts +8 -0
  162. package/dist/harnesses/claude-code.js +74 -0
  163. package/dist/harnesses/codex.d.ts +15 -0
  164. package/dist/harnesses/codex.js +54 -0
  165. package/dist/harnesses/gemini-cli.d.ts +12 -0
  166. package/dist/harnesses/gemini-cli.js +80 -0
  167. package/dist/harnesses/index.d.ts +27 -0
  168. package/dist/harnesses/index.js +54 -0
  169. package/dist/harnesses/opencode.d.ts +14 -0
  170. package/dist/harnesses/opencode.js +139 -0
  171. package/dist/harnesses/types.d.ts +106 -0
  172. package/dist/harnesses/types.js +26 -0
  173. package/dist/lib/agent-transform.d.ts +12 -0
  174. package/dist/lib/agent-transform.js +129 -0
  175. package/dist/lib/asset-catalog.d.ts +9 -0
  176. package/dist/lib/asset-catalog.js +56 -0
  177. package/dist/lib/atomic-write.d.ts +11 -0
  178. package/dist/lib/atomic-write.js +28 -0
  179. package/dist/lib/config-merger.d.ts +9 -2
  180. package/dist/lib/config-merger.js +44 -7
  181. package/dist/lib/display.d.ts +14 -0
  182. package/dist/lib/display.js +66 -0
  183. package/dist/lib/file-ops.d.ts +11 -0
  184. package/dist/lib/file-ops.js +40 -4
  185. package/dist/lib/hash.d.ts +1 -0
  186. package/dist/lib/hash.js +2 -1
  187. package/dist/lib/health.d.ts +2 -0
  188. package/dist/lib/health.js +10 -0
  189. package/dist/lib/manifest.d.ts +51 -5
  190. package/dist/lib/manifest.js +146 -13
  191. package/dist/lib/paths.d.ts +30 -3
  192. package/dist/lib/paths.js +98 -12
  193. package/dist/lib/settings-merger.d.ts +31 -8
  194. package/dist/lib/settings-merger.js +87 -24
  195. package/dist/lib/version.d.ts +2 -0
  196. package/dist/lib/version.js +10 -0
  197. package/dist/steps/agents.d.ts +4 -1
  198. package/dist/steps/agents.js +48 -9
  199. package/dist/steps/auth.js +26 -10
  200. package/dist/steps/cli.d.ts +53 -0
  201. package/dist/steps/cli.js +90 -0
  202. package/dist/steps/commands.d.ts +6 -1
  203. package/dist/steps/commands.js +36 -9
  204. package/dist/steps/detect.d.ts +3 -0
  205. package/dist/steps/detect.js +11 -0
  206. package/dist/steps/mcp.d.ts +6 -2
  207. package/dist/steps/mcp.js +39 -22
  208. package/dist/steps/metrics.d.ts +26 -10
  209. package/dist/steps/metrics.js +108 -108
  210. package/dist/steps/shell.d.ts +2 -0
  211. package/dist/steps/shell.js +26 -9
  212. package/dist/steps/signup.d.ts +7 -4
  213. package/dist/steps/signup.js +29 -20
  214. package/dist/steps/verify.d.ts +2 -2
  215. package/dist/steps/verify.js +118 -112
  216. package/package.json +40 -14
  217. package/assets/agents/docs-validator-agent.md +0 -490
  218. package/assets/agents/release-readiness-agent.md +0 -482
  219. package/assets/commands/agents/aristotle-analyst.md +0 -115
  220. package/assets/commands/agents/aristotle-explorer.md +0 -92
  221. package/assets/commands/agents/aristotle-forecaster.md +0 -114
  222. package/assets/commands/agents/aristotle-validator.md +0 -114
  223. package/assets/commands/agents/prompt-validate.md +0 -135
  224. package/assets/commands/agents/workflow-synthesis.md +0 -101
  225. package/assets/commands/workflows/aristotle.md +0 -543
  226. package/assets/commands/workflows/post-implementation.md +0 -577
  227. package/assets/commands/workflows/pre-implementation.md +0 -670
  228. package/assets/commands/workflows/prompt-audit.md +0 -754
  229. package/assets/commands/workflows/ship.md +0 -721
  230. package/dist/test/auth.test.d.ts +0 -1
  231. package/dist/test/auth.test.js +0 -43
  232. package/dist/test/config-io.test.d.ts +0 -1
  233. package/dist/test/config-io.test.js +0 -56
  234. package/dist/test/config-merger.test.d.ts +0 -1
  235. package/dist/test/config-merger.test.js +0 -94
  236. package/dist/test/detect.test.d.ts +0 -1
  237. package/dist/test/detect.test.js +0 -25
  238. package/dist/test/file-ops.test.d.ts +0 -1
  239. package/dist/test/file-ops.test.js +0 -100
  240. package/dist/test/hash.test.d.ts +0 -1
  241. package/dist/test/hash.test.js +0 -14
  242. package/dist/test/manifest.test.d.ts +0 -1
  243. package/dist/test/manifest.test.js +0 -78
  244. package/dist/test/paths.test.d.ts +0 -1
  245. package/dist/test/paths.test.js +0 -30
  246. package/dist/test/settings-merger.test.d.ts +0 -1
  247. package/dist/test/settings-merger.test.js +0 -167
  248. package/dist/test/shell-profile.test.d.ts +0 -1
  249. package/dist/test/shell-profile.test.js +0 -40
  250. package/dist/test/shell.test.d.ts +0 -1
  251. package/dist/test/shell.test.js +0 -71
  252. package/dist/test/signup.test.d.ts +0 -1
  253. package/dist/test/signup.test.js +0 -83
@@ -0,0 +1,56 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { ASSETS_DIR } from "./paths.js";
4
+ /**
5
+ * Parse YAML-like frontmatter from a markdown file.
6
+ * Returns key-value pairs between the opening and closing `---`.
7
+ */
8
+ function parseFrontmatter(content) {
9
+ const lines = content.split("\n");
10
+ if (lines[0]?.trim() !== "---")
11
+ return {};
12
+ const result = {};
13
+ for (let i = 1; i < lines.length; i++) {
14
+ const line = lines[i];
15
+ if (line.trim() === "---")
16
+ break;
17
+ const colonIdx = line.indexOf(":");
18
+ if (colonIdx > 0) {
19
+ const key = line.slice(0, colonIdx).trim();
20
+ const value = line.slice(colonIdx + 1).trim();
21
+ result[key] = value;
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+ /** Scan a commands subdirectory and return sorted entries with frontmatter metadata. */
27
+ async function scanCommandDir(dir) {
28
+ let files;
29
+ try {
30
+ files = await readdir(dir);
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ const entries = [];
36
+ for (const file of files.filter((f) => f.endsWith(".md"))) {
37
+ const content = await readFile(join(dir, file), "utf-8");
38
+ const fm = parseFrontmatter(content);
39
+ if (fm["name"]) {
40
+ entries.push({
41
+ name: fm["name"],
42
+ description: fm["description"] ?? "",
43
+ model: fm["model"] ?? "sonnet",
44
+ });
45
+ }
46
+ }
47
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
48
+ }
49
+ /** Get all agent command entries from assets. */
50
+ export async function getAgentCommands() {
51
+ return scanCommandDir(join(ASSETS_DIR, "claude-code", "commands", "agents"));
52
+ }
53
+ /** Get all workflow command entries from assets. */
54
+ export async function getWorkflowCommands() {
55
+ return scanCommandDir(join(ASSETS_DIR, "claude-code", "commands", "workflows"));
56
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Atomic Write
3
+ *
4
+ * Write-to-temp-then-rename pattern to prevent partial writes
5
+ * from corrupting user config files.
6
+ */
7
+ export interface AtomicWriteOptions {
8
+ /** File mode (permissions). Defaults to Node's default (0o666 before umask). */
9
+ mode?: number;
10
+ }
11
+ export declare function atomicWrite(path: string, content: string, options?: AtomicWriteOptions): Promise<void>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Atomic Write
3
+ *
4
+ * Write-to-temp-then-rename pattern to prevent partial writes
5
+ * from corrupting user config files.
6
+ */
7
+ import { writeFile, rename, unlink, chmod } from "node:fs/promises";
8
+ export async function atomicWrite(path, content, options) {
9
+ const tmp = `${path}.uluops-tmp`;
10
+ try {
11
+ await writeFile(tmp, content, { encoding: "utf-8", mode: options?.mode });
12
+ if (options?.mode) {
13
+ // Ensure mode is applied even if umask is permissive
14
+ await chmod(tmp, options.mode);
15
+ }
16
+ await rename(tmp, path);
17
+ }
18
+ catch (err) {
19
+ // Clean up temp file on failure
20
+ try {
21
+ await unlink(tmp);
22
+ }
23
+ catch {
24
+ // Temp file may not exist
25
+ }
26
+ throw err;
27
+ }
28
+ }
@@ -2,19 +2,26 @@ interface McpServerConfig {
2
2
  command: string;
3
3
  args: string[];
4
4
  env: Record<string, string>;
5
+ trust?: boolean;
5
6
  }
6
- interface ClaudeConfig {
7
+ export interface ClaudeConfig {
7
8
  mcpServers?: Record<string, McpServerConfig>;
8
9
  [key: string]: unknown;
9
10
  }
11
+ /** Check whether the UluOps MCP client packages exist on the npm registry. Returns lists of available and missing packages. */
12
+ export declare function checkMcpPackageAvailability(): Promise<{
13
+ available: string[];
14
+ missing: string[];
15
+ }>;
10
16
  /**
11
17
  * Read an existing config file, or return empty object if it doesn't exist.
18
+ * Throws on malformed JSON to prevent silent data loss during merge+write.
12
19
  */
13
20
  export declare function readConfig(path: string): Promise<ClaudeConfig>;
14
21
  /**
15
22
  * Merge UluOps MCP server entries into a config, preserving all other keys.
16
23
  */
17
- export declare function mergeUluopsMcp(config: ClaudeConfig, apiKey: string): ClaudeConfig;
24
+ export declare function mergeUluopsMcp(config: ClaudeConfig, apiKey: string, trust?: boolean): ClaudeConfig;
18
25
  /**
19
26
  * Remove UluOps MCP server entries from a config.
20
27
  */
@@ -1,21 +1,54 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { readFile } from "node:fs/promises";
2
+ import { atomicWrite } from "./atomic-write.js";
3
+ const MCP_PACKAGES = ["uluops-tracker-mcp-client", "uluops-registry-mcp-client"];
4
+ /** Check whether the UluOps MCP client packages exist on the npm registry. Returns lists of available and missing packages. */
5
+ export async function checkMcpPackageAvailability() {
6
+ const available = [];
7
+ const missing = [];
8
+ const results = await Promise.allSettled(MCP_PACKAGES.map((pkg) => fetch(`https://registry.npmjs.org/${pkg}`, {
9
+ method: "HEAD",
10
+ signal: AbortSignal.timeout(5000),
11
+ redirect: "follow",
12
+ }).then((res) => ({ pkg, ok: res.ok }))));
13
+ for (let i = 0; i < results.length; i++) {
14
+ const result = results[i];
15
+ if (result.status === "fulfilled" && result.value.ok) {
16
+ available.push(result.value.pkg);
17
+ }
18
+ else {
19
+ const pkg = result.status === "fulfilled"
20
+ ? result.value.pkg
21
+ : MCP_PACKAGES[i] ?? "unknown";
22
+ missing.push(pkg);
23
+ }
24
+ }
25
+ return { available, missing };
26
+ }
2
27
  /**
3
28
  * Read an existing config file, or return empty object if it doesn't exist.
29
+ * Throws on malformed JSON to prevent silent data loss during merge+write.
4
30
  */
5
31
  export async function readConfig(path) {
32
+ let raw;
33
+ try {
34
+ raw = await readFile(path, "utf-8");
35
+ }
36
+ catch {
37
+ return {}; // File doesn't exist — fresh config
38
+ }
6
39
  try {
7
- const raw = await readFile(path, "utf-8");
8
40
  return JSON.parse(raw);
9
41
  }
10
42
  catch {
11
- return {};
43
+ throw new Error(`Failed to parse config at ${path} — file contains invalid JSON`);
12
44
  }
13
45
  }
14
46
  /**
15
47
  * Merge UluOps MCP server entries into a config, preserving all other keys.
16
48
  */
17
- export function mergeUluopsMcp(config, apiKey) {
49
+ export function mergeUluopsMcp(config, apiKey, trust = false) {
18
50
  const existing = config.mcpServers ?? {};
51
+ const trustField = trust ? { trust: true } : {};
19
52
  return {
20
53
  ...config,
21
54
  mcpServers: {
@@ -24,9 +57,10 @@ export function mergeUluopsMcp(config, apiKey) {
24
57
  command: "npx",
25
58
  args: ["-y", "uluops-tracker-mcp-client"],
26
59
  env: {
27
- ULUOPS_TRACKER_API_URL: "https://api.uluops.ai/api/v1",
28
- ULUOPS_TRACKER_API_KEY: apiKey,
60
+ ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1",
61
+ ULUOPS_API_KEY: apiKey,
29
62
  },
63
+ ...trustField,
30
64
  },
31
65
  "uluops-registry": {
32
66
  command: "npx",
@@ -35,6 +69,7 @@ export function mergeUluopsMcp(config, apiKey) {
35
69
  ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry",
36
70
  ULUOPS_API_KEY: apiKey,
37
71
  },
72
+ ...trustField,
38
73
  },
39
74
  },
40
75
  };
@@ -59,5 +94,7 @@ export function removeUluopsMcp(config) {
59
94
  * Write config back to file, preserving formatting.
60
95
  */
61
96
  export async function writeConfig(path, config) {
62
- await writeFile(path, JSON.stringify(config, null, 2) + "\n");
97
+ await atomicWrite(path, JSON.stringify(config, null, 2) + "\n", {
98
+ mode: 0o600,
99
+ });
63
100
  }
@@ -0,0 +1,14 @@
1
+ import type { HarnessProfile } from "../harnesses/index.js";
2
+ declare const ok: (msg: string) => void;
3
+ declare const warn: (msg: string) => void;
4
+ declare const fail: (msg: string) => void;
5
+ declare const info: (msg: string) => void;
6
+ export { ok, warn, fail, info };
7
+ export declare function printSetupSummary(opts: {
8
+ profile: HarnessProfile;
9
+ agentCount: number;
10
+ commandCount: number;
11
+ apiKey: string;
12
+ }): Promise<void>;
13
+ export declare function maskKey(key: string): string;
14
+ export declare function printAgentList(): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import chalk from "chalk";
2
+ import { getAgentCommands, getWorkflowCommands } from "./asset-catalog.js";
3
+ const ok = (msg) => console.log(` ${chalk.green("✓")} ${msg}`);
4
+ const warn = (msg) => console.log(` ${chalk.yellow("⚠")} ${msg}`);
5
+ const fail = (msg) => console.log(` ${chalk.red("✗")} ${msg}`);
6
+ const info = (msg) => console.log(` ${msg}`);
7
+ export { ok, warn, fail, info };
8
+ export async function printSetupSummary(opts) {
9
+ console.log();
10
+ console.log(` ${chalk.dim("━".repeat(46))}`);
11
+ console.log();
12
+ const parts = [`${opts.agentCount} agents`];
13
+ if (opts.commandCount > 0)
14
+ parts.push(`${opts.commandCount} slash commands`);
15
+ if (opts.profile.hooks)
16
+ parts.push("metrics");
17
+ console.log(` ${chalk.bold("Setup complete!")} ${chalk.dim(`(${opts.profile.displayName})`)} ${parts.join(" · ")}`);
18
+ console.log();
19
+ if (opts.profile.name === "claude-code") {
20
+ await printAgentList();
21
+ }
22
+ const masked = maskKey(opts.apiKey);
23
+ info("For SDK/CLI usage, add to your shell profile:");
24
+ info(` ${chalk.cyan(`export ULUOPS_API_KEY="${masked}"`)}`);
25
+ console.log();
26
+ info(`Run again to update: ${chalk.cyan("npx @uluops/setup")}`);
27
+ console.log();
28
+ console.log(` ${chalk.dim("━".repeat(46))}`);
29
+ console.log();
30
+ console.log(` ${chalk.yellow.bold(`Restart ${opts.profile.displayName} to load agents.`)}`);
31
+ console.log();
32
+ }
33
+ export function maskKey(key) {
34
+ if (!key || key.length <= 4)
35
+ return "****";
36
+ const last4 = key.slice(-4);
37
+ return `${"*".repeat(Math.max(4, key.length - 4))}${last4}`;
38
+ }
39
+ export async function printAgentList() {
40
+ const workflows = await getWorkflowCommands();
41
+ const agents = await getAgentCommands();
42
+ if (workflows.length > 0) {
43
+ info(chalk.bold("WORKFLOWS"));
44
+ for (const wf of workflows) {
45
+ const cmd = `/workflows:${wf.name}`;
46
+ const desc = wf.description.length > 40
47
+ ? wf.description.slice(0, 37) + "..."
48
+ : wf.description;
49
+ info(` ${chalk.cyan(cmd.padEnd(34))}${desc}`);
50
+ }
51
+ console.log();
52
+ }
53
+ if (agents.length > 0) {
54
+ info(`${chalk.bold("AGENTS")} (run individually)${" ".repeat(26)}${chalk.dim("MODEL")}`);
55
+ for (const agent of agents) {
56
+ const cmd = `/agents:${agent.name}`;
57
+ const desc = agent.description.length > 17
58
+ ? agent.description.slice(0, 14) + "..."
59
+ : agent.description;
60
+ info(` ${chalk.cyan(cmd.padEnd(34))}${desc.padEnd(17)}${chalk.dim(agent.model)}`);
61
+ }
62
+ console.log();
63
+ }
64
+ info(chalk.dim(` This is the starter set. Browse more agents at registry.uluops.ai`));
65
+ console.log();
66
+ }
@@ -2,6 +2,11 @@
2
2
  * Copy a file if its content has changed (hash comparison). Returns "copied" or "skipped".
3
3
  */
4
4
  export declare function copyIfChanged(srcPath: string, destPath: string, dryRun: boolean): Promise<"copied" | "skipped">;
5
+ /**
6
+ * Write content to a file if it differs from the current content (hash comparison).
7
+ * Returns "copied" or "skipped".
8
+ */
9
+ export declare function writeIfChanged(destPath: string, content: string, dryRun: boolean): Promise<"copied" | "skipped">;
5
10
  /**
6
11
  * Remove files from a directory. Returns count of successfully removed files.
7
12
  */
@@ -14,6 +19,7 @@ export declare function syncAssets(opts: {
14
19
  srcDir: string;
15
20
  destDir: string;
16
21
  dryRun: boolean;
22
+ extension?: string;
17
23
  oldManifestFiles?: string[];
18
24
  }): Promise<{
19
25
  copied: number;
@@ -21,3 +27,8 @@ export declare function syncAssets(opts: {
21
27
  removed: number;
22
28
  files: string[];
23
29
  }>;
30
+ /**
31
+ * Back up a file to the UluOps backup directory before modifying it.
32
+ * No-op if the source file doesn't exist.
33
+ */
34
+ export declare function backupFile(srcPath: string, backupDir: string): Promise<void>;
@@ -1,5 +1,5 @@
1
- import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { readFile, writeFile, mkdir, unlink, access, copyFile, readdir } from "node:fs/promises";
2
+ import { join, basename } from "node:path";
3
3
  import { fileHash } from "./hash.js";
4
4
  /**
5
5
  * Copy a file if its content has changed (hash comparison). Returns "copied" or "skipped".
@@ -21,6 +21,26 @@ export async function copyIfChanged(srcPath, destPath, dryRun) {
21
21
  }
22
22
  return "copied";
23
23
  }
24
+ /**
25
+ * Write content to a file if it differs from the current content (hash comparison).
26
+ * Returns "copied" or "skipped".
27
+ */
28
+ export async function writeIfChanged(destPath, content, dryRun) {
29
+ const newHash = fileHash(content);
30
+ try {
31
+ const existing = await readFile(destPath, "utf-8");
32
+ if (newHash === fileHash(existing)) {
33
+ return "skipped";
34
+ }
35
+ }
36
+ catch {
37
+ // File doesn't exist yet
38
+ }
39
+ if (!dryRun) {
40
+ await writeFile(destPath, content);
41
+ }
42
+ return "copied";
43
+ }
24
44
  /**
25
45
  * Remove files from a directory. Returns count of successfully removed files.
26
46
  */
@@ -42,11 +62,11 @@ export async function unlinkFiles(dir, files) {
42
62
  * Returns list of copied files, skipped count, and removed count (for old manifest entries).
43
63
  */
44
64
  export async function syncAssets(opts) {
45
- const { readdir } = await import("node:fs/promises");
46
65
  if (!opts.dryRun) {
47
66
  await mkdir(opts.destDir, { recursive: true });
48
67
  }
49
- const assetFiles = (await readdir(opts.srcDir)).filter((f) => f.endsWith(".md"));
68
+ const ext = opts.extension ?? ".md";
69
+ const assetFiles = (await readdir(opts.srcDir)).filter((f) => f.endsWith(ext));
50
70
  let copied = 0;
51
71
  let skipped = 0;
52
72
  const errors = [];
@@ -84,3 +104,19 @@ export async function syncAssets(opts) {
84
104
  }
85
105
  return { copied, skipped, removed, files: assetFiles };
86
106
  }
107
+ /**
108
+ * Back up a file to the UluOps backup directory before modifying it.
109
+ * No-op if the source file doesn't exist.
110
+ */
111
+ export async function backupFile(srcPath, backupDir) {
112
+ try {
113
+ await access(srcPath);
114
+ }
115
+ catch {
116
+ return; // Nothing to back up
117
+ }
118
+ await mkdir(backupDir, { recursive: true });
119
+ const filename = basename(srcPath);
120
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
121
+ await copyFile(srcPath, join(backupDir, `${filename}.${timestamp}.bak`));
122
+ }
@@ -1 +1,2 @@
1
+ /** Return the full SHA-256 hash (64 hex chars) of the given string content. */
1
2
  export declare function fileHash(content: string): string;
package/dist/lib/hash.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ /** Return the full SHA-256 hash (64 hex chars) of the given string content. */
2
3
  export function fileHash(content) {
3
- return createHash("sha256").update(content).digest("hex").slice(0, 12);
4
+ return createHash("sha256").update(content).digest("hex");
4
5
  }
@@ -0,0 +1,2 @@
1
+ /** Shared health check timeout, configurable via ULUOPS_HEALTH_TIMEOUT env var. */
2
+ export declare function getHealthTimeout(): number;
@@ -0,0 +1,10 @@
1
+ /** Shared health check timeout, configurable via ULUOPS_HEALTH_TIMEOUT env var. */
2
+ export function getHealthTimeout() {
3
+ const env = process.env["ULUOPS_HEALTH_TIMEOUT"];
4
+ if (env) {
5
+ const ms = Number(env);
6
+ if (Number.isFinite(ms) && ms > 0)
7
+ return ms;
8
+ }
9
+ return 10_000;
10
+ }
@@ -1,16 +1,62 @@
1
- export interface Manifest {
2
- version: string;
1
+ /**
2
+ * Identifier for a single harness installation in the manifest.
3
+ *
4
+ * Today this is the profile name (e.g. `"claude-code"`), assuming a
5
+ * one-to-one mapping between profiles and installations. The day that
6
+ * assumption breaks — multiple Claude Code installs (project vs global,
7
+ * NVM versions, parallel beta channels) — this becomes the seam where
8
+ * fracture surfaces. When that happens, the right migration is to extend
9
+ * keys to `{profile.name}@{stable-instance-id}` (e.g. a hash of toolsDir)
10
+ * rather than overwriting silently. The type alias exists so the
11
+ * assumption is documented in the schema, not the comments.
12
+ */
13
+ export type HarnessInstanceKey = string;
14
+ /** Per-harness installation state. */
15
+ export interface HarnessManifest {
3
16
  installedAt: string;
17
+ setupVersion: string;
4
18
  mcpScope: "global" | "local";
5
19
  mcpConfigPath: string;
6
20
  defsScope: "global" | "local";
7
21
  defsPath: string;
8
- shellModified: boolean;
9
22
  agents: string[];
10
23
  commands: string[];
11
- /** Whether agent-metrics hook is configured */
12
- metricsHookInstalled?: boolean;
24
+ hooksInstalled: boolean;
25
+ /**
26
+ * Version of @uluops/agent-metrics whose dist/ was copied into the harness tree.
27
+ * Null when hooks are not installed or when the manifest predates this field.
28
+ * Verify uses this to detect drift between installed and currently-resolvable versions —
29
+ * the shared version ledger across the setup↔agent-metrics seam.
30
+ */
31
+ hooksInstalledVersion?: string | null;
32
+ }
33
+ /** Top-level manifest with per-harness entries. */
34
+ export interface Manifest {
35
+ version: string;
36
+ installedAt: string;
37
+ shellModified: boolean;
38
+ harnesses: Record<HarnessInstanceKey, HarnessManifest>;
39
+ /**
40
+ * Tracks whether `@uluops/cli` was installed globally by this setup run.
41
+ * Null/false when not installed by setup (user-installed or never installed).
42
+ * Uninstall only removes the global package when this is true — we don't
43
+ * remove what we didn't install.
44
+ */
45
+ cliInstalled?: boolean;
46
+ /** Version reported by `ulu --version` at install time, for drift detection. */
47
+ cliInstalledVersion?: string | null;
48
+ contentHash?: string;
49
+ }
50
+ export interface ManifestValidationResult {
51
+ valid: boolean;
52
+ errors: string[];
53
+ warnings: string[];
13
54
  }
55
+ /** Validate a manifest against the current filesystem state. */
56
+ export declare function validateManifest(manifest: Manifest): Promise<ManifestValidationResult>;
57
+ /** Load the install manifest. Tries new location first, falls back to legacy, auto-migrates. */
14
58
  export declare function loadManifest(): Promise<Manifest | null>;
59
+ /** Save the install manifest to ~/.uluops/manifest.json. Creates directory if needed. */
15
60
  export declare function saveManifest(manifest: Manifest): Promise<void>;
61
+ /** Delete the install manifest file from disk. Tries both locations. */
16
62
  export declare function deleteManifest(): Promise<void>;
@@ -1,6 +1,32 @@
1
- import { readFile, writeFile, unlink } from "node:fs/promises";
2
- import { getManifestPath } from "./paths.js";
3
- function isManifest(obj) {
1
+ import { readFile, unlink, access, mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { getManifestPath, getLegacyManifestPath, getUluopsDir } from "./paths.js";
4
+ import { fileHash } from "./hash.js";
5
+ import { atomicWrite } from "./atomic-write.js";
6
+ function isNewManifest(obj) {
7
+ if (typeof obj !== "object" || obj === null)
8
+ return false;
9
+ const m = obj;
10
+ if (typeof m["version"] !== "string" ||
11
+ typeof m["installedAt"] !== "string" ||
12
+ typeof m["shellModified"] !== "boolean" ||
13
+ typeof m["harnesses"] !== "object" ||
14
+ m["harnesses"] === null)
15
+ return false;
16
+ // Validate at least one harness entry has required fields
17
+ const harnesses = m["harnesses"];
18
+ for (const h of Object.values(harnesses)) {
19
+ if (typeof h !== "object" || h === null)
20
+ return false;
21
+ const hm = h;
22
+ if (typeof hm["mcpConfigPath"] !== "string" || typeof hm["defsPath"] !== "string")
23
+ return false;
24
+ if (!Array.isArray(hm["agents"]) || !Array.isArray(hm["commands"]))
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ function isLegacyManifest(obj) {
4
30
  if (typeof obj !== "object" || obj === null)
5
31
  return false;
6
32
  const m = obj;
@@ -9,26 +35,133 @@ function isManifest(obj) {
9
35
  typeof m["mcpConfigPath"] === "string" &&
10
36
  typeof m["defsPath"] === "string" &&
11
37
  Array.isArray(m["agents"]) &&
12
- Array.isArray(m["commands"]));
38
+ Array.isArray(m["commands"]) &&
39
+ !("harnesses" in m));
13
40
  }
14
- export async function loadManifest() {
41
+ function migrateManifest(old) {
42
+ return {
43
+ version: old.version,
44
+ installedAt: old.installedAt,
45
+ shellModified: old.shellModified,
46
+ harnesses: {
47
+ "claude-code": {
48
+ installedAt: old.installedAt,
49
+ setupVersion: old.version,
50
+ mcpScope: old.mcpScope,
51
+ mcpConfigPath: old.mcpConfigPath,
52
+ defsScope: old.defsScope,
53
+ defsPath: old.defsPath,
54
+ agents: old.agents,
55
+ commands: old.commands,
56
+ hooksInstalled: old.metricsHookInstalled ?? false,
57
+ },
58
+ },
59
+ };
60
+ }
61
+ /** Validate a manifest against the current filesystem state. */
62
+ export async function validateManifest(manifest) {
63
+ const errors = [];
64
+ const warnings = [];
65
+ for (const [harnessName, hm] of Object.entries(manifest.harnesses)) {
66
+ const mcpExists = await pathExists(hm.mcpConfigPath);
67
+ if (!mcpExists) {
68
+ errors.push(`[${harnessName}] MCP config path does not exist: ${hm.mcpConfigPath}`);
69
+ }
70
+ const defsExists = await pathExists(hm.defsPath);
71
+ if (!defsExists) {
72
+ errors.push(`[${harnessName}] Defs path does not exist: ${hm.defsPath}`);
73
+ }
74
+ if (hm.agents.length > 0 && defsExists) {
75
+ const missing = await findMissingFiles(hm.defsPath, "agents", hm.agents);
76
+ if (missing.length > 0) {
77
+ warnings.push(`[${harnessName}] Agent files missing from disk: ${missing.join(", ")}`);
78
+ }
79
+ }
80
+ if (hm.commands.length > 0 && defsExists) {
81
+ const missing = await findMissingFiles(hm.defsPath, "commands", hm.commands);
82
+ if (missing.length > 0) {
83
+ warnings.push(`[${harnessName}] Command files missing from disk: ${missing.join(", ")}`);
84
+ }
85
+ }
86
+ }
87
+ const manifestPath = getManifestPath();
15
88
  try {
16
- const raw = await readFile(getManifestPath(), "utf-8");
89
+ const raw = await readFile(manifestPath, "utf-8");
17
90
  const parsed = JSON.parse(raw);
18
- return isManifest(parsed) ? parsed : null;
91
+ const { contentHash: storedHash, ...withoutHash } = parsed;
92
+ const canonical = JSON.stringify(withoutHash, null, 2) + "\n";
93
+ const currentHash = fileHash(canonical);
94
+ if (storedHash && storedHash !== currentHash) {
95
+ warnings.push("Manifest file has been modified since installation — content hash mismatch");
96
+ }
19
97
  }
20
98
  catch {
21
- return null;
99
+ warnings.push("Cannot read manifest file to verify content hash");
22
100
  }
101
+ return { valid: errors.length === 0, errors, warnings };
23
102
  }
24
- export async function saveManifest(manifest) {
25
- await writeFile(getManifestPath(), JSON.stringify(manifest, null, 2) + "\n");
103
+ async function pathExists(p) {
104
+ try {
105
+ await access(p);
106
+ return true;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
26
111
  }
27
- export async function deleteManifest() {
112
+ async function findMissingFiles(baseDir, subDir, files) {
113
+ const results = await Promise.all(files.map(async (file) => ({
114
+ file,
115
+ exists: await pathExists(join(baseDir, subDir, file)),
116
+ })));
117
+ return results.filter((r) => !r.exists).map((r) => r.file);
118
+ }
119
+ async function readManifestFile(path) {
28
120
  try {
29
- await unlink(getManifestPath());
121
+ const raw = await readFile(path, "utf-8");
122
+ return JSON.parse(raw);
30
123
  }
31
124
  catch {
32
- // Already gone
125
+ return null;
126
+ }
127
+ }
128
+ /** Load the install manifest. Tries new location first, falls back to legacy, auto-migrates. */
129
+ export async function loadManifest() {
130
+ // Try new location first
131
+ const newData = await readManifestFile(getManifestPath());
132
+ if (newData && isNewManifest(newData))
133
+ return newData;
134
+ // Fall back to legacy location
135
+ const legacyData = await readManifestFile(getLegacyManifestPath());
136
+ if (legacyData && isLegacyManifest(legacyData)) {
137
+ return migrateManifest(legacyData);
138
+ }
139
+ // Also check if legacy location has new format (written by newer version but not yet moved)
140
+ if (legacyData && isNewManifest(legacyData))
141
+ return legacyData;
142
+ return null;
143
+ }
144
+ /** Save the install manifest to ~/.uluops/manifest.json. Creates directory if needed. */
145
+ export async function saveManifest(manifest) {
146
+ const dir = getUluopsDir();
147
+ await mkdir(dir, { recursive: true });
148
+ // Serialize without hash, compute hash of that content, embed it.
149
+ // Validation compares the stored hash against a re-hash of the file
150
+ // with contentHash stripped, so both sides agree on the input.
151
+ const { contentHash: _, ...withoutHash } = manifest;
152
+ const canonical = JSON.stringify(withoutHash, null, 2) + "\n";
153
+ const hash = fileHash(canonical);
154
+ const final = JSON.stringify({ ...withoutHash, contentHash: hash }, null, 2) + "\n";
155
+ await atomicWrite(getManifestPath(), final);
156
+ }
157
+ /** Delete the install manifest file from disk. Tries both locations. */
158
+ export async function deleteManifest() {
159
+ for (const path of [getManifestPath(), getLegacyManifestPath()]) {
160
+ try {
161
+ await unlink(path);
162
+ }
163
+ catch {
164
+ // Already gone
165
+ }
33
166
  }
34
167
  }