@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.
- package/CHANGELOG.md +32 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +71 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/defaults/skc-defaults.d.ts +13 -2
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/plugins/index.d.ts +1 -0
- package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/plugin-cli.ts +63 -6
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +61 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/defaults/skc-defaults.ts +27 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/plugins/index.ts +1 -0
- package/src/extensibility/plugins/manager.ts +50 -0
- package/src/extensibility/plugins/marketplace/manager.ts +42 -0
- package/src/extensibility/plugins/security-scanner.ts +477 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +10 -8
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +10 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +27 -2
- package/src/skc-runtime/workflow-manifest.ts +11 -1
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- 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
|
+
}
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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) {
|