sneakoscope 10.7.1 → 10.7.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/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/command-manifest-lite.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +44 -12
- package/dist/commands/bridge.js +14 -1
- package/dist/commands/codex.js +5 -1
- package/dist/commands/doctor-console.js +2 -0
- package/dist/commands/doctor.js +8 -9
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/codex-app/menubar/constants.js +0 -1
- package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -8
- package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
- package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +13 -4
- package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
- package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +52 -4
- package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
- package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +114 -5
- package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -2
- package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
- package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
- package/dist/core/codex-lb/desktop-service.js +41 -4
- package/dist/core/codex-lb/model-refresh-after.js +16 -0
- package/dist/core/commands/basic-cli.js +5 -0
- package/dist/core/commands/menubar-command.js +5 -1
- package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
- package/dist/core/subagents/model-tiers.js +19 -3
- package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
- package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
- package/package.json +1 -1
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
|
@@ -5,6 +5,14 @@ import { configureProviderCredential, recordProviderCredentialValidation, remove
|
|
|
5
5
|
import { configureBridgeProviderProfile, resolveBridgeProviderRegistry, setBridgeProviderEnabled } from '../provider-registry.js';
|
|
6
6
|
import { commandResult, activeProviderIds, controllerEnv, controllerPaths, nowIso, persistRuntimeSettings, providerCode, quiesceRunningBridge, resolveRawCredentials, stringArray, timeoutMs } from './shared.js';
|
|
7
7
|
import { desktopBridgeStatusV3, loadCore } from './status.js';
|
|
8
|
+
import { refreshModelCatalog } from './catalog.js';
|
|
9
|
+
async function refreshAfterProviderChange(reason, options) {
|
|
10
|
+
const core = await loadCore(options);
|
|
11
|
+
if (!core.activeCatalog.ok || !core.policy)
|
|
12
|
+
return null;
|
|
13
|
+
const { sync: _sync, ...summary } = await refreshModelCatalog(options, { reason, force: true, allowConfigWrites: true });
|
|
14
|
+
return summary;
|
|
15
|
+
}
|
|
8
16
|
export async function configureProvider(request, options) {
|
|
9
17
|
const paths = controllerPaths(options);
|
|
10
18
|
const restartDeferred = await quiesceRunningBridge(await loadCore(options), options);
|
|
@@ -34,12 +42,14 @@ export async function configureProvider(request, options) {
|
|
|
34
42
|
if (core.activeCatalog.ok && core.policy) {
|
|
35
43
|
await persistRuntimeSettings({ ...core, registry }, options, { restartService: false });
|
|
36
44
|
}
|
|
45
|
+
const modelRefresh = restartDeferred ? null : await refreshAfterProviderChange('provider.configure', options);
|
|
37
46
|
const status = await desktopBridgeStatusV3(options);
|
|
38
47
|
const blockers = restartDeferred ? ['desktop_bridge_restart_deferred_until_provider_validation'] : [];
|
|
39
48
|
return commandResult('provider.configure', true, status, {
|
|
40
49
|
configuration: configured,
|
|
41
50
|
restart_deferred: restartDeferred,
|
|
42
51
|
recovery_action: restartDeferred ? `bridge provider validate ${request.provider_id}` : null,
|
|
52
|
+
model_refresh: modelRefresh,
|
|
43
53
|
}, blockers, options);
|
|
44
54
|
}
|
|
45
55
|
export async function validateProvider(providerId, options) {
|
|
@@ -82,8 +92,9 @@ export async function validateProvider(providerId, options) {
|
|
|
82
92
|
});
|
|
83
93
|
}
|
|
84
94
|
}
|
|
95
|
+
const modelRefresh = ok ? await refreshAfterProviderChange('provider.validate', options) : null;
|
|
85
96
|
const status = await desktopBridgeStatusV3(options);
|
|
86
|
-
return commandResult('provider.validate', true, status, { provider_id: providerId, validated: ok, validation: publicValidationResult(result) }, ok ? [] : blockers, options);
|
|
97
|
+
return commandResult('provider.validate', true, status, { provider_id: providerId, validated: ok, validation: publicValidationResult(result), model_refresh: modelRefresh }, ok ? [] : blockers, options);
|
|
87
98
|
}
|
|
88
99
|
export async function setProviderState(providerId, enabled, operation, options) {
|
|
89
100
|
const paths = controllerPaths(options);
|
|
@@ -108,8 +119,9 @@ export async function setProviderState(providerId, enabled, operation, options)
|
|
|
108
119
|
});
|
|
109
120
|
}
|
|
110
121
|
}
|
|
122
|
+
const modelRefresh = await refreshAfterProviderChange(operation, options);
|
|
111
123
|
const status = await desktopBridgeStatusV3(options);
|
|
112
|
-
return commandResult(operation, true, status, { provider_id: providerId, enabled }, [], options);
|
|
124
|
+
return commandResult(operation, true, status, { provider_id: providerId, enabled, model_refresh: modelRefresh }, [], options);
|
|
113
125
|
}
|
|
114
126
|
export async function removeCredential(providerId, options) {
|
|
115
127
|
const paths = controllerPaths(options);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER
|
|
1
|
+
import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
|
|
2
2
|
import { readActiveCombinedBridgeCatalog } from '../combined-catalog.js';
|
|
3
3
|
import { desktopBridgeServiceStatus, desktopBridgeServicePaths, readDesktopBridgeServiceSettings } from '../desktop-service.js';
|
|
4
4
|
import { captureCodexAuthSnapshot } from '../desktop-auth-invariant.js';
|
|
@@ -276,8 +276,7 @@ function isManagedConfig(config) {
|
|
|
276
276
|
const top = String(config || '').split(/\n\s*\[/)[0] || '';
|
|
277
277
|
return [
|
|
278
278
|
DESKTOP_BRIDGE_MANAGED_MARKER,
|
|
279
|
-
DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER
|
|
280
|
-
DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER
|
|
279
|
+
DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER
|
|
281
280
|
].every((marker) => top.split(/\r?\n/).some((line) => line.trim() === marker))
|
|
282
281
|
&& /^\s*model_provider\s*=\s*"openai"\s*(?:#.*)?$/m.test(top);
|
|
283
282
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
|
|
1
|
+
import { listSelectableModels, refreshModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
|
|
2
2
|
import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, setOfficialModelsMode, setAuthPriority, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
|
|
3
3
|
import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
|
|
4
4
|
import { listSubagentModels, openRouterOnlyStatusCommand, setOpenRouterOnly, setSubagentModels } from './desktop-controller-v3/openrouter-only-commands.js';
|
|
@@ -55,6 +55,8 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
|
|
|
55
55
|
return listSelectableModels(options);
|
|
56
56
|
if (request.operation === 'models.select')
|
|
57
57
|
return selectExposedModels(request.public_ids, options);
|
|
58
|
+
if (request.operation === 'models.refresh')
|
|
59
|
+
return refreshModels(request, options);
|
|
58
60
|
if (request.operation === 'catalog.sync')
|
|
59
61
|
return syncCatalog(options);
|
|
60
62
|
if (request.operation === 'catalog.status') {
|
|
@@ -13,6 +13,8 @@ import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable
|
|
|
13
13
|
import { captureCodexAuthSnapshot } from './desktop-auth-invariant.js';
|
|
14
14
|
import { applyOfficialModelPassthrough, bridgeRoutePolicyPath, writeBridgeRoutingPolicy } from './provider-route-policy.js';
|
|
15
15
|
import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from './desktop-bridge/state.js';
|
|
16
|
+
import { REQUEST_COMPAT_INCLUDE_VALUE, REQUEST_COMPAT_MAX_EXTRA_INCLUDE } from './desktop-bridge/provider-request-compat.js';
|
|
17
|
+
import { bridgeRouteIndexPath, combinedBridgeCatalogPath, readActiveCombinedBridgeCatalog } from './combined-catalog.js';
|
|
16
18
|
import { PACKAGE_VERSION } from '../version.js';
|
|
17
19
|
import { canonicalChildModelId, readOpenRouterOnlyStateSync } from '../subagents/child-model-allowlist.js';
|
|
18
20
|
import { BRIDGE_OFFICIAL_ROUTE_ID } from './bridge-contracts.js';
|
|
@@ -133,7 +135,7 @@ export async function resolveDesktopBridgeActivationSettings(options = {}) {
|
|
|
133
135
|
const merged = { ...(persisted || {}), ...(options.settings || {}) };
|
|
134
136
|
const host = merged.listen_host || DEFAULT_DESKTOP_BRIDGE_HOST;
|
|
135
137
|
const port = merged.listen_port ?? await (options.selectAvailablePort || selectAvailableDesktopBridgePort)(host);
|
|
136
|
-
return defaultDesktopBridgeServiceSettings({ ...merged, ...(options.providerRegistry ? { provider_registry: options.providerRegistry } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}), client_capability_sha256: sha256(clientCapability), listen_host: host, listen_port: port });
|
|
138
|
+
return defaultDesktopBridgeServiceSettings({ ...merged, ...(options.providerRegistry ? { provider_registry: withPersistedRequestCompat(options.providerRegistry, persisted?.provider_registry) } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}), client_capability_sha256: sha256(clientCapability), listen_host: host, listen_port: port });
|
|
137
139
|
}
|
|
138
140
|
export async function readDesktopBridgeServiceSettings(file) {
|
|
139
141
|
let stat;
|
|
@@ -297,7 +299,8 @@ function normalizeEndpointForBinding(value) {
|
|
|
297
299
|
export async function resolveDesktopBridgeRuntimeConfig(options = {}) {
|
|
298
300
|
const home = options.home || options.env?.HOME || process.env.HOME || os.homedir();
|
|
299
301
|
const paths = overridePaths(desktopBridgeServicePaths(home), options);
|
|
300
|
-
|
|
302
|
+
const persisted = await readDesktopBridgeServiceSettings(paths.settings_path);
|
|
303
|
+
let settings = defaultDesktopBridgeServiceSettings({ ...(persisted || {}), ...(options.settings || {}), ...(options.providerRegistry ? { provider_registry: withPersistedRequestCompat(options.providerRegistry, persisted?.provider_registry) } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}) });
|
|
301
304
|
const clientCapability = options.clientCapability
|
|
302
305
|
? validateClientCapability(options.clientCapability)
|
|
303
306
|
: await readDesktopBridgeClientCapability(paths.client_capability_path);
|
|
@@ -551,12 +554,20 @@ async function autoApplyOfficialModelsAtServe(runtime, home, options) {
|
|
|
551
554
|
runtime.config.routePolicy = converged;
|
|
552
555
|
process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.official_models_auto_applied', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, mode, policy_generation: converged.policy_generation, secret_fields_redacted: true })}\n`);
|
|
553
556
|
}
|
|
557
|
+
async function activeModelsCatalog(home) {
|
|
558
|
+
const codexHome = path.join(path.resolve(home), '.codex');
|
|
559
|
+
const active = await readActiveCombinedBridgeCatalog(combinedBridgeCatalogPath(codexHome), bridgeRouteIndexPath(codexHome)).catch(() => null);
|
|
560
|
+
if (!active?.ok || active.catalog.models.length === 0)
|
|
561
|
+
return null;
|
|
562
|
+
return { generation: active.catalog.generation, models: active.catalog.models };
|
|
563
|
+
}
|
|
554
564
|
export async function serveDesktopBridge(options = {}) {
|
|
555
565
|
let handle = null;
|
|
556
566
|
try {
|
|
557
567
|
const runtime = await resolveDesktopBridgeRuntimeConfig(options);
|
|
558
568
|
const serveHome = options.home || options.env?.HOME || process.env.HOME || os.homedir();
|
|
559
569
|
await autoApplyOfficialModelsAtServe(runtime, serveHome, options);
|
|
570
|
+
runtime.config.modelsCatalog = await activeModelsCatalog(serveHome);
|
|
560
571
|
const supervised = desktopBridgeIsSupervised();
|
|
561
572
|
const skewMarkerPath = path.join(path.dirname(runtime.paths.state_path), 'desktop-bridge-skew-restart.json');
|
|
562
573
|
const prepared = await preflightDesktopBridge(runtime.config);
|
|
@@ -778,11 +789,37 @@ function normalizeProviderRegistrySnapshot(value) {
|
|
|
778
789
|
const providers = Object.fromEntries(['codex-lb', 'openrouter'].map((providerId) => {
|
|
779
790
|
const provider = registry.providers[providerId];
|
|
780
791
|
const sourceCatalogGeneration = provider.source_catalog_generation ?? provider.catalog_generation ?? null;
|
|
781
|
-
const { catalog_generation: _legacyCatalogGeneration, ...rest } = provider;
|
|
782
|
-
|
|
792
|
+
const { catalog_generation: _legacyCatalogGeneration, request_compat: requestCompat, ...rest } = provider;
|
|
793
|
+
const normalized = { ...rest, source_catalog_generation: sourceCatalogGeneration };
|
|
794
|
+
if (requestCompat !== undefined)
|
|
795
|
+
normalized.request_compat = validateRequestCompat(requestCompat);
|
|
796
|
+
return [providerId, normalized];
|
|
783
797
|
}));
|
|
784
798
|
return { ...registry, providers };
|
|
785
799
|
}
|
|
800
|
+
function validateRequestCompat(value) {
|
|
801
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
802
|
+
throw new Error('desktop_bridge_settings_request_compat_invalid');
|
|
803
|
+
const row = value;
|
|
804
|
+
if (Object.keys(row).some((key) => key !== 'extra_include'))
|
|
805
|
+
throw new Error('desktop_bridge_settings_request_compat_invalid');
|
|
806
|
+
const values = row.extra_include ?? [];
|
|
807
|
+
if (!Array.isArray(values) || values.length > REQUEST_COMPAT_MAX_EXTRA_INCLUDE
|
|
808
|
+
|| values.some((entry) => typeof entry !== 'string' || !REQUEST_COMPAT_INCLUDE_VALUE.test(entry))) {
|
|
809
|
+
throw new Error('desktop_bridge_settings_request_compat_invalid');
|
|
810
|
+
}
|
|
811
|
+
return { extra_include: [...new Set(values)] };
|
|
812
|
+
}
|
|
813
|
+
function withPersistedRequestCompat(registry, persisted) {
|
|
814
|
+
let providers = null;
|
|
815
|
+
for (const id of ['codex-lb', 'openrouter']) {
|
|
816
|
+
const compat = persisted?.providers?.[id]?.request_compat;
|
|
817
|
+
if (!compat || registry.providers[id].request_compat)
|
|
818
|
+
continue;
|
|
819
|
+
providers = { ...(providers ?? registry.providers), [id]: { ...registry.providers[id], request_compat: compat } };
|
|
820
|
+
}
|
|
821
|
+
return providers ? { ...registry, generation: sha256Stable(providers), providers } : registry;
|
|
822
|
+
}
|
|
786
823
|
function overridePaths(base, options) { return { settings_path: options.settingsPath || base.settings_path, state_path: options.statePath || base.state_path, client_capability_path: options.clientCapabilityPath || base.client_capability_path, launch_agent_path: options.launchAgentPath || base.launch_agent_path, stdout_log_path: options.stdoutLogPath || base.stdout_log_path, stderr_log_path: options.stderrLogPath || base.stderr_log_path }; }
|
|
787
824
|
function launchDomain(uid = typeof process.getuid === 'function' ? process.getuid() : 0) { return `gui/${uid}`; }
|
|
788
825
|
function launchService(uid) { return `${launchDomain(uid)}/${DESKTOP_BRIDGE_LAUNCHD_LABEL}`; }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { exists } from '../fsx.js';
|
|
4
|
+
import { desktopBridgeServicePaths } from './desktop-service.js';
|
|
5
|
+
export async function refreshModelListAfter(reason) {
|
|
6
|
+
if (process.platform !== 'darwin')
|
|
7
|
+
return null;
|
|
8
|
+
if (process.env.NODE_TEST_CONTEXT !== undefined || process.env.SKS_TEST_ISOLATION === '1' || process.env.SKS_RELEASE_UPGRADE_SMOKE === '1')
|
|
9
|
+
return null;
|
|
10
|
+
const home = path.resolve(process.env.HOME || os.homedir());
|
|
11
|
+
if (!(await exists(desktopBridgeServicePaths(home).settings_path)))
|
|
12
|
+
return null;
|
|
13
|
+
const { executeDesktopBridgeCommandV3 } = await import('./desktop-controller-v3.js');
|
|
14
|
+
const command = await executeDesktopBridgeCommandV3({ operation: 'models.refresh', reason, force: false, allow_config_writes: true }, { home, env: process.env }).catch(() => null);
|
|
15
|
+
return command?.result?.model_refresh ?? null;
|
|
16
|
+
}
|
|
@@ -330,6 +330,9 @@ export async function setupCommand(args = []) {
|
|
|
330
330
|
...[...(codexHooks?.activation?.blockers || []), ...(codexHooks?.project?.blockers || [])].map((blocker) => `codex_hooks:${blocker}`)
|
|
331
331
|
]
|
|
332
332
|
};
|
|
333
|
+
const modelRefresh = result.ok ? await (await import('../codex-lb/model-refresh-after.js')).refreshModelListAfter('setup') : null;
|
|
334
|
+
if (modelRefresh)
|
|
335
|
+
result.model_refresh = modelRefresh;
|
|
333
336
|
if (!result.ok)
|
|
334
337
|
process.exitCode = 1;
|
|
335
338
|
if (flag(args, '--json')) {
|
|
@@ -337,6 +340,8 @@ export async function setupCommand(args = []) {
|
|
|
337
340
|
return result;
|
|
338
341
|
}
|
|
339
342
|
console.log(`${result.ok ? 'Setup complete' : 'Setup blocked'}: ${root}`);
|
|
343
|
+
if (typeof modelRefresh?.line === 'string')
|
|
344
|
+
console.log(modelRefresh.line);
|
|
340
345
|
console.log(`Install scope: ${installScope}`);
|
|
341
346
|
console.log(`Codex CLI: ${cliTools.codex.status}${cliTools.codex.version ? ` ${cliTools.codex.version}` : ''}`);
|
|
342
347
|
console.log(`SKS Codex hooks: ${codexHooks?.active ? 'active for every Codex project' : installScope === 'project' ? 'this project only' : 'not active (run: sks doctor --fix)'}`);
|
|
@@ -31,7 +31,9 @@ export async function menubarCommand(subcommand = 'status', args = []) {
|
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
33
33
|
if (action === 'install') {
|
|
34
|
-
const
|
|
34
|
+
const installed = await installSksMenuBar({ root, ...(home ? { home } : {}), apply: true, launch: !flag(args, '--no-launch'), quiet: flag(args, '--json') });
|
|
35
|
+
const modelRefresh = await (await import('../codex-lb/model-refresh-after.js')).refreshModelListAfter('menubar-install');
|
|
36
|
+
const result = modelRefresh ? { ...installed, model_refresh: modelRefresh } : installed;
|
|
35
37
|
if (flag(args, '--json')) {
|
|
36
38
|
printJson(result);
|
|
37
39
|
if (!result.ok)
|
|
@@ -46,6 +48,8 @@ export async function menubarCommand(subcommand = 'status', args = []) {
|
|
|
46
48
|
console.log(`SKS menu bar install: ${result.status}`);
|
|
47
49
|
for (const actionLine of result.actions)
|
|
48
50
|
console.log(`- ${actionLine}`);
|
|
51
|
+
if (typeof modelRefresh?.line === 'string')
|
|
52
|
+
console.log(modelRefresh.line);
|
|
49
53
|
for (const warning of result.warnings)
|
|
50
54
|
console.log(`warning: ${warning}`);
|
|
51
55
|
for (const blocker of result.blockers)
|
|
@@ -101,10 +101,8 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
|
|
|
101
101
|
fix: input.fix
|
|
102
102
|
});
|
|
103
103
|
const status = await statusImpl({ home: bridgeHome, env: process.env });
|
|
104
|
-
const blockers = (status?.readiness?.blockers || []).map(String);
|
|
105
|
-
const stale = blockers.filter((blocker) => blocker.endsWith('_catalog_stale'));
|
|
106
104
|
const reverify = { fix: input.fix, bridgeHome, restarted: restarted.restarted };
|
|
107
|
-
if (!status?.management?.managed
|
|
105
|
+
if (!status?.management?.managed) {
|
|
108
106
|
return reverifyTransportIfDegraded({
|
|
109
107
|
...base,
|
|
110
108
|
ok: restarted.blockers.length === 0,
|
|
@@ -114,28 +112,33 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
|
|
|
114
112
|
}, reverify, statusImpl, executeImpl);
|
|
115
113
|
}
|
|
116
114
|
const attempts = [];
|
|
117
|
-
let
|
|
115
|
+
let refresh = null;
|
|
116
|
+
let stale = [];
|
|
118
117
|
for (let attempt = 1; attempt <= CATALOG_REPAIR_MAX_ATTEMPTS; attempt += 1) {
|
|
119
|
-
const
|
|
120
|
-
|
|
118
|
+
const command = await executeImpl({
|
|
119
|
+
operation: 'models.refresh',
|
|
120
|
+
reason: input.reason || (input.fix ? 'doctor-fix' : 'doctor'),
|
|
121
|
+
force: false,
|
|
122
|
+
allow_config_writes: input.fix
|
|
123
|
+
}, { home: bridgeHome, env: process.env });
|
|
124
|
+
refresh = command?.result?.model_refresh && typeof command.result.model_refresh === 'object' ? command.result.model_refresh : null;
|
|
121
125
|
const after = await statusImpl({ home: bridgeHome, env: process.env });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (commandOk && stillStale.length === 0) {
|
|
125
|
-
syncedAndVerified = true;
|
|
126
|
+
stale = (after?.readiness?.blockers || []).map(String).filter((blocker) => blocker.endsWith('_catalog_stale'));
|
|
127
|
+
if (refresh?.ok === true && stale.length === 0)
|
|
126
128
|
break;
|
|
127
|
-
}
|
|
128
|
-
attempts.push(`catalog_sync_attempt_${attempt}:${commandOk ? `still_stale:${stillStale.join(',')}` : (sync?.execution?.blockers || ['failed']).map(String).join(',')}`);
|
|
129
|
+
attempts.push(`model_refresh_attempt_${attempt}:${refresh?.ok === true ? `still_stale:${stale.join(',')}` : (refresh?.blockers || command?.execution?.blockers || ['failed']).map(String).join(',')}`);
|
|
129
130
|
}
|
|
131
|
+
const refreshed = refresh?.ok === true;
|
|
130
132
|
return reverifyTransportIfDegraded({
|
|
131
133
|
...base,
|
|
132
|
-
ok:
|
|
133
|
-
repaired:
|
|
134
|
-
warnings: [...restarted.warnings, ...(
|
|
134
|
+
ok: stale.length === 0 && restarted.blockers.length === 0,
|
|
135
|
+
repaired: (refreshed && refresh?.skipped !== true) || restarted.restarted,
|
|
136
|
+
warnings: [...restarted.warnings, ...(refresh?.warnings || []).map(String), ...(stale.length === 0 ? attempts : [])],
|
|
135
137
|
blockers: [
|
|
136
|
-
...(
|
|
138
|
+
...(stale.length === 0 ? [] : ['desktop_bridge_catalog_still_stale_after_repair', ...attempts]),
|
|
137
139
|
...restarted.blockers
|
|
138
|
-
]
|
|
140
|
+
],
|
|
141
|
+
model_refresh: refresh
|
|
139
142
|
}, reverify, statusImpl, executeImpl);
|
|
140
143
|
}
|
|
141
144
|
catch (error) {
|
|
@@ -56,16 +56,32 @@ function readableRows(file) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
let catalogPathMemo = null;
|
|
59
|
+
const BRIDGE_BASE_URL_MARKER = '# sks-desktop-bridge-managed-base-url';
|
|
60
|
+
function activeBridgeCatalogPath(codexHome) {
|
|
61
|
+
try {
|
|
62
|
+
const pointer = JSON.parse(fs.readFileSync(path.join(codexHome, 'sks', 'sks-bridge-active-generation.json'), 'utf8'));
|
|
63
|
+
const bundle = path.resolve(codexHome, 'sks', String(pointer?.bundle_directory || ''));
|
|
64
|
+
const file = path.join(bundle, String(pointer?.catalog_filename || ''));
|
|
65
|
+
return bundle.startsWith(path.join(codexHome, 'sks', '.sks-bridge-generations') + path.sep) && path.dirname(file) === bundle ? file : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
59
71
|
function configuredCatalogPath(input) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
72
|
+
const codexHome = codexHomePath(input);
|
|
73
|
+
const configPath = path.join(codexHome, 'config.toml');
|
|
74
|
+
const key = `${fileKey(configPath)}|${fileKey(path.join(codexHome, 'sks', 'sks-bridge-active-generation.json'))}`;
|
|
62
75
|
if (catalogPathMemo?.key === key)
|
|
63
76
|
return catalogPathMemo.path;
|
|
64
77
|
let resolved = null;
|
|
65
78
|
try {
|
|
66
79
|
const stat = fs.statSync(configPath);
|
|
67
80
|
if (stat.isFile() && stat.size <= 4 * 1024 * 1024) {
|
|
68
|
-
const
|
|
81
|
+
const config = fs.readFileSync(configPath, 'utf8');
|
|
82
|
+
const value = readTopLevelTomlString(config, 'model_catalog_json')?.trim();
|
|
83
|
+
if (!value && config.split(/\r?\n/).some((line) => line.trim() === BRIDGE_BASE_URL_MARKER))
|
|
84
|
+
resolved = activeBridgeCatalogPath(codexHome);
|
|
69
85
|
if (value) {
|
|
70
86
|
const env = input.env || process.env;
|
|
71
87
|
const home = input.home || env.HOME || os.homedir();
|
|
@@ -17,17 +17,18 @@ export async function runDesktopBridgeCatalogRepairStage() {
|
|
|
17
17
|
if (!(await exists(desktopBridgeServicePaths(home).settings_path))) {
|
|
18
18
|
return skip('desktop_bridge_catalog_repair_no_managed_bridge');
|
|
19
19
|
}
|
|
20
|
-
const repair = await repairDoctorDesktopBridgeCatalog({ fix: true }).catch((error) => ({
|
|
20
|
+
const repair = await repairDoctorDesktopBridgeCatalog({ fix: true, reason: 'update' }).catch((error) => ({
|
|
21
21
|
ok: false,
|
|
22
22
|
repaired: false,
|
|
23
23
|
warnings: [],
|
|
24
24
|
blockers: [`desktop_bridge_catalog_repair_failed:${error?.message || String(error)}`]
|
|
25
25
|
}));
|
|
26
|
+
const refreshLine = typeof repair.model_refresh?.line === 'string' ? [repair.model_refresh.line] : [];
|
|
26
27
|
if (repair.ok) {
|
|
27
28
|
return {
|
|
28
29
|
ok: true,
|
|
29
30
|
status: 'ok',
|
|
30
|
-
actions: [repair.repaired ? 'desktop_bridge_catalog_repaired' : 'desktop_bridge_catalog_clean'],
|
|
31
|
+
actions: [repair.repaired ? 'desktop_bridge_catalog_repaired' : 'desktop_bridge_catalog_clean', ...refreshLine],
|
|
31
32
|
blockers: [],
|
|
32
33
|
warnings: repair.warnings || []
|
|
33
34
|
};
|
|
@@ -35,7 +36,7 @@ export async function runDesktopBridgeCatalogRepairStage() {
|
|
|
35
36
|
return {
|
|
36
37
|
ok: true,
|
|
37
38
|
status: 'ok',
|
|
38
|
-
actions:
|
|
39
|
+
actions: refreshLine,
|
|
39
40
|
blockers: [],
|
|
40
41
|
warnings: [
|
|
41
42
|
...(repair.blockers || []).map((blocker) => `desktop_bridge_catalog_repair_incomplete:${blocker}`),
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '10.7.
|
|
1
|
+
export const PACKAGE_VERSION = '10.7.2';
|
|
@@ -33,7 +33,6 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
|
|
|
33
33
|
.mcpServers: MCPServersViewController(processClient: processClient, operations: operations, notifications: notifications),
|
|
34
34
|
.providers: ProvidersViewController(processClient: processClient, operations: operations),
|
|
35
35
|
.subagentModels: SubagentModelsViewController(processClient: processClient, operations: operations),
|
|
36
|
-
.remoteCoding: RemoteCodingViewController(),
|
|
37
36
|
.localDecision: LocalDecisionViewController(processClient: processClient, operations: operations),
|
|
38
37
|
.imageGeneration: ImageGenerationViewController(processClient: processClient, operations: operations),
|
|
39
38
|
.diagnostics: DiagnosticsViewController(processClient: processClient, operations: operations),
|
|
@@ -134,3 +134,35 @@ enum OverviewSummary {
|
|
|
134
134
|
return value
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/// What `sks bridge models refresh --json` reports, as SKS Center shows it.
|
|
139
|
+
/// Success is the payload's own `ok == true` at both levels; a result that only
|
|
140
|
+
/// carries a schema key is not one.
|
|
141
|
+
enum ModelRefreshSummary {
|
|
142
|
+
struct Outcome: Equatable {
|
|
143
|
+
let ok: Bool
|
|
144
|
+
let text: String
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static func outcome(fromJSONText text: String, at date: Date = Date()) -> Outcome {
|
|
148
|
+
outcome(fromJSON: jsonObject(text), at: date)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static func outcome(fromJSON object: [String: Any]?, at date: Date = Date()) -> Outcome {
|
|
152
|
+
let refresh = (object?["result"] as? [String: Any])?["model_refresh"] as? [String: Any]
|
|
153
|
+
guard object?["ok"] as? Bool == true, refresh?["ok"] as? Bool == true else {
|
|
154
|
+
return Outcome(ok: false, text: refresh?["line"] as? String ?? "Model list refresh failed. Run `sks bridge models refresh` to retry.")
|
|
155
|
+
}
|
|
156
|
+
let total = (refresh?["total"] as? NSNumber)?.intValue ?? 0
|
|
157
|
+
let formatter = DateFormatter()
|
|
158
|
+
formatter.dateFormat = "HH:mm"
|
|
159
|
+
let restart = refresh?["restart_codex_required"] as? Bool == true ? " · restart Codex to see it" : ""
|
|
160
|
+
return Outcome(ok: true, text: "Model list updated at \(formatter.string(from: date)) (\(total) models)\(restart)")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private static func jsonObject(_ text: String) -> [String: Any]? {
|
|
164
|
+
let start = text.firstIndex(of: "{") ?? text.startIndex
|
|
165
|
+
guard let data = String(text[start...]).data(using: .utf8) else { return nil }
|
|
166
|
+
return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -6,6 +6,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
6
6
|
private let status = NativeView.detail("Loading local SKS status…")
|
|
7
7
|
private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
|
|
8
8
|
private let recoveryStatus = NativeView.detail("Progress recovery: no operation state loaded yet.")
|
|
9
|
+
private let modelListStatus = NativeView.detail("Model list: refreshed by Refresh, Run Doctor and updates.")
|
|
9
10
|
private let healthBadge = NativeView.badge("Checking local status", color: .systemBlue)
|
|
10
11
|
private let statusSpinner = NativeView.spinner(label: "Checking SKS Center status")
|
|
11
12
|
private let snapshotDetails = NativeView.detail("")
|
|
@@ -36,7 +37,6 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
36
37
|
updateCodexButton.setAccessibilityHelp("Update the operator Codex CLI to the preferred latest channel.")
|
|
37
38
|
let buttons = NativeView.row([refreshButton, doctorButton, updateCodexButton])
|
|
38
39
|
let shortcuts = NativeView.row([
|
|
39
|
-
NativeView.button("Remote Coding…", target: self, action: #selector(openRemoteCoding)),
|
|
40
40
|
connectionsShortcut(),
|
|
41
41
|
NativeView.button("Decisions…", target: self, action: #selector(openDecisions)),
|
|
42
42
|
NativeView.button("Updates…", target: self, action: #selector(openUpdates)),
|
|
@@ -50,7 +50,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
50
50
|
status.setAccessibilityIdentifier("sks-overview-next-action")
|
|
51
51
|
let healthCard = NativeView.card(
|
|
52
52
|
title: "System health", subtitle: "",
|
|
53
|
-
views: [NativeView.row([healthBadge, statusSpinner]), status, components, buttons]
|
|
53
|
+
views: [NativeView.row([healthBadge, statusSpinner]), status, modelListStatus, components, buttons]
|
|
54
54
|
)
|
|
55
55
|
recoveryCard = NativeView.card(
|
|
56
56
|
title: "Progress, pause & recovery", subtitle: "Review the current operation before retrying.",
|
|
@@ -72,7 +72,6 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
72
72
|
return button
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
@objc private func openRemoteCoding() { openSection?("Remote Coding") }
|
|
76
75
|
@objc private func openProviders() { openSection?("Providers") }
|
|
77
76
|
@objc private func openDecisions() { openSection?("Decisions") }
|
|
78
77
|
@objc private func openUpdates() { openSection?("Updates") }
|
|
@@ -113,7 +112,15 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
@objc private func refreshStatus() {
|
|
116
|
-
loadStatus(forceUpdateRefresh: true)
|
|
115
|
+
loadStatus(forceUpdateRefresh: true) { [weak self] in self?.refreshModelList() }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Refresh, Run Doctor and a Codex CLI update each end with a fresh model list.
|
|
119
|
+
private func refreshModelList() {
|
|
120
|
+
modelListStatus.stringValue = "Refreshing the model list…"
|
|
121
|
+
processClient.run(["bridge", "models", "refresh", "--json"], timeout: NativeView.longMutationTimeout) { [weak self] result in
|
|
122
|
+
self?.modelListStatus.stringValue = ModelRefreshSummary.outcome(fromJSONText: result.output).text
|
|
123
|
+
}
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
@objc private func updateCodexCLI() {
|
|
@@ -145,11 +152,12 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
145
152
|
self.status.stringValue = ok
|
|
146
153
|
? "Codex CLI update completed. Refreshing shared update status…"
|
|
147
154
|
: "Codex CLI update needs attention. Open Updates or Diagnostics for structured guidance."
|
|
155
|
+
if ok { self.refreshModelList() }
|
|
148
156
|
self.loadStatus(forceUpdateRefresh: true)
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
private func loadStatus(forceUpdateRefresh: Bool) {
|
|
160
|
+
private func loadStatus(forceUpdateRefresh: Bool, then completion: (() -> Void)? = nil) {
|
|
153
161
|
generation += 1
|
|
154
162
|
let requestGeneration = generation
|
|
155
163
|
refreshButton?.isEnabled = false
|
|
@@ -196,6 +204,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
196
204
|
self.refreshButton?.isEnabled = true
|
|
197
205
|
self.statusSpinner.stopAnimation(nil)
|
|
198
206
|
self.renderStatus(update: update, mcp: mcp, partial: false)
|
|
207
|
+
completion?()
|
|
199
208
|
}
|
|
200
209
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
|
|
201
210
|
guard let self = self,
|
|
@@ -232,6 +241,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
232
241
|
if result.code == 0 {
|
|
233
242
|
NativeView.setBadge(self.healthBadge, text: "Doctor completed", color: .systemGreen)
|
|
234
243
|
self.status.stringValue = "Doctor completed. No blocking issue was reported."
|
|
244
|
+
self.refreshModelList()
|
|
235
245
|
} else {
|
|
236
246
|
NativeView.setBadge(self.healthBadge, text: "Doctor needs attention", color: .systemOrange)
|
|
237
247
|
self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
|
|
@@ -6,7 +6,6 @@ enum SidebarItem: String, CaseIterable {
|
|
|
6
6
|
case overview = "Overview"
|
|
7
7
|
case providers = "Providers"
|
|
8
8
|
case subagentModels = "Subagent Models"
|
|
9
|
-
case remoteCoding = "Remote Coding"
|
|
10
9
|
case updates = "Updates"
|
|
11
10
|
case mcpServers = "MCP Servers"
|
|
12
11
|
case localDecision = "Decisions"
|
|
@@ -25,7 +24,6 @@ enum SidebarItem: String, CaseIterable {
|
|
|
25
24
|
case .mcpServers: return "server.rack"
|
|
26
25
|
case .localDecision: return "cpu"
|
|
27
26
|
case .imageGeneration: return "photo"
|
|
28
|
-
case .remoteCoding: return "laptopcomputer.and.iphone"
|
|
29
27
|
case .diagnostics: return "stethoscope"
|
|
30
28
|
case .settings: return "gearshape"
|
|
31
29
|
}
|
|
@@ -231,6 +231,9 @@ final class UpdatesViewController: NSViewController, ControlCenterPage {
|
|
|
231
231
|
self.status.stringValue = "Review ready. Confirm to continue, or cancel to leave the current install unchanged."
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
if kind == "update", authoritativeState == .succeeded { // The update refreshed the model list; show what it holds now.
|
|
235
|
+
self.processClient.run(["bridge", "models", "refresh", "--json"], timeout: NativeView.mutationTimeout) { [weak self] refreshed in self?.status.stringValue = ModelRefreshSummary.outcome(fromJSONText: refreshed.output).text }
|
|
236
|
+
}
|
|
234
237
|
let updateOutput = kind == "update-status" ? result.output : nil
|
|
235
238
|
let approvalRequired = kind == "update-review" && result.code == 0
|
|
236
239
|
let updateAvailable = updateOutput != nil && NotificationCoordinator.updateIsAvailable(in: result.output)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "10.7.
|
|
4
|
+
"version": "10.7.2",
|
|
5
5
|
"description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import Cocoa
|
|
2
|
-
|
|
3
|
-
final class RemoteCodingViewController: NSViewController {
|
|
4
|
-
private static let websiteURL = URL(string: "https://paseo.sh/")!
|
|
5
|
-
private static let docsURL = URL(string: "https://paseo.sh/docs")!
|
|
6
|
-
|
|
7
|
-
override func loadView() {
|
|
8
|
-
let websiteButton = NativeView.button(
|
|
9
|
-
"Visit Paseo",
|
|
10
|
-
target: self,
|
|
11
|
-
action: #selector(openWebsite)
|
|
12
|
-
)
|
|
13
|
-
websiteButton.setAccessibilityLabel("Visit Paseo website")
|
|
14
|
-
websiteButton.setAccessibilityHelp("Open the independent Paseo website in your default browser.")
|
|
15
|
-
|
|
16
|
-
let docsButton = NativeView.button(
|
|
17
|
-
"Read Paseo Docs",
|
|
18
|
-
target: self,
|
|
19
|
-
action: #selector(openDocs)
|
|
20
|
-
)
|
|
21
|
-
docsButton.setAccessibilityLabel("Read Paseo documentation")
|
|
22
|
-
docsButton.setAccessibilityHelp("Open the official Paseo documentation in your default browser.")
|
|
23
|
-
|
|
24
|
-
let recommendation = NativeView.card(
|
|
25
|
-
title: "Paseo (recommended)",
|
|
26
|
-
subtitle: "Run Codex from your desktop, phone, web browser, or terminal.",
|
|
27
|
-
views: [
|
|
28
|
-
NativeView.row([websiteButton, docsButton]),
|
|
29
|
-
NativeView.detail("Paseo is a separate, open-source app. Follow its setup and security guidance to connect your devices.")
|
|
30
|
-
]
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
view = NativeView.page([
|
|
34
|
-
NativeView.title("Remote Coding"),
|
|
35
|
-
NativeView.detail(
|
|
36
|
-
"Continue coding from another device."
|
|
37
|
-
),
|
|
38
|
-
recommendation
|
|
39
|
-
])
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@objc private func openWebsite() {
|
|
43
|
-
NSWorkspace.shared.open(Self.websiteURL)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@objc private func openDocs() {
|
|
47
|
-
NSWorkspace.shared.open(Self.docsURL)
|
|
48
|
-
}
|
|
49
|
-
}
|