minecodex 0.2.1 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,6 +4,7 @@ import { createInterface } from "node:readline/promises";
4
4
  import { assertNodeSupported, MacPlatformAdapter } from "./platform.mjs";
5
5
  import { ensureConfig, ensureControlToken, readConfig } from "./config.mjs";
6
6
  import { createControlServer } from "./control-server.mjs";
7
+ import { createInstallReporter } from "./install-progress.mjs";
7
8
  import { createNpmAdapter } from "./npm-adapter.mjs";
8
9
  import { resolvePaths } from "./paths.mjs";
9
10
  import { RuntimeManager, writeServicePid } from "./runtime-manager.mjs";
@@ -118,20 +119,37 @@ export async function confirmRestartNow({ input = process.stdin, terminalOutput
118
119
  }
119
120
 
120
121
  async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
121
- assertNodeSupported(nodeVersion);
122
- platform.assertInstallSupported();
123
- await platform.assertCodexInstalled();
124
- const previousService = await serviceSnapshot(platform, paths);
125
- await ensureConfig(paths.configPath);
126
- await ensureControlToken(paths.tokenPath);
122
+ const reporter = createInstallReporter({ output });
123
+ reporter.heading("Installing MineCodex");
124
+ let previousService = null;
127
125
  try {
128
- await platform.installService({ cliPath, paths, start: true });
129
- const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
130
- const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
131
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
132
- || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
133
- throw new Error(`MineCodex service did not become ready. Check ${paths.serviceErrorLogPath}`);
134
- }
126
+ await reporter.step(1, 6, "Checking your system", async () => {
127
+ assertNodeSupported(nodeVersion);
128
+ platform.assertInstallSupported();
129
+ await platform.assertCodexInstalled();
130
+ });
131
+ await reporter.step(2, 6, "Preparing your settings", async () => {
132
+ await ensureConfig(paths.configPath);
133
+ await ensureControlToken(paths.tokenPath);
134
+ });
135
+ previousService = await reporter.step(3, 6, "Checking your current setup", async () => {
136
+ return serviceSnapshot(platform, paths);
137
+ });
138
+ await reporter.step(4, 6, "Installing the background helper", async () => {
139
+ await platform.installService({ cliPath, paths, start: true });
140
+ });
141
+ await reporter.step(5, 6, "Waiting for the background helper (up to 20 seconds)", async () => {
142
+ const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
143
+ if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) {
144
+ throw new Error("MineCodex service did not become ready. Check " + paths.serviceErrorLogPath);
145
+ }
146
+ });
147
+ await reporter.step(6, 6, "Waiting for MineCodex to be ready (up to 30 seconds)", async () => {
148
+ const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
149
+ if (!(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
150
+ throw new Error("MineCodex service did not become ready. Check " + paths.serviceErrorLogPath);
151
+ }
152
+ });
135
153
  } catch (error) {
136
154
  let rollbackError = null;
137
155
  try {
@@ -141,12 +159,12 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
141
159
  }
142
160
  throw appendRollbackError(error, rollbackError);
143
161
  }
144
- output("MineCodex installed. It will be enabled the next time Codex restarts.");
162
+ reporter.heading("MineCodex installed. It will be enabled the next time Codex restarts.");
145
163
  if (await confirmRestart()) {
146
164
  await restartCommand({ paths, output, fetchImpl });
147
165
  } else {
148
- output("Run `mcx restart` when you are ready.");
149
- }
166
+ reporter.heading("Run `mcx restart` when you are ready.");
167
+ }
150
168
  }
151
169
 
152
170
  async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
@@ -0,0 +1,56 @@
1
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+
3
+ function createInstallReporter({ output = console.log, terminalOutput = process.stdout } = {}) {
4
+ // 只有真实终端才显示 spinner;测试或脚本环境保持普通逐行文本。
5
+ const interactive = output === console.log && Boolean(terminalOutput?.isTTY);
6
+ let timer = null;
7
+
8
+ function stopSpinner() {
9
+ if (timer) clearInterval(timer);
10
+ timer = null;
11
+ }
12
+
13
+ function writeLine(line) {
14
+ if (!interactive) {
15
+ output(line);
16
+ return;
17
+ }
18
+ terminalOutput.write(`\r\x1b[2K${line}\n`);
19
+ }
20
+
21
+ function startSpinner(line) {
22
+ if (!interactive) {
23
+ output(line);
24
+ return;
25
+ }
26
+ let frame = 0;
27
+ terminalOutput.write(`\r\x1b[2K${SPINNER_FRAMES[frame]} ${line}`);
28
+ timer = setInterval(() => {
29
+ frame = (frame + 1) % SPINNER_FRAMES.length;
30
+ terminalOutput.write(`\r\x1b[2K${SPINNER_FRAMES[frame]} ${line}`);
31
+ }, 80);
32
+ }
33
+
34
+ return {
35
+ heading(line) {
36
+ writeLine(line);
37
+ },
38
+
39
+ async step(index, total, label, action) {
40
+ const prefix = `[${index}/${total}] ${label}`;
41
+ startSpinner(prefix);
42
+ try {
43
+ const result = await action();
44
+ stopSpinner();
45
+ writeLine(`✓ ${prefix}`);
46
+ return result;
47
+ } catch (error) {
48
+ stopSpinner();
49
+ writeLine(`✗ ${prefix}`);
50
+ throw error;
51
+ }
52
+ },
53
+ };
54
+ }
55
+
56
+ export { createInstallReporter };
@@ -1093,6 +1093,19 @@ export function createInjectionSource(features, {
1093
1093
  return formatIdentifier(identity.backend).toLowerCase() === nativeLabel ? native : thirdParty;
1094
1094
  }
1095
1095
 
1096
+ function visibleReasoningLevelsFor(model) {
1097
+ if (!model) return [];
1098
+ const capability = model.modelSelectorCapability;
1099
+ if (capability && Array.isArray(capability.levels)) {
1100
+ const allowed = new Set(capability.levels);
1101
+ return (model.supportedReasoningLevels ?? []).filter((level) => {
1102
+ const effort = String(level?.effort ?? level ?? "").toLowerCase();
1103
+ return allowed.has(effort);
1104
+ });
1105
+ }
1106
+ return model.supportedReasoningLevels ?? [];
1107
+ }
1108
+
1096
1109
  function loadModelFavorites(selector) {
1097
1110
  try {
1098
1111
  const value = JSON.parse(localStorage.getItem(selector.favoriteStorageKey) ?? "[]");
@@ -2106,7 +2119,13 @@ export function createInjectionSource(features, {
2106
2119
  function enhanceGenericModelPicker(parentMenu, selector) {
2107
2120
  const identity = modelIdentity(nativeModelValue(parentMenu));
2108
2121
  const model = modelDefinitionFor(identity, selector);
2109
- const efforts = model?.supportedReasoningLevels ?? [];
2122
+ const catalogModel = catalogModelFor(identity, selector);
2123
+ const modelWithCapability = model?.modelSelectorCapability ? model : {
2124
+ ...(model ?? {}),
2125
+ ...(catalogModel ?? {}),
2126
+ supportedReasoningLevels: model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [],
2127
+ };
2128
+ const efforts = visibleReasoningLevelsFor(modelWithCapability);
2110
2129
  if (!model || efforts.length < 2) return false;
2111
2130
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
2112
2131
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
@@ -2116,9 +2135,19 @@ export function createInjectionSource(features, {
2116
2135
  ?.getAttribute("data-selected-reasoning-effort")
2117
2136
  ?? model.defaultReasoningLevel
2118
2137
  ?? efforts[0].effort;
2138
+ const effortRank = (value) => {
2139
+ const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
2140
+ const index = order.indexOf(String(value ?? "").toLowerCase());
2141
+ return index < 0 ? -1 : index;
2142
+ };
2143
+ const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
2144
+ ?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
2145
+ .sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
2146
+ ?? efforts[0].effort;
2147
+ const selectedEffortValue = visibleEffort;
2119
2148
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
2120
2149
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
2121
- const key = `${identity.raw}:${selectedEffort}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
2150
+ const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
2122
2151
  const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
2123
2152
  const nativeTemplateReady = Boolean(
2124
2153
  parentMenu.firstElementChild
@@ -2140,7 +2169,7 @@ export function createInjectionSource(features, {
2140
2169
  existing?.remove();
2141
2170
  const nativeContainer = parentMenu.firstElementChild;
2142
2171
  if (!nativeContainer) return false;
2143
- const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort);
2172
+ const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffortValue);
2144
2173
  if (!shell) return false;
2145
2174
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2146
2175
  modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
@@ -11,6 +11,7 @@ import {
11
11
  selectEnabledFeatures,
12
12
  startFeatureProcesses,
13
13
  } from "./feature-registry.mjs";
14
+ import { applyVisibleReasoningLevels, loadModelCapabilities } from "./model-capabilities.mjs";
14
15
 
15
16
  const runtimeRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
16
17
  const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot);
@@ -22,8 +23,17 @@ const discoveredFeatures = await discoverFeatures(featuresRoot);
22
23
  if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
23
24
  const features = selectEnabledFeatures(discoveredFeatures, process.env.MINECODEX_ENABLED_FEATURES);
24
25
  const modelCatalog = await loadConfiguredModelCatalog();
26
+ const modelCapabilities = await loadModelCapabilities();
25
27
  for (const feature of features) {
26
- if (feature.modelSelector) feature.modelSelector.models = modelCatalog;
28
+ if (feature.modelSelector) {
29
+ feature.modelSelector.models = modelCatalog.map((model) => (
30
+ applyVisibleReasoningLevels(
31
+ model,
32
+ modelCapabilities.get(String(model.slug ?? "").toLowerCase()),
33
+ model.supportedReasoningLevels,
34
+ )
35
+ ));
36
+ }
27
37
  }
28
38
 
29
39
  let readyWritePromise = Promise.resolve();
@@ -0,0 +1,86 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const CODEX_REASONING_ORDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
6
+
7
+ function uniqueLevels(values) {
8
+ return [...new Set(values.filter(Boolean))];
9
+ }
10
+
11
+
12
+ function normalizedEfforts(entry) {
13
+ if (!Array.isArray(entry)) return null;
14
+ const levels = entry.map(String).filter((value) => (
15
+ CODEX_REASONING_ORDER.includes(value) || value === "none" || value === "minimal"
16
+ ));
17
+ return levels.length ? uniqueLevels(levels) : null;
18
+ }
19
+
20
+ export function resolveModelRecord(records, modelId) {
21
+ if (!records || typeof records !== "object") return undefined;
22
+ if (Object.prototype.hasOwnProperty.call(records, modelId)) return records[modelId];
23
+ const folded = modelId.toLowerCase();
24
+ for (const [key, value] of Object.entries(records)) {
25
+ if (key.toLowerCase() === folded) return value;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ export function mergeCapability(providerConfig, modelId) {
31
+ if (!providerConfig) return null;
32
+ const configuredEfforts = normalizedEfforts(
33
+ resolveModelRecord(providerConfig.modelReasoningEfforts, modelId),
34
+ );
35
+ if (configuredEfforts) {
36
+ return {
37
+ source: "opencodex",
38
+ kind: "effort",
39
+ levels: uniqueLevels(configuredEfforts),
40
+ map: resolveModelRecord(providerConfig.modelReasoningEffortMap, modelId) ?? null,
41
+ rawEfforts: configuredEfforts,
42
+ };
43
+ }
44
+ if (Array.isArray(providerConfig.noReasoningModels) && providerConfig.noReasoningModels.includes(modelId)) {
45
+ return { source: "opencodex", kind: "no-effort", levels: null, map: null, rawEfforts: [] };
46
+ }
47
+ return null;
48
+ }
49
+
50
+ export async function loadModelCapabilities({
51
+ opencodexConfigPath = process.env.OPENCODEX_CONFIG_PATH ?? path.join(os.homedir(), ".opencodex", "config.json"),
52
+ read = readFile,
53
+ } = {}) {
54
+ const configPayload = await read(opencodexConfigPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
55
+ const providers = configPayload?.providers ?? {};
56
+ const result = new Map();
57
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
58
+ const modelIds = new Set([
59
+ ...Object.keys(providerConfig?.modelReasoningEfforts ?? {}),
60
+ ...Object.keys(providerConfig?.modelReasoningEffortMap ?? {}),
61
+ ...Object.keys(providerConfig?.thinkingToggleModels ?? {}),
62
+ ...Object.keys(providerConfig?.thinkingBudgetModels ?? {}),
63
+ ...(Array.isArray(providerConfig?.noReasoningModels) ? providerConfig.noReasoningModels : []),
64
+ ]);
65
+ for (const modelId of modelIds) {
66
+ const capability = mergeCapability(providerConfig, modelId);
67
+ if (capability) result.set(`${providerId}/${modelId}`.toLowerCase(), capability);
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ export function applyVisibleReasoningLevels(catalogModel, capability, fallbackLevels) {
74
+ if (!catalogModel) return catalogModel;
75
+ const rawLevels = Array.isArray(fallbackLevels) ? fallbackLevels : (catalogModel.supportedReasoningLevels ?? []);
76
+ if (!Array.isArray(rawLevels)) return catalogModel;
77
+ if (capability && Array.isArray(capability.levels)) {
78
+ const allowed = new Set(capability.levels);
79
+ const visible = rawLevels.filter((level) => {
80
+ const effort = String(level?.effort ?? level ?? "").toLowerCase();
81
+ return allowed.has(effort);
82
+ });
83
+ return { ...catalogModel, supportedReasoningLevels: visible, modelSelectorCapability: capability };
84
+ }
85
+ return { ...catalogModel, modelSelectorCapability: capability ?? null };
86
+ }