@sayknow-cli/coding-agent 0.3.8 → 0.3.10
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 +13 -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 +15 -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 +7 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/notifications/config-commands.d.ts +38 -0
- package/dist/types/notifications/config.d.ts +16 -0
- package/dist/types/notifications/index.d.ts +14 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +22 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +10 -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/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/package.json +7 -7
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +38 -10
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/keybindings.ts +1 -1
- 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 +11 -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 +4 -4
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +26 -3
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +83 -20
- package/src/modes/controllers/event-controller.ts +11 -2
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +62 -26
- package/src/modes/controllers/selector-controller.ts +43 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +3 -9
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +90 -0
- package/src/notifications/config.ts +27 -0
- package/src/notifications/index.ts +184 -14
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +143 -0
- package/src/notifications/telegram-daemon.ts +431 -91
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +42 -1
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +104 -13
- package/src/session/agent-session.ts +48 -22
- package/src/skc-runtime/psmux-detect.ts +17 -2
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-runtime.ts +10 -1
- package/src/slash-commands/builtin-registry.ts +78 -1
- 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/utils/pasted-image-path.ts +170 -0
|
@@ -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,
|
|
@@ -262,6 +263,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
262
263
|
"notifications.enabled": { type: "boolean", default: false },
|
|
263
264
|
"notifications.telegram.botToken": { type: "string", default: undefined },
|
|
264
265
|
"notifications.telegram.chatId": { type: "string", default: undefined },
|
|
266
|
+
"notifications.telegram.rich.enabled": { type: "boolean", default: true },
|
|
267
|
+
"notifications.telegram.richDraft.enabled": { type: "boolean", default: false },
|
|
265
268
|
"notifications.discord.botToken": { type: "string", default: undefined },
|
|
266
269
|
"notifications.discord.channelId": { type: "string", default: undefined },
|
|
267
270
|
"notifications.slack.botToken": { type: "string", default: undefined },
|
|
@@ -827,13 +830,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
827
830
|
// Reasoning and prompts
|
|
828
831
|
defaultThinkingLevel: {
|
|
829
832
|
type: "enum",
|
|
830
|
-
values:
|
|
833
|
+
values: DEFAULT_THINKING_LEVELS,
|
|
831
834
|
default: THINKING_EFFORTS[3],
|
|
832
835
|
ui: {
|
|
833
836
|
tab: "model",
|
|
834
837
|
label: "Thinking Level",
|
|
835
838
|
description: "Reasoning depth for thinking-capable models",
|
|
836
|
-
options: [...
|
|
839
|
+
options: [...DEFAULT_THINKING_LEVELS.map(getThinkingLevelMetadata)],
|
|
837
840
|
},
|
|
838
841
|
},
|
|
839
842
|
|
|
@@ -3610,6 +3613,12 @@ export interface NotificationsSettings {
|
|
|
3610
3613
|
telegram: {
|
|
3611
3614
|
botToken: string | undefined;
|
|
3612
3615
|
chatId: string | undefined;
|
|
3616
|
+
rich: {
|
|
3617
|
+
enabled: boolean;
|
|
3618
|
+
};
|
|
3619
|
+
richDraft: {
|
|
3620
|
+
enabled: boolean;
|
|
3621
|
+
};
|
|
3613
3622
|
};
|
|
3614
3623
|
discord: {
|
|
3615
3624
|
botToken: string | undefined;
|
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();
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { findRepoRoot } from "../capability/fs";
|
|
5
|
+
import type { Skill as CapabilitySkill } from "../capability/skill";
|
|
6
|
+
import type { SkillsSettings } from "../config/settings-schema";
|
|
7
|
+
import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
|
|
8
|
+
import type { Skill } from "./skills";
|
|
9
|
+
|
|
10
|
+
export type RuntimeSkillDiscoverySource = "project" | "user";
|
|
11
|
+
|
|
12
|
+
export interface RuntimeSkillDiscoveryCandidate {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
source: RuntimeSkillDiscoverySource;
|
|
16
|
+
path: string;
|
|
17
|
+
useWhen?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiscoverRuntimeSkillsOptions {
|
|
21
|
+
cwd: string;
|
|
22
|
+
home?: string;
|
|
23
|
+
query?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
source?: RuntimeSkillDiscoverySource | "all";
|
|
26
|
+
policy?: SkillsSettings;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRuntimeHome(): string {
|
|
30
|
+
return process.env.HOME || os.homedir();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_LIMIT = 20;
|
|
34
|
+
const MAX_LIMIT = 50;
|
|
35
|
+
|
|
36
|
+
function normalizeLimit(limit: number | undefined): number {
|
|
37
|
+
if (limit === undefined || !Number.isFinite(limit)) return DEFAULT_LIMIT;
|
|
38
|
+
return Math.max(1, Math.min(MAX_LIMIT, Math.trunc(limit)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getProjectSkillDirs(cwd: string, home: string): Promise<{ dirs: string[]; repoRoot: string | null }> {
|
|
42
|
+
const dirs: string[] = [];
|
|
43
|
+
let current = path.resolve(cwd);
|
|
44
|
+
const resolvedHome = path.resolve(home);
|
|
45
|
+
const repoRoot = await findRepoRoot(current);
|
|
46
|
+
const stop = path.resolve(repoRoot ?? current);
|
|
47
|
+
while (true) {
|
|
48
|
+
if (current !== resolvedHome) {
|
|
49
|
+
dirs.push(path.join(current, ".skc", "skills"));
|
|
50
|
+
}
|
|
51
|
+
if (current === stop) break;
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (parent === current) break;
|
|
54
|
+
current = parent;
|
|
55
|
+
}
|
|
56
|
+
return { dirs, repoRoot };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getUseWhen(skill: CapabilitySkill): string[] | undefined {
|
|
60
|
+
const frontmatter = skill.frontmatter as Record<string, unknown> | undefined;
|
|
61
|
+
const values: string[] = [];
|
|
62
|
+
const globs = frontmatter?.globs;
|
|
63
|
+
if (Array.isArray(globs)) {
|
|
64
|
+
values.push(...globs.filter((value): value is string => typeof value === "string"));
|
|
65
|
+
} else if (typeof globs === "string") {
|
|
66
|
+
values.push(globs);
|
|
67
|
+
}
|
|
68
|
+
for (const key of ["use_when", "useWhen", "conditions"]) {
|
|
69
|
+
const raw = frontmatter?.[key];
|
|
70
|
+
if (typeof raw === "string") values.push(raw);
|
|
71
|
+
if (Array.isArray(raw)) values.push(...raw.filter((value): value is string => typeof value === "string"));
|
|
72
|
+
}
|
|
73
|
+
return values.length > 0 ? values : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toRuntimeSkill(skill: CapabilitySkill, source: RuntimeSkillDiscoverySource): Skill {
|
|
77
|
+
return {
|
|
78
|
+
name: skill.name,
|
|
79
|
+
description: typeof skill.frontmatter?.description === "string" ? skill.frontmatter.description : "",
|
|
80
|
+
filePath: skill.path,
|
|
81
|
+
baseDir: skill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
82
|
+
source: `runtime:${source}`,
|
|
83
|
+
hide: skill.frontmatter?.hide === true,
|
|
84
|
+
_source: { ...skill._source, providerName: "Runtime skill discovery" },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function sourceEnabled(source: RuntimeSkillDiscoverySource, policy: SkillsSettings | undefined): boolean {
|
|
88
|
+
if (policy?.enabled !== true) return false;
|
|
89
|
+
if (source === "project") return policy.enablePiProject === true;
|
|
90
|
+
if (source === "user") return policy.enablePiUser === true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function matchesIncludePatterns(name: string, includeSkills: string[] | undefined): boolean {
|
|
95
|
+
if (!includeSkills || includeSkills.length === 0) return true;
|
|
96
|
+
return includeSkills.some(pattern => new Bun.Glob(pattern).match(name));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function matchesIgnorePatterns(name: string, ignoredSkills: string[] | undefined): boolean {
|
|
100
|
+
if (!ignoredSkills || ignoredSkills.length === 0) return false;
|
|
101
|
+
return ignoredSkills.some(pattern => new Bun.Glob(pattern).match(name));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isDisabledSkill(name: string, disabledExtensions: string[] | undefined): boolean {
|
|
105
|
+
return (disabledExtensions ?? []).some(id => id === `skill:${name}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAllowedByPolicy(
|
|
109
|
+
skill: CapabilitySkill,
|
|
110
|
+
source: RuntimeSkillDiscoverySource,
|
|
111
|
+
policy: SkillsSettings | undefined,
|
|
112
|
+
): boolean {
|
|
113
|
+
if (!sourceEnabled(source, policy)) return false;
|
|
114
|
+
if (isDisabledSkill(skill.name, policy?.disabledExtensions)) return false;
|
|
115
|
+
if (matchesIgnorePatterns(skill.name, policy?.ignoredSkills)) return false;
|
|
116
|
+
if (!matchesIncludePatterns(skill.name, policy?.includeSkills)) return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function matchesQuery(candidate: RuntimeSkillDiscoveryCandidate, query: string): boolean {
|
|
120
|
+
const normalized = query.trim().toLowerCase();
|
|
121
|
+
if (!normalized) return true;
|
|
122
|
+
const haystack = [candidate.name, candidate.description, candidate.source, ...(candidate.useWhen ?? [])]
|
|
123
|
+
.join("\n")
|
|
124
|
+
.toLowerCase();
|
|
125
|
+
return normalized
|
|
126
|
+
.split(/\s+/)
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.every(term => haystack.includes(term));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function realPathOrSelf(filePath: string): Promise<string> {
|
|
132
|
+
try {
|
|
133
|
+
return await fs.realpath(filePath);
|
|
134
|
+
} catch {
|
|
135
|
+
return filePath;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function discoverRuntimeSkills(
|
|
140
|
+
options: DiscoverRuntimeSkillsOptions,
|
|
141
|
+
): Promise<RuntimeSkillDiscoveryCandidate[]> {
|
|
142
|
+
const home = options.home ?? getRuntimeHome();
|
|
143
|
+
const source = options.source ?? "all";
|
|
144
|
+
const policy = options.policy;
|
|
145
|
+
const scanJobs: Array<Promise<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource }[]>> = [];
|
|
146
|
+
const projectSkills = await getProjectSkillDirs(options.cwd, home);
|
|
147
|
+
const projectContext = { cwd: options.cwd, home, repoRoot: projectSkills.repoRoot };
|
|
148
|
+
if ((source === "all" || source === "project") && sourceEnabled("project", policy)) {
|
|
149
|
+
for (const dir of projectSkills.dirs) {
|
|
150
|
+
scanJobs.push(
|
|
151
|
+
scanSkillsFromDir(projectContext, {
|
|
152
|
+
dir,
|
|
153
|
+
providerId: "runtime",
|
|
154
|
+
level: "project",
|
|
155
|
+
requireDescription: true,
|
|
156
|
+
}).then(result => result.items.map(skill => ({ skill, source: "project" as const }))),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if ((source === "all" || source === "user") && sourceEnabled("user", policy)) {
|
|
161
|
+
scanJobs.push(
|
|
162
|
+
scanSkillsFromDir(
|
|
163
|
+
{ cwd: options.cwd, home, repoRoot: home },
|
|
164
|
+
{ dir: path.join(home, ".skc", "skills"), providerId: "runtime", level: "user", requireDescription: true },
|
|
165
|
+
).then(result => result.items.map(skill => ({ skill, source: "user" as const }))),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const seenNames = new Set<string>();
|
|
170
|
+
const seenPaths = new Set<string>();
|
|
171
|
+
const candidates: RuntimeSkillDiscoveryCandidate[] = [];
|
|
172
|
+
for (const entry of (await Promise.all(scanJobs)).flat()) {
|
|
173
|
+
if (!isAllowedByPolicy(entry.skill, entry.source, policy)) continue;
|
|
174
|
+
const realPath = await realPathOrSelf(entry.skill.path);
|
|
175
|
+
if (seenPaths.has(realPath) || seenNames.has(entry.skill.name)) continue;
|
|
176
|
+
seenPaths.add(realPath);
|
|
177
|
+
seenNames.add(entry.skill.name);
|
|
178
|
+
const candidate: RuntimeSkillDiscoveryCandidate = {
|
|
179
|
+
name: entry.skill.name,
|
|
180
|
+
description:
|
|
181
|
+
typeof entry.skill.frontmatter?.description === "string" ? entry.skill.frontmatter.description : "",
|
|
182
|
+
source: entry.source,
|
|
183
|
+
path: entry.skill.path,
|
|
184
|
+
useWhen: getUseWhen(entry.skill),
|
|
185
|
+
};
|
|
186
|
+
if (matchesQuery(candidate, options.query ?? "")) candidates.push(candidate);
|
|
187
|
+
}
|
|
188
|
+
candidates.sort((a, b) => compareSkillOrder(a.name, a.path, b.name, b.path));
|
|
189
|
+
return candidates.slice(0, normalizeLimit(options.limit));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function findRuntimeSkillByName(
|
|
193
|
+
cwd: string,
|
|
194
|
+
name: string,
|
|
195
|
+
policy?: SkillsSettings,
|
|
196
|
+
home = getRuntimeHome(),
|
|
197
|
+
): Promise<Skill | undefined> {
|
|
198
|
+
const normalized = name.trim();
|
|
199
|
+
if (!normalized) return undefined;
|
|
200
|
+
const scanJobs: Array<Promise<{ skill: CapabilitySkill; source: RuntimeSkillDiscoverySource }[]>> = [];
|
|
201
|
+
const projectSkills = await getProjectSkillDirs(cwd, home);
|
|
202
|
+
const projectContext = { cwd, home, repoRoot: projectSkills.repoRoot };
|
|
203
|
+
if (sourceEnabled("project", policy)) {
|
|
204
|
+
scanJobs.push(
|
|
205
|
+
...projectSkills.dirs.map(dir =>
|
|
206
|
+
scanSkillsFromDir(projectContext, {
|
|
207
|
+
dir,
|
|
208
|
+
providerId: "runtime",
|
|
209
|
+
level: "project",
|
|
210
|
+
requireDescription: true,
|
|
211
|
+
}).then(result => result.items.map(skill => ({ skill, source: "project" as const }))),
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (sourceEnabled("user", policy)) {
|
|
216
|
+
scanJobs.push(
|
|
217
|
+
scanSkillsFromDir(
|
|
218
|
+
{ cwd, home, repoRoot: home },
|
|
219
|
+
{ dir: path.join(home, ".skc", "skills"), providerId: "runtime", level: "user", requireDescription: true },
|
|
220
|
+
).then(result => result.items.map(skill => ({ skill, source: "user" as const }))),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
for (const entry of (await Promise.all(scanJobs)).flat()) {
|
|
224
|
+
if (entry.skill.name === normalized && isAllowedByPolicy(entry.skill, entry.source, policy)) {
|
|
225
|
+
return toRuntimeSkill(entry.skill, entry.source);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|