@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
|
@@ -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
|
+
}
|