@glwhappen/web-code 1.32.9 → 1.32.11
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.de.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.tr.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/api-docs.html +6 -7
- package/dist/assets/{index-D_7CSvqO.js → index-CBo8yakG.js} +276 -261
- package/dist/assets/index-Dl5QP21C.css +32 -0
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +57 -34
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -3
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +3 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/gemini-response-handler.js +34 -0
- package/dist-server/server/gemini-response-handler.js.map +1 -1
- package/dist-server/server/index.js +131 -19
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +143 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +42 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +264 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +18 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/openai-codex.js +32 -4
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +287 -0
- package/dist-server/server/opencode-cli.js.map +1 -0
- package/dist-server/server/opencode-cli.test.js +84 -0
- package/dist-server/server/opencode-cli.test.js.map +1 -0
- package/dist-server/server/routes/agent.js +21 -8
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +202 -209
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/cursor.js +2 -2
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/routes/settings.js +0 -10
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/tests/commands.test.js +76 -0
- package/dist-server/server/routes/tests/commands.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +286 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/package.json +3 -1
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +841 -0
- package/server/claude-sdk.js +64 -35
- package/server/cursor-cli.js +6 -3
- package/server/gemini-cli.js +7 -1
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +150 -19
- package/server/modules/database/index.ts +1 -0
- package/server/modules/projects/services/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
- package/server/modules/providers/README.md +11 -3
- package/server/modules/providers/list/claude/claude-models.provider.ts +193 -0
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +158 -0
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
- package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
- package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
- package/server/modules/providers/provider.registry.ts +2 -0
- package/server/modules/providers/provider.routes.ts +62 -2
- package/server/modules/providers/services/mcp.service.ts +1 -12
- package/server/modules/providers/services/provider-models.service.ts +325 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
- package/server/modules/providers/tests/mcp.test.ts +93 -6
- package/server/modules/providers/tests/opencode-models.test.ts +73 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +336 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +66 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +21 -1
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/openai-codex.js +40 -4
- package/server/opencode-cli.js +336 -0
- package/server/opencode-cli.test.js +95 -0
- package/server/routes/agent.js +22 -8
- package/server/routes/commands.js +254 -233
- package/server/routes/cursor.js +2 -2
- package/server/routes/settings.js +1 -10
- package/server/routes/tests/commands.test.js +82 -0
- package/server/shared/interfaces.ts +45 -0
- package/server/shared/types.ts +88 -1
- package/server/shared/utils.ts +384 -0
- package/dist/assets/index-DdxLnCfK.css +0 -32
- package/dist-server/shared/modelConstants.js +0 -99
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -107
package/server/routes/cursor.js
CHANGED
|
@@ -2,7 +2,7 @@ import express from 'express';
|
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { CURSOR_FALLBACK_MODELS } from '../modules/providers/list/cursor/cursor-models.provider.js';
|
|
6
6
|
|
|
7
7
|
const router = express.Router();
|
|
8
8
|
|
|
@@ -29,7 +29,7 @@ router.get('/config', async (req, res) => {
|
|
|
29
29
|
config: {
|
|
30
30
|
version: 1,
|
|
31
31
|
model: {
|
|
32
|
-
modelId:
|
|
32
|
+
modelId: CURSOR_FALLBACK_MODELS.DEFAULT,
|
|
33
33
|
displayName: 'GPT-5',
|
|
34
34
|
},
|
|
35
35
|
permissions: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
|
|
2
3
|
import { apiKeysDb, credentialsDb, notificationPreferencesDb, pushSubscriptionsDb } from '../modules/database/index.js';
|
|
3
4
|
import { getPublicKey } from '../services/vapid-keys.js';
|
|
4
5
|
import { createNotificationEvent, notifyUserIfEnabled } from '../services/notification-orchestrator.js';
|
|
@@ -273,14 +274,4 @@ router.post('/push/unsubscribe', async (req, res) => {
|
|
|
273
274
|
}
|
|
274
275
|
});
|
|
275
276
|
|
|
276
|
-
// Host OS for UI (e.g. hide Cursor agent when the backend runs on Windows).
|
|
277
|
-
router.get('/server-env', async (req, res) => {
|
|
278
|
-
try {
|
|
279
|
-
res.json({ platform: process.platform });
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.error('Error reading server environment:', error);
|
|
282
|
-
res.status(500).json({ error: 'Failed to read server environment' });
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
277
|
export default router;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { executeModelsCommand } from '../commands.js';
|
|
5
|
+
import { providerModelsService } from '../../modules/providers/services/provider-models.service.js';
|
|
6
|
+
|
|
7
|
+
test('models command returns available models only for the active provider', async () => {
|
|
8
|
+
const originalGetProviderModels = providerModelsService.getProviderModels;
|
|
9
|
+
const originalGetCurrentActiveModel = providerModelsService.getCurrentActiveModel;
|
|
10
|
+
let getCurrentActiveModelCalls = 0;
|
|
11
|
+
|
|
12
|
+
providerModelsService.getProviderModels = async () => ({
|
|
13
|
+
models: {
|
|
14
|
+
OPTIONS: [{ value: 'gpt-5.4', label: 'gpt-5.4' }],
|
|
15
|
+
DEFAULT: 'gpt-5.4',
|
|
16
|
+
},
|
|
17
|
+
cache: {
|
|
18
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
19
|
+
expiresAt: '2026-01-04T00:00:00.000Z',
|
|
20
|
+
source: 'fresh',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
providerModelsService.getCurrentActiveModel = async () => {
|
|
24
|
+
getCurrentActiveModelCalls += 1;
|
|
25
|
+
return {
|
|
26
|
+
model: 'gpt-5.3-codex',
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = await executeModelsCommand([], {
|
|
32
|
+
provider: 'codex',
|
|
33
|
+
model: 'gpt-5.4',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.equal(result.type, 'builtin');
|
|
37
|
+
assert.equal(result.action, 'models');
|
|
38
|
+
assert.equal(result.data.current.provider, 'codex');
|
|
39
|
+
assert.equal(result.data.current.model, 'gpt-5.4');
|
|
40
|
+
assert.deepEqual(Object.keys(result.data.available), ['codex']);
|
|
41
|
+
assert.deepEqual(result.data.available.codex, result.data.availableModels);
|
|
42
|
+
assert.ok(result.data.availableModels.includes('gpt-5.4'));
|
|
43
|
+
assert.equal(result.data.available.claude, undefined);
|
|
44
|
+
assert.equal(result.data.available.cursor, undefined);
|
|
45
|
+
assert.equal(getCurrentActiveModelCalls, 0);
|
|
46
|
+
} finally {
|
|
47
|
+
providerModelsService.getProviderModels = originalGetProviderModels;
|
|
48
|
+
providerModelsService.getCurrentActiveModel = originalGetCurrentActiveModel;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('models command falls back to claude for unsupported providers', async () => {
|
|
53
|
+
const originalGetProviderModels = providerModelsService.getProviderModels;
|
|
54
|
+
const originalGetCurrentActiveModel = providerModelsService.getCurrentActiveModel;
|
|
55
|
+
|
|
56
|
+
providerModelsService.getProviderModels = async () => ({
|
|
57
|
+
models: {
|
|
58
|
+
OPTIONS: [{ value: 'default', label: 'Default (recommended)' }],
|
|
59
|
+
DEFAULT: 'default',
|
|
60
|
+
},
|
|
61
|
+
cache: {
|
|
62
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
63
|
+
expiresAt: '2026-01-04T00:00:00.000Z',
|
|
64
|
+
source: 'fresh',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
providerModelsService.getCurrentActiveModel = async () => ({
|
|
68
|
+
model: 'default',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await executeModelsCommand([], {
|
|
73
|
+
provider: 'unknown-provider',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.equal(result.data.current.provider, 'claude');
|
|
77
|
+
assert.deepEqual(Object.keys(result.data.available), ['claude']);
|
|
78
|
+
} finally {
|
|
79
|
+
providerModelsService.getProviderModels = originalGetProviderModels;
|
|
80
|
+
providerModelsService.getCurrentActiveModel = originalGetCurrentActiveModel;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
ProviderSkill,
|
|
8
8
|
ProviderSkillListOptions,
|
|
9
9
|
ProviderAuthStatus,
|
|
10
|
+
ProviderChangeActiveModelInput,
|
|
11
|
+
ProviderCurrentActiveModel,
|
|
12
|
+
ProviderModelsDefinition,
|
|
10
13
|
ProviderMcpServer,
|
|
14
|
+
ProviderSessionActiveModelChange,
|
|
11
15
|
UpsertProviderMcpServerInput,
|
|
12
16
|
} from '@/shared/types.js';
|
|
13
17
|
|
|
@@ -20,6 +24,7 @@ import type {
|
|
|
20
24
|
*/
|
|
21
25
|
export interface IProvider {
|
|
22
26
|
readonly id: LLMProvider;
|
|
27
|
+
readonly models: IProviderModels;
|
|
23
28
|
readonly mcp: IProviderMcp;
|
|
24
29
|
readonly auth: IProviderAuth;
|
|
25
30
|
readonly skills: IProviderSkills;
|
|
@@ -27,6 +32,46 @@ export interface IProvider {
|
|
|
27
32
|
readonly sessionSynchronizer: IProviderSessionSynchronizer;
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
// ---------------------------
|
|
36
|
+
//----------------- PROVIDER MODEL INTERFACE ------------
|
|
37
|
+
/**
|
|
38
|
+
* Model catalog contract for one provider.
|
|
39
|
+
*
|
|
40
|
+
* Implementations are responsible for resolving the provider's currently
|
|
41
|
+
* supported models and converting them into the shared
|
|
42
|
+
* `ProviderModelsDefinition` shape used by backend routes and frontend model
|
|
43
|
+
* pickers. The `DEFAULT` field should be the most appropriate default selection
|
|
44
|
+
* for that provider at the time the catalog is read.
|
|
45
|
+
*/
|
|
46
|
+
export interface IProviderModels {
|
|
47
|
+
/**
|
|
48
|
+
* Returns the provider's currently supported model catalog.
|
|
49
|
+
*/
|
|
50
|
+
getSupportedModels(): Promise<ProviderModelsDefinition>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the currently active model for one session or provider runtime.
|
|
54
|
+
*
|
|
55
|
+
* Implementations must use the provider-specific lookup mechanism approved
|
|
56
|
+
* for that provider and fall back only to the provider catalog default when
|
|
57
|
+
* no active model can be resolved.
|
|
58
|
+
*/
|
|
59
|
+
getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Persists a session-scoped model override that the next resumed turn should
|
|
63
|
+
* honor for this provider.
|
|
64
|
+
*
|
|
65
|
+
* This does not require the provider to mutate an already running remote
|
|
66
|
+
* session in-place. Instead, adapters store the user's explicit model choice
|
|
67
|
+
* so the backend resume path can add the correct provider-native model option
|
|
68
|
+
* on the next CLI/SDK invocation for the same session.
|
|
69
|
+
*/
|
|
70
|
+
changeActiveModel(
|
|
71
|
+
input: ProviderChangeActiveModelInput,
|
|
72
|
+
): Promise<ProviderSessionActiveModelChange>;
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
// ---------------------------
|
|
31
76
|
//----------------- PROVIDER AUTH INTERFACE ------------
|
|
32
77
|
/**
|
package/server/shared/types.ts
CHANGED
|
@@ -66,7 +66,94 @@ export type AuthenticatedWebSocketRequest = IncomingMessage & {
|
|
|
66
66
|
* Use this as the source of truth whenever a function or payload needs to identify
|
|
67
67
|
* a specific LLM integration.
|
|
68
68
|
*/
|
|
69
|
-
export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor';
|
|
69
|
+
export type LLMProvider = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One selectable model row (matches the documentation `public/modelConstants.js` option shape).
|
|
73
|
+
*/
|
|
74
|
+
export type ProviderModelOption = {
|
|
75
|
+
value: string;
|
|
76
|
+
label: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Provider model catalog returned by `GET /api/providers/:provider/models`.
|
|
82
|
+
*/
|
|
83
|
+
export type ProviderModelsDefinition = {
|
|
84
|
+
OPTIONS: ProviderModelOption[];
|
|
85
|
+
DEFAULT: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Cache metadata returned alongside one provider model catalog.
|
|
90
|
+
*
|
|
91
|
+
* `updatedAt` is when the current cached snapshot was last refreshed from the
|
|
92
|
+
* provider itself. `expiresAt` is the backend cache expiry timestamp, and
|
|
93
|
+
* `source` tells callers whether the current response came from in-memory cache,
|
|
94
|
+
* persisted disk cache, or a fresh provider fetch.
|
|
95
|
+
*/
|
|
96
|
+
export type ProviderModelsCacheInfo = {
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
expiresAt: string;
|
|
99
|
+
source: 'memory' | 'disk' | 'fresh';
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Full provider model lookup result returned by the backend service layer.
|
|
104
|
+
*
|
|
105
|
+
* Use this shape when a caller needs both the selectable model catalog and the
|
|
106
|
+
* cache metadata that explains how current the catalog is.
|
|
107
|
+
*/
|
|
108
|
+
export type ProviderModelsResult = {
|
|
109
|
+
models: ProviderModelsDefinition;
|
|
110
|
+
cache: ProviderModelsCacheInfo;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ---------------------------
|
|
114
|
+
//----------------- PROVIDER ACTIVE MODEL TYPES ------------
|
|
115
|
+
/**
|
|
116
|
+
* Provider-neutral result for the model that is actively driving a session or
|
|
117
|
+
* provider runtime at the time of lookup.
|
|
118
|
+
*
|
|
119
|
+
* `model` must always be populated. Provider adapters should use the
|
|
120
|
+
* provider-specific lookup method requested by the caller, and only fall back
|
|
121
|
+
* to the provider catalog `DEFAULT` value when the active model cannot be read.
|
|
122
|
+
*/
|
|
123
|
+
export type ProviderCurrentActiveModel = {
|
|
124
|
+
model: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Input payload used when one session needs to use a different model on its
|
|
129
|
+
* next resumed turn.
|
|
130
|
+
*
|
|
131
|
+
* This is a backend-owned session override, not a claim that the provider has
|
|
132
|
+
* already switched the currently running session in-place. Provider adapters
|
|
133
|
+
* persist this request so the next CLI/SDK resume can inject the chosen model
|
|
134
|
+
* using the provider-specific mechanism supported by that runtime.
|
|
135
|
+
*/
|
|
136
|
+
export type ProviderChangeActiveModelInput = {
|
|
137
|
+
sessionId: string;
|
|
138
|
+
model: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Provider-neutral session model-change state.
|
|
143
|
+
*
|
|
144
|
+
* `supported` indicates whether the provider adapter supports the app's
|
|
145
|
+
* session-scoped resume override flow. `changed` is the persisted boolean the
|
|
146
|
+
* resume layer checks before forcing a model on the next resumed turn. When
|
|
147
|
+
* `changed` is `false`, `model` is `null` and the runtime should use the
|
|
148
|
+
* normal request/default model selection path.
|
|
149
|
+
*/
|
|
150
|
+
export type ProviderSessionActiveModelChange = {
|
|
151
|
+
provider: LLMProvider;
|
|
152
|
+
sessionId: string;
|
|
153
|
+
supported: boolean;
|
|
154
|
+
changed: boolean;
|
|
155
|
+
model: string | null;
|
|
156
|
+
};
|
|
70
157
|
|
|
71
158
|
/**
|
|
72
159
|
* Message/event variants emitted by provider adapters and normalized transports.
|
package/server/shared/utils.ts
CHANGED
|
@@ -22,7 +22,13 @@ import type {
|
|
|
22
22
|
AnyRecord,
|
|
23
23
|
ApiSuccessShape,
|
|
24
24
|
AppErrorOptions,
|
|
25
|
+
LLMProvider,
|
|
25
26
|
NormalizedMessage,
|
|
27
|
+
ProviderChangeActiveModelInput,
|
|
28
|
+
ProviderCurrentActiveModel,
|
|
29
|
+
ProviderModelsDefinition,
|
|
30
|
+
ProviderSessionActiveModelChange,
|
|
31
|
+
ProviderSkillSource,
|
|
26
32
|
WorkspacePathValidationResult,
|
|
27
33
|
} from '@/shared/types.js';
|
|
28
34
|
|
|
@@ -474,6 +480,231 @@ export const readStringRecord = (value: unknown): Record<string, string> | undef
|
|
|
474
480
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
475
481
|
};
|
|
476
482
|
|
|
483
|
+
// ---------------------------
|
|
484
|
+
//----------------- PROVIDER MODEL LOOKUP UTILITIES ------------
|
|
485
|
+
/**
|
|
486
|
+
* Builds the standard "default current model" result used when a provider
|
|
487
|
+
* cannot resolve a session-backed active model.
|
|
488
|
+
*
|
|
489
|
+
* Provider model adapters should call this after loading their supported model
|
|
490
|
+
* catalog so the fallback stays aligned with the provider's current `DEFAULT`
|
|
491
|
+
* selection instead of drifting to a hard-coded duplicate.
|
|
492
|
+
*/
|
|
493
|
+
export function buildDefaultProviderCurrentActiveModel(
|
|
494
|
+
models: ProviderModelsDefinition,
|
|
495
|
+
): ProviderCurrentActiveModel {
|
|
496
|
+
return {
|
|
497
|
+
model: models.DEFAULT,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ---------------------------
|
|
502
|
+
//----------------- PROVIDER SESSION MODEL CHANGE UTILITIES ------------
|
|
503
|
+
type ProviderSessionActiveModelChangeCacheEntry = ProviderSessionActiveModelChange & {
|
|
504
|
+
updatedAt: string;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
type ProviderSessionActiveModelChangeCacheFile = {
|
|
508
|
+
version: number;
|
|
509
|
+
entries: Record<string, ProviderSessionActiveModelChangeCacheEntry>;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION = 1;
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Resolves the backend-owned cache file used for session-scoped resume model
|
|
516
|
+
* overrides.
|
|
517
|
+
*
|
|
518
|
+
* The file lives under `~/.cloudcli` because these overrides are an application
|
|
519
|
+
* concern rather than a provider-native config file. Providers, routes, and
|
|
520
|
+
* runtime command launchers should all use this helper instead of re-creating
|
|
521
|
+
* the path so the storage location stays consistent.
|
|
522
|
+
*/
|
|
523
|
+
export function getProviderSessionActiveModelChangesPath(): string {
|
|
524
|
+
return path.join(os.homedir(), '.cloudcli', 'provider-session-active-model-changes.json');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const buildProviderSessionActiveModelChangeKey = (
|
|
528
|
+
provider: LLMProvider,
|
|
529
|
+
sessionId: string,
|
|
530
|
+
): string => `${provider}:${sessionId}`;
|
|
531
|
+
|
|
532
|
+
const isProviderSessionActiveModelChangeCacheEntry = (
|
|
533
|
+
value: unknown,
|
|
534
|
+
): value is ProviderSessionActiveModelChangeCacheEntry => {
|
|
535
|
+
const record = readObjectRecord(value);
|
|
536
|
+
return Boolean(
|
|
537
|
+
record
|
|
538
|
+
&& typeof record.provider === 'string'
|
|
539
|
+
&& typeof record.sessionId === 'string'
|
|
540
|
+
&& typeof record.supported === 'boolean'
|
|
541
|
+
&& typeof record.changed === 'boolean'
|
|
542
|
+
&& (typeof record.model === 'string' || record.model === null)
|
|
543
|
+
&& typeof record.updatedAt === 'string',
|
|
544
|
+
);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const readProviderSessionActiveModelChangeCacheFile = async (
|
|
548
|
+
filePath: string,
|
|
549
|
+
): Promise<ProviderSessionActiveModelChangeCacheFile> => {
|
|
550
|
+
try {
|
|
551
|
+
const raw = await readFile(filePath, 'utf8');
|
|
552
|
+
const parsed = readObjectRecord(JSON.parse(raw));
|
|
553
|
+
if (
|
|
554
|
+
!parsed
|
|
555
|
+
|| parsed.version !== PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION
|
|
556
|
+
|| !readObjectRecord(parsed.entries)
|
|
557
|
+
) {
|
|
558
|
+
return {
|
|
559
|
+
version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
|
|
560
|
+
entries: {},
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const entries = Object.fromEntries(
|
|
565
|
+
Object.entries(parsed.entries).filter((entry): entry is [string, ProviderSessionActiveModelChangeCacheEntry] =>
|
|
566
|
+
isProviderSessionActiveModelChangeCacheEntry(entry[1]),
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
|
|
572
|
+
entries,
|
|
573
|
+
};
|
|
574
|
+
} catch {
|
|
575
|
+
return {
|
|
576
|
+
version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
|
|
577
|
+
entries: {},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const writeProviderSessionActiveModelChangeCacheFile = async (
|
|
583
|
+
filePath: string,
|
|
584
|
+
payload: ProviderSessionActiveModelChangeCacheFile,
|
|
585
|
+
): Promise<void> => {
|
|
586
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
587
|
+
await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const buildUnsupportedProviderSessionActiveModelChange = (
|
|
591
|
+
provider: LLMProvider,
|
|
592
|
+
sessionId: string,
|
|
593
|
+
): ProviderSessionActiveModelChange => ({
|
|
594
|
+
provider,
|
|
595
|
+
sessionId,
|
|
596
|
+
supported: false,
|
|
597
|
+
changed: false,
|
|
598
|
+
model: null,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Reads the persisted session model-change state for one provider session.
|
|
603
|
+
*
|
|
604
|
+
* Runtime resume paths use this to decide whether they should inject a
|
|
605
|
+
* provider-specific model argument/thread option for the next resumed turn.
|
|
606
|
+
* Missing cache entries are normalized to `{ changed: false }` so callers can
|
|
607
|
+
* treat absence as "use the ordinary model selection flow".
|
|
608
|
+
*/
|
|
609
|
+
export async function readProviderSessionActiveModelChange(
|
|
610
|
+
provider: LLMProvider,
|
|
611
|
+
sessionId: string,
|
|
612
|
+
options: {
|
|
613
|
+
filePath?: string;
|
|
614
|
+
supported?: boolean;
|
|
615
|
+
} = {},
|
|
616
|
+
): Promise<ProviderSessionActiveModelChange> {
|
|
617
|
+
const normalizedSessionId = sessionId.trim();
|
|
618
|
+
if (!normalizedSessionId) {
|
|
619
|
+
return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const supported = options.supported ?? true;
|
|
623
|
+
if (!supported) {
|
|
624
|
+
return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const filePath = options.filePath ?? getProviderSessionActiveModelChangesPath();
|
|
628
|
+
const cacheFile = await readProviderSessionActiveModelChangeCacheFile(filePath);
|
|
629
|
+
const cacheEntry = cacheFile.entries[
|
|
630
|
+
buildProviderSessionActiveModelChangeKey(provider, normalizedSessionId)
|
|
631
|
+
];
|
|
632
|
+
|
|
633
|
+
if (!cacheEntry || !cacheEntry.changed || !cacheEntry.model?.trim()) {
|
|
634
|
+
return {
|
|
635
|
+
provider,
|
|
636
|
+
sessionId: normalizedSessionId,
|
|
637
|
+
supported: true,
|
|
638
|
+
changed: false,
|
|
639
|
+
model: null,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
provider,
|
|
645
|
+
sessionId: normalizedSessionId,
|
|
646
|
+
supported: true,
|
|
647
|
+
changed: true,
|
|
648
|
+
model: cacheEntry.model.trim(),
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Persists a session model-change request for one provider.
|
|
654
|
+
*
|
|
655
|
+
* Provider adapters call this when the frontend explicitly selects a different
|
|
656
|
+
* model for an existing session. The stored `changed: true` flag is the single
|
|
657
|
+
* source of truth used later by resume paths to decide whether they should add
|
|
658
|
+
* a provider-native model override on the next invocation.
|
|
659
|
+
*/
|
|
660
|
+
export async function writeProviderSessionActiveModelChange(
|
|
661
|
+
provider: LLMProvider,
|
|
662
|
+
input: ProviderChangeActiveModelInput,
|
|
663
|
+
options: {
|
|
664
|
+
filePath?: string;
|
|
665
|
+
supported?: boolean;
|
|
666
|
+
} = {},
|
|
667
|
+
): Promise<ProviderSessionActiveModelChange> {
|
|
668
|
+
const normalizedSessionId = input.sessionId.trim();
|
|
669
|
+
const normalizedModel = input.model.trim();
|
|
670
|
+
const supported = options.supported ?? true;
|
|
671
|
+
|
|
672
|
+
if (!supported) {
|
|
673
|
+
return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!normalizedSessionId || !normalizedModel) {
|
|
677
|
+
return {
|
|
678
|
+
provider,
|
|
679
|
+
sessionId: normalizedSessionId,
|
|
680
|
+
supported: true,
|
|
681
|
+
changed: false,
|
|
682
|
+
model: null,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const filePath = options.filePath ?? getProviderSessionActiveModelChangesPath();
|
|
687
|
+
const cacheFile = await readProviderSessionActiveModelChangeCacheFile(filePath);
|
|
688
|
+
cacheFile.entries[buildProviderSessionActiveModelChangeKey(provider, normalizedSessionId)] = {
|
|
689
|
+
provider,
|
|
690
|
+
sessionId: normalizedSessionId,
|
|
691
|
+
supported: true,
|
|
692
|
+
changed: true,
|
|
693
|
+
model: normalizedModel,
|
|
694
|
+
updatedAt: new Date().toISOString(),
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
await writeProviderSessionActiveModelChangeCacheFile(filePath, cacheFile);
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
provider,
|
|
701
|
+
sessionId: normalizedSessionId,
|
|
702
|
+
supported: true,
|
|
703
|
+
changed: true,
|
|
704
|
+
model: normalizedModel,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
477
708
|
// ---------------------------
|
|
478
709
|
//----------------- WEBSOCKET PAYLOAD PARSING UTILITIES ------------
|
|
479
710
|
/**
|
|
@@ -567,6 +798,67 @@ export const writeJsonConfig = async (filePath: string, data: Record<string, unk
|
|
|
567
798
|
|
|
568
799
|
// ---------------------------
|
|
569
800
|
//----------------- PROVIDER SKILL FILE UTILITIES ------------
|
|
801
|
+
async function hasGitMarker(dirPath: string): Promise<boolean> {
|
|
802
|
+
try {
|
|
803
|
+
const gitMarkerStats = await stat(path.join(dirPath, '.git'));
|
|
804
|
+
return gitMarkerStats.isDirectory() || gitMarkerStats.isFile();
|
|
805
|
+
} catch {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Finds the highest git worktree root visible from a starting directory.
|
|
812
|
+
*
|
|
813
|
+
* Provider skill systems such as Codex and OpenCode walk upward through parent
|
|
814
|
+
* folders when resolving repository/project skills. Use this helper when a
|
|
815
|
+
* provider needs the topmost `.git` marker instead of only the nearest one, so
|
|
816
|
+
* monorepos and nested package folders discover shared root-level skills once.
|
|
817
|
+
*/
|
|
818
|
+
export async function findTopmostGitRoot(startPath: string): Promise<string | null> {
|
|
819
|
+
let currentPath = path.resolve(startPath);
|
|
820
|
+
let topmostGitRoot: string | null = null;
|
|
821
|
+
|
|
822
|
+
while (true) {
|
|
823
|
+
if (await hasGitMarker(currentPath)) {
|
|
824
|
+
topmostGitRoot = currentPath;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const parentPath = path.dirname(currentPath);
|
|
828
|
+
if (parentPath === currentPath) {
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
currentPath = parentPath;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return topmostGitRoot;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Adds one provider skill source after normalizing and de-duplicating its root.
|
|
840
|
+
*
|
|
841
|
+
* Provider skill lookup rules often point at overlapping folders (for example a
|
|
842
|
+
* workspace folder can also be the git root). Use this helper while building a
|
|
843
|
+
* provider's `ProviderSkillSource[]` so the shared skills scanner reads each
|
|
844
|
+
* physical root once and still preserves provider-specific scope/command data.
|
|
845
|
+
*/
|
|
846
|
+
export function addUniqueProviderSkillSource(
|
|
847
|
+
sources: ProviderSkillSource[],
|
|
848
|
+
seenRootDirs: Set<string>,
|
|
849
|
+
source: ProviderSkillSource,
|
|
850
|
+
): void {
|
|
851
|
+
const normalizedRootDir = path.resolve(source.rootDir);
|
|
852
|
+
if (seenRootDirs.has(normalizedRootDir)) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
seenRootDirs.add(normalizedRootDir);
|
|
857
|
+
sources.push({ ...source, rootDir: normalizedRootDir });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ---------------------------
|
|
861
|
+
//----------------- PROVIDER SKILL MARKDOWN UTILITIES ------------
|
|
570
862
|
/**
|
|
571
863
|
* Finds direct child skill markdown files under a provider skill root.
|
|
572
864
|
*
|
|
@@ -677,6 +969,98 @@ export function normalizeSessionName(rawValue: string | undefined, fallback: str
|
|
|
677
969
|
return normalized.slice(0, 120);
|
|
678
970
|
}
|
|
679
971
|
|
|
972
|
+
// ---------------------------
|
|
973
|
+
//----------------- PROVIDER SESSION VALUE NORMALIZATION UTILITIES ------------
|
|
974
|
+
/**
|
|
975
|
+
* Converts provider-native timestamps into ISO strings.
|
|
976
|
+
*
|
|
977
|
+
* Provider CLIs commonly persist epoch timestamps as milliseconds, seconds, or
|
|
978
|
+
* already-formatted date strings. Use this helper when normalizing session
|
|
979
|
+
* metadata or transcript events so every provider writes the same ISO timestamp
|
|
980
|
+
* shape to API responses and database rows.
|
|
981
|
+
*/
|
|
982
|
+
export function normalizeProviderTimestamp(value: unknown): string {
|
|
983
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
984
|
+
const millis = value < 1_000_000_000_000 ? value * 1000 : value;
|
|
985
|
+
return new Date(millis).toISOString();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (typeof value === 'string' && value.trim()) {
|
|
989
|
+
const parsed = Number(value);
|
|
990
|
+
if (Number.isFinite(parsed)) {
|
|
991
|
+
return normalizeProviderTimestamp(parsed);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const date = new Date(value);
|
|
995
|
+
if (!Number.isNaN(date.getTime())) {
|
|
996
|
+
return date.toISOString();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return new Date().toISOString();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Parses a JSON string or narrows an existing object into a plain record.
|
|
1005
|
+
*
|
|
1006
|
+
* Use this when provider databases store structured JSON inside text columns.
|
|
1007
|
+
* Invalid JSON, arrays, and primitive values return `null` so callers can skip
|
|
1008
|
+
* malformed optional metadata without hiding the rest of a session transcript.
|
|
1009
|
+
*/
|
|
1010
|
+
export function readJsonRecord(value: unknown): AnyRecord | null {
|
|
1011
|
+
if (typeof value !== 'string') {
|
|
1012
|
+
return readObjectRecord(value);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
return readObjectRecord(JSON.parse(value));
|
|
1017
|
+
} catch {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ---------------------------
|
|
1023
|
+
//----------------- OPENCODE SESSION STORAGE UTILITIES ------------
|
|
1024
|
+
/**
|
|
1025
|
+
* Resolves the OpenCode SQLite session database path.
|
|
1026
|
+
*
|
|
1027
|
+
* OpenCode stores session, message, part, and project metadata in one shared
|
|
1028
|
+
* `opencode.db` file under its XDG data directory. Provider readers and
|
|
1029
|
+
* synchronizers should use this path for read-only access and should never store
|
|
1030
|
+
* it as a deletable transcript path for an individual app session row.
|
|
1031
|
+
*/
|
|
1032
|
+
export function getOpenCodeDatabasePath(): string {
|
|
1033
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ---------------------------
|
|
1037
|
+
//----------------- SAFE DIRECTORY NAME UTILITIES ------------
|
|
1038
|
+
/**
|
|
1039
|
+
* Validates that a user or provider supplied identifier can safely be treated
|
|
1040
|
+
* as one leaf directory name under an existing root folder.
|
|
1041
|
+
*
|
|
1042
|
+
* Use this before composing paths like `<root>/<session-id>/file.db>` to block
|
|
1043
|
+
* path traversal and accidental nested paths. The returned string is trimmed but
|
|
1044
|
+
* otherwise unchanged so callers can still match the provider's on-disk naming.
|
|
1045
|
+
*/
|
|
1046
|
+
export function sanitizeLeafDirectoryName(inputName: string, label = 'directory name'): string {
|
|
1047
|
+
const normalized = inputName.trim();
|
|
1048
|
+
if (!normalized) {
|
|
1049
|
+
throw new Error(`${label} is required.`);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (
|
|
1053
|
+
normalized.includes('..')
|
|
1054
|
+
|| normalized.includes(path.posix.sep)
|
|
1055
|
+
|| normalized.includes(path.win32.sep)
|
|
1056
|
+
|| normalized !== path.basename(normalized)
|
|
1057
|
+
) {
|
|
1058
|
+
throw new Error(`Invalid ${label} "${inputName}".`);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return normalized;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
680
1064
|
// ---------------------------
|
|
681
1065
|
//----------------- SESSION SYNCHRONIZER FILESYSTEM HELPERS ------------
|
|
682
1066
|
/**
|