@cjhyy/code-shell-core 0.8.13 → 0.9.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/dist/automation/desktop-authority-client.d.ts +9 -0
- package/dist/automation/desktop-authority-client.js +47 -0
- package/dist/automation/scheduler.d.ts +8 -0
- package/dist/automation/scheduler.js +37 -1
- package/dist/automation/store.js +14 -1
- package/dist/capabilities/index.d.ts +1 -0
- package/dist/cli/agent-server-stdio.js +8 -4
- package/dist/engine/engine-workspace-authority.d.ts +35 -0
- package/dist/engine/engine-workspace-authority.js +136 -0
- package/dist/engine/engine.d.ts +5 -10
- package/dist/engine/engine.js +108 -112
- package/dist/engine/input-attachments.d.ts +1 -0
- package/dist/engine/input-attachments.js +6 -2
- package/dist/engine/run-environment.d.ts +8 -3
- package/dist/engine/run-environment.js +33 -8
- package/dist/engine/run-image-input.d.ts +1 -0
- package/dist/engine/run-image-input.js +1 -0
- package/dist/engine/run-session-open.d.ts +2 -1
- package/dist/engine/run-session-open.js +7 -1
- package/dist/engine/run-setup.d.ts +1 -0
- package/dist/engine/run-setup.js +2 -1
- package/dist/engine/run-tooling.d.ts +2 -0
- package/dist/engine/run-tooling.js +3 -0
- package/dist/engine/run-types.d.ts +4 -0
- package/dist/engine/run-workspace.d.ts +6 -1
- package/dist/engine/run-workspace.js +47 -0
- package/dist/engine/subagent-spawner.d.ts +1 -0
- package/dist/engine/subagent-spawner.js +1 -0
- package/dist/engine/turn-loop.js +17 -0
- package/dist/engine/types.d.ts +2 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.extension.d.ts +1 -1
- package/dist/index.internal.d.ts +3 -2
- package/dist/index.internal.js +2 -0
- package/dist/index.js +2 -2
- package/dist/llm/client-base.d.ts +2 -1
- package/dist/llm/client-base.js +3 -1
- package/dist/llm/providers/anthropic.js +24 -25
- package/dist/llm/providers/openai.d.ts +4 -1
- package/dist/llm/providers/openai.js +76 -13
- package/dist/panel-apps/index.d.ts +1 -1
- package/dist/panel-apps/index.js +1 -1
- package/dist/panel-apps/installer.d.ts +29 -0
- package/dist/panel-apps/installer.js +124 -15
- package/dist/plugins/pluginAutomationTemplates.d.ts +2 -0
- package/dist/plugins/pluginAutomationTemplates.js +4 -0
- package/dist/plugins/pluginCatalog.d.ts +6 -0
- package/dist/plugins/pluginCatalog.js +7 -2
- package/dist/plugins/pluginContent.d.ts +1 -1
- package/dist/plugins/pluginContent.js +2 -10
- package/dist/prompt/composer.d.ts +4 -1
- package/dist/prompt/composer.js +11 -3
- package/dist/protocol/chat-session-manager.d.ts +42 -1
- package/dist/protocol/chat-session-manager.js +167 -4
- package/dist/protocol/chat-session.d.ts +11 -1
- package/dist/protocol/chat-session.js +20 -2
- package/dist/protocol/mobile-remote-types.d.ts +15 -0
- package/dist/protocol/server.d.ts +1 -2
- package/dist/protocol/server.js +28 -51
- package/dist/protocol/session-workspace-rpc.d.ts +23 -0
- package/dist/protocol/session-workspace-rpc.js +171 -0
- package/dist/protocol/types.d.ts +47 -1
- package/dist/protocol/types.js +4 -0
- package/dist/session/session-manager.d.ts +25 -0
- package/dist/session/session-manager.js +106 -6
- package/dist/session/transcript.d.ts +9 -0
- package/dist/session/transcript.js +81 -0
- package/dist/settings/manager.d.ts +12 -0
- package/dist/settings/manager.js +31 -0
- package/dist/tool-system/builtin/configure-model-connection.d.ts +36 -0
- package/dist/tool-system/builtin/configure-model-connection.js +396 -0
- package/dist/tool-system/builtin/cron.d.ts +11 -0
- package/dist/tool-system/builtin/cron.js +27 -2
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +4 -6
- package/dist/tool-system/builtin/edit-model-catalog.js +5 -8
- package/dist/tool-system/builtin/edit.js +5 -3
- package/dist/tool-system/builtin/index.js +14 -0
- package/dist/tool-system/builtin/install-capability.js +22 -8
- package/dist/tool-system/builtin/settings-changed.d.ts +9 -0
- package/dist/tool-system/builtin/settings-changed.js +18 -0
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +12 -3
- package/dist/tool-system/executor.js +1 -1
- package/dist/tool-system/path-policy.d.ts +11 -3
- package/dist/tool-system/path-policy.js +56 -36
- package/dist/types.d.ts +11 -0
- package/dist/workspace/canonical-key.d.ts +7 -0
- package/dist/workspace/canonical-key.js +39 -0
- package/dist/workspace/workspace-context.d.ts +26 -0
- package/dist/workspace/workspace-context.js +112 -0
- package/package.json +1 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigureModelConnection — safely materialize one catalog model into the
|
|
3
|
+
* unified settings.modelConnections store without exposing or copying API
|
|
4
|
+
* keys. The write is schema-validated, lock-protected, atomic, and updates the
|
|
5
|
+
* selected tag default in the same transaction when requested.
|
|
6
|
+
*/
|
|
7
|
+
import { SettingsManager } from "../../settings/manager.js";
|
|
8
|
+
import { getMergedCatalog } from "../../model-catalog/index.js";
|
|
9
|
+
import { modelEntriesFromConnections } from "../../engine/model-connections-pool.js";
|
|
10
|
+
import { ModelPool } from "../../llm/model-pool.js";
|
|
11
|
+
import { createLLMClient } from "../../llm/client-factory.js";
|
|
12
|
+
import { isCredentialCompatible, } from "../../model-catalog/resolve.js";
|
|
13
|
+
import { notifySettingsChanged } from "./settings-changed.js";
|
|
14
|
+
function redactCredentialSecrets(message, credentials) {
|
|
15
|
+
let redacted = message;
|
|
16
|
+
for (const credential of credentials) {
|
|
17
|
+
if (credential.apiKey)
|
|
18
|
+
redacted = redacted.replaceAll(credential.apiKey, "[REDACTED]");
|
|
19
|
+
}
|
|
20
|
+
return redacted;
|
|
21
|
+
}
|
|
22
|
+
export async function probeTextModelConnection(connection, credentials, catalog, options = {}) {
|
|
23
|
+
const entry = modelEntriesFromConnections([connection], credentials, catalog)[0];
|
|
24
|
+
if (!entry)
|
|
25
|
+
return { ok: false, error: "the saved connection could not be resolved" };
|
|
26
|
+
const pool = new ModelPool([entry]);
|
|
27
|
+
const config = pool.toLLMConfig(entry);
|
|
28
|
+
try {
|
|
29
|
+
const client = await (options.createClient ?? createLLMClient)(config, {
|
|
30
|
+
temperature: 0,
|
|
31
|
+
timeout: 30_000,
|
|
32
|
+
retryMaxAttempts: 1,
|
|
33
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
34
|
+
});
|
|
35
|
+
const response = await client.createMessage({
|
|
36
|
+
systemPrompt: "You are a connection health check. Follow the user's reply instruction.",
|
|
37
|
+
messages: [{ role: "user", content: "Reply with READY only." }],
|
|
38
|
+
tools: [],
|
|
39
|
+
maxTokens: 32,
|
|
40
|
+
stream: false,
|
|
41
|
+
signal: AbortSignal.timeout(30_000),
|
|
42
|
+
requestVisible: false,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
response: response.text.slice(0, 200),
|
|
47
|
+
stopReason: response.stopReason,
|
|
48
|
+
...(response.usage
|
|
49
|
+
? {
|
|
50
|
+
usage: {
|
|
51
|
+
promptTokens: response.usage.promptTokens,
|
|
52
|
+
completionTokens: response.usage.completionTokens,
|
|
53
|
+
totalTokens: response.usage.totalTokens,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
: {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
61
|
+
const safeError = redactCredentialSecrets(raw, credentials);
|
|
62
|
+
return { ok: false, error: safeError.slice(0, 1000) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const DEFAULT_DEPS = {
|
|
66
|
+
makeSettingsManager: (cwd, scope) => new SettingsManager(cwd, scope),
|
|
67
|
+
getCatalog: getMergedCatalog,
|
|
68
|
+
notifySettingsChanged,
|
|
69
|
+
testTextConnection: probeTextModelConnection,
|
|
70
|
+
};
|
|
71
|
+
export const configureModelConnectionToolDef = {
|
|
72
|
+
name: "ConfigureModelConnection",
|
|
73
|
+
description: "Create or update a configured model connection from an existing catalog model. " +
|
|
74
|
+
"Use this after EditModelCatalog when the user wants the model ready to use, not merely " +
|
|
75
|
+
"listed as a template. It validates the catalog model, reuses a compatible existing " +
|
|
76
|
+
"credential by id (never returns or copies its API key), seeds catalog parameter defaults, " +
|
|
77
|
+
"and atomically updates modelConnections plus defaults. User scope requires a full/trusted " +
|
|
78
|
+
"host context. If more than one compatible credential exists, pass credentialId explicitly. " +
|
|
79
|
+
"Set testConnection=true to make one small real request and report whether routing worked.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
catalogId: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Existing merged catalog provider id, e.g. 'openrouter'.",
|
|
86
|
+
},
|
|
87
|
+
model: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Exact model preset value in that provider, e.g. 'openai/gpt-5.6-luna'.",
|
|
90
|
+
},
|
|
91
|
+
connectionId: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Optional stable instance id. Omit to update an existing connection for the same " +
|
|
94
|
+
"catalog model or generate a collision-free id.",
|
|
95
|
+
},
|
|
96
|
+
credentialId: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Existing compatible credential id. Omit to reuse the current connection credential " +
|
|
99
|
+
"or auto-select when exactly one compatible credential exists.",
|
|
100
|
+
},
|
|
101
|
+
paramValues: {
|
|
102
|
+
type: "object",
|
|
103
|
+
description: "Optional model parameter overrides keyed by catalog ParamSpec name. Catalog defaults " +
|
|
104
|
+
"are seeded first; unknown names or invalid enum/type/range values are rejected.",
|
|
105
|
+
},
|
|
106
|
+
setDefault: {
|
|
107
|
+
type: "boolean",
|
|
108
|
+
description: "Set this connection as the default for its catalog tag. The first configured " +
|
|
109
|
+
"connection for a tag becomes default automatically.",
|
|
110
|
+
},
|
|
111
|
+
testConnection: {
|
|
112
|
+
type: "boolean",
|
|
113
|
+
description: "After saving a text connection, send one small real request through it. This may " +
|
|
114
|
+
"incur a tiny provider charge. A failed test is reported without rolling back the " +
|
|
115
|
+
"validated connection.",
|
|
116
|
+
},
|
|
117
|
+
scope: {
|
|
118
|
+
type: "string",
|
|
119
|
+
enum: ["user", "project"],
|
|
120
|
+
description: "Settings layer to update. Defaults to user in a full desktop host, otherwise project.",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ["catalogId", "model"],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
function isRecord(value) {
|
|
127
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
function connectionIdIsSafe(value) {
|
|
130
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
131
|
+
}
|
|
132
|
+
function slugPart(value) {
|
|
133
|
+
return (value
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.split("/")
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.pop()
|
|
138
|
+
?.replace(/[^a-z0-9._-]+/gu, "-")
|
|
139
|
+
.replace(/^-+|-+$/gu, "") || "model");
|
|
140
|
+
}
|
|
141
|
+
function uniqueConnectionId(entry, model, taken) {
|
|
142
|
+
const base = !taken.has(entry.id) ? entry.id : `${entry.id}-${slugPart(model)}`;
|
|
143
|
+
if (!taken.has(base))
|
|
144
|
+
return base;
|
|
145
|
+
let suffix = 2;
|
|
146
|
+
while (taken.has(`${base}-${suffix}`))
|
|
147
|
+
suffix += 1;
|
|
148
|
+
return `${base}-${suffix}`;
|
|
149
|
+
}
|
|
150
|
+
function paramDefaults(preset) {
|
|
151
|
+
const values = {};
|
|
152
|
+
for (const spec of preset.params ?? []) {
|
|
153
|
+
if (spec.default !== undefined)
|
|
154
|
+
values[spec.name] = spec.default;
|
|
155
|
+
}
|
|
156
|
+
return values;
|
|
157
|
+
}
|
|
158
|
+
function validateParamValue(spec, value) {
|
|
159
|
+
if (spec.control === "enum") {
|
|
160
|
+
if (typeof value !== "string")
|
|
161
|
+
return "must be a string enum value";
|
|
162
|
+
if (spec.options?.length && !spec.options.includes(value)) {
|
|
163
|
+
return `must be one of [${spec.options.join(", ")}]`;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
if (spec.control === "number") {
|
|
168
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
169
|
+
return "must be a finite number";
|
|
170
|
+
if (spec.min !== undefined && value < spec.min)
|
|
171
|
+
return `must be >= ${spec.min}`;
|
|
172
|
+
if (spec.max !== undefined && value > spec.max)
|
|
173
|
+
return `must be <= ${spec.max}`;
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (spec.control === "toggle") {
|
|
177
|
+
return typeof value === "boolean" ? undefined : "must be a boolean";
|
|
178
|
+
}
|
|
179
|
+
return typeof value === "string" ? undefined : "must be a string";
|
|
180
|
+
}
|
|
181
|
+
function validateParamValues(values, specs) {
|
|
182
|
+
if (Object.keys(values).length > 64)
|
|
183
|
+
return "paramValues exceeds 64 entries";
|
|
184
|
+
const byName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
185
|
+
for (const [name, value] of Object.entries(values)) {
|
|
186
|
+
const spec = byName.get(name);
|
|
187
|
+
if (!spec)
|
|
188
|
+
return `unknown parameter "${name}" for this model`;
|
|
189
|
+
const issue = validateParamValue(spec, value);
|
|
190
|
+
if (issue)
|
|
191
|
+
return `parameter "${name}" ${issue}`;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
function connectionsFrom(settings) {
|
|
196
|
+
return Array.isArray(settings.modelConnections)
|
|
197
|
+
? settings.modelConnections
|
|
198
|
+
: [];
|
|
199
|
+
}
|
|
200
|
+
function credentialsFrom(settings) {
|
|
201
|
+
return Array.isArray(settings.credentials) ? settings.credentials : [];
|
|
202
|
+
}
|
|
203
|
+
function findTargetConnection(connections, entry, model, explicitId) {
|
|
204
|
+
if (explicitId)
|
|
205
|
+
return connections.find((connection) => connection.id === explicitId);
|
|
206
|
+
return connections.find((connection) => connection.catalogId === entry.id &&
|
|
207
|
+
connection.tag === entry.tag &&
|
|
208
|
+
connection.model === model);
|
|
209
|
+
}
|
|
210
|
+
function selectCredential(entry, catalog, credentials, requestedId, existingId) {
|
|
211
|
+
const compatible = credentials.filter((credential) => isCredentialCompatible(entry, credential, catalog) &&
|
|
212
|
+
(entry.needsKey === false || Boolean(credential.apiKey?.trim())));
|
|
213
|
+
const selectedId = requestedId ?? existingId;
|
|
214
|
+
if (selectedId) {
|
|
215
|
+
const selected = credentials.find((credential) => credential.id === selectedId);
|
|
216
|
+
if (!selected)
|
|
217
|
+
return { error: `credential "${selectedId}" does not exist in this scope` };
|
|
218
|
+
if (!isCredentialCompatible(entry, selected, catalog)) {
|
|
219
|
+
return { error: `credential "${selectedId}" is not compatible with catalog "${entry.id}"` };
|
|
220
|
+
}
|
|
221
|
+
if (entry.needsKey !== false && !selected.apiKey?.trim()) {
|
|
222
|
+
return { error: `credential "${selectedId}" has no usable API key` };
|
|
223
|
+
}
|
|
224
|
+
return { credentialId: selected.id };
|
|
225
|
+
}
|
|
226
|
+
if (entry.needsKey === false)
|
|
227
|
+
return {};
|
|
228
|
+
if (compatible.length === 1)
|
|
229
|
+
return { credentialId: compatible[0].id };
|
|
230
|
+
if (compatible.length === 0) {
|
|
231
|
+
return { error: `no compatible credential is configured for catalog "${entry.id}"` };
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
error: `multiple compatible credentials exist for catalog "${entry.id}": ` +
|
|
235
|
+
`${compatible.map((credential) => credential.id).join(", ")}. Pass credentialId explicitly.`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export async function configureModelConnectionTool(args, ctx, deps = DEFAULT_DEPS) {
|
|
239
|
+
if (!ctx)
|
|
240
|
+
return "Error: ConfigureModelConnection requires a scoped tool context.";
|
|
241
|
+
if (ctx.settingsScope === "isolated") {
|
|
242
|
+
return "Error: isolated sessions cannot persist model connections.";
|
|
243
|
+
}
|
|
244
|
+
const requestedScope = args.scope;
|
|
245
|
+
const scope = requestedScope === "user" || requestedScope === "project"
|
|
246
|
+
? requestedScope
|
|
247
|
+
: ctx.settingsScope === "full"
|
|
248
|
+
? "user"
|
|
249
|
+
: "project";
|
|
250
|
+
if (scope === "user" && ctx.settingsScope !== "full") {
|
|
251
|
+
return "Error: user-scope model connections require settingsScope=full.";
|
|
252
|
+
}
|
|
253
|
+
const catalogId = typeof args.catalogId === "string" ? args.catalogId.trim() : "";
|
|
254
|
+
const model = typeof args.model === "string" ? args.model.trim() : "";
|
|
255
|
+
const connectionId = typeof args.connectionId === "string" ? args.connectionId.trim() : undefined;
|
|
256
|
+
const credentialId = typeof args.credentialId === "string" ? args.credentialId.trim() : undefined;
|
|
257
|
+
if (!catalogId)
|
|
258
|
+
return "Error: catalogId is required.";
|
|
259
|
+
if (!model)
|
|
260
|
+
return "Error: model is required.";
|
|
261
|
+
if (connectionId && !connectionIdIsSafe(connectionId)) {
|
|
262
|
+
return "Error: connectionId must be 1-128 safe id characters (letters, numbers, . _ : -).";
|
|
263
|
+
}
|
|
264
|
+
if (args.paramValues !== undefined && !isRecord(args.paramValues)) {
|
|
265
|
+
return "Error: paramValues must be an object.";
|
|
266
|
+
}
|
|
267
|
+
if (args.setDefault !== undefined && typeof args.setDefault !== "boolean") {
|
|
268
|
+
return "Error: setDefault must be a boolean.";
|
|
269
|
+
}
|
|
270
|
+
if (args.testConnection !== undefined && typeof args.testConnection !== "boolean") {
|
|
271
|
+
return "Error: testConnection must be a boolean.";
|
|
272
|
+
}
|
|
273
|
+
const catalog = deps.getCatalog();
|
|
274
|
+
const entry = catalog.find((candidate) => candidate.id === catalogId);
|
|
275
|
+
if (!entry)
|
|
276
|
+
return `Error: catalog "${catalogId}" does not exist.`;
|
|
277
|
+
const preset = entry.modelPresets?.find((candidate) => candidate.value === model);
|
|
278
|
+
if (!preset) {
|
|
279
|
+
return `Error: model "${model}" is not declared in catalog "${catalogId}".`;
|
|
280
|
+
}
|
|
281
|
+
if (args.testConnection === true && entry.tag !== "text") {
|
|
282
|
+
return "Error: testConnection currently supports text catalog entries only.";
|
|
283
|
+
}
|
|
284
|
+
const manager = deps.makeSettingsManager(ctx.cwd, ctx.settingsScope === "full" ? "full" : "project");
|
|
285
|
+
let effectiveSettings;
|
|
286
|
+
let targetSettings;
|
|
287
|
+
try {
|
|
288
|
+
effectiveSettings = manager.get();
|
|
289
|
+
targetSettings = manager.getForScope(scope, ctx.cwd);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
return `Error: settings validation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
293
|
+
}
|
|
294
|
+
const targetConnections = connectionsFrom(targetSettings);
|
|
295
|
+
const preflightExisting = findTargetConnection(targetConnections, entry, model, connectionId);
|
|
296
|
+
if (connectionId &&
|
|
297
|
+
preflightExisting &&
|
|
298
|
+
(preflightExisting.catalogId !== entry.id || preflightExisting.tag !== entry.tag)) {
|
|
299
|
+
return `Error: connectionId "${connectionId}" already belongs to another catalog or tag.`;
|
|
300
|
+
}
|
|
301
|
+
const effectiveCredentials = scope === "user" ? credentialsFrom(targetSettings) : credentialsFrom(effectiveSettings);
|
|
302
|
+
const selected = selectCredential(entry, catalog, effectiveCredentials, credentialId, preflightExisting?.credentialId);
|
|
303
|
+
if (selected.error)
|
|
304
|
+
return `Error: ${selected.error}`;
|
|
305
|
+
const providedParams = args.paramValues;
|
|
306
|
+
let outcome;
|
|
307
|
+
try {
|
|
308
|
+
manager.mutateSettingsForScope(scope, ctx.cwd, (current) => {
|
|
309
|
+
const connections = connectionsFrom(current);
|
|
310
|
+
const existing = findTargetConnection(connections, entry, model, connectionId);
|
|
311
|
+
if (connectionId &&
|
|
312
|
+
existing &&
|
|
313
|
+
(existing.catalogId !== entry.id || existing.tag !== entry.tag)) {
|
|
314
|
+
throw new Error(`connectionId "${connectionId}" was concurrently claimed`);
|
|
315
|
+
}
|
|
316
|
+
// Revalidate user-scope credential references inside the same locked
|
|
317
|
+
// file transaction so a concurrent credential deletion cannot leave a
|
|
318
|
+
// newly written dangling reference. Project connections may intentionally
|
|
319
|
+
// inherit a user credential from the effective settings layer.
|
|
320
|
+
if (scope === "user" && selected.credentialId) {
|
|
321
|
+
const lockedSelection = selectCredential(entry, catalog, credentialsFrom(current), selected.credentialId, undefined);
|
|
322
|
+
if (lockedSelection.error)
|
|
323
|
+
throw new Error(lockedSelection.error);
|
|
324
|
+
}
|
|
325
|
+
const id = existing?.id ??
|
|
326
|
+
connectionId ??
|
|
327
|
+
uniqueConnectionId(entry, model, new Set(connections.map((connection) => connection.id)));
|
|
328
|
+
const defaults = paramDefaults(preset);
|
|
329
|
+
const keepExistingParams = existing?.model === model && providedParams === undefined;
|
|
330
|
+
const paramValues = keepExistingParams
|
|
331
|
+
? (existing?.paramValues ?? defaults)
|
|
332
|
+
: { ...defaults, ...(providedParams ?? {}) };
|
|
333
|
+
const paramIssue = validateParamValues(paramValues, preset.params ?? []);
|
|
334
|
+
if (paramIssue)
|
|
335
|
+
throw new Error(paramIssue);
|
|
336
|
+
const connection = {
|
|
337
|
+
...(existing ?? {}),
|
|
338
|
+
id,
|
|
339
|
+
catalogId: entry.id,
|
|
340
|
+
tag: entry.tag,
|
|
341
|
+
model,
|
|
342
|
+
...(selected.credentialId ? { credentialId: selected.credentialId } : {}),
|
|
343
|
+
...(Object.keys(paramValues).length > 0 ? { paramValues } : {}),
|
|
344
|
+
};
|
|
345
|
+
if (!selected.credentialId)
|
|
346
|
+
delete connection.credentialId;
|
|
347
|
+
if (Object.keys(paramValues).length === 0)
|
|
348
|
+
delete connection.paramValues;
|
|
349
|
+
const nextConnections = existing
|
|
350
|
+
? connections.map((candidate) => (candidate.id === existing.id ? connection : candidate))
|
|
351
|
+
: [...connections, connection];
|
|
352
|
+
current.modelConnections = nextConnections;
|
|
353
|
+
const rawDefaults = isRecord(current.defaults) ? current.defaults : {};
|
|
354
|
+
const becameDefault = args.setDefault === true || typeof rawDefaults[entry.tag] !== "string";
|
|
355
|
+
if (becameDefault)
|
|
356
|
+
current.defaults = { ...rawDefaults, [entry.tag]: id };
|
|
357
|
+
outcome = {
|
|
358
|
+
action: existing ? "updated" : "added",
|
|
359
|
+
connection,
|
|
360
|
+
becameDefault,
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return `Error: could not configure model connection: ${error instanceof Error ? error.message : String(error)}`;
|
|
366
|
+
}
|
|
367
|
+
if (!outcome)
|
|
368
|
+
return "Error: model connection write produced no result.";
|
|
369
|
+
deps.notifySettingsChanged();
|
|
370
|
+
let verification;
|
|
371
|
+
if (args.testConnection === true) {
|
|
372
|
+
try {
|
|
373
|
+
// Reload credential metadata after the settings transaction. This avoids
|
|
374
|
+
// probing with a stale key if another settings writer rotated it between
|
|
375
|
+
// preflight and persistence.
|
|
376
|
+
const refreshed = scope === "user" ? manager.getForScope("user", ctx.cwd) : manager.get();
|
|
377
|
+
verification = await deps.testTextConnection(outcome.connection, credentialsFrom(refreshed), catalog);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
381
|
+
verification = {
|
|
382
|
+
ok: false,
|
|
383
|
+
error: `could not start connection test: ${redactCredentialSecrets(raw, effectiveCredentials)}`.slice(0, 1000),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return JSON.stringify({
|
|
388
|
+
ok: true,
|
|
389
|
+
action: outcome.action,
|
|
390
|
+
scope,
|
|
391
|
+
connection: outcome.connection,
|
|
392
|
+
defaultForTag: outcome.becameDefault ? entry.tag : undefined,
|
|
393
|
+
hotReloadRequested: true,
|
|
394
|
+
...(verification ? { verification } : {}),
|
|
395
|
+
}, null, 2);
|
|
396
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ToolDefinition } from "../../types.js";
|
|
5
5
|
import type { ToolContext } from "../context.js";
|
|
6
|
+
import type { CronJob, CreateJobOptions } from "../../automation/scheduler.js";
|
|
6
7
|
export { cronListToolDef } from "./cron-list.definition.js";
|
|
7
8
|
/** Sink notified after a cron job is created/deleted, so the host (Electron
|
|
8
9
|
* main) can reload+arm the scheduler that actually executes jobs. The worker
|
|
@@ -10,6 +11,16 @@ export { cronListToolDef } from "./cron-list.definition.js";
|
|
|
10
11
|
* host never learns about an AI-created job until the user opens the UI. */
|
|
11
12
|
type CronChangedSink = () => void;
|
|
12
13
|
export declare function setCronChangedSink(sink: CronChangedSink | null): void;
|
|
14
|
+
export interface CronCreateAuthorityInput extends CreateJobOptions {
|
|
15
|
+
name: string;
|
|
16
|
+
schedule: string;
|
|
17
|
+
prompt: string;
|
|
18
|
+
/** Current Engine Session, used by Desktop Main as authority even for standalone jobs. */
|
|
19
|
+
authoritySessionId?: string;
|
|
20
|
+
}
|
|
21
|
+
export type CronCreateAuthority = (input: CronCreateAuthorityInput) => Promise<CronJob>;
|
|
22
|
+
/** Desktop installs a Main-process delegate; other hosts keep the local scheduler path. */
|
|
23
|
+
export declare function setCronCreateAuthority(authority: CronCreateAuthority | null): void;
|
|
13
24
|
export declare const cronCreateToolDef: ToolDefinition;
|
|
14
25
|
export declare function cronCreateTool(args: Record<string, unknown>, context?: ToolContext): Promise<string>;
|
|
15
26
|
export declare const cronDeleteToolDef: ToolDefinition;
|
|
@@ -17,6 +17,11 @@ function fireCronChanged() {
|
|
|
17
17
|
// Notifying the host is best-effort; never break the tool on it.
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
let cronCreateAuthority = null;
|
|
21
|
+
/** Desktop installs a Main-process delegate; other hosts keep the local scheduler path. */
|
|
22
|
+
export function setCronCreateAuthority(authority) {
|
|
23
|
+
cronCreateAuthority = authority;
|
|
24
|
+
}
|
|
20
25
|
export const cronCreateToolDef = {
|
|
21
26
|
name: "CronCreate",
|
|
22
27
|
description: "Create a scheduled automation job that runs a prompt on a recurring schedule. " +
|
|
@@ -125,15 +130,33 @@ export async function cronCreateTool(args, context) {
|
|
|
125
130
|
// time (the model never supplies it — getCurrentSid() reads the running
|
|
126
131
|
// Engine's ALS context). Empty/unknown sid → treat as standalone.
|
|
127
132
|
const resumeSessionId = args.continueInSession === true && getCurrentSid() ? getCurrentSid() : undefined;
|
|
133
|
+
const stableWorkspace = context?.workspace && !context.workspace.projectId.startsWith("legacy-")
|
|
134
|
+
? {
|
|
135
|
+
projectId: context.workspace.projectId,
|
|
136
|
+
rootId: context.workspace.sessionMainRootId,
|
|
137
|
+
}
|
|
138
|
+
: undefined;
|
|
128
139
|
let job;
|
|
129
140
|
try {
|
|
130
|
-
|
|
141
|
+
const createInput = {
|
|
142
|
+
name,
|
|
143
|
+
schedule,
|
|
144
|
+
prompt,
|
|
131
145
|
...(timezone !== undefined ? { timezone } : {}),
|
|
132
146
|
...(cwd !== undefined ? { cwd } : {}),
|
|
147
|
+
...(stableWorkspace ?? {}),
|
|
133
148
|
...(permissionLevel !== undefined ? { permissionLevel } : {}),
|
|
134
149
|
...(once ? { once: true } : {}),
|
|
135
150
|
...(resumeSessionId !== undefined ? { resumeSessionId } : {}),
|
|
136
|
-
|
|
151
|
+
...(getCurrentSid() ? { authoritySessionId: getCurrentSid() } : {}),
|
|
152
|
+
};
|
|
153
|
+
if (cronCreateAuthority) {
|
|
154
|
+
job = await cronCreateAuthority(createInput);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const { name: _name, schedule: _schedule, prompt: _prompt, authoritySessionId: _authoritySessionId, ...createOptions } = createInput;
|
|
158
|
+
job = cronScheduler.create(name, schedule, prompt, createOptions);
|
|
159
|
+
}
|
|
137
160
|
}
|
|
138
161
|
catch (err) {
|
|
139
162
|
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -162,12 +185,14 @@ export async function cronDeleteTool(args) {
|
|
|
162
185
|
const id = args.jobId;
|
|
163
186
|
if (!id)
|
|
164
187
|
return "Error: jobId is required";
|
|
188
|
+
cronScheduler.loadJobs({ arm: false });
|
|
165
189
|
const deleted = cronScheduler.delete(id);
|
|
166
190
|
if (deleted)
|
|
167
191
|
fireCronChanged();
|
|
168
192
|
return deleted ? `Cron job #${id} deleted.` : `Cron job #${id} not found.`;
|
|
169
193
|
}
|
|
170
194
|
export async function cronListTool(_args) {
|
|
195
|
+
cronScheduler.loadJobs({ arm: false });
|
|
171
196
|
const jobs = cronScheduler.list();
|
|
172
197
|
if (jobs.length === 0)
|
|
173
198
|
return "No cron jobs scheduled.";
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ToolDefinition } from "../../types.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
export declare function setModelCatalogChangedSink(sink: ModelCatalogChangedSink | null): void;
|
|
2
|
+
import { type SettingsChangedSink } from "./settings-changed.js";
|
|
3
|
+
/** Backwards-compatible catalog-specific name for the shared settings/resource
|
|
4
|
+
* invalidation sink. */
|
|
5
|
+
export declare function setModelCatalogChangedSink(sink: SettingsChangedSink | null): void;
|
|
7
6
|
export declare const editModelCatalogToolDef: ToolDefinition;
|
|
8
7
|
export declare function editModelCatalogTool(args: Record<string, unknown>): Promise<string>;
|
|
9
|
-
export {};
|
|
@@ -13,6 +13,7 @@ import { saveCatalogEntry } from "../../model-catalog/save-entry.js";
|
|
|
13
13
|
import { catalogEntrySchema, modelPresetSchema, } from "../../model-catalog/types.js";
|
|
14
14
|
import { upsertModelPreset } from "../../model-catalog/upsert.js";
|
|
15
15
|
import { PROVIDER_KINDS } from "../../llm/provider-kinds.js";
|
|
16
|
+
import { notifySettingsChanged, setSettingsChangedSink, } from "./settings-changed.js";
|
|
16
17
|
const CATALOG_ADAPTER_KINDS = [...Object.keys(PROVIDER_KINDS), "fal"].join("|");
|
|
17
18
|
/**
|
|
18
19
|
* Recognise first-party provider hosts so the catalog tool can reject the
|
|
@@ -52,17 +53,13 @@ function providerIdentityError(entry) {
|
|
|
52
53
|
`protocol="${protocolHint}". adapterKind identifies the provider/gateway account; ` +
|
|
53
54
|
`protocol only identifies the HTTP wire format.`);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
/** Backwards-compatible catalog-specific name for the shared settings/resource
|
|
57
|
+
* invalidation sink. */
|
|
56
58
|
export function setModelCatalogChangedSink(sink) {
|
|
57
|
-
|
|
59
|
+
setSettingsChangedSink(sink);
|
|
58
60
|
}
|
|
59
61
|
function fireModelCatalogChanged() {
|
|
60
|
-
|
|
61
|
-
modelCatalogChangedSink?.();
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Host notification is best-effort; the catalog write itself succeeded.
|
|
65
|
-
}
|
|
62
|
+
notifySettingsChanged();
|
|
66
63
|
}
|
|
67
64
|
export const editModelCatalogToolDef = {
|
|
68
65
|
name: "EditModelCatalog",
|
|
@@ -51,8 +51,10 @@ export async function editTool(args, ctx) {
|
|
|
51
51
|
if (!existsSync(filePath))
|
|
52
52
|
return `Error: File not found: ${filePath}`;
|
|
53
53
|
try {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
54
|
+
const roots = ctx?.workspace?.roots.map((root) => root.path);
|
|
55
|
+
const digest = ctx?.workspace?.rootsDigest;
|
|
56
|
+
const approvedPath = getFinalWritePathSnapshot(args, filePath, cwd, roots, digest);
|
|
57
|
+
const beforeRead = revalidateFinalWritePath(filePath, cwd, approvedPath, roots, digest);
|
|
56
58
|
if ("error" in beforeRead)
|
|
57
59
|
return { ok: false, error: beforeRead.error };
|
|
58
60
|
const raw = await readFile(beforeRead.resolvedPath, "utf-8");
|
|
@@ -75,7 +77,7 @@ export async function editTool(args, ctx) {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
const updatedLf = replaceAll ? content.split(oldLf).join(newLf) : content.replace(oldLf, newLf);
|
|
78
|
-
const beforeWrite = revalidateFinalWritePath(filePath, cwd, approvedPath);
|
|
80
|
+
const beforeWrite = revalidateFinalWritePath(filePath, cwd, approvedPath, roots, digest);
|
|
79
81
|
if ("error" in beforeWrite)
|
|
80
82
|
return { ok: false, error: beforeWrite.error };
|
|
81
83
|
await writeFileNoFollow(beforeWrite.resolvedPath, applyEol(updatedLf, eol));
|
|
@@ -5,6 +5,7 @@ import { readToolDef, readTool } from "./read.js";
|
|
|
5
5
|
import { writeToolDef, writeTool } from "./write.js";
|
|
6
6
|
import { generateImageToolDef, generateImageTool, isGenerateImageAvailable, } from "./generate-image.js";
|
|
7
7
|
import { editModelCatalogToolDef, editModelCatalogTool } from "./edit-model-catalog.js";
|
|
8
|
+
import { configureModelConnectionToolDef, configureModelConnectionTool, } from "./configure-model-connection.js";
|
|
8
9
|
import { generateVideoToolDef, generateVideoTool, isGenerateVideoAvailable, } from "./generate-video.js";
|
|
9
10
|
import { viewImageToolDef, viewImageTool } from "./view-image.js";
|
|
10
11
|
import { editToolDef, editTool } from "./edit.js";
|
|
@@ -123,6 +124,19 @@ const BUILTIN_CONTRIBUTIONS = [
|
|
|
123
124
|
execute: editModelCatalogTool,
|
|
124
125
|
exposure: expose(GENERAL_TAGS),
|
|
125
126
|
},
|
|
127
|
+
{
|
|
128
|
+
definition: {
|
|
129
|
+
...configureModelConnectionToolDef,
|
|
130
|
+
source: "builtin",
|
|
131
|
+
permissionDefault: "ask",
|
|
132
|
+
isReadOnly: false,
|
|
133
|
+
// One lock-protected settings transaction; concurrent model connection
|
|
134
|
+
// mutations must serialize to preserve unique ids and defaults.
|
|
135
|
+
isConcurrencySafe: false,
|
|
136
|
+
},
|
|
137
|
+
execute: configureModelConnectionTool,
|
|
138
|
+
exposure: expose(GENERAL_TAGS),
|
|
139
|
+
},
|
|
126
140
|
{
|
|
127
141
|
definition: {
|
|
128
142
|
...generateImageToolDef,
|