@sayknow-cli/coding-agent 0.3.1 → 0.3.4

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 (216) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/plugin-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/commands/plugin.d.ts +6 -0
  7. package/dist/types/commands/session.d.ts +6 -0
  8. package/dist/types/config/keybindings.d.ts +2 -2
  9. package/dist/types/config/model-profile-activation.d.ts +8 -1
  10. package/dist/types/config/model-profiles.d.ts +2 -2
  11. package/dist/types/config/model-registry.d.ts +3 -3
  12. package/dist/types/config/models-config-schema.d.ts +0 -5
  13. package/dist/types/config/settings-schema.d.ts +79 -68
  14. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  15. package/dist/types/export/html/template.generated.d.ts +1 -1
  16. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  17. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  18. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  20. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  21. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  22. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  23. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  24. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  25. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  26. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  27. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  28. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  29. package/dist/types/i18n/messages/en.d.ts +0 -1
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/settings-defs.d.ts +0 -1
  34. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  35. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  36. package/dist/types/modes/theme/theme.d.ts +1 -1
  37. package/dist/types/notifications/html-format.d.ts +11 -0
  38. package/dist/types/notifications/index.d.ts +149 -1
  39. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  41. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  42. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  43. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  44. package/dist/types/notifications/recent-activity.d.ts +35 -0
  45. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  46. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  47. package/dist/types/notifications/topic-registry.d.ts +12 -9
  48. package/dist/types/runtime-mcp/types.d.ts +7 -0
  49. package/dist/types/sdk.d.ts +2 -0
  50. package/dist/types/session/agent-session.d.ts +14 -4
  51. package/dist/types/session/blob-store.d.ts +25 -0
  52. package/dist/types/session/session-manager.d.ts +57 -0
  53. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  54. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  55. package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
  56. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  57. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  58. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  59. package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
  60. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
  61. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +9 -1
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  65. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  66. package/dist/types/tools/computer-gc.d.ts +23 -0
  67. package/dist/types/tools/cron.d.ts +36 -61
  68. package/dist/types/tools/index.d.ts +3 -2
  69. package/dist/types/tools/resource-gc.d.ts +54 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/url-guard.d.ts +6 -3
  72. package/dist/types/web/scrapers/types.d.ts +5 -0
  73. package/dist/types/web/scrapers/utils.d.ts +7 -1
  74. package/dist/types/web/search/index.d.ts +1 -0
  75. package/dist/types/web/search/providers/utils.d.ts +11 -4
  76. package/package.json +11 -9
  77. package/scripts/g004-tmux-smoke.ts +100 -0
  78. package/scripts/g005-daemon-smoke.ts +180 -0
  79. package/scripts/g011-daemon-path-smoke.ts +153 -0
  80. package/src/cli/args.ts +0 -1
  81. package/src/cli/fast-help.ts +29 -2
  82. package/src/cli/mcp-cli.ts +272 -0
  83. package/src/cli/plugin-cli.ts +66 -3
  84. package/src/cli/web-search-cli.ts +5 -0
  85. package/src/cli.ts +30 -12
  86. package/src/commands/mcp.ts +117 -0
  87. package/src/commands/plugin.ts +4 -0
  88. package/src/commands/session.ts +18 -0
  89. package/src/config/keybindings.ts +2 -2
  90. package/src/config/model-profile-activation.ts +62 -8
  91. package/src/config/model-profiles.ts +3 -4
  92. package/src/config/model-registry.ts +3 -6
  93. package/src/config/models-config-schema.ts +1 -1
  94. package/src/config/settings-schema.ts +88 -103
  95. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  96. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  97. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  98. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  99. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  100. package/src/export/html/index.ts +2 -2
  101. package/src/export/html/template.generated.ts +1 -1
  102. package/src/export/html/template.js +0 -12
  103. package/src/extensibility/extensions/runner.ts +1 -0
  104. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  105. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  106. package/src/extensibility/skc-plugins/index.ts +9 -0
  107. package/src/extensibility/skc-plugins/injection.ts +109 -0
  108. package/src/extensibility/skc-plugins/installer.ts +434 -0
  109. package/src/extensibility/skc-plugins/loader.ts +3 -1
  110. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  111. package/src/extensibility/skc-plugins/observability.ts +84 -0
  112. package/src/extensibility/skc-plugins/paths.ts +1 -1
  113. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  114. package/src/extensibility/skc-plugins/registry.ts +180 -0
  115. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  116. package/src/extensibility/skc-plugins/schema.ts +250 -20
  117. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  118. package/src/extensibility/skc-plugins/types.ts +199 -3
  119. package/src/extensibility/skc-plugins/validation.ts +80 -0
  120. package/src/extensibility/skills.ts +15 -0
  121. package/src/goals/tools/goal-tool.ts +14 -1
  122. package/src/hooks/skill-state.ts +57 -0
  123. package/src/i18n/messages/de.ts +0 -1
  124. package/src/i18n/messages/en.ts +0 -1
  125. package/src/i18n/messages/es.ts +0 -1
  126. package/src/i18n/messages/fr.ts +0 -1
  127. package/src/i18n/messages/ja.ts +0 -1
  128. package/src/i18n/messages/ko.ts +0 -1
  129. package/src/i18n/messages/zh.ts +0 -1
  130. package/src/internal-urls/docs-index.generated.ts +14 -11
  131. package/src/main.ts +14 -3
  132. package/src/modes/bridge/bridge-mode.ts +11 -0
  133. package/src/modes/components/assistant-message.ts +49 -1
  134. package/src/modes/components/custom-editor.ts +2 -0
  135. package/src/modes/components/footer.ts +2 -3
  136. package/src/modes/components/hook-editor.ts +1 -1
  137. package/src/modes/components/hook-selector.ts +67 -43
  138. package/src/modes/components/model-selector.ts +65 -12
  139. package/src/modes/components/settings-defs.ts +1 -2
  140. package/src/modes/components/settings-selector.ts +2 -7
  141. package/src/modes/components/status-line/git-utils.ts +25 -0
  142. package/src/modes/components/status-line.ts +10 -11
  143. package/src/modes/components/welcome.ts +2 -3
  144. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  145. package/src/modes/controllers/selector-controller.ts +59 -11
  146. package/src/modes/interactive-mode.ts +4 -1
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  148. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  149. package/src/modes/theme/defaults/index.ts +2 -0
  150. package/src/modes/theme/theme.ts +0 -4
  151. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  152. package/src/notifications/html-format.ts +38 -0
  153. package/src/notifications/index.ts +242 -12
  154. package/src/notifications/lifecycle-commands.ts +238 -0
  155. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  156. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  157. package/src/notifications/operator-runtime.ts +171 -0
  158. package/src/notifications/rate-limit-pool.ts +19 -0
  159. package/src/notifications/recent-activity.ts +132 -0
  160. package/src/notifications/telegram-daemon.ts +778 -257
  161. package/src/notifications/telegram-reference.ts +25 -7
  162. package/src/notifications/topic-registry.ts +23 -9
  163. package/src/prompts/agents/executor.md +2 -2
  164. package/src/prompts/system/system-prompt.md +2 -2
  165. package/src/prompts/tools/cron.md +5 -3
  166. package/src/prompts/tools/read.md +1 -1
  167. package/src/runtime-mcp/transports/stdio.ts +38 -4
  168. package/src/runtime-mcp/types.ts +7 -0
  169. package/src/sdk.ts +162 -10
  170. package/src/session/agent-session.ts +210 -74
  171. package/src/session/blob-store.ts +196 -8
  172. package/src/session/session-manager.ts +762 -12
  173. package/src/skc-runtime/launch-tmux.ts +68 -19
  174. package/src/skc-runtime/psmux-detect.ts +239 -0
  175. package/src/skc-runtime/state-renderer.ts +13 -0
  176. package/src/skc-runtime/team-runtime.ts +56 -23
  177. package/src/skc-runtime/tmux-common.ts +88 -4
  178. package/src/skc-runtime/tmux-sessions.ts +111 -9
  179. package/src/skc-runtime/ultragoal-guard.ts +192 -8
  180. package/src/skc-runtime/ultragoal-runtime.ts +296 -16
  181. package/src/slash-commands/builtin-registry.ts +23 -3
  182. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  183. package/src/slash-commands/helpers/parse.ts +2 -1
  184. package/src/system-prompt.ts +9 -0
  185. package/src/task/executor.ts +31 -7
  186. package/src/task/index.ts +2 -0
  187. package/src/tools/ask.ts +5 -1
  188. package/src/tools/bash.ts +9 -0
  189. package/src/tools/browser/tab-supervisor.ts +86 -2
  190. package/src/tools/composer-bash-policy.ts +96 -0
  191. package/src/tools/computer-gc.ts +66 -0
  192. package/src/tools/computer.ts +2 -0
  193. package/src/tools/cron.ts +75 -112
  194. package/src/tools/fetch.ts +18 -2
  195. package/src/tools/index.ts +5 -9
  196. package/src/tools/read.ts +25 -55
  197. package/src/tools/renderers.ts +0 -2
  198. package/src/tools/resource-gc.ts +291 -0
  199. package/src/tools/ultragoal-ask-guard.ts +7 -1
  200. package/src/utils/changelog.ts +8 -0
  201. package/src/web/insane/url-guard.ts +18 -14
  202. package/src/web/scrapers/types.ts +143 -45
  203. package/src/web/scrapers/utils.ts +70 -19
  204. package/src/web/search/index.ts +1 -0
  205. package/src/web/search/providers/utils.ts +22 -5
  206. package/vendor/insane-search/MANIFEST.json +3 -1
  207. package/vendor/insane-search/engine/__init__.py +14 -0
  208. package/vendor/insane-search/engine/content_safety.py +151 -0
  209. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  210. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  211. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  212. package/dist/types/tools/inspect-image.d.ts +0 -31
  213. package/src/prompts/tools/inspect-image-system.md +0 -20
  214. package/src/prompts/tools/inspect-image.md +0 -32
  215. package/src/tools/inspect-image-renderer.ts +0 -103
  216. package/src/tools/inspect-image.ts +0 -172
@@ -15,7 +15,14 @@
15
15
  */
16
16
 
17
17
  import * as fs from "node:fs";
18
- import { bold, buildButtonGrid, escapeHtml, TELEGRAM_PARSE_MODE, truncateTelegramHtml } from "./html-format";
18
+ import {
19
+ bold,
20
+ buildCompactChoiceGrid,
21
+ escapeHtml,
22
+ numberedOptionList,
23
+ TELEGRAM_PARSE_MODE,
24
+ truncateTelegramHtml,
25
+ } from "./html-format";
19
26
  import { renderThreadedFrame } from "./threaded-render";
20
27
 
21
28
  /** One inline-keyboard button. */
@@ -129,8 +136,9 @@ export function buildActionMessage(action: {
129
136
  const text = `❓ ${bold(action.question ?? "Question")}`;
130
137
  const options = action.options ?? [];
131
138
  if (options.length === 0) return { text: truncateTelegramHtml(`${text}\n\n(reply with text)`) };
132
- const inline_keyboard = buildButtonGrid(options, i => encodeCallbackData(action.id, i));
133
- return { text: truncateTelegramHtml(text), inline_keyboard };
139
+ const body = `${text}\n\n${numberedOptionList(options)}`;
140
+ const inline_keyboard = buildCompactChoiceGrid(options, i => encodeCallbackData(action.id, i));
141
+ return { text: truncateTelegramHtml(body), inline_keyboard };
134
142
  }
135
143
 
136
144
  /** A protocol `reply` frame the client should send to the server. */
@@ -235,13 +243,23 @@ export function routeInboundUpdate(update: unknown, ctx: RouteInboundContext): R
235
243
  return { kind: "ignore" };
236
244
  }
237
245
 
238
- /** Read `{url, token}` from an endpoint discovery file. */
239
- export function readEndpoint(path: string): { url: string; token: string } {
240
- const raw = JSON.parse(fs.readFileSync(path, "utf8")) as { url?: unknown; token?: unknown };
246
+ /** Read `{url, token, pid?, stale?}` from an endpoint discovery file. */
247
+ export function readEndpoint(path: string): { url: string; token: string; pid?: number; stale?: boolean } {
248
+ const raw = JSON.parse(fs.readFileSync(path, "utf8")) as {
249
+ url?: unknown;
250
+ token?: unknown;
251
+ pid?: unknown;
252
+ stale?: unknown;
253
+ };
241
254
  if (typeof raw.url !== "string" || typeof raw.token !== "string") {
242
255
  throw new Error(`invalid endpoint file: ${path}`);
243
256
  }
244
- return { url: raw.url, token: raw.token };
257
+ return {
258
+ url: raw.url,
259
+ token: raw.token,
260
+ pid: typeof raw.pid === "number" ? raw.pid : undefined,
261
+ stale: raw.stale === true,
262
+ };
245
263
  }
246
264
 
247
265
  /** Options for {@link runTelegramReferenceClient}. */
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Per-session forum-topic registry for the threaded session surface.
3
3
  *
4
- * Each SKC session owns exactly one Telegram forum topic in the paired private
5
- * DM. The topic is created once (via `createForumTopic`) and REUSED on resume,
6
- * keyed by session id, so a resumed session streams back into its existing
7
- * thread/history. The registry also tracks whether the one-time identity header
8
- * has already been pinned, so it is sent exactly once per topic even across
4
+ * Each SKC session owns one active Telegram forum topic in the paired private
5
+ * DM. The topic is created via `createForumTopic`, reused while the session
6
+ * remains active, and removed from the registry when the daemon deletes it on
7
+ * shutdown. The registry also tracks whether the one-time identity header has
8
+ * already been pinned, so it is sent exactly once per active topic, even across
9
9
  * reconnects.
10
10
  *
11
11
  * State is a plain serialisable map persisted beside the daemon state files;
@@ -65,20 +65,25 @@ export class TopicRegistry {
65
65
  return this.byTopic.get(topicId);
66
66
  }
67
67
 
68
+ /** All session ids with a persisted topic record. */
69
+ sessionIds(): string[] {
70
+ return [...this.topics.keys()];
71
+ }
72
+
68
73
  /** The existing topic record for a session, if any. */
69
74
  get(sessionId: string): TopicRecord | undefined {
70
75
  return this.topics.get(sessionId);
71
76
  }
72
77
 
73
78
  /**
74
- * Return the existing topic for `sessionId`, or create one via `create`
75
- * (called only on first use). Reuse-on-resume: an existing record is
76
- * returned without invoking `create`.
79
+ * Return the existing active topic for `sessionId`, or create one via
80
+ * `create` (called only on first use).
77
81
  */
78
82
  async getOrCreateTopic(
79
83
  sessionId: string,
80
84
  create: () => Promise<string>,
81
85
  now: () => number = Date.now,
86
+ name?: string,
82
87
  ): Promise<TopicRecord> {
83
88
  const existing = this.topics.get(sessionId);
84
89
  if (existing) return existing;
@@ -90,7 +95,7 @@ export class TopicRegistry {
90
95
  if (pending) return pending;
91
96
  const promise = (async () => {
92
97
  const topicId = await create();
93
- const record: TopicRecord = { topicId, identitySent: false, createdAt: now() };
98
+ const record: TopicRecord = { topicId, name, identitySent: false, createdAt: now() };
94
99
  this.topics.set(sessionId, record);
95
100
  this.byTopic.set(topicId, sessionId);
96
101
  return record;
@@ -126,6 +131,15 @@ export class TopicRegistry {
126
131
  return true;
127
132
  }
128
133
 
134
+ /** Remove a session topic record after Telegram deletes the topic. */
135
+ delete(sessionId: string): boolean {
136
+ const record = this.topics.get(sessionId);
137
+ if (!record) return false;
138
+ this.topics.delete(sessionId);
139
+ this.byTopic.delete(record.topicId);
140
+ return true;
141
+ }
142
+
129
143
  /** Serialise for atomic persistence beside the daemon state. */
130
144
  serialize(): TopicRegistryState {
131
145
  return { topics: Object.fromEntries(this.topics) };
@@ -36,8 +36,8 @@ This mode activates only when the assignment explicitly labels Executor as Ultra
36
36
 
37
37
  When active:
38
38
  - Start from the approved plan/spec/acceptance criteria, then user-facing contracts, then implementation code only as supporting evidence. Treat plan/code mismatches as blockers.
39
- - Exercise the real user-facing invocation rather than inspecting internals alone. Live artifacts must be runtime-valid: GUI/web needs a real automation transcript plus non-uniform screenshot; CLI needs executed argv-only replay; native/desktop/TUI needs a real screenshot, PTY capture with control codes, or app-automation transcript. `inlineEvidence` is supplemental only and is never sole proof for live surfaces.
40
- - For CLI evidence, emit argv-only replay JSON with `schemaVersion: 1`, `kind: "cli-replay"`, `replaySafe: true`, and `command` as a string array. Use only allowlisted deterministic executables/arguments, or mark unsafe/non-deterministic commands with audited `replayExempt` metadata plus a valid structural fallback artifact. `replayExempt` must use exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs`; allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
39
+ - Exercise the real user-facing invocation rather than inspecting internals alone. Live artifacts must be runtime-valid: GUI/web needs a real automation transcript plus non-uniform screenshot; CLI needs executed argv-only replay; native/desktop/TUI needs a real screenshot, PTY capture with control codes, or app-automation transcript. API/package surfaces need a real artifact file or typed receipt whose artifact `kind` contains `api`, `package`, `consumer`, `black-box`, or `test-report`; good kinds include `api-package-test-report`, `package-consumer-report`, and `black-box-api-receipt`. Algorithm/math surfaces need a real artifact file or typed receipt whose artifact `kind` contains `property`, `boundary`, `edge`, `adversarial`, `failure`, `math`, `algorithm`, or `test-report`; good kinds include `property-test-report` and `algorithm-boundary-report`. `inlineEvidence` is supplemental only and is never sole proof for live surfaces.
40
+ - For CLI evidence, emit argv-only replay JSON with `schemaVersion: 1`, `kind: "cli-replay"`, `replaySafe: true`, and `command` as a string array. Use only allowlisted deterministic executables/arguments: `bun --version`, `node --version`, deterministic `bun/node -e "console.log(...)"`, `npm|pnpm|yarn --version`, `npm|pnpm|yarn list`, read-only `git status|rev-parse|merge-base|diff|show|log` with safe args, and `skc read|status`. Mark any other command with audited `replayExempt` metadata plus a valid structural fallback artifact. `replayExempt` must use exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs`; allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
41
41
  - Native/TUI evidence must be structural, not prose-only: screenshot, app transcript, or PTY artifact with terminal control codes.
42
42
  - Do not call the `ask` tool while an Ultragoal run is active; record unresolved decisions with `skc ultragoal record-review-blockers`.
43
43
  - Try to break the work with adversarial cases, not just happy-path confirmations.
@@ -194,9 +194,9 @@ Delegate by default for multi-file changes, refactors, new features, tests, and
194
194
  </detached-subagents>
195
195
  {{/has}}
196
196
 
197
- {{#has tools "inspect_image"}}
197
+ {{#has tools "read"}}
198
198
  <images>
199
- For image understanding, use `{{toolRefs.inspect_image}}` with a specific question instead of reading raw image metadata only.
199
+ For image understanding, call `{{toolRefs.read}}` on the image path; the image is returned inline for direct visual inspection.
200
200
  </images>
201
201
  {{/has}}
202
202
 
@@ -1,12 +1,14 @@
1
1
  Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks let you re-run a prompt automatically on an interval — poll a deployment, babysit a PR, check back on a long-running build, or remind yourself to do something later in the session.
2
2
 
3
- `CronCreate` accepts a standard 5-field cron expression in your local timezone, the prompt to run, and whether the job recurs or fires once. It returns an 8-character job id you can pass to `CronDelete`. Each session can hold up to 50 scheduled tasks. Recurring tasks auto-expire 7 days after creation; one-shot tasks self-delete after firing.
3
+ Use a single `op` field to select the operation:
4
4
 
5
- `CronList` enumerates every scheduled task in the session. `CronDelete` cancels a task by id.
5
+ - `op: "create"` accepts a standard 5-field `cron_expression` in your local timezone, the `prompt` to run, and `recurring` (whether the job recurs or fires once). It returns an 8-character job id you can pass to `op: "delete"`. Each session can hold up to 50 scheduled tasks. Recurring tasks auto-expire 7 days after creation; one-shot tasks self-delete after firing.
6
+ - `op: "list"` enumerates every scheduled task in the session.
7
+ - `op: "delete"` cancels a task by `id`.
6
8
 
7
9
  ## Cron expressions
8
10
 
9
- `CronCreate` accepts 5-field cron: `minute hour day-of-month month day-of-week`. All fields support `*`, single values (`5`), steps (`*/15`), ranges (`1-5`), and comma lists (`1,15,30`). Day-of-week uses `0`/`7` for Sunday through `6` for Saturday. Extended syntax like `L`, `W`, `?`, or month/weekday names is not supported.
11
+ `op: "create"` accepts 5-field cron: `minute hour day-of-month month day-of-week`. All fields support `*`, single values (`5`), steps (`*/15`), ranges (`1-5`), and comma lists (`1,15,30`). Day-of-week uses `0`/`7` for Sunday through `6` for Saturday. Extended syntax like `L`, `W`, `?`, or month/weekday names is not supported.
10
12
 
11
13
  | Example | Meaning |
12
14
  | :------------ | :--------------------------- |
@@ -46,7 +46,7 @@ Extracts text from PDF, Word, PowerPoint, Excel, RTF, and EPUB. Notebooks (`.ipy
46
46
 
47
47
  # Images
48
48
 
49
- Reading an image path returns metadata (mime, bytes, dimensions, channels, alpha). For actual visual analysis, call `inspect_image` with the path and a question describing what to inspect.
49
+ Reading an image path returns the image itself for visual inspection by a vision-capable model.
50
50
 
51
51
  # Archives
52
52
 
@@ -24,6 +24,38 @@ import { toJsonRpcError } from "../../runtime-mcp/types";
24
24
  */
25
25
  const CLOSE_WAIT_MS = 1_000;
26
26
 
27
+ /**
28
+ * Build a minimal environment for a no-inherit stdio MCP child. Only OS-level
29
+ * keys needed to locate/run an interpreter (PATH, HOME, temp, locale, and the
30
+ * Windows system essentials) are copied from the host; everything else
31
+ * (API keys, tokens, secrets) is withheld. Explicit `env` overrides win.
32
+ */
33
+ function buildMinimalStdioEnv(explicit?: Record<string, string>): Record<string, string> {
34
+ const allow = [
35
+ "PATH",
36
+ "HOME",
37
+ "TMPDIR",
38
+ "TEMP",
39
+ "TMP",
40
+ "LANG",
41
+ "LC_ALL",
42
+ "LC_CTYPE",
43
+ "SHELL",
44
+ "USER",
45
+ "SystemRoot",
46
+ "SYSTEMROOT",
47
+ "PATHEXT",
48
+ "COMSPEC",
49
+ "WINDIR",
50
+ ];
51
+ const env: Record<string, string> = {};
52
+ for (const key of allow) {
53
+ const value = Bun.env[key];
54
+ if (typeof value === "string") env[key] = value;
55
+ }
56
+ return { ...env, ...explicit };
57
+ }
58
+
27
59
  export class StdioTransport implements MCPTransport {
28
60
  #process: OwnedProcess | null = null;
29
61
  #pendingRequests = new Map<
@@ -63,10 +95,12 @@ export class StdioTransport implements MCPTransport {
63
95
  if (this.#connected) return;
64
96
 
65
97
  const args = this.config.args ?? [];
66
- const env = {
67
- ...Bun.env,
68
- ...this.config.env,
69
- };
98
+ const env = this.config.noInheritEnv
99
+ ? buildMinimalStdioEnv(this.config.env)
100
+ : {
101
+ ...Bun.env,
102
+ ...this.config.env,
103
+ };
70
104
 
71
105
  this.#process = spawnOwnedProcess([this.config.command, ...args], {
72
106
  cwd: this.config.cwd ?? getProjectDir(),
@@ -81,6 +81,13 @@ export interface MCPStdioServerConfig extends MCPServerConfigBase {
81
81
  command: string;
82
82
  args?: string[];
83
83
  env?: Record<string, string>;
84
+ /**
85
+ * When true, the child process is NOT given the host environment. Only a
86
+ * minimal OS allowlist (PATH/HOME/temp/locale) plus any explicit `env` keys
87
+ * are passed. Used for third-party plugin-bundle MCP servers so they cannot
88
+ * read host secrets from the inherited environment.
89
+ */
90
+ noInheritEnv?: boolean;
84
91
  cwd?: string;
85
92
  }
86
93
 
package/src/sdk.ts CHANGED
@@ -73,7 +73,13 @@ import {
73
73
  wrapRegisteredTools,
74
74
  } from "./extensibility/extensions";
75
75
  import { ExtensionRuntime } from "./extensibility/extensions/loader";
76
+ import { type ConstrainedPluginHook, loadConstrainedPluginHooks } from "./extensibility/skc-plugins/constrained-hooks";
76
77
  import { resolveCurrentPhaseForParent } from "./extensibility/skc-plugins/injection";
78
+ import {
79
+ buildPluginMcpConfigs,
80
+ loadAlwaysOnPluginTools,
81
+ renderAlwaysOnSystemAppendices,
82
+ } from "./extensibility/skc-plugins/runtime-adapters";
77
83
  import { loadActiveSubskillTools } from "./extensibility/skc-plugins/tools";
78
84
  import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "./extensibility/skills";
79
85
  import type { FileSlashCommand } from "./extensibility/slash-commands";
@@ -136,6 +142,7 @@ import {
136
142
  setPreferredImageProvider,
137
143
  setPreferredSearchProvider,
138
144
  setSearchFallbackProviders,
145
+ setSearchHardTimeoutMs,
139
146
  type Tool,
140
147
  type ToolSession,
141
148
  WebSearchTool,
@@ -748,6 +755,27 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
748
755
  };
749
756
  }
750
757
 
758
+ export function createPluginHooksExtension(hooks: ConstrainedPluginHook[]): ExtensionFactory {
759
+ return api => {
760
+ for (const hook of hooks) {
761
+ // Constrained plugin hooks register exactly their declared event handler
762
+ // through the standard extension API; the loader already denied every
763
+ // session-mutation/command/exec capability at load time. At execution we
764
+ // additionally enforce the declared `target`: a tool-scoped hook only
765
+ // fires for its declared tool, never for arbitrary tool events.
766
+ const target = hook.target;
767
+ const handler = target
768
+ ? (event: { toolName?: string; tool?: { name?: string }; name?: string }, ...rest: unknown[]) => {
769
+ const toolName = event?.toolName ?? event?.tool?.name ?? event?.name;
770
+ if (toolName !== target) return undefined;
771
+ return (hook.handler as (...a: unknown[]) => unknown)(event, ...rest);
772
+ }
773
+ : hook.handler;
774
+ (api.on as (event: string, handler: (...args: unknown[]) => unknown) => void)(hook.event, handler);
775
+ }
776
+ };
777
+ }
778
+
751
779
  // Factory
752
780
 
753
781
  /**
@@ -916,6 +944,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
916
944
  webSearchFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
917
945
  );
918
946
  }
947
+ const webSearchTimeout = settings.get("web_search.timeout");
948
+ if (typeof webSearchTimeout === "number" && Number.isFinite(webSearchTimeout) && webSearchTimeout > 0) {
949
+ setSearchHardTimeoutMs(webSearchTimeout * 1000);
950
+ }
919
951
 
920
952
  const imageProvider = settings.get("providers.image");
921
953
  if (
@@ -1233,6 +1265,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1233
1265
  get model() {
1234
1266
  return agent?.state.model ?? model;
1235
1267
  },
1268
+ get serviceTier() {
1269
+ // Live parent service-tier intent (e.g. runtime `/fast on|off`), inherited
1270
+ // by `inherit` subagents. Only fall back to the startup tier when there is
1271
+ // no live agent yet — never `??`, or an intentional `/fast off`
1272
+ // (serviceTier === undefined) would be resurrected to the startup value.
1273
+ return agent ? agent.serviceTier : initialServiceTier;
1274
+ },
1236
1275
  getAgentId: () => resolvedAgentId,
1237
1276
  bashAllowedPrefixes: options.bashAllowedPrefixes,
1238
1277
  bashRestrictionProfile: options.bashRestrictionProfile,
@@ -1327,14 +1366,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1327
1366
 
1328
1367
  // MCP runtime discovery is quarantined for the SKC surface. Keep an
1329
1368
  // explicitly supplied manager only for legacy in-process callers that own
1330
- // lifecycle themselves; never discover project/user MCP configs here.
1331
- const mcpManager: MCPManager | undefined = options.mcpManager;
1369
+ // lifecycle themselves; never discover project/user MCP configs here. The
1370
+ // owned manager for always-on plugin-bundle MCP servers is created further
1371
+ // below, after `customTools` is populated, so its tools can be surfaced as
1372
+ // always-on tools per the plugin product contract.
1373
+ let mcpManager: MCPManager | undefined = options.mcpManager;
1374
+ let ownsMcpManager = false;
1332
1375
  const customTools: CustomTool[] = [];
1333
- // Only top-level sessions own the global MCPManager. Subagents already
1334
- // receive the parent's manager via `options.mcpManager`, and reassigning
1335
- // the singleton to the same value is a no-op \u2014 keep the gate explicit
1336
- // to mirror the AsyncJobManager ownership rule.
1337
- if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1338
1376
 
1339
1377
  // Add image tools when the active model or configured image providers can generate images.
1340
1378
  const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
@@ -1388,6 +1426,84 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1388
1426
  }
1389
1427
  }
1390
1428
 
1429
+ // Always-on SKC plugin bundle tools (validated registry surfaces). This is
1430
+ // additive and a no-op when no plugins are installed for the cwd. Surfaces
1431
+ // are hash-verified and collision-checked; declared names are authoritative.
1432
+ try {
1433
+ const pluginToolResult = await loadAlwaysOnPluginTools({
1434
+ cwd,
1435
+ reservedToolNames: [...getReservedSubskillToolNames(), ...customTools.map(tool => tool.name)],
1436
+ });
1437
+ if (pluginToolResult.tools.length > 0) customTools.push(...pluginToolResult.tools);
1438
+ for (const q of pluginToolResult.quarantine) {
1439
+ logger.warn("Quarantined SKC plugin surface", { plugin: q.plugin, surface: q.surfaceId, code: q.code });
1440
+ }
1441
+ } catch (error) {
1442
+ logger.warn("Failed to load always-on SKC plugin tools", { error });
1443
+ }
1444
+
1445
+ // Always-on SKC plugin-bundle MCP servers. Top-level sessions own a manager
1446
+ // and connect the validated servers; subagents inherit the parent's manager
1447
+ // via options.mcpManager and never spawn their own (prevents duplicate
1448
+ // processes and leaks). Per the plugin product contract, connected MCP tools
1449
+ // are surfaced as always-on tools rather than gated behind MCP selection.
1450
+ if (!mcpManager && !options.parentTaskPrefix) {
1451
+ try {
1452
+ const { configs, quarantine } = await buildPluginMcpConfigs({ cwd });
1453
+ for (const q of quarantine) {
1454
+ logger.warn("Quarantined SKC plugin MCP", { plugin: q.plugin, surface: q.surfaceId, code: q.code });
1455
+ }
1456
+ if (Object.keys(configs).length > 0) {
1457
+ const owned = new MCPManager(cwd);
1458
+ try {
1459
+ const sources = Object.fromEntries(
1460
+ Object.keys(configs).map(name => [
1461
+ name,
1462
+ { provider: "skc-plugins", providerName: "SKC plugin bundle", level: "project" as const },
1463
+ ]),
1464
+ );
1465
+ const result = await owned.connectServers(configs, sources as never);
1466
+ for (const [server, err] of result.errors) {
1467
+ logger.warn("SKC plugin MCP connect failed", { path: `mcp:${server}`, error: err });
1468
+ }
1469
+ if (result.connectedServers.length > 0) {
1470
+ mcpManager = owned;
1471
+ ownsMcpManager = true;
1472
+ customTools.push(...(result.tools as CustomTool[]));
1473
+ } else {
1474
+ await owned.disconnectAll().catch(() => {});
1475
+ }
1476
+ } catch (error) {
1477
+ // Avoid leaking partially-started server processes on failure.
1478
+ await owned.disconnectAll().catch(() => {});
1479
+ throw error;
1480
+ }
1481
+ }
1482
+ } catch (error) {
1483
+ logger.warn("Failed to wire SKC plugin MCP servers", { error });
1484
+ }
1485
+ } else if (options.parentTaskPrefix) {
1486
+ // Subagent: inherit the parent's always-on plugin MCP tools WITHOUT
1487
+ // owning the manager (no connect, no callbacks, no disposal). The
1488
+ // top-level session installed its manager as the process-global
1489
+ // instance; reading getTools() surfaces the same always-on tools so the
1490
+ // product decision holds for subagent sessions too.
1491
+ const inherited = mcpManager ?? MCPManager.instance();
1492
+ if (inherited) {
1493
+ try {
1494
+ const inheritedTools = inherited.getTools();
1495
+ if (inheritedTools.length > 0) customTools.push(...(inheritedTools as CustomTool[]));
1496
+ } catch (error) {
1497
+ logger.warn("Failed to inherit plugin MCP tools in subagent", { error });
1498
+ }
1499
+ }
1500
+ }
1501
+ // Only top-level sessions own the global MCPManager. Subagents already
1502
+ // receive the parent's manager via options.mcpManager; reassigning the
1503
+ // singleton to the same value is a no-op. Keep the gate explicit to mirror
1504
+ // the AsyncJobManager ownership rule.
1505
+ if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1506
+
1391
1507
  // Custom tool and extension discovery is quarantined from the public SKC utility surface.
1392
1508
  // Explicit SDK extension factories are still honored; callers use them to
1393
1509
  // register in-process tools/providers without enabling filesystem discovery.
@@ -1395,6 +1511,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1395
1511
  if (customTools.length > 0) {
1396
1512
  inlineExtensions.push(createCustomToolsExtension(customTools));
1397
1513
  }
1514
+
1515
+ // Always-on constrained plugin hooks (validated registry surfaces). Additive
1516
+ // and a no-op without installed plugins; the loader denies all dangerous APIs.
1517
+ try {
1518
+ const pluginHookResult = await loadConstrainedPluginHooks({ cwd });
1519
+ if (pluginHookResult.hooks.length > 0) {
1520
+ inlineExtensions.push(createPluginHooksExtension(pluginHookResult.hooks));
1521
+ }
1522
+ for (const q of pluginHookResult.quarantine) {
1523
+ logger.warn("Quarantined SKC plugin hook", { plugin: q.plugin, surface: q.surfaceId, code: q.code });
1524
+ }
1525
+ } catch (error) {
1526
+ logger.warn("Failed to load constrained SKC plugin hooks", { error });
1527
+ }
1398
1528
  let notificationCfg: NotificationConfig | undefined;
1399
1529
  try {
1400
1530
  notificationCfg = getNotificationConfig(Settings.instance);
@@ -1693,6 +1823,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1693
1823
  }
1694
1824
  appendPrompt = parts.join("\n\n");
1695
1825
  }
1826
+ let pluginSystemAppendices = "";
1827
+ try {
1828
+ pluginSystemAppendices = await renderAlwaysOnSystemAppendices({ cwd });
1829
+ } catch (error) {
1830
+ logger.warn("Failed to render SKC plugin system appendices", { error });
1831
+ }
1696
1832
  const defaultPrompt = await buildSystemPromptInternal({
1697
1833
  cwd,
1698
1834
  skills,
@@ -1703,6 +1839,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1703
1839
  alwaysApplyRules,
1704
1840
  skillsSettings: settings.getGroup("skills"),
1705
1841
  appendSystemPrompt: appendPrompt,
1842
+ pluginAppendices: pluginSystemAppendices,
1706
1843
  repeatToolDescriptions,
1707
1844
  intentField,
1708
1845
  mcpDiscoveryMode: false,
@@ -2059,6 +2196,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2059
2196
  // AsyncJobManager on teardown; subagents inherit the parent's and
2060
2197
  // **MUST NOT** tear it down.
2061
2198
  ownedAsyncJobManager: asyncJobManager,
2199
+ // Only the owned plugin-bundle MCP manager is torn down on dispose;
2200
+ // subagents/callers that merely observe the global must not (see
2201
+ // AgentSession.dispose).
2202
+ ownedMcpManager: ownsMcpManager ? mcpManager : undefined,
2062
2203
  scopedModels: options.scopedModels,
2063
2204
  promptTemplates,
2064
2205
  slashCommands,
@@ -2223,9 +2364,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2223
2364
  // Wire MCP manager callbacks to session for reactive tool updates.
2224
2365
  // Skip when reusing a parent's manager — the parent owns the callbacks.
2225
2366
  if (mcpManager && !options.mcpManager) {
2226
- mcpManager.setOnToolsChanged(tools => {
2227
- void session.refreshMCPTools(tools);
2228
- });
2367
+ // The owned plugin-bundle manager surfaces its tools as always-on custom
2368
+ // tools (registered above), so it must NOT drive refreshMCPTools — that
2369
+ // path strips MCP bridge tools and re-gates them behind MCP selection,
2370
+ // which would deactivate the always-on plugin tools. Reactive tool
2371
+ // updates remain wired only for externally supplied managers.
2372
+ // The owned manager is disconnected by AgentSession.dispose via
2373
+ // ownedMcpManager; only externally supplied managers wire reactive
2374
+ // refreshMCPTools (the owned always-on path must not, or it would
2375
+ // deactivate the plugin tools).
2376
+ if (!ownsMcpManager) {
2377
+ mcpManager.setOnToolsChanged(tools => {
2378
+ void session.refreshMCPTools(tools);
2379
+ });
2380
+ }
2229
2381
  // Wire prompt refresh → rebuild MCP prompt slash commands
2230
2382
  mcpManager.setOnPromptsChanged(serverName => {
2231
2383
  const promptCommands = buildMCPPromptCommands(mcpManager);