@cloudcli-ai/cloudcli 1.31.5 → 1.33.0
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-MOIvpy53.css +32 -0
- package/dist/assets/{index-DqMVUeZS.js → index-skXSjBje.js} +290 -270
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +67 -39
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -9
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +163 -35
- 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/database/migrations.js +11 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +1 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/projects/projects.routes.js +12 -3
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
- package/dist-server/server/modules/projects/services/project-delete.service.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 +47 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +1 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +27 -7
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -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-session-synchronizer.provider.js +55 -2
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +4 -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-session-synchronizer.provider.js +54 -2
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +50 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +4 -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-session-synchronizer.provider.js +8 -30
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +187 -24
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +4 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.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-session-synchronizer.provider.js +24 -8
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +10 -10
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +4 -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 +65 -3
- 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-conversations-search.service.js +129 -13
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- 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 +13 -4
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/skills.service.js +11 -0
- package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
- 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 +262 -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 +259 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.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 +87 -29
- 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 +204 -211
- 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/claude-cli-path.js +103 -0
- package/dist-server/server/shared/claude-cli-path.js.map +1 -0
- package/dist-server/server/shared/claude-cli-path.test.js +45 -0
- package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
- package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
- package/dist-server/server/shared/frontmatter.js.map +1 -0
- package/dist-server/server/shared/utils.js +368 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/commandParser.js +3 -3
- package/dist-server/server/utils/commandParser.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 +74 -40
- package/server/cursor-cli.js +6 -11
- package/server/gemini-cli.js +197 -43
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +144 -19
- package/server/modules/database/index.ts +1 -0
- package/server/modules/database/migrations.ts +12 -0
- package/server/modules/database/repositories/projects.db.ts +13 -0
- package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/sessions.db.ts +60 -9
- package/server/modules/database/schema.ts +1 -0
- package/server/modules/projects/projects.routes.ts +20 -3
- package/server/modules/projects/services/project-delete.service.ts +16 -1
- package/server/modules/projects/services/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +71 -1
- package/server/modules/providers/README.md +354 -0
- package/server/modules/providers/index.ts +2 -1
- package/server/modules/providers/list/claude/claude-auth.provider.ts +38 -13
- package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
- package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
- package/server/modules/providers/list/claude/claude.provider.ts +11 -1
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
- package/server/modules/providers/list/codex/codex-skills.provider.ts +60 -0
- package/server/modules/providers/list/codex/codex.provider.ts +11 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -26
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +11 -1
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +10 -11
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
- package/server/modules/providers/list/gemini/gemini.provider.ts +11 -1
- 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 +157 -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 +97 -4
- 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-conversations-search.service.ts +162 -13
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +14 -4
- package/server/modules/providers/services/sessions.service.ts +112 -9
- package/server/modules/providers/services/skills.service.ts +15 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +4 -0
- package/server/modules/providers/shared/skills/skills.provider.ts +64 -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 +334 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +512 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/openai-codex.js +99 -30
- 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 +256 -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/claude-cli-path.test.ts +61 -0
- package/server/shared/claude-cli-path.ts +139 -0
- package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
- package/server/shared/interfaces.ts +64 -0
- package/server/shared/types.ts +166 -1
- package/server/shared/utils.ts +478 -0
- package/server/utils/commandParser.js +5 -3
- package/dist/assets/index-6rYG_mRo.css +0 -32
- package/dist-server/server/utils/frontmatter.js.map +0 -1
- package/dist-server/shared/modelConstants.js +0 -98
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -106
package/server/shared/utils.ts
CHANGED
|
@@ -17,11 +17,18 @@ import readline from 'node:readline';
|
|
|
17
17
|
|
|
18
18
|
import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
19
19
|
|
|
20
|
+
import { parseFrontMatter } from '@/shared/frontmatter.js';
|
|
20
21
|
import type {
|
|
21
22
|
AnyRecord,
|
|
22
23
|
ApiSuccessShape,
|
|
23
24
|
AppErrorOptions,
|
|
25
|
+
LLMProvider,
|
|
24
26
|
NormalizedMessage,
|
|
27
|
+
ProviderChangeActiveModelInput,
|
|
28
|
+
ProviderCurrentActiveModel,
|
|
29
|
+
ProviderModelsDefinition,
|
|
30
|
+
ProviderSessionActiveModelChange,
|
|
31
|
+
ProviderSkillSource,
|
|
25
32
|
WorkspacePathValidationResult,
|
|
26
33
|
} from '@/shared/types.js';
|
|
27
34
|
|
|
@@ -412,6 +419,231 @@ export const readStringRecord = (value: unknown): Record<string, string> | undef
|
|
|
412
419
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
413
420
|
};
|
|
414
421
|
|
|
422
|
+
// ---------------------------
|
|
423
|
+
//----------------- PROVIDER MODEL LOOKUP UTILITIES ------------
|
|
424
|
+
/**
|
|
425
|
+
* Builds the standard "default current model" result used when a provider
|
|
426
|
+
* cannot resolve a session-backed active model.
|
|
427
|
+
*
|
|
428
|
+
* Provider model adapters should call this after loading their supported model
|
|
429
|
+
* catalog so the fallback stays aligned with the provider's current `DEFAULT`
|
|
430
|
+
* selection instead of drifting to a hard-coded duplicate.
|
|
431
|
+
*/
|
|
432
|
+
export function buildDefaultProviderCurrentActiveModel(
|
|
433
|
+
models: ProviderModelsDefinition,
|
|
434
|
+
): ProviderCurrentActiveModel {
|
|
435
|
+
return {
|
|
436
|
+
model: models.DEFAULT,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------
|
|
441
|
+
//----------------- PROVIDER SESSION MODEL CHANGE UTILITIES ------------
|
|
442
|
+
type ProviderSessionActiveModelChangeCacheEntry = ProviderSessionActiveModelChange & {
|
|
443
|
+
updatedAt: string;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
type ProviderSessionActiveModelChangeCacheFile = {
|
|
447
|
+
version: number;
|
|
448
|
+
entries: Record<string, ProviderSessionActiveModelChangeCacheEntry>;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION = 1;
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Resolves the backend-owned cache file used for session-scoped resume model
|
|
455
|
+
* overrides.
|
|
456
|
+
*
|
|
457
|
+
* The file lives under `~/.cloudcli` because these overrides are an application
|
|
458
|
+
* concern rather than a provider-native config file. Providers, routes, and
|
|
459
|
+
* runtime command launchers should all use this helper instead of re-creating
|
|
460
|
+
* the path so the storage location stays consistent.
|
|
461
|
+
*/
|
|
462
|
+
export function getProviderSessionActiveModelChangesPath(): string {
|
|
463
|
+
return path.join(os.homedir(), '.cloudcli', 'provider-session-active-model-changes.json');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const buildProviderSessionActiveModelChangeKey = (
|
|
467
|
+
provider: LLMProvider,
|
|
468
|
+
sessionId: string,
|
|
469
|
+
): string => `${provider}:${sessionId}`;
|
|
470
|
+
|
|
471
|
+
const isProviderSessionActiveModelChangeCacheEntry = (
|
|
472
|
+
value: unknown,
|
|
473
|
+
): value is ProviderSessionActiveModelChangeCacheEntry => {
|
|
474
|
+
const record = readObjectRecord(value);
|
|
475
|
+
return Boolean(
|
|
476
|
+
record
|
|
477
|
+
&& typeof record.provider === 'string'
|
|
478
|
+
&& typeof record.sessionId === 'string'
|
|
479
|
+
&& typeof record.supported === 'boolean'
|
|
480
|
+
&& typeof record.changed === 'boolean'
|
|
481
|
+
&& (typeof record.model === 'string' || record.model === null)
|
|
482
|
+
&& typeof record.updatedAt === 'string',
|
|
483
|
+
);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const readProviderSessionActiveModelChangeCacheFile = async (
|
|
487
|
+
filePath: string,
|
|
488
|
+
): Promise<ProviderSessionActiveModelChangeCacheFile> => {
|
|
489
|
+
try {
|
|
490
|
+
const raw = await readFile(filePath, 'utf8');
|
|
491
|
+
const parsed = readObjectRecord(JSON.parse(raw));
|
|
492
|
+
if (
|
|
493
|
+
!parsed
|
|
494
|
+
|| parsed.version !== PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION
|
|
495
|
+
|| !readObjectRecord(parsed.entries)
|
|
496
|
+
) {
|
|
497
|
+
return {
|
|
498
|
+
version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
|
|
499
|
+
entries: {},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const entries = Object.fromEntries(
|
|
504
|
+
Object.entries(parsed.entries).filter((entry): entry is [string, ProviderSessionActiveModelChangeCacheEntry] =>
|
|
505
|
+
isProviderSessionActiveModelChangeCacheEntry(entry[1]),
|
|
506
|
+
),
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
|
|
511
|
+
entries,
|
|
512
|
+
};
|
|
513
|
+
} catch {
|
|
514
|
+
return {
|
|
515
|
+
version: PROVIDER_SESSION_ACTIVE_MODEL_CHANGE_CACHE_VERSION,
|
|
516
|
+
entries: {},
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const writeProviderSessionActiveModelChangeCacheFile = async (
|
|
522
|
+
filePath: string,
|
|
523
|
+
payload: ProviderSessionActiveModelChangeCacheFile,
|
|
524
|
+
): Promise<void> => {
|
|
525
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
526
|
+
await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const buildUnsupportedProviderSessionActiveModelChange = (
|
|
530
|
+
provider: LLMProvider,
|
|
531
|
+
sessionId: string,
|
|
532
|
+
): ProviderSessionActiveModelChange => ({
|
|
533
|
+
provider,
|
|
534
|
+
sessionId,
|
|
535
|
+
supported: false,
|
|
536
|
+
changed: false,
|
|
537
|
+
model: null,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Reads the persisted session model-change state for one provider session.
|
|
542
|
+
*
|
|
543
|
+
* Runtime resume paths use this to decide whether they should inject a
|
|
544
|
+
* provider-specific model argument/thread option for the next resumed turn.
|
|
545
|
+
* Missing cache entries are normalized to `{ changed: false }` so callers can
|
|
546
|
+
* treat absence as "use the ordinary model selection flow".
|
|
547
|
+
*/
|
|
548
|
+
export async function readProviderSessionActiveModelChange(
|
|
549
|
+
provider: LLMProvider,
|
|
550
|
+
sessionId: string,
|
|
551
|
+
options: {
|
|
552
|
+
filePath?: string;
|
|
553
|
+
supported?: boolean;
|
|
554
|
+
} = {},
|
|
555
|
+
): Promise<ProviderSessionActiveModelChange> {
|
|
556
|
+
const normalizedSessionId = sessionId.trim();
|
|
557
|
+
if (!normalizedSessionId) {
|
|
558
|
+
return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const supported = options.supported ?? true;
|
|
562
|
+
if (!supported) {
|
|
563
|
+
return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const filePath = options.filePath ?? getProviderSessionActiveModelChangesPath();
|
|
567
|
+
const cacheFile = await readProviderSessionActiveModelChangeCacheFile(filePath);
|
|
568
|
+
const cacheEntry = cacheFile.entries[
|
|
569
|
+
buildProviderSessionActiveModelChangeKey(provider, normalizedSessionId)
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
if (!cacheEntry || !cacheEntry.changed || !cacheEntry.model?.trim()) {
|
|
573
|
+
return {
|
|
574
|
+
provider,
|
|
575
|
+
sessionId: normalizedSessionId,
|
|
576
|
+
supported: true,
|
|
577
|
+
changed: false,
|
|
578
|
+
model: null,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
provider,
|
|
584
|
+
sessionId: normalizedSessionId,
|
|
585
|
+
supported: true,
|
|
586
|
+
changed: true,
|
|
587
|
+
model: cacheEntry.model.trim(),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Persists a session model-change request for one provider.
|
|
593
|
+
*
|
|
594
|
+
* Provider adapters call this when the frontend explicitly selects a different
|
|
595
|
+
* model for an existing session. The stored `changed: true` flag is the single
|
|
596
|
+
* source of truth used later by resume paths to decide whether they should add
|
|
597
|
+
* a provider-native model override on the next invocation.
|
|
598
|
+
*/
|
|
599
|
+
export async function writeProviderSessionActiveModelChange(
|
|
600
|
+
provider: LLMProvider,
|
|
601
|
+
input: ProviderChangeActiveModelInput,
|
|
602
|
+
options: {
|
|
603
|
+
filePath?: string;
|
|
604
|
+
supported?: boolean;
|
|
605
|
+
} = {},
|
|
606
|
+
): Promise<ProviderSessionActiveModelChange> {
|
|
607
|
+
const normalizedSessionId = input.sessionId.trim();
|
|
608
|
+
const normalizedModel = input.model.trim();
|
|
609
|
+
const supported = options.supported ?? true;
|
|
610
|
+
|
|
611
|
+
if (!supported) {
|
|
612
|
+
return buildUnsupportedProviderSessionActiveModelChange(provider, normalizedSessionId);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!normalizedSessionId || !normalizedModel) {
|
|
616
|
+
return {
|
|
617
|
+
provider,
|
|
618
|
+
sessionId: normalizedSessionId,
|
|
619
|
+
supported: true,
|
|
620
|
+
changed: false,
|
|
621
|
+
model: null,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const filePath = options.filePath ?? getProviderSessionActiveModelChangesPath();
|
|
626
|
+
const cacheFile = await readProviderSessionActiveModelChangeCacheFile(filePath);
|
|
627
|
+
cacheFile.entries[buildProviderSessionActiveModelChangeKey(provider, normalizedSessionId)] = {
|
|
628
|
+
provider,
|
|
629
|
+
sessionId: normalizedSessionId,
|
|
630
|
+
supported: true,
|
|
631
|
+
changed: true,
|
|
632
|
+
model: normalizedModel,
|
|
633
|
+
updatedAt: new Date().toISOString(),
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
await writeProviderSessionActiveModelChangeCacheFile(filePath, cacheFile);
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
provider,
|
|
640
|
+
sessionId: normalizedSessionId,
|
|
641
|
+
supported: true,
|
|
642
|
+
changed: true,
|
|
643
|
+
model: normalizedModel,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
415
647
|
// ---------------------------
|
|
416
648
|
//----------------- WEBSOCKET PAYLOAD PARSING UTILITIES ------------
|
|
417
649
|
/**
|
|
@@ -503,6 +735,160 @@ export const writeJsonConfig = async (filePath: string, data: Record<string, unk
|
|
|
503
735
|
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
504
736
|
};
|
|
505
737
|
|
|
738
|
+
// ---------------------------
|
|
739
|
+
//----------------- PROVIDER SKILL FILE UTILITIES ------------
|
|
740
|
+
async function hasGitMarker(dirPath: string): Promise<boolean> {
|
|
741
|
+
try {
|
|
742
|
+
const gitMarkerStats = await stat(path.join(dirPath, '.git'));
|
|
743
|
+
return gitMarkerStats.isDirectory() || gitMarkerStats.isFile();
|
|
744
|
+
} catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Finds the highest git worktree root visible from a starting directory.
|
|
751
|
+
*
|
|
752
|
+
* Provider skill systems such as Codex and OpenCode walk upward through parent
|
|
753
|
+
* folders when resolving repository/project skills. Use this helper when a
|
|
754
|
+
* provider needs the topmost `.git` marker instead of only the nearest one, so
|
|
755
|
+
* monorepos and nested package folders discover shared root-level skills once.
|
|
756
|
+
*/
|
|
757
|
+
export async function findTopmostGitRoot(startPath: string): Promise<string | null> {
|
|
758
|
+
let currentPath = path.resolve(startPath);
|
|
759
|
+
let topmostGitRoot: string | null = null;
|
|
760
|
+
|
|
761
|
+
while (true) {
|
|
762
|
+
if (await hasGitMarker(currentPath)) {
|
|
763
|
+
topmostGitRoot = currentPath;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const parentPath = path.dirname(currentPath);
|
|
767
|
+
if (parentPath === currentPath) {
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
currentPath = parentPath;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return topmostGitRoot;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Adds one provider skill source after normalizing and de-duplicating its root.
|
|
779
|
+
*
|
|
780
|
+
* Provider skill lookup rules often point at overlapping folders (for example a
|
|
781
|
+
* workspace folder can also be the git root). Use this helper while building a
|
|
782
|
+
* provider's `ProviderSkillSource[]` so the shared skills scanner reads each
|
|
783
|
+
* physical root once and still preserves provider-specific scope/command data.
|
|
784
|
+
*/
|
|
785
|
+
export function addUniqueProviderSkillSource(
|
|
786
|
+
sources: ProviderSkillSource[],
|
|
787
|
+
seenRootDirs: Set<string>,
|
|
788
|
+
source: ProviderSkillSource,
|
|
789
|
+
): void {
|
|
790
|
+
const normalizedRootDir = path.resolve(source.rootDir);
|
|
791
|
+
if (seenRootDirs.has(normalizedRootDir)) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
seenRootDirs.add(normalizedRootDir);
|
|
796
|
+
sources.push({ ...source, rootDir: normalizedRootDir });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ---------------------------
|
|
800
|
+
//----------------- PROVIDER SKILL MARKDOWN UTILITIES ------------
|
|
801
|
+
/**
|
|
802
|
+
* Finds direct child skill markdown files under a provider skill root.
|
|
803
|
+
*
|
|
804
|
+
* Skill systems usually store one skill per child directory, so direct mode
|
|
805
|
+
* scans only `<root>/<skill-name>/SKILL.md`. Recursive mode is reserved for
|
|
806
|
+
* provider sources that can nest skills arbitrarily, and it returns every
|
|
807
|
+
* descendant `SKILL.md`. Missing or unreadable roots return an empty list
|
|
808
|
+
* because users may not have every provider installed or configured.
|
|
809
|
+
*/
|
|
810
|
+
export async function findProviderSkillMarkdownFiles(
|
|
811
|
+
rootDir: string,
|
|
812
|
+
options: { recursive?: boolean } = {},
|
|
813
|
+
): Promise<string[]> {
|
|
814
|
+
const skillFiles: string[] = [];
|
|
815
|
+
|
|
816
|
+
const collectRecursive = async (dirPath: string): Promise<void> => {
|
|
817
|
+
let entries;
|
|
818
|
+
try {
|
|
819
|
+
entries = await readdir(dirPath, { withFileTypes: true });
|
|
820
|
+
} catch {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const skillPath = path.join(dirPath, 'SKILL.md');
|
|
826
|
+
const skillStats = await stat(skillPath);
|
|
827
|
+
if (skillStats.isFile()) {
|
|
828
|
+
skillFiles.push(skillPath);
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
// Directories without SKILL.md are expected while walking plugin trees.
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
for (const entry of entries) {
|
|
835
|
+
if (entry.isDirectory()) {
|
|
836
|
+
await collectRecursive(path.join(dirPath, entry.name));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
if (options.recursive) {
|
|
842
|
+
await collectRecursive(rootDir);
|
|
843
|
+
return skillFiles.sort((left, right) => left.localeCompare(right));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
848
|
+
|
|
849
|
+
for (const entry of entries) {
|
|
850
|
+
if (!entry.isDirectory()) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const skillPath = path.join(rootDir, entry.name, 'SKILL.md');
|
|
855
|
+
try {
|
|
856
|
+
const skillStats = await stat(skillPath);
|
|
857
|
+
if (skillStats.isFile()) {
|
|
858
|
+
skillFiles.push(skillPath);
|
|
859
|
+
}
|
|
860
|
+
} catch {
|
|
861
|
+
// A partial skill directory should not block discovery of sibling skills.
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return skillFiles.sort((left, right) => left.localeCompare(right));
|
|
866
|
+
} catch {
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Reads the `name` and `description` fields from a provider skill markdown file.
|
|
873
|
+
*
|
|
874
|
+
* The metadata is expected in markdown front matter. If a skill omits `name`, the
|
|
875
|
+
* parent directory name is used as a stable fallback so providers can still
|
|
876
|
+
* expose the skill. Missing descriptions are normalized to an empty string.
|
|
877
|
+
*/
|
|
878
|
+
export async function readProviderSkillMarkdownDefinition(
|
|
879
|
+
skillPath: string,
|
|
880
|
+
): Promise<{ name: string; description: string }> {
|
|
881
|
+
const content = await readFile(skillPath, 'utf8');
|
|
882
|
+
const parsed = parseFrontMatter(content);
|
|
883
|
+
const data = readObjectRecord(parsed.data) ?? {};
|
|
884
|
+
const fallbackName = path.basename(path.dirname(skillPath));
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
name: readOptionalString(data.name) ?? fallbackName,
|
|
888
|
+
description: readOptionalString(data.description) ?? '',
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
506
892
|
// ---------------------------
|
|
507
893
|
//----------------- SESSION SYNCHRONIZER TITLE HELPERS ------------
|
|
508
894
|
/**
|
|
@@ -522,6 +908,98 @@ export function normalizeSessionName(rawValue: string | undefined, fallback: str
|
|
|
522
908
|
return normalized.slice(0, 120);
|
|
523
909
|
}
|
|
524
910
|
|
|
911
|
+
// ---------------------------
|
|
912
|
+
//----------------- PROVIDER SESSION VALUE NORMALIZATION UTILITIES ------------
|
|
913
|
+
/**
|
|
914
|
+
* Converts provider-native timestamps into ISO strings.
|
|
915
|
+
*
|
|
916
|
+
* Provider CLIs commonly persist epoch timestamps as milliseconds, seconds, or
|
|
917
|
+
* already-formatted date strings. Use this helper when normalizing session
|
|
918
|
+
* metadata or transcript events so every provider writes the same ISO timestamp
|
|
919
|
+
* shape to API responses and database rows.
|
|
920
|
+
*/
|
|
921
|
+
export function normalizeProviderTimestamp(value: unknown): string {
|
|
922
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
923
|
+
const millis = value < 1_000_000_000_000 ? value * 1000 : value;
|
|
924
|
+
return new Date(millis).toISOString();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (typeof value === 'string' && value.trim()) {
|
|
928
|
+
const parsed = Number(value);
|
|
929
|
+
if (Number.isFinite(parsed)) {
|
|
930
|
+
return normalizeProviderTimestamp(parsed);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const date = new Date(value);
|
|
934
|
+
if (!Number.isNaN(date.getTime())) {
|
|
935
|
+
return date.toISOString();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return new Date().toISOString();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Parses a JSON string or narrows an existing object into a plain record.
|
|
944
|
+
*
|
|
945
|
+
* Use this when provider databases store structured JSON inside text columns.
|
|
946
|
+
* Invalid JSON, arrays, and primitive values return `null` so callers can skip
|
|
947
|
+
* malformed optional metadata without hiding the rest of a session transcript.
|
|
948
|
+
*/
|
|
949
|
+
export function readJsonRecord(value: unknown): AnyRecord | null {
|
|
950
|
+
if (typeof value !== 'string') {
|
|
951
|
+
return readObjectRecord(value);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
try {
|
|
955
|
+
return readObjectRecord(JSON.parse(value));
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// ---------------------------
|
|
962
|
+
//----------------- OPENCODE SESSION STORAGE UTILITIES ------------
|
|
963
|
+
/**
|
|
964
|
+
* Resolves the OpenCode SQLite session database path.
|
|
965
|
+
*
|
|
966
|
+
* OpenCode stores session, message, part, and project metadata in one shared
|
|
967
|
+
* `opencode.db` file under its XDG data directory. Provider readers and
|
|
968
|
+
* synchronizers should use this path for read-only access and should never store
|
|
969
|
+
* it as a deletable transcript path for an individual app session row.
|
|
970
|
+
*/
|
|
971
|
+
export function getOpenCodeDatabasePath(): string {
|
|
972
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ---------------------------
|
|
976
|
+
//----------------- SAFE DIRECTORY NAME UTILITIES ------------
|
|
977
|
+
/**
|
|
978
|
+
* Validates that a user or provider supplied identifier can safely be treated
|
|
979
|
+
* as one leaf directory name under an existing root folder.
|
|
980
|
+
*
|
|
981
|
+
* Use this before composing paths like `<root>/<session-id>/file.db>` to block
|
|
982
|
+
* path traversal and accidental nested paths. The returned string is trimmed but
|
|
983
|
+
* otherwise unchanged so callers can still match the provider's on-disk naming.
|
|
984
|
+
*/
|
|
985
|
+
export function sanitizeLeafDirectoryName(inputName: string, label = 'directory name'): string {
|
|
986
|
+
const normalized = inputName.trim();
|
|
987
|
+
if (!normalized) {
|
|
988
|
+
throw new Error(`${label} is required.`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (
|
|
992
|
+
normalized.includes('..')
|
|
993
|
+
|| normalized.includes(path.posix.sep)
|
|
994
|
+
|| normalized.includes(path.win32.sep)
|
|
995
|
+
|| normalized !== path.basename(normalized)
|
|
996
|
+
) {
|
|
997
|
+
throw new Error(`Invalid ${label} "${inputName}".`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return normalized;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
525
1003
|
// ---------------------------
|
|
526
1004
|
//----------------- SESSION SYNCHRONIZER FILESYSTEM HELPERS ------------
|
|
527
1005
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
1
2
|
import { promises as fs } from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
|
-
import { execFile } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
+
|
|
5
6
|
import { parse as parseShellCommand } from 'shell-quote';
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
import { parseFrontMatter } from '../shared/frontmatter.js';
|
|
7
9
|
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
|
|
@@ -32,7 +34,7 @@ const BASH_COMMAND_ALLOWLIST = [
|
|
|
32
34
|
*/
|
|
33
35
|
export function parseCommand(content) {
|
|
34
36
|
try {
|
|
35
|
-
const parsed =
|
|
37
|
+
const parsed = parseFrontMatter(content);
|
|
36
38
|
return {
|
|
37
39
|
data: parsed.data || {},
|
|
38
40
|
content: parsed.content || '',
|