@codevector/cli 0.8.0 → 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();
@@ -20199,10 +20917,11 @@ _codevector_on_chpwd
20199
20917
  `;
20200
20918
  var FISH_SNIPPET = `# codevector shell hook (fish) \u2014 auto-activates credentials on cd
20201
20919
  function _codevector_on_pwd --on-variable PWD
20202
- set -l out (command codevector env --shell fish 2>/dev/null)
20203
- if test -n "$out"
20204
- eval $out
20205
- end
20920
+ # Pipe straight to \`source\`. Capturing into a variable and re-running it
20921
+ # breaks here: fish command substitution splits on newlines and re-joins
20922
+ # with spaces, collapsing the multi-line export script into one broken
20923
+ # \`set\` command.
20924
+ command codevector env --shell fish 2>/dev/null | source
20206
20925
  end
20207
20926
  # Run once for the current directory at shell startup.
20208
20927
  _codevector_on_pwd
@@ -20227,23 +20946,132 @@ if (-not $global:_CODEVECTOR_PROMPT_WRAPPED) {
20227
20946
  // src/index.ts
20228
20947
  init_init();
20229
20948
 
20230
- // src/commands/models.ts
20949
+ // src/commands/keys.ts
20231
20950
  init_dist();
20232
20951
  init_dist5();
20233
20952
  init_api_client();
20234
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
+ });
20235
21069
 
20236
- // src/lib/table.ts
20237
- function renderTable(headers, rows) {
20238
- const widths = headers.map(
20239
- (h2, i) => Math.max(h2.length, ...rows.map((row) => (row[i] ?? "").length))
20240
- );
20241
- const pad = (s, i) => s.padEnd(widths[i] ?? 0);
20242
- const divider = widths.map((w3) => "\u2500".repeat(w3)).join(" ");
20243
- return [headers.map(pad).join(" "), divider, ...rows.map((row) => row.map(pad).join(" "))].join(
20244
- "\n"
20245
- );
20246
- }
21070
+ // src/commands/models.ts
21071
+ init_dist();
21072
+ init_dist5();
21073
+ init_api_client();
21074
+ init_credentials();
20247
21075
 
20248
21076
  // src/commands/sync-models.ts
20249
21077
  init_dist();
@@ -20251,12 +21079,14 @@ init_dist5();
20251
21079
  init_api_client();
20252
21080
  init_opencode();
20253
21081
  init_credentials();
21082
+ init_paths();
21083
+ init_project_config();
20254
21084
  init_prompt();
20255
21085
  var SCOPES2 = ["local", "project", "user"];
20256
21086
  var modelsSyncCommand = defineCommand({
20257
21087
  meta: {
20258
21088
  name: "sync",
20259
- 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."
20260
21090
  },
20261
21091
  args: {
20262
21092
  scope: {
@@ -20271,19 +21101,53 @@ var modelsSyncCommand = defineCommand({
20271
21101
  }
20272
21102
  },
20273
21103
  async run({ args }) {
20274
- const creds = await readCredentials();
20275
- if (!creds) {
20276
- 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;
20277
21140
  }
20278
- ge("Sync models");
21141
+ const profile = resolution.profile;
21142
+ ge(`Sync models for group "${profile.groupName ?? "group"}"`);
20279
21143
  const target = await resolveTarget(args);
20280
- const client = gatewayClient(creds.gatewayUrl, creds.apiKey, 1e4);
21144
+ const client = gatewayClient(profile.gatewayUrl, profile.apiKey, 1e4);
20281
21145
  const s = ft();
20282
- s.start("Loading reachable models\u2026");
21146
+ s.start("Loading group models\u2026");
20283
21147
  let reachable;
20284
21148
  try {
20285
21149
  const res = await call(parseResponse(client.models.$get()));
20286
- reachable = res.data.filter((m2) => m2.kind === "chat").map((m2) => ({
21150
+ reachable = res.data.filter((m2) => m2.kind === "chat" && m2.callable).map((m2) => ({
20287
21151
  slug: m2.slug,
20288
21152
  displayName: m2.displayName,
20289
21153
  contextWindow: m2.contextWindow,
@@ -20303,7 +21167,7 @@ var modelsSyncCommand = defineCommand({
20303
21167
  releaseDate: m2.releaseDate,
20304
21168
  family: m2.family
20305
21169
  }));
20306
- s.stop(`${reachable.length} model${reachable.length === 1 ? "" : "s"} reachable`);
21170
+ s.stop(`${reachable.length} model${reachable.length === 1 ? "" : "s"} in group`);
20307
21171
  } catch (err) {
20308
21172
  s.stop("Could not load models");
20309
21173
  R2.error(err instanceof ApiClientError ? err.message : String(err));
@@ -20392,20 +21256,25 @@ var modelsListCommand = defineCommand({
20392
21256
  );
20393
21257
  return;
20394
21258
  }
21259
+ const ordered = [...filtered].sort((a, b2) => Number(b2.callable) - Number(a.callable));
20395
21260
  const headers = [
20396
21261
  "SLUG",
20397
21262
  "KIND",
20398
21263
  "STATUS",
21264
+ "SOURCE",
21265
+ "USABLE",
20399
21266
  "PROVIDER",
20400
21267
  "CONTEXT",
20401
21268
  "IN $/MTOK",
20402
21269
  "OUT $/MTOK",
20403
21270
  "NAME"
20404
21271
  ];
20405
- const cells = filtered.map((m2) => [
21272
+ const cells = ordered.map((m2) => [
20406
21273
  m2.slug,
20407
21274
  m2.kind,
20408
21275
  m2.status,
21276
+ m2.sources.length ? m2.sources.join(", ") : "\u2014",
21277
+ m2.callable ? "yes" : "",
20409
21278
  m2.providerKind ?? "\u2014",
20410
21279
  m2.contextWindow ? m2.contextWindow.toLocaleString() : "\u2014",
20411
21280
  m2.inputPricePerMtok ?? "\u2014",
@@ -20413,6 +21282,11 @@ var modelsListCommand = defineCommand({
20413
21282
  m2.displayName
20414
21283
  ]);
20415
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
+ }
20416
21290
  } catch (err) {
20417
21291
  s?.stop("Could not load models");
20418
21292
  R2.error(err instanceof ApiClientError ? err.message : String(err));
@@ -20435,6 +21309,7 @@ var modelsCommand = defineCommand({
20435
21309
  init_dist();
20436
21310
  init_dist5();
20437
21311
  init_claude_code();
21312
+ init_cline();
20438
21313
  init_opencode();
20439
21314
  init_api_client();
20440
21315
  init_credentials();
@@ -20443,8 +21318,77 @@ init_prompt();
20443
21318
  init_project_context();
20444
21319
  var WRITERS3 = {
20445
21320
  "claude-code": writeClaudeCodeConfig,
21321
+ cline: writeClineConfig,
20446
21322
  opencode: writeOpencodeConfig
20447
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
+ }
20448
21392
  var profileListCommand = defineCommand({
20449
21393
  meta: {
20450
21394
  name: "list",
@@ -20500,7 +21444,9 @@ var profileSwitchCommand = defineCommand({
20500
21444
  await setActiveProfile(selected);
20501
21445
  const active = await getActiveProfile();
20502
21446
  if (!active) {
20503
- 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
+ );
20504
21450
  process.exitCode = 1;
20505
21451
  return;
20506
21452
  }
@@ -20511,86 +21457,76 @@ Gateway: ${active.gatewayUrl}
20511
21457
  Key: ${maskApiKey(active.apiKey)}`,
20512
21458
  `Profile: ${selected}`
20513
21459
  );
20514
- const toolConfigs = active.toolConfigs ?? [];
20515
- if (toolConfigs.length === 0) {
20516
- R2.info("No stored tool configurations for this profile; nothing to reapply.");
20517
- }
20518
- const fetched = await fetchReachableModels(active.gatewayUrl, active.apiKey);
20519
- if (!fetched.ok) {
20520
- R2.warn(
20521
- `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."
20522
21464
  );
21465
+ return;
21466
+ }
21467
+ if (outcome.kind === "unreachable") {
21468
+ R2.warn(`Could not reach the gateway to verify reachable models: ${outcome.reason}.`);
20523
21469
  R2.info(
20524
21470
  "Stored tool configurations were left unchanged. Re-run `codevector profile switch` once the gateway is reachable to reapply them."
20525
21471
  );
20526
21472
  return;
20527
21473
  }
20528
- const reachable = fetched.models;
20529
- const project = resolveProjectContext(userCwd()).project ?? void 0;
20530
- const results = [];
20531
- const persisted = [];
20532
- for (const tc of toolConfigs) {
20533
- const tool = tc.tool;
20534
- const writer = WRITERS3[tool];
20535
- if (!writer) {
20536
- results.push({
20537
- tool: tc.tool,
20538
- status: "skipped",
20539
- path: "",
20540
- scope: tc.scope,
20541
- notes: `Unsupported tool "${tc.tool}".`
20542
- });
20543
- continue;
20544
- }
20545
- const storedModelSlug = tc.modelSlug;
20546
- const reachableModel = storedModelSlug ? reachable.find((m2) => m2.slug === storedModelSlug) : void 0;
20547
- const modelArg = reachableModel ? { slug: reachableModel.slug, providerKind: reachableModel.providerKind } : void 0;
20548
- let extraNote;
20549
- if (storedModelSlug && !reachableModel) {
20550
- extraNote = `Pinned model "${tc.modelSlug}" is not reachable with this profile; model pin removed.`;
20551
- }
20552
- try {
20553
- const result = writer({
20554
- gatewayUrl: active.gatewayUrl,
20555
- apiKey: active.apiKey,
20556
- scope: tc.scope,
20557
- ...project ? { project } : {},
20558
- ...modelArg ? { model: modelArg } : {},
20559
- availableModels: reachable
20560
- });
20561
- if (extraNote) {
20562
- result.notes = result.notes ? `${result.notes} ${extraNote}` : extraNote;
20563
- }
20564
- results.push(result);
20565
- if (result.status === "configured") {
20566
- persisted.push({
20567
- tool: result.tool,
20568
- scope: result.scope,
20569
- ...modelArg ? { modelSlug: modelArg.slug } : {}
20570
- });
20571
- }
20572
- } catch (err) {
20573
- results.push({
20574
- tool: tc.tool,
20575
- status: "skipped",
20576
- path: "",
20577
- scope: tc.scope,
20578
- notes: err instanceof Error ? err.message : String(err)
20579
- });
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}`;
20580
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
+ );
20581
21522
  }
20582
- if (persisted.length > 0) {
20583
- 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;
20584
21529
  }
20585
- const lines = results.map((r) => {
20586
- if (r.status === "configured") {
20587
- const noteStr = r.notes ? `
20588
- ${r.notes}` : "";
20589
- return ` \u2713 ${r.tool} \u2192 ${r.path} [${r.scope}]${noteStr}`;
20590
- }
20591
- return ` ! ${r.tool}: skipped \u2014 ${r.notes ?? "unknown reason"}`;
20592
- });
20593
- Se(lines.join("\n"), "Tool configurations updated");
20594
21530
  }
20595
21531
  });
20596
21532
  var profileCommand = defineCommand({
@@ -20600,14 +21536,15 @@ var profileCommand = defineCommand({
20600
21536
  },
20601
21537
  subCommands: {
20602
21538
  list: profileListCommand,
20603
- switch: profileSwitchCommand
21539
+ switch: profileSwitchCommand,
21540
+ delete: profileDeleteCommand
20604
21541
  }
20605
21542
  });
20606
21543
  async function fetchReachableModels(gatewayUrl, apiKey) {
20607
21544
  const client = gatewayClient(gatewayUrl, apiKey, 1e4);
20608
21545
  try {
20609
21546
  const res = await call(parseResponse(client.models.$get()));
20610
- 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) => ({
20611
21548
  slug: m2.slug,
20612
21549
  providerKind: m2.providerKind,
20613
21550
  displayName: m2.displayName,
@@ -20639,8 +21576,8 @@ init_dist();
20639
21576
  init_dist5();
20640
21577
  init_api_client();
20641
21578
  init_credentials();
20642
- import { readFile, writeFile } from "fs/promises";
20643
- 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";
20644
21581
  import { spawn } from "child_process";
20645
21582
  async function getClient() {
20646
21583
  const creds = await readCredentials();
@@ -20735,6 +21672,37 @@ var skillsUploadCommand = defineCommand({
20735
21672
  }
20736
21673
  }
20737
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
+ });
20738
21706
  var skillsSyncCommand = defineCommand({
20739
21707
  meta: {
20740
21708
  name: "sync",
@@ -20791,6 +21759,28 @@ var skillsSyncCommand = defineCommand({
20791
21759
  child.on("error", reject);
20792
21760
  });
20793
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
+ }
20794
21784
  } catch (err) {
20795
21785
  s.stop("Install failed");
20796
21786
  R2.error(err instanceof Error ? err.message : String(err));
@@ -20806,6 +21796,7 @@ var skillsCommand = defineCommand({
20806
21796
  subCommands: {
20807
21797
  list: skillsListCommand,
20808
21798
  upload: skillsUploadCommand,
21799
+ delete: skillsDeleteCommand,
20809
21800
  sync: skillsSyncCommand
20810
21801
  }
20811
21802
  });
@@ -20815,6 +21806,8 @@ init_dist();
20815
21806
  init_dist5();
20816
21807
  init_api_client();
20817
21808
  init_credentials();
21809
+ init_paths();
21810
+ init_project_config();
20818
21811
  var statusCommand = defineCommand({
20819
21812
  meta: {
20820
21813
  name: "status",
@@ -20831,20 +21824,151 @@ var statusCommand = defineCommand({
20831
21824
  `Signed in as ${creds.email}
20832
21825
  Gateway: ${creds.gatewayUrl}
20833
21826
  API key: ${maskApiKey(creds.apiKey)}
21827
+ Billing: ${creds.groupName ? `group "${creds.groupName}"` : "personal"}
20834
21828
  Last saved: ${creds.savedAt}`,
20835
21829
  "Credentials"
20836
21830
  );
21831
+ const pinned = readProjectConfig(userCwd())?.config.groups ?? [];
21832
+ if (pinned.length > 0) {
21833
+ R2.info(`This repo bills group(s): ${pinned.join(", ")}`);
21834
+ }
20837
21835
  const client = gatewayClient(creds.gatewayUrl, creds.apiKey, 1e4);
20838
21836
  const s = ft();
20839
21837
  s.start("Reaching gateway\u2026");
20840
21838
  try {
21839
+ const serverVersion = await checkServerCompatibility(creds.gatewayUrl);
20841
21840
  const res = await call(parseResponse(client.models.$get()));
21841
+ const versionSuffix = serverVersion ? ` (server ${serverVersion})` : "";
20842
21842
  s.stop(
20843
- 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.`
20844
21844
  );
20845
21845
  } catch (err) {
20846
- s.stop("Gateway unreachable");
20847
- 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));
20848
21972
  process.exitCode = 1;
20849
21973
  }
20850
21974
  }
@@ -20854,47 +21978,47 @@ Last saved: ${creds.savedAt}`,
20854
21978
  init_dist();
20855
21979
  init_dist5();
20856
21980
  init_api_client();
20857
- import { existsSync as existsSync12 } from "fs";
21981
+ import { existsSync as existsSync14 } from "fs";
20858
21982
 
20859
21983
  // src/lib/backup.ts
20860
21984
  import {
20861
21985
  copyFileSync,
20862
- existsSync as existsSync11,
20863
- mkdirSync as mkdirSync6,
21986
+ existsSync as existsSync13,
21987
+ mkdirSync as mkdirSync8,
20864
21988
  readdirSync,
20865
21989
  statSync as statSync4
20866
21990
  } from "fs";
20867
- import { basename, dirname as dirname6, join as join13 } from "path";
20868
- import { homedir as homedir6 } from "os";
20869
- 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");
20870
21994
  function backupTimestamp(d = /* @__PURE__ */ new Date()) {
20871
21995
  return d.toISOString().replace(/:/g, "-");
20872
21996
  }
20873
21997
  function backupFile(sourcePath, tool, timestamp) {
20874
- if (!existsSync11(sourcePath)) return void 0;
20875
- const destDir = join13(BACKUP_ROOT, timestamp, tool);
20876
- mkdirSync6(destDir, { recursive: true, mode: 448 });
20877
- 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));
20878
22002
  copyFileSync(sourcePath, dest);
20879
22003
  return dest;
20880
22004
  }
20881
22005
  function listBackupRuns() {
20882
- if (!existsSync11(BACKUP_ROOT)) return [];
22006
+ if (!existsSync13(BACKUP_ROOT)) return [];
20883
22007
  const entries = readdirSync(BACKUP_ROOT, { withFileTypes: true }).filter((e2) => e2.isDirectory()).map((e2) => e2.name).sort().reverse();
20884
22008
  const runs = [];
20885
22009
  for (const ts of entries) {
20886
- const dir = join13(BACKUP_ROOT, ts);
22010
+ const dir = join16(BACKUP_ROOT, ts);
20887
22011
  const tools = readdirSync(dir, { withFileTypes: true }).filter((e2) => e2.isDirectory());
20888
22012
  const collected = [];
20889
22013
  for (const toolDir of tools) {
20890
- const toolPath = join13(dir, toolDir.name);
22014
+ const toolPath = join16(dir, toolDir.name);
20891
22015
  for (const file2 of readdirSync(toolPath, { withFileTypes: true })) {
20892
22016
  if (!file2.isFile()) continue;
20893
22017
  collected.push({
20894
22018
  tool: toolDir.name,
20895
22019
  original: "",
20896
22020
  // resolved by caller per tool — see restoreBackup()
20897
- backup: join13(toolPath, file2.name)
22021
+ backup: join16(toolPath, file2.name)
20898
22022
  });
20899
22023
  }
20900
22024
  }
@@ -20904,10 +22028,10 @@ function listBackupRuns() {
20904
22028
  return runs;
20905
22029
  }
20906
22030
  function restoreBackup(backupPath, originalPath) {
20907
- if (!existsSync11(backupPath)) {
22031
+ if (!existsSync13(backupPath)) {
20908
22032
  throw new Error(`Backup file missing: ${backupPath}`);
20909
22033
  }
20910
- mkdirSync6(dirname6(originalPath), { recursive: true });
22034
+ mkdirSync8(dirname7(originalPath), { recursive: true });
20911
22035
  copyFileSync(backupPath, originalPath);
20912
22036
  }
20913
22037
  function backupRunMtime(dir) {
@@ -20919,16 +22043,20 @@ init_credentials();
20919
22043
  init_prompt();
20920
22044
  init_shell_hook();
20921
22045
  init_claude_code();
22046
+ init_cline();
20922
22047
  init_opencode();
20923
- var TOOLS = ["claude-code", "opencode"];
22048
+ var TOOLS = ["claude-code", "cline", "opencode"];
20924
22049
  var WRITERS4 = {
20925
22050
  "claude-code": writeClaudeCodeConfig,
22051
+ cline: writeClineConfig,
20926
22052
  opencode: writeOpencodeConfig
20927
22053
  };
20928
22054
  function userPathFor(tool) {
20929
22055
  switch (tool) {
20930
22056
  case "claude-code":
20931
22057
  return claudeSettingsPath("user");
22058
+ case "cline":
22059
+ return clineSettingsPath("user");
20932
22060
  case "opencode":
20933
22061
  return opencodeSettingsPath("user");
20934
22062
  }
@@ -20944,10 +22072,11 @@ var systemConfigureCommand = defineCommand({
20944
22072
  if (!profiles) {
20945
22073
  throw new Error("Not signed in. Run `codevector auth login` first.");
20946
22074
  }
20947
- const creds = profiles.profiles[profiles.activeProfile];
22075
+ const activeProfileName = profiles.activeProfile;
22076
+ const creds = profiles.profiles[activeProfileName];
20948
22077
  if (!creds) {
20949
22078
  throw new Error(
20950
- `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.`
20951
22080
  );
20952
22081
  }
20953
22082
  ge("codevector system configure");
@@ -20996,6 +22125,10 @@ var systemConfigureCommand = defineCommand({
20996
22125
  R2.warn(`${r.tool}: skipped \u2014 ${r.notes ?? "unknown reason"}`);
20997
22126
  }
20998
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
+ }
20999
22132
  if (backups.length > 0) {
21000
22133
  Se(
21001
22134
  `To roll back: codevector system restore --timestamp ${timestamp}`,
@@ -21067,7 +22200,7 @@ var systemRestoreCommand = defineCommand({
21067
22200
  }
21068
22201
  const target = userPathFor(e2.tool);
21069
22202
  plan.push({ tool: e2.tool, backup: e2.backup, target });
21070
- R2.info(` ${e2.tool} \u2192 ${target}${existsSync12(target) ? " (will overwrite)" : " (new)"}`);
22203
+ R2.info(` ${e2.tool} \u2192 ${target}${existsSync14(target) ? " (will overwrite)" : " (new)"}`);
21071
22204
  }
21072
22205
  if (plan.length === 0) {
21073
22206
  ye("Nothing to restore.");
@@ -21109,7 +22242,7 @@ async function fetchReachableChatModels4(creds) {
21109
22242
  s.start("Loading reachable models\u2026");
21110
22243
  try {
21111
22244
  const res = await call(parseResponse(client.models.$get()));
21112
- 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) => ({
21113
22246
  slug: m2.slug,
21114
22247
  displayName: m2.displayName,
21115
22248
  contextWindow: m2.contextWindow,
@@ -21153,147 +22286,9 @@ var systemCommand = defineCommand({
21153
22286
  // src/commands/update.ts
21154
22287
  init_dist();
21155
22288
  init_dist5();
22289
+ init_package();
21156
22290
  import { spawnSync } from "child_process";
21157
-
21158
- // package.json
21159
- var package_default = {
21160
- name: "@codevector/cli",
21161
- version: "0.8.0",
21162
- description: "CodeVector CLI \u2014 installs and configures first-party coding-tool integrations.",
21163
- license: "UNLICENSED",
21164
- bin: {
21165
- codevector: "./bin/codevector.mjs"
21166
- },
21167
- files: [
21168
- "bin",
21169
- "dist",
21170
- "scripts",
21171
- "src/hooks"
21172
- ],
21173
- type: "module",
21174
- publishConfig: {
21175
- access: "public"
21176
- },
21177
- scripts: {
21178
- dev: "tsx src/index.ts",
21179
- build: "tsup",
21180
- "type-check": "tsc --noEmit",
21181
- test: "vitest run",
21182
- "test:watch": "vitest",
21183
- postinstall: "node scripts/postinstall.mjs"
21184
- },
21185
- dependencies: {
21186
- "@clack/prompts": "1.4.0",
21187
- citty: "^0.2.2",
21188
- hono: "4.12.16",
21189
- "smol-toml": "^1.6.1"
21190
- },
21191
- devDependencies: {
21192
- "@codevector/api": "workspace:*",
21193
- "@codevector/common": "workspace:*",
21194
- "@types/node": "25.6.0",
21195
- tsup: "^8.5.1",
21196
- tsx: "4.21.0",
21197
- typescript: "6.0.3",
21198
- vitest: "4.1.5",
21199
- zod: "4.4.2"
21200
- },
21201
- engines: {
21202
- node: ">=20"
21203
- }
21204
- };
21205
-
21206
- // src/commands/update.ts
21207
22291
  init_prompt();
21208
-
21209
- // src/lib/update-notifier.ts
21210
- init_paths();
21211
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
21212
- import { join as join14 } from "path";
21213
- var PKG_NAME = "@codevector/cli";
21214
- var REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
21215
- var CHECK_CACHE_FILE = join14(CODEVECTOR_CONFIG_DIR, "update-check.json");
21216
- var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
21217
- var FETCH_TIMEOUT_MS = 2e3;
21218
- function readCache() {
21219
- if (!existsSync13(CHECK_CACHE_FILE)) return void 0;
21220
- try {
21221
- const raw = readFileSync10(CHECK_CACHE_FILE, "utf8");
21222
- const parsed = JSON.parse(raw);
21223
- if (typeof parsed.checkedAt !== "number") return void 0;
21224
- return {
21225
- latest: typeof parsed.latest === "string" ? parsed.latest : null,
21226
- checkedAt: parsed.checkedAt
21227
- };
21228
- } catch {
21229
- return void 0;
21230
- }
21231
- }
21232
- function readCachedLatestVersion() {
21233
- return readCache()?.latest ?? null;
21234
- }
21235
- function writeCache(cache) {
21236
- try {
21237
- mkdirSync7(CODEVECTOR_CONFIG_DIR, { recursive: true, mode: 448 });
21238
- writeFileSync9(CHECK_CACHE_FILE, JSON.stringify(cache));
21239
- } catch {
21240
- }
21241
- }
21242
- function isNewer(current, latest) {
21243
- const a = current.split(".").map((p2) => Number.parseInt(p2, 10));
21244
- const b2 = latest.split(".").map((p2) => Number.parseInt(p2, 10));
21245
- for (let i = 0; i < Math.max(a.length, b2.length); i++) {
21246
- const ai = a[i] ?? 0;
21247
- const bi = b2[i] ?? 0;
21248
- if (Number.isNaN(ai) || Number.isNaN(bi)) return latest > current;
21249
- if (bi > ai) return true;
21250
- if (bi < ai) return false;
21251
- }
21252
- return false;
21253
- }
21254
- function printUpdateNoticeIfCached(currentVersion) {
21255
- if (process.env.CODEVECTOR_NO_UPDATE_CHECK === "1") return;
21256
- try {
21257
- const cache = readCache();
21258
- if (!cache || !cache.latest) return;
21259
- if (isNewer(currentVersion, cache.latest)) {
21260
- process.stderr.write(
21261
- `\x1B[33m\u203A\x1B[0m A new version of @codevector/cli is available: ${currentVersion} \u2192 ${cache.latest}. Run \`codevector update\` to install.
21262
- `
21263
- );
21264
- }
21265
- } catch {
21266
- }
21267
- }
21268
- function scheduleUpdateCheck() {
21269
- if (process.env.CODEVECTOR_NO_UPDATE_CHECK === "1") return;
21270
- const cache = readCache();
21271
- const now = Date.now();
21272
- if (cache && now - cache.checkedAt < CHECK_TTL_MS) return;
21273
- void fetchLatestVersion().then((latest) => {
21274
- writeCache({ latest, checkedAt: Date.now() });
21275
- }).catch(() => {
21276
- writeCache({ latest: cache?.latest ?? null, checkedAt: Date.now() });
21277
- });
21278
- }
21279
- async function fetchLatestVersion() {
21280
- const controller = new AbortController();
21281
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
21282
- timer.unref?.();
21283
- try {
21284
- const res = await fetch(REGISTRY_URL, {
21285
- signal: controller.signal,
21286
- headers: { accept: "application/json" }
21287
- });
21288
- if (!res.ok) return null;
21289
- const body = await res.json();
21290
- return typeof body.version === "string" ? body.version : null;
21291
- } finally {
21292
- clearTimeout(timer);
21293
- }
21294
- }
21295
-
21296
- // src/commands/update.ts
21297
22292
  var PKG_NAME2 = "@codevector/cli";
21298
22293
  var updateCommand = defineCommand({
21299
22294
  meta: {
@@ -21413,6 +22408,117 @@ function installCommand(manager) {
21413
22408
  }
21414
22409
  }
21415
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
+
21416
22522
  // src/commands/usage.ts
21417
22523
  init_dist();
21418
22524
  init_dist5();
@@ -21506,6 +22612,7 @@ function buildQuery(args) {
21506
22612
 
21507
22613
  // src/commands/version.ts
21508
22614
  init_dist();
22615
+ init_package();
21509
22616
  var versionCommand = defineCommand({
21510
22617
  meta: {
21511
22618
  name: "version",
@@ -21517,6 +22624,7 @@ var versionCommand = defineCommand({
21517
22624
  });
21518
22625
 
21519
22626
  // src/index.ts
22627
+ init_package();
21520
22628
  var main = defineCommand({
21521
22629
  meta: {
21522
22630
  name: "codevector",
@@ -21531,13 +22639,17 @@ var main = defineCommand({
21531
22639
  github: githubCommand,
21532
22640
  doctor: doctorCommand,
21533
22641
  env: envCommand,
22642
+ groups: groupsCommand,
21534
22643
  hook: hookCommand,
22644
+ keys: keysCommand,
21535
22645
  status: statusCommand,
22646
+ sync: syncCommand,
21536
22647
  system: systemCommand,
21537
22648
  models: modelsCommand,
21538
22649
  profile: profileCommand,
21539
22650
  skills: skillsCommand,
21540
22651
  update: updateCommand,
22652
+ use: useCommand,
21541
22653
  usage: usageCommand,
21542
22654
  version: versionCommand
21543
22655
  }