@ngockhoale/ukit 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 3.1.5 - 2026-09-26
6
+
7
+ - **Install/update warns when the decision lane has no API key.** After a project refresh, `ukit install` (and `ukit update`'s refresh) prints a bold-red warning with setup instructions — `ukit decision --base-url <url> --key <key>`, `~/.ukit/storage/gatewayDecision.json`, or `UKIT_DECISION_API_KEY` — whenever no dedicated decision key and no shared UNIC token are resolvable. When a key (or the shared UNIC token fallback) is found, a one-line confirmation is printed instead.
8
+
9
+ ## 3.1.4 - 2026-09-26
10
+
11
+ - **`ukit decision` — user-facing decision-lane gateway setup.** Resolution order (per field): env `UKIT_DECISION_BASE_URL`/`UKIT_DECISION_API_KEY`/`UKIT_DECISION_SCHEME` → `~/.ukit/storage/gatewayDecision.json` (new per-lane file; `gateway<Lane>.json` pattern) → `~/.ukit/storage/config.json` `decisionGateway` → `openai.unicjsc.com` + shared UNIC token. File field names mirror the env vars; `scheme` selects `Authorization: Bearer` vs `x-api-key`. Both files are seeded by `ukit install` with the full key set (`mergeStrategy: skip` — `ukit update` never overwrites user values). Commands: `--status`, `--base-url/--key/--scheme`, `--set-unic`, `--clear`.
12
+
5
13
  ## 3.1.3 - 2026-09-26
6
14
 
7
15
  - **`unic-decision-multilingual` becomes the shipped default checkpoint** — the variant is credential-bound on UNIC Provider (verified via live `health-probe` + `safe-vs-risky-lane` decision). `english`/`unknownLanguage` classes keep the bare `unic-decision` alias; `model_not_found` auto-fallback still covers unbound variants.
@@ -41,6 +41,17 @@ items:
41
41
  packs:
42
42
  - core
43
43
 
44
+ - id: user-gateway-decision
45
+ type: config
46
+ sourceTemplate: storage/gatewayDecision.json
47
+ targetPath: storage/gatewayDecision.json
48
+ requires: []
49
+ mergeStrategy: skip
50
+ variables: []
51
+ enabledByDefault: true
52
+ packs:
53
+ - core
54
+
44
55
  - id: user-playbook-issue
45
56
  type: config
46
57
  sourceTemplate: playbooks/issue-implementation.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,150 @@
1
+ // `ukit decision` — decision-lane gateway setup (S6 follow-up 2026-09-26).
2
+ //
3
+ // The decision lane resolves its own endpoint/key independently of the
4
+ // engine's LLM gateway: ~/.ukit/storage/gatewayDecision.json →
5
+ // ~/.ukit/storage/config.json "decisionGateway" → UKIT_DECISION_* env →
6
+ // default https://openai.unicjsc.com + shared UNIC token fallback. This
7
+ // command edits gatewayDecision.json (one file per lane: gateway<Lane>.json)
8
+ // so a user who runs the engine on a non-UNIC provider (e.g. OpenAI) can
9
+ // point the decision lane at UNIC — or anywhere — without hand-editing JSON.
10
+ //
11
+ // Usage:
12
+ // ukit decision --status resolved baseUrl/key source + redacted key
13
+ // ukit decision --set-unic reset to UNIC defaults (empty overrides)
14
+ // ukit decision --base-url <url> --key <k> [--scheme auth-token|api-key]
15
+ // ukit decision --key <k> update key only (keeps stored baseUrl)
16
+ // ukit decision --clear reset fields to template defaults
17
+ //
18
+ // Writes are atomic-ish (tmp + rename); unknown keys are preserved verbatim.
19
+
20
+ import fs from 'node:fs/promises';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+ import { resolveDecisionBaseUrl, resolveDecisionApiKey } from '../../core/gatewayProbe.js';
24
+
25
+ const GATEWAY_FILE = path.join('.ukit', 'storage', 'gatewayDecision.json');
26
+ const UNIC_DEFAULT = 'https://openai.unicjsc.com';
27
+ // Template defaults — written on --clear/--set-unic so the file always keeps
28
+ // its full key set (empty = default endpoint + shared UNIC token). Field
29
+ // names mirror the env vars exactly.
30
+ const TEMPLATE_BLOCK = {
31
+ _help: "unic-decision lane endpoint. Empty UKIT_DECISION_BASE_URL = default https://openai.unicjsc.com + shared UNIC token. UKIT_DECISION_SCHEME: 'auth-token' (Bearer) or 'api-key' (x-api-key).",
32
+ UKIT_DECISION_BASE_URL: '',
33
+ UKIT_DECISION_API_KEY: '',
34
+ UKIT_DECISION_SCHEME: 'auth-token',
35
+ };
36
+
37
+ const VALID_SCHEMES = new Set(['auth-token', 'api-key']);
38
+ const HELP_FLAGS = new Set(['--help', '-h', 'help']);
39
+
40
+ function printUsage(stream = console.log) {
41
+ stream('Usage: ukit decision --status');
42
+ stream(' ukit decision --base-url <url> --key <key> [--scheme auth-token|api-key]');
43
+ stream(' ukit decision --key <key>');
44
+ stream(' ukit decision --set-unic | --clear');
45
+ stream('');
46
+ stream('Configure the decision-lane gateway (unic-decision plane), stored in');
47
+ stream(`~/${GATEWAY_FILE.replaceAll(path.sep, '/')}. The decision`);
48
+ stream('lane is independent of the engine LLM endpoint (ANTHROPIC_BASE_URL etc.) —');
49
+ stream('point it at UNIC even when Claude/Codex/omp run on another provider.');
50
+ }
51
+
52
+ function parseArgs(argv) {
53
+ const out = { setUnic: false, clear: false, status: false };
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (a === '--base-url') out.baseUrl = argv[++i];
57
+ else if (a === '--key') out.key = argv[++i];
58
+ else if (a === '--scheme') out.scheme = argv[++i];
59
+ else if (a === '--set-unic') out.setUnic = true;
60
+ else if (a === '--clear') out.clear = true;
61
+ else if (a === '--status') out.status = true;
62
+ else return { error: `unknown flag: ${a}` };
63
+ }
64
+ return out;
65
+ }
66
+
67
+ async function readGatewayFile(filePath) {
68
+ try {
69
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ async function writeGatewayFile(filePath, json) {
76
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
77
+ const tmp = `${filePath}.tmp-${process.pid}`;
78
+ await fs.writeFile(tmp, JSON.stringify(json, null, 2) + '\n', { mode: 0o600 });
79
+ await fs.rename(tmp, filePath);
80
+ }
81
+
82
+ function redact(value) {
83
+ if (!value) return null;
84
+ return value.length <= 8 ? '***' : `${value.slice(0, 4)}…${value.slice(-4)}`;
85
+ }
86
+
87
+ async function printStatus(projectRoot, homeDir = os.homedir()) {
88
+ const base = resolveDecisionBaseUrl({ homeDir });
89
+ const key = await resolveDecisionApiKey({ projectRoot, homeDir });
90
+ console.log(JSON.stringify({
91
+ baseUrl: base.baseUrl,
92
+ baseUrlSource: base.source,
93
+ apiKey: redact(key.value),
94
+ apiKeySource: key.source,
95
+ scheme: key.scheme,
96
+ overrideFile: path.join(homeDir, GATEWAY_FILE),
97
+ }, null, 2));
98
+ }
99
+
100
+ export async function runDecision({ projectRoot, argv, homeDir = os.homedir() }) {
101
+ if (argv.some((a) => HELP_FLAGS.has(a))) {
102
+ printUsage();
103
+ return;
104
+ }
105
+ const args = parseArgs(argv);
106
+ if (args.error) {
107
+ console.error(args.error);
108
+ printUsage(console.error);
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ if (args.scheme && !VALID_SCHEMES.has(args.scheme)) {
113
+ console.error(`--scheme must be one of: ${[...VALID_SCHEMES].join(', ')}`);
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ const filePath = path.join(homeDir, GATEWAY_FILE);
119
+
120
+ if (args.status || (!args.baseUrl && !args.key && !args.setUnic && !args.clear)) {
121
+ await printStatus(projectRoot, homeDir);
122
+ return;
123
+ }
124
+
125
+ const json = await readGatewayFile(filePath);
126
+
127
+ if (args.clear || args.setUnic) {
128
+ // Reset to the full template block — the file always keeps its complete
129
+ // key set so the user only has to fill in values.
130
+ const out = { ...TEMPLATE_BLOCK };
131
+ if (args.setUnic) out.UKIT_DECISION_BASE_URL = UNIC_DEFAULT;
132
+ for (const key of Object.keys(json)) if (!(key in out)) out[key] = json[key];
133
+ await writeGatewayFile(filePath, out);
134
+ await printStatus(projectRoot, homeDir);
135
+ return;
136
+ }
137
+
138
+ if (!args.baseUrl && !args.key && !args.scheme) {
139
+ console.error('nothing to set — pass --base-url, --key and/or --scheme');
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ const out = { ...TEMPLATE_BLOCK, ...json };
144
+ if (args.baseUrl) out.UKIT_DECISION_BASE_URL = args.baseUrl;
145
+ if (args.key) out.UKIT_DECISION_API_KEY = args.key;
146
+ if (args.scheme) out.UKIT_DECISION_SCHEME = args.scheme;
147
+
148
+ await writeGatewayFile(filePath, out);
149
+ await printStatus(projectRoot, homeDir);
150
+ }
@@ -2,6 +2,7 @@ import { buildPathConfig } from '../../core/paths.js';
2
2
  import { runInstallPipeline } from '../../core/runInstallPipeline.js';
3
3
  import { formatRepairReport } from '../../core/repairBrokenHooks.js';
4
4
  import { formatGatewayResilienceReport } from '../../core/gatewayResilienceEnv.js';
5
+ import { resolveDecisionApiKey, resolveDecisionBaseUrl } from '../../core/gatewayProbe.js';
5
6
  import { buildCodeIndex, buildDerivedIndexArtifacts } from '../../index/buildIndex.js';
6
7
  import { installIndexRefreshHooks } from '../../index/gitHooks.js';
7
8
  import fs from 'node:fs/promises';
@@ -339,6 +340,29 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
339
340
  console.log(`[UKit] ${line}`);
340
341
  }
341
342
 
343
+ // Decision-lane credential check: warn (bold red) when no dedicated key is
344
+ // configured. Resolution: env UKIT_DECISION_API_KEY →
345
+ // ~/.ukit/storage/gatewayDecision.json → config.json decisionGateway →
346
+ // shared UNIC token. Tell the user exactly how to set it.
347
+ try {
348
+ const decisionKey = await resolveDecisionApiKey({ projectRoot });
349
+ const decisionBase = resolveDecisionBaseUrl({});
350
+ const R = '';
351
+ const X = '';
352
+ if (!decisionKey?.value) {
353
+ console.warn(`${R}[UKit] WARNING — decision lane (unic-decision) has NO API key configured.${X}`);
354
+ console.warn(`${R}[UKit] The typed-decision features will report 'unavailable' and fall back to deterministic rules.${X}`);
355
+ console.warn(`${R}[UKit] Set it with: ukit decision --base-url https://openai.unicjsc.com --key <YOUR_KEY>${X}`);
356
+ console.warn(`${R}[UKit] Or edit ~/.ukit/storage/gatewayDecision.json -> UKIT_DECISION_API_KEY, or export UKIT_DECISION_API_KEY=<key>.${X}`);
357
+ } else if (decisionKey.source === 'shared-unic-token') {
358
+ console.log(`[UKit] Decision lane: using shared UNIC token at ${decisionBase.baseUrl} (no dedicated key needed).`);
359
+ } else {
360
+ console.log(`[UKit] Decision lane: key configured (${decisionKey.source}) at ${decisionBase.baseUrl}.`);
361
+ }
362
+ } catch {
363
+ // advisory only — never fail install on the check
364
+ }
365
+
342
366
  const docsLabels = [
343
367
  'docs/PROJECT.md',
344
368
  'docs/MEMORY.md',
package/src/cli/index.js CHANGED
@@ -11,7 +11,7 @@ import { runMetrics } from './commands/metrics.js';
11
11
  import { runTelemetry } from './commands/telemetry.js';
12
12
  import { runFeedback } from './commands/feedback.js';
13
13
  import { runPlaybook } from './commands/playbook.js';
14
-
14
+ import { runDecision } from './commands/decision.js';
15
15
  const GLOBAL_FLAGS = new Set(['--help', '-h', '--version', '-v']);
16
16
 
17
17
  export async function runCli({ argv, packageRoot, projectRoot, packageVersion }) {
@@ -94,6 +94,11 @@ export async function runCli({ argv, packageRoot, projectRoot, packageVersion })
94
94
  return;
95
95
  }
96
96
 
97
+ if (command === 'decision') {
98
+ await runDecision({ projectRoot, argv: commandArgv });
99
+ return;
100
+ }
101
+
97
102
  if (command === 'build' && (commandArgv[0] ?? '').toLowerCase() === 'index') {
98
103
  await runIndexTools({ projectRoot, argv: ['build', ...commandArgv.slice(1)] });
99
104
  return;
@@ -26,45 +26,61 @@ const SETTINGS_RELATIVE_PROBE_PATH = path.join('.claude', 'settings.json');
26
26
  // --- Decision-plane gateway resolution (UNIC_DECISION_MIGRATION backlog) -----
27
27
  // The decision plane is a SEPARATE lane from the host's LLM endpoint: it must
28
28
  // reach the UNIC OpenAI-compatible provider no matter what Claude Code/omp is
29
- // configured to. Chain: ~/.ukit/storage/gateway.json {decision:{baseUrl,
30
- // apiKey, scheme}} → env UKIT_DECISION_BASE_URL / UKIT_DECISION_API_KEY →
31
- // DEFAULT_DECISION_BASE_URL + the shared UNIC token (ANTHROPIC_*) so a host
32
- // already pointed at UNIC works with zero extra config. Missing key → the
33
- // caller surfaces 'unavailable' and deterministic policy stays authoritative.
29
+ // configured to. Override chain (first hit wins, per field):
30
+ // 1. env UKIT_DECISION_BASE_URL / UKIT_DECISION_API_KEY / UKIT_DECISION_SCHEME
31
+ // 2. ~/.ukit/storage/gatewayDecision.json — fields are named exactly like
32
+ // the env vars ({UKIT_DECISION_BASE_URL, UKIT_DECISION_API_KEY,
33
+ // UKIT_DECISION_SCHEME}); one file per lane: gateway<Lane>.json.
34
+ // 3. ~/.ukit/storage/config.json -> "decisionGateway" (same env-style names)
35
+ // 4. DEFAULT_DECISION_BASE_URL + the shared UNIC token (ANTHROPIC_*).
36
+ // Missing key → the caller surfaces 'unavailable' and deterministic policy
37
+ // stays authoritative.
34
38
  export const DEFAULT_DECISION_BASE_URL = 'https://openai.unicjsc.com';
35
- const USER_GATEWAY_REL = path.join('.ukit', 'storage', 'gateway.json');
39
+ const USER_GATEWAY_REL = path.join('.ukit', 'storage', 'gatewayDecision.json');
40
+ const USER_CONFIG_REL = path.join('.ukit', 'storage', 'config.json');
36
41
 
37
- function readUserGatewayDecision(homeDir) {
38
- if (!homeDir) return null;
39
- const json = safeReadJson(path.join(homeDir, USER_GATEWAY_REL));
40
- const decision = json?.decision;
41
- return decision && typeof decision === 'object' ? decision : null;
42
+ function readUserDecisionBlock(homeDir) {
43
+ if (!homeDir) return {};
44
+ const g = safeReadJson(path.join(homeDir, USER_GATEWAY_REL));
45
+ const c = safeReadJson(path.join(homeDir, USER_CONFIG_REL))?.decisionGateway;
46
+ const gj = g && typeof g === 'object' ? g : null;
47
+ const cj = c && typeof c === 'object' ? c : null;
48
+ // Field-level merge — the lane file wins field-by-field over the config
49
+ // block (env still outranks both, applied by the resolvers below).
50
+ return {
51
+ baseUrl: pickString(gj?.UKIT_DECISION_BASE_URL, cj?.UKIT_DECISION_BASE_URL),
52
+ apiKey: pickString(gj?.UKIT_DECISION_API_KEY, cj?.UKIT_DECISION_API_KEY),
53
+ scheme: pickString(gj?.UKIT_DECISION_SCHEME, cj?.UKIT_DECISION_SCHEME),
54
+ source: gj ? 'gatewayDecision-json' : (cj ? 'ukit-config-json' : null),
55
+ };
42
56
  }
43
57
 
44
58
  // Endpoint for the decision lane. Never throws; file and env malformed values
45
59
  // are ignored, the default is the final fallback (not ANTHROPIC_BASE_URL —
46
- // that env may point at a non-decision gateway).
60
+ // that env may point at a non-decision gateway). Env wins over the user files.
47
61
  export function resolveDecisionBaseUrl({ homeDir = os.homedir(), env = process.env } = {}) {
48
- const file = readUserGatewayDecision(homeDir);
49
- const fromFile = pickString(file?.baseUrl);
50
- if (fromFile) return { baseUrl: fromFile, source: 'ukit-gateway-json' };
51
62
  const fromEnv = pickString(env?.UKIT_DECISION_BASE_URL);
52
63
  if (fromEnv) return { baseUrl: fromEnv, source: 'env' };
64
+ const block = readUserDecisionBlock(homeDir);
65
+ if (block.baseUrl) return { baseUrl: block.baseUrl, source: block.source };
53
66
  return { baseUrl: DEFAULT_DECISION_BASE_URL, source: 'default' };
54
67
  }
55
68
 
56
- // API key for the decision lane. `scheme`: 'auth-token' → Authorization: Bearer
57
- // (UNIC OpenAI-compatible default) | 'api-key' → x-api-key. Falls back to the
69
+ // API key for the decision lane. `scheme` (env UKIT_DECISION_SCHEME or the
70
+ // file field of the same name): 'auth-token' → Authorization: Bearer (UNIC
71
+ // OpenAI-compatible default) | 'api-key' → x-api-key. Falls back to the
58
72
  // host's UNIC token so existing installs need no duplicate credential.
59
73
  export function resolveDecisionApiKey({ projectRoot, homeDir = os.homedir(), env = process.env } = {}) {
60
- const file = readUserGatewayDecision(homeDir);
61
- const fileKey = pickString(file?.apiKey);
62
- if (fileKey) {
63
- const scheme = file?.scheme === 'api-key' ? 'api-key' : 'auth-token';
64
- return { value: fileKey, scheme, source: 'ukit-gateway-json' };
65
- }
66
74
  const envKey = pickString(env?.UKIT_DECISION_API_KEY);
67
- if (envKey) return { value: envKey, scheme: 'auth-token', source: 'env' };
75
+ if (envKey) {
76
+ const envScheme = env?.UKIT_DECISION_SCHEME === 'api-key' ? 'api-key' : 'auth-token';
77
+ return { value: envKey, scheme: envScheme, source: 'env' };
78
+ }
79
+ const block = readUserDecisionBlock(homeDir);
80
+ if (block.apiKey) {
81
+ const scheme = block.scheme === 'api-key' ? 'api-key' : 'auth-token';
82
+ return { value: block.apiKey, scheme, source: block.source };
83
+ }
68
84
  const shared = resolveGatewayApiKey({ projectRoot, homeDir, env });
69
85
  if (shared?.value) return { ...shared, source: 'shared-unic-token' };
70
86
  return { value: null, scheme: null, source: null };
@@ -210,7 +210,7 @@ export function createDecisionClient({
210
210
  }
211
211
 
212
212
  async function resolveEndpoint() {
213
- // Decision lane resolves its own endpoint (user-level gateway.json →
213
+ // Decision lane resolves its own endpoint (gatewayDecision.json →
214
214
  // env → openai.unicjsc.com default), independent of the host's LLM
215
215
  // endpoint — the UNIC provider is used even when the engine points
216
216
  // elsewhere.
@@ -332,37 +332,49 @@ function pickString(...candidates) {
332
332
 
333
333
  // --- Decision-plane gateway resolution -------------------------------------
334
334
  // The decision lane is independent of the host's LLM endpoint: default is the
335
- // UNIC OpenAI-compatible provider; ~/.ukit/storage/gateway.json {decision:
336
- // {baseUrl, apiKey, scheme}} overrides; env UKIT_DECISION_BASE_URL /
337
- // UKIT_DECISION_API_KEY next; the shared UNIC token (ANTHROPIC_*) is the
338
- // credential fallback so existing installs need nothing extra.
335
+ // UNIC OpenAI-compatible provider. Override chain (first hit wins per field):
336
+ // 1. env UKIT_DECISION_BASE_URL / UKIT_DECISION_API_KEY / UKIT_DECISION_SCHEME
337
+ // 2. ~/.ukit/storage/gatewayDecision.json — fields named exactly like the
338
+ // env vars; one file per lane: gateway<Lane>.json.
339
+ // 3. ~/.ukit/storage/config.json -> "decisionGateway" (same env-style names)
340
+ // 4. shared UNIC token (ANTHROPIC_*) — existing installs need nothing extra.
339
341
  const DEFAULT_DECISION_BASE_URL = 'https://openai.unicjsc.com';
340
- const USER_GATEWAY_REL = () => path.join('.ukit', 'storage', 'gateway.json');
341
-
342
- function readUserGatewayDecision(homeDir) {
343
- if (!homeDir) return null;
344
- const json = safeReadJson(path.join(homeDir, USER_GATEWAY_REL()));
345
- const decision = json?.decision;
346
- return decision && typeof decision === 'object' ? decision : null;
342
+ const USER_GATEWAY_REL = () => path.join('.ukit', 'storage', 'gatewayDecision.json');
343
+ const USER_CONFIG_REL = () => path.join('.ukit', 'storage', 'config.json');
344
+
345
+ function readUserDecisionBlock(homeDir) {
346
+ if (!homeDir) return {};
347
+ const g = safeReadJson(path.join(homeDir, USER_GATEWAY_REL()));
348
+ const c = safeReadJson(path.join(homeDir, USER_CONFIG_REL()))?.decisionGateway;
349
+ const gj = g && typeof g === 'object' ? g : null;
350
+ const cj = c && typeof c === 'object' ? c : null;
351
+ return {
352
+ baseUrl: pickString(gj?.UKIT_DECISION_BASE_URL, cj?.UKIT_DECISION_BASE_URL),
353
+ apiKey: pickString(gj?.UKIT_DECISION_API_KEY, cj?.UKIT_DECISION_API_KEY),
354
+ scheme: pickString(gj?.UKIT_DECISION_SCHEME, cj?.UKIT_DECISION_SCHEME),
355
+ source: gj ? 'gatewayDecision-json' : (cj ? 'ukit-config-json' : null),
356
+ };
347
357
  }
348
358
 
349
359
  function resolveDecisionBaseUrl({ homeDir, env }) {
350
- const fileUrl = pickString(readUserGatewayDecision(homeDir)?.baseUrl);
351
- if (fileUrl) return { baseUrl: fileUrl, source: 'ukit-gateway-json' };
352
360
  const envUrl = pickString(env?.UKIT_DECISION_BASE_URL);
353
361
  if (envUrl) return { baseUrl: envUrl, source: 'env' };
362
+ const block = readUserDecisionBlock(homeDir);
363
+ if (block.baseUrl) return { baseUrl: block.baseUrl, source: block.source };
354
364
  return { baseUrl: DEFAULT_DECISION_BASE_URL, source: 'default' };
355
365
  }
356
366
 
357
367
  function resolveDecisionApiKey({ projectRoot, homeDir, env }) {
358
- const file = readUserGatewayDecision(homeDir);
359
- const fileKey = pickString(file?.apiKey);
360
- if (fileKey) {
361
- const scheme = file?.scheme === 'api-key' ? 'api-key' : 'auth-token';
362
- return { value: fileKey, scheme, source: 'ukit-gateway-json' };
363
- }
364
368
  const envKey = pickString(env?.UKIT_DECISION_API_KEY);
365
- if (envKey) return { value: envKey, scheme: 'auth-token', source: 'env' };
369
+ if (envKey) {
370
+ const envScheme = env?.UKIT_DECISION_SCHEME === 'api-key' ? 'api-key' : 'auth-token';
371
+ return { value: envKey, scheme: envScheme, source: 'env' };
372
+ }
373
+ const block = readUserDecisionBlock(homeDir);
374
+ if (block.apiKey) {
375
+ const scheme = block.scheme === 'api-key' ? 'api-key' : 'auth-token';
376
+ return { value: block.apiKey, scheme, source: block.source };
377
+ }
366
378
  const shared = resolveGatewayApiKey({ projectRoot, homeDir, env });
367
379
  if (shared?.value) return { ...shared, source: 'shared-unic-token' };
368
380
  return { value: null, scheme: null, source: null };
@@ -12,5 +12,11 @@
12
12
  "compact": {
13
13
  "tokenThreshold": 150000,
14
14
  "hardCapTokens": 500000
15
+ },
16
+ "decisionGateway": {
17
+ "_help": "unic-decision lane endpoint — field names mirror the env vars. Fill only to override the default UNIC provider (https://openai.unicjsc.com); the lane file ~/.ukit/storage/gatewayDecision.json takes precedence over this block. Empty = default + shared UNIC token. scheme: 'auth-token' (Bearer) or 'api-key' (x-api-key). Prefer `ukit decision` over manual edits; ukit update never overwrites your values.",
18
+ "UKIT_DECISION_BASE_URL": "",
19
+ "UKIT_DECISION_API_KEY": "",
20
+ "UKIT_DECISION_SCHEME": "auth-token"
15
21
  }
16
22
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_help": "unic-decision lane endpoint — field names mirror the env vars. Fill only to override the default UNIC provider (https://openai.unicjsc.com). Empty values = default endpoint + shared UNIC token. UKIT_DECISION_SCHEME: 'auth-token' (Authorization: Bearer) or 'api-key' (x-api-key). Prefer `ukit decision --base-url <url> --key <key> [--scheme s]` over manual edits. UKit update never overwrites your values.",
3
+ "UKIT_DECISION_BASE_URL": "",
4
+ "UKIT_DECISION_API_KEY": "",
5
+ "UKIT_DECISION_SCHEME": "auth-token"
6
+ }