@dexto/agent-management 1.3.0 → 1.5.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.
Files changed (94) hide show
  1. package/dist/AgentFactory.cjs +152 -0
  2. package/dist/AgentFactory.d.ts +121 -0
  3. package/dist/AgentFactory.d.ts.map +1 -0
  4. package/dist/AgentFactory.js +132 -0
  5. package/dist/AgentManager.cjs +226 -0
  6. package/dist/AgentManager.d.ts +191 -0
  7. package/dist/AgentManager.d.ts.map +1 -0
  8. package/dist/AgentManager.js +192 -0
  9. package/dist/config/config-enrichment.cjs +23 -3
  10. package/dist/config/config-enrichment.d.ts +20 -5
  11. package/dist/config/config-enrichment.d.ts.map +1 -1
  12. package/dist/config/config-enrichment.js +22 -3
  13. package/dist/config/config-manager.cjs +340 -3
  14. package/dist/config/config-manager.d.ts +158 -7
  15. package/dist/config/config-manager.d.ts.map +1 -1
  16. package/dist/config/config-manager.js +325 -3
  17. package/dist/config/discover-prompts.cjs +103 -0
  18. package/dist/config/discover-prompts.d.ts +28 -0
  19. package/dist/config/discover-prompts.d.ts.map +1 -0
  20. package/dist/config/discover-prompts.js +73 -0
  21. package/dist/config/errors.cjs +2 -2
  22. package/dist/config/errors.js +2 -2
  23. package/dist/config/index.cjs +14 -2
  24. package/dist/config/index.d.ts +2 -2
  25. package/dist/config/index.d.ts.map +1 -1
  26. package/dist/config/index.js +21 -3
  27. package/dist/index.cjs +109 -6
  28. package/dist/index.d.ts +9 -6
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +111 -6
  31. package/dist/installation.cjs +239 -0
  32. package/dist/installation.d.ts +72 -0
  33. package/dist/installation.d.ts.map +1 -0
  34. package/dist/installation.js +202 -0
  35. package/dist/models/custom-models.cjs +157 -0
  36. package/dist/models/custom-models.d.ts +94 -0
  37. package/dist/models/custom-models.d.ts.map +1 -0
  38. package/dist/models/custom-models.js +117 -0
  39. package/dist/models/index.cjs +89 -0
  40. package/dist/models/index.d.ts +11 -0
  41. package/dist/models/index.d.ts.map +1 -0
  42. package/dist/models/index.js +68 -0
  43. package/dist/models/path-resolver.cjs +154 -0
  44. package/dist/models/path-resolver.d.ts +77 -0
  45. package/dist/models/path-resolver.d.ts.map +1 -0
  46. package/dist/models/path-resolver.js +108 -0
  47. package/dist/models/state-manager.cjs +220 -0
  48. package/dist/models/state-manager.d.ts +138 -0
  49. package/dist/models/state-manager.d.ts.map +1 -0
  50. package/dist/models/state-manager.js +184 -0
  51. package/dist/preferences/error-codes.cjs +2 -0
  52. package/dist/preferences/error-codes.d.ts +3 -1
  53. package/dist/preferences/error-codes.d.ts.map +1 -1
  54. package/dist/preferences/error-codes.js +2 -0
  55. package/dist/preferences/index.d.ts +1 -1
  56. package/dist/preferences/index.d.ts.map +1 -1
  57. package/dist/preferences/loader.cjs +32 -6
  58. package/dist/preferences/loader.d.ts +23 -4
  59. package/dist/preferences/loader.d.ts.map +1 -1
  60. package/dist/preferences/loader.js +32 -6
  61. package/dist/preferences/schemas.cjs +21 -3
  62. package/dist/preferences/schemas.d.ts +52 -24
  63. package/dist/preferences/schemas.d.ts.map +1 -1
  64. package/dist/preferences/schemas.js +28 -4
  65. package/dist/registry/registry.cjs +28 -45
  66. package/dist/registry/registry.d.ts +8 -6
  67. package/dist/registry/registry.d.ts.map +1 -1
  68. package/dist/registry/registry.js +26 -44
  69. package/dist/registry/types.d.ts +11 -13
  70. package/dist/registry/types.d.ts.map +1 -1
  71. package/dist/resolver.cjs +82 -43
  72. package/dist/resolver.d.ts +7 -5
  73. package/dist/resolver.d.ts.map +1 -1
  74. package/dist/resolver.js +83 -44
  75. package/dist/utils/api-key-resolver.cjs +19 -1
  76. package/dist/utils/api-key-resolver.d.ts.map +1 -1
  77. package/dist/utils/api-key-resolver.js +19 -1
  78. package/dist/utils/api-key-store.cjs +46 -0
  79. package/dist/utils/api-key-store.d.ts +27 -0
  80. package/dist/utils/api-key-store.d.ts.map +1 -1
  81. package/dist/utils/api-key-store.js +44 -0
  82. package/dist/utils/env-file.cjs +20 -68
  83. package/dist/utils/env-file.d.ts +2 -1
  84. package/dist/utils/env-file.d.ts.map +1 -1
  85. package/dist/utils/env-file.js +20 -68
  86. package/dist/writer.cjs +20 -2
  87. package/dist/writer.d.ts +1 -0
  88. package/dist/writer.d.ts.map +1 -1
  89. package/dist/writer.js +20 -2
  90. package/package.json +2 -2
  91. package/dist/AgentOrchestrator.cjs +0 -263
  92. package/dist/AgentOrchestrator.d.ts +0 -191
  93. package/dist/AgentOrchestrator.d.ts.map +0 -1
  94. package/dist/AgentOrchestrator.js +0 -239
@@ -0,0 +1,72 @@
1
+ import type { AgentMetadata } from './AgentManager.js';
2
+ export interface InstallOptions {
3
+ /** Directory where agents are stored (default: ~/.dexto/agents) */
4
+ agentsDir?: string;
5
+ }
6
+ /**
7
+ * Install agent from bundled registry to local directory
8
+ *
9
+ * @param agentId ID of the agent to install from bundled registry
10
+ * @param options Installation options
11
+ * @returns Path to the installed agent's main config file
12
+ *
13
+ * @throws {DextoRuntimeError} If agent not found in bundled registry or installation fails
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * await installBundledAgent('coding-agent');
18
+ * console.log('Agent installed to ~/.dexto/agents/coding-agent');
19
+ * ```
20
+ */
21
+ export declare function installBundledAgent(agentId: string, options?: InstallOptions): Promise<string>;
22
+ /**
23
+ * Install custom agent from local path
24
+ *
25
+ * @param agentId Unique ID for the custom agent
26
+ * @param sourcePath Absolute path to agent YAML file or directory
27
+ * @param metadata Agent metadata (name, description, author, tags)
28
+ * @param options Installation options
29
+ * @returns Path to the installed agent's main config file
30
+ *
31
+ * @throws {DextoRuntimeError} If agent ID already exists or installation fails
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * await installCustomAgent('my-agent', '/path/to/agent.yml', {
36
+ * name: 'My Agent',
37
+ * description: 'Custom agent for my use case',
38
+ * author: 'John Doe',
39
+ * tags: ['custom']
40
+ * });
41
+ * ```
42
+ */
43
+ export declare function installCustomAgent(agentId: string, sourcePath: string, metadata: Pick<AgentMetadata, 'name' | 'description' | 'author' | 'tags'>, options?: InstallOptions): Promise<string>;
44
+ /**
45
+ * Uninstall agent by removing it from disk and user registry
46
+ *
47
+ * @param agentId ID of the agent to uninstall
48
+ * @param options Installation options
49
+ *
50
+ * @throws {DextoRuntimeError} If agent not installed
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * await uninstallAgent('my-custom-agent');
55
+ * console.log('Agent uninstalled');
56
+ * ```
57
+ */
58
+ export declare function uninstallAgent(agentId: string, options?: InstallOptions): Promise<void>;
59
+ /**
60
+ * List installed agents
61
+ *
62
+ * @param options Installation options
63
+ * @returns Array of installed agent IDs
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const installed = await listInstalledAgents();
68
+ * console.log(installed); // ['coding-agent', 'my-custom-agent']
69
+ * ```
70
+ */
71
+ export declare function listInstalledAgents(options?: InstallOptions): Promise<string[]>;
72
+ //# sourceMappingURL=installation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installation.d.ts","sourceRoot":"","sources":["../src/installation.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,cAAc;IAC3B,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAuCD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,MAAM,CAAC,CA6FjB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,kBAAkB,CACpC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC,EACzE,OAAO,CAAC,EAAE,cAAc,GACzB,OAAO,CAAC,MAAM,CAAC,CA2FjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B7F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYrF"}
@@ -0,0 +1,202 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ import { logger } from "@dexto/core";
4
+ import { getDextoGlobalPath, resolveBundledScript, copyDirectory } from "./utils/path.js";
5
+ import { RegistryError } from "./registry/errors.js";
6
+ import { ConfigError } from "./config/errors.js";
7
+ function getAgentsDir(options) {
8
+ return options?.agentsDir ?? getDextoGlobalPath("agents");
9
+ }
10
+ function getUserRegistryPath(agentsDir) {
11
+ return path.join(agentsDir, "registry.json");
12
+ }
13
+ async function loadUserRegistry(registryPath) {
14
+ try {
15
+ const content = await fs.readFile(registryPath, "utf-8");
16
+ return JSON.parse(content);
17
+ } catch (error) {
18
+ if (error.code === "ENOENT") {
19
+ return { agents: [] };
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+ async function saveUserRegistry(registryPath, registry) {
25
+ await fs.mkdir(path.dirname(registryPath), { recursive: true });
26
+ await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
27
+ }
28
+ async function installBundledAgent(agentId, options) {
29
+ const agentsDir = getAgentsDir(options);
30
+ const bundledRegistryPath = resolveBundledScript("agents/agent-registry.json");
31
+ logger.info(`Installing agent: ${agentId}`);
32
+ let bundledRegistry;
33
+ try {
34
+ const content = await fs.readFile(bundledRegistryPath, "utf-8");
35
+ bundledRegistry = JSON.parse(content);
36
+ } catch (error) {
37
+ throw RegistryError.registryParseError(
38
+ bundledRegistryPath,
39
+ error instanceof Error ? error.message : String(error)
40
+ );
41
+ }
42
+ const agentEntry = bundledRegistry.agents[agentId];
43
+ if (!agentEntry) {
44
+ const available = Object.keys(bundledRegistry.agents);
45
+ throw RegistryError.agentNotFound(agentId, available);
46
+ }
47
+ const targetDir = path.join(agentsDir, agentId);
48
+ try {
49
+ await fs.access(targetDir);
50
+ logger.info(`Agent '${agentId}' already installed`);
51
+ const mainFile = agentEntry.main || path.basename(agentEntry.source);
52
+ return path.join(targetDir, mainFile);
53
+ } catch {
54
+ }
55
+ await fs.mkdir(agentsDir, { recursive: true });
56
+ const sourcePath = resolveBundledScript(`agents/${agentEntry.source}`);
57
+ const tempDir = `${targetDir}.tmp.${Date.now()}`;
58
+ try {
59
+ if (agentEntry.source.endsWith("/")) {
60
+ await copyDirectory(sourcePath, tempDir);
61
+ } else {
62
+ await fs.mkdir(tempDir, { recursive: true });
63
+ const targetFile = path.join(tempDir, path.basename(sourcePath));
64
+ await fs.copyFile(sourcePath, targetFile);
65
+ }
66
+ await fs.rename(tempDir, targetDir);
67
+ logger.info(`\u2713 Installed agent '${agentId}' to ${targetDir}`);
68
+ const userRegistryPath = getUserRegistryPath(agentsDir);
69
+ const userRegistry = await loadUserRegistry(userRegistryPath);
70
+ if (!userRegistry.agents.some((a) => a.id === agentId)) {
71
+ const mainFile = agentEntry.main || path.basename(agentEntry.source);
72
+ userRegistry.agents.push({
73
+ id: agentId,
74
+ name: agentEntry.name,
75
+ description: agentEntry.description,
76
+ configPath: `./${agentId}/${mainFile}`,
77
+ author: agentEntry.author,
78
+ tags: agentEntry.tags
79
+ });
80
+ await saveUserRegistry(userRegistryPath, userRegistry);
81
+ }
82
+ return path.join(targetDir, agentEntry.main || path.basename(agentEntry.source));
83
+ } catch (error) {
84
+ try {
85
+ await fs.rm(tempDir, { recursive: true, force: true });
86
+ } catch {
87
+ }
88
+ throw RegistryError.installationFailed(
89
+ agentId,
90
+ error instanceof Error ? error.message : String(error)
91
+ );
92
+ }
93
+ }
94
+ async function installCustomAgent(agentId, sourcePath, metadata, options) {
95
+ const agentsDir = getAgentsDir(options);
96
+ const targetDir = path.join(agentsDir, agentId);
97
+ logger.info(`Installing custom agent: ${agentId}`);
98
+ try {
99
+ const bundledRegistryPath = resolveBundledScript("agents/agent-registry.json");
100
+ const bundledContent = await fs.readFile(bundledRegistryPath, "utf-8");
101
+ const bundledRegistry = JSON.parse(bundledContent);
102
+ if (agentId in bundledRegistry.agents) {
103
+ throw RegistryError.customAgentNameConflict(agentId);
104
+ }
105
+ } catch (error) {
106
+ if (error instanceof Error && error.message.includes("conflicts with builtin")) {
107
+ throw error;
108
+ }
109
+ logger.debug(
110
+ `Could not validate against bundled registry: ${error instanceof Error ? error.message : String(error)}`
111
+ );
112
+ }
113
+ try {
114
+ await fs.access(targetDir);
115
+ throw RegistryError.agentAlreadyExists(agentId);
116
+ } catch (error) {
117
+ if (error.code !== "ENOENT") {
118
+ throw error;
119
+ }
120
+ }
121
+ const resolvedSource = path.resolve(sourcePath);
122
+ let stat;
123
+ try {
124
+ stat = await fs.stat(resolvedSource);
125
+ } catch (_error) {
126
+ throw ConfigError.fileNotFound(resolvedSource);
127
+ }
128
+ await fs.mkdir(agentsDir, { recursive: true });
129
+ try {
130
+ if (stat.isDirectory()) {
131
+ await copyDirectory(resolvedSource, targetDir);
132
+ } else {
133
+ await fs.mkdir(targetDir, { recursive: true });
134
+ const filename = path.basename(resolvedSource);
135
+ await fs.copyFile(resolvedSource, path.join(targetDir, filename));
136
+ }
137
+ logger.info(`\u2713 Installed custom agent '${agentId}' to ${targetDir}`);
138
+ const userRegistryPath = getUserRegistryPath(agentsDir);
139
+ const userRegistry = await loadUserRegistry(userRegistryPath);
140
+ const configFile = stat.isDirectory() ? "agent.yml" : path.basename(resolvedSource);
141
+ userRegistry.agents.push({
142
+ id: agentId,
143
+ name: metadata.name || agentId,
144
+ description: metadata.description,
145
+ configPath: `./${agentId}/${configFile}`,
146
+ author: metadata.author,
147
+ tags: metadata.tags || []
148
+ });
149
+ await saveUserRegistry(userRegistryPath, userRegistry);
150
+ return path.join(targetDir, configFile);
151
+ } catch (error) {
152
+ try {
153
+ await fs.rm(targetDir, { recursive: true, force: true });
154
+ } catch {
155
+ }
156
+ throw RegistryError.installationFailed(
157
+ agentId,
158
+ error instanceof Error ? error.message : String(error)
159
+ );
160
+ }
161
+ }
162
+ async function uninstallAgent(agentId, options) {
163
+ const agentsDir = getAgentsDir(options);
164
+ const targetDir = path.join(agentsDir, agentId);
165
+ logger.info(`Uninstalling agent: ${agentId}`);
166
+ try {
167
+ await fs.access(targetDir);
168
+ } catch (_error) {
169
+ throw RegistryError.agentNotInstalled(agentId);
170
+ }
171
+ await fs.rm(targetDir, { recursive: true, force: true });
172
+ logger.info(`\u2713 Removed agent directory: ${targetDir}`);
173
+ const userRegistryPath = getUserRegistryPath(agentsDir);
174
+ try {
175
+ const userRegistry = await loadUserRegistry(userRegistryPath);
176
+ userRegistry.agents = userRegistry.agents.filter((a) => a.id !== agentId);
177
+ await saveUserRegistry(userRegistryPath, userRegistry);
178
+ logger.info(`\u2713 Removed '${agentId}' from user registry`);
179
+ } catch (error) {
180
+ logger.warn(
181
+ `Failed to update user registry: ${error instanceof Error ? error.message : String(error)}`
182
+ );
183
+ }
184
+ }
185
+ async function listInstalledAgents(options) {
186
+ const agentsDir = getAgentsDir(options);
187
+ try {
188
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
189
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
190
+ } catch (error) {
191
+ if (error.code === "ENOENT") {
192
+ return [];
193
+ }
194
+ throw error;
195
+ }
196
+ }
197
+ export {
198
+ installBundledAgent,
199
+ installCustomAgent,
200
+ listInstalledAgents,
201
+ uninstallAgent
202
+ };
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var custom_models_exports = {};
30
+ __export(custom_models_exports, {
31
+ CUSTOM_MODEL_PROVIDERS: () => CUSTOM_MODEL_PROVIDERS,
32
+ CustomModelSchema: () => CustomModelSchema,
33
+ deleteCustomModel: () => deleteCustomModel,
34
+ getCustomModel: () => getCustomModel,
35
+ getCustomModelsPath: () => getCustomModelsPath,
36
+ loadCustomModels: () => loadCustomModels,
37
+ saveCustomModel: () => saveCustomModel
38
+ });
39
+ module.exports = __toCommonJS(custom_models_exports);
40
+ var import_zod = require("zod");
41
+ var import_fs = require("fs");
42
+ var path = __toESM(require("path"), 1);
43
+ var import_path = require("../utils/path.js");
44
+ const CUSTOM_MODEL_PROVIDERS = [
45
+ "openai-compatible",
46
+ "openrouter",
47
+ "litellm",
48
+ "glama",
49
+ "bedrock",
50
+ "ollama",
51
+ "local",
52
+ "vertex"
53
+ ];
54
+ const CustomModelSchema = import_zod.z.object({
55
+ name: import_zod.z.string().min(1),
56
+ provider: import_zod.z.enum(CUSTOM_MODEL_PROVIDERS).default("openai-compatible"),
57
+ baseURL: import_zod.z.string().url().optional(),
58
+ displayName: import_zod.z.string().optional(),
59
+ maxInputTokens: import_zod.z.number().int().positive().optional(),
60
+ maxOutputTokens: import_zod.z.number().int().positive().optional(),
61
+ // Optional per-model API key. For openai-compatible this is the primary key source.
62
+ // For litellm/glama/openrouter this overrides the provider-level env var key.
63
+ apiKey: import_zod.z.string().optional(),
64
+ // File path for local GGUF models. Required when provider is 'local'.
65
+ // Stores the absolute path to the .gguf file on disk.
66
+ filePath: import_zod.z.string().optional()
67
+ }).superRefine((data, ctx) => {
68
+ if ((data.provider === "openai-compatible" || data.provider === "litellm") && !data.baseURL) {
69
+ ctx.addIssue({
70
+ code: import_zod.z.ZodIssueCode.custom,
71
+ path: ["baseURL"],
72
+ message: `Base URL is required for ${data.provider} provider`
73
+ });
74
+ }
75
+ if (data.provider === "local" && !data.filePath) {
76
+ ctx.addIssue({
77
+ code: import_zod.z.ZodIssueCode.custom,
78
+ path: ["filePath"],
79
+ message: "File path is required for local provider"
80
+ });
81
+ }
82
+ if (data.provider === "local" && data.filePath && !data.filePath.endsWith(".gguf")) {
83
+ ctx.addIssue({
84
+ code: import_zod.z.ZodIssueCode.custom,
85
+ path: ["filePath"],
86
+ message: "File path must be a .gguf file"
87
+ });
88
+ }
89
+ });
90
+ const StorageSchema = import_zod.z.object({
91
+ version: import_zod.z.literal(1),
92
+ models: import_zod.z.array(CustomModelSchema)
93
+ });
94
+ function getCustomModelsPath() {
95
+ return (0, import_path.getDextoGlobalPath)("models", "custom-models.json");
96
+ }
97
+ async function loadCustomModels() {
98
+ const filePath = getCustomModelsPath();
99
+ try {
100
+ const content = await import_fs.promises.readFile(filePath, "utf-8");
101
+ const parsed = StorageSchema.safeParse(JSON.parse(content));
102
+ if (!parsed.success) {
103
+ console.warn(
104
+ `[custom-models] Failed to parse ${filePath}: ${parsed.error.issues.map((i) => i.message).join(", ")}`
105
+ );
106
+ return [];
107
+ }
108
+ return parsed.data.models;
109
+ } catch (error) {
110
+ if (error.code === "ENOENT") {
111
+ return [];
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+ async function saveCustomModel(model) {
117
+ const parsed = CustomModelSchema.safeParse(model);
118
+ if (!parsed.success) {
119
+ throw new Error(`Invalid model: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
120
+ }
121
+ const models = await loadCustomModels();
122
+ const existingIndex = models.findIndex((m) => m.name === parsed.data.name);
123
+ if (existingIndex >= 0) {
124
+ models[existingIndex] = parsed.data;
125
+ } else {
126
+ models.push(parsed.data);
127
+ }
128
+ await writeCustomModels(models);
129
+ }
130
+ async function deleteCustomModel(name) {
131
+ const models = await loadCustomModels();
132
+ const filtered = models.filter((m) => m.name !== name);
133
+ if (filtered.length === models.length) {
134
+ return false;
135
+ }
136
+ await writeCustomModels(filtered);
137
+ return true;
138
+ }
139
+ async function getCustomModel(name) {
140
+ const models = await loadCustomModels();
141
+ return models.find((m) => m.name === name) ?? null;
142
+ }
143
+ async function writeCustomModels(models) {
144
+ const filePath = getCustomModelsPath();
145
+ await import_fs.promises.mkdir(path.dirname(filePath), { recursive: true });
146
+ await import_fs.promises.writeFile(filePath, JSON.stringify({ version: 1, models }, null, 2), "utf-8");
147
+ }
148
+ // Annotate the CommonJS export names for ESM import in node:
149
+ 0 && (module.exports = {
150
+ CUSTOM_MODEL_PROVIDERS,
151
+ CustomModelSchema,
152
+ deleteCustomModel,
153
+ getCustomModel,
154
+ getCustomModelsPath,
155
+ loadCustomModels,
156
+ saveCustomModel
157
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Custom Models Persistence
3
+ *
4
+ * Manages saved custom model configurations for openai-compatible and openrouter providers.
5
+ * Stored in ~/.dexto/models/custom-models.json
6
+ */
7
+ import { z } from 'zod';
8
+ /** Providers that support custom models */
9
+ export declare const CUSTOM_MODEL_PROVIDERS: readonly ["openai-compatible", "openrouter", "litellm", "glama", "bedrock", "ollama", "local", "vertex"];
10
+ export type CustomModelProvider = (typeof CUSTOM_MODEL_PROVIDERS)[number];
11
+ /**
12
+ * Schema for a saved custom model configuration.
13
+ * - openai-compatible: requires baseURL, optional per-model apiKey
14
+ * - openrouter: baseURL is auto-injected, maxInputTokens from registry
15
+ * - litellm: requires baseURL, uses LITELLM_API_KEY or per-model override
16
+ * - glama: fixed baseURL, uses GLAMA_API_KEY or per-model override
17
+ * - bedrock: no baseURL, uses AWS credentials from environment
18
+ * - ollama: optional baseURL (defaults to http://localhost:11434)
19
+ * - local: no baseURL, uses local GGUF files via node-llama-cpp
20
+ * - vertex: no baseURL, uses Google Cloud ADC
21
+ *
22
+ * TODO: For hosted deployments, API keys should be stored in a secure
23
+ * key management service (e.g., AWS Secrets Manager, HashiCorp Vault)
24
+ * rather than in the local JSON file. Current approach is suitable for
25
+ * local CLI usage where the file is in ~/.dexto/ (user-private).
26
+ */
27
+ export declare const CustomModelSchema: z.ZodEffects<z.ZodObject<{
28
+ name: z.ZodString;
29
+ provider: z.ZodDefault<z.ZodEnum<["openai-compatible", "openrouter", "litellm", "glama", "bedrock", "ollama", "local", "vertex"]>>;
30
+ baseURL: z.ZodOptional<z.ZodString>;
31
+ displayName: z.ZodOptional<z.ZodString>;
32
+ maxInputTokens: z.ZodOptional<z.ZodNumber>;
33
+ maxOutputTokens: z.ZodOptional<z.ZodNumber>;
34
+ apiKey: z.ZodOptional<z.ZodString>;
35
+ filePath: z.ZodOptional<z.ZodString>;
36
+ }, "strip", z.ZodTypeAny, {
37
+ name: string;
38
+ provider: "openai-compatible" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama";
39
+ apiKey?: string | undefined;
40
+ baseURL?: string | undefined;
41
+ filePath?: string | undefined;
42
+ displayName?: string | undefined;
43
+ maxInputTokens?: number | undefined;
44
+ maxOutputTokens?: number | undefined;
45
+ }, {
46
+ name: string;
47
+ provider?: "openai-compatible" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | undefined;
48
+ apiKey?: string | undefined;
49
+ baseURL?: string | undefined;
50
+ filePath?: string | undefined;
51
+ displayName?: string | undefined;
52
+ maxInputTokens?: number | undefined;
53
+ maxOutputTokens?: number | undefined;
54
+ }>, {
55
+ name: string;
56
+ provider: "openai-compatible" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama";
57
+ apiKey?: string | undefined;
58
+ baseURL?: string | undefined;
59
+ filePath?: string | undefined;
60
+ displayName?: string | undefined;
61
+ maxInputTokens?: number | undefined;
62
+ maxOutputTokens?: number | undefined;
63
+ }, {
64
+ name: string;
65
+ provider?: "openai-compatible" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | undefined;
66
+ apiKey?: string | undefined;
67
+ baseURL?: string | undefined;
68
+ filePath?: string | undefined;
69
+ displayName?: string | undefined;
70
+ maxInputTokens?: number | undefined;
71
+ maxOutputTokens?: number | undefined;
72
+ }>;
73
+ export type CustomModel = z.output<typeof CustomModelSchema>;
74
+ /**
75
+ * Get the path to the custom models storage file.
76
+ */
77
+ export declare function getCustomModelsPath(): string;
78
+ /**
79
+ * Load custom models from storage.
80
+ */
81
+ export declare function loadCustomModels(): Promise<CustomModel[]>;
82
+ /**
83
+ * Save a custom model to storage.
84
+ */
85
+ export declare function saveCustomModel(model: CustomModel): Promise<void>;
86
+ /**
87
+ * Delete a custom model by name.
88
+ */
89
+ export declare function deleteCustomModel(name: string): Promise<boolean>;
90
+ /**
91
+ * Get a specific custom model by name.
92
+ */
93
+ export declare function getCustomModel(name: string): Promise<CustomModel | null>;
94
+ //# sourceMappingURL=custom-models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-models.d.ts","sourceRoot":"","sources":["../../src/models/custom-models.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,2CAA2C;AAC3C,eAAO,MAAM,sBAAsB,0GASzB,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CxB,CAAC;AAEP,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAO7D;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAmB/D;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUtE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAG9E"}
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ import { promises as fs } from "fs";
3
+ import * as path from "path";
4
+ import { getDextoGlobalPath } from "../utils/path.js";
5
+ const CUSTOM_MODEL_PROVIDERS = [
6
+ "openai-compatible",
7
+ "openrouter",
8
+ "litellm",
9
+ "glama",
10
+ "bedrock",
11
+ "ollama",
12
+ "local",
13
+ "vertex"
14
+ ];
15
+ const CustomModelSchema = z.object({
16
+ name: z.string().min(1),
17
+ provider: z.enum(CUSTOM_MODEL_PROVIDERS).default("openai-compatible"),
18
+ baseURL: z.string().url().optional(),
19
+ displayName: z.string().optional(),
20
+ maxInputTokens: z.number().int().positive().optional(),
21
+ maxOutputTokens: z.number().int().positive().optional(),
22
+ // Optional per-model API key. For openai-compatible this is the primary key source.
23
+ // For litellm/glama/openrouter this overrides the provider-level env var key.
24
+ apiKey: z.string().optional(),
25
+ // File path for local GGUF models. Required when provider is 'local'.
26
+ // Stores the absolute path to the .gguf file on disk.
27
+ filePath: z.string().optional()
28
+ }).superRefine((data, ctx) => {
29
+ if ((data.provider === "openai-compatible" || data.provider === "litellm") && !data.baseURL) {
30
+ ctx.addIssue({
31
+ code: z.ZodIssueCode.custom,
32
+ path: ["baseURL"],
33
+ message: `Base URL is required for ${data.provider} provider`
34
+ });
35
+ }
36
+ if (data.provider === "local" && !data.filePath) {
37
+ ctx.addIssue({
38
+ code: z.ZodIssueCode.custom,
39
+ path: ["filePath"],
40
+ message: "File path is required for local provider"
41
+ });
42
+ }
43
+ if (data.provider === "local" && data.filePath && !data.filePath.endsWith(".gguf")) {
44
+ ctx.addIssue({
45
+ code: z.ZodIssueCode.custom,
46
+ path: ["filePath"],
47
+ message: "File path must be a .gguf file"
48
+ });
49
+ }
50
+ });
51
+ const StorageSchema = z.object({
52
+ version: z.literal(1),
53
+ models: z.array(CustomModelSchema)
54
+ });
55
+ function getCustomModelsPath() {
56
+ return getDextoGlobalPath("models", "custom-models.json");
57
+ }
58
+ async function loadCustomModels() {
59
+ const filePath = getCustomModelsPath();
60
+ try {
61
+ const content = await fs.readFile(filePath, "utf-8");
62
+ const parsed = StorageSchema.safeParse(JSON.parse(content));
63
+ if (!parsed.success) {
64
+ console.warn(
65
+ `[custom-models] Failed to parse ${filePath}: ${parsed.error.issues.map((i) => i.message).join(", ")}`
66
+ );
67
+ return [];
68
+ }
69
+ return parsed.data.models;
70
+ } catch (error) {
71
+ if (error.code === "ENOENT") {
72
+ return [];
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ async function saveCustomModel(model) {
78
+ const parsed = CustomModelSchema.safeParse(model);
79
+ if (!parsed.success) {
80
+ throw new Error(`Invalid model: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
81
+ }
82
+ const models = await loadCustomModels();
83
+ const existingIndex = models.findIndex((m) => m.name === parsed.data.name);
84
+ if (existingIndex >= 0) {
85
+ models[existingIndex] = parsed.data;
86
+ } else {
87
+ models.push(parsed.data);
88
+ }
89
+ await writeCustomModels(models);
90
+ }
91
+ async function deleteCustomModel(name) {
92
+ const models = await loadCustomModels();
93
+ const filtered = models.filter((m) => m.name !== name);
94
+ if (filtered.length === models.length) {
95
+ return false;
96
+ }
97
+ await writeCustomModels(filtered);
98
+ return true;
99
+ }
100
+ async function getCustomModel(name) {
101
+ const models = await loadCustomModels();
102
+ return models.find((m) => m.name === name) ?? null;
103
+ }
104
+ async function writeCustomModels(models) {
105
+ const filePath = getCustomModelsPath();
106
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
107
+ await fs.writeFile(filePath, JSON.stringify({ version: 1, models }, null, 2), "utf-8");
108
+ }
109
+ export {
110
+ CUSTOM_MODEL_PROVIDERS,
111
+ CustomModelSchema,
112
+ deleteCustomModel,
113
+ getCustomModel,
114
+ getCustomModelsPath,
115
+ loadCustomModels,
116
+ saveCustomModel
117
+ };