@sayknow-cli/coding-agent 0.3.9 → 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.
Files changed (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/types/cli/args.d.ts +1 -0
  3. package/dist/types/cli/notify-cli.d.ts +5 -0
  4. package/dist/types/cli.d.ts +3 -0
  5. package/dist/types/commands/launch.d.ts +3 -0
  6. package/dist/types/config/model-profile-activation.d.ts +7 -7
  7. package/dist/types/config/model-registry.d.ts +11 -3
  8. package/dist/types/config/model-resolver.d.ts +2 -2
  9. package/dist/types/config/settings-schema.d.ts +1 -1
  10. package/dist/types/config/settings.d.ts +7 -0
  11. package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
  12. package/dist/types/modes/components/settings-selector.d.ts +2 -2
  13. package/dist/types/modes/components/thinking-selector.d.ts +6 -1
  14. package/dist/types/modes/types.d.ts +1 -0
  15. package/dist/types/notifications/config-commands.d.ts +29 -0
  16. package/dist/types/notifications/index.d.ts +12 -1
  17. package/dist/types/notifications/threaded-render.d.ts +2 -0
  18. package/dist/types/notifications/topic-registry.d.ts +4 -5
  19. package/dist/types/runtime-credential-selector.d.ts +7 -0
  20. package/dist/types/sdk.d.ts +7 -1
  21. package/dist/types/session/agent-session.d.ts +6 -3
  22. package/dist/types/tools/index.d.ts +1 -0
  23. package/dist/types/tools/skill-discovery.d.ts +40 -0
  24. package/package.json +7 -7
  25. package/src/cli/args.ts +8 -0
  26. package/src/cli/notify-cli.ts +27 -1
  27. package/src/cli/update-cli.ts +24 -63
  28. package/src/cli.ts +5 -0
  29. package/src/commands/launch.ts +5 -0
  30. package/src/commands/notify.ts +2 -2
  31. package/src/config/model-profile-activation.ts +23 -7
  32. package/src/config/model-registry.ts +107 -11
  33. package/src/config/model-resolver.ts +5 -16
  34. package/src/config/settings-schema.ts +3 -2
  35. package/src/config/settings.ts +24 -1
  36. package/src/extensibility/runtime-skill-discovery.ts +229 -0
  37. package/src/internal-urls/docs-index.generated.ts +1 -1
  38. package/src/main.ts +26 -3
  39. package/src/modes/components/settings-selector.ts +3 -3
  40. package/src/modes/components/thinking-selector.ts +79 -18
  41. package/src/modes/controllers/input-controller.ts +50 -9
  42. package/src/modes/controllers/selector-controller.ts +7 -5
  43. package/src/modes/types.ts +1 -0
  44. package/src/modes/utils/ui-helpers.ts +48 -15
  45. package/src/notifications/config-commands.ts +70 -0
  46. package/src/notifications/index.ts +156 -9
  47. package/src/notifications/rich-render.ts +1 -0
  48. package/src/notifications/telegram-daemon.ts +87 -4
  49. package/src/notifications/threaded-render.ts +14 -0
  50. package/src/notifications/topic-registry.ts +9 -8
  51. package/src/prompts/tools/skill-discovery.md +13 -0
  52. package/src/runtime-credential-selector.ts +35 -0
  53. package/src/sdk.ts +68 -5
  54. package/src/session/agent-session.ts +42 -18
  55. package/src/skc-runtime/psmux-detect.ts +17 -2
  56. package/src/system-prompt.ts +4 -6
  57. package/src/tools/index.ts +4 -0
  58. package/src/tools/skill-discovery.ts +73 -0
  59. package/src/tools/skill.ts +15 -2
@@ -473,6 +473,8 @@ export interface PromptOptions {
473
473
  images?: ImageContent[];
474
474
  /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */
475
475
  streamingBehavior?: "steer" | "followUp";
476
+ /** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
477
+ followUpQueuePolicy?: "respect-mode" | "sequential";
476
478
  /** Optional tool choice override for the next LLM call. */
477
479
  toolChoice?: ToolChoice;
478
480
  /** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
@@ -5304,7 +5306,9 @@ export class AgentSession {
5304
5306
  throw new AgentBusyError();
5305
5307
  }
5306
5308
  if (options.streamingBehavior === "followUp") {
5307
- await this.#queueFollowUp(expandedText, options?.images);
5309
+ await this.#queueFollowUp(expandedText, options?.images, {
5310
+ forceOneAtATime: options.followUpQueuePolicy === "sequential",
5311
+ });
5308
5312
  } else {
5309
5313
  await this.#queueSteer(expandedText, options?.images);
5310
5314
  }
@@ -5422,7 +5426,7 @@ export class AgentSession {
5422
5426
 
5423
5427
  async promptCustomMessage<T = unknown>(
5424
5428
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5425
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
5429
+ options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
5426
5430
  ): Promise<void> {
5427
5431
  const textContent =
5428
5432
  typeof message.content === "string"
@@ -5436,7 +5440,10 @@ export class AgentSession {
5436
5440
  if (!options?.streamingBehavior) {
5437
5441
  throw new AgentBusyError();
5438
5442
  }
5439
- await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
5443
+ await this.sendCustomMessage(message, {
5444
+ deliverAs: options.streamingBehavior,
5445
+ followUpQueuePolicy: options.followUpQueuePolicy,
5446
+ });
5440
5447
  return;
5441
5448
  }
5442
5449
 
@@ -5783,13 +5790,19 @@ export class AgentSession {
5783
5790
  /**
5784
5791
  * Queue a follow-up message to process after the agent would otherwise stop.
5785
5792
  */
5786
- async followUp(text: string, images?: ImageContent[]): Promise<void> {
5793
+ async followUp(
5794
+ text: string,
5795
+ images?: ImageContent[],
5796
+ options?: Pick<PromptOptions, "followUpQueuePolicy">,
5797
+ ): Promise<void> {
5787
5798
  if (text.startsWith("/")) {
5788
5799
  this.#throwIfExtensionCommand(text);
5789
5800
  }
5790
5801
 
5791
5802
  const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
5792
- await this.#queueFollowUp(expandedText, images);
5803
+ await this.#queueFollowUp(expandedText, images, {
5804
+ forceOneAtATime: options?.followUpQueuePolicy === "sequential",
5805
+ });
5793
5806
  }
5794
5807
 
5795
5808
  /**
@@ -5826,19 +5839,22 @@ export class AgentSession {
5826
5839
  /**
5827
5840
  * Internal: Queue a follow-up message (already expanded, no extension command check).
5828
5841
  */
5829
- async #queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {
5842
+ async #queueFollowUp(text: string, images?: ImageContent[], options?: { forceOneAtATime?: boolean }): Promise<void> {
5830
5843
  const displayText = text || (images && images.length > 0 ? "[Image]" : "");
5831
5844
  this.#followUpMessages.push(this.#createQueuedDisplayEntry(displayText));
5832
5845
  const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
5833
5846
  if (images && images.length > 0) {
5834
5847
  content.push(...images);
5835
5848
  }
5836
- this.agent.followUp({
5837
- role: "user",
5838
- content,
5839
- attribution: "user",
5840
- timestamp: Date.now(),
5841
- });
5849
+ this.agent.followUp(
5850
+ {
5851
+ role: "user",
5852
+ content,
5853
+ attribution: "user",
5854
+ timestamp: Date.now(),
5855
+ },
5856
+ options?.forceOneAtATime ? { forceOneAtATime: true } : undefined,
5857
+ );
5842
5858
  // When fully idle AND the session is in a resumable assistant-ended state,
5843
5859
  // schedule an immediate continue so the queued follow-up is delivered
5844
5860
  // without waiting for the next user turn. We gate on isStreaming (model
@@ -5986,7 +6002,11 @@ export class AgentSession {
5986
6002
  */
5987
6003
  async sendCustomMessage<T = unknown>(
5988
6004
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
5989
- options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
6005
+ options?: {
6006
+ triggerTurn?: boolean;
6007
+ deliverAs?: "steer" | "followUp" | "nextTurn";
6008
+ followUpQueuePolicy?: "respect-mode" | "sequential";
6009
+ },
5990
6010
  ): Promise<void> {
5991
6011
  const appMessage: CustomMessage<T> = {
5992
6012
  role: "custom",
@@ -6004,7 +6024,10 @@ export class AgentSession {
6004
6024
  }
6005
6025
 
6006
6026
  if (options?.deliverAs === "followUp") {
6007
- this.agent.followUp(appMessage);
6027
+ this.agent.followUp(
6028
+ appMessage,
6029
+ options.followUpQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined,
6030
+ );
6008
6031
  } else {
6009
6032
  this.agent.steer(appMessage);
6010
6033
  }
@@ -6930,7 +6953,7 @@ export class AgentSession {
6930
6953
 
6931
6954
  /**
6932
6955
  * Set thinking level.
6933
- * Saves the effective metadata-clamped level to session and settings only if it changes.
6956
+ * Saves the effective metadata-clamped level to the session, and to settings when requested.
6934
6957
  */
6935
6958
  setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
6936
6959
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
@@ -6939,11 +6962,12 @@ export class AgentSession {
6939
6962
  this.#thinkingLevel = effectiveLevel;
6940
6963
  this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
6941
6964
 
6965
+ if (persist && effectiveLevel !== undefined) {
6966
+ this.settings.set("defaultThinkingLevel", effectiveLevel);
6967
+ }
6968
+
6942
6969
  if (isChanging) {
6943
6970
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
6944
- if (persist && effectiveLevel !== undefined && effectiveLevel !== ThinkingLevel.Off) {
6945
- this.settings.set("defaultThinkingLevel", effectiveLevel);
6946
- }
6947
6971
  this.#emit({ type: "thinking_level_changed", thinkingLevel: effectiveLevel });
6948
6972
  }
6949
6973
  }
@@ -115,6 +115,20 @@ function outputMentionsPsmux(output: string): boolean {
115
115
  return PSMUX_VERSION_MARKERS.some(marker => output.includes(marker));
116
116
  }
117
117
 
118
+ function normalizedCommandBaseName(command: string): string {
119
+ const normalized = command
120
+ .trim()
121
+ .replace(/^['"]|['"]$/g, "")
122
+ .replace(/\\/g, "/");
123
+ const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
124
+ return basename.endsWith(".exe") ? basename.slice(0, -4) : basename;
125
+ }
126
+
127
+ function isNamedPsmuxCommand(command: string): boolean {
128
+ const basename = normalizedCommandBaseName(command);
129
+ return basename === "psmux" || basename === "pmux";
130
+ }
131
+
118
132
  function resolveBinaryPath(candidate: string): string | null {
119
133
  return activeBinaryResolver(candidate);
120
134
  }
@@ -194,13 +208,14 @@ export function resolveSkcTmuxBinary(options: ResolveSkcTmuxBinaryOptions = {}):
194
208
  const runner = options.runner ?? readSpawnRunner();
195
209
  const explicit = env.SKC_TMUX_COMMAND?.trim() || env.SKC_TEAM_TMUX_COMMAND?.trim();
196
210
  if (explicit) {
197
- const isPsmux = detectPsmux(explicit, { env, runner });
211
+ const isPsmux =
212
+ platform === "win32" && isNamedPsmuxCommand(explicit) ? true : detectPsmux(explicit, { env, runner });
198
213
  return { command: explicit, isPsmux, viaExplicitOverride: true };
199
214
  }
200
215
  if (platform === "win32") {
201
216
  for (const candidate of PSMUX_BINARY_NAMES) {
202
217
  if (resolveBinaryPath(candidate)) {
203
- const isPsmux = detectPsmux(candidate, { env, runner });
218
+ const isPsmux = isNamedPsmuxCommand(candidate) ? true : detectPsmux(candidate, { env, runner });
204
219
  return { command: candidate, isPsmux, viaExplicitOverride: false };
205
220
  }
206
221
  }
@@ -507,7 +507,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
507
507
  ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
508
508
  : Promise.resolve([]);
509
509
 
510
- const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFiles, skills, workspaceTree] =
510
+ const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFiles, , workspaceTree] =
511
511
  await Promise.all([
512
512
  withDeadline(
513
513
  "customPrompt",
@@ -581,11 +581,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
581
581
  description: tools?.get(name)?.description ?? "",
582
582
  }));
583
583
 
584
- // Filter skills for the rendered system prompt:
585
- // - require the `read` tool so the model can actually fetch skill content;
586
- // - drop skills with frontmatter `hide: true` (still loadable via skill:// and /skill:<name>).
587
- const hasRead = tools?.has("read");
588
- const filteredSkills = hasRead ? skills.filter(skill => skill.hide !== true) : [];
584
+ // Runtime skills are discovered by `skill_discovery` and loaded narrowly through
585
+ // `skill`; do not dump the full custom skill catalog into the core prompt.
586
+ const filteredSkills: Skill[] = [];
589
587
 
590
588
  const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
591
589
  resolvedCustomPrompt,
@@ -56,6 +56,7 @@ import { reportFindingTool } from "./review";
56
56
  import { SearchTool } from "./search";
57
57
  import { SearchToolBm25Tool } from "./search-tool-bm25";
58
58
  import { SkillTool } from "./skill";
59
+ import { SkillDiscoveryTool } from "./skill-discovery";
59
60
  import { loadSshTool } from "./ssh";
60
61
  import { SubagentTool } from "./subagent";
61
62
  import { TelegramSendTool } from "./telegram-send";
@@ -95,6 +96,7 @@ export * from "./review";
95
96
  export * from "./search";
96
97
  export * from "./search-tool-bm25";
97
98
  export * from "./skill";
99
+ export * from "./skill-discovery";
98
100
  export * from "./ssh";
99
101
  export * from "./subagent";
100
102
  export * from "./telegram-send";
@@ -411,6 +413,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
411
413
  todo_write: s => new TodoWriteTool(s),
412
414
  web_search: s => new WebSearchTool(s),
413
415
  search_tool_bm25: SearchToolBm25Tool.createIf,
416
+ skill_discovery: SkillDiscoveryTool.createIf,
414
417
  telegram_send: TelegramSendTool.createIf,
415
418
  write: s => new WriteTool(s),
416
419
  skill: SkillTool.createIf,
@@ -570,6 +573,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
570
573
  if (name === "search_tool_bm25") return discoveryActive;
571
574
  if (name === "calc") return session.settings.get("calc.enabled");
572
575
  if (name === "skill") return session.settings.get("skill.enabled");
576
+ if (name === "skill_discovery") return session.settings.get("skill.enabled");
573
577
  if (name === "browser") return session.settings.get("browser.enabled");
574
578
  if (name === "computer") return isComputerCallable(session);
575
579
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
@@ -0,0 +1,73 @@
1
+ import type { AgentTool, AgentToolResult } from "@sayknow-cli/agent-core";
2
+ import { prompt, untilAborted } from "@sayknow-cli/utils";
3
+ import * as z from "zod/v4";
4
+ import { discoverRuntimeSkills, type RuntimeSkillDiscoveryCandidate } from "../extensibility/runtime-skill-discovery";
5
+ import skillDiscoveryDescription from "../prompts/tools/skill-discovery.md" with { type: "text" };
6
+ import type { ToolSession } from ".";
7
+
8
+ const skillDiscoverySchema = z
9
+ .object({
10
+ query: z
11
+ .string()
12
+ .optional()
13
+ .describe("words to match against skill name, description, source, or use conditions"),
14
+ source: z.enum(["all", "project", "user"]).default("all").optional().describe("skill source scope to search"),
15
+ limit: z.number().min(1).max(50).default(20).optional().describe("maximum results"),
16
+ })
17
+ .strict();
18
+
19
+ export type SkillDiscoveryToolInput = z.infer<typeof skillDiscoverySchema>;
20
+
21
+ export interface SkillDiscoveryToolDetails {
22
+ candidates: RuntimeSkillDiscoveryCandidate[];
23
+ count: number;
24
+ }
25
+
26
+ export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema, SkillDiscoveryToolDetails> {
27
+ readonly name = "skill_discovery";
28
+ readonly label = "SkillDiscovery";
29
+ readonly summary = "Discover project and user runtime skills by thin metadata";
30
+ readonly loadMode = "essential";
31
+ readonly description: string;
32
+ readonly parameters = skillDiscoverySchema;
33
+ readonly strict = true;
34
+
35
+ readonly #session: ToolSession;
36
+
37
+ constructor(session: ToolSession) {
38
+ this.#session = session;
39
+ this.description = prompt.render(skillDiscoveryDescription);
40
+ }
41
+
42
+ #getRuntimeSkillPolicy() {
43
+ return {
44
+ ...this.#session.settings.getGroup("skills"),
45
+ disabledExtensions: this.#session.settings.get("disabledExtensions"),
46
+ };
47
+ }
48
+
49
+ static createIf(session: ToolSession): SkillDiscoveryTool | null {
50
+ if (session.settings.get("skill.enabled") === false) return null;
51
+ return new SkillDiscoveryTool(session);
52
+ }
53
+
54
+ async execute(
55
+ _toolCallId: string,
56
+ input: SkillDiscoveryToolInput,
57
+ signal?: AbortSignal,
58
+ ): Promise<AgentToolResult<SkillDiscoveryToolDetails>> {
59
+ return untilAborted(signal, async () => {
60
+ const candidates = await discoverRuntimeSkills({
61
+ cwd: this.#session.cwd,
62
+ query: input.query,
63
+ source: input.source ?? "all",
64
+ limit: input.limit,
65
+ policy: this.#getRuntimeSkillPolicy(),
66
+ });
67
+ return {
68
+ content: [{ type: "text", text: JSON.stringify({ candidates, count: candidates.length }, null, 2) }],
69
+ details: { candidates, count: candidates.length },
70
+ };
71
+ });
72
+ }
73
+ }
@@ -18,6 +18,7 @@
18
18
  import type { AgentTool, AgentToolResult } from "@sayknow-cli/agent-core";
19
19
  import { prompt, untilAborted } from "@sayknow-cli/utils";
20
20
  import * as z from "zod/v4";
21
+ import { findRuntimeSkillByName } from "../extensibility/runtime-skill-discovery";
21
22
  import { resolveSubskillActivationForSkillInvocation } from "../extensibility/skc-plugins";
22
23
  import { buildSkillPromptMessage } from "../extensibility/skills";
23
24
  import skillDescription from "../prompts/tools/skill.md" with { type: "text" };
@@ -64,6 +65,13 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
64
65
  this.description = prompt.render(skillDescription);
65
66
  }
66
67
 
68
+ #getRuntimeSkillPolicy() {
69
+ return {
70
+ ...this.#session.settings.getGroup("skills"),
71
+ disabledExtensions: this.#session.settings.get("disabledExtensions"),
72
+ };
73
+ }
74
+
67
75
  static createIf(session: ToolSession): SkillTool | null {
68
76
  // The tool can only chain when the session can deliver the same-turn
69
77
  // custom message. Without `sendCustomMessage` (e.g. minimal tool
@@ -110,10 +118,15 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
110
118
  );
111
119
  }
112
120
 
113
- const skill = skills.find(s => s.name === requestedName);
121
+ const skill =
122
+ skills.find(s => s.name === requestedName) ??
123
+ (await findRuntimeSkillByName(this.#session.cwd, requestedName, this.#getRuntimeSkillPolicy()));
114
124
  if (!skill) {
115
125
  const available = skills.map(s => s.name).sort();
116
- const hint = available.length > 0 ? ` Available: ${available.join(", ")}` : "";
126
+ const hint =
127
+ available.length > 0
128
+ ? ` Available: ${available.join(", ")}. Use skill_discovery to find project/user runtime skills.`
129
+ : " Use skill_discovery to find project/user runtime skills.";
117
130
  throw new ToolError(`skill tool: unknown skill "${requestedName}".${hint}`);
118
131
  }
119
132