@sayknow-cli/coding-agent 0.2.6 → 0.3.0

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 (145) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +71 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +13 -2
  18. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  19. package/dist/types/extensibility/plugins/index.d.ts +1 -0
  20. package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  23. package/dist/types/modes/interactive-mode.d.ts +1 -1
  24. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  25. package/dist/types/modes/types.d.ts +7 -1
  26. package/dist/types/notifications/config-commands.d.ts +26 -0
  27. package/dist/types/notifications/config.d.ts +61 -0
  28. package/dist/types/notifications/helpers.d.ts +55 -0
  29. package/dist/types/notifications/html-format.d.ts +62 -0
  30. package/dist/types/notifications/index.d.ts +28 -0
  31. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  32. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  33. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  34. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  35. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  36. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  37. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  38. package/dist/types/notifications/threaded-render.d.ts +66 -0
  39. package/dist/types/notifications/topic-registry.d.ts +67 -0
  40. package/dist/types/rlm/index.d.ts +12 -0
  41. package/dist/types/session/agent-session.d.ts +39 -2
  42. package/dist/types/session/auth-storage.d.ts +1 -1
  43. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  44. package/dist/types/setup/credential-import.d.ts +3 -0
  45. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  46. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  47. package/dist/types/skc-runtime/state-writer.d.ts +2 -0
  48. package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
  49. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  50. package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
  51. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
  52. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  53. package/dist/types/tools/index.d.ts +18 -0
  54. package/dist/types/tools/subagent.d.ts +3 -0
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +3 -0
  57. package/src/async/job-manager.ts +5 -1
  58. package/src/cli/daemon-cli.ts +122 -0
  59. package/src/cli/notify-cli.ts +274 -0
  60. package/src/cli/plugin-cli.ts +63 -6
  61. package/src/cli/setup-cli.ts +173 -84
  62. package/src/cli.ts +3 -3
  63. package/src/commands/daemon.ts +47 -0
  64. package/src/commands/notify.ts +61 -0
  65. package/src/commands/setup.ts +11 -1
  66. package/src/config/model-profile-activation.ts +74 -5
  67. package/src/config/model-profiles.ts +7 -4
  68. package/src/config/model-registry.ts +6 -3
  69. package/src/config/models-config-schema.ts +1 -1
  70. package/src/config/settings-schema.ts +61 -0
  71. package/src/coordinator/contract.ts +3 -0
  72. package/src/coordinator-mcp/server.ts +270 -1
  73. package/src/daemon/builtin.ts +46 -0
  74. package/src/daemon/control-types.ts +65 -0
  75. package/src/daemon/runtime.ts +51 -0
  76. package/src/defaults/skc/rules/ponytail.md +68 -0
  77. package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
  78. package/src/defaults/skc-defaults.ts +27 -2
  79. package/src/edit/modes/replace.ts +1 -1
  80. package/src/extensibility/extensions/runner.ts +4 -0
  81. package/src/extensibility/extensions/types.ts +8 -0
  82. package/src/extensibility/plugins/index.ts +1 -0
  83. package/src/extensibility/plugins/manager.ts +50 -0
  84. package/src/extensibility/plugins/marketplace/manager.ts +42 -0
  85. package/src/extensibility/plugins/security-scanner.ts +477 -0
  86. package/src/goals/tools/goal-tool.ts +11 -2
  87. package/src/hashline/hash.ts +1 -1
  88. package/src/internal-urls/docs-index.generated.ts +10 -8
  89. package/src/main.ts +30 -0
  90. package/src/modes/acp/acp-event-mapper.ts +1 -0
  91. package/src/modes/components/hook-editor.ts +7 -2
  92. package/src/modes/components/oauth-selector.ts +19 -0
  93. package/src/modes/controllers/event-controller.ts +20 -0
  94. package/src/modes/controllers/selector-controller.ts +80 -17
  95. package/src/modes/interactive-mode.ts +6 -2
  96. package/src/modes/runtime-init.ts +1 -0
  97. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  98. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  99. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  101. package/src/modes/types.ts +7 -1
  102. package/src/modes/utils/ui-helpers.ts +23 -0
  103. package/src/notifications/config-commands.ts +50 -0
  104. package/src/notifications/config.ts +107 -0
  105. package/src/notifications/helpers.ts +135 -0
  106. package/src/notifications/html-format.ts +389 -0
  107. package/src/notifications/index.ts +700 -0
  108. package/src/notifications/rate-limit-pool.ts +179 -0
  109. package/src/notifications/telegram-cli.ts +194 -0
  110. package/src/notifications/telegram-daemon-cli.ts +74 -0
  111. package/src/notifications/telegram-daemon-control.ts +370 -0
  112. package/src/notifications/telegram-daemon.ts +1370 -0
  113. package/src/notifications/telegram-reference.ts +335 -0
  114. package/src/notifications/threaded-inbound.ts +80 -0
  115. package/src/notifications/threaded-render.ts +155 -0
  116. package/src/notifications/topic-registry.ts +133 -0
  117. package/src/rlm/index.ts +19 -0
  118. package/src/sdk.ts +16 -0
  119. package/src/session/agent-session.ts +113 -3
  120. package/src/session/auth-storage.ts +3 -0
  121. package/src/session/session-dump-format.ts +43 -2
  122. package/src/session/session-manager.ts +39 -5
  123. package/src/setup/credential-auto-import.ts +258 -0
  124. package/src/setup/credential-import.ts +17 -0
  125. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  126. package/src/setup/host-plugin-setup.ts +142 -0
  127. package/src/skc-runtime/deep-interview-recorder.ts +2 -2
  128. package/src/skc-runtime/launch-tmux.ts +10 -2
  129. package/src/skc-runtime/state-runtime.ts +18 -10
  130. package/src/skc-runtime/state-writer.ts +8 -8
  131. package/src/skc-runtime/tmux-common.ts +8 -0
  132. package/src/skc-runtime/tmux-sessions.ts +8 -1
  133. package/src/skc-runtime/ultragoal-guard.ts +57 -2
  134. package/src/skc-runtime/ultragoal-runtime.ts +105 -19
  135. package/src/skc-runtime/workflow-manifest.generated.json +27 -2
  136. package/src/skc-runtime/workflow-manifest.ts +11 -1
  137. package/src/slash-commands/builtin-registry.ts +4 -1
  138. package/src/task/executor.ts +5 -1
  139. package/src/tools/ask-answer-registry.ts +25 -0
  140. package/src/tools/ask.ts +77 -6
  141. package/src/tools/image-gen.ts +5 -8
  142. package/src/tools/index.ts +19 -0
  143. package/src/tools/inspect-image.ts +16 -11
  144. package/src/tools/subagent-render.ts +7 -0
  145. package/src/tools/subagent.ts +38 -7
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Notify CLI command handlers.
3
+ *
4
+ * Handles `skc notify` setup/status and the hidden daemon entrypoint.
5
+ */
6
+ import { createInterface } from "node:readline/promises";
7
+ import { APP_NAME } from "@sayknow-cli/utils";
8
+ import chalk from "chalk";
9
+ import { Settings } from "../config/settings";
10
+ import { getNotificationConfig, maskToken } from "../notifications/config";
11
+
12
+ export type NotifyAction = "setup" | "status" | "daemon-internal";
13
+
14
+ export interface NotifyCommandArgs {
15
+ action: NotifyAction;
16
+ smoke?: boolean;
17
+ rawArgs: string[];
18
+ token?: string;
19
+ chatId?: string;
20
+ redact?: boolean;
21
+ }
22
+
23
+ export interface NotifyCommandDeps {
24
+ fetchImpl?: typeof fetch;
25
+ apiBase?: string;
26
+ settings?: Settings;
27
+ setupToken?: string;
28
+ pollTimeoutMs?: number;
29
+ pollIntervalMs?: number;
30
+ setupChatId?: string;
31
+ setupRedact?: boolean;
32
+ }
33
+
34
+ interface TelegramApiResponse<T> {
35
+ ok: boolean;
36
+ result?: T;
37
+ description?: string;
38
+ }
39
+
40
+ interface TelegramUpdate {
41
+ update_id: number;
42
+ message?: {
43
+ chat?: {
44
+ id?: number | string;
45
+ type?: string;
46
+ };
47
+ };
48
+ }
49
+
50
+ const DEFAULT_API_BASE = "https://api.telegram.org";
51
+ const DEFAULT_POLL_TIMEOUT_MS = 60_000;
52
+ const DEFAULT_POLL_INTERVAL_MS = 1_000;
53
+
54
+ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
55
+ if (args.length === 0 || args[0] !== "notify") {
56
+ return undefined;
57
+ }
58
+
59
+ const action = args[1];
60
+ if (action === "setup" || action === "status") {
61
+ const rest = args.slice(2);
62
+ const flag = (name: string): string | undefined => {
63
+ const i = rest.indexOf(name);
64
+ return i >= 0 ? rest[i + 1] : undefined;
65
+ };
66
+ return {
67
+ action,
68
+ rawArgs: rest,
69
+ token: flag("--token"),
70
+ chatId: flag("--chat-id"),
71
+ redact: rest.includes("--redact"),
72
+ };
73
+ }
74
+ if (action === "daemon-internal") {
75
+ return {
76
+ action,
77
+ smoke: args.slice(2).includes("--smoke"),
78
+ rawArgs: args.slice(2),
79
+ };
80
+ }
81
+
82
+ return { action: "status", rawArgs: args.slice(1) };
83
+ }
84
+
85
+ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyCommandDeps = {}): Promise<void> {
86
+ switch (cmd.action) {
87
+ case "setup":
88
+ await runSetup({
89
+ ...deps,
90
+ setupToken: deps.setupToken ?? cmd.token,
91
+ setupChatId: deps.setupChatId ?? cmd.chatId,
92
+ setupRedact: deps.setupRedact ?? cmd.redact,
93
+ });
94
+ return;
95
+ case "status":
96
+ await runStatus(deps);
97
+ return;
98
+ case "daemon-internal": {
99
+ const m = await import("../notifications/telegram-daemon-cli");
100
+ if (cmd.smoke) {
101
+ await m.runDaemonSmoke();
102
+ } else {
103
+ await m.runDaemonInternal(cmd.rawArgs);
104
+ }
105
+ return;
106
+ }
107
+ }
108
+ }
109
+
110
+ async function getSettings(deps: NotifyCommandDeps): Promise<Settings> {
111
+ return deps.settings ?? (await Settings.init());
112
+ }
113
+
114
+ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
115
+ const settings = await getSettings(deps);
116
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
117
+ const apiBase = deps.apiBase ?? DEFAULT_API_BASE;
118
+ const token = deps.setupToken ?? (await promptForToken());
119
+ if (!token.trim()) {
120
+ throw new Error("Telegram bot token is required.");
121
+ }
122
+
123
+ await callTelegram(fetchImpl, apiBase, token, "getMe", {});
124
+ process.stdout.write(
125
+ "Token validated. Message your bot now from the private Telegram chat to pair notifications.\n",
126
+ );
127
+
128
+ let chatId: string;
129
+ if (deps.setupChatId?.trim()) {
130
+ chatId = deps.setupChatId.trim();
131
+ process.stdout.write(`Using provided chat id ${chatId} (non-interactive).\n`);
132
+ } else {
133
+ const stale = await getUpdates(fetchImpl, apiBase, token, { timeout: 0, allowed_updates: ["message"] });
134
+ const offset = nextOffset(stale);
135
+ chatId = await waitForPrivateChat(fetchImpl, apiBase, token, {
136
+ offset,
137
+ pollTimeoutMs: deps.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
138
+ pollIntervalMs: deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
139
+ });
140
+ }
141
+
142
+ settings.set("notifications.telegram.botToken", token);
143
+ settings.set("notifications.telegram.chatId", chatId);
144
+ settings.set("notifications.enabled", true);
145
+ if (deps.setupRedact) settings.set("notifications.redact", true);
146
+ await settings.flush();
147
+
148
+ process.stdout.write(`Notifications enabled. botToken=${maskToken(token)} chatId=${chatId}\n`);
149
+ }
150
+
151
+ async function promptForToken(): Promise<string> {
152
+ if (!process.stdin.isTTY) {
153
+ throw new Error("notify setup requires an interactive TTY unless setupToken is injected.");
154
+ }
155
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
156
+ try {
157
+ return (await rl.question("Telegram BotFather token: ")).trim();
158
+ } finally {
159
+ rl.close();
160
+ }
161
+ }
162
+
163
+ async function runStatus(deps: NotifyCommandDeps): Promise<void> {
164
+ const settings = await getSettings(deps);
165
+ const cfg = getNotificationConfig(settings);
166
+ process.stdout.write(
167
+ `${chalk.bold("Notifications")}\n` +
168
+ ` enabled: ${cfg.enabled}\n` +
169
+ ` botToken: ${maskToken(cfg.botToken)}\n` +
170
+ ` chatId: ${cfg.chatId ?? "(unset)"}\n` +
171
+ ` redact: ${cfg.redact}\n`,
172
+ );
173
+ }
174
+
175
+ async function waitForPrivateChat(
176
+ fetchImpl: typeof fetch,
177
+ apiBase: string,
178
+ token: string,
179
+ opts: { offset: number | undefined; pollTimeoutMs: number; pollIntervalMs: number },
180
+ ): Promise<string> {
181
+ const deadline = Date.now() + opts.pollTimeoutMs;
182
+ let offset = opts.offset;
183
+ let sawRejectedChatType: string | undefined;
184
+
185
+ while (Date.now() <= deadline) {
186
+ const updates = await getUpdates(fetchImpl, apiBase, token, { offset, timeout: 0, allowed_updates: ["message"] });
187
+ offset = nextOffset(updates, offset);
188
+ for (const update of updates) {
189
+ const chat = update.message?.chat;
190
+ if (!chat) continue;
191
+ if (chat.type === "private" && chat.id !== undefined) {
192
+ return String(chat.id);
193
+ }
194
+ if (chat.type === "group" || chat.type === "supergroup" || chat.type === "channel") {
195
+ sawRejectedChatType = chat.type;
196
+ process.stderr.write(
197
+ `Rejected ${chat.type} chat. Pairing requires a private Telegram chat with the bot.\n`,
198
+ );
199
+ }
200
+ }
201
+ if (opts.pollIntervalMs > 0) {
202
+ await new Promise(resolve =>
203
+ setTimeout(resolve, Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now()))),
204
+ );
205
+ }
206
+ }
207
+
208
+ if (sawRejectedChatType) {
209
+ throw new Error(`Pairing rejected ${sawRejectedChatType} chat; message the bot from a private chat.`);
210
+ }
211
+ throw new Error("Timed out waiting for a private Telegram message to pair notifications.");
212
+ }
213
+
214
+ function nextOffset(updates: TelegramUpdate[], fallback?: number): number | undefined {
215
+ let max = fallback === undefined ? undefined : fallback - 1;
216
+ for (const update of updates) {
217
+ if (typeof update.update_id === "number" && (max === undefined || update.update_id > max)) {
218
+ max = update.update_id;
219
+ }
220
+ }
221
+ return max === undefined ? fallback : max + 1;
222
+ }
223
+
224
+ async function getUpdates(
225
+ fetchImpl: typeof fetch,
226
+ apiBase: string,
227
+ token: string,
228
+ params: Record<string, unknown>,
229
+ ): Promise<TelegramUpdate[]> {
230
+ return await callTelegram<TelegramUpdate[]>(fetchImpl, apiBase, token, "getUpdates", params);
231
+ }
232
+
233
+ async function callTelegram<T>(
234
+ fetchImpl: typeof fetch,
235
+ apiBase: string,
236
+ token: string,
237
+ method: string,
238
+ body: Record<string, unknown>,
239
+ ): Promise<T> {
240
+ const response = await fetchImpl(`${apiBase.replace(/\/$/, "")}/bot${token}/${method}`, {
241
+ method: "POST",
242
+ headers: { "content-type": "application/json" },
243
+ body: JSON.stringify(body),
244
+ });
245
+ let payload: TelegramApiResponse<T>;
246
+ try {
247
+ payload = (await response.json()) as TelegramApiResponse<T>;
248
+ } catch {
249
+ throw new Error(`Telegram ${method} returned invalid JSON.`);
250
+ }
251
+ if (!response.ok || !payload.ok) {
252
+ throw new Error(`Telegram ${method} failed: ${payload.description ?? response.statusText}`);
253
+ }
254
+ return payload.result as T;
255
+ }
256
+
257
+ export function printNotifyHelp(): void {
258
+ process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram notifications
259
+
260
+ ${chalk.bold("Usage:")}
261
+ ${APP_NAME} notify setup
262
+ ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
263
+ ${APP_NAME} notify status
264
+
265
+ ${chalk.bold("Subcommands:")}
266
+ setup Pair a Telegram bot token with a private chat
267
+ status Show notification configuration without secrets
268
+
269
+ ${chalk.bold("Examples:")}
270
+ ${APP_NAME} notify setup
271
+ ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
272
+ ${APP_NAME} notify status
273
+ `);
274
+ }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { APP_NAME, getProjectDir } from "@sayknow-cli/utils";
8
8
  import chalk from "chalk";
9
+ import { Settings } from "../config/settings";
9
10
  import { resolveOrDefaultProjectRegistryPath } from "../discovery/helpers";
10
11
  import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
11
12
  import {
@@ -15,6 +16,7 @@ import {
15
16
  getPluginsCacheDir,
16
17
  MarketplaceManager,
17
18
  } from "../extensibility/plugins/marketplace/index.js";
19
+ import { type ScanReport, scanPluginDir } from "../extensibility/plugins/security-scanner";
18
20
  import { theme } from "../modes/theme/theme";
19
21
 
20
22
  // =============================================================================
@@ -147,6 +149,10 @@ export { classifyInstallTarget } from "./classify-install-target";
147
149
  * Run a plugin command.
148
150
  */
149
151
  export async function runPluginCommand(cmd: PluginCommandArgs): Promise<void> {
152
+ // Initialize settings so plugin commands can read config (e.g. the install-time
153
+ // security-scan mode/threshold); mirrors the other CLI command handlers. The scan
154
+ // itself also tolerates an uninitialized Settings, so this is best-effort.
155
+ await Settings.init().catch(() => {});
150
156
  const manager = new PluginManager();
151
157
 
152
158
  switch (cmd.action) {
@@ -342,6 +348,21 @@ async function handleUpgrade(args: string[], flags: PluginCommandArgs["flags"]):
342
348
  }
343
349
  }
344
350
 
351
+ function printSecurityAdvisory(report: ScanReport): void {
352
+ const riskColor = report.riskLevel === "high" ? chalk.red : report.riskLevel === "medium" ? chalk.yellow : chalk.dim;
353
+ console.log(
354
+ riskColor(`${theme.status.warning} Security advisory: ${report.riskLevel} risk (score ${report.score})`),
355
+ );
356
+ for (const f of report.findings.slice(0, 5)) {
357
+ const loc = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
358
+ console.log(chalk.dim(` ${f.id}: ${loc}`));
359
+ }
360
+ if (report.findings.length > 5) {
361
+ console.log(chalk.dim(` ... and ${report.findings.length - 5} more finding(s)`));
362
+ }
363
+ console.log(chalk.dim(" Advisory only (regex-based, may have false positives)."));
364
+ }
365
+
345
366
  async function handleInstall(
346
367
  manager: PluginManager,
347
368
  packages: string[],
@@ -368,11 +389,28 @@ async function handleInstall(
368
389
  force: flags.force,
369
390
  scope: flags.scope,
370
391
  });
371
- console.log(
372
- chalk.green(
373
- `${theme.status.success} Installed ${target.name} from ${target.marketplace} (${entry.version})`,
374
- ),
375
- );
392
+ if (flags.json) {
393
+ // advisory is attached below via the scan path
394
+ } else {
395
+ console.log(
396
+ chalk.green(
397
+ `${theme.status.success} Installed ${target.name} from ${target.marketplace} (${entry.version})`,
398
+ ),
399
+ );
400
+ }
401
+ // Post-install advisory scan (non-blocking)
402
+ try {
403
+ const report = await scanPluginDir(entry.installPath);
404
+ if (report.riskLevel !== "none") {
405
+ if (flags.json) {
406
+ console.log(JSON.stringify({ installed: target.name, securityAdvisory: report }, null, 2));
407
+ } else {
408
+ printSecurityAdvisory(report);
409
+ }
410
+ }
411
+ } catch {
412
+ // advisory — never fail install
413
+ }
376
414
  } catch (err) {
377
415
  console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
378
416
  process.exit(1);
@@ -394,7 +432,17 @@ async function handleInstall(
394
432
  const result = await manager.install(spec, { force: flags.force, dryRun: flags.dryRun });
395
433
 
396
434
  if (flags.json) {
397
- console.log(JSON.stringify(result, null, 2));
435
+ // Post-install advisory scan for JSON output
436
+ let securityAdvisory: ScanReport | undefined;
437
+ if (!flags.dryRun && result.path) {
438
+ try {
439
+ const report = await scanPluginDir(result.path);
440
+ if (report.riskLevel !== "none") securityAdvisory = report;
441
+ } catch {
442
+ // advisory
443
+ }
444
+ }
445
+ console.log(JSON.stringify(securityAdvisory ? { ...result, securityAdvisory } : result, null, 2));
398
446
  } else {
399
447
  if (flags.dryRun) {
400
448
  console.log(chalk.dim(`[dry-run] Would install ${spec}`));
@@ -406,6 +454,15 @@ async function handleInstall(
406
454
  if (result.manifest.description) {
407
455
  console.log(chalk.dim(` ${result.manifest.description}`));
408
456
  }
457
+ // Post-install advisory scan
458
+ if (result.path) {
459
+ try {
460
+ const report = await scanPluginDir(result.path);
461
+ if (report.riskLevel !== "none") printSecurityAdvisory(report);
462
+ } catch {
463
+ // advisory
464
+ }
465
+ }
409
466
  }
410
467
  }
411
468
  } catch (err) {