@uluops/setup 0.6.3 → 0.6.5

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.
@@ -47,13 +47,14 @@ class OpenCodeMcpConfig {
47
47
  merge(config, apiKey) {
48
48
  const raw = config["mcp"];
49
49
  const existing = (typeof raw === "object" && raw !== null ? raw : {});
50
+ // Backend URLs resolved by @uluops/ops-mcp and @uluops/registry-mcp via
51
+ // their bundled SDKs. See lib/config-merger.ts for rationale.
50
52
  const tracker = {
51
53
  type: "local",
52
54
  command: ["npx", "-y", "@uluops/ops-mcp"],
53
55
  enabled: true,
54
56
  timeout: 30000,
55
57
  environment: {
56
- ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1",
57
58
  ULUOPS_API_KEY: apiKey,
58
59
  },
59
60
  };
@@ -63,7 +64,6 @@ class OpenCodeMcpConfig {
63
64
  enabled: true,
64
65
  timeout: 30000,
65
66
  environment: {
66
- ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry",
67
67
  ULUOPS_API_KEY: apiKey,
68
68
  },
69
69
  };
@@ -49,6 +49,10 @@ export async function readConfig(path) {
49
49
  export function mergeUluopsMcp(config, apiKey, trust = false) {
50
50
  const existing = config.mcpServers ?? {};
51
51
  const trustField = trust ? { trust: true } : {};
52
+ // Backend URLs are resolved by the respective MCP servers (@uluops/ops-mcp
53
+ // and @uluops/registry-mcp) against their bundled SDKs. Stamping
54
+ // ULUOPS_BASE_URL / ULUOPS_REGISTRY_URL here would override that resolution
55
+ // with a value that could go stale if production endpoints ever shift.
52
56
  return {
53
57
  ...config,
54
58
  mcpServers: {
@@ -57,7 +61,6 @@ export function mergeUluopsMcp(config, apiKey, trust = false) {
57
61
  command: "npx",
58
62
  args: ["-y", "@uluops/ops-mcp"],
59
63
  env: {
60
- ULUOPS_BASE_URL: "https://api.uluops.ai/api/v1",
61
64
  ULUOPS_API_KEY: apiKey,
62
65
  },
63
66
  ...trustField,
@@ -66,7 +69,6 @@ export function mergeUluopsMcp(config, apiKey, trust = false) {
66
69
  command: "npx",
67
70
  args: ["-y", "@uluops/registry-mcp"],
68
71
  env: {
69
- ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry",
70
72
  ULUOPS_API_KEY: apiKey,
71
73
  },
72
74
  ...trustField,
@@ -87,7 +87,10 @@ async function readCredentialsFile() {
87
87
  return defaultProfile?.apiKey ?? defaultProfile?.api_key;
88
88
  }
89
89
  async function validateKey(apiKey) {
90
- const url = "https://api.uluops.ai/api/v1/registry/users/me";
90
+ // Self-identity lives in ops-uluops-api (`/api/v1/auth/me`), not the
91
+ // registry-api users namespace. Registry-api `/users/:id` Zod-validates the
92
+ // id as UUID, so /users/me returns 400 with `id: ["Invalid uuid"]`.
93
+ const url = "https://api.uluops.ai/api/v1/auth/me";
91
94
  try {
92
95
  const res = await fetch(url, {
93
96
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -99,8 +102,9 @@ async function validateKey(apiKey) {
99
102
  if (!res.ok) {
100
103
  throw new Error(`API returned ${res.status}. Try --skip-validation to continue offline.`);
101
104
  }
102
- const data = (await res.json());
103
- return { email: data.email ?? null };
105
+ // ops-uluops-api wraps user payloads as { data: { email, ... } }
106
+ const body = (await res.json());
107
+ return { email: body.data?.email ?? null };
104
108
  }
105
109
  catch (err) {
106
110
  // fetch() throws TypeError for network failures (ENOTFOUND, ECONNREFUSED).
@@ -8,3 +8,15 @@ export interface McpResult {
8
8
  export declare function installMcp(profile: HarnessProfile, apiKey: string, scope: "global" | "local", dryRun: boolean): Promise<McpResult>;
9
9
  /** Remove UluOps MCP server entries from the harness config. */
10
10
  export declare function uninstallMcp(profile: HarnessProfile, configPath: string): Promise<void>;
11
+ /**
12
+ * Append `entry` to a .gitignore file, creating it if missing.
13
+ *
14
+ * Discriminates ENOENT (file does not exist → create fresh) from other read
15
+ * errors (permission denied, I/O error, EISDIR → warn and skip). The previous
16
+ * implementation caught everything and wrote a single-line file, silently
17
+ * clobbering user content on any read failure.
18
+ *
19
+ * `reader` is injected for testing the error-discrimination behavior; defaults
20
+ * to the real fs reader.
21
+ */
22
+ export declare function ensureGitignoreEntry(gitignorePath: string, entry: string, reader?: (path: string) => Promise<string>): Promise<void>;
package/dist/steps/mcp.js CHANGED
@@ -38,20 +38,39 @@ async function backupConfig(harnessName, configPath) {
38
38
  }
39
39
  async function addToGitignore(localConfigFilename) {
40
40
  const root = await findProjectRoot();
41
- const gitignorePath = join(root, ".gitignore");
42
41
  try {
43
42
  await access(join(root, ".git"));
44
43
  }
45
44
  catch {
46
45
  return;
47
46
  }
47
+ await ensureGitignoreEntry(join(root, ".gitignore"), localConfigFilename);
48
+ }
49
+ /**
50
+ * Append `entry` to a .gitignore file, creating it if missing.
51
+ *
52
+ * Discriminates ENOENT (file does not exist → create fresh) from other read
53
+ * errors (permission denied, I/O error, EISDIR → warn and skip). The previous
54
+ * implementation caught everything and wrote a single-line file, silently
55
+ * clobbering user content on any read failure.
56
+ *
57
+ * `reader` is injected for testing the error-discrimination behavior; defaults
58
+ * to the real fs reader.
59
+ */
60
+ export async function ensureGitignoreEntry(gitignorePath, entry, reader = (p) => readFile(p, "utf-8")) {
61
+ let content;
48
62
  try {
49
- const content = await readFile(gitignorePath, "utf-8");
50
- if (content.includes(localConfigFilename))
51
- return;
52
- await atomicWrite(gitignorePath, content.trimEnd() + `\n${localConfigFilename}\n`);
63
+ content = await reader(gitignorePath);
53
64
  }
54
- catch {
55
- await atomicWrite(gitignorePath, `${localConfigFilename}\n`);
65
+ catch (err) {
66
+ if (err.code === "ENOENT") {
67
+ await atomicWrite(gitignorePath, `${entry}\n`);
68
+ return;
69
+ }
70
+ console.warn(`Warning: could not read ${gitignorePath} (${err.message}). Skipping .gitignore update.`);
71
+ return;
56
72
  }
73
+ if (content.includes(entry))
74
+ return;
75
+ await atomicWrite(gitignorePath, content.trimEnd() + `\n${entry}\n`);
57
76
  }
@@ -24,12 +24,13 @@ export async function writeShellExport(profilePath, apiKey, dryRun) {
24
24
  return;
25
25
  }
26
26
  const startIdx = content.indexOf(FENCE_START);
27
- const endIdx = content.indexOf(FENCE_END);
27
+ const endIdx = content.lastIndexOf(FENCE_END);
28
28
  if (!dryRun) {
29
29
  await backupProfile(profilePath);
30
30
  }
31
31
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
32
- // Replace existing fenced block (use last FENCE_END after FENCE_START to handle duplicates)
32
+ // Replace existing fenced block using lastIndexOf for FENCE_END collapses
33
+ // any duplicate blocks left by earlier buggy installs into a single new block
33
34
  const before = content.slice(0, startIdx);
34
35
  const after = content.slice(endIdx + FENCE_END.length);
35
36
  if (!dryRun) {
@@ -52,7 +53,7 @@ export async function removeShellExport(profilePath) {
52
53
  return;
53
54
  }
54
55
  const startIdx = content.indexOf(FENCE_START);
55
- const endIdx = content.indexOf(FENCE_END);
56
+ const endIdx = content.lastIndexOf(FENCE_END);
56
57
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
57
58
  await backupProfile(profilePath);
58
59
  const before = content.slice(0, startIdx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uluops/setup",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Zero-friction installer for UluOps agentic harnesses",
5
5
  "license": "MIT",
6
6
  "repository": {