@codevector/cli 0.8.1 → 0.10.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.
package/dist/index.js CHANGED
@@ -2084,7 +2084,72 @@ var init_client2 = __esm({
2084
2084
  }
2085
2085
  });
2086
2086
 
2087
+ // package.json
2088
+ var package_default;
2089
+ var init_package = __esm({
2090
+ "package.json"() {
2091
+ package_default = {
2092
+ name: "@codevector/cli",
2093
+ version: "0.10.0",
2094
+ description: "CodeVector CLI \u2014 installs and configures first-party coding-tool integrations.",
2095
+ license: "UNLICENSED",
2096
+ bin: {
2097
+ codevector: "./bin/codevector.mjs"
2098
+ },
2099
+ files: [
2100
+ "bin",
2101
+ "dist",
2102
+ "scripts",
2103
+ "src/hooks"
2104
+ ],
2105
+ type: "module",
2106
+ publishConfig: {
2107
+ access: "public"
2108
+ },
2109
+ scripts: {
2110
+ dev: "tsx src/index.ts",
2111
+ build: "tsup",
2112
+ "type-check": "tsc --noEmit",
2113
+ test: "vitest run",
2114
+ "test:watch": "vitest",
2115
+ postinstall: "node scripts/postinstall.mjs"
2116
+ },
2117
+ dependencies: {
2118
+ "@clack/prompts": "1.4.0",
2119
+ citty: "^0.2.2",
2120
+ hono: "4.12.16",
2121
+ "smol-toml": "^1.6.1"
2122
+ },
2123
+ devDependencies: {
2124
+ "@codevector/api": "workspace:*",
2125
+ "@codevector/common": "workspace:*",
2126
+ "@types/node": "25.6.0",
2127
+ tsup: "^8.5.1",
2128
+ tsx: "4.21.0",
2129
+ typescript: "6.0.3",
2130
+ vitest: "4.1.5",
2131
+ zod: "4.4.2"
2132
+ },
2133
+ engines: {
2134
+ node: ">=20"
2135
+ }
2136
+ };
2137
+ }
2138
+ });
2139
+
2087
2140
  // src/lib/api-client.ts
2141
+ function healthClient(baseUrl, timeoutMs = 5e3) {
2142
+ const client = hc(trimRightSlash(baseUrl), {
2143
+ fetch: (input, init) => {
2144
+ const controller = new AbortController();
2145
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2146
+ return fetch(input, { ...init, signal: controller.signal }).finally(
2147
+ () => clearTimeout(timer)
2148
+ );
2149
+ }
2150
+ });
2151
+ return client.health;
2152
+ }
2088
2153
  function gatewayClient(gatewayUrl, apiKey, timeoutMs = 15e3) {
2089
2154
  const client = hc(trimRightSlash(gatewayUrl), {
2090
2155
  fetch: (input, init) => {
@@ -2092,6 +2157,7 @@ function gatewayClient(gatewayUrl, apiKey, timeoutMs = 15e3) {
2092
2157
  const timer = setTimeout(() => controller.abort(), timeoutMs);
2093
2158
  const headers = new Headers(init?.headers);
2094
2159
  headers.set("x-api-key", apiKey);
2160
+ headers.set("x-codevector-cli-version", package_default.version);
2095
2161
  return fetch(input, { ...init, headers, signal: controller.signal }).finally(
2096
2162
  () => clearTimeout(timer)
2097
2163
  );
@@ -2108,7 +2174,10 @@ async function call(promise2) {
2108
2174
  throw new ApiClientError(
2109
2175
  err.statusCode,
2110
2176
  errShape?.code ?? `HTTP_${err.statusCode}`,
2111
- err.message
2177
+ // Prefer the server's human-facing description (e.g. the UPGRADE_REQUIRED
2178
+ // "run `codevector update`" text) over hono/client's generic
2179
+ // "<status> <statusText>" message.
2180
+ errShape?.description ?? err.message
2112
2181
  );
2113
2182
  }
2114
2183
  const asError = err;
@@ -2130,6 +2199,7 @@ var init_api_client = __esm({
2130
2199
  "src/lib/api-client.ts"() {
2131
2200
  "use strict";
2132
2201
  init_client2();
2202
+ init_package();
2133
2203
  ApiClientError = class extends Error {
2134
2204
  constructor(status, code, message) {
2135
2205
  super(message);
@@ -17513,17 +17583,128 @@ var init_gitignore = __esm({
17513
17583
  }
17514
17584
  });
17515
17585
 
17516
- // src/config-writers/opencode.ts
17517
- import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
17586
+ // src/config-writers/cline.ts
17587
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
17518
17588
  import { homedir as homedir3 } from "os";
17519
- import { join as join4 } from "path";
17589
+ import { dirname as dirname2, join as join4 } from "path";
17590
+ function clineDataDir() {
17591
+ const home = homedir3();
17592
+ return join4(home, ".cline", "data", "settings");
17593
+ }
17594
+ function clineSettingsPath(scope) {
17595
+ switch (scope) {
17596
+ case "user":
17597
+ return join4(clineDataDir(), PROVIDERS_FILE);
17598
+ case "project":
17599
+ return join4(userCwd(), CLINE_CONFIG_DIR, PROVIDERS_FILE);
17600
+ case "local":
17601
+ return join4(userCwd(), CLINE_CONFIG_DIR, PROVIDERS_FILE);
17602
+ }
17603
+ }
17604
+ function detectClineConfig() {
17605
+ const scopes = ["local", "project", "user"];
17606
+ for (const scope of scopes) {
17607
+ const path = clineSettingsPath(scope);
17608
+ if (!existsSync3(path)) continue;
17609
+ try {
17610
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
17611
+ const providers = raw.providers;
17612
+ if (Array.isArray(providers)) {
17613
+ for (const p2 of providers) {
17614
+ if (typeof p2.baseUrl === "string" && p2.baseUrl.includes("/gateway/")) {
17615
+ return { scope, modelSlug: p2.defaultModel };
17616
+ }
17617
+ }
17618
+ }
17619
+ } catch {
17620
+ }
17621
+ }
17622
+ return void 0;
17623
+ }
17624
+ function readProvidersFile(path) {
17625
+ try {
17626
+ const raw = readFileSync4(path, "utf8").trim();
17627
+ if (!raw) return {};
17628
+ const parsed = JSON.parse(raw);
17629
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
17630
+ return parsed;
17631
+ }
17632
+ return {};
17633
+ } catch {
17634
+ return {};
17635
+ }
17636
+ }
17637
+ function trimRightSlash3(url2) {
17638
+ return url2.endsWith("/") ? url2.slice(0, -1) : url2;
17639
+ }
17640
+ var CLINE_CONFIG_DIR, PROVIDERS_FILE, writeClineConfig;
17641
+ var init_cline = __esm({
17642
+ "src/config-writers/cline.ts"() {
17643
+ "use strict";
17644
+ init_gitignore();
17645
+ init_paths();
17646
+ CLINE_CONFIG_DIR = ".cline";
17647
+ PROVIDERS_FILE = "providers.json";
17648
+ writeClineConfig = ({
17649
+ gatewayUrl,
17650
+ apiKey,
17651
+ scope,
17652
+ project,
17653
+ model
17654
+ }) => {
17655
+ const path = clineSettingsPath(scope);
17656
+ const baseUrl = `${trimRightSlash3(gatewayUrl)}/gateway/openai/v1`;
17657
+ mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
17658
+ const existing = readProvidersFile(path);
17659
+ const providers = (existing.providers ?? []).filter(
17660
+ (p2) => p2.id !== "codevector" && p2.baseUrl !== baseUrl
17661
+ );
17662
+ providers.push({
17663
+ type: "openai-compatible",
17664
+ id: "codevector",
17665
+ apiKey,
17666
+ baseUrl,
17667
+ ...model?.slug ? { defaultModel: model.slug } : {}
17668
+ });
17669
+ writeFileSync3(path, `${JSON.stringify({ ...existing, providers }, null, 2)}
17670
+ `);
17671
+ const notes = [];
17672
+ if (scope === "local") {
17673
+ if (ensureGitignored(join4(CLINE_CONFIG_DIR, PROVIDERS_FILE))) {
17674
+ notes.push(`Added \`${CLINE_CONFIG_DIR}/${PROVIDERS_FILE}\` to .gitignore.`);
17675
+ }
17676
+ }
17677
+ const authCmd = `cline auth --provider openai-compatible --apikey "${apiKey.slice(0, 8)}\u2026" --baseurl "${baseUrl}"${model?.slug ? ` --modelid "${model.slug}"` : ""}`;
17678
+ notes.push(
17679
+ `Alternatively, run \`${authCmd}\` (requires cline CLI: \`npm i -g cline\`).`
17680
+ );
17681
+ if (scope !== "user" && !project) {
17682
+ notes.push(
17683
+ "Project slug could not be derived (no git remote and no .codevector.json). Per-project usage attribution will be blank until you set one."
17684
+ );
17685
+ }
17686
+ return {
17687
+ tool: "cline",
17688
+ status: "configured",
17689
+ path,
17690
+ scope,
17691
+ notes: notes.length > 0 ? notes.join(" ") : void 0
17692
+ };
17693
+ };
17694
+ }
17695
+ });
17696
+
17697
+ // src/config-writers/opencode.ts
17698
+ import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
17699
+ import { homedir as homedir4 } from "os";
17700
+ import { join as join5 } from "path";
17520
17701
  function syncOpencodeModels(path, availableModels) {
17521
- if (!existsSync3(path)) {
17702
+ if (!existsSync4(path)) {
17522
17703
  throw new Error(
17523
17704
  `No opencode config found at ${path}. Run \`codevector configure opencode\` first.`
17524
17705
  );
17525
17706
  }
17526
- const raw = readFileSync4(path, "utf8");
17707
+ const raw = readFileSync5(path, "utf8");
17527
17708
  let parsed;
17528
17709
  try {
17529
17710
  parsed = raw.trim().length === 0 ? {} : JSON.parse(raw);
@@ -17542,7 +17723,7 @@ function syncOpencodeModels(path, availableModels) {
17542
17723
  }
17543
17724
  const models = buildModelEntries(availableModels);
17544
17725
  codevector.models = models;
17545
- writeFileSync3(path, `${JSON.stringify(parsed, null, 2)}
17726
+ writeFileSync4(path, `${JSON.stringify(parsed, null, 2)}
17546
17727
  `);
17547
17728
  return Object.keys(models).length;
17548
17729
  }
@@ -17591,10 +17772,10 @@ function parsePrice(raw) {
17591
17772
  return Number.isFinite(n) ? n : void 0;
17592
17773
  }
17593
17774
  function forceReplaceCgwModels(path, models) {
17594
- if (!existsSync3(path)) return;
17775
+ if (!existsSync4(path)) return;
17595
17776
  let parsed;
17596
17777
  try {
17597
- parsed = JSON.parse(readFileSync4(path, "utf8"));
17778
+ parsed = JSON.parse(readFileSync5(path, "utf8"));
17598
17779
  } catch {
17599
17780
  return;
17600
17781
  }
@@ -17604,25 +17785,25 @@ function forceReplaceCgwModels(path, models) {
17604
17785
  const codevector = provider[PROVIDER_PREFIX];
17605
17786
  if (!isObject3(codevector)) return;
17606
17787
  codevector.models = models;
17607
- writeFileSync3(path, `${JSON.stringify(parsed, null, 2)}
17788
+ writeFileSync4(path, `${JSON.stringify(parsed, null, 2)}
17608
17789
  `);
17609
17790
  }
17610
17791
  function opencodeSettingsPath(scope) {
17611
17792
  switch (scope) {
17612
17793
  case "user":
17613
- return join4(homedir3(), ".config", "opencode", "opencode.json");
17794
+ return join5(homedir4(), ".config", "opencode", "opencode.json");
17614
17795
  case "project":
17615
17796
  case "local":
17616
- return join4(userCwd(), "opencode.json");
17797
+ return join5(userCwd(), "opencode.json");
17617
17798
  }
17618
17799
  }
17619
17800
  function detectOpencodeConfig() {
17620
17801
  const scopes = ["local", "project", "user"];
17621
17802
  for (const scope of scopes) {
17622
17803
  const path = opencodeSettingsPath(scope);
17623
- if (!existsSync3(path)) continue;
17804
+ if (!existsSync4(path)) continue;
17624
17805
  try {
17625
- const raw = JSON.parse(readFileSync4(path, "utf8"));
17806
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
17626
17807
  if (raw.provider?.codevector?.options?.baseURL) {
17627
17808
  const modelSlug = raw.model?.startsWith("codevector/") ? raw.model.slice("codevector/".length) : raw.model;
17628
17809
  return { scope, modelSlug };
@@ -17633,10 +17814,10 @@ function detectOpencodeConfig() {
17633
17814
  return void 0;
17634
17815
  }
17635
17816
  function removeStaleGatewayProviders(path, gateway) {
17636
- if (!existsSync3(path)) return;
17817
+ if (!existsSync4(path)) return;
17637
17818
  let parsed;
17638
17819
  try {
17639
- const raw = readFileSync4(path, "utf8");
17820
+ const raw = readFileSync5(path, "utf8");
17640
17821
  if (raw.trim().length === 0) return;
17641
17822
  parsed = JSON.parse(raw);
17642
17823
  } catch {
@@ -17659,7 +17840,7 @@ function removeStaleGatewayProviders(path, gateway) {
17659
17840
  }
17660
17841
  }
17661
17842
  if (mutated) {
17662
- writeFileSync3(path, `${JSON.stringify(parsed, null, 2)}
17843
+ writeFileSync4(path, `${JSON.stringify(parsed, null, 2)}
17663
17844
  `);
17664
17845
  }
17665
17846
  }
@@ -17679,7 +17860,7 @@ function buildCustomHeaders2(scope, project) {
17679
17860
  }
17680
17861
  return headers;
17681
17862
  }
17682
- function trimRightSlash3(url2) {
17863
+ function trimRightSlash4(url2) {
17683
17864
  return url2.endsWith("/") ? url2.slice(0, -1) : url2;
17684
17865
  }
17685
17866
  var ENV_VAR_NAME, PROVIDER_PREFIX, writeOpencodeConfig;
@@ -17700,7 +17881,7 @@ var init_opencode = __esm({
17700
17881
  availableModels = []
17701
17882
  }) => {
17702
17883
  const path = opencodeSettingsPath(scope);
17703
- const gateway = trimRightSlash3(gatewayUrl);
17884
+ const gateway = trimRightSlash4(gatewayUrl);
17704
17885
  const keyLiteral = scope === "project" ? `{env:${ENV_VAR_NAME}}` : apiKey;
17705
17886
  removeStaleGatewayProviders(path, gateway);
17706
17887
  const models = buildModelEntries(availableModels);
@@ -17749,8 +17930,8 @@ var init_opencode = __esm({
17749
17930
  });
17750
17931
 
17751
17932
  // src/lib/credentials-lock.ts
17752
- import { closeSync, constants, mkdirSync as mkdirSync2, openSync, statSync as statSync2, unlinkSync } from "fs";
17753
- import { dirname as dirname2 } from "path";
17933
+ import { closeSync, constants, mkdirSync as mkdirSync3, openSync, statSync as statSync2, unlinkSync } from "fs";
17934
+ import { dirname as dirname3 } from "path";
17754
17935
  function syncSleep(ms) {
17755
17936
  Atomics.wait(sleepView, 0, 0, ms);
17756
17937
  }
@@ -17773,7 +17954,7 @@ function withCredentialsLockSync(fn) {
17773
17954
  }
17774
17955
  }
17775
17956
  function acquireLock() {
17776
- mkdirSync2(dirname2(LOCK_FILE), { recursive: true, mode: 448 });
17957
+ mkdirSync3(dirname3(LOCK_FILE), { recursive: true, mode: 448 });
17777
17958
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
17778
17959
  try {
17779
17960
  const fd = openSync(
@@ -17827,24 +18008,24 @@ var init_credentials_lock = __esm({
17827
18008
  // src/lib/credentials.ts
17828
18009
  import {
17829
18010
  chmodSync as chmodSync2,
17830
- mkdirSync as mkdirSync3,
17831
- readFileSync as readFileSync5,
18011
+ mkdirSync as mkdirSync4,
18012
+ readFileSync as readFileSync6,
17832
18013
  renameSync as renameSync2,
17833
18014
  rmSync,
17834
18015
  statSync as statSync3,
17835
- writeFileSync as writeFileSync4
18016
+ writeFileSync as writeFileSync5
17836
18017
  } from "fs";
17837
- import { dirname as dirname3 } from "path";
18018
+ import { dirname as dirname4 } from "path";
17838
18019
  function writeProfilesFile(payload) {
17839
18020
  const parsed = ProfilesFileSchema.parse(payload);
17840
- const dir = dirname3(CREDENTIALS_FILE);
17841
- mkdirSync3(dir, { recursive: true, mode: 448 });
18021
+ const dir = dirname4(CREDENTIALS_FILE);
18022
+ mkdirSync4(dir, { recursive: true, mode: 448 });
17842
18023
  try {
17843
18024
  chmodSync2(dir, 448);
17844
18025
  } catch {
17845
18026
  }
17846
18027
  const tmp = `${CREDENTIALS_FILE}.${process.pid}.tmp`;
17847
- writeFileSync4(tmp, JSON.stringify(parsed, null, 2), { mode: 384 });
18028
+ writeFileSync5(tmp, JSON.stringify(parsed, null, 2), { mode: 384 });
17848
18029
  try {
17849
18030
  chmodSync2(tmp, 384);
17850
18031
  } catch {
@@ -17854,7 +18035,7 @@ function writeProfilesFile(payload) {
17854
18035
  function readProfilesSync() {
17855
18036
  let raw;
17856
18037
  try {
17857
- raw = readFileSync5(CREDENTIALS_FILE, "utf8");
18038
+ raw = readFileSync6(CREDENTIALS_FILE, "utf8");
17858
18039
  } catch (err) {
17859
18040
  if (err.code === "ENOENT") return null;
17860
18041
  throw err;
@@ -17873,7 +18054,7 @@ function readProfilesSync() {
17873
18054
  return withCredentialsLockSync(() => {
17874
18055
  let lockedRaw;
17875
18056
  try {
17876
- lockedRaw = readFileSync5(CREDENTIALS_FILE, "utf8");
18057
+ lockedRaw = readFileSync6(CREDENTIALS_FILE, "utf8");
17877
18058
  } catch (err) {
17878
18059
  if (err.code === "ENOENT") {
17879
18060
  return null;
@@ -17885,7 +18066,7 @@ function readProfilesSync() {
17885
18066
  return ProfilesFileSchema.parse(lockedParsed);
17886
18067
  }
17887
18068
  const oldProfile = ProfileSchema.parse(lockedParsed);
17888
- const seeded = seedToolConfigsFromDisk();
18069
+ const seeded = detectOnDiskToolConfigs();
17889
18070
  const defaultProfile = {
17890
18071
  ...oldProfile,
17891
18072
  ...seeded.length > 0 ? { toolConfigs: seeded } : {}
@@ -17901,7 +18082,7 @@ function readProfilesSync() {
17901
18082
  return migrated;
17902
18083
  });
17903
18084
  }
17904
- function seedToolConfigsFromDisk() {
18085
+ function detectOnDiskToolConfigs() {
17905
18086
  const out = [];
17906
18087
  const claude = detectClaudeCodeConfig();
17907
18088
  if (claude) {
@@ -17919,6 +18100,14 @@ function seedToolConfigsFromDisk() {
17919
18100
  ...opencode.modelSlug ? { modelSlug: opencode.modelSlug } : {}
17920
18101
  });
17921
18102
  }
18103
+ const cline = detectClineConfig();
18104
+ if (cline) {
18105
+ out.push({
18106
+ tool: "cline",
18107
+ scope: cline.scope,
18108
+ ...cline.modelSlug ? { modelSlug: cline.modelSlug } : {}
18109
+ });
18110
+ }
17922
18111
  return out;
17923
18112
  }
17924
18113
  async function readProfiles() {
@@ -18020,6 +18209,7 @@ var init_credentials = __esm({
18020
18209
  "use strict";
18021
18210
  init_zod();
18022
18211
  init_claude_code();
18212
+ init_cline();
18023
18213
  init_opencode();
18024
18214
  init_credentials_lock();
18025
18215
  init_paths();
@@ -18034,7 +18224,18 @@ var init_credentials = __esm({
18034
18224
  userId: external_exports.uuid(),
18035
18225
  email: external_exports.email(),
18036
18226
  savedAt: external_exports.iso.datetime(),
18037
- toolConfigs: external_exports.array(ToolConfigSchema).optional()
18227
+ toolConfigs: external_exports.array(ToolConfigSchema).optional(),
18228
+ // Group-scoped profile: the group this profile's key bills/records against.
18229
+ // Attribution is server-side (bound to the key); these are persisted only so
18230
+ // `profile list` / `status` / `doctor` can show the billed group without a
18231
+ // round-trip. `groupId` is the stable key (survives a rename); `groupName` is
18232
+ // display only. Absent for a personal profile.
18233
+ groupId: external_exports.uuid().optional(),
18234
+ groupName: external_exports.string().min(1).optional(),
18235
+ // Server-side id of the API key this profile holds. Stored on mint so
18236
+ // `sync` can match a local profile to a server key record exactly (prefix
18237
+ // is non-unique). Absent on legacy profiles (matched by prefix instead).
18238
+ keyId: external_exports.uuid().optional()
18038
18239
  });
18039
18240
  ProfilesFileSchema = external_exports.object({
18040
18241
  activeProfile: external_exports.string().min(1),
@@ -18076,16 +18277,16 @@ var init_prompt = __esm({
18076
18277
  });
18077
18278
 
18078
18279
  // src/lib/project-config.ts
18079
- import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
18080
- import { dirname as dirname4, join as join5, parse as parsePath, resolve } from "path";
18280
+ import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
18281
+ import { dirname as dirname5, join as join6, parse as parsePath, resolve } from "path";
18081
18282
  function findProjectConfigPath(startDir) {
18082
18283
  let dir = resolve(startDir);
18083
18284
  const root = parsePath(dir).root;
18084
18285
  while (true) {
18085
- const candidate = join5(dir, PROJECT_CONFIG_FILENAME);
18086
- if (existsSync4(candidate)) return candidate;
18286
+ const candidate = join6(dir, PROJECT_CONFIG_FILENAME);
18287
+ if (existsSync5(candidate)) return candidate;
18087
18288
  if (dir === root) return null;
18088
- const parent = dirname4(dir);
18289
+ const parent = dirname5(dir);
18089
18290
  if (parent === dir) return null;
18090
18291
  dir = parent;
18091
18292
  }
@@ -18093,7 +18294,7 @@ function findProjectConfigPath(startDir) {
18093
18294
  function readProjectConfigAt(path) {
18094
18295
  let raw;
18095
18296
  try {
18096
- raw = readFileSync6(path, "utf8");
18297
+ raw = readFileSync7(path, "utf8");
18097
18298
  } catch {
18098
18299
  return null;
18099
18300
  }
@@ -18115,7 +18316,7 @@ function readProjectConfig(startDir) {
18115
18316
  }
18116
18317
  function writeProjectConfig(path, config2) {
18117
18318
  const parsed = ProjectConfigSchema.parse(config2);
18118
- writeFileSync5(path, `${JSON.stringify(parsed, null, 2)}
18319
+ writeFileSync6(path, `${JSON.stringify(parsed, null, 2)}
18119
18320
  `, { mode: 420 });
18120
18321
  }
18121
18322
  var PROJECT_CONFIG_FILENAME, ProjectConfigSchema;
@@ -18129,14 +18330,18 @@ var init_project_config = __esm({
18129
18330
  projectName: external_exports.string().min(1).optional(),
18130
18331
  ticketPattern: external_exports.string().min(1).optional(),
18131
18332
  gateway: external_exports.url().optional(),
18132
- tools: external_exports.array(ToolConfigSchema).optional()
18333
+ tools: external_exports.array(ToolConfigSchema).optional(),
18334
+ // Candidate groups this repo bills. The shell hook resolves them to the one
18335
+ // local key the developer holds (see resolveProfileForRepo). A list, not a
18336
+ // single pin: a repo shared by two teams lists both; each dev bills their own.
18337
+ groups: external_exports.array(external_exports.string().min(1)).optional()
18133
18338
  });
18134
18339
  }
18135
18340
  });
18136
18341
 
18137
18342
  // src/lib/project-context.ts
18138
18343
  import { execFileSync } from "child_process";
18139
- import { join as join6 } from "path";
18344
+ import { join as join7 } from "path";
18140
18345
  function safeGit(args, cwd) {
18141
18346
  try {
18142
18347
  const out = execFileSync("git", args, {
@@ -18174,7 +18379,7 @@ function resolveProjectContext(cwd = process.cwd(), ticketPattern = DEFAULT_TICK
18174
18379
  return { project, ticket };
18175
18380
  }
18176
18381
  function readLocalConfig(cwd) {
18177
- const config2 = readProjectConfigAt(join6(cwd, PROJECT_CONFIG_FILENAME));
18382
+ const config2 = readProjectConfigAt(join7(cwd, PROJECT_CONFIG_FILENAME));
18178
18383
  if (!config2) return null;
18179
18384
  const result = {};
18180
18385
  if (config2.projectName) result.projectName = config2.projectName;
@@ -18216,9 +18421,9 @@ var init_shell = __esm({
18216
18421
  });
18217
18422
 
18218
18423
  // src/lib/shell-hook.ts
18219
- import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
18220
- import { homedir as homedir4 } from "os";
18221
- import { join as join7 } from "path";
18424
+ import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
18425
+ import { homedir as homedir5 } from "os";
18426
+ import { join as join8 } from "path";
18222
18427
  function shellHookRecipe(shell) {
18223
18428
  return RECIPES[shell];
18224
18429
  }
@@ -18251,19 +18456,19 @@ function shellHookOneLiner(shell = detectShell()) {
18251
18456
  return `Optional: add \`${line}\` to ${rc} so credentials auto-activate on cd.`;
18252
18457
  }
18253
18458
  function rcCandidates(shell, platform = process.platform) {
18254
- const home = homedir4();
18459
+ const home = homedir5();
18255
18460
  switch (shell) {
18256
18461
  case "bash":
18257
- return platform === "darwin" ? [join7(home, ".bash_profile"), join7(home, ".bashrc")] : [join7(home, ".bashrc")];
18462
+ return platform === "darwin" ? [join8(home, ".bash_profile"), join8(home, ".bashrc")] : [join8(home, ".bashrc")];
18258
18463
  case "zsh":
18259
- return [join7(home, ".zshrc")];
18464
+ return [join8(home, ".zshrc")];
18260
18465
  case "fish":
18261
- return [join7(home, ".config", "fish", "config.fish")];
18466
+ return [join8(home, ".config", "fish", "config.fish")];
18262
18467
  case "powershell": {
18263
- const docs = join7(home, "Documents");
18468
+ const docs = join8(home, "Documents");
18264
18469
  return [
18265
- join7(docs, "PowerShell", "Microsoft.PowerShell_profile.ps1"),
18266
- join7(docs, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1")
18470
+ join8(docs, "PowerShell", "Microsoft.PowerShell_profile.ps1"),
18471
+ join8(docs, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1")
18267
18472
  ];
18268
18473
  }
18269
18474
  }
@@ -18271,7 +18476,7 @@ function rcCandidates(shell, platform = process.platform) {
18271
18476
  function isHookInstalled(shell, platform = process.platform) {
18272
18477
  for (const path of rcCandidates(shell, platform)) {
18273
18478
  try {
18274
- if (existsSync5(path) && /codevector hook/.test(readFileSync7(path, "utf8"))) return true;
18479
+ if (existsSync6(path) && /codevector hook/.test(readFileSync8(path, "utf8"))) return true;
18275
18480
  } catch {
18276
18481
  }
18277
18482
  }
@@ -18297,8 +18502,8 @@ __export(init_exports, {
18297
18502
  initCommand: () => initCommand
18298
18503
  });
18299
18504
  import { execFileSync as execFileSync2 } from "child_process";
18300
- import { existsSync as existsSync6 } from "fs";
18301
- import { join as join8 } from "path";
18505
+ import { existsSync as existsSync7 } from "fs";
18506
+ import { join as join9 } from "path";
18302
18507
  function isInteractive(args) {
18303
18508
  if (!process.stdout.isTTY) return false;
18304
18509
  return !args.gateway;
@@ -18429,8 +18634,8 @@ var init_init = __esm({
18429
18634
  },
18430
18635
  async run({ args }) {
18431
18636
  const cwd = userCwd();
18432
- const target = join8(cwd, PROJECT_CONFIG_FILENAME);
18433
- const existing = existsSync6(target) ? readProjectConfigAt(target) : null;
18637
+ const target = join9(cwd, PROJECT_CONFIG_FILENAME);
18638
+ const existing = existsSync7(target) ? readProjectConfigAt(target) : null;
18434
18639
  const interactive = isInteractive(args);
18435
18640
  if (existing && !args.force && !interactive) {
18436
18641
  throw new Error(
@@ -18482,9 +18687,9 @@ var init_init = __esm({
18482
18687
  });
18483
18688
 
18484
18689
  // src/commands/configure.ts
18485
- import { homedir as homedir5 } from "os";
18486
- import { existsSync as existsSync7 } from "fs";
18487
- import { join as join9, sep } from "path";
18690
+ import { homedir as homedir6 } from "os";
18691
+ import { existsSync as existsSync8 } from "fs";
18692
+ import { join as join10, sep } from "path";
18488
18693
  async function resolveTools(args) {
18489
18694
  if (args.all) return [...ALL_TOOLS];
18490
18695
  if (args.tool) {
@@ -18545,12 +18750,14 @@ function pathHint(tools, scope) {
18545
18750
  switch (tool) {
18546
18751
  case "claude-code":
18547
18752
  return relativizeHomeAndCwd(claudeSettingsPath(scope));
18753
+ case "cline":
18754
+ return relativizeHomeAndCwd(clineSettingsPath(scope));
18548
18755
  case "opencode":
18549
18756
  return relativizeHomeAndCwd(opencodeSettingsPath(scope));
18550
18757
  }
18551
18758
  }
18552
18759
  function relativizeHomeAndCwd(absolutePath) {
18553
- const home = homedir5();
18760
+ const home = homedir6();
18554
18761
  const cwd = userCwd();
18555
18762
  if (home && absolutePath.startsWith(`${home}${sep}`)) {
18556
18763
  return `~${absolutePath.slice(home.length)}`;
@@ -18564,7 +18771,7 @@ function isTool(value) {
18564
18771
  return ALL_TOOLS.includes(value);
18565
18772
  }
18566
18773
  function mergeToolsIntoProjectConfig(cwd, tools, gatewayUrl) {
18567
- const path = join9(cwd, PROJECT_CONFIG_FILENAME);
18774
+ const path = join10(cwd, PROJECT_CONFIG_FILENAME);
18568
18775
  const existing = readProjectConfigAt(path) ?? {};
18569
18776
  const byTool = new Map((existing.tools ?? []).map((c) => [c.tool, c]));
18570
18777
  for (const cfg of tools) byTool.set(cfg.tool, cfg);
@@ -18584,7 +18791,7 @@ async function fetchReachableChatModels2(creds) {
18584
18791
  s.start("Loading reachable models\u2026");
18585
18792
  try {
18586
18793
  const res = await call(parseResponse(client.models.$get()));
18587
- const models = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
18794
+ const models = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
18588
18795
  slug: m2.slug,
18589
18796
  providerKind: m2.providerKind,
18590
18797
  displayName: m2.displayName,
@@ -18658,12 +18865,14 @@ var init_configure = __esm({
18658
18865
  init_project_config();
18659
18866
  init_shell_hook();
18660
18867
  init_claude_code();
18868
+ init_cline();
18661
18869
  init_opencode();
18662
18870
  WRITERS2 = {
18663
18871
  "claude-code": writeClaudeCodeConfig,
18872
+ cline: writeClineConfig,
18664
18873
  opencode: writeOpencodeConfig
18665
18874
  };
18666
- ALL_TOOLS = ["claude-code", "opencode"];
18875
+ ALL_TOOLS = ["claude-code", "cline", "opencode"];
18667
18876
  SCOPES = ["local", "project"];
18668
18877
  configureCommand = defineCommand({
18669
18878
  meta: {
@@ -18699,7 +18908,7 @@ var init_configure = __esm({
18699
18908
  );
18700
18909
  }
18701
18910
  const cwd = userCwd();
18702
- if (process.stdout.isTTY && !existsSync7(join9(cwd, PROJECT_CONFIG_FILENAME))) {
18911
+ if (process.stdout.isTTY && !existsSync8(join10(cwd, PROJECT_CONFIG_FILENAME))) {
18703
18912
  const runInit = unwrap(
18704
18913
  await ue({
18705
18914
  message: `No ${PROJECT_CONFIG_FILENAME} in this repo yet. Run \`codevector init\` first to set it up (project name, ticket pattern, gateway)?`,
@@ -18907,12 +19116,15 @@ var authLoginCommand = defineCommand({
18907
19116
  apiKey,
18908
19117
  gatewayUrl: config2.gatewayPublicUrl,
18909
19118
  userId: me2.user.id,
18910
- email: me2.user.email
19119
+ email: me2.user.email,
19120
+ ...me2.attributionGroupId ? { groupId: me2.attributionGroupId, groupName: me2.attributionGroupName ?? void 0 } : {}
18911
19121
  });
19122
+ const billingNote = stored.groupName ? `Billing: group "${stored.groupName}"` : "Billing: personal";
18912
19123
  s.stop(`Signed in as ${stored.email} (${profileName})`);
18913
19124
  Se(
18914
19125
  `Gateway: ${stored.gatewayUrl}
18915
19126
  Profile: ${profileName}
19127
+ ${billingNote}
18916
19128
  Next: run \`codevector configure claude-code\`.`,
18917
19129
  "Credentials saved"
18918
19130
  );
@@ -19074,9 +19286,7 @@ var configSyncCommand = defineCommand({
19074
19286
  ge("codevector sync");
19075
19287
  const found = readProjectConfig(userCwd());
19076
19288
  if (!found) {
19077
- throw new Error(
19078
- "No .codevector.json on the path to root. Run `codevector init` first."
19079
- );
19289
+ throw new Error("No .codevector.json on the path to root. Run `codevector init` first.");
19080
19290
  }
19081
19291
  const { config: config2, path: manifestPath } = found;
19082
19292
  R2.info(`Manifest: ${manifestPath}`);
@@ -19098,7 +19308,7 @@ var configSyncCommand = defineCommand({
19098
19308
  `Active profile "${profiles.activeProfile}" is missing from credentials. Run \`codevector auth login\`.`
19099
19309
  );
19100
19310
  }
19101
- if (config2.gateway && trimRightSlash4(creds.gatewayUrl) !== trimRightSlash4(config2.gateway)) {
19311
+ if (config2.gateway && trimRightSlash5(creds.gatewayUrl) !== trimRightSlash5(config2.gateway)) {
19102
19312
  R2.warn(
19103
19313
  `Active profile points at ${creds.gatewayUrl} but manifest pins ${config2.gateway}. Sync will use the active profile.`
19104
19314
  );
@@ -19183,7 +19393,7 @@ async function fetchReachableChatModels(creds) {
19183
19393
  s.start("Loading reachable models\u2026");
19184
19394
  try {
19185
19395
  const res = await call(parseResponse(client.models.$get()));
19186
- const models = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
19396
+ const models = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
19187
19397
  slug: m2.slug,
19188
19398
  providerKind: m2.providerKind,
19189
19399
  displayName: m2.displayName,
@@ -19214,7 +19424,7 @@ async function fetchReachableChatModels(creds) {
19214
19424
  return [];
19215
19425
  }
19216
19426
  }
19217
- function trimRightSlash4(url2) {
19427
+ function trimRightSlash5(url2) {
19218
19428
  return url2.endsWith("/") ? url2.slice(0, -1) : url2;
19219
19429
  }
19220
19430
 
@@ -19237,25 +19447,26 @@ init_dist();
19237
19447
  init_dist5();
19238
19448
  init_api_client();
19239
19449
  init_claude_code();
19450
+ init_cline();
19240
19451
  init_opencode();
19241
19452
  init_credentials();
19242
- import { existsSync as existsSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
19243
- import { join as join11 } from "path";
19453
+ import { existsSync as existsSync11, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "fs";
19454
+ import { join as join13 } from "path";
19244
19455
 
19245
19456
  // src/lib/install-pref.ts
19246
19457
  init_paths();
19247
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, realpathSync, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
19248
- import { join as join10, sep as sep2 } from "path";
19458
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync9, realpathSync, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
19459
+ import { join as join11, sep as sep2 } from "path";
19249
19460
  import { fileURLToPath } from "url";
19250
- var INSTALL_PREF_FILE = join10(CODEVECTOR_CONFIG_DIR, "install.json");
19461
+ var INSTALL_PREF_FILE = join11(CODEVECTOR_CONFIG_DIR, "install.json");
19251
19462
  var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn"];
19252
19463
  function isPackageManager(v2) {
19253
19464
  return typeof v2 === "string" && PACKAGE_MANAGERS.includes(v2);
19254
19465
  }
19255
19466
  function readInstallPref() {
19256
- if (!existsSync8(INSTALL_PREF_FILE)) return void 0;
19467
+ if (!existsSync9(INSTALL_PREF_FILE)) return void 0;
19257
19468
  try {
19258
- const raw = readFileSync8(INSTALL_PREF_FILE, "utf8");
19469
+ const raw = readFileSync9(INSTALL_PREF_FILE, "utf8");
19259
19470
  const parsed = JSON.parse(raw);
19260
19471
  if (!isPackageManager(parsed.packageManager)) return void 0;
19261
19472
  const source = parsed.source === "user" ? "user" : "auto";
@@ -19269,9 +19480,9 @@ function readInstallPref() {
19269
19480
  }
19270
19481
  }
19271
19482
  function writeInstallPref(pref) {
19272
- mkdirSync4(CODEVECTOR_CONFIG_DIR, { recursive: true, mode: 448 });
19483
+ mkdirSync5(CODEVECTOR_CONFIG_DIR, { recursive: true, mode: 448 });
19273
19484
  const tmp = `${INSTALL_PREF_FILE}.${process.pid}.tmp`;
19274
- writeFileSync6(tmp, JSON.stringify(pref, null, 2));
19485
+ writeFileSync7(tmp, JSON.stringify(pref, null, 2));
19275
19486
  renameSync3(tmp, INSTALL_PREF_FILE);
19276
19487
  }
19277
19488
  function packageManagerFromPath(installPath) {
@@ -19314,6 +19525,147 @@ init_project_config();
19314
19525
  init_paths();
19315
19526
  init_shell();
19316
19527
  init_shell_hook();
19528
+
19529
+ // src/lib/version-check.ts
19530
+ init_api_client();
19531
+
19532
+ // src/lib/update-notifier.ts
19533
+ init_paths();
19534
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
19535
+ import { join as join12 } from "path";
19536
+ var PKG_NAME = "@codevector/cli";
19537
+ var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
19538
+ var CHECK_CACHE_FILE = join12(CODEVECTOR_CONFIG_DIR, "update-check.json");
19539
+ var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
19540
+ var FETCH_TIMEOUT_MS = 2e3;
19541
+ function readCache() {
19542
+ if (!existsSync10(CHECK_CACHE_FILE)) return void 0;
19543
+ try {
19544
+ const raw = readFileSync10(CHECK_CACHE_FILE, "utf8");
19545
+ const parsed = JSON.parse(raw);
19546
+ if (typeof parsed.checkedAt !== "number") return void 0;
19547
+ return {
19548
+ latest: typeof parsed.latest === "string" ? parsed.latest : null,
19549
+ checkedAt: parsed.checkedAt
19550
+ };
19551
+ } catch {
19552
+ return void 0;
19553
+ }
19554
+ }
19555
+ function readCachedLatestVersion() {
19556
+ return readCache()?.latest ?? null;
19557
+ }
19558
+ function writeCache(cache) {
19559
+ try {
19560
+ mkdirSync6(CODEVECTOR_CONFIG_DIR, { recursive: true, mode: 448 });
19561
+ writeFileSync8(CHECK_CACHE_FILE, JSON.stringify(cache));
19562
+ } catch {
19563
+ }
19564
+ }
19565
+ function isNewer(current, latest) {
19566
+ const a = current.split(".").map((p2) => Number.parseInt(p2, 10));
19567
+ const b2 = latest.split(".").map((p2) => Number.parseInt(p2, 10));
19568
+ for (let i = 0; i < Math.max(a.length, b2.length); i++) {
19569
+ const ai = a[i] ?? 0;
19570
+ const bi = b2[i] ?? 0;
19571
+ if (Number.isNaN(ai) || Number.isNaN(bi)) return latest > current;
19572
+ if (bi > ai) return true;
19573
+ if (bi < ai) return false;
19574
+ }
19575
+ return false;
19576
+ }
19577
+ function printUpdateNoticeIfCached(currentVersion) {
19578
+ if (process.env.CODEVECTOR_NO_UPDATE_CHECK === "1") return;
19579
+ try {
19580
+ const cache = readCache();
19581
+ if (!cache || !cache.latest) return;
19582
+ if (isNewer(currentVersion, cache.latest)) {
19583
+ process.stderr.write(
19584
+ `\x1B[33m\u203A\x1B[0m A new version of @codevector/cli is available: ${currentVersion} \u2192 ${cache.latest}. Run \`codevector update\` to install.
19585
+ `
19586
+ );
19587
+ }
19588
+ } catch {
19589
+ }
19590
+ }
19591
+ function scheduleUpdateCheck() {
19592
+ if (process.env.CODEVECTOR_NO_UPDATE_CHECK === "1") return;
19593
+ const cache = readCache();
19594
+ const now = Date.now();
19595
+ if (cache && now - cache.checkedAt < CHECK_TTL_MS) return;
19596
+ void fetchLatestVersion().then((latest) => {
19597
+ writeCache({ latest, checkedAt: Date.now() });
19598
+ }).catch(() => {
19599
+ writeCache({ latest: cache?.latest ?? null, checkedAt: Date.now() });
19600
+ });
19601
+ }
19602
+ async function fetchLatestVersion() {
19603
+ const controller = new AbortController();
19604
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
19605
+ timer.unref?.();
19606
+ try {
19607
+ const res = await fetch(REGISTRY_URL, {
19608
+ signal: controller.signal,
19609
+ headers: { accept: "application/json" }
19610
+ });
19611
+ if (!res.ok) return null;
19612
+ const body = await res.json();
19613
+ return typeof body.version === "string" ? body.version : null;
19614
+ } finally {
19615
+ clearTimeout(timer);
19616
+ }
19617
+ }
19618
+
19619
+ // src/lib/version-check.ts
19620
+ init_package();
19621
+ var MIN_SERVER_VERSION = "";
19622
+ var IncompatibleVersionError = class extends Error {
19623
+ constructor(message) {
19624
+ super(message);
19625
+ this.name = "IncompatibleVersionError";
19626
+ }
19627
+ };
19628
+ function evaluateCompatibility(input) {
19629
+ const { cliVersion, minServerVersion, health } = input;
19630
+ if (health.minCliVersion && isNewer(cliVersion, health.minCliVersion)) {
19631
+ return `This CLI (${cliVersion}) is too old for the gateway, which requires >= ${health.minCliVersion}. Run \`codevector update\` to upgrade.`;
19632
+ }
19633
+ if (minServerVersion) {
19634
+ if (!health.version) {
19635
+ return `This CLI requires a gateway >= ${minServerVersion}, but the gateway is too old to report its version. Ask your admin to upgrade the gateway.`;
19636
+ }
19637
+ if (isNewer(health.version, minServerVersion)) {
19638
+ return `This CLI requires a gateway >= ${minServerVersion}, but the gateway is ${health.version}. Ask your admin to upgrade the gateway.`;
19639
+ }
19640
+ }
19641
+ return null;
19642
+ }
19643
+ async function fetchHealthVersion(baseUrl) {
19644
+ try {
19645
+ const res = await healthClient(baseUrl).$get();
19646
+ if (!res.ok) return null;
19647
+ const body = await res.json();
19648
+ return {
19649
+ version: typeof body.version === "string" ? body.version : void 0,
19650
+ minCliVersion: typeof body.minCliVersion === "string" ? body.minCliVersion : void 0
19651
+ };
19652
+ } catch {
19653
+ return null;
19654
+ }
19655
+ }
19656
+ async function checkServerCompatibility(baseUrl) {
19657
+ const health = await fetchHealthVersion(baseUrl);
19658
+ if (!health) return void 0;
19659
+ const problem = evaluateCompatibility({
19660
+ cliVersion: package_default.version,
19661
+ minServerVersion: MIN_SERVER_VERSION,
19662
+ health
19663
+ });
19664
+ if (problem) throw new IncompatibleVersionError(problem);
19665
+ return health.version;
19666
+ }
19667
+
19668
+ // src/commands/doctor.ts
19317
19669
  var CLAUDE_SCOPE_ORDER = ["local", "project", "user"];
19318
19670
  var doctorCommand = defineCommand({
19319
19671
  meta: {
@@ -19335,6 +19687,21 @@ var doctorCommand = defineCommand({
19335
19687
  label: "credentials",
19336
19688
  detail: `${creds.email} @ ${creds.gatewayUrl} (${maskApiKey(creds.apiKey)})`
19337
19689
  });
19690
+ checks.push({
19691
+ level: "ok",
19692
+ label: "billing",
19693
+ detail: creds.groupName ? `group "${creds.groupName}"` : "personal"
19694
+ });
19695
+ const pinned = readProjectConfig(userCwd())?.config.groups ?? [];
19696
+ if (pinned.length > 0) {
19697
+ const activeGroup = creds.groupName ?? null;
19698
+ const matches = activeGroup != null && pinned.includes(activeGroup);
19699
+ checks.push({
19700
+ level: matches ? "ok" : "warn",
19701
+ label: "repo group binding",
19702
+ detail: matches ? `active key bills "${activeGroup}" (pinned: ${pinned.join(", ")})` : `pinned ${pinned.join(", ")} but active key bills ${activeGroup ?? "personal"} \u2014 run \`codevector use\``
19703
+ });
19704
+ }
19338
19705
  if (process.platform !== "win32") {
19339
19706
  checks.push(
19340
19707
  credentialsFileModeOk() ? { level: "ok", label: "credentials permissions", detail: "chmod 600" } : {
@@ -19360,6 +19727,22 @@ var doctorCommand = defineCommand({
19360
19727
  const message = err instanceof ApiClientError ? `${err.code}: ${err.message}` : String(err);
19361
19728
  checks.push({ level: "fail", label: "gateway /me", detail: message });
19362
19729
  }
19730
+ try {
19731
+ const serverVersion = await checkServerCompatibility(creds.gatewayUrl);
19732
+ checks.push(
19733
+ serverVersion ? { level: "ok", label: "gateway version", detail: serverVersion } : {
19734
+ level: "warn",
19735
+ label: "gateway version",
19736
+ detail: "gateway did not report a version \u2014 it predates version reporting"
19737
+ }
19738
+ );
19739
+ } catch (err) {
19740
+ checks.push({
19741
+ level: "fail",
19742
+ label: "gateway version",
19743
+ detail: err instanceof Error ? err.message : String(err)
19744
+ });
19745
+ }
19363
19746
  }
19364
19747
  checks.push(inspectClaudeSettings());
19365
19748
  checks.push(inspectShellHook());
@@ -19391,9 +19774,9 @@ function inspectUpdateManager() {
19391
19774
  function inspectClaudeSettings() {
19392
19775
  for (const scope of CLAUDE_SCOPE_ORDER) {
19393
19776
  const path = claudeSettingsPath(scope);
19394
- if (!existsSync9(path)) continue;
19777
+ if (!existsSync11(path)) continue;
19395
19778
  try {
19396
- const raw = JSON.parse(readFileSync9(path, "utf8"));
19779
+ const raw = JSON.parse(readFileSync11(path, "utf8"));
19397
19780
  if (typeof raw.env?.ANTHROPIC_BASE_URL === "string") {
19398
19781
  return {
19399
19782
  level: "ok",
@@ -19461,7 +19844,7 @@ function inspectManifestDrift() {
19461
19844
  });
19462
19845
  continue;
19463
19846
  }
19464
- if (!existsSync9(path)) {
19847
+ if (!existsSync11(path)) {
19465
19848
  checks.push({
19466
19849
  level: "fail",
19467
19850
  label: `manifest drift: ${tool.tool}`,
@@ -19471,7 +19854,7 @@ function inspectManifestDrift() {
19471
19854
  }
19472
19855
  let raw;
19473
19856
  try {
19474
- raw = readFileSync9(path, "utf8");
19857
+ raw = readFileSync11(path, "utf8");
19475
19858
  } catch (err) {
19476
19859
  checks.push({
19477
19860
  level: "fail",
@@ -19501,7 +19884,7 @@ function pruneAcceptanceHook() {
19501
19884
  const cleaned = [];
19502
19885
  for (const scope of CLAUDE_SCOPE_ORDER) {
19503
19886
  const path = claudeSettingsPath(scope);
19504
- if (!existsSync9(path)) continue;
19887
+ if (!existsSync11(path)) continue;
19505
19888
  try {
19506
19889
  if (removeAcceptanceHookFromSettings(path)) cleaned.push(scope);
19507
19890
  } catch {
@@ -19514,8 +19897,8 @@ function pruneAcceptanceHook() {
19514
19897
  detail: `cleared stale hook from claude-code settings [${cleaned.join(", ")}]`
19515
19898
  });
19516
19899
  }
19517
- const legacyHooksDir = join11(CODEVECTOR_CONFIG_DIR, "hooks");
19518
- if (existsSync9(legacyHooksDir)) {
19900
+ const legacyHooksDir = join13(CODEVECTOR_CONFIG_DIR, "hooks");
19901
+ if (existsSync11(legacyHooksDir)) {
19519
19902
  try {
19520
19903
  rmSync2(legacyHooksDir, { recursive: true, force: true });
19521
19904
  checks.push({
@@ -19529,7 +19912,7 @@ function pruneAcceptanceHook() {
19529
19912
  return checks;
19530
19913
  }
19531
19914
  function removeAcceptanceHookFromSettings(path) {
19532
- const parsed = JSON.parse(readFileSync9(path, "utf8"));
19915
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
19533
19916
  const hooks = parsed.hooks;
19534
19917
  if (!isPlainObject3(hooks)) return false;
19535
19918
  let mutated = false;
@@ -19543,7 +19926,7 @@ function removeAcceptanceHookFromSettings(path) {
19543
19926
  else hooks[event] = entries;
19544
19927
  }
19545
19928
  if (Object.keys(hooks).length === 0) delete parsed.hooks;
19546
- if (mutated) writeFileSync7(path, `${JSON.stringify(parsed, null, 2)}
19929
+ if (mutated) writeFileSync9(path, `${JSON.stringify(parsed, null, 2)}
19547
19930
  `);
19548
19931
  return mutated;
19549
19932
  }
@@ -19584,6 +19967,8 @@ function manifestToolPath(tool, scope) {
19584
19967
  switch (tool) {
19585
19968
  case "claude-code":
19586
19969
  return claudeSettingsPath(scope);
19970
+ case "cline":
19971
+ return clineSettingsPath(scope);
19587
19972
  case "opencode":
19588
19973
  return opencodeSettingsPath(scope);
19589
19974
  default:
@@ -19611,7 +19996,7 @@ init_credentials();
19611
19996
  init_paths();
19612
19997
  init_project_config();
19613
19998
  init_shell();
19614
- import { dirname as dirname5 } from "path";
19999
+ import { dirname as dirname6 } from "path";
19615
20000
  var envCommand = defineCommand({
19616
20001
  meta: {
19617
20002
  name: "env",
@@ -19629,7 +20014,7 @@ var envCommand = defineCommand({
19629
20014
  const cwd = userCwd();
19630
20015
  const previousDir = process.env.CODEVECTOR_ACTIVE_DIR ?? null;
19631
20016
  const found = readProjectConfig(cwd);
19632
- const repoDir = found?.path ? dirname5(found.path) : null;
20017
+ const repoDir = found?.path ? dirname6(found.path) : null;
19633
20018
  const gateway = found?.config.gateway ?? null;
19634
20019
  const projectName = found?.config.projectName ?? null;
19635
20020
  const lines = [];
@@ -19638,16 +20023,25 @@ var envCommand = defineCommand({
19638
20023
  process.stdout.write(lines.join("\n") + (lines.length > 0 ? "\n" : ""));
19639
20024
  return;
19640
20025
  }
19641
- if (previousDir === repoDir) {
19642
- return;
19643
- }
19644
20026
  const profiles = await readProfiles();
19645
- const match = pickProfileForGateway(
20027
+ const repoGroups = found?.config.groups ?? null;
20028
+ const resolution = resolveProfileForRepo(
19646
20029
  profiles?.profiles ?? {},
19647
20030
  profiles?.activeProfile ?? null,
19648
- gateway
20031
+ gateway,
20032
+ repoGroups
19649
20033
  );
19650
- if (!match) {
20034
+ if (resolution.kind === "no-group-key") {
20035
+ if (previousDir) emitDeactivate(lines, shell, previousDir);
20036
+ emitEcho(
20037
+ lines,
20038
+ shell,
20039
+ `codevector: this repo bills group(s) "${resolution.groups.join(", ")}" but you have no key for any. Run \`codevector sync\` or \`codevector use\`.`
20040
+ );
20041
+ process.stdout.write(lines.join("\n") + "\n");
20042
+ return;
20043
+ }
20044
+ if (resolution.kind === "none") {
19651
20045
  if (previousDir) emitDeactivate(lines, shell, previousDir);
19652
20046
  emitEcho(
19653
20047
  lines,
@@ -19657,12 +20051,26 @@ var envCommand = defineCommand({
19657
20051
  process.stdout.write(lines.join("\n") + "\n");
19658
20052
  return;
19659
20053
  }
19660
- const { name: profileName, profile } = match;
20054
+ const { name: profileName, profile } = resolution;
20055
+ const previousProfile = process.env.CODEVECTOR_ACTIVE_PROFILE ?? null;
20056
+ if (!shouldReexport(previousDir, repoDir, previousProfile, profileName)) {
20057
+ return;
20058
+ }
19661
20059
  const headers = buildAnthropicCustomHeaders(projectName);
19662
- emitExport(lines, shell, "ANTHROPIC_BASE_URL", `${trimRightSlash5(profile.gatewayUrl)}/gateway/anthropic`);
20060
+ emitExport(
20061
+ lines,
20062
+ shell,
20063
+ "ANTHROPIC_BASE_URL",
20064
+ `${trimRightSlash6(profile.gatewayUrl)}/gateway/anthropic`
20065
+ );
19663
20066
  emitExport(lines, shell, "ANTHROPIC_API_KEY", profile.apiKey);
19664
20067
  if (headers) emitExport(lines, shell, "ANTHROPIC_CUSTOM_HEADERS", headers);
19665
- emitExport(lines, shell, "OPENAI_BASE_URL", `${trimRightSlash5(profile.gatewayUrl)}/gateway/openai/v1`);
20068
+ emitExport(
20069
+ lines,
20070
+ shell,
20071
+ "OPENAI_BASE_URL",
20072
+ `${trimRightSlash6(profile.gatewayUrl)}/gateway/openai/v1`
20073
+ );
19666
20074
  emitExport(lines, shell, "OPENAI_API_KEY", profile.apiKey);
19667
20075
  emitExport(lines, shell, "CODEVECTOR_ACTIVE_DIR", repoDir);
19668
20076
  emitExport(lines, shell, "CODEVECTOR_ACTIVE_PROFILE", profileName);
@@ -19670,11 +20078,14 @@ var envCommand = defineCommand({
19670
20078
  emitEcho(
19671
20079
  lines,
19672
20080
  shell,
19673
- `codevector: ${profileName} -> ${trimRightSlash5(profile.gatewayUrl)}${projectSuffix}`
20081
+ `codevector: ${profileName} -> ${trimRightSlash6(profile.gatewayUrl)}${projectSuffix}`
19674
20082
  );
19675
20083
  process.stdout.write(lines.join("\n") + "\n");
19676
20084
  }
19677
20085
  });
20086
+ function shouldReexport(previousDir, repoDir, previousProfile, pickedProfile) {
20087
+ return !(previousDir === repoDir && previousProfile === pickedProfile);
20088
+ }
19678
20089
  function resolveShell(raw) {
19679
20090
  if (!raw) return detectShell() ?? "bash";
19680
20091
  if (!SHELLS.includes(raw)) {
@@ -19728,7 +20139,7 @@ function quoteForShell(shell, value) {
19728
20139
  }
19729
20140
  return `'${value.replace(/'/g, `'\\''`)}'`;
19730
20141
  }
19731
- function trimRightSlash5(url2) {
20142
+ function trimRightSlash6(url2) {
19732
20143
  return url2.endsWith("/") ? url2.slice(0, -1) : url2;
19733
20144
  }
19734
20145
  function buildAnthropicCustomHeaders(projectName) {
@@ -19737,19 +20148,24 @@ function buildAnthropicCustomHeaders(projectName) {
19737
20148
  if (!safe) return null;
19738
20149
  return `x-project: ${safe}`;
19739
20150
  }
19740
- function pickProfileForGateway(profiles, activeProfile, gateway) {
19741
- const target = trimRightSlash5(gateway);
19742
- const matches = Object.entries(profiles).filter(
19743
- ([, p2]) => trimRightSlash5(p2.gatewayUrl) === target
20151
+ function resolveProfileForRepo(profiles, activeProfile, gateway, repoGroups) {
20152
+ const target = trimRightSlash6(gateway);
20153
+ const gatewayMatches = Object.entries(profiles).filter(
20154
+ ([, p2]) => trimRightSlash6(p2.gatewayUrl) === target
19744
20155
  );
19745
- if (matches.length === 0) return null;
20156
+ if (gatewayMatches.length === 0) return { kind: "none" };
20157
+ const groups = repoGroups?.filter((g) => g.length > 0) ?? [];
20158
+ const pool = groups.length === 0 ? gatewayMatches : gatewayMatches.filter(([, p2]) => p2.groupName != null && groups.includes(p2.groupName));
20159
+ if (pool.length === 0) return { kind: "no-group-key", groups };
20160
+ const picked = preferActive(pool, activeProfile);
20161
+ return { kind: "match", name: picked[0], profile: picked[1] };
20162
+ }
20163
+ function preferActive(pool, activeProfile) {
19746
20164
  if (activeProfile) {
19747
- const active = matches.find(([name]) => name === activeProfile);
19748
- if (active) return { name: active[0], profile: active[1] };
20165
+ const active = pool.find(([name]) => name === activeProfile);
20166
+ if (active) return active;
19749
20167
  }
19750
- const sorted = [...matches].sort((a, b2) => a[0].localeCompare(b2[0]));
19751
- const picked = sorted[0];
19752
- return { name: picked[0], profile: picked[1] };
20168
+ return [...pool].sort((a, b2) => a[0].localeCompare(b2[0]))[0];
19753
20169
  }
19754
20170
 
19755
20171
  // src/commands/github/index.ts
@@ -19762,46 +20178,42 @@ init_api_client();
19762
20178
  init_credentials();
19763
20179
  init_paths();
19764
20180
  init_prompt();
19765
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
19766
- import { join as join12 } from "path";
20181
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync10 } from "fs";
20182
+ import { join as join14 } from "path";
19767
20183
 
19768
20184
  // src/commands/github/workflow-yaml.ts
19769
20185
  function generateWorkflowYaml(opts) {
19770
20186
  const baseUrl = opts.gatewayUrl.replace(/\/$/, "");
19771
20187
  const sanitized = { ...opts, gatewayUrl: baseUrl };
19772
- return sanitized.tool === "claude-code" ? claudeCodeWorkflow(sanitized) : opencodeWorkflow(sanitized);
20188
+ switch (sanitized.tool) {
20189
+ case "claude-code":
20190
+ return claudeCodeWorkflow(sanitized);
20191
+ case "cline":
20192
+ return clineWorkflow(sanitized);
20193
+ case "opencode":
20194
+ return opencodeWorkflow(sanitized);
20195
+ }
20196
+ }
20197
+ function prNumberExpr() {
20198
+ return "${{ github.event.pull_request.number || github.event.issue.number }}";
20199
+ }
20200
+ function baseRefExpr() {
20201
+ return "${{ github.base_ref }}";
19773
20202
  }
19774
20203
  function claudeCodeWorkflow(opts) {
19775
- const modelEnv = opts.model ? ` ANTHROPIC_MODEL: ${opts.model}
19776
- ` : "";
19777
- return [
19778
- `# Generated by codevector github init. Do not edit manually.`,
19779
- `# Routes all PR review requests through the CodeVector gateway.`,
19780
- `name: CodeVector PR Review (Claude Code)`,
19781
- ``,
19782
- `on:`,
19783
- ` pull_request:`,
19784
- ` types: [opened, synchronize]`,
19785
- ` issue_comment:`,
19786
- ` types: [created]`,
19787
- ` pull_request_review_comment:`,
19788
- ` types: [created]`,
19789
- ``,
19790
- `concurrency:`,
19791
- ` group: codevector-pr-review-claude-code-\${{ github.event.pull_request.number || github.event.issue.number }}`,
19792
- ` cancel-in-progress: true`,
19793
- ``,
19794
- `permissions:`,
19795
- ` contents: read`,
19796
- ` pull-requests: write`,
19797
- ``,
19798
- `jobs:`,
20204
+ const modelEnv = ` ANTHROPIC_MODEL: ${opts.model || "claude-sonnet-4-6"}
20205
+ `;
20206
+ const concurrencyGroup = `codevector-pr-review-claude-code-${prNumberExpr()}`;
20207
+ const reviewJob = [
19799
20208
  ` review:`,
19800
20209
  ` if: |`,
19801
20210
  ` github.event_name == 'pull_request' ||`,
19802
20211
  ` (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@claude')) ||`,
19803
20212
  ` (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))`,
19804
20213
  ` runs-on: ubuntu-latest`,
20214
+ ` concurrency:`,
20215
+ ` group: ${concurrencyGroup}`,
20216
+ ` cancel-in-progress: true`,
19805
20217
  ` steps:`,
19806
20218
  ` - uses: actions/checkout@v4`,
19807
20219
  ` - uses: actions/setup-node@v4`,
@@ -19816,14 +20228,153 @@ function claudeCodeWorkflow(opts) {
19816
20228
  ` run: |`,
19817
20229
  ` claude --dangerously-skip-permissions -p "Review this PR and provide concise, actionable feedback. Focus on bugs, security issues, and code quality." \\`,
19818
20230
  ` --output-format text \\`,
19819
- ` --allowedTools "Bash(git diff origin/\${{ github.base_ref }}...HEAD),Bash(git log origin/\${{ github.base_ref }}..HEAD),Read" \\`,
20231
+ ` --allowedTools "Bash(git diff origin/${baseRefExpr()}...HEAD),Bash(git log origin/${baseRefExpr()}..HEAD),Read" \\`,
19820
20232
  ` > review.md`,
19821
20233
  ` - name: Post review comment`,
19822
20234
  ` env:`,
19823
20235
  ` GH_TOKEN: \${{ github.token }}`,
19824
20236
  ` run: |`,
19825
- ` gh pr comment \${{ github.event.pull_request.number || github.event.issue.number }} --body-file review.md`
19826
- ].join("\n").trimEnd() + "\n";
20237
+ ` gh pr comment ${prNumberExpr()} --body-file review.md`
20238
+ ].join("\n");
20239
+ const skillJobs = (opts.skills ?? []).map(
20240
+ (name) => claudeCodeSkillJob(opts, name)
20241
+ );
20242
+ return workflowHeader("Claude Code", [reviewJob, ...skillJobs]);
20243
+ }
20244
+ function claudeCodeSkillJob(opts, skillName) {
20245
+ const outputFile = `skill-${skillName}.md`;
20246
+ const modelEnv = ` ANTHROPIC_MODEL: ${opts.model || "claude-sonnet-4-6"}
20247
+ `;
20248
+ return [
20249
+ ` skill-${skillName}:`,
20250
+ ` if: |`,
20251
+ ` github.event_name == 'pull_request' ||`,
20252
+ ` (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@skill:${skillName}')) ||`,
20253
+ ` (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@skill:${skillName}'))`,
20254
+ ` runs-on: ubuntu-latest`,
20255
+ ` concurrency:`,
20256
+ ` group: codevector-skill-${skillName}-${prNumberExpr()}`,
20257
+ ` cancel-in-progress: true`,
20258
+ ` steps:`,
20259
+ ` - uses: actions/checkout@v4`,
20260
+ ` - uses: actions/setup-node@v4`,
20261
+ ` with:`,
20262
+ ` node-version: '24'`,
20263
+ ` - run: npm install -g @anthropic-ai/claude-code @codevector/cli`,
20264
+ ` - name: Authenticate with CodeVector`,
20265
+ ` env:`,
20266
+ ` CODEVECTOR_API_KEY: \${{ secrets.${opts.secretName} }}`,
20267
+ ` run: |`,
20268
+ ` codevector auth login \\`,
20269
+ ` --gateway-url ${opts.gatewayUrl} \\`,
20270
+ ` --api-key "$CODEVECTOR_API_KEY" \\`,
20271
+ ` --profile ci`,
20272
+ ` - name: Sync skill pack`,
20273
+ ` run: codevector skills sync ${skillName}`,
20274
+ ` - name: Review PR with skill`,
20275
+ ` env:`,
20276
+ ` ANTHROPIC_API_KEY: \${{ secrets.${opts.secretName} }}`,
20277
+ ` ANTHROPIC_BASE_URL: ${opts.gatewayUrl}/gateway/anthropic`,
20278
+ `${modelEnv}`,
20279
+ ` run: |`,
20280
+ ` claude --dangerously-skip-permissions -p "Review this PR using the ${skillName} skill. Write your findings to ${outputFile}." \\`,
20281
+ ` --output-format text \\`,
20282
+ ` --allowedTools "Bash(git diff origin/${baseRefExpr()}...HEAD),Bash(git log origin/${baseRefExpr()}..HEAD),Read" \\`,
20283
+ ` > ${outputFile}`,
20284
+ ` - name: Post review comment`,
20285
+ ` env:`,
20286
+ ` GH_TOKEN: \${{ github.token }}`,
20287
+ ` run: |`,
20288
+ ` gh pr comment ${prNumberExpr()} --body-file ${outputFile}`
20289
+ ].join("\n");
20290
+ }
20291
+ function clineWorkflow(opts) {
20292
+ const modelFlag = opts.model ? ` --modelid "${opts.model}"` : "";
20293
+ const concurrencyGroup = `codevector-pr-review-cline-${prNumberExpr()}`;
20294
+ const reviewJob = [
20295
+ ` review:`,
20296
+ ` if: |`,
20297
+ ` github.event_name == 'pull_request' ||`,
20298
+ ` (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@cline')) ||`,
20299
+ ` (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@cline'))`,
20300
+ ` runs-on: ubuntu-latest`,
20301
+ ` concurrency:`,
20302
+ ` group: ${concurrencyGroup}`,
20303
+ ` cancel-in-progress: true`,
20304
+ ` steps:`,
20305
+ ` - uses: actions/checkout@v4`,
20306
+ ` - uses: actions/setup-node@v4`,
20307
+ ` with:`,
20308
+ ` node-version: '24'`,
20309
+ ` - run: npm install -g cline`,
20310
+ ` - name: Configure Cline auth`,
20311
+ ` run: |`,
20312
+ ` cline auth --provider openai-compatible --apikey "\${{ secrets.${opts.secretName} }}" --baseurl "${opts.gatewayUrl}/gateway/openai/v1"${modelFlag}`,
20313
+ ` - name: Review PR`,
20314
+ ` env:`,
20315
+ ` GH_TOKEN: \${{ github.token }}`,
20316
+ ` CLINE_COMMAND_PERMISSIONS: |`,
20317
+ ` {`,
20318
+ ` "allow": ["gh pr diff *", "gh pr view *", "git log *"],`,
20319
+ ` "deny": ["rm -rf *", "sudo *"]`,
20320
+ ` }`,
20321
+ ` run: |`,
20322
+ ` cline --auto-approve true "Review this PR. Fetch the diff with \`gh pr diff \${{ github.event.pull_request.number }}\`, analyze the changes, and post a single comprehensive review comment using \`gh pr comment\`."`
20323
+ ].join("\n");
20324
+ const skillJobs = (opts.skills ?? []).map(
20325
+ (name) => clineSkillJob(opts, name)
20326
+ );
20327
+ return workflowHeader("Cline", [reviewJob, ...skillJobs]);
20328
+ }
20329
+ function clineSkillJob(opts, skillName) {
20330
+ const modelFlag = opts.model ? ` --modelid "${opts.model}"` : "";
20331
+ const outputFile = `skill-${skillName}.md`;
20332
+ return [
20333
+ ` skill-${skillName}:`,
20334
+ ` if: |`,
20335
+ ` github.event_name == 'pull_request' ||`,
20336
+ ` (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@skill:${skillName}')) ||`,
20337
+ ` (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@skill:${skillName}'))`,
20338
+ ` runs-on: ubuntu-latest`,
20339
+ ` concurrency:`,
20340
+ ` group: codevector-skill-${skillName}-${prNumberExpr()}`,
20341
+ ` cancel-in-progress: true`,
20342
+ ` steps:`,
20343
+ ` - uses: actions/checkout@v4`,
20344
+ ` - uses: actions/setup-node@v4`,
20345
+ ` with:`,
20346
+ ` node-version: '24'`,
20347
+ ` - run: npm install -g cline @codevector/cli`,
20348
+ ` - name: Authenticate with CodeVector`,
20349
+ ` env:`,
20350
+ ` CODEVECTOR_API_KEY: \${{ secrets.${opts.secretName} }}`,
20351
+ ` run: |`,
20352
+ ` codevector auth login \\`,
20353
+ ` --gateway-url ${opts.gatewayUrl} \\`,
20354
+ ` --api-key "$CODEVECTOR_API_KEY" \\`,
20355
+ ` --profile ci`,
20356
+ ` - name: Sync skill pack`,
20357
+ ` run: codevector skills sync ${skillName}`,
20358
+ ` - name: Configure Cline auth`,
20359
+ ` run: |`,
20360
+ ` cline auth --provider openai-compatible --apikey "\${{ secrets.${opts.secretName} }}" --baseurl "${opts.gatewayUrl}/gateway/openai/v1"${modelFlag}`,
20361
+ ` - name: Review PR with skill`,
20362
+ ` env:`,
20363
+ ` GH_TOKEN: \${{ github.token }}`,
20364
+ ` CLINE_COMMAND_PERMISSIONS: |`,
20365
+ ` {`,
20366
+ ` "allow": ["gh pr diff *", "gh pr view *", "git log *"],`,
20367
+ ` "deny": ["rm -rf *", "sudo *"]`,
20368
+ ` }`,
20369
+ ` run: |`,
20370
+ ` cline --auto-approve true "Review this PR using the ${skillName} skill. Write your findings to ${outputFile}." \\`,
20371
+ ` > ${outputFile}`,
20372
+ ` - name: Post review comment`,
20373
+ ` env:`,
20374
+ ` GH_TOKEN: \${{ github.token }}`,
20375
+ ` run: |`,
20376
+ ` gh pr comment ${prNumberExpr()} --body-file ${outputFile}`
20377
+ ].join("\n");
19827
20378
  }
19828
20379
  function opencodeModelEntryLines(model) {
19829
20380
  const name = model.split("-").map((w3) => w3.charAt(0).toUpperCase() + w3.slice(1)).join(" ");
@@ -19837,34 +20388,17 @@ function opencodeModelEntryLines(model) {
19837
20388
  }
19838
20389
  function opencodeWorkflow(opts) {
19839
20390
  const model = opts.model || "deepseek-pro";
19840
- return [
19841
- `# Generated by codevector github init. Do not edit manually.`,
19842
- `# Routes all PR review requests through the CodeVector gateway via OpenCode CLI.`,
19843
- `name: CodeVector PR Review (OpenCode)`,
19844
- ``,
19845
- `on:`,
19846
- ` pull_request:`,
19847
- ` types: [opened, synchronize]`,
19848
- ` issue_comment:`,
19849
- ` types: [created]`,
19850
- ` pull_request_review_comment:`,
19851
- ` types: [created]`,
19852
- ``,
19853
- `concurrency:`,
19854
- ` group: codevector-pr-review-opencode-\${{ github.event.pull_request.number || github.event.issue.number }}`,
19855
- ` cancel-in-progress: true`,
19856
- ``,
19857
- `permissions:`,
19858
- ` contents: read`,
19859
- ` pull-requests: write`,
19860
- ``,
19861
- `jobs:`,
20391
+ const concurrencyGroup = `codevector-pr-review-opencode-${prNumberExpr()}`;
20392
+ const reviewJob = [
19862
20393
  ` review:`,
19863
20394
  ` if: |`,
19864
20395
  ` github.event_name == 'pull_request' ||`,
19865
20396
  ` (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@opencode')) ||`,
19866
20397
  ` (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@opencode'))`,
19867
20398
  ` runs-on: ubuntu-latest`,
20399
+ ` concurrency:`,
20400
+ ` group: ${concurrencyGroup}`,
20401
+ ` cancel-in-progress: true`,
19868
20402
  ` steps:`,
19869
20403
  ` - uses: actions/checkout@v4`,
19870
20404
  ` - name: Install OpenCode CLI`,
@@ -19894,7 +20428,7 @@ function opencodeWorkflow(opts) {
19894
20428
  ` env:`,
19895
20429
  ` GH_TOKEN: \${{ github.token }}`,
19896
20430
  ` run: |`,
19897
- ` PR_NUMBER=\${{ github.event.pull_request.number || github.event.issue.number }}`,
20431
+ ` PR_NUMBER=${prNumberExpr()}`,
19898
20432
  ` gh pr diff $PR_NUMBER > pr_diff.txt`,
19899
20433
  ` - name: Fetch AI Review`,
19900
20434
  ` env:`,
@@ -19910,15 +20444,114 @@ function opencodeWorkflow(opts) {
19910
20444
  ` env:`,
19911
20445
  ` GH_TOKEN: \${{ github.token }}`,
19912
20446
  ` run: |`,
19913
- ` PR_NUMBER=\${{ github.event.pull_request.number || github.event.issue.number }}`,
20447
+ ` PR_NUMBER=${prNumberExpr()}`,
19914
20448
  ` gh pr comment $PR_NUMBER --body-file review.md`
20449
+ ].join("\n");
20450
+ const skillJobs = (opts.skills ?? []).map(
20451
+ (name) => opencodeSkillJob(opts, name, model)
20452
+ );
20453
+ return workflowHeader("OpenCode", [reviewJob, ...skillJobs]);
20454
+ }
20455
+ function opencodeSkillJob(opts, skillName, model) {
20456
+ const outputFile = `skill-${skillName}.md`;
20457
+ return [
20458
+ ` skill-${skillName}:`,
20459
+ ` if: |`,
20460
+ ` github.event_name == 'pull_request' ||`,
20461
+ ` (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '@skill:${skillName}')) ||`,
20462
+ ` (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@skill:${skillName}'))`,
20463
+ ` runs-on: ubuntu-latest`,
20464
+ ` concurrency:`,
20465
+ ` group: codevector-skill-${skillName}-${prNumberExpr()}`,
20466
+ ` cancel-in-progress: true`,
20467
+ ` steps:`,
20468
+ ` - uses: actions/checkout@v4`,
20469
+ ` - name: Install OpenCode CLI`,
20470
+ ` run: curl -fsSL https://opencode.ai/install | bash`,
20471
+ ` - run: npm install -g @codevector/cli`,
20472
+ ` - name: Authenticate with CodeVector`,
20473
+ ` env:`,
20474
+ ` CODEVECTOR_API_KEY: \${{ secrets.${opts.secretName} }}`,
20475
+ ` run: |`,
20476
+ ` codevector auth login \\`,
20477
+ ` --gateway-url ${opts.gatewayUrl} \\`,
20478
+ ` --api-key "$CODEVECTOR_API_KEY" \\`,
20479
+ ` --profile ci`,
20480
+ ` - name: Sync skill pack`,
20481
+ ` run: codevector skills sync ${skillName}`,
20482
+ ` - name: Configure OpenCode`,
20483
+ ` run: |`,
20484
+ ` cat > opencode.json << 'OPEOF'`,
20485
+ ` {`,
20486
+ ` "$schema": "https://opencode.ai/config.json",`,
20487
+ ` "provider": {`,
20488
+ ` "codevector": {`,
20489
+ ` "options": {`,
20490
+ ` "apiKey": "{env:CODEVECTOR_API_KEY}",`,
20491
+ ` "baseURL": "${opts.gatewayUrl}/gateway/openai/v1",`,
20492
+ ` "headers": {`,
20493
+ ` "x-client-app": "opencode",`,
20494
+ ` "x-project": "codevector"`,
20495
+ ` }`,
20496
+ ` },`,
20497
+ ...opencodeModelEntryLines(model),
20498
+ ` }`,
20499
+ ` },`,
20500
+ ` "model": "codevector/${model}"`,
20501
+ ` }`,
20502
+ ` OPEOF`,
20503
+ ` - name: Generate PR Diff`,
20504
+ ` env:`,
20505
+ ` GH_TOKEN: \${{ github.token }}`,
20506
+ ` run: |`,
20507
+ ` PR_NUMBER=${prNumberExpr()}`,
20508
+ ` gh pr diff $PR_NUMBER > pr_diff.txt`,
20509
+ ` - name: Fetch AI Review with skill`,
20510
+ ` env:`,
20511
+ ` CODEVECTOR_API_KEY: \${{ secrets.${opts.secretName} }}`,
20512
+ ` run: |`,
20513
+ ` export PATH="$HOME/.opencode/bin:$PATH"`,
20514
+ ` opencode run --model codevector/${model} \\`,
20515
+ ` --dangerously-skip-permissions \\`,
20516
+ ` "Review this PR using the ${skillName} skill. Write your findings to ${outputFile}." \\`,
20517
+ ` -f pr_diff.txt \\`,
20518
+ ` > ${outputFile}`,
20519
+ ` - name: Post review comment`,
20520
+ ` env:`,
20521
+ ` GH_TOKEN: \${{ github.token }}`,
20522
+ ` run: |`,
20523
+ ` PR_NUMBER=${prNumberExpr()}`,
20524
+ ` gh pr comment $PR_NUMBER --body-file ${outputFile}`
20525
+ ].join("\n");
20526
+ }
20527
+ function workflowHeader(toolLabel, jobs) {
20528
+ return [
20529
+ `# Generated by codevector github init. Do not edit manually.`,
20530
+ `# Routes all PR review requests through the CodeVector gateway.`,
20531
+ `name: CodeVector PR Review (${toolLabel})`,
20532
+ ``,
20533
+ `on:`,
20534
+ ` pull_request:`,
20535
+ ` types: [opened, synchronize]`,
20536
+ ` issue_comment:`,
20537
+ ` types: [created]`,
20538
+ ` pull_request_review_comment:`,
20539
+ ` types: [created]`,
20540
+ ``,
20541
+ `permissions:`,
20542
+ ` contents: read`,
20543
+ ` pull-requests: write`,
20544
+ ``,
20545
+ `jobs:`,
20546
+ ...jobs
19915
20547
  ].join("\n").trimEnd() + "\n";
19916
20548
  }
19917
20549
 
19918
20550
  // src/commands/github/init.ts
19919
- var ALL_TOOLS2 = ["claude-code", "opencode"];
20551
+ var ALL_TOOLS2 = ["claude-code", "cline", "opencode"];
19920
20552
  var WORKFLOW_FILENAMES = {
19921
20553
  "claude-code": "codevector-pr-review-claude-code.yml",
20554
+ cline: "codevector-pr-review-cline.yml",
19922
20555
  opencode: "codevector-pr-review-opencode.yml"
19923
20556
  };
19924
20557
  var githubInitCommand = defineCommand({
@@ -19929,8 +20562,8 @@ var githubInitCommand = defineCommand({
19929
20562
  args: {
19930
20563
  tool: {
19931
20564
  type: "string",
19932
- description: "Tool: claude-code or opencode. Prompted if omitted.",
19933
- valueHint: "claude-code|opencode"
20565
+ description: "Tool: claude-code, cline, or opencode. Prompted if omitted.",
20566
+ valueHint: "claude-code|cline|opencode"
19934
20567
  },
19935
20568
  model: {
19936
20569
  type: "string",
@@ -19954,6 +20587,10 @@ var githubInitCommand = defineCommand({
19954
20587
  type: "boolean",
19955
20588
  default: false,
19956
20589
  description: "Print the workflow to stdout instead of writing to disk."
20590
+ },
20591
+ skills: {
20592
+ type: "string",
20593
+ description: "Comma-separated list of skill pack names to generate CI jobs for."
19957
20594
  }
19958
20595
  },
19959
20596
  async run({ args }) {
@@ -19964,11 +20601,13 @@ var githubInitCommand = defineCommand({
19964
20601
  const tool = await resolveTool(args, interactive);
19965
20602
  const model = await resolveModel(args, tool, gatewayUrl, profile, interactive);
19966
20603
  const secretName = args["secret-name"] ?? "CODEVECTOR_API_KEY";
20604
+ const skills = args.skills ? args.skills.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
19967
20605
  const yaml = generateWorkflowYaml({
19968
20606
  tool,
19969
20607
  gatewayUrl: gatewayUrl.replace(/\/$/, ""),
19970
20608
  secretName,
19971
- ...model ? { model } : {}
20609
+ ...model ? { model } : {},
20610
+ ...skills ? { skills } : {}
19972
20611
  });
19973
20612
  if (args["dry-run"]) {
19974
20613
  process.stdout.write(yaml);
@@ -19982,9 +20621,9 @@ var githubInitCommand = defineCommand({
19982
20621
  return;
19983
20622
  }
19984
20623
  const cwd = userCwd();
19985
- const workflowsDir = join12(cwd, ".github", "workflows");
19986
- const filePath = join12(workflowsDir, WORKFLOW_FILENAMES[tool]);
19987
- if (existsSync10(filePath) && !args.force) {
20624
+ const workflowsDir = join14(cwd, ".github", "workflows");
20625
+ const filePath = join14(workflowsDir, WORKFLOW_FILENAMES[tool]);
20626
+ if (existsSync12(filePath) && !args.force) {
19988
20627
  if (!interactive) {
19989
20628
  throw new Error(
19990
20629
  `${filePath} already exists. Pass --force to overwrite.`
@@ -20001,8 +20640,8 @@ var githubInitCommand = defineCommand({
20001
20640
  return;
20002
20641
  }
20003
20642
  }
20004
- mkdirSync5(workflowsDir, { recursive: true });
20005
- writeFileSync8(filePath, yaml, { mode: 420 });
20643
+ mkdirSync7(workflowsDir, { recursive: true });
20644
+ writeFileSync10(filePath, yaml, { mode: 420 });
20006
20645
  if (interactive) {
20007
20646
  R2.success(`Wrote ${filePath}`);
20008
20647
  Se(
@@ -20018,7 +20657,7 @@ var githubInitCommand = defineCommand({
20018
20657
  });
20019
20658
  function isInteractive2(args) {
20020
20659
  if (!process.stdout.isTTY) return false;
20021
- return !args.tool || !args["gateway-url"];
20660
+ return !args.tool || !args["gateway-url"] || !args.model;
20022
20661
  }
20023
20662
  function resolveGatewayUrl(args, profile, interactive) {
20024
20663
  if (args["gateway-url"]) {
@@ -20057,14 +20696,17 @@ async function resolveTool(args, interactive) {
20057
20696
  message: "Which tool should the workflow use for PR review?",
20058
20697
  options: [
20059
20698
  { value: "claude-code", label: "Claude Code", hint: "uses the @anthropic-ai/claude-code CLI" },
20699
+ { value: "cline", label: "Cline", hint: "uses the cline CLI (npm i -g cline)" },
20060
20700
  { value: "opencode", label: "OpenCode", hint: "uses the OpenCode CLI" }
20061
20701
  ],
20062
20702
  initialValue: "claude-code"
20063
20703
  })
20064
20704
  );
20065
20705
  }
20066
- async function resolveModel(args, _tool, gatewayUrl, profile, interactive) {
20706
+ async function resolveModel(args, tool, gatewayUrl, profile, interactive) {
20067
20707
  if (args.model) return args.model;
20708
+ const configuredModel = profile?.toolConfigs?.find((c) => c.tool === tool)?.modelSlug;
20709
+ if (configuredModel) return configuredModel;
20068
20710
  if (!interactive) return void 0;
20069
20711
  const models = profile ? await fetchReachableChatModels3(gatewayUrl, profile.apiKey) : [];
20070
20712
  if (models.length === 0) {
@@ -20094,7 +20736,7 @@ async function fetchReachableChatModels3(gatewayUrl, apiKey) {
20094
20736
  s.start("Loading reachable models\u2026");
20095
20737
  try {
20096
20738
  const res = await call(parseResponse(client.models.$get()));
20097
- const models = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
20739
+ const models = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
20098
20740
  slug: m2.slug,
20099
20741
  providerKind: m2.providerKind,
20100
20742
  displayName: m2.displayName
@@ -20124,6 +20766,82 @@ var githubCommand = defineCommand({
20124
20766
  }
20125
20767
  });
20126
20768
 
20769
+ // src/commands/groups.ts
20770
+ init_dist();
20771
+ init_dist5();
20772
+ init_api_client();
20773
+ init_credentials();
20774
+
20775
+ // src/lib/table.ts
20776
+ function renderTable(headers, rows) {
20777
+ const widths = headers.map(
20778
+ (h2, i) => Math.max(h2.length, ...rows.map((row) => (row[i] ?? "").length))
20779
+ );
20780
+ const pad = (s, i) => s.padEnd(widths[i] ?? 0);
20781
+ const divider = widths.map((w3) => "\u2500".repeat(w3)).join(" ");
20782
+ return [headers.map(pad).join(" "), divider, ...rows.map((row) => row.map(pad).join(" "))].join(
20783
+ "\n"
20784
+ );
20785
+ }
20786
+
20787
+ // src/commands/groups.ts
20788
+ var groupsListCommand = defineCommand({
20789
+ meta: {
20790
+ name: "list",
20791
+ description: "List groups you can mint a key for."
20792
+ },
20793
+ args: {
20794
+ json: {
20795
+ type: "boolean",
20796
+ description: "Emit raw JSON (for scripting)."
20797
+ }
20798
+ },
20799
+ async run({ args }) {
20800
+ const active = await getActiveProfile();
20801
+ if (!active) {
20802
+ R2.warn("Not signed in. Run `codevector auth login` to get started.");
20803
+ process.exitCode = 1;
20804
+ return;
20805
+ }
20806
+ const client = gatewayClient(active.gatewayUrl, active.apiKey, 1e4);
20807
+ const s = args.json ? null : ft();
20808
+ s?.start("Loading groups\u2026");
20809
+ try {
20810
+ const res = await call(parseResponse(client.groups.$get()));
20811
+ s?.stop(`${res.data.length} group${res.data.length === 1 ? "" : "s"}`);
20812
+ if (args.json) {
20813
+ process.stdout.write(`${JSON.stringify(res.data, null, 2)}
20814
+ `);
20815
+ return;
20816
+ }
20817
+ if (res.data.length === 0) {
20818
+ R2.info("You are not a member of any groups. Ask an admin to add you.");
20819
+ return;
20820
+ }
20821
+ Se(
20822
+ renderTable(
20823
+ ["NAME", "ID"],
20824
+ res.data.map((g) => [g.name, g.id])
20825
+ ),
20826
+ "Your groups"
20827
+ );
20828
+ } catch (err) {
20829
+ s?.stop("Could not load groups");
20830
+ R2.error(err instanceof ApiClientError ? err.message : String(err));
20831
+ process.exitCode = 1;
20832
+ }
20833
+ }
20834
+ });
20835
+ var groupsCommand = defineCommand({
20836
+ meta: {
20837
+ name: "groups",
20838
+ description: "List the groups you can mint keys for."
20839
+ },
20840
+ subCommands: {
20841
+ list: groupsListCommand
20842
+ }
20843
+ });
20844
+
20127
20845
  // src/commands/hook.ts
20128
20846
  init_dist();
20129
20847
  init_shell();
@@ -20228,23 +20946,132 @@ if (-not $global:_CODEVECTOR_PROMPT_WRAPPED) {
20228
20946
  // src/index.ts
20229
20947
  init_init();
20230
20948
 
20231
- // src/commands/models.ts
20949
+ // src/commands/keys.ts
20232
20950
  init_dist();
20233
20951
  init_dist5();
20234
20952
  init_api_client();
20235
20953
  init_credentials();
20954
+ var sortProfileNames = (names) => names.sort((a, b2) => {
20955
+ if (a === "default") return -1;
20956
+ if (b2 === "default") return 1;
20957
+ return a.localeCompare(b2);
20958
+ });
20959
+ var keysCreateCommand = defineCommand({
20960
+ meta: {
20961
+ name: "create",
20962
+ description: "Mint a group-scoped key and save it as a profile."
20963
+ },
20964
+ args: {
20965
+ group: {
20966
+ type: "string",
20967
+ description: "Name of the group to bill (you must be a member).",
20968
+ required: true
20969
+ },
20970
+ name: {
20971
+ type: "string",
20972
+ description: "Optional label for the new key."
20973
+ }
20974
+ },
20975
+ async run({ args }) {
20976
+ const active = await getActiveProfile();
20977
+ if (!active) {
20978
+ R2.warn("Not signed in. Run `codevector auth login` to get started.");
20979
+ process.exitCode = 1;
20980
+ return;
20981
+ }
20982
+ const client = gatewayClient(active.gatewayUrl, active.apiKey, 1e4);
20983
+ const s = ft();
20984
+ s.start(`Minting a key for group "${args.group}"\u2026`);
20985
+ try {
20986
+ const groupsRes = await call(parseResponse(client.groups.$get()));
20987
+ const group = groupsRes.data.find((g) => g.name === args.group);
20988
+ if (!group) {
20989
+ s.stop("Group not found");
20990
+ R2.error(
20991
+ `You are not a member of a group named "${args.group}". Run \`codevector groups list\` to see yours.`
20992
+ );
20993
+ process.exitCode = 1;
20994
+ return;
20995
+ }
20996
+ const res = await call(
20997
+ parseResponse(
20998
+ client.keys.$post({
20999
+ json: { groupId: group.id, ...args.name ? { name: args.name } : {} }
21000
+ })
21001
+ )
21002
+ );
21003
+ const minted = res.key;
21004
+ const profiles = await readProfiles();
21005
+ const previousActive = profiles?.activeProfile;
21006
+ writeProfile(group.name, {
21007
+ apiKey: minted.plaintext,
21008
+ gatewayUrl: active.gatewayUrl,
21009
+ userId: active.userId,
21010
+ email: active.email,
21011
+ groupId: group.id,
21012
+ groupName: group.name,
21013
+ keyId: minted.id
21014
+ });
21015
+ if (previousActive && previousActive !== group.name) {
21016
+ await setActiveProfile(previousActive);
21017
+ }
21018
+ s.stop(`Minted a group key and saved profile "${group.name}".`);
21019
+ Se(
21020
+ `Profile "${group.name}" bills group "${group.name}".
21021
+ Run \`codevector profile switch\` and pick "${group.name}" to point your editors at it.`,
21022
+ "Group key created"
21023
+ );
21024
+ } catch (err) {
21025
+ s.stop("Could not mint group key");
21026
+ R2.error(err instanceof ApiClientError ? err.message : String(err));
21027
+ process.exitCode = 1;
21028
+ }
21029
+ }
21030
+ });
21031
+ var keysListCommand = defineCommand({
21032
+ meta: {
21033
+ name: "list",
21034
+ description: "List saved keys/profiles and the group each one bills."
21035
+ },
21036
+ async run() {
21037
+ const profiles = await readProfiles();
21038
+ if (!profiles || Object.keys(profiles.profiles).length === 0) {
21039
+ R2.info(
21040
+ "No keys saved. Run `codevector auth login`, or `codevector keys create --group <name>`."
21041
+ );
21042
+ return;
21043
+ }
21044
+ const names = sortProfileNames(Object.keys(profiles.profiles));
21045
+ const headers = ["", "PROFILE", "GROUP", "KEY", "GATEWAY"];
21046
+ const cells = names.map((name) => {
21047
+ const p2 = profiles.profiles[name];
21048
+ return [
21049
+ name === profiles.activeProfile ? "*" : "",
21050
+ name,
21051
+ p2.groupName ?? "personal",
21052
+ maskApiKey(p2.apiKey),
21053
+ p2.gatewayUrl
21054
+ ];
21055
+ });
21056
+ Se(renderTable(headers, cells), "Keys");
21057
+ }
21058
+ });
21059
+ var keysCommand = defineCommand({
21060
+ meta: {
21061
+ name: "keys",
21062
+ description: "Mint and list group-scoped API keys."
21063
+ },
21064
+ subCommands: {
21065
+ create: keysCreateCommand,
21066
+ list: keysListCommand
21067
+ }
21068
+ });
20236
21069
 
20237
- // src/lib/table.ts
20238
- function renderTable(headers, rows) {
20239
- const widths = headers.map(
20240
- (h2, i) => Math.max(h2.length, ...rows.map((row) => (row[i] ?? "").length))
20241
- );
20242
- const pad = (s, i) => s.padEnd(widths[i] ?? 0);
20243
- const divider = widths.map((w3) => "\u2500".repeat(w3)).join(" ");
20244
- return [headers.map(pad).join(" "), divider, ...rows.map((row) => row.map(pad).join(" "))].join(
20245
- "\n"
20246
- );
20247
- }
21070
+ // src/commands/models.ts
21071
+ init_dist();
21072
+ init_dist5();
21073
+ init_api_client();
21074
+ init_credentials();
20248
21075
 
20249
21076
  // src/commands/sync-models.ts
20250
21077
  init_dist();
@@ -20252,12 +21079,14 @@ init_dist5();
20252
21079
  init_api_client();
20253
21080
  init_opencode();
20254
21081
  init_credentials();
21082
+ init_paths();
21083
+ init_project_config();
20255
21084
  init_prompt();
20256
21085
  var SCOPES2 = ["local", "project", "user"];
20257
21086
  var modelsSyncCommand = defineCommand({
20258
21087
  meta: {
20259
21088
  name: "sync",
20260
- description: "Refresh the model list (with names, costs, context limits) in an existing opencode.json."
21089
+ description: "Refresh the configured group's model list (names, costs, context limits) in an existing opencode.json."
20261
21090
  },
20262
21091
  args: {
20263
21092
  scope: {
@@ -20272,19 +21101,53 @@ var modelsSyncCommand = defineCommand({
20272
21101
  }
20273
21102
  },
20274
21103
  async run({ args }) {
20275
- const creds = await readCredentials();
20276
- if (!creds) {
20277
- throw new Error("Not signed in. Run `codevector auth login` first.");
21104
+ const found = readProjectConfig(userCwd());
21105
+ if (!found || !found.config.gateway) {
21106
+ R2.warn("No .codevector.json with a `gateway` here. Run `codevector init` first.");
21107
+ process.exitCode = 1;
21108
+ return;
21109
+ }
21110
+ const repoGroups = found.config.groups ?? [];
21111
+ if (repoGroups.length === 0) {
21112
+ R2.info(
21113
+ "Model sync is available only when a group is configured for this repo. Run `codevector use <group>` to bind one."
21114
+ );
21115
+ return;
21116
+ }
21117
+ const profiles = await readProfiles();
21118
+ if (!profiles) {
21119
+ R2.warn("Not signed in. Run `codevector auth login` first.");
21120
+ process.exitCode = 1;
21121
+ return;
21122
+ }
21123
+ const resolution = resolveProfileForRepo(
21124
+ profiles.profiles,
21125
+ profiles.activeProfile,
21126
+ found.config.gateway,
21127
+ repoGroups
21128
+ );
21129
+ if (resolution.kind === "no-group-key") {
21130
+ R2.warn(
21131
+ `No local key for this repo's group${resolution.groups.length === 1 ? "" : "s"} (${resolution.groups.join(", ")}). Run \`codevector use <group>\` first.`
21132
+ );
21133
+ process.exitCode = 1;
21134
+ return;
21135
+ }
21136
+ if (resolution.kind === "none") {
21137
+ R2.warn("No profile matches this repo's gateway. Run `codevector auth login` for it.");
21138
+ process.exitCode = 1;
21139
+ return;
20278
21140
  }
20279
- ge("Sync models");
21141
+ const profile = resolution.profile;
21142
+ ge(`Sync models for group "${profile.groupName ?? "group"}"`);
20280
21143
  const target = await resolveTarget(args);
20281
- const client = gatewayClient(creds.gatewayUrl, creds.apiKey, 1e4);
21144
+ const client = gatewayClient(profile.gatewayUrl, profile.apiKey, 1e4);
20282
21145
  const s = ft();
20283
- s.start("Loading reachable models\u2026");
21146
+ s.start("Loading group models\u2026");
20284
21147
  let reachable;
20285
21148
  try {
20286
21149
  const res = await call(parseResponse(client.models.$get()));
20287
- reachable = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
21150
+ reachable = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
20288
21151
  slug: m2.slug,
20289
21152
  displayName: m2.displayName,
20290
21153
  contextWindow: m2.contextWindow,
@@ -20304,7 +21167,7 @@ var modelsSyncCommand = defineCommand({
20304
21167
  releaseDate: m2.releaseDate,
20305
21168
  family: m2.family
20306
21169
  }));
20307
- s.stop(`${reachable.length} model${reachable.length === 1 ? "" : "s"} reachable`);
21170
+ s.stop(`${reachable.length} model${reachable.length === 1 ? "" : "s"} in group`);
20308
21171
  } catch (err) {
20309
21172
  s.stop("Could not load models");
20310
21173
  R2.error(err instanceof ApiClientError ? err.message : String(err));
@@ -20393,20 +21256,25 @@ var modelsListCommand = defineCommand({
20393
21256
  );
20394
21257
  return;
20395
21258
  }
21259
+ const ordered = [...filtered].sort((a, b2) => Number(b2.callable) - Number(a.callable));
20396
21260
  const headers = [
20397
21261
  "SLUG",
20398
21262
  "KIND",
20399
21263
  "STATUS",
21264
+ "SOURCE",
21265
+ "USABLE",
20400
21266
  "PROVIDER",
20401
21267
  "CONTEXT",
20402
21268
  "IN $/MTOK",
20403
21269
  "OUT $/MTOK",
20404
21270
  "NAME"
20405
21271
  ];
20406
- const cells = filtered.map((m2) => [
21272
+ const cells = ordered.map((m2) => [
20407
21273
  m2.slug,
20408
21274
  m2.kind,
20409
21275
  m2.status,
21276
+ m2.sources.length ? m2.sources.join(", ") : "\u2014",
21277
+ m2.callable ? "yes" : "",
20410
21278
  m2.providerKind ?? "\u2014",
20411
21279
  m2.contextWindow ? m2.contextWindow.toLocaleString() : "\u2014",
20412
21280
  m2.inputPricePerMtok ?? "\u2014",
@@ -20414,6 +21282,11 @@ var modelsListCommand = defineCommand({
20414
21282
  m2.displayName
20415
21283
  ]);
20416
21284
  Se(renderTable(headers, cells), "Reachable models");
21285
+ if (ordered.some((m2) => !m2.callable)) {
21286
+ R2.info(
21287
+ "Models without USABLE are reachable through another profile. Run `codevector use <group>` to switch."
21288
+ );
21289
+ }
20417
21290
  } catch (err) {
20418
21291
  s?.stop("Could not load models");
20419
21292
  R2.error(err instanceof ApiClientError ? err.message : String(err));
@@ -20436,6 +21309,7 @@ var modelsCommand = defineCommand({
20436
21309
  init_dist();
20437
21310
  init_dist5();
20438
21311
  init_claude_code();
21312
+ init_cline();
20439
21313
  init_opencode();
20440
21314
  init_api_client();
20441
21315
  init_credentials();
@@ -20444,8 +21318,77 @@ init_prompt();
20444
21318
  init_project_context();
20445
21319
  var WRITERS3 = {
20446
21320
  "claude-code": writeClaudeCodeConfig,
21321
+ cline: writeClineConfig,
20447
21322
  opencode: writeOpencodeConfig
20448
21323
  };
21324
+ async function applyProfileToolConfigs(profileName, active) {
21325
+ const toolConfigs = active.toolConfigs?.length ? active.toolConfigs : detectOnDiskToolConfigs();
21326
+ if (toolConfigs.length === 0) {
21327
+ return { kind: "no-tools" };
21328
+ }
21329
+ const fetched = await fetchReachableModels(active.gatewayUrl, active.apiKey);
21330
+ if (!fetched.ok) {
21331
+ return { kind: "unreachable", reason: fetched.reason };
21332
+ }
21333
+ const reachable = fetched.models;
21334
+ const project = resolveProjectContext(userCwd()).project ?? void 0;
21335
+ const results = [];
21336
+ const persisted = [];
21337
+ for (const tc of toolConfigs) {
21338
+ const tool = tc.tool;
21339
+ const writer = WRITERS3[tool];
21340
+ if (!writer) {
21341
+ results.push({
21342
+ tool: tc.tool,
21343
+ status: "skipped",
21344
+ path: "",
21345
+ scope: tc.scope,
21346
+ notes: `Unsupported tool "${tc.tool}".`
21347
+ });
21348
+ continue;
21349
+ }
21350
+ const storedModelSlug = tc.modelSlug;
21351
+ const reachableModel = storedModelSlug ? reachable.find((m2) => m2.slug === storedModelSlug) : void 0;
21352
+ const modelArg = reachableModel ? { slug: reachableModel.slug, providerKind: reachableModel.providerKind } : void 0;
21353
+ let extraNote;
21354
+ if (storedModelSlug && !reachableModel) {
21355
+ extraNote = `Pinned model "${tc.modelSlug}" is not reachable with this profile; model pin removed.`;
21356
+ }
21357
+ try {
21358
+ const result = writer({
21359
+ gatewayUrl: active.gatewayUrl,
21360
+ apiKey: active.apiKey,
21361
+ scope: tc.scope,
21362
+ ...project ? { project } : {},
21363
+ ...modelArg ? { model: modelArg } : {},
21364
+ availableModels: reachable
21365
+ });
21366
+ if (extraNote) {
21367
+ result.notes = result.notes ? `${result.notes} ${extraNote}` : extraNote;
21368
+ }
21369
+ results.push(result);
21370
+ if (result.status === "configured") {
21371
+ persisted.push({
21372
+ tool: result.tool,
21373
+ scope: result.scope,
21374
+ ...modelArg ? { modelSlug: modelArg.slug } : {}
21375
+ });
21376
+ }
21377
+ } catch (err) {
21378
+ results.push({
21379
+ tool: tc.tool,
21380
+ status: "skipped",
21381
+ path: "",
21382
+ scope: tc.scope,
21383
+ notes: err instanceof Error ? err.message : String(err)
21384
+ });
21385
+ }
21386
+ }
21387
+ if (persisted.length > 0) {
21388
+ updateProfileToolConfigs(profileName, persisted);
21389
+ }
21390
+ return { kind: "applied", results };
21391
+ }
20449
21392
  var profileListCommand = defineCommand({
20450
21393
  meta: {
20451
21394
  name: "list",
@@ -20501,7 +21444,9 @@ var profileSwitchCommand = defineCommand({
20501
21444
  await setActiveProfile(selected);
20502
21445
  const active = await getActiveProfile();
20503
21446
  if (!active) {
20504
- R2.error(`Profile "${selected}" disappeared mid-switch. Re-run \`codevector profile switch\`.`);
21447
+ R2.error(
21448
+ `Profile "${selected}" disappeared mid-switch. Re-run \`codevector profile switch\`.`
21449
+ );
20505
21450
  process.exitCode = 1;
20506
21451
  return;
20507
21452
  }
@@ -20512,86 +21457,76 @@ Gateway: ${active.gatewayUrl}
20512
21457
  Key: ${maskApiKey(active.apiKey)}`,
20513
21458
  `Profile: ${selected}`
20514
21459
  );
20515
- const toolConfigs = active.toolConfigs ?? [];
20516
- if (toolConfigs.length === 0) {
20517
- R2.info("No stored tool configurations for this profile; nothing to reapply.");
20518
- }
20519
- const fetched = await fetchReachableModels(active.gatewayUrl, active.apiKey);
20520
- if (!fetched.ok) {
20521
- R2.warn(
20522
- `Could not reach the gateway to verify reachable models: ${fetched.reason}.`
21460
+ const outcome = await applyProfileToolConfigs(selected, active);
21461
+ if (outcome.kind === "no-tools") {
21462
+ R2.info(
21463
+ "No tool configurations found for this profile and none detected on disk; nothing to reapply."
20523
21464
  );
21465
+ return;
21466
+ }
21467
+ if (outcome.kind === "unreachable") {
21468
+ R2.warn(`Could not reach the gateway to verify reachable models: ${outcome.reason}.`);
20524
21469
  R2.info(
20525
21470
  "Stored tool configurations were left unchanged. Re-run `codevector profile switch` once the gateway is reachable to reapply them."
20526
21471
  );
20527
21472
  return;
20528
21473
  }
20529
- const reachable = fetched.models;
20530
- const project = resolveProjectContext(userCwd()).project ?? void 0;
20531
- const results = [];
20532
- const persisted = [];
20533
- for (const tc of toolConfigs) {
20534
- const tool = tc.tool;
20535
- const writer = WRITERS3[tool];
20536
- if (!writer) {
20537
- results.push({
20538
- tool: tc.tool,
20539
- status: "skipped",
20540
- path: "",
20541
- scope: tc.scope,
20542
- notes: `Unsupported tool "${tc.tool}".`
20543
- });
20544
- continue;
20545
- }
20546
- const storedModelSlug = tc.modelSlug;
20547
- const reachableModel = storedModelSlug ? reachable.find((m2) => m2.slug === storedModelSlug) : void 0;
20548
- const modelArg = reachableModel ? { slug: reachableModel.slug, providerKind: reachableModel.providerKind } : void 0;
20549
- let extraNote;
20550
- if (storedModelSlug && !reachableModel) {
20551
- extraNote = `Pinned model "${tc.modelSlug}" is not reachable with this profile; model pin removed.`;
20552
- }
20553
- try {
20554
- const result = writer({
20555
- gatewayUrl: active.gatewayUrl,
20556
- apiKey: active.apiKey,
20557
- scope: tc.scope,
20558
- ...project ? { project } : {},
20559
- ...modelArg ? { model: modelArg } : {},
20560
- availableModels: reachable
20561
- });
20562
- if (extraNote) {
20563
- result.notes = result.notes ? `${result.notes} ${extraNote}` : extraNote;
20564
- }
20565
- results.push(result);
20566
- if (result.status === "configured") {
20567
- persisted.push({
20568
- tool: result.tool,
20569
- scope: result.scope,
20570
- ...modelArg ? { modelSlug: modelArg.slug } : {}
20571
- });
20572
- }
20573
- } catch (err) {
20574
- results.push({
20575
- tool: tc.tool,
20576
- status: "skipped",
20577
- path: "",
20578
- scope: tc.scope,
20579
- notes: err instanceof Error ? err.message : String(err)
20580
- });
21474
+ const lines = outcome.results.map((r) => {
21475
+ if (r.status === "configured") {
21476
+ const noteStr = r.notes ? `
21477
+ ${r.notes}` : "";
21478
+ return ` \u2713 ${r.tool} \u2192 ${r.path} [${r.scope}]${noteStr}`;
20581
21479
  }
21480
+ return ` ! ${r.tool}: skipped \u2014 ${r.notes ?? "unknown reason"}`;
21481
+ });
21482
+ Se(lines.join("\n"), "Tool configurations updated");
21483
+ }
21484
+ });
21485
+ var profileDeleteCommand = defineCommand({
21486
+ meta: {
21487
+ name: "delete",
21488
+ description: "Delete a saved profile."
21489
+ },
21490
+ args: {
21491
+ name: {
21492
+ type: "string",
21493
+ description: "Profile name to delete. Prompts with a picker if omitted."
21494
+ }
21495
+ },
21496
+ async run({ args }) {
21497
+ const profiles = await readProfiles();
21498
+ if (!profiles || Object.keys(profiles.profiles).length === 0) {
21499
+ R2.warn("No profiles configured. Nothing to delete.");
21500
+ process.exitCode = 1;
21501
+ return;
21502
+ }
21503
+ let target = args.name;
21504
+ if (!target) {
21505
+ const names = Object.keys(profiles.profiles).sort((a, b2) => {
21506
+ if (a === "default") return -1;
21507
+ if (b2 === "default") return 1;
21508
+ return a.localeCompare(b2);
21509
+ });
21510
+ const options = names.map((n) => ({
21511
+ value: n,
21512
+ label: n,
21513
+ hint: n === profiles.activeProfile ? "active" : void 0
21514
+ }));
21515
+ target = unwrap(
21516
+ await xe({
21517
+ message: "Select profile to delete",
21518
+ options,
21519
+ initialValue: profiles.activeProfile
21520
+ })
21521
+ );
20582
21522
  }
20583
- if (persisted.length > 0) {
20584
- updateProfileToolConfigs(selected, persisted);
21523
+ const removed = deleteProfile(target);
21524
+ if (removed) {
21525
+ R2.success(`Deleted profile "${target}".`);
21526
+ } else {
21527
+ R2.error(`Profile "${target}" does not exist.`);
21528
+ process.exitCode = 1;
20585
21529
  }
20586
- const lines = results.map((r) => {
20587
- if (r.status === "configured") {
20588
- const noteStr = r.notes ? `
20589
- ${r.notes}` : "";
20590
- return ` \u2713 ${r.tool} \u2192 ${r.path} [${r.scope}]${noteStr}`;
20591
- }
20592
- return ` ! ${r.tool}: skipped \u2014 ${r.notes ?? "unknown reason"}`;
20593
- });
20594
- Se(lines.join("\n"), "Tool configurations updated");
20595
21530
  }
20596
21531
  });
20597
21532
  var profileCommand = defineCommand({
@@ -20601,14 +21536,15 @@ var profileCommand = defineCommand({
20601
21536
  },
20602
21537
  subCommands: {
20603
21538
  list: profileListCommand,
20604
- switch: profileSwitchCommand
21539
+ switch: profileSwitchCommand,
21540
+ delete: profileDeleteCommand
20605
21541
  }
20606
21542
  });
20607
21543
  async function fetchReachableModels(gatewayUrl, apiKey) {
20608
21544
  const client = gatewayClient(gatewayUrl, apiKey, 1e4);
20609
21545
  try {
20610
21546
  const res = await call(parseResponse(client.models.$get()));
20611
- const models = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
21547
+ const models = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
20612
21548
  slug: m2.slug,
20613
21549
  providerKind: m2.providerKind,
20614
21550
  displayName: m2.displayName,
@@ -20640,8 +21576,8 @@ init_dist();
20640
21576
  init_dist5();
20641
21577
  init_api_client();
20642
21578
  init_credentials();
20643
- import { readFile, writeFile } from "fs/promises";
20644
- import { resolve as resolve2 } from "path";
21579
+ import { mkdir, readdir, readFile, symlink, writeFile } from "fs/promises";
21580
+ import { join as join15, resolve as resolve2 } from "path";
20645
21581
  import { spawn } from "child_process";
20646
21582
  async function getClient() {
20647
21583
  const creds = await readCredentials();
@@ -20736,6 +21672,37 @@ var skillsUploadCommand = defineCommand({
20736
21672
  }
20737
21673
  }
20738
21674
  });
21675
+ var skillsDeleteCommand = defineCommand({
21676
+ meta: {
21677
+ name: "delete",
21678
+ description: "Delete a skill pack from the gateway."
21679
+ },
21680
+ args: {
21681
+ name: {
21682
+ type: "positional",
21683
+ description: "Name of the skill pack to delete.",
21684
+ required: true
21685
+ }
21686
+ },
21687
+ async run({ args }) {
21688
+ const client = await getClient();
21689
+ if (!client) return;
21690
+ const s = ft();
21691
+ s.start(`Deleting skill pack "${args.name}"\u2026`);
21692
+ try {
21693
+ await call(
21694
+ parseResponse(
21695
+ client["skill-packs"][":name"].$delete({ param: { name: args.name } })
21696
+ )
21697
+ );
21698
+ s.stop(`Skill pack "${args.name}" deleted.`);
21699
+ } catch (err) {
21700
+ s.stop("Delete failed");
21701
+ R2.error(err instanceof ApiClientError ? err.message : String(err));
21702
+ process.exitCode = 1;
21703
+ }
21704
+ }
21705
+ });
20739
21706
  var skillsSyncCommand = defineCommand({
20740
21707
  meta: {
20741
21708
  name: "sync",
@@ -20792,6 +21759,28 @@ var skillsSyncCommand = defineCommand({
20792
21759
  child.on("error", reject);
20793
21760
  });
20794
21761
  s.stop("Skills installed");
21762
+ const agentsDir = resolve2(".agents/skills");
21763
+ let entries = [];
21764
+ try {
21765
+ entries = await readdir(agentsDir, { withFileTypes: true }).then((list) => list.filter((d) => d.isDirectory()).map((d) => d.name));
21766
+ } catch {
21767
+ }
21768
+ if (entries.length > 0) {
21769
+ for (const tool of ["claude", "opencode"]) {
21770
+ const toolDir = resolve2(`.${tool}/skills`);
21771
+ await mkdir(toolDir, { recursive: true });
21772
+ for (const name of entries) {
21773
+ const linkPath = join15(toolDir, name);
21774
+ try {
21775
+ await symlink(join15(agentsDir, name), linkPath, "dir");
21776
+ } catch (e2) {
21777
+ if (e2.code !== "EEXIST") {
21778
+ R2.warn(`Could not link ${name} into .${tool}/skills/: ${e2 instanceof Error ? e2.message : String(e2)}`);
21779
+ }
21780
+ }
21781
+ }
21782
+ }
21783
+ }
20795
21784
  } catch (err) {
20796
21785
  s.stop("Install failed");
20797
21786
  R2.error(err instanceof Error ? err.message : String(err));
@@ -20807,6 +21796,7 @@ var skillsCommand = defineCommand({
20807
21796
  subCommands: {
20808
21797
  list: skillsListCommand,
20809
21798
  upload: skillsUploadCommand,
21799
+ delete: skillsDeleteCommand,
20810
21800
  sync: skillsSyncCommand
20811
21801
  }
20812
21802
  });
@@ -20816,6 +21806,8 @@ init_dist();
20816
21806
  init_dist5();
20817
21807
  init_api_client();
20818
21808
  init_credentials();
21809
+ init_paths();
21810
+ init_project_config();
20819
21811
  var statusCommand = defineCommand({
20820
21812
  meta: {
20821
21813
  name: "status",
@@ -20832,20 +21824,151 @@ var statusCommand = defineCommand({
20832
21824
  `Signed in as ${creds.email}
20833
21825
  Gateway: ${creds.gatewayUrl}
20834
21826
  API key: ${maskApiKey(creds.apiKey)}
21827
+ Billing: ${creds.groupName ? `group "${creds.groupName}"` : "personal"}
20835
21828
  Last saved: ${creds.savedAt}`,
20836
21829
  "Credentials"
20837
21830
  );
21831
+ const pinned = readProjectConfig(userCwd())?.config.groups ?? [];
21832
+ if (pinned.length > 0) {
21833
+ R2.info(`This repo bills group(s): ${pinned.join(", ")}`);
21834
+ }
20838
21835
  const client = gatewayClient(creds.gatewayUrl, creds.apiKey, 1e4);
20839
21836
  const s = ft();
20840
21837
  s.start("Reaching gateway\u2026");
20841
21838
  try {
21839
+ const serverVersion = await checkServerCompatibility(creds.gatewayUrl);
20842
21840
  const res = await call(parseResponse(client.models.$get()));
21841
+ const versionSuffix = serverVersion ? ` (server ${serverVersion})` : "";
20843
21842
  s.stop(
20844
- res.data.length === 0 ? "Gateway reachable \u2014 no models granted. Ask your admin for access." : `Gateway reachable \u2014 ${res.data.length} model${res.data.length === 1 ? "" : "s"} available. Run \`codevector models\` to list them.`
21843
+ res.data.length === 0 ? `Gateway reachable${versionSuffix} \u2014 no models granted. Ask your admin for access.` : `Gateway reachable${versionSuffix} \u2014 ${res.data.length} model${res.data.length === 1 ? "" : "s"} available. Run \`codevector models\` to list them.`
20845
21844
  );
20846
21845
  } catch (err) {
20847
- s.stop("Gateway unreachable");
20848
- R2.error(err instanceof ApiClientError ? err.message : String(err));
21846
+ s.stop(err instanceof ApiClientError ? "Gateway unreachable" : "Gateway incompatible");
21847
+ R2.error(err instanceof Error ? err.message : String(err));
21848
+ process.exitCode = 1;
21849
+ }
21850
+ }
21851
+ });
21852
+
21853
+ // src/commands/sync.ts
21854
+ init_dist();
21855
+ init_dist5();
21856
+ init_api_client();
21857
+ init_credentials();
21858
+
21859
+ // src/lib/sync-plan.ts
21860
+ function keyUsable(k2, now) {
21861
+ if (!k2.isActive) return false;
21862
+ if (k2.expiresAt && new Date(k2.expiresAt).getTime() <= now.getTime()) return false;
21863
+ return true;
21864
+ }
21865
+ function findServerKey(profile, serverKeys) {
21866
+ if (profile.keyId) {
21867
+ const byId = serverKeys.find((k2) => k2.id === profile.keyId);
21868
+ if (byId) return byId;
21869
+ }
21870
+ return serverKeys.find((k2) => profile.apiKey.startsWith(k2.prefix));
21871
+ }
21872
+ function computeSyncActions(memberships, serverKeys, localProfiles, now) {
21873
+ const mints = [];
21874
+ const reminds = [];
21875
+ const prunes = [];
21876
+ const memberIds = new Set(memberships.map((m2) => m2.id));
21877
+ for (const m2 of memberships) {
21878
+ const profile = localProfiles.find((p2) => p2.groupId === m2.id);
21879
+ if (!profile) {
21880
+ mints.push({ type: "mint", groupId: m2.id, groupName: m2.name });
21881
+ continue;
21882
+ }
21883
+ const key = findServerKey(profile, serverKeys);
21884
+ if (!key || !keyUsable(key, now)) {
21885
+ reminds.push({ type: "remint", profileName: profile.name, groupId: m2.id, groupName: m2.name });
21886
+ }
21887
+ }
21888
+ for (const p2 of localProfiles) {
21889
+ if (!memberIds.has(p2.groupId)) {
21890
+ prunes.push({ type: "prune", profileName: p2.name, groupName: p2.groupName ?? p2.name });
21891
+ }
21892
+ }
21893
+ return [...mints, ...reminds, ...prunes];
21894
+ }
21895
+
21896
+ // src/commands/sync.ts
21897
+ var syncCommand = defineCommand({
21898
+ meta: {
21899
+ name: "sync",
21900
+ description: "Reconcile local group keys with your server memberships + key status."
21901
+ },
21902
+ args: {
21903
+ prune: {
21904
+ type: "boolean",
21905
+ description: "Also delete local profiles for groups you've left (destructive)."
21906
+ }
21907
+ },
21908
+ async run({ args }) {
21909
+ const active = await getActiveProfile();
21910
+ if (!active) {
21911
+ R2.warn("Not signed in. Run `codevector auth login` first.");
21912
+ process.exitCode = 1;
21913
+ return;
21914
+ }
21915
+ const client = gatewayClient(active.gatewayUrl, active.apiKey, 1e4);
21916
+ const s = ft();
21917
+ s.start("Reconciling\u2026");
21918
+ try {
21919
+ const [groupsRes, keysRes] = await Promise.all([
21920
+ call(parseResponse(client.groups.$get())),
21921
+ call(parseResponse(client.keys.$get()))
21922
+ ]);
21923
+ const profiles = await readProfiles();
21924
+ const previousActive = profiles?.activeProfile ?? null;
21925
+ const local = Object.entries(profiles?.profiles ?? {}).filter(([, p2]) => p2.groupId).map(([name, p2]) => ({
21926
+ name,
21927
+ groupId: p2.groupId,
21928
+ groupName: p2.groupName,
21929
+ keyId: p2.keyId,
21930
+ apiKey: p2.apiKey
21931
+ }));
21932
+ const actions = computeSyncActions(groupsRes.data, keysRes.data, local, /* @__PURE__ */ new Date());
21933
+ s.stop(`${actions.length} change${actions.length === 1 ? "" : "s"}`);
21934
+ const summary = [];
21935
+ for (const a of actions) {
21936
+ if (a.type === "mint" || a.type === "remint") {
21937
+ const res = await call(
21938
+ parseResponse(client.keys.$post({ json: { groupId: a.groupId } }))
21939
+ );
21940
+ writeProfile(a.groupName, {
21941
+ apiKey: res.key.plaintext,
21942
+ gatewayUrl: active.gatewayUrl,
21943
+ userId: active.userId,
21944
+ email: active.email,
21945
+ groupId: a.groupId,
21946
+ groupName: a.groupName,
21947
+ keyId: res.key.id
21948
+ });
21949
+ summary.push(`${a.type === "mint" ? "minted" : "re-minted"} "${a.groupName}"`);
21950
+ } else if (a.type === "prune") {
21951
+ if (args.prune) {
21952
+ deleteProfile(a.profileName);
21953
+ summary.push(`pruned "${a.profileName}"`);
21954
+ } else {
21955
+ summary.push(`stale "${a.profileName}" \u2014 run with --prune to delete`);
21956
+ }
21957
+ }
21958
+ }
21959
+ if (previousActive) {
21960
+ const stillExists = (await readProfiles())?.profiles[previousActive];
21961
+ if (stillExists) await setActiveProfile(previousActive);
21962
+ }
21963
+ Se(summary.length ? summary.join("\n") : "Everything is up to date.", "Sync");
21964
+ } catch (err) {
21965
+ s.stop("Sync failed");
21966
+ const e2 = err instanceof ApiClientError ? err : null;
21967
+ if (e2?.code === "FORBIDDEN") {
21968
+ R2.warn("Group budgets are not enabled on this gateway \u2014 nothing to sync.");
21969
+ return;
21970
+ }
21971
+ R2.error(e2 ? e2.message : String(err));
20849
21972
  process.exitCode = 1;
20850
21973
  }
20851
21974
  }
@@ -20855,47 +21978,47 @@ Last saved: ${creds.savedAt}`,
20855
21978
  init_dist();
20856
21979
  init_dist5();
20857
21980
  init_api_client();
20858
- import { existsSync as existsSync12 } from "fs";
21981
+ import { existsSync as existsSync14 } from "fs";
20859
21982
 
20860
21983
  // src/lib/backup.ts
20861
21984
  import {
20862
21985
  copyFileSync,
20863
- existsSync as existsSync11,
20864
- mkdirSync as mkdirSync6,
21986
+ existsSync as existsSync13,
21987
+ mkdirSync as mkdirSync8,
20865
21988
  readdirSync,
20866
21989
  statSync as statSync4
20867
21990
  } from "fs";
20868
- import { basename, dirname as dirname6, join as join13 } from "path";
20869
- import { homedir as homedir6 } from "os";
20870
- var BACKUP_ROOT = process.env.CODEVECTOR_BACKUP_ROOT ?? join13(homedir6(), ".codevector", "backups");
21991
+ import { basename, dirname as dirname7, join as join16 } from "path";
21992
+ import { homedir as homedir7 } from "os";
21993
+ var BACKUP_ROOT = process.env.CODEVECTOR_BACKUP_ROOT ?? join16(homedir7(), ".codevector", "backups");
20871
21994
  function backupTimestamp(d = /* @__PURE__ */ new Date()) {
20872
21995
  return d.toISOString().replace(/:/g, "-");
20873
21996
  }
20874
21997
  function backupFile(sourcePath, tool, timestamp) {
20875
- if (!existsSync11(sourcePath)) return void 0;
20876
- const destDir = join13(BACKUP_ROOT, timestamp, tool);
20877
- mkdirSync6(destDir, { recursive: true, mode: 448 });
20878
- const dest = join13(destDir, basename(sourcePath));
21998
+ if (!existsSync13(sourcePath)) return void 0;
21999
+ const destDir = join16(BACKUP_ROOT, timestamp, tool);
22000
+ mkdirSync8(destDir, { recursive: true, mode: 448 });
22001
+ const dest = join16(destDir, basename(sourcePath));
20879
22002
  copyFileSync(sourcePath, dest);
20880
22003
  return dest;
20881
22004
  }
20882
22005
  function listBackupRuns() {
20883
- if (!existsSync11(BACKUP_ROOT)) return [];
22006
+ if (!existsSync13(BACKUP_ROOT)) return [];
20884
22007
  const entries = readdirSync(BACKUP_ROOT, { withFileTypes: true }).filter((e2) => e2.isDirectory()).map((e2) => e2.name).sort().reverse();
20885
22008
  const runs = [];
20886
22009
  for (const ts of entries) {
20887
- const dir = join13(BACKUP_ROOT, ts);
22010
+ const dir = join16(BACKUP_ROOT, ts);
20888
22011
  const tools = readdirSync(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory());
20889
22012
  const collected = [];
20890
22013
  for (const toolDir of tools) {
20891
- const toolPath = join13(dir, toolDir.name);
22014
+ const toolPath = join16(dir, toolDir.name);
20892
22015
  for (const file2 of readdirSync(toolPath, { withFileTypes: true })) {
20893
22016
  if (!file2.isFile()) continue;
20894
22017
  collected.push({
20895
22018
  tool: toolDir.name,
20896
22019
  original: "",
20897
22020
  // resolved by caller per tool — see restoreBackup()
20898
- backup: join13(toolPath, file2.name)
22021
+ backup: join16(toolPath, file2.name)
20899
22022
  });
20900
22023
  }
20901
22024
  }
@@ -20905,10 +22028,10 @@ function listBackupRuns() {
20905
22028
  return runs;
20906
22029
  }
20907
22030
  function restoreBackup(backupPath, originalPath) {
20908
- if (!existsSync11(backupPath)) {
22031
+ if (!existsSync13(backupPath)) {
20909
22032
  throw new Error(`Backup file missing: ${backupPath}`);
20910
22033
  }
20911
- mkdirSync6(dirname6(originalPath), { recursive: true });
22034
+ mkdirSync8(dirname7(originalPath), { recursive: true });
20912
22035
  copyFileSync(backupPath, originalPath);
20913
22036
  }
20914
22037
  function backupRunMtime(dir) {
@@ -20920,16 +22043,20 @@ init_credentials();
20920
22043
  init_prompt();
20921
22044
  init_shell_hook();
20922
22045
  init_claude_code();
22046
+ init_cline();
20923
22047
  init_opencode();
20924
- var TOOLS = ["claude-code", "opencode"];
22048
+ var TOOLS = ["claude-code", "cline", "opencode"];
20925
22049
  var WRITERS4 = {
20926
22050
  "claude-code": writeClaudeCodeConfig,
22051
+ cline: writeClineConfig,
20927
22052
  opencode: writeOpencodeConfig
20928
22053
  };
20929
22054
  function userPathFor(tool) {
20930
22055
  switch (tool) {
20931
22056
  case "claude-code":
20932
22057
  return claudeSettingsPath("user");
22058
+ case "cline":
22059
+ return clineSettingsPath("user");
20933
22060
  case "opencode":
20934
22061
  return opencodeSettingsPath("user");
20935
22062
  }
@@ -20945,10 +22072,11 @@ var systemConfigureCommand = defineCommand({
20945
22072
  if (!profiles) {
20946
22073
  throw new Error("Not signed in. Run `codevector auth login` first.");
20947
22074
  }
20948
- const creds = profiles.profiles[profiles.activeProfile];
22075
+ const activeProfileName = profiles.activeProfile;
22076
+ const creds = profiles.profiles[activeProfileName];
20949
22077
  if (!creds) {
20950
22078
  throw new Error(
20951
- `Active profile "${profiles.activeProfile}" is missing from credentials. Run \`codevector auth login\` to recover.`
22079
+ `Active profile "${activeProfileName}" is missing from credentials. Run \`codevector auth login\` to recover.`
20952
22080
  );
20953
22081
  }
20954
22082
  ge("codevector system configure");
@@ -20997,6 +22125,10 @@ var systemConfigureCommand = defineCommand({
20997
22125
  R2.warn(`${r.tool}: skipped \u2014 ${r.notes ?? "unknown reason"}`);
20998
22126
  }
20999
22127
  }
22128
+ const persisted = results.filter((r) => r.status === "configured").map((r) => ({ tool: r.tool, scope: "user" }));
22129
+ if (persisted.length > 0) {
22130
+ updateProfileToolConfigs(activeProfileName, persisted);
22131
+ }
21000
22132
  if (backups.length > 0) {
21001
22133
  Se(
21002
22134
  `To roll back: codevector system restore --timestamp ${timestamp}`,
@@ -21068,7 +22200,7 @@ var systemRestoreCommand = defineCommand({
21068
22200
  }
21069
22201
  const target = userPathFor(e2.tool);
21070
22202
  plan.push({ tool: e2.tool, backup: e2.backup, target });
21071
- R2.info(` ${e2.tool} \u2192 ${target}${existsSync12(target) ? " (will overwrite)" : " (new)"}`);
22203
+ R2.info(` ${e2.tool} \u2192 ${target}${existsSync14(target) ? " (will overwrite)" : " (new)"}`);
21072
22204
  }
21073
22205
  if (plan.length === 0) {
21074
22206
  ye("Nothing to restore.");
@@ -21110,7 +22242,7 @@ async function fetchReachableChatModels4(creds) {
21110
22242
  s.start("Loading reachable models\u2026");
21111
22243
  try {
21112
22244
  const res = await call(parseResponse(client.models.$get()));
21113
- const models = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
22245
+ const models = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
21114
22246
  slug: m2.slug,
21115
22247
  displayName: m2.displayName,
21116
22248
  contextWindow: m2.contextWindow,
@@ -21154,147 +22286,9 @@ var systemCommand = defineCommand({
21154
22286
  // src/commands/update.ts
21155
22287
  init_dist();
21156
22288
  init_dist5();
22289
+ init_package();
21157
22290
  import { spawnSync } from "child_process";
21158
-
21159
- // package.json
21160
- var package_default = {
21161
- name: "@codevector/cli",
21162
- version: "0.8.1",
21163
- description: "CodeVector CLI \u2014 installs and configures first-party coding-tool integrations.",
21164
- license: "UNLICENSED",
21165
- bin: {
21166
- codevector: "./bin/codevector.mjs"
21167
- },
21168
- files: [
21169
- "bin",
21170
- "dist",
21171
- "scripts",
21172
- "src/hooks"
21173
- ],
21174
- type: "module",
21175
- publishConfig: {
21176
- access: "public"
21177
- },
21178
- scripts: {
21179
- dev: "tsx src/index.ts",
21180
- build: "tsup",
21181
- "type-check": "tsc --noEmit",
21182
- test: "vitest run",
21183
- "test:watch": "vitest",
21184
- postinstall: "node scripts/postinstall.mjs"
21185
- },
21186
- dependencies: {
21187
- "@clack/prompts": "1.4.0",
21188
- citty: "^0.2.2",
21189
- hono: "4.12.16",
21190
- "smol-toml": "^1.6.1"
21191
- },
21192
- devDependencies: {
21193
- "@codevector/api": "workspace:*",
21194
- "@codevector/common": "workspace:*",
21195
- "@types/node": "25.6.0",
21196
- tsup: "^8.5.1",
21197
- tsx: "4.21.0",
21198
- typescript: "6.0.3",
21199
- vitest: "4.1.5",
21200
- zod: "4.4.2"
21201
- },
21202
- engines: {
21203
- node: ">=20"
21204
- }
21205
- };
21206
-
21207
- // src/commands/update.ts
21208
22291
  init_prompt();
21209
-
21210
- // src/lib/update-notifier.ts
21211
- init_paths();
21212
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
21213
- import { join as join14 } from "path";
21214
- var PKG_NAME = "@codevector/cli";
21215
- var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
21216
- var CHECK_CACHE_FILE = join14(CODEVECTOR_CONFIG_DIR, "update-check.json");
21217
- var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
21218
- var FETCH_TIMEOUT_MS = 2e3;
21219
- function readCache() {
21220
- if (!existsSync13(CHECK_CACHE_FILE)) return void 0;
21221
- try {
21222
- const raw = readFileSync10(CHECK_CACHE_FILE, "utf8");
21223
- const parsed = JSON.parse(raw);
21224
- if (typeof parsed.checkedAt !== "number") return void 0;
21225
- return {
21226
- latest: typeof parsed.latest === "string" ? parsed.latest : null,
21227
- checkedAt: parsed.checkedAt
21228
- };
21229
- } catch {
21230
- return void 0;
21231
- }
21232
- }
21233
- function readCachedLatestVersion() {
21234
- return readCache()?.latest ?? null;
21235
- }
21236
- function writeCache(cache) {
21237
- try {
21238
- mkdirSync7(CODEVECTOR_CONFIG_DIR, { recursive: true, mode: 448 });
21239
- writeFileSync9(CHECK_CACHE_FILE, JSON.stringify(cache));
21240
- } catch {
21241
- }
21242
- }
21243
- function isNewer(current, latest) {
21244
- const a = current.split(".").map((p2) => Number.parseInt(p2, 10));
21245
- const b2 = latest.split(".").map((p2) => Number.parseInt(p2, 10));
21246
- for (let i = 0; i < Math.max(a.length, b2.length); i++) {
21247
- const ai = a[i] ?? 0;
21248
- const bi = b2[i] ?? 0;
21249
- if (Number.isNaN(ai) || Number.isNaN(bi)) return latest > current;
21250
- if (bi > ai) return true;
21251
- if (bi < ai) return false;
21252
- }
21253
- return false;
21254
- }
21255
- function printUpdateNoticeIfCached(currentVersion) {
21256
- if (process.env.CODEVECTOR_NO_UPDATE_CHECK === "1") return;
21257
- try {
21258
- const cache = readCache();
21259
- if (!cache || !cache.latest) return;
21260
- if (isNewer(currentVersion, cache.latest)) {
21261
- process.stderr.write(
21262
- `\x1B[33m\u203A\x1B[0m A new version of @codevector/cli is available: ${currentVersion} \u2192 ${cache.latest}. Run \`codevector update\` to install.
21263
- `
21264
- );
21265
- }
21266
- } catch {
21267
- }
21268
- }
21269
- function scheduleUpdateCheck() {
21270
- if (process.env.CODEVECTOR_NO_UPDATE_CHECK === "1") return;
21271
- const cache = readCache();
21272
- const now = Date.now();
21273
- if (cache && now - cache.checkedAt < CHECK_TTL_MS) return;
21274
- void fetchLatestVersion().then((latest) => {
21275
- writeCache({ latest, checkedAt: Date.now() });
21276
- }).catch(() => {
21277
- writeCache({ latest: cache?.latest ?? null, checkedAt: Date.now() });
21278
- });
21279
- }
21280
- async function fetchLatestVersion() {
21281
- const controller = new AbortController();
21282
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
21283
- timer.unref?.();
21284
- try {
21285
- const res = await fetch(REGISTRY_URL, {
21286
- signal: controller.signal,
21287
- headers: { accept: "application/json" }
21288
- });
21289
- if (!res.ok) return null;
21290
- const body = await res.json();
21291
- return typeof body.version === "string" ? body.version : null;
21292
- } finally {
21293
- clearTimeout(timer);
21294
- }
21295
- }
21296
-
21297
- // src/commands/update.ts
21298
22292
  var PKG_NAME2 = "@codevector/cli";
21299
22293
  var updateCommand = defineCommand({
21300
22294
  meta: {
@@ -21414,6 +22408,117 @@ function installCommand(manager) {
21414
22408
  }
21415
22409
  }
21416
22410
 
22411
+ // src/commands/use.ts
22412
+ init_dist();
22413
+ init_dist5();
22414
+ init_api_client();
22415
+ init_credentials();
22416
+ init_paths();
22417
+ init_prompt();
22418
+ init_project_config();
22419
+ function addGroupToConfig(config2, groupName) {
22420
+ const existing = config2.groups ?? [];
22421
+ if (existing.includes(groupName)) return config2;
22422
+ return { ...config2, groups: [...existing, groupName] };
22423
+ }
22424
+ var useCommand = defineCommand({
22425
+ meta: {
22426
+ name: "use",
22427
+ description: "Bind this repo to a group: pin it in .codevector.json and activate its key."
22428
+ },
22429
+ args: {
22430
+ group: {
22431
+ type: "positional",
22432
+ required: false,
22433
+ description: "Group name to bill for this repo. Prompted if omitted."
22434
+ }
22435
+ },
22436
+ async run({ args }) {
22437
+ const active = await getActiveProfile();
22438
+ if (!active) {
22439
+ R2.warn("Not signed in. Run `codevector auth login` first.");
22440
+ process.exitCode = 1;
22441
+ return;
22442
+ }
22443
+ const found = readProjectConfig(userCwd());
22444
+ if (!found || !found.config.gateway) {
22445
+ R2.error("No .codevector.json with a `gateway` here. Run `codevector init` first.");
22446
+ process.exitCode = 1;
22447
+ return;
22448
+ }
22449
+ const client = gatewayClient(active.gatewayUrl, active.apiKey, 1e4);
22450
+ const s = ft();
22451
+ s.start("Loading your groups\u2026");
22452
+ try {
22453
+ const groupsRes = await call(parseResponse(client.groups.$get()));
22454
+ s.stop(`${groupsRes.data.length} group${groupsRes.data.length === 1 ? "" : "s"}`);
22455
+ if (groupsRes.data.length === 0) {
22456
+ R2.info("You are not a member of any groups. Ask an admin to add you.");
22457
+ return;
22458
+ }
22459
+ const chosenName = args.group ?? unwrap(
22460
+ await xe({
22461
+ message: "Which group should this repo bill?",
22462
+ options: groupsRes.data.map((g) => ({ value: g.name, label: g.name }))
22463
+ })
22464
+ );
22465
+ const group = groupsRes.data.find((g) => g.name === chosenName);
22466
+ if (!group) {
22467
+ R2.error(
22468
+ `You are not a member of a group named "${chosenName}". Run \`codevector groups list\` to see yours.`
22469
+ );
22470
+ process.exitCode = 1;
22471
+ return;
22472
+ }
22473
+ const [keysRes, profiles] = await Promise.all([
22474
+ call(parseResponse(client.keys.$get())),
22475
+ readProfiles()
22476
+ ]);
22477
+ const localForGroup = Object.entries(profiles?.profiles ?? {}).filter(([, p2]) => p2.groupId === group.id).map(([name, p2]) => ({
22478
+ name,
22479
+ groupId: p2.groupId,
22480
+ groupName: p2.groupName,
22481
+ keyId: p2.keyId,
22482
+ apiKey: p2.apiKey
22483
+ }));
22484
+ const [action] = computeSyncActions(
22485
+ [{ id: group.id, name: group.name }],
22486
+ keysRes.data,
22487
+ localForGroup,
22488
+ /* @__PURE__ */ new Date()
22489
+ );
22490
+ if (action && (action.type === "mint" || action.type === "remint")) {
22491
+ const mintS = ft();
22492
+ mintS.start(`Minting a key for "${group.name}"\u2026`);
22493
+ const res = await call(parseResponse(client.keys.$post({ json: { groupId: group.id } })));
22494
+ writeProfile(group.name, {
22495
+ apiKey: res.key.plaintext,
22496
+ gatewayUrl: active.gatewayUrl,
22497
+ userId: active.userId,
22498
+ email: active.email,
22499
+ groupId: group.id,
22500
+ groupName: group.name,
22501
+ keyId: res.key.id
22502
+ });
22503
+ mintS.stop(`Minted profile "${group.name}".`);
22504
+ }
22505
+ writeProjectConfig(found.path, addGroupToConfig(found.config, group.name));
22506
+ await setActiveProfile(group.name);
22507
+ const nowActive = (await readProfiles())?.profiles[group.name];
22508
+ if (nowActive) await applyProfileToolConfigs(group.name, nowActive);
22509
+ Se(
22510
+ `This repo now bills group "${group.name}".
22511
+ The shell hook will export its key on the next prompt.`,
22512
+ "Repo bound"
22513
+ );
22514
+ } catch (err) {
22515
+ s.stop("Failed");
22516
+ R2.error(err instanceof ApiClientError ? err.message : String(err));
22517
+ process.exitCode = 1;
22518
+ }
22519
+ }
22520
+ });
22521
+
21417
22522
  // src/commands/usage.ts
21418
22523
  init_dist();
21419
22524
  init_dist5();
@@ -21507,6 +22612,7 @@ function buildQuery(args) {
21507
22612
 
21508
22613
  // src/commands/version.ts
21509
22614
  init_dist();
22615
+ init_package();
21510
22616
  var versionCommand = defineCommand({
21511
22617
  meta: {
21512
22618
  name: "version",
@@ -21518,6 +22624,7 @@ var versionCommand = defineCommand({
21518
22624
  });
21519
22625
 
21520
22626
  // src/index.ts
22627
+ init_package();
21521
22628
  var main = defineCommand({
21522
22629
  meta: {
21523
22630
  name: "codevector",
@@ -21532,13 +22639,17 @@ var main = defineCommand({
21532
22639
  github: githubCommand,
21533
22640
  doctor: doctorCommand,
21534
22641
  env: envCommand,
22642
+ groups: groupsCommand,
21535
22643
  hook: hookCommand,
22644
+ keys: keysCommand,
21536
22645
  status: statusCommand,
22646
+ sync: syncCommand,
21537
22647
  system: systemCommand,
21538
22648
  models: modelsCommand,
21539
22649
  profile: profileCommand,
21540
22650
  skills: skillsCommand,
21541
22651
  update: updateCommand,
22652
+ use: useCommand,
21542
22653
  usage: usageCommand,
21543
22654
  version: versionCommand
21544
22655
  }