@sayknow-cli/coding-agent 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +11 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +6 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +29 -0
- package/dist/types/notifications/index.d.ts +12 -1
- package/dist/types/notifications/threaded-render.d.ts +2 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/package.json +7 -7
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +27 -1
- package/src/cli/update-cli.ts +24 -63
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +107 -11
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +3 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +26 -3
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +79 -18
- package/src/modes/controllers/input-controller.ts +50 -9
- package/src/modes/controllers/selector-controller.ts +7 -5
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +70 -0
- package/src/notifications/index.ts +156 -9
- package/src/notifications/rich-render.ts +1 -0
- package/src/notifications/telegram-daemon.ts +87 -4
- package/src/notifications/threaded-render.ts +14 -0
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +68 -5
- package/src/session/agent-session.ts +42 -18
- package/src/skc-runtime/psmux-detect.ts +17 -2
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
package/src/cli/update-cli.ts
CHANGED
|
@@ -14,10 +14,9 @@ import { installDefaultSkcDefinitions } from "../defaults/skc-defaults";
|
|
|
14
14
|
import { theme } from "../modes/theme/theme";
|
|
15
15
|
|
|
16
16
|
const RELEASE_REPO = "jaybeyond/Sayknow_CLI";
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const NPM_MANAGED_PACKAGES = [PACKAGE, SCOPED_CLI_PACKAGE] as const;
|
|
17
|
+
const PACKAGE = "@sayknow-cli/coding-agent";
|
|
18
|
+
const NPM_WRAPPER_PACKAGE = "sayknow-cli";
|
|
19
|
+
const NPM_MANAGED_PACKAGES = [NPM_WRAPPER_PACKAGE, PACKAGE] as const;
|
|
21
20
|
|
|
22
21
|
interface ReleaseInfo {
|
|
23
22
|
tag: string;
|
|
@@ -137,79 +136,35 @@ function defaultPackageExists(_packageName: string, packageRoot: string): boolea
|
|
|
137
136
|
return fs.existsSync(path.join(packageRoot, "package.json"));
|
|
138
137
|
}
|
|
139
138
|
|
|
140
|
-
function
|
|
139
|
+
function npmPackageRootForBinPath(binPath: string, packageName: string, platform: PathPlatform): string {
|
|
141
140
|
const pathApi = pathApiForPlatform(platform);
|
|
142
141
|
const segments = packageName.split("/");
|
|
143
142
|
return pathApi.join(pathApi.dirname(binPath), "node_modules", ...segments);
|
|
144
143
|
}
|
|
145
144
|
|
|
146
|
-
function packageRootFromNodeModulesPath(
|
|
147
|
-
binPath: string,
|
|
148
|
-
packageName: string,
|
|
149
|
-
platform: PathPlatform,
|
|
150
|
-
): string | undefined {
|
|
151
|
-
const pathApi = pathApiForPlatform(platform);
|
|
152
|
-
const packageSegments = packageName.split("/");
|
|
153
|
-
const parts = pathApi.normalize(binPath).split(pathApi.sep).filter(Boolean);
|
|
154
|
-
for (let index = 0; index < parts.length; index++) {
|
|
155
|
-
if (parts[index] !== "node_modules") continue;
|
|
156
|
-
const packageStart = index + 1;
|
|
157
|
-
const matched = packageSegments.every((segment, offset) => parts[packageStart + offset] === segment);
|
|
158
|
-
if (!matched) continue;
|
|
159
|
-
const prefix = binPath.startsWith(pathApi.sep) ? pathApi.sep : "";
|
|
160
|
-
return pathApi.join(prefix, ...parts.slice(0, packageStart + packageSegments.length));
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function npmPackageRootsForPosixShim(binPath: string, packageName: string): string[] {
|
|
166
|
-
const pathApi = path.posix;
|
|
167
|
-
const binDir = pathApi.dirname(binPath);
|
|
168
|
-
const packageSegments = packageName.split("/");
|
|
169
|
-
return [
|
|
170
|
-
pathApi.join(pathApi.dirname(binDir), "lib", "node_modules", ...packageSegments),
|
|
171
|
-
pathApi.join(binDir, "node_modules", ...packageSegments),
|
|
172
|
-
];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
145
|
function resolveNpmManagedTarget(
|
|
176
146
|
ompPath: string,
|
|
177
147
|
platform: PathPlatform = process.platform,
|
|
178
148
|
packageExists: PackageExists = defaultPackageExists,
|
|
179
149
|
): PackageManagerTarget | undefined {
|
|
150
|
+
if (platform !== "win32") return undefined;
|
|
180
151
|
const pathApi = pathApiForPlatform(platform);
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
if (platform === "win32") {
|
|
186
|
-
const extension = pathApi.extname(ompPath).toLowerCase();
|
|
187
|
-
if (extension !== ".cmd" && extension !== ".ps1") return undefined;
|
|
188
|
-
if (basename !== APP_NAME.toLowerCase()) return undefined;
|
|
189
|
-
for (const packageName of NPM_MANAGED_PACKAGES) {
|
|
190
|
-
const packageRoot = npmPackageRootForWindowsShim(ompPath, packageName, platform);
|
|
191
|
-
if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
192
|
-
}
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
|
|
152
|
+
const extension = pathApi.extname(ompPath).toLowerCase();
|
|
153
|
+
if (extension !== ".cmd" && extension !== ".ps1") return undefined;
|
|
154
|
+
const basename = pathApi.basename(ompPath, extension).toLowerCase();
|
|
196
155
|
if (basename !== APP_NAME.toLowerCase()) return undefined;
|
|
156
|
+
|
|
197
157
|
for (const packageName of NPM_MANAGED_PACKAGES) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (packageRoot && packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
201
|
-
}
|
|
202
|
-
for (const packageRoot of npmPackageRootsForPosixShim(ompPath, packageName)) {
|
|
203
|
-
if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
204
|
-
}
|
|
158
|
+
const packageRoot = npmPackageRootForBinPath(ompPath, packageName, platform);
|
|
159
|
+
if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
205
160
|
}
|
|
206
161
|
return undefined;
|
|
207
162
|
}
|
|
208
163
|
|
|
209
164
|
function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
210
|
-
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return "bun";
|
|
211
165
|
if (resolveNpmManagedTarget(ompPath)) return "npm";
|
|
212
|
-
return "binary";
|
|
166
|
+
if (!bunBinDir) return "binary";
|
|
167
|
+
return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
|
|
213
168
|
}
|
|
214
169
|
|
|
215
170
|
export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
@@ -228,10 +183,16 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
228
183
|
const ompPath = resolveSkcPath();
|
|
229
184
|
|
|
230
185
|
if (ompPath) {
|
|
231
|
-
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return { method: "bun" };
|
|
232
186
|
const npmTarget = resolveNpmManagedTarget(ompPath);
|
|
233
187
|
if (npmTarget) return { method: "npm", packageName: npmTarget.packageName };
|
|
234
|
-
|
|
188
|
+
const method = resolveUpdateMethod(ompPath, bunBinDir);
|
|
189
|
+
if (method === "bun") return { method };
|
|
190
|
+
if (method === "npm") {
|
|
191
|
+
throw new Error(
|
|
192
|
+
formatUnsupportedTargetMessage(`Could not resolve npm package root for ${APP_NAME} shim ${ompPath}`),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return { method, path: ompPath };
|
|
235
196
|
}
|
|
236
197
|
|
|
237
198
|
if (bunBinDir) return { method: "bun" };
|
|
@@ -251,7 +212,7 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
|
251
212
|
|
|
252
213
|
const data = (await response.json()) as { version: string };
|
|
253
214
|
const version = data.version;
|
|
254
|
-
const tag =
|
|
215
|
+
const tag = `v${version}`;
|
|
255
216
|
|
|
256
217
|
return {
|
|
257
218
|
tag,
|
|
@@ -389,7 +350,7 @@ function formatBinaryInstallInstruction(platform: NodeJS.Platform = process.plat
|
|
|
389
350
|
function formatManualUpdateInstructions(platform: NodeJS.Platform = process.platform): string {
|
|
390
351
|
return [
|
|
391
352
|
`If ${APP_NAME} was installed with Bun, run: bun install -g ${PACKAGE}@latest`,
|
|
392
|
-
`If ${APP_NAME} was installed with npm, pnpm, or another package manager,
|
|
353
|
+
`If ${APP_NAME} was installed with npm, pnpm, or another package manager, update it with that same manager.`,
|
|
393
354
|
formatBinaryInstallInstruction(platform),
|
|
394
355
|
].join("\n");
|
|
395
356
|
}
|
|
@@ -404,7 +365,7 @@ function buildReleaseBinaryUrl(
|
|
|
404
365
|
arch: string = process.arch,
|
|
405
366
|
): string {
|
|
406
367
|
const binaryName = getBinaryName(platform, arch);
|
|
407
|
-
const tag =
|
|
368
|
+
const tag = `v${version}`;
|
|
408
369
|
return `https://github.com/${RELEASE_REPO}/releases/download/${tag}/${binaryName}`;
|
|
409
370
|
}
|
|
410
371
|
|
package/src/cli.ts
CHANGED
|
@@ -146,6 +146,10 @@ export class RootHelpCommand extends Command {
|
|
|
146
146
|
default: Flags.boolean({ description: "Persist --mpreset as the default model profile" }),
|
|
147
147
|
provider: Flags.string({ description: "Provider to use (legacy; prefer --model)" }),
|
|
148
148
|
"api-key": Flags.string({ description: "API key (defaults to env vars)" }),
|
|
149
|
+
credential: Flags.string({
|
|
150
|
+
description:
|
|
151
|
+
"Stored credential selector: email:<addr>, id:<n>, account:<id>, project:<id>, or provider/email:<addr>",
|
|
152
|
+
}),
|
|
149
153
|
"system-prompt": Flags.string({ description: "System prompt (default: coding assistant prompt)" }),
|
|
150
154
|
"append-system-prompt": Flags.string({ description: "Append text or file contents to the system prompt" }),
|
|
151
155
|
"allow-home": Flags.boolean({ description: "Allow starting in ~ without auto-switching to a temp dir" }),
|
|
@@ -191,6 +195,7 @@ export class RootHelpCommand extends Command {
|
|
|
191
195
|
`# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
|
|
192
196
|
`# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
|
|
193
197
|
`# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
|
|
198
|
+
`# Pin a stored credential for this session\n ${APP_NAME} --credential email:me@example.com`,
|
|
194
199
|
`# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-medium`,
|
|
195
200
|
`# Persist a model profile as the default\n ${APP_NAME} --mpreset opencodego --default`,
|
|
196
201
|
`# Export a session file to HTML\n ${APP_NAME} --export ~/.skc/agent/sessions/--path--/session.jsonl`,
|
package/src/commands/launch.ts
CHANGED
|
@@ -87,6 +87,10 @@ export default class Index extends Command {
|
|
|
87
87
|
"api-key": Flags.string({
|
|
88
88
|
description: "API key (defaults to env vars)",
|
|
89
89
|
}),
|
|
90
|
+
credential: Flags.string({
|
|
91
|
+
description:
|
|
92
|
+
"Stored credential selector: email:<addr>, id:<n>, account:<id>, project:<id>, or provider/email:<addr>",
|
|
93
|
+
}),
|
|
90
94
|
"system-prompt": Flags.string({
|
|
91
95
|
description: "System prompt (default: coding assistant prompt)",
|
|
92
96
|
}),
|
|
@@ -181,6 +185,7 @@ export default class Index extends Command {
|
|
|
181
185
|
`# Launch in a sibling git worktree\n ${APP_NAME} --worktree`,
|
|
182
186
|
`# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`,
|
|
183
187
|
`# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`,
|
|
188
|
+
`# Pin a stored credential for this session\n ${APP_NAME} --credential email:me@example.com`,
|
|
184
189
|
`# Activate a model profile for this session\n ${APP_NAME} --mpreset codex-medium`,
|
|
185
190
|
`# Persist a model profile as the default\n ${APP_NAME} --mpreset opencodego --default`,
|
|
186
191
|
`# Export a session file to HTML\n ${APP_NAME} --export ~/.skc/agent/sessions/--path--/session.jsonl`,
|
package/src/commands/notify.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Configure Telegram notifications.
|
|
3
3
|
*/
|
|
4
4
|
import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
|
|
5
|
-
import { type NotifyAction, type NotifyCommandArgs,
|
|
5
|
+
import { type NotifyAction, type NotifyCommandArgs, runNotifyCliCommand } from "../cli/notify-cli";
|
|
6
6
|
import { initTheme } from "../modes/theme/theme";
|
|
7
7
|
|
|
8
8
|
const ACTIONS: NotifyAction[] = ["setup", "status", "daemon-internal"];
|
|
@@ -56,6 +56,6 @@ export default class Notify extends Command {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
if (action !== "daemon-internal") await initTheme();
|
|
59
|
-
await
|
|
59
|
+
await runNotifyCliCommand(cmd);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
2
2
|
import type { Api, Model } from "@sayknow-cli/ai";
|
|
3
3
|
import type { AgentSession } from "../session/agent-session";
|
|
4
4
|
import { formatClampedModelSelector } from "../thinking";
|
|
@@ -43,7 +43,10 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
43
43
|
settings: Pick<Settings, "get">;
|
|
44
44
|
profileName: string;
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
export interface ApplyModelProfileActivationOptions {
|
|
47
|
+
persistDefault?: boolean;
|
|
48
|
+
thinkingLevelOverride?: ThinkingLevel;
|
|
49
|
+
}
|
|
47
50
|
export interface PreparedModelProfileActivation {
|
|
48
51
|
profileName: string;
|
|
49
52
|
session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
|
|
@@ -351,25 +354,31 @@ export async function prepareModelProfileActivation(
|
|
|
351
354
|
|
|
352
355
|
export async function applyPreparedModelProfileActivation(
|
|
353
356
|
prepared: PreparedModelProfileActivation,
|
|
354
|
-
options:
|
|
357
|
+
options: ApplyModelProfileActivationOptions = {},
|
|
355
358
|
): Promise<void> {
|
|
356
359
|
const previousModel = prepared.previousModel;
|
|
357
360
|
const previousThinkingLevel = prepared.previousThinkingLevel;
|
|
358
361
|
const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
|
|
359
362
|
const previousModelRoles = prepared.previousModelRoles;
|
|
360
363
|
const previousPersistedDefault = prepared.settings.get("modelProfile.default");
|
|
364
|
+
const previousDefaultThinkingLevel = prepared.settings.get("defaultThinkingLevel");
|
|
361
365
|
const previousActiveModelProfile = prepared.previousActiveModelProfile;
|
|
362
366
|
const previousSessionDefaultModel = prepared.previousSessionDefaultModel;
|
|
363
367
|
let modelChanged = false;
|
|
364
368
|
let overridesChanged = false;
|
|
365
369
|
let defaultChanged = false;
|
|
366
370
|
let modelRolesChanged = false;
|
|
371
|
+
let defaultThinkingChanged = false;
|
|
367
372
|
|
|
368
373
|
try {
|
|
369
374
|
if (prepared.defaultModel) {
|
|
370
|
-
await prepared.session.setModelTemporary(
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
await prepared.session.setModelTemporary(
|
|
376
|
+
prepared.defaultModel,
|
|
377
|
+
options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
|
|
378
|
+
{
|
|
379
|
+
persistAsSessionDefault: true,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
373
382
|
modelChanged = true;
|
|
374
383
|
}
|
|
375
384
|
if (Object.keys(prepared.modelRoles).length > 0) {
|
|
@@ -386,6 +395,10 @@ export async function applyPreparedModelProfileActivation(
|
|
|
386
395
|
if (options.persistDefault) {
|
|
387
396
|
prepared.settings.set("modelRoles", {});
|
|
388
397
|
prepared.settings.set("task.agentModelOverrides", {});
|
|
398
|
+
if (prepared.defaultThinkingLevel !== undefined && prepared.defaultThinkingLevel !== ThinkingLevel.Inherit) {
|
|
399
|
+
prepared.settings.set("defaultThinkingLevel", prepared.defaultThinkingLevel);
|
|
400
|
+
defaultThinkingChanged = true;
|
|
401
|
+
}
|
|
389
402
|
prepared.settings.set("modelProfile.default", prepared.profileName);
|
|
390
403
|
defaultChanged = true;
|
|
391
404
|
await prepared.settings.flush();
|
|
@@ -396,6 +409,9 @@ export async function applyPreparedModelProfileActivation(
|
|
|
396
409
|
prepared.settings.set("modelProfile.default", previousPersistedDefault);
|
|
397
410
|
prepared.settings.set("modelRoles", previousModelRoles);
|
|
398
411
|
prepared.settings.set("task.agentModelOverrides", previousAgentModelOverrides);
|
|
412
|
+
if (defaultThinkingChanged) {
|
|
413
|
+
prepared.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel);
|
|
414
|
+
}
|
|
399
415
|
}
|
|
400
416
|
if (modelRolesChanged) {
|
|
401
417
|
prepared.settings.override("modelRoles", previousModelRoles);
|
|
@@ -430,7 +446,7 @@ export async function applyPreparedModelProfileActivation(
|
|
|
430
446
|
|
|
431
447
|
export async function activateModelProfile(
|
|
432
448
|
options: PrepareModelProfileActivationOptions,
|
|
433
|
-
applyOptions:
|
|
449
|
+
applyOptions: ApplyModelProfileActivationOptions = {},
|
|
434
450
|
): Promise<void> {
|
|
435
451
|
const prepared = await prepareModelProfileActivation(options);
|
|
436
452
|
await applyPreparedModelProfileActivation(prepared, applyOptions);
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
type Api,
|
|
5
5
|
type AssistantMessageEventStream,
|
|
6
|
+
type AuthCredentialSelector,
|
|
6
7
|
type CacheRetention,
|
|
7
8
|
type Context,
|
|
8
9
|
createModelManager,
|
|
@@ -83,7 +84,8 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
|
|
86
|
-
|
|
87
|
+
export const MODEL_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
88
|
+
export const MODEL_PROFILE_NAME_PATTERN_DESCRIPTION = "lowercase letters, numbers, dots, underscores, or hyphens";
|
|
87
89
|
export type SkcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
|
|
88
90
|
|
|
89
91
|
export interface SkcModelAssignmentTargetInfo extends ModelRoleInfo {
|
|
@@ -922,10 +924,13 @@ function resolveCustomModelReference(modelId: string): Model<Api> | undefined {
|
|
|
922
924
|
}
|
|
923
925
|
|
|
924
926
|
function applyStandaloneCustomModelPolicies(model: CustomModelOverlay): CustomModelOverlay {
|
|
925
|
-
if (model.
|
|
927
|
+
if (model.contextWindow !== undefined) {
|
|
926
928
|
return model;
|
|
927
929
|
}
|
|
928
|
-
|
|
930
|
+
if (model.id === "gpt-5.4" && model.provider !== "github-copilot") {
|
|
931
|
+
return { ...model, contextWindow: 1_000_000 };
|
|
932
|
+
}
|
|
933
|
+
return model;
|
|
929
934
|
}
|
|
930
935
|
|
|
931
936
|
function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuildOptions): Model<Api> {
|
|
@@ -1590,10 +1595,7 @@ export class ModelRegistry {
|
|
|
1590
1595
|
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1591
1596
|
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1592
1597
|
}
|
|
1593
|
-
await
|
|
1594
|
-
await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(checkedConfig.data, null, 2));
|
|
1595
|
-
this.#modelsConfigFile.invalidate();
|
|
1596
|
-
this.#reloadStaticModels();
|
|
1598
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1597
1599
|
const modelMapping = { ...definition.model_mapping };
|
|
1598
1600
|
const profile: ModelProfileDefinition = {
|
|
1599
1601
|
name: normalizedName,
|
|
@@ -1604,6 +1606,84 @@ export class ModelRegistry {
|
|
|
1604
1606
|
};
|
|
1605
1607
|
return profile;
|
|
1606
1608
|
}
|
|
1609
|
+
|
|
1610
|
+
async renameCustomModelProfile(name: string, displayName: string): Promise<ModelProfileDefinition> {
|
|
1611
|
+
const normalizedName = name.trim();
|
|
1612
|
+
const nextDisplayName = displayName.trim();
|
|
1613
|
+
if (!normalizedName) throw new Error("Profile name is required.");
|
|
1614
|
+
if (!nextDisplayName) throw new Error("Profile display name is required.");
|
|
1615
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "rename");
|
|
1616
|
+
const nextProfiles = {
|
|
1617
|
+
...(current.profiles ?? {}),
|
|
1618
|
+
[normalizedName]: {
|
|
1619
|
+
...profile,
|
|
1620
|
+
display_name: nextDisplayName,
|
|
1621
|
+
},
|
|
1622
|
+
};
|
|
1623
|
+
const checkedConfig = ModelsConfigSchema.safeParse({ ...current, profiles: nextProfiles });
|
|
1624
|
+
if (!checkedConfig.success) {
|
|
1625
|
+
const first = checkedConfig.error.issues[0];
|
|
1626
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1627
|
+
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1628
|
+
}
|
|
1629
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1630
|
+
const modelMapping = { ...profile.model_mapping };
|
|
1631
|
+
return {
|
|
1632
|
+
name: normalizedName,
|
|
1633
|
+
displayName: nextDisplayName,
|
|
1634
|
+
requiredProviders: aggregateModelProfileRequiredProviders(profile.required_providers, { modelMapping }),
|
|
1635
|
+
modelMapping,
|
|
1636
|
+
source: "user",
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
|
|
1641
|
+
const normalizedName = name.trim();
|
|
1642
|
+
if (!normalizedName) throw new Error("Profile name is required.");
|
|
1643
|
+
const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
|
|
1644
|
+
const nextProfiles = { ...(current.profiles ?? {}) };
|
|
1645
|
+
delete nextProfiles[normalizedName];
|
|
1646
|
+
const checkedConfig = ModelsConfigSchema.safeParse({
|
|
1647
|
+
...current,
|
|
1648
|
+
profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
|
|
1649
|
+
});
|
|
1650
|
+
if (!checkedConfig.success) {
|
|
1651
|
+
const first = checkedConfig.error.issues[0];
|
|
1652
|
+
const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
|
|
1653
|
+
throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
|
|
1654
|
+
}
|
|
1655
|
+
await this.#writeCheckedModelsConfig(checkedConfig.data);
|
|
1656
|
+
return profile;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
#loadCustomProfileForMutation(
|
|
1660
|
+
normalizedName: string,
|
|
1661
|
+
action: "rename" | "delete",
|
|
1662
|
+
): { current: ModelsConfig; profile: ModelProfileConfig } {
|
|
1663
|
+
const loaded = this.#modelsConfigFile.tryLoad();
|
|
1664
|
+
if (loaded.status === "error") {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
`Cannot ${action} custom model profile because ${this.#modelsConfigFile.path()} is invalid. Fix the existing config before modifying presets.`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const current = loaded.value ?? this.#modelsConfigFile.createDefault();
|
|
1670
|
+
const profile = current.profiles?.[normalizedName];
|
|
1671
|
+
if (!profile) {
|
|
1672
|
+
const existing = this.#modelProfiles.get(normalizedName);
|
|
1673
|
+
if (existing && existing.source !== "user") {
|
|
1674
|
+
throw new Error(`Cannot ${action} bundled model profile: ${normalizedName}.`);
|
|
1675
|
+
}
|
|
1676
|
+
throw new Error(`Custom model profile does not exist: ${normalizedName}.`);
|
|
1677
|
+
}
|
|
1678
|
+
return { current, profile };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
|
|
1682
|
+
await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
|
|
1683
|
+
await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(config, null, 2));
|
|
1684
|
+
this.#modelsConfigFile.invalidate();
|
|
1685
|
+
this.#reloadStaticModels();
|
|
1686
|
+
}
|
|
1607
1687
|
applyConfiguredModelBindings(targetSettings: Settings): void {
|
|
1608
1688
|
this.#modelBindingsTargetSettings = targetSettings;
|
|
1609
1689
|
this.#applyConfiguredModelBindingsToTarget();
|
|
@@ -2579,21 +2659,37 @@ export class ModelRegistry {
|
|
|
2579
2659
|
/**
|
|
2580
2660
|
* Get API key for a model.
|
|
2581
2661
|
*/
|
|
2582
|
-
async getApiKey(
|
|
2662
|
+
async getApiKey(
|
|
2663
|
+
model: Model<Api>,
|
|
2664
|
+
sessionId?: string,
|
|
2665
|
+
options: { credentialSelector?: AuthCredentialSelector } = {},
|
|
2666
|
+
): Promise<string | undefined> {
|
|
2583
2667
|
if (this.#keylessProviders.has(model.provider) && !this.authStorage.hasAuth(model.provider)) {
|
|
2584
2668
|
return kNoAuth;
|
|
2585
2669
|
}
|
|
2586
|
-
return this.authStorage.getApiKey(model.provider, sessionId, {
|
|
2670
|
+
return this.authStorage.getApiKey(model.provider, sessionId, {
|
|
2671
|
+
baseUrl: model.baseUrl,
|
|
2672
|
+
modelId: model.id,
|
|
2673
|
+
credentialSelector: options.credentialSelector,
|
|
2674
|
+
});
|
|
2587
2675
|
}
|
|
2588
2676
|
|
|
2589
2677
|
/**
|
|
2590
2678
|
* Get API key for a provider (e.g., "openai").
|
|
2591
2679
|
*/
|
|
2592
|
-
async getApiKeyForProvider(
|
|
2680
|
+
async getApiKeyForProvider(
|
|
2681
|
+
provider: string,
|
|
2682
|
+
sessionId?: string,
|
|
2683
|
+
baseUrl?: string,
|
|
2684
|
+
options: { credentialSelector?: AuthCredentialSelector } = {},
|
|
2685
|
+
): Promise<string | undefined> {
|
|
2593
2686
|
if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
|
|
2594
2687
|
return kNoAuth;
|
|
2595
2688
|
}
|
|
2596
|
-
return this.authStorage.getApiKey(provider, sessionId, {
|
|
2689
|
+
return this.authStorage.getApiKey(provider, sessionId, {
|
|
2690
|
+
baseUrl,
|
|
2691
|
+
credentialSelector: options.credentialSelector,
|
|
2692
|
+
});
|
|
2597
2693
|
}
|
|
2598
2694
|
|
|
2599
2695
|
async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
|
|
@@ -3,15 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { ThinkingLevel } from "@sayknow-cli/agent-core";
|
|
6
|
-
import {
|
|
7
|
-
type Api,
|
|
8
|
-
clampThinkingLevelForModel,
|
|
9
|
-
DEFAULT_MODEL_PER_PROVIDER,
|
|
10
|
-
type Effort,
|
|
11
|
-
type KnownProvider,
|
|
12
|
-
type Model,
|
|
13
|
-
modelsAreEqual,
|
|
14
|
-
} from "@sayknow-cli/ai";
|
|
6
|
+
import { type Api, DEFAULT_MODEL_PER_PROVIDER, type KnownProvider, type Model, modelsAreEqual } from "@sayknow-cli/ai";
|
|
15
7
|
import { fuzzyMatch } from "@sayknow-cli/tui";
|
|
16
8
|
import { logger } from "@sayknow-cli/utils";
|
|
17
9
|
import chalk from "chalk";
|
|
@@ -1197,7 +1189,7 @@ export async function findInitialModel(options: {
|
|
|
1197
1189
|
isContinuing: boolean;
|
|
1198
1190
|
defaultProvider?: string;
|
|
1199
1191
|
defaultModelId?: string;
|
|
1200
|
-
defaultThinkingSelector?:
|
|
1192
|
+
defaultThinkingSelector?: ThinkingLevel;
|
|
1201
1193
|
modelRegistry: InitialModelRegistry;
|
|
1202
1194
|
}): Promise<InitialModelResult> {
|
|
1203
1195
|
const {
|
|
@@ -1212,7 +1204,7 @@ export async function findInitialModel(options: {
|
|
|
1212
1204
|
} = options;
|
|
1213
1205
|
|
|
1214
1206
|
let model: Model<Api> | undefined;
|
|
1215
|
-
let thinkingLevel:
|
|
1207
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
1216
1208
|
|
|
1217
1209
|
// 1. CLI args take priority
|
|
1218
1210
|
if (cliProvider && cliModel) {
|
|
@@ -1233,10 +1225,7 @@ export async function findInitialModel(options: {
|
|
|
1233
1225
|
: (scoped.thinkingLevel ?? defaultThinkingSelector);
|
|
1234
1226
|
return {
|
|
1235
1227
|
model: scoped.model,
|
|
1236
|
-
thinkingLevel:
|
|
1237
|
-
scopedThinkingSelector === ThinkingLevel.Off
|
|
1238
|
-
? ThinkingLevel.Off
|
|
1239
|
-
: clampThinkingLevelForModel(scoped.model, scopedThinkingSelector),
|
|
1228
|
+
thinkingLevel: resolveThinkingLevelForModel(scoped.model, scopedThinkingSelector),
|
|
1240
1229
|
fallbackMessage: undefined,
|
|
1241
1230
|
};
|
|
1242
1231
|
}
|
|
@@ -1246,7 +1235,7 @@ export async function findInitialModel(options: {
|
|
|
1246
1235
|
const found = modelRegistry.find(defaultProvider, defaultModelId);
|
|
1247
1236
|
if (found) {
|
|
1248
1237
|
model = found;
|
|
1249
|
-
thinkingLevel =
|
|
1238
|
+
thinkingLevel = resolveThinkingLevelForModel(found, defaultThinkingSelector);
|
|
1250
1239
|
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
1251
1240
|
}
|
|
1252
1241
|
}
|
|
@@ -5,6 +5,7 @@ import { EDIT_MODES } from "../utils/edit-mode";
|
|
|
5
5
|
import { CONFIGURABLE_SEARCH_PROVIDER_IDS } from "../web/search/types";
|
|
6
6
|
|
|
7
7
|
const THINKING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"] as readonly Effort[];
|
|
8
|
+
const DEFAULT_THINKING_LEVELS = ["off", ...THINKING_EFFORTS] as const;
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
DEFAULT_DISABLED_EXTENSIONS,
|
|
@@ -829,13 +830,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
829
830
|
// Reasoning and prompts
|
|
830
831
|
defaultThinkingLevel: {
|
|
831
832
|
type: "enum",
|
|
832
|
-
values:
|
|
833
|
+
values: DEFAULT_THINKING_LEVELS,
|
|
833
834
|
default: THINKING_EFFORTS[3],
|
|
834
835
|
ui: {
|
|
835
836
|
tab: "model",
|
|
836
837
|
label: "Thinking Level",
|
|
837
838
|
description: "Reasoning depth for thinking-capable models",
|
|
838
|
-
options: [...
|
|
839
|
+
options: [...DEFAULT_THINKING_LEVELS.map(getThinkingLevelMetadata)],
|
|
839
840
|
},
|
|
840
841
|
},
|
|
841
842
|
|
package/src/config/settings.ts
CHANGED
|
@@ -375,6 +375,25 @@ export class Settings {
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Like {@link flush}, but rejects if the durable save fails instead of
|
|
380
|
+
* swallowing the error. Use where the caller must confirm persistence before
|
|
381
|
+
* reporting success (e.g. the Telegram `/rich` toggle). In-memory instances
|
|
382
|
+
* ({@link isolated}) short-circuit in {@link #saveNow} and never throw.
|
|
383
|
+
*/
|
|
384
|
+
async flushOrThrow(): Promise<void> {
|
|
385
|
+
if (this.#saveTimer) {
|
|
386
|
+
clearTimeout(this.#saveTimer);
|
|
387
|
+
this.#saveTimer = undefined;
|
|
388
|
+
}
|
|
389
|
+
if (this.#savePromise) {
|
|
390
|
+
await this.#savePromise;
|
|
391
|
+
}
|
|
392
|
+
if (this.#modified.size > 0) {
|
|
393
|
+
await this.#saveNow({ throwOnError: true });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
378
397
|
async cloneForCwd(cwd: string): Promise<Settings> {
|
|
379
398
|
const cloned = new Settings({
|
|
380
399
|
cwd,
|
|
@@ -818,7 +837,7 @@ export class Settings {
|
|
|
818
837
|
}, 100);
|
|
819
838
|
}
|
|
820
839
|
|
|
821
|
-
async #saveNow(): Promise<void> {
|
|
840
|
+
async #saveNow(options: { throwOnError?: boolean } = {}): Promise<void> {
|
|
822
841
|
if (!this.#persist || !this.#configPath || this.#modified.size === 0) return;
|
|
823
842
|
|
|
824
843
|
const configPath = this.#configPath;
|
|
@@ -847,6 +866,10 @@ export class Settings {
|
|
|
847
866
|
for (const p of modifiedPaths) {
|
|
848
867
|
this.#modified.add(p);
|
|
849
868
|
}
|
|
869
|
+
if (options.throwOnError) {
|
|
870
|
+
this.#rebuildMerged();
|
|
871
|
+
throw error;
|
|
872
|
+
}
|
|
850
873
|
}
|
|
851
874
|
|
|
852
875
|
this.#rebuildMerged();
|