@codevector/cli 0.3.2 → 0.3.4

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/README.md CHANGED
@@ -34,7 +34,6 @@ That's the whole onboarding. Pick a tool, pick a scope, done.
34
34
  | -------------------------------- | ---------------------------------------- | ---------------------- | ---------------------------------------------------------- | --------------------- |
35
35
  | **Claude Code** | `~/.claude/settings.json` | user / project / local | literal in JSON (`ANTHROPIC_AUTH_TOKEN`) | `Stop` + `SessionEnd` |
36
36
  | **opencode** (sst/opencode) | `~/.config/opencode/opencode.json` | user / project / local | literal (user + local); env-var reference in project scope | — |
37
- | **qwen-code** (Alibaba Qwen CLI) | `~/.qwen/settings.json` + `~/.qwen/.env` | user / project / local | `.env` file alongside settings (`envKey` reference) | `Stop` + `SessionEnd` |
38
37
  | **codex** (OpenAI codex CLI) | `~/.codex/config.toml` | **user only** | shell env var (codex does not auto-load `.env`) | not wired |
39
38
 
40
39
  Each writer emits both Anthropic and OpenAI-compatible provider entries where the tool supports both wire formats. codex is OpenAI-only.
@@ -49,7 +48,7 @@ Three scopes, same names across every supported tool that allows more than one:
49
48
  | --------- | ------------------------------------------------------------------------------------------------ | ------------------------------- | --------------------------------------------------------- |
50
49
  | `user` | your home directory (`~/.claude/…`, `~/.codex/…`, etc.) | no (outside the repo) | your personal default across every project |
51
50
  | `project` | this repo's config dir (`./.claude/settings.json`, etc.) | **yes** — shared with teammates | base URL only; we never embed the API key here |
52
- | `local` | this repo's local-override file (`./.claude/settings.local.json`, `./opencode.json`, `./.qwen/`) | no — auto-gitignored | the usual default: per-seat API key stays on your machine |
51
+ | `local` | this repo's local-override file (`./.claude/settings.local.json`, `./opencode.json`) | no — auto-gitignored | the usual default: per-seat API key stays on your machine |
53
52
 
54
53
  `local` is the default when you don't pass `--scope`. The CLI adds gitignore entries automatically for tools that don't natively ignore their local file.
55
54
 
@@ -171,12 +170,10 @@ Override the config root with `CODEVECTOR_CONFIG_DIR`.
171
170
 
172
171
  ## Config file reference
173
172
 
174
- | Tool | File | Format |
175
- | ----------------- | --------------------------------------------------------- | ---------------------- |
176
- | Claude Code | `~/.claude/settings.json` / `.claude/settings.local.json` | JSON |
177
- | opencode | `~/.config/opencode/opencode.json` / `./opencode.json` | JSON (JSONC tolerated) |
178
- | qwen-code | `~/.qwen/settings.json` / `.qwen/settings.json` | JSON |
179
- | qwen-code secrets | `~/.qwen/.env` / `.qwen/.env` | dotenv |
180
- | codex | `~/.codex/config.toml` | TOML |
181
- | codevector credentials | `~/.config/codevector/credentials.json` | JSON (chmod 0600) |
182
- | codevector hook script | `~/.config/codevector/hooks/acceptance.sh` | shell |
173
+ | Tool | File | Format |
174
+ | ---------------------- | --------------------------------------------------------- | ---------------------- |
175
+ | Claude Code | `~/.claude/settings.json` / `.claude/settings.local.json` | JSON |
176
+ | opencode | `~/.config/opencode/opencode.json` / `./opencode.json` | JSON (JSONC tolerated) |
177
+ | codex | `~/.codex/config.toml` | TOML |
178
+ | codevector credentials | `~/.config/codevector/credentials.json` | JSON (chmod 0600) |
179
+ | codevector hook script | `~/.config/codevector/hooks/acceptance.sh` | shell |
package/dist/index.js CHANGED
@@ -16558,7 +16558,9 @@ async function readCredentials() {
16558
16558
  try {
16559
16559
  parsed = JSON.parse(raw);
16560
16560
  } catch {
16561
- throw new Error(`${CREDENTIALS_FILE} is not valid JSON. Run \`codevector logout\` and re-login.`);
16561
+ throw new Error(
16562
+ `${CREDENTIALS_FILE} is not valid JSON. Run \`codevector logout\` and re-login.`
16563
+ );
16562
16564
  }
16563
16565
  return CredentialsFileSchema.parse(parsed);
16564
16566
  }
@@ -16668,7 +16670,9 @@ var authLogoutCommand = defineCommand({
16668
16670
  run() {
16669
16671
  const removed = deleteCredentials();
16670
16672
  if (removed) {
16671
- R2.success("Logged out. Hooks are left in place \u2014 run `codevector auth login` to sign back in.");
16673
+ R2.success(
16674
+ "Logged out. Hooks are left in place \u2014 run `codevector auth login` to sign back in."
16675
+ );
16672
16676
  } else {
16673
16677
  R2.info("No credentials were found. Nothing to do.");
16674
16678
  }
@@ -16749,7 +16753,7 @@ function assertHttpsUrl(urlString) {
16749
16753
  }
16750
16754
 
16751
16755
  // src/commands/configure.ts
16752
- import { homedir as homedir6 } from "os";
16756
+ import { homedir as homedir5 } from "os";
16753
16757
 
16754
16758
  // src/lib/hooks.ts
16755
16759
  import { chmodSync as chmodSync2, copyFileSync, existsSync, mkdirSync as mkdirSync2 } from "fs";
@@ -17997,6 +18001,7 @@ var writeOpencodeConfig = ({
17997
18001
  gatewayUrl,
17998
18002
  apiKey,
17999
18003
  scope,
18004
+ project,
18000
18005
  model,
18001
18006
  availableModels = []
18002
18007
  }) => {
@@ -18005,14 +18010,16 @@ var writeOpencodeConfig = ({
18005
18010
  const keyLiteral = scope === "project" ? `{env:${ENV_VAR_NAME2}}` : apiKey;
18006
18011
  removeStaleGatewayProviders(path, gateway);
18007
18012
  const models = buildModelEntries(availableModels);
18013
+ const options = {
18014
+ apiKey: keyLiteral,
18015
+ baseURL: `${gateway}/gateway/openai/v1`,
18016
+ headers: buildCustomHeaders2(scope, project)
18017
+ };
18008
18018
  const patch = {
18009
18019
  $schema: "https://opencode.ai/config.json",
18010
18020
  provider: {
18011
18021
  [PROVIDER_PREFIX]: {
18012
- options: {
18013
- apiKey: keyLiteral,
18014
- baseURL: `${gateway}/gateway/openai/v1`
18015
- },
18022
+ options,
18016
18023
  models
18017
18024
  }
18018
18025
  }
@@ -18033,6 +18040,11 @@ var writeOpencodeConfig = ({
18033
18040
  `Project scope avoids embedding secrets. Export ${ENV_VAR_NAME2}=<your key> before running opencode, or switch to local scope to embed the key.`
18034
18041
  );
18035
18042
  }
18043
+ if (scope !== "user" && !project) {
18044
+ notes.push(
18045
+ "Project slug could not be derived (no git remote and no .codevector.json). Per-project usage attribution will be blank until you set one."
18046
+ );
18047
+ }
18036
18048
  return {
18037
18049
  tool: "opencode",
18038
18050
  status: "configured",
@@ -18043,7 +18055,9 @@ var writeOpencodeConfig = ({
18043
18055
  };
18044
18056
  function syncOpencodeModels(path, availableModels) {
18045
18057
  if (!existsSync5(path)) {
18046
- throw new Error(`No opencode config found at ${path}. Run \`codevector configure opencode\` first.`);
18058
+ throw new Error(
18059
+ `No opencode config found at ${path}. Run \`codevector configure opencode\` first.`
18060
+ );
18047
18061
  }
18048
18062
  const raw = readFileSync5(path, "utf8");
18049
18063
  let parsed;
@@ -18172,126 +18186,20 @@ function removeStaleGatewayProviders(path, gateway) {
18172
18186
  function isObject4(v2) {
18173
18187
  return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
18174
18188
  }
18175
- function trimRightSlash4(url2) {
18176
- return url2.endsWith("/") ? url2.slice(0, -1) : url2;
18177
- }
18178
-
18179
- // src/config-writers/qwen-code.ts
18180
- import { homedir as homedir5 } from "os";
18181
- import { join as join8 } from "path";
18182
-
18183
- // src/lib/env-file.ts
18184
- import { chmodSync as chmodSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
18185
- import { dirname as dirname5 } from "path";
18186
- function mergeEnvFile(path, entries) {
18187
- mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
18188
- const existing = existsSync6(path) ? readFileSync6(path, "utf8") : "";
18189
- const lines = existing.split("\n");
18190
- const seen = /* @__PURE__ */ new Set();
18191
- const updated = lines.map((line) => {
18192
- const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
18193
- if (!match) return line;
18194
- const key = match[1];
18195
- if (key === void 0 || !(key in entries)) return line;
18196
- seen.add(key);
18197
- return `${key}=${quote(entries[key] ?? "")}`;
18198
- });
18199
- for (const [key, value] of Object.entries(entries)) {
18200
- if (!seen.has(key)) updated.push(`${key}=${quote(value)}`);
18201
- }
18202
- while (updated.length > 0 && updated[updated.length - 1] === "") updated.pop();
18203
- writeFileSync6(path, `${updated.join("\n")}
18204
- `);
18205
- try {
18206
- chmodSync5(path, 384);
18207
- } catch {
18208
- }
18209
- }
18210
- function quote(v2) {
18211
- const escaped = v2.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
18212
- return `"${escaped}"`;
18213
- }
18214
-
18215
- // src/config-writers/qwen-code.ts
18216
- var ENV_VAR_NAME3 = "CODEVECTOR_GATEWAY_KEY";
18217
- var writeQwenCodeConfig = ({
18218
- gatewayUrl,
18219
- apiKey,
18220
- hookScriptPath,
18221
- scope,
18222
- model
18223
- }) => {
18224
- const path = qwenSettingsPath(scope);
18225
- const gateway = trimRightSlash5(gatewayUrl);
18226
- const patch = {
18227
- modelProviders: {
18228
- anthropic: [
18229
- {
18230
- id: "codevector-anthropic",
18231
- name: "CodeVector Gateway (Anthropic)",
18232
- envKey: ENV_VAR_NAME3,
18233
- baseUrl: `${gateway}/gateway/anthropic`
18234
- }
18235
- ],
18236
- openai: [
18237
- {
18238
- id: "codevector-openai",
18239
- name: "CodeVector Gateway (OpenAI-compat)",
18240
- envKey: ENV_VAR_NAME3,
18241
- baseUrl: `${gateway}/gateway/openai/v1`
18242
- }
18243
- ]
18244
- },
18245
- hooks: {
18246
- Stop: [{ matcher: "*", hooks: [{ type: "command", command: hookScriptPath, async: true }] }],
18247
- SessionEnd: [
18248
- { matcher: "*", hooks: [{ type: "command", command: hookScriptPath, async: true }] }
18249
- ]
18250
- }
18189
+ function buildCustomHeaders2(scope, project) {
18190
+ const safe = (v2) => v2.replace(/[\r\n]/g, "").trim();
18191
+ const headers = {
18192
+ "x-client-app": "opencode"
18251
18193
  };
18252
- if (model) patch.model = { name: model.slug };
18253
- mergeJsonFile(path, patch, {
18254
- hookArrayPaths: ["hooks.Stop", "hooks.SessionEnd"]
18255
- });
18256
- const notes = [];
18257
- if (scope === "project") {
18258
- notes.push(
18259
- `Project scope avoids writing the API key. Export ${ENV_VAR_NAME3}=<your key> in your shell, or switch to local scope to drop it into .qwen/.env.`
18260
- );
18261
- } else {
18262
- const envPath = qwenEnvPath(scope);
18263
- mergeEnvFile(envPath, { [ENV_VAR_NAME3]: apiKey });
18264
- notes.push(`API key written to ${envPath} (chmod 600).`);
18265
- }
18266
- if (scope === "local") {
18267
- if (ensureGitignored(".qwen/")) {
18268
- notes.push("Added `.qwen/` to .gitignore.");
18194
+ if (scope !== "user" && project) {
18195
+ const slug = safe(project);
18196
+ if (slug) {
18197
+ headers["x-project"] = slug;
18269
18198
  }
18270
18199
  }
18271
- return {
18272
- tool: "qwen-code",
18273
- status: "configured",
18274
- path,
18275
- scope,
18276
- notes: notes.length > 0 ? notes.join(" ") : void 0
18277
- };
18278
- };
18279
- function qwenSettingsPath(scope) {
18280
- return join8(qwenRootForScope(scope), "settings.json");
18281
- }
18282
- function qwenEnvPath(scope) {
18283
- return join8(qwenRootForScope(scope), ".env");
18284
- }
18285
- function qwenRootForScope(scope) {
18286
- switch (scope) {
18287
- case "user":
18288
- return join8(homedir5(), ".qwen");
18289
- case "project":
18290
- case "local":
18291
- return join8(userCwd(), ".qwen");
18292
- }
18200
+ return headers;
18293
18201
  }
18294
- function trimRightSlash5(url2) {
18202
+ function trimRightSlash4(url2) {
18295
18203
  return url2.endsWith("/") ? url2.slice(0, -1) : url2;
18296
18204
  }
18297
18205
 
@@ -18299,10 +18207,9 @@ function trimRightSlash5(url2) {
18299
18207
  var WRITERS = {
18300
18208
  "claude-code": writeClaudeCodeConfig,
18301
18209
  opencode: writeOpencodeConfig,
18302
- "qwen-code": writeQwenCodeConfig,
18303
18210
  codex: writeCodexConfig
18304
18211
  };
18305
- var ALL_TOOLS = ["claude-code", "opencode", "qwen-code", "codex"];
18212
+ var ALL_TOOLS = ["claude-code", "opencode", "codex"];
18306
18213
  var SCOPES = ["local", "project", "user"];
18307
18214
  var configureCommand = defineCommand({
18308
18215
  meta: {
@@ -18313,7 +18220,7 @@ var configureCommand = defineCommand({
18313
18220
  tool: {
18314
18221
  type: "positional",
18315
18222
  required: false,
18316
- description: "Tool to configure. Prompted if omitted. One of: claude-code, opencode, qwen-code, codex, all."
18223
+ description: "Tool to configure. Prompted if omitted. One of: claude-code, opencode, codex, all."
18317
18224
  },
18318
18225
  scope: {
18319
18226
  type: "string",
@@ -18422,9 +18329,13 @@ async function resolveTools(args) {
18422
18329
  `Unsupported tool "${args.tool}". Known: ${ALL_TOOLS.join(", ")}, or pass --all.`
18423
18330
  );
18424
18331
  }
18332
+ Se(
18333
+ "Use arrow keys to move, space to toggle, a to toggle all, enter to confirm.",
18334
+ "Controls"
18335
+ );
18425
18336
  const picked = unwrap(
18426
18337
  await ve({
18427
- message: "Which tools do you want to configure?",
18338
+ message: "Which tools do you want to configure? (space to select/deselect, enter to confirm)",
18428
18339
  options: ALL_TOOLS.map((t) => ({ value: t, label: t })),
18429
18340
  initialValues: ["claude-code"],
18430
18341
  required: true
@@ -18441,7 +18352,7 @@ async function resolveScope(raw, tools) {
18441
18352
  }
18442
18353
  return unwrap(
18443
18354
  await Ee({
18444
- message: "Where should these settings be written?",
18355
+ message: "Where should these settings be written? (arrow keys to move, enter to select)",
18445
18356
  options: [
18446
18357
  {
18447
18358
  value: "local",
@@ -18471,14 +18382,12 @@ function pathHint(tools, scope) {
18471
18382
  return relativizeHomeAndCwd(claudeSettingsPath(scope));
18472
18383
  case "opencode":
18473
18384
  return relativizeHomeAndCwd(opencodeSettingsPath(scope));
18474
- case "qwen-code":
18475
- return relativizeHomeAndCwd(qwenSettingsPath(scope));
18476
18385
  case "codex":
18477
18386
  return scope === "user" ? relativizeHomeAndCwd(codexConfigPath()) : `${relativizeHomeAndCwd(codexConfigPath())} (codex is user-scope only)`;
18478
18387
  }
18479
18388
  }
18480
18389
  function relativizeHomeAndCwd(absolutePath) {
18481
- const home = homedir6();
18390
+ const home = homedir5();
18482
18391
  const cwd = userCwd();
18483
18392
  if (home && absolutePath.startsWith(`${home}/`)) {
18484
18393
  return `~${absolutePath.slice(home.length)}`;
@@ -18539,7 +18448,7 @@ async function pickPinnedModel(models) {
18539
18448
  const SKIP = "__skip__";
18540
18449
  const picked = unwrap(
18541
18450
  await Ee({
18542
- message: "Pin a default model for these tools?",
18451
+ message: "Pin a default model for these tools? (arrow keys to move, enter to select)",
18543
18452
  options: [
18544
18453
  {
18545
18454
  value: SKIP,
@@ -18562,7 +18471,7 @@ async function pickPinnedModel(models) {
18562
18471
  }
18563
18472
 
18564
18473
  // src/commands/doctor.ts
18565
- import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
18474
+ import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
18566
18475
  var CLAUDE_SCOPE_ORDER = ["local", "project", "user"];
18567
18476
  var doctorCommand = defineCommand({
18568
18477
  meta: {
@@ -18611,7 +18520,7 @@ var doctorCommand = defineCommand({
18611
18520
  checks.push({ level: "fail", label: "gateway /me", detail: message });
18612
18521
  }
18613
18522
  checks.push(inspectClaudeSettings());
18614
- if (!existsSync7(ACCEPTANCE_HOOK_FILE)) {
18523
+ if (!existsSync6(ACCEPTANCE_HOOK_FILE)) {
18615
18524
  checks.push({
18616
18525
  level: "warn",
18617
18526
  label: "acceptance hook",
@@ -18636,9 +18545,9 @@ var doctorCommand = defineCommand({
18636
18545
  function inspectClaudeSettings() {
18637
18546
  for (const scope of CLAUDE_SCOPE_ORDER) {
18638
18547
  const path = claudeSettingsPath(scope);
18639
- if (!existsSync7(path)) continue;
18548
+ if (!existsSync6(path)) continue;
18640
18549
  try {
18641
- const raw = JSON.parse(readFileSync7(path, "utf8"));
18550
+ const raw = JSON.parse(readFileSync6(path, "utf8"));
18642
18551
  if (typeof raw.env?.ANTHROPIC_BASE_URL === "string") {
18643
18552
  return {
18644
18553
  level: "ok",
@@ -18676,9 +18585,9 @@ function emit(checks) {
18676
18585
  }
18677
18586
 
18678
18587
  // src/commands/init.ts
18679
- import { existsSync as existsSync8, writeFileSync as writeFileSync7 } from "fs";
18588
+ import { existsSync as existsSync7, writeFileSync as writeFileSync6 } from "fs";
18680
18589
  import { execFileSync as execFileSync2 } from "child_process";
18681
- import { join as join9 } from "path";
18590
+ import { join as join8 } from "path";
18682
18591
  var DEFAULT_TICKET_PATTERN2 = "[A-Z]+-\\d+";
18683
18592
  var initCommand = defineCommand({
18684
18593
  meta: {
@@ -18701,8 +18610,8 @@ var initCommand = defineCommand({
18701
18610
  },
18702
18611
  run({ args }) {
18703
18612
  const cwd = userCwd();
18704
- const target = join9(cwd, ".codevector.json");
18705
- if (existsSync8(target) && !args.force) {
18613
+ const target = join8(cwd, ".codevector.json");
18614
+ if (existsSync7(target) && !args.force) {
18706
18615
  throw new Error(`.codevector.json already exists in ${cwd}. Pass --force to overwrite.`);
18707
18616
  }
18708
18617
  const projectName = args.project ?? deriveProjectName(cwd);
@@ -18712,7 +18621,7 @@ var initCommand = defineCommand({
18712
18621
  );
18713
18622
  }
18714
18623
  const ticketPattern = args["ticket-pattern"] ?? DEFAULT_TICKET_PATTERN2;
18715
- writeFileSync7(target, `${JSON.stringify({ projectName, ticketPattern }, null, 2)}
18624
+ writeFileSync6(target, `${JSON.stringify({ projectName, ticketPattern }, null, 2)}
18716
18625
  `, {
18717
18626
  mode: 420
18718
18627
  });
@@ -18827,7 +18736,7 @@ async function resolveTarget(args) {
18827
18736
  }
18828
18737
  const picked = unwrap(
18829
18738
  await Ee({
18830
- message: "Which opencode.json should be refreshed?",
18739
+ message: "Which opencode.json should be refreshed? (arrow keys to move, enter to select)",
18831
18740
  options: [
18832
18741
  { value: "local", label: "local", hint: "./opencode.json (this repo)" },
18833
18742
  { value: "project", label: "project", hint: "./opencode.json (this repo, committed)" },
@@ -19053,7 +18962,7 @@ function buildQuery(args) {
19053
18962
  // package.json
19054
18963
  var package_default = {
19055
18964
  name: "@codevector/cli",
19056
- version: "0.3.2",
18965
+ version: "0.3.4",
19057
18966
  description: "CodeVector CLI \u2014 installs and configures first-party coding-tool integrations.",
19058
18967
  license: "UNLICENSED",
19059
18968
  bin: {
@@ -19082,9 +18991,9 @@ var package_default = {
19082
18991
  "smol-toml": "^1.6.1"
19083
18992
  },
19084
18993
  devDependencies: {
19085
- "@types/node": "25.6.0",
19086
18994
  "@codevector/api": "workspace:*",
19087
18995
  "@codevector/common": "workspace:*",
18996
+ "@types/node": "25.6.0",
19088
18997
  tsup: "^8.5.1",
19089
18998
  tsx: "4.21.0",
19090
18999
  typescript: "6.0.3",